bumparena 0.0.1
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/.github/workflows/npm-publish.yml +34 -0
- package/LICENSE +22 -0
- package/README.md +74 -0
- package/arena.ts +214 -0
- package/dist/arena.d.ts +58 -0
- package/dist/arena.js +178 -0
- package/package.json +16 -0
- package/test/test.ts +38 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm run build
|
|
20
|
+
- run: npm test
|
|
21
|
+
|
|
22
|
+
publish-npm:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 20
|
|
30
|
+
registry-url: https://registry.npmjs.org/
|
|
31
|
+
- run: npm ci
|
|
32
|
+
- run: npm publish
|
|
33
|
+
env:
|
|
34
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eugen252009
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
## High-Speed: Linear Bulk Import
|
|
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.
|
|
4
|
+
|
|
5
|
+
This makes it ideal for tight loops, streaming parsers, and bulk loaders where predictable memory behavior is critical.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
### Allocation Strategies
|
|
10
|
+
|
|
11
|
+
#### Performance & GC Efficiency
|
|
12
|
+
|
|
13
|
+
This Arena is explicitly designed to bypass the typical performance pitfalls of the V8 runtime:
|
|
14
|
+
|
|
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.
|
|
17
|
+
|
|
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.
|
|
20
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### Technical Architecture
|
|
27
|
+
|
|
28
|
+
#### Memory Layout (16-Byte Header)
|
|
29
|
+
|
|
30
|
+
Each allocation block is prefixed with a fixed-size metadata header:
|
|
31
|
+
|
|
32
|
+
- Size
|
|
33
|
+
- Generation ID
|
|
34
|
+
- Flags / reserved space
|
|
35
|
+
|
|
36
|
+
`reserve()` eagerly initializes these headers to ensure full compatibility with `read()` and `label()` without requiring additional bookkeeping passes.
|
|
37
|
+
|
|
38
|
+
This guarantees **O(1)** access and validation at read time.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
#### Pointer Security & Validation
|
|
43
|
+
|
|
44
|
+
An `ArenaLocation` is encoded as a **64-bit BigInt**:
|
|
45
|
+
|
|
46
|
+
- **High 32 bits** → Physical byte offset inside the arena
|
|
47
|
+
- **Low 32 bits** → Generation ID
|
|
48
|
+
|
|
49
|
+
When `read()` is called, the arena validates the pointer by comparing its generation ID with the one stored in the block header:
|
|
50
|
+
|
|
51
|
+
- ✅ Match → valid, data is returned
|
|
52
|
+
- ❌ Mismatch → block was freed or recycled → `null` is returned
|
|
53
|
+
|
|
54
|
+
This provides **use-after-free protection** without reference tracking, weak maps, or GC involvement.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
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
|
|
67
|
+
|
|
68
|
+
To run the showcase directly, execute the file with Node.js. No external dependencies required.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### ⚖️ License
|
|
73
|
+
|
|
74
|
+
MIT License — use freely in commercial and non-commercial projects.
|
package/arena.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export type ArenaLocation = bigint & { readonly __data_pointer: unique symbol };
|
|
2
|
+
export interface ArenaOptions {
|
|
3
|
+
initalSize?: number
|
|
4
|
+
littleEndian?: boolean
|
|
5
|
+
allignment?: 8 | 16 | 32 | 64
|
|
6
|
+
bucketOffsets?: number[];
|
|
7
|
+
bucketCapacities?: number[];
|
|
8
|
+
}
|
|
9
|
+
export interface ArenaCustomHeaders {
|
|
10
|
+
header0: number,
|
|
11
|
+
header1: number,
|
|
12
|
+
header2: number
|
|
13
|
+
}
|
|
14
|
+
export interface ArenaHeaders {
|
|
15
|
+
totalLength: number
|
|
16
|
+
payloadlength: number
|
|
17
|
+
deleted: boolean
|
|
18
|
+
header0: number
|
|
19
|
+
header1: number
|
|
20
|
+
header2: number
|
|
21
|
+
}
|
|
22
|
+
const enum SHIFTOFFSETS {
|
|
23
|
+
BYTE_8 = 0, // No SHIFT needed
|
|
24
|
+
BYTE_16 = 1,
|
|
25
|
+
BYTE_32 = 2,
|
|
26
|
+
BYTE_64 = 3,
|
|
27
|
+
}
|
|
28
|
+
const enum HEADERS {
|
|
29
|
+
TOTAL_LENGTH_0_32 = 0,
|
|
30
|
+
PAYLOAD_LENGTH_0_32 = 1,
|
|
31
|
+
GENERATION_BYTE_0_32 = 2,
|
|
32
|
+
DELETED_8 = 12,
|
|
33
|
+
USER_STATUS_0_8 = 13,
|
|
34
|
+
USER_STATUS_1_8 = 14,
|
|
35
|
+
USER_STATUS_2_8 = 15,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Arena {
|
|
39
|
+
private HEADER_SIZE_BYTES = 16
|
|
40
|
+
private _buffer: ArrayBuffer
|
|
41
|
+
private _view8: Uint8Array
|
|
42
|
+
private _view32: Uint32Array
|
|
43
|
+
private _offset: number
|
|
44
|
+
private _emptySpots: Uint32Array
|
|
45
|
+
private _allignMask: 7 | 15 | 31 | 63
|
|
46
|
+
private _allignShift: number
|
|
47
|
+
private _bucketOffsets: number[];
|
|
48
|
+
private _bucketCapacities: number[];
|
|
49
|
+
private _bucketcount: number;
|
|
50
|
+
|
|
51
|
+
constructor(options?: ArenaOptions) {
|
|
52
|
+
this._buffer = new ArrayBuffer(options?.initalSize || 64 * 1024)
|
|
53
|
+
this._view8 = new Uint8Array(this._buffer);
|
|
54
|
+
this._view32 = new Uint32Array(this._buffer)
|
|
55
|
+
this._offset = 0;
|
|
56
|
+
//@ts-ignore
|
|
57
|
+
this._allignMask = ((options?.allignment || 8) - 1!) as number
|
|
58
|
+
this._allignShift = Math.log2(this._allignMask + 1);
|
|
59
|
+
this._bucketCapacities = options?.bucketCapacities || [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024]
|
|
60
|
+
this._bucketOffsets = [];
|
|
61
|
+
this._bucketcount = this._bucketCapacities.length;
|
|
62
|
+
let currentOffset = 0;
|
|
63
|
+
for (const cap of this._bucketCapacities) {
|
|
64
|
+
this._bucketOffsets.push(currentOffset)
|
|
65
|
+
currentOffset += (cap + 1)
|
|
66
|
+
}
|
|
67
|
+
this._emptySpots = new Uint32Array(currentOffset)
|
|
68
|
+
}
|
|
69
|
+
private _getBucketCount(bucketIdx: number): number {
|
|
70
|
+
return this._emptySpots[this._bucketOffsets[bucketIdx]!]!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _setBucketCount(bucketIdx: number, count: number): void {
|
|
74
|
+
this._emptySpots[this._bucketOffsets[bucketIdx]!] = count;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private _getBucketOffset(bucketIdx: number, slotIdx: number): number {
|
|
78
|
+
return this._emptySpots[this._bucketOffsets[bucketIdx]! + slotIdx + 1]!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private _setBucketOffset(bucketIdx: number, slotIdx: number, offset: number): void {
|
|
82
|
+
this._emptySpots[this._bucketOffsets[bucketIdx]! + slotIdx + 1] = offset;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private _initBlock(start: number, dataLength: number, { header0, header1, header2 }: { header0?: number, header1?: number, header2?: number }) {
|
|
86
|
+
const h0 = (header0 || 0) & 0xFF;
|
|
87
|
+
const h1 = (header1 || 0) & 0xFF;
|
|
88
|
+
const h2 = (header2 || 0) & 0xFF;
|
|
89
|
+
const shift32 = start >> SHIFTOFFSETS.BYTE_32
|
|
90
|
+
this._view32[shift32 + HEADERS.TOTAL_LENGTH_0_32] = (dataLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask
|
|
91
|
+
this._view32[shift32 + HEADERS.PAYLOAD_LENGTH_0_32] = dataLength
|
|
92
|
+
this._view32[shift32 + (HEADERS.DELETED_8 >> SHIFTOFFSETS.BYTE_32)] = (h2 << 24) | (h1 << 16) | (h0 << 8) | 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public alloc(data: Uint8Array, headers?: ArenaCustomHeaders): ArenaLocation {
|
|
96
|
+
if (headers == undefined) headers = { header0: 0, header1: 0, header2: 0 }
|
|
97
|
+
const totalBlockSize = (data.byteLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
|
|
98
|
+
const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
|
|
99
|
+
if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
|
|
100
|
+
const count = this._getBucketCount(bucketIdx);
|
|
101
|
+
if (count > 0) {
|
|
102
|
+
const recycledOffset = this._getBucketOffset(bucketIdx, count - 1);
|
|
103
|
+
this._setBucketCount(bucketIdx, count - 1);
|
|
104
|
+
this._initBlock(recycledOffset, data.length, headers);
|
|
105
|
+
return (BigInt(recycledOffset) << 32n | BigInt(this._view32[(recycledOffset >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const start = this._offset
|
|
109
|
+
this._checkForSpace(data.byteLength + this.HEADER_SIZE_BYTES) && this._resize()
|
|
110
|
+
this._initBlock(start, data.byteLength, headers || {})
|
|
111
|
+
this._view8.set(data, start + this.HEADER_SIZE_BYTES)
|
|
112
|
+
this._offset = (data.byteLength + this.HEADER_SIZE_BYTES + this._offset + this._allignMask) & ~this._allignMask;
|
|
113
|
+
return (BigInt(start) << 32n | BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation
|
|
114
|
+
}
|
|
115
|
+
public read(location: ArenaLocation): Uint8Array | null {
|
|
116
|
+
let start = Number(BigInt(location as bigint) >> 32n)
|
|
117
|
+
const generation = BigInt(location as bigint) & 0xFFFFFFFFn
|
|
118
|
+
let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!
|
|
119
|
+
const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]!)
|
|
120
|
+
if (diff !== 0n) return null
|
|
121
|
+
return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + Number(length))
|
|
122
|
+
}
|
|
123
|
+
public free(location: ArenaLocation): ArenaLocation {
|
|
124
|
+
let start = Number(BigInt(location as bigint) >> 32n)
|
|
125
|
+
const currgen = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!
|
|
126
|
+
this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32] = currgen + 1 // this part can wrap around
|
|
127
|
+
this._view8[start + HEADERS.DELETED_8] = 1
|
|
128
|
+
const totalBlockSize = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32]!
|
|
129
|
+
const bucketIdx = (totalBlockSize >> this._allignShift) - 1
|
|
130
|
+
if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
|
|
131
|
+
const count = this._getBucketCount(bucketIdx)
|
|
132
|
+
const capacity = this._bucketCapacities[bucketIdx]!
|
|
133
|
+
if (count < capacity) {
|
|
134
|
+
this._setBucketOffset(bucketIdx, count, start);
|
|
135
|
+
this._setBucketCount(bucketIdx, count + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return 0n as ArenaLocation
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _checkForSpace(size: number): boolean {
|
|
142
|
+
if (this._buffer.byteLength >= this._offset + size) return false
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _resize() {
|
|
147
|
+
//@ts-ignore
|
|
148
|
+
this._buffer = this._buffer.transfer(this._buffer.byteLength * 2)
|
|
149
|
+
this._view8 = new Uint8Array(this._buffer)
|
|
150
|
+
this._view32 = new Uint32Array(this._buffer)
|
|
151
|
+
}
|
|
152
|
+
public size(): number {
|
|
153
|
+
return this._buffer.byteLength
|
|
154
|
+
}
|
|
155
|
+
public getBuffer(): Uint8Array {
|
|
156
|
+
return this._view8.subarray(0, this._offset)
|
|
157
|
+
}
|
|
158
|
+
public reserve(size: number): Uint8Array {
|
|
159
|
+
const start = this._offset
|
|
160
|
+
this._checkForSpace(start + this.HEADER_SIZE_BYTES + size + this._allignMask & ~this._allignMask) && this._resize()
|
|
161
|
+
this._initBlock(start, size, {})
|
|
162
|
+
this._offset = this._offset + size + this.HEADER_SIZE_BYTES + this._allignMask & ~this._allignMask
|
|
163
|
+
return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + size)
|
|
164
|
+
}
|
|
165
|
+
public translate(ptr: ArenaLocation) {
|
|
166
|
+
let start = Number(BigInt(ptr as bigint) >> 32n)
|
|
167
|
+
const generation = BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)
|
|
168
|
+
return { start, generation }
|
|
169
|
+
}
|
|
170
|
+
public readWithHeaders(ptr: ArenaLocation): Uint8Array | null {
|
|
171
|
+
let start = Number(BigInt(ptr as bigint) >> 32n)
|
|
172
|
+
const generation = BigInt(ptr as bigint) & 0xFFFFFFFFn
|
|
173
|
+
let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!
|
|
174
|
+
const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]!)
|
|
175
|
+
if (diff !== 0n) return null
|
|
176
|
+
return this._view8.subarray(start + 12, start + this.HEADER_SIZE_BYTES + Number(length))
|
|
177
|
+
}
|
|
178
|
+
public label(): Array<ArenaLocation> {
|
|
179
|
+
const ptrArray = new Array<ArenaLocation>()
|
|
180
|
+
const limit = this._offset;
|
|
181
|
+
let pos = 0
|
|
182
|
+
while (pos < limit) {
|
|
183
|
+
const totalLength = this._view32[pos >> SHIFTOFFSETS.BYTE_32]!;
|
|
184
|
+
if (totalLength === 0) {
|
|
185
|
+
pos = (pos + 16) & ~15
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const isDeleted = this._view8[pos + HEADERS.DELETED_8] === 1
|
|
189
|
+
if (!isDeleted) {
|
|
190
|
+
ptrArray.push(((BigInt(pos) << 32n) | BigInt(this._view32[(pos >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation);
|
|
191
|
+
}
|
|
192
|
+
pos += totalLength
|
|
193
|
+
if (totalLength === 0) break;
|
|
194
|
+
}
|
|
195
|
+
return ptrArray
|
|
196
|
+
}
|
|
197
|
+
public getHeaders(ptr: ArenaLocation): ArenaHeaders {
|
|
198
|
+
let start = Number(BigInt(ptr as bigint) >> 32n)
|
|
199
|
+
return {
|
|
200
|
+
totalLength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32]!.toString()),
|
|
201
|
+
payloadlength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!.toString()),
|
|
202
|
+
deleted: this._view8[start + HEADERS.DELETED_8] === 1,
|
|
203
|
+
header0: Number(this._view8[start + HEADERS.USER_STATUS_0_8]!.toString()),
|
|
204
|
+
header1: Number(this._view8[start + HEADERS.USER_STATUS_1_8]!.toString()),
|
|
205
|
+
header2: Number(this._view8[start + HEADERS.USER_STATUS_2_8]!.toString()),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
public estimate(size: number, amnt: number): number {
|
|
209
|
+
return (((size + this._allignMask) & ~this._allignMask) + this.HEADER_SIZE_BYTES) * amnt
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
package/dist/arena.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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 _getBucketCount;
|
|
38
|
+
private _setBucketCount;
|
|
39
|
+
private _getBucketOffset;
|
|
40
|
+
private _setBucketOffset;
|
|
41
|
+
private _initBlock;
|
|
42
|
+
alloc(data: Uint8Array, headers?: ArenaCustomHeaders): ArenaLocation;
|
|
43
|
+
read(location: ArenaLocation): Uint8Array | null;
|
|
44
|
+
free(location: ArenaLocation): ArenaLocation;
|
|
45
|
+
private _checkForSpace;
|
|
46
|
+
private _resize;
|
|
47
|
+
size(): number;
|
|
48
|
+
getBuffer(): Uint8Array;
|
|
49
|
+
reserve(size: number): Uint8Array;
|
|
50
|
+
translate(ptr: ArenaLocation): {
|
|
51
|
+
start: number;
|
|
52
|
+
generation: bigint;
|
|
53
|
+
};
|
|
54
|
+
readWithHeaders(ptr: ArenaLocation): Uint8Array | null;
|
|
55
|
+
label(): Array<ArenaLocation>;
|
|
56
|
+
getHeaders(ptr: ArenaLocation): ArenaHeaders;
|
|
57
|
+
estimate(size: number, amnt: number): number;
|
|
58
|
+
}
|
package/dist/arena.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
var SHIFTOFFSETS;
|
|
2
|
+
(function (SHIFTOFFSETS) {
|
|
3
|
+
SHIFTOFFSETS[SHIFTOFFSETS["BYTE_8"] = 0] = "BYTE_8";
|
|
4
|
+
SHIFTOFFSETS[SHIFTOFFSETS["BYTE_16"] = 1] = "BYTE_16";
|
|
5
|
+
SHIFTOFFSETS[SHIFTOFFSETS["BYTE_32"] = 2] = "BYTE_32";
|
|
6
|
+
SHIFTOFFSETS[SHIFTOFFSETS["BYTE_64"] = 3] = "BYTE_64";
|
|
7
|
+
})(SHIFTOFFSETS || (SHIFTOFFSETS = {}));
|
|
8
|
+
var HEADERS;
|
|
9
|
+
(function (HEADERS) {
|
|
10
|
+
HEADERS[HEADERS["TOTAL_LENGTH_0_32"] = 0] = "TOTAL_LENGTH_0_32";
|
|
11
|
+
HEADERS[HEADERS["PAYLOAD_LENGTH_0_32"] = 1] = "PAYLOAD_LENGTH_0_32";
|
|
12
|
+
HEADERS[HEADERS["GENERATION_BYTE_0_32"] = 2] = "GENERATION_BYTE_0_32";
|
|
13
|
+
HEADERS[HEADERS["DELETED_8"] = 12] = "DELETED_8";
|
|
14
|
+
HEADERS[HEADERS["USER_STATUS_0_8"] = 13] = "USER_STATUS_0_8";
|
|
15
|
+
HEADERS[HEADERS["USER_STATUS_1_8"] = 14] = "USER_STATUS_1_8";
|
|
16
|
+
HEADERS[HEADERS["USER_STATUS_2_8"] = 15] = "USER_STATUS_2_8";
|
|
17
|
+
})(HEADERS || (HEADERS = {}));
|
|
18
|
+
export class Arena {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.HEADER_SIZE_BYTES = 16;
|
|
21
|
+
this._buffer = new ArrayBuffer(options?.initalSize || 64 * 1024);
|
|
22
|
+
this._view8 = new Uint8Array(this._buffer);
|
|
23
|
+
this._view32 = new Uint32Array(this._buffer);
|
|
24
|
+
this._offset = 0;
|
|
25
|
+
//@ts-ignore
|
|
26
|
+
this._allignMask = ((options?.allignment || 8) - 1);
|
|
27
|
+
this._allignShift = Math.log2(this._allignMask + 1);
|
|
28
|
+
this._bucketCapacities = options?.bucketCapacities || [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024];
|
|
29
|
+
this._bucketOffsets = [];
|
|
30
|
+
this._bucketcount = this._bucketCapacities.length;
|
|
31
|
+
let currentOffset = 0;
|
|
32
|
+
for (const cap of this._bucketCapacities) {
|
|
33
|
+
this._bucketOffsets.push(currentOffset);
|
|
34
|
+
currentOffset += (cap + 1);
|
|
35
|
+
}
|
|
36
|
+
this._emptySpots = new Uint32Array(currentOffset);
|
|
37
|
+
}
|
|
38
|
+
_getBucketCount(bucketIdx) {
|
|
39
|
+
return this._emptySpots[this._bucketOffsets[bucketIdx]];
|
|
40
|
+
}
|
|
41
|
+
_setBucketCount(bucketIdx, count) {
|
|
42
|
+
this._emptySpots[this._bucketOffsets[bucketIdx]] = count;
|
|
43
|
+
}
|
|
44
|
+
_getBucketOffset(bucketIdx, slotIdx) {
|
|
45
|
+
return this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1];
|
|
46
|
+
}
|
|
47
|
+
_setBucketOffset(bucketIdx, slotIdx, offset) {
|
|
48
|
+
this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1] = offset;
|
|
49
|
+
}
|
|
50
|
+
_initBlock(start, dataLength, { header0, header1, header2 }) {
|
|
51
|
+
const h0 = (header0 || 0) & 0xFF;
|
|
52
|
+
const h1 = (header1 || 0) & 0xFF;
|
|
53
|
+
const h2 = (header2 || 0) & 0xFF;
|
|
54
|
+
const shift32 = start >> SHIFTOFFSETS.BYTE_32;
|
|
55
|
+
this._view32[shift32 + HEADERS.TOTAL_LENGTH_0_32] = (dataLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
|
|
56
|
+
this._view32[shift32 + HEADERS.PAYLOAD_LENGTH_0_32] = dataLength;
|
|
57
|
+
this._view32[shift32 + (HEADERS.DELETED_8 >> SHIFTOFFSETS.BYTE_32)] = (h2 << 24) | (h1 << 16) | (h0 << 8) | 0;
|
|
58
|
+
}
|
|
59
|
+
alloc(data, headers) {
|
|
60
|
+
if (headers == undefined)
|
|
61
|
+
headers = { header0: 0, header1: 0, header2: 0 };
|
|
62
|
+
const totalBlockSize = (data.byteLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
|
|
63
|
+
const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
|
|
64
|
+
if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
|
|
65
|
+
const count = this._getBucketCount(bucketIdx);
|
|
66
|
+
if (count > 0) {
|
|
67
|
+
const recycledOffset = this._getBucketOffset(bucketIdx, count - 1);
|
|
68
|
+
this._setBucketCount(bucketIdx, count - 1);
|
|
69
|
+
this._initBlock(recycledOffset, data.length, headers);
|
|
70
|
+
return (BigInt(recycledOffset) << 32n | BigInt(this._view32[(recycledOffset >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const start = this._offset;
|
|
74
|
+
this._checkForSpace(data.byteLength + this.HEADER_SIZE_BYTES) && this._resize();
|
|
75
|
+
this._initBlock(start, data.byteLength, headers || {});
|
|
76
|
+
this._view8.set(data, start + this.HEADER_SIZE_BYTES);
|
|
77
|
+
this._offset = (data.byteLength + this.HEADER_SIZE_BYTES + this._offset + this._allignMask) & ~this._allignMask;
|
|
78
|
+
return (BigInt(start) << 32n | BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]));
|
|
79
|
+
}
|
|
80
|
+
read(location) {
|
|
81
|
+
let start = Number(BigInt(location) >> 32n);
|
|
82
|
+
const generation = BigInt(location) & 0xffffffffn;
|
|
83
|
+
let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32];
|
|
84
|
+
const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]);
|
|
85
|
+
if (diff !== 0n)
|
|
86
|
+
return null;
|
|
87
|
+
return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + Number(length));
|
|
88
|
+
}
|
|
89
|
+
free(location) {
|
|
90
|
+
let start = Number(BigInt(location) >> 32n);
|
|
91
|
+
const currgen = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32];
|
|
92
|
+
this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32] = currgen + 1; // this part can wrap around
|
|
93
|
+
this._view8[start + HEADERS.DELETED_8] = 1;
|
|
94
|
+
const totalBlockSize = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32];
|
|
95
|
+
const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
|
|
96
|
+
if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
|
|
97
|
+
const count = this._getBucketCount(bucketIdx);
|
|
98
|
+
const capacity = this._bucketCapacities[bucketIdx];
|
|
99
|
+
if (count < capacity) {
|
|
100
|
+
this._setBucketOffset(bucketIdx, count, start);
|
|
101
|
+
this._setBucketCount(bucketIdx, count + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return 0n;
|
|
105
|
+
}
|
|
106
|
+
_checkForSpace(size) {
|
|
107
|
+
if (this._buffer.byteLength >= this._offset + size)
|
|
108
|
+
return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
_resize() {
|
|
112
|
+
//@ts-ignore
|
|
113
|
+
this._buffer = this._buffer.transfer(this._buffer.byteLength * 2);
|
|
114
|
+
this._view8 = new Uint8Array(this._buffer);
|
|
115
|
+
this._view32 = new Uint32Array(this._buffer);
|
|
116
|
+
}
|
|
117
|
+
size() {
|
|
118
|
+
return this._buffer.byteLength;
|
|
119
|
+
}
|
|
120
|
+
getBuffer() {
|
|
121
|
+
return this._view8.subarray(0, this._offset);
|
|
122
|
+
}
|
|
123
|
+
reserve(size) {
|
|
124
|
+
const start = this._offset;
|
|
125
|
+
this._checkForSpace(start + this.HEADER_SIZE_BYTES + size + this._allignMask & ~this._allignMask) && this._resize();
|
|
126
|
+
this._initBlock(start, size, {});
|
|
127
|
+
this._offset = this._offset + size + this.HEADER_SIZE_BYTES + this._allignMask & ~this._allignMask;
|
|
128
|
+
return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + size);
|
|
129
|
+
}
|
|
130
|
+
translate(ptr) {
|
|
131
|
+
let start = Number(BigInt(ptr) >> 32n);
|
|
132
|
+
const generation = BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]);
|
|
133
|
+
return { start, generation };
|
|
134
|
+
}
|
|
135
|
+
readWithHeaders(ptr) {
|
|
136
|
+
let start = Number(BigInt(ptr) >> 32n);
|
|
137
|
+
const generation = BigInt(ptr) & 0xffffffffn;
|
|
138
|
+
let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32];
|
|
139
|
+
const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]);
|
|
140
|
+
if (diff !== 0n)
|
|
141
|
+
return null;
|
|
142
|
+
return this._view8.subarray(start + 12, start + this.HEADER_SIZE_BYTES + Number(length));
|
|
143
|
+
}
|
|
144
|
+
label() {
|
|
145
|
+
const ptrArray = new Array();
|
|
146
|
+
const limit = this._offset;
|
|
147
|
+
let pos = 0;
|
|
148
|
+
while (pos < limit) {
|
|
149
|
+
const totalLength = this._view32[pos >> SHIFTOFFSETS.BYTE_32];
|
|
150
|
+
if (totalLength === 0) {
|
|
151
|
+
pos = (pos + 16) & ~15;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const isDeleted = this._view8[pos + HEADERS.DELETED_8] === 1;
|
|
155
|
+
if (!isDeleted) {
|
|
156
|
+
ptrArray.push(((BigInt(pos) << 32n) | BigInt(this._view32[(pos >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32])));
|
|
157
|
+
}
|
|
158
|
+
pos += totalLength;
|
|
159
|
+
if (totalLength === 0)
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
return ptrArray;
|
|
163
|
+
}
|
|
164
|
+
getHeaders(ptr) {
|
|
165
|
+
let start = Number(BigInt(ptr) >> 32n);
|
|
166
|
+
return {
|
|
167
|
+
totalLength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32].toString()),
|
|
168
|
+
payloadlength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32].toString()),
|
|
169
|
+
deleted: this._view8[start + HEADERS.DELETED_8] === 1,
|
|
170
|
+
header0: Number(this._view8[start + HEADERS.USER_STATUS_0_8].toString()),
|
|
171
|
+
header1: Number(this._view8[start + HEADERS.USER_STATUS_1_8].toString()),
|
|
172
|
+
header2: Number(this._view8[start + HEADERS.USER_STATUS_2_8].toString()),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
estimate(size, amnt) {
|
|
176
|
+
return (((size + this._allignMask) & ~this._allignMask) + this.HEADER_SIZE_BYTES) * amnt;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bumparena",
|
|
3
|
+
"version":"0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/arena.js",
|
|
7
|
+
"module": "dist/arena.js",
|
|
8
|
+
"types": "dist/arena.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "node ./dist/arena.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^25.2.3"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/test/test.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Arena, ArenaCustomHeaders } from "../arena.ts"
|
|
2
|
+
|
|
3
|
+
//Tests
|
|
4
|
+
function Test() {
|
|
5
|
+
console.log("running Tests")
|
|
6
|
+
const a = new Arena()
|
|
7
|
+
const testdata = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
|
|
8
|
+
const headers = { header0: 42, header1: 24, header2: 240 };
|
|
9
|
+
_testCheckPtrAccess(a, testdata, headers)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function _testCheckPtrAccess(a: Arena, testdata: Uint8Array, headers: ArenaCustomHeaders) {
|
|
13
|
+
// USE-AFTER-FREE
|
|
14
|
+
const useAfterFreePtr = a.alloc(testdata, headers)
|
|
15
|
+
a.free(useAfterFreePtr)
|
|
16
|
+
// free Slot reuse
|
|
17
|
+
const ptr = a.alloc(testdata, headers)
|
|
18
|
+
const res = a.read(ptr)
|
|
19
|
+
if (res == null) throw new Error(`${testdata} Could not read data`)
|
|
20
|
+
const resultheaders = a.getHeaders(ptr)
|
|
21
|
+
|
|
22
|
+
if (resultheaders.totalLength !== testdata.byteLength + 16) throw new Error(`totalLength not right: got: ${resultheaders.totalLength} needed: ${testdata.byteLength}`)
|
|
23
|
+
if (resultheaders.payloadlength !== testdata.byteLength) throw new Error(`payloadLength not right: got: ${resultheaders.payloadlength} needed: ${testdata.byteLength}`)
|
|
24
|
+
if (resultheaders.header0 !== headers.header0) throw new Error(`header0: got: ${resultheaders.header0} needed: ${headers.header0}`)
|
|
25
|
+
if (resultheaders.header1 !== headers.header1) throw new Error(`header1: got: ${resultheaders.header1} needed: ${headers.header1}`)
|
|
26
|
+
if (resultheaders.header2 !== headers.header2) throw new Error(`header2: got: ${resultheaders.header2} needed: ${headers.header2}`)
|
|
27
|
+
|
|
28
|
+
if (res.toString() !== testdata.toString()) {
|
|
29
|
+
throw new Error(`${testdata} had a problem with data integrety`)
|
|
30
|
+
}
|
|
31
|
+
//final use after free with Generation this should never be possible
|
|
32
|
+
if (a.read(useAfterFreePtr) !== null) {
|
|
33
|
+
throw new Error(`Use after Free is Possible, this is a big Problem`)
|
|
34
|
+
}
|
|
35
|
+
console.log("CheckPtrAccess Done!")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Test()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ES2020"],
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"noImplicitOverride": true,
|
|
14
|
+
|
|
15
|
+
// für Build / .d.ts:
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"emitDeclarationOnly": false,
|
|
18
|
+
"outDir": "dist"
|
|
19
|
+
},
|
|
20
|
+
"include": ["arena.ts"]
|
|
21
|
+
}
|