bumparena 0.0.6 → 0.0.8

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.
package/README.md CHANGED
@@ -1,74 +1,90 @@
1
- ## High-Speed: Linear Bulk Import
1
+ # BumpArena
2
2
 
3
- Use `reserve()` for high-volume, linear data ingestion. To guarantee maximum throughput and deterministic behavior, `reserve()` **always appends** memory at the end of the arena buffer. No compaction, no relocation, no surprises.
3
+ **4x faster than standard arrays and uses only ~40% of the RAM!** 🚀💾
4
4
 
5
- This makes it ideal for tight loops, streaming parsers, and bulk loaders where predictable memory behavior is critical.
5
+ BumpArena is a high-performance memory arena for JavaScript and TypeScript. It provides contiguous memory allocation, fast pointer-based access, and minimal garbage collection overhead, making it ideal for handling large datasets efficiently.
6
6
 
7
7
  ---
8
8
 
9
- ### Allocation Strategies
9
+ ## Features
10
10
 
11
- #### Performance & GC Efficiency
11
+ - Contiguous memory buffer for fast allocations
12
+ - Pointer-based access with generation tracking
13
+ - Recycled buckets for efficient memory reuse
14
+ - Compact memory footprint
15
+ - Compatible with TypeScript and JavaScript
12
16
 
13
- This Arena is explicitly designed to bypass the typical performance pitfalls of the V8 runtime:
17
+ ---
18
+
19
+ ## Installation
14
20
 
15
- **GC Invisibility**
16
- The entire arena is backed by a single `ArrayBuffer`. Since the Garbage Collector does not traverse raw buffer contents, this completely avoids costly mark-and-sweep scans over arena memory.
21
+ ```bash
22
+ npm install bumparena
23
+ ```
17
24
 
18
- **Zero Object Churn**
19
- Data insertion does not allocate JavaScript objects. All writes are performed directly against the buffer. The engine reuses the same execution paths without producing heap garbage.
25
+ or using Yarn:
20
26
 
21
- **Deferred Management Overhead**
22
- Logical pointers (`ArenaLocation`, implemented as `BigInt`) are created *only* when `label()` is invoked. This keeps pointer bookkeeping out of performance-critical hot paths.
27
+ ```bash
28
+ yarn add bumparena
29
+ ```
23
30
 
24
31
  ---
25
32
 
26
- ### Technical Architecture
33
+ ## Quick Start
34
+
35
+ ```ts
36
+ import { Arena } from "bumparena";
27
37
 
28
- #### Memory Layout (16-Byte Header)
38
+ // Create a new arena with default settings
39
+ const arena = new Arena();
29
40
 
30
- Each allocation block is prefixed with a fixed-size metadata header:
41
+ // Allocate some data
42
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
43
+ const ptr = arena.alloc(data);
31
44
 
32
- - Size
33
- - Generation ID
34
- - Flags / reserved space
45
+ // Read the data back
46
+ const readData = arena.read(ptr);
47
+ console.log(readData); // Uint8Array [1,2,3,4,5]
35
48
 
36
- `reserve()` eagerly initializes these headers to ensure full compatibility with `read()` and `label()` without requiring additional bookkeeping passes.
49
+ // Free the allocation
50
+ arena.free(ptr);
37
51
 
38
- This guarantees **O(1)** access and validation at read time.
52
+ // Reserve a block for manual writes
53
+ const reserved = arena.reserve(10);
54
+ reserved.set(new Uint8Array([10, 20, 30]));
55
+ console.log(reserved);
56
+ ```
39
57
 
40
58
  ---
41
59
 
42
- #### Pointer Security & Validation
60
+ ## Advanced Usage
43
61
 
44
- An `ArenaLocation` is encoded as a **64-bit BigInt**:
62
+ - Direct allocation from existing buffers with `directAlloc()`
63
+ - Custom headers for allocations (`header0`, `header1`, `header2`)
64
+ - Iterate all allocations using `label()`
65
+ - Estimate memory usage with `estimate(size, amount)`
45
66
 
46
- - **High 32 bits** → Physical byte offset inside the arena
47
- - **Low 32 bits** → Generation ID
67
+ ---
48
68
 
49
- When `read()` is called, the arena validates the pointer by comparing its generation ID with the one stored in the block header:
69
+ ## Benchmarks
50
70
 
51
- - Match valid, data is returned
52
- - ❌ Mismatch → block was freed or recycled → `null` is returned
71
+ | Implementation | Time | Heap Used | Notes |
72
+ |-----------------------|------------|-----------|------------|
73
+ | BumpArena (Optimized) | 183,098 ms | 4.82 GB | 50M items |
74
+ | Standard Array | 765,961 ms | 11.22 GB | 50M items |
53
75
 
54
- This provides **use-after-free protection** without reference tracking, weak maps, or GC involvement.
76
+ > ~4x faster and uses ~40% of the RAM compared to standard JavaScript arrays
55
77
 
56
78
  ---
57
79
 
58
- ### 📖 Documentation & Tests
59
-
60
- The integrated test suite doubles as a **reference implementation**.
61
- See the `// --- SHOWCASE & TESTS ---` section at the end of the source file for:
62
-
63
- - Stress tests for dynamic resizing
64
- - Validation of pointer expiration semantics
65
- - Advanced bucket reuse and fragmentation behavior
66
- - Edge cases around generation rollover
80
+ ## Compatibility
67
81
 
68
- To run the showcase directly, execute the file with Node.js. No external dependencies required.
82
+ - Node.js
83
+ - Bun ✅
84
+ - Browser (via compiled JS) ✅
69
85
 
70
86
  ---
71
87
 
72
- ### ⚖️ License
88
+ ## License
73
89
 
74
- MIT License — use freely in commercial and non-commercial projects.
90
+ MIT © eugen252009
@@ -0,0 +1,64 @@
1
+ export type ArenaLocation = bigint & {
2
+ readonly __data_pointer: unique symbol;
3
+ };
4
+ export interface ArenaOptions {
5
+ initalSize?: number;
6
+ littleEndian?: boolean;
7
+ allignment?: 8 | 16 | 32 | 64;
8
+ bucketOffsets?: number[];
9
+ bucketCapacities?: number[];
10
+ }
11
+ export interface ArenaCustomHeaders {
12
+ header0: number;
13
+ header1: number;
14
+ header2: number;
15
+ }
16
+ export interface ArenaHeaders {
17
+ totalLength: number;
18
+ payloadlength: number;
19
+ deleted: boolean;
20
+ header0: number;
21
+ header1: number;
22
+ header2: number;
23
+ }
24
+ export declare class Arena {
25
+ private HEADER_SIZE_BYTES;
26
+ private _buffer;
27
+ private _view8;
28
+ private _view32;
29
+ private _offset;
30
+ private _emptySpots;
31
+ private _allignMask;
32
+ private _allignShift;
33
+ private _bucketOffsets;
34
+ private _bucketCapacities;
35
+ private _bucketcount;
36
+ constructor(options?: ArenaOptions);
37
+ private _u;
38
+ private _idx32;
39
+ private _makePtr;
40
+ private _getOffset;
41
+ private _getBucketCount;
42
+ private _setBucketCount;
43
+ private _getBucketOffset;
44
+ private _setBucketOffset;
45
+ private _initBlock;
46
+ alloc(data: Uint8Array, headers?: ArenaCustomHeaders): ArenaLocation;
47
+ read(location: ArenaLocation): Uint8Array | null;
48
+ free(location: ArenaLocation): ArenaLocation;
49
+ private _checkForSpace;
50
+ private _resize;
51
+ size(): number;
52
+ getBuffer(): Uint8Array;
53
+ reserve(size: number): Uint8Array;
54
+ translate(ptr: ArenaLocation): {
55
+ start: number;
56
+ generation: bigint;
57
+ };
58
+ readWithHeaders(ptr: ArenaLocation): Uint8Array | null;
59
+ label(): Array<ArenaLocation>;
60
+ getHeaders(ptr: ArenaLocation): ArenaHeaders;
61
+ estimate(size: number, amnt: number): number;
62
+ directAlloc(source: Uint8Array, startn: number, endn: number): ArenaLocation;
63
+ clear(): void;
64
+ }
package/dist/arena.js ADDED
@@ -0,0 +1,223 @@
1
+ var HEADERS;
2
+ (function (HEADERS) {
3
+ HEADERS[HEADERS["TOTAL_LENGTH_0_32"] = 0] = "TOTAL_LENGTH_0_32";
4
+ HEADERS[HEADERS["PAYLOAD_LENGTH_0_32"] = 1] = "PAYLOAD_LENGTH_0_32";
5
+ HEADERS[HEADERS["GENERATION_BYTE_0_32"] = 2] = "GENERATION_BYTE_0_32";
6
+ HEADERS[HEADERS["DELETED_8"] = 12] = "DELETED_8";
7
+ HEADERS[HEADERS["USER_STATUS_0_8"] = 13] = "USER_STATUS_0_8";
8
+ HEADERS[HEADERS["USER_STATUS_1_8"] = 14] = "USER_STATUS_1_8";
9
+ HEADERS[HEADERS["USER_STATUS_2_8"] = 15] = "USER_STATUS_2_8";
10
+ })(HEADERS || (HEADERS = {}));
11
+ export class Arena {
12
+ constructor(options) {
13
+ this.HEADER_SIZE_BYTES = 16;
14
+ this._buffer = new ArrayBuffer(options?.initalSize || 64 * 1024);
15
+ this._view8 = new Uint8Array(this._buffer);
16
+ this._view32 = new Uint32Array(this._buffer);
17
+ this._offset = 0;
18
+ //@ts-ignore
19
+ this._allignMask = ((options?.allignment || 8) - 1);
20
+ this._allignShift = Math.log2(this._allignMask + 1);
21
+ this._bucketCapacities = options?.bucketCapacities || [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024];
22
+ this._bucketOffsets = [];
23
+ this._bucketcount = this._bucketCapacities.length;
24
+ let currentOffset = 0;
25
+ for (const cap of this._bucketCapacities) {
26
+ this._bucketOffsets.push(currentOffset);
27
+ currentOffset += (cap + 1);
28
+ }
29
+ this._emptySpots = new Uint32Array(currentOffset);
30
+ }
31
+ _u(n) {
32
+ return n >>> 0;
33
+ }
34
+ _idx32(byteOffset) {
35
+ return (byteOffset >>> 0) >> 2;
36
+ }
37
+ _makePtr(offset, gen) {
38
+ const bOffset = BigInt(offset >>> 0);
39
+ const bGen = BigInt(gen ?? 0);
40
+ return ((bOffset << 32n) | (bGen & 0xffffffffn));
41
+ }
42
+ _getOffset(ptr) {
43
+ return Number(BigInt(ptr) >> 32n) >>> 0;
44
+ }
45
+ _getBucketCount(bucketIdx) {
46
+ return this._emptySpots[this._bucketOffsets[bucketIdx]];
47
+ }
48
+ _setBucketCount(bucketIdx, count) {
49
+ this._emptySpots[this._bucketOffsets[bucketIdx]] = count;
50
+ }
51
+ _getBucketOffset(bucketIdx, slotIdx) {
52
+ return this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1];
53
+ }
54
+ _setBucketOffset(bucketIdx, slotIdx, offset) {
55
+ this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1] = offset;
56
+ }
57
+ _initBlock(start, dataLength, headers) {
58
+ const idx = this._idx32(start);
59
+ const { header0: h0, header1: h1, header2: h2 } = headers;
60
+ this._view32[idx + HEADERS.TOTAL_LENGTH_0_32] = this._u((dataLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask);
61
+ this._view32[idx + HEADERS.PAYLOAD_LENGTH_0_32] = dataLength;
62
+ this._view32[idx + (HEADERS.DELETED_8 >> 2)] = ((h2 || 0) << 24) | ((h1 || 0) << 16) | ((h0 || 0) << 8);
63
+ this._view8[start + HEADERS.DELETED_8] = 0;
64
+ }
65
+ alloc(data, headers) {
66
+ headers || (headers = { header0: 0, header1: 0, header2: 0 });
67
+ const needed = (data.byteLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
68
+ const bucketIdx = (needed >> this._allignShift) - 1;
69
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
70
+ const count = this._getBucketCount(bucketIdx);
71
+ if (count > 0) {
72
+ const recycledOffset = this._u(this._getBucketOffset(bucketIdx, count - 1));
73
+ this._setBucketCount(bucketIdx, count - 1);
74
+ this._initBlock(recycledOffset, data.byteLength, headers);
75
+ const gen = this._view32[this._idx32(recycledOffset) + HEADERS.GENERATION_BYTE_0_32];
76
+ return this._makePtr(recycledOffset, gen);
77
+ }
78
+ }
79
+ const start = this._u(this._offset);
80
+ if (this._checkForSpace(needed))
81
+ this._resize();
82
+ this._initBlock(start, data.byteLength, headers);
83
+ this._view8.set(data, start + this.HEADER_SIZE_BYTES);
84
+ this._offset = this._u((start + needed) & ~this._allignMask);
85
+ const gen = this._view32[this._idx32(start) + HEADERS.GENERATION_BYTE_0_32];
86
+ return this._makePtr(start, gen);
87
+ }
88
+ read(location) {
89
+ const { start, generation } = this.translate(location);
90
+ const idx = this._idx32(start);
91
+ const currgen = BigInt(this._view32[idx + HEADERS.GENERATION_BYTE_0_32]);
92
+ if (generation !== currgen)
93
+ return null;
94
+ if (this._view8[start + HEADERS.DELETED_8] === 1)
95
+ return null;
96
+ const dataLength = this._view32[idx + HEADERS.PAYLOAD_LENGTH_0_32];
97
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + dataLength);
98
+ }
99
+ free(location) {
100
+ const { start, generation: _ } = this.translate(location);
101
+ const idx = this._idx32(start);
102
+ const currgen = this._view32[idx + HEADERS.GENERATION_BYTE_0_32];
103
+ this._view32[idx + HEADERS.GENERATION_BYTE_0_32] = currgen + 1;
104
+ this._view8[this._u(start) + HEADERS.DELETED_8] = 1;
105
+ const totalBlockSize = this._view32[this._idx32(start) + HEADERS.TOTAL_LENGTH_0_32];
106
+ const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
107
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
108
+ const count = this._getBucketCount(bucketIdx);
109
+ const capacity = this._bucketCapacities[bucketIdx];
110
+ if (count < capacity) {
111
+ this._setBucketOffset(bucketIdx, count, start);
112
+ this._setBucketCount(bucketIdx, count + 1);
113
+ }
114
+ }
115
+ return this._makePtr(0, 0);
116
+ }
117
+ _checkForSpace(size) {
118
+ if (this._buffer.byteLength >= (this._offset + size))
119
+ return false;
120
+ return true;
121
+ }
122
+ _resize() {
123
+ //@ts-ignore
124
+ if (this._buffer.transfer)
125
+ this._buffer = this._buffer.transfer(this._buffer.byteLength * 2);
126
+ else {
127
+ const newBuffer = new ArrayBuffer(this._buffer.byteLength * 2);
128
+ new Uint8Array(newBuffer).set(this._view8);
129
+ this._buffer = newBuffer;
130
+ }
131
+ this._view8 = new Uint8Array(this._buffer);
132
+ this._view32 = new Uint32Array(this._buffer);
133
+ }
134
+ size() {
135
+ return this._buffer.byteLength;
136
+ }
137
+ getBuffer() {
138
+ return this._view8.subarray(0, this._u(this._offset));
139
+ }
140
+ reserve(size) {
141
+ const start = this._u(this._offset);
142
+ this._checkForSpace((this.HEADER_SIZE_BYTES + size + this._allignMask) & ~this._allignMask) && this._resize();
143
+ this._initBlock(start, size, { header0: 0, header1: 0, header2: 0 });
144
+ this._offset = ((this._offset + size + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask) >>> 0;
145
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + size);
146
+ }
147
+ translate(ptr) {
148
+ const start = this._getOffset(ptr);
149
+ const generation = ptr & 0xffffffffn;
150
+ return { start, generation };
151
+ }
152
+ readWithHeaders(ptr) {
153
+ const { start, generation } = this.translate(ptr);
154
+ let idx32 = this._idx32(start);
155
+ let length = this._view32[idx32 + HEADERS.PAYLOAD_LENGTH_0_32];
156
+ if (BigInt(this._view32[idx32 + HEADERS.GENERATION_BYTE_0_32]) !== generation)
157
+ return null;
158
+ return this._view8.subarray(start + 12, start + this.HEADER_SIZE_BYTES + Number(length));
159
+ }
160
+ label() {
161
+ const ptrArray = new Array();
162
+ const limit = this._offset;
163
+ let pos = 0;
164
+ while (this._u(pos) < this._u(limit)) {
165
+ const totalLength = this._view32[this._idx32(pos)];
166
+ if (totalLength === 0) {
167
+ pos = this._idx32((pos + 16) & ~15);
168
+ continue;
169
+ }
170
+ const isDeleted = this._view8[this._u(pos) + HEADERS.DELETED_8] === 1;
171
+ if (!isDeleted) {
172
+ ptrArray.push(this._makePtr(pos, this._view32[this._idx32(pos) + HEADERS.GENERATION_BYTE_0_32]));
173
+ }
174
+ pos += totalLength;
175
+ if (totalLength === 0)
176
+ break;
177
+ }
178
+ return ptrArray;
179
+ }
180
+ getHeaders(ptr) {
181
+ const { start, generation: _ } = this.translate(ptr);
182
+ const idx = this._u(start);
183
+ return {
184
+ totalLength: Number(this._view32[this._idx32(start) + HEADERS.TOTAL_LENGTH_0_32].toString()),
185
+ payloadlength: Number(this._view32[this._idx32(start) + HEADERS.PAYLOAD_LENGTH_0_32].toString()),
186
+ deleted: this._view8[idx + HEADERS.DELETED_8] === 1,
187
+ header0: Number(this._view8[idx + HEADERS.USER_STATUS_0_8].toString()),
188
+ header1: Number(this._view8[idx + HEADERS.USER_STATUS_1_8].toString()),
189
+ header2: Number(this._view8[idx + HEADERS.USER_STATUS_2_8].toString()),
190
+ };
191
+ }
192
+ estimate(size, amnt) {
193
+ return (((size + this._allignMask) & ~this._allignMask) + this.HEADER_SIZE_BYTES) * amnt;
194
+ }
195
+ directAlloc(source, startn, endn) {
196
+ const start = this._u(this._offset);
197
+ const len = endn - startn;
198
+ const needed = (len + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
199
+ const bucketIdx = (needed >> this._allignShift) - 1;
200
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
201
+ const count = this._getBucketCount(bucketIdx);
202
+ if (count > 0) {
203
+ const recycledOffset = this._u(this._getBucketOffset(bucketIdx, count - 1));
204
+ this._setBucketCount(bucketIdx, count - 1);
205
+ this._initBlock(recycledOffset, len, { header0: 0, header1: 0, header2: 0 });
206
+ const gen = this._view32[this._idx32(recycledOffset) + HEADERS.GENERATION_BYTE_0_32];
207
+ this._view8.set(source.subarray(startn, endn), recycledOffset + this.HEADER_SIZE_BYTES);
208
+ return this._makePtr(recycledOffset, gen);
209
+ }
210
+ }
211
+ if (this._checkForSpace(needed))
212
+ this._resize();
213
+ this._initBlock(start, len, { header0: 0, header1: 0, header2: 0 });
214
+ this._offset = this._u((start + needed) & ~this._allignMask);
215
+ this._view8.set(source.subarray(startn, endn), start + this.HEADER_SIZE_BYTES);
216
+ const gen = this._view32[this._idx32(start) + HEADERS.GENERATION_BYTE_0_32];
217
+ return this._makePtr(start, gen);
218
+ }
219
+ clear() {
220
+ this._offset = 0;
221
+ this._emptySpots.fill(0);
222
+ }
223
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bumparena",
3
- "version":"0.0.6",
3
+ "version":"0.0.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/arena.js",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "test": "node ./dist/test/test.test.js",
13
- "pub": "tsc; npm publish"
13
+ "push": "bun test && tsc && npm pub"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
package/bench/bench.md DELETED
@@ -1,38 +0,0 @@
1
- # Benchmark 1
2
-
3
- ## bun arena.ts;
4
- 183098.55 ms
5
- {
6
- rss: 11240919040,
7
- heapTotal: 8595075072,
8
- heapUsed: 4817152633,
9
- external: 2157456969,
10
- arrayBuffers: 2156686216,
11
- }
12
- arenasize: 2097152 KB
13
-
14
-
15
-
16
- ## bun array.ts
17
- 765961.27 ms
18
- Count: 50000000
19
- {
20
- rss: 28036014080,
21
- heapTotal: 9476152320,
22
- heapUsed: 11222438346,
23
- external: 7170077276,
24
- arrayBuffers: 2921230601,
25
- }
26
-
27
- ---
28
-
29
- ## Direct Comparison
30
-
31
- | Metric | Arena Implementation (Optimized) | Standard Array Implementation | Difference / Factor |
32
- | :--- | :--- | :--- | :--- |
33
- | **Total Time** | **183,098 ms** (3.05 min) | **765,961 ms** (12.76 min) | **~4.2x Faster** |
34
- | **Time per 50M Lines** | **~18.3 sec** | **~76.6 sec** | **- 58.3 sec / Round** |
35
- | **RAM Usage (RSS)** | **11.24 GB** | **28.04 GB** | **16.8 GB Saved** |
36
- | **Heap Used (JS Objects)** | **4.82 GB** | **11.22 GB** | **2.3x More Efficient** |
37
- | **External (Buffer)** | **2.16 GB** | **7.17 GB** | **Compact Memory Footprint** |
38
-