From 3dd28934914e5273a281f7f01cb9026724854ee1 Mon Sep 17 00:00:00 2001 From: Sergey Zenchenko Date: Mon, 3 Jun 2019 13:33:48 +0300 Subject: [PATCH 1/8] Experimental cache string decoder for map keys --- src/Decoder.ts | 2 +- src/decode.ts | 137 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 10 deletions(-) diff --git a/src/Decoder.ts b/src/Decoder.ts index bb2e9422..ea980621 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -5,7 +5,7 @@ import { utf8DecodeJs, TEXT_DECODER_AVAILABLE, TEXT_DECODER_THRESHOLD, utf8Decod import { createDataView, ensureUint8Array } from "./utils/typedArrays"; import { WASM_AVAILABLE, WASM_STR_THRESHOLD, utf8DecodeWasm } from "./wasmFunctions"; -enum State { +export enum State { ARRAY, MAP_KEY, MAP_VALUE, diff --git a/src/decode.ts b/src/decode.ts index ba93cf89..f311dc66 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -1,5 +1,6 @@ import { ExtensionCodecType } from "./ExtensionCodec"; -import { Decoder } from "./Decoder"; +import { Decoder, State } from "./Decoder"; +import { utf8DecodeJs } from "./utils/utf8"; export type DecodeOptions = Partial< Readonly<{ @@ -35,15 +36,133 @@ export type DecodeOptions = Partial< export const defaultDecodeOptions: DecodeOptions = {}; +interface KeyCacheRecord { + readonly bytes: Uint8Array; + readonly key: string; + hits: number; +} + +class CachedKeyDecoder { + private caches: Array>; + + constructor(private maxKeyLength: number = 32) { + this.caches = new Array>(this.maxKeyLength + 1); + } + + public accepts(keyLength: number) { + return keyLength > 0 && keyLength <= this.maxKeyLength; + } + + public get(bytes: Uint8Array): string | null { + const chunks = this.caches[bytes.length]; + + if (chunks) { + return this.findKey(bytes, chunks); + } else { + return null; + } + } + + private findKey(bytes: Uint8Array, chunks: Array): string | null { + const size = bytes.length; + let prevHits = 0; + let result: string | null = null; + FIND_CHUNK: for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + + if (i > 0 && prevHits < chunk.hits) { + // Sort chunks by number of hits + // in order to improve search speed for most used keys + const prevChunk = chunks[i - 1]; + chunks[i] = prevChunk; + chunks[i - 1] = chunk; + prevHits = prevChunk.hits; + } else { + prevHits = chunk.hits; + } + + for (let j = 0; j < size / 2; j++) { + if (chunk.bytes[j] !== bytes[j]) { + continue FIND_CHUNK; + } + + if (chunk.bytes[size - j - 1] !== bytes[size - j - 1]) { + continue FIND_CHUNK; + } + } + + chunk.hits++; + + result = chunk.key; + + break; + } + + return result; + } + + public cache(bytes: Uint8Array, value: string) { + let chunks: Array = this.caches[bytes.length]; + + if (!chunks) { + chunks = []; + this.caches[bytes.length] = chunks; + } + + chunks.push({ + bytes: bytes, + key: value, + hits: 1, + }); + } + + public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { + const stringBytes = bytes.subarray(inputOffset, inputOffset + byteLength); + let value = this.get(stringBytes); + + if (!value) { + value = utf8DecodeJs(bytes, inputOffset, byteLength); + this.cache(bytes.slice(inputOffset, inputOffset + byteLength), value); + } + + return value; + } +} + +class CustomDecoder extends Decoder { + private maxCachedKeyLength = 32; + public cachedKeyDecoder = new CachedKeyDecoder(this.maxCachedKeyLength); + + public decodeUtf8String(byteLength: number, headerOffset: number): string { + let isKey = false; + + if (this.stack.length > 0) { + const state = this.stack[this.stack.length - 1]; + + isKey = state.type === State.MAP_KEY; + } + + if (isKey && byteLength > 0 && byteLength < this.maxCachedKeyLength) { + const offset = this.pos + headerOffset; + const value = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength); + this.pos += headerOffset + byteLength; + return value; + } else { + return super.decodeUtf8String(byteLength, headerOffset); + } + } +} + +const decoder = new CustomDecoder( + defaultDecodeOptions.extensionCodec, + defaultDecodeOptions.maxStrLength, + defaultDecodeOptions.maxBinLength, + defaultDecodeOptions.maxArrayLength, + defaultDecodeOptions.maxMapLength, + defaultDecodeOptions.maxExtLength, +); + export function decode(buffer: ArrayLike, options: DecodeOptions = defaultDecodeOptions): unknown { - const decoder = new Decoder( - options.extensionCodec, - options.maxStrLength, - options.maxBinLength, - options.maxArrayLength, - options.maxMapLength, - options.maxExtLength, - ); decoder.setBuffer(buffer); // decodeSync() requires only one buffer return decoder.decodeOneSync(); } From e3c0f74198aba4c966baff2fda21b548bd28721e Mon Sep 17 00:00:00 2001 From: Sergey Zenchenko Date: Mon, 3 Jun 2019 16:58:57 +0300 Subject: [PATCH 2/8] Improve performance --- src/decode.ts | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/decode.ts b/src/decode.ts index f311dc66..318d0a17 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -49,25 +49,27 @@ class CachedKeyDecoder { this.caches = new Array>(this.maxKeyLength + 1); } - public accepts(keyLength: number) { - return keyLength > 0 && keyLength <= this.maxKeyLength; - } - - public get(bytes: Uint8Array): string | null { - const chunks = this.caches[bytes.length]; + public get(bytes: Uint8Array, inputOffset: number, byteLength: number): string | null { + const chunks = this.caches[byteLength]; if (chunks) { - return this.findKey(bytes, chunks); + return this.findKey(bytes, inputOffset, byteLength, chunks); } else { return null; } } - private findKey(bytes: Uint8Array, chunks: Array): string | null { - const size = bytes.length; + private findKey( + bytes: Uint8Array, + inputOffset: number, + byteLength: number, + chunks: Array, + ): string | null { let prevHits = 0; - let result: string | null = null; - FIND_CHUNK: for (let i = 0; i < chunks.length; i++) { + const chunksLength = chunks.length; + const halfLength = byteLength / 2; + const endPosition = inputOffset + byteLength; + FIND_CHUNK: for (let i = 0; i < chunksLength; i++) { const chunk = chunks[i]; if (i > 0 && prevHits < chunk.hits) { @@ -81,24 +83,22 @@ class CachedKeyDecoder { prevHits = chunk.hits; } - for (let j = 0; j < size / 2; j++) { - if (chunk.bytes[j] !== bytes[j]) { + for (let j = 0; j < halfLength; j++) { + if (chunk.bytes[j] !== bytes[inputOffset + j]) { continue FIND_CHUNK; } - if (chunk.bytes[size - j - 1] !== bytes[size - j - 1]) { + if (chunk.bytes[byteLength - j - 1] !== bytes[endPosition - j - 1]) { continue FIND_CHUNK; } } chunk.hits++; - result = chunk.key; - - break; + return chunk.key; } - return result; + return null; } public cache(bytes: Uint8Array, value: string) { @@ -117,12 +117,12 @@ class CachedKeyDecoder { } public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { - const stringBytes = bytes.subarray(inputOffset, inputOffset + byteLength); - let value = this.get(stringBytes); + let value = this.get(bytes, inputOffset, byteLength); if (!value) { value = utf8DecodeJs(bytes, inputOffset, byteLength); - this.cache(bytes.slice(inputOffset, inputOffset + byteLength), value); + const stringsBytes = bytes.slice(inputOffset, inputOffset + byteLength); + this.cache(stringsBytes, value); } return value; @@ -135,14 +135,15 @@ class CustomDecoder extends Decoder { public decodeUtf8String(byteLength: number, headerOffset: number): string { let isKey = false; + const canBeDecodedAsKey = byteLength > 0 && byteLength < this.maxCachedKeyLength; - if (this.stack.length > 0) { + if (canBeDecodedAsKey && this.stack.length > 0) { const state = this.stack[this.stack.length - 1]; isKey = state.type === State.MAP_KEY; } - if (isKey && byteLength > 0 && byteLength < this.maxCachedKeyLength) { + if (isKey && canBeDecodedAsKey) { const offset = this.pos + headerOffset; const value = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength); this.pos += headerOffset + byteLength; From 8be40570ab97983ce00332569fdf8a4e32e3bacc Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 11:23:13 +0900 Subject: [PATCH 3/8] fix CachedKeyDecoder's remaining issues --- src/CachedKeyDecoder.ts | 69 ++++++++++++++++ src/Decoder.ts | 20 ++++- src/decode.ts | 150 ++-------------------------------- test/CachedKeyDecoder.test.ts | 75 +++++++++++++++++ 4 files changed, 171 insertions(+), 143 deletions(-) create mode 100644 src/CachedKeyDecoder.ts create mode 100644 test/CachedKeyDecoder.test.ts diff --git a/src/CachedKeyDecoder.ts b/src/CachedKeyDecoder.ts new file mode 100644 index 00000000..79539666 --- /dev/null +++ b/src/CachedKeyDecoder.ts @@ -0,0 +1,69 @@ +import { utf8DecodeJs } from "./utils/utf8"; + +interface KeyCacheRecord { + readonly bytes: Uint8Array; + readonly value: string; + hits: number; +} + +const DEFAULT_MAX_KEY_LENGTH = 16; +const DEFAULT_MAX_LENGTH_PER_KEY = 32; + +export class CachedKeyDecoder { + private readonly caches: Array>; + + constructor( + private readonly maxKeyLength = DEFAULT_MAX_KEY_LENGTH, + private readonly maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY, + ) { + // avoid `new Array(N)` to create a non-sparse array for performance. + this.caches = []; + for (let i = 0; i < this.maxKeyLength; i++) { + this.caches.push([]); + } + } + + public canBeCached(byteLength: number) { + return byteLength > 0 && byteLength <= this.maxKeyLength; + } + + private get(bytes: Uint8Array, inputOffset: number, byteLength: number): string | null { + const records = this.caches[byteLength - 1]; + const recordsLength = records.length; + + FIND_CHUNK: for (let i = 0; i < recordsLength; i++) { + const record = records[i]; + + for (let j = 0; j < byteLength; j++) { + if (record.bytes[j] !== bytes[inputOffset + j]) { + continue FIND_CHUNK; + } + } + + record.hits++; + return record.value; + } + return null; + } + + private store(bytes: Uint8Array, value: string) { + const records = this.caches[bytes.length - 1]; + const hits = 1; + records.unshift({ bytes, value, hits }); + if (records.length > this.maxLengthPerKey) { + records.pop(); + } + } + + public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { + const cachedValue = this.get(bytes, inputOffset, byteLength); + if (cachedValue) { + return cachedValue; + } + + const value = utf8DecodeJs(bytes, inputOffset, byteLength); + const byteSlice = bytes.slice(inputOffset, inputOffset + byteLength); + this.store(byteSlice, value); + return value; + } +} diff --git a/src/Decoder.ts b/src/Decoder.ts index da8b99f8..0e55fd28 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -4,8 +4,9 @@ import { getInt64, getUint64 } from "./utils/int"; import { utf8DecodeJs, TEXT_ENCODING_AVAILABLE, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8"; import { createDataView, ensureUint8Array } from "./utils/typedArrays"; import { WASM_AVAILABLE, WASM_STR_THRESHOLD, utf8DecodeWasm } from "./wasmFunctions"; +import { CachedKeyDecoder } from "./CachedKeyDecoder"; -export enum State { +enum State { ARRAY, MAP_KEY, MAP_VALUE, @@ -50,6 +51,8 @@ const MORE_DATA = new DataViewIndexOutOfBoundsError("Insufficient data"); const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max +const sharedCachedKeyDecoder = new CachedKeyDecoder(); + export class Decoder { totalPos = 0; pos = 0; @@ -59,6 +62,9 @@ export class Decoder { headByte = HEAD_BYTE_REQUIRED; readonly stack: Array = []; + // TODO: parameterize this property. + readonly cachedKeyDecoder = sharedCachedKeyDecoder; + constructor( readonly extensionCodec = ExtensionCodec.defaultCodec, readonly maxStrLength = DEFAULT_MAX_LENGTH, @@ -482,7 +488,9 @@ export class Decoder { const offset = this.pos + headerOffset; let object: string; - if (TEXT_ENCODING_AVAILABLE && byteLength > TEXT_DECODER_THRESHOLD) { + if (this.stateIsMapKey() && this.cachedKeyDecoder.canBeCached(byteLength)) { + object = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength); + } else if (TEXT_ENCODING_AVAILABLE && byteLength > TEXT_DECODER_THRESHOLD) { object = utf8DecodeTD(this.bytes, offset, byteLength); } else if (WASM_AVAILABLE && byteLength > WASM_STR_THRESHOLD) { object = utf8DecodeWasm(this.bytes, offset, byteLength); @@ -493,6 +501,14 @@ export class Decoder { return object; } + stateIsMapKey(): boolean { + if (this.stack.length > 0) { + const state = this.stack[this.stack.length - 1]; + return state.type === State.MAP_KEY; + } + return false; + } + decodeBinary(byteLength: number, headOffset: number): Uint8Array { if (byteLength > this.maxBinLength) { throw new Error(`Max length exceeded: bin length (${byteLength}) > maxBinLength (${this.maxBinLength})`); diff --git a/src/decode.ts b/src/decode.ts index f8220713..4e895507 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -1,6 +1,5 @@ import { ExtensionCodecType } from "./ExtensionCodec"; -import { Decoder, State } from "./Decoder"; -import { utf8DecodeJs } from "./utils/utf8"; +import { Decoder } from "./Decoder"; export type DecodeOptions = Partial< Readonly<{ @@ -36,133 +35,6 @@ export type DecodeOptions = Partial< export const defaultDecodeOptions: DecodeOptions = {}; -interface KeyCacheRecord { - readonly bytes: Uint8Array; - readonly key: string; - hits: number; -} - -class CachedKeyDecoder { - private readonly caches: Array>; - - constructor(private readonly maxKeyLength: number = 32) { - this.caches = new Array>(this.maxKeyLength + 1); - } - - public get(bytes: Uint8Array, inputOffset: number, byteLength: number): string | null { - const chunks = this.caches[byteLength]; - - if (chunks) { - return this.findKey(bytes, inputOffset, byteLength, chunks); - } else { - return null; - } - } - - private findKey( - bytes: Uint8Array, - inputOffset: number, - byteLength: number, - chunks: Array, - ): string | null { - let prevHits = 0; - const chunksLength = chunks.length; - const halfLength = byteLength / 2; - const endPosition = inputOffset + byteLength; - FIND_CHUNK: for (let i = 0; i < chunksLength; i++) { - const chunk = chunks[i]; - - if (i > 0 && prevHits < chunk.hits) { - // Sort chunks by number of hits - // in order to improve search speed for most used keys - const prevChunk = chunks[i - 1]; - chunks[i] = prevChunk; - chunks[i - 1] = chunk; - prevHits = prevChunk.hits; - } else { - prevHits = chunk.hits; - } - - for (let j = 0; j < halfLength; j++) { - if (chunk.bytes[j] !== bytes[inputOffset + j]) { - continue FIND_CHUNK; - } - - if (chunk.bytes[byteLength - j - 1] !== bytes[endPosition - j - 1]) { - continue FIND_CHUNK; - } - } - - chunk.hits++; - - return chunk.key; - } - - return null; - } - - public cache(bytes: Uint8Array, value: string) { - let chunks: Array = this.caches[bytes.length]; - - if (!chunks) { - chunks = []; - this.caches[bytes.length] = chunks; - } - - chunks.push({ - bytes: bytes, - key: value, - hits: 1, - }); - } - - public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string { - let value = this.get(bytes, inputOffset, byteLength); - - if (!value) { - value = utf8DecodeJs(bytes, inputOffset, byteLength); - const stringsBytes = bytes.slice(inputOffset, inputOffset + byteLength); - this.cache(stringsBytes, value); - } - - return value; - } -} - -class CustomDecoder extends Decoder { - private readonly maxCachedKeyLength = 32; - public cachedKeyDecoder = new CachedKeyDecoder(this.maxCachedKeyLength); - - public decodeUtf8String(byteLength: number, headerOffset: number): string { - let isKey = false; - const canBeDecodedAsKey = byteLength > 0 && byteLength < this.maxCachedKeyLength; - - if (canBeDecodedAsKey && this.stack.length > 0) { - const state = this.stack[this.stack.length - 1]; - - isKey = state.type === State.MAP_KEY; - } - - if (isKey && canBeDecodedAsKey) { - const offset = this.pos + headerOffset; - const value = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength); - this.pos += headerOffset + byteLength; - return value; - } else { - return super.decodeUtf8String(byteLength, headerOffset); - } - } -} - -const sharedDecoder = new CustomDecoder( - defaultDecodeOptions.extensionCodec, - defaultDecodeOptions.maxStrLength, - defaultDecodeOptions.maxBinLength, - defaultDecodeOptions.maxArrayLength, - defaultDecodeOptions.maxMapLength, - defaultDecodeOptions.maxExtLength, -); - /** * It decodes a MessagePack-encoded buffer. * @@ -172,18 +44,14 @@ export function decode( buffer: ArrayLike | ArrayBuffer, options: DecodeOptions = defaultDecodeOptions, ): unknown { - const decoder = - options === defaultDecodeOptions - ? sharedDecoder - : new CustomDecoder( - options.extensionCodec, - options.maxStrLength, - options.maxBinLength, - options.maxArrayLength, - options.maxMapLength, - options.maxExtLength, - ); - + const decoder = new Decoder( + options.extensionCodec, + options.maxStrLength, + options.maxBinLength, + options.maxArrayLength, + options.maxMapLength, + options.maxExtLength, + ); decoder.setBuffer(buffer); // decodeSync() requires only one buffer return decoder.decodeOneSync(); } diff --git a/test/CachedKeyDecoder.test.ts b/test/CachedKeyDecoder.test.ts new file mode 100644 index 00000000..abbcee6c --- /dev/null +++ b/test/CachedKeyDecoder.test.ts @@ -0,0 +1,75 @@ +import assert from "assert"; +import { CachedKeyDecoder } from "../src/CachedKeyDecoder"; +import { utf8EncodeJs, utf8Count } from "../src/utils/utf8"; + +function tryDecode(decoder: CachedKeyDecoder, str: string): string { + const byteLength = utf8Count(str); + const bytes = new Uint8Array(byteLength); + utf8EncodeJs(str, bytes, 0); + if (!decoder.canBeCached(byteLength)) { + throw new Error("Unexpected precondition"); + } + return decoder.decode(bytes, 0, byteLength); +} + +describe("CachedKeyDecoder", () => { + context("basic behavior", () => { + it("decodes a string", () => { + const decoder = new CachedKeyDecoder(); + + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + + // console.dir(decoder, { depth: 100 }); + }); + + it("decodes strings", () => { + const decoder = new CachedKeyDecoder(); + + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + assert.deepStrictEqual(tryDecode(decoder, "bar"), "bar"); + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + + // console.dir(decoder, { depth: 100 }); + }); + + it("decodes strings with purging records", () => { + const decoder = new CachedKeyDecoder(16, 2); + + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + assert.deepStrictEqual(tryDecode(decoder, "bar"), "bar"); + + // the next `tryDecode()` should purge the cache of "foo" + assert.deepStrictEqual(tryDecode(decoder, "baz"), "baz"); + + // with newly created an internal cache record + assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + }); + }); + + context("edge cases", () => { + // len=0 is not supported because it is just an empty string + it("decodes str with len=1", () => { + const decoder = new CachedKeyDecoder(); + + assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); + assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); + assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); + assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); + + //console.dir(decoder, { depth: 100 }); + }); + + it("decodes str with len=maxKeyLength", () => { + const decoder = new CachedKeyDecoder(1); + + assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); + assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); + assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); + assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); + + //console.dir(decoder, { depth: 100 }); + }); + }); +}); From 4d57efe377fe3661cc4d6c424b68b0ebedb5ae48 Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 11:31:44 +0900 Subject: [PATCH 4/8] fix possible unexpected behaviors for NodeJS Buffer --- src/CachedKeyDecoder.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CachedKeyDecoder.ts b/src/CachedKeyDecoder.ts index 79539666..d59cc33f 100644 --- a/src/CachedKeyDecoder.ts +++ b/src/CachedKeyDecoder.ts @@ -62,8 +62,9 @@ export class CachedKeyDecoder { } const value = utf8DecodeJs(bytes, inputOffset, byteLength); - const byteSlice = bytes.slice(inputOffset, inputOffset + byteLength); - this.store(byteSlice, value); + // Ensure to copy a slice of bytes because the byte may be NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer. + const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength); + this.store(slicedCopyOfBytes, value); return value; } } From fdd2f28662afac390fd800dfe4ae9903f3130414 Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 14:05:43 +0900 Subject: [PATCH 5/8] tweaks --- benchmark/key-decoder.ts | 49 +++++++++++++++++++++++++++++++++++ src/CachedKeyDecoder.ts | 15 ++++++----- test/CachedKeyDecoder.test.ts | 17 ++++++------ 3 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 benchmark/key-decoder.ts diff --git a/benchmark/key-decoder.ts b/benchmark/key-decoder.ts new file mode 100644 index 00000000..91bb8436 --- /dev/null +++ b/benchmark/key-decoder.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-console */ +import { utf8EncodeJs, utf8Count, utf8DecodeJs, utf8DecodeTD } from "../src/utils/utf8"; +import { utf8DecodeWasm } from "../src/wasmFunctions"; + +// @ts-ignore +import Benchmark from "benchmark"; +import { CachedKeyDecoder } from "../src/CachedKeyDecoder"; + +type InputType = { + bytes: Uint8Array; + byteLength: number; + str: string; +}; + +const keys: Array = Object.keys(require("./benchmark-from-msgpack-lite-data.json")).map((str) => { + const byteLength = utf8Count(str); + const bytes = new Uint8Array(new ArrayBuffer(byteLength)); + + utf8EncodeJs(str, bytes, 0); + + return { bytes, byteLength, str }; +}); + +for (const dataSet of [keys]) { + const keyDecoder = new CachedKeyDecoder(); + + const suite = new Benchmark.Suite(); + + suite.add("utf8DecodeJs", () => { + for (const data of dataSet) { + if (utf8DecodeJs(data.bytes, 0, data.byteLength) !== data.str) { + throw new Error("wrong result!"); + } + } + }); + + suite.add("CachedKeyDecoder", () => { + for (const data of dataSet) { + if (keyDecoder.decode(data.bytes, 0, data.byteLength) !== data.str) { + throw new Error("wrong result!"); + } + } + }); + suite.on("cycle", (event: any) => { + console.log(String(event.target)); + }); + + suite.run(); +} diff --git a/src/CachedKeyDecoder.ts b/src/CachedKeyDecoder.ts index d59cc33f..02c5b26a 100644 --- a/src/CachedKeyDecoder.ts +++ b/src/CachedKeyDecoder.ts @@ -3,7 +3,6 @@ import { utf8DecodeJs } from "./utils/utf8"; interface KeyCacheRecord { readonly bytes: Uint8Array; readonly value: string; - hits: number; } const DEFAULT_MAX_KEY_LENGTH = 16; @@ -39,8 +38,6 @@ export class CachedKeyDecoder { continue FIND_CHUNK; } } - - record.hits++; return record.value; } return null; @@ -48,10 +45,14 @@ export class CachedKeyDecoder { private store(bytes: Uint8Array, value: string) { const records = this.caches[bytes.length - 1]; - const hits = 1; - records.unshift({ bytes, value, hits }); - if (records.length > this.maxLengthPerKey) { - records.pop(); + const record: KeyCacheRecord = { bytes, value }; + + if (records.length >= this.maxLengthPerKey) { + // `records` are full! + // Set `record` to a randomized position. + records[(Math.random() * records.length) | 0] = record; + } else { + records.push(record); } } diff --git a/test/CachedKeyDecoder.test.ts b/test/CachedKeyDecoder.test.ts index abbcee6c..7559d183 100644 --- a/test/CachedKeyDecoder.test.ts +++ b/test/CachedKeyDecoder.test.ts @@ -35,16 +35,17 @@ describe("CachedKeyDecoder", () => { }); it("decodes strings with purging records", () => { - const decoder = new CachedKeyDecoder(16, 2); + const decoder = new CachedKeyDecoder(16, 4); - assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); - assert.deepStrictEqual(tryDecode(decoder, "bar"), "bar"); + for (let i = 0; i < 100; i++) { + assert.deepStrictEqual(tryDecode(decoder, "foo1"), "foo1"); + assert.deepStrictEqual(tryDecode(decoder, "foo2"), "foo2"); + assert.deepStrictEqual(tryDecode(decoder, "foo3"), "foo3"); + assert.deepStrictEqual(tryDecode(decoder, "foo4"), "foo4"); + assert.deepStrictEqual(tryDecode(decoder, "foo5"), "foo5"); + } - // the next `tryDecode()` should purge the cache of "foo" - assert.deepStrictEqual(tryDecode(decoder, "baz"), "baz"); - - // with newly created an internal cache record - assert.deepStrictEqual(tryDecode(decoder, "foo"), "foo"); + console.dir(decoder, { depth: 100 }); }); }); From 24a304cb770d2998b41a0799f00f91d3a9a7543f Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 14:12:18 +0900 Subject: [PATCH 6/8] cleanup --- benchmark/key-decoder.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmark/key-decoder.ts b/benchmark/key-decoder.ts index 91bb8436..a7c3a679 100644 --- a/benchmark/key-decoder.ts +++ b/benchmark/key-decoder.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console */ -import { utf8EncodeJs, utf8Count, utf8DecodeJs, utf8DecodeTD } from "../src/utils/utf8"; -import { utf8DecodeWasm } from "../src/wasmFunctions"; +import { utf8EncodeJs, utf8Count, utf8DecodeJs } from "../src/utils/utf8"; // @ts-ignore import Benchmark from "benchmark"; From dd1252add55f39d813f0ed614c1a4db2a4a7eefc Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 14:24:14 +0900 Subject: [PATCH 7/8] update benchmark results; now decode() is 1.5x times faster than previous! --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 50d9c2ec..9f2eb701 100644 --- a/README.md +++ b/README.md @@ -394,17 +394,17 @@ NodeJS before v10 will work by importing `@msgpack/msgpack/dist.es5/msgpack`. ## Benchmark -Benchmark on NodeJS/v12.3.1 +Benchmark on NodeJS/v12.7.0 operation | op | ms | op/s ----------------------------------------------------------------- | ------: | ----: | ------: -buf = Buffer.from(JSON.stringify(obj)); | 497600 | 5000 | 99520 -buf = JSON.stringify(obj); | 969500 | 5000 | 193900 -obj = JSON.parse(buf); | 345300 | 5000 | 69060 -buf = require("msgpack-lite").encode(obj); | 369100 | 5000 | 73820 -obj = require("msgpack-lite").decode(buf); | 278900 | 5000 | 55780 -buf = require("@msgpack/msgpack").encode(obj); | 556900 | 5000 | 111380 -obj = require("@msgpack/msgpack").decode(buf); | 502200 | 5000 | 100440 +buf = Buffer.from(JSON.stringify(obj)); | 507700 | 5000 | 101540 +buf = JSON.stringify(obj); | 958100 | 5000 | 191620 +obj = JSON.parse(buf); | 346500 | 5000 | 69300 +buf = require("msgpack-lite").encode(obj); | 361800 | 5001 | 72345 +obj = require("msgpack-lite").decode(buf); | 267400 | 5000 | 53480 +buf = require("@msgpack/msgpack").encode(obj); | 510200 | 5000 | 102040 +obj = require("@msgpack/msgpack").decode(buf); | 825500 | 5000 | 165100 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. From 11803d83b57219578619ddb8eefc929732619ab3 Mon Sep 17 00:00:00 2001 From: "FUJI Goro (gfx)" Date: Fri, 2 Aug 2019 14:34:39 +0900 Subject: [PATCH 8/8] comment out console.dir in tests --- test/CachedKeyDecoder.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/CachedKeyDecoder.test.ts b/test/CachedKeyDecoder.test.ts index 7559d183..63a1a558 100644 --- a/test/CachedKeyDecoder.test.ts +++ b/test/CachedKeyDecoder.test.ts @@ -45,7 +45,7 @@ describe("CachedKeyDecoder", () => { assert.deepStrictEqual(tryDecode(decoder, "foo5"), "foo5"); } - console.dir(decoder, { depth: 100 }); + // console.dir(decoder, { depth: 100 }); }); }); @@ -59,7 +59,7 @@ describe("CachedKeyDecoder", () => { assert.deepStrictEqual(tryDecode(decoder, "f"), "f"); assert.deepStrictEqual(tryDecode(decoder, "a"), "a"); - //console.dir(decoder, { depth: 100 }); + // console.dir(decoder, { depth: 100 }); }); it("decodes str with len=maxKeyLength", () => {