File size: 5,622 Bytes
5655a42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import { mkdtempSync, readdirSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { performHeapDump } from './memory.js'
const ENV_KEYS = ['HERMES_AUTO_HEAPDUMP', 'HERMES_HEAPDUMP_DIR', 'HERMES_HEAPDUMP_MAX_BYTES'] as const
describe('performHeapDump auto opt-in gate (#21767)', () => {
let saved: Record<string, string | undefined>
let dir: string
beforeEach(() => {
saved = {}
for (const k of ENV_KEYS) {
saved[k] = process.env[k]
delete process.env[k]
}
dir = mkdtempSync(join(tmpdir(), 'hermes-heapdump-test-'))
process.env.HERMES_HEAPDUMP_DIR = dir
})
afterEach(() => {
for (const k of ENV_KEYS) {
if (saved[k] === undefined) {
delete process.env[k]
} else {
process.env[k] = saved[k]
}
}
rmSync(dir, { force: true, recursive: true })
})
it('writes diagnostics only for auto-high without HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.diagPath).toBeDefined()
expect(result.heapPath).toBeUndefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.diagnostics.json'))).toBe(true)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(false)
})
it('writes diagnostics only for auto-critical without HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('auto-critical')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.heapPath).toBeUndefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(false)
})
it('writes both diagnostics and snapshot for auto-high when HERMES_AUTO_HEAPDUMP=1', async () => {
process.env.HERMES_AUTO_HEAPDUMP = '1'
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBeUndefined()
expect(result.diagPath).toBeDefined()
expect(result.heapPath).toBeDefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(true)
})
it('accepts truthy spellings (true|yes|on, case-insensitive) as opt-in', async () => {
for (const value of ['true', 'YES', 'On']) {
process.env.HERMES_AUTO_HEAPDUMP = value
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.heapPath).toBeDefined()
}
})
it('treats other values (0, off, garbage) as opt-out for auto triggers', async () => {
for (const value of ['0', 'off', 'nope']) {
process.env.HERMES_AUTO_HEAPDUMP = value
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.heapPath).toBeUndefined()
}
})
it('writes both for manual triggers regardless of HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('manual')
expect(result.success).toBe(true)
expect(result.suppressed).toBeUndefined()
expect(result.heapPath).toBeDefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(true)
})
})
describe('heapdump retention guard (#21767)', () => {
let savedDir: string | undefined
let savedMax: string | undefined
let dir: string
beforeEach(() => {
savedDir = process.env.HERMES_HEAPDUMP_DIR
savedMax = process.env.HERMES_HEAPDUMP_MAX_BYTES
delete process.env.HERMES_AUTO_HEAPDUMP
dir = mkdtempSync(join(tmpdir(), 'hermes-heapdump-prune-'))
process.env.HERMES_HEAPDUMP_DIR = dir
})
afterEach(() => {
if (savedDir === undefined) {
delete process.env.HERMES_HEAPDUMP_DIR
} else {
process.env.HERMES_HEAPDUMP_DIR = savedDir
}
if (savedMax === undefined) {
delete process.env.HERMES_HEAPDUMP_MAX_BYTES
} else {
process.env.HERMES_HEAPDUMP_MAX_BYTES = savedMax
}
rmSync(dir, { force: true, recursive: true })
})
it('evicts oldest files when total bytes exceed the cap, retaining the newest', async () => {
// 4 pre-existing dumps, 1KB each, with ascending mtimes (oldest first).
const blob = 'x'.repeat(1024)
const now = Date.now()
for (let i = 0; i < 4; i++) {
const p = join(dir, `old-${i}.heapsnapshot`)
writeFileSync(p, blob)
const t = (now - (4 - i) * 60_000) / 1000
utimesSync(p, t, t)
}
// Cap at 2KB → a fresh diagnostics write should trigger a prune down to ~cap.
process.env.HERMES_HEAPDUMP_MAX_BYTES = String(2 * 1024)
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
const remaining = readdirSync(dir)
const totalBytes = remaining.reduce((acc, f) => acc + statSync(join(dir, f)).size, 0)
// Contract: prune evicts oldest-first until total <= cap, but always keeps
// the single newest file even if it alone exceeds the cap. So either the
// total is under cap, or exactly one (newest) file remains.
expect(totalBytes <= 2 * 1024 || remaining.length === 1).toBe(true)
// The old 1KB dumps must have been pruned down from the original four.
expect(remaining.length).toBeLessThan(5)
// The brand-new diagnostics sidecar must survive the prune.
expect(remaining.some(f => f.endsWith('.diagnostics.json'))).toBe(true)
})
})
|