There are various usecases in which we want to pass Bytes directly from Javascript to the Elm application and vice versa. A concrete usecase I struggle with is Websockets with binary data (protobuf encoding). Unfortunately, Elm's primary interop mechanism, Ports, is based on JSON encoding and as such unable to transfer Bytes (in the concrete form of Uint8Array, DataView or ArrayBuffer).
The goal of this benchmark is to compare the different available workarounds.
TL;DR: For transferring small amounts (< 5kb) of data at a time, a JSON compatible encoding such as Array (of numbers) or Base64 is fine. The difference between the two is negligible. For big amounts of data, use a HTTP prototype hack (more details below). The overhead of the HTTP method is remarkably low, so there is no benefit of implementing multiple methods and deciding which method to use depending on size.
Note: "identity" measures overhead due to the method used for benchmarking by just passing the Bytes back and forth as Json.Encode.Value.
While Bytes cannot be directly represented in JSON,
an array of numbers can. Thus we can copy each element of the Uint8Array
to a standard JS array and revert the process on the Elm side via the Bytes.Encode
/Bytes.Decode
API.
This seems to perform reasonable well for small workloads but causes slowdown for large Arrays due to the O(n) copying.
Similar approach but more traditional in a sense, since bytes are often encoded as Base64 in the browser, for example in URLs. Seems to be very similar to the Int Array approach in speed. I would be interested to compare the memory usages between the two, but I was not sure how to best accomplish that. Suggestions/PRs welcome!
Warning: Calling File.toBytes
apparently causes memory to be leaked: elm/file#31
The package elm/file
includes a File.decoder
as well as a File.toBytes
function.
Therefore we can call new File(bytes, '')
on the JS side and pass that into Elm over a port without any trouble. The benchmark shows the overhead is constant/independent of the bytes size.
This seems to perform consistently worse that the below Http approach so I would not recommend it.
I call this one Taskport, since the idea is based on the elm-taskport library.
Essentially, we send a HTTP request from Elm with a nonsense URL like elm://post
.
On the JS side, we monkeypatch the XHRHttpRequest prototype by intercepting the open
and send
methods. We check for the nonsense URL or prefix and send the bytes from JS directly instead of sending a request.
This, for some reason, has remarkably low overhead according to the benchmark and is thus the best solution from Elm to JS for large bytes size. Even for small byte sizes and the impractical JS to Elm direction (JS tells the Elm runtime over a port that it should get some data via HTTP), the performance rivals the Json-based approaches, so you shouldn't need to implement multiple approaches.
Each contestant has a JS file which
- implements a
send
method with the signatureUint8Array -> Promise<number>
. The returned number is the array length and used as a sanity check that the bytes were passed successfully - implements a
receive
method with the signature() -> Promise<Uint8Array>
.
The benchmark in dist/index.js
then executes each send method for each contestant sequentially and measures the time via
the Browsers performance API. Methods are executed n = 100
times and the average time is used to hopefully even out garbage collector spikes a bit. Afterwards, each receive method is called sequentially with the same approach.
Uint8Arrays of different sizes are generated outside of the timing in Javascript.
Chart.js is used to visualize the results.