Skip to content

Commit

Permalink
feat: object codec compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
cha0s committed Nov 25, 2024
1 parent 7ada42d commit 9637235
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 85 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# crunches :muscle:

The (as of the time of writing this) smallest **and** fastest JavaScript value serialization library in the wild. **2 kB** gzipped. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets.
The (as of the time of writing this) smallest **and** fastest JavaScript value serialization library in the wild. **2.3 kB** gzipped. Efficiently encode and decode your values to and from `ArrayBuffer`s. Integrates very well with WebSockets.

## Example

Expand Down Expand Up @@ -75,7 +75,7 @@ In this example, the size of payload is only **22 bytes**. `JSON.stringify` woul
[SchemaPack](https://github.com/phretaddin/schemapack/tree/master) (huge respect from and inspiration for this library! :heart:) is great for packing objects into Node buffers. Over time, this approach has become outdated in favor of modern standards like `ArrayBuffer`.
It is also frequently desirable to preallocate and reuse buffers for performance reasons. SchemaPack always allocates new buffers when encoding. The performance hit is generally less than the naive case since Node is good about buffer pooling, but performance degrades in the browser (and doesn't exist on any other platform). Buffer reuse is the Correct Way™. Even with Node's pooling, we are still roughly **twice as fast or faster than SchemaPack** in many cases. (We could probably get even faster if we did crazy stuff like compiled unrolled codecs like SchemaPack does. PRs along those lines would be interesting if there's big gains! :muscle:)
It is also frequently desirable to preallocate and reuse buffers for performance reasons. SchemaPack always allocates new buffers when encoding. The performance hit is generally less than the naive case since Node is good about buffer pooling, but performance degrades in the browser (and doesn't exist on any other platform). Buffer reuse is the Correct Way™.
I also wanted an implementation that does amazing things like [boolean coalescence](#boolean-coalescence) and [optional fields](#optional-fields) (also with [coalescence](#optional-field-coalescence)) as well as supporting more even more types like `Map`s, `Set`s, `Date`s, etc.
Expand Down Expand Up @@ -160,7 +160,7 @@ class YourCodec {
decode(view: DataView, target: {byteOffset: number}): any

// return the number of bytes written
encode(value: any, view: DataView, byteOffset = 0): number
encode(value: any, view: DataView, byteOffset): number

size(value: any): number

Expand All @@ -182,7 +182,7 @@ class MyDateCodec extends Codecs.string {
return new Date(decoded);
}

encode(value, view, byteOffset = 0) {
encode(value, view, byteOffset) {
// convert it to a string
const converted = new Date(value).toISOString();
// pass it along to the `string` codec's encode method
Expand Down
192 changes: 111 additions & 81 deletions src/codecs/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,107 +7,137 @@ class ObjectCodec {
$$optionals = 0;

constructor(blueprint) {
let encoderCode = '';
let decoderCode = `
const value = {};
`;
let i = 0;
for (const key in blueprint.properties) {
const property = blueprint.properties[key];
if (!(property.type in Codecs)) {
throw new TypeError(`No such codec '${property.type}'`);
}
if ('bool' === property.type) {
this.$$booleans += 1;
}
const codec = new Codecs[property.type](property);
if (property.optional) {
this.$$optionals += 1;
}
this.$$codecs.push({codec, key, property});
}
}

decode(view, target) {
const booleanFlags = [];
const optionalFlags = [];
let currentBoolean = 0;
let currentOptional = 0;
let {$$booleans} = this;
const booleanBackpatches = [];
const optionalCount = Math.ceil(this.$$optionals / 8);
for (let i = 0; i < optionalCount; ++i) {
optionalFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
}
const value = {};
for (const {codec, key, property} of this.$$codecs) {
if (property.optional) {
const index = currentOptional >> 3;
const bit = currentOptional & 7;
currentOptional += 1;
const isPresent = optionalFlags[index] & (1 << bit);
if (!isPresent) {
if ('bool' === property.type) {
$$booleans -= 1;
decoderCode += `
index = currentOptional >> 3;
bit = currentOptional & 7;
currentOptional += 1;
if (!(optionalFlags[index] & (1 << bit))) {
${
'bool' === property.type
? '$$booleans -= 1'
: ''
}
}
continue;
}
else {
`;
encoderCode += `
index = currentOptional >> 3;
bit = currentOptional & 7;
isPresent = 'undefined' !== typeof value['${key}'];
optionalFlags[index] |= (isPresent ? 1 : 0) << bit;
currentOptional += 1;
if (isPresent) {
`;
}
if ('bool' === property.type) {
const index = currentBoolean >> 3;
const bit = currentBoolean & 7;
currentBoolean += 1;
booleanBackpatches.push({bit, index, key});
this.$$booleans += 1;
decoderCode += `
index = currentBoolean >> 3;
bit = currentBoolean & 7;
currentBoolean += 1;
booleanBackpatches.push({bit, index, key: '${key}'});
`;
encoderCode += `
index = currentBoolean >> 3;
bit = currentBoolean & 7;
booleanFlags[index] |= (value['${key}'] ? 1 : 0) << bit;
currentBoolean += 1;
`;
}
else {
value[key] = codec.decode(view, target);
}
}
const booleanCount = Math.ceil($$booleans / 8);
if (booleanCount > 0) {
for (let i = 0; i < booleanCount; ++i) {
booleanFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
decoderCode += `value['${key}'] = this.$$codecs[${i}].codec.decode(view, target);`;
encoderCode += `written += this.$$codecs[${i}].codec.encode(value['${key}'], view, byteOffset + written);`;
}
for (const {bit, index, key} of booleanBackpatches) {
value[key] = !!(booleanFlags[index] & (1 << bit));
if (property.optional) {
decoderCode += '}';
encoderCode += '}';
}
this.$$codecs.push({codec, key, property});
this.$$flatCodecs
i += 1;
}
return value;
}

encode(value, view, byteOffset) {
const booleanFlags = [];
const optionalFlags = [];
let currentBoolean = 0;
let currentOptional = 0;
let written = 0;
written += Math.ceil(this.$$optionals / 8);
for (const {codec, key, property} of this.$$codecs) {
if (property.optional) {
const index = currentOptional >> 3;
const bit = currentOptional & 7;
const isPresent = 'undefined' !== typeof value[key];
optionalFlags[index] |= (isPresent ? 1 : 0) << bit;
currentOptional += 1;
if (!isPresent) {
continue;
if (this.$$booleans > 0) {
decoderCode = `
let currentBoolean = 0;
let {$$booleans} = this;
const booleanBackpatches = [];
` + decoderCode;
decoderCode += `
const booleanFlags = [];
const booleanCount = Math.ceil($$booleans / 8);
if (booleanCount > 0) {
for (let i = 0; i < booleanCount; ++i) {
booleanFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
}
for (const {bit, index, key} of booleanBackpatches) {
value[key] = !!(booleanFlags[index] & (1 << bit));
}
}
}
if ('bool' === property.type) {
const index = currentBoolean >> 3;
const bit = currentBoolean & 7;
booleanFlags[index] |= (value[key] ? 1 : 0) << bit;
currentBoolean += 1;
}
else {
written += codec.encode(value[key], view, byteOffset + written);
}
`;
encoderCode += `
for (let i = 0; i < booleanFlags.length; ++i) {
view.setUint8(byteOffset + written + i, booleanFlags[i]);
}
written += booleanFlags.length;
`;
encoderCode = `
const booleanFlags = [];
let currentBoolean = 0;
` + encoderCode;
}
for (let i = 0; i < booleanFlags.length; ++i) {
view.setUint8(byteOffset + written + i, booleanFlags[i]);
if (this.$$optionals > 0) {
decoderCode = `
const optionalFlags = [];
let currentOptional = 0;
const optionalCount = Math.ceil(this.$$optionals / 8);
for (let i = 0; i < optionalCount; ++i) {
optionalFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
}
` + decoderCode;
encoderCode += `
for (let i = 0; i < optionalFlags.length; ++i) {
view.setUint8(byteOffset + i, optionalFlags[i]);
}
`;
encoderCode = `
const optionalFlags = [];
let currentOptional = 0;
let isPresent;
written += Math.ceil(this.$$optionals / 8);
` + encoderCode
}
written += booleanFlags.length;
for (let i = 0; i < optionalFlags.length; ++i) {
view.setUint8(byteOffset + i, optionalFlags[i]);
if (this.$$optionals > 0 || this.$$booleans > 0) {
decoderCode = `
let bit, index;
` + decoderCode;
encoderCode = `
let bit, index;
` + encoderCode;
}
return written;
encoderCode = `
let written = 0;
` + encoderCode;
encoderCode += `
return written;
`;
decoderCode += 'return value';
this.decode = new Function('view, target', decoderCode);
this.encode = new Function('value, view, byteOffset', encoderCode);
}

size(value) {
Expand Down

0 comments on commit 9637235

Please sign in to comment.