Skip to content

Stop releases memory steps its target holds; a capture ends with its stream - #31

Merged
MelbourneDeveloper merged 1 commit into
mainfrom
fix/memory-capture-release
Oct 29, 2026
Merged

MelbourneDeveloper merged 1 commit into
mainfrom
fix/memory-capture-release

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TLDR

memory/stop now ends every step still in flight on its session before it answers, and the .NET engine stops waiting the moment a capture's event stream ends. A target that dies mid-capture is now PROCESS_NOT_FOUND.

Details

SharpLsp's Windows profiler CI hit this as a flake. The interval autopilot's last capture was sent about 30 ms before teardown stopped tracking, and the debuggee was then killed. The engine sat out its full 90 s SessionRun deadline: the host log shows lspProfiler/memory/snapshot finishing after 90273 ms. Until it answered, the autopilot stayed busy, so the next test's interval session never captured ("2 automatic capture(s) not met within 40000ms (last: 0)").

  • [PROF-MEMORY-STOP] (new spec section). New memory/gate.rs:
    • Every step except stop runs under its session's StepGate.
    • stop ends the steps in flight the way cancellation does ([PROF-HOST-CANCEL]): it drops their futures, and kill_on_drop terminates their tools. The target keeps running.
    • stop waits until each step has answered (OTHER, "memory session <id> stopped before the step finished"), then lets the runtime release the session.
    • Stopping one session never touches another's steps.
  • Engine SessionRun.Await. Waits for the watcher or the end of the stream, whichever comes first, and stops only a session that is still streaming. A gc whose stream ends early is now a failure instead of observed: false.
  • Engine Errors.Describe. Any failure of a command whose --pid is gone is ProcessNotFound ("process <pid> is not running"). Before, a stream cut off mid-read surfaced as Failed: "Unable to read beyond the end of the stream.".
  • Refactor. The memory steps move from service.rs (631 lines) into service/memory_steps.rs, which brings service.rs to 493 lines.

How Do The Automated Tests Prove It Works?

Each new test was run red first.

crates/lsp-profiler-dotnet/tests/dotnet_held_e2e.rs (Unix):

  • The target is frozen in the kernel's stop state (kill -STOP, confirmed by ps), so a snapshot or diff of it is held for as long as it stays frozen. No timer is involved.
  • Red: all 3 tests hung until the 60 s guard ("the released step answers"). Green: 2.5 s.
  • stop_releases_a_snapshot_its_frozen_target_holds and stop_releases_a_diff_its_frozen_target_holds assert that:
    • stop answers Ack and deletes the graphs;
    • the held step answers the exact stopped error;
    • later steps are an unknown session;
    • the target is still alive, and once thawed a new session snapshots it.
  • stopping_one_session_leaves_another_sessions_held_step_held holds two sessions:
    • stopping the first releases only its step;
    • the second's step is still held and its graphs remain;
    • stopping the second then releases its step.

dotnet/LspProfiler.DotNet.Engine.Tests/SessionRunTests.cs has 6 permutations:

  • the stream ends first;
  • the stream fails first;
  • the watcher finishes while the stream is still running;
  • the watcher finishes after the stream has ended;
  • the deadline passes;
  • a stopped stream never ends.

Each asserts the outcome, the number of stops, and the stream's state. Red: a stream that had already ended still waited out the deadline (DeadlinePassed), stopped a session that no longer existed, and threw on a faulted stream.

dotnet/LspProfiler.DotNet.Engine.Tests/FailureTests.cs crosses 8 causes with 3 target states (gone, live, none). With the target gone the kind is always ProcessNotFound and the message is exact. Otherwise each cause keeps its own kind and message. Red: the 8 gone-target cases returned their generic kinds.

Regressions. All green on Linux:

Suite Passed
lsp-profiler unit tests 79
lsp-profiler-dotnet unit tests 15
dotnet_e2e 14
dotnet_fsharp_e2e (with strace) 20
Engine unit tests 29

clippy --workspace --all-targets -D warnings, fmt --check and the spec-citation check also pass.

…stream

A direct runtime reads the live heap, so a memory step can wait on its target
indefinitely. Two places let that wedge an editor:

- memory/stop answered while a step on the session was still in flight. An
  editor's interval autopilot whose capture was in flight at stop stayed busy
  until that request answered, and never captured the next session.
- the .NET engine waited out its 90 s deadline after the event stream had
  ended, because the target took the session with it. A target that died
  mid-capture therefore held its step for 90 s, and then failed as a generic
  "Failed" instead of PROCESS_NOT_FOUND.

Stop now ends every step in flight on its session the way cancellation does
(dropping the future terminates its tools, the target keeps running), waits
for each to answer, then releases the session ([PROF-MEMORY-STOP]). The engine
stops waiting the moment the stream ends, stops only a session still
streaming, and reports any failure of a command whose target is gone as
ProcessNotFound ([PROF-DOTNET-HEAP-CAPTURE], [PROF-DOTNET-GC]).

The memory steps move out of service.rs into service/memory_steps.rs, which
brings service.rs under the 500-line limit.
@MelbourneDeveloper
MelbourneDeveloper merged commit 8458096 into main Oct 29, 2026
3 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the fix/memory-capture-release branch October 29, 2026 07:17
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