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

This commit is contained in:
Ammar Bandukwala
2022-11-17 16:56:56 +00:00
committed by GitHub
parent 60cec022eb
commit acf34d4295
15 changed files with 187 additions and 120 deletions
+10 -5
View File
@@ -651,11 +651,10 @@ export interface TemplateACL {
}
// From codersdk/templates.go
export interface TemplateBuildTimeStats {
readonly start_ms?: number
readonly stop_ms?: number
readonly delete_ms?: number
}
export type TemplateBuildTimeStats = Record<
WorkspaceTransition,
TransitionStats
>
// From codersdk/templates.go
export interface TemplateDAUsResponse {
@@ -697,6 +696,12 @@ export interface TraceConfig {
readonly capture_logs: DeploymentConfigField<boolean>
}
// From codersdk/templates.go
export interface TransitionStats {
readonly P50?: number
readonly P95?: number
}
// From codersdk/templates.go
export interface UpdateActiveTemplateVersion {
readonly id: string
@@ -42,7 +42,7 @@ export const TemplateStats: FC<TemplateStatsProps> = ({
/>
<StatsItem
label={Language.buildTimeLabel}
value={formatTemplateBuildTime(template.build_time_stats.start_ms)}
value={formatTemplateBuildTime(template.build_time_stats.start.P50)}
/>
<StatsItem
label={Language.activeVersionLabel}
+5 -9
View File
@@ -19,7 +19,7 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
import { AlertBanner } from "../AlertBanner/AlertBanner"
import { useTranslation } from "react-i18next"
import {
EstimateTransitionTime,
ActiveTransition,
WorkspaceBuildProgress,
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
import { AgentRow } from "components/Resources/AgentRow"
@@ -115,13 +115,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
/>
)
let buildTimeEstimate: number | undefined = undefined
let isTransitioning: boolean | undefined = undefined
let transitionStats: TypesGen.TransitionStats | undefined = undefined
if (template !== undefined) {
;[buildTimeEstimate, isTransitioning] = EstimateTransitionTime(
template,
workspace,
)
transitionStats = ActiveTransition(template, workspace)
}
return (
@@ -195,10 +191,10 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate={handleUpdate}
/>
{isTransitioning !== undefined && isTransitioning && (
{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
buildEstimate={buildTimeEstimate}
transitionStats={transitionStats}
/>
)}
@@ -21,7 +21,10 @@ const Template: Story<WorkspaceBuildProgressProps> = (args) => (
export const Starting = Template.bind({})
Starting.args = {
buildEstimate: 10000,
transitionStats: {
P50: 10000,
P95: 10010,
},
workspace: {
...MockStartingWorkspace,
latest_build: {
@@ -39,11 +42,17 @@ Starting.args = {
export const StartingUnknown = Template.bind({})
StartingUnknown.args = {
...Starting.args,
buildEstimate: undefined,
transitionStats: undefined,
}
export const StartingPassedEstimate = Template.bind({})
StartingPassedEstimate.args = {
...Starting.args,
buildEstimate: 1000,
transitionStats: { P50: 1000, P95: 1000 },
}
export const StartingHighVariaton = Template.bind({})
StartingHighVariaton.args = {
...Starting.args,
transitionStats: { P50: 10000, P95: 20000 },
}
@@ -1,6 +1,6 @@
import LinearProgress from "@material-ui/core/LinearProgress"
import makeStyles from "@material-ui/core/styles/makeStyles"
import { Template, Workspace } from "api/typesGenerated"
import { TransitionStats, Template, Workspace } from "api/typesGenerated"
import dayjs, { Dayjs } from "dayjs"
import { FC, useEffect, useState } from "react"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
@@ -9,71 +9,99 @@ import duration from "dayjs/plugin/duration"
dayjs.extend(duration)
// ActiveTransition gets the build estimate for the workspace,
// if it is in a transition state.
export const ActiveTransition = (
template: Template,
workspace: Workspace,
): TransitionStats | undefined => {
const status = workspace.latest_build.status
switch (status) {
case "starting":
return template.build_time_stats.start
case "stopping":
return template.build_time_stats.stop
case "deleting":
return template.build_time_stats.delete
default:
return undefined
}
}
const estimateFinish = (
startedAt: Dayjs,
buildEstimate: number,
): [number, string] => {
const realPercentage = dayjs().diff(startedAt) / buildEstimate
p50: number,
p95: number,
): [number | undefined, string] => {
const sinceStart = dayjs().diff(startedAt)
const secondsLeft = (est: number) =>
Math.max(
Math.ceil(dayjs.duration((1 - sinceStart / est) * est).asSeconds()),
0,
)
const maxPercentage = 1
if (realPercentage > maxPercentage) {
return [maxPercentage * 100, "Any moment now..."]
}
const lowGuess = secondsLeft(p50)
const highGuess = secondsLeft(p95)
return [
realPercentage * 100,
`~${Math.ceil(
dayjs.duration((1 - realPercentage) * buildEstimate).asSeconds(),
)} seconds remaining...`,
const anyMomentNow: [number | undefined, string] = [
undefined,
"Any moment now...",
]
const p50percent = (sinceStart * 100) / p50
if (highGuess <= 0) {
return anyMomentNow
}
const diff = highGuess - lowGuess
if (diff < 3) {
// If there is sufficient consistency, keep display simple.
return [p50percent, `${highGuess} seconds remaining...`]
}
return [p50percent, `Up to ${highGuess} seconds remaining...`]
}
export interface WorkspaceBuildProgressProps {
workspace: Workspace
buildEstimate?: number
}
// EstimateTransitionTime gets the build estimate for the workspace,
// if it is in a transition state.
export const EstimateTransitionTime = (
template: Template,
workspace: Workspace,
): [number | undefined, boolean] => {
switch (workspace.latest_build.status) {
case "starting":
return [template.build_time_stats.start_ms, true]
case "stopping":
return [template.build_time_stats.stop_ms, true]
case "deleting":
return [template.build_time_stats.delete_ms, true]
default:
// Not in a transition state
return [undefined, false]
}
transitionStats: TransitionStats
}
export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
workspace,
buildEstimate,
transitionStats: transitionStats,
}) => {
const styles = useStyles()
const job = workspace.latest_build.job
const [progressValue, setProgressValue] = useState<number | undefined>(0)
const [progressText, setProgressText] = useState<string | undefined>(
"Finding ETA...",
)
// By default workspace is updated every second, which can cause visual stutter
// when the build estimate is a few seconds. The timer ensures no observable
// stutter in all cases.
useEffect(() => {
const updateProgress = () => {
if (job.status !== "running" || buildEstimate === undefined) {
if (
job.status !== "running" ||
transitionStats.P50 === undefined ||
transitionStats.P95 === undefined
) {
setProgressValue(undefined)
setProgressText(undefined)
return
}
const est = estimateFinish(dayjs(job.started_at), buildEstimate)[0]
const [est, text] = estimateFinish(
dayjs(job.started_at),
transitionStats.P50,
transitionStats.P95,
)
setProgressValue(est)
setProgressText(text)
}
setTimeout(updateProgress, 5)
}, [progressValue, job, buildEstimate])
}, [progressValue, job, transitionStats])
return (
<div className={styles.stack}>
@@ -84,9 +112,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
// (e.g. the build isn't yet running). If we flicker from the
// indeterminate bar to the determinate bar, the vigilant user
// perceives the bar jumping from 100% to 0%.
progressValue !== undefined &&
progressValue < 100 &&
buildEstimate !== undefined
progressValue !== undefined && progressValue < 100
? "determinate"
: "indeterminate"
}
@@ -97,17 +123,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
/>
<div className={styles.barHelpers}>
<div className={styles.label}>{`Build ${job.status}`}</div>
<div className={styles.label}>
{(() => {
if (job.status !== "running") {
return ""
} else if (buildEstimate !== undefined) {
return estimateFinish(dayjs(job.started_at), buildEstimate)[1]
} else {
return "Unknown ETA"
}
})()}
</div>
<div className={styles.label}>{progressText}</div>
</div>
</div>
)
@@ -241,7 +241,7 @@ export const TemplatesPageView: FC<
style={{ color: theme.palette.text.secondary }}
>
{formatTemplateBuildTime(
template.build_time_stats.start_ms,
template.build_time_stats.start.P50,
)}
</span>
</TableCellLink>
+12 -3
View File
@@ -193,9 +193,18 @@ export const MockTemplate: TypesGen.Template = {
workspace_owner_count: 2,
active_user_count: 1,
build_time_stats: {
start_ms: 1000,
stop_ms: 2000,
delete_ms: 3000,
start: {
P50: 1000,
P95: 1500,
},
stop: {
P50: 1000,
P95: 1500,
},
delete: {
P50: 1000,
P95: 1500,
},
},
description: "This is a test description.",
default_ttl_ms: 24 * 60 * 60 * 1000,