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/ssh13,595 files (oldest from 2026-10-31, growing every run),command/shell1,940,@nckrtl/certificate-script-assets1,536. - A 4-second
straceof serve: 10,798statxcalls on.swamp/auto-definitions/@swamp/ssh, 25getdents64listings of that directory, and repeated full reads ofroute-cert-assets-<uuid>.yamlandobservation-*.yamlauto-definition files. - A 3-second
strace: 833realpath-style walks (each about 12readlinkcalls, about 3,000 failingreadlink/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). Runningswamp run gcby hand removed 123 runs, 36,623 outputs and 62 MB and cutstatxcalls 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
- Create a workflow whose step uses
behaviorTypewithbehaviorName: '${{ "x-" + run.id }}'(one auto-definition per run). - Run it a few thousand times under
swamp serve. - 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.
Open
No activity in this phase yet.
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):
- Tries
<id>.yamlin the type directory. - Tries
idToActualPath, a cache that lives on the repository instance. - Falls back to
findAll(type), thenlocateByIdInDir(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/sshheld 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.