summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/services/highlight/highlight-service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/services/highlight/highlight-service.ts')
-rw-r--r--polygerrit-ui/app/services/highlight/highlight-service.ts159
1 files changed, 159 insertions, 0 deletions
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
new file mode 100644
index 0000000000..80da2602c7
--- /dev/null
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -0,0 +1,159 @@
+/**
+ * @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;
+ }
+}