Skip to content

Commit cb8c218

Browse files
authored
Merge #3429 perf: avoid searching for SAM CLI
2 parents 30c6a20 + 33dbecb commit cb8c218

File tree

11 files changed

+102
-99
lines changed

11 files changed

+102
-99
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: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,40 @@ 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'], 'no', '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 =
43+
await DefaultSamCliLocationProvider.getSamCliLocator().getLocation()
44+
perflog.done()
45+
return DefaultSamCliLocationProvider.cachedSamLocation
2246
}
2347

2448
public static getSamCliLocator(): SamCliLocationProvider {
@@ -43,7 +67,6 @@ abstract class BaseSamCliLocator {
4367
this.verifyOs()
4468
}
4569

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

@@ -69,11 +92,10 @@ abstract class BaseSamCliLocator {
6992

7093
for (const fullPath of fullPaths) {
7194
if (!BaseSamCliLocator.didFind) {
72-
this.logger.verbose(`Searching for SAM CLI in: ${fullPath}`)
95+
this.logger.verbose(`samCliLocator: searching in: ${fullPath}`)
7396
}
7497
const context: SamCliValidatorContext = {
7598
samCliLocation: async () => fullPath,
76-
getSamCliExecutableId: async () => 'bogus',
7799
getSamCliInfo: async () => new SamCliInfoInvocation(fullPath).execute(),
78100
}
79101
const validator = new DefaultSamCliValidator(context)
@@ -84,10 +106,12 @@ abstract class BaseSamCliLocator {
84106
BaseSamCliLocator.didFind = true
85107
return { path: fullPath, version: validationResult.version }
86108
}
87-
this.logger.warn(`Found invalid SAM executable (${validationResult.validation}): ${fullPath}`)
109+
this.logger.warn(
110+
`samCliLocator: found invalid SAM CLI: (${validationResult.validation}): ${fullPath}`
111+
)
88112
} catch (e) {
89113
const err = e as Error
90-
this.logger.error(err)
114+
this.logger.error('samCliLocator failed: %s', err.message)
91115
}
92116
}
93117
}
@@ -156,6 +180,7 @@ class WindowsSamCliLocator extends BaseSamCliLocator {
156180

157181
class UnixSamCliLocator extends BaseSamCliLocator {
158182
private static readonly locationPaths: string[] = [
183+
'/opt/homebrew/bin/sam',
159184
'/usr/local/bin',
160185
'/usr/bin',
161186
// WEIRD BUT TRUE: brew installs to /home/linuxbrew/.linuxbrew if

src/shared/sam/cli/samCliSettings.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ export class SamCliSettings extends fromExtensionManifest('aws.samcli', descript
9494
}
9595

9696
/**
97-
* Gets location of `sam` from user config, or tries to find `sam` on the
98-
* system if the user config is invalid.
97+
* Gets location of `sam` from:
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
99101
*
100102
* @returns `autoDetected=true` if auto-detection was _attempted_.
101103
*/

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/shared/systemUtilities.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,32 @@ export class SystemUtilities {
183183
// TODO: implement this by checking the file mode
184184
// public static async checkExactPerms(file: string | vscode.Uri, perms: `${PermissionsTriplet}${PermissionsTriplet}${PermissionsTriplet}`)
185185

186+
/**
187+
* Tries to execute a program at path `p` with the given args and
188+
* optionally checks the output for `expected`.
189+
*
190+
* @param p path to a program to execute
191+
* @param args program args
192+
* @param doLog log failures
193+
* @param expected output must contain this string
194+
*/
195+
public static async tryRun(
196+
p: string,
197+
args: string[],
198+
logging: 'yes' | 'no' | 'noresult' = 'yes',
199+
expected?: string
200+
): Promise<boolean> {
201+
const proc = new ChildProcess(p, args, { logging: 'no' })
202+
const r = await proc.run()
203+
const ok = r.exitCode === 0 && (expected === undefined || r.stdout.includes(expected))
204+
if (logging === 'noresult') {
205+
getLogger().info('tryRun: %s: %s', ok ? 'ok' : 'failed', proc)
206+
} else if (logging !== 'no') {
207+
getLogger().info('tryRun: %s: %s %O', ok ? 'ok' : 'failed', proc, proc.result())
208+
}
209+
return ok
210+
}
211+
186212
/**
187213
* Gets the fullpath to `code` (VSCode CLI), or falls back to "code" (not
188214
* absolute) if it works.
@@ -217,13 +243,10 @@ export class SystemUtilities {
217243
if (!vsc || (vsc !== 'code' && !fs.existsSync(vsc))) {
218244
continue
219245
}
220-
const proc = new ChildProcess(vsc, ['--version'])
221-
const r = await proc.run()
222-
if (r.exitCode === 0) {
246+
if (await SystemUtilities.tryRun(vsc, ['--version'])) {
223247
SystemUtilities.vscPath = vsc
224248
return vsc
225249
}
226-
getLogger().warn('getVscodeCliPath: failed: %s %O', proc, proc.result())
227250
}
228251

229252
return undefined
@@ -245,8 +268,7 @@ export class SystemUtilities {
245268

246269
for (const tsc of tscPaths) {
247270
// Try to run "tsc -v".
248-
const result = await new ChildProcess(tsc, ['-v']).run()
249-
if (result.exitCode === 0 && result.stdout.includes('Version')) {
271+
if (await SystemUtilities.tryRun(tsc, ['-v'], 'yes', 'Version')) {
250272
return tsc
251273
}
252274
}
@@ -275,13 +297,10 @@ export class SystemUtilities {
275297
if (!p || ('ssh' !== p && !fs.existsSync(p))) {
276298
continue
277299
}
278-
const proc = new ChildProcess(p, ['-G', 'x'])
279-
const r = await proc.run()
280-
if (r.exitCode === 0) {
300+
if (await SystemUtilities.tryRun(p, ['-G', 'x'], 'noresult' /* "ssh -G" prints quasi-sensitive info. */)) {
281301
SystemUtilities.sshPath = p
282302
return p
283303
}
284-
getLogger().warn('findSshPath: failed: %s', proc)
285304
}
286305
}
287306

@@ -300,13 +319,10 @@ export class SystemUtilities {
300319
if (!p || ('git' !== p && !fs.existsSync(p))) {
301320
continue
302321
}
303-
const proc = new ChildProcess(p, ['--version'])
304-
const r = await proc.run()
305-
if (r.exitCode === 0) {
322+
if (await SystemUtilities.tryRun(p, ['--version'])) {
306323
SystemUtilities.gitPath = p
307324
return p
308325
}
309-
getLogger().warn('findGitPath: failed: %s', proc)
310326
}
311327
}
312328

@@ -323,13 +339,10 @@ export class SystemUtilities {
323339
if (!p || ('bash' !== p && !fs.existsSync(p))) {
324340
continue
325341
}
326-
const proc = new ChildProcess(p, ['--version'])
327-
const r = await proc.run()
328-
if (r.exitCode === 0) {
342+
if (await SystemUtilities.tryRun(p, ['--version'])) {
329343
SystemUtilities.bashPath = p
330344
return p
331345
}
332-
getLogger().warn('findBashPath: failed: %s', proc)
333346
}
334347
}
335348
}

src/shared/utilities/asyncCollection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ async function promise<T>(iterable: AsyncIterable<T>): Promise<T[]> {
222222
function addToMap<T, U extends string>(map: Map<string, T>, selector: KeySelector<T, U> | StringProperty<T>, item: T) {
223223
const key = typeof selector === 'function' ? selector(item) : item[selector]
224224
if (key) {
225-
if (map.has(key as keyof typeof map['keys'])) {
225+
if (map.has(key as keyof (typeof map)['keys'])) {
226226
throw new Error(`Duplicate key found when converting AsyncIterable to map: ${key}`)
227227
}
228228

229-
map.set(key as keyof typeof map['keys'], item)
229+
map.set(key as keyof (typeof map)['keys'], item)
230230
}
231231
}
232232

src/shared/utilities/functionUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ export function onceChanged<T, U extends any[]>(fn: (...args: U) => T): (...args
5656
let ran = false
5757
let prevArgs = ''
5858

59-
return (...args) => ((ran && prevArgs === args.map(String).join(':'))
60-
? val
61-
: ((val = fn(...args)), (ran = true), (prevArgs = args.map(String).join(':')), val))
59+
return (...args) =>
60+
ran && prevArgs === args.map(String).join(':')
61+
? val
62+
: ((val = fn(...args)), (ran = true), (prevArgs = args.map(String).join(':')), val)
6263
}
6364

6465
/**

src/shared/utilities/typeConstructors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export type FromDescriptor<T extends TypeDescriptor> = {
5151

5252
// `Symbol` and `BigInt` are included here, though in-practice
5353
const primitives = [Number, String, Boolean, Object, Symbol, BigInt]
54-
function isPrimitiveConstructor(type: unknown): type is typeof primitives[number] {
54+
function isPrimitiveConstructor(type: unknown): type is (typeof primitives)[number] {
5555
return !!primitives.find(p => p === type)
5656
}
5757

src/stepFunctions/activation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ async function registerStepFunctionCommands(
8888
)
8989
}
9090

91-
export function initalizeWebviewPaths(context: vscode.ExtensionContext): typeof globals['visualizationResourcePaths'] {
91+
export function initalizeWebviewPaths(
92+
context: vscode.ExtensionContext
93+
): (typeof globals)['visualizationResourcePaths'] {
9294
// Location for script in body of webview that handles input from user
9395
// and calls the code to render state machine graph
9496

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export class MockSamCliProcessInvoker implements SamCliProcessInvoker {
2121

2222
this.validateArgs(invokeSettings.arguments)
2323

24-
return ({
24+
return {
2525
exitCode: 0,
26-
} as any) as ChildProcessResult
26+
} as any as ChildProcessResult
2727
}
2828
}
2929

0 commit comments

Comments
 (0)