Skip to content

Commit cb424c3

Browse files
fortmarekclaude
andcommitted
fix: ignore stale substep timestamps when computing target compilation duration
For incremental builds, Xcode reuses substep entries from previous build sessions for files that don't need recompilation. Those substeps keep their original (older) timestamps and are not flagged `wasFetchedFromCache`. The previous logic in `addCompilationTimesToTarget` took the latest substep `compilationEndTimestamp` regardless of whether it actually ran in this build session, which produced a negative `compilationDuration` whenever the chosen substep predated the target's `startTimestamp`. Filter out such stale substeps by requiring `compilationEndTimestamp >= parent.startTimestamp` in both `addCompilationTimesToTarget` and `addCompilationTimesToApp`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff2bb9b commit cb424c3

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

Sources/XCLogParser/parser/ParserBuildSteps.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,16 @@ public final class ParserBuildSteps {
373373

374374
private func addCompilationTimesToTarget(_ target: BuildStep) -> BuildStep {
375375

376+
// For incremental builds, Xcode reuses substep entries from prior build sessions for files
377+
// that didn't need recompilation. Those substeps keep their original (older) timestamps and
378+
// are not flagged `wasFetchedFromCache`. Filtering them out by requiring the substep to
379+
// have ended at or after the target started avoids producing a negative compilationDuration.
376380
let lastCompilationStep = target.subSteps
377-
.filter { $0.isCompilationStep() && $0.fetchedFromCache == false }
381+
.filter {
382+
$0.isCompilationStep()
383+
&& $0.fetchedFromCache == false
384+
&& $0.compilationEndTimestamp >= target.startTimestamp
385+
}
378386
.max { $0.compilationEndTimestamp < $1.compilationEndTimestamp }
379387
guard let lastStep = lastCompilationStep else {
380388
return target.with(newCompilationEndTimestamp: target.startTimestamp, andCompilationDuration: 0.0)
@@ -385,7 +393,11 @@ public final class ParserBuildSteps {
385393

386394
private func addCompilationTimesToApp(_ app: BuildStep) -> BuildStep {
387395
let lastCompilationStep = app.subSteps
388-
.filter { $0.compilationDuration > 0 && $0.fetchedFromCache == false }
396+
.filter {
397+
$0.compilationDuration > 0
398+
&& $0.fetchedFromCache == false
399+
&& $0.compilationEndTimestamp >= app.startTimestamp
400+
}
389401
.max { $0.compilationEndTimestamp < $1.compilationEndTimestamp }
390402
guard let lastStep = lastCompilationStep else {
391403
return app.with(newCompilationEndTimestamp: app.startTimestamp,

Tests/XCLogParserTests/ParserTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,31 @@ note: use 'updatedDoSomething' instead\r doSomething()\r ^~~~~~~~~~~\r
346346
XCTAssertEqual(expectedCompilationDuration, targetStep.compilationDuration)
347347
}
348348

349+
func testParseTargetCompilationTimesIgnoresStaleSubsteps() {
350+
// For incremental builds, Xcode reuses substep entries from previous build sessions for
351+
// files that didn't need recompilation. Those substeps keep their original (older)
352+
// timestamps and are not flagged `wasFetchedFromCache`. The target's compilationDuration
353+
// must remain non-negative; substeps that ended before the target started belong to a
354+
// prior session and should be ignored.
355+
let now = Date().timeIntervalSince1970
356+
let staleCompileEnd = now - 1000
357+
let staleStep = makeFakeBuildStep(title: "Stale Compile",
358+
type: .detail,
359+
detailStepType: .cCompilation,
360+
startTimestamp: now - 1010,
361+
fetchedFromCache: false)
362+
.with(newCompilationEndTimestamp: staleCompileEnd, andCompilationDuration: 10)
363+
var targetStep = makeFakeBuildStep(title: "Build Target",
364+
type: .target,
365+
detailStepType: .none,
366+
startTimestamp: now,
367+
fetchedFromCache: false).with(subSteps: [staleStep])
368+
369+
targetStep = parser.addCompilationTimes(step: targetStep)
370+
XCTAssertEqual(targetStep.startTimestamp, targetStep.compilationEndTimestamp)
371+
XCTAssertEqual(0.0, targetStep.compilationDuration)
372+
}
373+
349374
func testParseAppCompilationTimes() {
350375
let expectedCompilationDuration = 50.0
351376
let now = Date().timeIntervalSince1970

0 commit comments

Comments
 (0)