Skip to content

Commit 980caec

Browse files
committed
feat: add memory leak detection script for react benchmark
1 parent b89b3e3 commit 980caec

File tree

5 files changed

+220
-49
lines changed

5 files changed

+220
-49
lines changed

.changeset/wild-jeans-take.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
3+
---

benchmark/react/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"bench:004-various-update": "benchx_cli run dist/004-various-update.lynx.bundle --wait-for-id=stop-benchmark-true",
1212
"bench:005-load-script": "benchx_cli run dist/005-load-script.lynx.bundle",
1313
"bench:006-static-raw-text": "benchx_cli run dist/006-static-raw-text.lynx.bundle --wait-for-id=stop-benchmark-true",
14+
"bench:detect-leak": "node scripts/detectLeak.mjs",
1415
"build": "rspeedy build",
1516
"dev": "rspeedy dev",
1617
"perfetto": "pnpm run --sequential --stream --aggregate-output '/^perfetto:.*/'",
@@ -34,6 +35,7 @@
3435
"@lynx-js/qrcode-rsbuild-plugin": "workspace:*",
3536
"@lynx-js/react-rsbuild-plugin": "workspace:*",
3637
"@lynx-js/rspeedy": "workspace:*",
38+
"@lynx-js/trace-processor": "^0.0.1",
3739
"@lynx-js/type-element-api": "0.0.3",
3840
"@lynx-js/types": "3.7.0",
3941
"@types/react": "^18.3.28"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2026 The Lynx Authors. All rights reserved.
2+
// Licensed under the Apache License Version 2.0 that can be found in the
3+
// LICENSE file in the root directory of this source tree.
4+
5+
import fs from 'node:fs/promises';
6+
import path from 'node:path';
7+
import { fileURLToPath } from 'node:url';
8+
9+
import { WasmEngine } from '@lynx-js/trace-processor';
10+
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12+
const distDir = path.resolve(__dirname, '../dist');
13+
14+
async function main() {
15+
if (
16+
!(await fs
17+
.stat(distDir)
18+
.then((s) => s.isDirectory())
19+
.catch(() => false))
20+
) {
21+
console.info('No dist directory found. Skipping leak detection.');
22+
return;
23+
}
24+
25+
const files = await fs.readdir(distDir);
26+
const ptraceFiles = files.filter((file) => file.endsWith('.ptrace'));
27+
28+
if (ptraceFiles.length === 0) {
29+
console.info('No .ptrace files found in dist directory.');
30+
return;
31+
}
32+
33+
let hasLeak = false;
34+
35+
for (const file of ptraceFiles) {
36+
console.info(`Analyzing ${file}...`);
37+
const filePath = path.join(distDir, file);
38+
const fileContent = await fs.readFile(filePath);
39+
40+
using engine = new WasmEngine('detectLeak');
41+
await engine.parse(fileContent);
42+
await engine.notifyEof();
43+
44+
const countQuery = `
45+
SELECT count(*) as count
46+
FROM slice s
47+
JOIN args a1 ON s.arg_set_id = a1.arg_set_id
48+
JOIN args a2 ON s.arg_set_id = a2.arg_set_id
49+
WHERE s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag'
50+
`;
51+
const countResult = await engine.query(countQuery);
52+
const countIt = countResult.iter({ count: 0 });
53+
if (countIt.valid()) {
54+
const count = countIt.get('count');
55+
if (count === 0) {
56+
console.error(
57+
`Error: No FiberElement::Constructor events found in ${file}.`,
58+
);
59+
process.exit(1); // eslint-disable-line n/no-process-exit
60+
}
61+
}
62+
63+
const query = `\
64+
WITH
65+
CtorIds AS (
66+
SELECT
67+
a1.int_value AS id,
68+
a2.string_value AS tag
69+
FROM
70+
slice s
71+
JOIN
72+
args a1 ON s.arg_set_id = a1.arg_set_id
73+
JOIN
74+
args a2 ON s.arg_set_id = a2.arg_set_id
75+
WHERE
76+
s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag'
77+
),
78+
DtorIds AS (
79+
SELECT
80+
a.int_value AS id
81+
FROM
82+
slice s
83+
JOIN
84+
args a ON s.arg_set_id = a.arg_set_id
85+
WHERE
86+
s.name = 'FiberElement::Destructor' AND a.key = 'debug.id'
87+
)
88+
SELECT
89+
ctor.id, ctor.tag
90+
FROM
91+
CtorIds ctor
92+
LEFT JOIN
93+
DtorIds dtor ON ctor.id = dtor.id
94+
WHERE
95+
dtor.id IS NULL;
96+
`;
97+
98+
const result = await engine.query(query);
99+
100+
if (result.numRows() > 0) {
101+
console.error(`Memory leak detected in ${file}!`);
102+
const columns = result.columns();
103+
104+
for (const it = result.iter({}); it.valid(); it.next()) {
105+
const row = {};
106+
for (const name of columns) {
107+
row[name] = it.get(name);
108+
}
109+
console.error(`Leaked Element:`, row);
110+
}
111+
hasLeak = true;
112+
} else {
113+
console.info(`No leaks detected in ${file}.`);
114+
}
115+
}
116+
117+
if (hasLeak) {
118+
process.exit(1); // eslint-disable-line n/no-process-exit
119+
}
120+
}
121+
122+
await main();

examples/react-compiler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@lynx-js/react-rsbuild-plugin": "workspace:*",
1717
"@lynx-js/rspeedy": "workspace:*",
1818
"@lynx-js/types": "3.7.0",
19-
"@rsbuild/plugin-babel": "1.0.6",
19+
"@rsbuild/plugin-babel": "2.0.0-alpha.3",
2020
"@types/react": "^18.3.28",
2121
"babel-plugin-react-compiler": "0.0.0-experimental-fe727a3-20250909"
2222
}

0 commit comments

Comments
 (0)