36

What is the preferable way of appending/combining ArrayBuffers?

I'm receiving and parsing network packets with a variety of data structures. Incoming messages are read into ArrayBuffers. If a partial packet arrives I need to store it and wait for the next message before re-attempting to parse it.

Currently I'm doing something like this:

function appendBuffer( buffer1, buffer2 ) {
  var tmp = new Uint8Array( buffer1.byteLength + buffer2.byteLength );
  tmp.set( new Uint8Array( buffer1 ), 0 );
  tmp.set( new Uint8Array( buffer2 ), buffer1.byteLength );
  return tmp.buffer;
}

Obviously you can't get around having to create a new buffer as ArrayBuffers are of a fixed length, but is it necessary to initialize typed arrays? Upon arrival I just want is to be able to treat the buffers as buffers; types and structures are of no concern.

user1421750
  • 1,200
  • 2
  • 9
  • 16
  • 2
    possible duplicate of [Typed Arrays in Gecko 2: Float32Array concatenation and expansion](http://stackoverflow.com/questions/4554252/typed-arrays-in-gecko-2-float32array-concatenation-and-expansion) – Esailija May 28 '12 at 14:30
  • @Esailija, the solution to the above question offers my current approach which is combining typed arrays into a new buffer. Which is fine when you want to deal with typed arrays. I want to avoid them altogether. My question is whether this is possible. – user1421750 May 28 '12 at 14:53
  • 2
    well you only have `.slice` with `ArrayBuffer`, not much can be done with that. Then you have `.append` with `BlobBuilder` but it's gonna be much more complicated than what you are already doing. Is there a real problem with your current approach? – Esailija May 28 '12 at 15:06
  • 2
    @Esailija my real concern is performance, although I haven't reached that point is testing yet. This just seemed like a backhanded way of doing it. Still getting used to JS! Thanks anyway. – user1421750 May 28 '12 at 16:13
  • Each `ArrayBuffer` has a fixed size, so copying data is the only way. You want to use native code to do the copy, not Javascript loops, so `Uint8Array.set` is your best bet - it can detect when its argument is another `Uint8Array` and do a high-speed C-style `memcpy`. So, your original code example is about as fast & simple as it can be - the `ArrayBuffer` class has no useful methods, since its designers expected typed array's to be the primary interface. For that reason, I prefer to use `Uint8Array` for "raw data", not `ArrayBuffer`. – William Swanson Jan 11 '20 at 22:27

4 Answers4

8

Why not using a Blob ? (I realize it might not have been available at that time).

Just create a Blob with your data, like var blob = new Blob([array1,array2,string,...]) and turn it back into an ArrayBuffer (if needed) using a FileReader (see this).

Check this : What's the difference between BlobBuilder and the new Blob constructor? And this : MDN Blob API

EDIT :

I wanted to compare the efficiency of these two methods (Blobs, and the method used in the question) and created a JSPerf : http://jsperf.com/appending-arraybuffers

Seems like using Blobs is slower (In fact, I guess it's the use of Filereader to read the Blob that takes the most time). So now you know ;) Maybe it would me more efficient when there are more than 2 ArrayBuffer (like reconstructing a file from its chunks).

Community
  • 1
  • 1
Jb Drucker
  • 972
  • 8
  • 14
4
function concat (views: ArrayBufferView[]) {
    let length = 0
    for (const v of views)
        length += v.byteLength
        
    let buf = new Uint8Array(length)
    let offset = 0
    for (const v of views) {
        const uint8view = new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
        buf.set(uint8view, offset)
        offset += uint8view.byteLength
    }
    
    return buf
}
Hongfei Shen
  • 103
  • 1
  • 7
  • Works for me! For reference, `Uint8Array.set()` is documented [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set). – personal_cloud Nov 25 '22 at 07:34
2

It seems you've already concluded that there is no way around creating a new array buffer. However, for performance sake, it could be beneficial to append the contents of the buffer to a standard array object, then create a new array buffer or typed array from that.

var data = [];

function receive_buffer(buffer) {
    var i, len = data.length;

    for(i = 0; i < buffer.length; i++)
        data[len + i] = buffer[i];

    if( buffer_stream_done()) 
        callback( new Uint8Array(data));
}

Most javascript engines will already have some space set aside for dynamically allocated memory. This method will utilize that space instead of creating numerous new memory allocations, which can be a performance killer inside the operating system kernel. On top of that you'll also shave off a few function calls.

A second, more involved option would be to allocate the memory beforehand. If you know the maximum size of any data stream then you could create an array buffer of that size, fill it up (partially if necessary) then empty it when done.

Finally, if performance is your primary goal, and you know the maximum packet size (instead of the entire stream) then start out with a handful of array buffers of that size. As you fill up your pre-allocated memory, create new buffers between network calls -- asynchronously if possible.

Duco
  • 721
  • 7
  • 11
  • 1
    I would love to see some bench marking on this one. – Lupus Ossorum Nov 21 '19 at 15:21
  • I just may, if I can learn to use JSperf XD. Although, these days, I wouldn't recommend the last option -- it could get too big and crash the tab in the browser, or maybe even cause an overflow in Node (guessing) – Duco Nov 30 '19 at 09:21
-3

You could always use DataView (http://www.khronos.org/registry/typedarray/specs/latest/#8) rather than a specific typed array, but as has been mentioned in the comments to your question, you can't actually do much with ArrayBuffer on its own.

Cyphus
  • 793
  • 5
  • 16
  • How would that be better than a Uint8Array for this purpose? – BHSPitMonkey May 19 '13 at 23:03
  • @BHSPitMonkey It's not really - the only reason for mentioning it is that it doesn't assume a particular element type. – Cyphus May 20 '13 at 08:16
  • 8
    There's nothing wrong with treating everything as a Uint8 if the purpose is to simply copy the data, which is the case here. DataView is great if you need the flexibility it's there for, but it's much much less performant for this purpose. See: http://jsperf.com/uint8array-vs-dataview3 – BHSPitMonkey May 23 '13 at 02:38