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 { reconcile, reconcileWithHistory, diff, undiff } from './index';
|
||||||
|
import { installWasmLeakDetector, checkForWasmLeaks } from './wasm-leak-detector';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.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', () => {
|
describe('reconcile', () => {
|
||||||
it('call reconcile without cursors', () => {
|
it('call reconcile without cursors', () => {
|
||||||
expect(reconcile('Hello', 'Hello world', 'Hi world').text).toEqual('Hi world');
|
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 {
|
function toTextWithCursors(textWithCursor: wasmTextWithCursors): TextWithCursors {
|
||||||
|
const wasmCursors = textWithCursor.cursors();
|
||||||
|
const cursors = wasmCursors.map(toCursorPosition);
|
||||||
|
for (const cursor of wasmCursors) {
|
||||||
|
cursor.free();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: textWithCursor.text(),
|
text: textWithCursor.text(),
|
||||||
cursors: textWithCursor.cursors().map(toCursorPosition),
|
cursors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,9 +344,11 @@ function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSpanWithHistory(textWithHistory: wasmSpanWithHistory): SpanWithHistory {
|
function toSpanWithHistory(span: wasmSpanWithHistory): SpanWithHistory {
|
||||||
return {
|
const result = {
|
||||||
text: textWithHistory.text(),
|
text: span.text(),
|
||||||
history: textWithHistory.history(),
|
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