zandaqo / structurae

Data structures for high-performance JavaScript applications.
MIT License
701 stars 21 forks source link

SharedArrayBuffer support? #16

Open eranimo opened 4 years ago

eranimo commented 4 years ago

Is it possible to support SharedArrayBuffers?

zandaqo commented 4 years ago

@eranimo Sure, most all structures using buffers extend interfaces such as TypedArrays or DataView, and as far as I know, you can use those with SharedArrayBuffers same as ArrayBuffers, you'll just have to create the said buffer first and instantiate the structure on it, e.g. a grid using a shared buffer:

const { GridMixin } = require('structurae');
const Grid = GridMixin(Int8Array);
// let's get the required byte length of a buffer to hold a grid of 16 rows and 8 columns
const length = Grid.getLength(16, 8);
// create a shared buffer of that length
const sharedBuffer = new SharedArrayBuffer(length);
// instantiate our structure using the sharedBuffer
const grid = new Grid({ rows: 16, columns: 8 }, sharedBuffer)
eranimo commented 4 years ago

That seems to work for ObjectView but not MapView. After setting any field in a MapView getting it right after always returns undefined. The buffer looks empty.

zandaqo commented 4 years ago

That's true, MapView is a special case here for now: it lacks the ability to write into an existing buffer and MapView.from creates a new one every time. For this particular case there is a workaround though, set MapView.maxView to a DataView that uses a SharedArrayBuffer:

const SomeMapView = MapViewMixin({ $id: 'SomeMapView, type: 'object', properties: { a: { type: 'number' } } });
SomeMapView.maxView = new DataView(new SharedArrayBuffer(8192));
// now all instances of SomeMapClass will use SharedArrayBuffer instead of ArrayBuffer
// you can set MapView.maxView as well if you want to use shared buffers in all map views.
// the size of the buffer is the maximum possible size of a single view
// it defaults to 8192 but can be any supported size

const view = SomeMapView.from({ a: 1 });
view.buffer instanceof SharedArrayBuffer
//=> true
zandaqo commented 4 years ago

In the latest version (3.2.0) I've added support for nesting MapViews and now MapViews can be written within existing ArrayBuffers. That is, we can use SharedArrayBuffers with MapViews the same way as with ObjectViews:

const SomeMapView = MapViewMixin({ $id: 'SomeMapView', type: 'object', properties: { a: { type: 'number' } } });
const requiredLength = SomeMapView.getLength({ a: 1 });
const buffer = new SharedArrayBuffer(requiredLength);
const view = SomeMapView.from({ a: 1 }, new DataView(buffer));
view.buffer instanceof SharedArrayBuffer
//=> true

Unlike the workaround with MapView.maxView, this way we can choose to use normal or shared buffers for each instance.

eranimo commented 4 years ago

Thanks, that does seem to work. What about ArrayView? For context, I'm using your excellent library to build an entity component system on top of SharedArrayBuffers (components implemented as MapViews) that works across many Workers and the main thread.

zandaqo commented 4 years ago

Thanks, that does seem to work. What about ArrayView?

The same principle, just use the max amount of items as an argument to get the required length:

const SomeArrayView = ArrayViewMixin(SomeObjectView);
const requiredLength = SomeArrayView.getLength(10); // the length of a buffer to hold 10 object views
const buffer = new SharedArrayBuffer(requiredLength);
const view = SomeArrayView.from([], new DataView(buffer));
view.buffer instanceof SharedArrayBuffer
// true

In general, all view types have *View.getLength method to get the required buffer size in bytes. For ObjectView we don't need any arguments because the size is fixed. For MapView and VectorView we supply the object or array that's being encoded, since the size depends on the value. For ArrayView we provide the amount of items it needs to hold since all items are of the same size.

For context, I'm using your excellent library to build an entity component system on top of SharedArrayBuffers (components implemented as MapViews) that works across many Workers and the main thread.

I'm glad you find it usefull. Truth be told, I don't have much experience with SharedBuffers per se, since they were disabled in browsers shortly after their introduction. However, I do use the views with pre-allocated buffers especially when integrating with WebAssembly, hence, the ability to use existing buffers was important and it should work equally well with normal and shared buffers alike.