Skip to content

chore(diag): log fast-skip rejection reason in CI bench gate - #1067

Merged
carlos-alm merged 2 commits into
mainfrom
fix/1066-fast-skip-diag
May 5, 2026
Merged

chore(diag): log fast-skip rejection reason in CI bench gate#1067
carlos-alm merged 2 commits into
mainfrom
fix/1066-fast-skip-diag

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Add env-gated diagnostic logging to `detectNoChanges` (`src/domain/graph/builder/stages/detect-changes.ts`) — every `return false` now emits a `[fast-skip]` line via `info()` naming the check that rejected and including the relevant values (mtime, size, file path, table-empty reason). The `true` path also logs to confirm the fast-skip fired.
  • Set `CODEGRAPH_FAST_SKIP_DIAG: "1"` at the `pre-publish-benchmark` job level in `.github/workflows/publish.yml` so the next workflow_dispatch publish run surfaces the cause of the regression in publish gate: native incremental rebuilds regress to ~2s, JS fast-skip not firing in CI #1066.

Behavior is unchanged when the env var is unset, so regular incremental builds stay quiet.

Why

The v3.10.0 publish gate (run 25361005381) measured native incremental no-op rebuilds at ~2000ms vs the 10–13ms 3.9.6 baseline — exactly the regression PR #1064's JS-side fast-skip was meant to fix. Code inspection (issue #1066) narrowed the cause to one of:

  • mtime precision mismatch between Rust-written `file_hashes.mtime` and JS-read `Math.floor(stat.mtimeMs)`
  • pending-analysis guard tripping because `cfg_blocks` / `dataflow` are empty after the full build
  • `forceFullRebuild` set due to engine/version mismatch
  • file enumeration drift between full and noop

Reading the code didn't pinpoint which. One CI run with this diagnostic does.

What to look for in the next publish run

After merging and triggering a workflow_dispatch publish, search the "Run incremental benchmark" / "Run build benchmark" step logs for `[fast-skip]`. Expected outcomes:

  • `true: all checks passed (N files)` → fast-skip is firing; regression is elsewhere (look at orchestrator path)
  • `false: mtime/size diff for X: stat=A/B stored=C/D` → confirms the mtime-precision hypothesis
  • `false: pending-analysis guard — cfg_blocks is empty` → confirms the guard is the cause despite Rust orchestrator allegedly populating the table
  • `false: tracked file no longer collected: X` → file enumeration disagrees between writes and reads

Test plan

  • Merge to main
  • Trigger workflow_dispatch on Publish workflow
  • Inspect `Run build benchmark` and `Run incremental benchmark` step logs for `[fast-skip]` lines
  • File a follow-up fix targeting the identified root cause and revert this diag PR

Refs #1066

The pre-publish bench gate's native incremental-rebuild metrics regressed
from ~10ms to ~2000ms in the v3.10.0 publish run, indicating the JS-side
fast-skip from #1064 isn't firing in CI. Reading the code didn't pinpoint
which check is rejecting — the suspects (mtime precision mismatch, file
enumeration drift, pending-analysis guard) all look plausible.

Wrap each return-false in detectNoChanges with an env-gated info() log
that names the check and prints the differing values. Set the env var
on the pre-publish-benchmark job so the next workflow_dispatch surfaces
the cause in stderr (visible in CI logs; bench scripts only redirect
stdout to JSON files).

Behavior unchanged when CODEGRAPH_FAST_SKIP_DIAG is unset — gate is
opt-in to keep regular incremental builds quiet.

Refs #1066

Impact: 2 functions changed, 7 affected
@claude

claude Bot commented May 5, 2026

Copy link
Copy Markdown

Claude finished @carlos-alm's task —— View job


PR Review Complete

Reviewing PR #1067: chore(diag): log fast-skip rejection reason in CI bench gate

Review Tasks

  • Analyze root cause and problem being solved
  • Review changed files for implementation quality
  • Check for configuration changes and test integrity issues
  • Assess backlog compliance (zero-dep, foundation-aligned, etc.)
  • Provide comprehensive review feedback

Root Cause Analysis

Problem Being Solved: Native incremental no-op rebuilds regressed from 10-13ms baseline (v3.9.6) to ~2000ms (v3.10.0), precisely contradicting PR #1064's fast-skip optimization. Code inspection identified 4 potential causes but couldn't pinpoint which.

Why This Approach: Targeted diagnostic logging is justified here because:

  • The regression is real and measurable (200x performance degradation)
  • Static code analysis was insufficient to identify the root cause
  • Temporary, environment-gated logging provides observable data with zero production impact
  • Alternative approaches (blind fixes, extensive refactoring) would be higher-risk

Risk Assessment: Very low. Changes are purely additive logging behind an environment gate.

Code Quality Review

Implementation Quality: ⭐⭐⭐⭐⭐ Excellent

  • Clean separation of concerns with diag/log variables
  • Consistent logging format with [fast-skip] prefix
  • Detailed diagnostic output for each failure reason with relevant values
  • Zero overhead when disabled (early return pattern in log function)
  • Logs both failure and success paths for complete observability

Technical Correctness: ⭐⭐⭐⭐⭐ Perfect

  • All logging preserves existing logic exactly - no behavioral changes
  • Proper env var parsing (=== '1' prevents truthy issues)
  • Comprehensive coverage of all return false paths in detectNoChanges() (detect-changes.ts:537-624)
  • Includes the success path logging with file count for verification

Configuration Change: ✓ Appropriate

  • Only affects the pre-publish-benchmark job in CI
  • Well-documented with inline comment explaining purpose and removal plan
  • No relaxation of rules or thresholds

Backlog Compliance

  • Zero-dep: ✓ No new runtime dependencies
  • Foundation-aligned: ✓ Pure diagnostic tool, no cloud APIs, maintains zero-infrastructure principle
  • Problem-fit: 4/5 — Directly addresses performance regression blocking CI gates
  • Breaking: No — purely additive, zero behavior change when env var unset
  • Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Critical Concerns

None. This is an exemplary diagnostic PR.

Strengths:

  1. Surgical precision: Only touches the exact function causing issues
  2. Zero production impact: Environment-gated with no performance overhead
  3. Comprehensive diagnostics: Logs every possible rejection reason with context
  4. Temporary by design: Clear removal plan once root cause identified
  5. Excellent documentation: Comments explain the purpose, scope, and removal plan

Minor Observations:

  • The mtime precision diagnostic (line 600-602 in detect-changes.ts) will likely identify the root cause - this is the most suspicious potential mismatch
  • Consider the CI run outputs carefully: multiple false positives may indicate compound issues

Final Recommendation

  • Rating: ⭐⭐⭐⭐⭐ (5/5)
  • Action: APPROVE
  • Reasoning: This is exactly how performance regression diagnostics should be implemented - minimal, targeted, temporary, and comprehensive. The PR will provide the exact data needed to identify and fix the underlying issue without any risk to the codebase.

Next Steps After Merge:

  1. Trigger workflow_dispatch publish
  2. Search benchmark step logs for [fast-skip] patterns
  3. File targeted fix PR based on diagnostic output
  4. Revert this diagnostic PR once root cause is resolved

@greptile-apps

greptile-apps Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

  • Adds env-gated [fast-skip] diagnostic logging to every early-return in detectNoChanges and to the pre-flight guard conditions in pipeline.ts, so the next CI benchmark run surfaces which of the four regression hypotheses from publish gate: native incremental rebuilds regress to ~2s, JS fast-skip not firing in CI #1066 is active.
  • CODEGRAPH_FAST_SKIP_DIAG: \"1\" is set at the pre-publish-benchmark job level in publish.yml; all other builds remain unaffected.
  • One minor diagnostic gap: if the pre-flight try block throws, the catch falls back to debug() (verbose-only), meaning that path produces no [fast-skip] line in CI output.

Confidence Score: 5/5

Safe to merge — diagnostic-only change with no behavioral impact when the env var is unset.

All production code paths are unaffected when CODEGRAPH_FAST_SKIP_DIAG is unset. The single P2 finding (silent catch path) only reduces diagnostic coverage for an uncommon exception scenario; it cannot cause incorrect output or regressions.

src/domain/graph/builder/pipeline.ts — catch block uses verbose-only debug() instead of info() for the exception path when the diag env var is set.

Important Files Changed

Filename Overview
src/domain/graph/builder/stages/detect-changes.ts Adds env-gated [fast-skip] diagnostic logging to every early-return path in detectNoChanges; logic is unchanged when the env var is unset.
src/domain/graph/builder/pipeline.ts Adds pre-flight gate diagnostic block that enumerates all short-circuiting conditions; the catch block on pre-flight failure uses debug() (verbose-only) instead of info(), leaving a silent path when the env var is set and the try block throws.
.github/workflows/publish.yml Sets CODEGRAPH_FAST_SKIP_DIAG: '1' at the pre-publish-benchmark job level so the env var propagates to all steps; correctly commented as temporary diagnostic.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildGraph called] --> B{CODEGRAPH_FAST_SKIP_DIAG=1?}
    B -- Yes --> C[Collect failing pre-flight conditions]
    C --> D{reasons.length > 0?}
    D -- Yes --> E["info: [fast-skip] false: pre-flight gate skipped — reasons"]
    B -- No --> F[Skip diag block]
    F --> G
    D -- No --> G{nativeAvailable && native && incremental && !forceFullRebuild && !scope?}
    E --> H[Pre-flight condition failed — skip detectNoChanges]
    G -- No --> H
    G -- Yes --> I[collectFiles]
    I --> J[detectNoChanges]
    J --> K{result?}
    K -- true --> L["info: [fast-skip] true: all checks passed (N files)"]
    K -- false --> M["info: [fast-skip] false: specific reason"]
    I -- throws --> N[catch block]
    J -- throws --> N
    N --> O["debug: native fast-skip pre-flight failed (verbose-only)"]
    O --> P["No [fast-skip] log visible in CI without --verbose"]
    L --> Q[Return early — graph up to date]
    M --> R[Fall through to orchestrator]
    H --> R
    P --> R
Loading

Fix All in Claude Code

Reviews (2): Last reviewed commit: "chore(diag): log call-site fast-skip rej..." | Re-trigger Greptile

Comment on lines +543 to +549
// Diagnostic logging gated by env var — used by the bench gate to surface
// why the fast-skip is not firing on CI runners (#1066). Off by default to
// avoid noise on every regular incremental build.
const diag = process.env.CODEGRAPH_FAST_SKIP_DIAG === '1';
const log = (reason: string): void => {
if (diag) info(`[fast-skip] ${reason}`);
};

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.

P1 Diagnostic blind spot: forceFullRebuild hypothesis not covered

In pipeline.ts (line 1017), detectNoChanges is only reached when !ctx.forceFullRebuild. If forceFullRebuild is the cause of the regression (one of the four stated hypotheses in the PR description), no [fast-skip] line will ever appear in the logs — the entire guard is bypassed before entering this function. The CI run would be silent and inconclusive for that hypothesis, wasting the diagnostic opportunity.

Consider adding a log at the call site in pipeline.ts before the condition, for example logging when forceFullRebuild is true so all four hypotheses produce observable output.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c6c4d46 — added env-gated diagnostic logging at the pipeline.ts call site (lines 1013-1027) that names which guard(s) short-circuited (nativeAvailable, engineName, incremental, forceFullRebuild, scope) when the fast-skip is bypassed. All four hypotheses from #1066 now produce observable output even if detectNoChanges is never entered.

Comment on lines 581 to +604
for (const file of allFiles) {
const relPath = normalizePath(path.relative(rootDir, file));
const record = existing.get(relPath);
if (!record) return false;
if (!record) {
log(`false: collected file missing from file_hashes: ${relPath}`);
return false;
}
const stat = fileStat(file) as FileStat | undefined;
if (!stat) return false;
if (!stat) {
log(`false: stat failed for ${relPath}`);
return false;
}
const storedMtime = record.mtime || 0;
const storedSize = record.size || 0;
if (storedSize <= 0) return false;
if (Math.floor(stat.mtimeMs) !== storedMtime || stat.size !== storedSize) return false;
if (storedSize <= 0) {
log(`false: stored size <= 0 for ${relPath} (stored=${record.size})`);
return false;
}
if (Math.floor(stat.mtimeMs) !== storedMtime || stat.size !== storedSize) {
log(
`false: mtime/size diff for ${relPath}: stat=${Math.floor(stat.mtimeMs)}/${stat.size} stored=${storedMtime}/${storedSize} (mtimeMs=${stat.mtimeMs})`,
);
return false;
}

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.

P2 Only the first mismatch is logged across all files

The allFiles loop exits on the very first failing file. If the root cause is an mtime drift, you'll see exactly one false: mtime/size diff line. That's fine for a single-file regression, but if multiple files exhibit drift you won't see a count or pattern. For this single-run diagnostic this is acceptable, but worth noting: if the log shows the first rejected file and the follow-up fix doesn't resolve the issue, a broader scan might be needed.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — for this single-run diagnostic, logging the first mismatch is intentional and acceptable as you note. If the follow-up fix doesn't fully resolve the issue, we'll widen to a count/pattern scan in the next iteration. Leaving the early-exit behaviour as-is for this PR to keep the diagnostic minimal and easy to revert.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

3 functions changed8 callers affected across 7 files

  • buildGraph in src/domain/graph/builder/pipeline.ts:993 (6 transitive callers)
  • detectNoChanges in src/domain/graph/builder/stages/detect-changes.ts:537 (6 transitive callers)
  • log in src/domain/graph/builder/stages/detect-changes.ts:547 (5 transitive callers)

Greptile flagged that the original detect-changes diagnostic only fires
once detectNoChanges is entered. The pipeline.ts call-site guards
(forceFullRebuild, engineName, incremental, scope) bypass the function
entirely, so a forceFullRebuild-driven regression (one of #1066's
hypotheses) would produce zero [fast-skip] output and waste the CI
diagnostic run.

Add an env-gated log at the call site that names the failing guard(s)
when any short-circuit, ensuring all four hypotheses produce observable
output. Behaviour is unchanged when CODEGRAPH_FAST_SKIP_DIAG is unset.

Refs #1066
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
carlos-alm merged commit 4a6989c into main May 5, 2026
29 checks passed
@carlos-alm
carlos-alm deleted the fix/1066-fast-skip-diag branch May 5, 2026 18:52
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant