diff --git a/bun.lock b/bun.lock index 15366b6..9cc3873 100644 --- a/bun.lock +++ b/bun.lock @@ -9,14 +9,14 @@ "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "asn1js": "^3.0.10", - "lru-cache": "^11.4.0", + "lru-cache": "^11.5.1", "pako": "^2.1.0", "pkijs": "^3.4.0", }, "devDependencies": { - "@cantoo/pdf-lib": "^2.6.5", - "@google-cloud/kms": "^5.5.0", - "@google-cloud/secret-manager": "^6.1.2", + "@cantoo/pdf-lib": "^2.9.1", + "@google-cloud/kms": "^5.5.1", + "@google-cloud/secret-manager": "^6.1.3", "@types/bun": "^1.3.14", "@types/pako": "^2.0.4", "@vitest/coverage-v8": "4.0.16", @@ -24,11 +24,11 @@ "lint-staged": "^16.4.0", "oxfmt": "^0.50.0", "oxlint": "~1.39.0", - "oxlint-tsgolint": "^0.11.1", + "oxlint-tsgolint": "^0.11.5", "pdf-lib": "^1.17.1", - "tsdown": "^0.22.0", + "tsdown": "^0.22.2", "typescript": "^5.9.3", - "vitest": "^4.1.6", + "vitest": "^4.1.8", }, "peerDependencies": { "@google-cloud/kms": "^5.0.0", @@ -53,7 +53,7 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@cantoo/pdf-lib": ["@cantoo/pdf-lib@2.7.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "crypto-js": "^4.2.0", "node-html-better-parser": ">=1.4.0", "pako": "^1.0.11", "tslib": ">=2" } }, "sha512-oHVfp0JrHYodyF18dG1r85LStjJJs42LLoiu+elB9qZu9YfYoE66omF5hJ3gEKEXk6PPUUEDpBngeZiHMSnueQ=="], + "@cantoo/pdf-lib": ["@cantoo/pdf-lib@2.9.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "html-entities": "^2.3.2", "node-html-better-parser": ">=1.5.9", "pako": "^2.2.0", "tslib": ">=2" } }, "sha512-wAKXXDjp5lO0wS6d+k0FRpmWsULb95WhHKYx2fonTbv4zIfuIdCBz2nyTVVDGJpNDLl6o4w9aSTxQisBvvd60A=="], "@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], @@ -409,8 +409,6 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -553,7 +551,7 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "node-html-better-parser": ["node-html-better-parser@1.5.8", "", { "dependencies": { "html-entities": "^2.3.2" } }, "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw=="], + "node-html-better-parser": ["node-html-better-parser@1.5.9", "", { "dependencies": { "html-entities": "^2.3.2" } }, "sha512-z1I5UINMezJXYL9cH3h0a9KBth2G978gSLlfkpQ+CQzzVHVQy9gpARgm9eDsz1O4gn1HtgUqjdAIYxKFZm6uHQ=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], @@ -717,7 +715,7 @@ "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@cantoo/pdf-lib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "@cantoo/pdf-lib/pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], diff --git a/package.json b/package.json index dfd76f2..821cbbc 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "pkijs": "^3.4.0" }, "devDependencies": { - "@cantoo/pdf-lib": "^2.7.1", + "@cantoo/pdf-lib": "^2.9.1", "@google-cloud/kms": "^5.5.1", "@google-cloud/secret-manager": "^6.1.3", "@types/bun": "^1.3.14", diff --git a/reports/benchmarks.md b/reports/benchmarks.md index a2ae59b..e1943f6 100644 --- a/reports/benchmarks.md +++ b/reports/benchmarks.md @@ -1,8 +1,10 @@ # Benchmark Report -> Generated on 2026-08-17 at 06:52:09 UTC +> Generated on 2026-08-24 at 06:06:56 UTC > -> System: linux | AMD EPYC 7763 64-Core Processor (4 cores) | 16GB RAM | Bun 1.3.14 +> System: darwin | Apple M4 Pro (12 cores) | 24GB RAM | Bun 1.3.14 +> +> Libraries: @libpdf/core 0.4.1 (this repo), pdf-lib 1.17.1, @cantoo/pdf-lib 2.9.1 --- @@ -20,234 +22,234 @@ ### Load PDF -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :-------------- | ------: | -------: | -------: | -----: | ------: | -| libpdf | 55.1 | 18.15ms | 19.44ms | ±1.31% | 28 | -| @cantoo/pdf-lib | 4.6 | 218.96ms | 231.60ms | ±1.67% | 10 | -| pdf-lib | 4.5 | 220.20ms | 228.05ms | ±1.23% | 10 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :-------------- | ------: | ------: | ------: | -----: | ------: | +| libpdf | 600.6 | 1.67ms | 2.10ms | ±0.70% | 301 | +| pdf-lib | 32.5 | 30.81ms | 32.37ms | ±1.76% | 17 | +| @cantoo/pdf-lib | 31.9 | 31.35ms | 33.24ms | ±1.51% | 16 | -- **libpdf** is 12.07x faster than @cantoo/pdf-lib -- **libpdf** is 12.13x faster than pdf-lib +- **libpdf** is 18.50x faster than pdf-lib +- **libpdf** is 18.83x faster than @cantoo/pdf-lib ### Create blank PDF -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :-------------- | ------: | ----: | -----: | -----: | ------: | -| libpdf | 12.2K | 82us | 170us | ±3.09% | 6,113 | -| pdf-lib | 3.0K | 330us | 1.45ms | ±2.78% | 1,515 | -| @cantoo/pdf-lib | 2.6K | 380us | 1.57ms | ±3.37% | 1,317 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :-------------- | ------: | ----: | ----: | -----: | ------: | +| libpdf | 29.0K | 35us | 48us | ±0.78% | 14,484 | +| @cantoo/pdf-lib | 6.6K | 151us | 793us | ±2.20% | 3,321 | +| pdf-lib | 6.1K | 165us | 739us | ±4.06% | 3,035 | -- **libpdf** is 4.04x faster than pdf-lib -- **libpdf** is 4.64x faster than @cantoo/pdf-lib +- **libpdf** is 4.36x faster than @cantoo/pdf-lib +- **libpdf** is 4.77x faster than pdf-lib ### Add 10 pages -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :-------------- | ------: | ----: | -----: | -----: | ------: | -| libpdf | 8.0K | 126us | 268us | ±0.94% | 3,980 | -| @cantoo/pdf-lib | 2.3K | 435us | 2.03ms | ±3.27% | 1,151 | -| pdf-lib | 2.2K | 445us | 1.68ms | ±2.83% | 1,123 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :-------------- | ------: | ----: | ----: | -----: | ------: | +| libpdf | 13.5K | 74us | 104us | ±1.01% | 6,753 | +| @cantoo/pdf-lib | 4.9K | 204us | 932us | ±2.07% | 2,457 | +| pdf-lib | 4.5K | 224us | 931us | ±2.65% | 2,231 | -- **libpdf** is 3.46x faster than @cantoo/pdf-lib -- **libpdf** is 3.55x faster than pdf-lib +- **libpdf** is 2.75x faster than @cantoo/pdf-lib +- **libpdf** is 3.03x faster than pdf-lib ### Draw 50 rectangles -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :-------------- | ------: | -----: | -----: | -----: | ------: | -| libpdf | 2.8K | 355us | 888us | ±1.45% | 1,410 | -| pdf-lib | 647.2 | 1.55ms | 6.60ms | ±9.02% | 324 | -| @cantoo/pdf-lib | 506.8 | 1.97ms | 7.12ms | ±7.97% | 255 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :-------------- | ------: | ----: | -----: | -----: | ------: | +| libpdf | 5.1K | 196us | 564us | ±1.09% | 2,545 | +| pdf-lib | 1.5K | 665us | 2.31ms | ±4.29% | 752 | +| @cantoo/pdf-lib | 1.2K | 816us | 2.12ms | ±3.25% | 614 | -- **libpdf** is 4.35x faster than pdf-lib -- **libpdf** is 5.56x faster than @cantoo/pdf-lib +- **libpdf** is 3.39x faster than pdf-lib +- **libpdf** is 4.15x faster than @cantoo/pdf-lib ### Load and save PDF | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | -------: | -------: | -----: | ------: | -| libpdf | 51.9 | 19.26ms | 30.66ms | ±5.62% | 26 | -| pdf-lib | 3.2 | 312.48ms | 328.07ms | ±1.29% | 10 | -| @cantoo/pdf-lib | 1.7 | 602.35ms | 678.42ms | ±3.38% | 10 | +| libpdf | 587.3 | 1.70ms | 2.19ms | ±0.88% | 294 | +| pdf-lib | 17.7 | 56.37ms | 59.79ms | ±2.40% | 10 | +| @cantoo/pdf-lib | 7.7 | 129.73ms | 132.91ms | ±1.06% | 10 | -- **libpdf** is 16.22x faster than pdf-lib -- **libpdf** is 31.27x faster than @cantoo/pdf-lib +- **libpdf** is 33.11x faster than pdf-lib +- **libpdf** is 76.20x faster than @cantoo/pdf-lib ### Load, modify, and save PDF | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | -------: | -------: | -----: | ------: | -| pdf-lib | 3.2 | 311.96ms | 324.91ms | ±1.22% | 10 | -| libpdf | 3.0 | 335.92ms | 352.02ms | ±1.35% | 10 | -| @cantoo/pdf-lib | 1.7 | 592.93ms | 622.90ms | ±1.49% | 10 | +| libpdf | 33.2 | 30.14ms | 32.37ms | ±2.10% | 17 | +| pdf-lib | 18.8 | 53.31ms | 58.30ms | ±2.85% | 10 | +| @cantoo/pdf-lib | 7.7 | 129.42ms | 131.52ms | ±0.82% | 10 | -- **pdf-lib** is 1.08x faster than libpdf -- **pdf-lib** is 1.90x faster than @cantoo/pdf-lib +- **libpdf** is 1.77x faster than pdf-lib +- **libpdf** is 4.29x faster than @cantoo/pdf-lib ### Extract single page from 100-page PDF -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :-------------- | ------: | -----: | ------: | -----: | ------: | -| libpdf | 273.1 | 3.66ms | 4.22ms | ±0.91% | 137 | -| pdf-lib | 112.0 | 8.93ms | 10.97ms | ±1.95% | 56 | -| @cantoo/pdf-lib | 104.8 | 9.54ms | 15.18ms | ±3.03% | 53 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :-------------- | ------: | -----: | -----: | -----: | ------: | +| libpdf | 405.7 | 2.47ms | 2.92ms | ±0.88% | 203 | +| pdf-lib | 138.5 | 7.22ms | 8.24ms | ±1.63% | 70 | +| @cantoo/pdf-lib | 135.3 | 7.39ms | 9.13ms | ±1.23% | 68 | -- **libpdf** is 2.44x faster than pdf-lib -- **libpdf** is 2.61x faster than @cantoo/pdf-lib +- **libpdf** is 2.93x faster than pdf-lib +- **libpdf** is 3.00x faster than @cantoo/pdf-lib ### Split 100-page PDF into single-page PDFs | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | ------: | ------: | -----: | ------: | -| libpdf | 24.6 | 40.69ms | 44.31ms | ±2.13% | 13 | -| pdf-lib | 13.9 | 71.78ms | 79.07ms | ±5.19% | 7 | -| @cantoo/pdf-lib | 13.2 | 76.00ms | 84.91ms | ±5.84% | 7 | +| libpdf | 39.1 | 25.55ms | 26.49ms | ±0.97% | 20 | +| @cantoo/pdf-lib | 23.8 | 41.93ms | 43.63ms | ±1.75% | 12 | +| pdf-lib | 23.8 | 41.97ms | 44.67ms | ±2.63% | 12 | -- **libpdf** is 1.76x faster than pdf-lib -- **libpdf** is 1.87x faster than @cantoo/pdf-lib +- **libpdf** is 1.64x faster than @cantoo/pdf-lib +- **libpdf** is 1.64x faster than pdf-lib ### Split 2000-page PDF into single-page PDFs (0.9MB) | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | -------: | -------: | -----: | ------: | -| libpdf | 1.3 | 775.20ms | 775.20ms | ±0.00% | 1 | -| pdf-lib | 0.746 | 1.34s | 1.34s | ±0.00% | 1 | -| @cantoo/pdf-lib | 0.698 | 1.43s | 1.43s | ±0.00% | 1 | +| libpdf | 2.0 | 494.73ms | 494.73ms | ±0.00% | 1 | +| pdf-lib | 1.3 | 791.22ms | 791.22ms | ±0.00% | 1 | +| @cantoo/pdf-lib | 1.2 | 813.65ms | 813.65ms | ±0.00% | 1 | -- **libpdf** is 1.73x faster than pdf-lib -- **libpdf** is 1.85x faster than @cantoo/pdf-lib +- **libpdf** is 1.60x faster than pdf-lib +- **libpdf** is 1.64x faster than @cantoo/pdf-lib ### Copy 10 pages between documents | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | ------: | ------: | -----: | ------: | -| libpdf | 213.4 | 4.69ms | 5.53ms | ±1.09% | 107 | -| pdf-lib | 87.3 | 11.46ms | 13.09ms | ±1.27% | 44 | -| @cantoo/pdf-lib | 76.9 | 13.01ms | 14.06ms | ±1.65% | 39 | +| libpdf | 323.5 | 3.09ms | 3.78ms | ±1.01% | 162 | +| pdf-lib | 101.9 | 9.81ms | 10.87ms | ±1.34% | 51 | +| @cantoo/pdf-lib | 89.5 | 11.18ms | 12.49ms | ±1.64% | 45 | -- **libpdf** is 2.45x faster than pdf-lib -- **libpdf** is 2.78x faster than @cantoo/pdf-lib +- **libpdf** is 3.17x faster than pdf-lib +- **libpdf** is 3.62x faster than @cantoo/pdf-lib ### Merge 2 x 100-page PDFs | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | ------: | ------: | -----: | ------: | -| libpdf | 64.7 | 15.46ms | 19.28ms | ±1.91% | 33 | -| pdf-lib | 19.2 | 51.99ms | 53.22ms | ±0.70% | 10 | -| @cantoo/pdf-lib | 16.1 | 62.15ms | 62.87ms | ±0.72% | 9 | +| libpdf | 93.0 | 10.76ms | 15.01ms | ±2.09% | 47 | +| pdf-lib | 20.8 | 47.97ms | 50.44ms | ±1.84% | 11 | +| @cantoo/pdf-lib | 17.3 | 57.71ms | 59.50ms | ±1.32% | 9 | -- **libpdf** is 3.36x faster than pdf-lib -- **libpdf** is 4.02x faster than @cantoo/pdf-lib +- **libpdf** is 4.46x faster than pdf-lib +- **libpdf** is 5.37x faster than @cantoo/pdf-lib ### Fill FINTRAC form fields | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | ------: | ------: | -----: | ------: | -| libpdf | 45.7 | 21.90ms | 28.15ms | ±4.26% | 23 | -| pdf-lib | 35.8 | 27.93ms | 40.97ms | ±6.27% | 18 | -| @cantoo/pdf-lib | 35.5 | 28.17ms | 36.45ms | ±5.73% | 18 | +| libpdf | 75.2 | 13.30ms | 15.84ms | ±2.27% | 38 | +| pdf-lib | 50.0 | 20.01ms | 22.70ms | ±1.83% | 25 | +| @cantoo/pdf-lib | 48.2 | 20.76ms | 22.46ms | ±1.77% | 25 | -- **libpdf** is 1.28x faster than pdf-lib -- **libpdf** is 1.29x faster than @cantoo/pdf-lib +- **libpdf** is 1.50x faster than pdf-lib +- **libpdf** is 1.56x faster than @cantoo/pdf-lib ### Fill and flatten FINTRAC form | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------- | ------: | ------: | ------: | -----: | ------: | -| libpdf | 57.2 | 17.49ms | 21.06ms | ±2.37% | 29 | +| libpdf | 86.6 | 11.55ms | 15.85ms | ±2.38% | 44 | | pdf-lib | FAILED | - | - | - | 0 | -| @cantoo/pdf-lib | 30.9 | 32.34ms | 48.77ms | ±7.63% | 16 | +| @cantoo/pdf-lib | 45.7 | 21.88ms | 24.74ms | ±2.53% | 23 | -- **libpdf** is 1.85x faster than @cantoo/pdf-lib +- **libpdf** is 1.90x faster than @cantoo/pdf-lib ## Copying ### Copy pages between documents -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :------------------------------ | ------: | -----: | ------: | -----: | ------: | -| copy 1 page | 879.5 | 1.14ms | 2.42ms | ±2.60% | 440 | -| copy 10 pages from 100-page PDF | 203.5 | 4.91ms | 8.59ms | ±2.89% | 102 | -| copy all 100 pages | 122.6 | 8.15ms | 13.21ms | ±2.89% | 62 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :------------------------------ | ------: | -----: | -----: | -----: | ------: | +| copy 1 page | 1.7K | 592us | 936us | ±0.84% | 845 | +| copy 10 pages from 100-page PDF | 327.7 | 3.05ms | 3.83ms | ±1.10% | 164 | +| copy all 100 pages | 181.0 | 5.52ms | 7.38ms | ±1.61% | 91 | -- **copy 1 page** is 4.32x faster than copy 10 pages from 100-page PDF -- **copy 1 page** is 7.17x faster than copy all 100 pages +- **copy 1 page** is 5.15x faster than copy 10 pages from 100-page PDF +- **copy 1 page** is 9.33x faster than copy all 100 pages ### Duplicate pages within same document -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :---------------------------------------- | ------: | -----: | -----: | -----: | ------: | -| duplicate page 0 | 964.8 | 1.04ms | 1.51ms | ±0.82% | 483 | -| duplicate all pages (double the document) | 960.8 | 1.04ms | 1.68ms | ±1.07% | 481 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :---------------------------------------- | ------: | ----: | -----: | -----: | ------: | +| duplicate page 0 | 1.5K | 671us | 1.06ms | ±1.06% | 745 | +| duplicate all pages (double the document) | 1.4K | 716us | 1.84ms | ±4.67% | 699 | -- **duplicate page 0** is 1.00x faster than duplicate all pages (double the document) +- **duplicate page 0** is 1.07x faster than duplicate all pages (double the document) ### Merge PDFs -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :---------------------- | ------: | ------: | ------: | -----: | ------: | -| merge 2 small PDFs | 625.9 | 1.60ms | 2.07ms | ±1.04% | 314 | -| merge 10 small PDFs | 122.4 | 8.17ms | 11.80ms | ±1.72% | 62 | -| merge 2 x 100-page PDFs | 66.9 | 14.96ms | 20.68ms | ±2.50% | 34 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :---------------------- | ------: | ------: | -------: | ------: | ------: | +| merge 2 small PDFs | 1.0K | 989us | 1.33ms | ±0.75% | 506 | +| merge 10 small PDFs | 179.2 | 5.58ms | 6.47ms | ±1.11% | 90 | +| merge 2 x 100-page PDFs | 69.3 | 14.44ms | 123.94ms | ±45.58% | 35 | -- **merge 2 small PDFs** is 5.11x faster than merge 10 small PDFs -- **merge 2 small PDFs** is 9.36x faster than merge 2 x 100-page PDFs +- **merge 2 small PDFs** is 5.64x faster than merge 10 small PDFs +- **merge 2 small PDFs** is 14.59x faster than merge 2 x 100-page PDFs ## Drawing | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :---------------------------------- | ------: | -----: | -----: | -----: | ------: | -| draw 100 lines | 1.7K | 572us | 1.18ms | ±1.24% | 874 | -| draw 100 rectangles | 1.6K | 641us | 1.35ms | ±1.88% | 781 | -| draw 100 circles | 1.1K | 937us | 1.78ms | ±1.67% | 534 | -| create 10 pages with mixed content | 661.1 | 1.51ms | 2.66ms | ±2.12% | 331 | -| draw 100 text lines (standard font) | 601.6 | 1.66ms | 2.96ms | ±2.10% | 301 | +| draw 100 lines | 3.0K | 334us | 640us | ±0.93% | 1,496 | +| draw 100 rectangles | 2.9K | 347us | 613us | ±0.83% | 1,443 | +| draw 100 circles | 1.7K | 573us | 957us | ±0.96% | 873 | +| create 10 pages with mixed content | 1.1K | 881us | 1.31ms | ±1.05% | 568 | +| draw 100 text lines (standard font) | 945.1 | 1.06ms | 1.46ms | ±0.95% | 473 | -- **draw 100 lines** is 1.12x faster than draw 100 rectangles -- **draw 100 lines** is 1.64x faster than draw 100 circles -- **draw 100 lines** is 2.64x faster than create 10 pages with mixed content -- **draw 100 lines** is 2.90x faster than draw 100 text lines (standard font) +- **draw 100 lines** is 1.04x faster than draw 100 rectangles +- **draw 100 lines** is 1.71x faster than draw 100 circles +- **draw 100 lines** is 2.63x faster than create 10 pages with mixed content +- **draw 100 lines** is 3.16x faster than draw 100 text lines (standard font) ## Forms -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :---------------- | ------: | ------: | ------: | -----: | ------: | -| read field values | 342.2 | 2.92ms | 5.33ms | ±2.63% | 172 | -| get form fields | 314.9 | 3.18ms | 5.87ms | ±3.14% | 158 | -| flatten form | 124.4 | 8.04ms | 10.66ms | ±1.62% | 63 | -| fill text fields | 80.5 | 12.42ms | 16.09ms | ±3.50% | 41 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :---------------- | ------: | -----: | ------: | -----: | ------: | +| read field values | 542.3 | 1.84ms | 2.26ms | ±0.73% | 272 | +| get form fields | 518.0 | 1.93ms | 2.52ms | ±1.05% | 259 | +| flatten form | 176.2 | 5.68ms | 10.87ms | ±3.48% | 89 | +| fill text fields | 134.7 | 7.42ms | 8.44ms | ±1.31% | 68 | -- **read field values** is 1.09x faster than get form fields -- **read field values** is 2.75x faster than flatten form -- **read field values** is 4.25x faster than fill text fields +- **read field values** is 1.05x faster than get form fields +- **read field values** is 3.08x faster than flatten form +- **read field values** is 4.03x faster than fill text fields ## Loading -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :--------------------- | ------: | ------: | ------: | -----: | ------: | -| load small PDF (888B) | 15.7K | 64us | 188us | ±1.41% | 7,827 | -| load medium PDF (19KB) | 11.1K | 90us | 128us | ±0.64% | 5,547 | -| load form PDF (116KB) | 704.4 | 1.42ms | 2.61ms | ±2.05% | 353 | -| load heavy PDF (2.0MB) | 55.4 | 18.04ms | 20.34ms | ±1.76% | 28 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :--------------------- | ------: | -----: | -----: | -----: | ------: | +| load small PDF (888B) | 27.8K | 36us | 73us | ±0.64% | 13,897 | +| load medium PDF (19KB) | 16.1K | 62us | 120us | ±0.70% | 8,066 | +| load form PDF (116KB) | 1.0K | 981us | 1.35ms | ±0.84% | 510 | +| load heavy PDF (9.9MB) | 614.0 | 1.63ms | 2.05ms | ±0.82% | 308 | -- **load small PDF (888B)** is 1.41x faster than load medium PDF (19KB) -- **load small PDF (888B)** is 22.22x faster than load form PDF (116KB) -- **load small PDF (888B)** is 282.32x faster than load heavy PDF (2.0MB) +- **load small PDF (888B)** is 1.72x faster than load medium PDF (19KB) +- **load small PDF (888B)** is 27.26x faster than load form PDF (116KB) +- **load small PDF (888B)** is 45.26x faster than load heavy PDF (9.9MB) ## Saving -| Benchmark | ops/sec | Mean | p99 | RME | Samples | -| :--------------------------------- | ------: | ------: | ------: | -----: | ------: | -| save unmodified (19KB) | 8.5K | 117us | 310us | ±1.59% | 4,262 | -| incremental save (19KB) | 5.7K | 177us | 409us | ±1.21% | 2,831 | -| save with modifications (19KB) | 1.2K | 865us | 1.64ms | ±1.96% | 578 | -| save heavy PDF (2.0MB) | 54.0 | 18.52ms | 19.37ms | ±1.12% | 28 | -| incremental save heavy PDF (2.0MB) | 49.7 | 20.11ms | 22.82ms | ±1.71% | 25 | +| Benchmark | ops/sec | Mean | p99 | RME | Samples | +| :--------------------------------- | ------: | -----: | -----: | -----: | ------: | +| save unmodified (19KB) | 14.2K | 70us | 173us | ±1.24% | 7,106 | +| incremental save (19KB) | 10.6K | 94us | 169us | ±0.67% | 5,322 | +| save with modifications (19KB) | 2.0K | 509us | 819us | ±0.69% | 983 | +| save heavy PDF (9.9MB) | 585.1 | 1.71ms | 2.12ms | ±0.75% | 293 | +| incremental save heavy PDF (9.9MB) | 438.4 | 2.28ms | 2.65ms | ±0.98% | 220 | -- **save unmodified (19KB)** is 1.51x faster than incremental save (19KB) -- **save unmodified (19KB)** is 7.37x faster than save with modifications (19KB) -- **save unmodified (19KB)** is 157.83x faster than save heavy PDF (2.0MB) -- **save unmodified (19KB)** is 171.42x faster than incremental save heavy PDF (2.0MB) +- **save unmodified (19KB)** is 1.34x faster than incremental save (19KB) +- **save unmodified (19KB)** is 7.24x faster than save with modifications (19KB) +- **save unmodified (19KB)** is 24.29x faster than save heavy PDF (9.9MB) +- **save unmodified (19KB)** is 32.42x faster than incremental save heavy PDF (9.9MB) ## Splitting @@ -255,32 +257,32 @@ | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :--------------------------------------- | ------: | ------: | ------: | -----: | ------: | -| extractPages (1 page from small PDF) | 894.5 | 1.12ms | 2.17ms | ±2.68% | 448 | -| extractPages (1 page from 100-page PDF) | 278.1 | 3.60ms | 4.20ms | ±0.85% | 140 | -| extractPages (1 page from 2000-page PDF) | 17.7 | 56.49ms | 66.21ms | ±4.38% | 10 | +| extractPages (1 page from small PDF) | 1.7K | 598us | 995us | ±0.93% | 836 | +| extractPages (1 page from 100-page PDF) | 423.5 | 2.36ms | 2.75ms | ±0.71% | 212 | +| extractPages (1 page from 2000-page PDF) | 26.3 | 38.01ms | 39.35ms | ±0.86% | 14 | -- **extractPages (1 page from small PDF)** is 3.22x faster than extractPages (1 page from 100-page PDF) -- **extractPages (1 page from small PDF)** is 50.53x faster than extractPages (1 page from 2000-page PDF) +- **extractPages (1 page from small PDF)** is 3.95x faster than extractPages (1 page from 100-page PDF) +- **extractPages (1 page from small PDF)** is 63.51x faster than extractPages (1 page from 2000-page PDF) ### Split into single-page PDFs | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :-------------------------- | ------: | -------: | -------: | -----: | ------: | -| split 100-page PDF (0.1MB) | 23.9 | 41.87ms | 45.60ms | ±2.39% | 12 | -| split 2000-page PDF (0.9MB) | 1.3 | 757.55ms | 757.55ms | ±0.00% | 1 | +| split 100-page PDF (0.1MB) | 39.0 | 25.65ms | 26.87ms | ±1.14% | 20 | +| split 2000-page PDF (0.9MB) | 2.0 | 494.59ms | 494.59ms | ±0.00% | 1 | -- **split 100-page PDF (0.1MB)** is 18.09x faster than split 2000-page PDF (0.9MB) +- **split 100-page PDF (0.1MB)** is 19.28x faster than split 2000-page PDF (0.9MB) ### Batch page extraction | Benchmark | ops/sec | Mean | p99 | RME | Samples | | :----------------------------------------------------- | ------: | ------: | ------: | -----: | ------: | -| extract first 10 pages from 2000-page PDF | 16.9 | 59.28ms | 65.80ms | ±4.52% | 9 | -| extract first 100 pages from 2000-page PDF | 16.5 | 60.43ms | 62.13ms | ±1.68% | 9 | -| extract every 10th page from 2000-page PDF (200 pages) | 15.0 | 66.78ms | 69.86ms | ±2.08% | 8 | +| extract first 10 pages from 2000-page PDF | 25.9 | 38.63ms | 41.22ms | ±1.42% | 14 | +| extract first 100 pages from 2000-page PDF | 24.6 | 40.69ms | 41.49ms | ±1.09% | 13 | +| extract every 10th page from 2000-page PDF (200 pages) | 22.4 | 44.62ms | 45.13ms | ±0.59% | 12 | -- **extract first 10 pages from 2000-page PDF** is 1.02x faster than extract first 100 pages from 2000-page PDF -- **extract first 10 pages from 2000-page PDF** is 1.13x faster than extract every 10th page from 2000-page PDF (200 pages) +- **extract first 10 pages from 2000-page PDF** is 1.05x faster than extract first 100 pages from 2000-page PDF +- **extract first 10 pages from 2000-page PDF** is 1.15x faster than extract every 10th page from 2000-page PDF (200 pages) --- diff --git a/scripts/bench-comment.ts b/scripts/bench-comment.ts index 5f215bf..d63101d 100644 --- a/scripts/bench-comment.ts +++ b/scripts/bench-comment.ts @@ -9,6 +9,8 @@ import { readFileSync } from "node:fs"; +import { getLibraryVersions } from "./bench-versions"; + interface Bench { name: string; mean: number; @@ -116,7 +118,8 @@ const runner = process.env.BENCH_RUNNER ?? "local"; lines.push( `
Environment\n\n` + `- Runner: \`${runner}\`\n` + - `- Runtime: Bun ${process.versions.bun}\n\n` + + `- Runtime: Bun ${process.versions.bun}\n` + + `- Libraries: ${getLibraryVersions()}\n\n` + `*Results are machine-dependent.*\n` + `
`, ); diff --git a/scripts/bench-report.ts b/scripts/bench-report.ts index 37dc19b..e93e6e7 100644 --- a/scripts/bench-report.ts +++ b/scripts/bench-report.ts @@ -14,6 +14,8 @@ import { execSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { cpus, platform, totalmem } from "node:os"; +import { getLibraryVersions } from "./bench-versions"; + // ───────────────────────────────────────────────────────────────────────────── // Types for vitest bench JSON output // ───────────────────────────────────────────────────────────────────────────── @@ -122,6 +124,8 @@ function generateMarkdown(data: BenchmarkOutput): string { lines.push(`> Generated on ${dateStr} at ${timeStr} UTC`); lines.push(`>`); lines.push(`> System: ${getSystemInfo()}`); + lines.push(`>`); + lines.push(`> Libraries: ${getLibraryVersions()}`); lines.push(""); lines.push("---"); lines.push(""); diff --git a/scripts/bench-versions.ts b/scripts/bench-versions.ts new file mode 100644 index 0000000..c510768 --- /dev/null +++ b/scripts/bench-versions.ts @@ -0,0 +1,64 @@ +/** + * Resolves installed versions of the libraries used in benchmark comparisons. + * + * Versions are read from node_modules at generation time, so reports always + * reflect the packages that actually ran, not the semver ranges in + * package.json. + */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Competitor libraries used in benchmarks/comparison.bench.ts. */ +export const COMPARISON_LIBRARIES = ["pdf-lib", "@cantoo/pdf-lib"] as const; + +function readPackageMetadata(path: string): { name?: string; version?: string } { + const value: unknown = JSON.parse(readFileSync(path, "utf-8")); + + if (typeof value !== "object" || value === null) { + return {}; + } + + const name = "name" in value && typeof value.name === "string" ? value.name : undefined; + const version = + "version" in value && typeof value.version === "string" ? value.version : undefined; + + return { name, version }; +} + +/** + * Read the installed version of a package from node_modules. + * Returns "unknown" if the package cannot be resolved. + */ +export function getInstalledVersion(name: string, root = process.cwd()): string { + try { + const pkgPath = join(root, "node_modules", name, "package.json"); + const pkg = readPackageMetadata(pkgPath); + + return pkg.version ?? "unknown"; + } catch { + return "unknown"; + } +} + +/** + * Format a one-line summary of all benchmarked library versions, + * e.g. "@libpdf/core 0.1.0 (this repo), pdf-lib 1.17.1, @cantoo/pdf-lib 2.9.1". + */ +export function getLibraryVersions(root = process.cwd()): string { + let ownLabel = "@libpdf/core"; + + try { + const pkg = readPackageMetadata(join(root, "package.json")); + + ownLabel = `${pkg.name ?? "@libpdf/core"} ${pkg.version ?? "unknown"} (this repo)`; + } catch { + ownLabel = "@libpdf/core unknown (this repo)"; + } + + const competitors = COMPARISON_LIBRARIES.map( + name => `${name} ${getInstalledVersion(name, root)}`, + ); + + return [ownLabel, ...competitors].join(", "); +}