Merge branch 'master' into buffer_optimizations
This commit is contained in:
+91
-31
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user