diff options
Diffstat (limited to 'polygerrit-ui/app/services/highlight/highlight-service.ts')
-rw-r--r-- | polygerrit-ui/app/services/highlight/highlight-service.ts | 159 |
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; + } +} |