File
Blob: src/worker/git/pack/rewrite/ownership.ts
| 1 | import type { OrderedPackSnapshot } from "@/worker/git/operations/fetch/types"; |
| 2 | import type { Logger } from "@/worker/common/logger"; |
| 3 | |
| 4 | import { collectPackedObjectCandidates } from "@/worker/git/object-store"; |
| 5 | import type { PackHeaderEx } from "../packMeta"; |
| 6 | import { |
| 7 | copySelectionRow, |
| 8 | ensurePackReadState, |
| 9 | readSelectedHeader, |
| 10 | selectionKey, |
| 11 | setSelectionEntryIdentity, |
| 12 | storeSelectionHeader, |
| 13 | type PackReadState, |
| 14 | type RewriteOptions, |
| 15 | type SelectionTable, |
| 16 | } from "./shared"; |
| 17 | |
| 18 | /** |
| 19 | * Planner-local OID owner table. |
| 20 | * |
| 21 | * This keeps canonical ownership keyed by the 20-byte object ID without |
| 22 | * materializing hex strings or per-entry JS objects. The table stores only |
| 23 | * live owner rows; duplicate rows are redirected and compacted later. |
| 24 | */ |
| 25 | export type SelectedOidLookup = { |
| 26 | count: number; |
| 27 | mask: number; |
| 28 | used: Uint8Array; |
| 29 | keys: Uint8Array; |
| 30 | owners: Int32Array; |
| 31 | }; |
| 32 | |
| 33 | /** |
| 34 | * Duplicate-run probes may re-read the same candidate entry header multiple |
| 35 | * times while different rows converge onto the same OID. Cache a copied header |
| 36 | * per pack position so those probes stay request-local and bounded. |
| 37 | */ |
| 38 | export type DuplicateHeaderCache = Map<number, PackHeaderEx | null>; |
| 39 | |
| 40 | export type SelectionStats = { |
| 41 | duplicateRedirects: number; |
| 42 | duplicateOwnerUpgrades: number; |
| 43 | duplicateOfsOwnerTakeovers: number; |
| 44 | duplicateHeaderProbes: number; |
| 45 | }; |
| 46 | |
| 47 | export type ClaimOwnerResult = |
| 48 | | { kind: "unchanged"; canonicalized: boolean } |
| 49 | | { kind: "swapped" } |
| 50 | | { kind: "error" } |
| 51 | | { kind: "takeover"; canonicalized: boolean; previousOwnerSel: number } |
| 52 | | { kind: "redirect"; canonicalized: boolean; targetSel: number; upgradedOwner: boolean }; |
| 53 | |
| 54 | type FullDuplicateCandidate = |
| 55 | | { kind: "none" } |
| 56 | | { kind: "redirect"; targetSel: number } |
| 57 | | { |
| 58 | kind: "swap"; |
| 59 | packSlot: number; |
| 60 | entryIndex: number; |
| 61 | offset: number; |
| 62 | nextOffset: number; |
| 63 | header: PackHeaderEx; |
| 64 | }; |
| 65 | |
| 66 | /** Smallest power-of-two table size for the open-addressed OID owner map. */ |
| 67 | function nextPowerOfTwo(value: number): number { |
| 68 | let out = 1; |
| 69 | while (out < value) out <<= 1; |
| 70 | return out; |
| 71 | } |
| 72 | |
| 73 | /** Hash a raw 20-byte object ID stored at `start` in `rawBytes`. */ |
| 74 | function hashOidAt(rawBytes: Uint8Array, start: number): number { |
| 75 | let hash = 2166136261; |
| 76 | for (let i = 0; i < 20; i++) { |
| 77 | hash ^= rawBytes[start + i]!; |
| 78 | hash = Math.imul(hash, 16777619); |
| 79 | } |
| 80 | return hash >>> 0; |
| 81 | } |
| 82 | |
| 83 | function slotKeyEquals( |
| 84 | keys: Uint8Array, |
| 85 | slot: number, |
| 86 | rawBytes: Uint8Array, |
| 87 | start: number |
| 88 | ): boolean { |
| 89 | const keyStart = slot * 20; |
| 90 | for (let i = 0; i < 20; i++) { |
| 91 | if (keys[keyStart + i] !== rawBytes[start + i]) return false; |
| 92 | } |
| 93 | return true; |
| 94 | } |
| 95 | |
| 96 | /** Create the planner's `OID -> owner selection slot` table. */ |
| 97 | export function createSelectedOidLookup(minimumEntries: number): SelectedOidLookup { |
| 98 | const slotCount = nextPowerOfTwo(Math.max(16, minimumEntries * 2)); |
| 99 | return { |
| 100 | count: 0, |
| 101 | mask: slotCount - 1, |
| 102 | used: new Uint8Array(slotCount), |
| 103 | keys: new Uint8Array(slotCount * 20), |
| 104 | owners: new Int32Array(slotCount).fill(-1), |
| 105 | }; |
| 106 | } |
| 107 | |
| 108 | /** Rebuild the owner map when it crosses the target load factor. */ |
| 109 | function rehashSelectedOidLookup(lookup: SelectedOidLookup, nextSlotCount: number): void { |
| 110 | const previousUsed = lookup.used; |
| 111 | const previousKeys = lookup.keys; |
| 112 | const previousOwners = lookup.owners; |
| 113 | |
| 114 | lookup.mask = nextSlotCount - 1; |
| 115 | lookup.used = new Uint8Array(nextSlotCount); |
| 116 | lookup.keys = new Uint8Array(nextSlotCount * 20); |
| 117 | lookup.owners = new Int32Array(nextSlotCount).fill(-1); |
| 118 | lookup.count = 0; |
| 119 | |
| 120 | for (let slot = 0; slot < previousUsed.length; slot++) { |
| 121 | if (!previousUsed[slot]) continue; |
| 122 | const keyStart = slot * 20; |
| 123 | setSelectedOidOwner(lookup, previousKeys, keyStart, previousOwners[slot]!); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | function ensureSelectedOidLookupCapacity(lookup: SelectedOidLookup): void { |
| 128 | if ((lookup.count + 1) * 10 <= lookup.used.length * 7) return; |
| 129 | rehashSelectedOidLookup(lookup, lookup.used.length * 2); |
| 130 | } |
| 131 | |
| 132 | /** Return the live owner row for an OID, or `-1` if the OID is unseen. */ |
| 133 | function findSelectedOidOwner( |
| 134 | lookup: SelectedOidLookup, |
| 135 | rawBytes: Uint8Array, |
| 136 | start: number |
| 137 | ): number { |
| 138 | let slot = hashOidAt(rawBytes, start) & lookup.mask; |
| 139 | while (lookup.used[slot]) { |
| 140 | if (slotKeyEquals(lookup.keys, slot, rawBytes, start)) { |
| 141 | return lookup.owners[slot]!; |
| 142 | } |
| 143 | slot = (slot + 1) & lookup.mask; |
| 144 | } |
| 145 | return -1; |
| 146 | } |
| 147 | |
| 148 | /** Install or replace the live owner row for an OID. */ |
| 149 | function setSelectedOidOwner( |
| 150 | lookup: SelectedOidLookup, |
| 151 | rawBytes: Uint8Array, |
| 152 | start: number, |
| 153 | ownerSel: number |
| 154 | ): void { |
| 155 | ensureSelectedOidLookupCapacity(lookup); |
| 156 | |
| 157 | let slot = hashOidAt(rawBytes, start) & lookup.mask; |
| 158 | while (lookup.used[slot]) { |
| 159 | if (slotKeyEquals(lookup.keys, slot, rawBytes, start)) { |
| 160 | lookup.owners[slot] = ownerSel; |
| 161 | return; |
| 162 | } |
| 163 | slot = (slot + 1) & lookup.mask; |
| 164 | } |
| 165 | |
| 166 | lookup.used[slot] = 1; |
| 167 | lookup.keys.set(rawBytes.subarray(start, start + 20), slot * 20); |
| 168 | lookup.owners[slot] = ownerSel; |
| 169 | lookup.count++; |
| 170 | } |
| 171 | |
| 172 | export function clonePackHeader(header: PackHeaderEx): PackHeaderEx { |
| 173 | return { |
| 174 | type: header.type, |
| 175 | sizeVarBytes: header.sizeVarBytes.slice(), |
| 176 | headerLen: header.headerLen, |
| 177 | baseOid: header.baseOid, |
| 178 | baseRel: header.baseRel, |
| 179 | }; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * Read and memoize a duplicate candidate header. |
| 184 | * |
| 185 | * The rewrite pass already reads the live row header once. This memo is only |
| 186 | * for duplicate-run scans that would otherwise reread the same pack entry |
| 187 | * header multiple times while deciding which OID owner should survive. |
| 188 | */ |
| 189 | async function readDuplicateCandidateHeader( |
| 190 | cache: DuplicateHeaderCache, |
| 191 | stats: SelectionStats, |
| 192 | key: number, |
| 193 | readState: PackReadState, |
| 194 | offset: number |
| 195 | ): Promise<PackHeaderEx | null> { |
| 196 | const cached = cache.get(key); |
| 197 | if (cached !== undefined) return cached; |
| 198 | |
| 199 | stats.duplicateHeaderProbes++; |
| 200 | const header = await readSelectedHeader(readState, offset); |
| 201 | const cloned = header ? clonePackHeader(header) : null; |
| 202 | cache.set(key, cloned); |
| 203 | return cloned; |
| 204 | } |
| 205 | |
| 206 | export async function claimCanonicalOwner( |
| 207 | table: SelectionTable, |
| 208 | sel: number, |
| 209 | snapshot: OrderedPackSnapshot, |
| 210 | readerStates: Map<number, PackReadState>, |
| 211 | dedupMap: Map<number, number>, |
| 212 | oidOwners: SelectedOidLookup, |
| 213 | duplicateHeaderCache: DuplicateHeaderCache, |
| 214 | stats: SelectionStats, |
| 215 | env: Env, |
| 216 | log: Logger, |
| 217 | warnedFlags: Set<string>, |
| 218 | options?: RewriteOptions |
| 219 | ): Promise<ClaimOwnerResult> { |
| 220 | // The rewrite output still prefers a single live row per OID, but OFS-pinned |
| 221 | // rows may need to reclaim ownership so pack-local base chains stay valid. |
| 222 | // Resolve that ownership before wiring any new base edges. |
| 223 | const oidStart = sel * 20; |
| 224 | const ownerSel = findSelectedOidOwner(oidOwners, table.oidsRaw, oidStart); |
| 225 | if (ownerSel >= 0) { |
| 226 | const currentKey = selectionKey(table.packSlots[sel]!, table.entryIndices[sel]!); |
| 227 | if (ownerSel === sel) { |
| 228 | return { kind: "unchanged", canonicalized: false }; |
| 229 | } |
| 230 | |
| 231 | if (table.ofsPinned[sel] && table.typeCodes[sel] >= 6 && !table.ofsPinned[ownerSel]) { |
| 232 | // OFS_DELTA children depend on the exact pack-local base position, not |
| 233 | // just the resulting OID. Let that exact row take ownership back so the |
| 234 | // later topology sort still sees the original acyclic within-pack chain. |
| 235 | stats.duplicateOfsOwnerTakeovers++; |
| 236 | setSelectedOidOwner(oidOwners, table.oidsRaw, oidStart, sel); |
| 237 | return { |
| 238 | kind: "takeover", |
| 239 | canonicalized: false, |
| 240 | previousOwnerSel: ownerSel, |
| 241 | }; |
| 242 | } |
| 243 | |
| 244 | // Keep future exact pack-position lookups collapsed onto the current live |
| 245 | // owner. Without this, later base resolution can recreate an extra row for |
| 246 | // the same pack entry after ownership already converged. |
| 247 | dedupMap.set(currentKey, ownerSel); |
| 248 | |
| 249 | if ( |
| 250 | !table.ofsPinned[ownerSel] && |
| 251 | table.typeCodes[sel]! < 6 && |
| 252 | table.typeCodes[ownerSel]! >= 6 && |
| 253 | upgradeOwnerSelectionToFull(table, ownerSel, sel) |
| 254 | ) { |
| 255 | // Preserve the existing owner slot when a later full-object duplicate |
| 256 | // arrives. This upgrades the owner in place and keeps any already-wired |
| 257 | // `baseSlots` stable. |
| 258 | stats.duplicateOwnerUpgrades++; |
| 259 | setSelectedOidOwner(oidOwners, table.oidsRaw, oidStart, ownerSel); |
| 260 | return { |
| 261 | kind: "redirect", |
| 262 | canonicalized: true, |
| 263 | targetSel: ownerSel, |
| 264 | upgradedOwner: true, |
| 265 | }; |
| 266 | } |
| 267 | |
| 268 | return { |
| 269 | kind: "redirect", |
| 270 | canonicalized: false, |
| 271 | targetSel: ownerSel, |
| 272 | upgradedOwner: false, |
| 273 | }; |
| 274 | } |
| 275 | |
| 276 | if (!table.ofsPinned[sel] && (table.typeCodes[sel] === 6 || table.typeCodes[sel] === 7)) { |
| 277 | // Delta rows get one extra chance to become the canonical owner by |
| 278 | // locating a full-object duplicate anywhere in the snapshot. |
| 279 | const candidate = await tryCanonicalizeDeltaSelectionToFull( |
| 280 | table, |
| 281 | sel, |
| 282 | snapshot, |
| 283 | readerStates, |
| 284 | dedupMap, |
| 285 | duplicateHeaderCache, |
| 286 | stats, |
| 287 | env, |
| 288 | log, |
| 289 | warnedFlags, |
| 290 | options |
| 291 | ); |
| 292 | if (candidate.kind === "redirect") { |
| 293 | // The best full-object duplicate is already selected elsewhere. Publish |
| 294 | // that row as the owner immediately so later duplicates short-circuit. |
| 295 | setSelectedOidOwner(oidOwners, table.oidsRaw, oidStart, candidate.targetSel); |
| 296 | return { |
| 297 | kind: "redirect", |
| 298 | canonicalized: true, |
| 299 | targetSel: candidate.targetSel, |
| 300 | upgradedOwner: false, |
| 301 | }; |
| 302 | } |
| 303 | if (candidate.kind === "swap") { |
| 304 | // No selected full owner exists yet, so mutate the current row in place |
| 305 | // to the chosen full-object duplicate and keep this same `sel` alive. |
| 306 | const altPack = snapshot.packs[candidate.packSlot]!; |
| 307 | setSelectionEntryIdentity(table, sel, candidate.packSlot, candidate.entryIndex, altPack.idx); |
| 308 | if ( |
| 309 | !storeSelectionHeader(table, sel, candidate.offset, candidate.nextOffset, candidate.header) |
| 310 | ) { |
| 311 | log.warn("rewrite:invalid-payload-length", { |
| 312 | packKey: altPack.packKey, |
| 313 | offset: candidate.offset, |
| 314 | }); |
| 315 | return { kind: "error" }; |
| 316 | } |
| 317 | table.baseSlots[sel] = -1; |
| 318 | setSelectedOidOwner(oidOwners, table.oidsRaw, oidStart, sel); |
| 319 | return { kind: "swapped" }; |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | setSelectedOidOwner(oidOwners, table.oidsRaw, oidStart, sel); |
| 324 | return { kind: "unchanged", canonicalized: false }; |
| 325 | } |
| 326 | |
| 327 | function upgradeOwnerSelectionToFull( |
| 328 | table: SelectionTable, |
| 329 | ownerSel: number, |
| 330 | fullSel: number |
| 331 | ): boolean { |
| 332 | // Preserve the existing owner slot so previously wired `baseSlots` keep |
| 333 | // pointing at the same `sel`. Only the owner's pack position and header |
| 334 | // fields change to the newly observed full-object duplicate. |
| 335 | if (table.typeCodes[fullSel] >= 6 || table.typeCodes[ownerSel] < 6) return false; |
| 336 | |
| 337 | // Keep the owner slot's pin state intact: children may already require this |
| 338 | // exact selection slot to stay the canonical OFS-stable base. |
| 339 | copySelectionRow(table, ownerSel, fullSel, { preserveTargetOfsPinned: true }); |
| 340 | table.baseSlots[ownerSel] = -1; |
| 341 | return true; |
| 342 | } |
| 343 | |
| 344 | async function tryCanonicalizeDeltaSelectionToFull( |
| 345 | table: SelectionTable, |
| 346 | sel: number, |
| 347 | snapshot: OrderedPackSnapshot, |
| 348 | readerStates: Map<number, PackReadState>, |
| 349 | dedupMap: Map<number, number>, |
| 350 | duplicateHeaderCache: DuplicateHeaderCache, |
| 351 | stats: SelectionStats, |
| 352 | env: Env, |
| 353 | log: Logger, |
| 354 | warnedFlags: Set<string>, |
| 355 | options?: RewriteOptions |
| 356 | ): Promise<FullDuplicateCandidate> { |
| 357 | const packSlot = table.packSlots[sel]!; |
| 358 | const entryIndex = table.entryIndices[sel]!; |
| 359 | const currentKey = selectionKey(packSlot, entryIndex); |
| 360 | const rawBytes = table.oidsRaw.subarray(sel * 20, sel * 20 + 20); |
| 361 | |
| 362 | // Scan duplicate runs in caller-provided snapshot order so tie-breaking |
| 363 | // stays deterministic. The first full-object duplicate wins; if it is |
| 364 | // already selected, redirect to that row instead of creating a second owner. |
| 365 | for (const candidate of collectPackedObjectCandidates(snapshot.packs, rawBytes)) { |
| 366 | if (candidate.packSlot === packSlot && candidate.objectIndex === entryIndex) continue; |
| 367 | |
| 368 | const altPack = snapshot.packs[candidate.packSlot]!; |
| 369 | const altReadState = await ensurePackReadState( |
| 370 | env, |
| 371 | altPack, |
| 372 | candidate.packSlot, |
| 373 | readerStates, |
| 374 | log, |
| 375 | warnedFlags, |
| 376 | options |
| 377 | ); |
| 378 | const altKey = selectionKey(candidate.packSlot, candidate.objectIndex); |
| 379 | const altHeader = await readDuplicateCandidateHeader( |
| 380 | duplicateHeaderCache, |
| 381 | stats, |
| 382 | altKey, |
| 383 | altReadState, |
| 384 | candidate.offset |
| 385 | ); |
| 386 | if (!altHeader || altHeader.type === 6 || altHeader.type === 7) continue; |
| 387 | |
| 388 | const existingOwner = dedupMap.get(altKey); |
| 389 | if (existingOwner !== undefined && existingOwner !== sel) { |
| 390 | // Keep the original position key pointing at the existing owner so any |
| 391 | // OFS bases already resolved against this slot continue to hit the |
| 392 | // canonical full-object selection after dead-slot compaction. |
| 393 | dedupMap.set(currentKey, existingOwner); |
| 394 | return { kind: "redirect", targetSel: existingOwner }; |
| 395 | } |
| 396 | |
| 397 | // Replace the selected delta in-place with the verified full-object |
| 398 | // duplicate. The original position key intentionally keeps pointing at |
| 399 | // `sel` so future OFS lookups collapse onto the same live owner slot. |
| 400 | dedupMap.set(altKey, sel); |
| 401 | return { |
| 402 | kind: "swap", |
| 403 | packSlot: candidate.packSlot, |
| 404 | entryIndex: candidate.objectIndex, |
| 405 | offset: candidate.offset, |
| 406 | nextOffset: candidate.nextOffset, |
| 407 | header: altHeader, |
| 408 | }; |
| 409 | } |
| 410 | |
| 411 | return { kind: "none" }; |
| 412 | } |