From 6280114b893aa49a8d1a77acaddad6a1d7150c01 Mon Sep 17 00:00:00 2001 From: raylras Date: Sun, 29 Dec 2024 22:04:15 +0800 Subject: [PATCH 1/2] perf: improve editing & relinking performance --- packages/zenscript/src/module.ts | 2 + packages/zenscript/src/reference/linker.ts | 26 ++++- .../src/workspace/document-builder.ts | 110 ++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 packages/zenscript/src/workspace/document-builder.ts diff --git a/packages/zenscript/src/module.ts b/packages/zenscript/src/module.ts index ba8da1dd..9fb36b0f 100644 --- a/packages/zenscript/src/module.ts +++ b/packages/zenscript/src/module.ts @@ -22,6 +22,7 @@ import { registerValidationChecks, ZenScriptValidator } from './validation/valid import { ZenScriptBracketManager } from './workspace/bracket-manager' import { ZenScriptConfigurationManager } from './workspace/configuration-manager' import { ZenScriptDescriptionCreator } from './workspace/description-creator' +import { ZenScriptDocumentBuilder } from './workspace/document-builder' import { ZenScriptPackageManager } from './workspace/package-manager' import { ZenScriptWorkspaceManager } from './workspace/workspace-manager' @@ -104,6 +105,7 @@ export const ZenScriptSharedModule: Module new ZenScriptWorkspaceManager(services), ConfigurationManager: services => new ZenScriptConfigurationManager(services), + DocumentBuilder: services => new ZenScriptDocumentBuilder(services), }, lsp: { NodeKindProvider: () => new ZenScriptNodeKindProvider(), diff --git a/packages/zenscript/src/reference/linker.ts b/packages/zenscript/src/reference/linker.ts index 6e94152f..e727dd9a 100644 --- a/packages/zenscript/src/reference/linker.ts +++ b/packages/zenscript/src/reference/linker.ts @@ -1,9 +1,15 @@ -import type { AstNodeDescription, LinkingError, ReferenceInfo } from 'langium' +import type { AstNode, AstNodeDescription, LangiumDocument, LinkingError, Reference, ReferenceInfo } from 'langium' import type { ZenScriptServices } from '../module' import { DefaultLinker } from 'langium' import { isImportDeclaration, isNamedTypeReference } from '../generated/ast' import { createUnknownAstDescription } from './synthetic' +declare module 'langium' { + interface Linker { + relink: (document: LangiumDocument, changedUris: Set) => void + } +} + export class ZenScriptLinker extends DefaultLinker { constructor(services: ZenScriptServices) { super(services) @@ -26,4 +32,22 @@ export class ZenScriptLinker extends DefaultLinker { return this.createLinkingError(refInfo) } + + relink(document: LangiumDocument, changedUris: Set) { + for (const ref of document.references) { + const targetUri = ref?.$nodeDescription?.documentUri.toString() + if (targetUri && changedUris.has(targetUri)) { + this.reset(ref) + } + } + } + + reset(ref: DefaultReference) { + delete ref._ref + } +} + +interface DefaultReference extends Reference { + _ref?: AstNode + _nodeDescription?: AstNodeDescription } diff --git a/packages/zenscript/src/workspace/document-builder.ts b/packages/zenscript/src/workspace/document-builder.ts new file mode 100644 index 00000000..a3ed32f3 --- /dev/null +++ b/packages/zenscript/src/workspace/document-builder.ts @@ -0,0 +1,110 @@ +import type { BuildOptions, LangiumDocument, URI } from 'langium' +import { DefaultDocumentBuilder, DocumentState, interruptAndCheck, stream } from 'langium' +import { CancellationToken } from 'vscode-languageserver' + +export class ZenScriptDocumentBuilder extends DefaultDocumentBuilder { + /* eslint-disable no-console */ + protected async buildDocuments(documents: LangiumDocument[], options: BuildOptions, cancelToken: CancellationToken): Promise { + console.log(`Building ${documents.length} documents`) + console.group() + + console.time('Prepare done') + this.prepareBuild(documents, options) + console.timeEnd('Prepare done') + + console.time('Parse done') + await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc => + this.langiumDocumentFactory.update(doc, cancelToken)) + console.timeEnd('Parse done') + + console.time('Compute exports done') + await this.runCancelable(documents, DocumentState.IndexedContent, cancelToken, doc => + this.indexManager.updateContent(doc, cancelToken)) + console.timeEnd('Compute exports done') + + console.time('Compute scope done') + await this.runCancelable(documents, DocumentState.ComputedScopes, cancelToken, async (doc) => { + const scopeComputation = this.serviceRegistry.getServices(doc.uri).references.ScopeComputation + doc.precomputedScopes = await scopeComputation.computeLocalScopes(doc, cancelToken) + }) + console.timeEnd('Compute scope done') + + console.time('Link done') + await this.runCancelable(documents, DocumentState.Linked, cancelToken, (doc) => { + const linker = this.serviceRegistry.getServices(doc.uri).references.Linker + return linker.link(doc, cancelToken) + }) + console.timeEnd('Link done') + + console.time('Index references done') + await this.runCancelable(documents, DocumentState.IndexedReferences, cancelToken, doc => + this.indexManager.updateReferences(doc, cancelToken)) + console.timeEnd('Index references done') + + console.time('Validation done') + const toBeValidated = documents.filter(doc => this.shouldValidate(doc)) + await this.runCancelable(toBeValidated, DocumentState.Validated, cancelToken, doc => + this.validate(doc, cancelToken)) + console.timeEnd('Validation done') + + console.groupEnd() + + // If we've made it to this point without being cancelled, we can mark the build state as completed. + for (const doc of documents) { + const state = this.buildState.get(doc.uri.toString()) + if (state) { + state.completed = true + } + } + } + + async update(changed: URI[], deleted: URI[], cancelToken = CancellationToken.None): Promise { + this.currentState = DocumentState.Changed + // Remove all metadata of documents that are reported as deleted + for (const deletedUri of deleted) { + this.langiumDocuments.deleteDocument(deletedUri) + this.buildState.delete(deletedUri.toString()) + this.indexManager.remove(deletedUri) + } + // Set the state of all changed documents to `Changed` so they are completely rebuilt + for (const changedUri of changed) { + const invalidated = this.langiumDocuments.invalidateDocument(changedUri) + if (!invalidated) { + // We create an unparsed, invalid document. + // This will be parsed as soon as we reach the first document builder phase. + // This allows to cancel the parsing process later in case we need it. + const newDocument = this.langiumDocumentFactory.fromModel({ $type: 'INVALID' }, changedUri) + newDocument.state = DocumentState.Changed + this.langiumDocuments.addDocument(newDocument) + } + this.buildState.delete(changedUri.toString()) + } + // Set the state of all documents that should be relinked (if not already lower) + console.time('Relink done') + const changedUris = stream(changed, deleted).map(uri => uri.toString()).toSet() + const linkedDocs = this.langiumDocuments.all.filter(doc => doc.state >= DocumentState.Linked) + for (const doc of linkedDocs) { + const linker = this.serviceRegistry.getServices(doc.uri).references.Linker + linker.relink(doc, changedUris) + } + console.timeEnd('Relink done') + + // Notify listeners of the update + await this.emitUpdate(changed, deleted) + // Only allow interrupting the execution after all state changes are done + await interruptAndCheck(cancelToken) + + // Collect and sort all documents that we should rebuild + const rebuildDocuments = this.sortDocuments( + this.langiumDocuments.all + .filter(doc => + // This includes those that were reported as changed and those that we selected for relinking + doc.state <= DocumentState.Linked + // This includes those for which a previous build has been cancelled + || !this.buildState.get(doc.uri.toString())?.completed, + ) + .toArray(), + ) + await this.buildDocuments(rebuildDocuments, this.updateBuildOptions, cancelToken) + } +} From 39e0bcf9429099f0b42dfa85b2c6daec0e43b6ef Mon Sep 17 00:00:00 2001 From: raylras Date: Tue, 31 Dec 2024 18:01:00 +0800 Subject: [PATCH 2/2] workaround for refresh semantic tokens --- .../src/workspace/document-builder.ts | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/zenscript/src/workspace/document-builder.ts b/packages/zenscript/src/workspace/document-builder.ts index a3ed32f3..5c469688 100644 --- a/packages/zenscript/src/workspace/document-builder.ts +++ b/packages/zenscript/src/workspace/document-builder.ts @@ -1,53 +1,68 @@ import type { BuildOptions, LangiumDocument, URI } from 'langium' +import type { Connection } from 'vscode-languageserver' +import type { ZenScriptSharedServices } from '../module' import { DefaultDocumentBuilder, DocumentState, interruptAndCheck, stream } from 'langium' import { CancellationToken } from 'vscode-languageserver' export class ZenScriptDocumentBuilder extends DefaultDocumentBuilder { + private connection: Connection | undefined + + constructor(services: ZenScriptSharedServices) { + super(services) + this.connection = services.lsp.Connection + } + /* eslint-disable no-console */ protected async buildDocuments(documents: LangiumDocument[], options: BuildOptions, cancelToken: CancellationToken): Promise { console.log(`Building ${documents.length} documents`) - console.group() - console.time('Prepare done') - this.prepareBuild(documents, options) - console.timeEnd('Prepare done') + console.group() + console.time('Finished building') - console.time('Parse done') - await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc => - this.langiumDocumentFactory.update(doc, cancelToken)) - console.timeEnd('Parse done') + try { + console.time('Prepare done') + this.prepareBuild(documents, options) + console.timeEnd('Prepare done') - console.time('Compute exports done') - await this.runCancelable(documents, DocumentState.IndexedContent, cancelToken, doc => - this.indexManager.updateContent(doc, cancelToken)) - console.timeEnd('Compute exports done') + console.time('Parse done') + await this.runCancelable(documents, DocumentState.Parsed, cancelToken, doc => + this.langiumDocumentFactory.update(doc, cancelToken)) + console.timeEnd('Parse done') - console.time('Compute scope done') - await this.runCancelable(documents, DocumentState.ComputedScopes, cancelToken, async (doc) => { - const scopeComputation = this.serviceRegistry.getServices(doc.uri).references.ScopeComputation - doc.precomputedScopes = await scopeComputation.computeLocalScopes(doc, cancelToken) - }) - console.timeEnd('Compute scope done') + console.time('Compute exports done') + await this.runCancelable(documents, DocumentState.IndexedContent, cancelToken, doc => + this.indexManager.updateContent(doc, cancelToken)) + console.timeEnd('Compute exports done') - console.time('Link done') - await this.runCancelable(documents, DocumentState.Linked, cancelToken, (doc) => { - const linker = this.serviceRegistry.getServices(doc.uri).references.Linker - return linker.link(doc, cancelToken) - }) - console.timeEnd('Link done') + console.time('Compute scope done') + await this.runCancelable(documents, DocumentState.ComputedScopes, cancelToken, async (doc) => { + const scopeComputation = this.serviceRegistry.getServices(doc.uri).references.ScopeComputation + doc.precomputedScopes = await scopeComputation.computeLocalScopes(doc, cancelToken) + }) + console.timeEnd('Compute scope done') - console.time('Index references done') - await this.runCancelable(documents, DocumentState.IndexedReferences, cancelToken, doc => - this.indexManager.updateReferences(doc, cancelToken)) - console.timeEnd('Index references done') + console.time('Link done') + await this.runCancelable(documents, DocumentState.Linked, cancelToken, (doc) => { + const linker = this.serviceRegistry.getServices(doc.uri).references.Linker + return linker.link(doc, cancelToken) + }) + console.timeEnd('Link done') - console.time('Validation done') - const toBeValidated = documents.filter(doc => this.shouldValidate(doc)) - await this.runCancelable(toBeValidated, DocumentState.Validated, cancelToken, doc => - this.validate(doc, cancelToken)) - console.timeEnd('Validation done') + console.time('Index references done') + await this.runCancelable(documents, DocumentState.IndexedReferences, cancelToken, doc => + this.indexManager.updateReferences(doc, cancelToken)) + console.timeEnd('Index references done') - console.groupEnd() + console.time('Validation done') + const toBeValidated = documents.filter(doc => this.shouldValidate(doc)) + await this.runCancelable(toBeValidated, DocumentState.Validated, cancelToken, doc => + this.validate(doc, cancelToken)) + console.timeEnd('Validation done') + } + finally { + console.timeEnd('Finished building') + console.groupEnd() + } // If we've made it to this point without being cancelled, we can mark the build state as completed. for (const doc of documents) { @@ -106,5 +121,7 @@ export class ZenScriptDocumentBuilder extends DefaultDocumentBuilder { .toArray(), ) await this.buildDocuments(rebuildDocuments, this.updateBuildOptions, cancelToken) + // Workaround, should be removed after Langium supports workspace lock + this.connection?.languages.semanticTokens.refresh() } }