Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7d4a7ae
dofs: add local hardlink support
aron-cf Jun 8, 2026
1728593
wsd: wire FUSE hardlink operations
aron-cf Jun 8, 2026
9cde927
script: cover hardlink writes in fs tests
aron-cf Jun 8, 2026
d7189fc
dofs: inline tiny sync writes
aron-cf Jun 8, 2026
e1b9e3f
wsd: evict clean FUSE buffers on release
aron-cf Jun 8, 2026
a3a2471
wsd: report FUSE buffer memory stats
aron-cf Jun 8, 2026
3ad116e
dofs: add direct range write primitives
aron-cf Jun 8, 2026
55b6433
dofs: use direct writes for fd mutations
aron-cf Jun 8, 2026
ee45474
dofs: expose direct write provider methods
aron-cf Jun 8, 2026
7cc81d1
wsd: write FUSE data directly into DOFS
aron-cf Jun 8, 2026
e643294
wsd: keep direct FUSE reads off the buffer cache
aron-cf Jun 8, 2026
4b806f7
dofs: sync inline direct-write bytes
aron-cf Jun 8, 2026
991ed30
dofs: add positional readRangeSync primitive
aron-cf Jun 8, 2026
7bcf1c5
wsd: dispatch dofs methods through prototype chain
aron-cf Jun 9, 2026
9851d51
dofs: keep untouched chunk rows on direct writes
aron-cf Jun 9, 2026
a4cdf81
wsd: expose database table sizes on /__wsd/stats
aron-cf Jun 9, 2026
17ee0c1
dofs: buffer writes per open file and drop inline storage
aron-cf Jun 9, 2026
8745502
wsd: route FUSE writes through the DOFS write buffer
aron-cf Jun 9, 2026
ebdf157
dofs: fold rowid and rev reads into INSERT/UPDATE via RETURNING
aron-cf Jun 9, 2026
4c0c4ef
dofs: cache file size on vfs_nodes so stat skips SUM
aron-cf Jun 9, 2026
b7bbde2
dofs: defer file creation to release time
aron-cf Jun 9, 2026
f4cad0a
wsd: align FUSE max_read/max_write with the dofs chunk size
aron-cf Jun 9, 2026
1db7fb8
dofs: cache vfs_blob_bytes payloads by hash
aron-cf Jun 9, 2026
417c07e
docs: add a performance section to the README
aron-cf Jun 9, 2026
87f30d9
docs: refresh README and schema docs for the buffered-write surface
aron-cf Jun 9, 2026
b4d557d
dofs: add tests for cached size, pending creates, and flush-on-rename
aron-cf Jun 9, 2026
152eef2
dofs: bridge the write buffer cache through link/rename/unlink
aron-cf Jun 9, 2026
b80cd16
docs: refresh schema invariants and tidy stale naming
aron-cf Jun 9, 2026
780699c
dofs,wsd: pin LRU eviction, multi-open buffers, cached size, and /__w…
aron-cf Jun 9, 2026
4221f18
examples/think: commit worker-configuration.d.ts
aron-cf Jun 9, 2026
99b9833
dofs: lint fixes
aron-cf Jun 9, 2026
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
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,85 @@ package-specific status and usage notes.
(`@cloudflare/workspace`) — the top-level Workspace package
consumed by Durable Objects. Work in progress.

## Performance

Numbers from `script/fs-bench.sh` and a full
`npm install` of [`cloudflare/sandbox-sdk`](https://github.com/cloudflare/sandbox-sdk)
(854 packages, 36,675 files), running
[`examples/wsd-container`](examples/wsd-container) on a Cloudflare
Containers **standard-2** instance (1 vCPU, 6 GiB memory, 12 GB disk).
The wsd FUSE mount lives at `/workspace`; the comparison columns are
an in-memory `tmpfs` at `/tmp` and the container's ext4 root disk at
`/var/tmp`.

Ratios are `wsd / baseline` — lower is faster, values below 1.0 mean
wsd beats the baseline.

### `fs-bench` (REPS=3, WARMUP=1, randomized targets)

| Scenario | wsd | tmpfs | tmpfs ratio | ext4 disk | disk ratio |
|---|---:|---:|---:|---:|---:|
| **tiny-file churn** | | | | | |
| create 1000 files | 560.6 ms | 83.2 ms | 6.7x | 303.2 ms | 1.85x |
| stat 1000 files | 1971.9 ms | 1324.2 ms | 1.49x | 2659.3 ms | **0.91x** |
| rm 1000 files | 827.7 ms | 322.7 ms | 2.56x | 1281.8 ms | **0.66x** |
| **directory traversal** | | | | | |
| mkdir tree (10×10×10) | 1597.5 ms | 1585.7 ms | 1.01x | 3034.7 ms | **0.74x** |
| find tree | 1813.6 ms | 1819.9 ms | 1.00x | 4404.2 ms | **0.72x** |
| **large file I/O** | | | | | |
| write 64 MiB | 230.6 ms | 47.3 ms | 4.87x | 16.8 ms | 16.93x |
| copy 64 MiB | 1037.2 ms | 37.4 ms | 27.75x | 39.8 ms | 40.46x |
| read 64 MiB | 437.5 ms | 22.6 ms | 19.33x | 25.6 ms | 39.72x |
| pure read 64 MiB | 263.1 ms | 8.3 ms | 31.54x | 8.5 ms | 30.26x |
| pure copy 64 MiB | 852.9 ms | 21.7 ms | 39.27x | 22.0 ms | 41.47x |
| overwrite 64 MiB | 272.6 ms | 8.3 ms | 32.91x | 8.5 ms | 43.35x |
| **git** | | | | | |
| git init + commit 100 files | 459.2 ms | 40.3 ms | 9.56x | 635.4 ms | **0.72x** |
| git clone (shallow, ~1MB) | 549.1 ms | 421.0 ms | 1.30x | 576.2 ms | **0.84x** |
| **npm** | | | | | |
| npm init + tiny install | 598.5 ms | 630.7 ms | **0.95x** | 630.7 ms | **0.95x** |

### Full `cloudflare/sandbox-sdk` `npm install`

| Target | Duration |
|---|---:|
| tmpfs (`/tmp`) | 34.3 s |
| wsd FUSE (`/workspace`) | 124.7 s |
| ext4 disk (`/var/tmp`) | 63.9 s |

wsd is ~2x slower than the container's ext4 disk for the full
`npm install`, and ~3.6x slower than tmpfs. The disk comparison is
the more realistic baseline for general usage.

### Where wsd is faster than the disk baseline

The in-memory inode store beats real disk on metadata-heavy work:
`stat`, `rm`, `mkdir tree`, `find tree`, `git init`, `git clone`,
`npm init`. Those eight scenarios cover most of the day-to-day cost
of tools like `git status`, module resolution, and incremental
builds.

### Where wsd is slower

Large sequential file I/O. The wsd write path hashes each
[`CHUNK_SIZE`](packages/dofs/src/fs/writeFile.ts) (512 KiB) chunk
into a content-addressed blob store on every release; that's how
the Durable Object can sync only the chunks that changed and
deduplicate identical content. The cost lands on raw
`dd`-style throughput numbers but rarely on real developer
workloads, which is why `npm init + tiny install` matches the disk
baseline despite `pure read 64 MiB` being 30x slower.

### Reproducing

```bash
bash script/run-fs-bench.sh
```

or against a deployed `wsd-container` instance, upload
[`script/fs-bench.sh`](script/fs-bench.sh) and run it with
`MOUNT=/workspace BASE=/tmp`.

## Documentation

- [`docs/`](docs/README.md) — design specification. Forward-looking;
Expand Down
30 changes: 24 additions & 6 deletions docs/03_filesystem_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ CREATE TABLE vfs_nodes (
mount_root TEXT, -- nullable; tags mount provenance
stub_size INTEGER, -- non-null while a lazy stub
manifest_hash BLOB, -- references vfs_manifests.hash
link_target TEXT -- non-null when type = 'symlink'
link_target TEXT, -- non-null when type = 'symlink'
size INTEGER NOT NULL DEFAULT 0 -- cached file size, kept in sync on writes
);
CREATE INDEX vfs_nodes_by_rev ON vfs_nodes(rev);
```
Expand All @@ -61,7 +62,11 @@ One row per live inode. `mount_root` records the mount this row
originated from, used for write-rejection and writable-mount mirroring.
`stub_size` is non-null while the file is a lazy-mount stub whose
bytes haven't been fetched yet — `stat()` reports it as the file size
and the first read fetches the bytes.
and the first read fetches the bytes. `size` denormalises the
chunk-sum file size onto the node row so `stat`, `lstat`, and the
positional read primitive can read it directly instead of running
`SUM(size) FROM vfs_chunks` on every call. Every write path stamps
it alongside `mode`/`mtime`/`rev`.

The `vfs_nodes_by_rev` index supports `coalesceChanges`'s
`WHERE rev > sinceRev` scan over live inodes, which the sync protocol
Expand Down Expand Up @@ -233,10 +238,23 @@ DO reload doesn't re-list.

- The root directory is always `inode = 1`, type `dir`, with no
parent dirent.
- A `vfs_nodes` row with `type = 'file'` has either:
- `stub_size NOT NULL` and no `vfs_chunks` rows (lazy stub), **or**
- `manifest_hash NOT NULL`, a matching `vfs_manifests` row, and
one `vfs_chunks` row per chunk.
- A `vfs_nodes` row with `type = 'file'` is in one of two shapes:
- **lazy stub**: `stub_size NOT NULL`, no `vfs_chunks` rows. The
first read fetches the bytes and migrates the row to the
committed shape.
- **committed file**: zero or more `vfs_chunks` rows (one per
chunk; an empty file has zero). `manifest_hash` is optional.
When set, a matching `vfs_manifests` row lists the same chunk
hashes and lets sync skip the per-chunk fetch on receivers
that already have the manifest. When `NULL`, sync walks
`vfs_chunks` directly. The buffered-write path commits chunks
with `manifest_hash = NULL`; the legacy whole-file
`writeFileSync` path stamps a manifest.
- For every file row, `vfs_nodes.size = COALESCE(SUM(vfs_chunks.size), 0)`
over its `vfs_chunks` rows. Every write path stamps the column
in the same `UPDATE` that bumps `mode`/`mtime`/`rev`, so `stat`,
`lstat`, and `readRangeSync` can read it directly instead of
running the aggregate.
- Every `vfs_chunks.hash` references an existing `vfs_blobs.hash`.
- Every `vfs_blobs.hash` has a matching `vfs_blob_bytes` row.
- Every `vfs_manifests.hash` referenced by
Expand Down
Loading