OpenClaw Test Performance
Use evidence first. The goal is real pnpm test, plugin-suite, and
plugin-inspector speed/RSS improvement with coverage intact, not runner tuning by
guesswork.
Workflow
- Read the relevant local
AGENTS.md files before editing:
src/agents/AGENTS.md for agent/import hotspots.
src/channels/AGENTS.md and src/plugins/AGENTS.md for plugin/channel
laziness.
src/gateway/AGENTS.md for server lifecycle tests.
test/helpers/AGENTS.md and
src/channels/plugins/contracts/test-helpers/AGENTS.md for shared contract
helpers.
src/infra/outbound/AGENTS.md for outbound/media/action tests.
- Establish a baseline before changing code:
- Prefer
pnpm test:perf:groups --full-suite --allow-failures --output <file>
for full-suite ranking.
- For bundled plugin breadth, run the smallest relevant
pnpm test:extensions:batch <plugin[,plugin...]> or plugin-inspector command
before jumping to the full extension sweep.
- For a scoped hotspot use:
/usr/bin/time -l pnpm test <file-or-files> --maxWorkers=1 --reporter=verbose
- For import-heavy suspicion add:
OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1.
- Separate wall/runner noise from real file cost:
- Compare Vitest duration, test body timing, import breakdown, wall time, and
max RSS.
- Re-run single files when grouped/full-suite numbers look stale or noisy.
- If a full-suite grouped run reports a lane failure but JSON says tests
passed, capture that as harness/noise and verify the suspect file directly.
- Pick the next attack by return and risk:
- High return: one file/test dominates seconds or RSS and has a clear root.
- High leverage: one plugin or SDK barrel causes every plugin-inspector or
extension-batch run to load broad runtime.
- Lower risk: static descriptors, target parsing, routing, auth bypass,
setup hints, registry fixtures, or test server lifecycle.
- Higher risk: real memory/runtime behavior, live providers, protocol
contracts, or broad production refactors.
- Fix the root cause, not the symptom:
- Move static metadata/parsing into narrow helpers or lightweight artifacts
reused by full runtime and fast paths.
- Prefer dependency injection, loaded-plugin-only lookup, explicit fixtures,
and pure helpers over broad mocks.
- Reuse suite-level servers/clients when a fresh handshake is irrelevant.
- Keep schedulers/background loops off unless the test proves scheduling.
- In plugin paths, move static metadata into manifest/lightweight artifacts
and keep runtime plugin loads behind explicit execution boundaries.
- Preserve coverage shape:
- Do not delete a slow integration proof unless the exact production
composition is extracted into a named helper and tested.
- Keep one cheap integration smoke when cross-component wiring matters.
- State explicitly what incidental coverage was removed, if any.
- Re-benchmark the same command after the change and compute seconds plus
percent gain.
- Update the running report when requested or when this thread is tracking one.
Include before/after commands, artifacts, coverage notes, verification, and
next attack order.
- Stage the intended paths, commit with standard Git, and push when the
user asked for commits/pushes. Stage only files touched for this attack.
Plugin-Suite Workflow
Use this section when perf work involves bundled plugins, plugin-inspector, SDK
barrels, package-boundary tests, or extension suites.
- Map the suite shape first:
- source tests:
pnpm test extensions/<id> or pnpm test:extensions:batch <id>
- package boundaries:
pnpm run test:extensions:package-boundary:canary and
pnpm run test:extensions:package-boundary:compile
- all bundled source tests:
pnpm test:extensions
- plugin import memory:
pnpm test:extensions:memory -- --json .artifacts/test-perf/extensions-memory.json
- plugin-inspector/report work: keep report primitives in
plugin-inspector;
keep wrappers thin and collect peak RSS when the command supports it.
- Start narrow, then widen:
- one plugin changed: run that plugin's tests and plugin-inspector slice.
- SDK/public barrel changed: add representative provider, channel, memory,
and feature plugins.
- loader/runtime mirror changed: add package-boundary checks and build/package
proof as needed.
- unknown shared plugin behavior: run
test:extensions:batch groups before
pnpm test:extensions.
- Treat plugin-inspector failures as product signals:
- JSON must parse.
- warnings/errors must be classified, not hidden.
- runtime capture should be quiet and config-tolerant.
- command output should include wall time, exit code, and peak RSS when
available.
- Follow
$openclaw-testing for host selection. Trusted source benchmarks
can run locally with comparable machine/load conditions. Use $crabbox when
clean packaging, Linux/platform behavior, isolation, or an explicit remote
request is part of the proof; reuse and clean up only the owned lease.
- If plugin performance is package-artifact sensitive, switch to
release-openclaw-plugin-testing and Package Acceptance rather than
trusting source-only timing.
Metric Collection
Collect at least one stable metric before and after. Prefer the same machine and
same command. For Testbox comparisons, use the same tbx_... id when possible.
| Metric |
Use for |
Preferred source |
| wall time |
user-visible suite cost |
/usr/bin/time -l, test wrapper duration, Testbox run time |
| Vitest duration |
test body/import cost |
Vitest output per file/shard |
| import duration |
broad barrel/runtime loads |
OPENCLAW_VITEST_IMPORT_DURATIONS=1 |
| max RSS |
memory pressure and OOM risk |
/usr/bin/time -l, pnpm test:extensions:memory, wrapper memory summaries |
| CPU/user/sys |
CPU-bound vs wait-bound split |
/usr/bin/time -l locally, Testbox job timing when local CPU is noisy |
| heap evidence |
real leak vs retained module graph |
openclaw-test-heap-leaks workflow |
Local scoped command with CPU/RSS:
timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
Plugin import memory profile:
pnpm build
pnpm test:extensions:memory -- --top 20 --json .artifacts/test-perf/extensions-memory.json
Targeted plugin import memory:
pnpm test:extensions:memory -- --extension discord --extension telegram --skip-combined
Heap/RSS escalation:
pnpm test:perf:groups \
--config test/vitest/vitest.unit-fast.config.ts \
--allow-failures \
--output .artifacts/test-perf/unit-fast-memory.json
pnpm test:perf:profile:runner -- \
--output-dir .artifacts/test-perf/vitest-runner-profile -- <file>
Use openclaw-test-heap-leaks when RSS keeps growing across intervals, workers
OOM, or the suspect command has app-object retention. Do not call RSS growth a
leak until snapshots or retainers support it.
Common Root Causes
- Full bundled channel/plugin runtime loaded for static data.
getChannelPlugin() fallback used when an already-loaded fixture or pure
parser would suffice.
- Broad
api.ts, runtime-api.ts, test-api.ts, or plugin-sdk barrels pulled
into hot tests.
- SDK root aliases or package barrels pulling focused subpaths back into a broad
plugin graph.
- Plugin-inspector loading runtime code just to render metadata, reports, or CI
policy scores.
- Bundled plugin capture reusing real config/home state instead of synthetic,
redacted, isolated state.
- Partial-real mocks using
importActual() around broad modules.
vi.resetModules() plus fresh imports in per-test loops.
- Test plugin registry seeded in
beforeAll while runtime state resets in
afterEach.
- Per-test gateway/server/client startup when state reset would suffice.
- Runtime/default model/auth selection paid by idle snapshots or fixtures.
- Plugin-owned media/action discovery triggered before checking whether args
contain plugin-owned fields.
- Parallel Vitest runs sharing
node_modules/.experimental-vitest-cache without
distinct OPENCLAW_VITEST_FS_MODULE_CACHE_PATH values.
Benchmark Commands
Scoped file:
timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
Scoped file with import breakdown:
timeout 240 /usr/bin/time -l env \
OPENCLAW_VITEST_IMPORT_DURATIONS=1 \
OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 \
pnpm test <file> --maxWorkers=1 --reporter=verbose
Grouped suite:
pnpm test:perf:groups --full-suite --allow-failures \
--output .artifacts/test-perf/<name>.json
Extension batch:
pnpm test:extensions:batch <plugin[,plugin...]> -- --reporter=verbose
All extension tests:
pnpm test:extensions
Package-boundary plugin checks:
pnpm run test:extensions:package-boundary:canary
pnpm run test:extensions:package-boundary:compile
Reuse an existing Vitest JSON report:
pnpm test:perf:groups --report <vitest-json> \
--output .artifacts/test-perf/<name>.json
Verification
- Always run the targeted test surface that proves the change.
- For source changes, run
pnpm check:changed before push; in maintainer
Testbox mode run it in the warmed Testbox.
- For test-only changes, run
pnpm test:changed or the exact edited tests.
- Run
pnpm build when touching lazy-loading, bundled artifacts, package
boundaries, dynamic imports, build output, or public surfaces.
- For plugin SDK/barrel/runtime changes, compare exact commits with
pnpm plugin-sdk:api:diff -- --base <base-sha> --head <head-sha> when the
public API surface may drift. For PR-local proof, use the branch merge base
as <base-sha> and the exact tested head commit as <head-sha>.
- For plugin-suite perf fixes, verify at least one representative plugin batch
plus the changed gate; use Package Acceptance if the bug only exists in a
packed artifact.
- If deps are missing/stale, run
pnpm install and retry the exact failed
command once.
- Use the report format:
| Metric | Before | After | Gain |
| -------------- | -----: | -----: | ------------: |
| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |
| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |
| CPU user/sys | `X/Ys` | `A/Bs` | explain |
Handoff
Keep the final concise:
- Root cause.
- Suite/plugin scope.
- Files changed.
- Before/after wall, Vitest/import, CPU, and RSS numbers where available.
- Leak classification if memory was involved: real leak, retained module graph,
or inconclusive.
- Coverage retained.
- Verification commands.
- Testbox ID or workflow URL for remote proof.
- Commit hash and push status.
1---2name: openclaw-test-performance3description: Benchmark, diagnose, and optimize OpenClaw test and plugin-suite runtime, import hotspots, CPU/RSS, heap growth, and slow coverage paths.4---5
6# OpenClaw Test Performance
7
8Use evidence first. The goal is real `pnpm test`, plugin-suite, and
9plugin-inspector speed/RSS improvement with coverage intact, not runner tuning by
10guesswork.
11
12## Workflow
13
141. Read the relevant local `AGENTS.md` files before editing:
15 - `src/agents/AGENTS.md` for agent/import hotspots.
16 - `src/channels/AGENTS.md` and `src/plugins/AGENTS.md` for plugin/channel
17 laziness.
18 - `src/gateway/AGENTS.md` for server lifecycle tests.
19 - `test/helpers/AGENTS.md` and
20 `src/channels/plugins/contracts/test-helpers/AGENTS.md` for shared contract
21 helpers.
22 - `src/infra/outbound/AGENTS.md` for outbound/media/action tests.
232. Establish a baseline before changing code:
24 - Prefer `pnpm test:perf:groups --full-suite --allow-failures --output <file>`
25 for full-suite ranking.
26 - For bundled plugin breadth, run the smallest relevant `pnpm
27test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
28 before jumping to the full extension sweep.
29 - For a scoped hotspot use:
30 `/usr/bin/time -l pnpm test <file-or-files> --maxWorkers=1 --reporter=verbose`
31 - For import-heavy suspicion add:
32 `OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1`.
333. Separate wall/runner noise from real file cost:
34 - Compare Vitest duration, test body timing, import breakdown, wall time, and
35 max RSS.
36 - Re-run single files when grouped/full-suite numbers look stale or noisy.
37 - If a full-suite grouped run reports a lane failure but JSON says tests
38 passed, capture that as harness/noise and verify the suspect file directly.
394. Pick the next attack by return and risk:
40 - High return: one file/test dominates seconds or RSS and has a clear root.
41 - High leverage: one plugin or SDK barrel causes every plugin-inspector or
42 extension-batch run to load broad runtime.
43 - Lower risk: static descriptors, target parsing, routing, auth bypass,
44 setup hints, registry fixtures, or test server lifecycle.
45 - Higher risk: real memory/runtime behavior, live providers, protocol
46 contracts, or broad production refactors.
475. Fix the root cause, not the symptom:
48 - Move static metadata/parsing into narrow helpers or lightweight artifacts
49 reused by full runtime and fast paths.
50 - Prefer dependency injection, loaded-plugin-only lookup, explicit fixtures,
51 and pure helpers over broad mocks.
52 - Reuse suite-level servers/clients when a fresh handshake is irrelevant.
53 - Keep schedulers/background loops off unless the test proves scheduling.
54 - In plugin paths, move static metadata into manifest/lightweight artifacts
55 and keep runtime plugin loads behind explicit execution boundaries.
566. Preserve coverage shape:
57 - Do not delete a slow integration proof unless the exact production
58 composition is extracted into a named helper and tested.
59 - Keep one cheap integration smoke when cross-component wiring matters.
60 - State explicitly what incidental coverage was removed, if any.
617. Re-benchmark the same command after the change and compute seconds plus
62 percent gain.
638. Update the running report when requested or when this thread is tracking one.
64 Include before/after commands, artifacts, coverage notes, verification, and
65 next attack order.
669. Stage the intended paths, commit with standard Git, and push when the
67 user asked for commits/pushes. Stage only files touched for this attack.
68
69## Plugin-Suite Workflow
70
71Use this section when perf work involves bundled plugins, plugin-inspector, SDK
72barrels, package-boundary tests, or extension suites.
73
741. Map the suite shape first:
75 - source tests: `pnpm test extensions/<id>` or `pnpm test:extensions:batch <id>`
76 - package boundaries: `pnpm run test:extensions:package-boundary:canary` and
77 `pnpm run test:extensions:package-boundary:compile`
78 - all bundled source tests: `pnpm test:extensions`
79 - plugin import memory: `pnpm test:extensions:memory -- --json .artifacts/test-perf/extensions-memory.json`
80 - plugin-inspector/report work: keep report primitives in `plugin-inspector`;
81 keep wrappers thin and collect peak RSS when the command supports it.
822. Start narrow, then widen:
83 - one plugin changed: run that plugin's tests and plugin-inspector slice.
84 - SDK/public barrel changed: add representative provider, channel, memory,
85 and feature plugins.
86 - loader/runtime mirror changed: add package-boundary checks and build/package
87 proof as needed.
88 - unknown shared plugin behavior: run `test:extensions:batch` groups before
89 `pnpm test:extensions`.
903. Treat plugin-inspector failures as product signals:
91 - JSON must parse.
92 - warnings/errors must be classified, not hidden.
93 - runtime capture should be quiet and config-tolerant.
94 - command output should include wall time, exit code, and peak RSS when
95 available.
964. Follow `$openclaw-testing` for host selection. Trusted source benchmarks
97 can run locally with comparable machine/load conditions. Use `$crabbox` when
98 clean packaging, Linux/platform behavior, isolation, or an explicit remote
99 request is part of the proof; reuse and clean up only the owned lease.
1005. If plugin performance is package-artifact sensitive, switch to
101 `release-openclaw-plugin-testing` and Package Acceptance rather than
102 trusting source-only timing.
103
104## Metric Collection
105
106Collect at least one stable metric before and after. Prefer the same machine and
107same command. For Testbox comparisons, use the same `tbx_...` id when possible.
108
109| Metric | Use for | Preferred source |
110| --------------- | ---------------------------------- | --------------------------------------------------------------------------- |
111| wall time | user-visible suite cost | `/usr/bin/time -l`, test wrapper duration, Testbox run time |
112| Vitest duration | test body/import cost | Vitest output per file/shard |
113| import duration | broad barrel/runtime loads | `OPENCLAW_VITEST_IMPORT_DURATIONS=1` |
114| max RSS | memory pressure and OOM risk | `/usr/bin/time -l`, `pnpm test:extensions:memory`, wrapper memory summaries |
115| CPU/user/sys | CPU-bound vs wait-bound split | `/usr/bin/time -l` locally, Testbox job timing when local CPU is noisy |
116| heap evidence | real leak vs retained module graph | `openclaw-test-heap-leaks` workflow |
117
118Local scoped command with CPU/RSS:
119
120```bash
121timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
122```
123
124Plugin import memory profile:
125
126```bash
127pnpm build
128pnpm test:extensions:memory -- --top 20 --json .artifacts/test-perf/extensions-memory.json
129```
130
131Targeted plugin import memory:
132
133```bash
134pnpm test:extensions:memory -- --extension discord --extension telegram --skip-combined
135```
136
137Heap/RSS escalation:
138
139```bash
140pnpm test:perf:groups \
141 --config test/vitest/vitest.unit-fast.config.ts \
142 --allow-failures \
143 --output .artifacts/test-perf/unit-fast-memory.json
144pnpm test:perf:profile:runner -- \
145 --output-dir .artifacts/test-perf/vitest-runner-profile -- <file>
146```
147
148Use `openclaw-test-heap-leaks` when RSS keeps growing across intervals, workers
149OOM, or the suspect command has app-object retention. Do not call RSS growth a
150leak until snapshots or retainers support it.
151
152## Common Root Causes
153
154- Full bundled channel/plugin runtime loaded for static data.
155- `getChannelPlugin()` fallback used when an already-loaded fixture or pure
156 parser would suffice.
157- Broad `api.ts`, `runtime-api.ts`, `test-api.ts`, or plugin-sdk barrels pulled
158 into hot tests.
159- SDK root aliases or package barrels pulling focused subpaths back into a broad
160 plugin graph.
161- Plugin-inspector loading runtime code just to render metadata, reports, or CI
162 policy scores.
163- Bundled plugin capture reusing real config/home state instead of synthetic,
164 redacted, isolated state.
165- Partial-real mocks using `importActual()` around broad modules.
166- `vi.resetModules()` plus fresh imports in per-test loops.
167- Test plugin registry seeded in `beforeAll` while runtime state resets in
168 `afterEach`.
169- Per-test gateway/server/client startup when state reset would suffice.
170- Runtime/default model/auth selection paid by idle snapshots or fixtures.
171- Plugin-owned media/action discovery triggered before checking whether args
172 contain plugin-owned fields.
173- Parallel Vitest runs sharing `node_modules/.experimental-vitest-cache` without
174 distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values.
175
176## Benchmark Commands
177
178Scoped file:
179
180```bash
181timeout 240 /usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose
182```
183
184Scoped file with import breakdown:
185
186```bash
187timeout 240 /usr/bin/time -l env \
188 OPENCLAW_VITEST_IMPORT_DURATIONS=1 \
189 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 \
190 pnpm test <file> --maxWorkers=1 --reporter=verbose
191```
192
193Grouped suite:
194
195```bash
196pnpm test:perf:groups --full-suite --allow-failures \
197 --output .artifacts/test-perf/<name>.json
198```
199
200Extension batch:
201
202```bash
203pnpm test:extensions:batch <plugin[,plugin...]> -- --reporter=verbose
204```
205
206All extension tests:
207
208```bash
209pnpm test:extensions
210```
211
212Package-boundary plugin checks:
213
214```bash
215pnpm run test:extensions:package-boundary:canary
216pnpm run test:extensions:package-boundary:compile
217```
218
219Reuse an existing Vitest JSON report:
220
221```bash
222pnpm test:perf:groups --report <vitest-json> \
223 --output .artifacts/test-perf/<name>.json
224```
225
226## Verification
227
228- Always run the targeted test surface that proves the change.
229- For source changes, run `pnpm check:changed` before push; in maintainer
230 Testbox mode run it in the warmed Testbox.
231- For test-only changes, run `pnpm test:changed` or the exact edited tests.
232- Run `pnpm build` when touching lazy-loading, bundled artifacts, package
233 boundaries, dynamic imports, build output, or public surfaces.
234- For plugin SDK/barrel/runtime changes, compare exact commits with
235 `pnpm plugin-sdk:api:diff -- --base <base-sha> --head <head-sha>` when the
236 public API surface may drift. For PR-local proof, use the branch merge base
237 as `<base-sha>` and the exact tested head commit as `<head-sha>`.
238- For plugin-suite perf fixes, verify at least one representative plugin batch
239 plus the changed gate; use Package Acceptance if the bug only exists in a
240 packed artifact.
241- If deps are missing/stale, run `pnpm install` and retry the exact failed
242 command once.
243- Use the report format:
244
245```markdown
246| Metric | Before | After | Gain |
247| -------------- | -----: | -----: | ------------: |
248| File wall time | `Xs` | `Ys` | `-Zs` (`P%`) |
249| Max RSS | `XMB` | `YMB` | `-ZMB` (`P%`) |
250| CPU user/sys | `X/Ys` | `A/Bs` | explain |
251```
252
253## Handoff
254
255Keep the final concise:
256
257- Root cause.
258- Suite/plugin scope.
259- Files changed.
260- Before/after wall, Vitest/import, CPU, and RSS numbers where available.
261- Leak classification if memory was involved: real leak, retained module graph,
262 or inconclusive.
263- Coverage retained.
264- Verification commands.
265- Testbox ID or workflow URL for remote proof.
266- Commit hash and push status.