From fad4821fd953ebe19e65b7e81da5d972fd1a01c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 15:34:48 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20O(n=C2=B2)=20payload=20accumulation=20in?= =?UTF-8?q?=20DCS/OSC/APC=20handlers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace repeated string concatenation with a chunk buffer that joins once on completion. Fixes #5916. Co-authored-by: Daniel Imms --- src/common/parser/ApcParser.ts | 15 +++++------ src/common/parser/DcsParser.ts | 15 +++++------ src/common/parser/OscParser.ts | 15 +++++------ src/common/parser/PayloadStringBuffer.ts | 34 ++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 src/common/parser/PayloadStringBuffer.ts diff --git a/src/common/parser/ApcParser.ts b/src/common/parser/ApcParser.ts index 8f921f8cb..d3f5b8e2e 100644 --- a/src/common/parser/ApcParser.ts +++ b/src/common/parser/ApcParser.ts @@ -7,6 +7,7 @@ import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, IS import { ParserConstants } from 'common/parser/Constants'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; +import { PayloadStringBuffer } from 'common/parser/PayloadStringBuffer'; const EMPTY_HANDLERS: IApcHandler[] = []; @@ -153,13 +154,13 @@ export class ApcParser implements IApcParser { export class ApcHandler implements IApcHandler { private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT; - private _data = ''; + private _data = new PayloadStringBuffer(); private _hitLimit: boolean = false; constructor(private _handler: (data: string) => boolean | Promise) { } public start(): void { - this._data = ''; + this._data.reset(); this._hitLimit = false; } @@ -167,9 +168,7 @@ export class ApcHandler implements IApcHandler { if (this._hitLimit) { return; } - this._data += utf32ToString(data, start, end); - if (this._data.length > ApcHandler._payloadLimit) { - this._data = ''; + if (this._data.add(utf32ToString(data, start, end), ApcHandler._payloadLimit)) { this._hitLimit = true; } } @@ -179,18 +178,18 @@ export class ApcHandler implements IApcHandler { if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data); + ret = this._handler(this._data.toString()); if (ret instanceof Promise) { // need to hold data until `ret` got resolved // dont care for errors, data will be freed anyway on next start return ret.then(res => { - this._data = ''; + this._data.reset(); this._hitLimit = false; return res; }); } } - this._data = ''; + this._data.reset(); this._hitLimit = false; return ret; } diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 692200230..be2fa39d5 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -8,6 +8,7 @@ import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandle import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { ParserConstants } from 'common/parser/Constants'; +import { PayloadStringBuffer } from 'common/parser/PayloadStringBuffer'; const EMPTY_HANDLERS: IDcsHandler[] = []; @@ -140,7 +141,7 @@ EMPTY_PARAMS.addParam(0); export class DcsHandler implements IDcsHandler { private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT; - private _data = ''; + private _data = new PayloadStringBuffer(); private _params: IParams = EMPTY_PARAMS; private _hitLimit: boolean = false; @@ -152,7 +153,7 @@ export class DcsHandler implements IDcsHandler { // perf optimization: // clone only, if we have non empty params, otherwise stick with default this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS; - this._data = ''; + this._data.reset(); this._hitLimit = false; } @@ -160,9 +161,7 @@ export class DcsHandler implements IDcsHandler { if (this._hitLimit) { return; } - this._data += utf32ToString(data, start, end); - if (this._data.length > DcsHandler._payloadLimit) { - this._data = ''; + if (this._data.add(utf32ToString(data, start, end), DcsHandler._payloadLimit)) { this._hitLimit = true; } } @@ -172,20 +171,20 @@ export class DcsHandler implements IDcsHandler { if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data, this._params); + ret = this._handler(this._data.toString(), this._params); if (ret instanceof Promise) { // need to hold data and params until `ret` got resolved // dont care for errors, data will be freed anyway on next start return ret.then(res => { this._params = EMPTY_PARAMS; - this._data = ''; + this._data.reset(); this._hitLimit = false; return res; }); } } this._params = EMPTY_PARAMS; - this._data = ''; + this._data.reset(); this._hitLimit = false; return ret; } diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 1f0c8cb23..6a17af817 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -7,6 +7,7 @@ import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, IS import { OscState, ParserConstants } from 'common/parser/Constants'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; +import { PayloadStringBuffer } from 'common/parser/PayloadStringBuffer'; const EMPTY_HANDLERS: IOscHandler[] = []; @@ -194,13 +195,13 @@ export class OscParser implements IOscParser { export class OscHandler implements IOscHandler { private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT; - private _data = ''; + private _data = new PayloadStringBuffer(); private _hitLimit: boolean = false; constructor(private _handler: (data: string) => boolean | Promise) { } public start(): void { - this._data = ''; + this._data.reset(); this._hitLimit = false; } @@ -208,9 +209,7 @@ export class OscHandler implements IOscHandler { if (this._hitLimit) { return; } - this._data += utf32ToString(data, start, end); - if (this._data.length > OscHandler._payloadLimit) { - this._data = ''; + if (this._data.add(utf32ToString(data, start, end), OscHandler._payloadLimit)) { this._hitLimit = true; } } @@ -220,18 +219,18 @@ export class OscHandler implements IOscHandler { if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data); + ret = this._handler(this._data.toString()); if (ret instanceof Promise) { // need to hold data until `ret` got resolved // dont care for errors, data will be freed anyway on next start return ret.then(res => { - this._data = ''; + this._data.reset(); this._hitLimit = false; return res; }); } } - this._data = ''; + this._data.reset(); this._hitLimit = false; return ret; } diff --git a/src/common/parser/PayloadStringBuffer.ts b/src/common/parser/PayloadStringBuffer.ts new file mode 100644 index 000000000..9caef25ea --- /dev/null +++ b/src/common/parser/PayloadStringBuffer.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Accumulates string payloads from multiple chunks without O(n²) string concatenation. + */ +export class PayloadStringBuffer { + private _chunks: string[] = []; + private _length = 0; + + public reset(): void { + this._chunks.length = 0; + this._length = 0; + } + + /** + * @returns true if the payload limit was exceeded (buffer is cleared in that case) + */ + public add(chunk: string, limit: number): boolean { + this._chunks.push(chunk); + this._length += chunk.length; + if (this._length > limit) { + this.reset(); + return true; + } + return false; + } + + public toString(): string { + return this._chunks.join(''); + } +}