├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .prettierrc.yml ├── LICENSE ├── README.md ├── img ├── bench-avro-extra.svg ├── bench-bson-extra.svg ├── bench-full.svg ├── bench-jsbin-extra.svg ├── bench-json-extra.svg └── bench-protobuf.svg ├── jest.config.js ├── package.json ├── src ├── _root.ts ├── benchmarks.ts ├── config.ts ├── data │ ├── google-protobuf_pb.js │ ├── pbf_pb.js │ └── test.proto ├── index.ts ├── plot.py ├── run-tests.sh └── utils │ ├── helper.ts │ └── utils.ts ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.py] 12 | indent_size = 4 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | es6: true 4 | node: true 5 | jest: true 6 | extends: 7 | - 'eslint:recommended' 8 | - 'plugin:@typescript-eslint/eslint-recommended' 9 | - 'plugin:@typescript-eslint/recommended' 10 | globals: 11 | Atomics: readonly 12 | SharedArrayBuffer: readonly 13 | parser: '@typescript-eslint/parser' 14 | parserOptions: 15 | ecmaVersion: 11 16 | sourceType: module 17 | plugins: 18 | - '@typescript-eslint' 19 | rules: 20 | '@typescript-eslint/no-explicit-any': off 21 | 'no-plusplus': warn 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJs 2 | node_modules/ 3 | /dist/ 4 | /dist-minified/ 5 | /coverage/ 6 | 7 | # IDE 8 | .idea/ 9 | .vscode/ 10 | 11 | # Project 12 | tmp/ 13 | /.local/ 14 | /media/ 15 | /img/ 16 | 17 | 18 | # CI 19 | .coveralls.yml 20 | 21 | # Optional 22 | # By standard package-lock should be commited but feel free to 23 | # ignore lock file if you do not require version locking and 24 | # want to avoid noise in your commit diffs. 25 | package-lock.json 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/** 3 | *.example.* 4 | *.spec.* 5 | *.js.map 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: true 2 | singleQuote: true 3 | trailingComma: all 4 | printWidth: 140 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Mattias E. O. Andersson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binary serialization comparison in JavaScript (Protocol Buffer, Avro, BSON, etc.) 2 | 3 | This is a comparison and benchmark of various binary serialization formats and libraries used in JavaScript as of 2020-07-28. 4 | 5 | I was myself trying to decide what binary serialization format I should use in my personal projects, and what started out simple soon turned into a rather extensive comparison. 6 | 7 | By sharing my findings, I hope it can be of help (and save time) to someone in a similar situation and perhaps inspire some developers to try out binary serialization. 8 | 9 | ## TL;DR (or Abstract) 10 | 11 | This article and benchmark attempts to answer what binary serialization library to use with regard to performance, compression size and ease of use. 12 | 13 | The following formats and libraries are compared: 14 | 15 | * Protocol Buffer: `protobuf-js`, `pbf`, `protons`, `google-protobuf` 16 | * Avro: `avsc` 17 | * BSON: `bson` 18 | * BSER: `bser` 19 | * JSBinary: `js-binary` 20 | 21 | Based on the current benchmark results in this article, the author would rank the top libraries in the following order (higher values are better, measurements are given as x times faster than JSON): 22 | 23 | 1. `avsc`: 10x encoding, 3-10x decoding 24 | 2. `js-binary`: 2x encoding, 2-8x decoding 25 | 3. `protobuf-js`: 0.5-1x encoding, 2-6x decoding, 26 | 4. `pbf`: 1.2x encoding, 1.0x decoding 27 | 5. `bser`: 0.5x encoding, 0.5x decoding 28 | 6. `bson`: 0.5x encoding, 0.7x decoding 29 | 30 | Ranked by encoded size (compared to JSON) only: 31 | 32 | 1. `avsc`, `js-binary`: 32%: 33 | 2. `protobuf-js`, `pbf`, `protons`, `google-protobuf`: 42% 34 | 3. `bser`: 67% 35 | 4. `bson`: 79% 36 | 5. `JSON`: 100% 37 | 38 | Due to various reasons outlined in the article, the author would not currenly recommend the following libraries: 39 | 40 | * `protons`: 2x encoding, 0.05x decoding 41 | * `google-protobuf`: 0.3-0.5x encoding, 0.8x decoding 42 | 43 | 44 | Feel free to skip to the [Conclusion](#conclusion) sections of the article to read the summarized motivation. For performance graphs and detailed measurements skip to [Result (final)](#result-final). To reproduce the measurements, skip to [Setup](#setup). 45 | 46 | 47 | ## Table of content 48 | 49 | - [Introduction](#introduction) 50 | - [Setup](#setup) 51 | - [Disclaimer](#disclaimer) 52 | - [Libraries](#libraries) 53 | - [Benchmark](#benchmark) 54 | - [Result (Protocol Buffers)](#result-protocol-buffers) 55 | - [Result (final)](#result-final) 56 | - [Result (extra)](#result-extra) 57 | - [Conclusion](#conclusion) 58 | 59 | ## Introduction 60 | 61 | Data serialization is ubiquitous in most areas such as sending and receiving data over the network or storing/reading data from the file system. While JSON is a common modus operandi (especially in JavaScript), using a binary serialization format typically provides an advantage in compression size and performance at the cost of losing human readability of the encoded data. 62 | 63 | Two common binary serialization formats across many programming languages are Protocol Buffers and Apache Avro. Avro is inherently a bit more compact than Protobuf, whereas Protobuf uses the additional data as field tags that could make it slightly more forgiving when changing the schema. For those interested, an excellent in-depth explanation has [already been written](https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html) by Martin Kleppmann. 64 | 65 | 66 | In addition to this, several more recent JavaScript-oriented libraries will be included in the comparison. 67 | 68 | This article will mostly focus on the performance aspect and provide a brief overview of each implementation, but as always, there will be pros and cons of each implementation that could be overlooked depending on your use-case. 69 | 70 | ## Setup 71 | 72 | To reproduce the benchmark results, follow these steps. 73 | 74 | * Install Node.js ( `12.18.3 LTS` is recommended). 75 | * Install dependencies: 76 | 77 | ```shell script 78 | npm install 79 | ``` 80 | * Use the default configuration or modify `src/config.ts`. 81 | * Select what libraries to test by changing `run-tests.sh` or use default that tests all libraries. 82 | * Run `run-tests.sh` (if you are on Windows use Git BASH or similar): 83 | 84 | ```shell script 85 | cd src 86 | . run-tests.sh 87 | ``` 88 | 89 | * Create graph images (requires Python with matplotlib installed) by running: 90 | 91 | ```shell script 92 | python plot.py 93 | ``` 94 | 95 | Graph settings can further be configured inside the script. Graph images are by default written to `img/` 96 | 97 | Measurements are accumulated into `src/tmp/plot.json` each benchmark iteration. If needed, simply delete the file to reset the graph. 98 | 99 | ## Disclaimer 100 | 101 | This article only focuses on measurements using JavaScript. Many of the measured formats support additional programming languages that could have different performance characteristics than indicated in this benchmark. 102 | 103 | Although outside the scope of this article, compression size (and thereby network transfer speed) can be further improved at the cost of encoding/decoding performance by combining the output with a compressor/decompressor library such `google/snappy` or `zlib`. 104 | 105 | This is the first time I use many of the listed libraries and as such there might still be additional optimizations that I am unaware of. Still, I believe my implementation is a good indication to what most users will end up using. 106 | 107 | Feel free to inspect my implementations in `src/benchmarks.ts`, and let me know if you find any glaring mistakes (or better yet by submitting a pull request). 108 | 109 | 110 | ## Libraries 111 | 112 | The following libraries and versions are tested (sorted by NPM weekly downloads): 113 | 114 | * `bser "6.10.1"` - 7,671k 115 | * `protobufjs "6.10.1"` - 3,449k 116 | * `bson "4.0.4"` - 1,826k 117 | * `pbf "3.2.1"` - 431k 118 | * `google-protobuf "4.0.0-rc.1"` - 348k 119 | * `avsc "5.4.21"` - 43k 120 | * `protons "1.2.1"` - 30k 121 | * ~~`avro-js "1.10.0"` - 1.2k~~ 122 | * `js-binary "1.2.0"` - 0.3k 123 | 124 | They are categorized as: 125 | 126 | * Protocol Buffer: `protobuf-js`, `pbf`, `protons`, `google-protobuf` 127 | * Avro: `avsc`, `avro-js` 128 | * BSON: `bson` 129 | * BSER: `bser` 130 | * JSBinary: `js-binary` 131 | 132 | 133 | `bser` is a binary serialization library developed for Facebook's "Watchman" filewatcher and is the most downloaded binary serialization library. It is however, mainly used for loca-IPC (inter process communication) as strings are represented as binary with no specific encoding. 134 | 135 | `google-protobuf` is Google's official Protocol Buffer release, but `protobufjs` is by far the more popular library. 136 | 137 | `avsc` seems to be the most popular Avro library. `avro-js` is an offical release by the Apache Foundation but this was excluded from the result section as it seems to be based on an older version of `avsc`, contains less features and both libraries yielded very similar benchmark results with a slight advantage to `avsc`. 138 | 139 | `bson` is the official JavaScript BSON library released by MongoDB. 140 | 141 | `js-binary` is the most obscure library (judging by weekly downloads) and uses a custom binary format that could make interoperability with other programming languages difficult, but this could also make it a good choice due to it being designed with JavaScript in mind. 142 | 143 | ## Benchmark 144 | 145 | Each format will be compared against JavaScript's native JSON library as a baseline, regarding compression size and encoding/decoding time. 146 | 147 | The data used in the benchmark is a growing array of tuples that is grown in increments of 1/8 of its previous size at each iteration, and (to speed things up) 1/4 when reaching sizes above 10 MB. In a real scenario it could be thought of as a list of vectors in a 2D or 3D space, such as a 3D-model or similarly data intensive object. 148 | 149 | To further complicate things the first element of the tuple is an integer. This will give a slight edge to some serialization-formats as an integer can be represented more compact in binary rather than floating-point number. 150 | 151 | To all formats that support multiple datatype sizes; integers are encoded as a 32-bit signed integer and decimal numbers are encoded as 64-bit floating-point numbers. 152 | 153 | The data is as follows: 154 | 155 | ```typescript 156 | [ 157 | [1, 4.0040000000000004, 1.0009999999999994], 158 | [2, 4.371033333333333, 0.36703333333333266], 159 | [3, 5.171833333333334, 0.4337666666666671], 160 | [4, 6.473133333333333, 0.36703333333333354], 161 | ... 162 | ] 163 | ``` 164 | 165 | The first challenge that arose is that not all measured serialization formats supports root level arrays, and almost no one seems to support tuples, and as such the arrays first need to be mapped to structs as follows: 166 | 167 | ```typescript 168 | { 169 | items: [ 170 | {x: 1, y: 4.0040000000000004, z: 1.0009999999999994}, 171 | {x: 2, y: 4.371033333333333, z: 0.36703333333333266}, 172 | {x: 3, y: 5.171833333333334, z: 0.4337666666666671}, 173 | {x: 4, y: 6.473133333333333, z: 0.36703333333333354}, 174 | ... 175 | ] 176 | } 177 | ``` 178 | 179 | This wrapped struct array is the final "payload" that is used in the benchmark unless specified otherwise. This further gives an advantage to some formats over JSON as duplicate information such as field names can be encoded more efficiently in a schema. 180 | 181 | It should be noted that the time to convert the unmapped data to the mapped structs is excluded from all measurements in the benchmark. 182 | 183 | ### Precautions 184 | 185 | Each appended element in the growing array is modified slightly so that all elements are unique. 186 | 187 | It was discovered that some libraries can considerably impact the performance of other libraries when measured in the same running process, possible due to memory reuse. To prevent this (and to get reproducible results) all measurements in the results section has been measured with each implementation running in an isolated Node.js process. 188 | 189 | To reduce unpredictable stalls by the automatic garbage collector in Node.js, the garbage collector is forcefully triggered before each measurement. This did not have any discernable impact on the measured performance other than reducing some randomness. 190 | 191 | ### Unmapped data 192 | 193 | Compared to the mapped (object with array of structs) data, the unmapped (array of arrays) data is more compact and contains less redundant information. As such additional performance could potentially be gained if the target application uses a similar representation internally. 194 | 195 | This is investigated in an additional result section that is found after the main result section. 196 | 197 | ### Hardware 198 | 199 | The benchmark is done in Node.js v12.16.3 on 64-bit Windows 10, with an Intel i7-4790K 4.00GHz CPU and 16 GB RAM. 200 | 201 | ## Result (Protocol Buffers) 202 | 203 | Protocol Buffer was tested more rigorously than the other formats and is thus given this dedicated section. 204 | 205 | An additional format `Protobuf (mixed)` is added to the comparison that uses `protons` during encoding and `protobuf-js` during decoding (this is explained further down). 206 | 207 | All protobuf-implementations in the test uses the following proto-file as schema. 208 | 209 | ```protobuf 210 | syntax = "proto3"; 211 | 212 | message Item { 213 | int32 x = 1; 214 | double y = 2; 215 | double z = 3; 216 | } 217 | 218 | message Items { 219 | repeated Item items = 1; 220 | } 221 | ``` 222 | 223 | It should be noted that all fields in version "proto3" are optional by default, which could be an advantage feature-wise and disadvantage performance-wise as many of the other measured formats are mandatory by default. 224 | 225 | ### Performance graph 226 | 227 | ![Benchmark of protocol buffers](img/bench-protobuf.svg) 228 | 229 | > This graph shows the encode/decode time of each implementation in seconds as well as ratio (compared to JSON) given the payload size in MB (measured as JSON). Note that a logaritmic scale is used on the `Encode/Decode time (s)` and `JSON size (MB)` axis. 230 | 231 | During encoding `protons` and `Protobuf (mixed)` perfomed the fastest at 2 times faster than native JSON at most payload sizes. `protobufjs` and `google-protobuf` perfomed the slowest at about 2-3 times slower. 232 | 233 | During decoding, `protobufjs`, `Protobuf (mixed)` performed the fastest at about 5 times faster than native JSON at most payload sizes (although native JSON catches up again at payloads above 200 MB). `protons` performed by far the slowest; 20-30 times slower, compared to native JSON. 234 | 235 | ### Compression ratio 236 | 237 | All implementations (`protobuf-js`, `pbf`, `protons`, `google-protobuf`) stayed consistent to the Protocol Buffer format and resulted in an identical compression ratio of 42% (compared to the corresponding file size of JSON) at all measured payload sizes. 238 | 239 | ### Maximum payload size 240 | 241 | This is a ranking of the estimated maximum safe payload limit (measured as JSON) each library was able to process: 242 | 243 | 1. `pbf`, `mixed`: 372 MB 244 | 2. `JSON`: 298 MB 245 | 3. `protobuf-js`: 153 MB 246 | 4. `google-protobuf`: 98 MB 247 | 5. `protons`: 40 MB 248 | 249 | When exceeding the payload limit (given the default JavaScript heap size), a heap allocation error occurred in most cases. 250 | 251 | ### Negative effects during decoding 252 | 253 | | |JSON|JS|Google|Protons|Pbf|mixed 254 | |---|---|---|---|---|---|--- 255 | |Prototype pollution | |x| |x| |x 256 | |Getters/Setters| | | |x| | | 257 | |Requires unwrapping| | |x| | 258 | |Unexpected field renames| | |x| | 259 | 260 | > This table shows an overview of negative effects during decoding. 261 | 262 | `pbf` convert cleanly to JavaScript without any detected remnats from the encoding process. 263 | 264 | `protobuf-js` (which also affects `Protobuf (mixed)`) contains serialization remnants (such as type name) hidden in the prototype of the decoded objects, but should behave as a normal plain JavaScript object for most purposes. 265 | 266 | `protons` should be usable as a data object but wraps all fields into getters/setters that could decrease performance. 267 | 268 | `google-protobuf` is wrapped in a builder pattern and need to be converted before it can be used and can introduce unexpected field renames. It is however free from metadata after the final conversion. 269 | 270 | It is as of now unknown if any of the polluted formats incur an additional overhead to plain objects as this is outside the current scope of this article, but it is something to keep in mind. 271 | 272 | ### Remarks 273 | 274 | #### Protobuf (JS) 275 | 276 | `protobuf-js` is slow at encoding but fast at decoding. 277 | 278 | During encoding it provided mixed result. At sizes below 1 MB it mostly performs better than the native JSON implementation, but at any larger sizes it performs 2 to 3 times worse. 279 | 280 | It was the only implementation that reached its max payload limit of 153 MB during encoding, all other formats reached their limit at decoding. It was however discovered that it can decode payloads (created by other implementations) of greater sizes, up to 372 MB. 281 | 282 | #### Protobuf (Pbf) 283 | 284 | `pbf` is average at encoding and decoding, but is deserialized cleanly without added remnants. 285 | 286 | By default `pbf` requires an extra build step where boilerplate code is generated from `.proto` file, though it seems to offer a few alternatives to streamline the process. 287 | 288 | It is also the only Protobuf format that is converted cleanly to JavaScript, which could be a distinct advantage in some cases. 289 | 290 | #### Protobuf (Google) 291 | 292 | `google-protobuf` is slow at encoding, performs average during decoding but might require additional decoding that would further decrease performance, requires extra setup and can cause unexpected renaming of variables. 293 | 294 | It does not seem to have an option for deserializing directly form JSON. Instead the following `Items` and `Item` classes are generated by the protocol buffer compiler that generates a Java-esque builder pattern that the date needs to be mapped into, as outlined here: 295 | 296 | ```typescript 297 | ... 298 | const ItemsWrap = Schema.Items; 299 | const ItemWrap = Schema.Item; 300 | const itemsWrap = new ItemsWrap(); 301 | const itemWraps = data.items.map(item => { 302 | const itemWrap = new ItemWrap(); 303 | itemWrap.setX(item.x); 304 | itemWrap.setY(item.y); 305 | itemWrap.setZ(item.z); 306 | return itemWrap; 307 | }); 308 | itemsWrap.setItemsList(itemWraps); 309 | return itemsWrap.serializeBinary(); 310 | ``` 311 | 312 | This also unexpectedly renames our array from "items" into "itemsList" which can catch some people of guard and affect the receiving code, as this is not something that is present in other tested Protocol Buffers. 313 | 314 | It performs the worst of the implementations during decoding at 2.5 to 3 times slower than native JSON, possible due to the builder overhead. 315 | 316 | Deserialization is also misleading. Though it seems to performs only slightly worse than native JSON, the data is still wrapped in the builder object which should be unsuitable for most purposes, and an additional call to ".toObject()" is required to fully convert it back to JSON, which would further decrease the performance and still includes the unexpected name change. 317 | 318 | #### Protobuf (Protons) 319 | 320 | `protons` is fast at encoding but very slow at decoding. 321 | 322 | The decoded object has all fields wrapped into getters, which might be partially responsible for the poor decoding performance, and while serviceable, could cause some issues depending on how the decoded data is used. The easiest way to remove all getters/setters is to perform a JSON serialization/deserialization which will further increase decoding time. 323 | 324 | It was only able to decode payloads of 47 MB in size, but opposite to `protobuf-js` it can encode payloads of much greater size. 325 | 326 | #### Protobuf (mixed) 327 | 328 | `Protobuf (mixed)` is fast at both encoding and decoding and is good at handling large file sizes. 329 | 330 | This implementation is simply a mix of `protobuf-js` and `protons`, where `protons` is used for encoding and `protobuf-js` for decoding. This result in the best overall performance of all protobuf implementations and can handle larger payloads than both formats can individually. While this might be too impromptu for most users, it gives us an estimate of how well either of these implementations could perform with some improvements. 331 | 332 | ### Further remarks 333 | 334 | Due to poor results and to reduce redundancy, `protons` and `google-js` will be excluded in further comparisons. 335 | 336 | ## Result (final) 337 | 338 | This is the final comparison of the various formats. 339 | 340 | ### Performance graph 341 | 342 | ![Benchmark of binary serialization](img/bench-full.svg) 343 | 344 | > This graph shows the encode/decode time of each implementation in seconds as well as ratio (compared to JSON) given the payload size in MB (measured as JSON). Note that a logaritmic scale is used on the `Encode/Decode time (s)` and `JSON size (MB)` axis. 345 | 346 | During encoding `avro-js` performed the fastest of all implementations (with good margin) at 10 times faster than native JSON at most payload sizes, followed by `js-binary` and `Protobuf (Mixed)` at 2 times faster. Although native JSON once again catches up at payloads above 200 MB (using the default Node.js heap size). 347 | 348 | During decoding, `avro-js`, `protobufjs`, `js-binary`, `Protobuf (Mixed)` all performed equally well at about 5 times faster than native JSON at most payload sizes. `bson` performed the slowest at 1.5 times slower. 349 | 350 | ### Compression ratio 351 | 352 | This is a ranking of the encoded size (compared to JSON) of each library: 353 | 354 | 1. `avsc`, `js-binary`: 32%: 355 | 2. `protobuf-js`, `pbf`: 42% 356 | 3. `bser`: 67% 357 | 4. `bson`: 79% 358 | 5. `JSON`: 100% 359 | 360 | ### Maximum payload size 361 | 362 | This is a ranking of the estimated maximum safe payload limit (measured as JSON) each library was able to process: 363 | 364 | 1. `avsc`, `jsbin`, `pbf`, `bser`, `Protobuf (mixed)`: 372 MB 365 | 2. `JSON`: 298 MB 366 | 3. `protobuf-js`: 153 MB 367 | 4. `bson`: 21 MB 368 | 369 | ### Negative effects during decoding 370 | 371 | | |BSON|JSBIN|AVRO|BSER 372 | |---|---|---|---|--- 373 | |Prototype pollution | | |x| 374 | 375 | > This table shows an overview of negative effects during decoding. 376 | 377 | `bson`, `js-binary` and `bser`, all convert cleanly to JavaScript without any detected remnats from the encoding process. 378 | 379 | `avro-js`, contains serialization remnants (such as type name) hidden in the prototype of the decoded objects, but should behave as a normal plain JavaScript object for most purposes. 380 | 381 | ### Remarks 382 | 383 | #### AVRO (Avsc) 384 | 385 | `avsc` has a more flexible schema definition than most other libraries. Normally an Avro schema is defined in JSON as in this example: 386 | 387 | ```typescript 388 | // Type for "float[][]" 389 | const type = avsc.Type.forSchema({ 390 | "type": "array", 391 | "items": { 392 | "type": "array", 393 | "items": "float", 394 | } 395 | }); 396 | type.schema(); // { "type": "array", ... } 397 | ``` 398 | 399 | But in `avsc` the same schema can be deferred from the data as: 400 | 401 | ```typescript 402 | // Type for "float[][]" 403 | const inferredType = avsc.Type.forValue([[0.5]]); 404 | inferredType.schema(); // { "type": "array", ... } 405 | ``` 406 | 407 | Or as a more complex object: 408 | ```typescript 409 | const AVSC_INT = 0; 410 | const AVSC_FLOAT = 0.5; 411 | const AVSC_DOUBLE = Infinity; 412 | 413 | // Type for "{items: { x?: number, y?: number, z?: number}[]}" 414 | const complexType = avsc.Type.forValue({ 415 | items: [ 416 | { x: AVSC_INT, y: AVSC_DOUBLE, z: AVSC_DOUBLE }, 417 | { x: null, y: null, z: null }, 418 | ] 419 | }); 420 | ``` 421 | 422 | 423 | #### JSBIN 424 | 425 | Like `avsc` it also has a more succinct schema definition than most other implementations. 426 | 427 | ``` typescript 428 | // Type for double[][] 429 | // Note that all float types in js-binary is 64-bit 430 | const type = new JsBin.Type([['float']]); 431 | 432 | // Type for {x?: int, y?: double, z?: double}[] 433 | const complexType = new JsBin.Type({ 434 | items: [ 435 | { 'x?': 'int', 'y?': 'float', 'z?': 'float' }, 436 | ], 437 | }); 438 | ``` 439 | 440 | ## Result (extra) 441 | 442 | As mentioned in the Benchmark chapter the original data needed to be simplified to a more verbose format that was supported by all serialization formats. As the unmapped data is more compact and contains less redundant information, additional performance could potentially be gained if the target application uses a similar representation internally. This will be investigated for `avsc`, `js-binary`, `bson` in this section. 443 | 444 | As neither formats support tuples, the tuple will be encoded as a 64-bit float array which is expected to increase encoded size ratio slightly as the first field of the tuple can no longer be encoded as a 32-bit integer. 445 | 446 | `avsc`, `js-binary` also has additional settings such as "optional fields", and `js-binary` has the datatype JSON that will be investigated with regards to performance. 447 | 448 | ### Performance graphs 449 | 450 | #### JSON 451 | 452 | ![Benchmark of binary serialization](img/bench-json-extra.svg) 453 | 454 | > Performance graph of `JSON` with different settings. 455 | 456 | | |JSON|JSON (unmapped) 457 | |---|---|--- 458 | |Size ratio|1.00|0.77 459 | 460 | Switching to unmapped data improved both encoding (1/4 faster) and decoding (1/3 faster). It also reduced size ratio to 0.77 of the original JSON. 461 | 462 | #### AVRO Avsc 463 | 464 | ![Benchmark of binary serialization](img/bench-avro-extra.svg) 465 | 466 | > Performance graph of `avsc` with different settings. 467 | 468 | | |AVRO Ascv|AVRO Ascv (optional)|AVRO Ascv (unmapped)| 469 | |---|---|---|--- 470 | |Size ratio|0.32|0.38|0.49 471 | |Payload limit|372 MB|372 MB|237 MB 472 | 473 | Making all fields `optional` did decrease performance somewhat from about 10 faster to 4 times faster (though still faster than most other formats). It also increases size ratio slightly. 474 | 475 | Switching to `unmapped` data also worsened performance similarly. One plausible explanation could be that the tuples are encoded as a dynamically sized array, which would make sense as the schema does not contain any information about the size of the tuple. However, performance is still good compared to other formats. Size ratio was also increased by 63% which is higher than expected as switching from 2 64-bit + 1 32-bit value to 3 64-bit values would only indicate a 20% increase. 476 | 477 | 478 | #### JSBIN 479 | 480 | ![Benchmark of binary serialization](img/bench-jsbin-extra.svg) 481 | 482 | > Performance graph of `js-binary` with different settings. 483 | 484 | | |JSBIN|JSBIN (optional)|JSBIN (unmapped)|JSBIN JSON (unmapped) 485 | |---|---|---|---|--- 486 | |Size ratio|0.32|0.38|0.48|0.77 487 | 488 | Switching to `unmapped` had a slight improvement on encoding speed, but apart from this `optional` and `unmapped` had almost no impact on performance. 489 | 490 | Increase in size ratio for both `optional` and `unmapped` is identical to `avsc`. 491 | 492 | Encoding all data using the `js-binary` datatype `json` performed almost identically to `JSON (unmapped)` as well as size ratio. This seems to indicate that datatype `json` simply consists of using native JSON to create a string that is then stored as datatype `string` in `js-binary`. 493 | 494 | #### BSON 495 | 496 | ![Benchmark of binary serialization](img/bench-bson-extra.svg) 497 | 498 | > Performance graph of `bson` with different settings. 499 | 500 | | |BSON|BSON (unmapped) 501 | |---|---|--- 502 | |Size ratio|0.79|0.79|0.48|0.77 503 | 504 | Unmapped BSON is still slower than JSON in most cases, but it did receive a performance improvement, especially during decoding. 505 | 506 | For some unexplained reason the encoded size ratio remained the same for both the mapped and unmapped data. This seems to indicate that BSON can optimize duplicate data, although the encoded size ratio is still relatively large compared to other formats. 507 | 508 | 509 | ## Conclusion 510 | 511 | The libraries `js-binary`, `pbf`, `bser` and `bson` all convert cleanly back to JavaScript without any detected remnants from the encoding process. `avro-js` and `js-binary` contained minor remnants. `google-protobuf` and `protons` had major remnants or ramifications. 512 | 513 | Overall `avsc` performed very well in all measurements, was easy to setup and seems to be the overall best performing serialization library. Switching from mandatory to optional fields slightly worsened performance and compression ratio, but still puts it on top. Performance was also slightly worse when processing small arrays compared to similar sized structs. 514 | 515 | `js-binary` performed well in all measurements, was easy to setup and is deserialized cleanly. One disadvantage being that it uses a custom binary format that could make interoperability with other programming languages difficult. Switching from mandatory to optional fields had almost no impact on performance. 516 | 517 | Regarding Protocol Buffer libraries it should be noted that all fields are optional by default in the latest schema version. 518 | 519 | `protobuf-js` was slow at encoding but fast at decoding, `protons` was fast at encoding but very slow at decoding. However, through a combination of using `protons` during encoding and `protobuf-js` during decoding, performance on pair with `js-binary` (with slightly worse encoding size) could be achieved. 520 | 521 | `pbf` performed only slightly better than native JSON but is deserialized cleanly without any remnants. 522 | 523 | `bser` was slower than native JSON at both encoding and decoding (slightly slower than BSON). It was however very good at handling large payloads, and provides a good compression ratio considering it does not require a schema, and is deserialized cleanly. 524 | 525 | `bson` was slower than native JSON at both encoding and decoding, provided only a modest compression ratio, and was bad at handling large payloads, but does not require a schema and is deserialized cleanly. 526 | 527 | `google-protobuf` was slow at encoding, performs average during decoding but might require additional decoding that would further decrease performance, requires extra setup and can cause unexpected renaming of variables. 528 | 529 | -------------------------------------------------------------------------------- /img/bench-bson-extra.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 62 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 345 | 376 | 397 | 409 | 410 | 441 | 452 | 464 | 488 | 500 | 515 | 544 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 582 | 583 | 584 | 585 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 687 | 706 | 727 | 748 | 774 | 794 | 824 | 840 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 938 | 939 | 940 | 981 | 982 | 983 | 1012 | 1013 | 1014 | 1043 | 1044 | 1045 | 1048 | 1049 | 1050 | 1053 | 1054 | 1055 | 1058 | 1059 | 1060 | 1063 | 1064 | 1065 | 1066 | 1077 | 1078 | 1079 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1123 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1219 | 1220 | 1221 | 1222 | 1223 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | 1358 | 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | 1366 | 1367 | 1368 | 1369 | 1370 | 1371 | 1372 | 1373 | 1374 | 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1490 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1582 | 1583 | 1584 | 1585 | 1586 | 1587 | 1588 | 1589 | 1590 | 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1604 | 1605 | 1606 | 1607 | 1648 | 1649 | 1650 | 1691 | 1692 | 1693 | 1722 | 1723 | 1724 | 1753 | 1754 | 1755 | 1758 | 1759 | 1760 | 1763 | 1764 | 1765 | 1768 | 1769 | 1770 | 1773 | 1774 | 1775 | 1776 | 1777 | 1778 | 1779 | 1780 | 1781 | 1782 | 1783 | 1784 | 1785 | -------------------------------------------------------------------------------- /img/bench-json-extra.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 62 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 345 | 376 | 397 | 409 | 410 | 441 | 452 | 464 | 488 | 500 | 515 | 544 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 582 | 583 | 584 | 585 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 602 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 821 | 840 | 861 | 882 | 908 | 928 | 958 | 974 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1072 | 1073 | 1074 | 1115 | 1116 | 1117 | 1120 | 1121 | 1122 | 1125 | 1126 | 1127 | 1130 | 1131 | 1132 | 1135 | 1136 | 1137 | 1138 | 1149 | 1150 | 1151 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 1195 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1250 | 1251 | 1252 | 1253 | 1254 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 1294 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | 1358 | 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | 1366 | 1367 | 1368 | 1369 | 1370 | 1371 | 1372 | 1373 | 1374 | 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 1486 | 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | 1494 | 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | 1503 | 1506 | 1507 | 1508 | 1509 | 1510 | 1511 | 1512 | 1513 | 1514 | 1515 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 1562 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 | 1576 | 1577 | 1578 | 1579 | 1580 | 1581 | 1582 | 1585 | 1586 | 1587 | 1588 | 1589 | 1590 | 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1620 | 1621 | 1622 | 1623 | 1624 | 1625 | 1626 | 1627 | 1628 | 1629 | 1630 | 1631 | 1632 | 1633 | 1634 | 1635 | 1636 | 1637 | 1638 | 1639 | 1640 | 1641 | 1642 | 1643 | 1644 | 1645 | 1686 | 1687 | 1688 | 1729 | 1730 | 1731 | 1734 | 1735 | 1736 | 1739 | 1740 | 1741 | 1744 | 1745 | 1746 | 1749 | 1750 | 1751 | 1752 | 1753 | 1754 | 1755 | 1756 | 1757 | 1758 | 1759 | 1760 | 1761 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.ts?$': 'ts-jest', 4 | }, 5 | testEnvironment: 'node', 6 | testRegex: __dirname + '/src/.*\\.spec\\.ts$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-serialization-benchmark", 3 | "description": "Comparison of JavaScript serialization libraries", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "author": "Mattias E. O. Andersson", 7 | "repository": "github:Adelost/javascript-serialization-benchmark", 8 | "homepage": "https://github.com/Adelost/javascript-serialization-benchmark#readme", 9 | "keywords": [], 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "sideEffects": false, 13 | "scripts": { 14 | "clean": "rimraf dist dist-minified coverage tmp", 15 | "start": "ts-node src/index.ts --expose-gc", 16 | "start-ts": "ts-node src/index.ts --expose-gc", 17 | "start-min": "node minified/index.js --expose-gc", 18 | "build": "tsc", 19 | "clean-build": "npm run clean && npm run build", 20 | "watch": "tsc -w", 21 | "minify": "webpack --display-modules", 22 | "test": "jest", 23 | "coverage": "jest --coverage", 24 | "coverage-upload": "jest --coverage && coveralls < coverage/lcov.info", 25 | "format": "prettier ./src --write", 26 | "format-check": "prettier ./src --check", 27 | "lint": "eslint ./src --ext ts && npm run format-check", 28 | "lint-fix": "npm run format && eslint ./src --ext ts --fix", 29 | "prepublishOnly": "npm run lint && npm run clean-build", 30 | "postversion": "git push && git push --tags" 31 | }, 32 | "dependencies": { 33 | "avro-js": "^1.10.0", 34 | "avsc": "^5.4.21", 35 | "bser": "^2.1.1", 36 | "bson": "^4.0.4", 37 | "google-protobuf": "^4.0.0-rc.1", 38 | "js-binary": "^1.2.0", 39 | "pbf": "^3.2.1", 40 | "protobufjs": "^6.10.1", 41 | "protons": "^1.2.1" 42 | }, 43 | "devDependencies": { 44 | "@types/jest": "25.2.3", 45 | "@typescript-eslint/eslint-plugin": "3.0.0", 46 | "@typescript-eslint/parser": "3.0.0", 47 | "coveralls": "3.1.0", 48 | "eslint": "7.1.0", 49 | "jest": "26.0.1", 50 | "prettier": "2.0.5", 51 | "rimraf": "3.0.2", 52 | "ts-jest": "26.0.0", 53 | "ts-loader": "7.0.4", 54 | "ts-node": "8.10.2", 55 | "typescript": "3.9.3", 56 | "webpack": "4.43.0", 57 | "webpack-cli": "3.3.11" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/_root.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | 3 | // Reliable definition of root directory that should be used for all absolute paths, 4 | // this is especially important during minification step 5 | export const ROOT_DIR = __dirname.replace(/\\/g, '/'); 6 | 7 | // Project directory definition 8 | export const PROJECT_DIR = dirname(ROOT_DIR); 9 | -------------------------------------------------------------------------------- /src/benchmarks.ts: -------------------------------------------------------------------------------- 1 | import avro from 'avro-js'; 2 | import avsc from 'avsc'; 3 | import BSON from 'bson'; 4 | import fs from 'fs'; 5 | import JsBin from 'js-binary'; 6 | import Pbf from 'pbf'; 7 | import protobufJs from 'protobufjs'; 8 | import protons from 'protons'; 9 | import { ROOT_DIR } from './_root'; 10 | import ProtoGoogleSchema from './data/google-protobuf_pb'; 11 | import ProtoPbfSchema from './data/pbf_pb'; 12 | import { benchmark, BenchmarkResult } from './utils/helper'; 13 | import bser from 'bser'; 14 | 15 | export function testJson(testData: any): Promise { 16 | return benchmark({ 17 | data: testData, 18 | encode: data => JSON.stringify(data), 19 | decode: data => JSON.parse(data), 20 | sampleDecoded: data => data.items[0], 21 | encoding: 'utf8', 22 | }); 23 | } 24 | 25 | export function testJsonUnmapped(testData: any): Promise { 26 | return benchmark({ 27 | data: testData, 28 | encode: data => JSON.stringify(data), 29 | decode: data => JSON.parse(data), 30 | sampleDecoded: data => data[0], 31 | encoding: 'utf8', 32 | }); 33 | } 34 | 35 | function createAvroSchemaBase(): any { 36 | return { 37 | name: 'items', 38 | type: 'record', 39 | fields: [ 40 | { 41 | name: 'items', 42 | type: { 43 | type: 'array', 44 | items: { 45 | name: 'item', 46 | type: 'record', 47 | fields: [ 48 | { name: 'x', type: 'int' }, 49 | { name: 'y', type: 'double' }, 50 | { name: 'z', type: 'double' }, 51 | ], 52 | }, 53 | }, 54 | }, 55 | ], 56 | } 57 | 58 | } 59 | 60 | const AvroJsSchema = avro.parse(createAvroSchemaBase()); 61 | 62 | export function testAvroJs(testData: any): Promise { 63 | const Schema = AvroJsSchema; 64 | return benchmark({ 65 | data: testData, 66 | encode: data => Schema.toBuffer(data), 67 | decode: data => Schema.fromBuffer(data), 68 | sampleDecoded: data => data.items[0], 69 | }); 70 | } 71 | 72 | const AVSC_DOUBLE = Infinity; 73 | const AVSC_INT = 0; 74 | 75 | const AvroAvscSchema = avsc.Type.forValue({ 76 | items: [ 77 | { x: AVSC_INT, y: AVSC_DOUBLE, z: AVSC_DOUBLE } 78 | ] 79 | }); 80 | 81 | const AvroAvscOptionalSchema = avsc.Type.forValue({ 82 | items: [ 83 | { x: AVSC_INT, y: AVSC_DOUBLE, z: AVSC_DOUBLE }, 84 | { x: null, y: null, z: null } 85 | ] 86 | }); 87 | 88 | const AvroAvscUnmappedSchema = avsc.Type.forValue([[AVSC_DOUBLE]]); 89 | 90 | export function testAvroAvsc(testData: any): Promise { 91 | const Schema = AvroAvscSchema; 92 | return benchmark({ 93 | data: testData, 94 | encode: data => Schema.toBuffer(data), 95 | decode: data => Schema.fromBuffer(data), 96 | sampleDecoded: data => data.items[0], 97 | }); 98 | } 99 | 100 | export function testAvroAvscOptional(testData: any): Promise { 101 | const Schema = AvroAvscOptionalSchema; 102 | return benchmark({ 103 | data: testData, 104 | encode: data => Schema.toBuffer(data), 105 | decode: data => Schema.fromBuffer(data), 106 | sampleDecoded: data => data.items[0], 107 | }); 108 | } 109 | 110 | export function testAvroAvscUnmapped(testData: any): Promise { 111 | const Schema = AvroAvscUnmappedSchema; 112 | return benchmark({ 113 | data: testData, 114 | encode: data => Schema.toBuffer(data), 115 | decode: data => Schema.fromBuffer(data), 116 | sampleDecoded: data => data[0], 117 | }); 118 | } 119 | 120 | const JsBinSchema = new JsBin.Type({ 121 | items: [ 122 | { x: 'int', y: 'float', z: 'float' }, // Note all float types in schema is 64-bit 123 | ], 124 | }); 125 | 126 | export function testJsBin(testData: any): Promise { 127 | const Schema = JsBinSchema; 128 | return benchmark({ 129 | data: testData, 130 | encode: data => Schema.encode(data), 131 | decode: data => Schema.decode(data), 132 | sampleDecoded: data => data.items[0], 133 | }); 134 | } 135 | 136 | const JsBinOptionalSchema = new JsBin.Type({ 137 | items: [ 138 | { 'x?': 'int', 'y?': 'float', 'z?': 'float' }, 139 | ], 140 | }); 141 | 142 | export function testJsBinOptional(testData: any): Promise { 143 | const Schema = JsBinOptionalSchema; 144 | return benchmark({ 145 | data: testData, 146 | encode: data => Schema.encode(data), 147 | decode: data => Schema.decode(data), 148 | sampleDecoded: data => data.items[0], 149 | }); 150 | } 151 | 152 | const JsBinUnmappedSchema = new JsBin.Type([['float']]); 153 | 154 | export function testJsBinUnmapped(testData: any): Promise { 155 | const Schema = JsBinUnmappedSchema; 156 | return benchmark({ 157 | data: testData, 158 | encode: data => Schema.encode(data), 159 | decode: data => Schema.decode(data), 160 | sampleDecoded: data => data[0], 161 | }); 162 | } 163 | 164 | const JsBinJsonUnmappedSchema = new JsBin.Type('json'); 165 | 166 | export function testJsBinJsonUnmapped(testData: any): Promise { 167 | const Schema = JsBinJsonUnmappedSchema; 168 | return benchmark({ 169 | data: testData, 170 | encode: data => Schema.encode(data), 171 | decode: data => Schema.decode(data), 172 | sampleDecoded: data => data[0], 173 | }); 174 | } 175 | 176 | let ProtobufJsSchema: any = null; 177 | 178 | export async function testProtoJs(testData: any): Promise { 179 | if (!ProtobufJsSchema) ProtobufJsSchema = await protobufJs.load(`${ROOT_DIR}/data/test.proto`); 180 | const Schema = ProtobufJsSchema.Items; 181 | return benchmark({ 182 | data: testData, 183 | encode: data => Schema.encode(data).finish(), 184 | decode: data => Schema.decode(data), 185 | sampleDecoded: data => data.items[0], 186 | }); 187 | } 188 | 189 | export async function testProtoPbf(testData: any): Promise { 190 | const Schema = ProtoPbfSchema.Items; 191 | return benchmark({ 192 | data: testData, 193 | encode: data => { 194 | const pbf = new Pbf(); 195 | Schema.write(data, pbf); 196 | return pbf.finish(); 197 | }, 198 | decode: data => { 199 | const pbf = new Pbf(data); 200 | return Schema.read(pbf); 201 | }, 202 | sampleDecoded: data => data.items[0], 203 | }); 204 | } 205 | 206 | export function testProtoGoogle(testData: any): Promise { 207 | const Schema = ProtoGoogleSchema; 208 | const ItemsWrap = Schema.Items; 209 | const ItemWrap = Schema.Item; 210 | return benchmark({ 211 | data: testData, 212 | encode: data => { 213 | const itemsWrap = new ItemsWrap(); 214 | const itemWraps = data.items.map(item => { 215 | const itemWrap = new ItemWrap(); 216 | itemWrap.setX(item.x); 217 | itemWrap.setY(item.y); 218 | itemWrap.setZ(item.z); 219 | return itemWrap; 220 | }); 221 | itemsWrap.setItemsList(itemWraps); 222 | return itemsWrap.serializeBinary(); 223 | }, 224 | decode: data => ItemsWrap.deserializeBinary(data), 225 | // sampleDecoded: data => data.itemsList[0], 226 | sampleDecoded: data => data.getItemsList()[0].toObject(), 227 | }); 228 | } 229 | 230 | const ProtobufProtonsSchema = protons(fs.readFileSync(`${ROOT_DIR}/data/test.proto`)); 231 | 232 | export function testProtoProtons(testData: any): Promise { 233 | const Schema = ProtobufProtonsSchema.Items; 234 | return benchmark({ 235 | data: testData, 236 | encode: data => Schema.encode(data), 237 | decode: data => Schema.decode(data), 238 | sampleDecoded: data => JSON.parse(JSON.stringify(data.items[0])), 239 | }); 240 | } 241 | 242 | const ProtoMixedEncodeSchema = protons(fs.readFileSync(`${ROOT_DIR}/data/test.proto`)); 243 | let ProtoMixedDecodeSchema: any = null; 244 | 245 | 246 | export async function testProtoMixed(testData: any): Promise { 247 | const EncodeSchema = ProtoMixedEncodeSchema.Items; 248 | if (!ProtoMixedDecodeSchema) ProtoMixedDecodeSchema = await protobufJs.load(`${ROOT_DIR}/data/test.proto`); 249 | const DecodeSchema = ProtoMixedDecodeSchema.Items; 250 | return benchmark({ 251 | data: testData, 252 | encode: data => EncodeSchema.encode(data), 253 | decode: data => DecodeSchema.decode(data), 254 | sampleDecoded: data => data.items[0], 255 | }); 256 | } 257 | 258 | export function testBson(testData: any): Promise { 259 | return benchmark({ 260 | data: testData, 261 | encode: data => BSON.serialize(data), 262 | decode: data => BSON.deserialize(data), 263 | sampleDecoded: data => data.items[0], 264 | }); 265 | } 266 | 267 | export function testBsonUnmapped(testData: any): Promise { 268 | return benchmark({ 269 | data: testData, 270 | encode: data => BSON.serialize(data), 271 | decode: data => BSON.deserialize(data), 272 | sampleDecoded: data => data[0], 273 | }); 274 | } 275 | 276 | export function testBser(testData: any): Promise { 277 | return benchmark({ 278 | data: testData, 279 | encode: data => bser.dumpToBuffer(data), 280 | decode: data => bser.loadFromBuffer(data), 281 | sampleDecoded: data => data.items[0], 282 | }); 283 | } 284 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const LOGGING = false; 2 | export const SPEED_FRACTION_NORMAL = 8; 3 | export const SPEED_FRACTION_FAST = 4; 4 | export const FAST_SPEED_LIMIT_MB = 10; 5 | 6 | export const MIN_SIZE_IN_MB = 0.1; 7 | 8 | export const MAX_SIZE_IN_MB = 299; 9 | // export const MAX_SIZE_IN_MB = 299; // MAX SIZE JSON 10 | // export const MAX_SIZE_IN_MB = 372; // MAX SIZE TEST DATA 11 | 12 | 13 | export const START_ITERATION = 0; 14 | // export const START_ITERATION = 80; // 3 MB 15 | // export const START_ITERATION = 85; // 5 MB 16 | // export const START_ITERATION = 90; // 10 MB 17 | // export const START_ITERATION = 95; // 31 MB 18 | // export const START_ITERATION = 100; // 97 MB 19 | // export const START_ITERATION = 105; // 297 MB 20 | // export const START_ITERATION = 106; // 371 MB 21 | // 22 | -------------------------------------------------------------------------------- /src/data/google-protobuf_pb.js: -------------------------------------------------------------------------------- 1 | // source: test.proto 2 | /** 3 | * @fileoverview 4 | * @enhanceable 5 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 6 | * field starts with 'MSG_' and isn't a translatable message. 7 | * @public 8 | */ 9 | // GENERATED CODE -- DO NOT EDIT! 10 | 11 | var jspb = require('google-protobuf'); 12 | var goog = jspb; 13 | var global = Function('return this')(); 14 | 15 | goog.exportSymbol('proto.Item', null, global); 16 | goog.exportSymbol('proto.Items', null, global); 17 | /** 18 | * Generated by JsPbCodeGenerator. 19 | * @param {Array=} opt_data Optional initial data array, typically from a 20 | * server response, or constructed directly in Javascript. The array is used 21 | * in place and becomes part of the constructed object. It is not cloned. 22 | * If no data is provided, the constructed object will be empty, but still 23 | * valid. 24 | * @extends {jspb.Message} 25 | * @constructor 26 | */ 27 | proto.Item = function(opt_data) { 28 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 29 | }; 30 | goog.inherits(proto.Item, jspb.Message); 31 | if (goog.DEBUG && !COMPILED) { 32 | /** 33 | * @public 34 | * @override 35 | */ 36 | proto.Item.displayName = 'proto.Item'; 37 | } 38 | /** 39 | * Generated by JsPbCodeGenerator. 40 | * @param {Array=} opt_data Optional initial data array, typically from a 41 | * server response, or constructed directly in Javascript. The array is used 42 | * in place and becomes part of the constructed object. It is not cloned. 43 | * If no data is provided, the constructed object will be empty, but still 44 | * valid. 45 | * @extends {jspb.Message} 46 | * @constructor 47 | */ 48 | proto.Items = function(opt_data) { 49 | jspb.Message.initialize(this, opt_data, 0, -1, proto.Items.repeatedFields_, null); 50 | }; 51 | goog.inherits(proto.Items, jspb.Message); 52 | if (goog.DEBUG && !COMPILED) { 53 | /** 54 | * @public 55 | * @override 56 | */ 57 | proto.Items.displayName = 'proto.Items'; 58 | } 59 | 60 | 61 | 62 | if (jspb.Message.GENERATE_TO_OBJECT) { 63 | /** 64 | * Creates an object representation of this proto. 65 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 66 | * Optional fields that are not set will be set to undefined. 67 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 68 | * For the list of reserved names please see: 69 | * net/proto2/compiler/js/internal/generator.cc#kKeyword. 70 | * @param {boolean=} opt_includeInstance Deprecated. whether to include the 71 | * JSPB instance for transitional soy proto support: 72 | * http://goto/soy-param-migration 73 | * @return {!Object} 74 | */ 75 | proto.Item.prototype.toObject = function(opt_includeInstance) { 76 | return proto.Item.toObject(opt_includeInstance, this); 77 | }; 78 | 79 | 80 | /** 81 | * Static version of the {@see toObject} method. 82 | * @param {boolean|undefined} includeInstance Deprecated. Whether to include 83 | * the JSPB instance for transitional soy proto support: 84 | * http://goto/soy-param-migration 85 | * @param {!proto.Item} msg The msg instance to transform. 86 | * @return {!Object} 87 | * @suppress {unusedLocalVariables} f is only used for nested messages 88 | */ 89 | proto.Item.toObject = function(includeInstance, msg) { 90 | var f, obj = { 91 | x: jspb.Message.getFieldWithDefault(msg, 1, 0), 92 | y: jspb.Message.getFloatingPointFieldWithDefault(msg, 2, 0.0), 93 | z: jspb.Message.getFloatingPointFieldWithDefault(msg, 3, 0.0) 94 | }; 95 | 96 | if (includeInstance) { 97 | obj.$jspbMessageInstance = msg; 98 | } 99 | return obj; 100 | }; 101 | } 102 | 103 | 104 | /** 105 | * Deserializes binary data (in protobuf wire format). 106 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 107 | * @return {!proto.Item} 108 | */ 109 | proto.Item.deserializeBinary = function(bytes) { 110 | var reader = new jspb.BinaryReader(bytes); 111 | var msg = new proto.Item; 112 | return proto.Item.deserializeBinaryFromReader(msg, reader); 113 | }; 114 | 115 | 116 | /** 117 | * Deserializes binary data (in protobuf wire format) from the 118 | * given reader into the given message object. 119 | * @param {!proto.Item} msg The message object to deserialize into. 120 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 121 | * @return {!proto.Item} 122 | */ 123 | proto.Item.deserializeBinaryFromReader = function(msg, reader) { 124 | while (reader.nextField()) { 125 | if (reader.isEndGroup()) { 126 | break; 127 | } 128 | var field = reader.getFieldNumber(); 129 | switch (field) { 130 | case 1: 131 | var value = /** @type {number} */ (reader.readInt32()); 132 | msg.setX(value); 133 | break; 134 | case 2: 135 | var value = /** @type {number} */ (reader.readDouble()); 136 | msg.setY(value); 137 | break; 138 | case 3: 139 | var value = /** @type {number} */ (reader.readDouble()); 140 | msg.setZ(value); 141 | break; 142 | default: 143 | reader.skipField(); 144 | break; 145 | } 146 | } 147 | return msg; 148 | }; 149 | 150 | 151 | /** 152 | * Serializes the message to binary data (in protobuf wire format). 153 | * @return {!Uint8Array} 154 | */ 155 | proto.Item.prototype.serializeBinary = function() { 156 | var writer = new jspb.BinaryWriter(); 157 | proto.Item.serializeBinaryToWriter(this, writer); 158 | return writer.getResultBuffer(); 159 | }; 160 | 161 | 162 | /** 163 | * Serializes the given message to binary data (in protobuf wire 164 | * format), writing to the given BinaryWriter. 165 | * @param {!proto.Item} message 166 | * @param {!jspb.BinaryWriter} writer 167 | * @suppress {unusedLocalVariables} f is only used for nested messages 168 | */ 169 | proto.Item.serializeBinaryToWriter = function(message, writer) { 170 | var f = undefined; 171 | f = message.getX(); 172 | if (f !== 0) { 173 | writer.writeInt32( 174 | 1, 175 | f 176 | ); 177 | } 178 | f = message.getY(); 179 | if (f !== 0.0) { 180 | writer.writeDouble( 181 | 2, 182 | f 183 | ); 184 | } 185 | f = message.getZ(); 186 | if (f !== 0.0) { 187 | writer.writeDouble( 188 | 3, 189 | f 190 | ); 191 | } 192 | }; 193 | 194 | 195 | /** 196 | * optional int32 x = 1; 197 | * @return {number} 198 | */ 199 | proto.Item.prototype.getX = function() { 200 | return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); 201 | }; 202 | 203 | 204 | /** 205 | * @param {number} value 206 | * @return {!proto.Item} returns this 207 | */ 208 | proto.Item.prototype.setX = function(value) { 209 | return jspb.Message.setProto3IntField(this, 1, value); 210 | }; 211 | 212 | 213 | /** 214 | * optional double y = 2; 215 | * @return {number} 216 | */ 217 | proto.Item.prototype.getY = function() { 218 | return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 2, 0.0)); 219 | }; 220 | 221 | 222 | /** 223 | * @param {number} value 224 | * @return {!proto.Item} returns this 225 | */ 226 | proto.Item.prototype.setY = function(value) { 227 | return jspb.Message.setProto3FloatField(this, 2, value); 228 | }; 229 | 230 | 231 | /** 232 | * optional double z = 3; 233 | * @return {number} 234 | */ 235 | proto.Item.prototype.getZ = function() { 236 | return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 3, 0.0)); 237 | }; 238 | 239 | 240 | /** 241 | * @param {number} value 242 | * @return {!proto.Item} returns this 243 | */ 244 | proto.Item.prototype.setZ = function(value) { 245 | return jspb.Message.setProto3FloatField(this, 3, value); 246 | }; 247 | 248 | 249 | 250 | /** 251 | * List of repeated fields within this message type. 252 | * @private {!Array} 253 | * @const 254 | */ 255 | proto.Items.repeatedFields_ = [1]; 256 | 257 | 258 | 259 | if (jspb.Message.GENERATE_TO_OBJECT) { 260 | /** 261 | * Creates an object representation of this proto. 262 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 263 | * Optional fields that are not set will be set to undefined. 264 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 265 | * For the list of reserved names please see: 266 | * net/proto2/compiler/js/internal/generator.cc#kKeyword. 267 | * @param {boolean=} opt_includeInstance Deprecated. whether to include the 268 | * JSPB instance for transitional soy proto support: 269 | * http://goto/soy-param-migration 270 | * @return {!Object} 271 | */ 272 | proto.Items.prototype.toObject = function(opt_includeInstance) { 273 | return proto.Items.toObject(opt_includeInstance, this); 274 | }; 275 | 276 | 277 | /** 278 | * Static version of the {@see toObject} method. 279 | * @param {boolean|undefined} includeInstance Deprecated. Whether to include 280 | * the JSPB instance for transitional soy proto support: 281 | * http://goto/soy-param-migration 282 | * @param {!proto.Items} msg The msg instance to transform. 283 | * @return {!Object} 284 | * @suppress {unusedLocalVariables} f is only used for nested messages 285 | */ 286 | proto.Items.toObject = function(includeInstance, msg) { 287 | var f, obj = { 288 | itemsList: jspb.Message.toObjectList(msg.getItemsList(), 289 | proto.Item.toObject, includeInstance) 290 | }; 291 | 292 | if (includeInstance) { 293 | obj.$jspbMessageInstance = msg; 294 | } 295 | return obj; 296 | }; 297 | } 298 | 299 | 300 | /** 301 | * Deserializes binary data (in protobuf wire format). 302 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 303 | * @return {!proto.Items} 304 | */ 305 | proto.Items.deserializeBinary = function(bytes) { 306 | var reader = new jspb.BinaryReader(bytes); 307 | var msg = new proto.Items; 308 | return proto.Items.deserializeBinaryFromReader(msg, reader); 309 | }; 310 | 311 | 312 | /** 313 | * Deserializes binary data (in protobuf wire format) from the 314 | * given reader into the given message object. 315 | * @param {!proto.Items} msg The message object to deserialize into. 316 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 317 | * @return {!proto.Items} 318 | */ 319 | proto.Items.deserializeBinaryFromReader = function(msg, reader) { 320 | while (reader.nextField()) { 321 | if (reader.isEndGroup()) { 322 | break; 323 | } 324 | var field = reader.getFieldNumber(); 325 | switch (field) { 326 | case 1: 327 | var value = new proto.Item; 328 | reader.readMessage(value,proto.Item.deserializeBinaryFromReader); 329 | msg.addItems(value); 330 | break; 331 | default: 332 | reader.skipField(); 333 | break; 334 | } 335 | } 336 | return msg; 337 | }; 338 | 339 | 340 | /** 341 | * Serializes the message to binary data (in protobuf wire format). 342 | * @return {!Uint8Array} 343 | */ 344 | proto.Items.prototype.serializeBinary = function() { 345 | var writer = new jspb.BinaryWriter(); 346 | proto.Items.serializeBinaryToWriter(this, writer); 347 | return writer.getResultBuffer(); 348 | }; 349 | 350 | 351 | /** 352 | * Serializes the given message to binary data (in protobuf wire 353 | * format), writing to the given BinaryWriter. 354 | * @param {!proto.Items} message 355 | * @param {!jspb.BinaryWriter} writer 356 | * @suppress {unusedLocalVariables} f is only used for nested messages 357 | */ 358 | proto.Items.serializeBinaryToWriter = function(message, writer) { 359 | var f = undefined; 360 | f = message.getItemsList(); 361 | if (f.length > 0) { 362 | writer.writeRepeatedMessage( 363 | 1, 364 | f, 365 | proto.Item.serializeBinaryToWriter 366 | ); 367 | } 368 | }; 369 | 370 | 371 | /** 372 | * repeated Item items = 1; 373 | * @return {!Array} 374 | */ 375 | proto.Items.prototype.getItemsList = function() { 376 | return /** @type{!Array} */ ( 377 | jspb.Message.getRepeatedWrapperField(this, proto.Item, 1)); 378 | }; 379 | 380 | 381 | /** 382 | * @param {!Array} value 383 | * @return {!proto.Items} returns this 384 | */ 385 | proto.Items.prototype.setItemsList = function(value) { 386 | return jspb.Message.setRepeatedWrapperField(this, 1, value); 387 | }; 388 | 389 | 390 | /** 391 | * @param {!proto.Item=} opt_value 392 | * @param {number=} opt_index 393 | * @return {!proto.Item} 394 | */ 395 | proto.Items.prototype.addItems = function(opt_value, opt_index) { 396 | return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.Item, opt_index); 397 | }; 398 | 399 | 400 | /** 401 | * Clears the list making it empty but non-null. 402 | * @return {!proto.Items} returns this 403 | */ 404 | proto.Items.prototype.clearItemsList = function() { 405 | return this.setItemsList([]); 406 | }; 407 | 408 | 409 | goog.object.extend(exports, proto); 410 | -------------------------------------------------------------------------------- /src/data/pbf_pb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // code generated by pbf v3.2.1 2 | 3 | // Item ======================================== 4 | 5 | var Item = exports.Item = {}; 6 | 7 | Item.read = function (pbf, end) { 8 | return pbf.readFields(Item._readField, {x: 0, y: 0, z: 0}, end); 9 | }; 10 | Item._readField = function (tag, obj, pbf) { 11 | if (tag === 1) obj.x = pbf.readVarint(true); 12 | else if (tag === 2) obj.y = pbf.readDouble(); 13 | else if (tag === 3) obj.z = pbf.readDouble(); 14 | }; 15 | Item.write = function (obj, pbf) { 16 | if (obj.x) pbf.writeVarintField(1, obj.x); 17 | if (obj.y) pbf.writeDoubleField(2, obj.y); 18 | if (obj.z) pbf.writeDoubleField(3, obj.z); 19 | }; 20 | 21 | // Items ======================================== 22 | 23 | var Items = exports.Items = {}; 24 | 25 | Items.read = function (pbf, end) { 26 | return pbf.readFields(Items._readField, {items: []}, end); 27 | }; 28 | Items._readField = function (tag, obj, pbf) { 29 | if (tag === 1) obj.items.push(Item.read(pbf, pbf.readVarint() + pbf.pos)); 30 | }; 31 | Items.write = function (obj, pbf) { 32 | if (obj.items) for (var i = 0; i < obj.items.length; i++) pbf.writeMessage(1, Item.write, obj.items[i]); 33 | }; 34 | -------------------------------------------------------------------------------- /src/data/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Item { 4 | int32 x = 1; 5 | double y = 2; 6 | double z = 3; 7 | } 8 | 9 | message Items { 10 | repeated Item items = 1; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as bench from './benchmarks'; 2 | import { runTest } from './utils/helper'; 3 | 4 | 5 | const TESTS = { 6 | testJson: () => runTest('JSON', ({ data }) => bench.testJson(data), 298), 7 | 8 | testBson: () => runTest('BSON', ({ data }) => bench.testBson(data), 21), 9 | 10 | testAvroJs: () => runTest('AVRO JS', ({ data }) => bench.testAvroJs(data), 372), 11 | testAvroAvsc: () => runTest('AVRO Avsc', ({ data }) => bench.testAvroAvsc(data), 372), 12 | testAvroAvscOptional: () => runTest('AVRO Avsc (optional)', ({ data }) => bench.testAvroAvscOptional(data), 372), 13 | 14 | 15 | testProtoJs: () => runTest('PROTOBUF JS', ({ data }) => bench.testProtoJs(data), 153), 16 | testProtoPbf: () => runTest('PROTOBUF Pbf', ({ data }) => bench.testProtoPbf(data), 372), 17 | testProtoGoogle: () => runTest('PROTOBUF Google', ({ data }) => bench.testProtoGoogle(data), 98), 18 | testProtoProtons: () => runTest('PROTOBUF Protons', ({ data }) => bench.testProtoProtons(data), 40), 19 | testProtoMixed: () => runTest('PROTOBUF mixed', ({ data }) => bench.testProtoMixed(data), 372), 20 | 21 | testJsBin: () => runTest('JSBIN', ({ data }) => bench.testJsBin(data), 372), 22 | testJsBinOptional: () => runTest('JSBIN (optional)', ({ data }) => bench.testJsBinOptional(data), 372), 23 | 24 | testBser: () => runTest('BSER', ({ data }) => bench.testBser(data), 372), 25 | 26 | testJsonUnmapped: () => runTest('JSON (unmapped)', ({ unmappedData }) => bench.testJsonUnmapped(unmappedData), 298), 27 | testAvroAvscUnmapped: () => runTest('AVRO Avsc (unmapped)', ({ unmappedData }) => bench.testAvroAvscUnmapped(unmappedData), 237), 28 | testJsBinUnmapped: () => runTest('JSBIN (unmapped)', ({ unmappedData }) => bench.testJsBinUnmapped(unmappedData), 372), 29 | testJsBinJsonUnmapped: () => runTest('JSBIN JSON (unmapped)', ({ unmappedData }) => bench.testJsBinJsonUnmapped(unmappedData), 372), 30 | testBsonUnmapped: () => runTest('BSON (unmapped)', ({ unmappedData }) => bench.testBsonUnmapped(unmappedData), 21), 31 | }; 32 | 33 | 34 | (async () => { 35 | const args = process.argv.slice(3); 36 | console.log('Arguments', args); 37 | if (args.length) { 38 | for (const arg of args) { 39 | if (TESTS[arg]) { 40 | await TESTS[arg](); 41 | } 42 | } 43 | } else { 44 | await runDefault(); 45 | } 46 | })(); 47 | 48 | async function runDefault() { 49 | console.log('Running default'); 50 | await TESTS.testJson(); 51 | 52 | await TESTS.testBson(); 53 | 54 | await TESTS.testAvroJs(); 55 | await TESTS.testAvroAvsc(); 56 | await TESTS.testAvroAvscOptional(); 57 | 58 | await TESTS.testProtoJs(); 59 | await TESTS.testProtoPbf(); 60 | await TESTS.testProtoGoogle(); 61 | await TESTS.testProtoProtons(); 62 | await TESTS.testProtoMixed(); 63 | 64 | await TESTS.testJsBin(); 65 | await TESTS.testJsBinOptional(); 66 | 67 | await TESTS.testBser(); 68 | 69 | await TESTS.testJsonUnmapped(); 70 | await TESTS.testAvroAvscUnmapped(); 71 | await TESTS.testJsBinUnmapped(); 72 | await TESTS.testJsBinJsonUnmapped(); 73 | await TESTS.testBsonUnmapped(); 74 | } 75 | -------------------------------------------------------------------------------- /src/plot.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import matplotlib.pyplot as plt 4 | import json as json 5 | 6 | INPUT_FILE = 'tmp/plot.json' 7 | OUTPUT_DIR = '../img' 8 | OUTPUT_NAME = 'tmp.svg' 9 | 10 | PRINT_SIZE_RATIO = False 11 | SAVE_IMAGE = True 12 | ONLY_RATIO = False 13 | 14 | X_LABEL = 'JSON size (MB)' 15 | X_MIN_VALUE = 0.5 16 | X_MAX_VALUE = 2000 17 | ALLOWED_LABELS = [] 18 | 19 | 20 | def main(): 21 | global OUTPUT_NAME 22 | global ALLOWED_LABELS 23 | global ONLY_RATIO 24 | global PRINT_SIZE_RATIO 25 | global X_MIN_VALUE 26 | 27 | # # Hack to print size ratios at 10 MB (ignore displayed image) 28 | X_MIN_VALUE = 10 29 | PRINT_SIZE_RATIO = True 30 | plot() 31 | PRINT_SIZE_RATIO = False 32 | X_MIN_VALUE = 0.5 33 | 34 | # Main test 35 | OUTPUT_NAME='bench-full.svg' 36 | ALLOWED_LABELS = [ 37 | "JSON", 38 | "JSBIN", 39 | "AVRO Avsc", 40 | "BSER", 41 | "BSON", 42 | "PROTOBUF JS", 43 | "PROTOBUF Pbf", 44 | "PROTOBUF mixed", 45 | 46 | ] 47 | plot() 48 | # 49 | # Protocol buffers 50 | OUTPUT_NAME='bench-protobuf.svg' 51 | ALLOWED_LABELS = [ 52 | "JSON", 53 | "PROTOBUF JS", 54 | "PROTOBUF Pbf", 55 | "PROTOBUF Protons", 56 | "PROTOBUF Google", 57 | "PROTOBUF mixed", 58 | ] 59 | plot() 60 | 61 | # Result extra 62 | OUTPUT_NAME='bench-unmapped.svg' 63 | ONLY_RATIO = True 64 | 65 | # JSON extra 66 | OUTPUT_NAME='bench-json-extra.svg' 67 | ALLOWED_LABELS = [ 68 | "JSON", 69 | "JSON (unmapped)", 70 | ] 71 | plot() 72 | 73 | # Avro extra 74 | OUTPUT_NAME='bench-avro-extra.svg' 75 | ALLOWED_LABELS = [ 76 | "JSON", 77 | "JSON (unmapped)", 78 | "AVRO Avsc", 79 | "AVRO Avsc (optional)", 80 | "AVRO Avsc (unmapped)", 81 | ] 82 | plot() 83 | 84 | # BSON extra 85 | OUTPUT_NAME='bench-bson-extra.svg' 86 | ALLOWED_LABELS = [ 87 | "JSON", 88 | "JSON (unmapped)", 89 | "BSON", 90 | "BSON (unmapped)", 91 | ] 92 | plot() 93 | 94 | # JSBIN extra 95 | OUTPUT_NAME='bench-jsbin-extra.svg' 96 | ALLOWED_LABELS = [ 97 | "JSON", 98 | "JSON (unmapped)", 99 | "JSBIN", 100 | "JSBIN (optional)", 101 | "JSBIN (unmapped)", 102 | "JSBIN JSON (unmapped)", 103 | ] 104 | plot() 105 | 106 | def plot(): 107 | columns = 3 108 | if ALLOWED_LABELS: 109 | columns = math.ceil(len(ALLOWED_LABELS) / 2) 110 | if PRINT_SIZE_RATIO: 111 | global X_MIN_VALUE 112 | X_MIN_VALUE = 10 113 | ax = plot_x('JSON', 'encodedSize', 1, 'Encoded size', True) 114 | ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.38), ncol=columns, fancybox=True) 115 | else: 116 | if ONLY_RATIO: 117 | plt.figure(figsize=(10, 4.15)) 118 | ax = plot_x('JSON', 'encodedTime', 1, 'Encode time (ratio)', False) 119 | ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.38), ncol=columns, fancybox=True) 120 | plot_x('JSON', 'decodedTime', 2, 'Decode time (ratio)', False) 121 | else: 122 | plt.figure(figsize=(10, 8.5)) 123 | ax = plot_x(None, 'encodedTime', 1, 'Encode time (s)', True) 124 | ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.5), ncol=columns, fancybox=True) 125 | plot_x('JSON', 'encodedTime', 2, 'Encode time (ratio)', False) 126 | plot_x(None, 'decodedTime', 3, 'Decode time (s)', True) 127 | plot_x('JSON', 'decodedTime', 4, 'Decode time (ratio)', False) 128 | 129 | if SAVE_IMAGE: 130 | plt.savefig(f'{OUTPUT_DIR}/{OUTPUT_NAME}') 131 | else: 132 | plt.show() 133 | 134 | 135 | with open(INPUT_FILE) as json_file: 136 | TEST_DATA = json.load(json_file) 137 | 138 | 139 | def plot_x(baselineKey, yKey, id, yLabel, log): 140 | baseline = None 141 | if baselineKey: 142 | baseline = TEST_DATA[baselineKey] 143 | if ONLY_RATIO: 144 | plt.subplot(2, 1, id) 145 | else: 146 | plt.subplot(4, 1, id) 147 | if ALLOWED_LABELS: 148 | keys = ALLOWED_LABELS 149 | else: 150 | keys = TEST_DATA 151 | 152 | for key in keys: 153 | item = TEST_DATA[key] 154 | x_values = item['x'] 155 | x_start, x_stop = find_x_range(x_values) 156 | y_values = item['y'][yKey] 157 | label = item['label'] 158 | x_values = x_values[x_start:x_stop] 159 | y_values = y_values[x_start:x_stop] 160 | if baseline: 161 | baseline_y_values = baseline['y'][yKey][x_start:x_stop] 162 | y_values_ratios = calc_ratios(y_values, baseline_y_values) 163 | y_values = y_values_ratios 164 | # y_values = pad_y_values(x_values, y_values) 165 | print(f'{label}, {yKey}, start x:{round(x_values[0], 2)}, y:{round(y_values[0], 2)}') 166 | plt.plot(x_values, y_values, label=label) 167 | 168 | ax = plt.gca() 169 | ax.set_xscale('log') 170 | if log: 171 | ax.set_yscale('log') 172 | plt.xlabel(X_LABEL) 173 | plt.ylabel(yLabel or yKey) 174 | plt.grid() 175 | return ax 176 | 177 | 178 | def pad_y_values(x_values, y_values): 179 | diff = len(x_values) - len(y_values) 180 | if diff > 0: 181 | y_values += [None] * diff 182 | return y_values 183 | 184 | 185 | def calc_ratios(y_values, baseline_y_values): 186 | y_values_ratios = [] 187 | for i, v in enumerate(y_values): 188 | if i < len(baseline_y_values) and baseline_y_values[i] is not 0: 189 | ratio = v / baseline_y_values[i] 190 | else: 191 | ratio = 0 192 | y_values_ratios.append(ratio) 193 | return y_values_ratios 194 | 195 | 196 | def find_x_range(xs): 197 | x_start = 0 198 | x_stop = -1 199 | for index, value in enumerate(xs): 200 | if value >= X_MIN_VALUE: 201 | x_start = index 202 | break 203 | for index, value in enumerate(xs): 204 | if value > X_MAX_VALUE: 205 | x_stop = index - 1 206 | break 207 | return x_start, x_stop 208 | 209 | 210 | if __name__ == '__main__': 211 | main() 212 | 213 | # JSON, encodedSize, start x:10.47, y:1.0 214 | # BSON, encodedSize, start x:10.47, y:0.79 215 | # AVRO, encodedSize, start x:10.47, y:0.32 216 | # PROTOBUF JS, encodedSize, start x:10.47, y:0.42 217 | # PROTOBUF Google, encodedSize, start x:10.47, y:0.42 218 | # PROTOBUF Protons, encodedSize, start x:10.47, y:0.42 219 | # PROTOBUF mixed, encodedSize, start x:10.47, y:0.42 220 | # JSBIN, encodedSize, start x:10.47, y:0.32 221 | # JSBIN (optional), encodedSize, start x:10.47, y:0.38 222 | # JSON (unmapped), encodedSize, start x:10.47, y:0.77 223 | # JSBIN (unmapped), encodedSize, start x:10.47, y:0.48 224 | # JSBIN JSON (unmapped), encodedSize, start x:10.47, y:0.77 225 | # BSON (unmapped), encodedSize, start x:10.47, y:0.79 226 | -------------------------------------------------------------------------------- /src/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm start 'testJson' 4 | 5 | npm start 'testBson' 6 | 7 | npm start 'testAvroJs' 8 | npm start 'testAvroAvsc' 9 | npm start 'testAvroAvscOptional' 10 | 11 | npm start 'testProtoJs' 12 | npm start 'testProtoPbf' 13 | npm start 'testProtoGoogle' 14 | npm start 'testProtoProtons' 15 | npm start 'testProtoMixed' 16 | 17 | npm start 'testJsBin' 18 | npm start 'testJsBinOptional' 19 | 20 | npm start 'testBser' 21 | 22 | npm start 'testJsonUnmapped' 23 | npm start 'testAvroAvscUnmapped' 24 | npm start 'testJsBinUnmapped' 25 | npm start 'testJsBinJsonUnmapped' 26 | npm start 'testBsonUnmapped' 27 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { ROOT_DIR } from '../_root'; 3 | import { 4 | FAST_SPEED_LIMIT_MB, 5 | MAX_SIZE_IN_MB, 6 | MIN_SIZE_IN_MB, 7 | SPEED_FRACTION_FAST, 8 | SPEED_FRACTION_NORMAL, 9 | START_ITERATION, 10 | } from '../config'; 11 | import { bytesToMb, getNanoTime, log, runGarbageCollection, secondsFromNanoTime, sleep } from './utils'; 12 | 13 | const PLOT_FILE_PATH = `${ROOT_DIR}/tmp/plot.json`; 14 | 15 | 16 | export async function runTest(label: string, fn: (payload) => Promise, limit: number) { 17 | console.log(`TEST ${label}`); 18 | const start = getNanoTime(); 19 | const xValues: number[] = []; 20 | const results: BenchmarkResult[] = []; 21 | let unmappedData = [ 22 | [1, 4.1040000000000004, 1.1009999999999994], 23 | [2, 4.371033333333333, 0.36703333333333266], 24 | [3, 5.171833333333334, 0.4337666666666671], 25 | [4, 6.473133333333333, 0.36703333333333354], 26 | ]; 27 | let data: any = null; 28 | let sizeInMb = 0; 29 | let i = 0; 30 | while (true) { 31 | let fractionIncrement = sizeInMb > FAST_SPEED_LIMIT_MB ? SPEED_FRACTION_FAST : SPEED_FRACTION_NORMAL; 32 | const dataIncrement = unmappedData 33 | // Increase size by fraction limit 34 | .slice(0, Math.round(unmappedData.length / fractionIncrement)) 35 | // Slightly alter data to make sure each value is unique 36 | .map(value => [value[0] + 1, value[1] + 1 / 3, value[2] + 1 / 3]); 37 | unmappedData = [...unmappedData, ...dataIncrement]; 38 | // Ugly performance optimization to avoid calculating object size 39 | // on large payloads (which can be slow) 40 | if (sizeInMb < FAST_SPEED_LIMIT_MB * 1.5) { 41 | data = mapTestData(unmappedData); 42 | sizeInMb = bytesToMb(JSON.stringify(data).length); 43 | } else { 44 | sizeInMb += sizeInMb / fractionIncrement; 45 | } 46 | i += 1; 47 | // Allows skipping to larger payloads without having to calculate 48 | // object (which can be slow on large payloads) 49 | if (i < START_ITERATION) { 50 | continue; 51 | } 52 | if (sizeInMb > MAX_SIZE_IN_MB || sizeInMb > limit) { 53 | break; 54 | } 55 | if (sizeInMb >= FAST_SPEED_LIMIT_MB * 1.5) { 56 | data = mapTestData(unmappedData); 57 | } 58 | if (sizeInMb >= MIN_SIZE_IN_MB) { 59 | log(`\n${i} DATA SIZE: ${sizeInMb}`); 60 | results.push(await fn({ unmappedData, data })); 61 | xValues.push(sizeInMb); 62 | } else { 63 | log(`# ${i} DATA SIZE: ${sizeInMb}`); 64 | } 65 | } 66 | console.log(`TOTAL TIME: ${secondsFromNanoTime(start).toFixed(2)} s`); 67 | savePlotData(results, xValues, label); 68 | } 69 | 70 | function mapTestData(unmappedData: number[][]) { 71 | return { 72 | items: unmappedData.map(([x, y, z]) => ({ x, y, z })), 73 | }; 74 | } 75 | 76 | export interface BenchmarkArgs { 77 | encode: (data) => any; 78 | data: any; 79 | sampleDecoded: (data) => any; 80 | decode: (data) => any; 81 | baseline?: BenchmarkResult, 82 | encoding?: BufferEncoding; 83 | } 84 | 85 | export interface BenchmarkResult { 86 | decodedTime: number; 87 | encodedSize: number; 88 | encodedTime: number; 89 | } 90 | 91 | export async function benchmark(args: BenchmarkArgs): Promise { 92 | runGarbageCollection(); 93 | await sleep(5); 94 | let start = getNanoTime(); 95 | const encoded = args.encode(args.data); 96 | const encodedTime = secondsFromNanoTime(start); 97 | 98 | const encodedSize: number = bytesToMb(encoded.length); 99 | 100 | // // Saving and loading data from disk does not seem to affect performance much 101 | // fs.writeFileSync(`${ROOT_DIR}/tmp/tmp`, encoded); 102 | // const loaded = fs.readFileSync(`${ROOT_DIR}/tmp/tmp`, args.encoding ? args.encoding : undefined); 103 | // const decoded = args.decode(loaded); 104 | 105 | runGarbageCollection(); 106 | await sleep(5); 107 | start = getNanoTime(); 108 | const decoded = args.decode(encoded); 109 | const decodedTime = secondsFromNanoTime(start); 110 | 111 | const sample = args.sampleDecoded(decoded); 112 | 113 | const out = { 114 | encodedTime, 115 | decodedTime, 116 | encodedSize, 117 | }; 118 | log( 119 | out.encodedTime.toFixed(2), 120 | out.decodedTime.toFixed(2), 121 | out.encodedSize.toFixed(2), 122 | sample, 123 | ); 124 | return out; 125 | } 126 | 127 | 128 | export function loadPlotData(): any { 129 | if (!fs.existsSync(PLOT_FILE_PATH)) { 130 | fs.writeFileSync(PLOT_FILE_PATH, JSON.stringify({})); 131 | } 132 | return JSON.parse(fs.readFileSync(PLOT_FILE_PATH, 'utf8')); 133 | } 134 | 135 | export function savePlotData(results: BenchmarkResult[], xValues: number[], label: string) { 136 | const plot = loadPlotData(); 137 | const y = {}; 138 | results.forEach(result => { 139 | Object.entries(result).forEach(([k, v]) => { 140 | y[k] = y[k] || []; 141 | y[k].push(v); 142 | }); 143 | }); 144 | plot[label] = { 145 | label: label, 146 | x: xValues, 147 | y, 148 | }; 149 | fs.writeFileSync(PLOT_FILE_PATH, JSON.stringify(plot, null, 2)); 150 | } 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { LOGGING } from '../config'; 2 | 3 | export function secondsFrom(startTime: number): number { 4 | const now = Date.now(); 5 | return (now - startTime) / 1000; 6 | } 7 | 8 | export function getNanoTime(): number { 9 | const time = process.hrtime(); 10 | return time[0] + time[1] / 1e9; 11 | } 12 | 13 | export function secondsFromNanoTime(startTime: number): number { 14 | const now = getNanoTime(); 15 | return now - startTime; 16 | } 17 | 18 | export function bytesToHumanReadable(bytes: number): string { 19 | if (bytes < 1024) return `${bytes} B`; 20 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 21 | const num = (bytes / Math.pow(1024, i)); 22 | return `${num.toFixed(2)} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}`; 23 | } 24 | 25 | export function bytesToMb(bytes: number): number { 26 | return bytes / (1024 * 1024); 27 | } 28 | 29 | 30 | export function log(...args) { 31 | if (LOGGING) { 32 | console.log(...args); 33 | } 34 | } 35 | 36 | export function sleep(ms: number): Promise { 37 | return new Promise(resolve => setTimeout(() => resolve(), ms)); 38 | } 39 | 40 | export function runGarbageCollection(): void { 41 | if (global.gc) { 42 | global.gc(); 43 | } 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "target": "es6", 7 | "module": "commonjs", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "sourceMap": true, 14 | "declaration": true, 15 | "noImplicitAny": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.ts', 3 | target: 'node', 4 | mode: 'production', 5 | // mode: 'development', 6 | // devtool: 'inline-source-map', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/, 13 | }, 14 | ], 15 | }, 16 | resolve: { 17 | extensions: ['.tsx', '.ts', '.js'], 18 | }, 19 | node: false, 20 | output: { 21 | filename: 'index.js', 22 | path: `${__dirname}/minified`, 23 | }, 24 | }; 25 | --------------------------------------------------------------------------------