diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 1c75b3b9..269d9374 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -11,6 +11,8 @@ import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/rend import { Disposable, toDisposable } from 'common/Lifecycle'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; +import { ILogService } from 'common/services/Services'; +import { traceCall } from 'common/services/LogService'; interface IVertices { attributes: Float32Array; @@ -212,6 +214,7 @@ export class GlyphRenderer extends Disposable { return this._atlas ? this._atlas.beginFrame() : true; } + @traceCall 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 diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 97e2ce0c..3e091f0b 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -8,9 +8,10 @@ import { ITerminal } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; +import { setTraceLogger } from 'common/services/LogService'; export class WebglAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; @@ -51,8 +52,13 @@ export class WebglAddon extends Disposable implements ITerminalAddon { const charSizeService: ICharSizeService = unsafeCore._charSizeService; const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; const decorationService: IDecorationService = unsafeCore._decorationService; + const logService: ILogService = unsafeCore._logService; const themeService: IThemeService = unsafeCore._themeService; + // Set trace logger just in case it hasn't been yet which could happen when the addon is + // bundled separately to the core module + setTraceLogger(logService); + this._renderer = this.register(new WebglRenderer( terminal, characterJoinerService, diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index c21086e2..ae770410 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -16,8 +16,8 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; -import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; -import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { Disposable, MutableDisposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; +import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; import { CharData, IBufferLine, ICellData } from 'common/Types'; import { IDisposable, Terminal } from 'xterm'; import { GlyphRenderer } from './GlyphRenderer'; @@ -27,10 +27,11 @@ import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { IRenderLayer } from './renderLayer/Types'; import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { IWebGL2RenderingContext } from './Types'; +import { traceCall } from 'common/services/LogService'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; + private _cursorBlinkStateManager: MutableDisposable = new MutableDisposable(); private _charAtlasDisposable: IDisposable | undefined; private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; @@ -202,7 +203,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleBlur(this._terminal); } - this._cursorBlinkStateManager?.pause(); + this._cursorBlinkStateManager.value?.pause(); // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } @@ -211,7 +212,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleFocus(this._terminal); } - this._cursorBlinkStateManager?.resume(); + this._cursorBlinkStateManager.value?.resume(); // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } @@ -228,7 +229,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.handleCursorMove(this._terminal); } - this._cursorBlinkStateManager?.restartBlinkAnimation(); + this._cursorBlinkStateManager.value?.restartBlinkAnimation(); } private _handleOptionsChanged(): void { @@ -311,7 +312,7 @@ export class WebglRenderer extends Disposable implements IRenderer { l.reset(this._terminal); } - this._cursorBlinkStateManager?.restartBlinkAnimation(); + this._cursorBlinkStateManager.value?.restartBlinkAnimation(); this._updateCursorBlink(); } @@ -323,6 +324,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return false; } + @traceCall public renderRows(start: number, end: number): void { if (!this._isAttached) { if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) { @@ -357,21 +359,18 @@ export class WebglRenderer extends Disposable implements IRenderer { // Render this._rectangleRenderer?.renderBackgrounds(); this._glyphRenderer?.render(this._model); - if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isCursorVisible) { + if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible) { this._rectangleRenderer?.renderCursor(); } } private _updateCursorBlink(): void { if (this._terminal.options.cursorBlink) { - if (!this._cursorBlinkStateManager) { - this._cursorBlinkStateManager = this.register(new CursorBlinkStateManager(() => { - this._requestRedrawCursor(); - }, this._coreBrowserService)); - } + this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => { + this._requestRedrawCursor(); + }, this._coreBrowserService); } else { - this._cursorBlinkStateManager?.dispose(); - this._cursorBlinkStateManager = undefined; + this._cursorBlinkStateManager.clear(); } // Request a refresh from the terminal as management of rendering is being // moved back to the terminal @@ -406,7 +405,7 @@ export class WebglRenderer extends Disposable implements IRenderer { const isCursorVisible = this._coreService.isCursorInitialized && !this._coreService.isCursorHidden && - (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isCursorVisible); + (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible); this._model.cursor = undefined; let modelUpdated = false; diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index f752dc5b..74989973 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -21,6 +21,7 @@ }, "strict": true, "downlevelIteration": true, + "experimentalDecorators": true, "types": [ "../../../node_modules/@types/mocha" ] diff --git a/demo/client.ts b/demo/client.ts index ce6a5c64..7c586528 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -433,7 +433,7 @@ function initOptions(term: TerminalType): void { fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - logLevel: ['debug', 'info', 'warn', 'error', 'off'], + logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'], theme: ['default', 'xtermjs', 'sapphire', 'light'], wordSeparator: null }; diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index eec560e0..3ee21519 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -16,6 +16,7 @@ import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { EventEmitter } from 'common/EventEmitter'; import { IColorContrastCache } from 'browser/Types'; +import { traceCall } from 'common/services/LogService'; /** * A shared object which is used to draw nothing for a particular cell. @@ -425,6 +426,7 @@ export class TextureAtlas implements ITextureAtlas { return this._config.colors.contrastCache; } + @traceCall private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; diff --git a/src/common/Lifecycle.test.ts b/src/common/Lifecycle.test.ts index 6f7c7884..8f8a4932 100644 --- a/src/common/Lifecycle.test.ts +++ b/src/common/Lifecycle.test.ts @@ -4,7 +4,8 @@ */ import { assert } from 'chai'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, MutableDisposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; class TestDisposable extends Disposable { public get isDisposed(): boolean { @@ -43,3 +44,52 @@ describe('Disposable', () => { }); }); }); + +describe('MutableDisposable', () => { + const mutable = new MutableDisposable(); + class TrackedDisposable extends Disposable { + public get isDisposed(): boolean { return this._isDisposed; } + } + describe('value', () => { + it('should set the value', () => { + const d1 = new TrackedDisposable(); + mutable.value = d1; + assert.strictEqual(mutable.value, d1); + assert.isFalse(d1.isDisposed); + }); + it('should dispose of any previous value', () => { + const d1 = new TrackedDisposable(); + const d2 = new TrackedDisposable(); + mutable.value = d1; + mutable.value = d2; + assert.strictEqual(mutable.value, d2); + assert.isTrue(d1.isDisposed); + assert.isFalse(d2.isDisposed); + }); + }); + describe('clear', () => { + it('should clear and dispose of the object', () => { + const d1 = new TrackedDisposable(); + mutable.value = d1; + mutable.clear(); + assert.strictEqual(mutable.value, undefined); + assert.isTrue(d1.isDisposed); + }); + }); + it('dispose', () => { + it('should dispose of the object', () => { + const d1 = new TrackedDisposable(); + mutable.value = d1; + mutable.dispose(); + assert.strictEqual(mutable.value, undefined); + assert.isTrue(d1.isDisposed); + }); + it('should prevent using the MutableDisposable again', () => { + const d1 = new TrackedDisposable(); + mutable.value = d1; + mutable.dispose(); + mutable.value = new TrackedDisposable(); + assert.strictEqual(mutable.value, undefined); + }); + }); +}); diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index b3a7cc21..659403de 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -50,6 +50,42 @@ export abstract class Disposable implements IDisposable { } } +export class MutableDisposable implements IDisposable { + private _value?: T; + private _isDisposed = false; + + /** + * Gets the value if it exists. + */ + public get value(): T | undefined { + return this._isDisposed ? undefined : this._value; + } + + /** + * Sets the value, disposing of the old value if it exists. + */ + public set value(value: T | undefined) { + if (this._isDisposed || value === this._value) { + return; + } + this._value?.dispose(); + this._value = value; + } + + /** + * Resets the stored value and disposes of the previously stored value. + */ + public clear(): void { + this.value = undefined; + } + + public dispose(): void { + this._isDisposed = true; + this._value?.dispose(); + this._value = undefined; + } +} + /** * Wrap a function in a disposable. */ diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index ceb0dc29..fcf0c2cd 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -105,6 +105,7 @@ export class MockCoreService implements ICoreService { export class MockLogService implements ILogService { public serviceBrand: any; public logLevel = LogLevelEnum.DEBUG; + public trace(message: any, ...optionalParams: any[]): void { } public debug(message: any, ...optionalParams: any[]): void { } public info(message: any, ...optionalParams: any[]): void { } public warn(message: any, ...optionalParams: any[]): void { } diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index b297049e..b3a7450c 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -21,6 +21,7 @@ interface IConsole { declare const console: IConsole; const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { + trace: LogLevelEnum.TRACE, debug: LogLevelEnum.DEBUG, info: LogLevelEnum.INFO, warn: LogLevelEnum.WARN, @@ -42,6 +43,9 @@ export class LogService extends Disposable implements ILogService { super(); this._updateLogLevel(); this.register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel())); + + // For trace logging, assume the latest created log service is valid + traceLogger = this; } private _updateLogLevel(): void { @@ -61,6 +65,12 @@ export class LogService extends Disposable implements ILogService { type.call(console, (this._optionsService.options.logger ? '' : LOG_PREFIX) + message, ...optionalParams); } + public trace(message: string, ...optionalParams: any[]): void { + if (this._logLevel <= LogLevelEnum.TRACE) { + this._log(this._optionsService.options.logger?.trace.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams); + } + } + public debug(message: string, ...optionalParams: any[]): void { if (this._logLevel <= LogLevelEnum.DEBUG) { this._log(this._optionsService.options.logger?.debug.bind(this._optionsService.options.logger) ?? console.log, message, optionalParams); @@ -85,3 +95,30 @@ export class LogService extends Disposable implements ILogService { } } } + +let traceLogger: ILogService; +export function setTraceLogger(logger: ILogService): void { + traceLogger = logger; +} + +/** + * A decorator that can be used to automatically log trace calls to the decorated function. + */ +export function traceCall(_target: any, key: string, descriptor: any): any { + if (typeof descriptor.value !== 'function') { + throw new Error('not supported'); + } + const fnKey = 'value'; + const fn = descriptor.value; + descriptor[fnKey] = function (...args: any[]) { + // Early exit + if (traceLogger.logLevel !== LogLevelEnum.TRACE) { + return fn.apply(this, args); + } + + traceLogger.trace(`GlyphRenderer#${fn.name}(${args.map(e => JSON.stringify(e)).join(', ')})`); + const result = fn.apply(this, args); + traceLogger.trace(`GlyphRenderer#${fn.name} return`, result); + return result; + }; +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 625fc7de..52c2a79f 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -142,11 +142,12 @@ export interface IInstantiationService { } export enum LogLevelEnum { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - OFF = 4 + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + OFF = 5 } export const ILogService = createDecorator('LogService'); @@ -155,6 +156,7 @@ export interface ILogService { readonly logLevel: LogLevelEnum; + trace(message: any, ...optionalParams: any[]): void; debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; warn(message: any, ...optionalParams: any[]): void; @@ -201,7 +203,7 @@ export interface IOptionsService { } export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; +export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off'; export interface ITerminalOptions { allowProposedApi?: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 118c5905..99aa8237 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -11,7 +11,7 @@ declare module 'xterm-headless' { /** * A string representing log level. */ - export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; + export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off'; /** * An object containing options for the terminal. @@ -100,11 +100,12 @@ declare module 'xterm-headless' { * What log level to use, this will log for all levels below and including * what is set: * - * 1. debug - * 2. info (default) - * 3. warn - * 4. error - * 5. off + * 1. trace + * 2. debug + * 3. info (default) + * 4. warn + * 5. error + * 6. off */ logLevel?: LogLevel; @@ -314,9 +315,14 @@ declare module 'xterm-headless' { * A replacement logger for `console`. */ export interface ILogger { + /** + * Log a trace message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to trace. + */ + trace(message: string, ...args: any[]): void; /** * Log a debug message, this will only be called if - * {@link ITerminalOptions.logLevel} is set to debug. + * {@link ITerminalOptions.logLevel} is set to debug or below. */ debug(message: string, ...args: any[]): void; /** diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d7c5e320..3f603b9e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -18,7 +18,7 @@ declare module 'xterm' { /** * A string representing log level. */ - export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; + export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off'; /** * An object containing options for the terminal. @@ -158,11 +158,12 @@ declare module 'xterm' { * What log level to use, this will log for all levels below and including * what is set: * - * 1. debug - * 2. info (default) - * 3. warn - * 4. error - * 5. off + * 1. trace + * 2. debug + * 3. info (default) + * 4. warn + * 5. error + * 6. off */ logLevel?: LogLevel; @@ -391,9 +392,14 @@ declare module 'xterm' { * A replacement logger for `console`. */ export interface ILogger { + /** + * Log a trace message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to trace. + */ + trace(message: string, ...args: any[]): void; /** * Log a debug message, this will only be called if - * {@link ITerminalOptions.logLevel} is set to debug. + * {@link ITerminalOptions.logLevel} is set to debug or below. */ debug(message: string, ...args: any[]): void; /**