-
Notifications
You must be signed in to change notification settings - Fork 133
Expand file tree
/
Copy pathallocator.test.ts
More file actions
143 lines (126 loc) · 4.44 KB
/
Copy pathallocator.test.ts
File metadata and controls
143 lines (126 loc) · 4.44 KB
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
/**
* Allocator balance checks.
*
* Each test verifies that a sequence of operations leaves
* JSMallocState.malloc_count unchanged — i.e., every js_malloc call in the C
* layer is matched by exactly one js_free call.
*
* If malloc_count drifts downward by exactly N after N calls, a pointer was
* freed via js_free() without ever having been allocated via js_malloc()
* (e.g. by using the system malloc() instead). Over time this causes the
* internal allocator accounting to underflow, corrupting the GC threshold and
* eventually causing bad behavior.
*/
import assert from "assert"
import { afterEach, beforeEach, describe, it } from "vitest"
import type { QuickJSContext, QuickJSWASMModule } from "."
import {
DEBUG_ASYNC,
DEBUG_SYNC,
getQuickJS,
memoizePromiseFactory as memoizeNewModule,
newQuickJSAsyncWASMModule,
newQuickJSWASMModule,
Scope,
} from "."
// ---------------------------------------------------------------------------
// Harness
type AllocatorCheck = (ctx: QuickJSContext) => void
/**
* Read malloc_count from the runtime's internal allocator state.
*
* The temporary handle from computeMemoryUsage() is disposed inside a nested
* Scope so it does not inflate the count we return.
*/
function readMallocCount(ctx: QuickJSContext): number {
return Scope.withScope((s) => {
const handle = s.manage(ctx.runtime.computeMemoryUsage())
return (ctx.dump(handle) as any).malloc_count as number
})
}
/**
* For each named check function, register a vitest test that:
*
* 1. Creates a fresh QuickJS context.
* 2. Warms up the computeMemoryUsage atom cache so subsequent calls do not
* allocate new atoms and shift the baseline.
* 3. Captures malloc_count before the check.
* 4. Runs the check body (which controls its own loop count and any setup).
* 5. Captures malloc_count after.
* 6. Asserts the count is unchanged.
*
* A drop equal to the number of calls indicates one unmatched js_free per
* call — a malloc/js_malloc mismatch in the C layer.
*/
function checkContextForAllocatorBalance(
getModule: () => Promise<QuickJSWASMModule>,
checks: Record<string, AllocatorCheck>,
) {
let ctx: QuickJSContext
beforeEach(async () => {
const wasmModule = await getModule()
ctx = wasmModule.newContext()
// Warm up: ensure all property-name atoms used by computeMemoryUsage() are
// already cached so they don't shift the baseline on our before/after reads.
Scope.withScope((s) => {
s.manage(ctx.runtime.computeMemoryUsage())
})
})
afterEach(() => {
ctx.dispose()
})
for (const [name, check] of Object.entries(checks)) {
it(`allocator balance: ${name}`, () => {
const countBefore = readMallocCount(ctx)
check(ctx)
const countAfter = readMallocCount(ctx)
assert.strictEqual(
countAfter,
countBefore,
`malloc_count must not drift during "${name}": ` +
`was ${countBefore}, now ${countAfter} ` +
`(drop: ${countBefore - countAfter}). ` +
`A drop equal to the number of calls indicates one unmatched ` +
`js_free per call (likely a malloc/js_malloc mismatch in the C layer).`,
)
})
}
}
// ---------------------------------------------------------------------------
// Test cases
const checks: Record<string, AllocatorCheck> = {
getOwnPropertyNames(ctx) {
const obj = ctx.newObject()
ctx.setProp(obj, "a", ctx.true)
ctx.setProp(obj, "b", ctx.true)
ctx.setProp(obj, "c", ctx.true)
ctx.setProp(obj, "d", ctx.true)
ctx.setProp(obj, "e", ctx.true)
for (let i = 0; i < 1_000; i++) {
const result = ctx.getOwnPropertyNames(obj, { strings: true })
if (result.error) {
result.error.dispose()
} else {
result.value.dispose()
}
}
obj.dispose()
},
}
describe("Allocator balance checks", () => {
describe("DEBUG sync module", () => {
const loader = memoizeNewModule(() => newQuickJSWASMModule(DEBUG_SYNC))
checkContextForAllocatorBalance(loader, checks)
})
describe("RELEASE sync module", () => {
checkContextForAllocatorBalance(getQuickJS, checks)
})
describe.skip("DEBUG async module", () => {
const loader = memoizeNewModule(() => newQuickJSAsyncWASMModule(DEBUG_ASYNC))
checkContextForAllocatorBalance(loader, checks)
})
describe("RELEASE async module", () => {
const loader = memoizeNewModule(() => newQuickJSAsyncWASMModule())
checkContextForAllocatorBalance(loader, checks)
})
})