Skip to content

Commit

Permalink
perf: byteOffset as a side-effect
Browse files Browse the repository at this point in the history
  • Loading branch information
cha0s committed Nov 25, 2024
1 parent f5f3cf0 commit 1a83d43
Show file tree
Hide file tree
Showing 38 changed files with 152 additions and 135 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ You may define your own codecs that handle encoding and decoding values. There i
```js
class YourCodec {

// return the number of bytes read and the value
decode(view: DataView, byteOffset = 0): {read: number, value: any}
// return the value
decode(view: DataView, target: {byteOffset: number}): any

// return the number of bytes written
encode(value: any, view: DataView, byteOffset = 0): number
Expand All @@ -175,11 +175,11 @@ import {Codecs} from 'crunches';
// will coerce strings to `Date`s
class MyDateCodec extends Codecs.string {

decode(view, byteOffset = 0) {
decode(view, target) {
// let the `string` codec decode the string
const decoded = super.decode(view, byteOffset);
const decoded = super.decode(view, target);
// pass it to the `Date` constructor
return {read: decoded.read, value: new Date(decoded.value)};
return new Date(decoded);
}

encode(value, view, byteOffset = 0) {
Expand All @@ -203,6 +203,8 @@ Codecs.myDate = MyDateCodec;
All this codec does is coerce `Date`s to and from strings. It leans on the built-in `string` codec to handle the actual over-the-wire encoding and string size calculation.
Inside your codec, you must increment `target.byteOffset` as you decode bytes.
Just set a key on the `Codecs` object and go. Too easy!
## Schema types
Expand Down Expand Up @@ -335,3 +337,6 @@ SchemaPack's `varint` types only work up to $2^{30}-1$ whereas `crunches` uses m
**Q**: Why no TypeScript support?
**A**: Feel free to create an issue.
**Q**: This library isn't **always** faster than SchemaPack!
**A**: That's true, at least for now. SchemaPack does a lot of black magic and compiles codecs on-the-fly. However, it is consistently faster when using larger aggregate types even in its current state.
13 changes: 5 additions & 8 deletions src/codecs/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@ class ArrayCodec {
this.$$elementCodec = new Codecs[blueprint.element.type](blueprint.element);
}

decode(view, byteOffset = 0) {
let read = 0;
const length = view.getUint32(byteOffset);
read += 4;
decode(view, target = {byteOffset: 0}) {
const length = view.getUint32(target.byteOffset);
target.byteOffset += 4;
const value = Array(length);
for (let i = 0; i < length; ++i) {
const decoded = this.$$elementCodec.decode(view, byteOffset + read);
value[i] = decoded.value;
read += decoded.read;
value[i] = this.$$elementCodec.decode(view, target);
}
return {read, value};
return value;
}

encode(value, view, byteOffset = 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/codecs/array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ test('array', async () => {
});
const value = [1, 2, 3, 4];
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
6 changes: 4 additions & 2 deletions src/codecs/bool.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class BoolCodec {
decode(view, byteOffset = 0) {
return {read: 1, value: !!view.getUint8(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = !!view.getUint8(target.byteOffset);
target.byteOffset += 1;
return value;
}
encode(value, view, byteOffset = 0) {
view.setUint8(byteOffset, !!value);
Expand Down
8 changes: 4 additions & 4 deletions src/codecs/bool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ test('bool', async () => {
const codec = new Codec();
const value = true;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});

test('coerced bool', async () => {
const codec = new Codec();
const value = 'truthy value';
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value: !!value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(!!value);
});
12 changes: 7 additions & 5 deletions src/codecs/buffer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
class BufferCodec {

decode(view, byteOffset = 0) {
const length = view.getUint32(byteOffset);
decode(view, target = {byteOffset: 0}) {
const length = view.getUint32(target.byteOffset);
target.byteOffset += 4;
const value = new ArrayBuffer(length);
if (0 === length) {
return {read: 4, value};
return value;
}
new Uint8Array(value).set(new Uint8Array(view.buffer, 4 + view.byteOffset + byteOffset));
return {read: 4 + length, value};
new Uint8Array(value).set(new Uint8Array(view.buffer, view.byteOffset + target.byteOffset));
target.byteOffset += length;
return value;
}

encode(value, view, byteOffset = 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/codecs/buffer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ test('buffer', async () => {
});
const value = new Map([[1, 'one'], [2, 'two']]);
const mapView = new DataView(new ArrayBuffer(mapSchema.size(value)));
const written = mapSchema.encode(value, mapView);
mapSchema.encode(value, mapView);
const bufferSchema = new Codec();
const bufferView = new DataView(new ArrayBuffer(bufferSchema.size(mapView.buffer)));
bufferSchema.encode(mapView.buffer, bufferView);
const newMapView = new DataView(bufferSchema.decode(bufferView).value);
expect(mapSchema.decode(newMapView)).to.deep.equal({read: written, value});
const newMapView = new DataView(bufferSchema.decode(bufferView));
expect(mapSchema.decode(newMapView)).to.deep.equal(value);
});
5 changes: 2 additions & 3 deletions src/codecs/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import StringCodec from './string.js';
// will coerce strings to `Date`s
class DateCodec extends StringCodec {

decode(view, byteOffset = 0) {
const decoded = super.decode(view, byteOffset);
return {read: decoded.read, value: new Date(decoded.value)};
decode(view, target = {byteOffset: 0}) {
return new Date(super.decode(view, target));
}

encode(value, view, byteOffset = 0) {
Expand Down
12 changes: 6 additions & 6 deletions src/codecs/date.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ test('date', async () => {
const codec = new Codec();
const value = new Date('2024-11-24T18:58:48.912Z');
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
codec.encode(value, view);
const decoded = codec.decode(view);
expect(decoded.value).to.be.instanceOf(Date);
expect(codec.decode(view)).to.deep.equal({read: written, value});
expect(decoded).to.be.instanceOf(Date);
expect(codec.decode(view)).to.deep.equal(value);
});

test('coerce date', async () => {
const codec = new Codec();
const value = '2024-11-24T18:58:48.912Z';
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
codec.encode(value, view);
const decoded = codec.decode(view);
expect(decoded.value).to.be.instanceOf(Date);
expect(codec.decode(view)).to.deep.equal({read: written, value: new Date(value)});
expect(decoded).to.be.instanceOf(Date);
expect(codec.decode(view)).to.deep.equal(new Date(value));
});
6 changes: 4 additions & 2 deletions src/codecs/float32.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Float32Codec {
decode(view, byteOffset = 0) {
return {read: 4, value: view.getFloat32(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = view.getFloat32(target.byteOffset);
target.byteOffset += 4;
return value;
}
encode(value, view, byteOffset = 0) {
view.setFloat32(byteOffset, value);
Expand Down
8 changes: 4 additions & 4 deletions src/codecs/float32.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ test('float32', async () => {
const codec = new Codec();
const value = 1 / 4;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});

test('float32 infinity', async () => {
const codec = new Codec();
const value = Infinity;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
6 changes: 4 additions & 2 deletions src/codecs/float64.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Float64Codec {
decode(view, byteOffset = 0) {
return {read: 8, value: view.getFloat64(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = view.getFloat64(target.byteOffset);
target.byteOffset += 8;
return value;
}
encode(value, view, byteOffset = 0) {
view.setFloat64(byteOffset, value);
Expand Down
8 changes: 4 additions & 4 deletions src/codecs/float64.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ test('float64', async () => {
const codec = new Codec();
const value = 1 / 7;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});

test('float64 infinity', async () => {
const codec = new Codec();
const value = Infinity;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
6 changes: 4 additions & 2 deletions src/codecs/int16.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Int16Codec {
decode(view, byteOffset = 0) {
return {read: 2, value: view.getInt16(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = view.getInt16(target.byteOffset);
target.byteOffset += 2;
return value;
}
encode(value, view, byteOffset = 0) {
view.setInt16(byteOffset, value);
Expand Down
4 changes: 2 additions & 2 deletions src/codecs/int16.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ test('int16', async () => {
const codec = new Codec();
const value = 32;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
6 changes: 4 additions & 2 deletions src/codecs/int32.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Int32Codec {
decode(view, byteOffset = 0) {
return {read: 4, value: view.getInt32(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = view.getInt32(target.byteOffset);
target.byteOffset += 4;
return value;
}
encode(value, view, byteOffset = 0) {
view.setInt32(byteOffset, value);
Expand Down
4 changes: 2 additions & 2 deletions src/codecs/int32.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ test('int32', async () => {
const codec = new Codec();
const value = 32;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
6 changes: 4 additions & 2 deletions src/codecs/int8.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Int8Codec {
decode(view, byteOffset = 0) {
return {read: 1, value: view.getInt8(byteOffset)};
decode(view, target = {byteOffset: 0}) {
const value = view.getInt8(target.byteOffset);
target.byteOffset += 1;
return value;
}
encode(value, view, byteOffset = 0) {
view.setInt8(byteOffset, value);
Expand Down
4 changes: 2 additions & 2 deletions src/codecs/int8.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ test('int8', async () => {
const codec = new Codec();
const value = 32;
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});
7 changes: 3 additions & 4 deletions src/codecs/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ class MapCodec extends ArrayCodec {
});
}

decode(view, byteOffset = 0) {
const decoded = super.decode(view, byteOffset);
decode(view, target = {byteOffset: 0}) {
const value = new Map();
for (const {key, value: mapValue} of decoded.value) {
for (const {key, value: mapValue} of super.decode(view, target)) {
value.set(key, mapValue);
}
return {read: decoded.read, value};
return value;
}

encode(value, view, byteOffset = 0) {
Expand Down
8 changes: 4 additions & 4 deletions src/codecs/map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ test('map', async () => {
});
const value = new Map([[1, 'one'], [2, 'two']]);
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(value);
});

test('coerce map', async () => {
Expand All @@ -30,6 +30,6 @@ test('coerce map', async () => {
});
const value = [[1, 'one'], [2, 'two']];
const view = new DataView(new ArrayBuffer(codec.size(value)));
const written = codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal({read: written, value: new Map(value)});
codec.encode(value, view);
expect(codec.decode(view)).to.deep.equal(new Map(value));
});
17 changes: 7 additions & 10 deletions src/codecs/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,18 @@ class ObjectCodec {
}
}

decode(view, byteOffset = 0) {
decode(view, target = {byteOffset: 0}) {
const booleanFlags = [];
const optionalFlags = [];
let currentBoolean = 0;
let currentOptional = 0;
let read = 0;
let {$$booleans} = this;
const booleanBackpatches = [];
const optionalCount = Math.ceil(this.$$optionals / 8);
for (let i = 0; i < optionalCount; ++i) {
optionalFlags.push(view.getUint8(byteOffset + i));
optionalFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
}
read += optionalCount;
const value = {};
for (const {codec, key, property} of this.$$codecs) {
if (property.optional) {
Expand All @@ -57,22 +56,20 @@ class ObjectCodec {
booleanBackpatches.push({bit, index, key});
}
else {
const decoded = codec.decode(view, byteOffset + read);
value[key] = decoded.value;
read += decoded.read;
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(byteOffset + read + i));
booleanFlags.push(view.getUint8(target.byteOffset));
target.byteOffset += 1;
}
for (const {bit, index, key} of booleanBackpatches) {
value[key] = !!(booleanFlags[index] & (1 << bit));
}
read += booleanCount;
}
return {read, value};
return value;
}

encode(value, view, byteOffset = 0) {
Expand Down
Loading

0 comments on commit 1a83d43

Please sign in to comment.