OPFS sync write benchmark
2026-09-16T03:20:09.457Z · AMD Ryzen 7 7800X3D 8-Core Processor (16 threads), 65 GB, linux 7.0.0-31-generic x64
Config: preset=quick, N=2000, S=4096 B, chunks=64 KiB / 1 MiB, R=3, M=200 · Profiles on: ext4 (/)
- Traced with strace: durability syscalls are counted below; ptrace overhead applies to those calls only.
Per-call write cost
Workloads 1–4. small-overwrite is the fixed per-call floor; small-append adds the cost of growing the file; chunked-append writes the same bytes in far fewer calls; append+flush is a control, since both browsers flush per call. Mean time per call in the median run, log scale; the distance between the marks is the ratio between browsers, printed on the right. Hover a mark for p50 and p99.
| Workload | Chrome 152 µs/call | p50 / p99 (µs) | total | Firefox 155 µs/call | p50 / p99 (µs) | total | Firefox 155 ÷ Chrome 152 |
|---|---|---|---|---|---|---|---|
| small-append 2000 × write(4096 B) at offset = current size; the file grows on every call | 25.5 µs | 10 / 15 | 51.1 ms | 287 µs | 280 / 400 | 573.5 ms | 11.2× |
| small-overwrite 2000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes | 12.7 µs | 10 / 15 | 25.3 ms | 283 µs | 280 / 340 | 566.9 ms | 22.4× |
| chunked-append (64 KiB) 125 × write(65536 B) appending; the same 8192000 B total as small-append | 46.9 µs | 20 / 900 | 5.9 ms | 293 µs | 300 / 360 | 36.7 ms | 6.3× |
| chunked-append (1 MiB) 8 × write(1048576 B) appending; the same 8192000 B total as small-append | 366 µs | 160 / 670 | 2.9 ms | 438 µs | 420 / 460 | 3.5 ms | 1.2× |
| append+flush small-append with flush() after every write (2000 × 4096 B) | 5142 µs | 5490 / 6290 | 10.3 s | 5949 µs | 5960 / 10540 | 11.9 s | 1.2× |
Per-file cost
Workload 5. Each step of create+open, first write, close and delete timed on its own, per file, plus the full cycle. Populating a file system pays the first three per file. Mean time per call in the median run, log scale; the distance between the marks is the ratio between browsers, printed on the right. Hover a mark for p50 and p99.
| Workload | Chrome 152 µs/call | p50 / p99 (µs) | total | Firefox 155 µs/call | p50 / p99 (µs) | total | Firefox 155 ÷ Chrome 152 |
|---|---|---|---|---|---|---|---|
| new-file: create+open getFileHandle({create: true}) + createSyncAccessHandle() on a name that does not exist yet | 1693 µs | 1670 / 1970 | 338.6 ms | 49.1 ms | 47720 / 62020 | 9.8 s | 29.0× |
| new-file: first write first write(4096 B, {at: 0}) into the empty file | 365 µs | 360 / 420 | 73.0 ms | 587 µs | 580 / 760 | 117.3 ms | 1.6× |
| new-file: close close() on the access handle | 496 µs | 485 / 605 | 99.2 ms | 31.9 ms | 31220 / 40520 | 6.4 s | 64.2× |
| new-file: delete removeEntry(name) | 480 µs | 465 / 615 | 96.0 ms | 32.3 ms | 31580 / 40360 | 6.5 s | 67.4× |
| new-file: full cycle create+open → write 4096 B → close → delete, per file | 3035 µs | 3000 / 3405 | 607.1 ms | 113.9 ms | 111100 / 150120 | 22.8 s | 37.5× |
Durability syscalls per call (strace)
fsync,fdatasync,syncfs,sync_file_range counted by strace and attributed to each pass's timed loop by wall-clock window; "per call" divides by calls × passes, warm-up included. "Outside the loop" is the per-pass file creation and deletion that workloads 1–4 do around their timed loop; new-file times those steps inside the loop.
| Workload | Chrome 152 syncs/call | ms in sync/call | outside loop, per pass | Firefox 155 syncs/call | ms in sync/call | outside loop, per pass |
|---|---|---|---|---|---|---|
| small-append | 0.00 | 0.00 | 0.00 | 0.01 | 0.09 | 26 |
| small-overwrite | 0.00 | 0.00 | 0.25 | 0.00 | 0.01 | 21 |
| chunked-append (64 KiB) | 0.00 | 0.01 | 0.50 | 0.00 | 0.00 | 21 |
| chunked-append (1 MiB) | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 21 |
| append+flush | 1.0 | 5.34 | 1.0 | 1.0 | 5.55 | 22 |
| new-file | 0.00 | 0.00 | 0.00 | 21 | 111.43 | 0.00 |
Chrome 152: 8351 calls, 44.6 s inside them, 233 outside any workload. Most-synced files:
profile/Default/File System/000/t/00/00000017: 2000 calls, 10.47 sprofile/Default/File System/000/t/00/00000018: 2000 calls, 10.12 sprofile/Default/File System/000/t/00/00000019: 2000 calls, 11.07 sprofile/Default/File System/000/t/00/00000020: 2000 calls, 10.15 sprofile/Default: 25 calls, 0.25 sprofile/Default/Web Data-journal: 24 calls, 0.18 sprofile/Default/Affiliation Database-journal: 16 calls, 0.11 sprofile/Default/Reporting and NEL-journal: 15 calls, 0.11 s
Firefox 155: 25759 calls, 138.7 s inside them, 311 outside any workload. Most-synced files:
profile/storage/default/http+++127.0.0.1+44213/fs/metadata.sqlite-journal: 5750 calls, 30.57 sprofile/storage/default/http+++127.0.0.1+44213/fs: 5750 calls, 30.65 sprofile/storage/default/http+++127.0.0.1+44213/fs/metadata.sqlite: 5750 calls, 30.30 sprofile/storage/default/http+++127.0.0.1+44213/fs/YI/YIN2PGVCO5HGFBMXRITH2SCSESIMWVF4RA66LSEJ7G2ZJQFTJTCA: 2000 calls, 11.53 sprofile/storage/default/http+++127.0.0.1+44213/fs/OM/OMKL7A62YKIR5FNZXR7KVKRGZHWFNYQ5FRWJV5XJL5F36NM4BEVA: 2000 calls, 10.85 sprofile/storage/default/http+++127.0.0.1+44213/fs/OQ/OQVEJJKZOJMSZVZ4PLVBOJ6VJAO4ECYRNVJNSBXM3Q3K6E5XWRXQ: 2000 calls, 10.19 sprofile/storage/default/http+++127.0.0.1+44213/fs/R5/R5M7P3S25NWISAUGKKSO22Y7JW6U2Z54GO6QRKU32TCX7JAYZBWA: 2000 calls, 11.21 sprofile/storage/permanent/chrome/idb/3870112724rsegmnoittet-es.sqlite-wal: 52 calls, 0.39 s
Environment
| Browser | Version | Mode | Profile on | performance.now() step | Isolated | Executable | User agent |
|---|---|---|---|---|---|---|---|
| Chrome 152 | Chrome/152.0.7977.82 | headless, strace | ext4 (/) | 5 µs | yes | /usr/bin/google-chrome | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 |
| Firefox 155 | firefox/155.0.1 | headless, strace | ext4 (/) | 20 µs | yes | /home/theo/.cache/puppeteer/firefox/linux-stable_155.0.1/firefox/firefox | Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0 |
Chrome 152: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark** Environment: - User agent: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36` - Platform: Linux x86_64; hardwareConcurrency: 16 - Timestamp: 2026-09-16T03:20:09.457Z - performance.now() resolution in the worker: 5.0 µs (crossOriginIsolated: true) - navigator.storage.estimate(): 0.0 MB used of 10737 MB - Config: preset=quick, N=2000, S=4096 B, chunks=64 KiB / 1 MiB, R=3, M=200 Median run of R=3 (by total time) after one warm-up pass; latency percentiles are per call; "1st call" is call number 1 of that run (for the append workloads, the first write into the freshly created file): | Workload | Calls | Bytes/call | Total (ms) | Total min–max (ms) | ops/s | MB/s | p50 (µs) | p90 (µs) | p99 (µs) | max (µs) | 1st call (µs) | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | small-append | 2,000 | 4,096 | 51.1 | 50.4–53.1 | 39,150 | 160.4 | 10.0 | 10.0 | 15.0 | 15750.0 | 385.0 | | small-overwrite | 2,000 | 4,096 | 25.3 | 24.0–27.0 | 78,958 | 323.4 | 10.0 | 10.0 | 15.0 | 9140.0 | 15.0 | | chunked-append (64 KiB) | 125 | 65,536 | 5.9 | 5.7–6.1 | 21,313 | 1396.8 | 20.0 | 25.0 | 900.0 | 1220.0 | 390.0 | | chunked-append (1 MiB) | 8 | 1,048,576 | 2.9 | 2.6–3.3 | 2,735 | 2800.7 | 160.0 | 670.0 | 670.0 | 670.0 | 635.0 | | append+flush | 2,000 | 4,096 | 10284.4 | 10250.9–11208.2 | 194 | 0.8 | 5490.0 | 5665.0 | 6290.0 | 10075.0 | 5915.0 | | new-file: create+open | 200 | – | 338.6 | 338.3–338.8 | 591 | – | 1670.0 | 1800.0 | 1970.0 | 2010.0 | 1955.0 | | new-file: first write | 200 | 4,096 | 73.0 | 72.6–73.7 | 2,738 | 11.2 | 360.0 | 395.0 | 420.0 | 440.0 | 395.0 | | new-file: close | 200 | – | 99.2 | 98.5–99.4 | 2,015 | – | 485.0 | 535.0 | 605.0 | 620.0 | 530.0 | | new-file: delete | 200 | – | 96.0 | 95.4–97.2 | 2,083 | – | 465.0 | 525.0 | 615.0 | 620.0 | 445.0 | | new-file: full cycle | 200 | 4,096 | 607.1 | 606.2–608.3 | 329 | 1.3 | 3000.0 | 3200.0 | 3405.0 | 3990.0 | 2930.0 | Derived: - Fixed per-call floor (small-overwrite, size never changes): 12.7 µs/call mean, 10.0 µs p50 - Size-growing write (small-append): 25.5 µs/call mean, 10.0 µs p50 - Extra cost per call when the write grows the file (append − overwrite): 12.9 µs mean, 0.0 µs p50 - Same 8.2 MB in 125 × 64 KiB calls: 5.9 ms (1397 MB/s) vs 51.1 ms for 2,000 × 4 KiB; the small calls are 8.7× slower for the same bytes - Same 8.2 MB in 8 × 1 MiB calls: 2.9 ms (2801 MB/s) vs 51.1 ms for 2,000 × 4 KiB; the small calls are 17.5× slower for the same bytes - flush() after every write adds 5116.7 µs/call on top of small-append (append+flush: 5142.2 µs/call mean) - small-append: first call into the fresh file 385.0 µs vs p50 10.0 / p99 15.0 µs; across runs 340.0–400.0 µs (far above p99) - chunked-append (64 KiB): first call into the fresh file 390.0 µs vs p50 20.0 / p99 900.0 µs; across runs 355.0–395.0 µs - chunked-append (1 MiB): first call into the fresh file 635.0 µs vs p50 160.0 / p99 670.0 µs; across runs 495.0–635.0 µs - append+flush: first call into the fresh file 5915.0 µs vs p50 5490.0 / p99 6290.0 µs; across runs 4925.0–5915.0 µs - new-file: 3.0 ms per file (329 files/s) = create+open 1.7 + first write 0.4 + close 0.5 + delete 0.5 ms - new-file without delete (what populating OPFS pays per file): 2.6 ms - First write into a new file (4 KiB): 365.2 µs vs 25.5 µs per small-append call
Firefox 155: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark** Environment: - User agent: `Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0` - Platform: Linux x86_64; hardwareConcurrency: 16 - Timestamp: 2026-09-16T03:20:58.645Z - performance.now() resolution in the worker: 20 µs (crossOriginIsolated: true) - navigator.storage.estimate(): 0.5 MB used of 10737 MB - Config: preset=quick, N=2000, S=4096 B, chunks=64 KiB / 1 MiB, R=3, M=200 Median run of R=3 (by total time) after one warm-up pass; latency percentiles are per call; "1st call" is call number 1 of that run (for the append workloads, the first write into the freshly created file): | Workload | Calls | Bytes/call | Total (ms) | Total min–max (ms) | ops/s | MB/s | p50 (µs) | p90 (µs) | p99 (µs) | max (µs) | 1st call (µs) | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | small-append | 2,000 | 4,096 | 573.5 | 545.6–578.2 | 3,487 | 14.3 | 280.0 | 300.0 | 400.0 | 1100.0 | 520.0 | | small-overwrite | 2,000 | 4,096 | 566.9 | 551.4–606.2 | 3,528 | 14.5 | 280.0 | 300.0 | 340.0 | 780.0 | 340.0 | | chunked-append (64 KiB) | 125 | 65,536 | 36.7 | 34.9–37.9 | 3,408 | 223.3 | 300.0 | 300.0 | 360.0 | 440.0 | 440.0 | | chunked-append (1 MiB) | 8 | 1,048,576 | 3.5 | 3.4–3.6 | 2,286 | 2340.6 | 420.0 | 460.0 | 460.0 | 460.0 | 420.0 | | append+flush | 2,000 | 4,096 | 11898.4 | 11227.0–12248.6 | 168 | 0.7 | 5960.0 | 7160.0 | 10540.0 | 15440.0 | 6960.0 | | new-file: create+open | 200 | – | 9818.6 | 9639.7–10752.1 | 20 | – | 47720.0 | 50620.0 | 62020.0 | 115080.0 | 58620.0 | | new-file: first write | 200 | 4,096 | 117.3 | 113.5–117.6 | 1,705 | 7.0 | 580.0 | 640.0 | 760.0 | 1440.0 | 620.0 | | new-file: close | 200 | – | 6376.7 | 6316.2–7102.7 | 31 | – | 31220.0 | 32560.0 | 40520.0 | 41400.0 | 40800.0 | | new-file: delete | 200 | – | 6467.9 | 6408.4–7155.2 | 31 | – | 31580.0 | 33040.0 | 40360.0 | 60900.0 | 39860.0 | | new-file: full cycle | 200 | 4,096 | 22777.8 | 22482.5–25128.6 | 9 | 0.0 | 111100.0 | 117000.0 | 150120.0 | 177800.0 | 139840.0 | Derived: - Fixed per-call floor (small-overwrite, size never changes): 283.4 µs/call mean, 280.0 µs p50 - Size-growing write (small-append): 286.7 µs/call mean, 280.0 µs p50 - Extra cost per call when the write grows the file (append − overwrite): 3.3 µs mean, 0.0 µs p50 - Same 8.2 MB in 125 × 64 KiB calls: 36.7 ms (223 MB/s) vs 573.5 ms for 2,000 × 4 KiB; the small calls are 15.6× slower for the same bytes - Same 8.2 MB in 8 × 1 MiB calls: 3.5 ms (2341 MB/s) vs 573.5 ms for 2,000 × 4 KiB; the small calls are 163.9× slower for the same bytes - flush() after every write adds 5662.5 µs/call on top of small-append (append+flush: 5949.2 µs/call mean) - small-append: first call into the fresh file 520.0 µs vs p50 280.0 / p99 400.0 µs; across runs 320.0–520.0 µs - chunked-append (64 KiB): first call into the fresh file 440.0 µs vs p50 300.0 / p99 360.0 µs; across runs 280.0–440.0 µs - chunked-append (1 MiB): first call into the fresh file 420.0 µs vs p50 420.0 / p99 460.0 µs; across runs 380.0–420.0 µs - append+flush: first call into the fresh file 6960.0 µs vs p50 5960.0 / p99 10540.0 µs; across runs 4960.0–6960.0 µs - new-file: 113.9 ms per file (9 files/s) = create+open 49.1 + first write 0.6 + close 31.9 + delete 32.3 ms - new-file without delete (what populating OPFS pays per file): 81.6 ms - First write into a new file (4 KiB): 586.5 µs vs 286.7 µs per small-append call