diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 48b550c9ed010..a8719397a1559 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9020,6 +9020,28 @@ const docTemplate = `{ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "stage": { + "$ref": "#/definitions/codersdk.TimingStage" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -9034,7 +9056,7 @@ const docTemplate = `{ "type": "integer" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -9042,6 +9064,12 @@ const docTemplate = `{ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, @@ -12170,7 +12198,7 @@ const docTemplate = `{ "type": "string" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -13473,6 +13501,29 @@ const docTemplate = `{ "TemplateVersionWarningUnsupportedWorkspaces" ] }, + "codersdk.TimingStage": { + "type": "string", + "enum": [ + "init", + "plan", + "graph", + "apply", + "start", + "stop", + "cron", + "connect" + ], + "x-enum-varnames": [ + "TimingStageInit", + "TimingStagePlan", + "TimingStageGraph", + "TimingStageApply", + "TimingStageStart", + "TimingStageStop", + "TimingStageCron", + "TimingStageConnect" + ] + }, "codersdk.TokenConfig": { "type": "object", "properties": { @@ -14806,7 +14857,14 @@ const docTemplate = `{ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { + "description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version", "type": "array", "items": { "$ref": "#/definitions/codersdk.AgentScriptTiming" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c9c79b443d3d0..88bf71bf05758 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7992,6 +7992,28 @@ } } }, + "codersdk.AgentConnectionTiming": { + "type": "object", + "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, + "stage": { + "$ref": "#/definitions/codersdk.TimingStage" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" + } + } + }, "codersdk.AgentScriptTiming": { "type": "object", "properties": { @@ -8006,7 +8028,7 @@ "type": "integer" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -8014,6 +8036,12 @@ }, "status": { "type": "string" + }, + "workspace_agent_id": { + "type": "string" + }, + "workspace_agent_name": { + "type": "string" } } }, @@ -10986,7 +11014,7 @@ "type": "string" }, "stage": { - "type": "string" + "$ref": "#/definitions/codersdk.TimingStage" }, "started_at": { "type": "string", @@ -12228,6 +12256,29 @@ "enum": ["UNSUPPORTED_WORKSPACES"], "x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"] }, + "codersdk.TimingStage": { + "type": "string", + "enum": [ + "init", + "plan", + "graph", + "apply", + "start", + "stop", + "cron", + "connect" + ], + "x-enum-varnames": [ + "TimingStageInit", + "TimingStagePlan", + "TimingStageGraph", + "TimingStageApply", + "TimingStageStart", + "TimingStageStop", + "TimingStageCron", + "TimingStageConnect" + ] + }, "codersdk.TokenConfig": { "type": "object", "properties": { @@ -13475,7 +13526,14 @@ "codersdk.WorkspaceBuildTimings": { "type": "object", "properties": { + "agent_connection_timings": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.AgentConnectionTiming" + } + }, "agent_script_timings": { + "description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version", "type": "array", "items": { "$ref": "#/definitions/codersdk.AgentScriptTiming" diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 515330f2edefb..45320522abc39 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -2891,13 +2891,15 @@ func (s *MethodTestSuite) TestSystemFunctions() { }) rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ { - StartedAt: timing.StartedAt, - EndedAt: timing.EndedAt, - Stage: timing.Stage, - ScriptID: timing.ScriptID, - ExitCode: timing.ExitCode, - Status: timing.Status, - DisplayName: script.DisplayName, + StartedAt: timing.StartedAt, + EndedAt: timing.EndedAt, + Stage: timing.Stage, + ScriptID: timing.ScriptID, + ExitCode: timing.ExitCode, + Status: timing.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }, } check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8214a9f6b77ff..c76de01df0b55 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5895,15 +5895,31 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex break } } + if script.ID == uuid.Nil { + return nil, xerrors.Errorf("script with ID %s not found", t.ScriptID) + } + + var agent database.WorkspaceAgent + for _, a := range agents { + if a.ID == script.WorkspaceAgentID { + agent = a + break + } + } + if agent.ID == uuid.Nil { + return nil, xerrors.Errorf("agent with ID %s not found", t.ScriptID) + } rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{ - ScriptID: t.ScriptID, - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: t.Stage, - Status: t.Status, - DisplayName: script.DisplayName, + ScriptID: t.ScriptID, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: t.Stage, + Status: t.Status, + DisplayName: script.DisplayName, + WorkspaceAgentID: agent.ID, + WorkspaceAgentName: agent.Name, }) } return rows, nil diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e72db60f3b051..e997601883579 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11444,7 +11444,11 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks } const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id @@ -11454,13 +11458,15 @@ WHERE workspace_builds.id = $1 ` type GetWorkspaceAgentScriptTimingsByBuildIDRow struct { - ScriptID uuid.UUID `db:"script_id" json:"script_id"` - StartedAt time.Time `db:"started_at" json:"started_at"` - EndedAt time.Time `db:"ended_at" json:"ended_at"` - ExitCode int32 `db:"exit_code" json:"exit_code"` - Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` - Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` - DisplayName string `db:"display_name" json:"display_name"` + ScriptID uuid.UUID `db:"script_id" json:"script_id"` + StartedAt time.Time `db:"started_at" json:"started_at"` + EndedAt time.Time `db:"ended_at" json:"ended_at"` + ExitCode int32 `db:"exit_code" json:"exit_code"` + Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"` + Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"` + DisplayName string `db:"display_name" json:"display_name"` + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"` } func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) { @@ -11480,6 +11486,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context &i.Stage, &i.Status, &i.DisplayName, + &i.WorkspaceAgentID, + &i.WorkspaceAgentName, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 2c26740db1d88..df7c829861cb2 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -303,7 +303,11 @@ VALUES RETURNING workspace_agent_script_timings.*; -- name: GetWorkspaceAgentScriptTimingsByBuildID :many -SELECT workspace_agent_script_timings.*, workspace_agent_scripts.display_name +SELECT + workspace_agent_script_timings.*, + workspace_agent_scripts.display_name, + workspace_agents.id as workspace_agent_id, + workspace_agents.name as workspace_agent_name FROM workspace_agent_script_timings INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 0974d85b54d6c..44c34d8a25da3 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -957,15 +957,29 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err) } + resources, err := api.Database.GetWorkspaceResourcesByJobID(ctx, build.JobID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace resources: %w", err) + } + resourceIDs := make([]uuid.UUID, 0, len(resources)) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agents: %w", err) + } + res := codersdk.WorkspaceBuildTimings{ - ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), - AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)), + AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)), + AgentConnectionTimings: make([]codersdk.AgentConnectionTiming, 0, len(agents)), } for _, t := range provisionerTimings { res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{ JobID: t.JobID, - Stage: string(t.Stage), + Stage: codersdk.TimingStage(t.Stage), Source: t.Source, Action: t.Action, Resource: t.Resource, @@ -975,12 +989,23 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild) } for _, t := range agentScriptTimings { res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{ - StartedAt: t.StartedAt, - EndedAt: t.EndedAt, - ExitCode: t.ExitCode, - Stage: string(t.Stage), - Status: string(t.Status), - DisplayName: t.DisplayName, + StartedAt: t.StartedAt, + EndedAt: t.EndedAt, + ExitCode: t.ExitCode, + Stage: codersdk.TimingStage(t.Stage), + Status: string(t.Status), + DisplayName: t.DisplayName, + WorkspaceAgentID: t.WorkspaceAgentID.String(), + WorkspaceAgentName: t.WorkspaceAgentName, + }) + } + for _, agent := range agents { + res.AgentConnectionTimings = append(res.AgentConnectionTimings, codersdk.AgentConnectionTiming{ + WorkspaceAgentID: agent.ID.String(), + WorkspaceAgentName: agent.Name, + StartedAt: agent.CreatedAt, + Stage: codersdk.TimingStageConnect, + EndedAt: agent.FirstConnectedAt.Time, }) } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index e8eeca0f49d66..3aae3989df5b4 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1183,8 +1183,9 @@ func TestPostWorkspaceBuild(t *testing.T) { }) } -//nolint:paralleltest func TestWorkspaceBuildTimings(t *testing.T) { + t.Parallel() + // Setup the test environment with a template and version db, pubsub := dbtestutil.NewDB(t) client := coderdtest.New(t, &coderdtest.Options{ @@ -1237,10 +1238,13 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) } - //nolint:paralleltest t.Run("NonExistentBuild", func(t *testing.T) { - // When: fetching an inexistent build + t.Parallel() + + // Given: a non-existent build buildID := uuid.New() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) _, err := client.WorkspaceBuildTimings(ctx, buildID) @@ -1250,10 +1254,13 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Contains(t, err.Error(), "not found") }) - //nolint:paralleltest t.Run("EmptyTimings", func(t *testing.T) { - // When: fetching timings for a build with no timings + t.Parallel() + + // Given: a build with no timings build := makeBuild() + + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) @@ -1264,25 +1271,27 @@ func TestWorkspaceBuildTimings(t *testing.T) { require.Empty(t, res.AgentScriptTimings) }) - //nolint:paralleltest t.Run("ProvisionerTimings", func(t *testing.T) { - // When: fetching timings for a build with provisioner timings + t.Parallel() + + // Given: a build with provisioner timings build := makeBuild() provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.ProvisionerTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.ProvisionerTimings, 5) for i := range res.ProvisionerTimings { timingRes := res.ProvisionerTimings[i] genTiming := provisionerTimings[i] require.Equal(t, genTiming.Resource, timingRes.Resource) require.Equal(t, genTiming.Action, timingRes.Action) - require.Equal(t, string(genTiming.Stage), timingRes.Stage) + require.Equal(t, string(genTiming.Stage), string(timingRes.Stage)) require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String()) require.Equal(t, genTiming.Source, timingRes.Source) require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) @@ -1290,9 +1299,10 @@ func TestWorkspaceBuildTimings(t *testing.T) { } }) - //nolint:paralleltest t.Run("AgentScriptTimings", func(t *testing.T) { - // When: fetching timings for a build with agent script timings + t.Parallel() + + // Given: a build with agent script timings build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1305,27 +1315,31 @@ func TestWorkspaceBuildTimings(t *testing.T) { }) agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5) - // Then: return a response with the expected timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) - require.Len(t, res.AgentScriptTimings, 5) + // Then: return a response with the expected timings + require.Len(t, res.AgentScriptTimings, 5) for i := range res.AgentScriptTimings { timingRes := res.AgentScriptTimings[i] genTiming := agentScriptTimings[i] require.Equal(t, genTiming.ExitCode, timingRes.ExitCode) require.Equal(t, string(genTiming.Status), timingRes.Status) - require.Equal(t, string(genTiming.Stage), timingRes.Stage) + require.Equal(t, string(genTiming.Stage), string(timingRes.Stage)) require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli()) require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli()) + require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agent.Name, timingRes.WorkspaceAgentName) } }) - //nolint:paralleltest t.Run("NoAgentScripts", func(t *testing.T) { - // When: fetching timings for a build with no agent scripts + t.Parallel() + + // Given: a build with no agent scripts build := makeBuild() resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, @@ -1334,29 +1348,88 @@ func TestWorkspaceBuildTimings(t *testing.T) { ResourceID: resource.ID, }) - // Then: return a response with empty agent script timings + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) }) // Some workspaces might not have agents. It is improbable, but possible. - //nolint:paralleltest t.Run("NoAgents", func(t *testing.T) { - // When: fetching timings for a build with no agents + t.Parallel() + + // Given: a build with no agents build := makeBuild() dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ JobID: build.JobID, }) - // Then: return a response with empty agent script timings - // trigger build + // When: fetching timings for the build ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) res, err := client.WorkspaceBuildTimings(ctx, build.ID) require.NoError(t, err) + + // Then: return a response with empty agent script timings require.Empty(t, res.AgentScriptTimings) + require.Empty(t, res.AgentConnectionTimings) + }) + + t.Run("AgentConnectionTimings", func(t *testing.T) { + t.Parallel() + + // Given: a build with an agent + build := makeBuild() + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + + // When: fetching timings for the build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) + require.NoError(t, err) + + // Then: return a response with the expected timings + require.Len(t, res.AgentConnectionTimings, 1) + for i := range res.ProvisionerTimings { + timingRes := res.AgentConnectionTimings[i] + require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID) + require.Equal(t, agent.Name, timingRes.WorkspaceAgentName) + require.NotEmpty(t, timingRes.StartedAt) + require.NotEmpty(t, timingRes.EndedAt) + } + }) + + t.Run("MultipleAgents", func(t *testing.T) { + t.Parallel() + + // Given: a build with multiple agents + build := makeBuild() + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agents := make([]database.WorkspaceAgent, 5) + for i := range agents { + agents[i] = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) + } + + // When: fetching timings for the build + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + res, err := client.WorkspaceBuildTimings(ctx, build.ID) + require.NoError(t, err) + + // Then: return a response with the expected timings + require.Len(t, res.AgentConnectionTimings, 5) }) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 3cb00c313f4bf..761be48a9e488 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -175,28 +175,57 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID) return params, json.NewDecoder(res.Body).Decode(¶ms) } +type TimingStage string + +const ( + // Based on ProvisionerJobTimingStage + TimingStageInit TimingStage = "init" + TimingStagePlan TimingStage = "plan" + TimingStageGraph TimingStage = "graph" + TimingStageApply TimingStage = "apply" + // Based on WorkspaceAgentScriptTimingStage + TimingStageStart TimingStage = "start" + TimingStageStop TimingStage = "stop" + TimingStageCron TimingStage = "cron" + // Custom timing stage to represent the time taken to connect to an agent + TimingStageConnect TimingStage = "connect" +) + type ProvisionerTiming struct { - JobID uuid.UUID `json:"job_id" format:"uuid"` - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` - Stage string `json:"stage"` - Source string `json:"source"` - Action string `json:"action"` - Resource string `json:"resource"` + JobID uuid.UUID `json:"job_id" format:"uuid"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + Stage TimingStage `json:"stage"` + Source string `json:"source"` + Action string `json:"action"` + Resource string `json:"resource"` } type AgentScriptTiming struct { - StartedAt time.Time `json:"started_at" format:"date-time"` - EndedAt time.Time `json:"ended_at" format:"date-time"` - ExitCode int32 `json:"exit_code"` - Stage string `json:"stage"` - Status string `json:"status"` - DisplayName string `json:"display_name"` + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + ExitCode int32 `json:"exit_code"` + Stage TimingStage `json:"stage"` + Status string `json:"status"` + DisplayName string `json:"display_name"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` +} + +type AgentConnectionTiming struct { + StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt time.Time `json:"ended_at" format:"date-time"` + Stage TimingStage `json:"stage"` + WorkspaceAgentID string `json:"workspace_agent_id"` + WorkspaceAgentName string `json:"workspace_agent_name"` } type WorkspaceBuildTimings struct { ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"` - AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + // TODO: Consolidate agent-related timing metrics into a single struct when + // updating the API version + AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"` + AgentConnectionTimings []AgentConnectionTiming `json:"agent_connection_timings"` } func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (WorkspaceBuildTimings, error) { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index d49ab50fbb1ef..fc7ecdaa5d570 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1016,14 +1016,25 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -1033,7 +1044,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 384933e5795af..c7c1a729476c8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -349,6 +349,28 @@ | --------- | ------ | -------- | ------------ | ----------- | | `license` | string | true | | | +## codersdk.AgentConnectionTiming + +```json +{ + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- | +| `ended_at` | string | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | + ## codersdk.AgentScriptTiming ```json @@ -356,22 +378,26 @@ "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------- | ------- | -------- | ------------ | ----------- | -| `display_name` | string | false | | | -| `ended_at` | string | false | | | -| `exit_code` | integer | false | | | -| `stage` | string | false | | | -| `started_at` | string | false | | | -| `status` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- | +| `display_name` | string | false | | | +| `ended_at` | string | false | | | +| `exit_code` | integer | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | +| `status` | string | false | | | +| `workspace_agent_id` | string | false | | | +| `workspace_agent_name` | string | false | | | ## codersdk.AgentSubsystem @@ -4359,22 +4385,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------ | ------ | -------- | ------------ | ----------- | -| `action` | string | false | | | -| `ended_at` | string | false | | | -| `job_id` | string | false | | | -| `resource` | string | false | | | -| `source` | string | false | | | -| `stage` | string | false | | | -| `started_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------ | -------------------------------------------- | -------- | ------------ | ----------- | +| `action` | string | false | | | +| `ended_at` | string | false | | | +| `job_id` | string | false | | | +| `resource` | string | false | | | +| `source` | string | false | | | +| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | | +| `started_at` | string | false | | | ## codersdk.ProxyHealthReport @@ -5717,6 +5743,27 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | ------------------------ | | `UNSUPPORTED_WORKSPACES` | +## codersdk.TimingStage + +```json +"init" +``` + +### Properties + +#### Enumerated Values + +| Value | +| --------- | +| `init` | +| `plan` | +| `graph` | +| `apply` | +| `start` | +| `stop` | +| `cron` | +| `connect` | + ## codersdk.TokenConfig ```json @@ -7381,14 +7428,25 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -7398,7 +7456,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] @@ -7407,10 +7465,11 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | -| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | | -| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------------- | ------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------- | +| `agent_connection_timings` | array of [codersdk.AgentConnectionTiming](#codersdkagentconnectiontiming) | false | | | +| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | Agent script timings Consolidate agent-related timing metrics into a single struct when updating the API version | +| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | | ## codersdk.WorkspaceConnectionLatencyMS diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 283dab5db91b5..183a59ddd13a3 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1641,14 +1641,25 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ ```json { + "agent_connection_timings": [ + { + "ended_at": "2019-08-24T14:15:22Z", + "stage": "init", + "started_at": "2019-08-24T14:15:22Z", + "workspace_agent_id": "string", + "workspace_agent_name": "string" + } + ], "agent_script_timings": [ { "display_name": "string", "ended_at": "2019-08-24T14:15:22Z", "exit_code": 0, - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z", - "status": "string" + "status": "string", + "workspace_agent_id": "string", + "workspace_agent_name": "string" } ], "provisioner_timings": [ @@ -1658,7 +1669,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \ "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f", "resource": "string", "source": "string", - "stage": "string", + "stage": "init", "started_at": "2019-08-24T14:15:22Z" } ] diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 0e8981ba71ea4..45f7ac3bb7fe6 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -57,9 +57,18 @@ export const infiniteWorkspaceBuilds = ( }; }; -export const workspaceBuildTimings = (workspaceBuildId: string) => { +// We use readyAgentsCount to invalidate the query when an agent connects +export const workspaceBuildTimings = ( + workspaceBuildId: string, + readyAgentsCount: number, +) => { return { - queryKey: ["workspaceBuilds", workspaceBuildId, "timings"], + queryKey: [ + "workspaceBuilds", + workspaceBuildId, + "timings", + { readyAgentsCount }, + ], queryFn: () => API.workspaceBuildTimings(workspaceBuildId), }; }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 8bb28637e526a..619961c457b36 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,14 +32,25 @@ export interface AddLicenseRequest { readonly license: string; } +// From codersdk/workspacebuilds.go +export interface AgentConnectionTiming { + readonly started_at: string; + readonly ended_at: string; + readonly stage: TimingStage; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; +} + // From codersdk/workspacebuilds.go export interface AgentScriptTiming { readonly started_at: string; readonly ended_at: string; readonly exit_code: number; - readonly stage: string; + readonly stage: TimingStage; readonly status: string; readonly display_name: string; + readonly workspace_agent_id: string; + readonly workspace_agent_name: string; } // From codersdk/templates.go @@ -1104,7 +1115,7 @@ export interface ProvisionerTiming { readonly job_id: string; readonly started_at: string; readonly ended_at: string; - readonly stage: string; + readonly stage: TimingStage; readonly source: string; readonly action: string; readonly resource: string; @@ -1986,6 +1997,7 @@ export interface WorkspaceBuildParameter { export interface WorkspaceBuildTimings { readonly provisioner_timings: Readonly>; readonly agent_script_timings: Readonly>; + readonly agent_connection_timings: Readonly>; } // From codersdk/workspaces.go @@ -2226,6 +2238,10 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES" export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"] +// From codersdk/workspacebuilds.go +export type TimingStage = "apply" | "connect" | "cron" | "graph" | "init" | "plan" | "start" | "stop" +export const TimingStages: TimingStage[] = ["apply", "connect", "cron", "graph", "init", "plan", "start", "stop"] + // From codersdk/workspaces.go export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode" export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"] diff --git a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx index 3f1f7d761e748..b1c0bd89bc5fe 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ResourcesChart.tsx @@ -1,8 +1,5 @@ -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; +import { type Theme, useTheme } from "@emotion/react"; import { type FC, useState } from "react"; -import { Link } from "react-router-dom"; import { Bar } from "./Chart/Bar"; import { Chart, @@ -30,7 +27,7 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import type { Stage } from "./StagesChart"; type ResourceTiming = { name: string; @@ -40,14 +37,12 @@ type ResourceTiming = { }; export type ResourcesChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ResourceTiming[]; onBack: () => void; }; export const ResourcesChart: FC = ({ - category, stage, timings, onBack, @@ -71,11 +66,11 @@ export const ResourcesChart: FC = ({ @@ -89,7 +84,7 @@ export const ResourcesChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx index 64d97bff7cfdb..3824913a87b43 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/ScriptsChart.tsx @@ -27,7 +27,7 @@ import { makeTicks, mergeTimeRanges, } from "./Chart/utils"; -import type { StageCategory } from "./StagesChart"; +import type { Stage } from "./StagesChart"; type ScriptTiming = { name: string; @@ -37,14 +37,12 @@ type ScriptTiming = { }; export type ScriptsChartProps = { - category: StageCategory; - stage: string; + stage: Stage; timings: ScriptTiming[]; onBack: () => void; }; export const ScriptsChart: FC = ({ - category, stage, timings, onBack, @@ -66,11 +64,11 @@ export const ScriptsChart: FC = ({ @@ -84,7 +82,7 @@ export const ScriptsChart: FC = ({ - {stage} stage + {stage.name} stage {visibleTimings.map((t) => ( diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index dc5550dcfed98..cfb4285f77eda 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -1,6 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import ErrorSharp from "@mui/icons-material/ErrorSharp"; import InfoOutlined from "@mui/icons-material/InfoOutlined"; +import type { TimingStage } from "api/typesGenerated"; import type { FC } from "react"; import { Bar, ClickableBar } from "./Chart/Bar"; import { Blocks } from "./Chart/Blocks"; @@ -28,118 +29,34 @@ import { mergeTimeRanges, } from "./Chart/utils"; -export type StageCategory = { - name: string; - id: "provisioning" | "workspaceBoot"; -}; - -const stageCategories: StageCategory[] = [ - { - name: "provisioning", - id: "provisioning", - }, - { - name: "workspace boot", - id: "workspaceBoot", - }, -] as const; - export type Stage = { - name: string; - categoryID: StageCategory["id"]; + /** + * The name is used to identify the stage. + */ + name: TimingStage; + /** + * The value to display in the stage label. This can differ from the stage + * name to provide more context or clarity. + */ + label: string; + /** + * The section is used to group stages together. + */ + section: string; + /** + * The tooltip is used to provide additional information about the stage. + */ tooltip: Omit; }; -export const stages: Stage[] = [ - { - name: "init", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform initialization - - Download providers & modules. - - - ), - }, - }, - { - name: "plan", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform plan - - Compare state of desired vs actual resources and compute changes to - be made. - - - ), - }, - }, - { - name: "graph", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform graph - - List all resources in plan, used to update coderd database. - - - ), - }, - }, - { - name: "apply", - categoryID: "provisioning", - tooltip: { - title: ( - <> - Terraform apply - - Execute Terraform plan to create/modify/delete resources into - desired states. - - - ), - }, - }, - { - name: "start", - categoryID: "workspaceBoot", - tooltip: { - title: ( - <> - Start - - Scripts executed when the agent is starting. - - - ), - }, - }, -]; - type StageTiming = { - name: string; - /** + stage: Stage; /** * Represents the number of resources included in this stage that can be * inspected. This value is used to display individual blocks within the bar, * indicating that the stage consists of multiple resource time blocks. */ visibleResources: number; - /** - * Represents the category of the stage. This value is used to group stages - * together in the chart. For example, all provisioning stages are grouped - * together. - */ - categoryID: StageCategory["id"]; /** * Represents the time range of the stage. This value is used to calculate the * duration of the stage and to position the stage within the chart. This can @@ -155,7 +72,7 @@ type StageTiming = { export type StagesChartProps = { timings: StageTiming[]; - onSelectStage: (timing: StageTiming, category: StageCategory) => void; + onSelectStage: (stage: Stage) => void; }; export const StagesChart: FC = ({ @@ -167,27 +84,28 @@ export const StagesChart: FC = ({ ); const totalTime = calcDuration(totalRange); const [ticks, scale] = makeTicks(totalTime); + const sections = Array.from(new Set(timings.map((t) => t.stage.section))); return ( - {stageCategories.map((c) => { - const stagesInCategory = stages.filter( - (s) => s.categoryID === c.id, - ); + {sections.map((section) => { + const stages = timings + .filter((t) => t.stage.section === section) + .map((t) => t.stage); return ( - - {c.name} + + {section} - {stagesInCategory.map((stage) => ( + {stages.map((stage) => ( - {stage.name} + {stage.label} @@ -201,19 +119,19 @@ export const StagesChart: FC = ({ - {stageCategories.map((category) => { + {sections.map((section) => { const stageTimings = timings.filter( - (t) => t.categoryID === category.id, + (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) { return ( ); } @@ -223,18 +141,18 @@ export const StagesChart: FC = ({ return ( {/** We only want to expand stages with more than one resource */} {t.visibleResources > 1 ? ( { - onSelectStage(t, category); + onSelectStage(t.stage); }} > {t.error && ( @@ -281,3 +199,103 @@ const styles = { cursor: "pointer", }), } satisfies Record>; + +export const provisioningStages: Stage[] = [ + { + name: "init", + label: "init", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform initialization + + Download providers & modules. + + + ), + }, + }, + { + name: "plan", + label: "plan", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform plan + + Compare state of desired vs actual resources and compute changes to + be made. + + + ), + }, + }, + { + name: "graph", + label: "graph", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform graph + + List all resources in plan, used to update coderd database. + + + ), + }, + }, + { + name: "apply", + label: "apply", + section: "provisioning", + tooltip: { + title: ( + <> + Terraform apply + + Execute Terraform plan to create/modify/delete resources into + desired states. + + + ), + }, + }, +]; + +export const agentStages = (section: string): Stage[] => { + return [ + { + name: "connect", + label: "connect", + section, + tooltip: { + title: ( + <> + Connect + + Establish an RPC connection with the control plane. + + + ), + }, + }, + { + name: "start", + label: "run startup scripts", + section, + tooltip: { + title: ( + <> + Run startup scripts + + Execute each agent startup script. + + + ), + }, + }, + ]; +}; diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index f546e271395ab..5e3ccb86151d0 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -11,6 +11,7 @@ const meta: Meta = { defaultIsOpen: true, provisionerTimings: WorkspaceTimingsResponse.provisioner_timings, agentScriptTimings: WorkspaceTimingsResponse.agent_script_timings, + agentConnectionTimings: WorkspaceTimingsResponse.agent_connection_timings, }, parameters: { chromatic, @@ -32,6 +33,7 @@ export const Loading: Story = { args: { provisionerTimings: undefined, agentScriptTimings: undefined, + agentConnectionTimings: undefined, }, }; @@ -45,7 +47,7 @@ export const ClickToOpen: Story = { play: async ({ canvasElement }) => { const user = userEvent.setup(); const canvas = within(canvasElement); - await user.click(canvas.getByRole("button")); + await user.click(canvas.getByText("Build timeline", { exact: false })); await canvas.findByText("provisioning"); }, }; @@ -58,9 +60,9 @@ export const ClickToClose: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); await canvas.findByText("provisioning"); - await user.click(canvas.getByText("Provisioning time", { exact: false })); + await user.click(canvas.getByText("Build timeline", { exact: false })); await waitFor(() => - expect(canvas.getByText("workspace boot")).not.toBeVisible(), + expect(canvas.queryByText("workspace boot")).not.toBeInTheDocument(), ); }, }; @@ -96,7 +98,7 @@ export const NavigateToStartStage: Story = { const user = userEvent.setup(); const canvas = within(canvasElement); const detailsButton = canvas.getByRole("button", { - name: "View start details", + name: "View run startup scripts details", }); await user.click(detailsButton); await canvas.findByText("Startup Script"); diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 9e16e55bae36e..9fe12b122a35c 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -4,19 +4,27 @@ import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp"; import Button from "@mui/material/Button"; import Collapse from "@mui/material/Collapse"; import Skeleton from "@mui/material/Skeleton"; -import type { AgentScriptTiming, ProvisionerTiming } from "api/typesGenerated"; +import type { + AgentConnectionTiming, + AgentScriptTiming, + ProvisionerTiming, +} from "api/typesGenerated"; import { type FC, useState } from "react"; import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils"; import { ResourcesChart, isCoderResource } from "./ResourcesChart"; import { ScriptsChart } from "./ScriptsChart"; -import { type StageCategory, StagesChart, stages } from "./StagesChart"; +import { + type Stage, + StagesChart, + agentStages, + provisioningStages, +} from "./StagesChart"; type TimingView = | { name: "default" } | { name: "detailed"; - stage: string; - category: StageCategory; + stage: Stage; filter: string; }; @@ -24,20 +32,37 @@ type WorkspaceTimingsProps = { defaultIsOpen?: boolean; provisionerTimings: readonly ProvisionerTiming[] | undefined; agentScriptTimings: readonly AgentScriptTiming[] | undefined; + agentConnectionTimings: readonly AgentConnectionTiming[] | undefined; }; export const WorkspaceTimings: FC = ({ provisionerTimings = [], agentScriptTimings = [], + agentConnectionTimings = [], defaultIsOpen = false, }) => { const [view, setView] = useState({ name: "default" }); - const timings = [...provisionerTimings, ...agentScriptTimings]; + const timings = [ + ...provisionerTimings, + ...agentScriptTimings, + ...agentConnectionTimings, + ]; const [isOpen, setIsOpen] = useState(defaultIsOpen); const isLoading = timings.length === 0; + // All stages + const agentStageLabels = Array.from( + new Set( + agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), + ), + ); + const stages = [ + ...provisioningStages, + ...agentStageLabels.flatMap((a) => agentStages(a)), + ]; + const displayProvisioningTime = () => { - const totalRange = mergeTimeRanges(timings.map(extractRange)); + const totalRange = mergeTimeRanges(timings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); return humanizeDuration(totalDuration); }; @@ -81,7 +106,7 @@ export const WorkspaceTimings: FC = ({ const stageRange = stageTimings.length === 0 ? undefined - : mergeTimeRanges(stageTimings.map(extractRange)); + : mergeTimeRanges(stageTimings.map(toTimeRange)); // Prevent users from inspecting internal coder resources in // provisioner timings. @@ -93,67 +118,63 @@ export const WorkspaceTimings: FC = ({ }); return { + stage: s, range: stageRange, - name: s.name, - categoryID: s.categoryID, visibleResources: visibleResources.length, error: stageTimings.some( (t) => "status" in t && t.status === "exit_failure", ), }; })} - onSelectStage={(t, category) => { + onSelectStage={(stage) => { setView({ + stage, name: "detailed", - stage: t.name, - category, filter: "", }); }} /> )} - {view.name === "detailed" && - view.category.id === "provisioning" && ( - t.stage === view.stage) - .map((t) => { - return { - range: extractRange(t), + {view.name === "detailed" && ( + <> + {view.stage.section === "provisioning" && ( + t.stage === view.stage.name) + .map((t) => ({ + range: toTimeRange(t), name: t.resource, source: t.source, action: t.action, - }; - })} - category={view.category} - stage={view.stage} - onBack={() => { - setView({ name: "default" }); - }} - /> - )} - - {view.name === "detailed" && - view.category.id === "workspaceBoot" && ( - t.stage === view.stage) - .map((t) => { - return { - range: extractRange(t), - name: t.display_name, - status: t.status, - exitCode: t.exit_code, - }; - })} - category={view.category} - stage={view.stage} - onBack={() => { - setView({ name: "default" }); - }} - /> - )} + }))} + stage={view.stage} + onBack={() => { + setView({ name: "default" }); + }} + /> + )} + + {view.stage.name === "start" && ( + t.stage === view.stage.name) + .map((t) => { + return { + range: toTimeRange(t), + name: t.display_name, + status: t.status, + exitCode: t.exit_code, + }; + })} + stage={view.stage} + onBack={() => { + setView({ name: "default" }); + }} + /> + )} + + )} )} @@ -161,9 +182,10 @@ export const WorkspaceTimings: FC = ({ ); }; -const extractRange = ( - timing: ProvisionerTiming | AgentScriptTiming, -): TimeRange => { +const toTimeRange = (timing: { + started_at: string; + ended_at: string; +}): TimeRange => { return { startedAt: new Date(timing.started_at), endedAt: new Date(timing.ended_at), diff --git a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts index 828959f424107..589d95e6153da 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/storybookData.ts @@ -355,6 +355,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Startup Script", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650915Z", @@ -363,6 +365,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Dotfiles", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650715Z", @@ -371,6 +375,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Personalize", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650512Z", @@ -379,6 +385,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "install_slackme", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650659Z", @@ -387,6 +395,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Coder Login", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650666Z", @@ -395,6 +405,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "File Browser", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.652425Z", @@ -403,6 +415,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "code-server", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, { started_at: "2024-10-14T11:30:56.650423Z", @@ -411,6 +425,17 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = { stage: "start", status: "ok", display_name: "Git Clone", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", + }, + ], + agent_connection_timings: [ + { + started_at: "2024-10-14T11:30:55.650423Z", + ended_at: "2024-10-14T11:30:56.650423Z", + stage: "connect", + workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269", + workspace_agent_name: "dev", }, ], }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index c54ab25c1006c..5b9919474a620 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -267,8 +267,9 @@ export const Workspace: FC = ({ )} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 6859a5ada7882..cdb47f86f508c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC = ({ const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); // Build Timings. Fetch build timings only when the build job is completed. + const readyAgents = workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => a && a.lifecycle_state !== "starting"); const timingsQuery = useQuery({ - ...workspaceBuildTimings(workspace.latest_build.id), + ...workspaceBuildTimings(workspace.latest_build.id, readyAgents.length), enabled: Boolean(workspace.latest_build.job.completed_at), });