mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
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
This commit is contained in:
@@ -50,7 +50,7 @@ describe('crash breadcrumb store', () => {
|
||||
})
|
||||
|
||||
it('caps retained high-water profiles', () => {
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
for (let index = 0; index < 9; index += 1) {
|
||||
recordCrashBreadcrumb('renderer_memory_highwater', {
|
||||
rendererSurface: `surface-${index}`,
|
||||
thresholdPct: 80
|
||||
@@ -59,7 +59,46 @@ describe('crash breadcrumb store', () => {
|
||||
|
||||
expect(
|
||||
getCrashBreadcrumbSnapshot().map((breadcrumb) => breadcrumb.data?.rendererSurface)
|
||||
).toEqual(['surface-1', 'surface-2', 'surface-3', 'surface-4'])
|
||||
).toEqual([
|
||||
'surface-1',
|
||||
'surface-2',
|
||||
'surface-3',
|
||||
'surface-4',
|
||||
'surface-5',
|
||||
'surface-6',
|
||||
'surface-7',
|
||||
'surface-8'
|
||||
])
|
||||
})
|
||||
|
||||
it('retains both threshold ladders for both renderer surfaces', () => {
|
||||
for (const rendererSurface of ['main', 'dashboard-popout']) {
|
||||
for (const thresholdPct of [60, 80]) {
|
||||
recordCrashBreadcrumb('renderer_memory_highwater', { rendererSurface, thresholdPct })
|
||||
}
|
||||
for (const thresholdPrivateMB of [600, 1000]) {
|
||||
recordCrashBreadcrumb('renderer_memory_highwater', {
|
||||
rendererSurface,
|
||||
thresholdPrivateMB
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
getCrashBreadcrumbSnapshot().map((breadcrumb) => [
|
||||
breadcrumb.data?.rendererSurface,
|
||||
breadcrumb.data?.thresholdPct ?? breadcrumb.data?.thresholdPrivateMB
|
||||
])
|
||||
).toEqual([
|
||||
['main', 60],
|
||||
['main', 80],
|
||||
['main', 600],
|
||||
['main', 1000],
|
||||
['dashboard-popout', 60],
|
||||
['dashboard-popout', 80],
|
||||
['dashboard-popout', 600],
|
||||
['dashboard-popout', 1000]
|
||||
])
|
||||
})
|
||||
|
||||
it('redacts sensitive breadcrumb fields before they can be snapshotted', () => {
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from '../../shared/crash-reporting'
|
||||
|
||||
const MAX_BREADCRUMBS = 30
|
||||
// Why: retain two thresholds for each renderer surface without growing the ring.
|
||||
const MAX_RETAINED_BREADCRUMBS = 4
|
||||
// Two threshold ladders, two marks each, across both renderer surfaces.
|
||||
const MAX_RETAINED_BREADCRUMBS = 8
|
||||
// Why: coalesceKey embeds an open-string agentType (length-trimmed only, never
|
||||
// enum-checked), so the key space is unbounded over a long multi-agent/SSH session.
|
||||
// Bound the coalesce map the same way ProcessGoneDedupe bounds its key map.
|
||||
@@ -42,8 +42,14 @@ function retainedBreadcrumbKey(breadcrumb: CrashReportBreadcrumb): string | null
|
||||
return null
|
||||
}
|
||||
const surface = breadcrumb.data?.rendererSurface
|
||||
const threshold = breadcrumb.data?.thresholdPct
|
||||
return `${breadcrumb.name}:${String(surface)}:${String(threshold)}:${breadcrumb.origin ?? 'global'}`
|
||||
// Why both: the heap-ratio marks and the private-footprint marks are separate
|
||||
// one-shot ladders. Keying only on `thresholdPct` collapses every footprint
|
||||
// crumb onto one `undefined` slot, so the second mark evicts the first.
|
||||
const threshold =
|
||||
breadcrumb.data?.thresholdPct !== undefined
|
||||
? `pct${String(breadcrumb.data.thresholdPct)}`
|
||||
: `privMB${String(breadcrumb.data?.thresholdPrivateMB)}`
|
||||
return `${breadcrumb.name}:${String(surface)}:${threshold}:${breadcrumb.origin ?? 'global'}`
|
||||
}
|
||||
|
||||
/** Returns the stored breadcrumb so coalescing can refresh the entry it owns. */
|
||||
|
||||
Reference in New Issue
Block a user