Fix O(n²) payload accumulation in DCS/OSC/APC handlers

Replace repeated string concatenation with a chunk buffer that joins
once on completion. Fixes #5916.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-27 15:34:48 +00:00
co-authored by Daniel Imms
parent c62e6dd765
commit fad4821fd9
4 changed files with 55 additions and 24 deletions
+7 -8
View File
@@ -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<boolean>) { }
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;
}
+7 -8
View File
@@ -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;
}
+7 -8
View File
@@ -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<boolean>) { }
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;
}
+34
View File
@@ -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('');
}
}