Skip to content

Progress operation log: never truncate within a host's run, trim only whole operations - #16

Merged
MelbourneDeveloper merged 12 commits into
mainfrom
fix/unlocated-memory-sites
Oct 18, 2026
Merged

MelbourneDeveloper merged 12 commits into
mainfrom
fix/unlocated-memory-sites

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TLDR

The progress operation log, the e2e seam hosts read, stops truncating partway through a host's test run, so its length stays a cursor and every end: keeps its begin:.

Details

  • progress-ops.ts trimmed single entries off the front once the log passed 500. A host runs all its suites in one extension host, and SharpLsp's profiler run now records more than 500 operations. The log's length then froze at the cap, so recordedOperations().slice(before) saw nothing of a refresh it had just recorded, and a trimmed begin: left its end: orphaned, so the log claimed a notification closed that never opened.
  • OPERATION_LOG_CAP is now 10,000 entries (a few hundred KB at most), far above a whole host run. An overflow trims back to half the cap, and only at a boundary where no operation is open (settledPrefix), so the log is never unbalanced.

How Do The Automated Tests Prove It Works?

  • New progress-ops.test.ts, "every operation of a long run stays recorded, balanced, with its length a cursor", records 600 operations, each with a step. It asserts that all 1,800 entries are kept, that the first operation is still present, that slice(before) after one more operation is exactly its begin and end, and that begins equal ends. Before the fix it failed with "begin, step and end of every operation are kept".
  • The full UI suite passes 162/162.
  • In SharpLsp's profiler chunk this fixes profiler-ux's "manual panel refresh runs under the view's progress bar" and "every recorded progress operation closes", which failed on Linux and Windows only once the run passed 500 operations.

An array or a framework type has no source location ([PROF-DOTNET-SOURCE]),
and the spec keeps such a site in the UI with no diagnostic. The wire adapter
turned its absent file and line into "" and line 0, so the dashboard listed a
0th line and decorations tried to paint a file named "". MemoryAllocation's
file and line are now optional: an unlocated site lists by name and paints no
line.
The objects step summarised only the twenty largest types, so a type too
small to rank (an F# union case such as OrderStatus+Paid, or the one list
that holds a leak) had no row agreeing with its totals. The summary now
appends that type's row after the ranked ones, keeping largest-first order.
Each test crate compiles tests/support on its own, and only the F# suite
drives a fixture's line protocol, so Fixture::send and the stdout reader it
reads were dead code in the C# suite's crate, which denies dead_code. Both
now carry a documented per-item allow.
The fixture serialised its workload and its line protocol with one lock
wrapped around Hot.spin, so the CPU profile no longer put Hot.spin on the
busy thread's stack for most ticks and the F# CPU suite failed. The workload
now runs lock-free and flags its own busy state, and idle work keeps
allocating so a snapshot's induced collection can run. The FSharp.Core
anchor test reads the order count from the reply, whatever its prefix.
…to targets

The retention graph ignored is_root: decodeRefGraph dropped it, and the canvas
coloured nodes by depth and drew only their types. A root's label, such as an
F# module value's `static sessions`, never reached the screen, and roots were
not distinct, against [PROF-UI-REFGRAPH]. The random force layout also turned
every graph with many targets into an unreadable knot.

Roots now survive decoding, are drawn in their own colour and named by their
label above their type. Rows follow each node's depth: roots on top, targets
along the bottom, and a row too tight to label names its type once with a count.
toProfileResult turned a frame the runtime could not place, such as
System.Threading.Thread.StartCallback() with no framework PDB here, into
file "" and line 0, which the results table rendered as a :0 link to
nowhere. The location is now both fields or neither, shared with the memory
sites; the heat map paints only located functions and the table lists the
rest by name.
A `duration` on start capped the recorded time, but the session stayed in
`cpu.list` and kept sending progress until someone stopped it, so a timed
session never ended by itself ([PROF-CPU-SESSIONS]). Once its duration
has elapsed, a session is no longer listed as active and its progress
ends; its recording is still kept for the next `stop`.
Stop cleared the UI's tracking state and decorations but never sent
`memory.stop`, so the server kept the session and its heap graphs, and
answered snapshots for a session the user had stopped. Stop now ends the
server session before the UI forgets its id, and a direct runtime whose
program ended ends its session too.
…e peak

The CPU session enabled the runtime provider beside the sampler. While any
session enables it, the runtime keeps its list of the types it has already
logged, so every heap snapshot taken during a CPU session after the first
reported the types an earlier snapshot named as bare addresses. The CPU
session now enables only the sampler; each segment's rundown still names
its frames.

peak_bytes was each snapshot's own heap size, so a release and a collection
lowered it. The backend now keeps the session's peak, as the wire defines it.

The F# engine suite becomes a module test crate under 500 lines a file,
clippy-clean, with the line-protocol zoo tests and failing-first tests for
both fixes.
main squashed #13 and #14 from this branch's earlier commits, so their
content is already here; the tree is unchanged.
The log trimmed single entries once it passed 500. A host runs all its
suites in one extension host, and SharpLsp's profiler run now records more
than that, so the log's length froze at the cap (slice(before) saw nothing
of a refresh) and a trimmed begin: left its end: orphaned. The cap is now
10,000, far above a whole run, and an overflow trims back to half of it
only where no operation is open.
@MelbourneDeveloper
MelbourneDeveloper merged commit ccc59bf into main Oct 18, 2026
3 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the fix/unlocated-memory-sites branch October 18, 2026 09:02
MelbourneDeveloper added a commit to Nimblesite/SharpLsp that referenced this pull request Oct 18, 2026
The profiler UI's progress operation log trimmed single entries past 500,
which SharpLsp's profiler run now exceeds, so the refresh-progress and
balanced-progress checks read a truncated log.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant