Skip to content

Commit

Permalink
Merge pull request #4081 from Tyriar/more_gc
Browse files Browse the repository at this point in the history
More garbage collection optimizations for GlyphRenderer
  • Loading branch information
Tyriar authored Aug 28, 2022
2 parents eb31850 + caaca16 commit 4effa38
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 87 deletions.
67 changes: 35 additions & 32 deletions addons/xterm-addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ const INDICES_PER_CELL = 10;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;

/** Work variables to avoid garbage collection. */
const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = {
i: 0,
glyph: undefined,
leftCellPadding: 0,
clippedPixels: 0
};

export class GlyphRenderer extends Disposable {
private _atlas: WebglCharAtlas | undefined;

Expand Down Expand Up @@ -170,18 +178,20 @@ export class GlyphRenderer extends Disposable {
}

public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
}

private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
const terminal = this._terminal;

const i = (y * terminal.cols + x) * INDICES_PER_CELL;
w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL;

// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}

Expand All @@ -190,47 +200,40 @@ export class GlyphRenderer extends Disposable {
}

// Get the glyph
let rasterizedGlyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
} else {
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}

// Fill empty if no glyph was found
if (!rasterizedGlyph) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}

const leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
if (bg !== lastBg && rasterizedGlyph.offset.x > leftCellPadding) {
const clippedPixels = rasterizedGlyph.offset.x - leftCellPadding;
w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) {
w.clippedPixels = w.glyph.offset.x - w.leftCellPadding;
// a_origin
array[i ] = -(rasterizedGlyph.offset.x - clippedPixels) + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft;
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = (rasterizedGlyph.size.x - clippedPixels) / this._dimensions.scaledCanvasWidth;
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth;
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x + clippedPixels / this._atlas.cacheCanvas.width;
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width;
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
// a_texsize
array[i + 6] = rasterizedGlyph.sizeClipSpace.x - clippedPixels / this._atlas.cacheCanvas.width;
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width;
array[w.i + 7] = w.glyph.sizeClipSpace.y;
} else {
// a_origin
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft;
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth;
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
array[w.i + 4] = w.glyph.texturePositionClipSpace.x;
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
// a_texsize
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
array[w.i + 6] = w.glyph.sizeClipSpace.x;
array[w.i + 7] = w.glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
Expand Down
55 changes: 17 additions & 38 deletions addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { color, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';

// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
Expand Down Expand Up @@ -52,11 +53,16 @@ interface ICharAtlasActiveRow {
height: number;
}

/** Work variables to avoid garbage collection. */
const w: { glyph: IRasterizedGlyph | undefined } = {
glyph: undefined
};

export class WebglCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;

private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();

// The texture that the atlas is drawn to
public cacheCanvas: HTMLCanvasElement;
Expand Down Expand Up @@ -124,13 +130,7 @@ export class WebglCharAtlas implements IDisposable {
// Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
this._cacheMap[i] = {
[DEFAULT_COLOR]: {
[DEFAULT_COLOR]: {
[DEFAULT_EXT]: rasterizedGlyph
}
}
};
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
}
}

Expand All @@ -148,8 +148,8 @@ export class WebglCharAtlas implements IDisposable {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._cacheMap = {};
this._cacheMapCombined = {};
this._cacheMap.clear();
this._cacheMapCombined.clear();
this._currentRow.x = 0;
this._currentRow.y = 0;
this._currentRow.height = 0;
Expand All @@ -169,39 +169,18 @@ export class WebglCharAtlas implements IDisposable {
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
private _getFromCacheMap(
cacheMap: { [key: string | number]: IRasterizedGlyphSet },
cacheMap: FourKeyMap<string | number, number, number, number, IRasterizedGlyph>,
key: string | number,
bg: number,
fg: number,
ext: number
): IRasterizedGlyph {
let rasterizedGlyphSet = cacheMap[key];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
cacheMap[key] = rasterizedGlyphSet;
w.glyph = cacheMap.get(key, bg, fg, ext);
if (!w.glyph) {
w.glyph = this._drawToCache(key, bg, fg, ext);
cacheMap.set(key, bg, fg, ext, w.glyph);
}

let rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (!rasterizedGlyphSetBg) {
rasterizedGlyphSetBg = {};
rasterizedGlyphSet[bg] = rasterizedGlyphSetBg;
}

let rasterizedGlyph: IRasterizedGlyph | undefined;
let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg];
if (!rasterizedGlyphSetFg) {
rasterizedGlyphSetFg = {};
rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg;
} else {
rasterizedGlyph = rasterizedGlyphSetFg[ext];
}

if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(key, bg, fg, ext);
rasterizedGlyphSetFg[ext] = rasterizedGlyph;
}

return rasterizedGlyph;
return w.glyph;
}

private _getColorFromAnsiIndex(idx: number): IColor {
Expand Down
29 changes: 12 additions & 17 deletions src/browser/ColorContrastCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,30 @@

import { IColorContrastCache } from 'browser/Types';
import { IColor } from 'common/Types';
import { TwoKeyMap } from 'common/MultiKeyMap';

export class ColorContrastCache implements IColorContrastCache {
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};

public clear(): void {
this._color = {};
this._rgba = {};
}
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
private _css: TwoKeyMap</* bg */number, /* fg */number, string | null> = new TwoKeyMap();

public setCss(bg: number, fg: number, value: string | null): void {
if (!this._rgba[bg]) {
this._rgba[bg] = {};
}
this._rgba[bg]![fg] = value;
this._css.set(bg, fg, value);
}

public getCss(bg: number, fg: number): string | null | undefined {
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
return this._css.get(bg, fg);
}

public setColor(bg: number, fg: number, value: IColor | null): void {
if (!this._color[bg]) {
this._color[bg] = {};
}
this._color[bg]![fg] = value;
this._color.set(bg, fg, value);
}

public getColor(bg: number, fg: number): IColor | null | undefined {
return this._color[bg] ? this._color[bg]![fg] : undefined;
return this._color.get(bg, fg);
}

public clear(): void {
this._color.clear();
this._css.clear();
}
}
69 changes: 69 additions & 0 deletions src/common/MultiKeyMap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { assert } from 'chai';
import { FourKeyMap, TwoKeyMap } from 'common/MultiKeyMap';

const strictEqual = assert.strictEqual;

describe('TwoKeyMap', () => {
let map: TwoKeyMap<number | string, number | string, string>;

beforeEach(() => {
map = new TwoKeyMap();
});

it('set, get', () => {
strictEqual(map.get(1, 2), undefined);
map.set(1, 2, 'foo');
strictEqual(map.get(1, 2), 'foo');
map.set(1, 3, 'bar');
strictEqual(map.get(1, 2), 'foo');
strictEqual(map.get(1, 3), 'bar');
map.set(2, 2, 'foo2');
map.set(2, 3, 'bar2');
strictEqual(map.get(1, 2), 'foo');
strictEqual(map.get(1, 3), 'bar');
strictEqual(map.get(2, 2), 'foo2');
strictEqual(map.get(2, 3), 'bar2');
});
it('clear', () => {
strictEqual(map.get(1, 2), undefined);
map.set(1, 2, 'foo');
strictEqual(map.get(1, 2), 'foo');
map.clear();
strictEqual(map.get(1, 2), undefined);
});
});

describe('FourKeyMap', () => {
let map: FourKeyMap<number | string, number | string, number | string, number | string, string>;

beforeEach(() => {
map = new FourKeyMap();
});

it('set, get', () => {
strictEqual(map.get(1, 2, 3, 4), undefined);
map.set(1, 2, 3, 4, 'foo');
strictEqual(map.get(1, 2, 3, 4), 'foo');
map.set(1, 3, 3, 4, 'bar');
strictEqual(map.get(1, 2, 3, 4), 'foo');
strictEqual(map.get(1, 3, 3, 4), 'bar');
map.set(2, 2, 3, 4, 'foo2');
map.set(2, 3, 3, 4, 'bar2');
strictEqual(map.get(1, 2, 3, 4), 'foo');
strictEqual(map.get(1, 3, 3, 4), 'bar');
strictEqual(map.get(2, 2, 3, 4), 'foo2');
strictEqual(map.get(2, 3, 3, 4), 'bar2');
});
it('clear', () => {
strictEqual(map.get(1, 2, 3, 4), undefined);
map.set(1, 2, 3, 4, 'foo');
strictEqual(map.get(1, 2, 3, 4), 'foo');
map.clear();
strictEqual(map.get(1, 2, 3, 4), undefined);
});
});
42 changes: 42 additions & 0 deletions src/common/MultiKeyMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/

export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {};

public set(first: TFirst, second: TSecond, value: TValue): void {
if (!this._data[first]) {
this._data[first] = {};
}
this._data[first as string | number]![second] = value;
}

public get(first: TFirst, second: TSecond): TValue | undefined {
return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined;
}

public clear(): void {
this._data = {};
}
}

export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();

public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
if (!this._data.get(first, second)) {
this._data.set(first, second, new TwoKeyMap());
}
this._data.get(first, second)!.set(third, fourth, value);
}

public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
return this._data.get(first, second)?.get(third, fourth);
}

public clear(): void {
this._data.clear();
}
}

0 comments on commit 4effa38

Please sign in to comment.