@nxtedition/slice 1.0.1 → 1.0.2

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.
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts CHANGED
@@ -21,15 +21,10 @@ export declare class Slice {
21
21
  [Symbol.toStringTag](): string;
22
22
  [util.inspect.custom](depth: number, options: util.InspectOptionsStylized, inspect: typeof util.inspect): string;
23
23
  }
24
- /**
25
- * A buddy allocator using XOR bitmap for efficient coalescing.
26
- * Each bit tracks the XOR of a buddy pair's allocation state.
27
- * When a bit is 0, both buddies have the same state (both free or both allocated).
28
- * When a bit is 1, the buddies have different states.
29
- */
30
- export declare class BuddyAllocator {
24
+ export declare class PoolAllocator {
31
25
  #private;
32
26
  constructor(poolTotal?: number);
27
+ isFromPool(slice: Slice): boolean;
33
28
  realloc(slice: Slice, byteLength: number): Slice;
34
29
  get stats(): {
35
30
  size: number;
@@ -39,7 +34,5 @@ export declare class BuddyAllocator {
39
34
  poolUsed: number;
40
35
  poolSize: number;
41
36
  poolCount: number;
42
- poolList: number[];
43
37
  };
44
- isFromPool(slice: Slice | null | undefined): boolean;
45
38
  }
package/lib/index.js CHANGED
@@ -81,7 +81,11 @@ export class Slice {
81
81
  sourceEnd += this.byteOffset
82
82
  }
83
83
 
84
- if (this.buffer === target && sourceStart === targetStart) {
84
+ if (
85
+ sourceStart === targetStart &&
86
+ this.buffer.buffer === target.buffer &&
87
+ this.buffer.byteOffset === target.byteOffset
88
+ ) {
85
89
  return sourceEnd - sourceStart
86
90
  }
87
91
 
@@ -217,74 +221,25 @@ export class Slice {
217
221
  }
218
222
  }
219
223
 
220
- /**
221
- * A buddy allocator using XOR bitmap for efficient coalescing.
222
- * Each bit tracks the XOR of a buddy pair's allocation state.
223
- * When a bit is 0, both buddies have the same state (both free or both allocated).
224
- * When a bit is 1, the buddies have different states.
225
- */
226
- export class BuddyAllocator {
227
- #size = 0
228
- #padding = 0
229
- #poolCount = 0
224
+ export class PoolAllocator {
225
+ #size = 0
230
226
 
227
+ #pools = []
231
228
  #poolBuffer
232
- #poolTotal
233
- #minOrder
234
- #maxOrder
235
-
236
- // Free lists indexed by order - simple arrays with push/pop
237
- #freeLists = []
238
-
239
- // XOR bitmap for coalescing - single contiguous buffer for cache locality
240
- // Each order has a view into this buffer
241
- // Bit = 0: both buddies same state, Bit = 1: different states
242
- #bitmaps = []
243
- #bitmapBuffer = null
229
+ #poolOffset = 0
230
+ #poolSize = 0
231
+ #poolCount = 0
232
+ #padding = 0
244
233
 
245
234
  constructor(poolTotal = 128 * 1024 * 1024) {
246
- // Handle invalid pool sizes gracefully - fall back to no pool
247
- if (!Number.isFinite(poolTotal) || poolTotal <= 0) {
248
- this.#poolBuffer = Buffer.allocUnsafe(0)
249
- this.#poolTotal = 0
250
- this.#minOrder = 3
251
- this.#maxOrder = 0
252
- return
253
- }
254
-
255
- // Round up to nearest power of 2
256
- this.#maxOrder = Math.ceil(Math.log2(poolTotal))
257
- this.#poolTotal = 1 << this.#maxOrder
258
-
259
- // Minimum block size is 8 bytes (order 3)
260
- this.#minOrder = 3
261
-
262
- this.#poolBuffer = Buffer.allocUnsafe(this.#poolTotal)
263
-
264
- // Calculate total bitmap size and offsets for each order
265
- // Total pairs = 2^(maxOrder-minOrder) - 1, total words ≈ that / 32
266
- let totalWords = 0
267
- const offsets = []
268
- for (let order = 0; order <= this.#maxOrder; order++) {
269
- offsets.push(totalWords)
270
- const numPairs = 1 << Math.max(0, this.#maxOrder - order - 1)
271
- totalWords += Math.ceil(numPairs / 32)
272
- }
273
-
274
- // Allocate single contiguous buffer for all bitmaps
275
- this.#bitmapBuffer = new ArrayBuffer(totalWords * 4)
276
-
277
- // Create views into the buffer for each order
278
- for (let order = 0; order <= this.#maxOrder; order++) {
279
- this.#freeLists.push([])
280
-
281
- const numPairs = 1 << Math.max(0, this.#maxOrder - order - 1)
282
- const numWords = Math.ceil(numPairs / 32)
283
- this.#bitmaps.push(new Uint32Array(this.#bitmapBuffer, offsets[order] * 4, numWords))
235
+ this.#poolBuffer = Buffer.allocUnsafe(Number.isFinite(poolTotal) ? poolTotal : 0)
236
+ for (let n = 0; 2 ** n <= 256 * 1024; n++) {
237
+ this.#pools.push([])
284
238
  }
239
+ }
285
240
 
286
- // Initially, the entire buffer is one free block at max order
287
- this.#freeLists[this.#maxOrder].push(0)
241
+ isFromPool(slice ) {
242
+ return slice != null && slice.buffer === this.#poolBuffer
288
243
  }
289
244
 
290
245
  realloc(slice , byteLength ) {
@@ -292,35 +247,32 @@ export class BuddyAllocator {
292
247
  throw new TypeError(`Invalid byteLength: ${byteLength}`)
293
248
  }
294
249
 
295
- if (slice == null) {
296
- slice = new Slice()
297
- } else if (!(slice instanceof Slice)) {
298
- throw new TypeError('slice must be a Slice instance')
299
- }
300
-
301
250
  if (slice.byteLength === byteLength) {
302
251
  return slice
303
252
  }
304
253
 
305
- const dstOrder = byteLength === 0 ? 0 : this.#getOrder(byteLength)
254
+ // Ceil to nearest power of two.
255
+ const dstIdx = byteLength <= 8 ? 3 : 32 - Math.clz32(byteLength - 1)
306
256
 
307
257
  if (this.isFromPool(slice)) {
308
- // Derive order from maxByteLength
309
- const srcOrder = 31 - Math.clz32(slice.maxByteLength)
258
+ const srcIdx = 32 - Math.clz32(slice.maxByteLength - 1)
259
+
260
+ if (slice.maxByteLength !== 1 << srcIdx) {
261
+ throw new Error(`Invalid pool state`)
262
+ }
310
263
 
311
264
  this.#size -= slice.byteLength
312
265
  this.#padding -= slice.maxByteLength - slice.byteLength
313
266
 
314
- if (srcOrder === dstOrder) {
267
+ if (srcIdx === dstIdx) {
315
268
  slice.byteLength = byteLength
316
269
  this.#size += slice.byteLength
317
270
  this.#padding += slice.maxByteLength - slice.byteLength
318
271
  return slice
319
272
  }
320
273
 
321
- // Free the old block
322
- this.#freeBlock(slice.byteOffset, srcOrder)
323
- this.#poolCount--
274
+ this.#pools[srcIdx].push(slice.byteOffset)
275
+ this.#poolSize -= slice.maxByteLength
324
276
  }
325
277
 
326
278
  slice.reset()
@@ -329,134 +281,52 @@ export class BuddyAllocator {
329
281
  return slice
330
282
  }
331
283
 
332
- const offset = this.#allocBlock(dstOrder)
333
- if (offset == null) {
334
- // Fall back to regular allocation
335
- slice.buffer = Buffer.allocUnsafeSlow(byteLength)
336
- slice.byteOffset = 0
284
+ const maxByteLength = 1 << dstIdx
285
+ if (this.#pools.length > 32 || maxByteLength < byteLength || (maxByteLength & 0x7) !== 0) {
286
+ throw new Error(`Invalid pool state`)
287
+ }
288
+
289
+ if (dstIdx < this.#pools.length && this.#pools[dstIdx]?.length) {
290
+ slice.buffer = this.#poolBuffer
291
+ slice.byteOffset = this.#pools[dstIdx].pop()
337
292
  slice.byteLength = byteLength
338
- slice.maxByteLength = byteLength
339
- } else {
293
+ slice.maxByteLength = maxByteLength
294
+
295
+ this.#poolSize += maxByteLength
296
+ } else if (
297
+ dstIdx < this.#pools.length &&
298
+ this.#poolOffset + maxByteLength <= this.#poolBuffer.byteLength
299
+ ) {
340
300
  slice.buffer = this.#poolBuffer
341
- slice.byteOffset = offset
301
+ slice.byteOffset = this.#poolOffset
342
302
  slice.byteLength = byteLength
343
- slice.maxByteLength = 1 << dstOrder
303
+ slice.maxByteLength = maxByteLength
344
304
 
345
- this.#poolCount++
346
- this.#size += byteLength
347
- this.#padding += slice.maxByteLength - slice.byteLength
305
+ this.#poolOffset += maxByteLength
306
+ this.#poolCount += 1
307
+ this.#poolSize += maxByteLength
308
+ } else {
309
+ slice.buffer = Buffer.allocUnsafeSlow(byteLength)
310
+ slice.byteOffset = 0
311
+ slice.byteLength = byteLength
312
+ slice.maxByteLength = byteLength
348
313
  }
349
314
 
315
+ this.#size += slice.byteLength
316
+ this.#padding += slice.maxByteLength - slice.byteLength
317
+
350
318
  return slice
351
319
  }
352
320
 
353
321
  get stats() {
354
- // Calculate free space from all free lists
355
- let freeSpace = 0
356
- for (let order = this.#minOrder; order <= this.#maxOrder; order++) {
357
- freeSpace += this.#freeLists[order].length << order
358
- }
359
-
360
- const poolUsed = this.#poolTotal - freeSpace
361
-
362
322
  return {
363
323
  size: this.#size,
364
324
  padding: this.#padding,
365
325
  ratio: this.#size > 0 ? (this.#size + this.#padding) / this.#size : 1,
366
- poolTotal: this.#poolTotal,
367
- poolUsed,
368
- poolSize: poolUsed,
326
+ poolTotal: this.#poolBuffer.byteLength,
327
+ poolUsed: this.#poolOffset,
328
+ poolSize: this.#poolSize,
369
329
  poolCount: this.#poolCount,
370
- poolList: this.#freeLists.map((list) => list.length),
371
- }
372
- }
373
-
374
- isFromPool(slice ) {
375
- return slice != null && slice.buffer === this.#poolBuffer
376
- }
377
-
378
- #toggleBit(offset , order ) {
379
- if (order >= this.#maxOrder) {
380
- return false
381
- }
382
- const pairIndex = offset >>> (order + 1)
383
- const wordIndex = pairIndex >>> 5
384
- const bitMask = 1 << (pairIndex & 31)
385
- const bitmap = this.#bitmaps[order]
386
- bitmap[wordIndex] ^= bitMask
387
- return (bitmap[wordIndex] & bitMask) !== 0
388
- }
389
-
390
- #allocBlock(order ) {
391
- // Try to allocate from pool
392
- if (order > this.#maxOrder || this.#poolTotal === 0) {
393
- return null
394
- }
395
-
396
- // Find the smallest order with a free block
397
- let currentOrder = order
398
- while (currentOrder <= this.#maxOrder && this.#freeLists[currentOrder].length === 0) {
399
- currentOrder++
400
- }
401
-
402
- if (currentOrder > this.#maxOrder) {
403
- return null
404
330
  }
405
-
406
- // Pop the free block
407
- const offset = this.#freeLists[currentOrder].pop()
408
-
409
- // Found block at exact order - just toggle and return
410
- if (currentOrder === order) {
411
- this.#toggleBit(offset, order)
412
- return offset
413
- }
414
-
415
- // Split down to the requested order
416
- while (currentOrder > order) {
417
- currentOrder--
418
- // Add buddy to free list
419
- const buddyOffset = offset ^ (1 << currentOrder)
420
- this.#freeLists[currentOrder].push(buddyOffset)
421
- // Toggle bit - one buddy free, one allocated/kept
422
- this.#toggleBit(offset, currentOrder)
423
- }
424
-
425
- return offset
426
- }
427
-
428
- #freeBlock(offset , order ) {
429
- while (order < this.#maxOrder) {
430
- // Toggle bit and check if we can coalesce
431
- const isDifferent = this.#toggleBit(offset, order)
432
-
433
- if (isDifferent) {
434
- // Bit is now 1: buddy is allocated, can't coalesce
435
- this.#freeLists[order].push(offset)
436
- return
437
- }
438
-
439
- // Bit is now 0: both buddies are free, coalesce
440
- // Remove buddy from free list
441
- const buddyOffset = offset ^ (1 << order)
442
- const idx = this.#freeLists[order].indexOf(buddyOffset)
443
- if (idx !== -1) {
444
- // Swap with last and pop for O(1) removal
445
- const last = this.#freeLists[order].length - 1
446
- this.#freeLists[order][idx] = this.#freeLists[order][last]
447
- this.#freeLists[order].pop()
448
- }
449
-
450
- // Move up to parent level
451
- offset = offset & ~(1 << order)
452
- order++
453
- }
454
-
455
- // Reached max order
456
- this.#freeLists[order].push(offset)
457
- }
458
-
459
- #getOrder(byteLength ) {
460
- return byteLength <= 1 << this.#minOrder ? this.#minOrder : 32 - Math.clz32(byteLength - 1)
461
331
  }
462
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/slice",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -19,6 +19,5 @@
19
19
  "amaroc": "^1.0.1",
20
20
  "rimraf": "^6.1.2",
21
21
  "typescript": "^5.9.3"
22
- },
23
- "gitHead": "2517ca631e010f17f7ba8be4e1b854ad18cbd746"
22
+ }
24
23
  }