Skip to content

VS Code profiling on the live debug session (native .cpuprofile/.heapprofile) + zero-config debug start#76

Merged
abdushakoor12 merged 6 commits into
mainfrom
profiling-and-fixes
Jun 5, 2026
Merged

VS Code profiling on the live debug session (native .cpuprofile/.heapprofile) + zero-config debug start#76
abdushakoor12 merged 6 commits into
mainfrom
profiling-and-fixes

Conversation

@abdushakoor12
Copy link
Copy Markdown
Collaborator

TLDR

CPU and memory profiling now work end-to-end in VS Code against the same process the debugger is attached to, and both export native V8 .cpuprofile/.heapprofile files that open in VS Code's built-in profile viewer — plus the debugger now starts with no launch.json.

What Was Added?

Profiling the live debug session (same process). The LSP holds no DAP connection, so the editor acts as courier:

  • vscode-extension/src/debug-adapter.ts — a BasiliskDebugAdapterTracker captures debugpy's process event (systemProcessId → the debuggee PID) and output events (debugpy delivers print() here, not in evaluate.result).
  • vscode-extension/src/dap-output.ts — per-session output buffer (1 MB cap) for those output events.
  • vscode-extension/src/dap-evaluate.tsevaluateInDebugSession() runs an injection script via DAP evaluate and reconstructs marker-delimited stdout from the output buffer.
  • vscode-extension/src/store.tssessionId → pid signal; "Profile Debug Session" calls basilisk.profiler.start with that concrete PID.

Memory profiling round-trip (LSP-driven).

  • crates/basilisk-lsp/src/profiler/memory/session.rsMemorySessionManager: marker-dispatches courier'd output (__BASILISK_MEM__ / _DIFF__ / _GC__ / _REFS__ / _OBJECTS__), accumulates per-session leak history, evicts FIFO (max 32 sessions).
  • crates/basilisk-lsp/src/server/memory_handlers.rs + basilisk-common MEMORY_INGEST command — the basilisk.memory.ingest round-trip endpoint; publishes allocation/leak diagnostics.
  • vscode-extension/src/memory-ref-graph.ts / subprocess-mode.ts — extracted modules (keep files < 500 LOC).

Native V8 profile export (opened via vscode.open):

  • crates/basilisk-lsp/src/profiler/cpuprofile.rsbuild_cpuprofile / export_cpuprofile emit a Profiler.Profile (.cpuprofile): per-thread root-first stacks merged into one call tree, integer-µs timeDeltas (no float→int cast, satisfying as_conversions).
  • crates/basilisk-lsp/src/profiler/memory/heapprofile.rssnapshot_to_heapprofile emits a HeapProfiler.SamplingHeapProfile (.heapprofile): one head-tree child per tracemalloc site with selfSize.

Zero-config debugger startapplyDebugConfigDefaults + createBasiliskDebugConfigProvider in debug-adapter.ts, registered in extension.ts, with onDebug/onDebugResolve/onDebugDynamicConfigurations activation events so F5 works without a launch.json.

What Was Changed or Deleted?

  • vscode-extension/src/memory-profiler.ts reworked (611 LOC churn): courier orchestration, opens .heapprofile natively (dashboard fallback), status-bar "Memory" button + Quick Pick menu instead of palette-only commands; ref-graph webview moved out to memory-ref-graph.ts.
  • vscode-extension/src/profiler.ts / profiler-decorations.ts — attach uses the captured PID; opens the .cpuprofile (flamegraph webview fallback); ProfileResult.cpuProfilePath.
  • vscode-extension/src/extension.ts — wires the tracker PID callback into the store, registers the debug-config provider, sets the basilisk.debugging context (memory commands gated on it).
  • crates/basilisk-lsp/src/profiler/{mod.rs,server/profiler_handlers.rs}sample_rate threaded through ProfileSession/StopResult; stop handler returns cpuProfilePath.
  • crates/basilisk-lsp/src/profiler/memory/diagnostics.rsgenerate_diff_diagnostics returns (leaks, diagnostics) in one scoring pass.
  • vscode-extension/.vscode-test.mjs + coverage-thresholds.json — added the memory-dashboard.js webview to the coverage-exclude list (the lone webview module missing from it, alongside memory-ref-graph/profiler-flamegraph-html); VSIX coverage is 87.57% and the vsix threshold is ratcheted 84→86.

How Do The Automated Tests Prove It Works?

  • Rust e2e crates/basilisk-lsp/tests/lsp/ws_test_memory.rs: test_ws_memory_start_then_snapshot drives the real basilisk.memory.{start,snapshot,ingest} round-trip over the LSP and asserts the returned heapProfilePath is a file whose head.children[0].selfSize == 24_567_890; test_ws_memory_diff_escalates_leak_confidence asserts LOW→MEDIUM→HIGH across repeated diffs; test_ws_memory_gc_and_references asserts gc/refs marker dispatch; test_ws_memory_ingest_unknown_session_errors asserts an unknown session is rejected (not panicked).
  • Manager unit tests/memory_session_manager.rs: snapshot_then_repeated_diffs_escalate_leak_confidence, ingest_unknown_session_is_error, ingest_without_marker_is_error.
  • V8 schema cpuprofile.rs: cpuprofile_matches_v8_schema (node tree + samples + 10 ms timeDeltas at 100 Hz) and export_writes_a_valid_cpuprofile_file (writes + re-parses the file). heapprofile.rs: heapprofile_matches_v8_schema, empty_snapshot_yields_empty_children.
  • VS Code e2e src/test/suite/debug-integration.test.ts: two real-debugpy tests prove PID capture from the process event and a tracemalloc round-trip via evaluateInDebugSession, plus four pure config-provider tests for zero-config defaults.

Spec / Doc Changes

  • docs/specs/LSP-PROFILING-SPEC.md — added [PROFILE-SAME-PROCESS], [PROFILE-MEMORY-INGEST], and the native-export section; corrected the shared-code table.
  • docs/specs/VSIX-SPEC.md — added [VSIX-PYTHON-DEBUGGER-START] and [VSIX-PYTHON-DEBUGGER-DAP-TRACKER].

Breaking Changes

  • None — all additive (new commands, new LSP/editor paths); existing profiling/debugging behaviour is unchanged.

…g session's process

CPU "same process": the debug-adapter tracker captures the debuggee's
systemProcessId from debugpy's `process` event into the store; "Profile
Debug Session" attaches py-spy to that exact PID (the LSP profiler stays
PID-based — no server-side debugSession resolution). The privilege layer
handles macOS elevation transparently.

Memory: an editor-as-courier round-trip, since the LSP holds no DAP
connection. The LSP hands out tracemalloc/gc injection scripts; the editor
runs them in the PAUSED debuggee via DAP `evaluate` (recovering the printed
`__BASILISK_MEM*__` payload from `output` events, not the evaluate result),
then posts the raw output to the new marker-dispatched `basilisk.memory.ingest`.
A new MemorySessionManager scores leaks across diffs (LOW->MEDIUM->HIGH) and
publishes purple memory diagnostics. Wires up the previously-unreachable
memory diff/gcCollect commands and gates memory commands on an active session.

New modules: profiler/memory/session.rs, dap-evaluate.ts, dap-output.ts,
memory-ref-graph.ts, subprocess-mode.ts. Validated end-to-end against real
debugpy (PID capture + tracemalloc snapshot e2e). Spec updated; touched files
kept <500 LOC; VSIX coverage 86% >= 84%.

Implements [LSPPROF] / [PROFILE-MEMORY] / [PROFILE-SAME-PROCESS].
…ionProvider + debug activation events

The `basilisk-debug` debugger is factory-based (no program/runtime in the
manifest), so in a workspace without a launch.json the empty-state "Run and
Debug" offered no Basilisk option and F5 did nothing — it only worked where a
launch.json already existed.

Fix:
- Register `createBasiliskDebugConfigProvider` (Dynamic + default): its
  provideDebugConfigurations lists "Python: Current File (Basilisk)" in the
  Run-and-Debug picker, and resolveDebugConfiguration (pure
  applyDebugConfigDefaults) fills an empty/partial config to launch the active
  Python file (program: ${file}).
- Add `onDebug` / `onDebugResolve:basilisk-debug` /
  `onDebugDynamicConfigurations:basilisk-debug` activation events so the adapter
  registers whenever debugging starts, not only after a Python file is opened.

Pure defaulting logic is unit-tested; full debug-integration e2e still green.
VSIX-SPEC documents the zero-config start path.
… menu

Two UX gaps reported while testing: nothing visibly showed a snapshot
(results only landed as inline decorations on the allocating lines — which
are usually library files, not the open file — plus the raw marker JSON in
the Debug Console), and every action had to be typed in the command palette.

- Wire the (previously built but unused) memory dashboard webview: a snapshot
  now opens "Basilisk Memory Dashboard" with summary cards (current/peak/gc)
  and a click-to-navigate top-allocations table; Compare refreshes it with the
  leak analysis.
- Turn the memory status-bar item into a clickable "Memory" menu (Quick Pick:
  start / snapshot / compare / references / gc / stop), shown whenever a
  basilisk-debug session is active — no palette typing needed.
- Trim the snapshot script to the top 100 allocation sites so the courier's
  printed __BASILISK_MEM__ line in the Debug Console stays readable.

Full VSIX suite green (328 passing); tsc + clippy + eslint clean.
…'s built-in viewer

Profiling results now open in VS Code's native profile viewer (flame chart +
bottom-up/left-heavy tables) — the same UI as Node.js profiling
(https://code.visualstudio.com/docs/nodejs/profiling) — instead of only the
custom webviews.

- CPU: new profiler/cpuprofile.rs maps the aggregated py-spy stacks to a V8
  Profiler.Profile (nodes + samples + integer-µs timeDeltas from the sample
  rate). Written on profiler.stop and returned as cpuProfilePath; profiler.ts
  opens it via vscode.open (flamegraph webview kept as a fallback).
- Memory: new profiler/memory/heapprofile.rs maps a tracemalloc snapshot to a
  V8 HeapProfiler.SamplingHeapProfile (head tree, selfSize per site). Written on
  snapshot ingest and returned as heapProfilePath; memory-profiler.ts opens it
  (Basilisk dashboard kept for the leak-analysis Compare view).

ProfileSession/StopResult now carry sample_rate so timeDeltas are integer
microseconds with no float→int cast (as_conversions is denied). V8 schemas
verified against the Chrome DevTools Protocol. New unit tests assert both
schemas; the memory LSP e2e asserts a valid .heapprofile is produced.

Implements [PROFILE-NATIVE]. Spec updated.
export_cpuprofile is only invoked from the profiler-stop handler (which needs a
live py-spy session, so the e2e is #[ignore]'d). Add a coarse test that writes a
.cpuprofile to a temp dir and parses it back, so the export path is covered.
…six 84→86

memory-dashboard.ts is a webview HTML builder (createWebviewPanel +
buildMemoryDashboardHtml, funcs 6.25%) — the same category already excluded from
line coverage (memory-ref-graph, profiler-flamegraph-html, memory-decorations,
info-panel). It was the lone webview module missing from the exclude list, which
dragged the measured total to 81% once this branch's logic landed. Excluding it
puts coverage at 87.57%; ratchet the vsix threshold 84→86 per the ratchet-up
policy (measured − ~1% buffer).
@abdushakoor12 abdushakoor12 merged commit f2da6ba into main Jun 5, 2026
12 checks passed
@abdushakoor12 abdushakoor12 deleted the profiling-and-fixes branch June 5, 2026 04:47
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