@daneren2005/shared-memory-objects 0.0.3 → 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daneren2005/shared-memory-objects",
3
- "version": "0.0.3",
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,6 +1,6 @@
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
6
  import SharedMap, { type SharedMapMemory } from './shared-map';
@@ -24,6 +24,7 @@ export {
24
24
  MemoryBuffer,
25
25
  MemoryHeap,
26
26
  type MemoryHeapMemory,
27
+ type GrowBufferData,
27
28
 
28
29
  SharedList,
29
30
  type SharedListMemory,
@@ -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,12 +62,12 @@ 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 {
68
69
  if(config && config.initWithBlock) {
69
70
  this.firstBlock = new AllocatedMemory(memory, config.initWithBlock);
70
- console.log(`init block at ${this.firstBlock.data.byteOffset}`);
71
71
  } else {
72
72
  this.firstBlock = memory.allocUI32(FIRST_BLOCK_RECORD_KEEPING_COUNT);
73
73
  }
@@ -120,12 +120,12 @@ export default class SharedList<T extends Uint32Array | Int32Array | Float32Arra
120
120
 
121
121
  if(lastBlockPointer) {
122
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?
123
124
  let lastBlock = new Uint32Array(this.memory.buffers[lastBlockPosition].buf, lastBlockByteOffset, 1);
124
125
  storeRawPointer(lastBlock, 0, newBlockPointer);
125
126
  } else {
126
127
  // First item - store on first block
127
128
  storeRawPointer(this.firstBlock.data, 0, newBlockPointer);
128
- console.log(`insert first block ${newBlockPointer} at ${this.firstBlock.data.byteOffset}`);
129
129
  }
130
130
 
131
131
  // Always update new last buffer position and length
@@ -172,12 +172,16 @@ export default class SharedList<T extends Uint32Array | Int32Array | Float32Arra
172
172
  *[Symbol.iterator]() {
173
173
  let currentIndex = 0;
174
174
  let { bufferPosition: nextBlockPosition, bufferByteOffset: nextBlockByteOffset } = loadPointer(this.firstBlock.data, 0);
175
- console.log(`checking first block: ${nextBlockByteOffset} at ${this.firstBlock.data.byteOffset}`);
176
175
  let lastBlockData = this.firstBlock.data;
177
176
  let lastBlockPosition = 0;
178
177
  let lastBlockByteOffset = 0;
179
178
  while(nextBlockByteOffset) {
180
179
  let memPool = this.memory.buffers[nextBlockPosition];
180
+ // Short circuit iterations if we can't access memory
181
+ if(!memPool) {
182
+ return;
183
+ }
184
+
181
185
  let blockRecord = new Uint32Array(memPool.buf, nextBlockByteOffset, 2);
182
186
  let blockData = this.getDataBlock(blockRecord);
183
187