Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 0a5966e

Browse files
authored
Merge pull request #54 from msgpack/cached_key_string_decoder
Cached string decoder for map keys (CachedKeyDecoder)
2 parents a0be621 + 11803d8 commit 0a5966e

File tree

6 files changed

+221
-9
lines changed

6 files changed

+221
-9
lines changed

‎README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -394,17 +394,17 @@ NodeJS before v10 will work by importing `@msgpack/msgpack/dist.es5/msgpack`.
394394

395395
## Benchmark
396396

397-
Benchmark on NodeJS/v12.3.1
397+
Benchmark on NodeJS/v12.7.0
398398

399399
operation | op | ms | op/s
400400
----------------------------------------------------------------- | ------: | ----: | ------:
401-
buf = Buffer.from(JSON.stringify(obj)); | 497600 | 5000 | 99520
402-
buf = JSON.stringify(obj); | 969500 | 5000 | 193900
403-
obj = JSON.parse(buf); | 345300 | 5000 | 69060
404-
buf = require("msgpack-lite").encode(obj); | 369100 | 5000 | 73820
405-
obj = require("msgpack-lite").decode(buf); | 278900 | 5000 | 55780
406-
buf = require("@msgpack/msgpack").encode(obj); | 556900 | 5000 | 111380
407-
obj = require("@msgpack/msgpack").decode(buf); | 502200 | 5000 | 100440
401+
buf = Buffer.from(JSON.stringify(obj)); | 507700 | 5000 | 101540
402+
buf = JSON.stringify(obj); | 958100 | 5000 | 191620
403+
obj = JSON.parse(buf); | 346500 | 5000 | 69300
404+
buf = require("msgpack-lite").encode(obj); | 361800 | 5001 | 72345
405+
obj = require("msgpack-lite").decode(buf); | 267400 | 5000 | 53480
406+
buf = require("@msgpack/msgpack").encode(obj); | 510200 | 5000 | 102040
407+
obj = require("@msgpack/msgpack").decode(buf); | 825500 | 5000 | 165100
408408

409409
Note that `Buffer.from()` for `JSON.stringify()` is added to emulate I/O where a JavaScript string must be converted into a byte array encoded in UTF-8, whereas MessagePack's `encode()` returns a byte array.
410410

‎benchmark/key-decoder.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* eslint-disable no-console */
2+
import { utf8EncodeJs, utf8Count, utf8DecodeJs } from "../src/utils/utf8";
3+
4+
// @ts-ignore
5+
import Benchmark from "benchmark";
6+
import { CachedKeyDecoder } from "../src/CachedKeyDecoder";
7+
8+
type InputType = {
9+
bytes: Uint8Array;
10+
byteLength: number;
11+
str: string;
12+
};
13+
14+
const keys: Array<InputType> = Object.keys(require("./benchmark-from-msgpack-lite-data.json")).map((str) => {
15+
const byteLength = utf8Count(str);
16+
const bytes = new Uint8Array(new ArrayBuffer(byteLength));
17+
18+
utf8EncodeJs(str, bytes, 0);
19+
20+
return { bytes, byteLength, str };
21+
});
22+
23+
for (const dataSet of [keys]) {
24+
const keyDecoder = new CachedKeyDecoder();
25+
26+
const suite = new Benchmark.Suite();
27+
28+
suite.add("utf8DecodeJs", () => {
29+
for (const data of dataSet) {
30+
if (utf8DecodeJs(data.bytes, 0, data.byteLength) !== data.str) {
31+
throw new Error("wrong result!");
32+
}
33+
}
34+
});
35+
36+
suite.add("CachedKeyDecoder", () => {
37+
for (const data of dataSet) {
38+
if (keyDecoder.decode(data.bytes, 0, data.byteLength) !== data.str) {
39+
throw new Error("wrong result!");
40+
}
41+
}
42+
});
43+
suite.on("cycle", (event: any) => {
44+
console.log(String(event.target));
45+
});
46+
47+
suite.run();
48+
}

‎src/CachedKeyDecoder.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { utf8DecodeJs } from "./utils/utf8";
2+
3+
interface KeyCacheRecord {
4+
readonly bytes: Uint8Array;
5+
readonly value: string;
6+
}
7+
8+
const DEFAULT_MAX_KEY_LENGTH = 16;
9+
const DEFAULT_MAX_LENGTH_PER_KEY = 32;
10+
11+
export class CachedKeyDecoder {
12+
private readonly caches: Array<Array<KeyCacheRecord>>;
13+
14+
constructor(
15+
private readonly maxKeyLength = DEFAULT_MAX_KEY_LENGTH,
16+
private readonly maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY,
17+
) {
18+
// avoid `new Array(N)` to create a non-sparse array for performance.
19+
this.caches = [];
20+
for (let i = 0; i < this.maxKeyLength; i++) {
21+
this.caches.push([]);
22+
}
23+
}
24+
25+
public canBeCached(byteLength: number) {
26+
return byteLength > 0 && byteLength <= this.maxKeyLength;
27+
}
28+
29+
private get(bytes: Uint8Array, inputOffset: number, byteLength: number): string | null {
30+
const records = this.caches[byteLength - 1];
31+
const recordsLength = records.length;
32+
33+
FIND_CHUNK: for (let i = 0; i < recordsLength; i++) {
34+
const record = records[i];
35+
36+
for (let j = 0; j < byteLength; j++) {
37+
if (record.bytes[j] !== bytes[inputOffset + j]) {
38+
continue FIND_CHUNK;
39+
}
40+
}
41+
return record.value;
42+
}
43+
return null;
44+
}
45+
46+
private store(bytes: Uint8Array, value: string) {
47+
const records = this.caches[bytes.length - 1];
48+
const record: KeyCacheRecord = { bytes, value };
49+
50+
if (records.length >= this.maxLengthPerKey) {
51+
// `records` are full!
52+
// Set `record` to a randomized position.
53+
records[(Math.random() * records.length) | 0] = record;
54+
} else {
55+
records.push(record);
56+
}
57+
}
58+
59+
public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string {
60+
const cachedValue = this.get(bytes, inputOffset, byteLength);
61+
if (cachedValue) {
62+
return cachedValue;
63+
}
64+
65+
const value = utf8DecodeJs(bytes, inputOffset, byteLength);
66+
// Ensure to copy a slice of bytes because the byte may be NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer.
67+
const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength);
68+
this.store(slicedCopyOfBytes, value);
69+
return value;
70+
}
71+
}

‎src/Decoder.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getInt64, getUint64 } from "./utils/int";
44
import { utf8DecodeJs, TEXT_ENCODING_AVAILABLE, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8";
55
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
66
import { WASM_AVAILABLE, WASM_STR_THRESHOLD, utf8DecodeWasm } from "./wasmFunctions";
7+
import { CachedKeyDecoder } from "./CachedKeyDecoder";
78

89
enum State {
910
ARRAY,
@@ -50,6 +51,8 @@ const MORE_DATA = new DataViewIndexOutOfBoundsError("Insufficient data");
5051

5152
const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max
5253

54+
const sharedCachedKeyDecoder = new CachedKeyDecoder();
55+
5356
export class Decoder {
5457
totalPos = 0;
5558
pos = 0;
@@ -59,6 +62,9 @@ export class Decoder {
5962
headByte = HEAD_BYTE_REQUIRED;
6063
readonly stack: Array<StackState> = [];
6164

65+
// TODO: parameterize this property.
66+
readonly cachedKeyDecoder = sharedCachedKeyDecoder;
67+
6268
constructor(
6369
readonly extensionCodec = ExtensionCodec.defaultCodec,
6470
readonly maxStrLength = DEFAULT_MAX_LENGTH,
@@ -482,7 +488,9 @@ export class Decoder {
482488

483489
const offset = this.pos + headerOffset;
484490
let object: string;
485-
if (TEXT_ENCODING_AVAILABLE && byteLength > TEXT_DECODER_THRESHOLD) {
491+
if (this.stateIsMapKey() && this.cachedKeyDecoder.canBeCached(byteLength)) {
492+
object = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength);
493+
} else if (TEXT_ENCODING_AVAILABLE && byteLength > TEXT_DECODER_THRESHOLD) {
486494
object = utf8DecodeTD(this.bytes, offset, byteLength);
487495
} else if (WASM_AVAILABLE && byteLength > WASM_STR_THRESHOLD) {
488496
object = utf8DecodeWasm(this.bytes, offset, byteLength);
@@ -493,6 +501,14 @@ export class Decoder {
493501
return object;
494502
}
495503

504+
stateIsMapKey(): boolean {
505+
if (this.stack.length > 0) {
506+
const state = this.stack[this.stack.length - 1];
507+
return state.type === State.MAP_KEY;
508+
}
509+
return false;
510+
}
511+
496512
decodeBinary(byteLength: number, headOffset: number): Uint8Array {
497513
if (byteLength > this.maxBinLength) {
498514
throw new Error(`Max length exceeded: bin length (${byteLength}) > maxBinLength (${this.maxBinLength})`);

‎test/CachedKeyDecoder.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from "assert";
2+
import { CachedKeyDecoder } from "../src/CachedKeyDecoder";
3+
import { utf8EncodeJs, utf8Count } from "../src/utils/utf8";
4+
5+
function tryDecode(decoder: CachedKeyDecoder, str: string): string {
6+
const byteLength = utf8Count(str);
7+
const bytes = new Uint8Array(byteLength);
8+
utf8EncodeJs(str, bytes, 0);
9+
if (!decoder.canBeCached(byteLength)) {
10+
throw new Error("Unexpected precondition");
11+
}
12+
return decoder.decode(bytes, 0, byteLength);
13+
}
14+
15+
describe("CachedKeyDecoder", () => {
16+
context("basic behavior", () => {
17+
it("decodes a string", () => {
18+
const decoder = new CachedKeyDecoder();
19+
20+
assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo");
21+
assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo");
22+
assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo");
23+
24+
// console.dir(decoder, { depth: 100 });
25+
});
26+
27+
it("decodes strings", () => {
28+
const decoder = new CachedKeyDecoder();
29+
30+
assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo");
31+
assert.deepStrictEqual(tryDecode(decoder, "bar"), "bar");
32+
assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo");
33+
34+
// console.dir(decoder, { depth: 100 });
35+
});
36+
37+
it("decodes strings with purging records", () => {
38+
const decoder = new CachedKeyDecoder(16, 4);
39+
40+
for (let i = 0; i < 100; i++) {
41+
assert.deepStrictEqual(tryDecode(decoder, "foo1"), "foo1");
42+
assert.deepStrictEqual(tryDecode(decoder, "foo2"), "foo2");
43+
assert.deepStrictEqual(tryDecode(decoder, "foo3"), "foo3");
44+
assert.deepStrictEqual(tryDecode(decoder, "foo4"), "foo4");
45+
assert.deepStrictEqual(tryDecode(decoder, "foo5"), "foo5");
46+
}
47+
48+
// console.dir(decoder, { depth: 100 });
49+
});
50+
});
51+
52+
context("edge cases", () => {
53+
// len=0 is not supported because it is just an empty string
54+
it("decodes str with len=1", () => {
55+
const decoder = new CachedKeyDecoder();
56+
57+
assert.deepStrictEqual(tryDecode(decoder, "f"), "f");
58+
assert.deepStrictEqual(tryDecode(decoder, "a"), "a");
59+
assert.deepStrictEqual(tryDecode(decoder, "f"), "f");
60+
assert.deepStrictEqual(tryDecode(decoder, "a"), "a");
61+
62+
// console.dir(decoder, { depth: 100 });
63+
});
64+
65+
it("decodes str with len=maxKeyLength", () => {
66+
const decoder = new CachedKeyDecoder(1);
67+
68+
assert.deepStrictEqual(tryDecode(decoder, "f"), "f");
69+
assert.deepStrictEqual(tryDecode(decoder, "a"), "a");
70+
assert.deepStrictEqual(tryDecode(decoder, "f"), "f");
71+
assert.deepStrictEqual(tryDecode(decoder, "a"), "a");
72+
73+
//console.dir(decoder, { depth: 100 });
74+
});
75+
});
76+
});

‎test/mocha.opts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
--require ts-node/register
22
--require tsconfig-paths/register
3+
--timeout 10000

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /