Skip to content

Commit f95b15b

Browse files
authored
Merge pull request msgpack#228 from sergeyzenchenko/stack-state-reuse
Reuse stack states during decoding to optimize GC load
2 parents 6e22414 + cf3a015 commit f95b15b

File tree

1 file changed

+91
-19
lines changed

1 file changed

+91
-19
lines changed

src/Decoder.ts

+91-19
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,89 @@ type StackArrayState = {
8686
position: number;
8787
};
8888

89+
class StackPool {
90+
private readonly stack: Array<StackState> = [];
91+
private stackHeadPosition = -1;
92+
93+
public get length(): number {
94+
return this.stackHeadPosition + 1;
95+
}
96+
97+
public top(): StackState | undefined {
98+
return this.stack[this.stackHeadPosition];
99+
}
100+
101+
public pushArrayState(size: number) {
102+
const state = this.getUninitializedStateFromPool() as StackArrayState;
103+
104+
state.type = STATE_ARRAY;
105+
state.position = 0;
106+
state.size = size;
107+
state.array = new Array(size);
108+
}
109+
110+
public pushMapState(size: number) {
111+
const state = this.getUninitializedStateFromPool() as StackMapState;
112+
113+
state.type = STATE_MAP_KEY;
114+
state.readCount = 0;
115+
state.size = size;
116+
state.map = {};
117+
}
118+
119+
private getUninitializedStateFromPool() {
120+
this.stackHeadPosition++;
121+
122+
if (this.stackHeadPosition === this.stack.length) {
123+
124+
const partialState: Partial<StackState> = {
125+
type: undefined,
126+
size: 0,
127+
array: undefined,
128+
position: 0,
129+
readCount: 0,
130+
map: undefined,
131+
key: null,
132+
};
133+
134+
this.stack.push(partialState as StackState)
135+
}
136+
137+
return this.stack[this.stackHeadPosition];
138+
}
139+
140+
public release(state: StackState): void {
141+
const topStackState = this.stack[this.stackHeadPosition];
142+
143+
if (topStackState !== state) {
144+
throw new Error("Invalid stack state. Released state is not on top of the stack.");
145+
}
146+
147+
if (state.type === STATE_ARRAY) {
148+
const partialState = state as Partial<StackArrayState>;
149+
partialState.size = 0;
150+
partialState.array = undefined;
151+
partialState.position = 0;
152+
partialState.type = undefined;
153+
}
154+
155+
if (state.type === STATE_MAP_KEY || state.type === STATE_MAP_VALUE) {
156+
const partialState = state as Partial<StackMapState>;
157+
partialState.size = 0;
158+
partialState.map = undefined;
159+
partialState.readCount = 0;
160+
partialState.type = undefined;
161+
}
162+
163+
this.stackHeadPosition--;
164+
}
165+
166+
public reset(): void {
167+
this.stack.length = 0;
168+
this.stackHeadPosition = -1;
169+
}
170+
}
171+
89172
type StackState = StackArrayState | StackMapState;
90173

91174
const HEAD_BYTE_REQUIRED = -1;
@@ -125,7 +208,7 @@ export class Decoder<ContextType = undefined> {
125208
private view = EMPTY_VIEW;
126209
private bytes = EMPTY_BYTES;
127210
private headByte = HEAD_BYTE_REQUIRED;
128-
private readonly stack: Array<StackState> = [];
211+
private readonly stack = new StackPool();
129212

130213
public constructor(options?: DecoderOptions<ContextType>) {
131214
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
@@ -143,7 +226,7 @@ export class Decoder<ContextType = undefined> {
143226
private reinitializeState() {
144227
this.totalPos = 0;
145228
this.headByte = HEAD_BYTE_REQUIRED;
146-
this.stack.length = 0;
229+
this.stack.reset();
147230

148231
// view, bytes, and pos will be re-initialized in setBuffer()
149232
}
@@ -465,13 +548,13 @@ export class Decoder<ContextType = undefined> {
465548
const stack = this.stack;
466549
while (stack.length > 0) {
467550
// arrays and maps
468-
const state = stack[stack.length - 1]!;
551+
const state = stack.top()!;
469552
if (state.type === STATE_ARRAY) {
470553
state.array[state.position] = object;
471554
state.position++;
472555
if (state.position === state.size) {
473-
stack.pop();
474556
object = state.array;
557+
stack.release(state);
475558
} else {
476559
continue DECODE;
477560
}
@@ -493,8 +576,8 @@ export class Decoder<ContextType = undefined> {
493576
state.readCount++;
494577

495578
if (state.readCount === state.size) {
496-
stack.pop();
497579
object = state.map;
580+
stack.release(state);
498581
} else {
499582
state.key = null;
500583
state.type = STATE_MAP_KEY;
@@ -543,26 +626,15 @@ export class Decoder<ContextType = undefined> {
543626
throw new DecodeError(`Max length exceeded: map length (${size}) > maxMapLengthLength (${this.maxMapLength})`);
544627
}
545628

546-
this.stack.push({
547-
type: STATE_MAP_KEY,
548-
size,
549-
key: null,
550-
readCount: 0,
551-
map: {},
552-
});
629+
this.stack.pushMapState(size);
553630
}
554631

555632
private pushArrayState(size: number) {
556633
if (size > this.maxArrayLength) {
557634
throw new DecodeError(`Max length exceeded: array length (${size}) > maxArrayLength (${this.maxArrayLength})`);
558635
}
559636

560-
this.stack.push({
561-
type: STATE_ARRAY,
562-
size,
563-
array: new Array<unknown>(size),
564-
position: 0,
565-
});
637+
this.stack.pushArrayState(size);
566638
}
567639

568640
private decodeUtf8String(byteLength: number, headerOffset: number): string {
@@ -589,7 +661,7 @@ export class Decoder<ContextType = undefined> {
589661

590662
private stateIsMapKey(): boolean {
591663
if (this.stack.length > 0) {
592-
const state = this.stack[this.stack.length - 1]!;
664+
const state = this.stack.top()!;
593665
return state.type === STATE_MAP_KEY;
594666
}
595667
return false;

0 commit comments

Comments
 (0)