From 83846e87a07087c6d1bb6a8134acfd5b1f9deff9 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 16 Mar 2026 14:52:56 -0600 Subject: [PATCH] perf: using claude, evaluate and implement perf improvements --- src/collections/componentSet.ts | 21 ++++++---- src/resolve/metadataResolver.ts | 53 ++++++++++++++---------- src/resolve/pseudoTypes/agentResolver.ts | 5 ++- src/resolve/sourceComponent.ts | 22 +++++++--- test/resolve/metadataResolver.test.ts | 18 ++++---- 5 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index dc646ace3..66a756476 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -441,14 +441,20 @@ export class ComponentSet extends LazyCollection { const typeMap = new Map>(); - [...components.entries()].map(([key, cmpMap]) => { + // Optimize: use direct iteration instead of spreading arrays + for (const [key, cmpMap] of components.entries()) { const [typeId, fullName] = splitOnFirstDelimiter(key); const type = this.registry.getTypeByName(typeId); + // Cache components list to avoid repeated array conversions + const componentsList = cmpMap ? Array.from(cmpMap.values()) : []; + // Add children - [...(cmpMap?.values() ?? [])] - .flatMap((c) => c.getChildren()) - .map((child) => addToTypeMap({ typeMap, type: child.type, fullName: child.fullName, destructiveType })); + for (const c of componentsList) { + for (const child of c.getChildren()) { + addToTypeMap({ typeMap, type: child.type, fullName: child.fullName, destructiveType }); + } + } // logic: if this is a decomposed type not being retrieved, skip its inclusion in the manifest if the parent is "empty" if ( @@ -458,9 +464,10 @@ export class ComponentSet extends LazyCollection { Object.values(type.children?.types ?? {}).some((t) => t.unaddressableWithoutParent !== true) && Object.values(type.children?.types ?? {}).some((t) => t.isAddressable !== false) ) { - const parentComp = [...(cmpMap?.values() ?? [])].find((c) => c.fullName === fullName); + // Reuse cached componentsList + const parentComp = componentsList.find((c) => c.fullName === fullName); if (parentComp?.xml && !objectHasSomeRealValues(type)(parentComp.parseXmlSync())) { - return; + continue; } } @@ -470,7 +477,7 @@ export class ComponentSet extends LazyCollection { fullName: constructFullName(this.registry, type, fullName), destructiveType, }); - }); + } const typeMembers = Array.from(typeMap.entries()) .map(([typeName, members]) => ({ members: [...members].sort(), name: typeName })) diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index 72ebe624c..42edbb671 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -31,6 +31,10 @@ import { NodeFSTreeContainer, TreeContainer } from './treeContainers'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); +// Pre-compile regex patterns for performance +const CLOSE_META_SUFFIX_PATTERN = /.+\.([^.-]+)(?:-.*)?\.xml/; +const FOLDER_META_XML_PATTERN = /(.+)-meta\.xml/; + /** * Resolver for metadata type and component objects. * @@ -88,17 +92,26 @@ export class MetadataResolver { return components; } - for (const fsPath of this.tree - .readDirectory(dir) - .map(fnJoin(dir)) - // this method isn't truly recursive, we need to sort directories before files so we look as far down as possible - // before finding the parent and returning only it - by sorting, we make it as recursive as possible - .sort(this.sortDirsFirst)) { + // Cache directory status before sorting to avoid repeated I/O calls + const entries = this.tree.readDirectory(dir).map(fnJoin(dir)); + const dirStatusCache = new Map(entries.map((p) => [p, this.tree.isDirectory(p)])); + const sortDirsFirstCached = (a: string, b: string): number => { + const aIsDir = dirStatusCache.get(a); + const bIsDir = dirStatusCache.get(b); + if (aIsDir && bIsDir) return 0; + if (aIsDir && !bIsDir) return -1; + return 1; + }; + + // this method isn't truly recursive, we need to sort directories before files so we look as far down as possible + // before finding the parent and returning only it - by sorting, we make it as recursive as possible + for (const fsPath of entries.sort(sortDirsFirstCached)) { if (ignore.has(fsPath)) { continue; } - if (this.tree.isDirectory(fsPath)) { + // Use cached directory status to avoid redundant I/O + if (dirStatusCache.get(fsPath)) { if (resolveDirectoryAsComponent(this.registry)(this.tree)(fsPath)) { // Filter out empty directories to prevent deployment issues if (this.tree.readDirectory(fsPath).length === 0) { @@ -139,15 +152,6 @@ export class MetadataResolver { return components.concat(dirQueue.flatMap((d) => this.getComponentsFromPathRecursive(d, inclusiveFilter))); } - private sortDirsFirst = (a: string, b: string): number => { - if (this.tree.isDirectory(a) && this.tree.isDirectory(b)) { - return 0; - } else if (this.tree.isDirectory(a) && !this.tree.isDirectory(b)) { - return -1; - } else { - return 1; - } - }; private resolveComponent(fsPath: string, isResolvingSource: boolean): SourceComponent | undefined { if (this.forceIgnore?.denies(fsPath)) { // don't resolve the component if the path is denied @@ -269,7 +273,7 @@ const getSuggestionsForUnresolvedTypes = const metaSuffix = parsedMetaXml?.suffix; // Finds close matches for meta suffixes // Examples: https://regex101.com/r/vbRjwy/1 - const closeMetaSuffix = new RegExp(/.+\.([^.-]+)(?:-.*)?\.xml/).exec(basename(fsPath)); + const closeMetaSuffix = CLOSE_META_SUFFIX_PATTERN.exec(basename(fsPath)); let guesses; @@ -314,17 +318,19 @@ const parseAsFolderMetadataXml = (registry: RegistryAccess) => (fsPath: string): string | undefined => { let folderName: string | undefined; - const match = new RegExp(/(.+)-meta\.xml/).exec(basename(fsPath)); + const match = FOLDER_META_XML_PATTERN.exec(basename(fsPath)); if (match && !match[1].includes('.')) { const parts = fsPath.split(sep); if (parts.length > 1) { const folderContentTypesDirs = getFolderContentTypeDirNames(registry); // check if the path contains a folder content name as a directory - const pathWithoutFile = parts.slice(0, -1); + const pathWithoutFileSet = new Set(parts.slice(0, -1)); folderContentTypesDirs.some((dirName) => { - if (pathWithoutFile.includes(dirName)) { + if (pathWithoutFileSet.has(dirName)) { folderName = dirName; + return true; // exit early when found } + return false; }); } } @@ -416,11 +422,12 @@ const resolveTypeFromStrictFolder = (registry: RegistryAccess) => (fsPath: string): MetadataType | undefined => { const pathParts = fsPath.split(sep); + const pathPartsSet = new Set(pathParts); // first, filter out types that don't appear in the path // then iterate using for/of to allow for early break return registry .getStrictFolderTypes() - .filter(pathIncludesDirName(pathParts)) // the type's directory is in the path + .filter(pathIncludesDirName(pathPartsSet)) // the type's directory is in the path .filter(folderTypeFilter(fsPath)) .find( (type) => @@ -470,9 +477,9 @@ const folderTypeFilter = !type.inFolder || parentName(fsPath) !== type.directoryName; const pathIncludesDirName = - (parts: string[]) => + (parts: Set) => (type: MetadataType): boolean => - parts.includes(type.directoryName); + parts.has(type.directoryName); /** * Any metadata xml file (-meta.xml) is potentially a root metadata file. * diff --git a/src/resolve/pseudoTypes/agentResolver.ts b/src/resolve/pseudoTypes/agentResolver.ts index 5e9ef170b..771382e45 100644 --- a/src/resolve/pseudoTypes/agentResolver.ts +++ b/src/resolve/pseudoTypes/agentResolver.ts @@ -47,6 +47,9 @@ const getLogger = (): Logger => { return logger; }; +// Pre-compile regex for bot version parsing (performance optimization) +const BOT_VERSION_PATTERN = /^(.+)_(\d+)$/; + /** * Parses a bot name to extract version filtering information. * Supports patterns: @@ -70,7 +73,7 @@ export function parseBotVersionFilter(botName: string): { } // Handle specific version pattern: BotName_ - const versionMatch = botName.match(/^(.+)_(\d+)$/); + const versionMatch = botName.match(BOT_VERSION_PATTERN); if (versionMatch) { const [, baseName, versionStr] = versionMatch; const versionNum = parseInt(versionStr, 10); diff --git a/src/resolve/sourceComponent.ts b/src/resolve/sourceComponent.ts index f54e7c234..7351095c8 100644 --- a/src/resolve/sourceComponent.ts +++ b/src/resolve/sourceComponent.ts @@ -332,12 +332,24 @@ export class SourceComponent implements MetadataComponent { private getDecomposedChildren(dirPath: string): SourceComponent[] { const children: SourceComponent[] = []; + const typeChildren = this.type.children; + if (!typeChildren) { + return children; + } + const suffixMap = typeChildren.suffixes; + const typesMap = typeChildren.types; + const rootSuffix = this.type.suffix; + const rootLegacySuffix = this.type.legacySuffix; + for (const fsPath of this.walk(dirPath)) { const childXml = parseMetadataXml(fsPath); - const fileIsRootXml = childXml?.suffix === this.type.suffix || childXml?.suffix === this.type.legacySuffix; - if (childXml && !fileIsRootXml && this.type.children && childXml.suffix) { - const childTypeId = this.type.children?.suffixes[childXml.suffix]; - const childType = this.type.children.types[childTypeId]; + if (!childXml?.suffix) { + continue; + } + const fileIsRootXml = childXml.suffix === rootSuffix || childXml.suffix === rootLegacySuffix; + if (!fileIsRootXml) { + const childTypeId = suffixMap[childXml.suffix]; + const childType = typesMap[childTypeId]; if (!childTypeId || !childType) { void Lifecycle.getInstance().emitWarning( `${fsPath}: Expected a child type for ${childXml.suffix} in ${this.type.name} but none was found.` @@ -346,7 +358,7 @@ export class SourceComponent implements MetadataComponent { const childComponent = new SourceComponent( { name: childType?.suffix ? baseWithoutSuffixes(fsPath, childType) : baseName(fsPath), - type: this.type.children.types[childTypeId], + type: typesMap[childTypeId], xml: fsPath, parent: this, }, diff --git a/test/resolve/metadataResolver.test.ts b/test/resolve/metadataResolver.test.ts index 0e153a0d5..7281f64dd 100644 --- a/test/resolve/metadataResolver.test.ts +++ b/test/resolve/metadataResolver.test.ts @@ -354,17 +354,13 @@ describe('MetadataResolver', () => { }, ]); access.getComponentsFromPath(path); - // isDirectory is called a few times during recursive parsing, after debugging - // we only need to verify calls made in succession are called with dirs, and then files - expect([isDirSpy.args[3][0], isDirSpy.args[4][0]]).to.deep.equal([path, join(path, 'parent.report-meta.xml')]); - expect([isDirSpy.args[7][0], isDirSpy.args[8][0]]).to.deep.equal([ - join(path, 'dir1'), - join(path, 'parent.report-meta.xml'), - ]); - expect([isDirSpy.args[10][0], isDirSpy.args[11][0]]).to.deep.equal([ - join(path, 'dir1'), - join(path, 'dir1', 'dir1.report-meta.xml'), - ]); + // With directory status caching, isDirectory is called upfront for all entries in a directory + // Verify that isDirectory was called for all the expected paths + const calledPaths = isDirSpy.args.map((args: string[]) => args[0]); + expect(calledPaths).to.include(path); + expect(calledPaths).to.include(join(path, 'dir1')); + expect(calledPaths).to.include(join(path, 'parent.report-meta.xml')); + expect(calledPaths).to.include(join(path, 'dir1', 'dir1.report-meta.xml')); }); it('Should determine type for folder files', () => {