Skip to main content
← Back to list
01Issue
BugOpenSwamp CLIPublicTeam
AssigneesNone

Relationships

#4280 Lookup by id of an auto-definition parses every file in its type directory; serve CPU grows with run count

Opened by nckrtl · 11/2/2026

Summary

YamlDefinitionRepository.locateByIdInDir falls back to a slow path that reads and YAML-parses every file in a type directory until one declares the requested id. Auto-definitions are saved as <name>.yaml, not <id>.yaml, so every lookup by id of an auto-definition takes the slow path. The type directories grow without bound when a workflow gives each run its own behaviorName (recommended so forEach items and concurrent runs do not serialize on one model lock). Nothing removes old auto-definitions, so the per-lookup cost grows with the total number of runs ever made.

Version

swamp 20261031.063736.0-sha.e459d219, swamp serve --remote-only --hot-reload --auth-mode token, filesystem datastore, Linux x86_64.

Measurements (one serve, 2026-11-02)

  • serve CPU 120–124% steady (about 65% user, 55% system), 81% on the main JS thread, while the serve log shows almost nothing (about 50 lines per 10 minutes).
  • Auto-definition type directories: @swamp/ssh 13,595 files (oldest from 2026-10-31, growing every run), command/shell 1,940, @nckrtl/certificate-script-assets 1,536.
  • A 4-second strace of serve: 10,798 statx calls on .swamp/auto-definitions/@swamp/ssh, 25 getdents64 listings of that directory, and repeated full reads of route-cert-assets-<uuid>.yaml and observation-*.yaml auto-definition files.
  • A 3-second strace: 833 realpath-style walks (each about 12 readlink calls, about 3,000 failing readlink/s); 48% resolve paths under .swamp/outputs/swamp/worker/set_status (24,215 files at the time).
  • Retention is configured and works (swamp doctor datastores --retention: no stored item older than its retention). Running swamp run gc by hand removed 123 runs, 36,623 outputs and 62 MB and cut statx calls about 10×, but serve CPU stayed at 120–124%. So the cost is not old runs; it is lookups that scale with directory size.

Code path

locateByIdInDir (definition repository): the fast path tries ${id}.yaml; on a miss it does for await (const entry of Deno.readDir(typeDir)), then Deno.readTextFile + parseYaml + Definition.fromData for each entry until the id matches. For auto-definitions the fast path always misses.

Expected

A lookup by id of an auto-definition costs O(1), for example through the id index serve already keeps for repository definitions, or by also storing auto-definitions under their id. A retention setting (or GC phase) for auto-definitions whose owning runs and data are gone would bound the directories as well.

Reproduce

  1. Create a workflow whose step uses behaviorType with behaviorName: '${{ "x-" + run.id }}' (one auto-definition per run).
  2. Run it a few thousand times under swamp serve.
  3. Watch serve CPU and strace -f -e trace=statx,openat -p <serve pid> while any later run of that behavior type executes: each lookup by id lists and parses the whole .swamp/auto-definitions/<type>/ directory.
02Bog Flow
◉OPEN○TRIAGED○IN PROGRESS○SHIPPED

Open

11/2/2026, 5:27:47 AM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

nckrtl commented 11/2/2026, 6:17:27 AM

Follow-up measurement. The --hot-reload file watcher is not involved: serve (PID 2628330) holds exactly one inotify watch, on the repository's workflows/ directory; .swamp/data, .swamp/outputs and auto-definitions are not watched. After native retention plus a manual 'swamp run gc' (123 runs, 36,623 outputs, 62 MB removed) and 'swamp data gc' (191 expired entries, 290 versions, 95 MB), serve still runs at about 105% CPU (47% user, 58% system). The remaining kernel time is 3,460 context switches/s, 2,400 futex waits/s and about 280 realpath walks/s (3,000 failing readlink/s), half on .swamp/outputs/swamp/worker/set_status paths. The auto-definition type directories (13,595 files under @swamp/ssh) are unchanged by any native GC command. 'swamp data gc' and 'swamp data prune' through serve each took over 10 minutes on a 94,351-record catalog and timed out the client.

nckrtl commented 11/2/2026, 8:35:37 AM

More evidence for the lookup by id, from the same serve (swamp 20261031.063736.0-sha.e459d219, filesystem datastore).

Code path

YamlDefinitionRepository.findById(type, id):

  1. Tries <id>.yaml in the type directory.
  2. Tries idToActualPath, a cache that lives on the repository instance.
  3. Falls back to findAll(type), then locateByIdInDir(secondaryBaseDir, type, id), which reads and parses every file in .swamp/auto-definitions/<type>/ until one declares the id.

Auto-definitions are saved as <name>.yaml, so step 1 never hits. The repository instance, and so its cache, is created per run context, so every run starts with an empty cache and rescans the whole directory.

Numbers

  • On 2026-11-02, @swamp/ssh held 13,775 auto-definitions, 8,260 of them from one forEach step that named its behavior <family>-<run.id>-<index>.
  • With work paused and the finished per-run auto-definitions moved out (13,775 to 410), serve CPU went from 120–135% to about 5%. Work was paused, so this is not a clean A/B. Serve on a fresh datastore idles at 2.7%.

Workaround we use

We name behaviors after their target instead of their run, for example route-ssh-<address>. The type directories then stay bounded by the number of targets.

Suggestion

Index auto-definitions by id, as serve already does for repository definitions, or store them under their id. Either would make this lookup O(1) whatever the naming.

Sign in to post a ripple.