Skip to content

Commit 9b88c69

Browse files
committed
perf(sam): avoid searching for SAM CLI
Problem: - `SamCliLocationProvider.getLocation()` is called many times and always searches for sam on the system, which is slow (especially on Windows). - Hard to follow the logic of SamCliValidator, too much indirection involving `isSamCliVersionCached()` and `getSamCliExecutableId()`. Solution: - Cache the last-found sam location in `DefaultSamCliLocationProvider` instead of whatever `DefaultSamCliValidator` was trying to do. - Use the cached location unless it fails `tryRun()`. This means Toolkit still "shells out" to `sam`, which can be slow, but at least it avoids a system search.
1 parent 4192e7d commit 9b88c69

File tree

5 files changed

+54
-73
lines changed

5 files changed

+54
-73
lines changed

src/shared/logger/logger.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,24 @@ export function getNullLogger(type?: 'channel' | 'debugConsole' | 'main'): Logge
142142
export function setLogger(logger: Logger | undefined, type?: 'channel' | 'debugConsole' | 'main') {
143143
toolkitLoggers[type ?? 'main'] = logger
144144
}
145+
146+
export class PerfLog {
147+
private log
148+
public readonly start
149+
150+
public constructor(public readonly topic: string) {
151+
const log = getLogger()
152+
this.log = log
153+
if (log.logLevelEnabled('verbose')) {
154+
this.start = Date.now()
155+
}
156+
}
157+
158+
public done(): void {
159+
if (!this.start) {
160+
return
161+
}
162+
const elapsed = Date.now() - this.start
163+
this.log.verbose('%s took %dms', this.topic, elapsed.toFixed(1))
164+
}
165+
}

src/shared/sam/cli/samCliLocator.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,39 @@ import * as filesystemUtilities from '../../filesystemUtilities'
99
import { getLogger, Logger } from '../../logger'
1010
import { SamCliInfoInvocation } from './samCliInfo'
1111
import { DefaultSamCliValidator, SamCliValidatorContext, SamCliVersionValidation } from './samCliValidator'
12+
import { SystemUtilities } from '../../systemUtilities'
13+
import { PerfLog } from '../../logger/logger'
1214

1315
export interface SamCliLocationProvider {
1416
getLocation(): Promise<{ path: string; version: string } | undefined>
1517
}
1618

1719
export class DefaultSamCliLocationProvider implements SamCliLocationProvider {
1820
private static samCliLocator: BaseSamCliLocator | undefined
21+
protected static cachedSamLocation: { path: string; version: string } | undefined
1922

23+
/** Checks that the given `sam` actually works by invoking `sam --version`. */
24+
private static async isValidSamLocation(samPath: string) {
25+
return await SystemUtilities.tryRun(samPath, ['--version'], true, 'SAM CLI')
26+
}
27+
28+
/**
29+
* Gets the last-found `sam` location, or searches the system if a working
30+
* `sam` wasn't previously found and cached.
31+
*/
2032
public async getLocation() {
21-
return DefaultSamCliLocationProvider.getSamCliLocator().getLocation()
33+
const perflog = new PerfLog('samCliLocator: getLocation')
34+
const cachedLoc = DefaultSamCliLocationProvider.cachedSamLocation
35+
36+
// Avoid searching the system for `sam` (especially slow on Windows).
37+
if (cachedLoc && await DefaultSamCliLocationProvider.isValidSamLocation(cachedLoc.path)) {
38+
perflog.done()
39+
return cachedLoc
40+
}
41+
42+
DefaultSamCliLocationProvider.cachedSamLocation = await DefaultSamCliLocationProvider.getSamCliLocator().getLocation()
43+
perflog.done()
44+
return DefaultSamCliLocationProvider.cachedSamLocation
2245
}
2346

2447
public static getSamCliLocator(): SamCliLocationProvider {
@@ -43,7 +66,6 @@ abstract class BaseSamCliLocator {
4366
this.verifyOs()
4467
}
4568

46-
// TODO: this method is being called multiple times on my Windows machine and is really slow
4769
public async getLocation() {
4870
let location = await this.findFileInFolders(this.getExecutableFilenames(), this.getExecutableFolders())
4971

@@ -69,11 +91,10 @@ abstract class BaseSamCliLocator {
6991

7092
for (const fullPath of fullPaths) {
7193
if (!BaseSamCliLocator.didFind) {
72-
this.logger.verbose(`Searching for SAM CLI in: ${fullPath}`)
94+
this.logger.verbose(`samCliLocator: searching in: ${fullPath}`)
7395
}
7496
const context: SamCliValidatorContext = {
7597
samCliLocation: async () => fullPath,
76-
getSamCliExecutableId: async () => 'bogus',
7798
getSamCliInfo: async () => new SamCliInfoInvocation(fullPath).execute(),
7899
}
79100
const validator = new DefaultSamCliValidator(context)
@@ -84,10 +105,10 @@ abstract class BaseSamCliLocator {
84105
BaseSamCliLocator.didFind = true
85106
return { path: fullPath, version: validationResult.version }
86107
}
87-
this.logger.warn(`Found invalid SAM executable (${validationResult.validation}): ${fullPath}`)
108+
this.logger.warn(`samCliLocator: found invalid SAM CLI: (${validationResult.validation}): ${fullPath}`)
88109
} catch (e) {
89110
const err = e as Error
90-
this.logger.error(err)
111+
this.logger.error('samCliLocator failed: %s', err.message)
91112
}
92113
}
93114
}

src/shared/sam/cli/samCliSettings.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ export class SamCliSettings extends fromExtensionManifest('aws.samcli', descript
9595

9696
/**
9797
* Gets location of `sam` from:
98-
* 1. previous saved location (if valid), or
99-
* 2. user config (if valid), or
100-
* 3. tries to find `sam` on the system if the user config is invalid.
98+
* 1. user config (if any; overrides auto-detected location)
99+
* 2. previous cached location (if valid)
100+
* 3. searching for `sam` on the system
101101
*
102102
* @returns `autoDetected=true` if auto-detection was _attempted_.
103103
*/

src/shared/sam/cli/samCliValidator.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { stat } from 'fs-extra'
76
import * as semver from 'semver'
87
import { ClassToInterfaceType } from '../../utilities/tsUtils'
98
import { SamCliSettings } from './samCliSettings'
@@ -63,9 +62,6 @@ export interface SamCliValidator {
6362
export type SamCliValidatorContext = ClassToInterfaceType<DefaultSamCliValidatorContext>
6463

6564
export class DefaultSamCliValidator implements SamCliValidator {
66-
private cachedSamInfoResponse?: SamCliInfoResponse
67-
private cachedSamCliVersionId?: string
68-
6965
public constructor(private readonly context: SamCliValidatorContext) {}
7066

7167
public async detectValidSamCli(): Promise<SamCliValidatorResult> {
@@ -83,29 +79,11 @@ export class DefaultSamCliValidator implements SamCliValidator {
8379
}
8480

8581
public async getVersionValidatorResult(): Promise<SamCliVersionValidatorResult> {
86-
const samCliId: string = await this.context.getSamCliExecutableId()
87-
if (!this.isSamCliVersionCached(samCliId)) {
88-
this.cachedSamInfoResponse = await this.context.getSamCliInfo()
89-
this.cachedSamCliVersionId = samCliId
90-
}
91-
92-
const version = this.cachedSamInfoResponse!.version
93-
82+
const r = await this.context.getSamCliInfo()
9483
return {
95-
version,
96-
validation: DefaultSamCliValidator.validateSamCliVersion(version),
97-
}
98-
}
99-
100-
private isSamCliVersionCached(samCliVersionId: string): boolean {
101-
if (!this.cachedSamInfoResponse) {
102-
return false
84+
version: r.version,
85+
validation: DefaultSamCliValidator.validateSamCliVersion(r.version),
10386
}
104-
if (!this.cachedSamCliVersionId) {
105-
return false
106-
}
107-
108-
return this.cachedSamCliVersionId === samCliVersionId
10987
}
11088

11189
public static validateSamCliVersion(version?: string): SamCliVersionValidation {
@@ -136,19 +114,6 @@ export class DefaultSamCliValidatorContext implements SamCliValidatorContext {
136114
return (await this.config.getOrDetectSamCli()).path
137115
}
138116

139-
public async getSamCliExecutableId(): Promise<string> {
140-
// Function should never get called if there is no SAM CLI
141-
const location = await this.samCliLocation()
142-
if (!location) {
143-
throw new Error('SAM CLI does not exist')
144-
}
145-
146-
// The modification timestamp of SAM CLI is used as the "distinct executable id"
147-
const stats = await stat(location)
148-
149-
return stats.mtime.valueOf().toString()
150-
}
151-
152117
public async getSamCliInfo(): Promise<SamCliInfoResponse> {
153118
const samPath = await this.samCliLocation()
154119
if (!samPath) {

src/test/shared/sam/cli/samCliValidator.test.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ describe('DefaultSamCliValidator', async function () {
2626
return this.mockSamLocation
2727
}
2828

29-
public async getSamCliExecutableId(): Promise<string> {
30-
return this.samCliVersionId
31-
}
3229
public async getSamCliInfo(): Promise<SamCliInfoResponse> {
3330
this.getInfoCallCount++
3431

@@ -116,29 +113,6 @@ describe('DefaultSamCliValidator', async function () {
116113
)
117114
})
118115
})
119-
120-
it('Uses the cached validation result', async function () {
121-
const validatorContext = new TestSamCliValidatorContext(minSamCliVersion)
122-
const samCliValidator = new DefaultSamCliValidator(validatorContext)
123-
124-
await samCliValidator.getVersionValidatorResult()
125-
await samCliValidator.getVersionValidatorResult()
126-
127-
assert.strictEqual(validatorContext.getInfoCallCount, 1, 'getInfo called more than once')
128-
})
129-
130-
it('Does not use the cached validation result if the SAM CLI timestamp changed', async function () {
131-
const validatorContext = new TestSamCliValidatorContext(minSamCliVersion)
132-
const samCliValidator = new DefaultSamCliValidator(validatorContext)
133-
134-
await samCliValidator.getVersionValidatorResult()
135-
136-
// Oh look, a new SAM CLI timestamp
137-
validatorContext.samCliVersionId = validatorContext.samCliVersionId + 'x'
138-
await samCliValidator.getVersionValidatorResult()
139-
140-
assert.strictEqual(validatorContext.getInfoCallCount, 2, 'getInfo was not called both times')
141-
})
142116
})
143117

144118
describe('validateSamCliVersion', async function () {

0 commit comments

Comments
 (0)