Skip to content

Commit 4f25d22

Browse files
authored
Add text-drawing benchmarks (#539)
Adds a few different benchmark variants, to test the performance impact of underline drawing and caching. For some reason, on my machine (Linux) I need to pass the `-p` (parallel) flag to tango when performing a comparison. Without it, the "new" code always comes back as ridiculously faster, even when it's not been changed at all. I can't find any information on why this would be.
1 parent 7a4b4c4 commit 4f25d22

File tree

6 files changed

+232
-2
lines changed

6 files changed

+232
-2
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

parley_bench/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ publish = false
1212
[dependencies]
1313
parley = { workspace = true, default-features = true }
1414
parley_dev = { workspace = true }
15+
parley_draw = { workspace = true, default-features = true, features = ["vello_cpu", "png"] }
1516
skrifa = { workspace = true }
1617
tango-bench = "0.6"
18+
vello_cpu = { workspace = true, default-features = true }
1719

1820
[[bench]]
1921
name = "main"

parley_bench/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ cargo export target/benchmarks -- bench --bench=main
2323
# Apply changes to Parley
2424

2525
# Compare changes with baseline
26-
cargo bench -q --bench=main -- compare target/benchmarks/main
26+
cargo bench -q --bench=main -- compare target/benchmarks/main -p
2727
```

parley_bench/benches/main.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,18 @@ use tango_bench::{tango_benchmarks, tango_main};
88
use parley_bench::benches::{defaults, styled};
99
use parley_bench::fontique_benches::system_fonts_init;
1010

11-
tango_benchmarks!(defaults(), styled(), system_fonts_init());
11+
use parley_bench::draw::{
12+
draw_no_underline_cold_cache, draw_no_underline_warm_cache, draw_with_underline_cold_cache,
13+
draw_with_underline_warm_cache,
14+
};
15+
16+
tango_benchmarks!(
17+
defaults(),
18+
styled(),
19+
draw_no_underline_cold_cache(),
20+
draw_no_underline_warm_cache(),
21+
draw_with_underline_cold_cache(),
22+
draw_with_underline_warm_cache(),
23+
system_fonts_init()
24+
);
1225
tango_main!();

parley_bench/src/draw.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Copyright 2025 the Parley Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! # Draw Benchmarks
5+
//!
6+
//! Benchmarks for text rendering using `parley_draw` with `vello_cpu`.
7+
8+
use crate::{ColorBrush, FONT_FAMILY_LIST, with_contexts};
9+
use parley::{
10+
Alignment, AlignmentOptions, FontFamily, Layout, PositionedLayoutItem, StyleProperty,
11+
};
12+
use parley_draw::{Glyph, GlyphCaches, GlyphRunBuilder};
13+
use std::hint::black_box;
14+
use tango_bench::{Benchmark, benchmark_fn};
15+
use vello_cpu::{RenderContext, kurbo};
16+
17+
const DISPLAY_SCALE: f32 = 1.0;
18+
const QUANTIZE: bool = true;
19+
const MAX_ADVANCE: f32 = 400.0 * DISPLAY_SCALE;
20+
const PADDING: u16 = 20;
21+
22+
/// Long sample text for benchmarking.
23+
const SAMPLE_TEXT: &str = "Call me Ishmael. Some years ago—never mind how long precisely—having\
24+
little or no money in my purse, and nothing particular to interest me\
25+
on shore, I thought I would sail about a little and see the watery part\
26+
of the world. It is a way I have of driving off the spleen and\
27+
regulating the circulation. Whenever I find myself growing grim about\
28+
the mouth; whenever it is a damp, drizzly November in my soul; whenever\
29+
I find myself involuntarily pausing before coffin warehouses, and\
30+
bringing up the rear of every funeral I meet; and especially whenever\
31+
my hypos get such an upper hand of me, that it requires a strong moral\
32+
principle to prevent me from deliberately stepping into the street, and\
33+
methodically knocking people’s hats off—then, I account it high time to\
34+
get to sea as soon as I can. This is my substitute for pistol and ball.\
35+
With a philosophical flourish Cato throws himself upon his sword; I\
36+
quietly take to the ship. There is nothing surprising in this. If they\
37+
but knew it, almost all men in their degree, some time or other,\
38+
cherish very nearly the same feelings towards the ocean with me.
39+
40+
There now is your insular city of the Manhattoes, belted round by\
41+
wharves as Indian isles by coral reefs—commerce surrounds it with her\
42+
surf. Right and left, the streets take you waterward. Its extreme\
43+
downtown is the battery, where that noble mole is washed by waves, and\
44+
cooled by breezes, which a few hours previous were out of sight of\
45+
land. Look at the crowds of water-gazers there.";
46+
47+
/// Builds a layout with or without underlines.
48+
fn build_layout(text: &str, underline: bool) -> Layout<ColorBrush> {
49+
with_contexts(|font_cx, layout_cx| {
50+
let mut builder = layout_cx.ranged_builder(font_cx, text, DISPLAY_SCALE, QUANTIZE);
51+
builder.push_default(FontFamily::from(FONT_FAMILY_LIST));
52+
builder.push_default(StyleProperty::FontSize(16.0));
53+
54+
if underline {
55+
builder.push(StyleProperty::Underline(true), 0..text.len());
56+
}
57+
58+
let mut layout: Layout<ColorBrush> = builder.build(text);
59+
layout.break_all_lines(Some(MAX_ADVANCE));
60+
layout.align(
61+
Some(MAX_ADVANCE),
62+
Alignment::Start,
63+
AlignmentOptions::default(),
64+
);
65+
layout
66+
})
67+
}
68+
69+
/// Renders a layout to a renderer, optionally with underlines.
70+
fn render_layout(
71+
layout: &Layout<ColorBrush>,
72+
renderer: &mut RenderContext,
73+
glyph_caches: &mut GlyphCaches,
74+
with_underline: bool,
75+
) {
76+
for line in layout.lines() {
77+
for item in line.items() {
78+
match item {
79+
PositionedLayoutItem::GlyphRun(glyph_run) => {
80+
let run = glyph_run.run();
81+
GlyphRunBuilder::new(run.font().clone(), *renderer.transform(), renderer)
82+
.font_size(run.font_size())
83+
.hint(true)
84+
.normalized_coords(run.normalized_coords())
85+
.fill_glyphs(
86+
glyph_run.positioned_glyphs().map(|glyph| Glyph {
87+
id: glyph.id,
88+
x: glyph.x,
89+
y: glyph.y,
90+
}),
91+
glyph_caches,
92+
);
93+
94+
if with_underline {
95+
if let Some(decoration) = &glyph_run.style().underline {
96+
let offset =
97+
decoration.offset.unwrap_or(run.metrics().underline_offset);
98+
let size = decoration.size.unwrap_or(run.metrics().underline_size);
99+
100+
let x = glyph_run.offset();
101+
let x1 = x + glyph_run.advance();
102+
let baseline = glyph_run.baseline();
103+
104+
GlyphRunBuilder::new(
105+
run.font().clone(),
106+
*renderer.transform(),
107+
renderer,
108+
)
109+
.font_size(run.font_size())
110+
.normalized_coords(run.normalized_coords())
111+
.render_decoration(
112+
glyph_run.positioned_glyphs().map(|glyph| Glyph {
113+
id: glyph.id,
114+
x: glyph.x,
115+
y: glyph.y,
116+
}),
117+
x..=x1,
118+
baseline,
119+
offset,
120+
size,
121+
1.0, // buffer around exclusions
122+
glyph_caches,
123+
);
124+
}
125+
}
126+
}
127+
PositionedLayoutItem::InlineBox(_) => {}
128+
}
129+
}
130+
}
131+
}
132+
133+
/// Creates the render context for drawing.
134+
fn create_renderer(layout: &Layout<ColorBrush>) -> RenderContext {
135+
#[expect(
136+
clippy::cast_possible_truncation,
137+
reason = "the layout's not *that* big"
138+
)]
139+
let width = layout.width().ceil() as u16 + PADDING * 2;
140+
#[expect(
141+
clippy::cast_possible_truncation,
142+
reason = "the layout's not *that* big"
143+
)]
144+
let height = layout.height().ceil() as u16 + PADDING * 2;
145+
146+
let mut renderer = RenderContext::new(width, height);
147+
renderer.set_transform(kurbo::Affine::translate(kurbo::Vec2::new(
148+
PADDING as f64,
149+
PADDING as f64,
150+
)));
151+
renderer
152+
}
153+
154+
/// Benchmark for drawing text without underlines, with a fresh cache each iteration.
155+
pub fn draw_no_underline_cold_cache() -> Vec<Benchmark> {
156+
vec![benchmark_fn("Draw - No underline (cold cache)", |b| {
157+
let layout = build_layout(SAMPLE_TEXT, false);
158+
159+
b.iter(move || {
160+
let layout = layout.clone();
161+
let mut renderer = create_renderer(&layout);
162+
let mut glyph_caches = GlyphCaches::new();
163+
render_layout(&layout, &mut renderer, &mut glyph_caches, false);
164+
black_box(&renderer);
165+
})
166+
})]
167+
}
168+
169+
/// Benchmark for drawing text without underlines, reusing the cache.
170+
pub fn draw_no_underline_warm_cache() -> Vec<Benchmark> {
171+
vec![benchmark_fn("Draw - No underline (warm cache)", |b| {
172+
let layout = build_layout(SAMPLE_TEXT, false);
173+
let mut glyph_caches = GlyphCaches::new();
174+
175+
b.iter(move || {
176+
let mut renderer = create_renderer(&layout);
177+
render_layout(&layout, &mut renderer, &mut glyph_caches, false);
178+
glyph_caches.maintain();
179+
black_box(&renderer);
180+
})
181+
})]
182+
}
183+
184+
/// Benchmark for drawing text with underlines, with a fresh cache each iteration.
185+
pub fn draw_with_underline_cold_cache() -> Vec<Benchmark> {
186+
vec![benchmark_fn("Draw - With underline (cold cache)", |b| {
187+
let layout = build_layout(SAMPLE_TEXT, true);
188+
189+
b.iter(move || {
190+
let layout = layout.clone();
191+
let mut renderer = create_renderer(&layout);
192+
let mut glyph_caches = GlyphCaches::new();
193+
render_layout(&layout, &mut renderer, &mut glyph_caches, true);
194+
black_box(&renderer);
195+
})
196+
})]
197+
}
198+
199+
/// Benchmark for drawing text with underlines, reusing the cache.
200+
pub fn draw_with_underline_warm_cache() -> Vec<Benchmark> {
201+
vec![benchmark_fn("Draw - With underline (warm cache)", |b| {
202+
let layout = build_layout(SAMPLE_TEXT, true);
203+
let mut glyph_caches = GlyphCaches::new();
204+
205+
b.iter(move || {
206+
let mut renderer = create_renderer(&layout);
207+
render_layout(&layout, &mut renderer, &mut glyph_caches, true);
208+
glyph_caches.maintain();
209+
black_box(&renderer);
210+
})
211+
})]
212+
}

parley_bench/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use parley::{
1818
};
1919

2020
pub mod benches;
21+
pub mod draw;
2122
pub mod fontique_benches;
2223

2324
/// A color brush.

0 commit comments

Comments
 (0)