diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e7b46c2f3445c..6afdb7ac8d1bb 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -419,7 +419,8 @@ CREATE TYPE provisioner_job_timing_stage AS ENUM ( 'init', 'plan', 'graph', - 'apply' + 'apply', + 'graph_second' ); CREATE TYPE provisioner_job_type AS ENUM ( @@ -843,15 +844,15 @@ BEGIN IF workspace_count > 0 THEN error_parts := array_append(error_parts, workspace_count || ' workspaces'); END IF; - + IF template_count > 0 THEN error_parts := array_append(error_parts, template_count || ' templates'); END IF; - + IF provisioner_keys_count > 0 THEN error_parts := array_append(error_parts, provisioner_keys_count || ' provisioner keys'); END IF; - + error_message := error_message || array_to_string(error_parts, ', ') || ' that must be deleted first'; RAISE EXCEPTION '%', error_message; END; diff --git a/coderd/database/migrations/000399_build_timing_second_graph.down.sql b/coderd/database/migrations/000399_build_timing_second_graph.down.sql new file mode 100644 index 0000000000000..ba621e12f64a1 --- /dev/null +++ b/coderd/database/migrations/000399_build_timing_second_graph.down.sql @@ -0,0 +1 @@ +-- Leave the enum as is, dropping enum values is not easily supported. diff --git a/coderd/database/migrations/000399_build_timing_second_graph.up.sql b/coderd/database/migrations/000399_build_timing_second_graph.up.sql new file mode 100644 index 0000000000000..65cce32bcd66c --- /dev/null +++ b/coderd/database/migrations/000399_build_timing_second_graph.up.sql @@ -0,0 +1,2 @@ +-- Currently have 2 graph stages in a workspace build +ALTER TYPE provisioner_job_timing_stage ADD VALUE IF NOT EXISTS 'graph_second'; diff --git a/coderd/database/models.go b/coderd/database/models.go index af901c31ccad5..4407221fff2b8 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2413,10 +2413,11 @@ func AllProvisionerJobStatusValues() []ProvisionerJobStatus { type ProvisionerJobTimingStage string const ( - ProvisionerJobTimingStageInit ProvisionerJobTimingStage = "init" - ProvisionerJobTimingStagePlan ProvisionerJobTimingStage = "plan" - ProvisionerJobTimingStageGraph ProvisionerJobTimingStage = "graph" - ProvisionerJobTimingStageApply ProvisionerJobTimingStage = "apply" + ProvisionerJobTimingStageInit ProvisionerJobTimingStage = "init" + ProvisionerJobTimingStagePlan ProvisionerJobTimingStage = "plan" + ProvisionerJobTimingStageGraph ProvisionerJobTimingStage = "graph" + ProvisionerJobTimingStageApply ProvisionerJobTimingStage = "apply" + ProvisionerJobTimingStageGraphSecond ProvisionerJobTimingStage = "graph_second" ) func (e *ProvisionerJobTimingStage) Scan(src interface{}) error { @@ -2459,7 +2460,8 @@ func (e ProvisionerJobTimingStage) Valid() bool { case ProvisionerJobTimingStageInit, ProvisionerJobTimingStagePlan, ProvisionerJobTimingStageGraph, - ProvisionerJobTimingStageApply: + ProvisionerJobTimingStageApply, + ProvisionerJobTimingStageGraphSecond: return true } return false @@ -2471,6 +2473,7 @@ func AllProvisionerJobTimingStageValues() []ProvisionerJobTimingStage { ProvisionerJobTimingStagePlan, ProvisionerJobTimingStageGraph, ProvisionerJobTimingStageApply, + ProvisionerJobTimingStageGraphSecond, } } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index a91148ab2ad9e..d7bb0c93a96f0 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -218,10 +218,11 @@ type TimingStage string const ( // Based on ProvisionerJobTimingStage - TimingStageInit TimingStage = "init" - TimingStagePlan TimingStage = "plan" - TimingStageGraph TimingStage = "graph" - TimingStageApply TimingStage = "apply" + TimingStageInit TimingStage = "init" + TimingStagePlan TimingStage = "plan" + TimingStageGraph TimingStage = "graph" + TimingStageGraphSecond TimingStage = "graph_second" + TimingStageApply TimingStage = "apply" // Based on WorkspaceAgentScriptTimingStage TimingStageStart TimingStage = "start" TimingStageStop TimingStage = "stop" diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go index 3d9270a6ddbab..5c1732117db39 100644 --- a/provisioner/terraform/executor.go +++ b/provisioner/terraform/executor.go @@ -607,7 +607,9 @@ func (e *executor) apply( } // `terraform show` & `terraform graph` + endGraph := e.timings.startStage(database.ProvisionerJobTimingStageGraphSecond) state, err := e.stateResources(ctx, killCtx) + endGraph(err) if err != nil { return nil, err } diff --git a/provisioner/terraform/timings.go b/provisioner/terraform/timings.go index 0b150d2eafd4d..6e04bc14b42aa 100644 --- a/provisioner/terraform/timings.go +++ b/provisioner/terraform/timings.go @@ -109,7 +109,11 @@ func (t *timingAggregator) ingest(ts time.Time, s *timingSpan) { return } - s.stage = t.stage + // Only set the stage if it hasn't already been set on the span. + // Explicitly set stage takes precedence. + if s.stage == "" { + s.stage = t.stage + } ts = dbtime.Time(ts.UTC()) switch s.kind { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c2c94aa314b3d..1be04f20fa904 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -5249,6 +5249,7 @@ export type TimingStage = | "connect" | "cron" | "graph" + | "graph_second" | "init" | "plan" | "start" @@ -5259,6 +5260,7 @@ export const TimingStages: TimingStage[] = [ "connect", "cron", "graph", + "graph_second", "init", "plan", "start", diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index 103d4717f20c6..8ab56f24fabe2 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -1,5 +1,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import type { TimingStage } from "api/typesGenerated"; +import { T } from "lodash/fp"; import { CircleAlertIcon, InfoIcon } from "lucide-react"; import type { FC } from "react"; import { Bar, ClickableBar } from "./Chart/Bar"; @@ -33,6 +34,10 @@ export type Stage = { * The name is used to identify the stage. */ name: TimingStage; + /** + * The graph stage actually runs twice, the second with another name. + */ + alternativeNames?: TimingStage[]; /** * The value to display in the stage label. This can differ from the stage * name to provide more context or clarity. @@ -48,7 +53,7 @@ export type Stage = { tooltip: Omit; }; -type StageTiming = { +export type StageTiming = { stage: Stage; /** * Represents the number of resources included in this stage that can be @@ -61,7 +66,7 @@ type StageTiming = { * duration of the stage and to position the stage within the chart. This can * be undefined if a stage has no timing data. */ - range: TimeRange | undefined; + ranges: TimeRange[] | undefined; /** * Display an error icon within the bar to indicate when a stage has failed. * This is used in the agent scripts stage. @@ -79,7 +84,10 @@ export const StagesChart: FC = ({ onSelectStage, }) => { const totalRange = mergeTimeRanges( - timings.map((t) => t.range).filter((t) => t !== undefined), + timings + .map((t) => t.ranges) + .filter((t) => t !== undefined) + .flat(), ); const totalTime = calcDuration(totalRange); const [ticks, scale] = makeTicks(totalTime); @@ -125,11 +133,12 @@ export const StagesChart: FC = ({ const stageTimings = timings.filter( (t) => t.stage.section === section, ); + return ( {stageTimings.map((t) => { // If the stage has no timing data, we just want to render an empty row - if (t.range === undefined) { + if (t.ranges === undefined) { return ( = ({ ); } - const value = calcDuration(t.range); - const offset = calcOffset(t.range, totalRange); - const validDuration = value > 0 && !Number.isNaN(value); + return t.ranges?.map((range, index) => { + const value = calcDuration(range); + const offset = calcOffset(range, totalRange); + const validDuration = value > 0 && !Number.isNaN(value); - return ( - - {/** We only want to expand stages with more than one resource */} - {t.visibleResources > 1 ? ( - { - onSelectStage(t.stage); - }} - > - {t.error && ( - - )} - - - ) : ( - - )} - {validDuration ? ( - {formatTime(value)} - ) : ( - ({ - color: theme.palette.error.main, - })} - > - Invalid - - )} - - ); + return ( + + {/** We only want to expand stages with more than one resource */} + {t.visibleResources > 1 ? ( + { + onSelectStage(t.stage); + }} + > + {t.error && ( + + )} + + + ) : ( + + )} + {validDuration ? ( + {formatTime(value)} + ) : ( + ({ + color: theme.palette.error.main, + })} + > + Invalid + + )} + + ); + }); })} ); @@ -247,6 +258,7 @@ export const provisioningStages: Stage[] = [ }, { name: "graph", + alternativeNames: ["graph_second"], label: "graph", section: "provisioning", tooltip: { diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index c4e0e63228314..d77d85a918435 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -28,6 +28,7 @@ import { provisioningStages, type Stage, StagesChart, + type StageTiming, } from "./StagesChart"; type TimingView = @@ -130,14 +131,25 @@ export const WorkspaceTimings: FC = ({
{view.name === "default" && ( { + timings={stages.map((s): StageTiming => { const stageTimings = timings.filter( - (t) => t.stage === s.name, + // graph has 2 stages, `graph` and `graph_second` + (t) => + t.stage === s.name || + s.alternativeNames?.includes(t.stage), ); - const stageRange = - stageTimings.length === 0 + + + const keyedRanges = stageTimings.reduce>( + (acc, t) => { + acc[t.stage] = acc[t.stage] || []; + acc[t.stage].push(toTimeRange(t)); + return acc; + }, {}); + + const stageRanges = stageTimings.length === 0 ? undefined - : mergeTimeRanges(stageTimings.map(toTimeRange)); + : Object.entries(keyedRanges).map(([_, ranges]) => mergeTimeRanges(ranges)); // Prevent users from inspecting internal coder resources in // provisioner timings because they were not useful to the @@ -158,7 +170,7 @@ export const WorkspaceTimings: FC = ({ return { stage: s, - range: stageRange, + ranges: stageRanges, visibleResources: visibleResources.length, error: stageTimings.some( (t) => "status" in t && t.status === "exit_failure", diff --git a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts index c45b56ec5a52e..36d23a702c4dc 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts @@ -364,6 +364,15 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { action: "create", resource: "coder_metadata.container_info[0]", }, + { + job_id: "86fd4143-d95f-4602-b464-1149ede62269", + started_at: "2024-10-14T11:30:53.693767Z", + ended_at: "2024-10-14T11:30:58.693767Z", + stage: "graph_second", + source: "terraform", + action: "terraform", + resource: "coder_graph_second_stage", + }, ], agent_script_timings: [ {