Skip to content

Commit e286ecf

Browse files
committed
docs(benchmarks): restyle chart, highlight fast + full progressbar2
Editorial-light theme (indigo brand): borderless panels, left-aligned titles, colored value labels, subtitle. Highlight both our variants in two shades -- fast (default) bold, full (widgets) muted -- and relabel panel B to 'progressbar2 (fast)' / 'progressbar2 (full)'. Bold both rows in report.md tables for consistency.
1 parent 94918a8 commit e286ecf

3 files changed

Lines changed: 117 additions & 30 deletions

File tree

benchmarks/chart.png

1.17 KB
Loading

benchmarks/report.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Python progress-bar library benchmark
22

3-
_Generated 2026-06-24 19:25. Subject: **progressbar2** (version 4.5.0)._
3+
_Generated 2026-06-27 03:03. Subject: **progressbar2** (version 4.5.0)._
44

55
Compares `progressbar2` against the most common alternatives across three independent dimensions. All rendered output is written to a real pseudo-terminal (pty) that is continuously drained, so every library believes it is attached to a TTY and actually draws — the comparison is apples-to-apples, not "is output suppressed when piped".
66

@@ -43,7 +43,7 @@ Rendering **forced on every single update** over **30,000** updates — i.e. the
4343

4444
| Library | Total time | Per rendered update | vs progressbar2 |
4545
|---|--:|--:|--:|
46-
| progressbar2-fast | 148.9 ms | 4.96 us | 0.19x |
46+
| **progressbar2-fast** | 148.9 ms | 4.96 us | 0.19x |
4747
| tqdm | 332.7 ms | 11.08 us | 0.44x |
4848
| **progressbar2** | 764.6 ms | 25.48 us | baseline |
4949
| rich | 5156.6 ms | 171.88 us | 6.75x |

benchmarks/report.py

Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,57 @@
1111

1212
matplotlib.use('Agg')
1313
import matplotlib.pyplot as plt # noqa: E402
14+
from matplotlib import font_manager # noqa: E402
1415

1516
HERE: str = os.path.dirname(os.path.abspath(__file__))
1617
SUBJECT: str = 'progressbar2'
17-
HIGHLIGHT: str = '#d62728' # progressbar2 bars
18-
OTHER: str = '#7f8fa6' # everyone else
18+
19+
# Our two variants share one brand hue so they read as siblings: the fast
20+
# default pops at full saturation, the full (widget) mode is a muted shade.
21+
# Every other library is a flat neutral grey.
22+
FAST: str = 'fast'
23+
FULL: str = 'full'
24+
OTHER: str = 'other'
25+
THEME: dict[str, typing.Any] = {
26+
'bg': '#ffffff',
27+
'fast': '#4f46e5', # bold indigo -> progressbar2 (fast), the default
28+
'full': '#c4b5fd', # muted indigo -> progressbar2 (full), widgets mode
29+
'other': '#d8dde6', # neutral grey -> every other library
30+
'text': '#1e2430',
31+
'subtext': '#5b6472',
32+
'grid': '#e7eaf0',
33+
'title': '#11151c',
34+
'bar_height': 0.6,
35+
}
36+
37+
38+
def _font() -> str:
39+
"""Prefer a clean sans; degrade gracefully where it is not installed."""
40+
have = {f.name for f in font_manager.fontManager.ttflist}
41+
for name in ('Helvetica Neue', 'Helvetica', 'Arial', 'DejaVu Sans'):
42+
if name in have:
43+
return name
44+
return 'sans-serif'
45+
46+
47+
def _classify(panel: str, key: str) -> str:
48+
"""Tag a library as our fast default, our full mode, or another lib."""
49+
if key == 'progressbar2-fast':
50+
return FAST
51+
if key == SUBJECT:
52+
# Panels A/C only benchmark the fast default; B splits fast vs full.
53+
return FAST if panel in ('A', 'C') else FULL
54+
return OTHER
55+
56+
57+
def _relabel(panel: str, key: str) -> str:
58+
"""Display name: spell out fast/full for our bars, raw key otherwise."""
59+
cls = _classify(panel, key)
60+
if cls == FAST:
61+
return 'progressbar2 (fast)'
62+
if cls == FULL:
63+
return 'progressbar2 (full)'
64+
return key
1965

2066

2167
def load() -> dict[str, typing.Any]:
@@ -32,62 +78,103 @@ def make_chart(data: dict[str, typing.Any]) -> str:
3278
b = data['scenario_b_forced_render']['libs']
3379
c = data['scenario_c_import_time']['libs']
3480

35-
panels: list[tuple[str, str, list[tuple[str, float]], bool]] = [
81+
panels: list[tuple[str, str, str, list[tuple[str, float]], bool]] = [
3682
(
37-
'A. Default iterator-wrap overhead\n(lower is faster)',
83+
'A',
84+
'Default iterator-wrap overhead',
3885
'nanoseconds added per iteration',
3986
_sorted({k: v['overhead_ns_per_iter'] for k, v in a.items()}),
4087
True,
4188
),
4289
(
43-
'B. Forced per-update render cost\n(lower is faster)',
90+
'B',
91+
'Forced per-update render cost',
4492
'microseconds per rendered update',
4593
_sorted({k: v['per_update_us'] for k, v in b.items()}),
4694
True,
4795
),
4896
(
49-
'C. Cold import time\n(lower is lighter)',
50-
'milliseconds (net of interpreter startup)',
97+
'C',
98+
'Cold import time',
99+
'milliseconds (net of startup)',
51100
_sorted({k: v['net_ms'] for k, v in c.items()}),
52101
False,
53102
),
54103
]
55104

56-
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
57-
for ax, (title, xlabel, pairs, logx) in zip(axes, panels):
58-
labels = [k for k, _ in pairs]
105+
color = {FAST: THEME['fast'], FULL: THEME['full'], OTHER: THEME['other']}
106+
107+
plt.rcParams['font.family'] = _font()
108+
fig, axes = plt.subplots(1, 3, figsize=(15.5, 5.2))
109+
fig.patch.set_facecolor(THEME['bg'])
110+
111+
for ax, (pid, ptitle, xlabel, pairs, logx) in zip(axes, panels):
112+
ax.set_facecolor(THEME['bg'])
113+
classes = [_classify(pid, k) for k, _ in pairs]
114+
labels = [_relabel(pid, k) for k, _ in pairs]
59115
values = [v for _, v in pairs]
60-
colors = [HIGHLIGHT if k == SUBJECT else OTHER for k in labels]
61-
ypos = range(len(labels))
62-
ax.barh(list(ypos), values, color=colors)
63-
ax.set_yticks(list(ypos))
64-
ax.set_yticklabels(labels)
116+
ypos = list(range(len(labels)))
117+
118+
ax.barh(
119+
ypos,
120+
values,
121+
height=THEME['bar_height'],
122+
color=[color[cls] for cls in classes],
123+
)
124+
ax.set_yticks(ypos)
125+
ax.set_yticklabels(labels, color=THEME['text'], fontsize=10)
65126
ax.invert_yaxis() # fastest at top
66-
ax.set_xlabel(xlabel)
67-
ax.set_title(title, fontsize=11, fontweight='bold')
127+
ax.set_xlabel(xlabel, color=THEME['subtext'], fontsize=9.5)
128+
ax.set_title(
129+
f'{pid}. {ptitle}',
130+
loc='left',
131+
fontsize=11.5,
132+
fontweight='bold',
133+
color=THEME['title'],
134+
pad=12,
135+
)
68136
if logx:
69137
ax.set_xscale('log')
70-
ax.grid(axis='x', linestyle=':', alpha=0.4)
138+
ax.grid(axis='x', color=THEME['grid'], linewidth=1)
139+
ax.set_axisbelow(True)
140+
for spine in ('top', 'right', 'left'):
141+
ax.spines[spine].set_visible(False)
142+
ax.spines['bottom'].set_color(THEME['grid'])
143+
ax.tick_params(colors=THEME['subtext'], length=0)
144+
ax.margins(x=0.2)
145+
71146
xmax = max(values)
72-
for y, val in zip(ypos, values):
147+
for y, val, cls in zip(ypos, values, classes):
73148
label = f'{val:.1f}' if val >= 1 else f'{val:.2f}'
74149
ax.text(
75-
val * 1.05 if logx else val + xmax * 0.01,
150+
val * 1.08 if logx else val + xmax * 0.015,
76151
y,
77152
label,
78153
va='center',
79-
fontsize=9,
154+
ha='left',
155+
fontsize=9.5,
156+
fontweight='normal' if cls == OTHER else 'bold',
157+
color=THEME['text'] if cls == OTHER else color[cls],
80158
)
81-
ax.margins(x=0.18)
82159

83160
fig.suptitle(
84161
'progressbar2 vs common Python progress-bar libraries',
85-
fontsize=14,
162+
fontsize=15,
86163
fontweight='bold',
164+
color=THEME['title'],
165+
y=0.975,
166+
)
167+
fig.text(
168+
0.5,
169+
0.915,
170+
'lower is faster / lighter — fastest at top',
171+
ha='center',
172+
fontsize=9.5,
173+
color=THEME['subtext'],
87174
)
88-
fig.tight_layout(rect=(0, 0, 1, 0.96))
175+
fig.tight_layout(rect=(0, 0, 1, 0.88))
89176
out = os.path.join(HERE, 'chart.png')
90-
fig.savefig(out, dpi=130)
177+
fig.savefig(out, dpi=140, facecolor=THEME['bg'])
91178
plt.close(fig)
92179
return out
93180

@@ -172,7 +259,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
172259
{k: vv['overhead_ns_per_iter'] for k, vv in a['libs'].items()}
173260
):
174261
vv = a['libs'][name]
175-
bold = '**' if name == SUBJECT else ''
262+
bold = '**' if name in (SUBJECT, 'progressbar2-fast') else ''
176263
w(
177264
f'| {bold}{name}{bold} | {vv["total_min_s"] * 1e3:.1f} ms '
178265
f'| {vv["overhead_ns_per_iter"]:.1f} ns '
@@ -195,7 +282,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
195282
{k: vv['per_update_us'] for k, vv in b['libs'].items()}
196283
):
197284
vv = b['libs'][name]
198-
bold = '**' if name == SUBJECT else ''
285+
bold = '**' if name in (SUBJECT, 'progressbar2-fast') else ''
199286
w(
200287
f'| {bold}{name}{bold} | {vv["total_min_s"] * 1e3:.1f} ms '
201288
f'| {vv["per_update_us"]:.2f} us '
@@ -221,7 +308,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
221308
w('|---|--:|')
222309
for name, v in _sorted({k: vv['net_ms'] for k, vv in c['libs'].items()}):
223310
vv = c['libs'][name]
224-
bold = '**' if name == SUBJECT else ''
311+
bold = '**' if name in (SUBJECT, 'progressbar2-fast') else ''
225312
w(f'| {bold}{name}{bold} | {vv["net_ms"]:.1f} ms |')
226313
w('')
227314

0 commit comments

Comments
 (0)