Skip to content

Reuse stack states during decoding to optimize GC load #228

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 1 commit into from
Jul 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 91 additions & 19 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,89 @@ type StackArrayState = {
position: number;
};

class StackPool {
private readonly stack: Array<StackState> = [];
private stackHeadPosition = -1;

public get length(): number {
return this.stackHeadPosition + 1;
}

public top(): StackState | undefined {
return this.stack[this.stackHeadPosition];
}

public pushArrayState(size: number) {
const state = this.getUninitializedStateFromPool() as StackArrayState;

state.type = STATE_ARRAY;
state.position = 0;
state.size = size;
state.array = new Array(size);
}

public pushMapState(size: number) {
const state = this.getUninitializedStateFromPool() as StackMapState;

state.type = STATE_MAP_KEY;
state.readCount = 0;
state.size = size;
state.map = {};
}

private getUninitializedStateFromPool() {
this.stackHeadPosition++;

if (this.stackHeadPosition === this.stack.length) {

const partialState: Partial<StackState> = {
type: undefined,
size: 0,
array: undefined,
position: 0,
readCount: 0,
map: undefined,
key: null,
};

this.stack.push(partialState as StackState)
}

return this.stack[this.stackHeadPosition];
}

public release(state: StackState): void {
const topStackState = this.stack[this.stackHeadPosition];

if (topStackState !== state) {
throw new Error("Invalid stack state. Released state is not on top of the stack.");
}

if (state.type === STATE_ARRAY) {
const partialState = state as Partial<StackArrayState>;
partialState.size = 0;
partialState.array = undefined;
partialState.position = 0;
partialState.type = undefined;
}

if (state.type === STATE_MAP_KEY || state.type === STATE_MAP_VALUE) {
const partialState = state as Partial<StackMapState>;
partialState.size = 0;
partialState.map = undefined;
partialState.readCount = 0;
partialState.type = undefined;
}

this.stackHeadPosition--;
}

public reset(): void {
this.stack.length = 0;
this.stackHeadPosition = -1;
}
}

type StackState = StackArrayState | StackMapState;

const HEAD_BYTE_REQUIRED = -1;
Expand Down Expand Up @@ -125,7 +208,7 @@ export class Decoder<ContextType = undefined> {
private view = EMPTY_VIEW;
private bytes = EMPTY_BYTES;
private headByte = HEAD_BYTE_REQUIRED;
private readonly stack: Array<StackState> = [];
private readonly stack = new StackPool();

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

// view, bytes, and pos will be re-initialized in setBuffer()
}
Expand Down Expand Up @@ -465,13 +548,13 @@ export class Decoder<ContextType = undefined> {
const stack = this.stack;
while (stack.length > 0) {
// arrays and maps
const state = stack[stack.length - 1]!;
const state = stack.top()!;
if (state.type === STATE_ARRAY) {
state.array[state.position] = object;
state.position++;
if (state.position === state.size) {
stack.pop();
object = state.array;
stack.release(state);
} else {
continue DECODE;
}
Expand All @@ -493,8 +576,8 @@ export class Decoder<ContextType = undefined> {
state.readCount++;

if (state.readCount === state.size) {
stack.pop();
object = state.map;
stack.release(state);
} else {
state.key = null;
state.type = STATE_MAP_KEY;
Expand Down Expand Up @@ -543,26 +626,15 @@ export class Decoder<ContextType = undefined> {
throw new DecodeError(`Max length exceeded: map length (${size}) > maxMapLengthLength (${this.maxMapLength})`);
}

this.stack.push({
type: STATE_MAP_KEY,
size,
key: null,
readCount: 0,
map: {},
});
this.stack.pushMapState(size);
}

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

this.stack.push({
type: STATE_ARRAY,
size,
array: new Array<unknown>(size),
position: 0,
});
this.stack.pushArrayState(size);
}

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

private stateIsMapKey(): boolean {
if (this.stack.length > 0) {
const state = this.stack[this.stack.length - 1]!;
const state = this.stack.top()!;
return state.type === STATE_MAP_KEY;
}
return false;
Expand Down