PaperForge Benchmark Suite

Copy Markdown View Source

PaperForge benchmarks are reproducible workload definitions, not promises for every machine. Run them with the same Elixir, OTP, scheduler count, hardware, and MIX_ENV=prod when comparing revisions.

Official workloads

IDWorkloadRunnerPurpose
PF-BENCH-001Small report, 25 rowsrender_profiles.exsSingle-document latency floor
PF-BENCH-002Medium report, 500 rowsrender_profiles.exsTypical multi-page reporting workload
PF-BENCH-003Large report, 5,000 rowsrender_profiles.exsPagination, table, allocation, and GC pressure
PF-BENCH-004Concurrent minimal, medium, or large documentsconcurrent_renders.exsThroughput and bounded-concurrency behavior

Run latency profiles

MIX_ENV=prod SAMPLES=30 WARMUPS=3 mix run benchmarks/render_profiles.exs

Set PROFILE=small, PROFILE=medium, or PROFILE=large to isolate a single document size. SAMPLES must be positive; WARMUPS may be zero. The runner reports median, p95, minimum, maximum, pages, bytes, process-local peak sampling, reductions, garbage collections, reclaimed words, and cache statistics.

Run concurrent profiles

MIX_ENV=prod WORKLOAD=medium JOBS=100 CONCURRENCY=1,2,5,10 \
  mix run benchmarks/concurrent_renders.exs

Use WORKLOAD=minimal, medium, or large. The runner reports total elapsed time, renders per second, job median and p95 latency, job-memory sampling, BEAM-wide memory sampling, reductions, garbage collections, and failures.

Comparison rules

  • Warm up before collecting samples.
  • Prefer medians and p95 over a single average.
  • Record runtime versions and scheduler count with each result.
  • Treat VM-wide memory as a workload-level signal; use process-local sampling when attributing memory to an individual render.
  • Require byte-identical output for deterministic benchmark inputs.

Reference results for 1.4.4

The following runs make the performance discussion concrete without pretending that one laptop defines every deployment. They were collected on Apple Silicon (Mac16,12) with MIX_ENV=prod, Elixir 1.20.2, OTP 29, and 10 schedulers. Every latency profile used 3 warmups and 30 measured samples.

ProfileContentPagesOutputTotal medianTotal p95Layout medianCold serialization medianPeak process memory median
Small25 table rows24.5 KB1.90 ms2.06 ms1.68 ms0.19 ms1.12 MB
Medium500 table rows1862.3 KB39.13 ms39.87 ms34.55 ms3.44 ms9.59 MB
Large5,000 table rows179616 KB824.77 ms832.83 ms773.58 ms35.54 ms97.08 MB

The p95 numbers show low variation on this host: 2.06 ms, 39.87 ms, and 832.83 ms respectively. These are same-revision reference measurements, not a claimed speedup over another PaperForge version or a guarantee for every machine.

What these results mean in plain language:

  • Small documents finish in roughly two milliseconds after the runtime is warm.
  • A 500-row report completed in about 39 milliseconds in this reference run.
  • The 5,000-row workload paginated a 179-page report in about 825 milliseconds while keeping the output near 616 KB. It is the profile to use when evaluating large-table changes.
  • Cache counters are included in every profile so a slowdown can be traced to text measurement, compression, or font-subset work instead of guessed at.

Reference concurrency results for 1.4.4

Concurrency answers a different question: how many independent documents can a BEAM application render under bounded load? These runs used the same runtime and host as the latency table. Each row is one scaling run rather than a 30-sample latency distribution.

Minimal one-page documents, 1,000 jobs

WorkersElapsedThroughputJob medianJob p95Failures
147.51 ms21,047.31 PDFs/s0.03 ms0.03 ms0
219.27 ms51,886.06 PDFs/s0.03 ms0.03 ms0
518.41 ms54,321.26 PDFs/s0.03 ms0.04 ms0
1018.32 ms54,570.26 PDFs/s0.03 ms0.04 ms0

This is an infrastructure micro-benchmark for the bounded worker path, not a claim that a complex invoice renders at 54,000 PDFs per second.

Medium reports, 500 rows and 18 pages, 100 jobs

WorkersElapsedThroughputJob medianJob p95Peak BEAM memoryFailures
14.08 s24.52 PDFs/s40.38 ms41.41 ms95.14 MB0
22.22 s45.11 PDFs/s44.19 ms45.31 ms119.21 MB0
51.11 s89.93 PDFs/s55.10 ms59.10 ms183.63 MB0
10814.30 ms122.80 PDFs/s79.78 ms88.69 ms247.13 MB0

Large reports, 5,000 rows and 179 pages, 20 jobs

WorkersElapsedThroughputJob medianJob p95Peak BEAM memoryFailures
116.85 s1.19 PDFs/s835.63 ms848.21 ms305.94 MB0
29.44 s2.12 PDFs/s942.10 ms952.13 ms503.57 MB0
55.17 s3.87 PDFs/s1.27 s1.33 s1.04 GB0
104.01 s4.99 PDFs/s1.98 s2.04 s1.84 GB0

For the large workload, 2 workers are the practical memory-throughput starting point on this reference machine. 5 workers can be appropriate only when the deployment has at least about 1 GB of BEAM memory available for this workload plus the application itself. Ten workers maximize throughput here but also materially increase per-job latency and memory pressure.

Release quality checks

Before the 1.4.4 reference measurements, the repository passed:

CheckResult
mix format --check-formattedPassed
mix compile --warnings-as-errorsPassed
mix test280 tests passed, including 1 property test
mix hex.buildPassed

The test suite covers deterministic output, validation and diagnostics, declarative templates, fonts, images, PDF interoperability, security, and performance-cache regression cases. It verifies correctness; benchmark runners measure speed and memory under explicit workloads.

Planned workload expansion

Image-heavy, embedded-font, and AES-256 benchmark profiles will be added only with dedicated reproducible runners and documented resource fixtures.