Skip to content

Commit 910e8a0

Browse files
Add parser profiler (#1993)
Co-authored-by: Yannick Daveluy <[email protected]>
1 parent 3ba3eb5 commit 910e8a0

File tree

7 files changed

+315
-19
lines changed

7 files changed

+315
-19
lines changed

packages/langium/src/default-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ export function createDefaultSharedCoreModule(context: DefaultSharedCoreModuleCo
117117
WorkspaceManager: (services) => new DefaultWorkspaceManager(services),
118118
FileSystemProvider: (services) => context.fileSystemProvider(services),
119119
WorkspaceLock: () => new DefaultWorkspaceLock(),
120-
ConfigurationProvider: (services) => new DefaultConfigurationProvider(services)
121-
}
120+
ConfigurationProvider: (services) => new DefaultConfigurationProvider(services),
121+
},
122+
profilers: {}
122123
};
123124
}

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

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
******************************************************************************/
66

77
/* eslint-disable @typescript-eslint/no-explicit-any */
8-
import type { DSLMethodOpts, ILexingError, IOrAlt, IParserErrorMessageProvider, IRecognitionException, IToken, TokenType, TokenVocabulary, IRuleConfig } from 'chevrotain';
9-
import type { AbstractElement, Action, Assignment, InfixRule, ParserRule } from '../languages/generated/ast.js';
108
import { isInfixRule } from '../languages/generated/ast.js';
9+
import type { AbstractElement, Action, Assignment, InfixRule, ParserRule } from '../languages/generated/ast.js';
10+
import type { DSLMethodOpts, ILexingError, IOrAlt, IParserErrorMessageProvider, IRecognitionException, IToken, ParserMethod, SubruleMethodOpts, TokenType, TokenVocabulary, IRuleConfig } from 'chevrotain';
1111
import type { Linker } from '../references/linker.js';
1212
import type { LangiumCoreServices } from '../services.js';
1313
import type { AstNode, AstReflection, CompositeCstNode, CstNode } from '../syntax-tree.js';
@@ -21,6 +21,7 @@ import { getExplicitRuleType, isDataTypeRule } from '../utils/grammar-utils.js';
2121
import { assignMandatoryProperties, getContainerOfType, linkContentToContainer } from '../utils/ast-utils.js';
2222
import { CstNodeBuilder } from './cst-node-builder.js';
2323
import type { LexingReport } from './token-builder.js';
24+
import type { ProfilingTask } from '../workspace/profiler.js';
2425

2526
export type ParseResult<T = AstNode> = {
2627
value: T,
@@ -140,11 +141,19 @@ export abstract class AbstractLangiumParser implements BaseParser {
140141
this.lexer = services.parser.Lexer;
141142
const tokens = this.lexer.definition;
142143
const production = services.LanguageMetaData.mode === 'production';
143-
this.wrapper = new ChevrotainWrapper(tokens, {
144-
...services.parser.ParserConfig,
145-
skipValidations: production,
146-
errorMessageProvider: services.parser.ParserErrorMessageProvider
147-
});
144+
if (services.shared.profilers.LangiumProfiler?.isActive('parsing')) {
145+
this.wrapper = new ProfilerWrapper(tokens, {
146+
...services.parser.ParserConfig,
147+
skipValidations: production,
148+
errorMessageProvider: services.parser.ParserErrorMessageProvider
149+
}, services.shared.profilers.LangiumProfiler.createTask('parsing', services.LanguageMetaData.languageId));
150+
} else {
151+
this.wrapper = new ChevrotainWrapper(tokens, {
152+
...services.parser.ParserConfig,
153+
skipValidations: production,
154+
errorMessageProvider: services.parser.ParserErrorMessageProvider
155+
});
156+
}
148157
}
149158

150159
alternatives(idx: number, choices: Array<IOrAlt<any>>): void {
@@ -283,7 +292,7 @@ export class LangiumParser extends AbstractLangiumParser {
283292
}
284293

285294
private doParse(rule: RuleResult): any {
286-
let result = rule.call(this.wrapper, {});
295+
let result = this.wrapper.rule(rule);
287296
if (this.stack.length > 0) {
288297
// In case the parser throws on the entry rule, `construct` is not called
289298
// We need to call it manually here
@@ -846,4 +855,40 @@ class ChevrotainWrapper extends EmbeddedActionsParser {
846855
wrapAtLeastOne(idx: number, callback: DSLMethodOpts<unknown>): void {
847856
this.atLeastOne(idx, callback);
848857
}
858+
rule(rule: RuleResult): any {
859+
return rule.call(this, {});
860+
}
861+
}
862+
863+
class ProfilerWrapper extends ChevrotainWrapper {
864+
private readonly task: ProfilingTask;
865+
constructor(tokens: TokenVocabulary, config: IParserConfig, task: ProfilingTask) {
866+
super(tokens, config);
867+
this.task = task;
868+
}
869+
870+
override rule(rule: RuleResult): any {
871+
this.task.start();
872+
this.task.startSubTask(this.ruleName(rule));
873+
try {
874+
return super.rule(rule);
875+
}
876+
finally {
877+
this.task.stopSubTask(this.ruleName(rule));
878+
this.task.stop();
879+
}
880+
}
881+
882+
private ruleName(rule: any): string {
883+
return rule.ruleName as string;
884+
}
885+
protected override subrule<ARGS extends unknown[], R>(idx: number, ruleToCall: ParserMethod<ARGS, R>, options?: SubruleMethodOpts<ARGS>): R {
886+
this.task.startSubTask(this.ruleName(ruleToCall));
887+
try {
888+
return super.subrule<ARGS, R>(idx, ruleToCall, options);
889+
}
890+
finally {
891+
this.task.stopSubTask(this.ruleName(ruleToCall));
892+
}
893+
}
849894
}

packages/langium/src/references/linker.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isAstNode, isAstNodeDescription, isLinkingError } from '../syntax-tree.
1414
import { findRootNode, streamAst, streamReferences } from '../utils/ast-utils.js';
1515
import { interruptAndCheck } from '../utils/promise-utils.js';
1616
import { DocumentState } from '../workspace/documents.js';
17+
import type { LangiumProfiler } from '../workspace/profiler.js';
1718

1819
/**
1920
* Language-specific service for resolving cross-references in the AST.
@@ -95,18 +96,44 @@ export class DefaultLinker implements Linker {
9596
protected readonly scopeProvider: ScopeProvider;
9697
protected readonly astNodeLocator: AstNodeLocator;
9798
protected readonly langiumDocuments: () => LangiumDocuments;
99+
protected readonly profiler: LangiumProfiler | undefined;
100+
protected readonly languageId: string;
98101

99102
constructor(services: LangiumCoreServices) {
100103
this.reflection = services.shared.AstReflection;
101104
this.langiumDocuments = () => services.shared.workspace.LangiumDocuments;
102105
this.scopeProvider = services.references.ScopeProvider;
103106
this.astNodeLocator = services.workspace.AstNodeLocator;
107+
this.profiler = services.shared.profilers.LangiumProfiler;
108+
this.languageId = services.LanguageMetaData.languageId;
104109
}
105110

106111
async link(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
107-
for (const node of streamAst(document.parseResult.value)) {
108-
await interruptAndCheck(cancelToken);
109-
streamReferences(node).forEach(ref => this.doLink(ref, document));
112+
if (this.profiler?.isActive('linking')) {
113+
const task = this.profiler.createTask('linking', this.languageId);
114+
task.start();
115+
try {
116+
for (const node of streamAst(document.parseResult.value)) {
117+
await interruptAndCheck(cancelToken);
118+
streamReferences(node).forEach(ref => {
119+
const name = `${node.$type}:${ref.property}`;
120+
task.startSubTask(name);
121+
try {
122+
this.doLink(ref, document);
123+
} finally {
124+
task.stopSubTask(name);
125+
}
126+
});
127+
}
128+
} finally {
129+
task.stop();
130+
}
131+
}
132+
else {
133+
for (const node of streamAst(document.parseResult.value)) {
134+
await interruptAndCheck(cancelToken);
135+
streamReferences(node).forEach(ref => this.doLink(ref, document));
136+
}
110137
}
111138
}
112139

packages/langium/src/services.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type { IndexManager } from './workspace/index-manager.js';
3737
import type { WorkspaceLock } from './workspace/workspace-lock.js';
3838
import type { Hydrator } from './serializer/hydrator.js';
3939
import type { WorkspaceManager } from './workspace/workspace-manager.js';
40+
import type { LangiumProfiler } from './workspace/profiler.js';
4041

4142
/**
4243
* The services generated by `langium-cli` for a specific language. These are derived from the
@@ -122,6 +123,9 @@ export type LangiumDefaultSharedCoreServices = {
122123
readonly WorkspaceLock: WorkspaceLock
123124
readonly WorkspaceManager: WorkspaceManager
124125
}
126+
readonly profilers: {
127+
readonly LangiumProfiler?: LangiumProfiler
128+
}
125129
}
126130

127131
/**

packages/langium/src/validation/document-validator.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { tokenToRange } from '../utils/cst-utils.js';
1919
import { interruptAndCheck, isOperationCancelled } from '../utils/promise-utils.js';
2020
import { diagnosticData } from './validation-registry.js';
2121
import type { LexingDiagnostic, LexingDiagnosticSeverity } from '../parser/token-builder.js';
22+
import type { LangiumProfiler } from '../workspace/profiler.js';
2223

2324
export interface ValidationOptions {
2425
/**
@@ -53,10 +54,14 @@ export class DefaultDocumentValidator implements DocumentValidator {
5354

5455
protected readonly validationRegistry: ValidationRegistry;
5556
protected readonly metadata: LanguageMetaData;
57+
protected readonly profiler: LangiumProfiler | undefined;
58+
protected readonly languageId: string;
5659

5760
constructor(services: LangiumCoreServices) {
5861
this.validationRegistry = services.validation.ValidationRegistry;
5962
this.metadata = services.LanguageMetaData;
63+
this.profiler = services.shared.profilers.LangiumProfiler;
64+
this.languageId = services.LanguageMetaData.languageId;
6065
}
6166

6267
async validateDocument(document: LangiumDocument, options: ValidationOptions = {}, cancelToken = CancellationToken.None): Promise<Diagnostic[]> {
@@ -201,13 +206,34 @@ export class DefaultDocumentValidator implements DocumentValidator {
201206
}
202207

203208
protected async validateAstNodes(rootNode: AstNode, options: ValidationOptions, acceptor: ValidationAcceptor, cancelToken = CancellationToken.None): Promise<void> {
204-
await Promise.all(streamAst(rootNode).map(async node => {
205-
await interruptAndCheck(cancelToken);
206-
const checks = this.validationRegistry.getChecks(node.$type, options.categories);
207-
for (const check of checks) {
208-
await check(node, acceptor, cancelToken);
209+
if (this.profiler?.isActive('validating')) {
210+
const task = this.profiler.createTask('validating', this.languageId);
211+
task.start();
212+
try {
213+
streamAst(rootNode).forEach(node => {
214+
task.startSubTask(node.$type);
215+
try {
216+
const checks = this.validationRegistry.getChecks(node.$type, options.categories);
217+
for (const check of checks) {
218+
check(node, acceptor, cancelToken);
219+
}
220+
} finally {
221+
task.stopSubTask(node.$type);
222+
}
223+
});
224+
} finally {
225+
task.stop();
209226
}
210-
}));
227+
}
228+
else {
229+
await Promise.all(streamAst(rootNode).map(async node => {
230+
await interruptAndCheck(cancelToken);
231+
const checks = this.validationRegistry.getChecks(node.$type, options.categories);
232+
for (const check of checks) {
233+
await check(node, acceptor, cancelToken);
234+
}
235+
}));
236+
}
211237
}
212238

213239
protected async validateAstAfter(rootNode: AstNode, options: ValidationOptions, acceptor: ValidationAcceptor, cancelToken = CancellationToken.None): Promise<void> {

packages/langium/src/workspace/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './file-system-provider.js';
1313
export * from './index-manager.js';
1414
export * from './workspace-lock.js';
1515
export * from './workspace-manager.js';
16+
export * from './profiler.js';

0 commit comments

Comments
 (0)