mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Add template build time stats (#4557)
- Expose time in Template pages - Show progress bar when building a workspace
This commit is contained in:
@@ -2,6 +2,7 @@ package metricscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
"github.com/coder/retry"
|
||||
)
|
||||
|
||||
// Cache holds the template DAU cache.
|
||||
// Cache holds the template metrics.
|
||||
// The aggregation queries responsible for these values can take up to a minute
|
||||
// on large deployments. Even in small deployments, aggregation queries can
|
||||
// take a few hundred milliseconds, which would ruin page load times and
|
||||
@@ -26,8 +27,9 @@ type Cache struct {
|
||||
database database.Store
|
||||
log slog.Logger
|
||||
|
||||
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
|
||||
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
|
||||
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
|
||||
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
|
||||
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]time.Duration]
|
||||
|
||||
done chan struct{}
|
||||
cancel func()
|
||||
@@ -128,8 +130,9 @@ func (c *Cache) refresh(ctx context.Context) error {
|
||||
}
|
||||
|
||||
var (
|
||||
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
|
||||
templateUniqueUsers = make(map[uuid.UUID]int)
|
||||
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
|
||||
templateUniqueUsers = make(map[uuid.UUID]int)
|
||||
templateAverageBuildTimeSec = make(map[uuid.UUID]time.Duration)
|
||||
)
|
||||
for _, template := range templates {
|
||||
rows, err := c.database.GetTemplateDAUs(ctx, template.ID)
|
||||
@@ -138,9 +141,24 @@ func (c *Cache) refresh(ctx context.Context) error {
|
||||
}
|
||||
templateDAUs[template.ID] = convertDAUResponse(rows)
|
||||
templateUniqueUsers[template.ID] = countUniqueUsers(rows)
|
||||
templateAvgBuildTime, err := c.database.GetTemplateAverageBuildTime(ctx, database.GetTemplateAverageBuildTimeParams{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
StartTime: sql.NullTime{
|
||||
Time: database.Time(time.Now().AddDate(0, -30, 0)),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templateAverageBuildTimeSec[template.ID] = time.Duration(float64(time.Second) * templateAvgBuildTime)
|
||||
}
|
||||
c.templateDAUResponses.Store(&templateDAUs)
|
||||
c.templateUniqueUsers.Store(&templateUniqueUsers)
|
||||
c.templateAverageBuildTime.Store(&templateAverageBuildTimeSec)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -220,3 +238,18 @@ func (c *Cache) TemplateUniqueUsers(id uuid.UUID) (int, bool) {
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func (c *Cache) TemplateAverageBuildTime(id uuid.UUID) (time.Duration, bool) {
|
||||
m := c.templateAverageBuildTime.Load()
|
||||
if m == nil {
|
||||
// Data loading.
|
||||
return -1, false
|
||||
}
|
||||
|
||||
resp, ok := (*m)[id]
|
||||
if !ok || resp <= 0 {
|
||||
// No data or not enough builds.
|
||||
return -1, false
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package metricscache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,7 +21,7 @@ func date(year, month, day int) time.Time {
|
||||
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
func TestCache_TemplateUsers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
@@ -197,3 +198,153 @@ func TestCache(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clockTime(t time.Time, hour, minute, sec int) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), hour, minute, sec, t.Nanosecond(), t.Location())
|
||||
}
|
||||
|
||||
func TestCache_BuildTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
someDay := date(2022, 10, 1)
|
||||
|
||||
type jobParams struct {
|
||||
startedAt time.Time
|
||||
completedAt time.Time
|
||||
}
|
||||
|
||||
type args struct {
|
||||
rows []jobParams
|
||||
}
|
||||
type want struct {
|
||||
buildTime time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{"empty", args{}, want{-1}},
|
||||
{"one", args{
|
||||
rows: []jobParams{
|
||||
{
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 10),
|
||||
},
|
||||
},
|
||||
}, want{time.Second * 10},
|
||||
},
|
||||
{"two", args{
|
||||
rows: []jobParams{
|
||||
{
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 10),
|
||||
},
|
||||
{
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 50),
|
||||
},
|
||||
},
|
||||
}, want{time.Second * 50},
|
||||
},
|
||||
{"three", args{
|
||||
rows: []jobParams{
|
||||
{
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 10),
|
||||
},
|
||||
{
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 50),
|
||||
}, {
|
||||
startedAt: clockTime(someDay, 10, 1, 0),
|
||||
completedAt: clockTime(someDay, 10, 1, 20),
|
||||
},
|
||||
},
|
||||
}, want{time.Second * 20},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
var (
|
||||
db = databasefake.New()
|
||||
cache = metricscache.New(db, slogtest.Make(t, nil), testutil.IntervalFast)
|
||||
)
|
||||
|
||||
defer cache.Close()
|
||||
|
||||
template, err := db.InsertTemplate(ctx, database.InsertTemplateParams{
|
||||
ID: uuid.New(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
templateVersion, err := db.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
gotBuildTime, ok := cache.TemplateAverageBuildTime(template.ID)
|
||||
require.False(t, ok, "template shouldn't have loaded yet")
|
||||
require.EqualValues(t, -1, gotBuildTime)
|
||||
|
||||
for _, row := range tt.args.rows {
|
||||
_, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
job, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
|
||||
StartedAt: sql.NullTime{Time: row.startedAt, Valid: true},
|
||||
Types: []database.ProvisionerType{
|
||||
database.ProvisionerTypeEcho,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
JobID: job.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
|
||||
ID: job.ID,
|
||||
CompletedAt: sql.NullTime{Time: row.completedAt, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.want.buildTime > 0 {
|
||||
require.Eventuallyf(t, func() bool {
|
||||
_, ok := cache.TemplateAverageBuildTime(template.ID)
|
||||
return ok
|
||||
}, testutil.WaitShort, testutil.IntervalMedium,
|
||||
"TemplateDAUs never populated",
|
||||
)
|
||||
|
||||
gotBuildTime, ok = cache.TemplateAverageBuildTime(template.ID)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tt.want.buildTime, gotBuildTime)
|
||||
} else {
|
||||
require.Never(t, func() bool {
|
||||
_, ok := cache.TemplateAverageBuildTime(template.ID)
|
||||
return ok
|
||||
}, testutil.WaitShort/2, testutil.IntervalMedium,
|
||||
"TemplateDAUs never populated",
|
||||
)
|
||||
|
||||
gotBuildTime, ok = cache.TemplateAverageBuildTime(template.ID)
|
||||
require.False(t, ok)
|
||||
require.Less(t, gotBuildTime, time.Duration(0))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user