From fb84b6db9b557fee85aeddeacbe973885925af16 Mon Sep 17 00:00:00 2001 From: hzy <28915578+hzy@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:29:08 +0800 Subject: [PATCH 1/2] feat: add memory leak detection script for react benchmark --- .changeset/wild-jeans-take.md | 3 + benchmark/react/package.json | 3 +- benchmark/react/scripts/detectLeak.mjs | 122 +++++++++++++++++++++++++ pnpm-lock.yaml | 16 ++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 .changeset/wild-jeans-take.md create mode 100644 benchmark/react/scripts/detectLeak.mjs diff --git a/.changeset/wild-jeans-take.md b/.changeset/wild-jeans-take.md new file mode 100644 index 0000000000..853d812bb3 --- /dev/null +++ b/.changeset/wild-jeans-take.md @@ -0,0 +1,3 @@ +--- + +--- diff --git a/benchmark/react/package.json b/benchmark/react/package.json index 8060c46af9..e498354b70 100644 --- a/benchmark/react/package.json +++ b/benchmark/react/package.json @@ -20,7 +20,7 @@ "perfetto:004-various-update": "benchx_cli -o dist/004-various-update.ptrace run dist/004-various-update.lynx.bundle --wait-for-id=stop-benchmark-true", "perfetto:005-load-script": "benchx_cli -o dist/005-load-script.ptrace run dist/005-load-script.lynx.bundle", "perfetto:006-static-raw-text": "benchx_cli -o dist/006-static-raw-text.ptrace run dist/006-static-raw-text.lynx.bundle --wait-for-id=stop-benchmark-true", - "test": "echo 'No tests specified'" + "test": "npm run perfetto && node scripts/detectLeak.mjs" }, "dependencies": { "@lynx-js/react": "workspace:*", @@ -34,6 +34,7 @@ "@lynx-js/qrcode-rsbuild-plugin": "workspace:*", "@lynx-js/react-rsbuild-plugin": "workspace:*", "@lynx-js/rspeedy": "workspace:*", + "@lynx-js/trace-processor": "^0.0.1", "@lynx-js/type-element-api": "0.0.3", "@lynx-js/types": "3.7.0", "@types/react": "^18.3.28" diff --git a/benchmark/react/scripts/detectLeak.mjs b/benchmark/react/scripts/detectLeak.mjs new file mode 100644 index 0000000000..3b0a999c0e --- /dev/null +++ b/benchmark/react/scripts/detectLeak.mjs @@ -0,0 +1,122 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { WasmEngine } from '@lynx-js/trace-processor'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const distDir = path.resolve(__dirname, '../dist'); + +async function main() { + if ( + !(await fs + .stat(distDir) + .then((s) => s.isDirectory()) + .catch(() => false)) + ) { + console.info('No dist directory found. Skipping leak detection.'); + return; + } + + const files = await fs.readdir(distDir); + const ptraceFiles = files.filter((file) => file.endsWith('.ptrace')); + + if (ptraceFiles.length === 0) { + console.info('No .ptrace files found in dist directory.'); + return; + } + + let hasLeak = false; + + for (const file of ptraceFiles) { + console.info(`Analyzing ${file}...`); + const filePath = path.join(distDir, file); + const fileContent = await fs.readFile(filePath); + + using engine = new WasmEngine('detectLeak'); + await engine.parse(fileContent); + await engine.notifyEof(); + + const countQuery = ` + SELECT count(*) as count + FROM slice s + JOIN args a1 ON s.arg_set_id = a1.arg_set_id + JOIN args a2 ON s.arg_set_id = a2.arg_set_id + WHERE s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag' + `; + const countResult = await engine.query(countQuery); + const countIt = countResult.iter({ count: 0 }); + if (countIt.valid()) { + const count = countIt.get('count'); + if (count === 0) { + console.error( + `Error: No FiberElement::Constructor events found in ${file}.`, + ); + process.exit(1); // eslint-disable-line n/no-process-exit + } + } + + const query = `\ +WITH +CtorIds AS ( + SELECT + a1.int_value AS id, + a2.string_value AS tag + FROM + slice s + JOIN + args a1 ON s.arg_set_id = a1.arg_set_id + JOIN + args a2 ON s.arg_set_id = a2.arg_set_id + WHERE + s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag' +), +DtorIds AS ( + SELECT + a.int_value AS id + FROM + slice s + JOIN + args a ON s.arg_set_id = a.arg_set_id + WHERE + s.name = 'FiberElement::Destructor' AND a.key = 'debug.id' +) +SELECT + ctor.id, ctor.tag +FROM + CtorIds ctor +LEFT JOIN + DtorIds dtor ON ctor.id = dtor.id +WHERE + dtor.id IS NULL; +`; + + const result = await engine.query(query); + + if (result.numRows() > 0) { + console.error(`Memory leak detected in ${file}!`); + const columns = result.columns(); + + for (const it = result.iter({}); it.valid(); it.next()) { + const row = {}; + for (const name of columns) { + row[name] = it.get(name); + } + console.error(`Leaked Element:`, row); + } + hasLeak = true; + } else { + console.info(`No leaks detected in ${file}.`); + } + } + + if (hasLeak) { + process.exit(1); // eslint-disable-line n/no-process-exit + } +} + +await main(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c00266389..db402b9d96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ importers: '@lynx-js/rspeedy': specifier: workspace:* version: link:../../packages/rspeedy/core + '@lynx-js/trace-processor': + specifier: ^0.0.1 + version: 0.0.1 '@lynx-js/type-element-api': specifier: 0.0.3 version: 0.0.3 @@ -3165,6 +3168,10 @@ packages: '@lynx-js/tasm@0.0.20': resolution: {integrity: sha512-ezMq43s59jqFuQ1YygpsUuZmGXw4XH+00RsB5RVmkYZuHQxEaLt/ECTOixF+9RixvAyhmxzF2eSURvmNckO9xg==} + '@lynx-js/trace-processor@0.0.1': + resolution: {integrity: sha512-Zyl74cKi+BDggeXroLQG4frbBuiQ0DB0yH0C3pMkUHWv17abWTdJ2mw/H9Cd1QiIr+aQHn1U2SRtsrbkmWMtJw==} + engines: {node: '>=18'} + '@lynx-js/type-config@3.6.0': resolution: {integrity: sha512-XS+Wdbs77iWaVqrs1HKp3LyTLLHbu/AwLwy837BinmyNO28YunkgnOz0dmuzjnGGsbKJB3Alm7RoYrH/PAsVuQ==} @@ -6620,6 +6627,9 @@ packages: resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + immutable@5.0.2: resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==} @@ -11669,6 +11679,10 @@ snapshots: '@lynx-js/tasm@0.0.20': {} + '@lynx-js/trace-processor@0.0.1': + dependencies: + immer: 10.2.0 + '@lynx-js/type-config@3.6.0': {} '@lynx-js/type-element-api@0.0.3': {} @@ -15989,6 +16003,8 @@ snapshots: ignore@7.0.4: {} + immer@10.2.0: {} + immutable@5.0.2: {} import-fresh@3.3.1: From 1f915cedd900e9ff70f1bd3bb15dc08768512355 Mon Sep 17 00:00:00 2001 From: hzy <28915578+hzy@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:39:54 +0800 Subject: [PATCH 2/2] fix(benchmark): avoid unsupported using syntax and ensure engine cleanup --- benchmark/react/scripts/detectLeak.mjs | 93 ++++++++++++++++---------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/benchmark/react/scripts/detectLeak.mjs b/benchmark/react/scripts/detectLeak.mjs index 3b0a999c0e..8a379cf8f6 100644 --- a/benchmark/react/scripts/detectLeak.mjs +++ b/benchmark/react/scripts/detectLeak.mjs @@ -11,6 +11,21 @@ import { WasmEngine } from '@lynx-js/trace-processor'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const distDir = path.resolve(__dirname, '../dist'); +async function disposeEngine(engine) { + // Node 22 doesn't support `using`; manually dispose resources if supported. + if (typeof engine.dispose === 'function') { + await engine.dispose(); + } else if (typeof engine.close === 'function') { + await engine.close(); + } else if (typeof engine.free === 'function') { + await engine.free(); + } else if (typeof engine[Symbol.asyncDispose] === 'function') { + await engine[Symbol.asyncDispose](); + } else if (typeof engine[Symbol.dispose] === 'function') { + engine[Symbol.dispose](); + } +} + async function main() { if ( !(await fs @@ -37,30 +52,32 @@ async function main() { const filePath = path.join(distDir, file); const fileContent = await fs.readFile(filePath); - using engine = new WasmEngine('detectLeak'); - await engine.parse(fileContent); - await engine.notifyEof(); - - const countQuery = ` - SELECT count(*) as count - FROM slice s - JOIN args a1 ON s.arg_set_id = a1.arg_set_id - JOIN args a2 ON s.arg_set_id = a2.arg_set_id - WHERE s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag' - `; - const countResult = await engine.query(countQuery); - const countIt = countResult.iter({ count: 0 }); - if (countIt.valid()) { - const count = countIt.get('count'); - if (count === 0) { - console.error( - `Error: No FiberElement::Constructor events found in ${file}.`, - ); - process.exit(1); // eslint-disable-line n/no-process-exit + const engine = new WasmEngine('detectLeak'); + try { + await engine.parse(fileContent); + await engine.notifyEof(); + + const countQuery = ` + SELECT count(*) as count + FROM slice s + JOIN args a1 ON s.arg_set_id = a1.arg_set_id + JOIN args a2 ON s.arg_set_id = a2.arg_set_id + WHERE s.name = 'FiberElement::Constructor' AND a1.key = 'debug.id' AND a2.key = 'debug.tag' + `; + const countResult = await engine.query(countQuery); + const countIt = countResult.iter({ count: 0 }); + if (countIt.valid()) { + const count = countIt.get('count'); + if (count === 0) { + console.error( + `Error: No FiberElement::Constructor events found in ${file}.`, + ); + hasLeak = true; + continue; + } } - } - const query = `\ + const query = `\ WITH CtorIds AS ( SELECT @@ -95,22 +112,25 @@ WHERE dtor.id IS NULL; `; - const result = await engine.query(query); + const result = await engine.query(query); - if (result.numRows() > 0) { - console.error(`Memory leak detected in ${file}!`); - const columns = result.columns(); + if (result.numRows() > 0) { + console.error(`Memory leak detected in ${file}!`); + const columns = result.columns(); - for (const it = result.iter({}); it.valid(); it.next()) { - const row = {}; - for (const name of columns) { - row[name] = it.get(name); + for (const it = result.iter({}); it.valid(); it.next()) { + const row = {}; + for (const name of columns) { + row[name] = it.get(name); + } + console.error(`Leaked Element:`, row); } - console.error(`Leaked Element:`, row); + hasLeak = true; + } else { + console.info(`No leaks detected in ${file}.`); } - hasLeak = true; - } else { - console.info(`No leaks detected in ${file}.`); + } finally { + await disposeEngine(engine); } } @@ -119,4 +139,7 @@ WHERE } } -await main(); +await main().catch((err) => { + console.error(err); + process.exit(1); // eslint-disable-line n/no-process-exit +});