Files
orca/src/preload/renderer-process-memory-reader.test.ts
T
Neil 91a500712c fix(crash-reporting): see the renderer memory the heap counters never report (#16449)
* fix(crash-reporting): see the renderer memory the heap counters never report

Windows renderer crash 36048e26 arrived with 618MB of private renderer memory
and a `renderer_memory` breadcrumb reporting a 150MB V8 heap. Both numbers were
right: xterm scrollback lives in `Uint32Array` backing stores and glyph atlases
live in GPU transfer buffers, and neither is counted by `usedHeapSize`,
`mallocedMemory`, or Blink's allocator.

That made the report unanalyzable. `renderer_memory_highwater` is the crumb
carrying the subsystem census that names what grew, and it is armed on
`usedHeapSize / heapSizeLimit`. At 150MB of a 4192MB limit that ratio is 3.6% —
nowhere near the 60% mark — so the census never reached a single one of these
reports.

Measured on Windows (6 worktrees x 4 terminal tabs, 8000 lines each, this app
at 4218d505): filling 24 mounted panes moved the renderer working set from
210MB to 656MB while `usedJSHeapSize` stayed at 43MB for the whole run.

Sample the renderer's own OS footprint through `process.getProcessMemoryInfo()`
(available in the sandboxed preload) and:

- report `privateMB`, `residentMB`, and `outsideHeapMB` — the footprint minus
  everything V8 and Blink admit to holding — on every `renderer_memory` crumb;
- arm the highwater census on private-footprint marks (600MB / 1000MB) as well
  as the heap ratio, so growth outside the JS heap now carries the pane and
  store census that names it.

The footprint read is async, so a sample annotates with the previous read and
refreshes in the background: one interval of staleness is irrelevant to a
footprint trend, and awaiting it would make every sample reentrant. A shell
without the bridge, or a runtime that withholds the read, keeps sampling
exactly as before.

Retained-breadcrumb keys now distinguish the two threshold ladders; keying only
on `thresholdPct` collapsed every footprint crumb onto one slot.

crash-diagnostics.ts split at the max-lines budget: memory sampling moves to
renderer-memory-sampling.ts and the shared payload shaping to
crash-breadcrumb-data.ts.

* fix(crash-reporting): retain all renderer memory marks
2026-08-25 18:38:42 -07:00

48 lines
2.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { readRendererProcessMemory } from './renderer-process-memory-reader'
// Why the partial type: Electron types `residentSet` as required, but Chromium
// omits it on macOS — the reader's optional handling exists for exactly that.
const source = (
getProcessMemoryInfo: () => Promise<Partial<Electron.ProcessMemoryInfo>>
): Parameters<typeof readRendererProcessMemory>[0] =>
({ getProcessMemoryInfo }) as unknown as Parameters<typeof readRendererProcessMemory>[0]
describe('readRendererProcessMemory', () => {
it('reports the private footprint in the kilobytes Electron returns', async () => {
await expect(
readRendererProcessMemory(
source(async () => ({ private: 632_832, residentSet: 1_143_808, shared: 0 }))
)
).resolves.toEqual({ privateKB: 632_832, residentKB: 1_143_808 })
})
it('omits the resident set where Chromium does not report one', async () => {
await expect(
readRendererProcessMemory(source(async () => ({ private: 1024, shared: 0 })))
).resolves.toEqual({ privateKB: 1024 })
})
it('returns null rather than throwing when the runtime withholds the read', async () => {
await expect(
readRendererProcessMemory(
source(() => Promise.reject(new Error('getProcessMemoryInfo unavailable')))
)
).resolves.toBeNull()
})
it('returns null for a non-finite private size', async () => {
// Why: a NaN would propagate into breadcrumb megabytes and read as a real
// footprint of zero, which is worse than reporting nothing.
await expect(
readRendererProcessMemory(source(async () => ({ private: Number.NaN, shared: 0 })))
).resolves.toBeNull()
})
it('does not call the API more than once per read', async () => {
const getProcessMemoryInfo = vi.fn(async () => ({ private: 2048, shared: 0 }))
await readRendererProcessMemory(source(getProcessMemoryInfo))
expect(getProcessMemoryInfo).toHaveBeenCalledTimes(1)
})
})