Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec5452fc0 | ||
|
|
81b3b1ec46 | ||
|
|
8ab575621d | ||
|
|
3b08b7dcef | ||
|
|
b44d036524 | ||
|
|
9872be0c29 |
@@ -436,9 +436,9 @@ export class ImageStorage implements IDisposable {
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
let e: IExtendedAttrsImage;
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
e = line._extendedAttrs?.[col] ?? EMPTY_ATTRS;
|
||||
} else {
|
||||
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
const maybeImg = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
|
||||
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
@@ -461,7 +461,7 @@ export class ImageStorage implements IDisposable {
|
||||
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
const nextE = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
|
||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
@@ -515,7 +515,7 @@ export class ImageStorage implements IDisposable {
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs?.[oldCol] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -560,7 +560,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1) {
|
||||
const orig = this._images.get(e.imageId)?.orig;
|
||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||
@@ -580,7 +580,7 @@ export class ImageStorage implements IDisposable {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||
const spec = this._images.get(e.imageId);
|
||||
if (spec) {
|
||||
@@ -611,7 +611,7 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const old = line._extendedAttrs[x];
|
||||
const old = line._extendedAttrs?.[x];
|
||||
if (old) {
|
||||
if (old.imageId !== undefined) {
|
||||
// found an old ExtendedAttrsImage, since we know that
|
||||
@@ -627,13 +627,13 @@ export class ImageStorage implements IDisposable {
|
||||
return;
|
||||
}
|
||||
// found a plain ExtendedAttrs instance, clone it to new entry
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
||||
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fall-through: always create new ExtendedAttrsImage entry
|
||||
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
||||
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
||||
}
|
||||
|
||||
private _evictOnAlternate(): void {
|
||||
@@ -652,7 +652,7 @@ export class ImageStorage implements IDisposable {
|
||||
}
|
||||
for (let x = 0; x < this._terminal.cols; ++x) {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const imgId = line._extendedAttrs[x]?.imageId;
|
||||
const imgId = line._extendedAttrs?.[x]?.imageId;
|
||||
if (imgId) {
|
||||
const spec = this._images.get(imgId);
|
||||
if (spec) {
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface IExtendedAttrsImage extends IExtendedAttrs {
|
||||
|
||||
/* eslint-disable */
|
||||
export interface IBufferLineExt extends IBufferLine {
|
||||
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
|
||||
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined} | undefined;
|
||||
_data: Uint32Array;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,10 @@ test.describe('ImageAddon', () => {
|
||||
}
|
||||
// bufferline privates
|
||||
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
|
||||
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
|
||||
strictEqual(await ctx.page.evaluate(`(() => {
|
||||
const ea = window.term._core.buffer.lines.get(0)._extendedAttrs;
|
||||
return ea === undefined || (typeof ea === 'object' && ea !== null);
|
||||
})()`), true);
|
||||
// inputhandler privates
|
||||
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData');
|
||||
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
|
||||
|
||||
@@ -71,12 +71,20 @@ export interface IBufferLineStringCache {
|
||||
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
|
||||
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
|
||||
*/
|
||||
/**
|
||||
* Shared empty sparse map for lines with no combining data.
|
||||
* Uses a sentinel + copy-on-write so `copyFrom` can detect empty `_combined` with
|
||||
* reference equality. `_extendedAttrs` uses `undefined` instead (addon-image writes
|
||||
* the map directly without BufferLine APIs).
|
||||
*/
|
||||
const EMPTY_SPARSE_MAP: {[index: number]: string} = Object.create(null);
|
||||
|
||||
export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array;
|
||||
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
protected _combined: {[index: number]: string} = EMPTY_SPARSE_MAP;
|
||||
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} | undefined;
|
||||
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
|
||||
public length: number;
|
||||
|
||||
@@ -121,6 +129,7 @@ export class BufferLine implements IBufferLine {
|
||||
this._invalidateStringCache();
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
this._ensureCombinedMap();
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
} else {
|
||||
@@ -211,7 +220,7 @@ export class BufferLine implements IBufferLine {
|
||||
cell.combinedData = '';
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
cell.extended = this._extendedAttrs[index]!;
|
||||
cell.extended = this._extendedAttrs![index]!;
|
||||
} else {
|
||||
// Do not mutate cell.extended in place: it may still reference this line's map entry from a
|
||||
// prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
|
||||
@@ -226,10 +235,11 @@ export class BufferLine implements IBufferLine {
|
||||
public setCell(index: number, cell: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
this._ensureCombinedMap();
|
||||
this._combined[index] = cell.combinedData;
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = cell.extended;
|
||||
this._ensureExtendedAttrsMap()[index] = cell.extended;
|
||||
}
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
|
||||
@@ -244,7 +254,7 @@ export class BufferLine implements IBufferLine {
|
||||
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
|
||||
this._invalidateStringCache();
|
||||
if (attrs.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = attrs.extended;
|
||||
this._ensureExtendedAttrsMap()[index] = attrs.extended;
|
||||
}
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
|
||||
@@ -262,12 +272,14 @@ export class BufferLine implements IBufferLine {
|
||||
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
// we already have a combined string, simply add
|
||||
this._ensureCombinedMap();
|
||||
this._combined[index] += stringFromCodePoint(codePoint);
|
||||
} else {
|
||||
if (content & Content.CODEPOINT_MASK) {
|
||||
// normal case for combining chars:
|
||||
// - move current leading char + new one into combined string
|
||||
// - set combined flag
|
||||
this._ensureCombinedMap();
|
||||
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
|
||||
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
|
||||
content |= Content.IS_COMBINED_MASK;
|
||||
@@ -410,11 +422,13 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
}
|
||||
// remove any cut off extended attributes
|
||||
const extKeys = Object.keys(this._extendedAttrs);
|
||||
for (let i = 0; i < extKeys.length; i++) {
|
||||
const key = parseInt(extKeys[i], 10);
|
||||
if (key >= cols) {
|
||||
delete this._extendedAttrs[key];
|
||||
if (this._extendedAttrs) {
|
||||
const extKeys = Object.keys(this._extendedAttrs);
|
||||
for (let i = 0; i < extKeys.length; i++) {
|
||||
const key = parseInt(extKeys[i], 10);
|
||||
if (key >= cols) {
|
||||
delete this._extendedAttrs[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,8 +464,8 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
this._combined = EMPTY_SPARSE_MAP;
|
||||
this._extendedAttrs = undefined;
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
}
|
||||
@@ -608,19 +622,57 @@ export class BufferLine implements IBufferLine {
|
||||
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
|
||||
const srcStart = srcCol * Constants.CELL_INDICIES;
|
||||
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
|
||||
this._ensureCombinedMap();
|
||||
this._combined[destCol] = src._combined[srcCol];
|
||||
}
|
||||
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
|
||||
this._ensureExtendedAttrsMap()[destCol] = src._extendedAttrs![srcCol]!;
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */
|
||||
private _copySparseMapsFrom(line: BufferLine): void {
|
||||
const srcCombined = line._combined;
|
||||
const srcExtended = line._extendedAttrs;
|
||||
|
||||
// Fast path: blank / scroll-recycle source (empty `_combined` sentinel, no extended map).
|
||||
if (srcCombined === EMPTY_SPARSE_MAP && !srcExtended) {
|
||||
this._combined = EMPTY_SPARSE_MAP;
|
||||
this._extendedAttrs = undefined;
|
||||
return;
|
||||
}
|
||||
this._copySparseMapsFromDense(line);
|
||||
}
|
||||
|
||||
/** Copy sparse maps by scanning `_data` flags (inlined, no per-cell function calls). */
|
||||
private _copySparseMapsFromDense(line: BufferLine): void {
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
this._copyCellMapsFrom(line, i, i);
|
||||
const srcData = line._data;
|
||||
const srcCombined = line._combined;
|
||||
const srcExtended = line._extendedAttrs;
|
||||
const len = line.length;
|
||||
const indices = Constants.CELL_INDICIES;
|
||||
const combined = this._combined;
|
||||
const extendedAttrs = this._extendedAttrs;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const start = i * indices;
|
||||
if (srcData[start + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
|
||||
combined[i] = srcCombined[i];
|
||||
}
|
||||
if (srcData[start + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
extendedAttrs[i] = srcExtended![i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureCombinedMap(): void {
|
||||
if (this._combined === EMPTY_SPARSE_MAP) {
|
||||
this._combined = {};
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureExtendedAttrsMap(): {[index: number]: IExtendedAttrs | undefined} {
|
||||
return this._extendedAttrs ??= {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { BgFlags, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
const ITERATIONS = 500_000;
|
||||
const stringCache = new BufferLineStringCache();
|
||||
const nullCell = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
const yCell = CellData.fromCharData([0, 'y', 1, 'y'.charCodeAt(0)]);
|
||||
|
||||
function fillPlainY(line: BufferLine, cols: number): void {
|
||||
for (let i = 0; i < cols; i++) {
|
||||
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
function setupScrollRecyclePair(cols: number): { recycled: BufferLine, blank: BufferLine } {
|
||||
const blank = new BufferLine(stringCache, cols, nullCell, false);
|
||||
const recycled = new BufferLine(stringCache, cols, yCell, false);
|
||||
fillPlainY(recycled, cols);
|
||||
return { recycled, blank };
|
||||
}
|
||||
|
||||
/** Source line with combining chars and extended attrs spread across columns. */
|
||||
function buildSparseSourceLine(cols: number): BufferLine {
|
||||
const line = new BufferLine(stringCache, cols, nullCell, false);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
if (i % 10 === 0) {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.extended.underlineStyle = UnderlineStyle.CURLY;
|
||||
cell.bg |= BgFlags.HAS_EXTENDED;
|
||||
line.setCell(i, cell);
|
||||
} else if (i % 17 === 0) {
|
||||
line.setCell(i, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
} else {
|
||||
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function setupCopyFromSparsePair(cols: number): { dest: BufferLine, src: BufferLine } {
|
||||
const src = buildSparseSourceLine(cols);
|
||||
const dest = new BufferLine(stringCache, cols, yCell, false);
|
||||
fillPlainY(dest, cols);
|
||||
return { dest, src };
|
||||
}
|
||||
|
||||
perfContext('BufferLine.copyFrom (scroll recycle)', () => {
|
||||
perfContext('cols=80', () => {
|
||||
let recycled: BufferLine;
|
||||
let blank: BufferLine;
|
||||
before(() => {
|
||||
({ recycled, blank } = setupScrollRecyclePair(80));
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
recycled.copyFrom(blank);
|
||||
}
|
||||
return { payloadSize: ITERATIONS };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('cols=279', () => {
|
||||
let recycled: BufferLine;
|
||||
let blank: BufferLine;
|
||||
before(() => {
|
||||
({ recycled, blank } = setupScrollRecyclePair(279));
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
recycled.copyFrom(blank);
|
||||
}
|
||||
return { payloadSize: ITERATIONS };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
});
|
||||
|
||||
perfContext('BufferLine.copyFrom (sparse source: combined + extended attrs)', () => {
|
||||
perfContext('cols=80', () => {
|
||||
let dest: BufferLine;
|
||||
let src: BufferLine;
|
||||
before(() => {
|
||||
({ dest, src } = setupCopyFromSparsePair(80));
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
dest.copyFrom(src);
|
||||
}
|
||||
return { payloadSize: ITERATIONS };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('cols=279', () => {
|
||||
let dest: BufferLine;
|
||||
let src: BufferLine;
|
||||
before(() => {
|
||||
({ dest, src } = setupCopyFromSparsePair(279));
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
dest.copyFrom(src);
|
||||
}
|
||||
return { payloadSize: ITERATIONS };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user