Repository navigation
Fix the open v0.22.0 review findings, and prove the profiler on real F# programs - #385
Merged
Merged
Conversation
… framework-switch order, Windows-only selections that run nothing - A single-target C# project whose name ends in (tfm) is no longer read as a framework context (#382) - A framework switch re-publishes open documents' diagnostics before announcing itself and asking for refreshes (#383) - Off Windows, a MOCHA_FILES pattern naming only windowsOnly suites fails instead of passing a run of nothing (#384)
MelbourneDeveloper
enabled auto-merge (squash)
October 18, 2026 06:22
The whole PR pipeline skips Dependabot, and the terminal gate passed the skipped run, so a security bump the sweep did not retire showed a green required check and could merge into main untested. The gate now fails it; bumps reach main only through the tested dependabot-upgrades consolidation PR.
…located, each diff replaces the last diff's leak badges (Nimblesite/lspkit#12)
…hreads counted once per tick, arrays never located, honest retention truncation and PROCESS_NOT_FOUND, fresh leak badges
The objects step keeps the requested type's own row when it is too small to rank among the twenty largest, so a union case such as OrderStatus+Paid agrees with its totals. Types whose PDB names a build server's checkout, as FSharp.Core's does, have no location instead of a path that does not exist here.
…rts already narrow
The CPU results panel and the memory dashboard checked the same SharpLsp brand, foreign-product and nonce rules in two copies, and the webviews suite kept its own copy of the kit's dashboard snapshot. One helper each brings duplication back under the Deslop ratchet; every assertion is unchanged.
The corner suite releases the ledger before counting 300 live orders and no longer claims the dashboard keeps a stopped session's snapshot, which memory-flows and the F# memory suite rule out. The memory-step helpers and error codes move into the soak kit, the wire suite splits into protocol, CPU and memory suites, and the fixture locations and builds move from profiler-kit into profiler-fixtures, so no suite or kit passes 500 lines.
FSharp.Core types no longer anchor at the build server's paths, sites with no location stay unlocated in the UI, the objects summary keeps the requested type's row, and the retention graph draws each root by its label.
Every profiler operation takes exactly one argument, its typed params object ([PROF-WIRE-METHODS]). The host passed the first argument on and ignored the rest, so `capabilities` answered `[]`, `[42]` and two objects alike. Anything but one JSON object is now INVALID_PARAMS before the service sees it; an absent argument list still means empty params.
… tools reusable
A new Profiling F# page (en, ja, zh) covers what the profiler does with F#'s
compiled code: closures and pipeline stages by name, records and union cases
painted on the line that builds them, module values named as written in the
retention graph, and the fix. The profiler, F# and memory-leak pages link to it,
and the F# pages play profiler-fsharp: FsStorefront's CPU and memory, narrated,
scored and captioned in every site language.
The film tools become a kit any film can use. A film is one file in films/: its
demo, its poster and its scenes, told through kit/acts/cpu.mjs and
kit/acts/memory.mjs. film.mjs (or make _film FILM=<film>) shoots and publishes
it. kit/set.mjs stages a demo from demos/ beside an isolated VS Code;
kit/workbench.mjs, camera.mjs and overlay.mjs are the moves, the recorder and
the captions; kit/sound/ speaks a reviewed script locally with Kokoro
(kokoro-js), synthesises an original score, ducks it under the voice and
normalises to -16 LUFS. The README says how to add a film.
{% video %} plays a narrated film unmuted with caption tracks; only autoplaying
loops stay muted.
Heap snapshots name every type during a CPU session, peak_bytes is the session peak, a timed CPU session ends, the UI's Stop ends the server session, and a hot function with no location stays unlocated. The F# CPU suite reads the now-optional hot-function line without assuming it.
The wire CPU suite waits for a timed session to leave cpu.list and holds its report to exactly the requested two seconds; the protocol suite rejects two params objects to cpu.list. The soak suites check the live status bar's own sample count and elapsed time, that a flat comparison resets a growth streak, and the undo history's closed generic through objectsByType, whose summary now keeps a type too small to rank.
The ground truth read the fixture's stage timers before the profiler had attached, so parse's measured share carried one to two seconds the profiler never sampled, while every share is of the profiled window ([PROF-AGGREGATION-LOGIC]). The timers are now read after live progress starts; every assertion and tolerance is unchanged.
…it bare
Every profiler operation is `workspace/executeCommand` with exactly one
argument, its typed params object (lspkit LSP-PROFILER-SPEC.md, the LSP
surface table, [PROF-WIRE-METHODS]), and the host now refuses anything
else. Three test call sites sent `cpu.list` with no argument at all,
which the host used to tolerate. Every production caller (the
lsp-profiler-ui library's 13 calls) already sends one object, and no
contribution or other editor runs these commands bare. The calls now pass
`{}`. No assertion changes.
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.
An F# project reads the C# projects it references from an in-memory Roslyn compile of their design-time command line, but that compile skipped the source generators the command line names. GitReader.Core polyfills .NET Framework with PolySharp, so for net48 its init accessors had no IsExternalInit, Roslyn emitted nothing, and the F# test project read a DLL nobody built: FS0078 and FS0084 on every net48 file. The compile now runs the /analyzer: assemblies' C# generators first, as the build does.
ensureRepoReady cloned and ran `dotnet restore` with execFileSync inside the extension host, in the same 600 s setup hook that then loads the solution and warms it up. A cold restore of GitReader on a Windows runner once took 596 s (fix/377's identical cold start took 114 s): VS Code was reported unresponsive for all of it, and the hook timed out although the restore succeeded. The clone and restore now run asynchronously, in a setup hook with a budget of its own. No assertion changes.
The generator pass passed mutable locals by reference to Roslyn's out parameters inside a match arm, which the F# optimizer turns into invalid IL in Release: every F# project referencing a C# project failed to load with "Common Language Runtime detected an invalid program". The pass now adds the driver's generated trees to the compilation, and a generator assembly that does not load here is skipped and logged instead of throwing.
MelbourneDeveloper
added a commit
that referenced
this pull request
Oct 19, 2026
## TLDR Website films play in Windows Chrome and Edge again: every video lists its H.264 MP4 before its VP9 WebM, and a new playback test proves frames actually decode. ## Details - **Root cause.** Windows Chrome/Edge accept the VP9 `<source>`, decode one frame, then stop with `PIPELINE_ERROR_DECODE`, and never try the next source. Every film listed WebM first, so every film failed there. - **`src/_data/videos.json`**: all 18 films now list `["mp4", "webm"]`. - **`video-figure.js`**: the VideoObject `contentUrl` points to the first (preferred) encoding instead of the last, so search engines link the MP4. - **New `tests/video-playback.spec.js`**: plays 20 films across docs and blog pages, then pauses, seeks to the middle and resumes. Each step checks that the clock advances, frames decode (`getVideoPlaybackQuality().totalVideoFrames`) and `video.error` stays null. A served file and a resolved `play()` don't prove playback. - **`tests/media.js`**: a new `expectFramesAdvance` helper, which also catches decode errors raised after `play()` resolves. - **`playwright.config.js`**: on Windows, the playback spec also runs on installed Chrome and Edge, where the bug reproduced; bundled Chromium and WebKit passed. The phone projects use installed Chrome there because Windows WebKit rejects known-good H.264. - **`docs/specs/DESIGN-SYSTEM.md` [WEB-DESIGN-MEDIA]** records the MP4-first rule and the playback-test requirement. - **Test expectations** in `profiler-media.spec.js` and `site-videos.spec.js` now expect MP4 first. - **Branch housekeeping.** This branch also merges the stale branches `feature/narrated-doc-films`, `feature/doc-stills` and `fix/382-netfx-review-findings` (all already squash-merged as #392, #393 and #385), plus `dependabot-upgrades`. None of them changes any file against main: - main already had every bump except TypeScript 7, which typescript-eslint 8.72 does not support yet (peer range `<6.1.0`); - the `lspkit` pin stays on lspkit `main` (`ccc59bf`). ## How Do The Automated Tests Prove It Works? - **`video-playback.spec.js`**: one test per film, named `<route> video <n> decodes, pauses, seeks and resumes`. It runs on Chromium, iPhone SE and iPhone 14 everywhere, and on Windows also on installed `chrome` and `msedge`. Each film must: - advance more than 1s and decode more than 5 new frames with `error === null`; - stay put while paused, so `currentTime` is unchanged after 250ms; - advance and decode again after seeking to `duration / 2`. - **`site-videos.spec.js` › `expectDescribed`** asserts the VideoObject `contentUrl` is the first listed source. - **`profiler-media.spec.js`** asserts the hero film's `<source>` order is MP4 then WebM. - **The full website Playwright suite (`make _test-website`)** passes locally, and Deslop reports 6.5% against the 6.65% threshold. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Fixes the three findings from the v0.22.0 static review that were still open on
main, and closes the review's CI gap for Dependabot PRs. Closes #382, closes #383, closes #384. It also proves the profiler on real F# programs: two F# fixtures, nine new VS Code suites, and the lspkit and host fixes they forced (Nimblesite/lspkit#12–#15). It adds a narrated Profiling F# docs page, and the film kit that shoots it.Details
v0.22.0 review findings
Workspace/TargetFrameworks.cs).FrameworkOfused to treat any final(…)inProject.Nameas a target framework. It now reads only the(tfm)thatMSBuildWorkspaceappends after the project file's stem, usingNativePaths.StemOf. A single-targetUtility(net48).csprojanswers{ active: null, available: [] }, as [NETFX-CONTEXT] requires.target_framework.rs,main.rs,diagnostics.rs).handle_setno longer announces. It returns aSwitchedvalue.main.rsclearsnav_cache, then calls the newdiagnostics::publish_now, and only then callsSwitched::announce, which sendssharplsp/targetFrameworkChanged, the declared refreshes and the response.publish_nowfetches and publishes every open document's diagnostics concurrently under the push gate, and waits for each first answer. A failed or provisional fetch goes on as an ordinary background push.fetch_and_publish_gatedandpublish_nowshare one newpublish_onceattempt.main.rs'spush_targetreplaces the sidecar/path lookup thattrigger_diagnosticsduplicated.src/test/suite/index.ts).resolveSuiteFilesis exported and takes its globs and platform. Off Windows, a pattern whose matches are allwindowsOnlysuites now throws, naming the pattern and [NETFX-SCOPE]. The newtest-runner-selection.test.tsis registered in theworkspacechunk..github/workflows/ci.yml). The whole pipeline skips Dependabot, and the terminalCIjob used to pass that empty run. A security bump thatdependabot-automerge.ymlfailed to sweep therefore showed a green required check and could merge intomainuntested. A new step,Fail a Dependabot PR to main, which ran no job, fails it. That enforces [DIST-CI-DEPENDABOT]'s rule that the testeddependabot-upgrades → mainconsolidation PR is the only route intomain.CI.FSharpCSharpReferences.fs). The in-memory compile of a referenced C# project now runs that project's source generators (e.g. PolySharp'sIsExternalInitpolyfill), so GitReader's net48 F# projects no longer read an unbuilt DLL (FS0078/FS0084). [SHARPLSP-ARCHITECTURE-PROJECTS-FSHARP-CSHARP-REFERENCES] says so;FSharpCSharpReferenceTests› "a C# project its source generator completes for .NET Framework is read in memory" failed first with FS0078/FS0084/FS0039.real-repo-helpers.ts,real-repo-kit.ts). The clone anddotnet restoreof a real repository now run asynchronously, in a setup hook with a budget of its own. A cold restore of a large multi-target solution (once about 10 minutes on a Windows runner, against 2 minutes usually) used to freeze the extension host and spend the whole 600 s budget that the solution load and warm-up also need. No assertion changes.docs/reviews/V0.22.0-STATIC-REVIEW.mdgains a Resolution table. The Run/F5samePathfinding was already fixed onmain. The Dependabot skip stays, but its terminal-gate pass is fixed.F# profiling
This PR also adds the F# profiler test bed ([PROFILER-SHARED-LIBRARY-VSCODE]), and moves
lspkitonto the profiler fixes those tests exposed.src/sharplsp/tests/fixtures/fsharp-profiling/). Both are real F# programs driven over a stdin line protocol:FsPricingEngineis the CPU workload. It runs CSV parsing, recursive discount rules, fuzzy search, route planning and reports, each on demand, on one workload thread.FsOrderLedgeris the memory workload. Every leak in it has its own F# module-value root:Maps;release,audit pauseandburn ongive every test a deterministic heap and CPU profile.src/editors/vscode/src/test/suite/):profiler-fsharp-cpu-e2e(new), andprofiler-fsharp-e2e(extended with an F# heap zoo): profile from the process row, F# hot functions and lines in their own source files, the results panel, exports.-memory-e2e: track, snapshot, diff, leak confidence, decorations on the constructing line.-retention-e2e: retention graphs rooted at module values by their source names, objects-by-type, GC.-corner-e2e: concurrent sessions, refusals and error codes, heap profile, generation bytes.-soak-e2eand-soak-cpu-e2e: long runs, many snapshots, coherence of the store and the UI.profiler-lsp-protocol-e2e,profiler-lsp-cpu-e2eandprofiler-lsp-memory-e2e.profiler-fsharp-kit,profiler-fsharp-soak-kit,profiler-fsharp-artifactsandprofiler-fixtures.profiler-fixturesholds the fixture locations and Release builds, moved out ofprofiler-kit.profiler-ui-debuggeetakes aDebuggeeSource, so the debug-launched suites can build an F# debuggee.profiler*.test.jschunk glob.031bfc8to lspkit main's6ea8dce, which carries the F# profiler fixes:UNSUPPORTED_RUNTIME;Order[]) is never located on the line that builds anOrder;PROCESS_NOT_FOUND;objectsByTypekeeps the requested type's own summary row when it is too small to rank among the twenty largest. An F# union case such asOrderStatus+Paidis an example.static sessions, in a layered layout from roots down to targets."", line 0;memory.stop, so the server session and its graphs end with it;cpu.listonce its duration elapses, and its report is kept for the nextstop.src/sharplsp/src/profiler/host.rs). A profiler command whose arguments are not exactly one params object isINVALID_PARAMS([PROF-WIRE-METHODS]); an absent argument list is still empty params. Every production caller already sends one object (the lsp-profiler-ui library's 13 calls). Three older suite call sites sentcpu.listbare; they now pass{}, with no assertion changes.docs/plans/PROFILER-PLAN.mdrecords the F# test bed and the 16 defects it found.A new Profiling F# page (en, ja, zh) shows what the profiler does with F#'s compiled code:
The profiler, F# and memory-leak pages link to it.
The F# pages play
profiler-fsharp, a narrated and captioned film ofFsStorefront's CPU and memory.{% video %}plays a narrated film unmuted with caption tracks; autoplaying loops stay muted.src/website/tools/profiler-film/). They become a reusable kit:films/, shot withmake _film FILM=<film>;FsStorefront, live indemos/;kit/holds the stage, camera, overlay and locally synthesised voice and score;How Do The Automated Tests Prove It Works?
v0.22.0 review findings
Each test was written first and confirmed to fail for its bug before the fix:
WorkspaceManagerTargetFrameworkTests.A_single_target_project_has_no_framework_to_chooseis now a theory overSingleandUtility(net48), on real restored projects.Assert.Null() Failure … Actual: "net48".Project, and refuse a switch.A_multi_targeted_project_named_like_a_framework_answers_its_declared_frameworksprovesTool(net10.0).csprojstill answers[net48, net10.0]and switches. All 9 tests in the class pass.test_full_stack_target_framework_switch(host e2e, real Roslyn sidecar). The fixture gained a#elsemember with an unused local, so onlynet10.0reportsCS0168. The newassert_switch_orderasserts that the publication carryingCS0168(at 8:37) arrives beforesharplsp/targetFrameworkChanged, which arrives before each refresh.test-runner-selection.test.tschecks a realwindowsOnlyname fromtest-chunks.json.netfx-language.test.jsandnetfx-*.test.jsthrow with the platform reason, while*.test.jsruns only the non-Windows suite.win32, the same patterns select their suites, and a pattern matching nothing still fails.Missing expected exception; with the guard: 2 passing.security-gates.test.mjs› the terminal gate fails a Dependabot PR to main, whose pipeline ran nothing. It parses the realci.ymland asserts:main, anddetect-changesskips Dependabot.github.actor == 'dependabot[bot]'step fails withexit 1beforeReport, namingdependabot-upgrades.AssertionError: a Dependabot PR must not report green. After: all 35tools/cicontract tests pass, and actionlint is clean.F# profiling
Each fix was proven by a test that failed before it.
Host:
profiler::host::tests::anything_but_one_params_object_is_malformedandone_params_object_or_no_argument_list_is_well_formed. The VS Code testprofiler-lsp-protocol-e2e› "all seventeen requests enforce their typed arguments" failed before the fix withsharplsp.lspProfiler.capabilities with [] should have failed.lspkit: each fix in Rename: methods, functions, local functions, and operators #12–Rename: fields and events #15 lands with a failing-first engine or UI test. For example:
dotnet_e2e›objects_of_a_small_type_keep_its_own_summary_rowfailed with no summary row forList<LeakedPayload>.a_duration_caps_the_recorded_timenow also requires thatcpu.listomits a session past its duration; it failed with the session still listed.lspkit's
make cipasses 412/412 and its UI suite passes 161.VS Code, run locally on macOS at lspkit
6ea8dce, coveringprofiler-fsharp-*,profiler-lsp-*andmemory-flows. Before the fixes: 24 passing, 8 failing. After: every test passes. One run gave 50 passing; the last, "the profiler tells the truth about four sustained F# workloads", passes (7/7 in its suite) since its Stopwatch window opens once samples flow ([PROF-AGGREGATION-LOGIC]). Before that, the window counted the profiler's unsampled attach time.capabilitiesaccepting[].The rest of the pipeline, run locally:
tscand citations all clean; Deslop at 6.59%.