Add wasm leak detector & fix leak
This commit is contained in:
parent
776571bc5e
commit
3abc45cb86
3 changed files with 88 additions and 5 deletions
|
|
@ -1,10 +1,22 @@
|
|||
import { reconcile, reconcileWithHistory, diff, undiff } from './index';
|
||||
import { installWasmLeakDetector, checkForWasmLeaks } from './wasm-leak-detector';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
installWasmLeakDetector();
|
||||
|
||||
afterEach(() => {
|
||||
const leaks = checkForWasmLeaks();
|
||||
if (leaks.length > 0) {
|
||||
throw new Error(
|
||||
`WASM memory leak: ${leaks.length} object(s) not freed:\n ${leaks.join('\n ')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
describe('reconcile', () => {
|
||||
it('call reconcile without cursors', () => {
|
||||
expect(reconcile('Hello', 'Hello world', 'Hi world').text).toEqual('Hi world');
|
||||
|
|
|
|||
|
|
@ -325,9 +325,15 @@ function toWasmCursorPosition({ id, position }: CursorPosition): wasmCursorPosit
|
|||
}
|
||||
|
||||
function toTextWithCursors(textWithCursor: wasmTextWithCursors): TextWithCursors {
|
||||
const wasmCursors = textWithCursor.cursors();
|
||||
const cursors = wasmCursors.map(toCursorPosition);
|
||||
for (const cursor of wasmCursors) {
|
||||
cursor.free();
|
||||
}
|
||||
|
||||
return {
|
||||
text: textWithCursor.text(),
|
||||
cursors: textWithCursor.cursors().map(toCursorPosition),
|
||||
cursors,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -338,9 +344,11 @@ function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
|
|||
};
|
||||
}
|
||||
|
||||
function toSpanWithHistory(textWithHistory: wasmSpanWithHistory): SpanWithHistory {
|
||||
return {
|
||||
text: textWithHistory.text(),
|
||||
history: textWithHistory.history(),
|
||||
function toSpanWithHistory(span: wasmSpanWithHistory): SpanWithHistory {
|
||||
const result = {
|
||||
text: span.text(),
|
||||
history: span.history(),
|
||||
};
|
||||
span.free();
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
63
reconcile-js/src/wasm-leak-detector.ts
Normal file
63
reconcile-js/src/wasm-leak-detector.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Test utility for detecting WASM memory leaks.
|
||||
*
|
||||
* wasm-bindgen registers every JS-side object with a `FinalizationRegistry`.
|
||||
* This detector patches `FinalizationRegistry.prototype.register` to collect
|
||||
* references to all WASM objects. After each test, {@link checkForWasmLeaks}
|
||||
* inspects `__wbg_ptr` on every tracked object - a non-zero pointer means
|
||||
* `.free()` was never called, i.e. a leak.
|
||||
*
|
||||
* Install once (before any WASM calls) and call {@link checkForWasmLeaks}
|
||||
* in an `afterEach` hook.
|
||||
*/
|
||||
|
||||
let trackedObjects: object[] = [];
|
||||
let originalRegister: Function | null = null;
|
||||
|
||||
interface WasmBindgenObject {
|
||||
__wbg_ptr: number;
|
||||
constructor: { name?: string };
|
||||
}
|
||||
|
||||
function isWasmBindgenObject(target: unknown): target is WasmBindgenObject {
|
||||
return (
|
||||
target !== null &&
|
||||
typeof target === 'object' &&
|
||||
'__wbg_ptr' in (target as Record<string, unknown>)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches `FinalizationRegistry.prototype.register` to track all wasm-bindgen
|
||||
* objects. Safe to call multiple times (idempotent).
|
||||
*/
|
||||
export function installWasmLeakDetector(): void {
|
||||
if (originalRegister) return;
|
||||
|
||||
originalRegister = FinalizationRegistry.prototype.register;
|
||||
|
||||
FinalizationRegistry.prototype.register = function (
|
||||
target: object,
|
||||
heldValue: unknown,
|
||||
unregisterToken?: object
|
||||
) {
|
||||
if (isWasmBindgenObject(target)) {
|
||||
trackedObjects.push(target);
|
||||
}
|
||||
return originalRegister!.call(this, target, heldValue, unregisterToken);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any tracked WASM objects whose `__wbg_ptr` is still non-zero
|
||||
* (i.e. `.free()` was never called). Clears the tracked set afterwards.
|
||||
*/
|
||||
export function checkForWasmLeaks(): string[] {
|
||||
const leaks = trackedObjects
|
||||
.filter(isWasmBindgenObject)
|
||||
.filter((obj) => obj.__wbg_ptr !== 0)
|
||||
.map((obj) => `${obj.constructor?.name ?? 'Unknown'} (ptr=${obj.__wbg_ptr})`);
|
||||
|
||||
trackedObjects = [];
|
||||
return leaks;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue