Skip to content

Commit 907be9c

Browse files
perf(startup): setup cfn template registry asynchronously #3370
Problem: The cfn registry setup can take a long time if there are a lot of yaml files. This can significantly slow down extension startup. Solution: - Initialize the registry asynchronously. - If the user triggers functionality that requires the template registry and it is not yet ready, show a progress message. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent aec0b16 commit 907be9c

File tree

2 files changed

+105
-9
lines changed

2 files changed

+105
-9
lines changed

src/shared/cloudformation/activation.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
77
import { getLogger } from '../logger'
88
import { localize } from '../utilities/vsCodeUtils'
99

10-
import { CloudFormationTemplateRegistry } from '../fs/templateRegistry'
10+
import { AsyncCloudFormationTemplateRegistry, CloudFormationTemplateRegistry } from '../fs/templateRegistry'
1111
import { getIdeProperties } from '../extensionUtilities'
1212
import { NoopWatcher } from '../fs/watchedFiles'
1313
import { createStarterTemplateFile } from './cloudformation'
@@ -35,11 +35,8 @@ export const devfileExcludePattern = /.*devfile\.(yaml|yml)/i
3535
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
3636
try {
3737
const registry = new CloudFormationTemplateRegistry()
38-
globals.templateRegistry = registry
39-
await registry.addExcludedPattern(devfileExcludePattern)
40-
await registry.addExcludedPattern(templateFileExcludePattern)
41-
await registry.addWatchPattern(templateFileGlobPattern)
42-
await registry.watchUntitledFiles()
38+
extensionContext.subscriptions.push(registry)
39+
setTemplateRegistryInGlobals(registry)
4340
} catch (e) {
4441
vscode.window.showErrorMessage(
4542
localize(
@@ -48,15 +45,49 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
4845
getIdeProperties().codelenses
4946
)
5047
)
51-
getLogger().error('Failed to activate template registry', e)
48+
getLogger().error('Failed to activate template registry: %s', e)
5249
// This prevents us from breaking for any reason later if it fails to load. Since
5350
// Noop watcher is always empty, we will get back empty arrays with no issues.
5451
globals.templateRegistry = new NoopWatcher() as unknown as CloudFormationTemplateRegistry
5552
}
5653
// If setting it up worked, add it to subscriptions so it is cleaned up at exit
5754
extensionContext.subscriptions.push(
58-
globals.templateRegistry,
5955
Commands.register('aws.cloudFormation.newTemplate', () => createStarterTemplateFile(false)),
6056
Commands.register('aws.sam.newTemplate', () => createStarterTemplateFile(true))
6157
)
6258
}
59+
60+
/**
61+
* Sets the `templateRegistry` property in the `globals` variable,
62+
* where the value of the property depends on whether the registry
63+
* is fully set up.
64+
*
65+
* This function exists to resolve the registry setup taking a long time
66+
* and slowing down the extension starting up.
67+
*/
68+
function setTemplateRegistryInGlobals(registry: CloudFormationTemplateRegistry) {
69+
const registrySetupFunc = async (registry: CloudFormationTemplateRegistry) => {
70+
await registry.addExcludedPattern(devfileExcludePattern)
71+
await registry.addExcludedPattern(templateFileExcludePattern)
72+
await registry.addWatchPattern(templateFileGlobPattern)
73+
await registry.watchUntitledFiles()
74+
}
75+
76+
const asyncRegistry = new AsyncCloudFormationTemplateRegistry(registry, registrySetupFunc)
77+
78+
Object.defineProperty(globals, 'templateRegistry', {
79+
set(newInstance: CloudFormationTemplateRegistry) {
80+
this.cfnInstance = newInstance
81+
},
82+
get() {
83+
// This condition handles testing scenarios where we may have
84+
// already set a mock object before activation.
85+
// Though in prod nothing should be calling this 'set' function.
86+
if (this.cfnInstance) {
87+
return this.cfnInstance
88+
}
89+
90+
return asyncRegistry.getInstance()
91+
},
92+
})
93+
}

src/shared/fs/templateRegistry.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import * as path from 'path'
1111
import { isInDirectory } from '../filesystemUtilities'
1212
import { dotNetRuntimes, goRuntimes, javaRuntimes } from '../../lambda/models/samLambdaRuntime'
1313
import { getLambdaDetails } from '../../lambda/utils'
14-
import { WatchedFiles, WatchedItem } from './watchedFiles'
14+
import { NoopWatcher, WatchedFiles, WatchedItem } from './watchedFiles'
1515
import { getLogger } from '../logger'
1616
import globals from '../extensionGlobals'
1717
import { isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils'
18+
import { sleep } from '../utilities/timeoutUtils'
19+
import { localize } from '../utilities/vsCodeUtils'
1820

1921
export class CloudFormationTemplateRegistry extends WatchedFiles<CloudFormation.Template> {
2022
protected name: string = 'CloudFormationTemplateRegistry'
@@ -68,6 +70,69 @@ export class CloudFormationTemplateRegistry extends WatchedFiles<CloudFormation.
6870
}
6971
}
7072

73+
/**
74+
* The purpose of this class is to resolve a {@link CloudFormationTemplateRegistry}
75+
* instance once the given setup is complete.
76+
*
77+
* This solves the issue where setup can take a while and if we
78+
* block on it the entire extension startup time is increased.
79+
*/
80+
export class AsyncCloudFormationTemplateRegistry {
81+
/** Setup of the registry can take a while, this property indicates it is done */
82+
private isSetup = false
83+
/** The message that is shown to the user to indicate the registry is being set up */
84+
private setupProgressMessage: Thenable<void> | undefined = undefined
85+
86+
/**
87+
* @param asyncSetupFunc registry setup that will be run async
88+
*/
89+
constructor(
90+
private readonly instance: CloudFormationTemplateRegistry,
91+
asyncSetupFunc: (instance: CloudFormationTemplateRegistry) => Promise<void>
92+
) {
93+
getLogger().info('cfn: starting template registry setup.')
94+
asyncSetupFunc(instance).then(() => {
95+
this.isSetup = true
96+
getLogger().info('cfn: template registry setup successful.')
97+
})
98+
}
99+
100+
/**
101+
* Returns the initial registry instance if setup has completed, otherwise
102+
* returning a temporary instance and showing a progress message to the user
103+
* to indicate setup is in progress.
104+
*/
105+
getInstance(): CloudFormationTemplateRegistry {
106+
if (this.isSetup) {
107+
return this.instance
108+
}
109+
110+
// Show user a message indicating setup is in progress
111+
if (this.setupProgressMessage === undefined) {
112+
this.setupProgressMessage = vscode.window.withProgress(
113+
{
114+
location: vscode.ProgressLocation.Notification,
115+
title: localize('AWS.codelens.waitingForTemplateRegistry', 'Scanning CloudFormation templates...'),
116+
cancellable: true,
117+
},
118+
async (progress, token) => {
119+
token.onCancellationRequested(() => {
120+
// Allows for new message to be created if templateRegistry variable attempted to be used again
121+
this.setupProgressMessage = undefined
122+
})
123+
getLogger().info('cfn: Waiting for template registry setup to complete.')
124+
while (!this.isSetup) {
125+
await sleep(2000)
126+
}
127+
getLogger().info('cfn: Finished waiting for template registry setup.')
128+
}
129+
)
130+
}
131+
132+
return new NoopWatcher() as unknown as CloudFormationTemplateRegistry
133+
}
134+
}
135+
71136
/**
72137
* Gets resources and additional metadata for resources tied to a filepath and handler.
73138
* Checks all registered templates by default; otherwise can operate on a subset TemplateDatum[]

0 commit comments

Comments
 (0)