use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use text_typeset::layout::block::BlockLayoutParams;
use text_typeset::layout::flow::FlowLayout;
#[path = "../tests/helpers.rs"]
mod helpers;
use helpers::{make_block, make_typesetter};
struct Counting;
static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATED.fetch_add(layout.size(), Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
ALLOCATED.fetch_sub(layout.size(), Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
fn live_bytes() -> usize {
ALLOCATED.load(Ordering::Relaxed)
}
const WIDTH: f32 = 2000.0;
const LINE: &str = "2026-07-16T12:00:00Z INFO worker: processed batch id=1234 in 42ms";
const SIZES: [usize; 3] = [1_000, 10_000, 100_000];
const APPEND_BATCH: usize = 1_000;
const REPS: usize = 5;
fn build_blocks(n: usize) -> Vec<BlockLayoutParams> {
(0..n).map(|i| make_block(i + 1, LINE)).collect()
}
fn median(mut xs: Vec<Duration>) -> Duration {
xs.sort_unstable();
xs[xs.len() / 2]
}
fn main() {
let ts = make_typesetter();
let registry = ts.font_registry();
println!();
println!("text-typeset — cost of appending one line to an N-line document");
println!("{:-<78}", "");
println!(
"{:>9} {:>16} {:>16} {:>14}",
"N", "full_relayout", "incremental", "speedup"
);
println!(
"{:>9} {:>16} {:>16} {:>14}",
"", "(per line)", "(per line)", ""
);
println!("{:-<78}", "");
for n in SIZES {
let blocks = build_blocks(n);
let full = median(
(0..REPS)
.map(|_| {
let params = blocks.clone();
let mut flow = FlowLayout::new();
let start = Instant::now();
flow.layout_blocks(registry, params, WIDTH);
let elapsed = start.elapsed();
std::hint::black_box(flow.content_height);
elapsed
})
.collect(),
);
let extra: Vec<BlockLayoutParams> = (0..APPEND_BATCH)
.map(|i| make_block(n + 1 + i, LINE))
.collect();
let batch = median(
(0..REPS)
.map(|_| {
let mut flow = FlowLayout::new();
flow.layout_blocks(registry, blocks.clone(), WIDTH);
let start = Instant::now();
for params in &extra {
flow.append_block(registry, params, WIDTH);
}
let elapsed = start.elapsed();
std::hint::black_box(flow.content_height);
elapsed
})
.collect(),
);
let per_append = batch / APPEND_BATCH as u32;
let speedup = full.as_secs_f64() / per_append.as_secs_f64().max(f64::MIN_POSITIVE);
println!(
"{:>9} {:>16} {:>16} {:>13.0}x",
n,
format!("{:?}", full),
format!("{:?}", per_append),
speedup
);
}
println!("{:-<78}", "");
println!(
"full_relayout = FlowLayout::layout_blocks(N) | incremental = FlowLayout::append_block(1)"
);
println!();
bench_resident_cost();
}
fn bench_resident_cost() {
println!("Cost of keeping N lines resident (scrolled to the tail, 800x600 viewport)");
println!("{:-<78}", "");
println!(
"{:>9} {:>14} {:>14} {:>16}",
"N", "render/frame", "memory", "bytes/line"
);
println!("{:-<78}", "");
for n in SIZES {
let before = live_bytes();
let mut ts = make_typesetter();
ts.set_viewport(800.0, 600.0);
ts.layout_blocks(build_blocks(n));
let tail = (ts.content_height() - 600.0).max(0.0);
ts.set_scroll_offset(tail);
let _ = ts.render();
let render = median(
(0..REPS)
.map(|_| {
let start = Instant::now();
let frame = ts.render();
let elapsed = start.elapsed();
std::hint::black_box(frame.glyphs.len());
elapsed
})
.collect(),
);
let resident = live_bytes().saturating_sub(before);
println!(
"{:>9} {:>14} {:>14} {:>16}",
n,
format!("{:?}", render),
format!("{:.1} MB", resident as f64 / (1024.0 * 1024.0)),
format!("{} B", resident / n.max(1))
);
}
println!("{:-<78}", "");
println!("render = DocumentFlow::render() with viewport culling over all N flow items");
println!();
bench_windowed();
}
fn bench_windowed() {
const WINDOW: usize = 100;
println!("Same documents, shaping only a {WINDOW}-row window (layout_window)");
println!("{:-<78}", "");
println!(
"{:>9} {:>14} {:>14} {:>16}",
"N", "window layout", "memory", "vs resident"
);
println!("{:-<78}", "");
for n in SIZES {
let before = live_bytes();
let mut ts = make_typesetter();
ts.set_viewport(800.0, 600.0);
ts.set_content_width(WIDTH);
let row_height = {
let mut probe = FlowLayout::new();
probe.append_block(ts.font_registry(), &make_block(1, LINE), WIDTH);
probe.blocks.get(&1).unwrap().height
};
let first = n.saturating_sub(WINDOW);
let window: Vec<(usize, BlockLayoutParams)> =
(first..n).map(|i| (i, make_block(i + 1, LINE))).collect();
let layout = median(
(0..REPS)
.map(|_| {
let start = Instant::now();
ts.flow.layout_window(&ts.service, &window, n, row_height);
let elapsed = start.elapsed();
std::hint::black_box(ts.flow.content_height());
elapsed
})
.collect(),
);
let resident = live_bytes().saturating_sub(before);
println!(
"{:>9} {:>14} {:>14} {:>16}",
n,
format!("{:?}", layout),
format!("{:.1} MB", resident as f64 / (1024.0 * 1024.0)),
format!("{} rows shaped", WINDOW.min(n))
);
}
println!("{:-<78}", "");
println!("memory is now flat in N: only the window is shaped, and");
println!("content_height still spans the whole document (scrollbar stays honest)");
println!();
}