Skip to content
File

Blob: src/worker/git/pack/rewrite/layout.ts

typescript140 lines
1import type { OrderedPackSnapshot } from "@/worker/git/operations/fetch/types";
2import type { Logger } from "@/worker/common/logger";
3 
4import { BinaryHeap } from "@/worker/common";
5import { ofsDeltaDistanceLength } from "../packMeta";
6import { HEADER_STABILITY_CAP, type SelectionTable } from "./shared";
7 
8export function canPassthroughSinglePack(
9 snapshot: OrderedPackSnapshot,
10 table: SelectionTable
11): boolean {
12 if (snapshot.packs.length !== 1 || table.count !== snapshot.packs[0]?.idx.count) {
13 return false;
14 }
15 
16 for (let sel = 0; sel < table.count; sel++) {
17 if (table.syntheticPayloads[sel]) return false;
18 }
19 return true;
20}
21 
22/**
23 * Populates `table.outputOrder` with a topologically valid output ordering
24 * (bases before deltas). Returns false on cycle or incomplete graph.
25 */
26export function buildOutputOrder(table: SelectionTable, log: Logger): boolean {
27 const n = table.count;
28 
29 // Build dependency graph using linked-list arrays (same pattern as
30 // src/worker/git/pack/indexer/resolve/dependencies.ts)
31 const indegree = new Uint8Array(n);
32 const childHead = new Int32Array(n).fill(-1);
33 const childNext = new Int32Array(n).fill(-1);
34 
35 for (let sel = 0; sel < n; sel++) {
36 const base = table.baseSlots[sel];
37 if (base < 0) continue;
38 indegree[sel]++;
39 childNext[sel] = childHead[base];
40 childHead[base] = sel;
41 }
42 
43 // Min-heap comparing (packSlots, offsets) for stable tie-breaking.
44 // Reuses the generic BinaryHeap from src/worker/common/heap.ts.
45 const cmp = (a: number, b: number) => {
46 const packDiff = table.packSlots[a] - table.packSlots[b];
47 if (packDiff !== 0) return packDiff;
48 return table.offsets[a] - table.offsets[b];
49 };
50 const roots: number[] = [];
51 for (let sel = 0; sel < n; sel++) {
52 if (indegree[sel] === 0) roots.push(sel);
53 }
54 const heap = new BinaryHeap<number>(cmp, roots);
55 
56 let cursor = 0;
57 while (!heap.isEmpty()) {
58 const sel = heap.pop()!;
59 table.outputOrder[cursor++] = sel;
60 
61 let child = childHead[sel];
62 while (child >= 0) {
63 indegree[child]--;
64 if (indegree[child] === 0) heap.push(child);
65 child = childNext[child];
66 }
67 }
68 
69 if (cursor !== n) {
70 log.warn("rewrite:topology-incomplete", { selected: n, ordered: cursor });
71 return false;
72 }
73 return true;
74}
75 
76/**
77 * Computes output header lengths and offsets, iterating until OFS_DELTA
78 * distance varints stabilize. Returns false on convergence failure.
79 */
80export function computeHeaderLengths(table: SelectionTable, log: Logger): boolean {
81 const n = table.count;
82 
83 // Seed initial output header lengths
84 for (let i = 0; i < n; i++) {
85 const sel = table.outputOrder[i];
86 const svLen = table.sizeVarLens[sel];
87 const type = table.typeCodes[sel];
88 
89 if (type === 6) {
90 // Initial estimate using the original source-pack OFS distance
91 const base = table.baseSlots[sel];
92 table.outputHeaderLens[sel] =
93 svLen + ofsDeltaDistanceLength(table.offsets[sel] - table.offsets[base]);
94 } else if (type === 7) {
95 table.outputHeaderLens[sel] = svLen + 20;
96 } else {
97 table.outputHeaderLens[sel] = svLen;
98 }
99 }
100 
101 for (let iteration = 0; iteration < HEADER_STABILITY_CAP; iteration++) {
102 // Compute cumulative output offsets (12-byte PACK header)
103 let cursor = 12;
104 for (let i = 0; i < n; i++) {
105 const sel = table.outputOrder[i];
106 table.outputOffsets[sel] = cursor;
107 cursor += table.outputHeaderLens[sel] + table.payloadLens[sel];
108 }
109 
110 // Recompute OFS_DELTA distances with new output offsets
111 let changed = false;
112 for (let i = 0; i < n; i++) {
113 const sel = table.outputOrder[i];
114 if (table.typeCodes[sel] !== 6) continue;
115 
116 const base = table.baseSlots[sel];
117 if (base < 0) continue;
118 
119 const distance = table.outputOffsets[sel] - table.outputOffsets[base];
120 const nextLen = table.sizeVarLens[sel] + ofsDeltaDistanceLength(distance);
121 if (nextLen !== table.outputHeaderLens[sel]) {
122 table.outputHeaderLens[sel] = nextLen;
123 changed = true;
124 }
125 }
126 
127 if (!changed) {
128 // The 5-byte sizeVarBuf and 32-bit OFS distance encoding assume < 4 GiB.
129 if (cursor > 0xffff_ffff) {
130 log.warn("rewrite:output-pack-exceeds-32bit", { totalBytes: cursor });
131 return false;
132 }
133 return true;
134 }
135 }
136 
137 log.warn("rewrite:header-lengths-did-not-converge", { selected: n });
138 return false;
139}