@daneren2005/shared-memory-objects 0.0.2 → 0.0.4

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,6 +1,9 @@
1
1
  # Shared Memory Objects
2
2
  A library to try to make making a multi-threaded game in Javascript possible. This package is to provide a wrapper to create objects and data structures that are backed by a SharedArrayBuffer and can be shared between multiple threads. The end result is a package that has all of the slowness of Javascript with all of the baggage of dealing with manual memory allocations. If you need to multi-thread you are probably better of just using a different language and compiling to WebAssembly. But if you, like me, just want to use Javascript/Typescript and are willing to deal with dealing with manual memory allocations then this library could save you some time.
3
3
 
4
+ A demo can be found at https://daneren2005.github.io/ecs-sharedarraybuffer-playground/#/shared-memory-objects
5
+ The code is at https://github.com/daneren2005/ecs-sharedarraybuffer-playground/tree/dev/src/shared-memory-objects
6
+
4
7
  ## Basics
5
8
  The core of this package is the MemoryHeap. You should usually just have a single heap that is shared between all of your different threads. Each heap can have multiple MemoryBuffers. By default each buffer is only 8KB but it can be configured up to 1MB, and you can have up to 4k buffers for a total of 4GB. When you allocate memory, if there is not enough space it will allocate another buffers automatically. When allocating memory, you will get a AllocatedMemory object that is a wrapper around the allocated memory by calling `heap.allocUI32({count of 32 bit numbers})`. By default AllocatedMemory is backed by a Uint32Array but you can get any type of array from `AllocatedMemory.getArray(Int32Array);`.
6
9
 
@@ -8,6 +11,50 @@ Each allocated memory location can be stored as an int pointer. You can use `ge
8
11
 
9
12
  When passing memory to another thread you can either pass a pointer or a serialized version of the buffer position/byte offset in order to re-create the object in the other thread.
10
13
 
14
+ ## Getting Started
15
+ `npm install @daneren2005/shared-memory-objects`
16
+
17
+ Example to update blocks of memory from a thread.
18
+ ```
19
+ let heap = new MemoryHeap();
20
+ let memory = heap.allocUI32(4);
21
+
22
+ // Pass memory to another thread
23
+ thread.postMessage({
24
+ heap: heap.getSharedMemory(),
25
+ memory: memory.getSharedMemory()
26
+ });
27
+
28
+ // From worker thread re-construct memory and change it
29
+ self.onmessage = (e) => {
30
+ let heap = new MemoryHeap(e.data.heap);
31
+ let memory = new AllocatedMemory(heap, e.data.memory);
32
+ memory.data[2] = 5;
33
+ };
34
+ ```
35
+
36
+ // Example to work with data structures from a thread. When constructing a new structure you just pass the heap. When re-creating a structure from an already initialized memory location pass the heap and the shared memory location for it.
37
+ ```
38
+ let heap = new MemoryHeap();
39
+ let list = new SharedList(heap);
40
+
41
+ // Pass memory to another thread
42
+ thread.postMessage({
43
+ heap: heap.getSharedMemory(),
44
+ list: list.getSharedMemory()
45
+ });
46
+
47
+ // From worker thread re-construct memory and change it
48
+ self.onmessage = (e) => {
49
+ let heap = new MemoryHeap(e.data.heap);
50
+ let list = new SharedList(heap, e.data.list);
51
+
52
+ list.push(5);
53
+ };
54
+ ```
55
+ let mainList = new SharedList(memory);
56
+ let secondList = new SharedList(memory, mainList.getSharedMemory());
57
+
11
58
  ## Data Structures
12
59
  - SharedList
13
60
  - SharedVector
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daneren2005/shared-memory-objects",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "author": "daneren2005@gmail.com",
5
5
  "description": "Creating objects with a SharedArrayBuffer",
6
6
  "homepage": "https://github.com/daneren2005/shared-memory-objects#readme",
@@ -34,7 +34,8 @@
34
34
  "test:ui": "vitest --ui --api 9527",
35
35
  "test:unit": "vitest run",
36
36
  "test:unit:watch": "vitest",
37
- "type-check": "tsc --build --force"
37
+ "type-check": "tsc --build --force",
38
+ "prepare": "husky"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@rushstack/eslint-patch": "^1.10.1",
package/src/main.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import AllocatedMemory, { type SharedAllocatedMemory } from './allocated-memory';
2
2
  import MemoryBuffer from './memory-buffer';
3
- import MemoryHeap, { type MemoryHeapMemory } from './memory-heap';
3
+ import MemoryHeap, { type MemoryHeapMemory, type GrowBufferData } from './memory-heap';
4
4
 
5
5
  import SharedList, { type SharedListMemory } from './shared-list';
6
- import SharedMap from './shared-map';
6
+ import SharedMap, { type SharedMapMemory } from './shared-map';
7
7
  import SharedPointerList from './shared-pointer-list';
8
8
  import SharedString from './shared-string';
9
- import SharedVector from './shared-vector';
9
+ import SharedVector, { type SharedVectorMemory } from './shared-vector';
10
10
 
11
- import type { TypedArrayConstructor } from './interfaces/typed-array-constructor';
11
+ export * from './interfaces/typed-array';
12
+ export * from './interfaces/typed-array-constructor';
12
13
 
13
14
  export * from './utils/16-from-32-array';
14
15
  export * from './utils/16-from-64-array';
@@ -23,13 +24,14 @@ export {
23
24
  MemoryBuffer,
24
25
  MemoryHeap,
25
26
  type MemoryHeapMemory,
27
+ type GrowBufferData,
26
28
 
27
29
  SharedList,
28
30
  type SharedListMemory,
29
31
  SharedMap,
32
+ type SharedMapMemory,
30
33
  SharedPointerList,
31
34
  SharedString,
32
35
  SharedVector,
33
-
34
- type TypedArrayConstructor
36
+ type SharedVectorMemory
35
37
  };
@@ -265,7 +265,7 @@ export default class MemoryBuffer {
265
265
  let block = this._used;
266
266
  while(block) {
267
267
  if(block === addr) {
268
- return this.blockSize(addr);
268
+ return this.blockSize(addr) - SIZEOF_MEM_BLOCK;
269
269
  }
270
270
  block = this.blockNext(block);
271
271
  }
@@ -1,18 +1,19 @@
1
- import AllocatedMemory from './allocated-memory';
1
+ import AllocatedMemory, { type SharedAllocatedMemory } from './allocated-memory';
2
2
  import prettyBytes from 'pretty-bytes';
3
3
  import { MAX_BYTE_OFFSET_LENGTH, MAX_POSITION_LENGTH } from './utils/pointer';
4
4
  import MemoryBuffer from './memory-buffer';
5
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
6
  const DEFAULT_BUFFER_SIZE = 8_192;
7
+ const BUFFER_SIZE_INDEX = 0;
8
+ const BUFFER_COUNT_INDEX = 1;
8
9
  export default class MemoryHeap {
9
10
  buffers: Array<MemoryBuffer>;
10
- onGrowBufferHandlers: Array<OnGrowBuffer> = [];
11
+ private onGrowBufferHandlers: Array<OnGrowBuffer> = [];
11
12
  isClone: boolean;
12
13
  private memory: AllocatedMemory;
13
14
 
14
15
  get bufferSize() {
15
- return this.memory.data[0];
16
+ return this.memory.data[BUFFER_SIZE_INDEX];
16
17
  }
17
18
 
18
19
  constructor(config?: MemoryHeapConfig | MemoryHeapMemory) {
@@ -40,7 +41,7 @@ export default class MemoryHeap {
40
41
  this.buffers = [
41
42
  startBuffer
42
43
  ];
43
- const data = startBuffer.callocAs('u32', 1);
44
+ const data = startBuffer.callocAs('u32', 2);
44
45
  if(data) {
45
46
  this.memory = new AllocatedMemory(this, {
46
47
  bufferPosition: 0,
@@ -49,7 +50,8 @@ export default class MemoryHeap {
49
50
  } else {
50
51
  throw new Error('Failed to initialize first byte from buffer');
51
52
  }
52
- this.memory.data[0] = bufferSize;
53
+ this.memory.data[BUFFER_SIZE_INDEX] = bufferSize;
54
+ this.memory.data[BUFFER_COUNT_INDEX] = 1;
53
55
  this.isClone = false;
54
56
 
55
57
  for(let i = 1; i < (config?.initialBuffers ?? 1); i++) {
@@ -58,28 +60,37 @@ export default class MemoryHeap {
58
60
  }
59
61
  }
60
62
 
61
- addSharedBuffer(buffer: SharedArrayBuffer) {
62
- this.buffers.push(new MemoryBuffer({
63
- buf: buffer,
63
+ addSharedBuffer(data: GrowBufferData) {
64
+ this.buffers[data.bufferPosition] = new MemoryBuffer({
65
+ buf: data.buffer,
64
66
  skipInitialization: true
65
- }));
67
+ });
66
68
  }
67
69
 
68
70
  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
71
  return new MemoryBuffer({
76
- buf: new SharedArrayBuffer(bufferSize ?? this.bufferSize)
72
+ buf: new SharedArrayBuffer(bufferSize ?? this.bufferSize),
73
+
74
+ // We can't use this unless we can 100% guarantee that every thread will stop using memory the instant it is freed
75
+ // ex: Allocate 16 bytes. Thread A frees that allocation and then allocates 12 bytes and 4 bytes, but Thread B is mid-execution on the old allocation can changes the internal state of the 4-byte allocation breaking everything
76
+ // After the internal state is wrong MemoryBuffer will loose track of which blocks are where and how big they are
77
+ compact: false,
78
+ split: false
77
79
  });
78
80
  }
79
81
 
82
+ addOnGrowBufferHandlers(handler: OnGrowBuffer) {
83
+ this.onGrowBufferHandlers.push(handler);
84
+ }
85
+
80
86
  allocUI32(count: number): AllocatedMemory {
81
87
  for(let i = 0; i < this.buffers.length; i++) {
82
88
  const buffer = this.buffers[i];
89
+ // Should just mean we haven't synced this buffer from another thread yet
90
+ if(!buffer) {
91
+ continue;
92
+ }
93
+
83
94
  // Should be fine to initialize all values as 0s since unsigned/signed ints and floats all store 0 as all 0s
84
95
  const data = buffer.callocAs('u32', count);
85
96
  if(data) {
@@ -96,8 +107,13 @@ export default class MemoryHeap {
96
107
 
97
108
  // If we get here we need to grow another buffer to continue allocating new memory
98
109
  const buffer = this.createBuffer();
99
- this.buffers.push(buffer);
100
- this.onGrowBufferHandlers.forEach(handler => handler(buffer.buf as SharedArrayBuffer));
110
+ let nextBufferPosition = Atomics.add(this.memory.data, BUFFER_COUNT_INDEX, 1);
111
+ // Setting index set by internal Atomic count so we can create new buffers from multiple threads and keep position consistent
112
+ this.buffers[nextBufferPosition] = buffer;
113
+ this.onGrowBufferHandlers.forEach(handler => handler({
114
+ bufferPosition: nextBufferPosition,
115
+ buffer: buffer.buf as SharedArrayBuffer
116
+ }));
101
117
 
102
118
  const data = buffer.callocAs('u32', count);
103
119
  if(data) {
@@ -110,6 +126,15 @@ export default class MemoryHeap {
110
126
  }
111
127
  }
112
128
 
129
+ getSharedAlloc(shared: SharedAllocatedMemory): AllocatedMemory | undefined {
130
+ // Should just mean it hasn't synced to this thread yet
131
+ if(this.buffers[shared.bufferPosition] === undefined) {
132
+ return undefined;
133
+ }
134
+
135
+ return new AllocatedMemory(this, shared);
136
+ }
137
+
113
138
  get currentUsed() {
114
139
  return this.totalAllocated - this.buffers.reduce((total, memPool) => total + memPool.stats().available, 0);
115
140
  }
@@ -136,7 +161,11 @@ function myPrettyBytes(bytes: number) {
136
161
  });
137
162
  }
138
163
 
139
- type OnGrowBuffer = (newBuffer: SharedArrayBuffer) => void;
164
+ type OnGrowBuffer = (newBuffer: GrowBufferData) => void;
165
+ interface GrowBufferData {
166
+ bufferPosition: number
167
+ buffer: SharedArrayBuffer
168
+ }
140
169
 
141
170
  interface MemoryHeapConfig {
142
171
  bufferSize?: number
@@ -146,4 +175,4 @@ interface MemoryHeapMemory {
146
175
  buffers: Array<SharedArrayBuffer>
147
176
  }
148
177
 
149
- export type { MemoryHeapConfig, MemoryHeapMemory };
178
+ export type { MemoryHeapConfig, MemoryHeapMemory, GrowBufferData };
@@ -62,6 +62,7 @@ export default class SharedList<T extends Uint32Array | Int32Array | Float32Arra
62
62
  this.memory = memory;
63
63
 
64
64
  if(config && 'firstBlock' in config) {
65
+ // TODO: How to handle referencing memory we don't have access to yet because buffer not synced from worker?
65
66
  this.firstBlock = new AllocatedMemory(memory, config.firstBlock);
66
67
  this.uint16Array = new Uint16Array(this.firstBlock.data.buffer, this.firstBlock.bufferByteOffset + (LENGTH_INDEX + 1) * Uint32Array.BYTES_PER_ELEMENT, 2);
67
68
  } else {
@@ -119,6 +120,7 @@ export default class SharedList<T extends Uint32Array | Int32Array | Float32Arra
119
120
 
120
121
  if(lastBlockPointer) {
121
122
  let { bufferPosition: lastBlockPosition, bufferByteOffset: lastBlockByteOffset } = getPointer(lastBlockPointer);
123
+ // TODO: How to handle referencing memory we don't have access to yet because buffer not synced from worker?
122
124
  let lastBlock = new Uint32Array(this.memory.buffers[lastBlockPosition].buf, lastBlockByteOffset, 1);
123
125
  storeRawPointer(lastBlock, 0, newBlockPointer);
124
126
  } else {
@@ -175,6 +177,11 @@ export default class SharedList<T extends Uint32Array | Int32Array | Float32Arra
175
177
  let lastBlockByteOffset = 0;
176
178
  while(nextBlockByteOffset) {
177
179
  let memPool = this.memory.buffers[nextBlockPosition];
180
+ // Short circuit iterations if we can't access memory
181
+ if(!memPool) {
182
+ return;
183
+ }
184
+
178
185
  let blockRecord = new Uint32Array(memPool.buf, nextBlockByteOffset, 2);
179
186
  let blockData = this.getDataBlock(blockRecord);
180
187