Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Benchmarks

on:
# Run on pushes to main (to keep report up to date)
push:
branches: [main]
# Run weekly on Mondays at 06:00 UTC
schedule:
- cron: "0 6 * * 1"
# Allow manual trigger
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write

jobs:
bench:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run benchmarks and generate report
run: bun run bench:report

- name: Upload JSON results
uses: actions/upload-artifact@v4
with:
name: bench-results
path: reports/bench-results.json
retention-days: 90

- name: Commit updated report
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git add reports/benchmarks.md

if git diff --staged --quiet; then
echo "No changes to benchmark report"
else
git commit -m "docs: update benchmark report"
git push
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ debug/
fixtures/benchmarks/
fixtures/private/

# Benchmark JSON results (machine-specific)
reports/bench-results.json

# Temporary files
tmp/
148 changes: 146 additions & 2 deletions benchmarks/comparison.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { PDFDocument } from "pdf-lib";
import { bench, describe } from "vitest";

import { PDF } from "../src";
import { loadFixture, getHeavyPdf } from "./fixtures";
import { getHeavyPdf, getSynthetic100, getSynthetic2000, loadFixture } from "./fixtures";

// Pre-load fixture
// Pre-load fixtures
const pdfBytes = await getHeavyPdf();
const synthetic100 = await getSynthetic100();
const synthetic2000 = await getSynthetic2000();

describe("Load PDF", () => {
bench("libpdf", async () => {
Expand Down Expand Up @@ -119,3 +121,145 @@ describe("Load, modify, and save PDF", () => {
await pdf.save();
});
});

// ─────────────────────────────────────────────────────────────────────────────
// Page splitting comparison (issue #26)
// ─────────────────────────────────────────────────────────────────────────────

describe("Extract single page from 100-page PDF", () => {
bench("libpdf", async () => {
const pdf = await PDF.load(synthetic100);
const extracted = await pdf.extractPages([0]);
await extracted.save();
});

bench("pdf-lib", async () => {
const pdf = await PDFDocument.load(synthetic100);
const newDoc = await PDFDocument.create();
const [page] = await newDoc.copyPages(pdf, [0]);
newDoc.addPage(page);
await newDoc.save();
});
});

describe("Split 100-page PDF into single-page PDFs", () => {
bench(
"libpdf",
async () => {
const pdf = await PDF.load(synthetic100);
const pageCount = pdf.getPageCount();

for (let i = 0; i < pageCount; i++) {
const single = await pdf.extractPages([i]);
await single.save();
}
},
{ warmupIterations: 1, iterations: 3 },
);

bench(
"pdf-lib",
async () => {
const pdf = await PDFDocument.load(synthetic100);
const pageCount = pdf.getPageCount();

for (let i = 0; i < pageCount; i++) {
const newDoc = await PDFDocument.create();
const [page] = await newDoc.copyPages(pdf, [i]);
newDoc.addPage(page);
await newDoc.save();
}
},
{ warmupIterations: 1, iterations: 3 },
);
});

describe(`Split 2000-page PDF into single-page PDFs (${(synthetic2000.length / 1024 / 1024).toFixed(1)}MB)`, () => {
bench(
"libpdf",
async () => {
const pdf = await PDF.load(synthetic2000);
const pageCount = pdf.getPageCount();

for (let i = 0; i < pageCount; i++) {
const single = await pdf.extractPages([i]);
await single.save();
}
},
{ warmupIterations: 0, iterations: 1, time: 0 },
);

bench(
"pdf-lib",
async () => {
const pdf = await PDFDocument.load(synthetic2000);
const pageCount = pdf.getPageCount();

for (let i = 0; i < pageCount; i++) {
const newDoc = await PDFDocument.create();
const [page] = await newDoc.copyPages(pdf, [i]);
newDoc.addPage(page);
await newDoc.save();
}
},
{ warmupIterations: 0, iterations: 1, time: 0 },
);
});

describe("Copy 10 pages between documents", () => {
bench("libpdf", async () => {
const source = await PDF.load(synthetic100);
const dest = PDF.create();
const indices = Array.from({ length: 10 }, (_, i) => i);
await dest.copyPagesFrom(source, indices);
await dest.save();
});

bench("pdf-lib", async () => {
const source = await PDFDocument.load(synthetic100);
const dest = await PDFDocument.create();
const indices = Array.from({ length: 10 }, (_, i) => i);
const pages = await dest.copyPages(source, indices);

for (const page of pages) {
dest.addPage(page);
}

await dest.save();
});
});

describe("Merge 2 x 100-page PDFs", () => {
bench(
"libpdf",
async () => {
const merged = await PDF.merge([synthetic100, synthetic100]);
await merged.save();
},
{ warmupIterations: 1, iterations: 3 },
);

bench(
"pdf-lib",
async () => {
const doc1 = await PDFDocument.load(synthetic100);
const doc2 = await PDFDocument.load(synthetic100);
const merged = await PDFDocument.create();

const pages1 = await merged.copyPages(doc1, doc1.getPageIndices());

for (const page of pages1) {
merged.addPage(page);
}

const pages2 = await merged.copyPages(doc2, doc2.getPageIndices());

for (const page of pages2) {
merged.addPage(page);
}

await merged.save();
},
{ warmupIterations: 1, iterations: 3 },
);
});
94 changes: 94 additions & 0 deletions benchmarks/copying.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* PDF page-copying and merging benchmarks.
*
* Tests the performance of copying pages between documents and merging
* multiple PDFs. These operations are closely related to splitting
* (issue #26) and represent the other side of the workflow.
*/

import { bench, describe } from "vitest";

import { PDF } from "../src";
import { getSynthetic100, loadFixture, mediumPdfPath } from "./fixtures";

// Pre-load fixtures
const mediumPdf = await loadFixture(mediumPdfPath);
const synthetic100 = await getSynthetic100();

// ─────────────────────────────────────────────────────────────────────────────
// Page copying
// ─────────────────────────────────────────────────────────────────────────────

describe("Copy pages between documents", () => {
bench("copy 1 page", async () => {
const source = await PDF.load(mediumPdf);
const dest = PDF.create();
await dest.copyPagesFrom(source, [0]);
await dest.save();
});

bench("copy 10 pages from 100-page PDF", async () => {
const source = await PDF.load(synthetic100);
const dest = PDF.create();
const indices = Array.from({ length: 10 }, (_, i) => i);
await dest.copyPagesFrom(source, indices);
await dest.save();
});

bench(
"copy all 100 pages",
async () => {
const source = await PDF.load(synthetic100);
const dest = PDF.create();
const indices = Array.from({ length: 100 }, (_, i) => i);
await dest.copyPagesFrom(source, indices);
await dest.save();
},
{ warmupIterations: 1, iterations: 3 },
);
});

// ─────────────────────────────────────────────────────────────────────────────
// Self-copy (page duplication)
// ─────────────────────────────────────────────────────────────────────────────

describe("Duplicate pages within same document", () => {
bench("duplicate page 0", async () => {
const pdf = await PDF.load(mediumPdf);
await pdf.copyPagesFrom(pdf, [0]);
await pdf.save();
});

bench("duplicate all pages (double the document)", async () => {
const pdf = await PDF.load(mediumPdf);
const indices = Array.from({ length: pdf.getPageCount() }, (_, i) => i);
await pdf.copyPagesFrom(pdf, indices);
await pdf.save();
});
});

// ─────────────────────────────────────────────────────────────────────────────
// Merging
// ─────────────────────────────────────────────────────────────────────────────

describe("Merge PDFs", () => {
bench("merge 2 small PDFs", async () => {
const merged = await PDF.merge([mediumPdf, mediumPdf]);
await merged.save();
});

bench("merge 10 small PDFs", async () => {
const sources = Array.from({ length: 10 }, () => mediumPdf);
const merged = await PDF.merge(sources);
await merged.save();
});

bench(
"merge 2 x 100-page PDFs",
async () => {
const merged = await PDF.merge([synthetic100, synthetic100]);
await merged.save();
},
{ warmupIterations: 1, iterations: 3 },
);
});
Loading