Merge branch 'master' into buffer_optimizations

This commit is contained in:
Jörg Breitbart
2022-09-28 13:22:16 +02:00
27 changed files with 317 additions and 174 deletions
+91 -31
View File
@@ -3,8 +3,14 @@
* @license MIT
*/
import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types';
let $r = 0;
let $g = 0;
let $b = 0;
let $a = 0;
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
@@ -29,8 +35,8 @@ export namespace channels {
*/
export namespace color {
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
$a = (fg.rgba & 0xFF) / 255;
if ($a === 1) {
return {
css: fg.css,
rgba: fg.rgba
@@ -42,11 +48,11 @@ export namespace color {
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = channels.toCss(r, g, b);
const rgba = channels.toRgba(r, g, b);
$r = bgR + Math.round((fgR - bgR) * $a);
$g = bgG + Math.round((fgG - bgG) * $a);
$b = bgB + Math.round((fgB - bgB) * $a);
const css = channels.toCss($r, $g, $b);
const rgba = channels.toRgba($r, $g, $b);
return { css, rgba };
}
@@ -68,25 +74,25 @@ export namespace color {
export function opaque(color: IColor): IColor {
const rgbaColor = (color.rgba | 0xFF) >>> 0;
const [r, g, b] = rgba.toChannels(rgbaColor);
[$r, $g, $b] = rgba.toChannels(rgbaColor);
return {
css: channels.toCss(r, g, b),
css: channels.toCss($r, $g, $b),
rgba: rgbaColor
};
}
export function opacity(color: IColor, opacity: number): IColor {
const a = Math.round(opacity * 0xFF);
const [r, g, b] = rgba.toChannels(color.rgba);
$a = Math.round(opacity * 0xFF);
[$r, $g, $b] = rgba.toChannels(color.rgba);
return {
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
css: channels.toCss($r, $g, $b, $a),
rgba: channels.toRgba($r, $g, $b, $a)
};
}
export function multiplyOpacity(color: IColor, factor: number): IColor {
const a = color.rgba & 0xFF;
return opacity(color, (a * factor) / 0xFF);
$a = color.rgba & 0xFF;
return opacity(color, ($a * factor) / 0xFF);
}
export function toColorRGB(color: IColor): IColorRGB {
@@ -98,21 +104,45 @@ export namespace color {
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
*/
export namespace css {
let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined;
if (!isNode) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
if (ctx) {
$ctx = ctx;
$ctx.globalCompositeOperation = 'copy';
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
}
}
/**
* Converts a css string to an IColor, this should handle all valid CSS color strings and will
* throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse.
*
* Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node
* environment.
*/
export function toColor(css: string): IColor {
// Formats: #rgb[a] and #rrggbb[aa]
if (css.match(/#[0-9a-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
const r = parseInt(css.slice(1, 2).repeat(2), 16);
const g = parseInt(css.slice(2, 3).repeat(2), 16);
const b = parseInt(css.slice(3, 4).repeat(2), 16);
return rgba.toColor(r, g, b);
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
return rgba.toColor($r, $g, $b);
}
case 5: { // #rgba
const r = parseInt(css.slice(1, 2).repeat(2), 16);
const g = parseInt(css.slice(2, 3).repeat(2), 16);
const b = parseInt(css.slice(3, 4).repeat(2), 16);
const a = parseInt(css.slice(4, 5).repeat(2), 16);
return rgba.toColor(r, g, b, a);
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
$a = parseInt(css.slice(4, 5).repeat(2), 16);
return rgba.toColor($r, $g, $b, $a);
}
case 7: // #rrggbb
return {
@@ -126,15 +156,45 @@ export namespace css {
};
}
}
// Formats: rgb() or rgba()
const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);
if (rgbaMatch) { // rgb() or rgba()
const r = parseInt(rgbaMatch[1]);
const g = parseInt(rgbaMatch[2]);
const b = parseInt(rgbaMatch[3]);
const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
return rgba.toColor(r, g, b, a);
if (rgbaMatch) {
$r = parseInt(rgbaMatch[1]);
$g = parseInt(rgbaMatch[2]);
$b = parseInt(rgbaMatch[3]);
$a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
return rgba.toColor($r, $g, $b, $a);
}
throw new Error('css.toColor: Unsupported css format');
// Validate the context is available for canvas-based color parsing
if (!$ctx || !$litmusColor) {
throw new Error('css.toColor: Unsupported css format');
}
// Validate the color using canvas fillStyle
// See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
$ctx.fillStyle = $litmusColor;
$ctx.fillStyle = css;
if (typeof $ctx.fillStyle !== 'string') {
throw new Error('css.toColor: Unsupported css format');
}
$ctx.fillRect(0, 0, 1, 1);
[$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data;
// Validate the color is non-transparent as color hue gets lost when drawn to the canvas
if ($a !== 0xFF) {
throw new Error('css.toColor: Unsupported css format');
}
// Extract the color from the canvas' fillStyle property which exposes the color value in rgba()
// format
// See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
return {
rgba: channels.toRgba($r, $g, $b, $a),
css
};
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ interface INavigator {
// we want this module to live in common.
declare const navigator: INavigator;
const isNode = (typeof navigator === 'undefined') ? true : false;
export const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
+147
View File
@@ -0,0 +1,147 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { isNode } from 'common/Platform';
interface ITaskQueue {
/**
* Adds a task to the queue which will run in a future idle callback.
*/
enqueue(task: () => void): void;
/**
* Flushes the queue, running all remaining tasks synchronously.
*/
flush(): void;
/**
* Clears any remaining tasks from the queue, these will not be run.
*/
clear(): void;
}
interface ITaskDeadline {
timeRemaining(): number;
}
type CallbackWithDeadline = (deadline: ITaskDeadline) => void;
abstract class TaskQueue implements ITaskQueue {
private _tasks: (() => void)[] = [];
private _idleCallback?: number;
private _i = 0;
protected abstract _requestCallback(callback: CallbackWithDeadline): number;
protected abstract _cancelCallback(identifier: number): void;
public enqueue(task: () => void): void {
this._tasks.push(task);
this._start();
}
public flush(): void {
while (this._i < this._tasks.length) {
this._tasks[this._i++]();
}
this.clear();
}
public clear(): void {
if (this._idleCallback) {
this._cancelCallback(this._idleCallback);
this._idleCallback = undefined;
}
this._i = 0;
this._tasks.length = 0;
}
private _start(): void {
if (!this._idleCallback) {
this._idleCallback = this._requestCallback(this._process.bind(this));
}
}
private _process(deadline: ITaskDeadline): void {
this._idleCallback = undefined;
let taskDuration = 0;
let longestTask = 0;
while (this._i < this._tasks.length) {
taskDuration = performance.now();
this._tasks[this._i++]();
taskDuration = performance.now() - taskDuration;
longestTask = Math.max(taskDuration, longestTask);
// Guess the following task will take a similar time to the longest task in this batch, allow
// additional room to try avoid exceeding the deadline
if (longestTask * 1.5 > deadline.timeRemaining()) {
this._start();
return;
}
}
this.clear();
}
}
/**
* A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames
* per second. The tasks will run in the order they are enqueued, but they will run some time later,
* and care should be taken to ensure they're non-urgent and will not introduce race conditions.
*/
export class PriorityTaskQueue extends TaskQueue {
protected _requestCallback(callback: CallbackWithDeadline): number {
return setTimeout(() => callback(this._createDeadline(16)));
}
protected _cancelCallback(identifier: number): void {
clearTimeout(identifier);
}
private _createDeadline(duration: number): ITaskDeadline {
const end = performance.now() + duration;
return {
timeRemaining: () => Math.max(0, end - performance.now())
};
}
}
class IdleTaskQueueInternal extends TaskQueue {
protected _requestCallback(callback: IdleRequestCallback): number {
return requestIdleCallback(callback);
}
protected _cancelCallback(identifier: number): void {
cancelIdleCallback(identifier);
}
}
/**
* A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's
* deadline given by the environment. The tasks will run in the order they are enqueued, but they
* will run some time later, and care should be taken to ensure they're non-urgent and will not
* introduce race conditions.
*
* This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? IdleTaskQueueInternal : PriorityTaskQueue;
/**
* An object that tracks a single debounced task that will run on the next idle frame. When called
* multiple times, only the last set task will run.
*/
export class DebouncedIdleTask {
private _queue: ITaskQueue;
constructor() {
this._queue = new IdleTaskQueue();
}
public set(task: () => void): void {
this._queue.clear();
this._queue.enqueue(task);
}
public flush(): void {
this._queue.flush();
}
}
+2 -8
View File
@@ -33,12 +33,6 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
// queueMicrotask polyfill for nodejs < v11
const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined')
? (cb: () => void) => { Promise.resolve().then(cb); }
: queueMicrotask;
export class WriteBuffer {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
@@ -105,7 +99,7 @@ export class WriteBuffer {
// schedule chunk processing for next event loop run
if (!this._writeBuffer.length) {
this._bufferOffset = 0;
setTimeout(() => this._innerWrite());
queueMicrotask(() => this._innerWrite());
}
this._pendingData += data.length;
@@ -194,7 +188,7 @@ export class WriteBuffer {
// 2. spawn a promise immediately resolving to `true`
// (executed on the same queue, thus properly aligned before continuation happens)
result.catch(err => {
qmt(() => {throw err;});
queueMicrotask(() => {throw err;});
return Promise.resolve(false);
}).then(continuation);
return;