Skip to content

Commit bb1be41

Browse files
authored
chore: add benchmark suite with pdf-lib comparisons (#8)
1 parent 334789d commit bb1be41

File tree

11 files changed

+715
-2
lines changed

11 files changed

+715
-2
lines changed

.agents/plans/043-benchmarks.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Plan: Basic Benchmarks
2+
3+
## Problem Statement
4+
5+
Users evaluating @libpdf/core need confidence that the library performs reasonably well. Currently there are no benchmarks, making it impossible to:
6+
7+
1. Demonstrate performance characteristics to potential users
8+
2. Compare against alternatives like pdf-lib
9+
3. Detect performance regressions during development
10+
11+
## Goals
12+
13+
- Provide basic benchmarks for common operations
14+
- Compare performance against pdf-lib where APIs overlap
15+
- Give users a rough sense of expected performance
16+
- Keep the benchmark suite minimal and maintainable
17+
18+
## Non-Goals
19+
20+
- Comprehensive micro-benchmarks for every operation
21+
- Benchmarking against pdf.js (different focus: rendering)
22+
- Achieving "fastest" status (correctness > speed)
23+
- CI integration (can add later if needed)
24+
25+
## Scope
26+
27+
### In Scope
28+
29+
- Loading PDFs (small, medium, large)
30+
- Saving PDFs (full write, incremental)
31+
- Drawing operations (shapes, text)
32+
- Form filling
33+
- Comparison with pdf-lib for overlapping operations
34+
35+
### Out of Scope
36+
37+
- Encryption/decryption benchmarks (security-sensitive)
38+
- Digital signature benchmarks (involves crypto)
39+
- Text extraction benchmarks (can add later)
40+
- Memory usage profiling
41+
42+
## Technical Approach
43+
44+
### Framework: Vitest Bench
45+
46+
Vitest 4.x (already installed) has built-in benchmarking support via `vitest bench`. This provides:
47+
48+
- Same configuration as existing tests
49+
- Warmup iterations, iteration counts, time limits
50+
- JSON output for potential CI integration
51+
- Familiar API for contributors
52+
53+
### Directory Structure
54+
55+
```
56+
benchmarks/
57+
loading.bench.ts # PDF.load() performance
58+
saving.bench.ts # PDF.save() performance
59+
drawing.bench.ts # Shape/text drawing
60+
forms.bench.ts # Form field operations
61+
comparison.bench.ts # libpdf vs pdf-lib head-to-head
62+
```
63+
64+
### Benchmark Categories
65+
66+
#### 1. Loading Performance
67+
68+
| Benchmark | Fixture | Description |
69+
| --------------- | ------------------------------------------ | ------------------------ |
70+
| Load small PDF | `basic/rot0.pdf` (888B) | Minimal parsing overhead |
71+
| Load medium PDF | `basic/sample.pdf` (19KB) | Typical document |
72+
| Load large PDF | `text/variety/us_constitution.pdf` (380KB) | Multi-page document |
73+
| Load with forms | `forms/sample_form.pdf` (116KB) | Form parsing |
74+
75+
#### 2. Saving Performance
76+
77+
| Benchmark | Description |
78+
| ----------------------- | ------------------------- |
79+
| Save unmodified | Serialize without changes |
80+
| Save with modifications | After adding content |
81+
| Incremental save | Append-only save |
82+
83+
#### 3. Drawing Performance
84+
85+
| Benchmark | Description |
86+
| ---------------------- | ----------------------- |
87+
| Draw rectangles (100x) | Many simple shapes |
88+
| Draw circles (100x) | Curved shapes |
89+
| Draw lines (100x) | Path operations |
90+
| Draw text (100 lines) | Text with standard font |
91+
92+
#### 4. Form Operations
93+
94+
| Benchmark | Description |
95+
| ---------------- | ------------------------- |
96+
| Fill text fields | Set values on text fields |
97+
| Get field values | Read form data |
98+
| Flatten form | Convert to static content |
99+
100+
#### 5. Library Comparison
101+
102+
Compare pdf-lib and libpdf on operations both support:
103+
104+
| Operation | Description |
105+
| ---------------- | ------------------------ |
106+
| Load PDF | Parse the same document |
107+
| Create blank PDF | New document creation |
108+
| Add pages | Insert blank pages |
109+
| Draw shapes | Rectangle/circle drawing |
110+
| Save PDF | Serialize to bytes |
111+
112+
### Fixture Selection
113+
114+
Use existing fixtures for small/medium, download a large public domain PDF:
115+
116+
- **Small**: `fixtures/basic/rot0.pdf` (888 bytes, minimal)
117+
- **Medium**: `fixtures/basic/sample.pdf` (19KB, typical)
118+
- **Large**: Download from Internet Archive or similar (~5-10MB, real-world document)
119+
- **Forms**: `fixtures/forms/sample_form.pdf` (116KB, interactive)
120+
121+
#### Large PDF Strategy
122+
123+
For "large" benchmarks, we need a multi-MB PDF to test real-world performance. Options:
124+
125+
1. **Internet Archive** — Public domain books/documents (e.g., government reports, old technical manuals)
126+
2. **NASA Technical Reports** — All public domain, many are 5-20MB
127+
3. **Project Gutenberg** — Public domain books with images
128+
129+
The benchmark will download the large PDF on first run and cache it in `fixtures/benchmarks/`. This keeps the repo size small while allowing real-world performance testing.
130+
131+
```typescript
132+
// benchmarks/fixtures.ts
133+
const LARGE_PDF_URL = "https://archive.org/download/..."; // TBD: specific URL
134+
const LARGE_PDF_PATH = "fixtures/benchmarks/large-document.pdf";
135+
136+
export async function getLargePdf(): Promise<Uint8Array> {
137+
if (await Bun.file(LARGE_PDF_PATH).exists()) {
138+
return Bun.file(LARGE_PDF_PATH).bytes();
139+
}
140+
// Download and cache
141+
const response = await fetch(LARGE_PDF_URL);
142+
const bytes = new Uint8Array(await response.arrayBuffer());
143+
await Bun.write(LARGE_PDF_PATH, bytes);
144+
return bytes;
145+
}
146+
```
147+
148+
The `fixtures/benchmarks/` directory will be gitignored.
149+
150+
### Example Usage
151+
152+
```typescript
153+
// benchmarks/loading.bench.ts
154+
import { bench, describe } from "vitest";
155+
import { PDF } from "../src";
156+
157+
const smallPdf = await Bun.file("fixtures/basic/rot0.pdf").bytes();
158+
const largePdf = await Bun.file("fixtures/text/variety/us_constitution.pdf").bytes();
159+
160+
describe("PDF Loading", () => {
161+
bench("load small PDF (888B)", async () => {
162+
await PDF.load(smallPdf);
163+
});
164+
165+
bench("load large PDF (380KB)", async () => {
166+
await PDF.load(largePdf);
167+
});
168+
});
169+
```
170+
171+
```typescript
172+
// benchmarks/comparison.bench.ts
173+
import { bench, describe } from "vitest";
174+
import { PDF } from "../src";
175+
import { PDFDocument } from "pdf-lib";
176+
177+
const pdfBytes = await Bun.file("fixtures/basic/sample.pdf").bytes();
178+
179+
describe("Load PDF", () => {
180+
bench("libpdf", async () => {
181+
await PDF.load(pdfBytes);
182+
});
183+
184+
bench("pdf-lib", async () => {
185+
await PDFDocument.load(pdfBytes);
186+
});
187+
});
188+
```
189+
190+
### Configuration
191+
192+
Add benchmark configuration to `vitest.config.ts`:
193+
194+
```typescript
195+
export default defineConfig({
196+
test: {
197+
// existing config...
198+
},
199+
bench: {
200+
include: ["benchmarks/**/*.bench.ts"],
201+
reporters: ["default"],
202+
},
203+
});
204+
```
205+
206+
Add npm script to `package.json`:
207+
208+
```json
209+
{
210+
"scripts": {
211+
"bench": "vitest bench"
212+
}
213+
}
214+
```
215+
216+
### Dependencies
217+
218+
pdf-lib will be added as a dev dependency for comparison benchmarks:
219+
220+
```bash
221+
bun add -d pdf-lib
222+
```
223+
224+
## Test Plan
225+
226+
- Run `bun run bench` successfully
227+
- All benchmarks complete without errors
228+
- Results display in readable format
229+
- Comparison benchmarks show both libraries
230+
231+
## Open Questions
232+
233+
1. **Should we include pdf-lib comparisons?**
234+
- Pro: Useful for users evaluating alternatives
235+
- Con: Adds maintenance burden, results vary by machine
236+
- **Decision**: Yes, include them — they're useful for users and we can note results are machine-dependent
237+
238+
2. **Should we set up CI benchmarking?**
239+
- Can be added later with CodSpeed or similar
240+
- For now, local benchmarks are sufficient
241+
242+
3. **How many iterations/warmup?**
243+
- Default Vitest settings should be fine
244+
- Can tune if results are noisy
245+
246+
## Risks
247+
248+
- **Performance may not be competitive**: That's okay — correctness and features matter more. Benchmarks help identify obvious issues.
249+
- **Results vary by machine**: Document that benchmarks are relative, not absolute.
250+
- **pdf-lib API differences**: Some operations may not be directly comparable; note differences in comments.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,8 @@ examples/output/
4848
# Debug files
4949
debug/
5050

51+
# Benchmark fixtures (downloaded at runtime)
52+
fixtures/benchmarks/
53+
5154
# Temporary files
52-
tmp/
55+
tmp/

benchmarks/comparison.bench.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Library comparison benchmarks.
3+
*
4+
* Compares @libpdf/core against pdf-lib for overlapping operations.
5+
* Results are machine-dependent and should be used for relative comparison only.
6+
*/
7+
8+
import { PDFDocument } from "pdf-lib";
9+
import { bench, describe } from "vitest";
10+
11+
import { PDF } from "../src";
12+
import { loadFixture, getHeavyPdf } from "./fixtures";
13+
14+
// Pre-load fixture
15+
const pdfBytes = await getHeavyPdf();
16+
17+
describe("Load PDF", () => {
18+
bench("libpdf", async () => {
19+
await PDF.load(pdfBytes);
20+
});
21+
22+
bench("pdf-lib", async () => {
23+
await PDFDocument.load(pdfBytes);
24+
});
25+
});
26+
27+
describe("Create blank PDF", () => {
28+
bench("libpdf", async () => {
29+
const pdf = PDF.create();
30+
await pdf.save();
31+
});
32+
33+
bench("pdf-lib", async () => {
34+
const pdf = await PDFDocument.create();
35+
await pdf.save();
36+
});
37+
});
38+
39+
describe("Add 10 pages", () => {
40+
bench("libpdf", async () => {
41+
const pdf = PDF.create();
42+
43+
for (let i = 0; i < 10; i++) {
44+
pdf.addPage();
45+
}
46+
47+
await pdf.save();
48+
});
49+
50+
bench("pdf-lib", async () => {
51+
const pdf = await PDFDocument.create();
52+
53+
for (let i = 0; i < 10; i++) {
54+
pdf.addPage();
55+
}
56+
57+
await pdf.save();
58+
});
59+
});
60+
61+
describe("Draw 50 rectangles", () => {
62+
bench("libpdf", async () => {
63+
const pdf = PDF.create();
64+
const page = pdf.addPage();
65+
66+
for (let i = 0; i < 50; i++) {
67+
page.drawRectangle({
68+
x: 50 + (i % 5) * 100,
69+
y: 50 + Math.floor(i / 5) * 70,
70+
width: 80,
71+
height: 50,
72+
});
73+
}
74+
75+
await pdf.save();
76+
});
77+
78+
bench("pdf-lib", async () => {
79+
const pdf = await PDFDocument.create();
80+
const page = pdf.addPage();
81+
82+
for (let i = 0; i < 50; i++) {
83+
page.drawRectangle({
84+
x: 50 + (i % 5) * 100,
85+
y: 50 + Math.floor(i / 5) * 70,
86+
width: 80,
87+
height: 50,
88+
});
89+
}
90+
91+
await pdf.save();
92+
});
93+
});
94+
95+
describe("Load and save PDF", () => {
96+
bench("libpdf", async () => {
97+
const pdf = await PDF.load(pdfBytes);
98+
await pdf.save();
99+
});
100+
101+
bench("pdf-lib", async () => {
102+
const pdf = await PDFDocument.load(pdfBytes);
103+
await pdf.save();
104+
});
105+
});
106+
107+
describe("Load, modify, and save PDF", () => {
108+
bench("libpdf", async () => {
109+
const pdf = await PDF.load(pdfBytes);
110+
const page = pdf.getPage(0)!;
111+
page.drawRectangle({ x: 50, y: 50, width: 100, height: 100 });
112+
await pdf.save();
113+
});
114+
115+
bench("pdf-lib", async () => {
116+
const pdf = await PDFDocument.load(pdfBytes);
117+
const page = pdf.getPage(0);
118+
page.drawRectangle({ x: 50, y: 50, width: 100, height: 100 });
119+
await pdf.save();
120+
});
121+
});

0 commit comments

Comments
 (0)