Skip to content

Commit af649ab

Browse files
committed
fix: report quick tasks in milliseconds
Durations under a second were rounded away, on the grounds that they are noise. The visible result was a column where only the slow rows carried a number, which reads as a fault rather than a decision, and it hid the difference between a task that finished instantly and one that never ran. Report milliseconds instead. A task that took three of them spent that time starting a process and doing nothing, which is worth seeing. The empty string had been doing double duty, standing for both "too quick to matter" and "never started". Only the second keeps it, decided by whether the task has a start time rather than by its duration.
1 parent 83b8b63 commit af649ab

3 files changed

Lines changed: 54 additions & 8 deletions

File tree

internal/tui/tui_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"path/filepath"
11+
"regexp"
1112
"strings"
1213
"testing"
1314
"time"
@@ -300,7 +301,7 @@ func TestTUIModelShowsMultipleIndependentRoots(t *testing.T) {
300301
m = updateTUIModel(t, m, started(4, 3, "unit"))
301302

302303
assert.Equal(t, []string{"build", "compile", "test", "unit"}, rowNames(m.taskRows()))
303-
list := ansi.Strip(m.taskList(40, 10))
304+
list := taskListWithoutDurations(t, m, 40, 10)
304305
assert.Contains(t, list, "● build\n└─ ● compile")
305306
assert.Contains(t, list, "● test\n└─ ● unit")
306307
}
@@ -1067,8 +1068,11 @@ func TestShortHelpKeepsTheWayOutOnANarrowTerminal(t *testing.T) {
10671068
func TestFormatDuration(t *testing.T) {
10681069
t.Parallel()
10691070

1070-
// Sub-second timings are noise in a task runner.
1071-
assert.Empty(t, formatDuration(400*time.Millisecond))
1071+
// A quick task reports milliseconds rather than nothing, so every row that
1072+
// ran carries a number.
1073+
assert.Equal(t, "0ms", formatDuration(0))
1074+
assert.Equal(t, "3ms", formatDuration(3*time.Millisecond))
1075+
assert.Equal(t, "400ms", formatDuration(400*time.Millisecond))
10721076
assert.Equal(t, "3.4s", formatDuration(3400*time.Millisecond))
10731077
assert.Equal(t, "12s", formatDuration(12*time.Second))
10741078
assert.Equal(t, "1m35s", formatDuration(95*time.Second))
@@ -1215,3 +1219,30 @@ func TestFooterPairsTheArrowKeys(t *testing.T) {
12151219
assert.Equal(t, []string{"↑/↓", "←/→"}, keys[len(keys)-2:], "the arrows are adjacent and last")
12161220
assert.Equal(t, "pane", bindings[len(bindings)-1].Help().Desc)
12171221
}
1222+
1223+
// taskListWithoutDurations renders the task pane with the right-aligned
1224+
// duration column trimmed, for assertions about names and tree structure.
1225+
func taskListWithoutDurations(t *testing.T, m tuiModel, width, height int) string {
1226+
t.Helper()
1227+
var lines []string
1228+
for line := range strings.SplitSeq(ansi.Strip(m.taskList(width, height)), "\n") {
1229+
lines = append(lines, strings.TrimRight(regexp.
1230+
MustCompile(`\s+\d[\dhms.]*$`).
1231+
ReplaceAllString(line, ""), " "))
1232+
}
1233+
return strings.Join(lines, "\n")
1234+
}
1235+
1236+
func TestTUIModelReportsNoDurationForTasksThatNeverRan(t *testing.T) {
1237+
t.Parallel()
1238+
1239+
m := newTUIModel(func() {})
1240+
m = updateTUIModel(t, m, started(1, 0, "build"))
1241+
m = updateTUIModel(t, m, scheduledUnder(2, 1, 1, "never-attempted"))
1242+
1243+
// A task that ran has a duration even if it was instant; one that never
1244+
// started has none, which is different from a duration of zero.
1245+
assert.Equal(t, "0ms", m.durationLabel(m.byID[1]))
1246+
assert.Empty(t, m.durationLabel(m.byID[2]))
1247+
assert.NotContains(t, ansi.Strip(m.taskList(40, 10)), "never-attempted 0ms")
1248+
}

internal/tui/view.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func (m tuiModel) taskList(width, height int) string {
213213
// The duration is right-aligned so durations line up and can be compared
214214
// down the column. It is dropped rather than squeezing the name on a
215215
// narrow pane.
216-
duration := formatDuration(m.elapsed(row.task))
216+
duration := m.durationLabel(row.task)
217217
available := width - lipgloss.Width(plainPrefix)
218218
durationWidth := 0
219219
if duration != "" && available-lipgloss.Width(duration)-1 >= minTaskNameWidth {
@@ -347,17 +347,31 @@ func taskNameStatus(name string, state taskState, width int, showStatus bool) (s
347347
return name, status
348348
}
349349

350+
// durationLabel is how long a task ran, or nothing at all for one that never
351+
// started. A pending or skipped task has no duration to report, as opposed to a
352+
// duration of zero.
353+
func (m tuiModel) durationLabel(task *tuiTask) string {
354+
if task.startedAt.IsZero() {
355+
return ""
356+
}
357+
return formatDuration(m.elapsed(task))
358+
}
359+
350360
// minTaskNameWidth is the room a name needs before a duration may take space
351361
// from it. Below that, knowing which task a row is matters more than knowing
352362
// how long it took.
353363
const minTaskNameWidth = 12
354364

355365
// formatDuration renders how long a task ran, short enough for a narrow pane.
356-
// Anything under a second is noise in a task runner, so it is left out.
366+
//
367+
// Quick tasks are reported in milliseconds rather than rounded away. A task that
368+
// took three milliseconds spent that time starting a process and doing nothing,
369+
// which is worth seeing, and a column where only the slow rows carry a number
370+
// reads as a fault rather than a decision.
357371
func formatDuration(d time.Duration) string {
358372
switch {
359373
case d < time.Second:
360-
return ""
374+
return fmt.Sprintf("%dms", d.Milliseconds())
361375
case d < 10*time.Second:
362376
return fmt.Sprintf("%.1fs", d.Seconds())
363377
case d < time.Minute:

website/src/next/docs/guide.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,8 +2865,9 @@ the following controls to scroll:
28652865
- Mouse wheel
28662866

28672867
Each task shows how long it ran, counting up while it is running and keeping its
2868-
final duration afterwards. On a narrow terminal the durations are dropped so
2869-
that task names keep their space.
2868+
final duration afterwards. Quick tasks are reported in milliseconds. A task that
2869+
has not started has no duration, which is not the same as a duration of zero. On
2870+
a narrow terminal the durations are dropped so that task names keep their space.
28702871

28712872
Press `f` to show the selected task's output fullscreen. Incoming output remains
28722873
visible; the view follows it while at the bottom and preserves the current

0 commit comments

Comments
 (0)