npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

dok-buffer-transport

v1.0.2

Published

System for serializing and transporting data via ArrayBuffer between a worker and a main thread.

Downloads

5

Readme

dok-buffer-transport

System for serializing and transporting data via ArrayBuffer between a worker and a main thread.


Workers are a way to improve performance by offloading computations to a background threads. Passing data between a the worker and the main thread can be a performance hit though, unless it is passed by reference through an ArrayBuffer as follow:

self.postMessage(payload, [payload.buffer]);

The ArrayBuffer gets passed without copy from worker to main thread or vice-versa.

To help with that, this package provides a system for serializing data into an ArrayBuffer and passing it from worker to main thread. This library is optimized for gaming, so it supports passing data continously within a game loop.

Usage

First, on the receiving end (likely the main thread), register commands into your BufferTransporter as follow:

const bufferTransport = new BufferTransport();
bufferTransport.register({
			id: Commands.SCORE,	// unsigned byte (integer from 0..255)
			parameters: "int",
			apply: score => showScore(score),
		}, {
			id: Commands.MOVE,
			parameters: "float,float,float",
			apply: (x,y,z) => moveTo(x,y,z),
		});

Inside your worker, also instantiate a bufferTransport, then send commands into it.

const bufferTransport = new BufferTransport();
bufferTranport.sendCommand(Commands.SCORE, 333);
bufferTransport.sendCommand(Command.MOVE, 5, 15, 3.3);

Then once you've sent all your command, pass the ArrayBuffer through with the worker's postMessage command.

const { dataView, byteCount } = bufferTransport;
self.postMessage({
  dataView,
  byteCount,
}, [dataView.buffer]);

The byteCount determines the effect number of bytes, but the dataView itself has a capacity of 8,000,000 bytes. Since we don't want to continously produce ArrayBuffers, on the main thread, you need to pass the ArrayBuffer back to the worker right after usage.

worker.addEventListener("message", event => {
  const { dataView, byteCount} = event.data;
  bufferTransport.setup(dataView, byteCount);
  bufferTransport.apply();	//	this executes all commands
  worker.postMessage({
			action: "returnBuffer",
			dataView,
		}, [ dataView.buffer ]);
});

On the worker side, put back the dataView into the BufferTransport class.

self.addEventListener('message', function(event) {
  if (event.data.action === "returnBuffer") {
    bufferTransport.returnBuffer(event.data.dataView);
  }
});

Note that the worker doesn't slow down to wait for the ArrayBuffer to be returned. It will continously work and produce new ArrayBuffers while waiting for the payload from main thread to be returned. This creates a cycle of ArrayBuffers that get passed into the main thread and returned to the worker. At 60fps, approximately, we get around 8-10 array buffers going around in circles.

Advanced Usage

Note that this system was meant to pass a large amount of data, fetching properties from sprites and sending them into array buffers directly consumable by WebGL. With the various options below, you can effectively use this library for that.

Registering parameters

Here are all the types you can use for defining the data you need to pass:

  • boolean: True / False
  • ubyte, byte: Bytes. Signed (-128,127) or unsigned (0..255)
  • ushort, short: Short or 16bit integers. Signed and unsigned.
  • uint, int: 32 bit signed or unsigned integer.
  • float: 32 bit floating point.
  • string: A "string"
  • object: A serializable javascript object { field: "value" }.
  • array: A serializable javascript array.
  • dataView: A DataView object.

For better performance, avoid using object, array and string.

Multiple parameters

You can pass parameters in a sequence during registration:

bufferTransport.register({
  ...
  parameters: "int,float,string",
});

You can use the * operator to repeat the type.

bufferTransport.register({
  ...
  parameters: "float*24",
});

This means that the command expects 24 floats.

Passing DataView

This is how BufferTransport is primarily meant to be used.

bufferTransport.register({
  ...
  parameters: "uint,[byte,byte,byte,byte]",
});

Notice the brackets []. Those determine that you want to have a DataView as the second parameter, and that it will be composed of 4 bytes. On the sender end (worker), the parameters are inputed sequentially:

bufferTransport.sendCommand(COMMAND, 10000, 1, 2, 3, 4);

This sets the uint parameter to 10000, and the DataView as an ArrayBuffer of 4 bytes containing [1,2,3,4]. On the receiving end however, you get a DataView.

bufferTransport.register({
  ...
  parameters: "uint,[byte,byte,byte,byte]",
  apply: (offset, dataView) => process(offset, dataView),
});

The function process will be called with 2 parameters, and offset set to 10000 and a DataView with 4 bytes, 1, 2, 3, 4.

The main purpose for using this is to pass data continously as array buffer, and load them directly into WebGL using gl.bufferSubData:

gl.bufferSubData(gl.ARRAY_BUFFER, offset, buffer);

Let's say you are sending a sprite, with x,y,z coordinates for its 4 corners. You would be sending 12 floating points and the command would be something like this:

bufferTransport.register({
  ...
  parameters: "uint,[float*12]",
  apply: (offset, dataView) => {
		gl.bindBuffer(vertexBuffer);
		gl.bufferSubData(gl.ARRAY_BUFFER, offset, dataView);
  }
});

Merge DataView

It is common to be sending several sprite continously. Therefore, you might end up with several sequential updates on the same buffer:

  • offset: 0, dataView: [3.5,3.5,3.5]
  • offset: 12, dataView: [0,2.5,6.0],
  • offset: 24, dataView: [10,100.0,7.5],

It would be inefficient for WebGL to repeatedly call bufferSubData for every single sprite. So for sequencial sprite, we have the option to merge DataView.

For that, we first have to assume that buffer for WebGL will be sent in a very specific format: First the offset of the GL buffer, then the dataView.

bufferTransport.register({
  ...
  parameters: "uint,[float*12]",
  apply: (offset, dataView) => process(offset, dataView),
});

Then instead of using sendCommand, we call bufferTransport.sendGLBuffer

bufferTransport.sendGLBuffer(Command.MOVE, offset, x, y, z, x, y, z ...)

When two sequential commands are sent, internally, BufferTransport understands that the same command was issued twice, and an offset follows the previous one without leaving any gap. It then merges the DataView instead separating into two separate commands.

When process gets called, the first paramter will be the offset of the first command, and the dataView will be of size 24 floats (24*4 bytes), containing the data for two sequential commands. This helps reduce let's say 1000 calls to bufferSubData into a single one.

Future Improvements

We should be able to further improve performance by allowing sending multiple smaller ArrayBuffers, rather than a giant 8mb one between worker and main thread (the main concern is not that we are copying 8mb, but simply that it does take a lot of memory, especially if we have several ArrayBuffers).

By splitting the large ArrayBuffer into smaller chunks, we can be can save space, because we would have smaller ArrayBuffer mostly at capacity.