Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: include entire stage timings in build timeline
  • Loading branch information
Emyrk committed Nov 18, 2025
commit 70d1169954afadebdab652d43ce7b1d33e98f8bd
8 changes: 7 additions & 1 deletion provisioner/terraform/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,10 +596,15 @@ func (e *executor) apply(
<-doneErr
}()

// `terraform apply`
endStage := e.timings.startStage(database.ProvisionerJobTimingStageApply)
err := e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter)
endStage(err)
if err != nil {
return nil, xerrors.Errorf("terraform apply: %w", err)
}

// `terraform show` & `terraform graph`
state, err := e.stateResources(ctx, killCtx)
if err != nil {
return nil, err
Expand All @@ -610,12 +615,13 @@ func (e *executor) apply(
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
}

agg := e.timings.aggregate()
return &proto.ApplyComplete{
Parameters: state.Parameters,
Resources: state.Resources,
ExternalAuthProviders: state.ExternalAuthProviders,
State: stateContent,
Timings: e.timings.aggregate(),
Timings: agg,
AiTasks: state.AITasks,
}, nil
}
Expand Down
42 changes: 39 additions & 3 deletions provisioner/terraform/timings.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ type timingKind string
// Copied from https://github.com/hashicorp/terraform/blob/01c0480e77263933b2b086dc8d600a69f80fad2d/internal/command/jsonformat/renderer.go
// We cannot reference these because they're in an internal package.
const (
// Stage markers are used to denote the beginning and end of stages. Without
// these, only discrete events within stages can be measured, which may omit
// setup/teardown time or other unmeasured overhead.
timingStageStart timingKind = "stage_start"
timingStageEnd timingKind = "stage_end"
timingStageError timingKind = "stage_error"

timingApplyStart timingKind = "apply_start"
timingApplyProgress timingKind = "apply_progress"
timingApplyComplete timingKind = "apply_complete"
Expand Down Expand Up @@ -109,13 +116,13 @@ func (t *timingAggregator) ingest(ts time.Time, s *timingSpan) {
ts = dbtime.Time(ts.UTC())

switch s.kind {
case timingApplyStart, timingProvisionStart, timingRefreshStart, timingInitStart, timingGraphStart:
case timingApplyStart, timingProvisionStart, timingRefreshStart, timingInitStart, timingGraphStart, timingStageStart:
s.start = ts
s.state = proto.TimingState_STARTED
case timingApplyComplete, timingProvisionComplete, timingRefreshComplete, timingInitComplete, timingGraphComplete:
case timingApplyComplete, timingProvisionComplete, timingRefreshComplete, timingInitComplete, timingGraphComplete, timingStageEnd:
s.end = ts
s.state = proto.TimingState_COMPLETED
case timingApplyErrored, timingProvisionErrored, timingInitErrored, timingGraphErrored:
case timingApplyErrored, timingProvisionErrored, timingInitErrored, timingGraphErrored, timingStageError:
s.end = ts
s.state = proto.TimingState_FAILED
case timingInitOutput:
Expand Down Expand Up @@ -176,8 +183,35 @@ func (t *timingAggregator) aggregate() []*proto.Timing {
return out
}

// startStage denotes the beginning of a stage and returns a function which
// should be called to mark the end of the stage. This is used to measure a
// stage's total duration across all it's discrete events and unmeasured
// overhead/events.
func (t *timingAggregator) startStage(stage database.ProvisionerJobTimingStage) (end func(err error)) {
ts := timingSpan{
kind: timingStageStart,
stage: stage,
resource: "coder_stage",
action: "terraform",
provider: "coder",
}
endTs := ts
t.ingest(dbtime.Now(), &ts)

return func(err error) {
endTs.kind = timingStageEnd
if err != nil {
endTs.kind = timingStageError
}
t.ingest(dbtime.Now(), &endTs)
}
}

func (l timingKind) Valid() bool {
return slices.Contains([]timingKind{
timingStageStart,
timingStageEnd,
timingStageError,
timingApplyStart,
timingApplyProgress,
timingApplyComplete,
Expand Down Expand Up @@ -210,6 +244,8 @@ func (l timingKind) Valid() bool {
// if all other attributes are identical.
func (l timingKind) Category() string {
switch l {
case timingStageStart, timingStageEnd, timingStageError:
return "stage"
case timingInitStart, timingInitComplete, timingInitErrored, timingInitOutput:
return "init"
case timingGraphStart, timingGraphComplete, timingGraphErrored:
Expand Down
15 changes: 15 additions & 0 deletions provisioner/terraform/timings_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,18 @@ func printTimings(t *testing.T, timings []*proto.Timing) {
terraform_internal.PrintTiming(t, a)
}
}

func TestTimingStages(t *testing.T) {
t.Parallel()

agg := &timingAggregator{
stage: database.ProvisionerJobTimingStageApply,
stateLookup: make(map[uint64]*timingSpan),
}

end := agg.startStage(database.ProvisionerJobTimingStageApply)
end(nil)

evts := agg.aggregate()
require.Len(t, evts, 1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
};

export const isCoderResource = (resource: string) => {
if(resource === "coder_stage") {
return false
}
return (
resource.startsWith("data.coder") ||
resource.startsWith("module.coder") ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,18 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
// user and would add noise.
const visibleResources = stageTimings.filter((t) => {
const isProvisionerTiming = "resource" in t;

if(isProvisionerTiming && t.resource === "coder_stage") {
return false;
}

return isProvisionerTiming
? !isCoderResource(t.resource)
: true;
});

// console.log(stageTimings)

return {
stage: s,
range: stageRange,
Expand Down