Skip to content

Commit 3dd2893

Browse files
Experimental cache string decoder for map keys
1 parent cdcc306 commit 3dd2893

File tree

2 files changed

+129
-10
lines changed

2 files changed

+129
-10
lines changed

src/Decoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { utf8DecodeJs, TEXT_DECODER_AVAILABLE, TEXT_DECODER_THRESHOLD, utf8Decod
55
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
66
import { WASM_AVAILABLE, WASM_STR_THRESHOLD, utf8DecodeWasm } from "./wasmFunctions";
77

8-
enum State {
8+
export enum State {
99
ARRAY,
1010
MAP_KEY,
1111
MAP_VALUE,

src/decode.ts

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ExtensionCodecType } from "./ExtensionCodec";
2-
import { Decoder } from "./Decoder";
2+
import { Decoder, State } from "./Decoder";
3+
import { utf8DecodeJs } from "./utils/utf8";
34

45
export type DecodeOptions = Partial<
56
Readonly<{
@@ -35,15 +36,133 @@ export type DecodeOptions = Partial<
3536

3637
export const defaultDecodeOptions: DecodeOptions = {};
3738

39+
interface KeyCacheRecord {
40+
readonly bytes: Uint8Array;
41+
readonly key: string;
42+
hits: number;
43+
}
44+
45+
class CachedKeyDecoder {
46+
private caches: Array<Array<KeyCacheRecord>>;
47+
48+
constructor(private maxKeyLength: number = 32) {
49+
this.caches = new Array<Array<KeyCacheRecord>>(this.maxKeyLength + 1);
50+
}
51+
52+
public accepts(keyLength: number) {
53+
return keyLength > 0 && keyLength <= this.maxKeyLength;
54+
}
55+
56+
public get(bytes: Uint8Array): string | null {
57+
const chunks = this.caches[bytes.length];
58+
59+
if (chunks) {
60+
return this.findKey(bytes, chunks);
61+
} else {
62+
return null;
63+
}
64+
}
65+
66+
private findKey(bytes: Uint8Array, chunks: Array<KeyCacheRecord>): string | null {
67+
const size = bytes.length;
68+
let prevHits = 0;
69+
let result: string | null = null;
70+
FIND_CHUNK: for (let i = 0; i < chunks.length; i++) {
71+
const chunk = chunks[i];
72+
73+
if (i > 0 && prevHits < chunk.hits) {
74+
// Sort chunks by number of hits
75+
// in order to improve search speed for most used keys
76+
const prevChunk = chunks[i - 1];
77+
chunks[i] = prevChunk;
78+
chunks[i - 1] = chunk;
79+
prevHits = prevChunk.hits;
80+
} else {
81+
prevHits = chunk.hits;
82+
}
83+
84+
for (let j = 0; j < size / 2; j++) {
85+
if (chunk.bytes[j] !== bytes[j]) {
86+
continue FIND_CHUNK;
87+
}
88+
89+
if (chunk.bytes[size - j - 1] !== bytes[size - j - 1]) {
90+
continue FIND_CHUNK;
91+
}
92+
}
93+
94+
chunk.hits++;
95+
96+
result = chunk.key;
97+
98+
break;
99+
}
100+
101+
return result;
102+
}
103+
104+
public cache(bytes: Uint8Array, value: string) {
105+
let chunks: Array<KeyCacheRecord> = this.caches[bytes.length];
106+
107+
if (!chunks) {
108+
chunks = [];
109+
this.caches[bytes.length] = chunks;
110+
}
111+
112+
chunks.push({
113+
bytes: bytes,
114+
key: value,
115+
hits: 1,
116+
});
117+
}
118+
119+
public decode(bytes: Uint8Array, inputOffset: number, byteLength: number): string {
120+
const stringBytes = bytes.subarray(inputOffset, inputOffset + byteLength);
121+
let value = this.get(stringBytes);
122+
123+
if (!value) {
124+
value = utf8DecodeJs(bytes, inputOffset, byteLength);
125+
this.cache(bytes.slice(inputOffset, inputOffset + byteLength), value);
126+
}
127+
128+
return value;
129+
}
130+
}
131+
132+
class CustomDecoder extends Decoder {
133+
private maxCachedKeyLength = 32;
134+
public cachedKeyDecoder = new CachedKeyDecoder(this.maxCachedKeyLength);
135+
136+
public decodeUtf8String(byteLength: number, headerOffset: number): string {
137+
let isKey = false;
138+
139+
if (this.stack.length > 0) {
140+
const state = this.stack[this.stack.length - 1];
141+
142+
isKey = state.type === State.MAP_KEY;
143+
}
144+
145+
if (isKey && byteLength > 0 && byteLength < this.maxCachedKeyLength) {
146+
const offset = this.pos + headerOffset;
147+
const value = this.cachedKeyDecoder.decode(this.bytes, offset, byteLength);
148+
this.pos += headerOffset + byteLength;
149+
return value;
150+
} else {
151+
return super.decodeUtf8String(byteLength, headerOffset);
152+
}
153+
}
154+
}
155+
156+
const decoder = new CustomDecoder(
157+
defaultDecodeOptions.extensionCodec,
158+
defaultDecodeOptions.maxStrLength,
159+
defaultDecodeOptions.maxBinLength,
160+
defaultDecodeOptions.maxArrayLength,
161+
defaultDecodeOptions.maxMapLength,
162+
defaultDecodeOptions.maxExtLength,
163+
);
164+
38165
export function decode(buffer: ArrayLike<number>, options: DecodeOptions = defaultDecodeOptions): unknown {
39-
const decoder = new Decoder(
40-
options.extensionCodec,
41-
options.maxStrLength,
42-
options.maxBinLength,
43-
options.maxArrayLength,
44-
options.maxMapLength,
45-
options.maxExtLength,
46-
);
47166
decoder.setBuffer(buffer); // decodeSync() requires only one buffer
48167
return decoder.decodeOneSync();
49168
}

0 commit comments

Comments
 (0)