@daneren2005/shared-memory-objects 0.0.0 → 0.0.2

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,4 +1,28 @@
1
1
  # Shared Memory Objects
2
- A library to try to make making a multi-threaded game in Javascript possible.
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
- TODO
4
+ ## Basics
5
+ 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
+
7
+ Each allocated memory location can be stored as an int pointer. You can use `getPointer(int)` to get the bufferPosition (ie: buffer index in the heap) and bufferByteOffset that the memory location points to. You can also convert a bufferPosition/bufferByteOffset pair to an int pointer with `createPointer(bufferPosition, bufferByteOffset)`. The pointer format is uses 12 bits for the buffer index and the remaining 20 bits for the byte offset in that buffer for a total of 1MB per buffer and 4GB total of memory. Each allocated memory object can return either a pointer via `allocatedMemory.pointer` or the raw position/byte offset via `allocatedMemory.getSharedMemory()`.
8
+
9
+ 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
+
11
+ ## Data Structures
12
+ - SharedList
13
+ - SharedVector
14
+ - SharedMap
15
+ - SharedString
16
+
17
+ ## Thread Safety
18
+ - Memory allocations is thread safe as long as it does not need to create a new buffer. Right now that can only be done from the main thread.
19
+ - SharedList, SharedVector, and SharedMap are all not thread safe.
20
+ - SharedString is thread safe with a lock on read/write with a cached version of the string so it doesn't need to lock after the first read unless the string has changed.
21
+
22
+ ## TODO
23
+ - Make creating new buffers from allocations possible from multiple threads
24
+ - Make data structures thread safe
25
+ - Add basic thread safe object example
26
+
27
+ ## Credit
28
+ The entire core of this library is based on a fork of @thi.ng/malloc found at https://github.com/thi-ng/umbrella/blob/develop/packages/malloc. The only big difference between our MemoryBuffer and their MemPool is making allocations/freeing memory thread safe.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daneren2005/shared-memory-objects",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
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",
@@ -8,7 +8,6 @@
8
8
  "sharedarraybuffer"
9
9
  ],
10
10
  "license": "MIT",
11
-
12
11
  "repository": {
13
12
  "type": "git",
14
13
  "url": "git+ssh://git@github.com/daneren2005/shared-memory-objects.git"
@@ -16,12 +15,15 @@
16
15
  "bugs": {
17
16
  "url": "https://github.com/daneren2005/shared-memory-objects/issues"
18
17
  },
19
-
20
18
  "type": "module",
21
19
  "files": [
22
- "dist"
20
+ "src/",
21
+ "!__tests__"
23
22
  ],
24
23
  "main": "./src/main.ts",
24
+ "exports": {
25
+ ".": "./src/main.ts"
26
+ },
25
27
  "scripts": {
26
28
  "dev": "vite",
27
29
  "lint": "eslint src/ --ext .js,.cjs,.mjs,.ts,.cts,.mts --ignore-path .gitignore",
@@ -0,0 +1,84 @@
1
+ import type { TypedArray } from './interfaces/typed-array';
2
+ import type { TypedArrayConstructor } from './interfaces/typed-array-constructor';
3
+ import type MemoryBuffer from './memory-buffer';
4
+ import type MemoryHeap from './memory-heap';
5
+ import { createPointer } from './utils/pointer';
6
+
7
+ export default class AllocatedMemory {
8
+ private readonly memory: MemoryHeap;
9
+
10
+ readonly bufferPosition: number;
11
+ get bufferByteOffset(): number {
12
+ return this.data.byteOffset;
13
+ }
14
+ get pointer(): number {
15
+ return createPointer(this.bufferPosition, this.bufferByteOffset);
16
+ }
17
+ private buffer: MemoryBuffer;
18
+ data: Uint32Array;
19
+
20
+ constructor(memory: MemoryHeap, config: AllocatedMemoryConfig | SharedAllocatedMemory) {
21
+ this.memory = memory;
22
+
23
+ if('buffer' in config) {
24
+ this.data = config.data;
25
+ this.buffer = config.buffer;
26
+ this.bufferPosition = this.memory.buffers.indexOf(config.buffer);
27
+ } else {
28
+ this.bufferPosition = config.bufferPosition;
29
+ this.buffer = memory.buffers[config.bufferPosition];
30
+
31
+ // Making sure these are the correct size is slow but in dev we want to make sure we aren't allowing to go out of bounds
32
+ if(import.meta.env.MODE === 'production') {
33
+ this.data = new Uint32Array(this.buffer.buf, config.bufferByteOffset);
34
+ } else {
35
+ this.data = new Uint32Array(this.buffer.buf, config.bufferByteOffset, this.buffer.lengthOf(config.bufferByteOffset));
36
+ }
37
+ }
38
+ }
39
+
40
+ getArray<T extends TypedArray>(type: TypedArrayConstructor<T>, offset: number, length: number): T {
41
+ if(import.meta.env.MODE === 'development') {
42
+ if(offset + length > this.data.length) {
43
+ console.warn(`Trying to grab more memory from AllocatedMemory.getArray then we have: ${offset} + ${length} > ${this.data.length}`);
44
+ }
45
+ }
46
+
47
+ return new type(this.data.buffer, this.data.byteOffset + offset * type.BYTES_PER_ELEMENT, length);
48
+ }
49
+ getArrayMemory(offset: number, length: number): SharedAllocatedMemory {
50
+ if(import.meta.env.MODE === 'development') {
51
+ if(offset + length > this.data.length) {
52
+ console.warn(`Trying to grab more memory from AllocatedMemory.getArrayMemory then we have: ${offset} + ${length} > ${this.data.length}`);
53
+ }
54
+ }
55
+
56
+ return {
57
+ bufferPosition: this.bufferPosition,
58
+ bufferByteOffset: this.bufferByteOffset + offset * this.data.BYTES_PER_ELEMENT
59
+ };
60
+ }
61
+
62
+ free() {
63
+ // NOTE: From worker thread you can't pass the array, you have to pass an explicit address to free
64
+ this.buffer.free(this.data.byteOffset);
65
+ }
66
+
67
+ getSharedMemory(): SharedAllocatedMemory {
68
+ return {
69
+ bufferPosition: this.bufferPosition,
70
+ bufferByteOffset: this.bufferByteOffset
71
+ };
72
+ }
73
+ }
74
+
75
+ interface AllocatedMemoryConfig {
76
+ data: Uint32Array
77
+ buffer: MemoryBuffer
78
+ }
79
+
80
+ interface SharedAllocatedMemory {
81
+ bufferPosition: number
82
+ bufferByteOffset: number
83
+ }
84
+ export type { SharedAllocatedMemory };
@@ -0,0 +1,3 @@
1
+ type Pow2 = 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100 | 0x200 | 0x400 | 0x800 | 0x1000 | 0x2000 | 0x4000 | 0x8000 | 0x10000 | 0x20000 | 0x40000 | 0x80000 | 0x100000 | 0x200000 | 0x400000 | 0x800000 | 0x1000000 | 0x2000000 | 0x4000000 | 0x8000000 | 0x10000000 | 0x20000000 | 0x40000000 | 0x80000000;
2
+
3
+ export type { Pow2 };
@@ -0,0 +1,6 @@
1
+ interface TypedArrayConstructor<T> {
2
+ new(buffer: ArrayBufferLike, byteOffset: number, length: number): T
3
+ BYTES_PER_ELEMENT: number
4
+ }
5
+
6
+ export type { TypedArrayConstructor };
@@ -0,0 +1 @@
1
+ export type TypedArray = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint8ClampedArray | Uint16Array | Uint32Array;
@@ -0,0 +1,41 @@
1
+ const UNLOCKED = 0;
2
+ const READ_LOCKED = 1;
3
+ const WRITE_LOCKED = 2;
4
+
5
+ export function readLock(data: Int32Array, index: number = 0) {
6
+ // Wait over and over again until we get that it was unlocked or read locked
7
+ while(Atomics.compareExchange(data, index, UNLOCKED, READ_LOCKED) === WRITE_LOCKED) {
8
+ Atomics.wait(data, index, WRITE_LOCKED);
9
+ }
10
+
11
+ Atomics.add(data, index + 1, 1);
12
+ }
13
+ export function writeLock(data: Int32Array, index: number = 0) {
14
+ // Write lock needs to be exclusive - wait until we were in UNLOCKED to proceed
15
+ let oldValue = Atomics.compareExchange(data, index, UNLOCKED, WRITE_LOCKED);
16
+ while(oldValue !== UNLOCKED) {
17
+ Atomics.wait(data, index, oldValue);
18
+ oldValue = Atomics.compareExchange(data, index, UNLOCKED, WRITE_LOCKED);
19
+ }
20
+ }
21
+
22
+ export function readUnlock(data: Int32Array, index: number = 0) {
23
+ let readCount = Atomics.sub(data, index + 1, 1) - 1;
24
+
25
+ if(readCount <= 0) {
26
+ if(Atomics.compareExchange(data, index, READ_LOCKED, UNLOCKED) !== READ_LOCKED) {
27
+ console.warn('We are unlocking when it was not read locked!');
28
+ }
29
+
30
+ Atomics.notify(data, index);
31
+ }
32
+ }
33
+ export function writeUnlock(data: Int32Array, index: number = 0) {
34
+ if(Atomics.compareExchange(data, index, WRITE_LOCKED, UNLOCKED) !== WRITE_LOCKED) {
35
+ console.warn('We are unlocking when it was not write locked!');
36
+ }
37
+
38
+ Atomics.notify(data, index);
39
+ }
40
+
41
+ export const READ_WRITE_LOCK_ALLOCATE_COUNT = 2;
@@ -0,0 +1,21 @@
1
+ const UNLOCKED = 0;
2
+ const LOCKED = 1;
3
+ export function lock(data: Int32Array, index: number = 0) {
4
+ // Wait over and over again until we are one who set this from UNLOCKED to LOCKED
5
+ while(Atomics.compareExchange(data, index, UNLOCKED, LOCKED) !== UNLOCKED) {
6
+ if('WorkerGlobalScope' in self) {
7
+ Atomics.wait(data, index, LOCKED);
8
+ } else {
9
+ // TODO: Spin-locks suck....
10
+ }
11
+ }
12
+ }
13
+ export function unlock(data: Int32Array, index: number = 0) {
14
+ if(Atomics.compareExchange(data, index, LOCKED, UNLOCKED) !== LOCKED) {
15
+ console.warn('We are unlocking when it was not locked!');
16
+ }
17
+
18
+ Atomics.notify(data, index);
19
+ }
20
+
21
+ export const SIMPLE_LOCK_ALLOCATE_COUNT = 1;
package/src/main.ts ADDED
@@ -0,0 +1,35 @@
1
+ import AllocatedMemory, { type SharedAllocatedMemory } from './allocated-memory';
2
+ import MemoryBuffer from './memory-buffer';
3
+ import MemoryHeap, { type MemoryHeapMemory } from './memory-heap';
4
+
5
+ import SharedList, { type SharedListMemory } from './shared-list';
6
+ import SharedMap from './shared-map';
7
+ import SharedPointerList from './shared-pointer-list';
8
+ import SharedString from './shared-string';
9
+ import SharedVector from './shared-vector';
10
+
11
+ import type { TypedArrayConstructor } from './interfaces/typed-array-constructor';
12
+
13
+ export * from './utils/16-from-32-array';
14
+ export * from './utils/16-from-64-array';
15
+ export * from './utils/float32-atomics';
16
+ export * from './utils/pointer';
17
+ export * from './lock/simple-lock';
18
+ export * from './lock/read-write-lock';
19
+
20
+ export {
21
+ AllocatedMemory,
22
+ type SharedAllocatedMemory,
23
+ MemoryBuffer,
24
+ MemoryHeap,
25
+ type MemoryHeapMemory,
26
+
27
+ SharedList,
28
+ type SharedListMemory,
29
+ SharedMap,
30
+ SharedPointerList,
31
+ SharedString,
32
+ SharedVector,
33
+
34
+ type TypedArrayConstructor
35
+ };