Skip to content

make Encoder/Decoder instances easily reusable #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 6, 2020
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = {
"@typescript-eslint/no-throw-literal": "warn",
"@typescript-eslint/no-extra-semi": "warn",
"@typescript-eslint/no-extra-non-null-assertion": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-use-before-define": "warn",
"@typescript-eslint/no-for-in-array": "warn",
"@typescript-eslint/no-unnecessary-condition": ["warn", { "allowConstantLoopConditions": true }],
Expand Down
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ deepStrictEqual(decode(encoded), object);
- [`decodeAsync(stream: AsyncIterable<ArrayLike<number>> | ReadableStream<ArrayLike<number>>, options?: DecodeAsyncOptions): Promise<unknown>`](#decodeasyncstream-asynciterablearraylikenumber--readablestreamarraylikenumber-options-decodeasyncoptions-promiseunknown)
- [`decodeArrayStream(stream: AsyncIterable<ArrayLike<number>> | ReadableStream<ArrayLike<number>>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodearraystreamstream-asynciterablearraylikenumber--readablestreamarraylikenumber-options-decodeasyncoptions-asynciterableunknown)
- [`decodeStream(stream: AsyncIterable<ArrayLike<number>> | ReadableStream<ArrayLike<number>>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodestreamstream-asynciterablearraylikenumber--readablestreamarraylikenumber-options-decodeasyncoptions-asynciterableunknown)
- [Extension Types](#extension-types)
- [Codec context](#codec-context)
- [Reusing Encoder and Decoder instances](#reusing-encoder-and-decoder-instances)
- [Extension Types](#extension-types)
- [ExtensionCodec context](#extensioncodec-context)
- [Handling BigInt with ExtensionCodec](#handling-bigint-with-extensioncodec)
- [The temporal module as timestamp extensions](#the-temporal-module-as-timestamp-extensions)
- [Decoding a Blob](#decoding-a-blob)
Expand Down Expand Up @@ -212,7 +213,27 @@ for await (const item of decodeStream(stream)) {
}
```

### Extension Types
### Reusing Encoder and Decoder instances

`Encoder` and `Decoder` classes is provided for better performance:

```typescript
import { deepStrictEqual } from "assert";
import { Encoder, Decoder } from "@msgpack/msgpack";

const encoder = new Encoder();
const decoder = new Decoder();

const encoded: Uint8Array = encoder.encode(object);
deepStrictEqual(decoder.decode(encoded), object);
```

According to our benchmark, reusing `Encoder` instance is about 2% faster
than `encode()` function, and reusing `Decoder` instance is about 35% faster
than `decode()` function. Note that the result should vary in environments
and data structure.

## Extension Types

To handle [MessagePack Extension Types](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types), this library provides `ExtensionCodec` class.

Expand Down Expand Up @@ -266,7 +287,7 @@ const decoded = decode(encoded, { extensionCodec });

Not that extension types for custom objects must be `[0, 127]`, while `[-1, -128]` is reserved for MessagePack itself.

#### Codec context
#### ExtensionCodec context

When using an extension codec, it may be necessary to keep encoding/decoding state, to keep track of which objects got encoded/re-created. To do this, pass a `context` to the `EncodeOptions` and `DecodeOptions` (and if using typescript, type the `ExtensionCodec` too). Don't forget to pass the `{extensionCodec, context}` along recursive encoding/decoding:

Expand Down
17 changes: 11 additions & 6 deletions benchmark/benchmark-from-msgpack-lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ var msgpack_lite = try_require("msgpack-lite");
var msgpack_js = try_require("msgpack-js");
var msgpack_js_v5 = try_require("msgpack-js-v5");
var msgpack5 = try_require("msgpack5");
var msgpack_unpack = try_require("msgpack-unpack");
var notepack = try_require("notepack");

msgpack5 = msgpack5 && msgpack5();
Expand Down Expand Up @@ -64,6 +63,17 @@ if (msgpack_msgpack) {
buf = bench('buf = require("@msgpack/msgpack").encode(obj);', msgpack_msgpack.encode, data);
obj = bench('obj = require("@msgpack/msgpack").decode(buf);', msgpack_msgpack.decode, buf);
runTest(obj);

const encoder = new msgpack_msgpack.Encoder();
const decoder = new msgpack_msgpack.Decoder();
buf = bench('buf = /* @msgpack/msgpack */ encoder.encode(obj);', (data) => encoder.encode(data), data);
obj = bench('obj = /* @msgpack/msgpack */ decoder.decode(buf);', (buf) => decoder.decode(buf), buf);
runTest(obj);

if (process.env.CACHE_HIT_RATE) {
const {hit, miss} = decoder.keyDecoder;
console.log(`CACHE_HIT_RATE: cache hit rate in CachedKeyDecoder: hit=${hit}, hiss=${miss}, hit rate=${hit / (hit + miss)}`);
}
}

if (msgpack_js_v5) {
Expand All @@ -90,11 +100,6 @@ if (notepack) {
runTest(obj);
}

if (msgpack_unpack) {
obj = bench('obj = require("msgpack-unpack").decode(buf);', msgpack_unpack, packed);
runTest(obj);
}

function JSON_stringify(src: any) {
return Buffer.from(JSON.stringify(src));
}
Expand Down
11 changes: 10 additions & 1 deletion src/CachedKeyDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ interface KeyCacheRecord {
const DEFAULT_MAX_KEY_LENGTH = 16;
const DEFAULT_MAX_LENGTH_PER_KEY = 16;

export class CachedKeyDecoder {
export interface KeyDecoder {
canBeCached(byteLength: number): boolean;
decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string;
}

export class CachedKeyDecoder implements KeyDecoder {
hit = 0;
miss = 0;
private readonly caches: Array<Array<KeyCacheRecord>>;

constructor(readonly maxKeyLength = DEFAULT_MAX_KEY_LENGTH, readonly maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY) {
Expand Down Expand Up @@ -57,8 +64,10 @@ export class CachedKeyDecoder {
public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string {
const cachedValue = this.get(bytes, inputOffset, byteLength);
if (cachedValue != null) {
this.hit++;
return cachedValue;
}
this.miss++;

const value = utf8DecodeJs(bytes, inputOffset, byteLength);
// Ensure to copy a slice of bytes because the byte may be NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer.
Expand Down
Loading