Skip to content

feat(diagnostics): a diagnostics log with Explorer stall probes (#322) - #325

Merged
Maxaubert merged 10 commits into
mainfrom
feat/322-diagnostics-log
Oct 8, 2026
Merged

Maxaubert merged 10 commits into
mainfrom
feat/322-diagnostics-log

Conversation

@Maxaubert

Copy link
Copy Markdown
Owner

Closes #322

Owner: "implement some robust logging and debugging into the program especially to catch stalls for example in explorer or in general".

The core half is Prism Terminal #141 (merged), released as core-v0.27.0. This PR pins core-v0.27.0 and supersedes bot PR #324, which bumped the same pin without Prism's half. Prism goes to 0.97.0 (minor, a feature).

What changes

  • A diagnostics log at %APPDATA%\Prism\logs\diag.jsonl (the core's writer): session line, window events, IPC timing (ipc-slow, ipc-error), main-thread lag with the calls in flight (main-lag), page stalls with a stack (page-stall, page-stack), page errors and rejections. Detailed logging (a setting) adds every call and the quick crumbs.
  • Explorer stall probes (crumbs): open-folder, navigate (per tab, with its reason: refresh, focus, dir-changed), watch, search-start / search-end (with the query), folder-size, archive-job, player-open, sort-slow (rate-gated: one line per trigger per 5 s, with held / heldMaxMs), guard-slow (the desktop path guard times itself). Quick re-reads (under 200 ms) and per-keystroke crumbs are Detailed only, so a dir-changed storm does not flood the quiet log.
  • Window events through src/main/diagWindow.ts: gone / unresponsive / responsive are left to the core (no double lines); hang and watchdog become window-slow, which npm run diag already ranks as a problem; handoff and restore are crumbs.
  • Quit order: the log stops after cancelAllConversions(), so the last lines of a quit are written.
  • Privacy: the README says the log holds full file and folder paths, archive names and destinations and search text in plain text, and that an extra Explorer window keeps its own log. The header hook is filtered to main frames.
  • e2e: diagLog, explorerDiag, and a closing Stalls table after every run (a report, not a gate). Under --e2e main exposes __e2eDiagFlush, called before app.exit.

longWaitChannels (never ipc-slow, never in a main-lag's inflight)

The rule: the call waits on the USER, or it is a long job by design whose work runs OFF main's thread and has a window, crumb or progress of its own. Work in main's thread stays timed (there a slow answer is the stall).

Channels Why
dialog:pick-folder, open:folder, dialog:pick-files, open:dialog, subs:pick, image:save-copy wait on a system dialog
browse:search, browse:suggest, search:files recursive walk or Everything, cancellable; search-* crumbs time it
folder:size full recursive scan, cancellable; folder-size crumb
archive:extract-to, archive:extract-members-picked, archive:extract-all, archive:extract-dir, archive:member-out 7-Zip / extractor jobs with the extraction window; archive-job crumbs
comic:open always 7-Zip, every page
video:convert ffmpeg, own progress
update:install the download (or the preview's fake progress)
file:paste-into, file:move copies with progress, as long as the bytes
file:duplicate async copy of a file that may be gigabytes
file:restore PowerShell child walks the Recycle Bin (20 s cap)
media:peaks, audio:synth ffmpeg / FluidSynth decode a whole track in a child
dictation:transcribe (DCH.transcribe) whisper-server / parakeet-cli; Parakeet MEASURED 0.72-0.86 s per pass, a cold GPU's first 31.8 s

Kept timed on purpose: archive:delete, archive:add, archive:move-members, archive:rename, doc:html (in process), folder:sizes-cached (its path guard is suspect 1), and archive:list / archive:extract / archive:member (a small .zip is read by adm-zip on main's thread, and an automatic preview is size-capped). A unit test holds every name to a registration in index.ts or the core's dictation table.

Gates

  • Typecheck: clean.
  • Lint: 0 errors, 7 warnings (all on lines this PR does not touch).
  • Unit: 179 files, 2603 tests passed, 2 skipped.
  • e2e diagLog and explorerDiag: pass. The navigated marker fix was checked in reverse: without it the focus re-read never appears and explorerDiag fails.
  • npm run e2e:terminal: all checks passed.
  • Full e2e: all 116 scenarios passed (one build, seven foreground runs of tools/e2e/run.mjs), including columnHeaders, panelsAlign, jsonc, dragLabel, toolbarIcons.

Stalls (from the full e2e run; a report, not a gate)

Scenario Kind ms Detail
diagLog page-stall 2500 expected, the scenario's own setTimeout busy loop
diagLog page-stack 2031 expected, diagE2eBusy
diagLog page-error / page-rejection - expected, thrown and rejected on purpose
neverWindowless gone (x9) - renderer crashed, caused on purpose by the scenario (the core's own lines)
neverWindowless, markTint crumb folder-size 1363-2259 C:\Users\Admin\.agents, .bun (size scans, off main)
extractCancel, sidebarPlaces, sidebarGround crumb folder-size 1097-2815 worktree folders, C:\$Recycle.Bin, C:\asr
extractCancel crumb archive-job 1056 the cancelled extraction
sidebarGround ipc-slow + main-lag 1243 / 1210 window-preferences:set
sidebarGround page-stall 1258 blocking 1208

The one real suspect the log found in our own suite is window-preferences:set in sidebarGround (1.2 s on main's thread), worth a follow-up issue. Runs 1, 5, 6 and 7 showed no stalls.

Follow-up (not in this PR)

dictation:transcribe belongs in the core's own LONG_WAIT too, so Prism Terminal gets the same fix; that is a core version bump in Prism Terminal.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

Maxaubert and others added 9 commits October 7, 2026 16:47
…322)

prism-term-core is pinned to core-v0.27.0-rc.1 (the diagnostics log's
release candidate, Maxaubert/PrismTerminal#141) and Prism goes to 0.94.0,
the next minor above every open PR.

- The preload spreads the core's diag bridge (createDiagApi).
- Settings gets the core's DiagnosticsPage (Detailed logging, Log files,
  Mark a problem) as a rail page in the bottom group, beside About; its
  rows are in Find a setting (coreSettingsIndex diagnostics: true) and in
  ROW_ORDER before About's.
- diagnosticsOptions joins the core's plain .ts modules resolved by path
  (vite, vitest, tsconfig), for the settings tests.
- settingsLook's rail and pages include Diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
The terminal core's diagnostics log (Maxaubert/PrismTerminal#140), wired
into Prism's main and page.

- startDiagnostics once the single-instance lock is won, before every
  ipcMain registration, with <userData>\logs as its folder.
- LONG_WAIT_CHANNELS (src/main/diagChannels.ts): Prism's dialogs,
  searches, the folder size scan, the extraction window's jobs, the video
  convert, the update download, copies and the decoders. In-process work
  stays timed: there a slow answer is the stall. A test holds every name
  to a registration.
- logWindow writes each window event into the log as well;
  window-crashes.log is kept.
- withStackPolicy on the window's document, so the page stack can be read.
- watchWindow on every window made, stop on the quit that goes ahead, a
  synchronous flush at a Windows shutdown.
- The boot entry starts the page's half before the app chunk loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…og (#322)

What the diagnostics design asked of the Explorer, so a stall there can be
told from its numbers:

- open-folder (useFolderBrowsing): path, reason (navigate, back, forward,
  refresh, focus, dir-changed, retry, arrival), ms, entries, cached; a
  navigation's read is said once though the location effect joins it.
- sort-slow (sortTiming.ts): the sort-and-filter pass times itself, with
  the row count and the input that made it run again (sizes, query, sort,
  listing, dates). Suspect 3.
- guard-slow (desktopAccess): one guard call of 50 ms or more, with the
  paths asked about and the grants held. Folder sizes filter a list as ONE
  call (insideDesktopAll), so the loop is what is timed. Suspect 1.
- search-start / search-end (ms, hits, source) / search-cancel; the filter
  per keystroke and a search replaced by the next are Detailed only.
- folder-size (path, ms), watch (path, ms): main crumbs through
  mainCrumb; a quick one is Detailed only.
- archive-job start and end (result, ms) from the extraction window's
  channel.
- preview on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
The rest of Prism's timeline for the diagnostics log:

- lib/tabCrumbs (pure, tested): tab-open, tab-close, tab-switch,
  project-open and player-open, read off the tab state rather than said
  at each of the dozen places a tab changes.
- sort: a column picked in the Explorer.
- settings-page: the Settings page looked at.

The update window, its install and dictation say theirs in the core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
README: the log is kept on this PC only, in %APPDATA%\Prism\logs, never
sent, and Settings > Diagnostics opens it. CLAUDE.md: read a stall from the
log first (Prism Terminal's docs/diagnostics.md, npm run diag -- --app
prism there), and where the wiring, the long-wait channels and the
Explorer's probes live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
diagLog makes the page stall, throw and reject, and calls an e2e-only
channel main answers after 600 ms (e2e:slow-ipc, registered under --e2e
alone), then quits and reads the log: the page-stall with its stack, the
error, the rejection and the ipc-slow line must all be there. It joins
e2e:terminal, since the log is the core's.

explorerDiag opens a folder of six files and a subfolder and presses the
Name header: the log must hold an open-folder crumb (reason navigate, the
read's ms, 7 entries, whether a cache painted first) and a sort crumb.

Every scenario's own log is read after it, and the run ends with a Stalls
table: lines of 1 s or more and errors, per scenario. A report, not a
gate: a slow line on a busy machine is a lead, not a failure. diagLog's
own lines are marked "(expected)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
From 0.27.0-rc.1 to the release. It also carries core 0.26.0's on switch
in the theme's accent (#138), so settingsLook leaves role="switch" out of
its "only Save wears the accent" count, the same change as the 0.26.0
bump (Prism #323).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…e-v0.27.0

Main moved to 0.96.0 with core-v0.26.0; this branch keeps core-v0.27.0
(the released diagnostics log) and takes the next minor, 0.97.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
- Window guard lines: gone, unresponsive and responsive are left to the
  core (they were written twice); a hang and the watchdog are window-slow,
  which npm run diag lists; handoff and restore are crumbs.
- The log stops LAST on the quit, after the shell, sidecar and conversion
  teardown.
- open-folder: the navigate marker is per tab and taken back once its read
  answers (a navigation to the folder on screen swallowed the next crumb);
  an abandoned read is still said (abandoned: true); the refresh key is
  taken before any early return, so a refresh from another tab is not
  mislabelled; a quick dir-changed or focus re-read is Detailed only.
- search-start is Detailed only; search-end reaches the quiet log only at
  500 ms or more, and carries the query.
- sort-slow is rate-gated per trigger (one line per 5 s, held passes
  counted on the next line).
- player-open from the preview pane beside the list is Detailed only.
- Long-wait channels: open:folder, comic:open, file:duplicate,
  file:restore and dictation:transcribe; the archive reads that can run
  in main's thread stay timed, and the rule says why.
- ownsDesktopDirectory times itself; the watch crumb is written only when
  a watcher was set up (or refused).
- The Document-Policy header hook is filtered to main frames, so other
  responses never reach main's thread.
- e2e: explorerDiag asserts one crumb per navigation and a focus re-read
  after a same-folder navigation (fails without the marker fix); the
  harness flushes main's log before app.exit for the Stalls table.
- README says the log holds full paths and search text; the CLAUDE.md
  entry is cut to the rule and the pointers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
A CI runner's temp folder is an 8.3 short path. The guard resolves a
granted folder that exists but not a child that does not, so the two
timing tests' paths read as outside on CI only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
@Maxaubert
Maxaubert merged commit 322be23 into main Oct 8, 2026
3 checks passed
@Maxaubert
Maxaubert deleted the feat/322-diagnostics-log branch October 8, 2026 09:51
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.

Diagnostics log: stalls, slow IPC, errors, Explorer probes

1 participant