Benchmark Sync And Async Recrypt
recrypt() blocks the event loop while it re-encrypts a document.
recryptAsync() does the same work on libuv's thread pool. The benchmark in
packages/native-with-source/benchmarks/recrypt-server.js
shows what that means for a server.
It starts an HTTP server in a child process with two routes:
/recrypt?mode=syncor/recrypt?mode=asyncre-encrypts a PDF withuserPassword,ownerPasswordanduserProtectionFlag: 4./pinganswers at once, like any cheap request on a busy server.
The parent process sends recrypt requests at a fixed concurrency and pings the server the whole time, so the client never shares the server's event loop.
Run It
The benchmark is a script you run by hand; it is not part of the test suite. From the root of a source checkout with a built addon:
npm run native:bench:recrypt
npm run native:bench:recrypt -- --input large.pdf --requests 32 --concurrency 8
npm run native:bench:recrypt -- --json
Inside packages/native-with-source, npm run bench:recrypt takes the same
options.
| Option | Default | Meaning |
|---|---|---|
--input |
generated PDF | The PDF to recrypt. |
--copies |
2 |
Copies of the test fixture in the generated PDF, about 8 MB each. |
--requests |
16 |
Recrypt requests per mode. |
--concurrency |
4 |
Recrypt requests in flight at once. |
--ping-interval |
10 |
Milliseconds between one ping's answer and the next ping. |
--json |
off | Print the measurements as JSON instead of a table. |
runBenchmark(options) and formatTable(result) are exported for scripts.
The file is type-checked against the package's declarations.
Read The Results
| Row | Meaning |
|---|---|
| wall time | Time for all recrypt requests of the mode. |
| recrypts per second | Recrypt throughput. |
| recrypt p50, p95 | How long a client waited for a recrypt response. |
| pings answered | Pings the server answered during the run. |
| ping p50, p99, max | How long a cheap request waited. This is the cost other clients pay. |
| loop delay | How late the server's event loop ran, from monitorEventLoopDelay. The maximum is the longest stall. |
| loop utilization | Share of the run the server's event loop was busy, from eventLoopUtilization. |
Example Results
A 15.9 MB generated PDF, 16 requests per mode with 4 in flight, on Linux x64
with 4 CPUs, Node.js 22.22 and the default UV_THREADPOOL_SIZE of 4:
recrypt |
recryptAsync |
|
|---|---|---|
| wall time | 4.55 s | 4.68 s |
| recrypts per second | 3.5 | 3.4 |
| recrypt p50 | 1107 ms | 1165 ms |
| recrypt p95 | 1667 ms | 1228 ms |
| pings answered | 9 | 425 |
| ping p50 | 520 ms | 0.6 ms |
| ping p99 | 1076 ms | 2.3 ms |
| loop delay max | 751 ms | 6.5 ms |
| loop utilization | 100% | 5% |
Throughput is the same, because recryptAsync runs one recrypt at a time just
as the synchronous server does. The difference is everything else. With
recrypt the server answered 9 pings in four and a half seconds, and a ping
waited up to a second. With recryptAsync pings were answered in under a
millisecond, and the event loop was idle 95% of the time.
The same run against a 95.6 MB PDF (--copies 12) took about 26 s per mode.
With recrypt the server answered 10 pings, and the event loop stalled for
up to 4.9 s; with recryptAsync it answered 2516 pings and never stalled for
more than 16 ms.
Times depend on the machine, the document and its encryption; run the benchmark on your own documents to size a server.