feat(site): add agent connection timings (#15276)

Local preview:

<img width="1260" alt="Screenshot 2024-10-29 at 16 16 01"
src="https://github.com/user-attachments/assets/10fdb20d-1f2a-4b0a-a8a1-171050ee620d">


Close https://github.com/coder/internal/issues/116

---------

Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Bruno Quaresma
2024-11-01 13:29:00 -03:00
committed by GitHub
co-authored by Danny Kopping
parent 18ef954a03
commit e232aee011
22 changed files with 747 additions and 304 deletions
+60 -2
View File
@@ -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"
+60 -2
View File
@@ -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"
+9 -7
View File
@@ -2894,13 +2894,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)
+23 -7
View File
@@ -5899,15 +5899,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
+16 -8
View File
@@ -11454,7 +11454,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
@@ -11464,13 +11468,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) {
@@ -11490,6 +11496,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context
&i.Stage,
&i.Status,
&i.DisplayName,
&i.WorkspaceAgentID,
&i.WorkspaceAgentName,
); err != nil {
return nil, err
}
+5 -1
View File
@@ -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
+34 -9
View File
@@ -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,
})
}
+95 -22
View File
@@ -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)
})
}
+43 -14
View File
@@ -175,28 +175,57 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID)
return params, json.NewDecoder(res.Body).Decode(&params)
}
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) {
+14 -3
View File
@@ -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"
}
]
+86 -27
View File
@@ -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
+14 -3
View File
@@ -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"
}
]
+11 -2
View File
@@ -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),
};
};
+18 -2
View File
@@ -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<Array<ProvisionerTiming>>;
readonly agent_script_timings: Readonly<Array<AgentScriptTiming>>;
readonly agent_connection_timings: Readonly<Array<AgentConnectionTiming>>;
}
// 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"]
@@ -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<ResourcesChartProps> = ({
category,
stage,
timings,
onBack,
@@ -71,11 +66,11 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
<ChartBreadcrumbs
breadcrumbs={[
{
label: category.name,
label: stage.section,
onClick: onBack,
},
{
label: stage,
label: stage.name,
},
]}
/>
@@ -89,7 +84,7 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
<ChartContent>
<YAxis>
<YAxisSection>
<YAxisHeader>{stage} stage</YAxisHeader>
<YAxisHeader>{stage.name} stage</YAxisHeader>
<YAxisLabels>
{visibleTimings.map((t) => (
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
@@ -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<ScriptsChartProps> = ({
category,
stage,
timings,
onBack,
@@ -66,11 +64,11 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
<ChartBreadcrumbs
breadcrumbs={[
{
label: category.name,
label: stage.section,
onClick: onBack,
},
{
label: stage,
label: stage.name,
},
]}
/>
@@ -84,7 +82,7 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
<ChartContent>
<YAxis>
<YAxisSection>
<YAxisHeader>{stage} stage</YAxisHeader>
<YAxisHeader>{stage.name} stage</YAxisHeader>
<YAxisLabels>
{visibleTimings.map((t) => (
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
@@ -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<TooltipProps, "children">;
};
export const stages: Stage[] = [
{
name: "init",
categoryID: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform initialization</TooltipTitle>
<TooltipShortDescription>
Download providers & modules.
</TooltipShortDescription>
</>
),
},
},
{
name: "plan",
categoryID: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform plan</TooltipTitle>
<TooltipShortDescription>
Compare state of desired vs actual resources and compute changes to
be made.
</TooltipShortDescription>
</>
),
},
},
{
name: "graph",
categoryID: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform graph</TooltipTitle>
<TooltipShortDescription>
List all resources in plan, used to update coderd database.
</TooltipShortDescription>
</>
),
},
},
{
name: "apply",
categoryID: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform apply</TooltipTitle>
<TooltipShortDescription>
Execute Terraform plan to create/modify/delete resources into
desired states.
</TooltipShortDescription>
</>
),
},
},
{
name: "start",
categoryID: "workspaceBoot",
tooltip: {
title: (
<>
<TooltipTitle>Start</TooltipTitle>
<TooltipShortDescription>
Scripts executed when the agent is starting.
</TooltipShortDescription>
</>
),
},
},
];
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<StagesChartProps> = ({
@@ -167,27 +84,28 @@ export const StagesChart: FC<StagesChartProps> = ({
);
const totalTime = calcDuration(totalRange);
const [ticks, scale] = makeTicks(totalTime);
const sections = Array.from(new Set(timings.map((t) => t.stage.section)));
return (
<Chart>
<ChartContent>
<YAxis>
{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 (
<YAxisSection key={c.id}>
<YAxisHeader>{c.name}</YAxisHeader>
<YAxisSection key={section}>
<YAxisHeader>{section}</YAxisHeader>
<YAxisLabels>
{stagesInCategory.map((stage) => (
{stages.map((stage) => (
<YAxisLabel
key={stage.name}
id={encodeURIComponent(stage.name)}
>
<span css={styles.stageLabel}>
{stage.name}
{stage.label}
<Tooltip {...stage.tooltip}>
<InfoOutlined css={styles.info} />
</Tooltip>
@@ -201,19 +119,19 @@ export const StagesChart: FC<StagesChartProps> = ({
</YAxis>
<XAxis ticks={ticks} scale={scale}>
{stageCategories.map((category) => {
{sections.map((section) => {
const stageTimings = timings.filter(
(t) => t.categoryID === category.id,
(t) => t.stage.section === section,
);
return (
<XAxisSection key={category.id}>
<XAxisSection key={section}>
{stageTimings.map((t) => {
// If the stage has no timing data, we just want to render an empty row
if (t.range === undefined) {
return (
<XAxisRow
key={t.name}
yAxisLabelId={encodeURIComponent(t.name)}
key={t.stage.name}
yAxisLabelId={encodeURIComponent(t.stage.name)}
/>
);
}
@@ -223,18 +141,18 @@ export const StagesChart: FC<StagesChartProps> = ({
return (
<XAxisRow
key={t.name}
yAxisLabelId={encodeURIComponent(t.name)}
key={t.stage.name}
yAxisLabelId={encodeURIComponent(t.stage.name)}
>
{/** We only want to expand stages with more than one resource */}
{t.visibleResources > 1 ? (
<ClickableBar
aria-label={`View ${t.name} details`}
aria-label={`View ${t.stage.label} details`}
scale={scale}
value={value}
offset={offset}
onClick={() => {
onSelectStage(t, category);
onSelectStage(t.stage);
}}
>
{t.error && (
@@ -281,3 +199,103 @@ const styles = {
cursor: "pointer",
}),
} satisfies Record<string, Interpolation<Theme>>;
export const provisioningStages: Stage[] = [
{
name: "init",
label: "init",
section: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform initialization</TooltipTitle>
<TooltipShortDescription>
Download providers & modules.
</TooltipShortDescription>
</>
),
},
},
{
name: "plan",
label: "plan",
section: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform plan</TooltipTitle>
<TooltipShortDescription>
Compare state of desired vs actual resources and compute changes to
be made.
</TooltipShortDescription>
</>
),
},
},
{
name: "graph",
label: "graph",
section: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform graph</TooltipTitle>
<TooltipShortDescription>
List all resources in plan, used to update coderd database.
</TooltipShortDescription>
</>
),
},
},
{
name: "apply",
label: "apply",
section: "provisioning",
tooltip: {
title: (
<>
<TooltipTitle>Terraform apply</TooltipTitle>
<TooltipShortDescription>
Execute Terraform plan to create/modify/delete resources into
desired states.
</TooltipShortDescription>
</>
),
},
},
];
export const agentStages = (section: string): Stage[] => {
return [
{
name: "connect",
label: "connect",
section,
tooltip: {
title: (
<>
<TooltipTitle>Connect</TooltipTitle>
<TooltipShortDescription>
Establish an RPC connection with the control plane.
</TooltipShortDescription>
</>
),
},
},
{
name: "start",
label: "run startup scripts",
section,
tooltip: {
title: (
<>
<TooltipTitle>Run startup scripts</TooltipTitle>
<TooltipShortDescription>
Execute each agent startup script.
</TooltipShortDescription>
</>
),
},
},
];
};
@@ -11,6 +11,7 @@ const meta: Meta<typeof WorkspaceTimings> = {
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");
@@ -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<WorkspaceTimingsProps> = ({
provisionerTimings = [],
agentScriptTimings = [],
agentConnectionTimings = [],
defaultIsOpen = false,
}) => {
const [view, setView] = useState<TimingView>({ 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<WorkspaceTimingsProps> = ({
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<WorkspaceTimingsProps> = ({
});
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" && (
<ResourcesChart
timings={provisionerTimings
.filter((t) => t.stage === view.stage)
.map((t) => {
return {
range: extractRange(t),
{view.name === "detailed" && (
<>
{view.stage.section === "provisioning" && (
<ResourcesChart
timings={provisionerTimings
.filter((t) => 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" });
}}
/>
)}
}))}
stage={view.stage}
onBack={() => {
setView({ name: "default" });
}}
/>
)}
{view.name === "detailed" &&
view.category.id === "workspaceBoot" && (
<ScriptsChart
timings={agentScriptTimings
.filter((t) => 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" });
}}
/>
)}
{view.stage.name === "start" && (
<ScriptsChart
timings={agentScriptTimings
.filter((t) => 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" });
}}
/>
)}
</>
)}
</div>
</Collapse>
)}
@@ -161,9 +182,10 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
);
};
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),
@@ -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",
},
],
};
+2 -1
View File
@@ -267,8 +267,9 @@ export const Workspace: FC<WorkspaceProps> = ({
)}
<WorkspaceTimings
agentScriptTimings={timings?.agent_script_timings}
provisionerTimings={timings?.provisioner_timings}
agentScriptTimings={timings?.agent_script_timings}
agentConnectionTimings={timings?.agent_connection_timings}
/>
</div>
</div>
@@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
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),
});