Skip to content

Batch sync path and index tombstone scans - #146

Merged
aron-cf merged 1 commit into
mainfrom
sync-path-batching
Sep 14, 2026
Merged

aron-cf merged 1 commit into
mainfrom
sync-path-batching

Conversation

@aron-cf

@aron-cf aron-cf commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

The walk to determine which files changed during a sync now happens once for a whole batch instead of once per file. The database walks the tree for every changed file once:

changed files before after
4,500 31,500 queries 9 queries

The names that come back are the same, including the awkward case of one file with several names. The old single-file version stays for callers that only ever need one.

The second problem was the scan that finds recently deleted files. Finding deletions newer than a marker would read the entire change log to return the handful of rows that were new. An index on the two columns the query filters by lets it jump straight to the recent entries:

before   SCAN vfs_changes USING INDEX vfs_changes_by_path
after    SEARCH vfs_changes USING INDEX vfs_changes_by_op_rev (op=? AND rev>?)

On a log of 40,000 rows, returning the same 50 rows went from 11 ms to 0.06 ms.


Devin Review

@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dba05e8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/dofs Patch
@cloudflare/computer-rpc Patch
@cloudflare/computer Patch
@cloudflare/computerd Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +119 to +123
UNION ALL
SELECT w.target, w.link, w.depth + 1, d.name, d.parent_inode
FROM walk w
JOIN vfs_dirents d ON d.child_inode = w.parent_inode
WHERE w.parent_inode <> ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Cyclic dirents never terminate resolution

pathsOfMany recurses forever when corrupted dirents form a parent cycle. Unlike pathOf, this walk has neither a visited set nor depth bound. A change scan containing that inode can exhaust SQLite resources instead of omitting the unreachable path.

Learn more

The filesystem schema does not enforce an acyclic parent graph, so the existing resolver treats cycles as possible corruption. The recursive term follows each parent without recording visited inodes or limiting depth. A cycle therefore keeps producing rows and prevents db.all from returning. The prior pathOf implementation bounded the same upward walk and returned no path when the root was never reached. Because coalesceChanges now resolves all touched inodes through this CTE, one cyclic chain blocks the entire change scan.

Example: Suppose inode 20 is named under directory 10, directory 10 is named under 11, and directory 11 is named under 10. Resolving inode 20 produces the sequence 20 → 10 → 11 → 10 → 11 indefinitely. The expected result is to omit inode 20 as unreachable, matching pathsOf.

Recommended fix: Carry a visited-inode set or equivalent cycle marker in walk and stop recursion before revisiting a parent. Also retain an explicit depth ceiling so malformed acyclic chains preserve the bounded behavior of pathOf.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Sep 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@146

commit: dba05e8

A push tick resolved every touched inode to its path(s) with one dirent
lookup per ancestor, issuing O(N x depth) statements. On a
node_modules-sized change set that is tens of thousands of round-trips,
and the cost is the statement count rather than the index: every one of
those lookups was already a covering-index hit.

pathsOfMany resolves a batch of inodes in a fixed number of recursive-CTE
statements, seeding one walk per (target, dirent) so hardlink names all
reach the wire. Measured on a node_modules-shaped tree: 6,300 statements
to 2 at 900 files, 31,500 to 9 at 4,500 files, with output identical to
the per-inode implementation for every inode.

The tombstone scan (rev > ? AND op = 'delete' GROUP BY path) had no index
it could drive both halves from, so the planner scanned vfs_changes in
path order, ignored the rev predicate, and read the whole table to return
the few rows in the watermark window. Schema v8 adds
vfs_changes_by_op_rev; the upgrade is index-only and rewrites no rows.

pathsOf and pathOf are retained: fs/rename, fs/rm, fs/writeFile and
sync/apply still resolve a single inode.
@aron-cf
aron-cf merged commit 7ce8259 into main Sep 14, 2026
19 checks passed
@aron-cf
aron-cf deleted the sync-path-batching branch September 14, 2026 12:46
@github-actions github-actions Bot mentioned this pull request Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant