Skip to content

feat(history): bounded-memory history core, parallel burndown, UAST spill, codefang mcp - #29

Merged
dmytrogajewski merged 7 commits into
mainfrom
codefang-ai
Oct 16, 2026
Merged

dmytrogajewski merged 7 commits into
mainfrom
codefang-ai

Conversation

@dmytrogajewski

Copy link
Copy Markdown
Contributor

Scope

Analyzer audit fixes, plus the restoration of bounded-memory history processing that the Go → Rust rewrite lost. CHANGELOG.md [Unreleased] lists every change.

  • Bounded-memory history core.
    • Every history walk folds through a worker pool (history/pool.rs). Workers claim runs of consecutive commits from a shared cursor, steal from idle peers, and run at most one chunk ahead of the in-order fold.
    • Free heap pages go back to the OS after each chunk (cf_meminfo::release_free_heap).
    • Output is byte-identical for every chunk size.
  • --memory-budget / --chunk-size, also as pipeline.memory_budget / pipeline.chunk_size.
    • Each walk plans its chunk size and worker count from its share of the budget and its measured per-walk costs.
    • libgit2 gets 25 % of the budget for its object cache and 7.5 % for mapped pack windows.
    • -v prints each plan.
  • UAST analyzers.
    • They parse one change at a time and run as one memoized pass.
    • Their finished results spill to an unlinked temp file while other history walks run.
  • history/burndown prepares its per-change work on the pool and reuses the pre-pass tree diffs of heavy commits: 59.4 s → 25.9 s.
  • --shotness-keep-ended (opt-in) keeps ended shotness entities, marked ended. The default report is unchanged.
  • codefang mcp serves codefang_analyze, uast_parse and codefang_history over stdio. Each tool result is the CLI's own report (cf_commands::render_run).
  • Removed: the retired Go differential gate and its oracle data.

Measurements

Workload: 31,923 kubernetes commits, codefang run -a 'history/*' --format json, 24 cores, quiet machine. Output is byte-identical to the previous build (md5 15fbc154f5393227f96f87ad6165cb4a, 538 MB).

Build / flags Wall Peak RSS
previous build 399.8 s 12.9 GiB
automatic budget (2 GiB) 334.4 s 3.56 GiB
--memory-budget 4GB 327.8 s 3.79 GiB

Test matrix

  • Gates: cargo fmt --check, make lint (0 findings), make deadcode (0 findings), 3138 tests passed.
  • Chunk-size equality: bins/codefang/tests/chunked_history_e2e.rs checks 10 history analyzers × 9 formats × chunk sizes {1, 2, 7, 1000}. It also covers the co-selected UAST pass, budget invariance, the plan lines and the spill path.
  • Other end-to-end suites:
    • mcp_e2e.rs: the handshake, tool listing, and all three tools against the CLI reports.
    • shotness_e2e.rs: both modes.
    • config_e2e.rs: the new keys.
  • Unit tests: the pool (ordering, stealing, failures, panics), the planner, the spill round trip, burndown's change facts against the sequential reference, and libgit2 native limits. Each new mechanism was red first or mutation-checked.

Risks

  • libgit2 options are process-global. The memory limits are set once, before the first repository opens (cf_gitlib::limit_native_memory).
  • Two new unsafe calls, each behind a safe wrapper with a test: malloc_trim(0) and libgit2's mapped-pack and object-cache limits.
  • tmpfs /tmp. On a tmpfs /tmp the spill leaves the process but not RAM; TMPDIR points it at a disk.
  • --shotness-keep-ended on long histories: the shotness report grows 3.4× (256 MB → 859 MB on the window above).

Rollback

  • --chunk-size 1000000 reproduces the unbounded walk.
  • Reverting this commit restores the previous pipeline. The report bytes are unchanged, so consumers are unaffected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KmiE8YcZ3GtgQW2bDbcgXo

drwatsno and others added 7 commits October 5, 2026 19:36
…pill, codefang mcp

Analyzer audit fixes plus the bounded-memory history pipeline
(FRD-bounded-memory-history, FRD-parallel-burndown, FRD-history-spill,
BUG-history-shotness-nested-entities). CHANGELOG.md [Unreleased] lists every
change; the highlights:

- Every history walk folds through a worker pool: workers claim runs of
  consecutive commits from a shared cursor, steal from idle peers, and run at
  most one chunk ahead of the in-order fold; free heap pages go back to the
  OS after every chunk. Output is byte-identical for every chunk size.
- --memory-budget / --chunk-size (and pipeline.memory_budget /
  pipeline.chunk_size): each walk plans its chunk and worker count from its
  share and measured per-walk costs; libgit2 gets a quarter of the budget
  for its object cache and 7.5 % for mapped pack windows; -v prints plans.
- The UAST analyzers parse one change at a time and run as one memoized
  pass; their finished results spill to an unlinked temp file while other
  history walks run.
- history/burndown prepares its per-change work on the pool and reuses the
  pre-pass tree diffs of heavy commits.
- history/shotness: --shotness-keep-ended keeps ended entities, marked.
- codefang mcp serves codefang_analyze, uast_parse and codefang_history
  over stdio; every tool result is the CLI's own report.
- The retired Go differential gate and oracle data are removed.

Measured on 31,923 kubernetes commits, history/* --format json, output
byte-identical to the previous build:
  previous build      399.8 s  12.9 GiB
  automatic budget    334.4 s   3.56 GiB
  --memory-budget 4GB 327.8 s   3.79 GiB

Gates: cargo fmt --check, make lint (0), make deadcode (0), 3138 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiE8YcZ3GtgQW2bDbcgXo
Hard tabs in the Go examples become spaces, code spans with a trailing
space are reworded, the global-flags heading follows its parent, a table
cell no longer contains a literal pipe, and the people-dict example fence
names its language. markdownlint-cli2 over the CI globs: 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmiE8YcZ3GtgQW2bDbcgXo
Full-report validation over 100 popular open-source repositories found two
report-validity defects.

- The schema registry described fields the reports did not emit for
  history/typos and history/imports, and omitted fields they did emit for
  history/burndown (`interactions`) and static/clones (`message`). The
  registry now declares exactly the report keys, matching site/analyzers.
  A new e2e contract test asserts schema keys equal report keys for every
  analyzer of a full run.

- A shallow clone yields an empty history walk, so every history analyzer
  reported empty results with exit 0 and no explanation. The run still
  succeeds with the same empty report, and now prints one
  `warning: shallow clone` line naming `git fetch --unshallow`.

Refs: site/how-to/run-in-ci.md, site/release-notes/unreleased.md
Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx
…e size

Full-report memory on large repositories was dominated by a few large
data files. Alacritty's tests/ref fixtures include a 19 MB JSON file that
needed over 3 GB to parse, and the full report was OOM-killed.

- The function analyzers (complexity, halstead, cohesion, clones) no
  longer build a tree for data and config formats (JSON, YAML, TOML, INI,
  CSV, PSV, dotenv, XML, git and ssh config). Those files have no
  functions, so an empty file root gives the same zero-function report.
  Verified byte-identical per-file output against the previous binary on a
  fixture of every such format and on 12 real repositories (48 runs).
  alacritty's static/complexity dropped from 4.7 GB and 46 s to 36 MB and
  2 s.
- The UAST parse size limit is now 8 MiB by default (was 64 MiB). Larger
  source files are skipped with a warning that names the limit. The new
  --max-parse-file-size flag (MiB, >= 1) raises it.
- StaticFilter::default() now carries the default limit; the derived
  default would have set it to zero and skipped every file.

Refs: site/release-notes/unreleased.md
Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx
…nd the mapping

The skip for data files used a hardcoded list of format names. Replace it
with two data-driven checks:

- enry's Linguist language type (data, programming, markup, prose), vendored
  from enry v2.1.0's data.LanguagesType as data/enry-types-v2.1.0.tsv and
  exposed as cf_langpath::language_type. The table is generated by
  tools/enrydump, which now writes both tables and reproduces the existing
  lookup table byte for byte.
- cf_uast::Parser::yields_functions: whether any rule of the file's mapping
  can produce a function node (type Function or Method, or the Function and
  Declaration roles), the same criteria the complexity analyzer uses.

A function parse is skipped only when enry types the file as data AND the
mapping cannot yield a function. enry's type alone is not enough: SQL and
GraphQL are data-typed but their mappings declare functions (CREATE FUNCTION,
query operations), so the type alone would change their reports.

Verified: function counts for SQL (1) and GraphQL (2) are unchanged, and the
four function analyzers match the previous binary byte-for-byte on the data
fixture and on 12 real repositories (48 runs). alacritty static/complexity
drops from 84 s to 2.3 s.

.gitignore: the new table is listed beside the other vendored tables, since
the blanket *.tsv rule would otherwise leave it untracked.

Refs: site/guide/cli-reference.md, site/release-notes/unreleased.md
Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx
The import analyzer now uses the same data-file skip as the function
analyzers. A file is not parsed when enry types it as data AND its mapping
cannot yield an Import or Call node, the only nodes the import rules start
from (cf_imports::IMPORT_TRIGGER_TYPES). Protocol Buffers is data-typed but
its mapping yields imports, so it is still parsed and its imports reported.

static/comments is deliberately not skipped: YAML, TOML, and INI comments
are real parsed nodes that are counted, so an empty tree would change the
report.

The predicate is generalized to Parser::may_yield(file, wanted), with
yields_functions as one caller. cf-commands has a single parse_unless_data
used by the function and import analyzers.

Verified byte-identical output against the previous binary for static/imports
on the data fixtures and on 12 real repositories (60 comparisons across the
five analyzers), and comments and complexity on the fixtures.

Refs: site/guide/cli-reference.md, site/release-notes/unreleased.md
Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx
The Format check in CI failed on the formatting of the new e2e tests and the
cf-uast re-exports. This is a formatting-only change; no code changed.

Claude-Session: https://claude.ai/code/session_01G4vrFqHuccLUdQpFcRCQRx
@dmytrogajewski
dmytrogajewski merged commit 0894ab0 into main Oct 16, 2026
7 checks passed
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.

3 participants