You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
core: portable shell scanner takes quadratic time in the argument count #59165
ShellScan.scan in packages/core/src/shell/scan.ts classifies every word of a command. For each word it walks the whole precommand chain again (bashBuiltinIndex). For printf, set and declare/export it also copies every earlier argument (state.words.slice(name + 1)). One command with many arguments therefore takes quadratic time, and the scan blocks the server while it runs. The portable scanner is used on the local and dev channels and with experimental.portable_shell_scanner.
Expected
Time that grows linearly with the argument count, as it does for echo.
Actual
command
2000 args
8000 args
32000 args
printf a a ...
72 ms
441 ms
7735 ms
set a a ...
46 ms
525 ms
6250 ms
declare -a -a ...
113 ms
1491 ms
command command ... ls
100 ms
1202 ms
echo a a ... (control)
2 ms
8 ms
29 ms
The existing "bounded time" cases in packages/core/test/shell-scan/adversarial.test.ts use a 500 ms budget.
Thanks for the clear report and numbers. I could reproduce this by calling ShellScan.scan(..., "bash") directly on v2.1.4, and it still happens on the latest dev (c54677bece).
When the argument count doubles, the scan takes about four times as long. Timings at 2000 / 8000 / 16000 args on v2.1.4:
printf a a ...: 65 / 665 / 2517 ms
set a a ...: 27 / 309 / 1663 ms
declare -a -a ...: 53 / 819 / 3329 ms
command command ... ls: 37 / 523 ms
echo a a ... (control): 1 / 6 / 8 ms
Latest dev gives nearly the same numbers (e.g. printf 2504 ms and declare 3163 ms at 16000 args). That is well past the 500 ms budget the existing bounded-time tests use. The cause matches your description: for every word, bashWordEvaluation re-walks the precommand chain in bashBuiltinIndex and copies all earlier words with state.words.slice(name + 1).
The script below reproduces it; pass it the path to an OpenCode checkout. Your PR #59163 addresses this and is still open.
Reproduction script (scan-timing.ts)
// Usage: bun /workspace/repro/scan-timing.ts <path-to-opencode-checkout>constrepo=process.argv[2]??"/workspace/repo"const{ ShellScan }=awaitimport(`${repo}/packages/core/src/shell/scan.ts`)constcases: Record<string,(n: number)=>string>={"printf a a ...": (n)=>"printf "+"a ".repeat(n),"set a a ...": (n)=>"set "+"a ".repeat(n),"declare -a -a ...": (n)=>"declare "+"-a ".repeat(n),"command command ... ls": (n)=>"command ".repeat(n)+"ls","echo a a ... (control)": (n)=>"echo "+"a ".repeat(n),}for(const[name,build]ofObject.entries(cases)){constrow: string[]=[]for(constnof[2000,8000,16000]){constinput=build(n)constt=performance.now()ShellScan.scan(input,"bash")row.push(`${n}: ${(performance.now()-t).toFixed(0)} ms`)}console.log(name.padEnd(26),row.join(" "))}
Activity
opencode-agent commentedon Oct 26, 2026
Thanks for the clear report and numbers. I could reproduce this by calling
ShellScan.scan(..., "bash")directly on v2.1.4, and it still happens on the latestdev(c54677bece).When the argument count doubles, the scan takes about four times as long. Timings at 2000 / 8000 / 16000 args on v2.1.4:
printf a a ...: 65 / 665 / 2517 msset a a ...: 27 / 309 / 1663 msdeclare -a -a ...: 53 / 819 / 3329 mscommand command ... ls: 37 / 523 msecho a a ...(control): 1 / 6 / 8 msLatest
devgives nearly the same numbers (e.g.printf2504 ms anddeclare3163 ms at 16000 args). That is well past the 500 ms budget the existing bounded-time tests use. The cause matches your description: for every word,bashWordEvaluationre-walks the precommand chain inbashBuiltinIndexand copies all earlier words withstate.words.slice(name + 1).The script below reproduces it; pass it the path to an OpenCode checkout. Your PR #59163 addresses this and is still open.
Reproduction script (scan-timing.ts)
Run it:
bun scan-timing.ts