Skip to content

Commit a5050f1

Browse files
authored
performance: Create a starter set of tools for measuring the CPU/memory impact of codei (#5446)
Problem: - We are sometimes running into problems where functions are using a ton of memory/cpu but we have no way to figure that out ahead of time without manually debugging/customer reports Solution: - Create a starter set of tools that allows us to measure the impact of different parts of code. These tools should be used as needed and surround heavy computational parts of the code i.e. collecting all files in the workspace, reading them in and putting them in a zip
1 parent 2bd23c4 commit a5050f1

File tree

8 files changed

+430
-112
lines changed

8 files changed

+430
-112
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import assert from 'assert'
7+
import { getLogger } from '../logger'
8+
import { isWeb } from '../extensionGlobals'
9+
10+
interface PerformanceMetrics {
11+
cpuUsage: number
12+
heapTotal: number
13+
duration: number
14+
}
15+
16+
interface TestOptions {
17+
darwin?: Partial<PerformanceMetrics>
18+
win32?: Partial<PerformanceMetrics>
19+
linux?: Partial<PerformanceMetrics>
20+
}
21+
22+
export interface PerformanceSpan<T> {
23+
value: T
24+
performance: PerformanceMetrics
25+
}
26+
27+
export class PerformanceTracker {
28+
#startPerformance:
29+
| {
30+
cpuUsage: NodeJS.CpuUsage
31+
memory: number
32+
duration: [number, number]
33+
}
34+
| undefined
35+
36+
constructor(private readonly name: string) {}
37+
38+
static enabled(name: string, trackPerformance?: boolean): boolean {
39+
return name === 'function_call' && (trackPerformance ?? false) && !isWeb()
40+
}
41+
42+
start() {
43+
this.#startPerformance = {
44+
cpuUsage: process.cpuUsage(),
45+
memory: process.memoryUsage().heapTotal,
46+
duration: process.hrtime(),
47+
}
48+
}
49+
50+
stop(): PerformanceMetrics | undefined {
51+
if (this.#startPerformance) {
52+
const endCpuUsage = process.cpuUsage(this.#startPerformance?.cpuUsage)
53+
const userCpuUsage = endCpuUsage.user / 1000000
54+
const systemCpuUsage = endCpuUsage.system / 1000000
55+
56+
const elapsedTime = process.hrtime(this.#startPerformance.duration)
57+
const duration = elapsedTime[0] + elapsedTime[1] / 1e9 // convert microseconds to seconds
58+
const usage = ((userCpuUsage + systemCpuUsage) / duration) * 100 // convert to percentage
59+
60+
const endMemoryUsage = process.memoryUsage().heapTotal - this.#startPerformance?.memory
61+
const endMemoryUsageInMB = endMemoryUsage / (1024 * 1024) // converting bytes to MB
62+
63+
return {
64+
cpuUsage: usage,
65+
heapTotal: endMemoryUsageInMB,
66+
duration,
67+
}
68+
} else {
69+
getLogger().debug(`PerformanceTracker: start performance not defined for ${this.name}`)
70+
}
71+
}
72+
}
73+
74+
export function performanceTest(options: TestOptions, name: string, fn: () => Promise<void>): Mocha.Test
75+
export function performanceTest(options: TestOptions, name: string, fn: () => void): Mocha.Test
76+
export function performanceTest(options: TestOptions, name: string, fn: () => void | Promise<void>) {
77+
const testOption = options[process.platform as 'linux' | 'darwin' | 'win32']
78+
79+
const performanceTracker = new PerformanceTracker(name)
80+
81+
return it(name, async () => {
82+
performanceTracker.start()
83+
await fn()
84+
const metrics = performanceTracker.stop()
85+
if (!metrics) {
86+
assert.fail('Performance metrics not found')
87+
}
88+
assertPerformanceMetrics(metrics, name, testOption)
89+
})
90+
}
91+
92+
function assertPerformanceMetrics(
93+
performanceMetrics: PerformanceMetrics,
94+
name: string,
95+
testOption?: Partial<PerformanceMetrics>
96+
) {
97+
const expectedCPUUsage = testOption?.cpuUsage ?? 50
98+
const foundCPUUsage = performanceMetrics.cpuUsage
99+
100+
assert(
101+
foundCPUUsage < expectedCPUUsage,
102+
`Expected total CPU usage for ${name} to be less than ${expectedCPUUsage}. Actual CPU usage was ${foundCPUUsage}`
103+
)
104+
105+
const expectedMemoryUsage = testOption?.heapTotal ?? 400
106+
const foundMemoryUsage = performanceMetrics.heapTotal
107+
assert(
108+
foundMemoryUsage < expectedMemoryUsage,
109+
`Expected total memory usage for ${name} to be less than ${expectedMemoryUsage}. Actual memory usage was ${foundMemoryUsage}`
110+
)
111+
112+
const expectedDuration = testOption?.duration ?? 5
113+
const foundDuration = performanceMetrics.duration
114+
assert(
115+
foundDuration < expectedDuration,
116+
`Expected total duration for ${name} to be less than ${expectedDuration}. Actual duration was ${foundDuration}`
117+
)
118+
}

packages/core/src/shared/telemetry/spans.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getTelemetryResult,
2424
} from '../errors'
2525
import { entries, NumericKeys } from '../utilities/tsUtils'
26+
import { PerformanceTracker } from '../performance/performance'
2627

2728
const AsyncLocalStorage: typeof AsyncLocalStorageClass =
2829
require('async_hooks').AsyncLocalStorage ??
@@ -97,6 +98,9 @@ export type SpanOptions = {
9798
/** True if this span should emit its telemetry events. Defaults to true if undefined. */
9899
emit?: boolean
99100

101+
/** True if this span should emit performance metrics acquired when running the function. Defaults to false if undefined */
102+
trackPerformance?: boolean
103+
100104
/**
101105
* Adds a function entry to the span stack.
102106
*
@@ -134,6 +138,7 @@ export type SpanOptions = {
134138
export class TelemetrySpan<T extends MetricBase = MetricBase> {
135139
#startTime?: Date
136140
#options: SpanOptions
141+
#performance?: PerformanceTracker
137142

138143
private readonly state: Partial<T> = {}
139144
private readonly definition = definitions[this.name] ?? {
@@ -157,6 +162,7 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
157162
// do emit by default
158163
emit: options?.emit === undefined ? true : options.emit,
159164
functionId: options?.functionId,
165+
trackPerformance: PerformanceTracker.enabled(this.name, options?.trackPerformance),
160166
}
161167
}
162168

@@ -195,6 +201,10 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
195201
*/
196202
public start(): this {
197203
this.#startTime = new globals.clock.Date()
204+
if (this.#options.trackPerformance) {
205+
;(this.#performance ??= new PerformanceTracker(this.name)).start()
206+
}
207+
198208
return this
199209
}
200210

@@ -206,6 +216,19 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
206216
public stop(err?: unknown): void {
207217
const duration = this.startTime !== undefined ? globals.clock.Date.now() - this.startTime.getTime() : undefined
208218

219+
if (this.#options.trackPerformance) {
220+
// TODO add these to the global metrics, right now it just forces them in the telemetry and ignores the type
221+
// if someone enables this action
222+
const performanceMetrics = this.#performance?.stop()
223+
if (performanceMetrics) {
224+
this.record({
225+
cpuUsage: performanceMetrics.cpuUsage,
226+
heapTotal: performanceMetrics.heapTotal,
227+
functionName: this.#options.functionId?.name ?? this.name,
228+
} as any)
229+
}
230+
}
231+
209232
if (this.#options.emit) {
210233
this.emit({
211234
duration,
@@ -307,11 +330,7 @@ export class TelemetryTracer extends TelemetryBase {
307330
* All changes made to {@link attributes} (via {@link record}) during the execution are
308331
* reverted after the execution completes.
309332
*/
310-
public run<T, U extends MetricName>(
311-
name: U,
312-
fn: (span: Metric<MetricShapes[U]>) => T,
313-
options?: SpanOptions | undefined
314-
): T {
333+
public run<T, U extends MetricName>(name: U, fn: (span: Metric<MetricShapes[U]>) => T, options?: SpanOptions): T {
315334
const span = this.createSpan(name, options).start()
316335
const frame = this.switchContext(span)
317336

0 commit comments

Comments
 (0)