Skip to content

feat(m664): single-pass walker — 21+ reader migrations, ~5× perf - #715

Merged
mlieberman85 merged 1 commit into
kusari-oss:mainfrom
mlieberman85:664-single-pass-walker
Aug 24, 2026
Merged

feat(m664): single-pass walker — 21+ reader migrations, ~5× perf#715
mlieberman85 merged 1 commit into
kusari-oss:mainfrom
mlieberman85:664-single-pass-walker

Conversation

@mlieberman85

Copy link
Copy Markdown
Contributor

Summary

  • Milestone 664: single-pass filesystem walker + reader-registry. Replaces per-reader safe_walk (20+ tree traversals per scan) with ONE traversal + per-file/per-dir dispatch to interested readers. New crate module waybill-cli/src/scan_fs/walk_registry/.
  • 21 readers + 3 originally-deferred migrated. Every production-path scan-tree walker now goes through the shared walker. Legacy pub fn read() retained under #[allow(dead_code)] per FR-004 coexistence.
  • Contract C10 descend_into API extension lets readers opt into descending shared-walker-skipped dirs (target/, build/, dist/, etc.) with C10-scoped visibility so byte-identity is preserved for non-requesting readers.
  • Perf validated against pre-m664 nightly. ansible 5.3×, pytorch 3.9×, mongo 5.2× wall-time improvement. See specs/664-single-pass-walker/perf-results.md + perf-comparison.md.

Perf validation (release-mode, warm-cache, macOS APFS, 2026-08-23)

Fixture Files Pre-m664 baseline Post-m664 measured Target Improvement
ansible 5,793 4.10s 0.777s ≤ 1.2s (SC-001) 5.3×
pytorch 21,651 4.30s 1.117s ≤ 1.5s (SC-002) 3.9×
mongo 55,190 15.68s 3.04s ≤ 3.0s (SC-003) 5.2× (40ms over absolute, exceeds ≥5× spec claim)
SC-005 (10k synth) 10,000 34.6 µs/file p95 ≤ 100 µs beats 65%

Shared walker log confirms passes=1 on every scan. Walker cost per file: 4-12 µs. See specs/664-single-pass-walker/perf-results.md for the full write-up.

Head-to-head vs cdxgen / syft / trivy

Also compared against cdxgen 12.1.5 / syft 1.44.0 / trivy 0.71.1 on the same three fixtures. See specs/664-single-pass-walker/perf-comparison.md. Highlights:

  • Walker cost per file is competitive — 4.4 µs/file on mongo, in line with syft/trivy.
  • waybill leads on dep-graph coverage — mongo 571 edges vs syft 356 / trivy 172 / cdxgen 105.
  • waybill still 1.9s slower than trivy on mongo — attributed to go_binary's 55k fs::metadata + read_binary probes (feature no other tool provides). Follow-up --no-binary-scan flag would close the gap.

Pre-m664 vs post-m664 byte-identity check

Ran pre-m664 nightly (v0.2.0-nightly.20260821) vs post-m664 HEAD on the three fixtures with identical flags. Two bugs surfaced and fixed same-session:

  1. Symlink-to-file skip regression in walker.rs. Shared walker used entry.file_type().is_file() (does NOT follow symlinks); legacy safe_walk used Path::is_file() (follows). Dropped pytorch's docs/requirements.txt (symlink) + 22 sphinx/matplotlib/ipython deps. Fix: stat-follow symlink targets.
  2. Composer + cocoapods missed dist/ subtree on mongo. Their legacy skip predicates were narrower than the shared walker default. Fix: added descend_into: [target, dist, build, out, coverage, bower_components] per contract C10.

Post-fix diff vs pre-m664: pytorch byte-identical; ansible +1 pkg:pypi/botocore (depth-10, coverage improvement from pilot depth 16 > legacy pip cap 6); mongo +1 pkg:cargo/gluesmith (same class). Remaining diffs are strict supersets — post-m664 finds MORE packages, never fewer.

Architecture (crate module waybill-cli/src/scan_fs/walk_registry/)

  • walker.rs::SharedWalker — single-pass tree walker with canonicalize-keyed visited-set (m054 loop guard), m113 ExclusionSet, m114 permissive semantics.
  • registry.rs::ReaderRegistry — dispatch table. Each ReaderRegistration carries a globset::GlobSet, optional on_file/on_dir, opaque per-scan state (Arc<dyn Any>), and new Option<globset::GlobSet> descend_into.
  • dispatch.rs — insertion-order dispatch (C1) + catch_unwind per callback (C4 panic isolation).
  • dir_index.rs::DirIndex — (dir → sorted-filenames) map for sibling lookup with zero extra syscalls (C6).
  • walk_context.rs::SharedWalkerContext — reader handle with dir_index(), exclude_set(), push(), typed state::<T>().

10 contract clauses in specs/664-single-pass-walker/contracts/registry-api.md.

Migrations

Phase 1-2 (US1 pilot): haskell, scala, erlang, rpm_file, ipk_file — clean-shape criteria; collectively ~790ms savings on ansible baseline.

Phase 3-4 (US2 remaining): pip, cargo, gem, gradle, kotlin_dsl, npm, nuget, pants_jvm, pants_shell, pants_go, cocoapods, composer, dart, elixir, swift, vcpkg, conan, bazel, cmake, golang, yocto_layers. Patterns applied: Option<Vec<PathBuf>> precomputed-paths (T037 cargo / T047 pants_go / T058 golang / T059 yocto_layers), sibling-lookup on_dir (T041 gradle / T052 swift), marker-detect (T045 pants_jvm / T053 vcpkg / T054 conan / T055 bazel / T056 cmake), ancestor-path filter for legacy-only skips (T048 cocoapods / T051 elixir / T058 golang).

Phase 5-6 (US3 close-out): audit + escape-hatch annotations, walker-audit rationale companion doc, CI diagnostic pointer, SC-005 microbenchmark, docs updates.

Originally deferred, all resolved 2026-08-23:

  • T029 yocto/recipe via Option<Vec<PathBuf>> precomputed-paths.
  • T039 maven via C10 descend_into: [target].
  • T057 go_binary via two-phase pilot pattern + C10 descend_into: [build, dist, out, coverage, venv].

Test plan

  • ./scripts/pre-pr.sh green (clippy + workspace test)
  • Workspace test 5183 passed / 0 failed
  • Walker-audit CI-shape reproduction empty diff
  • SC-001 (ansible ≤ 1.2s warm): measured 0.777s
  • SC-002 (pytorch ≤ 1.5s warm): measured 1.117s
  • SC-003 (mongo ≤ 3.0s warm): measured 3.04s (40ms over absolute; ≥5× improvement met at 5.2×)
  • SC-005 (10k-file p95 dispatch ≤ 100µs): measured 34.6µs
  • Pre-m664 vs post-m664 diff: strict superset on ansible/mongo; pytorch byte-identical
  • CI green (waiting on run)

Follow-up backlog (post-m664, out of scope for this PR)

Documented in specs/664-single-pass-walker/perf-comparison.md §Follow-up backlog:

  1. --no-binary-scan flag (biggest single win — mongo would drop from 3.04s to ~640ms; ~1 day).
  2. Extension-based cheap-reject in go_binary (half-day; small win).
  3. Startup-cost audit (~200-300ms fixed cost on small fixtures; ~1 day investigation).
  4. Parallel reader dispatch (FR-012 explicitly deferred by m664 so the single-pass baseline could be measured cleanly first).
  5. --discovery-only mode (syft-style shallow scan; ~2 days).

🤖 Generated with Claude Code

…traversal

Milestone 664 replaces the pre-existing per-reader `safe_walk` architecture
(where each ecosystem reader walked the scan tree independently — 20+
traversals per scan) with a single-pass shared walker + reader-registry.
Every migrated reader declares its interest via a `globset::GlobSet` +
`on_file`/`on_dir` callback; the shared walker traverses ONCE and
dispatches per-file matches in registration order.

## Perf validation (release-mode, warm-cache, macOS APFS, 2026-08-23)

- ansible (5,793 files):  4.10s → 0.777s  = **5.3x** (target SC-001 ≤ 1.2s, beats 35%)
- pytorch (21,651 files): 4.30s → 1.117s = **3.9x** (target SC-002 ≤ 1.5s, beats 26%)
- mongo (55,190 files):  15.68s → 3.04s  = **5.2x** (target SC-003 ≤ 3.0s, 40ms over absolute but exceeds ≥5x claim)
- SC-005 microbenchmark: per-file p95 = 34.6 µs (target ≤ 100 µs, beats 65%)

All spec improvement multipliers met or exceeded. See
`specs/664-single-pass-walker/perf-results.md` for full numbers +
`specs/664-single-pass-walker/perf-comparison.md` for side-by-side
against cdxgen 12.1.5 / syft 1.44.0 / trivy 0.71.1.

## Post-m664 pre-vs-post byte-identity check (2026-08-23)

Ran pre-m664 nightly (v0.2.0-nightly.20260821) vs post-m664 HEAD on the
same three fixtures with identical flags. Two m664 bugs surfaced and
fixed same-session:

1. Symlink-to-file skip regression in `walker.rs`. Shared walker used
   `entry.file_type().is_file()` (does NOT follow symlinks) but legacy
   `safe_walk` used `Path::is_file()` (follows). On pytorch, dropped
   `docs/requirements.txt` (symlink) + 22 sphinx/matplotlib/ipython
   dependencies. Fix: stat-follow symlink targets; symlink-to-file
   dispatched as file, symlink-to-dir descended (canonicalize
   visited-set catches loops).

2. Composer + cocoapods missed `dist/` subtree on mongo. Their legacy
   skip predicates were narrower than the shared walker default (which
   skips `dist/build/target/out/coverage/bower_components`). Fix: added
   `descend_into: [target, dist, build, out, coverage, bower_components]`
   per contract C10 — scoped visibility to the requesting reader only.

Post-fix diff vs pre-m664 nightly: pytorch byte-identical; ansible +1
`pkg:pypi/botocore` (depth-10, coverage improvement from pilot depth
16 > legacy pip cap 6); mongo +1 `pkg:cargo/gluesmith` (same class).
Remaining diffs are strict supersets — post-m664 finds MORE, never
fewer.

## Architecture

New crate module `waybill-cli/src/scan_fs/walk_registry/`:
- `walker.rs::SharedWalker` — single-pass tree walker with the same
  canonicalize-keyed visited-set + m113 ExclusionSet + m114 permissive
  semantics as legacy `safe_walk`.
- `registry.rs::ReaderRegistry` — dispatch table; each
  `ReaderRegistration` carries a `GlobSet` of filename patterns +
  optional `on_file`/`on_dir` callbacks + opaque per-scan `state`
  (`Arc<dyn Any>` slot).
- `dispatch.rs` — per-file/per-dir dispatch loop; iterates registrations
  in insertion order (contract C1); every callback wrapped in
  `catch_unwind` (contract C4 panic isolation).
- `dir_index.rs::DirIndex` — in-memory (directory → sorted-filenames)
  map. Reader callbacks consult via `ctx.dir_index().contains(dir, name)`
  for sibling-lookup with zero extra syscalls (contract C6).
- `walk_context.rs::SharedWalkerContext` — the reader-facing handle
  exposing `dir_index()`, `exclude_set()`, `push(reader_id, entry)`,
  and typed state retrieval.

Ten contract clauses documented in
`specs/664-single-pass-walker/contracts/registry-api.md` (C1 dispatch
order, C2 sibling-lookup freshness, C3 sorted-filename invariant, C4
panic isolation, C5 safe_walk semantic preservation, C6 zero extra
syscalls, C7 FR-006 byte-identity, C8 registration idempotency, C9
ReaderId uniqueness, C10 descend_into scope preservation).

## Migrations (21 readers + 3 originally-deferred, all resolved)

US1 pilot (5 readers): haskell, scala, erlang, rpm_file, ipk_file.
US2 remaining (16 readers): pip, cargo, gem, gradle, kotlin_dsl, npm,
nuget, pants_jvm, pants_shell, pants_go, cocoapods, composer, dart,
elixir, swift, vcpkg, conan, bazel, cmake, golang, yocto_layers.
Originally deferred (all resolved 2026-08-23):
- T029 yocto/recipe — resolved via `Option<Vec<PathBuf>>` precomputed-
  paths pattern (same shape as T047 pants_go / T058 golang).
- T039 maven — resolved via C10 `descend_into: [target]` + ancestor-
  path filter in `finalize` for top-level-pom semantic.
- T057 go_binary — resolved via two-phase pilot pattern
  (candidate-collection pilot + post-pilot finalize) plus C10
  `descend_into: [build, dist, out, coverage, venv]`.

Legacy `pub fn read()` shims retained under `#[allow(dead_code)]`
across every migrated reader per FR-004 coexistence (deletion is a
follow-up task after the coexistence period ends).

## Contract C10 — descend_into API extension (post-2026-08-23)

`ReaderRegistration` gained an `Option<globset::GlobSet>` field. When
set, the shared walker descends into normally-skipped directories
matching any pattern. Dispatch under such subtrees is SCOPED — only
the requesting readers receive callbacks, preserving byte-identity for
non-requesting readers.

## Test coverage

- 3 new contract-C10 unit tests in `walker.rs`.
- Two integration test crates: `walk_registry_integration.rs` +
  `perf_walk_dispatch.rs`.
- SC-005 microbenchmark: 10k-file synthetic tree, 5-warm-sample p95.
- SC-001/002/003 wall-time tests (env-gated on
  `WAYBILL_PERF_ANSIBLE_DIR` / `WAYBILL_PERF_PYTORCH_DIR` /
  `WAYBILL_PERF_MONGO_DIR`).
- Workspace test suite: 5183 passed / 0 failed (up from 5017 pre-m664
  — the +166 includes the new C10 tests, wall-time tests, and misc
  scaffolding).

## Docs

- `docs/design-notes.md` §"Shared-walker reader registry (milestone 664)".
- `specs/664-single-pass-walker/spec.md` + `plan.md` + `research.md` +
  `data-model.md` + `contracts/registry-api.md` + `quickstart.md` +
  `tasks.md` + `perf-results.md` + `perf-comparison.md`.
- `waybill-cli/src/scan_fs/walk.audit-allowlist.rationale.md` (T064
  companion doc — categorizes retained walker-audit entries into
  FR-005 escape hatch / non-scan-tree / infra / shim-retention).

## CI

- Walker-audit CI step (`.github/workflows/ci.yml`) gains the FR-008
  diagnostic pointer in its failure branch (T065).

## Follow-up backlog

Documented in `specs/664-single-pass-walker/perf-comparison.md
§Follow-up backlog`:
1. `--no-binary-scan` flag (biggest single win — mongo would drop
   from 3.04s to ~640ms).
2. Extension-based cheap-reject in go_binary.
3. Startup-cost audit (~200-300ms fixed cost on small fixtures).
4. Parallel reader dispatch (FR-012 explicitly deferred by this
   milestone so single-pass baseline could be measured cleanly first).
5. `--discovery-only` mode (syft-style shallow scan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mlieberman85
mlieberman85 merged commit 88f28c5 into kusari-oss:main Aug 24, 2026
23 checks passed
mlieberman85 added a commit that referenced this pull request Aug 27, 2026
Cuts a stable v0.3.0. 43 commits since v0.2.0 with substantial
feature additions:

Feature highlights (grouped):

* Resolvers + graph completeness:
  - m663 (#708, #709, #710, #712): local cache-probe resolver — reads
    ~/.m2, $GOMODCACHE, ~/.cargo, RubyGems, npm, pip caches for
    high-confidence PURL extraction without network. New C152
    `waybill:resolver-tier` doc-scope annotation.
  - m235 (#688-#701): Gradle transitive dep-graph ladder — four-tier
    resolution (subprocess → cache → static → lockfile-only) with
    C146-C150 doc-scope + per-component annotations.
  - m236 (#703-#706): universalize `waybill:unresolved-reason` (C151)
    across all design-tier readers.

* Architecture:
  - m664 (#715): single-pass filesystem walker — consolidates 21+
    ecosystem-reader walk sites into one tree traversal per scan.
    5-6× warm-cache perf improvement on large fixtures.
  - m665 (#719): `--no-binary-scan=<MODE>` flag — operator opt-out
    of Go-binary content probing. C153 `waybill:binary-scan-
    suppressed` doc-scope annotation across CDX/SPDX 2.3/SPDX 3.

* eBPF:
  - m234 (#682, #686): un-pin bpf-linker to 0.11.0 via pre-built
    binary install + composite action + daily canary + verify script.

* Fixes:
  - #711: verifier-friendly bound in tls_openssl::probe_ssl_read
    for kernel bumps.
  - #680: Go workspace-mode detection in mod_why preflight.
  - Several release.yml + cosign compat fixes (#674, #675, #681).

Golden regeneration:

* CDX goldens: 11 files, 1-line churn each — `metadata.tools[].version`
  bumped 0.2.0 → 0.3.0.
* SPDX 2.3 goldens: 11 files. Version-string churn cascades into
  content-addressed SPDXRef-DocumentRoot-* IDs and documentNamespace
  URLs (both content-hashed).
* SPDX 3 goldens: 11 files. Same content-addressing cascade into
  doc-/pkg-/anno-/rel- IRIs.

Verified: normalized+sorted diff on all 33 goldens shows zero
semantic change beyond the version string per project memory
`feedback_verify_golden_churn_normalized`.

Also updates `waybill-cli/tests/fixtures/pkg_alias_binding/image-
baz.cdx.json` (same one-line version bump on an out-of-tree
regression fixture).

Skipping local pre-PR per project memory
`feedback_release_bump_prepr_slow` — version bump invalidates the
full compile cache so pre-PR takes 30+ min; CI will verify.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant