Skip to content

Commit efeb10e

Browse files
authored
Show duration instead of rate for slow progress (#7209)
## 📝 Summary <!-- Provide a concise summary of what this pull request is addressing. If this PR fixes any issues, list them here by number (e.g., Fixes #123). --> When using the progress bar the duration (e.g. `2 min, 2s per iter`) of each loop will be shown instead of the rate (e.g. `0.01 iter/s`) when progress is slow. ## 🔍 Description of Changes <!-- Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily. --> When progress is slow the current implementation of the progress bar is not very helpful as it is harder to interpret `0.01 iter/s` than `1 min 40s per iter`. When the rate is very low the rounding also makes `100s` appear the same as `100m` (both `0.01 iter/s`). This commit detects when the rate drops below 1 iteration per second and switches the display to show time per iteration instead. ## Comparison Table |Rate [iter/s] | Old | New | |-----|----------|----------| |10|10 iter/s | 10 iter/s | |1|1 iter/s | 1 iter/s | |0.5|0.5 iter/s | 2s per iter | |0.3267|0.33 iter/s | 3.06s per iter | |0.0751|0.08 iter/s | 13s per iter | |0.009|0.01 iter/s | 1m, 11s per iter | |0.00165|0.01 iter/s| 10m, 2s per iter| This uses the same humanizer that is used for the ETA, so the styles match. There is one small change to the humanizer: if the seconds are greater than 10, no decimal places are shown on the smaller unit, to avoid outputs like `10m, 2.04s`. ## 📋 Checklist - [X] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md). - [ ] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable). - [X] I have added tests for the changes made. - [X] I have run the code and verified that it works as expected.
1 parent 03ea308 commit efeb10e

File tree

6 files changed

+113
-40
lines changed

6 files changed

+113
-40
lines changed

examples/outputs/progress_bar.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
@app.cell
88
def _():
99
import marimo as mo
10-
return (mo,)
10+
import asyncio
11+
12+
return (mo, asyncio)
1113

1214

1315
@app.cell
@@ -18,19 +20,35 @@ def _(mo):
1820

1921

2022
@app.cell
21-
async def _(mo, rerun):
22-
import asyncio
23+
async def _(mo, asyncio, rerun):
2324
rerun
2425
for _ in mo.status.progress_bar(
2526
range(10),
2627
title="Loading",
2728
subtitle="Please wait",
2829
show_eta=True,
29-
show_rate=True
30+
show_rate=True,
3031
):
3132
await asyncio.sleep(0.5)
3233
return
3334

3435

36+
@app.cell
37+
def _(mo):
38+
rerun_slow = mo.ui.button(label="Rerun Slow")
39+
rerun_slow
40+
return (rerun_slow,)
41+
42+
43+
@app.cell
44+
async def _(mo, asyncio, rerun_slow):
45+
rerun_slow
46+
for _ in mo.status.progress_bar(
47+
range(2), title="Loading", subtitle="Please wait", show_eta=True, show_rate=True
48+
):
49+
await asyncio.sleep(12)
50+
return
51+
52+
3553
if __name__ == "__main__":
3654
app.run()

frontend/src/plugins/layout/ProgressPlugin.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ export const ProgressComponent = ({
9393

9494
const elements: React.ReactNode[] = [];
9595
if (rate) {
96-
elements.push(
97-
<span key="rate">{rate} iter/s</span>,
98-
<span key="spacer-rate">&middot;</span>,
99-
);
96+
if (rate < 1) {
97+
elements.push(<span key="rate">{prettyTime(1 / rate)} per iter</span>);
98+
} else {
99+
elements.push(<span key="rate">{rate} iter/s</span>);
100+
}
101+
102+
elements.push(<span key="spacer-rate">&middot;</span>);
100103
}
101104

102105
if (!hasCompleted && eta) {
@@ -170,6 +173,6 @@ export function prettyTime(seconds: number): string {
170173
language: "shortEn",
171174
largest: 2,
172175
spacer: "",
173-
maxDecimalPoints: 2,
176+
maxDecimalPoints: seconds < 10 ? 2 : 0,
174177
});
175178
}

frontend/src/plugins/layout/__test__/ProgressPlugin.test.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,41 @@
22
import { expect, test } from "vitest";
33
import { prettyTime } from "../ProgressPlugin";
44

5-
// examples of expected output
6-
test("prettyTime", () => {
7-
// exact
8-
expect(prettyTime(0)).toMatchInlineSnapshot('"0s"');
9-
expect(prettyTime(1)).toMatchInlineSnapshot('"1s"');
10-
expect(prettyTime(60)).toMatchInlineSnapshot('"1m"');
11-
expect(prettyTime(60 * 60)).toMatchInlineSnapshot('"1h"');
12-
expect(prettyTime(60 * 60 * 24)).toMatchInlineSnapshot('"1d"');
13-
14-
// decimal
15-
expect(prettyTime(0.5)).toMatchInlineSnapshot('"0.5s"');
16-
expect(prettyTime(1.5)).toMatchInlineSnapshot('"1.5s"');
17-
expect(prettyTime(60 * 1.5)).toMatchInlineSnapshot('"1m, 30s"');
18-
expect(prettyTime(60 * 60 * 1.5)).toMatchInlineSnapshot('"1h, 30m"');
19-
expect(prettyTime(60 * 60 * 24 * 1.5)).toMatchInlineSnapshot('"1d, 12h"');
20-
5+
const Cases: Array<[number, string]> = [
6+
// exact values
7+
[0, "0s"],
8+
[1, "1s"],
9+
[5, "5s"],
10+
[15, "15s"],
11+
[60, "1m"],
12+
[100, "1m, 40s"],
13+
[60 * 60, "1h"],
14+
[60 * 60 * 24, "1d"],
15+
[60 * 60 * 24 * 7, "1w"],
16+
[60 * 60 * 24 * 8, "1w, 1d"],
17+
[60 * 60 * 24 * 30, "4w, 2d"],
18+
[60 * 60 * 24 * 366, "1y, 18h"],
19+
[60 * 60 * 24 * 466, "1y, 3mo"],
20+
// decimal values
21+
[0.5, "0.5s"],
22+
[1.5, "1.5s"],
23+
[5.2, "5.2s"],
24+
[5.33, "5.33s"],
25+
[15.2, "15s"],
26+
[60 * 1.5, "1m, 30s"],
27+
[100.2, "1m, 40s"],
28+
[60 * 60 * 1.5, "1h, 30m"],
29+
[60 * 60 * 24 * 1.5, "1d, 12h"],
2130
// edge cases
22-
expect(prettyTime(0)).toMatchInlineSnapshot('"0s"');
23-
expect(prettyTime(0.0001)).toMatchInlineSnapshot('"0s"');
24-
expect(prettyTime(0.001)).toMatchInlineSnapshot('"0s"');
25-
expect(prettyTime(0.01)).toMatchInlineSnapshot('"0.01s"');
26-
});
31+
[0, "0s"],
32+
[0.0001, "0s"],
33+
[0.001, "0s"],
34+
[0.01, "0.01s"],
35+
];
36+
37+
// generate one test per pair
38+
for (const [input, expected] of Cases) {
39+
test(`prettyTime(${input}) → ${expected}`, () => {
40+
expect(prettyTime(input)).toBe(expected);
41+
});
42+
}

marimo/_plugins/stateless/status/_progress.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ def _calculate_rate(self) -> Optional[float]:
135135
if diff == 0:
136136
return None
137137
rate = self.current / diff
138-
return round(rate, 2)
138+
# If rate is less than 1, return as is (e.g., 0.5)
139+
# As the UI will format it as "2s per iter"
140+
return round(rate, 2) if rate >= 1 else rate
139141

140142
def _get_rate(self) -> Optional[float]:
141143
if self.show_rate:
@@ -309,7 +311,7 @@ class progress_bar(Generic[S]):
309311
completion_title (str, optional): Optional title to show during completion.
310312
completion_subtitle (str, optional): Optional subtitle to show during completion.
311313
total (int, optional): Optional total number of items to iterate over.
312-
show_rate (bool, optional): If True, show the rate of progress (items per second).
314+
show_rate (bool, optional): If True, show the rate of progress (items per second or duration per iteration).
313315
show_eta (bool, optional): If True, show the estimated time of completion.
314316
remove_on_exit (bool, optional): If True, remove the progress bar from output on exit.
315317
disabled (bool, optional): If True, disable the progress bar.

marimo/_smoke_tests/async_iterator.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import marimo
22

3-
__generated_with = "0.15.3"
3+
__generated_with = "0.17.8"
44
app = marimo.App(width="medium")
55

66

@@ -14,26 +14,30 @@ async def sleep(seconds):
1414

1515

1616
@app.cell
17-
async def _() -> None:
17+
async def _():
1818
import marimo as mo
1919

2020
async def test_progress_async() -> None:
21-
2221
ait = sleep([0.3, 0.2, 0.1])
2322
result = [s async for s in mo.status.progress_bar(ait, total=3)]
2423
assert result == [0.1, 0.2, 0.3]
2524

2625
await test_progress_async()
27-
return
26+
return (mo,)
2827

2928

3029
@app.cell
31-
def _() -> None:
32-
return
30+
async def _(mo):
31+
async def test_progress_slow_async() -> None:
32+
test_durations = [250, 35, 10, 1.5]
33+
ait = sleep(test_durations)
34+
result = [
35+
s async for s in mo.status.progress_bar(ait, total=len(test_durations))
36+
]
3337

38+
assert result == sorted(test_durations)
3439

35-
@app.cell
36-
def _() -> None:
40+
await test_progress_slow_async()
3741
return
3842

3943

tests/_plugins/stateless/status/test_progress.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,36 @@ def test_update_progress(mock_flush: Any) -> None:
6767
mock_flush.assert_called_once()
6868

6969

70+
# Test update_progress method slow
71+
@patch("marimo._runtime.output._output.flush")
72+
def test_update_progress_slowly(mock_flush: Any) -> None:
73+
progress = _Progress(
74+
title="Test",
75+
subtitle="Running Slowly",
76+
total=5,
77+
show_rate=True,
78+
show_eta=True,
79+
)
80+
81+
# Mock sleep 120 seconds
82+
with patch("time.time", return_value=progress.start_time + 120):
83+
progress.update_progress(
84+
increment=1, title="Updated", subtitle="Still Running Slowly"
85+
)
86+
87+
rate = progress._get_rate()
88+
eta = progress._get_eta()
89+
90+
assert progress.current == 1
91+
assert progress.title == "Updated"
92+
assert progress.subtitle == "Still Running Slowly"
93+
assert rate is not None
94+
assert rate > 0.0
95+
assert eta is not None
96+
assert eta > 0.0
97+
mock_flush.assert_called_once()
98+
99+
70100
# Test update_progress without arguments
71101
@patch("marimo._runtime.output._output.flush")
72102
def test_update_progress_no_args(mock_flush: Any) -> None:

0 commit comments

Comments
 (0)