site: support high build time variation in progress bar (#4941)

This commit is contained in:
Ammar Bandukwala
2022-11-17 10:56:56 -06:00
committed by GitHub
parent 60cec022eb
commit acf34d4295
15 changed files with 187 additions and 120 deletions

View File

@ -300,17 +300,18 @@ func (q *fakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg datab
}
}
tryMedian := func(fs []float64) float64 {
tryPercentile := func(fs []float64, p float64) float64 {
if len(fs) == 0 {
return -1
}
sort.Float64s(fs)
return fs[len(fs)/2]
return fs[int(float64(len(fs))*p/100)]
}
var row database.GetTemplateAverageBuildTimeRow
row.DeleteMedian = tryMedian(deleteTimes)
row.StopMedian = tryMedian(stopTimes)
row.StartMedian = tryMedian(startTimes)
row.Delete50, row.Delete95 = tryPercentile(deleteTimes, 50), tryPercentile(deleteTimes, 95)
row.Stop50, row.Stop95 = tryPercentile(stopTimes, 50), tryPercentile(stopTimes, 95)
row.Start50, row.Start95 = tryPercentile(startTimes, 50), tryPercentile(startTimes, 95)
return row, nil
}

View File

@ -3090,9 +3090,12 @@ ORDER BY
SELECT
-- Postgres offers no clear way to DRY this short of a function or other
-- complexities.
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_median
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_50,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
FROM build_times
`
@ -3102,15 +3105,25 @@ type GetTemplateAverageBuildTimeParams struct {
}
type GetTemplateAverageBuildTimeRow struct {
StartMedian float64 `db:"start_median" json:"start_median"`
StopMedian float64 `db:"stop_median" json:"stop_median"`
DeleteMedian float64 `db:"delete_median" json:"delete_median"`
Start50 float64 `db:"start_50" json:"start_50"`
Stop50 float64 `db:"stop_50" json:"stop_50"`
Delete50 float64 `db:"delete_50" json:"delete_50"`
Start95 float64 `db:"start_95" json:"start_95"`
Stop95 float64 `db:"stop_95" json:"stop_95"`
Delete95 float64 `db:"delete_95" json:"delete_95"`
}
func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) {
row := q.db.QueryRowContext(ctx, getTemplateAverageBuildTime, arg.TemplateID, arg.StartTime)
var i GetTemplateAverageBuildTimeRow
err := row.Scan(&i.StartMedian, &i.StopMedian, &i.DeleteMedian)
err := row.Scan(
&i.Start50,
&i.Stop50,
&i.Delete50,
&i.Start95,
&i.Stop95,
&i.Delete95,
)
return i, err
}

View File

@ -142,8 +142,11 @@ ORDER BY
SELECT
-- Postgres offers no clear way to DRY this short of a function or other
-- complexities.
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_median,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_median
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_50,
coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_50,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_95,
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
FROM build_times
;

View File

@ -242,7 +242,11 @@ func (c *Cache) TemplateUniqueUsers(id uuid.UUID) (int, bool) {
}
func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeStats {
var unknown codersdk.TemplateBuildTimeStats
unknown := codersdk.TemplateBuildTimeStats{
codersdk.WorkspaceTransitionStart: {},
codersdk.WorkspaceTransitionStop: {},
codersdk.WorkspaceTransitionDelete: {},
}
m := c.templateAverageBuildTime.Load()
if m == nil {
@ -256,7 +260,7 @@ func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeS
return unknown
}
convertMedian := func(m float64) *int64 {
convertMillis := func(m float64) *int64 {
if m <= 0 {
return nil
}
@ -265,8 +269,17 @@ func (c *Cache) TemplateBuildTimeStats(id uuid.UUID) codersdk.TemplateBuildTimeS
}
return codersdk.TemplateBuildTimeStats{
StartMillis: convertMedian(resp.StartMedian),
StopMillis: convertMedian(resp.StopMedian),
DeleteMillis: convertMedian(resp.DeleteMedian),
codersdk.WorkspaceTransitionStart: {
P50: convertMillis(resp.Start50),
P95: convertMillis(resp.Start95),
},
codersdk.WorkspaceTransitionStop: {
P50: convertMillis(resp.Stop50),
P95: convertMillis(resp.Stop95),
},
codersdk.WorkspaceTransitionDelete: {
P50: convertMillis(resp.Delete50),
P95: convertMillis(resp.Delete95),
},
}
}

View File

@ -7,7 +7,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
@ -204,6 +203,12 @@ 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 requireBuildTimeStatsEmpty(t *testing.T, stats codersdk.TemplateBuildTimeStats) {
require.Empty(t, stats[codersdk.WorkspaceTransitionStart])
require.Empty(t, stats[codersdk.WorkspaceTransitionStop])
require.Empty(t, stats[codersdk.WorkspaceTransitionDelete])
}
func TestCache_BuildTime(t *testing.T) {
t.Parallel()
@ -296,7 +301,7 @@ func TestCache_BuildTime(t *testing.T) {
require.NoError(t, err)
gotStats := cache.TemplateBuildTimeStats(template.ID)
require.Empty(t, gotStats, "should not have loaded yet")
requireBuildTimeStatsEmpty(t, gotStats)
for _, row := range tt.args.rows {
_, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
@ -328,36 +333,32 @@ func TestCache_BuildTime(t *testing.T) {
}
if tt.want.loads {
wantTransition := codersdk.WorkspaceTransition(tt.args.transition)
require.Eventuallyf(t, func() bool {
stats := cache.TemplateBuildTimeStats(template.ID)
return stats != codersdk.TemplateBuildTimeStats{}
return stats[wantTransition] != codersdk.TransitionStats{}
}, testutil.WaitLong, testutil.IntervalMedium,
"BuildTime never populated",
)
gotStats = cache.TemplateBuildTimeStats(template.ID)
if tt.args.transition == database.WorkspaceTransitionDelete {
require.Nil(t, gotStats.StopMillis)
require.Nil(t, gotStats.StartMillis)
require.Equal(t, tt.want.buildTimeMs, *gotStats.DeleteMillis)
}
if tt.args.transition == database.WorkspaceTransitionStart {
require.Nil(t, gotStats.StopMillis)
require.Nil(t, gotStats.DeleteMillis)
require.Equal(t, tt.want.buildTimeMs, *gotStats.StartMillis)
}
if tt.args.transition == database.WorkspaceTransitionStop {
require.Nil(t, gotStats.StartMillis)
require.Nil(t, gotStats.DeleteMillis)
require.Equal(t, tt.want.buildTimeMs, *gotStats.StopMillis)
for transition, stats := range gotStats {
if transition == wantTransition {
require.Equal(t, tt.want.buildTimeMs, *stats.P50)
} else {
require.Empty(
t, stats, "%v", transition,
)
}
}
} else {
var stats codersdk.TemplateBuildTimeStats
require.Never(t, func() bool {
stats := cache.TemplateBuildTimeStats(template.ID)
return !assert.Empty(t, stats)
stats = cache.TemplateBuildTimeStats(template.ID)
requireBuildTimeStatsEmpty(t, stats)
return t.Failed()
}, testutil.WaitShort/2, testutil.IntervalMedium,
"BuildTimeStats populated",
"BuildTimeStats populated", stats,
)
}
})

View File

@ -536,7 +536,7 @@ func TestTemplateMetrics(t *testing.T) {
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Equal(t, -1, template.ActiveUserCount)
require.Empty(t, template.BuildTimeStats)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@ -607,7 +607,7 @@ func TestTemplateMetrics(t *testing.T) {
require.Eventuallyf(t, func() bool {
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
startMs := template.BuildTimeStats.StartMillis
startMs := template.BuildTimeStats[codersdk.WorkspaceTransitionStart].P50
return startMs != nil && *startMs > 1
},
testutil.WaitShort, testutil.IntervalFast,