@gmod/gbz-base 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,8 +15,20 @@ interface PageHeader {
15
15
  cellPointers: number
16
16
  }
17
17
 
18
+ function readUint16(page: Uint8Array, offset: number) {
19
+ return page[offset]! * 256 + page[offset + 1]!
20
+ }
21
+
22
+ function readUint32(page: Uint8Array, offset: number) {
23
+ return (
24
+ page[offset]! * 16777216 +
25
+ page[offset + 1]! * 65536 +
26
+ page[offset + 2]! * 256 +
27
+ page[offset + 3]!
28
+ )
29
+ }
30
+
18
31
  function readHeader(page: Uint8Array, start: number): PageHeader {
19
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
20
32
  const type = page[start]
21
33
  if (
22
34
  type !== INTERIOR_INDEX &&
@@ -29,25 +41,27 @@ function readHeader(page: Uint8Array, start: number): PageHeader {
29
41
  const interior = type === INTERIOR_INDEX || type === INTERIOR_TABLE
30
42
  return {
31
43
  type,
32
- cellCount: view.getUint16(start + 3),
33
- rightChild: interior ? view.getUint32(start + 8) : 0,
44
+ cellCount: readUint16(page, start + 3),
45
+ rightChild: interior ? readUint32(page, start + 8) : 0,
34
46
  cellPointers: start + (interior ? 12 : 8),
35
47
  }
36
48
  }
37
49
 
38
50
  function cellOffset(page: Uint8Array, header: PageHeader, index: number) {
39
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
40
- return view.getUint16(header.cellPointers + 2 * index)
51
+ return readUint16(page, header.cellPointers + 2 * index)
41
52
  }
42
53
 
43
- function readUint32(page: Uint8Array, offset: number) {
44
- const view = new DataView(page.buffer, page.byteOffset, page.byteLength)
45
- return view.getUint32(offset)
54
+ interface IndexCell {
55
+ values: SqlValue[]
56
+ leftChild: number
46
57
  }
47
58
 
59
+ const MAX_DECODED_INDEX_PAGES = 4096
60
+
48
61
  export class BTree {
49
62
  private readonly usable: number
50
63
  private pager: Pager
64
+ private decodedIndexPages = new Map<number, (IndexCell | undefined)[]>()
51
65
 
52
66
  constructor(pager: Pager, reservedBytes: number) {
53
67
  this.pager = pager
@@ -167,19 +181,54 @@ export class BTree {
167
181
  }
168
182
  }
169
183
  } else {
184
+ const children: number[] = []
170
185
  for (let i = 0; i < header.cellCount; i++) {
171
- yield* this.tableScan(readUint32(page, cellOffset(page, header, i)))
186
+ children.push(readUint32(page, cellOffset(page, header, i)))
187
+ }
188
+ children.push(header.rightChild)
189
+ this.pager.prefetch(children)
190
+ for (const child of children) {
191
+ yield* this.tableScan(child)
172
192
  }
173
- yield* this.tableScan(header.rightChild)
174
193
  }
175
194
  }
176
195
 
177
- private async indexCell(page: Uint8Array, header: PageHeader, index: number) {
178
- const offset = cellOffset(page, header, index)
179
- const interior = header.type === INTERIOR_INDEX
180
- const [size, afterSize] = readVarint(page, interior ? offset + 4 : offset)
181
- const values = decodeRecord(await this.payload(page, afterSize, size, true))
182
- return { values, leftChild: interior ? readUint32(page, offset) : 0 }
196
+ private decodedCells(pageNumber: number, header: PageHeader) {
197
+ let cells = this.decodedIndexPages.get(pageNumber)
198
+ if (cells) {
199
+ this.decodedIndexPages.delete(pageNumber)
200
+ } else {
201
+ cells = new Array<IndexCell | undefined>(header.cellCount)
202
+ if (this.decodedIndexPages.size >= MAX_DECODED_INDEX_PAGES) {
203
+ const oldest = this.decodedIndexPages.keys().next().value
204
+ if (oldest !== undefined) {
205
+ this.decodedIndexPages.delete(oldest)
206
+ }
207
+ }
208
+ }
209
+ this.decodedIndexPages.set(pageNumber, cells)
210
+ return cells
211
+ }
212
+
213
+ private async indexCell(
214
+ pageNumber: number,
215
+ page: Uint8Array,
216
+ header: PageHeader,
217
+ index: number,
218
+ ): Promise<IndexCell> {
219
+ const cells = this.decodedCells(pageNumber, header)
220
+ let cell = cells[index]
221
+ if (!cell) {
222
+ const offset = cellOffset(page, header, index)
223
+ const interior = header.type === INTERIOR_INDEX
224
+ const [size, afterSize] = readVarint(page, interior ? offset + 4 : offset)
225
+ cell = {
226
+ values: decodeRecord(await this.payload(page, afterSize, size, true)),
227
+ leftChild: interior ? readUint32(page, offset) : 0,
228
+ }
229
+ cells[index] = cell
230
+ }
231
+ return cell
183
232
  }
184
233
 
185
234
  async *indexScanFrom(
@@ -191,7 +240,7 @@ export class BTree {
191
240
  let high = header.cellCount
192
241
  while (first < high) {
193
242
  const mid = (first + high) >> 1
194
- const cell = await this.indexCell(page, header, mid)
243
+ const cell = await this.indexCell(root, page, header, mid)
195
244
  if (compareKey(cell.values, low) < 0) {
196
245
  first = mid + 1
197
246
  } else {
@@ -199,7 +248,7 @@ export class BTree {
199
248
  }
200
249
  }
201
250
  for (let i = first; i < header.cellCount; i++) {
202
- const cell = await this.indexCell(page, header, i)
251
+ const cell = await this.indexCell(root, page, header, i)
203
252
  if (header.type === INTERIOR_INDEX) {
204
253
  yield* this.indexScanFrom(cell.leftChild, low)
205
254
  }
@@ -223,7 +272,7 @@ export class BTree {
223
272
  let child = 0
224
273
  while (low < high) {
225
274
  const mid = (low + high) >> 1
226
- const cell = await this.indexCell(page, header, mid)
275
+ const cell = await this.indexCell(pageNumber, page, header, mid)
227
276
  if (compareKey(cell.values, key) <= 0) {
228
277
  best = cell.values
229
278
  low = mid + 1
@@ -33,6 +33,23 @@ export class Pager {
33
33
  this.maxBlocks = opts.maxBlocks ?? 256
34
34
  }
35
35
 
36
+ private async read(length: number, start: number) {
37
+ let attempt = 0
38
+ for (;;) {
39
+ try {
40
+ const bytes = await this.source.read(length, start)
41
+ this.bytesFetched += bytes.length
42
+ return bytes
43
+ } catch (error) {
44
+ attempt += 1
45
+ if (attempt >= 3) {
46
+ throw error
47
+ }
48
+ await new Promise(resolve => setTimeout(resolve, 200 * attempt))
49
+ }
50
+ }
51
+ }
52
+
36
53
  seed(index: number, bytes: Uint8Array) {
37
54
  if (
38
55
  bytes.length ===
@@ -51,19 +68,72 @@ export class Pager {
51
68
  }
52
69
  const start = index * this.blockSize
53
70
  const length = Math.min(this.blockSize, this.fileSize - start)
54
- const pending = this.source.read(length, start).then(bytes => {
55
- this.bytesFetched += bytes.length
56
- return bytes
57
- })
71
+ const pending = this.read(length, start)
58
72
  this.fetches += 1
59
73
  this.blocks.set(index, pending)
60
- if (this.blocks.size > this.maxBlocks) {
74
+ this.forgetOnFailure(pending, [index])
75
+ this.evict()
76
+ return pending
77
+ }
78
+
79
+ prefetch(pageNumbers: number[]) {
80
+ const wanted = [
81
+ ...new Set(
82
+ pageNumbers.map(pageNumber =>
83
+ Math.floor(((pageNumber - 1) * this.pageSize) / this.blockSize),
84
+ ),
85
+ ),
86
+ ]
87
+ .filter(index => !this.blocks.has(index))
88
+ .sort((a, b) => a - b)
89
+ let runStart = 0
90
+ while (runStart < wanted.length) {
91
+ let runEnd = runStart + 1
92
+ while (
93
+ runEnd < wanted.length &&
94
+ wanted[runEnd] === wanted[runEnd - 1]! + 1
95
+ ) {
96
+ runEnd += 1
97
+ }
98
+ this.fetchRun(wanted[runStart]!, runEnd - runStart)
99
+ runStart = runEnd
100
+ }
101
+ }
102
+
103
+ private fetchRun(firstIndex: number, count: number) {
104
+ const start = firstIndex * this.blockSize
105
+ const length = Math.min(count * this.blockSize, this.fileSize - start)
106
+ const pending = this.read(length, start)
107
+ this.fetches += 1
108
+ const indexes: number[] = []
109
+ for (let i = 0; i < count; i++) {
110
+ const within = i * this.blockSize
111
+ indexes.push(firstIndex + i)
112
+ this.blocks.set(
113
+ firstIndex + i,
114
+ pending.then(bytes => bytes.subarray(within, within + this.blockSize)),
115
+ )
116
+ }
117
+ this.forgetOnFailure(pending, indexes)
118
+ this.evict()
119
+ }
120
+
121
+ private forgetOnFailure(pending: Promise<Uint8Array>, indexes: number[]) {
122
+ pending.catch(() => {
123
+ for (const index of indexes) {
124
+ this.blocks.delete(index)
125
+ }
126
+ })
127
+ }
128
+
129
+ private evict() {
130
+ while (this.blocks.size > this.maxBlocks) {
61
131
  const oldest = this.blocks.keys().next().value
62
- if (oldest !== undefined) {
63
- this.blocks.delete(oldest)
132
+ if (oldest === undefined) {
133
+ break
64
134
  }
135
+ this.blocks.delete(oldest)
65
136
  }
66
- return pending
67
137
  }
68
138
 
69
139
  async page(pageNumber: number) {