FSA download checkpoints copy the whole file every 512 MiB (quadratic disk I/O on large files) #193
Labels
No labels
backend
bug
CI
dependencies
design
documentation
duplicate
enhancement
frontend
github_actions
good first issue
help wanted
invalid
javascript
question
refactoring
security
tooling
UX
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
NBIS/sda-download-ui#193
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Describe the bug
Follow-up to #173 (resume FSA downloads).
checkpointDownloadinfrontend/src/app/components/FileSystemAccessBatchDownloadActions.tsxcloses the writable stream every 512 MiB (FILE_SYSTEM_DOWNLOAD_CHECKPOINT_BYTES) and reopens it withcreateWritable({ keepExistingData: true }).According to MDN and the File System Standard,
keepExistingData: truemeans the existing file is first copied into the temporary swap file (.crswap) before writing continues. On macOS the swap file can be created copy-on-write, but on Windows and Linux ext4 it is a real copy (see this chromium-dev thread).The disk work per file therefore grows with the square of the file size. With checkpoint size C and file size N, the total copying is roughly N² / (2C):
With
FILE_SYSTEM_BATCH_CONCURRENCY = 2, two files can be doing this at the same time. The last chunk can also pushbytesSinceCheckpointover the limit just before the finalclose(), so a finished file is copied one extra time.Measured
I ran a standalone page that repeats the same loop as
checkpointDownload(write 64 MiB,close(),createWritable({ keepExistingData: true }),seek()) on Chrome 152, Ubuntu 24.04, ext4, local SSD, with a 1 GiB file and 15 checkpoints:close()seek()The run took 61.0 s in total: 3.2 s writing data, 40.4 s in
close()and 17.4 s reopening./proc/diskstatscounted 8351 MiB written for a 1024 MiB file. Writing the same 1 GiB through a single writable with oneclose()took 9.2 s (3.3 s of writes, 5.8 s for that oneclose()) and 1044 MiB of disk writes. Watching the folder during the run, the.crswapfile grows from 0 to the full current size at every reopen. That growth is the copy.On this machine both steps scale with the current file size:
close()at about 4 ms per MiB, which fits Chrome reading the whole swap file for its after-write check before moving it into place, and reopen at about 2 ms per MiB for the copy. Extrapolated to the 512 MiB interval in the PR, a 10 GiB file spends roughly 11 minutes in checkpoints and writes about 105 GiB to disk. A 50 GiB file spends roughly 4.5 hours and writes about 2.5 TiB.Steps to reproduce
iotopor Task Manager) and the size of the.crswapfile in the target folder..crswapfile is recreated with the full size of the file so far, and download throughput drops as the file grows.The numbers above come from a small HTML page (about 60 lines) that only uses the File System Access API. I can attach it if that helps.
Environment
Chrome or Chromium, Linux or Windows. Introduced by #173 (branch
149-resume-FSA-file-downloads).Expected behavior
nextWriteOffset >= totalBytes.Additional context
Possible approaches, from smallest change to largest:
FileSystemSyncAccessHandlein a worker (in-place writes, cheapflush()), and copy into the chosen folder once when the file is complete. This needs quota handling and a worker, so it is a bigger change.Cancelling with the button already commits the partial file through
close(), so checkpoints only matter for crashes and tab closes.Thanks @jhagberg. Related to and will be closed together with #182.
Thanks! I had a look at #182 and I am not sure it closes this one as written. If the time-based flush uses the same
close()+createWritable({ keepExistingData: true })mechanism, every flush copies the whole file so far into a new swap file and writes it back to disk, so on a 10 GiB file one flush would take about a minute on my machine. A time interval would then make this worse rather than better.Would it be worth adding "total disk writes stay proportional to the file size" to the AC of #182, or is the plan to change the flush mechanism there? One option might be one part file per chunk (each closed once) that are concatenated into the final file at the end, which does not need
move(). Staging in OPFS, whereflush()is cheap, would be another.Thanks for the input. The plan was to refine the issue and add what needs to be done, i.e. OPFS.
EDIT: The biggest practical bottleneck here is not the GB written but the close time that needs to go through the whole file. This and the fact that we would like to account for unstable network so flush frequently, makes OPFS the best option for fixing #182 and concomitantly this behavior, too.
Suggested best-effort fix to reduce open/close times was added in #149. Follow-up for implementing OPFS is left for #182.