diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index cc0f7f57..eaa27277 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -56,6 +56,7 @@ const plotTimeline = (trials: Trial[], mode: string) => { Running: "green", Waiting: "gray", } + const runningKey = "Running" const lastTrials = trials.slice(-maxBars) // To only show last elements const minDatetime = new Date( @@ -65,13 +66,38 @@ const plotTimeline = (trials: Trial[], mode: string) => { ) ) ) - const maxDatetime = new Date( - Math.max( - ...lastTrials.map( - (t) => t.datetime_start?.getTime() ?? minDatetime.getTime() - ) - ) + const maxRunDuration = Math.max( + ...trials.map((t) => { + return t.datetime_start === undefined || t.datetime_complete === undefined + ? -Infinity + : t.datetime_complete.getTime() - t.datetime_start.getTime() + }) ) + const hasRunning = + (maxRunDuration === -Infinity && + trials.some((t) => t.state === runningKey)) || + trials.some((t) => { + if (t.state !== runningKey) { + return false + } + const now = new Date().getTime() + const start = t.datetime_start?.getTime() ?? now + // This is an ad-hoc handling to check if the trial is running. + // We do not check via `trialState` because some trials may have state=RUNNING, + // even if they are not running because of unexpected job kills. + // In this case, we would like to ensure that these trials will not squash the timeline plot + // for the other trials. + return now - start < maxRunDuration * 5 + }) + const maxDatetime = hasRunning + ? new Date() + : new Date( + Math.max( + ...lastTrials.map( + (t) => t.datetime_complete?.getTime() ?? minDatetime.getTime() + ) + ) + ) const layout: Partial = { margin: { l: 50, @@ -93,11 +119,20 @@ const plotTimeline = (trials: Trial[], mode: string) => { } const makeTrace = (bars: Trial[], state: string, color: string) => { - const starts = bars.map((b) => b.datetime_start ?? new Date()) - const completes = bars.map((b, i) => b.datetime_complete ?? starts[i]) + const isRunning = state === runningKey + // Waiting trials should not squash other trials, so use `maxDatetime` instead of `new Date()`. + const starts = bars.map((b) => b.datetime_start ?? maxDatetime) + const runDurations = bars.map((b, i) => { + const startTime = starts[i].getTime() + const completeTime = isRunning + ? maxDatetime.getTime() + : b.datetime_complete?.getTime() ?? startTime + // By using 1 as the min value, we can recognize these bars at least when zooming in. + return Math.max(1, completeTime - startTime) + }) const trace: Partial = { type: "bar", - x: starts.map((s, i) => completes[i].getTime() - s.getTime()), + x: runDurations, y: bars.map((b) => b.number), // @ts-ignore: To suppress ts(2322) base: starts.map((s) => s.toISOString()),