Skip to content

Commit e78aeba

Browse files
authored
Implement async parsers (#1352)
1 parent 87715d1 commit e78aeba

File tree

15 files changed

+674
-5
lines changed

15 files changed

+674
-5
lines changed

packages/langium/src/default-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { DefaultCommentProvider } from './documentation/comment-provider.js';
3434
import { LangiumParserErrorMessageProvider } from './parser/langium-parser.js';
3535
import { DefaultAsyncParser } from './parser/async-parser.js';
3636
import { DefaultWorkspaceLock } from './workspace/workspace-lock.js';
37+
import { DefaultHydrator } from './serializer/hydrator.js';
3738

3839
/**
3940
* Context required for creating the default language-specific dependency injection module.
@@ -75,6 +76,7 @@ export function createDefaultCoreModule(context: DefaultCoreModuleContext): Modu
7576
References: (services) => new DefaultReferences(services)
7677
},
7778
serializer: {
79+
Hydrator: (services) => new DefaultHydrator(services),
7880
JsonSerializer: (services) => new DefaultJsonSerializer(services)
7981
},
8082
validation: {

packages/langium/src/grammar/langium-grammar-module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,12 @@ export const LangiumGrammarModule: Module<LangiumGrammarServices, PartialLangium
6767
*
6868
* @param context Shared module context, used to create additional shared modules
6969
* @param sharedModule Existing shared module to inject together with new shared services
70+
* @param module Additional/modified service implementations for the language services
7071
* @returns Shared services enriched with LSP services + Grammar services, per usual
7172
*/
7273
export function createLangiumGrammarServices(context: DefaultSharedModuleContext,
73-
sharedModule?: Module<LangiumSharedServices, PartialLangiumSharedServices>): {
74+
sharedModule?: Module<LangiumSharedServices, PartialLangiumSharedServices>,
75+
module?: Module<LangiumServices, PartialLangiumServices>): {
7476
shared: LangiumSharedServices,
7577
grammar: LangiumGrammarServices
7678
} {
@@ -82,7 +84,8 @@ export function createLangiumGrammarServices(context: DefaultSharedModuleContext
8284
const grammar = inject(
8385
createDefaultModule({ shared }),
8486
LangiumGrammarGeneratedModule,
85-
LangiumGrammarModule
87+
LangiumGrammarModule,
88+
module
8689
);
8790
addTypeCollectionPhase(shared, grammar);
8891
shared.ServiceRegistry.register(grammar);

packages/langium/src/node/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
******************************************************************************/
66

77
export * from './node-file-system-provider.js';
8+
export * from './worker-thread-async-parser.js';
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/******************************************************************************
2+
* Copyright 2023 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import type { LangiumCoreServices } from '../services.js';
8+
import { ParserWorker } from '../parser/async-parser.js';
9+
import { AbstractThreadedAsyncParser } from '../parser/async-parser.js';
10+
import { Worker } from 'node:worker_threads';
11+
12+
export class WorkerThreadAsyncParser extends AbstractThreadedAsyncParser {
13+
14+
protected workerPath: string | (() => string);
15+
16+
constructor(services: LangiumCoreServices, workerPath: string | (() => string)) {
17+
super(services);
18+
this.workerPath = workerPath;
19+
}
20+
21+
protected override createWorker(): ParserWorker {
22+
const path = typeof this.workerPath === 'function' ? this.workerPath() : this.workerPath;
23+
const worker = new Worker(path);
24+
const parserWorker = new WorkerThreadParserWorker(worker);
25+
return parserWorker;
26+
}
27+
28+
}
29+
30+
export class WorkerThreadParserWorker extends ParserWorker {
31+
32+
constructor(worker: Worker) {
33+
super(
34+
(message) => worker.postMessage(message),
35+
cb => worker.on('message', cb),
36+
cb => worker.on('error', cb),
37+
() => worker.terminate()
38+
);
39+
}
40+
41+
}

packages/langium/src/parser/async-parser.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import type { CancellationToken } from '../utils/cancellation.js';
88
import type { LangiumCoreServices } from '../services.js';
99
import type { AstNode } from '../syntax-tree.js';
1010
import type { LangiumParser, ParseResult } from './langium-parser.js';
11+
import type { Hydrator } from '../serializer/hydrator.js';
12+
import type { Event } from '../utils/event.js';
13+
import { Deferred, OperationCancelled } from '../utils/promise-utils.js';
14+
import { Emitter } from '../utils/event.js';
1115

1216
/**
1317
* Async parser that allows to cancel the current parsing process.
@@ -37,3 +41,156 @@ export class DefaultAsyncParser implements AsyncParser {
3741
return Promise.resolve(this.syncParser.parse<T>(text));
3842
}
3943
}
44+
45+
export abstract class AbstractThreadedAsyncParser implements AsyncParser {
46+
47+
/**
48+
* The thread count determines how many threads are used to parse files in parallel.
49+
* The default value is 8. Decreasing this value increases startup performance, but decreases parallel parsing performance.
50+
*/
51+
protected threadCount = 8;
52+
/**
53+
* The termination delay determines how long the parser waits for a thread to finish after a cancellation request.
54+
* The default value is 200(ms).
55+
*/
56+
protected terminationDelay = 200;
57+
protected workerPool: ParserWorker[] = [];
58+
protected queue: Array<Deferred<ParserWorker>> = [];
59+
60+
protected readonly hydrator: Hydrator;
61+
62+
constructor(services: LangiumCoreServices) {
63+
this.hydrator = services.serializer.Hydrator;
64+
}
65+
66+
protected initializeWorkers(): void {
67+
while (this.workerPool.length < this.threadCount) {
68+
const worker = this.createWorker();
69+
worker.onReady(() => {
70+
if (this.queue.length > 0) {
71+
const deferred = this.queue.shift();
72+
if (deferred) {
73+
worker.lock();
74+
deferred.resolve(worker);
75+
}
76+
}
77+
});
78+
this.workerPool.push(worker);
79+
}
80+
}
81+
82+
async parse<T extends AstNode>(text: string, cancelToken: CancellationToken): Promise<ParseResult<T>> {
83+
const worker = await this.acquireParserWorker(cancelToken);
84+
const deferred = new Deferred<ParseResult<T>>();
85+
let timeout: NodeJS.Timeout | undefined;
86+
// If the cancellation token is requested, we wait for a certain time before terminating the worker.
87+
// Since the cancellation token lives longer than the parsing process, we need to dispose the event listener.
88+
// Otherwise, we might accidentally terminate the worker after the parsing process has finished.
89+
const cancellation = cancelToken.onCancellationRequested(() => {
90+
timeout = setTimeout(() => {
91+
this.terminateWorker(worker);
92+
}, this.terminationDelay);
93+
});
94+
worker.parse(text).then(result => {
95+
result.value = this.hydrator.hydrate(result.value);
96+
deferred.resolve(result as ParseResult<T>);
97+
}).catch(err => {
98+
deferred.reject(err);
99+
}).finally(() => {
100+
cancellation.dispose();
101+
clearTimeout(timeout);
102+
});
103+
return deferred.promise;
104+
}
105+
106+
protected terminateWorker(worker: ParserWorker): void {
107+
worker.terminate();
108+
const index = this.workerPool.indexOf(worker);
109+
if (index >= 0) {
110+
this.workerPool.splice(index, 1);
111+
}
112+
}
113+
114+
protected async acquireParserWorker(cancelToken: CancellationToken): Promise<ParserWorker> {
115+
this.initializeWorkers();
116+
for (const worker of this.workerPool) {
117+
if (worker.ready) {
118+
worker.lock();
119+
return worker;
120+
}
121+
}
122+
const deferred = new Deferred<ParserWorker>();
123+
cancelToken.onCancellationRequested(() => {
124+
const index = this.queue.indexOf(deferred);
125+
if (index >= 0) {
126+
this.queue.splice(index, 1);
127+
}
128+
deferred.reject('OperationCancelled');
129+
});
130+
this.queue.push(deferred);
131+
return deferred.promise;
132+
}
133+
134+
protected abstract createWorker(): ParserWorker;
135+
}
136+
137+
export type WorkerMessagePost = (message: unknown) => void;
138+
export type WorkerMessageCallback = (cb: (message: unknown) => void) => void;
139+
140+
export class ParserWorker {
141+
142+
protected readonly sendMessage: WorkerMessagePost;
143+
protected readonly _terminate: () => void;
144+
protected readonly onReadyEmitter = new Emitter<void>();
145+
146+
protected deferred = new Deferred<ParseResult>();
147+
protected _ready = true;
148+
protected _parsing = false;
149+
150+
get ready(): boolean {
151+
return this._ready;
152+
}
153+
154+
get onReady(): Event<void> {
155+
return this.onReadyEmitter.event;
156+
}
157+
158+
constructor(sendMessage: WorkerMessagePost, onMessage: WorkerMessageCallback, onError: WorkerMessageCallback, terminate: () => void) {
159+
this.sendMessage = sendMessage;
160+
this._terminate = terminate;
161+
onMessage(result => {
162+
const parseResult = result as ParseResult;
163+
this.deferred.resolve(parseResult);
164+
this.unlock();
165+
});
166+
onError(error => {
167+
this.deferred.reject(error);
168+
this.unlock();
169+
});
170+
}
171+
172+
terminate(): void {
173+
this.deferred.reject(OperationCancelled);
174+
this._terminate();
175+
}
176+
177+
lock(): void {
178+
this._ready = false;
179+
}
180+
181+
unlock(): void {
182+
this._parsing = false;
183+
this._ready = true;
184+
this.onReadyEmitter.fire();
185+
}
186+
187+
parse(text: string): Promise<ParseResult> {
188+
if (this._parsing) {
189+
throw new Error('Parser worker is busy');
190+
}
191+
this._parsing = true;
192+
this.deferred = new Deferred();
193+
this.sendMessage(text);
194+
return this.deferred.promise;
195+
}
196+
}

0 commit comments

Comments
 (0)