Git backed by object storage because you can't stop me
git object-storage kefka
10

Configure Feed

Select the types of activity you want to include in your feed.

perf(s3fs): register chroot roots as recursive subtree prefixes

Make each repository's Chroot root register itself as a recursive subtree
prefix with the listing cache. This revives the subtree-scan optimization
(previously dead in chrooted deployments) so the entire repo is answered
from one delimiter-less S3 ListObjectsV2 instead of a list per folder.

Eliminates ~256 loose-object negative-lookup lists per clone and collapses
the background warmer from O(folders) lists/tick to O(1).

Changes:
- listingcache.go: roots field is now atomic.Pointer[[]string] with
registerRoot() method for lock-free runtime registration
- chroot.go: calls registerRoot() when creating a chroot
- listingcache_test.go: updated TestListingCacheChrootShares and added
TestListingCacheChrootSubtreeCollapsesLooseLookups regression guard

Verification: all tests pass, including race detector and cmd/objgitd
protocol tests.

Plan: docs/plans/why-does-objgit-do-zazzy-bentley.md
Assisted-by: Claude Opus 4.8 via Claude Code

Xe Iaso (May 30, 2026, 4:27 PM EDT) e8e749fc e1145432

+134 -17
+9
internal/s3fs/chroot.go
··· 28 28 packCache: fs3.packCache, 29 29 temps: make(map[string]*tempBuffer), 30 30 } 31 + 32 + // A chroot is one repository. Register its root as a recursive subtree prefix 33 + // so the whole repo is answered from one delimiter-less scan: this is what 34 + // makes subtree caching actually engage for the canonical "<repo>/refs/...", 35 + // "<repo>/objects/..." keys (a bucket-root prefix like "refs/" never matches 36 + // them), collapsing git's per-object and per-folder listings into one. 37 + if fs3.cache != nil && p != "" && p != "." { 38 + fs3.cache.registerRoot(p) 39 + } 31 40 return nfs, nil 32 41 } 33 42
+50 -4
internal/s3fs/listingcache.go
··· 149 149 client s3Client 150 150 bucket string 151 151 separator string 152 - roots []string // normalised RecursivePrefixes, longest first 152 + 153 + // roots is the set of recursive subtree prefixes (normalised, longest 154 + // first), held behind an atomic so S3FS.Chroot can register a repo's root at 155 + // runtime without locking the hot recursiveRoot read path. See registerRoot. 156 + roots atomic.Pointer[[]string] 153 157 154 158 clock func() time.Time // overridable in tests 155 159 ··· 179 183 if cfg.RecursivePrefixes == nil { 180 184 cfg.RecursivePrefixes = []string{"refs/"} 181 185 } 182 - return &ListingCache{ 186 + c := &ListingCache{ 183 187 ttl: cfg.TTL, 184 188 cfg: cfg, 185 189 client: client, 186 190 bucket: bucket, 187 191 separator: separator, 188 - roots: normalizeRoots(cfg.RecursivePrefixes), 189 192 clock: time.Now, 190 193 gens: map[string]uint64{}, 191 194 seen: map[string]time.Time{}, 195 + } 196 + roots := normalizeRoots(cfg.RecursivePrefixes) 197 + c.roots.Store(&roots) 198 + return c 199 + } 200 + 201 + // registerRoot adds root — a chroot repo root, normalised to a trailing 202 + // separator — to the recursive subtree prefixes, so the whole repo is served 203 + // from one delimiter-less scan instead of a delimited listing per folder (which 204 + // is what collapses git's per-object loose lookups under objects/<xx>/ and its 205 + // per-folder refs enumeration into a single ListObjectsV2). It is a no-op for an 206 + // empty root or one already registered, and is safe for concurrent callers: 207 + // S3FS.Chroot calls it the first time each repo's storer is built. 208 + func (c *ListingCache) registerRoot(root string) { 209 + if c == nil || root == "" { 210 + return 211 + } 212 + if !strings.HasSuffix(root, c.separator) { 213 + root += c.separator 214 + } 215 + for { 216 + cur := c.roots.Load() 217 + var existing []string 218 + if cur != nil { 219 + existing = *cur 220 + for _, r := range existing { 221 + if r == root { 222 + return 223 + } 224 + } 225 + } 226 + next := make([]string, len(existing), len(existing)+1) 227 + copy(next, existing) 228 + next = append(next, root) 229 + sort.Slice(next, func(i, j int) bool { return len(next[i]) > len(next[j]) }) 230 + if c.roots.CompareAndSwap(cur, &next) { 231 + return 232 + } 233 + // Lost a race with another registrar; reload and retry. 192 234 } 193 235 } 194 236 ··· 288 330 289 331 // recursiveRoot reports the longest configured recursive root at or above prefix. 290 332 func (c *ListingCache) recursiveRoot(prefix string) (string, bool) { 291 - for _, r := range c.roots { // longest first 333 + rp := c.roots.Load() 334 + if rp == nil { 335 + return "", false 336 + } 337 + for _, r := range *rp { // longest first 292 338 if prefix == r || strings.HasPrefix(prefix, r) { 293 339 return r, true 294 340 }
+75 -13
internal/s3fs/listingcache_test.go
··· 463 463 cache := newTestCache(stub, time.Hour) 464 464 rootfs := newCachedFS(t, stub, cache) 465 465 466 - // Populate the listing for repo/objects/ab/ via the root view. 467 - if _, err := rootfs.Stat("repo/objects/ab/missing1"); !errors.Is(err, fs.ErrNotExist) { 468 - t.Fatalf("root Stat: want ErrNotExist, got %v", err) 469 - } 470 - if stub.lists.Load() != 1 { 471 - t.Fatalf("root Stat lists = %d, want 1", stub.lists.Load()) 472 - } 473 - 474 - // A chroot view shares the same cache keyed by canonical prefix, so a Stat 475 - // under it hits the cached listing with no further S3. 466 + // A chroot is one repository: it registers its root as a recursive subtree 467 + // prefix, so the first lookup under it does one delimiter-less scan of the 468 + // whole repo and every later lookup anywhere in it is served from that scan. 476 469 sub, err := rootfs.Chroot("repo") 477 470 if err != nil { 478 471 t.Fatalf("Chroot: %v", err) 479 472 } 480 - l0 := stub.lists.Load() 481 - if _, err := sub.Stat("objects/ab/missing2"); !errors.Is(err, fs.ErrNotExist) { 473 + if _, err := sub.Stat("objects/ab/missing1"); !errors.Is(err, fs.ErrNotExist) { 482 474 t.Fatalf("chroot Stat: want ErrNotExist, got %v", err) 483 475 } 476 + if got := stub.lists.Load(); got != 1 { 477 + t.Fatalf("first chroot Stat: lists = %d, want 1 (one subtree scan)", got) 478 + } 479 + 480 + // A second lookup — different folder, via the shared root view's canonical 481 + // key — is answered from the cached subtree with no further S3. 482 + l0 := stub.lists.Load() 483 + if _, err := rootfs.Stat("repo/objects/ab/missing2"); !errors.Is(err, fs.ErrNotExist) { 484 + t.Fatalf("shared Stat: want ErrNotExist, got %v", err) 485 + } 484 486 if stub.lists.Load() != l0 { 485 - t.Fatalf("chroot Stat re-listed: %d->%d", l0, stub.lists.Load()) 487 + t.Fatalf("shared Stat re-listed: %d->%d", l0, stub.lists.Load()) 488 + } 489 + } 490 + 491 + // TestListingCacheChrootSubtreeCollapsesLooseLookups is the regression guard for 492 + // the ListObjectsV2 reduction: git probes many absent loose objects under 493 + // distinct objects/<xx>/ prefixes plus several refs folders, and on a chrooted 494 + // repo all of it must resolve from one subtree scan rather than a list per 495 + // folder. A push (write + invalidate) costs exactly one re-scan on the next read. 496 + func TestListingCacheChrootSubtreeCollapsesLooseLookups(t *testing.T) { 497 + stub := newStub( 498 + "myrepo.git/HEAD", 499 + "myrepo.git/refs/heads/main", 500 + "myrepo.git/objects/pack/pack-1.pack", 501 + "myrepo.git/objects/pack/pack-1.idx", 502 + ) 503 + cache := newTestCache(stub, time.Hour) 504 + rootfs := newCachedFS(t, stub, cache) 505 + 506 + repo, err := rootfs.Chroot("myrepo.git") 507 + if err != nil { 508 + t.Fatalf("Chroot: %v", err) 509 + } 510 + 511 + // Drive git's access pattern: many absent loose objects across distinct 512 + // two-hex prefixes, then the pack and refs folders. 513 + for _, xx := range []string{"00", "ab", "cd", "ef", "12", "9f"} { 514 + p := "objects/" + xx + "/0123456789012345678901234567890123456789" 515 + if _, err := repo.Stat(p); !errors.Is(err, fs.ErrNotExist) { 516 + t.Fatalf("Stat %q: want ErrNotExist, got %v", p, err) 517 + } 518 + } 519 + if _, err := repo.ReadDir("objects/pack"); err != nil { 520 + t.Fatalf("ReadDir objects/pack: %v", err) 521 + } 522 + if _, err := repo.ReadDir("refs/heads"); err != nil { 523 + t.Fatalf("ReadDir refs/heads: %v", err) 524 + } 525 + 526 + // All of the above is one subtree scan. Pre-fix this was one list per 527 + // distinct objects/<xx>/ prefix plus one per refs/pack folder. 528 + if got := stub.lists.Load(); got != 1 { 529 + t.Fatalf("loose-lookup storm: lists = %d, want 1", got) 530 + } 531 + 532 + // A push mutates the repo and bumps its generation; the next read re-scans 533 + // the subtree exactly once, then is free again. 534 + if err := repo.Remove("objects/pack/pack-1.pack"); err != nil { 535 + t.Fatalf("Remove (write): %v", err) 536 + } 537 + if _, err := repo.ReadDir("objects/pack"); err != nil { 538 + t.Fatalf("ReadDir after write: %v", err) 539 + } 540 + if got := stub.lists.Load(); got != 2 { 541 + t.Fatalf("after write: lists = %d, want 2 (one re-scan)", got) 542 + } 543 + if _, err := repo.Stat("objects/ab/0123456789012345678901234567890123456789"); !errors.Is(err, fs.ErrNotExist) { 544 + t.Fatalf("post-write Stat: want ErrNotExist, got %v", err) 545 + } 546 + if got := stub.lists.Load(); got != 2 { 547 + t.Fatalf("post-write cached Stat re-listed: lists = %d, want 2", got) 486 548 } 487 549 }