mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
* refactor: gin_helper::Promise managed by cppgc * fix: broken liveness test * refactor: move handle dependent members to base class
724 lines
30 KiB
TypeScript
724 lines
30 KiB
TypeScript
import { expect } from 'chai';
|
|
|
|
import { once } from 'node:events';
|
|
import * as path from 'node:path';
|
|
|
|
import { ifdescribe, isTestingBindingAvailable, startRemoteControlApp } from './lib/spec-helpers';
|
|
|
|
describe('cpp heap', () => {
|
|
describe('app module', () => {
|
|
it('should not allocate on every require', async () => {
|
|
const { remotely } = await startRemoteControlApp();
|
|
const [usedBefore, usedAfter] = await remotely(async () => {
|
|
const { getCppHeapStatistics } = require('node:v8');
|
|
const heapStatsBefore = getCppHeapStatistics('brief');
|
|
{
|
|
const { app } = require('electron');
|
|
console.log(app.name);
|
|
}
|
|
{
|
|
const { app } = require('electron');
|
|
console.log(app.dock);
|
|
}
|
|
const heapStatsAfter = getCppHeapStatistics('brief');
|
|
return [heapStatsBefore.used_size_bytes, heapStatsAfter.used_size_bytes];
|
|
});
|
|
expect(usedBefore).to.be.equal(usedAfter);
|
|
});
|
|
|
|
it('should record as node in heap snapshot', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string) => {
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
const state = recordState();
|
|
return containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / App']);
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js')
|
|
);
|
|
expect(result).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('session module', () => {
|
|
it('does not crash on exit with live session wrappers', async () => {
|
|
const rc = await startRemoteControlApp();
|
|
await rc.remotely(async () => {
|
|
const { app, session } = require('electron');
|
|
|
|
const sessions = [
|
|
session.defaultSession,
|
|
session.fromPartition('cppheap-exit'),
|
|
session.fromPartition('persist:cppheap-exit-persist')
|
|
];
|
|
|
|
// We want to test GC on shutdown, so add a global reference
|
|
// to these sessions to prevent pre-shutdown GC.
|
|
(globalThis as any).sessionRefs = sessions;
|
|
|
|
// We want to test CppGC-traced references during shutdown.
|
|
// The CppGC-managed cookies will do that; but since they're
|
|
// lazy-created, access them here to ensure they're live.
|
|
sessions.forEach((ses) => ses.cookies);
|
|
|
|
setTimeout(() => app.quit());
|
|
});
|
|
|
|
const [code] = await once(rc.process, 'exit');
|
|
expect(code).to.equal(0);
|
|
});
|
|
|
|
it('should record as node in heap snapshot', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string) => {
|
|
const { session, BrowserWindow } = require('electron');
|
|
const { once } = require('node:events');
|
|
const assert = require('node:assert');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
const session1 = session.defaultSession;
|
|
console.log(session1.getStoragePath());
|
|
const session2 = session.fromPartition('cppheap1');
|
|
const session3 = session.fromPartition('cppheap1');
|
|
const session4 = session.fromPartition('cppheap2');
|
|
console.log(session2.cookies);
|
|
assert.strictEqual(session2, session3);
|
|
assert.notStrictEqual(session2, session4);
|
|
const w = new BrowserWindow({
|
|
show: false,
|
|
webPreferences: {
|
|
session: session.fromPartition('cppheap1')
|
|
}
|
|
});
|
|
await w.loadURL('about:blank');
|
|
const state = recordState();
|
|
const isClosed = once(w, 'closed');
|
|
w.destroy();
|
|
await isClosed;
|
|
const numSessions = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / Session'], {
|
|
occurrences: 4
|
|
});
|
|
const canTraceJSReferences = containsRetainingPath(state.snapshot, [
|
|
'C++ Persistent roots',
|
|
'Electron / Session',
|
|
'Electron / Cookies'
|
|
]);
|
|
return numSessions && canTraceJSReferences;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js')
|
|
);
|
|
expect(result).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('cookies module', () => {
|
|
it('should return the same Cookies instance for the same session', async () => {
|
|
const { remotely } = await startRemoteControlApp();
|
|
const result = await remotely(async () => {
|
|
const { session } = require('electron');
|
|
const ses = session.fromPartition('cookies-identity');
|
|
const cookies1 = ses.cookies;
|
|
const cookies2 = ses.cookies;
|
|
return cookies1 === cookies2;
|
|
});
|
|
expect(result).to.equal(true, 'cookies getter should return the same instance');
|
|
});
|
|
|
|
it('should survive GC when only referenced through session', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string) => {
|
|
const { session } = require('electron');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
// Access cookies to create the C++ object, then drop the JS reference.
|
|
const ses = session.defaultSession;
|
|
let cookies: any = ses.cookies;
|
|
await cookies.get({});
|
|
cookies = null;
|
|
|
|
// Force GC — the Cookies object should survive because
|
|
// Session traces it via cppgc::Member.
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
|
|
const state = recordState();
|
|
const stillAlive = containsRetainingPath(state.snapshot, [
|
|
'C++ Persistent roots',
|
|
'Electron / Session',
|
|
'Electron / Cookies'
|
|
]);
|
|
return stillAlive;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js')
|
|
);
|
|
expect(result).to.equal(true, 'Cookies should survive GC when traced from Session');
|
|
});
|
|
});
|
|
|
|
ifdescribe(isTestingBindingAvailable())('SafeV8Function callback conversion', () => {
|
|
const gcTestArgv = ['--js-flags=--expose-gc'];
|
|
|
|
it('retains repeating callback while held, allows multiple invocations, then releases', async () => {
|
|
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
|
const result = await remotely(async () => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
const waitForGC = async (fn: () => boolean) => {
|
|
for (let i = 0; i < 30; ++i) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
if (fn()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
let callCount = 0;
|
|
let repeating: any = () => {
|
|
callCount++;
|
|
};
|
|
const repeatingWeakRef = new WeakRef(repeating);
|
|
testingBinding.holdRepeatingCallbackForTesting(repeating);
|
|
repeating = null;
|
|
|
|
const invoked0 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
|
const invoked1 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
|
const invoked2 = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
|
|
|
testingBinding.clearHeldCallbacksForTesting();
|
|
const releasedAfterClear = await waitForGC(() => repeatingWeakRef.deref() === undefined);
|
|
|
|
return { invoked0, invoked1, invoked2, callCount, releasedAfterClear };
|
|
});
|
|
|
|
expect(result.invoked0).to.equal(true, 'first invocation should succeed');
|
|
expect(result.invoked1).to.equal(true, 'second invocation should succeed');
|
|
expect(result.invoked2).to.equal(true, 'third invocation should succeed');
|
|
expect(result.callCount).to.equal(3, 'callback should have been called 3 times');
|
|
expect(result.releasedAfterClear).to.equal(true, 'callback should be released after clear');
|
|
});
|
|
|
|
it('consumes once callback on first invoke and releases it', async () => {
|
|
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
|
const result = await remotely(async () => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
const waitForGC = async (fn: () => boolean) => {
|
|
for (let i = 0; i < 30; ++i) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
if (fn()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
let callCount = 0;
|
|
let once: any = () => {
|
|
callCount++;
|
|
};
|
|
const onceWeakRef = new WeakRef(once);
|
|
testingBinding.holdOnceCallbackForTesting(once);
|
|
once = null;
|
|
|
|
const first = testingBinding.invokeHeldOnceCallbackForTesting();
|
|
const second = testingBinding.invokeHeldOnceCallbackForTesting();
|
|
|
|
testingBinding.clearHeldCallbacksForTesting();
|
|
const released = await waitForGC(() => onceWeakRef.deref() === undefined);
|
|
|
|
return { first, second, callCount, released };
|
|
});
|
|
|
|
expect(result.first).to.equal(true, 'first invoke should succeed');
|
|
expect(result.second).to.equal(false, 'second invoke should fail (consumed)');
|
|
expect(result.callCount).to.equal(1, 'callback should have been called once');
|
|
expect(result.released).to.equal(true, 'callback should be released after consume + clear');
|
|
});
|
|
|
|
it('releases replaced repeating callback while keeping latest callback alive', async () => {
|
|
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
|
const result = await remotely(async () => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
const waitForGC = async (fn: () => boolean) => {
|
|
for (let i = 0; i < 30; ++i) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
if (fn()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
let callbackA: any = () => {};
|
|
const weakA = new WeakRef(callbackA);
|
|
testingBinding.holdRepeatingCallbackForTesting(callbackA);
|
|
callbackA = null;
|
|
|
|
let callbackB: any = () => {};
|
|
const weakB = new WeakRef(callbackB);
|
|
testingBinding.holdRepeatingCallbackForTesting(callbackB);
|
|
callbackB = null;
|
|
|
|
const releasedA = await waitForGC(() => weakA.deref() === undefined);
|
|
|
|
testingBinding.clearHeldCallbacksForTesting();
|
|
const releasedB = await waitForGC(() => weakB.deref() === undefined);
|
|
|
|
return { releasedA, releasedB };
|
|
});
|
|
|
|
expect(result.releasedA).to.equal(true, 'replaced callback A should be released');
|
|
expect(result.releasedB).to.equal(true, 'callback B should be released after clear');
|
|
});
|
|
|
|
it('keeps callback alive while copied holder exists and releases after all copies clear', async () => {
|
|
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
|
const result = await remotely(async () => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
const waitForGC = async (fn: () => boolean) => {
|
|
for (let i = 0; i < 30; ++i) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
if (fn()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
let repeating: any = () => {};
|
|
const weakRef = new WeakRef(repeating);
|
|
testingBinding.holdRepeatingCallbackForTesting(repeating);
|
|
repeating = null;
|
|
|
|
const copied = testingBinding.copyHeldRepeatingCallbackForTesting();
|
|
const countAfterCopy = testingBinding.getHeldRepeatingCallbackCountForTesting();
|
|
testingBinding.clearPrimaryHeldRepeatingCallbackForTesting();
|
|
|
|
const invokedViaCopy = testingBinding.invokeCopiedRepeatingCallbackForTesting();
|
|
|
|
testingBinding.clearHeldCallbacksForTesting();
|
|
const releasedAfterClear = await waitForGC(() => weakRef.deref() === undefined);
|
|
|
|
return { copied, countAfterCopy, invokedViaCopy, releasedAfterClear };
|
|
});
|
|
|
|
expect(result.copied).to.equal(true, 'copy should succeed');
|
|
expect(result.countAfterCopy).to.equal(2, 'should have 2 holders after copy');
|
|
expect(result.invokedViaCopy).to.equal(true, 'invoke via copy should succeed');
|
|
expect(result.releasedAfterClear).to.equal(true, 'callback should be released after all copies clear');
|
|
});
|
|
|
|
it('does not leak repeating callback when callback throws during invocation', async () => {
|
|
const { remotely } = await startRemoteControlApp(gcTestArgv);
|
|
const result = await remotely(async () => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
const waitForGC = async (fn: () => boolean) => {
|
|
for (let i = 0; i < 30; ++i) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
if (fn()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
let throwing: any = () => {
|
|
throw new Error('expected test throw');
|
|
};
|
|
const weakRef = new WeakRef(throwing);
|
|
testingBinding.holdRepeatingCallbackForTesting(throwing);
|
|
throwing = null;
|
|
|
|
const invokeResult = testingBinding.invokeHeldRepeatingCallbackForTesting();
|
|
|
|
testingBinding.clearHeldCallbacksForTesting();
|
|
const releasedAfterClear = await waitForGC(() => weakRef.deref() === undefined);
|
|
|
|
return { invokeResult, releasedAfterClear };
|
|
});
|
|
|
|
expect(result.invokeResult).to.equal(false, 'invoke should fail (callback throws)');
|
|
expect(result.releasedAfterClear).to.equal(true, 'throwing callback should be released after clear');
|
|
});
|
|
});
|
|
|
|
describe('menu module', () => {
|
|
// Regression test for https://github.com/electron/electron/issues/50791
|
|
it('should not leak when rebuilding application menu', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--js-flags=--expose-gc']);
|
|
const result = await remotely(async () => {
|
|
const { Menu } = require('electron');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
const { getCppHeapStatistics } = require('node:v8');
|
|
|
|
function buildLargeMenu() {
|
|
return Menu.buildFromTemplate(
|
|
Array.from({ length: 10 }, (_, i) => ({
|
|
label: `Menu ${i}`,
|
|
submenu: Array.from({ length: 20 }, (_, j) => ({
|
|
label: `Item ${i}-${j}`,
|
|
click: () => {}
|
|
}))
|
|
}))
|
|
);
|
|
}
|
|
|
|
async function rebuildAndMeasure(n: number) {
|
|
for (let i = 0; i < n; i++) {
|
|
Menu.setApplicationMenu(buildLargeMenu());
|
|
}
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
return getCppHeapStatistics('brief').used_size_bytes;
|
|
}
|
|
|
|
await rebuildAndMeasure(50);
|
|
const after1 = await rebuildAndMeasure(50);
|
|
const after2 = await rebuildAndMeasure(50);
|
|
return { after1, after2 };
|
|
});
|
|
|
|
const growth = result.after2 - result.after1;
|
|
expect(growth).to.be.at.most(
|
|
result.after1 * 0.1,
|
|
`C++ heap grew by ${growth} bytes between two identical rounds of 100 menu rebuilds — likely a leak`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('internal event', () => {
|
|
it('should record as node in heap snapshot', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string) => {
|
|
const { BrowserWindow } = require('electron');
|
|
const { once } = require('node:events');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
|
|
const w = new BrowserWindow({
|
|
show: false
|
|
});
|
|
await w.loadURL('about:blank');
|
|
const state = recordState();
|
|
const isClosed = once(w, 'closed');
|
|
w.destroy();
|
|
await isClosed;
|
|
const eventNativeStackReference = containsRetainingPath(state.snapshot, [
|
|
'C++ native stack roots',
|
|
'Electron / Event'
|
|
]);
|
|
const noPersistentReference = !containsRetainingPath(state.snapshot, [
|
|
'C++ Persistent roots',
|
|
'Electron / Event'
|
|
]);
|
|
return eventNativeStackReference && noPersistentReference;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js')
|
|
);
|
|
expect(result).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('powerMonitor module', () => {
|
|
it('should retain native PowerMonitor via JS module reference', async () => {
|
|
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await rc.remotely(
|
|
async (heap: string) => {
|
|
const { powerMonitor, app } = require('electron');
|
|
const { recordState } = require(heap);
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
// Register a listener to trigger native PowerMonitor creation.
|
|
const listener = () => {};
|
|
powerMonitor.on('suspend', listener);
|
|
|
|
// Add and remove several listeners to exercise the path,
|
|
// then GC to ensure no duplicate native objects are created.
|
|
for (let i = 0; i < 5; i++) {
|
|
const tmp = () => {};
|
|
powerMonitor.on('resume', tmp);
|
|
powerMonitor.removeListener('resume', tmp);
|
|
}
|
|
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
|
|
const state = recordState();
|
|
const nodes = state.snapshot.filter((node: any) => node.name === 'Electron / PowerMonitor');
|
|
const found = nodes.length > 0;
|
|
const noDuplicates = nodes.length === 1;
|
|
|
|
setTimeout(() => app.quit());
|
|
return { found, noDuplicates };
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap')
|
|
);
|
|
|
|
const [code] = await once(rc.process, 'exit');
|
|
expect(code).to.equal(0);
|
|
expect(result.found).to.equal(true, 'PowerMonitor should be in snapshot (held by JS module)');
|
|
expect(result.noDuplicates).to.equal(true, 'should have exactly one PowerMonitor instance');
|
|
});
|
|
});
|
|
|
|
describe('utilityProcess module', () => {
|
|
it('should appear in heap snapshot while process is running', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
|
const { utilityProcess } = require('electron');
|
|
const { once } = require('node:events');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
|
|
const child = utilityProcess.fork(fixturePath);
|
|
await once(child, 'spawn');
|
|
|
|
const state = recordState();
|
|
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
|
|
|
|
child.kill();
|
|
await once(child, 'exit');
|
|
return found;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
|
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
|
|
);
|
|
expect(result).to.equal(true);
|
|
});
|
|
|
|
it('should be released from heap snapshot after process exits', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await remotely(
|
|
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
|
const { utilityProcess } = require('electron');
|
|
const { once } = require('node:events');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
let child: any = utilityProcess.fork(fixturePath);
|
|
await once(child, 'exit');
|
|
child = null;
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
|
|
const state = recordState();
|
|
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
|
|
return !found;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
|
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
|
|
);
|
|
expect(result).to.equal(true, 'UtilityProcess should be released after exit and GC');
|
|
});
|
|
|
|
it('should survive GC when JS reference is dropped but process is still running', async () => {
|
|
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await rc.remotely(
|
|
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
|
const { utilityProcess, app } = require('electron');
|
|
const { once } = require('node:events');
|
|
const { recordState } = require(heap);
|
|
const { containsRetainingPath } = require(snapshotHelper);
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
let child: any = utilityProcess.fork(fixturePath);
|
|
await once(child, 'spawn');
|
|
child = null;
|
|
|
|
// Force GC — the process should still be alive because
|
|
// SelfKeepAlive roots the C++ wrapper.
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
|
|
const state = recordState();
|
|
const stillAlive = containsRetainingPath(state.snapshot, [
|
|
'C++ Persistent roots',
|
|
'Electron / UtilityProcess'
|
|
]);
|
|
|
|
setTimeout(() => app.quit());
|
|
return stillAlive;
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
|
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
|
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
|
|
);
|
|
|
|
const [code] = await once(rc.process, 'exit');
|
|
expect(code).to.equal(0);
|
|
expect(result).to.equal(true, 'UtilityProcess should survive GC while process is running');
|
|
});
|
|
|
|
it('should not leak when forking multiple processes', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--js-flags=--expose-gc']);
|
|
const result = await remotely(
|
|
async (fixturePath: string) => {
|
|
const { utilityProcess } = require('electron');
|
|
const { once } = require('node:events');
|
|
const { getCppHeapStatistics } = require('node:v8');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
|
|
async function forkAndWait() {
|
|
const child = utilityProcess.fork(fixturePath);
|
|
await once(child, 'exit');
|
|
}
|
|
|
|
async function measure(n: number) {
|
|
for (let i = 0; i < n; i++) {
|
|
await forkAndWait();
|
|
}
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
return getCppHeapStatistics('brief').used_size_bytes;
|
|
}
|
|
|
|
await measure(5);
|
|
const after1 = await measure(10);
|
|
const after2 = await measure(10);
|
|
return { after1, after2 };
|
|
},
|
|
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
|
|
);
|
|
|
|
const growth = result.after2 - result.after1;
|
|
expect(growth).to.be.at.most(
|
|
result.after1 * 0.1,
|
|
`C++ heap grew by ${growth} bytes between rounds — likely a leak`
|
|
);
|
|
});
|
|
});
|
|
|
|
ifdescribe(process.platform === 'darwin')('autoUpdater module', () => {
|
|
it('is retained after garbage collection', async () => {
|
|
const rc = await startRemoteControlApp(['--js-flags=--expose-gc']);
|
|
const result = await rc.remotely(async () => {
|
|
const { autoUpdater } = require('electron');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
const wr = new WeakRef(autoUpdater);
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
return {
|
|
retained: wr.deref() !== undefined,
|
|
functional: typeof autoUpdater.getFeedURL() === 'string'
|
|
};
|
|
});
|
|
expect(result.retained).to.equal(true, 'autoUpdater should survive GC');
|
|
expect(result.functional).to.equal(true, 'autoUpdater should still be functional after GC');
|
|
});
|
|
|
|
it('should record as node in heap snapshot', async () => {
|
|
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await rc.remotely(
|
|
async (heap: string) => {
|
|
const { autoUpdater, app } = require('electron');
|
|
const { recordState } = require(heap);
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
console.log(autoUpdater.getFeedURL());
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
const state = recordState();
|
|
const nodes = state.snapshot.filter((node: any) => node.name === 'Electron / AutoUpdater');
|
|
const found = nodes.length > 0;
|
|
const noDuplicates = nodes.length === 1;
|
|
setTimeout(() => app.quit());
|
|
return { found, noDuplicates };
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap')
|
|
);
|
|
|
|
const [code] = await once(rc.process, 'exit');
|
|
expect(code).to.equal(0);
|
|
expect(result.found).to.equal(true, 'AutoUpdater should be in snapshot (held by JS module)');
|
|
expect(result.noDuplicates).to.equal(true, 'should have exactly one AutoUpdater instance');
|
|
});
|
|
});
|
|
|
|
ifdescribe(isTestingBindingAvailable())('gin_helper::Promise cppgc', () => {
|
|
it('does not crash on exit with a live PromiseBase', async () => {
|
|
const rc = await startRemoteControlApp();
|
|
await rc.remotely(async () => {
|
|
const { app } = require('electron');
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
|
|
// Create a gin_helper::Promise<void> and hold it in a static C++
|
|
// variable. It will outlive the isolate and be destroyed during
|
|
// static destruction — this must not crash.
|
|
testingBinding.holdPromiseForTesting();
|
|
|
|
setTimeout(() => app.quit());
|
|
});
|
|
|
|
const [code] = await once(rc.process, 'exit');
|
|
expect(code).to.equal(0);
|
|
});
|
|
|
|
it('PromiseHandle should not leak', async () => {
|
|
const { remotely } = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
|
const result = await remotely(
|
|
async (heap: string) => {
|
|
const testingBinding = (process as any)._linkedBinding('electron_common_testing');
|
|
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
|
const { recordState } = require(heap);
|
|
|
|
const countPromiseHandles = () =>
|
|
recordState().snapshot.filter((node: any) => node.name === 'Electron / PromiseHandle').length;
|
|
|
|
const gc = async () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
v8Util.requestGarbageCollectionForTesting();
|
|
}
|
|
};
|
|
|
|
testingBinding.holdPromiseForTesting();
|
|
const beforeGC = countPromiseHandles();
|
|
await gc();
|
|
const afterGC = countPromiseHandles();
|
|
|
|
testingBinding.clearHeldPromiseForTesting();
|
|
await gc();
|
|
const afterClear = countPromiseHandles();
|
|
|
|
return { beforeGC, afterGC, afterClear };
|
|
},
|
|
path.join(__dirname, '../../third_party/electron_node/test/common/heap')
|
|
);
|
|
|
|
expect(result.afterGC).to.be.at.least(result.beforeGC, 'held PromiseHandle must survive GC');
|
|
expect(result.afterClear).to.be.lessThan(result.afterGC, 'clearing should release the PromiseHandle');
|
|
});
|
|
});
|
|
});
|