Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/leak-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Scan Leak

on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:

jobs:
scan-leak:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run test:leak
- name: Upload leak artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: scan-leak-artifacts
path: |
test/leak/artifacts/**
isolate-*.log
if-no-files-found: ignore
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ reference/
node_modules/
dist/
*.tsbuildinfo
test/leak/artifacts/
test/leak/profiles/*.log
test/leak/profiles/*.cpuprofile
test/leak/profiles/*.heapsnapshot
test/leak/profiles/.clinic/
isolate-*.log
69 changes: 69 additions & 0 deletions docs/scan-leak-debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Scan Leak Debugging

The scan leak harness exercises the EVM `scanAnnouncements` hot path with a synthetic
10,000-announcement dataset for 10,000 scan iterations. It samples RSS, heap, and external
memory, writes heap snapshots, and fails when RSS grows faster than the configured slope.

## Run the Leak Test

```sh
pnpm run test:leak
```

The script runs Node with `--expose-gc`, so the harness forces GC before samples and runs the
WeakRef retention check. Generated artifacts are written to `test/leak/artifacts/`.

Useful tuning knobs:

```sh
LEAK_ITERATIONS=10000 \
LEAK_ANNOUNCEMENTS=10000 \
LEAK_SAMPLE_INTERVAL=100 \
LEAK_SNAPSHOT_INTERVAL=2500 \
LEAK_MAX_RSS_SLOPE_KB=2 \
pnpm run test:leak
```

Set `LEAK_SNAPSHOT_INTERVAL=0` to disable heap snapshots for faster local loops.

## Profiling

Generate V8 profiler output:

```sh
pnpm run profile:scan:v8
```

This creates an `isolate-*.log` file. Convert it with Node's profiler tooling:

```sh
node --prof-process isolate-*.log > test/leak/profiles/v8-profile.txt
```

Run clinic.js doctor:

```sh
pnpm run profile:scan:clinic
```

The clinic command uses `pnpm dlx`, so it can download clinic when it is not installed locally.
Keep large raw profiler output out of git; commit only small summaries or representative HTML/PNG
when needed as profiling evidence.

## Interpreting Results

RSS is the resident set size: total memory held by the process. The harness computes a linear
regression slope in KB per scan iteration after dropping the first 10% of samples as warmup. A
stable scanner should have a near-flat RSS slope even if individual samples move up and down.

Heap snapshots are written with `v8.writeHeapSnapshot()`. Open them in Chrome DevTools Memory
panel and compare early versus late snapshots. Look for retained arrays, closures, listeners,
maps, timers, or worker resources that grow with iteration count.

Common scanner leak sources:

- Closures retaining announcement arrays or buffers after a scan completes.
- Event listener accumulation across scan calls.
- Unbounded browser or IndexedDB cache growth; the documented cap is 50 MB when that cache exists.
- Worker threads not terminating on success or error.
- Timers or unresolved promises keeping the event loop alive.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test": "vitest run --exclude test/leak/**",
"test:leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leak/scan-leak.test.ts --pool=threads --poolOptions.threads.singleThread",
"profile:scan:v8": "node --expose-gc --prof ./node_modules/vitest/vitest.mjs run test/leak/scan-leak.test.ts --pool=threads --poolOptions.threads.singleThread",
"profile:scan:clinic": "pnpm dlx clinic doctor -- node --expose-gc ./node_modules/vitest/vitest.mjs run test/leak/scan-leak.test.ts --pool=threads --poolOptions.threads.singleThread",
"test:watch": "vitest",
"clean": "rm -rf dist",
"format": "prettier --write .",
Expand Down
21 changes: 21 additions & 0 deletions test/leak/profiles/scan-leak-profile-summary.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Scan Leak Profile Summary</title>
</head>
<body>
<h1>Scan Leak Profile Summary</h1>
<p>
Lightweight profiling evidence placeholder for the scan leak harness. Large raw V8 logs, heap
snapshots, and clinic output are ignored by git and should be attached to CI runs or issues
when investigating regressions.
</p>
<ul>
<li>Harness: <code>test/leak/scan-leak.test.ts</code></li>
<li>Default iterations: <code>10000</code></li>
<li>Default announcements: <code>10000</code></li>
<li>Default RSS slope limit: <code>2 KB/iteration</code></li>
</ul>
</body>
</html>
120 changes: 120 additions & 0 deletions test/leak/profiles/v8-profile-summary.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
Statistical profiling result from isolate-0000026134411000-8648-v8.log, (1110 ticks, 0 unaccounted, 0 excluded).

[Shared libraries]:
ticks total nonlib name
933 84.1% C:\WINDOWS\SYSTEM32\ntdll.dll
161 14.5% C:\Program Files\nodejs\node.exe

[JavaScript]:
ticks total nonlib name
2 0.2% 12.5% Builtin: KeyedLoadIC
2 0.2% 12.5% Builtin: InterpreterEntryTrampoline
1 0.1% 6.3% JS: ^parseSource node:internal/deps/cjs-module-lexer/lexer:85:22
1 0.1% 6.3% JS: ^loadAndTranslate node:internal/modules/esm/loader:513:25
1 0.1% 6.3% JS: ^isInScope file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:15553:20
1 0.1% 6.3% JS: ^internalBinding node:internal/bootstrap/realm:185:45
1 0.1% 6.3% JS: *getPathFromURLWin32 node:internal/url:1449:29
1 0.1% 6.3% Builtin: StringAdd_CheckNone
1 0.1% 6.3% Builtin: StoreIC_NoFeedback
1 0.1% 6.3% Builtin: StoreIC
1 0.1% 6.3% Builtin: KeyedStoreIC
1 0.1% 6.3% Builtin: CopyDataProperties
1 0.1% 6.3% Builtin: CallFunction_ReceiverIsNullOrUndefined

[C++]:
ticks total nonlib name

[Summary]:
ticks total nonlib name
15 1.4% 93.8% JavaScript
0 0.0% 0.0% C++
5 0.5% 31.3% GC
1094 98.6% Shared libraries

[C++ entry points]:
ticks cpp total name

[Bottom up (heavy) profile]:
Note: percentage shows a share of a particular caller in the total
amount of its parent calls.
Callers occupying less than 1.0% are not shown.

ticks parent name
933 84.1% C:\WINDOWS\SYSTEM32\ntdll.dll
28 3.0% C:\Program Files\nodejs\node.exe
2 7.1% JS: ^addSegmentInternal file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:777:28
2 100.0% JS: ~maybeAddSegment file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:744:23
2 100.0% JS: ^traceMappings file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:864:23
2 100.0% JS: ~remapping file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:941:19
2 7.1% Builtin: ArrayFilter
1 50.0% JS: ~getStateString$1 file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:99:26
1 100.0% JS: ~reportTestSummary file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:403:19
1 100.0% JS: ~reportSummary file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:398:15
1 50.0% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:1121:27
1 100.0% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/chunk.js:10:29
1 100.0% Script: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:1:1
1 3.6% JS: ~visit file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:15259:7
1 100.0% JS: ~visit file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:15259:7
1 100.0% JS: ~visit file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:15259:7
1 100.0% JS: ~visit file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:15259:7
1 3.6% JS: ~transformRequest file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:22555:26
1 100.0% JS: ~transformRequest file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:34844:18
1 100.0% JS: ~transformRequest file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:25454:19
1 100.0% JS: ~_transformRequest file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite-node@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vite-node/dist/server.mjs:392:25
1 3.6% JS: ~setupPortReferencing node:internal/worker/io:210:30
1 100.0% JS: ~oninit node:internal/worker/io:148:16
1 100.0% C:\Program Files\nodejs\node.exe
1 100.0% JS: ~Worker node:internal/worker:134:14
1 3.6% JS: ~resolveConfig file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:35409:29
1 100.0% Builtin: AsyncFunctionAwaitResolveClosure
1 3.6% JS: ~picomatch.test C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\picomatch.js:116:18
1 100.0% JS: ~matcher C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\picomatch.js:65:19
1 100.0% JS: ~arrayMatcher C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\picomatch.js:34:26
1 100.0% JS: ~excludePredicate file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/tinyglobby@0.2.16/node_modules/tinyglobby/dist/index.mjs:218:27
1 3.6% JS: ~parse$13 file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:5661:24
1 100.0% Builtin: AsyncFunctionAwaitResolveClosure
1 3.6% JS: ~log file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/cli-api.BkDphVBG.js:5472:5
1 100.0% JS: ~log file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:199:5
1 100.0% JS: ~reportTestSummary file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:403:19
1 100.0% JS: ~reportSummary file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:398:15
1 3.6% JS: ~loadAndTransform file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:22615:32
1 100.0% Builtin: AsyncFunctionAwaitResolveClosure
1 100.0% Builtin: CallApiCallbackGeneric
1 100.0% JS: ^processTicksAndRejections node:internal/process/task_queues:72:35
1 3.6% JS: ~handlePlugins C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\esbuild@0.27.7\node_modules\esbuild\lib\main.js:1352:21
1 100.0% JS: ~buildOrContextImpl C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\esbuild@0.27.7\node_modules\esbuild\lib\main.js:1110:28
1 100.0% JS: ~buildOrContext C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\esbuild@0.27.7\node_modules\esbuild\lib\main.js:942:24
1 100.0% JS: ~<anonymous> C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\esbuild@0.27.7\node_modules\esbuild\lib\main.js:2315:37
1 3.6% JS: ~finalizeResolution node:internal/modules/esm/resolve:228:28
1 100.0% JS: ^moduleResolve node:internal/modules/esm/resolve:827:23
1 100.0% JS: ^defaultResolve node:internal/modules/esm/resolve:939:24
1 100.0% JS: ~defaultResolve node:internal/modules/esm/loader:675:17
1 3.6% JS: ~defaultResolve node:internal/modules/esm/resolve:939:24
1 100.0% JS: ~defaultResolve node:internal/modules/esm/loader:675:17
1 100.0% JS: ~#cachedDefaultResolve node:internal/modules/esm/loader:628:24
1 100.0% JS: ~resolve node:internal/modules/esm/loader:612:10
1 3.6% JS: ~analyzeRepeatedExtglob C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\parse.js:283:32
1 100.0% JS: ~extglobClose C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\parse.js:509:24
1 100.0% JS: ^parse C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\parse.js:326:15
1 100.0% JS: ~picomatch.makeRe C:\Users\RACEEY\dev\drip\sdk\node_modules\.pnpm\picomatch@4.0.4\node_modules\picomatch\lib\picomatch.js:293:20
1 3.6% JS: ~Socket._writeGeneric node:net:940:42
1 100.0% JS: ~Socket._write node:net:982:35
1 100.0% JS: ~writeOrBuffer node:internal/streams/writable:548:23
1 100.0% JS: ~_write node:internal/streams/writable:453:16
1 3.6% JS: ~<anonymous> node:internal/crypto/webidl:1:1
1 100.0% JS: ^compileForInternalLoader node:internal/bootstrap/realm:384:27
1 100.0% JS: ^requireBuiltin node:internal/bootstrap/realm:421:24
1 100.0% JS: ~getRandomValues node:internal/crypto/webcrypto:945:25
1 3.6% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:538:22
1 100.0% Builtin: ArrayReduce
1 100.0% JS: ~sum file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:537:13
1 100.0% JS: ~reportTestSummary file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vitest@3.2.4_@types+node@25.6.0_jiti@2.6.1/node_modules/vitest/dist/chunks/index.VByaPkjc.js:403:19
1 3.6% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/tinyexec@0.3.2/node_modules/tinyexec/dist/main.js:13:19
1 100.0% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/tinyexec@0.3.2/node_modules/tinyexec/dist/main.js:266:12
1 100.0% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/tinyexec@0.3.2/node_modules/tinyexec/dist/main.js:13:19
1 100.0% JS: ~<anonymous> file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/tinyexec@0.3.2/node_modules/tinyexec/dist/main.js:344:12
1 3.6% JS: ^originalPositionFor$1 file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:888:31
1 100.0% JS: ^traceMappings file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:864:23
1 100.0% JS: ~remapping file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:941:19
1 100.0% JS: ~combineSourcemaps file:///C:/Users/RACEEY/dev/drip/sdk/node_modules/.pnpm/vite@7.3.2_@types+node@25.6.0_jiti@2.6.1/node_modules/vite/dist/node/chunks/config.js:2311:27
1 3.6% JS: ^get exports node:internal/modules/package_json_reader:80:20
Loading