From f0a448fabef74fd31cb47d16fbd6df1e89cd9241 Mon Sep 17 00:00:00 2001 From: WarmthDawn Date: Sat, 16 Nov 2024 20:46:34 +0800 Subject: [PATCH 1/2] perf: improve performance by doument cache and precomputed scopes --- packages/shared/src/tree/hierarchy-tree.ts | 1 + packages/zenscript/src/module.ts | 3 + .../src/reference/dynamic-provider.ts | 32 ++-- .../src/reference/member-provider.ts | 54 ++++--- .../src/reference/scope-computation.ts | 2 +- .../zenscript/src/reference/scope-provider.ts | 57 +++++-- packages/zenscript/src/reference/synthetic.ts | 3 +- .../zenscript/src/typing/type-computer.ts | 36 +++-- packages/zenscript/src/utils/cache.ts | 44 ++++++ packages/zenscript/src/utils/document.ts | 23 ++- .../zenscript/src/workspace/class-index.ts | 149 ++++++++++++++++++ .../src/workspace/package-manager.ts | 22 ++- 12 files changed, 358 insertions(+), 68 deletions(-) create mode 100644 packages/zenscript/src/utils/cache.ts create mode 100644 packages/zenscript/src/workspace/class-index.ts diff --git a/packages/shared/src/tree/hierarchy-tree.ts b/packages/shared/src/tree/hierarchy-tree.ts index d699e866..846806e2 100644 --- a/packages/shared/src/tree/hierarchy-tree.ts +++ b/packages/shared/src/tree/hierarchy-tree.ts @@ -37,6 +37,7 @@ export class HierarchyNode { readonly parent?: HierarchyNode readonly children: Map> readonly data: Set + syntehticData: V | undefined constructor(name: string, parent?: HierarchyNode) { this.name = name diff --git a/packages/zenscript/src/module.ts b/packages/zenscript/src/module.ts index 1c6a0465..d027b0e1 100644 --- a/packages/zenscript/src/module.ts +++ b/packages/zenscript/src/module.ts @@ -16,6 +16,7 @@ import { ZenScriptScopeProvider } from './reference/scope-provider' import { ZenScriptTypeComputer } from './typing/type-computer' import { registerValidationChecks, ZenScriptValidator } from './validation/validator' import { ZenScriptBracketManager } from './workspace/bracket-manager' +import { ZenScriptClassIndex } from './workspace/class-index' import { ZenScriptConfigurationManager } from './workspace/configuration-manager' import { ZenScriptPackageManager } from './workspace/package-manager' import { ZenScriptWorkspaceManager } from './workspace/workspace-manager' @@ -37,6 +38,7 @@ export interface ZenScriptAddedServices { workspace: { PackageManager: ZenScriptPackageManager BracketManager: ZenScriptBracketManager + ClassIndex: ZenScriptClassIndex } } @@ -74,6 +76,7 @@ export const ZenScriptModule: Module new ZenScriptPackageManager(services), BracketManager: services => new ZenScriptBracketManager(services), + ClassIndex: services => new ZenScriptClassIndex(services), }, parser: { TokenBuilder: () => new CustomTokenBuilder(), diff --git a/packages/zenscript/src/reference/dynamic-provider.ts b/packages/zenscript/src/reference/dynamic-provider.ts index ef9d48eb..2141127e 100644 --- a/packages/zenscript/src/reference/dynamic-provider.ts +++ b/packages/zenscript/src/reference/dynamic-provider.ts @@ -2,9 +2,10 @@ import type { AstNode, AstNodeDescription, AstNodeDescriptionProvider } from 'la import type { ZenScriptAstType } from '../generated/ast' import type { ZenScriptServices } from '../module' import type { TypeComputer } from '../typing/type-computer' +import type { ZenScriptClassIndex } from '../workspace/class-index' import type { MemberProvider } from './member-provider' import { AstUtils, stream } from 'langium' -import { isCallExpression, isClassDeclaration, isFunctionDeclaration, isOperatorFunctionDeclaration } from '../generated/ast' +import { isCallExpression, isClassDeclaration, isFunctionDeclaration } from '../generated/ast' import { isClassType, isFunctionType } from '../typing/type-description' export interface DynamicProvider { @@ -18,11 +19,13 @@ export class ZenScriptDynamicProvider implements DynamicProvider { private readonly descriptions: AstNodeDescriptionProvider private readonly typeComputer: TypeComputer private readonly memberProvider: MemberProvider + private readonly classIndex: ZenScriptClassIndex constructor(services: ZenScriptServices) { this.descriptions = services.workspace.AstNodeDescriptionProvider this.typeComputer = services.typing.TypeComputer this.memberProvider = services.references.MemberProvider + this.classIndex = services.workspace.ClassIndex } getDynamics(source: AstNode): AstNodeDescription[] { @@ -37,7 +40,7 @@ export class ZenScriptDynamicProvider implements DynamicProvider { // dynamic this const classDecl = AstUtils.getContainerOfType(source, isClassDeclaration) if (classDecl) { - dynamics.push(this.descriptions.createDescription(classDecl, 'this')) + dynamics.push(this.classIndex.get(classDecl).thisSymbol) } // dynamic arguments @@ -48,11 +51,10 @@ export class ZenScriptDynamicProvider implements DynamicProvider { const paramType = receiverType.paramTypes[index] if (isClassType(paramType)) { stream(this.memberProvider.getMembers(paramType.declaration)) - .map(it => it.node) - .filter(it => isFunctionDeclaration(it)) - .filter(it => it.prefix === 'static') - .filter(it => it.parameters.length === 0) - .forEach(it => dynamics.push(this.descriptions.createDescription(it, it.name))) + .filter(it => isFunctionDeclaration(it.node) + && it.node.prefix === 'static' + && it.node.parameters.length === 0) + .forEach(it => dynamics.push(it)) } } } @@ -63,14 +65,14 @@ export class ZenScriptDynamicProvider implements DynamicProvider { MemberAccess: (source) => { const dynamics: AstNodeDescription[] = [] - // dynamic member - const operatorDecl = stream(this.memberProvider.getMembers(source.receiver)) - .map(it => it.node) - .filter(it => isOperatorFunctionDeclaration(it)) - .filter(it => it.parameters.length === 1) - .find(it => it.op === '.') - if (operatorDecl) { - dynamics.push(this.descriptions.createDescription(operatorDecl.parameters[0], source.target.$refText)) + const receiverType = this.typeComputer.inferType(source.receiver) + + if (isClassType(receiverType)) { + // dynamic member + const operatorDecl = this.classIndex.findOperators(receiverType.declaration, '.').at(0) + if (operatorDecl) { + dynamics.push(this.descriptions.createDescription(operatorDecl.parameters[0], source.target.$refText)) + } } return dynamics diff --git a/packages/zenscript/src/reference/member-provider.ts b/packages/zenscript/src/reference/member-provider.ts index 1edbcf9f..32488366 100644 --- a/packages/zenscript/src/reference/member-provider.ts +++ b/packages/zenscript/src/reference/member-provider.ts @@ -1,33 +1,39 @@ -import type { AstNode, AstNodeDescription, AstNodeDescriptionProvider } from 'langium' +import type { AstNode, AstNodeDescription, AstNodeDescriptionProvider, Stream } from 'langium' import type { ZenScriptAstType } from '../generated/ast' import type { ZenScriptServices } from '../module' import type { TypeComputer } from '../typing/type-computer' +import type { ZenScriptClassIndex } from '../workspace/class-index' +import type { PackageManager } from '../workspace/package-manager' import type { ZenScriptSyntheticAstType } from './synthetic' -import { stream } from 'langium' +import { AstUtils, stream } from 'langium' import { isClassDeclaration, isVariableDeclaration } from '../generated/ast' import { ClassType, isAnyType, isClassType, isFunctionType, type Type, type ZenScriptType } from '../typing/type-description' -import { getClassChain, isStatic } from '../utils/ast' -import { createSyntheticAstNodeDescription, isSyntheticAstNode } from './synthetic' +import { getPrecomputedDescription } from '../utils/document' +import { isSyntheticAstNode } from './synthetic' export interface MemberProvider { - getMembers: (source: AstNode | Type | undefined) => AstNodeDescription[] + getMembers: (source: AstNode | Type | undefined) => Stream } type SourceMap = ZenScriptAstType & ZenScriptType & ZenScriptSyntheticAstType -type RuleMap = { [K in keyof SourceMap]?: (source: SourceMap[K]) => AstNodeDescription[] } +type RuleMap = { [K in keyof SourceMap]?: (source: SourceMap[K]) => Stream } export class ZenScriptMemberProvider implements MemberProvider { private readonly descriptions: AstNodeDescriptionProvider private readonly typeComputer: TypeComputer + private readonly classIndex: ZenScriptClassIndex + private readonly packageManager: PackageManager constructor(services: ZenScriptServices) { this.descriptions = services.workspace.AstNodeDescriptionProvider this.typeComputer = services.typing.TypeComputer + this.classIndex = services.workspace.ClassIndex + this.packageManager = services.workspace.PackageManager } - getMembers(source: AstNode | Type | undefined): AstNodeDescription[] { + getMembers(source: AstNode | Type | undefined): Stream { // @ts-expect-error allowed index type - return this.rules[source?.$type]?.call(this, source) ?? [] + return this.rules[source?.$type]?.call(this, source) ?? stream() } private readonly rules: RuleMap = { @@ -35,11 +41,14 @@ export class ZenScriptMemberProvider implements MemberProvider { const declarations = stream(source.children.values()) .filter(it => it.isDataNode()) .flatMap(it => it.data) - .map(it => this.descriptions.createDescription(it, undefined)) + .map((it) => { + const document = AstUtils.getDocument(it) + return getPrecomputedDescription(document, it) + }) const packages = stream(source.children.values()) .filter(it => it.isInternalNode()) - .map(it => createSyntheticAstNodeDescription('SyntheticHierarchyNode', it.name, it)) - return stream(declarations, packages).toArray() + .map(it => this.packageManager.syntheticDescriptionOf(it)) + return stream(declarations, packages) }, Script: (source) => { @@ -49,7 +58,11 @@ export class ZenScriptMemberProvider implements MemberProvider { source.statements.filter(it => isVariableDeclaration(it)) .filter(it => it.prefix === 'static') .forEach(it => members.push(it)) - return members.map(it => this.descriptions.createDescription(it, undefined)) + return stream(members + .map((it) => { + const document = AstUtils.getDocument(it) + return getPrecomputedDescription(document, it) + })) }, ImportDeclaration: (source) => { @@ -57,10 +70,9 @@ export class ZenScriptMemberProvider implements MemberProvider { }, ClassDeclaration: (source) => { - return getClassChain(source) - .flatMap(it => it.members) - .filter(it => isStatic(it)) - .map(it => this.descriptions.createDescription(it, undefined)) + const index = this.classIndex.get(source) + + return index.streamDescriptions(true) }, VariableDeclaration: (source) => { @@ -81,7 +93,7 @@ export class ZenScriptMemberProvider implements MemberProvider { MemberAccess: (source) => { const target = source.target.ref if (!target) { - return [] + return stream() } if (isSyntheticAstNode(target)) { @@ -120,7 +132,7 @@ export class ZenScriptMemberProvider implements MemberProvider { if (isAnyType(receiverType)) { return this.getMembers(receiverType) } - return [] + return stream() }, BracketExpression: (source) => { @@ -159,10 +171,8 @@ export class ZenScriptMemberProvider implements MemberProvider { }, ClassType: (source) => { - return getClassChain(source.declaration) - .flatMap(it => it.members) - .filter(it => !isStatic(it)) - .map(it => this.descriptions.createDescription(it, undefined)) + const index = this.classIndex.get(source.declaration) + return index.streamDescriptions(false) }, } } diff --git a/packages/zenscript/src/reference/scope-computation.ts b/packages/zenscript/src/reference/scope-computation.ts index 880a155c..7a9b232d 100644 --- a/packages/zenscript/src/reference/scope-computation.ts +++ b/packages/zenscript/src/reference/scope-computation.ts @@ -1,4 +1,4 @@ -import type { AstNode, AstNodeDescription, LangiumDocument } from 'langium' +import type { AstNode, AstNodeDescription, LangiumDocument, PrecomputedScopes } from 'langium' import type { Script } from '../generated/ast' import type { ZenScriptServices } from '../module' import { DefaultScopeComputation } from 'langium' diff --git a/packages/zenscript/src/reference/scope-provider.ts b/packages/zenscript/src/reference/scope-provider.ts index 9ab62cc3..27f869d1 100644 --- a/packages/zenscript/src/reference/scope-provider.ts +++ b/packages/zenscript/src/reference/scope-provider.ts @@ -1,15 +1,16 @@ import type { AstNode, AstNodeDescription, ReferenceInfo, Scope } from 'langium' import type { ZenScriptAstType } from '../generated/ast' import type { ZenScriptServices } from '../module' +import type { ZenScriptClassIndex } from '../workspace/class-index' import type { PackageManager } from '../workspace/package-manager' import type { DynamicProvider } from './dynamic-provider' import type { MemberProvider } from './member-provider' import { substringBeforeLast } from '@intellizen/shared' -import { AstUtils, DefaultScopeProvider, EMPTY_SCOPE, stream } from 'langium' +import { AstUtils, DefaultScopeProvider, EMPTY_SCOPE, MapScope, stream, StreamScope } from 'langium' import { ClassDeclaration, ImportDeclaration, isClassDeclaration, TypeParameter } from '../generated/ast' import { getPathAsString } from '../utils/ast' +import { getPrecomputedDescription } from '../utils/document' import { generateStream } from '../utils/stream' -import { createSyntheticAstNodeDescription } from './synthetic' type SourceMap = ZenScriptAstType type RuleMap = { [K in keyof SourceMap]?: (source: ReferenceInfo & { container: SourceMap[K] }) => Scope } @@ -18,12 +19,14 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { private readonly packageManager: PackageManager private readonly memberProvider: MemberProvider private readonly dynamicProvider: DynamicProvider + private readonly classIndex: ZenScriptClassIndex constructor(services: ZenScriptServices) { super(services) this.packageManager = services.workspace.PackageManager this.memberProvider = services.references.MemberProvider this.dynamicProvider = services.references.DynamicProvider + this.classIndex = services.workspace.ClassIndex } override getScope(context: ReferenceInfo): Scope { @@ -41,7 +44,7 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { .map(container => precomputed?.get(container)) .nonNullable() .map(descriptions => stream(descriptions).map(processor).nonNullable()) - .reduce((outer, descriptions) => this.createScope(descriptions, outer), outside as Scope) + .reduce((outer, descriptions) => new StreamScope(descriptions, outer), outside as Scope) } private dynamicScope(astNode: AstNode, outside?: Scope) { @@ -50,13 +53,14 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { private globalScope(outside?: Scope) { return this.createScope(this.indexManager.allElements(), outside) + // return new MapScope(this.indexManager.allElements(), outside) } private packageScope(outside?: Scope) { const packages = stream(this.packageManager.root.children.values()) .filter(it => it.isInternalNode()) - .map(it => createSyntheticAstNodeDescription('SyntheticHierarchyNode', it.name, it)) - return this.createScope(packages, outside) + .map(it => this.packageManager.syntheticDescriptionOf(it)) + return new StreamScope(packages, outside) } private classScope(outside?: Scope) { @@ -64,8 +68,30 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { .filter(it => it.isDataNode()) .flatMap(it => it.data) .filter(isClassDeclaration) - .map(it => this.descriptions.createDescription(it, it.name)) - return this.createScope(classes, outside) + .map((it) => { + const document = AstUtils.getDocument(it) + return getPrecomputedDescription(document, it) + }) + return new StreamScope(classes, outside) + } + + private getImportDescription(importDecl: ImportDeclaration): AstNodeDescription | undefined { + const refNode = importDecl.path.at(-1) + if (!refNode) { + return + } + + // access the ref to ensure the lookup of the import + const ref = refNode?.ref + if (!ref) { + return + } + + if (!importDecl.alias) { + return refNode?.$nodeDescription + } + + return this.descriptions.createDescription(ref, this.nameProvider.getName(importDecl), AstUtils.getDocument(ref)) } private readonly rules: RuleMap = { @@ -80,10 +106,13 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { const elements: AstNodeDescription[] = [] for (const sibling of siblings) { if (sibling.isDataNode()) { - sibling.data.forEach(it => elements.push(this.descriptions.createDescription(it, sibling.name))) + sibling.data.forEach((it) => { + const document = AstUtils.getDocument(it) + elements.push(getPrecomputedDescription(document, it)) + }) } else { - elements.push(createSyntheticAstNodeDescription('SyntheticHierarchyNode', sibling.name, sibling)) + elements.push(this.packageManager.syntheticDescriptionOf(sibling)) } } return this.createScope(elements) @@ -101,8 +130,7 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { return case ImportDeclaration: { const importDecl = desc.node as ImportDeclaration - const ref = importDecl.path.at(-1)?.ref ?? importDecl - return this.descriptions.createDescription(ref, this.nameProvider.getName(importDecl)) + return this.getImportDescription(importDecl) ?? desc } default: return desc @@ -114,7 +142,7 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { MemberAccess: (source) => { const outer = this.dynamicScope(source.container) const members = this.memberProvider.getMembers(source.container.receiver) - return this.createScope(members, outer) + return new StreamScope(members, outer) }, NamedTypeReference: (source) => { @@ -127,8 +155,7 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { return desc case ImportDeclaration: { const importDecl = desc.node as ImportDeclaration - const ref = importDecl.path.at(-1)?.ref ?? importDecl - return this.descriptions.createDescription(ref, this.nameProvider.getName(importDecl)) + return this.getImportDescription(importDecl) ?? desc } } } @@ -137,7 +164,7 @@ export class ZenScriptScopeProvider extends DefaultScopeProvider { else { const prev = source.container.path[source.index - 1].ref const members = this.memberProvider.getMembers(prev) - return this.createScope(members) + return new StreamScope(members) } }, } diff --git a/packages/zenscript/src/reference/synthetic.ts b/packages/zenscript/src/reference/synthetic.ts index b69a2d92..5a9178fe 100644 --- a/packages/zenscript/src/reference/synthetic.ts +++ b/packages/zenscript/src/reference/synthetic.ts @@ -10,9 +10,10 @@ export function createSyntheticAstNodeDescription MemberProvider + private readonly cache: ZenScriptDocumentCache + private readonly classIndex: ZenScriptClassIndex constructor(services: ZenScriptServices) { this.packageManager = services.workspace.PackageManager this.bracketManager = services.workspace.BracketManager this.memberProvider = () => services.references.MemberProvider + this.cache = new ZenScriptDocumentCache(services.shared) + this.classIndex = services.workspace.ClassIndex } public inferType(node: AstNode | undefined): Type | undefined { - // @ts-expect-error allowed index type - return this.rules[node?.$type]?.call(this, node) + if (!node) { + return + } + + const uri = AstUtils.findRootNode(node).$document?.uri?.toString() + if (!uri) { + // @ts-expect-error allowed index type + return this.rules[node?.$type]?.call(this, node) + } + + return this.cache.get(uri, node, () => { + // @ts-expect-error allowed index type + return this.rules[node?.$type]?.call(this, node) + }) } private classTypeOf(className: BuiltinTypes | string, substitutions: TypeParameterSubstitutions = new Map()): ClassType { @@ -147,10 +165,7 @@ export class ZenScriptTypeComputer implements TypeComputer { return } - const operatorDecl = this.memberProvider().getMembers(rangeType) - .map(it => it.node) - .filter(it => isOperatorFunctionDeclaration(it)) - .filter(it => it.op === 'for') + const operatorDecl = this.classIndex.findOperators(rangeType, 'for') .filter(it => it.parameters.length === length) .at(0) @@ -195,7 +210,7 @@ export class ZenScriptTypeComputer implements TypeComputer { .map(it => it.node) .filter(it => isFunctionDeclaration(it)) .filter(it => it.prefix === 'lambda') - .at(0) + .head() return this.inferType(lambdaDecl?.parameters.at(index)) } } @@ -325,10 +340,7 @@ export class ZenScriptTypeComputer implements TypeComputer { return receiverType } - const operatorDecl = this.memberProvider().getMembers(source.receiver) - .map(it => it.node) - .filter(it => isOperatorFunctionDeclaration(it)) - .filter(it => it.op === '[]') + const operatorDecl = this.classIndex.findOperators(receiverType, '[]') .at(0) let returnType = this.inferType(operatorDecl?.returnTypeRef) if (isClassType(receiverType)) { diff --git a/packages/zenscript/src/utils/cache.ts b/packages/zenscript/src/utils/cache.ts new file mode 100644 index 00000000..f6d86557 --- /dev/null +++ b/packages/zenscript/src/utils/cache.ts @@ -0,0 +1,44 @@ +import type { LangiumSharedCoreServices } from 'langium' +import { ContextCache as LangiumContextCache } from 'langium' + +export class ZenScriptDocumentCache extends LangiumContextCache { + private readonly lastDocumentVersion = new Map() + constructor(sharedServices: LangiumSharedCoreServices) { + super(uri => uri.toString()) + + this.onDispose(sharedServices.workspace.DocumentBuilder.onUpdate((changed, deleted) => { + const allChangedUris = new Set() + changed.forEach((uri) => { + const lastDocumentVersion = this.lastDocumentVersion.get(uri.toString()) ?? 0 + const currentVersion = sharedServices.workspace.LangiumDocuments.getDocument(uri)?.textDocument?.version + + if (!currentVersion) { + allChangedUris.add(uri.toString()) + return + } + + if (lastDocumentVersion < currentVersion) { + allChangedUris.add(uri.toString()) + this.lastDocumentVersion.set(uri.toString(), currentVersion) + } + }) + + deleted.forEach((uri) => { + const uriString = uri.toString() + allChangedUris.add(uriString) + this.lastDocumentVersion.delete(uriString) + }) + + if (allChangedUris.size === 0) { + return + } + + const indexManager = sharedServices.workspace.IndexManager + sharedServices.workspace.LangiumDocuments.all.forEach((document) => { + if (indexManager.isAffected(document, allChangedUris)) { + this.clear(document.uri.toString()) + } + }) + })) + } +} diff --git a/packages/zenscript/src/utils/document.ts b/packages/zenscript/src/utils/document.ts index 3d7b4778..b50c0a9b 100644 --- a/packages/zenscript/src/utils/document.ts +++ b/packages/zenscript/src/utils/document.ts @@ -1,4 +1,4 @@ -import type { LangiumDocument } from 'langium' +import type { AstNode, AstNodeDescription, LangiumDocument } from 'langium' import type { Script } from '../generated/ast' import { substringBeforeLast } from '@intellizen/shared' import { UriUtils } from 'langium' @@ -29,3 +29,24 @@ export function getQualifiedName(document: LangiumDocument