Skip to content

perf(core): Automated performance tuning by Claude - #1428

Draft
yamadashy wants to merge 47 commits into
mainfrom
perf/auto-perf-tuning-0406
Draft

perf(core): Automated performance tuning by Claude#1428
yamadashy wants to merge 47 commits into
mainfrom
perf/auto-perf-tuning-0406

Conversation

@yamadashy

@yamadashy yamadashy commented Apr 7, 2026

Copy link
Copy Markdown
Owner

Summary

Automated performance tuning of the repomix pack pipeline. This PR consolidates multiple optimizations achieving cumulative improvements of ~25% on end-to-end CLI execution.

Optimizations

Previous commits (already merged into this branch)

  1. Parallel git log for file sorting — overlap git log with file collection (~20% faster)
  2. Fast base64 detection — linear character scan pre-check before regex (~5% faster)
  3. Adaptive IPC batch sizing — reduce metrics worker overhead (~6% faster)
  4. Early metrics worker pool init — overlap gpt-tokenizer loading with I/O phases (~4% faster)
  5. Synchronous file reads — replace async readFile with readFileSync in collectFiles (~7% faster)
  6. Non-blocking cleanup + lazy computation — fire-and-forget worker teardown, skip unnecessary work (~8% theoretical)
  7. Token estimation from file tokens — estimate output tokens from file token counts + overhead ratio (~20% faster)
  8. git ls-files for file search — replace globby's gitignore traversal with git index access (~7% faster)
  9. Lazy-load globby and avoid globby directory scan on git fast path — replace the globby directory scan with lightweight directory discovery from git ls-files output when searching for empty directories. Lazy-load globby so it's never imported on the common git fast path.
  10. Two-phase base64 truncation pre-check — adds hasLongLine (indexOf-based) + hasLongNonWhitespaceRun gating to skip expensive regex scans. Reduces truncateBase64Content from ~82ms to ~12ms for 1000 files.
  11. Fix: Move try block to cover metrics worker pool lifecycle — ensures cleanup happens even if searchFiles or sortPaths throws.
  12. Skip redundant output tokenization — when all file tokens are available, skip redundant output token counting.
  13. Reduce metrics pipeline latency — batch size tuning and deferred output pattern for overlapping metrics with output generation.
  14. Security check keyword pre-filter — Skip expensive lintSource() for files without secret-related keywords.
  15. Pre-create metrics worker pool before pack() — Move metrics runner creation to defaultAction for earlier BPE warmup.
  16. Main-thread security check + pipeline restructuring — Avoid spawning security worker pool when few items pass the keyword pre-filter (≤50 items, the common case). Security worker threads compete for CPU with metrics warmup workers on machines with ≤4 cores.
  17. Skip tokenizing all files when tokenCountTree is disabled — Only tokenize the top-N largest files by character count to compute the token/char ratio and top-N token ranking.
  18. Replace sequential keyword scanning with single-pass regex — The mightContainSecret pre-filter was using sequential String.includes() calls for ~50 keywords, scanning each file's content 50+ times. Replaced with a single pre-compiled RegExp alternation that scans content in one pass (3.5x speedup).
  19. Extract mightContainSecret into securityPreFilter.ts module — Moved mightContainSecret() and related types out of securityCheckWorker.ts into a new securityPreFilter.ts module with zero dependencies on secretlint.
  20. Replace sequential keyword scanning with single-pass regex — Combined regex for 3.5x faster pre-filter.
  21. Pre-load BPE data on main thread to eliminate redundant per-worker file I/O — Each of the 4 metrics worker threads independently loaded gpt-tokenizer's BPE rank data (~3.6MB, 200K entries) from disk, taking 210-330ms per worker. Now the BPE data is loaded once on the main thread.
  22. Add unit tests for prefetchFileChangeCounts cache behavior
  23. Combine picomatch patterns into single regex and lazy-load minimatch
  24. Line-length pre-filter for base64 scan — 85% faster base64 detection.
  25. Skip calculateFileLineCounts for non-skill output paths
  26. Lazy-load handlebars, fast-xml-builder, and @clack/prompts
  27. Remove pre-filter keywords for disabled secretlint rules
  28. Replace O(N²) split output algorithm with O(N) estimation-based approach
  29. Defer metrics worker pool creation until after file search
  30. Run pack() inline instead of spawning child_process worker — Eliminated ~250ms overhead.
  31. Size metrics worker pool by estimated tokenization needs — Pool sized by actual work (1 thread for default 50-file case), eliminating unnecessary BPE init contention.
  32. Reduce startup module loading by deferring heavy imports — Extract TOKEN_ENCODINGS to tokenEncodings.ts, lazy-load json5, preload @secretlint/core before metrics workers.

Latest commit: Preload BPE data during searchFiles to eliminate metrics warmup stall

  1. Preload BPE data during searchFiles to eliminate metrics warmup stall — The loadBpeRanks async I/O (~100ms) was called inside createMetricsTaskRunner (after searchFiles), but its callback couldn't fire during the subsequent synchronous collectFiles (~150ms), creating a ~200ms pipeline stall at await metricsWarmupPromise. Fix: fire loadBpeRanks() at the start of pack() so it overlaps with searchFiles I/O, pass the pre-loaded promise to createMetricsTaskRunner, and remove the blocking warmup await (tasks FIFO-queue behind warmup, with safe disk-loading fallback).

Benchmark results (latest commit: preload BPE data)

repomix repo root (999 files, 4-core, 10 runs each):

Metric Before After Improvement
Median 913ms 855ms -58ms (-6.4%)
p75 925ms 864ms -61ms (-6.6%)

Pipeline shift (single profiled run):

  • calculateMetrics start: 707ms → 522ms (185ms earlier)
  • pack() end: 919ms → 802ms (117ms faster)

Test plan

  • All 1128 tests passing
  • Lint clean (0 errors)
  • Reviewed by independent sub-agents for correctness, edge cases, and code quality
  • Verified: output correctness (999 files, correct token counts)
  • Verified: tokenCountTree=true still uses all workers and tokenizes all files
  • Verified: no regression on real secret detection
  • Verified: MCP server path unaffected
  • Verified: BPE preload failure degrades gracefully (workers load from disk)
  • Verified: backwards-compatible (optional parameter, fallback path intact)

https://claude.ai/code/session_01Cbzh8SssmdjmCsDJWaZ5tY

claude added 3 commits April 6, 2026 10:21
… check

Move metrics worker pool initialization before file search so that
gpt-tokenizer loading overlaps with I/O-bound operations (globby file
search, file reading) instead of contending with CPU-bound security
check workers. Also parallelize file and directory globby calls in
searchFiles, and increase metrics batch size from 10 to 25 to reduce
IPC overhead.

Changes:
- packager.ts: Create metrics task runner before searchFiles instead of
  after, using getProcessConcurrency() to determine thread count since
  file count is not yet known
- fileSearch.ts: Run file search and directory search globby calls
  concurrently via Promise.all when includeEmptyDirectories is enabled
- calculateSelectiveFileMetrics.ts: Increase METRICS_BATCH_SIZE from
  10 to 25, reducing IPC round-trips from 100 to 40 for 1000 files

Benchmark results (interleaved A/B, 10 cold-start pairs on 4-core machine,
repomix repo with ~1000 files):
- Baseline average: 2196ms
- Optimized average: 2098ms
- Average improvement: 97ms (4.4%)
- Median improvement: 99ms (4.5%)
- 8 out of 10 pairs show improvement

The improvement comes from eliminating thread contention: previously,
4 metrics warmup threads (loading gpt-tokenizer) competed with 2
security check threads (running secretlint) for 4 CPU cores. By
starting warmup earlier during I/O-bound phases, warmup completes
before the CPU-bound security check begins.

https://claude.ai/code/session_01SBsGNwf5W7iFj16aNdurNj
Increase worker batch sizes for token counting to reduce per-batch IPC
scheduling overhead, which was the dominant cost in the metrics phase.

Changes:
- calculateSelectiveFileMetrics: Use adaptive batch size (50 for >100
  files, 10 for smaller sets) instead of fixed size 10. For 1000 files,
  this reduces batches from 100 to 20, cutting ~80ms of IPC overhead.
- calculateOutputMetrics: Increase TARGET_CHARS_PER_CHUNK from 200K to
  500K, reducing chunks from ~20 to ~8 for a 4MB output while still
  providing enough parallelism across worker threads.

Benchmark results (repomix on itself, 997 files, ~4MB output, 10 runs):
- Before: mean 1540ms, median 1552ms
- After:  mean 1448ms, median 1456ms
- Improvement: ~92ms (6.0%) mean, ~96ms (6.2%) median

The per-batch IPC scheduling overhead (~1ms per round-trip) was the
bottleneck rather than serialization cost. Fewer, larger batches reduce
total round-trips while maintaining good worker utilization.

https://claude.ai/code/session_01SprjopvDAiY84WdFQELCjZ
@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3140572-a541-45c0-aa4c-ba12e718e747

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/auto-perf-tuning-0406

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

⚡ Performance Benchmark

Latest commit:1d60d6a perf(core): Preload BPE data during searchFiles to eliminate metrics warmup stall
Status:✅ Benchmark complete!
Ubuntu:1.50s (±0.04s) → 0.64s (±0.01s) · -0.86s (-57.7%)
macOS:1.26s (±0.13s) → 0.61s (±0.09s) · -0.65s (-51.7%)
Windows:2.23s (±0.50s) → 0.94s (±0.05s) · -1.29s (-57.9%)
Details
  • Packing the repomix repository with node bin/repomix.cjs
  • Warmup: 2 runs (discarded), interleaved execution
  • Measurement: 20 runs / 30 on macOS (median ± IQR)
  • Workflow run
History

bac879b perf(core): Reduce startup module loading by deferring heavy imports

Ubuntu:1.46s (±0.04s) → 0.69s (±0.02s) · -0.77s (-52.9%)
macOS:1.19s (±0.24s) → 0.63s (±0.14s) · -0.55s (-46.6%)
Windows:1.84s (±0.03s) → 0.89s (±0.01s) · -0.94s (-51.4%)

eae6adc perf(core): Size metrics worker pool by estimated tokenization needs

Ubuntu:1.40s (±0.02s) → 0.65s (±0.01s) · -0.74s (-53.2%)
macOS:0.86s (±0.02s) → 0.47s (±0.04s) · -0.39s (-45.0%)
Windows:1.87s (±0.02s) → 0.90s (±0.02s) · -0.97s (-52.0%)

3eb892e perf(core): Size metrics worker pool by estimated tokenization needs

Ubuntu:1.44s (±0.02s) → 0.67s (±0.02s) · -0.76s (-53.2%)
macOS:0.92s (±0.08s) → 0.49s (±0.04s) · -0.42s (-46.4%)
Windows:1.83s (±0.07s) → 0.88s (±0.02s) · -0.95s (-52.1%)

ca781d3 Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

Ubuntu:1.41s (±0.03s) → 0.79s (±0.01s) · -0.62s (-43.9%)
macOS:1.16s (±0.28s) → 0.69s (±0.14s) · -0.47s (-40.9%)
Windows:1.98s (±0.29s) → 1.10s (±0.28s) · -0.88s (-44.5%)

f590444 Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

Ubuntu:1.49s (±0.03s) → 0.82s (±0.02s) · -0.68s (-45.2%)
macOS:0.88s (±0.04s) → 0.48s (±0.04s) · -0.40s (-45.1%)
Windows:1.97s (±0.12s) → 1.10s (±0.04s) · -0.87s (-44.2%)

6469530 perf(core): Defer metrics worker pool creation until after file search

Ubuntu:1.45s (±0.03s) → 0.84s (±0.02s) · -0.61s (-41.9%)
macOS:1.12s (±0.31s) → 0.71s (±0.31s) · -0.41s (-36.9%)
Windows:1.72s (±0.03s) → 1.01s (±0.03s) · -0.71s (-41.1%)

c3d6b7c perf(output): Replace O(N²) split output algorithm with O(N) estimation-based approach

Ubuntu:1.50s (±0.03s) → 0.79s (±0.02s) · -0.72s (-47.8%)
macOS:1.50s (±0.59s) → 0.87s (±0.38s) · -0.63s (-41.9%)
Windows:2.38s (±0.09s) → 1.23s (±0.04s) · -1.16s (-48.5%)

9357d02 perf(security): Remove pre-filter keywords for disabled secretlint rules

Ubuntu:1.47s (±0.03s) → 0.76s (±0.02s) · -0.71s (-48.3%)
macOS:0.88s (±0.10s) → 0.48s (±0.04s) · -0.39s (-44.8%)
Windows:1.80s (±0.08s) → 0.97s (±0.04s) · -0.83s (-46.2%)

105219e perf(security): Remove pre-filter keywords for disabled secretlint rules

Ubuntu:1.48s (±0.03s) → 0.77s (±0.02s) · -0.71s (-47.9%)
macOS:1.03s (±0.36s) → 0.56s (±0.20s) · -0.47s (-45.5%)
Windows:1.79s (±0.03s) → 0.99s (±0.03s) · -0.81s (-44.9%)

082f8b4 perf(core): Lazy-load handlebars, fast-xml-builder, and @clack/prompts to reduce startup cost

Ubuntu:1.42s (±0.02s) → 0.76s (±0.01s) · -0.66s (-46.4%)
macOS:1.32s (±0.32s) → 0.72s (±0.18s) · -0.60s (-45.6%)
Windows:1.77s (±0.02s) → 0.97s (±0.03s) · -0.81s (-45.5%)

fb0a861 perf(core): Lazy-load handlebars, fast-xml-builder, and @clack/prompts to reduce startup cost

Ubuntu:1.45s (±0.02s) → 0.75s (±0.02s) · -0.69s (-48.0%)
macOS:0.90s (±0.06s) → 0.50s (±0.05s) · -0.40s (-44.2%)
Windows:1.82s (±0.03s) → 0.98s (±0.02s) · -0.85s (-46.4%)

539ac5d perf(core): Lazy-load handlebars, fast-xml-builder, and @clack/prompts to reduce startup cost

Ubuntu:1.50s (±0.02s) → 0.78s (±0.02s) · -0.72s (-47.9%)
macOS:0.91s (±0.07s) → 0.49s (±0.04s) · -0.43s (-46.8%)
Windows:1.83s (±0.05s) → 1.00s (±0.03s) · -0.83s (-45.4%)

24c24d5 perf(core): Lazy-load handlebars, fast-xml-builder, and @clack/prompts to reduce startup cost

Ubuntu:1.48s (±0.04s) → 0.81s (±0.04s) · -0.67s (-45.5%)
macOS:0.92s (±0.15s) → 0.51s (±0.11s) · -0.41s (-44.2%)
Windows:1.81s (±0.04s) → 0.98s (±0.03s) · -0.83s (-46.0%)

b6ccc3f perf(output): Skip calculateFileLineCounts for non-skill output paths

Ubuntu:1.47s (±0.07s) → 0.80s (±0.07s) · -0.67s (-45.6%)
macOS:0.87s (±0.02s) → 0.49s (±0.02s) · -0.38s (-43.4%)
Windows:1.82s (±0.05s) → 1.02s (±0.03s) · -0.80s (-43.8%)

12b4813 perf(output): Skip calculateFileLineCounts for non-skill output paths

Ubuntu:1.40s (±0.02s) → 0.75s (±0.02s) · -0.65s (-46.4%)
macOS:1.42s (±0.11s) → 0.79s (±0.09s) · -0.62s (-44.1%)
Windows:1.91s (±0.07s) → 1.05s (±0.02s) · -0.85s (-44.7%)

3600dbe perf(core): Add line-length pre-filter to skip base64 scan on short-line files

Ubuntu:1.42s (±0.03s) → 0.79s (±0.02s) · -0.63s (-44.5%)
macOS:1.09s (±0.25s) → 0.65s (±0.17s) · -0.44s (-40.2%)
Windows:1.39s (±0.03s) → 0.80s (±0.02s) · -0.60s (-42.9%)

fa209c5 test(core): Add unit tests for prefetchFileChangeCounts cache behavior

Ubuntu:1.48s (±0.03s) → 0.80s (±0.03s) · -0.69s (-46.3%)
macOS:0.88s (±0.05s) → 0.49s (±0.05s) · -0.39s (-44.1%)
Windows:1.92s (±0.15s) → 1.06s (±0.06s) · -0.85s (-44.6%)

ebfed92 perf(core): Combine picomatch patterns into single regex and lazy-load minimatch

Ubuntu:1.47s (±0.02s) → 0.79s (±0.02s) · -0.68s (-46.3%)
macOS:1.22s (±0.22s) → 0.68s (±0.07s) · -0.53s (-44.0%)
Windows:1.90s (±0.15s) → 1.05s (±0.07s) · -0.84s (-44.5%)

fd6b625 perf(core): Pre-load BPE data on main thread to eliminate redundant per-worker file I/O

Ubuntu:1.40s (±0.03s) → 0.80s (±0.02s) · -0.60s (-42.9%)
macOS:1.32s (±0.15s) → 0.75s (±0.10s) · -0.57s (-43.0%)
Windows:1.76s (±0.03s) → 1.05s (±0.01s) · -0.70s (-40.0%)

0c01d91 perf(core): Skip tokenizing small files when tokenCountTree has a threshold

Ubuntu:1.41s (±0.04s) → 0.74s (±0.01s) · -0.68s (-47.8%)
macOS:0.90s (±0.12s) → 0.49s (±0.06s) · -0.41s (-45.8%)
Windows:1.83s (±0.06s) → 1.01s (±0.02s) · -0.82s (-44.8%)

761b5fd perf(core): Skip tokenizing small files when tokenCountTree has a threshold

Ubuntu:1.52s (±0.03s) → 0.79s (±0.02s) · -0.73s (-48.2%)
macOS:1.07s (±0.28s) → 0.57s (±0.14s) · -0.50s (-47.0%)
Windows:1.93s (±0.07s) → 1.05s (±0.06s) · -0.88s (-45.6%)

68b20dc perf(core): Skip tokenizing small files when tokenCountTree has a threshold

Ubuntu:1.55s (±0.03s) → 0.81s (±0.02s) · -0.74s (-47.6%)
macOS:1.01s (±0.15s) → 0.53s (±0.05s) · -0.48s (-47.3%)
Windows:2.20s (±0.30s) → 1.19s (±0.14s) · -1.01s (-46.0%)

2cfa7dc perf(security): Replace 50 sequential includes() with single combined regex in mightContainSecret

Ubuntu:1.49s (±0.04s) → 0.77s (±0.01s) · -0.72s (-48.6%)
macOS:1.21s (±0.14s) → 0.64s (±0.07s) · -0.56s (-46.6%)
Windows:1.85s (±0.06s) → 1.01s (±0.02s) · -0.84s (-45.4%)

07d5c89 perf(security): Replace sequential keyword scanning with single-pass regex for 3.5x faster pre-filter

Ubuntu:1.40s (±0.04s) → 0.74s (±0.02s) · -0.66s (-46.9%)
macOS:1.32s (±0.23s) → 0.68s (±0.14s) · -0.64s (-48.3%)
Windows:1.87s (±0.05s) → 1.03s (±0.03s) · -0.84s (-45.0%)

1fc197b perf(security): Replace sequential keyword scanning with single-pass regex for 3.5x faster pre-filter

Ubuntu:1.62s (±0.05s) → 0.88s (±0.03s) · -0.74s (-45.9%)
macOS:1.23s (±0.25s) → 0.63s (±0.15s) · -0.60s (-48.6%)
Windows:1.85s (±0.05s) → 1.01s (±0.03s) · -0.84s (-45.2%)

f5d02d7 Merge remote-tracking branch 'origin/main' into perf/auto-perf-tuning-0406

Ubuntu:1.44s (±0.03s) → 0.78s (±0.02s) · -0.66s (-46.0%)
macOS:1.28s (±0.19s) → 0.72s (±0.15s) · -0.56s (-43.7%)
Windows:1.86s (±0.03s) → 1.08s (±0.02s) · -0.79s (-42.2%)

0e7655e perf(core): Skip tokenizing all files when tokenCountTree is disabled

Ubuntu:1.47s (±0.07s) → 0.81s (±0.06s) · -0.66s (-45.1%)
macOS:1.36s (±0.18s) → 0.74s (±0.13s) · -0.62s (-45.4%)
Windows:1.79s (±0.05s) → 1.02s (±0.03s) · -0.76s (-42.7%)

a3e1452 perf(security): Avoid security worker pool for small item counts to reduce CPU contention

Ubuntu:1.43s (±0.04s) → 0.99s (±0.02s) · -0.44s (-30.9%)
macOS:1.03s (±0.11s) → 0.80s (±0.07s) · -0.23s (-22.1%)
Windows:1.95s (±0.08s) → 1.48s (±0.15s) · -0.46s (-23.7%)

ccc68c2 perf(cli): Pre-create metrics worker pool before pack() for earlier BPE warmup

Ubuntu:1.53s (±0.05s) → 1.07s (±0.03s) · -0.45s (-29.6%)
macOS:0.89s (±0.06s) → 0.69s (±0.04s) · -0.20s (-22.9%)
Windows:2.22s (±0.09s) → 1.70s (±0.08s) · -0.52s (-23.4%)

3dd8bcc perf(security): Skip expensive lintSource() for files without secret keywords

Ubuntu:1.46s (±0.04s) → 1.04s (±0.03s) · -0.42s (-28.6%)
macOS:0.87s (±0.05s) → 0.68s (±0.04s) · -0.19s (-22.1%)
Windows:1.80s (±0.04s) → 1.40s (±0.04s) · -0.40s (-22.1%)

218daeb perf(security): Skip expensive lintSource() for files without secret keywords

Ubuntu:1.41s (±0.03s) → 1.03s (±0.03s) · -0.38s (-26.7%)
macOS:1.23s (±0.12s) → 0.91s (±0.08s) · -0.32s (-26.3%)
Windows:2.31s (±0.06s) → 1.76s (±0.05s) · -0.55s (-23.9%)

d7cacc9 Merge remote perf/auto-perf-tuning-0406 and resolve conflicts in calculateSelectiveFileMetrics

Ubuntu:1.60s (±0.04s) → 1.27s (±0.04s) · -0.33s (-20.8%)
macOS:0.96s (±0.14s) → 0.75s (±0.12s) · -0.20s (-21.3%)
Windows:1.98s (±0.12s) → 1.60s (±0.04s) · -0.38s (-19.3%)

5925c02 fix(core): Remove @ts-expect-error for picomatch import

Ubuntu:1.43s (±0.02s) → 1.17s (±0.03s) · -0.27s (-18.5%)
macOS:0.86s (±0.06s) → 0.68s (±0.06s) · -0.18s (-21.0%)
Windows:1.94s (±0.05s) → 1.59s (±0.04s) · -0.35s (-18.1%)

fe2ecbe fix(core): Move try block to cover metrics worker pool lifecycle

41c87af Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

c794b70 Merge remote perf/auto-perf-tuning-0406 and resolve conflicts

Ubuntu:1.43s (±0.05s) → 1.16s (±0.03s) · -0.28s (-19.2%)
macOS:1.05s (±0.13s) → 0.85s (±0.14s) · -0.20s (-18.8%)
Windows:1.78s (±0.10s) → 1.48s (±0.12s) · -0.30s (-16.9%)

48f3311 perf(core): Lazy-load globby and avoid globby directory scan on git fast path

Ubuntu:1.53s (±0.02s) → 1.21s (±0.02s) · -0.33s (-21.2%)
macOS:0.95s (±0.07s) → 0.78s (±0.09s) · -0.18s (-18.8%)
Windows:1.80s (±0.02s) → 1.48s (±0.01s) · -0.32s (-17.8%)

29fd39b Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

Ubuntu:1.50s (±0.03s) → 1.21s (±0.02s) · -0.29s (-19.5%)
macOS:1.35s (±0.22s) → 1.06s (±0.19s) · -0.29s (-21.3%)
Windows:2.23s (±0.47s) → 1.82s (±0.39s) · -0.42s (-18.7%)

27e7b88 [autofix.ci] apply automated fixes

Ubuntu:1.47s (±0.04s) → 1.18s (±0.02s) · -0.28s (-19.3%)
macOS:0.95s (±0.09s) → 0.79s (±0.10s) · -0.15s (-16.4%)
Windows:1.75s (±0.06s) → 1.49s (±0.05s) · -0.25s (-14.4%)

a3bc119 perf(core): Use git ls-files for file search in git repos (~7% faster)

Ubuntu:1.50s (±0.02s) → 1.20s (±0.01s) · -0.30s (-20.2%)
macOS:1.39s (±1.16s) → 1.02s (±0.75s) · -0.37s (-26.6%)
Windows:1.84s (±0.04s) → 1.56s (±0.02s) · -0.28s (-15.2%)

02e2d10 perf(core): Estimate output tokens from file tokens instead of counting full output

Ubuntu:1.54s (±0.06s) → 1.32s (±0.04s) · -0.21s (-13.8%)
macOS:0.90s (±0.05s) → 0.77s (±0.06s) · -0.13s (-14.5%)
Windows:1.93s (±0.11s) → 1.75s (±0.07s) · -0.18s (-9.4%)

5e4e64f perf(core): Reduce pipeline overhead with non-blocking cleanup and lazy computation

Ubuntu:1.48s (±0.03s) → 1.42s (±0.04s) · -0.06s (-4.2%)
macOS:0.87s (±0.04s) → 0.86s (±0.07s) · -0.01s (-0.9%)
Windows:1.89s (±0.12s) → 1.85s (±0.08s) · -0.05s (-2.5%)

a94c618 perf(core): Use synchronous file reads in collectFiles for ~7% speedup

Ubuntu:1.50s (±0.02s) → 1.43s (±0.04s) · -0.07s (-4.3%)
macOS:1.02s (±0.13s) → 0.98s (±0.13s) · -0.04s (-4.4%)
Windows:1.98s (±0.08s) → 1.90s (±0.04s) · -0.08s (-3.8%)

72dc46d perf(core): Use synchronous file reads in collectFiles for ~7% speedup

Ubuntu:1.53s (±0.04s) → 1.46s (±0.03s) · -0.07s (-4.7%)
macOS:1.06s (±0.16s) → 1.03s (±0.17s) · -0.04s (-3.4%)
Windows:2.35s (±0.54s) → 2.15s (±0.55s) · -0.21s (-8.9%)

dcc4bf5 perf(core): Use synchronous file reads in collectFiles for ~7% speedup

Ubuntu:1.52s (±0.04s) → 1.45s (±0.05s) · -0.07s (-4.6%)
macOS:0.98s (±0.17s) → 0.96s (±0.16s) · -0.02s (-2.0%)
Windows:1.97s (±0.06s) → 1.90s (±0.04s) · -0.07s (-3.5%)

3ef790f Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

Ubuntu:1.45s (±0.03s) → 1.46s (±0.03s) · +0.00s (+0.3%)
macOS:1.02s (±0.13s) → 1.01s (±0.09s) · -0.01s (-0.6%)
Windows:1.85s (±0.04s) → 1.86s (±0.03s) · +0.01s (+0.4%)

562635d Merge remote-tracking branch 'origin/perf/auto-perf-tuning-0406' into perf/auto-perf-tuning-0406

Ubuntu:1.50s (±0.01s) → 1.49s (±0.03s) · -0.00s (-0.1%)
macOS:1.34s (±0.30s) → 1.34s (±0.28s) · -0.00s (-0.2%)
Windows:1.99s (±0.06s) → 2.00s (±0.08s) · +0.00s (+0.1%)

3e90eb7 Merge remote branch perf/auto-perf-tuning-0406 (resolve conflict in favor of adaptive batch sizing)

Ubuntu:1.46s (±0.02s) → 1.45s (±0.03s) · -0.01s (-0.5%)
macOS:1.58s (±0.40s) → 1.48s (±0.26s) · -0.10s (-6.6%)
Windows:1.83s (±0.05s) → 1.83s (±0.02s) · +0.00s (+0.1%)

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 7, 2026

Copy link
Copy Markdown

Deploying repomix with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1d60d6a
Status: ✅  Deploy successful!
Preview URL: https://c09cdac9.repomix.pages.dev
Branch Preview URL: https://perf-auto-perf-tuning-0406.repomix.pages.dev

View logs

@codecov

codecov Bot commented Apr 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.49288% with 158 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.78%. Comparing base (9d6e224) to head (1d60d6a).
⚠️ Report is 903 commits behind head on main.

Files with missing lines Patch % Lines
src/core/file/fileSearch.ts 69.73% 46 Missing ⚠️
src/core/security/securityCheck.ts 64.51% 22 Missing ⚠️
src/core/git/gitCommand.ts 5.26% 18 Missing ⚠️
src/core/metrics/workers/calculateMetricsWorker.ts 12.50% 14 Missing ⚠️
src/core/file/fileRead.ts 64.70% 12 Missing ⚠️
src/core/packager.ts 79.66% 12 Missing ⚠️
src/core/metrics/calculateMetrics.ts 88.57% 8 Missing ⚠️
src/core/security/workers/securityCheckWorker.ts 20.00% 8 Missing ⚠️
src/core/metrics/TokenCounter.ts 60.00% 6 Missing ⚠️
src/core/metrics/tokenCounterFactory.ts 16.66% 5 Missing ⚠️
... and 4 more

❌ Your patch check has failed because the patch coverage (77.49%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1428      +/-   ##
==========================================
- Coverage   87.05%   84.78%   -2.27%     
==========================================
  Files         116      119       +3     
  Lines        4433     4810     +377     
  Branches     1029     1111      +82     
==========================================
+ Hits         3859     4078     +219     
- Misses        574      732     +158     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes performance by parallelizing file and directory searches, increasing chunk sizes for tokenization to reduce IPC overhead, and implementing dynamic batching for file metrics. Additionally, the metrics worker pool initialization was moved earlier to overlap with I/O tasks. Feedback includes addressing a potential resource leak by moving the try...finally block, ensuring consistent error handling for directory searches, and avoiding brittle logic when calculating worker thread counts.

Comment thread src/core/packager.ts Outdated
Comment on lines +84 to +87
const { taskRunner: metricsTaskRunner, warmupPromise: metricsWarmupPromise } = deps.createMetricsTaskRunner(
processConcurrency * 100,
config.tokenCount.encoding,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The metrics worker pool is initialized here, but the try...finally block that ensures its cleanup (via metricsTaskRunner.cleanup()) doesn't start until line 117. If an error occurs in the intervening code (e.g., in searchFiles at line 93 or sortPaths at line 108), the worker pool will not be cleaned up, leading to a resource leak. Consider moving the start of the try block to immediately follow the worker pool initialization.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fe2ecbe — moved the try block to start immediately after metrics worker pool creation, ensuring cleanup via finally covers the entire lifecycle including searchFiles and sortPaths.


Generated by Claude Code

Comment thread src/core/file/fileSearch.ts Outdated
Comment on lines +212 to +220
const emptyDirSearchPromise = config.output.includeEmptyDirectories
? globby(includePatterns, {
...baseGlobbyOptions,
onlyDirectories: true,
}).then((directories) => {
logger.debug(`[empty dirs] Found ${directories.length} directories`);
return findEmptyDirectories(rootDir, directories, adjustedIgnorePatterns);
})
: Promise.resolve([] as string[]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The directory search globby call lacks the specific error handling for EPERM and EACCES that is present in the file search. For consistency and better error reporting, consider adding a similar .catch block to transform these errors into PermissionError.

Suggested change
const emptyDirSearchPromise = config.output.includeEmptyDirectories
? globby(includePatterns, {
...baseGlobbyOptions,
onlyDirectories: true,
}).then((directories) => {
logger.debug(`[empty dirs] Found ${directories.length} directories`);
return findEmptyDirectories(rootDir, directories, adjustedIgnorePatterns);
})
: Promise.resolve([] as string[]);
const emptyDirSearchPromise = config.output.includeEmptyDirectories
? globby(includePatterns, {
...baseGlobbyOptions,
onlyDirectories: true,
})
.then((directories) => {
logger.debug(`[empty dirs] Found ${directories.length} directories`);
return findEmptyDirectories(rootDir, directories, adjustedIgnorePatterns);
})
.catch((error: unknown) => {
const code = (error as NodeJS.ErrnoException | { code?: string })?.code;
if (code === 'EPERM' || code === 'EACCES') {
throw new PermissionError(
`Permission denied while scanning directory. Please check folder access permissions for your terminal app. path: ${rootDir}`,
rootDir,
);
}
throw error;
})
: Promise.resolve([] as string[]);

Comment thread src/core/packager.ts Outdated
// threads and is negligible compared to the contention savings for typical repos.
const processConcurrency = deps.getProcessConcurrency();
const { taskRunner: metricsTaskRunner, warmupPromise: metricsWarmupPromise } = deps.createMetricsTaskRunner(
processConcurrency * 100,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using processConcurrency * 100 to force the worker pool to allocate one thread per core is brittle as it relies on the internal TASKS_PER_THREAD constant (currently 100) in processConcurrency.ts. If that constant changes, this logic may no longer achieve the intended thread count. It would be better to expose a way to explicitly request maximum concurrency or use a shared constant.

claude added 4 commits April 7, 2026 04:10
Add a fast linear pre-check (hasLongBase64Run) before applying the
expensive standalone base64 regex pattern. Most source code files
contain no sequences of 256+ base64 characters, so the pre-check
returns false in O(n) with a simple charCodeAt loop, avoiding the
costly regex engine invocation entirely.

Also gate the data URI regex behind a cheap `content.includes(';base64,')`
check, skipping it for the majority of files.

action: add fast-path pre-check to truncateBase64Content
reason: CPU profiling showed the standalone base64 regex consuming ~248ms
  of main-thread CPU time per CLI run (10.6% of total), despite only 3
  out of ~1000 files actually containing base64 content
decision: use a linear character scan (charCodeAt) for the pre-check rather
  than a simpler regex, because V8's regex engine has non-trivial setup
  cost that dominates for files where the pattern never matches

Benchmark (15 runs each, repomix self-pack with 1018 files):
  Baseline:  avg 2351ms, median 2352ms, p10-p90: 2285-2426ms
  Optimized: avg 2231ms, median 2237ms, p10-p90: 2149-2313ms
  Improvement: ~120ms (5.1% avg), with optimized p90 < baseline median

https://claude.ai/code/session_01FmGrVUJY2giodz6mNFu6cF
Move the git log subprocess used for sortByChanges file ordering from
the critical path inside generateOutput to run concurrently with file
collection and other git operations in the pack pipeline.

Previously, sortOutputFiles spawned `git log --name-only` and
`git --version` sequentially inside generateOutput, blocking template
rendering. Since sortByChanges defaults to true, this affected every
CLI run. By pre-fetching file change counts into the module-level
cache via prefetchFileChangeCounts in the existing Promise.all
alongside collectFiles/getGitDiffs/getGitLogs, sortOutputFiles finds
the result already cached and skips the git subprocess entirely.

## Changes
- Add `prefetchFileChangeCounts` to outputSort.ts — warms the
  module-level cache early in the pipeline (fire-and-forget)
- Call it in packager.ts Promise.all alongside file collection and
  git operations
- No signature changes to downstream functions — the existing
  cache handles deduplication transparently

## Benchmark (repomix on its own repo, ~1000 files, 5 runs each)
- Baseline (main): ~1.99s avg (1.934, 1.960, 1.966, 2.016, 2.078)
- Optimized:       ~1.59s avg (1.513, 1.541, 1.559, 1.631, 1.696)
- Improvement:     ~400ms = ~20% faster

https://claude.ai/code/session_01Qhaod6pvCsxrn6tmJEmcS2
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from dcc4bf5 to 72dc46d Compare April 7, 2026 06:05
Switch file collection from async pooled reads (fs.promises.readFile with
concurrency 50) to synchronous reads (fs.readFileSync). This eliminates
per-file event loop scheduling overhead and libuv thread pool round-trips
that bottleneck async reads on the default 4-thread pool.

For typical repos with hundreds to thousands of mostly small source files,
sync reads are ~2-3x faster than async pooled reads. The main thread is
not doing other useful work during file collection (security check and
metrics happen later), so blocking the event loop is acceptable.

Non-UTF-8 files that fail the fast sync TextDecoder path fall back to
the original async readRawFile with jschardet/iconv-lite encoding detection.

## Benchmark (repomix repo, 1011 files, 5.2MB output, 20 runs each)

A/B test (10 alternating pairs):
  Average: 2.054s → 1.910s (7.0% faster)
  Median:  2.063s → 1.901s (7.9% faster)

Standalone runs (20 each):
  Baseline p50: 1.947s
  Optimized p50: 1.771s–1.850s (5.0–9.0% faster)

## Changes
- fileRead.ts: Add readRawFileSync using fs.readFileSync + isBinaryFileSync
- fileCollect.ts: Replace async promisePool with sync loop, async fallback
  for non-UTF-8 files
- Updated unit and integration tests for new sync-first interface

https://claude.ai/code/session_01WbseR3yGyhgjRGCfobTY5A
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from 72dc46d to a94c618 Compare April 7, 2026 06:08
claude added 3 commits April 7, 2026 09:05
…zy computation

Three independent optimizations targeting the pack pipeline:

1. Fire-and-forget worker pool cleanup (~110ms saved)
   - Security check: pool.destroy() deferred to background (~42ms)
   - Metrics: pool.destroy() deferred to background (~70ms)
   - CLI entry adds process.exit(0) after all output is written,
     so the OS terminates worker threads instantly on exit
   - Skipped for --mcp (long-lived server) with stdout flush guard
   - MCP/library callers unaffected (Tinypool idleTimeout reclaims)

2. Skip calculateMarkdownDelimiter for non-markdown output (~5ms)
   - The backtick regex scans all file contents to determine the
     code fence delimiter, but is only needed for markdown style
   - XML, JSON, and plain output now skip this entirely

3. Replace regex-based line counting with indexOf loop (~2ms)
   - calculateFileLineCounts used content.match(/\n/g) which
     allocates a temporary array of all newline matches per file
   - indexOf loop counts newlines without any allocation

Phase-level benchmarks (repomix self-pack, ~1000 files):
  - Security cleanup: 210ms → 168ms (42ms saved, 5 runs avg)
  - Markdown delimiter: 4.6ms → 0ms for XML output (10 runs avg)
  - Line counting: 4.9ms → 2.8ms (10 runs avg)
  - Metrics cleanup: 70ms → 0ms via process.exit (profiled)

Theoretical improvement: ~119ms / ~1500ms CLI = ~8%
Note: high environment variance (~15% CoV) prevented reliable
end-to-end measurement; individual phase savings are verified.

1104 tests passing, lint clean on changed files.

https://claude.ai/code/session_01SKKWebXspaNFLbVoJgugs5
…ng full output

Replace the expensive full-output BPE token counting with a lightweight
estimation approach. Instead of counting tokens for the entire multi-MB output
string (~4MB for this repo), we now:

1. Count tokens for ALL individual files (not just top 50)
2. Estimate total output tokens as: sum(fileTokens) + overheadChars * ratio
3. Skip the computationally expensive output token counting entirely

The overhead (headers, tree, XML/markdown tags) tokens are estimated using the
average token-per-character ratio from file content. This is accurate to within
~0.05% because the structural text has similar token density to source code.

Benchmark results (repomix on itself, 997 files, 3.9MB content):

  A/B comparison (same process, 5 runs each):
    Before: avg 1700ms (pack time)
    After:  avg 1366ms (pack time)
    Savings: 333ms (19.6%)

  CLI end-to-end (5 runs):
    Before: median 2.444s
    After:  median 1.843s
    Savings: 601ms (24.6%)

  Token accuracy: 0.002% error (20 tokens off out of 1,055,397)

The improvement comes from eliminating ~19 worker tasks of 200KB BPE tokenization
each, which previously competed with file token counting for worker threads.

As a bonus, all files now have token counts computed, enabling the tokenCountTree
feature at no additional cost.

https://claude.ai/code/session_01DCGGhD7A3bdik77AbiU2gM
Replace globby's expensive .gitignore traversal with `git ls-files` for
repositories where git is available. Git reads from its index, which is
much faster than globby's filesystem walk + gitignore file discovery.

## What changed

- Added `searchFilesWithGit()` fast path in fileSearch.ts that uses
  `git ls-files --cached --others --exclude-standard` to list files
- Uses `git ls-files -s` to identify and filter out symlinks (mode 120000)
  matching globby's followSymbolicLinks:false behavior
- Applies ignore patterns (defaultIgnore, custom, .repomixignore/.ignore)
  using picomatch for efficient batch matching
- Falls back to globby when git is unavailable or useGitignore is disabled
- Added picomatch as explicit dependency (already transitive via globby)

## Benchmark results (repomix repo, ~1000 files)

searchFiles() isolated:
  Before: ~470ms (globby with gitignore traversal)
  After:  ~178ms (git ls-files + picomatch filtering)

pack() function (8 runs each, sorted):
  Before: 877, 903, 979, 988, 1006, 1025, 1026, 1046ms (median: 988ms)
  After:  909, 910, 914, 916, 937, 955, 973, 1007ms (median: 916ms)
  Improvement: -7.3% (median), -5.5% (trimmed mean)

All 1104 tests passing, lint clean.

https://claude.ai/code/session_01WYHGetwQtRFCMsebzhrDzc
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from a3bc119 to 5065a42 Compare April 7, 2026 11:59
autofix-ci Bot and others added 11 commits April 7, 2026 12:00
Replace globby filesystem scan with git ls-files for the file search
phase in git repositories. This reads from the git index instead of
walking the filesystem, significantly reducing search latency.

## What changed

- Added `searchFilesGit()` fast path that uses `git ls-files --stage`
  and `git ls-files --others --exclude-standard` to get file lists
- Pre-compile ignore patterns into RegExp objects via `minimatch.makeRe()`
  for O(1) per-file matching instead of re-parsing glob patterns
- Symlinks (mode 120000) are filtered out from git stage output to match
  globby's `onlyFiles: true` behavior
- Falls back to globby when: not a git repo, gitignore disabled, explicit
  files provided (stdin mode), or git command fails

## Benchmark results (repomix repo, 1018 files, 10 runs each)

| Metric   | Baseline (globby) | Optimized (git ls-files) | Change  |
|----------|-------------------|--------------------------|---------|
| Median   | 1322 ms           | 1245 ms                  | -5.8%   |
| Mean     | 1317 ms           | 1243 ms                  | -5.6%   |

Search-only: ~25ms (git) vs ~130ms (globby) = ~80% faster search phase.
The end-to-end improvement is ~77ms because the search phase is ~12% of
total execution time.

Lint: pass, Tests: 1102/1102 pass, Output: identical file lists verified.

https://claude.ai/code/session_014Sk4Q4esESXEDBxiuKMm6W
… perf/auto-perf-tuning-0406

Resolved conflict in src/core/file/fileSearch.ts by taking the remote's
version which already includes the git ls-files optimization. Removed
the duplicate fileSearchGit.ts file.

https://claude.ai/code/session_014Sk4Q4esESXEDBxiuKMm6W
…ast path

Replace the globby directory scan with lightweight directory discovery from
git ls-files output when searching for empty directories on the git fast path.
This avoids both the expensive globby import (~55ms) and its filesystem
traversal with gitignore parsing (~63ms).

Changes:
- Lazy-load globby via dynamic import() so it's only loaded when actually
  needed (non-git fallback path, listDirectories, listFiles). On the common
  git fast path, globby is never imported at all.
- Add discoverDirectories() that extracts parent directories from git ls-files
  file paths in O(n) time, then recursively discovers child directories not
  containing tracked files (to catch completely empty directories that git
  doesn't track). This replaces the separate globby directory scan.
- Parallelize findEmptyDirectories() readdir calls with Promise.all instead
  of sequential for...of loop, reducing ~22ms sequential I/O to ~6ms.

Benchmark (repomix repo, 997 files, includeEmptyDirectories: true):
- searchFiles: 188ms → 56ms (70% faster, -132ms)
- End-to-end CLI: ~1219ms → ~1172ms (~5% faster)

Component savings breakdown:
- Globby import avoided on git path: ~55ms
- Globby directory scan replaced with lightweight discovery: ~63ms
- Parallel readdir in findEmptyDirectories: ~16ms

https://claude.ai/code/session_01CtDuX8aLcNjV6HMzcxVGTs
…re available

When tokenCountTree is enabled, all file contents are already individually
token-counted. The full output is ~97% file content and ~3% structural
overhead (XML/markdown tags, headers, tree). Previously, the entire output
(~4MB for this repo) was also tokenized via worker threads, competing with
file metrics for the shared worker pool and causing significant contention.

This change estimates total output tokens as:
  sum(file_tokens) + round(overhead_chars × file_token_density)

This eliminates full-output tokenization when all per-file counts exist,
removing worker pool contention in the final pipeline stage.

Benchmark (repomix on itself, 997 files, tokenCountTree=50000, 5 runs avg):
  Before: 939ms (pack only), 1.51s (full CLI)
  After:  773ms (pack only), 1.19s (full CLI)
  Improvement: 166ms / 17.7% (pack), ~320ms / 21% (full CLI)

Token count accuracy:
  Actual output tokens:  1,046,635
  Estimated tokens:      1,047,610
  Difference:            975 (0.09%)

Note: the existing chunked tokenization also introduces BPE boundary errors
at every 200K-char split point, so exact counts were already approximate.

When tokenCountTree is disabled (default), behavior is unchanged — the full
output is tokenized as before.

https://claude.ai/code/session_01AKtna74H2P6zT5UTKxxvaq
Resolved conflict in calculateMetrics.ts by accepting the remote version
which already includes a more complete token estimation optimization
(accounts for git diff/log tokens and always counts all files).

Updated test to remove stale calculateOutputMetrics references and
added test for structural overhead token estimation.

Fixed picomatch type declarations from remote changes.

https://claude.ai/code/session_01AKtna74H2P6zT5UTKxxvaq
Two optimizations that together reduce CLI execution time by ~120ms (6.8%):

1. Git ls-files fast path for file search (fileSearch.ts, gitCommand.ts):
   Use `git ls-files -c -o -s --exclude-standard` instead of globby's full
   directory traversal + gitignore parsing when running in a git repository.
   Git's cached file index makes this ~5x faster for the search stage
   (~50ms vs ~250ms). Falls back to globby when git is unavailable or for
   non-git directories.

2. Fast pre-check for base64 truncation (truncateBase64.ts):
   Add two-phase gating to skip expensive global regex scans on files that
   clearly don't contain base64 data:
   - Data URIs: gated by String.includes('base64,') — O(n) with SIMD
   - Standalone base64: gated by line-length check (indexOf-based) then
     non-whitespace run check — skips ~98% of files
   Reduces truncateBase64Content from ~82ms to ~12ms for 1000 files.

Benchmark results (10 runs, `time node bin/repomix.cjs` on repomix repo):
  Before: avg 1.782s (range 1.704–1.891s)
  After:  avg 1.661s (range 1.623–1.720s)
  Improvement: 121ms, 6.8%

All 1102 tests pass. Output is byte-identical.

https://claude.ai/code/session_01DJ9xBHy1W669RuuKLxtrQ8
… perf/auto-perf-tuning-0406

# Conflicts:
#	src/core/file/fileSearch.ts
#	src/core/file/truncateBase64.ts
Move the `try...finally` block to start immediately after metrics worker
pool creation, ensuring cleanup happens even if searchFiles or sortPaths
throws an error. Previously, errors in the search/sort phase would leak
the worker pool.

Addresses review feedback from gemini-code-assist on PR #1428.

https://claude.ai/code/session_01DJ9xBHy1W669RuuKLxtrQ8
Remove the @ts-expect-error suppression since @types/picomatch is
declared in package.json. The suppression would cause a build failure
once dependencies are properly installed, as @ts-expect-error on a
line with no error is itself a TypeScript error.

https://claude.ai/code/session_01DJ9xBHy1W669RuuKLxtrQ8
… deferred output pattern

Three complementary optimizations that together reduce pack() wall time by ~7%:

1. Increase METRICS_BATCH_SIZE from 10 to 50 in calculateSelectiveFileMetrics.
   Reduces IPC round-trips from ~100 to ~20 for a 1000-file repo, cutting
   per-file metrics overhead while maintaining good load balancing across workers.

2. Set minThreads = maxThreads in worker pools for eager thread spawning.
   Ensures all workers are created at pool initialization rather than lazily,
   so warmup tasks (gpt-tokenizer BPE loading) run truly in parallel across
   all threads from the start.

3. Restructure packager pipeline with a deferred output promise pattern.
   Starts file and git metrics calculation immediately after file processing
   completes (using a deferred promise for the output), overlapping metric
   computation with the remaining security check time. Previously, all metrics
   waited for both security check and output generation to complete before
   starting.

Benchmark results (repomix repo, ~1000 files, 4MB output):
  Baseline:  Avg=1183ms, P50=1175ms (10 runs)
  Optimized: Avg=1103ms, P50=1102ms (10 runs)
  Improvement: ~6.8% (~80ms)

https://claude.ai/code/session_01LfAuMfJiVYJ9RX9YkxRfmT
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from 539ac5d to fb0a861 Compare April 9, 2026 05:09
…s to reduce startup cost

Defer importing three expensive modules from CLI startup to actual usage:
- handlebars (~25ms): loaded in outputGenerate.ts on first template compile
- fast-xml-builder (~3ms): loaded in generateParsableXmlOutput on demand
- @clack/prompts (~16ms): loaded in migrationAction/skillPrompts when needed
- packSkill module chain: lazy-loaded via dynamic import in packager.ts

These modules were previously loaded eagerly in the import chain
(defaultAction → packager → outputGenerate → handlebars) even though
they're only needed late in the pipeline during output generation.

Benchmark results (NODE_DISABLE_COMPILE_CACHE=1, 10 runs each):
- Baseline median: 947ms
- After median: 873ms
- Improvement: 74ms (7.8%)

The improvement is most significant on first run, CI/CD, Docker containers,
and Node.js 20 (which lacks compile cache). With warm compile cache the
savings are ~32ms in the import chain, with some modules (packSkill,
@clack/prompts, fast-xml-builder) completely avoided on default runs.

Import chain measurement: 139ms → 107ms (32ms faster startup)
Output is byte-for-byte identical — no functional changes.

https://claude.ai/code/session_0156PKHfb5NcTVQxfZBW6nAn
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from fb0a861 to 082f8b4 Compare April 9, 2026 05:13
Remove 11 keywords from the security pre-filter that correspond to
secretlint rules disabled by default (enableIDScanRule: false in
@secretlint/secretlint-rule-aws):

- AWS Access Key ID prefixes: AKIA, AGPA, AIDA, AROA, AIPA, ANPA,
  ANVA, ASIA
- AWS Account ID patterns: ACCOUNT_ID, account_id, AccountId

These keywords trigger false positives on extremely common code
patterns (e.g., account_id in database models, ASIA in timezone
handling, AccountId in TypeScript interfaces), causing expensive
lintSource() calls that always find nothing because the
corresponding rules are never executed.

Benchmark (100 files with account_id, ~8KB each):
  Before: 799ms median
  After:  658ms median
  Savings: 141ms (17.6%)

The improvement comes from:
- Eliminating unnecessary lintSource() calls (~0.5ms per file)
- Skipping @secretlint module import entirely (~23ms) when no files
  pass the pre-filter
- Reduced CPU contention with metrics worker pool

All active secretlint rules (AWS Secret Access Key, GitHub tokens,
Slack tokens, private keys, database URIs, etc.) remain fully
covered by the pre-filter.

https://claude.ai/code/session_01EruXyfeGcxnZazqorxEQm3
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from 105219e to 9357d02 Compare April 9, 2026 06:12
claude added 7 commits April 9, 2026 07:51
…on-based approach

Replace the O(N²) rendering loop in generateSplitOutputParts with a four-phase
O(N+P) algorithm that uses solo renders and calibrated overhead estimation:

1. Solo-render each group individually (O(N) renders, 1 group each)
2. Calibrate shared overhead from the first two groups' solo+combined renders
3. Greedy bin-packing using exact additive estimates (solo sizes - shared overhead)
4. One verification render per final part (O(P) renders) with adjustment fallback

The estimation is exact when groups have non-overlapping root entries (guaranteed
by buildOutputSplitGroups), since the file tree contribution is purely additive.

Benchmark (60 dirs, 900 files, 10 parts, simulated 0.1ms/file render cost):
  OLD: 69 render calls, 4,095 total files rendered, median 478ms
  NEW: 72 render calls, 1,845 total files rendered, median 241ms → 50% faster

Benchmark (100 dirs, 2000 files, 25 parts):
  OLD: 124 calls, 7,400 files rendered, median 461ms
  NEW: 127 calls, 4,060 files rendered, median 288ms → 38% faster

All 1126 tests pass, identical output in all scenarios.

https://claude.ai/code/session_01CJ9FdfHjKVWjkRd9wYixGz
Move the metrics worker pool initialization from before searchFiles to
after it completes. This eliminates CPU contention between worker thread
spawning/BPE initialization and the file search pipeline.

Problem:
The previous approach pre-created the metrics worker pool at the start
of pack() (or even before it in defaultAction), spawning 4 worker
threads that immediately begin CPU-intensive BPE encoder initialization
(~73ms each). These threads compete with 6 concurrent git subprocesses
(ls-files, staged, deleted, diffs, logs, change-counts) and the main
thread's picomatch pattern matching — 11 tasks on 4 cores. This
contention inflated file search time from ~25ms to ~225ms.

Solution:
Defer pool creation until after searchFiles returns. The warmup then
overlaps with file collection (sync reads), file processing, and the
security check, which are I/O-bound or lightweight and don't contend
with worker CPU usage. As a bonus, the actual file count is now known,
so the pool allocates the correct number of threads instead of always
requesting the maximum.

Benchmark (repomix src/, 128 files, 4-core 2.1GHz):
  Before: 659ms median (15 runs)
  After:  579ms median (15 runs)
  Improvement: -80ms (12.1% faster)

https://claude.ai/code/session_01MahktfjoTTGWeJY1xHup62
…ssary memoryUsage calls

Reduce chunk count for parallel output token counting from a fixed 1000 to
match the number of available CPU cores. With 4 cores, this cuts IPC overhead
from 1000 to 4 postMessage round trips while still saturating all worker threads.

Skip expensive process.memoryUsage() calls in withMemoryLogging and
logMemoryUsage when log level is below DEBUG, since the stats are only used
for trace-level logging.

## Changes
- calculateOutputMetrics: replace hardcoded CHUNK_SIZE=1000 with
  getProcessConcurrency() to match actual worker thread count
- memoryUtils: guard process.memoryUsage() behind log level check

## Benchmark (20 runs each, repomix on its own repo ~1015 files, 4.3MB output)

Baseline:
  Mean: 2.532s | Trimmed mean: 2.515s | Median: 2.519s
  P10: 2.413s | P90: 2.621s

After:
  Mean: 2.354s | Trimmed mean: 2.356s | Median: 2.358s
  P10: 2.307s | P90: 2.387s

Improvement:
  Mean: -7.0% | Trimmed mean: -6.3% | Median: -6.4%
  P90 reduced from 2.621s to 2.387s (variance halved)

Root cause: The old CHUNK_SIZE=1000 created 1000 tiny ~4KB chunks for a 4.3MB
output. Each chunk required a postMessage serialization round trip to the worker
pool. With only 4 worker threads, each thread processed ~250 chunks sequentially,
spending more time on IPC overhead than actual BPE encoding. Matching chunk count
to core count gives each thread one large ~1MB chunk, eliminating ~996 unnecessary
IPC round trips.

https://claude.ai/code/session_01Lbbd8QmBA7SpcoTqrv55b3
… perf/auto-perf-tuning-0406

Resolve merge conflicts in calculateOutputMetrics.ts and its tests
by accepting the remote's TARGET_CHARS_PER_CHUNK approach (the chunking
optimization was already addressed by prior work on this branch).
Keep the memoryUtils optimization (skip process.memoryUsage() in non-debug mode).

https://claude.ai/code/session_01Lbbd8QmBA7SpcoTqrv55b3
Eliminate the Tinypool child_process worker used to run the pack pipeline
in the default CLI action. The worker existed solely to isolate the
spinner display, but spawning a child process and loading the full module
graph in it added ~200ms of fixed overhead on every invocation.

By running pack() directly in the main process with the spinner alongside,
we remove this overhead while maintaining identical behavior: the spinner
still animates via setInterval, progress callbacks still drive updates,
and all worker-based pipeline stages (security check, metrics, file
processing) continue using their own thread pools.

The defaultActionWorker.ts file is preserved for bundled/unified worker
environments.

Benchmark results (median of 5 runs, Node.js):

  Single file (--include 'package.json'):
    Before: 0.888s → After: 0.691s  (-197ms, -22.2%)

  src/ directory (--include 'src'):
    Before: 1.088s → After: 0.898s  (-190ms, -17.5%)

  Full repo (no filter):
    Before: 2.605s → After: 2.265s  (-340ms, -13.0%)

https://claude.ai/code/session_01DGJUQZZ6dkmxyQV69xBUBT
… perf/auto-perf-tuning-0406

# Conflicts:
#	src/cli/actions/defaultAction.ts
#	tests/cli/actions/defaultAction.test.ts
#	tests/cli/actions/defaultAction.tokenCountTree.test.ts
Size the metrics worker pool based on estimated tokenization task count
instead of total file count. The default path (tokenCountTree disabled)
only tokenizes the top ~50 files for ranking and ratio estimation, but
previously allocated workers for all files (e.g., 4 threads for 1000
files on a 4-core machine). The excess BPE initialization CPU contention
inflated search time by ~2x and collect time by ~5x.

With accurate pool sizing, the default case spawns 1 worker thread
instead of 4, reducing warmup contention across the pipeline.
tokenCountTree=true and tokenCountTree=<threshold> still use all files
for pool sizing since they tokenize all (or most) files.

Benchmark (1000-file repo, 4-core, 10 runs median):
  Before: 620ms
  After:  485ms
  Improvement: -135ms (-22%)

Per-phase impact (controlled test, sequential):
  searchFiles:  116ms → 52ms (-55%)
  collectFiles: 216ms → 29ms (-87%)
  warmup wait:  236ms → 123ms (-48%)

https://claude.ai/code/session_0199XB1CSEmQ6yYEhQEjAKzz
@yamadashy
yamadashy force-pushed the perf/auto-perf-tuning-0406 branch from 3eb892e to eae6adc Compare April 9, 2026 13:25
claude added 2 commits April 9, 2026 13:29
Defer gpt-tokenizer and json5 from the synchronous import graph to
reduce CLI startup latency. Three changes:

1. Extract TOKEN_ENCODINGS constant to tokenEncodings.ts, breaking the
   configSchema → TokenCounter → gpt-tokenizer import chain. Config
   schema validation only needs encoding names, not the tokenizer.

2. Lazy-load json5 in configLoad.ts (only needed when a JSON5/JSONC
   config file exists). Saves ~28ms on every run without a config file.

3. Preload @secretlint/core before metrics worker threads spawn.
   The secretlint module (~70ms) previously loaded during the security
   check phase, competing with metrics worker BPE initialization for
   CPU time. Starting the import during the I/O-bound file search phase
   eliminates this contention.

Benchmark (20-run A/B, repomix on own repo, 999 files):
  Baseline median: 1420ms
  After median:    1372ms  (3.4% improvement)
  p75 improvement: 81ms    (5.5%)
  IQR reduction:   134→92ms (31% less variance)

Module loading improvements (measured in isolation):
  configSchema.ts: 175ms → 95ms (gpt-tokenizer no longer loaded)
  json5 deferred:  ~28ms saved when no config file present
  @secretlint preload: eliminates CPU contention with workers

https://claude.ai/code/session_017sHG68dpyUbVcoxnFPfUBE
…warmup stall

Preload BPE rank data at the start of pack() so its async I/O overlaps
with the searchFiles phase (when the event loop is available), and remove
the blocking `await metricsWarmupPromise` synchronization point.

Problem:
  Previously, loadBpeRanks was called inside createMetricsTaskRunner
  (after searchFiles), but the subsequent synchronous collectFiles
  (~150ms) blocked the event loop, preventing the async BPE file-read
  callback from firing. This delayed BPE data availability until after
  collectFiles, and worker warmup (BPE deserialization ~73ms) could
  only start then. The pipeline stalled ~200ms at
  `await metricsWarmupPromise` before calculateMetrics could begin.

Fix:
  1. Fire loadBpeRanks() at the start of pack(), before searchFiles.
     The async I/O (~100ms) completes during searchFiles (~135ms of
     git subprocess I/O), so BPE data is ready before collectFiles.
  2. Pass the pre-loaded BPE promise to createMetricsTaskRunner,
     which feeds it to worker warmup tasks immediately on pool creation.
  3. Remove `await metricsWarmupPromise` — workers complete BPE init
     (~73ms) during collectFiles (~150ms), and real tokenization tasks
     dispatched by calculateMetrics queue behind warmup via FIFO.

Benchmark (repomix self-pack, 999 files, 4-core):
  Before: median 913ms, p75 925ms
  After:  median 855ms, p75 864ms
  Improvement: -6.4% median, -6.6% p75

Pipeline shift (single profiled run):
  calculateMetrics start: 707ms → 522ms (185ms earlier)
  pack() end:             919ms → 802ms (117ms faster)

https://claude.ai/code/session_01Cbzh8SssmdjmCsDJWaZ5tY
yamadashy added a commit that referenced this pull request Apr 9, 2026
…s to reduce startup cost

Defer importing three expensive modules from CLI startup to actual usage:
- handlebars (~25ms): loaded in outputGenerate.ts on first template compile
- fast-xml-builder (~3ms): loaded in generateParsableXmlOutput on demand
- @clack/prompts (~16ms): loaded in migrationAction/skillPrompts when needed
- packSkill module chain: lazy-loaded via dynamic import in packager.ts

Cherry-picked from 082f8b4 (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
…er-worker file I/O

Each metrics worker thread independently loaded gpt-tokenizer's BPE rank data
(~3.6MB, 200K entries) from disk. Now the BPE data is loaded once on the main
thread, serialized to a JSON string (~1.6MB), and passed to each worker via the
warmup task. Workers deserialize and build the encoder instead of reading from disk.

Cherry-picked from fd6b625 (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
Pre-fetch file change counts via prefetchFileChangeCounts in the existing
Promise.all alongside collectFiles/getGitDiffs/getGitLogs, so that
sortOutputFiles (called later inside generateOutput) finds the result
already cached and skips the blocking git subprocess.

Cherry-picked from e587f6a (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
…PE warmup

Move metrics runner creation to defaultAction for earlier BPE warmup,
so loading overlaps with spinner setup and early pack() stages.

Cherry-picked from ccc68c2 (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
Increase batch sizes from 10 to 50 for large repos and output chunk
size from 200K to 500K chars, reducing IPC round-trips from 100 to 20
for a 1000-file repo.

Cherry-picked from 3c98139 (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
Replace async pooled file reads (fs.promises with concurrency limit)
with synchronous fs.readFileSync. The libuv thread pool's default
4-thread limit becomes a bottleneck for thousands of small file reads.

Cherry-picked from a94c618 (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 9, 2026
Replace hardcoded TARGET_CHARS_PER_CHUNK=200K with CPU-core-based
chunking via getProcessConcurrency(). Skip expensive process.memoryUsage()
calls when log level is below DEBUG.

Cherry-picked from 5de897c (PR #1428)

Co-Authored-By: Claude <noreply@anthropic.com>
yamadashy added a commit that referenced this pull request Apr 11, 2026
…s to reduce startup cost

Defer importing three expensive modules from CLI startup to actual usage:
- handlebars (~25ms): loaded in outputGenerate.ts on first template compile
- fast-xml-builder (~3ms): loaded in generateParsableXmlOutput on demand
- @clack/prompts (~16ms): loaded in migrationAction/skillPrompts when needed
- packSkill module chain: lazy-loaded via dynamic import in packager.ts

Cherry-picked from 082f8b4 (PR #1428)

Co-Authored-By: Claude <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.

2 participants