@daneren2005/shared-memory-objects 0.0.0 → 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.
@@ -0,0 +1,149 @@
1
+ import AllocatedMemory from './allocated-memory';
2
+ import prettyBytes from 'pretty-bytes';
3
+ import { MAX_BYTE_OFFSET_LENGTH, MAX_POSITION_LENGTH } from './utils/pointer';
4
+ import MemoryBuffer from './memory-buffer';
5
+
6
+ // TODO: Once we are certain this behaves correctly we should probably up to something like 1MB - we will have a ton of entities so don't want to waste time allocating new buffers constantly
7
+ const DEFAULT_BUFFER_SIZE = 8_192;
8
+ export default class MemoryHeap {
9
+ buffers: Array<MemoryBuffer>;
10
+ onGrowBufferHandlers: Array<OnGrowBuffer> = [];
11
+ isClone: boolean;
12
+ private memory: AllocatedMemory;
13
+
14
+ get bufferSize() {
15
+ return this.memory.data[0];
16
+ }
17
+
18
+ constructor(config?: MemoryHeapConfig | MemoryHeapMemory) {
19
+ if(config && 'buffers' in config) {
20
+ this.buffers = config.buffers.map(buffer => {
21
+ return new MemoryBuffer({
22
+ buf: buffer,
23
+ skipInitialization: true
24
+ });
25
+ });
26
+
27
+ // TODO: This should be programic instead of hoping the first allocation is always byte 40
28
+ this.memory = new AllocatedMemory(this, {
29
+ bufferPosition: 0,
30
+ bufferByteOffset: 40
31
+ });
32
+ this.isClone = true;
33
+ } else {
34
+ const bufferSize = config?.bufferSize ?? DEFAULT_BUFFER_SIZE;
35
+ if(bufferSize > MAX_BYTE_OFFSET_LENGTH) {
36
+ throw new Error(`Buffer size ${bufferSize} is greater than max ${MAX_BYTE_OFFSET_LENGTH} that we can reference with pointers`);
37
+ }
38
+
39
+ let startBuffer = this.createBuffer(bufferSize);
40
+ this.buffers = [
41
+ startBuffer
42
+ ];
43
+ const data = startBuffer.callocAs('u32', 1);
44
+ if(data) {
45
+ this.memory = new AllocatedMemory(this, {
46
+ bufferPosition: 0,
47
+ bufferByteOffset: data.byteOffset
48
+ });
49
+ } else {
50
+ throw new Error('Failed to initialize first byte from buffer');
51
+ }
52
+ this.memory.data[0] = bufferSize;
53
+ this.isClone = false;
54
+
55
+ for(let i = 1; i < (config?.initialBuffers ?? 1); i++) {
56
+ this.buffers.push(this.createBuffer(bufferSize));
57
+ }
58
+ }
59
+ }
60
+
61
+ addSharedBuffer(buffer: SharedArrayBuffer) {
62
+ this.buffers.push(new MemoryBuffer({
63
+ buf: buffer,
64
+ skipInitialization: true
65
+ }));
66
+ }
67
+
68
+ private createBuffer(bufferSize?: number): MemoryBuffer {
69
+ if(this.isClone) {
70
+ throw new Error('Creating new buffer from worker threads not currently supported');
71
+ }
72
+
73
+ // TODO: Look into if we should turn off splitting - I think memory is going to get fragmented really quick if we free an Entity with 100 bytes and re-allocate a new one with 80 bytes and just lose the rest
74
+ // As we add stuff like ListMemory that does tons of small allocations that might be fine since they can fill in any small space we have
75
+ return new MemoryBuffer({
76
+ buf: new SharedArrayBuffer(bufferSize ?? this.bufferSize)
77
+ });
78
+ }
79
+
80
+ allocUI32(count: number): AllocatedMemory {
81
+ for(let i = 0; i < this.buffers.length; i++) {
82
+ const buffer = this.buffers[i];
83
+ // Should be fine to initialize all values as 0s since unsigned/signed ints and floats all store 0 as all 0s
84
+ const data = buffer.callocAs('u32', count);
85
+ if(data) {
86
+ return new AllocatedMemory(this, {
87
+ data,
88
+ buffer
89
+ });
90
+ }
91
+ }
92
+
93
+ if(this.buffers.length >= MAX_POSITION_LENGTH) {
94
+ throw new Error(`Can't initialize a new buffer since it would have a position greater than the max of ${MAX_POSITION_LENGTH}`);
95
+ }
96
+
97
+ // If we get here we need to grow another buffer to continue allocating new memory
98
+ const buffer = this.createBuffer();
99
+ this.buffers.push(buffer);
100
+ this.onGrowBufferHandlers.forEach(handler => handler(buffer.buf as SharedArrayBuffer));
101
+
102
+ const data = buffer.callocAs('u32', count);
103
+ if(data) {
104
+ return new AllocatedMemory(this, {
105
+ data,
106
+ buffer
107
+ });
108
+ } else {
109
+ throw new Error(`Unable to allocate ${count} numbers even after adding a new buffer`);
110
+ }
111
+ }
112
+
113
+ get currentUsed() {
114
+ return this.totalAllocated - this.buffers.reduce((total, memPool) => total + memPool.stats().available, 0);
115
+ }
116
+ get totalAllocated() {
117
+ return this.buffers[0].buf.byteLength * this.buffers.length;
118
+ }
119
+
120
+ prettyMemory() {
121
+ return `${myPrettyBytes(this.currentUsed)} / ${myPrettyBytes(this.totalAllocated)}`;
122
+ }
123
+
124
+ getSharedMemory(): MemoryHeapMemory {
125
+ return {
126
+ buffers: this.buffers.map(buffer => buffer.buf as SharedArrayBuffer)
127
+ };
128
+ }
129
+ }
130
+
131
+ function myPrettyBytes(bytes: number) {
132
+ return prettyBytes(bytes, {
133
+ binary: true,
134
+ minimumFractionDigits: 1,
135
+ maximumFractionDigits: 1
136
+ });
137
+ }
138
+
139
+ type OnGrowBuffer = (newBuffer: SharedArrayBuffer) => void;
140
+
141
+ interface MemoryHeapConfig {
142
+ bufferSize?: number
143
+ initialBuffers?: number
144
+ }
145
+ interface MemoryHeapMemory {
146
+ buffers: Array<SharedArrayBuffer>
147
+ }
148
+
149
+ export type { MemoryHeapConfig, MemoryHeapMemory };
@@ -0,0 +1,95 @@
1
+ import AllocatedMemory from './allocated-memory';
2
+ import type MemoryHeap from './memory-heap';
3
+ import { getPointer } from './utils/pointer';
4
+
5
+ enum VALUE_TYPE {
6
+ UNDEFINED,
7
+ NUMBER,
8
+ STRING
9
+ }
10
+
11
+ // Be able to serialize a simple object into memory and re-create the object in another thread
12
+ // This does NOT support updating objects across threads
13
+ const MAGIC_NUMBER = 52361700;
14
+ export function serializeObjectToMemory<T extends object>(heap: MemoryHeap, object: T): AllocatedMemory {
15
+ const data: Array<number> = [];
16
+ const keys = Object.keys(object);
17
+ data.push(MAGIC_NUMBER);
18
+ data.push(keys.length);
19
+
20
+ // Create index first so we can quickly construct index
21
+ let keyIndexes: { [key:string]:number } = {};
22
+ keys.forEach(key => {
23
+ data.push(key.length);
24
+ addCharCodes(data, key);
25
+
26
+ keyIndexes[key] = data.length;
27
+ data.push(0);
28
+ });
29
+
30
+ keys.forEach(key => {
31
+ let keyIndex = keyIndexes[key];
32
+ // @ts-expect-error
33
+ let value = object[key];
34
+ if(Number.isFinite(value)) {
35
+ data[keyIndex] = data.length;
36
+ data.push(VALUE_TYPE.NUMBER);
37
+ data.push(value);
38
+ } else if(typeof value === 'string') {
39
+ data[keyIndex] = data.length;
40
+ data.push(VALUE_TYPE.STRING);
41
+ data.push(value.length);
42
+ addCharCodes(data, value);
43
+ }
44
+ });
45
+
46
+ let memory = heap.allocUI32(data.length);
47
+ let sharedData = memory.getArray(Float32Array, 0, data.length);
48
+ sharedData.set(data);
49
+
50
+ return memory;
51
+ }
52
+
53
+ export function createObjectFromPointer<T extends object>(heap: MemoryHeap, pointer: number) {
54
+ let memory = new AllocatedMemory(heap, getPointer(pointer));
55
+ return createObjectFromMemory<T>(memory);
56
+ }
57
+ export function createObjectFromMemory<T extends object>(memory: AllocatedMemory): T {
58
+ let sharedData = new Float32Array(memory.data.buffer, memory.bufferByteOffset);
59
+ if(sharedData[0] !== MAGIC_NUMBER) {
60
+ throw new Error('Trying to create object from invalid memory location');
61
+ }
62
+
63
+ const sharedObject: { [key:string]:any } = {};
64
+ const keyCount = sharedData[1];
65
+ let keyStartIndex = 2;
66
+ for(let i = 0; i < keyCount; i++) {
67
+ let keyLength = sharedData[keyStartIndex];
68
+ let keyData = new Float32Array(memory.data.buffer, memory.bufferByteOffset + (keyStartIndex + 1) * sharedData.BYTES_PER_ELEMENT, keyLength);
69
+ let key = String.fromCharCode.apply(null, [...keyData]);
70
+ let valueIndex = sharedData[keyStartIndex + keyLength + 1];
71
+ let valueType = sharedData[valueIndex];
72
+ switch(valueType) {
73
+ case VALUE_TYPE.NUMBER: {
74
+ sharedObject[key] = sharedData[valueIndex + 1];
75
+ break;
76
+ }
77
+ case VALUE_TYPE.STRING: {
78
+ let length = sharedData[valueIndex + 1];
79
+ let valueData = new Float32Array(memory.data.buffer, memory.bufferByteOffset + (valueIndex + 2) * sharedData.BYTES_PER_ELEMENT, length);
80
+ sharedObject[key] = String.fromCharCode.apply(null, [...valueData]);
81
+ break;
82
+ }
83
+ }
84
+
85
+ keyStartIndex += 2 + keyLength;
86
+ }
87
+
88
+ return sharedObject as T;
89
+ }
90
+
91
+ function addCharCodes(data: Array<number>, value: string) {
92
+ for(let i = 0; i < value.length; i++) {
93
+ data.push(value.charCodeAt(i));
94
+ }
95
+ }
@@ -0,0 +1,274 @@
1
+ import type { SharedAllocatedMemory } from './allocated-memory';
2
+ import AllocatedMemory from './allocated-memory';
3
+ import type { TypedArrayConstructor } from './interfaces/typed-array-constructor';
4
+ import type MemoryHeap from './memory-heap';
5
+ import { getPointer, loadPointer, loadRawPointer, replaceRawPointer, storePointer, storeRawPointer } from './utils/pointer';
6
+
7
+ enum TYPE {
8
+ uint32,
9
+ int32,
10
+ float32
11
+ }
12
+
13
+ // TODO: We need some sort of locking on insert/deletes!
14
+ const FIRST_BLOCK_RECORD_KEEPING_COUNT = 4;
15
+ const DATA_BLOCK_RECORD_KEEPING_COUNT = 1;
16
+ const LENGTH_INDEX = 2;
17
+ export default class SharedList<T extends Uint32Array | Int32Array | Float32Array = Uint32Array> implements Iterable<SharedListIterable<T>> {
18
+ static readonly ALLOCATE_COUNT = FIRST_BLOCK_RECORD_KEEPING_COUNT;
19
+
20
+ private memory: MemoryHeap;
21
+ /* First block
22
+ 32 index 0
23
+ uint16 0 - next buffer position
24
+ uint16 1 - next buffer index
25
+ 32 index 1
26
+ uint16 2 - last buffer position
27
+ uint16 3 - last buffer index
28
+ 32 index 2
29
+ uint32 4 - length
30
+ 32 index 3
31
+ uint16 6 - type
32
+ uint16 7 - data length (defaults to 1 number per data)
33
+ */
34
+ /* Other blocks
35
+ 32 index 0
36
+ uint16 0 - next buffer position
37
+ uint16 1 - next buffer index
38
+ 32 index 1 => data
39
+ */
40
+ private firstBlock: AllocatedMemory;
41
+ private uint16Array: Uint16Array;
42
+
43
+ get length(): number {
44
+ return Atomics.load(this.firstBlock.data, LENGTH_INDEX);
45
+ }
46
+
47
+ get type(): number {
48
+ return Atomics.load(this.uint16Array, 0);
49
+ }
50
+ private set type(value: number) {
51
+ Atomics.store(this.uint16Array, 0, value);
52
+ }
53
+ get dataLength(): number {
54
+ // Can technically be initialized by passing memory without actually every being called - need to make sure dataLength is always at least one
55
+ return Math.max(1, Atomics.load(this.uint16Array, 1));
56
+ }
57
+ private set dataLength(value: number) {
58
+ Atomics.store(this.uint16Array, 1, value);
59
+ }
60
+
61
+ constructor(memory: MemoryHeap, config?: SharedListConfig<T> | SharedListMemory) {
62
+ this.memory = memory;
63
+
64
+ if(config && 'firstBlock' in config) {
65
+ this.firstBlock = new AllocatedMemory(memory, config.firstBlock);
66
+ this.uint16Array = new Uint16Array(this.firstBlock.data.buffer, this.firstBlock.bufferByteOffset + (LENGTH_INDEX + 1) * Uint32Array.BYTES_PER_ELEMENT, 2);
67
+ } else {
68
+ if(config && config.initWithBlock) {
69
+ this.firstBlock = new AllocatedMemory(memory, config.initWithBlock);
70
+ } else {
71
+ this.firstBlock = memory.allocUI32(FIRST_BLOCK_RECORD_KEEPING_COUNT);
72
+ }
73
+ this.uint16Array = new Uint16Array(this.firstBlock.data.buffer, this.firstBlock.bufferByteOffset + (LENGTH_INDEX + 1) * Uint32Array.BYTES_PER_ELEMENT, 2);
74
+
75
+ const type = config?.type ?? Uint32Array;
76
+ if(type === Uint32Array) {
77
+ this.type = TYPE.uint32;
78
+ }
79
+ // @ts-expect-error
80
+ else if(type === Int32Array) {
81
+ this.type = TYPE.int32;
82
+ }
83
+ // @ts-expect-error
84
+ else if(type === Float32Array) {
85
+ this.type = TYPE.float32;
86
+ }
87
+ this.dataLength = config?.dataLength ?? 1;
88
+ }
89
+ }
90
+
91
+ insert(values: number | Array<number>) {
92
+ if(typeof values === 'number') {
93
+ values = [values];
94
+ }
95
+
96
+ let dataLength = this.dataLength;
97
+ if(values.length > dataLength) {
98
+ throw new Error(`Can't insert ${values.length} array into shared list of ${dataLength} dataLength`);
99
+ }
100
+ let newBlock = this.memory.allocUI32(DATA_BLOCK_RECORD_KEEPING_COUNT + dataLength);
101
+ let newData = this.getDataBlock(newBlock.data);
102
+ let newBlockPointer = newBlock.pointer;
103
+
104
+ for(let i = 0; i < values.length; i++) {
105
+ if(newData instanceof Int32Array || newData instanceof Uint32Array) {
106
+ Atomics.store(newData, i, values[i]);
107
+ } else {
108
+ // TODO: Should we replace with pass thru float32 conversion -> store?
109
+ newData[i] = values[i];
110
+ }
111
+ }
112
+
113
+ let lastBlockPointer;
114
+ let updateWorked = false;
115
+ while(!updateWorked) {
116
+ lastBlockPointer = loadRawPointer(this.firstBlock.data, 1);
117
+ updateWorked = replaceRawPointer(this.firstBlock.data, 1, newBlockPointer, lastBlockPointer);
118
+ }
119
+
120
+ if(lastBlockPointer) {
121
+ let { bufferPosition: lastBlockPosition, bufferByteOffset: lastBlockByteOffset } = getPointer(lastBlockPointer);
122
+ let lastBlock = new Uint32Array(this.memory.buffers[lastBlockPosition].buf, lastBlockByteOffset, 1);
123
+ storeRawPointer(lastBlock, 0, newBlockPointer);
124
+ } else {
125
+ // First item - store on first block
126
+ storeRawPointer(this.firstBlock.data, 0, newBlockPointer);
127
+ }
128
+
129
+ // Always update new last buffer position and length
130
+ Atomics.add(this.firstBlock.data, LENGTH_INDEX, 1);
131
+ }
132
+
133
+ deleteMatch(callback: (values: T, index: number) => boolean): boolean {
134
+ for(let { data, index, deleteCurrent } of this) {
135
+ if(callback(data, index)) {
136
+ deleteCurrent();
137
+ return true;
138
+ }
139
+ }
140
+
141
+ return false;
142
+ }
143
+ deleteIndex(deleteIndex: number): boolean {
144
+ if(deleteIndex >= this.length || deleteIndex < 0) {
145
+ return false;
146
+ }
147
+
148
+ return this.deleteMatch((values, index) => index === deleteIndex);
149
+ }
150
+ deleteValue(deleteValues: number | Array<number>) {
151
+ if(typeof deleteValues === 'number') {
152
+ return this.deleteMatch(values => values[0] === deleteValues);
153
+ } else {
154
+ return this.deleteMatch(values => {
155
+ if(values.length !== deleteValues.length) {
156
+ return false;
157
+ } else {
158
+ for(let i = 0; i < values.length; i++) {
159
+ if(values[i] !== deleteValues[i]) {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ return true;
165
+ }
166
+ });
167
+ }
168
+ }
169
+
170
+ *[Symbol.iterator]() {
171
+ let currentIndex = 0;
172
+ let { bufferPosition: nextBlockPosition, bufferByteOffset: nextBlockByteOffset } = loadPointer(this.firstBlock.data, 0);
173
+ let lastBlockData = this.firstBlock.data;
174
+ let lastBlockPosition = 0;
175
+ let lastBlockByteOffset = 0;
176
+ while(nextBlockByteOffset) {
177
+ let memPool = this.memory.buffers[nextBlockPosition];
178
+ let blockRecord = new Uint32Array(memPool.buf, nextBlockByteOffset, 2);
179
+ let blockData = this.getDataBlock(blockRecord);
180
+
181
+ let currentBlockPosition = nextBlockPosition;
182
+ let currentBlockByteOffset = nextBlockByteOffset;
183
+ ({ bufferPosition: nextBlockPosition, bufferByteOffset: nextBlockByteOffset } = loadPointer(blockRecord, 0));
184
+
185
+ let updateLastBlock = true;
186
+ yield {
187
+ data: blockData,
188
+ index: currentIndex,
189
+ deleteCurrent: () => {
190
+ // Move previous index to point to one after
191
+ storePointer(lastBlockData, 0, nextBlockPosition, nextBlockByteOffset);
192
+
193
+ // If this is the last item, update last block to be previous location
194
+ if(!nextBlockByteOffset) {
195
+ storePointer(this.firstBlock.data, 1, lastBlockPosition, lastBlockByteOffset);
196
+ }
197
+
198
+ memPool.free(blockRecord.byteOffset);
199
+ Atomics.sub(this.firstBlock.data, LENGTH_INDEX, 1);
200
+ updateLastBlock = false;
201
+ }
202
+ };
203
+
204
+ if(updateLastBlock) {
205
+ lastBlockData = blockRecord;
206
+ lastBlockPosition = currentBlockPosition;
207
+ lastBlockByteOffset = currentBlockByteOffset;
208
+ currentIndex++;
209
+ }
210
+ }
211
+ }
212
+
213
+ forEach(callback: (data: T) => void) {
214
+ for(let value of this) {
215
+ callback(value.data);
216
+ }
217
+ }
218
+
219
+ getSharedMemory(): SharedListMemory {
220
+ return {
221
+ firstBlock: this.firstBlock.getSharedMemory()
222
+ };
223
+ }
224
+
225
+ private getDataBlock(memory: Uint32Array): T {
226
+ const startIndex = memory.byteOffset + DATA_BLOCK_RECORD_KEEPING_COUNT * memory.BYTES_PER_ELEMENT;
227
+
228
+ switch(this.type) {
229
+ case TYPE.int32:
230
+ // @ts-expect-error
231
+ return new Int32Array(memory.buffer, startIndex, this.dataLength);
232
+ case TYPE.uint32:
233
+ // @ts-expect-error
234
+ return new Uint32Array(memory.buffer, startIndex, this.dataLength);
235
+ case TYPE.float32:
236
+ // @ts-expect-error
237
+ return new Float32Array(memory.buffer, startIndex, this.dataLength);
238
+ default:
239
+ throw new Error(`Unknown data block type ${this.type}`);
240
+ }
241
+ }
242
+
243
+ free() {
244
+ let { bufferPosition: nextBlockPosition, bufferByteOffset: nextBlockByteOffset } = loadPointer(this.firstBlock.data, 0);
245
+ while(nextBlockByteOffset) {
246
+ let allocatedMemory = new AllocatedMemory(this.memory, {
247
+ bufferPosition: nextBlockPosition,
248
+ bufferByteOffset: nextBlockByteOffset
249
+ });
250
+
251
+ ({ bufferPosition: nextBlockPosition, bufferByteOffset: nextBlockByteOffset } = loadPointer(allocatedMemory.data, 0));
252
+ allocatedMemory.free();
253
+ }
254
+
255
+ this.firstBlock.free();
256
+ }
257
+ }
258
+
259
+ interface SharedListConfig<T extends Uint32Array | Int32Array | Float32Array> {
260
+ initWithBlock?: SharedAllocatedMemory
261
+ type?: TypedArrayConstructor<T>
262
+ dataLength?: number
263
+ }
264
+ interface SharedListMemory {
265
+ firstBlock: SharedAllocatedMemory
266
+ }
267
+
268
+ interface SharedListIterable<T extends Uint32Array | Int32Array | Float32Array> {
269
+ data: T
270
+ index: number
271
+ deleteCurrent: () => void
272
+ }
273
+
274
+ export { type SharedListMemory };