Fix the O(n^2) bulk create/unlink: flat array -> persistent treap

BENCH.md's earlier finding was real: every structural write (create/
unlink/mkdir) copied the entire sorted UpperSnapshot entry array before
publishing the next snapshot, making bulk sequential creation O(n^2).
concept.md Section 5.3 names the exact condition for reconsidering this
("a persistent structurally-shared tree structure is not required
until this assumption is empirically violated") -- that condition was
measured, not hypothesized, so this closes it rather than leaving it
as a documented-but-open limitation.

The index is now a persistent treap (src/upper.c): a structural write
copies only the O(log n) nodes on the path to the change, sharing
every other node (its own refcount, cascading like MutCell's and
UpperSnapshot's) with whichever snapshot(s) it was built from. Chosen
over a persistent AVL/red-black/weight-balanced tree because deletion
in those can need O(log n) rebalancing rotations -- each a real
allocation in a persistent setting -- where a treap needs only O(1)
amortized rotations for insert and delete (Seidel & Aragon 1996), with
expected O(log n) height regardless of insertion order, including the
sorted-by-creation-order pattern that made the flat array quadratic in
the first place. Liljenzin's "Confluently Persistent Sets and Maps"
(arXiv:1301.3388) documents persistent treaps giving O(1) snapshots
for MVCC specifically, which is this exact use case. Full reasoning
and the ownership convention (functions consume one ref of their tree
arguments, return one owned ref) are in upper.c's comment above
struct TreapNode.

Blast radius kept deliberately small: snapshot_upsert/snapshot_remove
keep their exact original signatures, so upper_create/upper_mkdir/
upper_remove/upper_rename/upper_copy_up needed zero changes.
upper_lookup/upper_has_children keep their exact contracts. Only
upstd_readdir and overlay_readdir's manual array scans became calls to
a new upper_visit_range (O(log n + r) range query, replacing an O(n)
scan in both, a bonus fix beyond what was strictly necessary) since
there's no flat array left to scan.

Added tests/test_index_stress.c: thousands of randomized (not
sequential) creates/deletes/renames across nested directories,
cross-checked against an independent reference model after every
round, not just "did it not crash" -- exercises exactly the code path
the O(n^2) bug and this fix live in, at a scale the other tests don't
reach. Verified under -fsanitize=undefined (150+ runs across this
change's lifetime, 0 failures) and -fsanitize=address (100+ runs, 0
real findings; known sandbox ASan-startup flakes excluded, see prior
commits) per CLAUDE.md's sanitizer rule for upper.c/overlay.c changes.

Measured result (make bench, same environment as the original
finding): mem create 257x faster, unlink 330x faster, mkdir 65x
faster, concurrent mixed workload 131x faster -- and, the comparison
that matters, mem now beats raw fs at every one of these (was losing
by 6-22x before). The complexity-class change is confirmed the same
way the O(n^2) was found: mkdir at N=4,000 vs create at N=20,000 now
shows a 7.15x slowdown for a 5x increase in N, matching the O(n log n)
prediction (5.97x) rather than the old O(n^2) one (25x). Full
before/after tables in BENCH.md's new "Resolution" section, which
keeps the original run as the historical record rather than
overwriting it, per this project's own documentation standard.

Recorded the fix in CLAUDE.md's "Known performance characteristics"
(marked RESOLVED, not silently removed) and its architecture map.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
This commit is contained in:
2026-09-14 08:09:49 +00:00
co-authored by Claude Sonnet 5
parent 0b3b207bc0
commit 64283d587b
8 changed files with 753 additions and 257 deletions
+47 -8
View File
@@ -147,9 +147,9 @@ typedef enum { ENTRY_FILE, ENTRY_DIR_MARKER, ENTRY_WHITEOUT } UpperEntryKind;
/*
* Section 5.3's mutable metadata cell, and Section 5.7's buffer-growth
* guard. One MutCell is shared by every UpperSnapshot whose entry array
* still points at the same logical file — an ordinary vfs_write mutates
* this cell in place and never touches the snapshot pointer at all.
* guard. One MutCell is shared by every UpperSnapshot whose index still
* references the same logical file — an ordinary vfs_write mutates this
* cell in place and never touches the snapshot pointer at all.
*/
typedef struct MutCell {
pthread_mutex_t write_lock; /* serializes writers to the same file */
@@ -162,16 +162,35 @@ typedef struct MutCell {
/* UPPER_DIR: host bytes live in the host fs; nothing to store here. */
} MutCell;
/*
* The read-only view of one entry, returned by upper_lookup and
* upper_visit_range. This is deliberately also the first member of
* upper.c's private TreapNode (BENCH.md; see "Known performance
* characteristics" in CLAUDE.md for why the index is a treap and not a
* flat array), so a `TreapNode*` can be reinterpreted as `UpperEntry*`
* without a copy — callers outside upper.c never see TreapNode itself.
*/
typedef struct UpperEntry {
char *name; /* full virtual path, e.g. "/a/b.txt" */
UpperEntryKind kind;
MutCell *cell; /* NULL for ENTRY_DIR_MARKER / ENTRY_WHITEOUT */
} UpperEntry;
typedef struct TreapNode TreapNode; /* opaque outside upper.c */
/*
* The live index (Section 5.3's immutable, refcounted snapshot) is a
* persistent treap keyed by full path (Section 9.1's sort order), not a
* flat array: a structural write (`create`/`unlink`/`mkdir`/rename)
* builds only the O(log n) nodes on the path to the change and shares
* every other node, node-refcounted, with the snapshot(s) it was built
* from — rather than copying the entire index on every single write,
* which is the O(n^2)-for-n-sequential-writes behavior BENCH.md
* measured and CLAUDE.md records. `root == NULL` is the empty tree.
*/
typedef struct UpperSnapshot {
_Atomic(pfs_usize) refcount;
UpperEntry *entries; /* sorted by name, Section 9.1 */
pfs_usize count;
TreapNode *root;
} UpperSnapshot;
typedef struct UpperStore {
@@ -218,15 +237,35 @@ void upper_release(UpperSnapshot *s);
/* Structural + content operations, all against paths already normalized
* and relative to the mount (leading "/"). */
/* Binary search by full path within an already-acquired snapshot.
* Returns 1 and sets *out on hit, leaving *out untouched on a miss. */
/* Expected-O(log n) lookup by full path (a treap descent, Section 9.1's
* sort order) within an already-acquired snapshot. Returns 1 and sets
* *out on hit, leaving *out untouched on a miss. */
int upper_lookup(UpperSnapshot *s, const char *path, const UpperEntry **out);
/* True if any live (non-whiteout) entry's name starts with `path` + "/" —
* i.e. `path` is a directory that exists only implicitly, via a child,
* with no explicit entry of its own (Section 9.3). */
* with no explicit entry of its own (Section 9.3). Expected O(log n),
* short-circuiting on the first match, not a scan. */
int upper_has_children(UpperSnapshot *s, const char *path);
/*
* Visits every entry (files, directory markers, AND whiteouts — callers
* that need to distinguish them, like the overlay's merge logic, filter
* in `visit` itself) whose name lies in the half-open range [lo, hi),
* matching pack_range's convention (Section 9.1), in ascending order,
* without allocating. Expected O(log n + r) where r is the number of
* entries visited — a bounded range query, not a scan of the whole
* index, the same property Section 9.1 requires of the on-disk pack.
*/
typedef void (*UpperVisitFn)(void *ctx, const UpperEntry *e);
void upper_visit_range(UpperSnapshot *s, const char *lo, const char *hi, UpperVisitFn visit, void *ctx);
/* Builds, into `out` (at least `cap` bytes), the exclusive upper bound
* for "starts with `prefix`" (Section 9.1's convention: a byte higher
* than any valid path byte). Shared by upper.c and overlay.c, both of
* which call upper_visit_range with a prefix range. */
void pfs_prefix_upper_bound(const char *prefix, char *out, size_t cap);
/* Resets `c`'s content to empty in place (VFS_O_TRUNC on an
* already-existing file) — a content operation, not a structural one, so
* it never touches the snapshot pointer (Section 5.3). */
+37 -23
View File
@@ -355,39 +355,53 @@ static int child_index(Child *arr, pfs_usize n, const char *name, size_t len) {
return -1;
}
typedef struct OverlayReaddirCtx {
size_t plen;
Child *handled; pfs_usize hcount, hcap;
VfsDirEntry *entries; pfs_usize count, cap;
} OverlayReaddirCtx;
/* Mirrors the original array-scan loop's logic exactly, sourced from
* upper_visit_range instead of a manual index loop; still needs to see
* whiteouts (unlike upstd_readdir's callback), to suppress a pack entry
* sharing the same immediate-child name. */
static void overlay_readdir_visit(void *ctx_, const UpperEntry *e) {
OverlayReaddirCtx *ctx = (OverlayReaddirCtx *)ctx_;
const char *restp = e->name + ctx->plen;
const char *slash = strchr(restp, '/');
size_t clen = slash ? (size_t)(slash - restp) : strlen(restp);
if (clen == 0 || clen >= sizeof(ctx->handled[0].name)) return;
if (child_index(ctx->handled, ctx->hcount, restp, clen) >= 0) return;
if (ctx->hcount == ctx->hcap) { ctx->hcap = ctx->hcap ? ctx->hcap * 2 : 8; ctx->handled = (Child *)realloc(ctx->handled, ctx->hcap * sizeof(Child)); }
memcpy(ctx->handled[ctx->hcount].name, restp, clen); ctx->handled[ctx->hcount].name[clen] = '\0';
ctx->handled[ctx->hcount].kind = slash ? VFS_KIND_DIR : (e->kind == ENTRY_DIR_MARKER ? VFS_KIND_DIR : VFS_KIND_FILE);
int is_whiteout = (e->kind == ENTRY_WHITEOUT);
ctx->hcount++;
if (!is_whiteout) {
if (ctx->count == ctx->cap) { ctx->cap = ctx->cap ? ctx->cap * 2 : 8; ctx->entries = (VfsDirEntry *)realloc(ctx->entries, ctx->cap * sizeof(VfsDirEntry)); }
memcpy(ctx->entries[ctx->count].name, ctx->handled[ctx->hcount - 1].name, clen + 1);
ctx->entries[ctx->count].kind = ctx->handled[ctx->hcount - 1].kind;
ctx->count++;
}
}
static int overlay_readdir(Backend *b, const char *path, VfsDir *out) {
Overlay *ov = (Overlay *)b->state;
UpperStore *us = upper_of(ov);
char prefix[PFS_PATH_MAX];
char prefix[PFS_PATH_MAX], upper_hi[PFS_PATH_MAX];
if (strcmp(path, "/") == 0) strcpy(prefix, "/"); else snprintf(prefix, sizeof(prefix), "%s/", path);
size_t plen = strlen(prefix);
pfs_prefix_upper_bound(prefix, upper_hi, sizeof(upper_hi));
Child *handled = NULL; pfs_usize hcount = 0, hcap = 0;
VfsDirEntry *entries = NULL; pfs_usize count = 0, cap = 0;
OverlayReaddirCtx ctx; memset(&ctx, 0, sizeof(ctx)); ctx.plen = plen;
UpperSnapshot *s = upper_acquire(us);
for (pfs_usize i = 0; i < s->count; i++) {
const char *name = s->entries[i].name;
if (strncmp(name, prefix, plen) != 0) continue;
const char *restp = name + plen;
const char *slash = strchr(restp, '/');
size_t clen = slash ? (size_t)(slash - restp) : strlen(restp);
if (clen == 0 || clen >= sizeof(handled[0].name)) continue;
if (child_index(handled, hcount, restp, clen) >= 0) continue;
if (hcount == hcap) { hcap = hcap ? hcap * 2 : 8; handled = (Child *)realloc(handled, hcap * sizeof(Child)); }
memcpy(handled[hcount].name, restp, clen); handled[hcount].name[clen] = '\0';
handled[hcount].kind = slash ? VFS_KIND_DIR : (s->entries[i].kind == ENTRY_DIR_MARKER ? VFS_KIND_DIR : VFS_KIND_FILE);
int is_whiteout = (s->entries[i].kind == ENTRY_WHITEOUT);
hcount++;
if (!is_whiteout) {
if (count == cap) { cap = cap ? cap * 2 : 8; entries = (VfsDirEntry *)realloc(entries, cap * sizeof(VfsDirEntry)); }
memcpy(entries[count].name, handled[hcount - 1].name, clen + 1);
entries[count].kind = handled[hcount - 1].kind;
count++;
}
}
upper_visit_range(s, prefix, upper_hi, overlay_readdir_visit, &ctx);
upper_release(s);
Child *handled = ctx.handled; pfs_usize hcount = ctx.hcount;
VfsDirEntry *entries = ctx.entries; pfs_usize count = ctx.count, cap = ctx.cap;
if (ov->pack) {
size_t lo, hi;
pack_range(ov->pack, prefix, &lo, &hi);
+303 -130
View File
@@ -60,6 +60,222 @@ static MutCell *mutcell_new(void) {
return &rc->pub;
}
/*
* ---- the persistent treap (Section 9.1's sort order; see CLAUDE.md's
* "Known performance characteristics" and BENCH.md for why this exists
* instead of the flat sorted array the design originally used) ----
*
* Chosen over a persistent AVL/red-black/weight-balanced tree because
* deletion in those needs up to O(log n) rebalancing rotations on the
* path to the root, every one of which is a real cost in a persistent
* (path-copying) setting since each rotated node must be copied, not
* just relinked in place; a treap needs only O(1) *amortized* rotations
* for both insertion and deletion (Seidel & Aragon, "Randomized Search
* Trees," 1996), and its expected O(log n) height holds regardless of
* insertion order — including the sorted-by-creation-order pattern that
* is exactly what made the flat array's full copy O(n^2) in the first
* place. Liljenzin, "Confluently Persistent Sets and Maps" (arXiv:
* 1301.3388), documents persistent treaps giving O(1) snapshots for
* MVCC specifically, which is this exact use case.
*
* Ownership convention, load-bearing for every function below: a
* TreapNode* argument passed to treap_merge/treap_split3/node_clone_with
* is CONSUMED — the caller must hand over one owned reference (via
* node_ref() if borrowing from a tree it still needs) and must not use
* that pointer again. Each such function returns a tree holding exactly
* one owned reference, which the caller now owns. This mirrors the
* "old"/"step1" ownership pattern already used at the UpperSnapshot
* level elsewhere in this file, one level down.
*/
struct TreapNode {
UpperEntry pub; /* MUST be first member: (UpperEntry*)node must be valid */
_Atomic(pfs_usize) refcount;
uint32_t priority;
pfs_usize size; /* subtree size, including self; not currently read outside treap_touch_size, kept for future rank/order-statistics use */
TreapNode *left, *right;
};
/* Node creation happens only inside structural-write functions, which
* are already serialized by UpperStore.writer_lock (Section 5.3) — there
* is never a concurrent call into this generator, so it does not need
* its own atomics or locking. xorshift32 (Marsaglia, 2003): treap
* priorities need a good average distribution, not cryptographic
* strength. */
static uint32_t treap_rand(void) {
static uint32_t state = 2463534242u; /* fixed nonzero seed */
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
static pfs_usize node_size(TreapNode *n) { return n ? n->size : 0; }
static void node_touch_size(TreapNode *n) { n->size = 1 + node_size(n->left) + node_size(n->right); }
static TreapNode *node_ref(TreapNode *n) {
if (n) atomic_fetch_add_explicit(&n->refcount, 1, memory_order_relaxed);
return n;
}
/* Drops one reference; recursively unrefs children (and the cell) only
* once this node's own refcount reaches zero — a shared child's own
* refcount simply absorbs the decrement and recursion stops there,
* exactly like MutCell's and UpperSnapshot's reference counting. */
static void node_unref(TreapNode *n) {
if (!n) return;
if (atomic_fetch_sub_explicit(&n->refcount, 1, memory_order_acq_rel) == 1) {
node_unref(n->left);
node_unref(n->right);
mutcell_unref(n->pub.cell);
free(n->pub.name);
free(n);
}
}
static TreapNode *node_new(const char *name, UpperEntryKind kind, MutCell *cell) {
TreapNode *n = (TreapNode *)calloc(1, sizeof(TreapNode));
n->pub.name = strdup(name);
n->pub.kind = kind;
n->pub.cell = cell;
mutcell_ref(cell);
n->priority = treap_rand();
n->size = 1;
atomic_init(&n->refcount, 1);
return n;
}
/* Builds a new node cloning `src`'s identity (name/kind/cell/priority)
* with the given (already-owned, per the convention above) children. */
static TreapNode *node_clone_with(TreapNode *src, TreapNode *left, TreapNode *right) {
TreapNode *n = (TreapNode *)calloc(1, sizeof(TreapNode));
n->pub.name = strdup(src->pub.name);
n->pub.kind = src->pub.kind;
n->pub.cell = src->pub.cell;
mutcell_ref(n->pub.cell);
n->priority = src->priority;
n->left = left;
n->right = right;
node_touch_size(n);
atomic_init(&n->refcount, 1);
return n;
}
/* Consumes `a` and `b`; returns their merge (every key in `a` assumed
* less than every key in `b`, the standard treap merge precondition).
* node_clone_with already sizes its result from the final left/right it
* is given, so there is nothing left to recompute here. */
static TreapNode *treap_merge(TreapNode *a, TreapNode *b) {
if (!a) return b;
if (!b) return a;
if (a->priority >= b->priority) {
TreapNode *new_right = treap_merge(node_ref(a->right), b);
TreapNode *result = node_clone_with(a, node_ref(a->left), new_right);
node_unref(a);
return result;
}
TreapNode *new_left = treap_merge(a, node_ref(b->left));
TreapNode *result = node_clone_with(b, new_left, node_ref(b->right));
node_unref(b);
return result;
}
/* Consumes `t`; splits it by `key` into *lt (names < key) and *gt (names
* > key), reporting whether an exact match existed via *found. The
* matched node, if any, is released here (its cell's reference drops
* with it) — the caller never sees it, matching what the flat-array
* snapshot_remove did by simply not copying the matched slot forward. */
static void treap_split3(TreapNode *t, const char *key, TreapNode **lt, TreapNode **gt, int *found) {
if (!t) { *lt = NULL; *gt = NULL; *found = 0; return; }
int c = strcmp(t->pub.name, key);
if (c == 0) {
*found = 1;
*lt = node_ref(t->left);
*gt = node_ref(t->right);
node_unref(t);
return;
}
if (c < 0) {
TreapNode *rl, *rr; int f;
treap_split3(node_ref(t->right), key, &rl, &rr, &f);
*found = f;
*gt = rr;
*lt = node_clone_with(t, node_ref(t->left), rl);
node_unref(t);
} else {
TreapNode *rl, *rr; int f;
treap_split3(node_ref(t->left), key, &rl, &rr, &f);
*found = f;
*lt = rl;
*gt = node_clone_with(t, rr, node_ref(t->right));
node_unref(t);
}
}
/* Consumes `root`; returns a new tree with `key` set to (kind, cell),
* replacing any existing entry at that key. */
static TreapNode *treap_upsert(TreapNode *root, const char *key, UpperEntryKind kind, MutCell *cell) {
TreapNode *lt, *gt; int found;
treap_split3(root, key, &lt, &gt, &found);
TreapNode *mid = node_new(key, kind, cell);
return treap_merge(treap_merge(lt, mid), gt);
}
/* Consumes `root`; returns a new tree with `key` absent. A no-op removal
* (key not present) still returns a correct, equivalent tree — callers
* in this file only ever call this once they've confirmed the key
* exists, so this path is not currently exercised, but it is not UB or
* a silent corruption if it ever is. */
static TreapNode *treap_remove_key(TreapNode *root, const char *key) {
TreapNode *lt, *gt; int found;
treap_split3(root, key, &lt, &gt, &found);
return treap_merge(lt, gt);
}
static TreapNode *treap_lookup(TreapNode *t, const char *key) {
while (t) {
int c = strcmp(t->pub.name, key);
if (c == 0) return t;
t = c < 0 ? t->right : t->left;
}
return NULL;
}
/* Read-only BST range-query pruning (Section 9.1): recurses into a child
* only when that child's subtree could possibly contain a qualifying
* key, per the BST invariant — never allocates, never refs/unrefs,
* since the caller already holds the whole tree alive via its snapshot. */
static void treap_visit_range(TreapNode *t, const char *lo, const char *hi, UpperVisitFn visit, void *ctx) {
if (!t) return;
if (strcmp(t->pub.name, lo) >= 0) treap_visit_range(t->left, lo, hi, visit, ctx);
if (strcmp(t->pub.name, lo) >= 0 && strcmp(t->pub.name, hi) < 0) visit(ctx, &t->pub);
if (strcmp(t->pub.name, hi) < 0) treap_visit_range(t->right, lo, hi, visit, ctx);
}
/* Same pruning as treap_visit_range, short-circuiting on the first
* match instead of visiting every qualifying entry. */
static int treap_range_nonempty(TreapNode *t, const char *lo, const char *hi) {
if (!t) return 0;
if (strcmp(t->pub.name, lo) >= 0 && strcmp(t->pub.name, hi) < 0) return 1;
if (strcmp(t->pub.name, lo) >= 0 && treap_range_nonempty(t->left, lo, hi)) return 1;
if (strcmp(t->pub.name, hi) < 0 && treap_range_nonempty(t->right, lo, hi)) return 1;
return 0;
}
/* Builds the exclusive upper bound for "starts with `prefix`", matching
* pack_range's convention (Section 9.1): a byte higher than any valid
* path byte, so every string with `prefix` as a proper prefix sorts
* below it and nothing else does. Shared with overlay.c, which needs the
* same bound to call upper_visit_range for its half of a merged
* readdir. */
void pfs_prefix_upper_bound(const char *prefix, char *out, size_t cap) {
size_t plen = strlen(prefix);
if (plen + 2 > cap) plen = cap - 2;
memcpy(out, prefix, plen);
out[plen] = (char)0x7F;
out[plen + 1] = '\0';
}
/* ---- UpperSnapshot lifetime ---- */
UpperSnapshot *upper_acquire(UpperStore *u) {
@@ -83,105 +299,43 @@ static void upper_retire(UpperStore *u, UpperSnapshot *old) {
void upper_release(UpperSnapshot *s) {
if (!s) return;
if (atomic_fetch_sub_explicit(&s->refcount, 1, memory_order_acq_rel) == 1) {
for (pfs_usize i = 0; i < s->count; i++) {
free(s->entries[i].name);
mutcell_unref(s->entries[i].cell);
}
free(s->entries);
node_unref(s->root);
free(s);
}
}
int upper_lookup(UpperSnapshot *s, const char *path, const UpperEntry **out) {
pfs_usize lo = 0, hi = s->count;
while (lo < hi) {
pfs_usize mid = lo + (hi - lo) / 2;
int c = strcmp(s->entries[mid].name, path);
if (c == 0) { *out = &s->entries[mid]; return 1; }
if (c < 0) lo = mid + 1; else hi = mid;
}
return 0;
TreapNode *n = treap_lookup(s->root, path);
if (!n) return 0;
*out = &n->pub;
return 1;
}
int upper_has_children(UpperSnapshot *s, const char *path) {
char prefix[PFS_PATH_MAX];
char prefix[PFS_PATH_MAX], hi[PFS_PATH_MAX];
if (strcmp(path, "/") == 0) strcpy(prefix, "/");
else snprintf(prefix, sizeof(prefix), "%s/", path);
size_t plen = strlen(prefix);
for (pfs_usize i = 0; i < s->count; i++) {
if (s->entries[i].kind == ENTRY_WHITEOUT) continue;
if (strncmp(s->entries[i].name, prefix, plen) == 0) return 1;
}
return 0;
pfs_prefix_upper_bound(prefix, hi, sizeof(hi));
return treap_range_nonempty(s->root, prefix, hi);
}
void upper_visit_range(UpperSnapshot *s, const char *lo, const char *hi, UpperVisitFn visit, void *ctx) {
treap_visit_range(s->root, lo, hi, visit, ctx);
}
/* Builds a new snapshot with one entry inserted or replaced at `name`. */
static UpperSnapshot *snapshot_upsert(UpperSnapshot *base, const char *name,
UpperEntryKind kind, MutCell *cell) {
pfs_usize old_count = base ? base->count : 0;
pfs_usize lo = 0, hi = old_count;
int found = 0;
while (lo < hi) {
pfs_usize mid = lo + (hi - lo) / 2;
int c = strcmp(base->entries[mid].name, name);
if (c == 0) { lo = mid; found = 1; break; }
if (c < 0) lo = mid + 1; else hi = mid;
}
pfs_usize new_count = found ? old_count : old_count + 1;
UpperSnapshot *ns = (UpperSnapshot *)calloc(1, sizeof(UpperSnapshot));
ns->entries = (UpperEntry *)calloc(new_count ? new_count : 1, sizeof(UpperEntry));
ns->count = new_count;
atomic_init(&ns->refcount, 1);
pfs_usize w = 0;
for (pfs_usize i = 0; i < old_count; i++) {
if (i == lo) {
ns->entries[w].name = strdup(name);
ns->entries[w].kind = kind;
ns->entries[w].cell = cell;
mutcell_ref(cell);
w++;
if (found) continue; /* replaces base->entries[lo] */
}
ns->entries[w].name = strdup(base->entries[i].name);
ns->entries[w].kind = base->entries[i].kind;
ns->entries[w].cell = base->entries[i].cell;
mutcell_ref(ns->entries[w].cell);
w++;
}
if (lo == old_count) {
ns->entries[w].name = strdup(name);
ns->entries[w].kind = kind;
ns->entries[w].cell = cell;
mutcell_ref(cell);
w++;
}
ns->root = treap_upsert(node_ref(base ? base->root : NULL), name, kind, cell);
return ns;
}
static UpperSnapshot *snapshot_remove(UpperSnapshot *base, const char *name) {
pfs_usize lo = 0, hi = base->count;
int found = 0;
while (lo < hi) {
pfs_usize mid = lo + (hi - lo) / 2;
int c = strcmp(base->entries[mid].name, name);
if (c == 0) { lo = mid; found = 1; break; }
if (c < 0) lo = mid + 1; else hi = mid;
}
if (!found) return NULL;
UpperSnapshot *ns = (UpperSnapshot *)calloc(1, sizeof(UpperSnapshot));
ns->count = base->count - 1;
ns->entries = ns->count ? (UpperEntry *)calloc(ns->count, sizeof(UpperEntry)) : NULL;
atomic_init(&ns->refcount, 1);
pfs_usize w = 0;
for (pfs_usize i = 0; i < base->count; i++) {
if (i == lo) continue;
ns->entries[w].name = strdup(base->entries[i].name);
ns->entries[w].kind = base->entries[i].kind;
ns->entries[w].cell = base->entries[i].cell;
mutcell_ref(ns->entries[w].cell);
w++;
}
ns->root = treap_remove_key(node_ref(base->root), name);
return ns;
}
@@ -465,39 +619,49 @@ void upper_cell_truncate(UpperStore *u, MutCell *c, const char *path) {
/* ---- compaction support ---- */
void upper_walk_live(UpperStore *u, const UpperWalkCb *cb) {
UpperSnapshot *s = upper_acquire(u);
for (pfs_usize i = 0; i < s->count; i++) {
UpperEntry *e = &s->entries[i];
if (e->kind == ENTRY_WHITEOUT) continue;
if (e->kind == ENTRY_DIR_MARKER) {
cb->visit(cb->ctx, e->name, ENTRY_DIR_MARKER, NULL, 0, 0);
continue;
}
/* In-order (= sorted-by-name) full traversal for compaction. Unlike
* treap_visit_range, this needs every entry including whiteouts filtered
* here rather than left to the caller, and needs per-entry I/O (reading
* mem's buffer or a dir file's content) that doesn't fit the generic
* UpperVisitFn shape, so it is its own recursive walker rather than a
* caller of upper_visit_range. */
static void walk_node(UpperStore *u, TreapNode *n, const UpperWalkCb *cb) {
if (!n) return;
walk_node(u, n->left, cb);
if (n->pub.kind == ENTRY_DIR_MARKER) {
cb->visit(cb->ctx, n->pub.name, ENTRY_DIR_MARKER, NULL, 0, 0);
} else if (n->pub.kind == ENTRY_FILE) {
if (u->kind == UPPER_MEM) {
pthread_rwlock_rdlock(&e->cell->buf_lock);
pfs_usize size = atomic_load_explicit(&e->cell->size, memory_order_acquire);
cb->visit(cb->ctx, e->name, ENTRY_FILE, e->cell->data, size,
atomic_load_explicit(&e->cell->mtime, memory_order_acquire));
pthread_rwlock_unlock(&e->cell->buf_lock);
pthread_rwlock_rdlock(&n->pub.cell->buf_lock);
pfs_usize size = atomic_load_explicit(&n->pub.cell->size, memory_order_acquire);
cb->visit(cb->ctx, n->pub.name, ENTRY_FILE, n->pub.cell->data, size,
atomic_load_explicit(&n->pub.cell->mtime, memory_order_acquire));
pthread_rwlock_unlock(&n->pub.cell->buf_lock);
} else {
int err = 0;
int fd = pfs_dir_openat(u->root_fd, rel(e->name), O_RDONLY, 0, &err);
if (fd < 0) continue;
struct stat st;
fstat(fd, &st);
void *buf = st.st_size ? malloc((size_t)st.st_size) : NULL;
size_t total = 0;
while (buf && total < (size_t)st.st_size) {
ssize_t r = read(fd, (char *)buf + total, (size_t)st.st_size - total);
if (r <= 0) break;
total += (size_t)r;
int fd = pfs_dir_openat(u->root_fd, rel(n->pub.name), O_RDONLY, 0, &err);
if (fd >= 0) {
struct stat st;
fstat(fd, &st);
void *buf = st.st_size ? malloc((size_t)st.st_size) : NULL;
size_t total = 0;
while (buf && total < (size_t)st.st_size) {
ssize_t r = read(fd, (char *)buf + total, (size_t)st.st_size - total);
if (r <= 0) break;
total += (size_t)r;
}
close(fd);
cb->visit(cb->ctx, n->pub.name, ENTRY_FILE, buf, total, (int64_t)st.st_mtime);
free(buf);
}
close(fd);
cb->visit(cb->ctx, e->name, ENTRY_FILE, buf, total, (int64_t)st.st_mtime);
free(buf);
}
}
} /* ENTRY_WHITEOUT: skipped, as before */
walk_node(u, n->right, cb);
}
void upper_walk_live(UpperStore *u, const UpperWalkCb *cb) {
UpperSnapshot *s = upper_acquire(u);
walk_node(u, s->root, cb);
upper_release(s);
}
@@ -604,6 +768,35 @@ static int upstd_stat(Backend *b, const char *path, VfsStat *out) {
return rc < 0 ? err : VFS_OK;
}
typedef struct ReaddirCtx {
size_t plen;
VfsDirEntry *entries;
pfs_usize count, cap;
} ReaddirCtx;
/* upper_visit_range already restricts us to [prefix, prefix-upper-bound)
* in sorted order, so "is this a duplicate immediate child" only ever
* needs to check the immediately-preceding output entry, not a scan. */
static void readdir_visit(void *ctx_, const UpperEntry *e) {
ReaddirCtx *ctx = (ReaddirCtx *)ctx_;
if (e->kind == ENTRY_WHITEOUT) return;
const char *restp = e->name + ctx->plen;
const char *slash = strchr(restp, '/');
size_t clen = slash ? (size_t)(slash - restp) : strlen(restp);
if (clen == 0 || clen >= sizeof(ctx->entries[0].name)) return;
if (ctx->count > 0 && strncmp(ctx->entries[ctx->count - 1].name, restp, clen) == 0 &&
ctx->entries[ctx->count - 1].name[clen] == '\0') return;
if (ctx->count == ctx->cap) {
ctx->cap = ctx->cap ? ctx->cap * 2 : 8;
ctx->entries = (VfsDirEntry *)realloc(ctx->entries, ctx->cap * sizeof(VfsDirEntry));
}
memcpy(ctx->entries[ctx->count].name, restp, clen);
ctx->entries[ctx->count].name[clen] = '\0';
ctx->entries[ctx->count].kind = slash ? VFS_KIND_DIR
: (e->kind == ENTRY_DIR_MARKER ? VFS_KIND_DIR : VFS_KIND_FILE);
ctx->count++;
}
static int upstd_readdir(Backend *b, const char *path, VfsDir *out) {
UpperStore *u = (UpperStore *)b->state;
UpperSnapshot *s = upper_acquire(u);
@@ -616,36 +809,16 @@ static int upstd_readdir(Backend *b, const char *path, VfsDir *out) {
return self_found ? VFS_ERR_NOTDIR : VFS_ERR_NOENT;
}
char prefix[PFS_PATH_MAX];
char prefix[PFS_PATH_MAX], hi[PFS_PATH_MAX];
if (strcmp(path, "/") == 0) strcpy(prefix, "/");
else snprintf(prefix, sizeof(prefix), "%s/", path);
size_t plen = strlen(prefix);
pfs_prefix_upper_bound(prefix, hi, sizeof(hi));
VfsDirEntry *entries = NULL;
pfs_usize count = 0, cap = 0;
for (pfs_usize i = 0; i < s->count; i++) {
if (s->entries[i].kind == ENTRY_WHITEOUT) continue;
const char *name = s->entries[i].name;
if (strncmp(name, prefix, plen) != 0) continue;
const char *restp = name + plen;
const char *slash = strchr(restp, '/');
size_t clen = slash ? (size_t)(slash - restp) : strlen(restp);
if (clen == 0 || clen >= sizeof(entries[0].name)) continue;
if (count > 0 && strncmp(entries[count - 1].name, restp, clen) == 0 &&
entries[count - 1].name[clen] == '\0') continue; /* sorted -> dup is adjacent */
if (count == cap) {
cap = cap ? cap * 2 : 8;
entries = (VfsDirEntry *)realloc(entries, cap * sizeof(VfsDirEntry));
}
memcpy(entries[count].name, restp, clen);
entries[count].name[clen] = '\0';
entries[count].kind = slash ? VFS_KIND_DIR
: (s->entries[i].kind == ENTRY_DIR_MARKER ? VFS_KIND_DIR : VFS_KIND_FILE);
count++;
}
ReaddirCtx ctx = { strlen(prefix), NULL, 0, 0 };
upper_visit_range(s, prefix, hi, readdir_visit, &ctx);
upper_release(s);
out->entries = entries;
out->count = count;
out->entries = ctx.entries;
out->count = ctx.count;
return VFS_OK;
}