blob: 80da2602c754bc6377b915ed55c3521e33062306 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
SyntaxWorkerRequest,
SyntaxWorkerInit,
SyntaxWorkerResult,
SyntaxWorkerMessageType,
SyntaxLayerLine,
} from '../../types/syntax-worker-api';
import {prependOrigin} from '../../utils/url-util';
import {createWorker} from '../../utils/worker-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
import {Finalizable} from '../registry';
const hljsLibUrl = `${
window.STATIC_RESOURCE_PATH ?? ''
}/bower_components/highlightjs/highlight.min.js`;
const syntaxWorkerUrl = `${
window.STATIC_RESOURCE_PATH ?? ''
}/workers/syntax-worker.js`;
/**
* It is unlikely that a pool size greater than 3 will gain anything, because
* the app also needs the resources to process the results.
*/
const WORKER_POOL_SIZE = 3;
/**
* Safe guard for not killing the browser.
*/
export const CODE_MAX_LINES = 20 * 1000;
/**
* Safe guard for not killing the browser. Maximum in number of chars.
*/
const CODE_MAX_LENGTH = 25 * CODE_MAX_LINES;
/**
* Service for syntax highlighting. Maintains some HighlightJS workers doing
* their job in the background.
*/
export class HighlightService implements Finalizable {
// visible for testing
poolIdle: Set<Worker> = new Set();
// visible for testing
poolBusy: Set<Worker> = new Set();
// visible for testing
/** Queue for waiting that a worker becomes available. */
queueForWorker: Array<() => void> = [];
// visible for testing
/** Queue for waiting on the results of a worker. */
queueForResult: Map<Worker, (r: SyntaxLayerLine[]) => void> = new Map();
constructor(readonly reporting: ReportingService) {
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
this.addWorker();
}
}
/** Allows tests to produce fake workers. */
protected createWorker() {
return createWorker(prependOrigin(syntaxWorkerUrl));
}
/** Creates, initializes and then moves a worker to the idle pool. */
private addWorker() {
const worker = this.createWorker();
// Will move to the idle pool after being initialized.
this.poolBusy.add(worker);
worker.onmessage = (e: MessageEvent<SyntaxWorkerResult>) => {
this.handleResult(worker, e.data);
};
const initMsg: SyntaxWorkerInit = {
type: SyntaxWorkerMessageType.INIT,
url: prependOrigin(hljsLibUrl),
};
worker.postMessage(initMsg);
}
private moveIdleToBusy() {
const worker = this.poolIdle.values().next().value;
this.poolIdle.delete(worker);
this.poolBusy.add(worker);
return worker;
}
private moveBusyToIdle(worker: Worker) {
this.poolBusy.delete(worker);
this.poolIdle.add(worker);
const resolver = this.queueForWorker.shift();
if (resolver) resolver();
}
/**
* If there is worker in the idle pool, then return it. Otherwise wait for a
* worker to become a available.
*/
private async requestWorker(): Promise<Worker> {
if (this.poolIdle.size > 0) {
const worker = this.moveIdleToBusy();
return Promise.resolve(worker);
}
await new Promise<void>(r => this.queueForWorker.push(r));
return this.requestWorker();
}
/**
* A worker is done with its job. Move it back to the idle pool and notify the
* resolver that is waiting for the results.
*/
private handleResult(worker: Worker, result: SyntaxWorkerResult) {
this.moveBusyToIdle(worker);
if (result.error) {
this.reporting.error(new Error(`syntax worker failed: ${result.error}`));
}
const resolver = this.queueForResult.get(worker);
this.queueForResult.delete(worker);
if (resolver) resolver(result.ranges ?? []);
}
async highlight(
language?: string,
code?: string
): Promise<SyntaxLayerLine[]> {
if (!language || !code) return [];
if (code.length > CODE_MAX_LENGTH) return [];
const worker = await this.requestWorker();
const message: SyntaxWorkerRequest = {
type: SyntaxWorkerMessageType.REQUEST,
language,
code,
};
const promise = new Promise<SyntaxLayerLine[]>(r => {
this.queueForResult.set(worker, r);
});
worker.postMessage(message);
return await promise;
}
finalize() {
for (const worker of this.poolIdle) {
worker.terminate();
}
this.poolIdle.clear();
for (const worker of this.poolBusy) {
worker.terminate();
}
this.poolBusy.clear();
this.queueForResult.clear();
this.queueForWorker.length = 0;
}
}