Skip to content
File

Blob: src/worker/git/pack/indexer/resolve/payloadCache.ts

typescript178 lines
1import type { GitObjectType } from "@/worker/git/core/objects";
2 
3const LIST_END = -1;
4 
5export interface CacheEntry {
6 type: GitObjectType;
7 payload: Uint8Array;
8}
9 
10function encodeType(type: GitObjectType): number {
11 switch (type) {
12 case "commit":
13 return 1;
14 case "tree":
15 return 2;
16 case "blob":
17 return 3;
18 case "tag":
19 return 4;
20 }
21}
22 
23function decodeType(typeCode: number): GitObjectType {
24 switch (typeCode) {
25 case 1:
26 return "commit";
27 case 2:
28 return "tree";
29 case 3:
30 return "blob";
31 case 4:
32 return "tag";
33 default:
34 throw new Error(`payload-cache: unknown object type code ${typeCode}`);
35 }
36}
37 
38export class PayloadLRU {
39 private payloads: Array<Uint8Array | undefined>;
40 private typeCodes: Uint8Array;
41 private present: Uint8Array;
42 private prev: Int32Array;
43 private next: Int32Array;
44 private totalBytes = 0;
45 private budget: number;
46 private cachedEntries = 0;
47 private head = LIST_END;
48 private tail = LIST_END;
49 
50 /**
51 * Lifecycle-aware eviction: each base has a "last needed at" offset — the
52 * maximum pack offset of any delta that depends on it. Once the current
53 * processing offset passes this value, the base is safe to evict.
54 *
55 * The cache is indexed by pack entry number instead of a Map so V8 does not
56 * pay per-entry hash/object overhead on large delta-heavy packs.
57 */
58 private deadlinesArr: Uint32Array | null = null;
59 private currentOffset = 0;
60 
61 constructor(budget: number, capacity: number) {
62 this.budget = budget;
63 this.payloads = new Array<Uint8Array | undefined>(capacity);
64 this.typeCodes = new Uint8Array(capacity);
65 this.present = new Uint8Array(capacity);
66 this.prev = new Int32Array(capacity).fill(LIST_END);
67 this.next = new Int32Array(capacity).fill(LIST_END);
68 }
69 
70 /** Set per-entry eviction deadlines before the resolve pass begins. */
71 setDeadlines(deadlines: Uint32Array): void {
72 this.deadlinesArr = deadlines;
73 }
74 
75 /** Update the current processing offset so eviction can prioritize expired entries. */
76 setCurrentOffset(offset: number): void {
77 this.currentOffset = offset;
78 }
79 
80 get(index: number): CacheEntry | undefined {
81 if (!this.present[index]) return undefined;
82 const payload = this.payloads[index];
83 if (!payload) return undefined;
84 this.touch(index);
85 return {
86 type: decodeType(this.typeCodes[index]),
87 payload,
88 };
89 }
90 
91 set(index: number, entry: CacheEntry): void {
92 if (this.present[index]) {
93 this.remove(index);
94 }
95 if (entry.payload.length > this.budget) {
96 return;
97 }
98 
99 this.payloads[index] = entry.payload;
100 this.typeCodes[index] = encodeType(entry.type);
101 this.present[index] = 1;
102 this.cachedEntries++;
103 this.totalBytes += entry.payload.length;
104 this.appendTail(index);
105 this.evict();
106 }
107 
108 private touch(index: number): void {
109 if (this.tail === index) return;
110 this.detach(index);
111 this.appendTail(index);
112 }
113 
114 private appendTail(index: number): void {
115 this.prev[index] = this.tail;
116 this.next[index] = LIST_END;
117 if (this.tail !== LIST_END) {
118 this.next[this.tail] = index;
119 } else {
120 this.head = index;
121 }
122 this.tail = index;
123 }
124 
125 private detach(index: number): void {
126 const prevIndex = this.prev[index];
127 const nextIndex = this.next[index];
128 if (prevIndex !== LIST_END) {
129 this.next[prevIndex] = nextIndex;
130 } else {
131 this.head = nextIndex;
132 }
133 if (nextIndex !== LIST_END) {
134 this.prev[nextIndex] = prevIndex;
135 } else {
136 this.tail = prevIndex;
137 }
138 this.prev[index] = LIST_END;
139 this.next[index] = LIST_END;
140 }
141 
142 private remove(index: number): void {
143 if (!this.present[index]) return;
144 const payload = this.payloads[index];
145 this.detach(index);
146 this.present[index] = 0;
147 this.payloads[index] = undefined;
148 this.typeCodes[index] = 0;
149 this.cachedEntries--;
150 if (payload) {
151 this.totalBytes -= payload.length;
152 }
153 }
154 
155 private findEvictionCandidate(): number {
156 // Walk the LRU chain from oldest to newest so we still prefer entries that
157 // are both cold and past their dependency deadline. If none are expired yet,
158 // evicting the oldest entry preserves the original LRU fallback behavior.
159 let candidate = this.head;
160 while (candidate !== LIST_END) {
161 const deadline = this.deadlinesArr ? this.deadlinesArr[candidate] : 0;
162 if (deadline <= this.currentOffset) {
163 return candidate;
164 }
165 candidate = this.next[candidate];
166 }
167 return this.head;
168 }
169 
170 private evict(): void {
171 while (this.totalBytes > this.budget && this.cachedEntries > 1) {
172 const candidate = this.findEvictionCandidate();
173 if (candidate === LIST_END) break;
174 this.remove(candidate);
175 }
176 }
177}