Repository navigation
perf(interpreter): CoW subshell snapshots, attribute bitset, flag cache - #1767
Merged
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 32b3624 | Commit Preview URL Branch Preview URL |
May 26 2026, 12:21 AM |
Major hot-path overhaul targeting the allocation-heavy paths identified
in the analysis. Median -36% on the new criterion micro-benchmarks
(see crates/bashkit-bench/results/criterion-hotpath-perf-*.md).
Key changes:
- Wrap variables / arrays / assoc_arrays / functions / traps / aliases /
var_attrs / namerefs in `Arc<HashMap<...>>`. Subshell snapshot is now
an O(1) refcount bump instead of an O(n) deep clone; mutations go
through `vars_mut()` / `arrays_mut()` / etc. which `Arc::make_mut`
the inner map (zero-clone when refcount==1, one clone when a live
subshell mutates). cmdsubst/subst_nested_3: -60%.
- Replace the `_READONLY_X` / `_INTEGER_X` / `_LOWER_X` / `_UPPER_X` /
`_NAMEREF_X` `format!()`-built keys in `variables` with dedicated
`VarAttrs` bitset map and `namerefs: HashMap<String, String>`. Every
variable assignment used to allocate 4-5 `String`s for attribute
checks; now it's a single hashmap probe + bit test. set_variable
also moves through `get_mut` instead of `entry(.clone())` for the
call-stack walk so the common no-local-binding path is alloc-free.
- Cache shell option flags (`set -e/-u/-x/-o pipefail/-a/-f/-C/etc.`)
in a `BashFlags` bitfield. Hot-path checks (`is_nounset`,
`is_pipefail`, `is_errexit_enabled`, etc.) are now one bit test
instead of a HashMap lookup + string compare. SHOPT_X writes stay
in `variables` so `$-` / `set -o` rendering keeps working;
apply_builtin_side_effects refreshes the cache after every builtin
so set/shopt mutations stay coherent.
- Skip the `tokio::task::spawn_blocking` + `time::timeout` round-trip
for scripts <= 16 KB. The parser's `max_parser_operations` fuel
budget already bounds runaway parses, so the threadpool hop was
pure overhead on the inline-exec path. Large scripts still take
the pre-empted route. startup/empty: -65%.
- New `bench` profile inheriting release with fat LTO + codegen-units=1
so criterion measurements get the best cross-crate inlining. Day-to-day
release builds stay on thin LTO.
- New `crates/bashkit/benches/hotpath.rs` criterion benchmark covering
startup, loops, variables, command substitution, SHOPT flag overhead,
pipelines, functions, and parameter expansion. Use:
cargo bench --bench hotpath -- --save-baseline before
# land changes
cargo bench --bench hotpath -- --baseline before
Mechanical: `set_variable`, `unset`, `readonly`, `declare`, `export`,
`local` all migrated to the new helpers. ShellRef exposes
`var_attrs` / `namerefs` (mut) and helper methods (`is_var_readonly`,
`mark_var_readonly`, `readonly_names`) so builtins don't reach into
the legacy marker scheme.
Test counts unchanged (2281 pass; the single pre-existing rg
differential test failure reproduces on baseline and is unrelated).
- Remove dead `set_shopt_value` / `remove_shopt_value` / `is_var_integer` / `is_var_lower` / `is_var_upper` helpers. The flag cache and attribute reads are now driven directly from the live mutation paths; the helpers were left for future call sites that didn't materialize. - Drop the commented-out `aliases_mut`; aliases are only mutated by builtins via ShellRef, which `Arc::make_mut`s inline at the construction site. - Update `specs/threat-model.md` (TM-INJ-009 / TM-INJ-018 / TM-INJ-019 / TM-INJ-020 / TM-INJ-021) to describe the new `VarAttrs` bitset and dedicated `namerefs` map instead of the historical `_READONLY_X` / `_NAMEREF_X` marker scheme.
…ute_shell The `bash -e -c '...'` subprocess launcher (and `set -e ... unset SHOPT_e` variants) saved the parent's prior `SHOPT_*` value, applied the child's flag through `insert_variable_checked` (which now updates the `flags` bitfield), but on restore only did a bare `vars_mut().remove(&var)` when the parent had no entry — leaving the cached `BashFlags::ERREXIT` / `NOUNSET` / etc. bit set in the parent. Subsequent commands in the parent then mis-detected errexit, breaking out of `execute_script_body` before the user's follow-up commands could run. The spec_tests harness caught this: [bash-flags.test] bash_e_errexit got "" expected "exit:1\n" [bash-flags.test] bash_o_errexit got "" expected "exit:1\n" [bash-flags.test] bash_combined_eu got "" expected "exit:1\n" [bash-flags.test] bash_e_does_not_leak got "" expected "still running\n" [bash-flags.test] bash_duplicate_errexit_flags_do_not_leak got "" expected "still running\n" `execute_shell` now also clears the bit via `BashFlags::from_shopt_name` when removing the SHOPT entry — symmetric with `insert_variable_checked`. Reproduced locally; `cargo test -p bashkit --test spec_tests -- bash_spec_tests` now passes.
chaliy
force-pushed
the
claude/focused-cray-2iHu9
branch
from
May 26, 2026 00:20
8bb2303 to
32b3624
Compare
This was referenced May 26, 2026
chaliy
added a commit
that referenced
this pull request
May 27, 2026
## Summary Maintenance release rolling up 50 commits since v0.7.1. ## Highlights - **Maintenance release** — rolls up ~25 `rg` hardening fixes, a CoW subshell-snapshot perf landmark (#1767), security tightening (FD cap #1780, tool-hook enforcement for `command` #1781), a `BuiltinHelper` refactor (#1788), and test/build hygiene work. ## Publish-readiness report | Check | Result | |-------|--------| | `cargo fmt --check` | clean | | `cargo clippy --all-targets -- -D warnings` | clean | | `cargo test --features http_client` | 2965 pass (environmental rg-diff failure resolved with pinned ripgrep 15.1) | | `cargo publish --dry-run -p bashkit` (with CI's monty strip) | succeeds | | `cargo publish --dry-run -p bashkit-cli` | packaging succeeds; resolution fails only because bashkit 0.7.2 isn't on crates.io yet (chicken-and-egg, resolves post-merge per `publish.yml`) | | Version sync (Cargo.toml, bashkit-cli/Cargo.toml, bashkit-js/package.json, Cargo.lock) | all 0.7.2 | | Registry version checks (crates.io / npm / PyPI) | all currently 0.7.1; 0.7.2 > 0.7.1 | ## Test plan - [ ] CI green on this PR - [ ] Merge → `release.yml` creates GitHub Release + tag `v0.7.2` - [ ] `publish.yml` publishes bashkit + bashkit-cli to crates.io - [ ] `publish-python.yml` publishes wheels to PyPI - [ ] `publish-js.yml` publishes `@everruns/bashkit` to npm - [ ] `cli-binaries.yml` ships prebuilt binaries and bumps Homebrew tap
chaliy
added a commit
that referenced
this pull request
May 30, 2026
…he (#1767) Hot-path overhaul for script execution. Median -36.6% across 23 criterion micro-benchmarks vs. pre-change baseline. - CoW subshell snapshots: wrap variables/arrays/assoc_arrays/functions/traps/aliases/var_attrs/namerefs in Arc<HashMap<...>> so $(...) snapshot is O(1) refcount bump (Arc::make_mut on mutate). cmdsubst/subst_nested_3: -60%. - Attribute bitset: replace _READONLY_X/_INTEGER_X/_LOWER_X/_UPPER_X/_NAMEREF_X formatted-key markers with typed VarAttrs map + dedicated namerefs map. Removes 4-5 format!() allocations per variable assignment. - BashFlags cache: set -e/-u/-x/-o pipefail/-a/-f/-C/expand_aliases tracked in a bitfield. Hot-path checks become bit tests. apply_builtin_side_effects refreshes the cache after every builtin; execute_shell clears bits when restoring removed SHOPT_* entries. - Inline parse for scripts <= 16 KB: skip tokio spawn_blocking + timeout when parser fuel already bounds runaway. startup/empty: -65%. - set_variable clone reduction: call-stack walk uses get_mut instead of entry(.clone()); execute_for moves loop value instead of cloning. - [profile.bench] inherits release with lto=fat + codegen-units=1 for criterion measurements. - New crates/bashkit/benches/hotpath.rs criterion bench covering startup, loops, variables, command substitution, SHOPT overhead, pipelines, functions, parameter expansion. Updated specs/threat-model.md (TM-INJ-009/018/019/020/021) to describe the new attribute storage. is_internal_variable() still rejects the legacy prefixes from every write path as defense-in-depth.
chaliy
added a commit
that referenced
this pull request
May 30, 2026
## Summary Maintenance release rolling up 50 commits since v0.7.1. ## Highlights - **Maintenance release** — rolls up ~25 `rg` hardening fixes, a CoW subshell-snapshot perf landmark (#1767), security tightening (FD cap #1780, tool-hook enforcement for `command` #1781), a `BuiltinHelper` refactor (#1788), and test/build hygiene work. ## Publish-readiness report | Check | Result | |-------|--------| | `cargo fmt --check` | clean | | `cargo clippy --all-targets -- -D warnings` | clean | | `cargo test --features http_client` | 2965 pass (environmental rg-diff failure resolved with pinned ripgrep 15.1) | | `cargo publish --dry-run -p bashkit` (with CI's monty strip) | succeeds | | `cargo publish --dry-run -p bashkit-cli` | packaging succeeds; resolution fails only because bashkit 0.7.2 isn't on crates.io yet (chicken-and-egg, resolves post-merge per `publish.yml`) | | Version sync (Cargo.toml, bashkit-cli/Cargo.toml, bashkit-js/package.json, Cargo.lock) | all 0.7.2 | | Registry version checks (crates.io / npm / PyPI) | all currently 0.7.1; 0.7.2 > 0.7.1 | ## Test plan - [ ] CI green on this PR - [ ] Merge → `release.yml` creates GitHub Release + tag `v0.7.2` - [ ] `publish.yml` publishes bashkit + bashkit-cli to crates.io - [ ] `publish-python.yml` publishes wheels to PyPI - [ ] `publish-js.yml` publishes `@everruns/bashkit` to npm - [ ] `cli-binaries.yml` ships prebuilt binaries and bumps Homebrew tap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hot-path overhaul for script execution. New
cargo bench --bench hotpathmicro-benchmarks show median −36.6%, mean −39.7%, best −64.9% across 23 cases versus a saved baseline taken before this change. All micro-benches improved; nothing regressed.Full table:
crates/bashkit-bench/results/criterion-hotpath-perf-linux-x86_64-1779744742.md.What
variables/arrays/assoc_arrays/functions/traps/aliases/var_attrs/namerefsinArc<HashMap<…>>.$(...)and((...))substitution snapshot is now an O(1) refcount bump instead of an O(n) deep clone; mutations go throughvars_mut()/arrays_mut()/ etc. which callArc::make_mut(zero clone at refcount=1, one clone when a live subshell mutates)._READONLY_X/_INTEGER_X/_LOWER_X/_UPPER_X/_NAMEREF_Xformatted-key markers (which lived insidevariables) with a typedVarAttrsbitset map and a dedicatednamerefsmap. Every variable assignment used to allocate 4–5Strings just for attribute checks; now it's one map probe + bit test.BashFlagscache — cache shell-option flags (set -e/-u/-x/-o pipefail/-a/-f/-C,shopt expand_aliases) in aBashFlagsbitfield. Hot-path checks (is_nounset,is_pipefail,is_errexit_enabled,set -aallexport inset_variable, etc.) are now a single bit test instead of a HashMap lookup + string compare.SHOPT_Xwrites stay invariablesso$-/set -orendering keeps working;apply_builtin_side_effectsrefreshes the cache after every builtin sosetbuiltin mutations stay coherent.spawn_blockingfor small scripts — scripts ≤ 16 KB parse inline. The parser'smax_parser_operationsfuel budget bounds runaway parses without the threadpool/timer hop. Larger scripts keep the pre-emptable route.set_variableclone reduction — call-stack walk usesget_mutinstead ofentry(.clone()); the global insert path movesvaluestraight in when allexport is off.execute_formoves the iteration value intoset_variableinstead of cloning it.Subshell(...),execute_cmd_subst, andexpand_command_subs_in_arithmeticnow sharesnapshot_subshell_state/restore_subshell_state. Same struct also coversvar_attrs,namerefs, and theflagsbitfield — these used to silently leak across substitution boundaries.[profile.bench]— inherits release withlto = "fat"+codegen-units = 1so criterion measurements get maximum cross-crate inlining. Day-to-day release builds stay onlto = "thin".crates/bashkit/benches/hotpath.rs— criterion bench covering startup, loops, variables, command substitution, SHOPT flag overhead, pipelines, functions, and parameter expansion. Use:cargo bench --bench hotpath -- --save-baseline before # land changes cargo bench --bench hotpath -- --baseline beforeWhy
Two of the lowest-hanging perf items in the deep analysis:
$(...)substitution was eagerly cloning all ofvariables,arrays,assoc_arrays,functions,traps,aliases,memory_budget, andexec_fd_table. For a script with N variables that's O(N) per substitution. CoW Arc snapshots turn the no-mutation case (the common case —$(echo $x), command queries) into pure refcount work.format!("_READONLY_{}", name),format!("_INTEGER_{}", name),format!("_LOWER_{}", name),format!("_UPPER_{}", name)and four matching HashMap probes — even when no attributes were set. Tightfor i in ...; do x=...; doneloops paid 4–5 allocations per iteration just to discover the variable has no attributes.How
variables: HashMap<String, String>→Arc<HashMap<String, String>>arrays,assoc_arrays,functions,traps,aliases→ wrapped inArc<>var_attrs: Arc<HashMap<String, VarAttrs>>,namerefs: Arc<HashMap<String, String>>,flags: BashFlagsvars_mut()/arrays_mut()/assoc_arrays_mut()/functions_mut()/traps_mut()/var_attrs_mut()/namerefs_mut()helpers thatArc::make_mutonce and return&mut HashMap.ContextandShellRef(passed to builtins) hold&mut HashMapborrowed viaArc::make_mutinline at the construction site, so the builtin API is unchanged.interpreter/mod.rs,builtins/vars.rs(Unset,Readonly),builtins/export.rsto the new attribute API.ShellRefexposesis_var_readonly,mark_var_readonly,readonly_namesfor builtins.specs/threat-model.md: TM-INJ-009 / TM-INJ-018 / TM-INJ-019 / TM-INJ-020 / TM-INJ-021 mitigation text now reflects theVarAttrsbitset + dedicatednamerefsstorage. The legacy prefix block (is_internal_variable) is kept as defense-in-depth.Test plan
cargo test -p bashkit --lib --features http_client— 2342 pass, 1 fail (the pre-existingbuiltins::rg::tests::diff_rg_matches_real_rg_casesdifferential against the hostrgbinary — reproduces on baseline, unrelated to this PR).cargo test -p bashkit --features http_client,jq --test security_audit_pocs— 32 pass, 0 fail (TM-INJ injection POCs against declare/readonly/export/local).cargo test -p bashkit --features http_client,jq --test threat_model_tests— 181 pass, 0 fail.cargo test -p bashkit --features http_client,jq --test blackbox_security_tests— 88 pass, 0 fail.cargo test -p bashkit --features http_client,jq --test custom_builtins_tests— 20 pass, 0 fail.cargo clippy --all-targets -- -D warnings— clean (also fixed one pre-existinguseless_conversionwarning in passing).cargo fmt --check— clean.bashkitCLI:declare -rreadonly enforcement,declare -u/-lcase attrs,declare -nnamerefs,set -e/-u/-a,forloops, command substitution, subshell isolation, pipelines — all behave as before.cargo bench --bench hotpath -- --baseline before— median −36.6% vs. pre-change baseline (full table incrates/bashkit-bench/results/).Generated by Claude Code