Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(childprocess): add new interfaces for tracking child processes #6394

Open
wants to merge 56 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
53aaeef
implement basic tracker class
Hweinstock Dec 23, 2024
d48f1b3
add option in dev menu to start processes
Hweinstock Dec 23, 2024
44fe332
add dependency to track pid usage
Hweinstock Dec 26, 2024
9d40215
warn on exceeding thresholds
Hweinstock Dec 27, 2024
bcb6423
add first test case
Hweinstock Dec 30, 2024
8975c33
expand test suite
Hweinstock Dec 30, 2024
72681fd
simplify and expand test suite
Hweinstock Dec 31, 2024
5c652b6
add more tests
Hweinstock Dec 31, 2024
6bb8c74
refactor
Hweinstock Dec 31, 2024
a9224d3
remove tracker interface
Hweinstock Dec 31, 2024
4bfff0a
refactor wording
Hweinstock Jan 3, 2025
61e6fbf
Merge branch 'master' into process/track
Hweinstock Jan 3, 2025
2df7506
use topic logger
Hweinstock Jan 3, 2025
efe336e
improve test performance by properly stopping timer
Hweinstock Jan 3, 2025
30ab5b5
fix circular dep
Hweinstock Jan 3, 2025
10537e1
add debugging messages for flaky test
Hweinstock Jan 3, 2025
46a556b
add safe wrapper around stop
Hweinstock Jan 3, 2025
3c1531f
avoid waitUntil with stubbed clock
Hweinstock Jan 3, 2025
e9b4605
remove dependency
Hweinstock Jan 3, 2025
c1975f9
avoid circular dep
Hweinstock Jan 3, 2025
59f8215
fix logging statements
Hweinstock Jan 3, 2025
39175c0
clean up tests
Hweinstock Jan 3, 2025
e04eefe
increase waiting period to avoid flakiness
Hweinstock Jan 3, 2025
abf3603
add some debugging information for flaky test
Hweinstock Jan 6, 2025
4f2eaf0
handle result and process individually and explicitly
Hweinstock Jan 6, 2025
b0976b3
remove repeat tests
Hweinstock Jan 6, 2025
ec3abbf
reword logging statement
Hweinstock Jan 6, 2025
422fa1d
Merge branch 'master' into process/track
Hweinstock Jan 6, 2025
9f24127
add dummy command
Hweinstock Jan 7, 2025
2d31675
convert tracker to singleton class
Hweinstock Jan 7, 2025
ee81478
log all usage at info level
Hweinstock Jan 7, 2025
6385594
switch command name
Hweinstock Jan 7, 2025
b3d1fd4
add testing
Hweinstock Jan 7, 2025
be8fffb
refactor tests to avoid mocking when not necessary
Hweinstock Jan 7, 2025
6572e6b
add changelog
Hweinstock Jan 7, 2025
31fd2b9
add tests for telemetry
Hweinstock Jan 7, 2025
c0bc44e
implement telemetry for exceeding thresholds
Hweinstock Jan 8, 2025
fee1fcb
merge in telemetry changes
Hweinstock Jan 8, 2025
1286ee2
add process as str to telemetry
Hweinstock Jan 8, 2025
e25df1b
remove explicit adding now that tracker is singleton
Hweinstock Jan 8, 2025
5090323
add a safety clear before tests run
Hweinstock Jan 8, 2025
7685707
regroup tests
Hweinstock Jan 8, 2025
7a702ff
refactor sub-functions
Hweinstock Jan 15, 2025
db973f5
Merge branch 'master' into process/track
Hweinstock Jan 15, 2025
2e94873
merge in upstream changes
Hweinstock Jan 15, 2025
36c8d68
merge in upstream changes
Hweinstock Jan 17, 2025
dcd48d6
merge in upstream again
Hweinstock Jan 17, 2025
7622e5c
add units to process stat keys
Hweinstock Jan 17, 2025
bf515e0
resolve circ dep
Hweinstock Jan 17, 2025
045f9f0
fix failing tests
Hweinstock Jan 17, 2025
30dd7d4
rename commands and remove outdated changelog
Hweinstock Jan 17, 2025
73b4210
update changelog
Hweinstock Jan 17, 2025
0762cb7
update telemetry
Hweinstock Jan 17, 2025
628a9d3
make it clear it is currently being logged
Hweinstock Jan 17, 2025
a427db9
Merge branch 'origin-master' into process/command
Hweinstock Jan 17, 2025
b329ede
update changelog to match
Hweinstock Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Subprocesses are now tracked and visible by \"Log All Active Subprocesses\" in command palette"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we introduce it as a more generic Show Stats or Report Performance command? Even if it only has process stuff currently.

}
5 changes: 5 additions & 0 deletions packages/amazonq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@
"title": "%AWS.command.viewLogs%",
"category": "%AWS.amazonq.title%"
},
{
"command": "aws.amazonq.logActiveProcceses",
"title": "%AWS.command.logActiveProcceses%",
"category": "%AWS.title%"
},
{
"command": "aws.amazonq.github",
"title": "%AWS.command.github%",
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
"AWS.command.submitFeedback": "Send Feedback...",
"AWS.command.downloadSchemaItemCode": "Download Code Bindings",
"AWS.command.viewLogs": "View Logs",
"AWS.command.logActiveProcceses": "Log All Active Subprocesses",
"AWS.command.cloudWatchLogs.searchLogGroup": "Search Log Group",
"AWS.command.cloudWatchLogs.tailLogGroup": "Tail Log Group",
"AWS.command.sam.newTemplate": "Create new SAM Template",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { registerCommands } from './commands'
import endpoints from '../resources/endpoints.json'
import { getLogger, maybeShowMinVscodeWarning, setupUninstallHandler } from './shared'
import { showViewLogsMessage } from './shared/utilities/messages'
import { ChildProcessTracker } from './shared/utilities/processUtils'

disableAwsSdkWarning()

Expand Down Expand Up @@ -171,6 +172,12 @@ export async function activateCommon(

await activateViewsShared(extContext.extensionContext)

context.subscriptions.push(
Commands.register(
`aws.${contextPrefix}.logActiveProcceses`,
async () => await ChildProcessTracker.instance.logAllUsage()
)
)
return extContext
}

Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/shared/telemetry/vscodeTelemetry.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
{
"types": [
{
"name": "size",
"type": "int",
"description": "A generic size for when units are clear"
},
{
"name": "childProcess",
"type": "string",
"description": "A string representation of a ChildProcess"
},
{
"name": "systemResource",
"type": "string",
"allowedValues": ["cpu", "memory"],
"description": "The type of system resource being measured"
},
{
"name": "amazonGenerateApproachLatency",
"type": "double",
Expand Down Expand Up @@ -372,6 +388,30 @@
}
]
},
{
"name": "ide_logActiveProcesses",
"description": "Emitted when user invokes logActiveProcesses command",
"metadata": [
{
"type": "size",
"required": true
}
]
},
{
"name": "ide_childProcessWarning",
"description": "Child Process warning due to high system usage",
"metadata": [
{
"type": "systemResource",
"required": true
},
{
"type": "childProcess",
"required": true
}
]
},
{
"name": "vscode_executeCommand",
"description": "Emitted whenever a registered Toolkit command is executed",
Expand Down
107 changes: 79 additions & 28 deletions packages/core/src/shared/utilities/processUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import * as proc from 'child_process' // eslint-disable-line no-restricted-imports
import * as crossSpawn from 'cross-spawn'
import * as logger from '../logger'
import { Timeout, CancellationError, waitUntil } from './timeoutUtils'
import { PollingSet } from './pollingSet'
import { getLogger } from '../logger/logger'
import { telemetry } from '../telemetry'

export interface RunParameterContext {
/** Reports an error parsed from the stdin/stdout streams. */
Expand Down Expand Up @@ -62,28 +62,35 @@ export interface ChildProcessResult {
}

export const eof = Symbol('EOF')

type pid = number
export interface ProcessStats {
memory: number
cpu: number
}
export class ChildProcessTracker {
static readonly pollingInterval: number = 10000 // Check usage every 10 seconds
static readonly thresholds: ProcessStats = {
memory: 100 * 1024 * 1024, // 100 MB
memory: 100, // 100 MB
cpu: 50,
}
static readonly logger = getLogger('childProcess')
#processByPid: Map<number, ChildProcess> = new Map<number, ChildProcess>()
#pids: PollingSet<number>
readonly logger: logger.Logger
static #instance: ChildProcessTracker

static get instance(): ChildProcessTracker {
return (this.#instance ??= new ChildProcessTracker())
}

#processByPid: Map<pid, ChildProcess> = new Map<pid, ChildProcess>()
#pids: PollingSet<pid>

public constructor() {
private constructor() {
this.logger = getLogger('childProcess')
this.#pids = new PollingSet(ChildProcessTracker.pollingInterval, () => this.monitor())
}

private cleanUp() {
const terminatedProcesses = Array.from(this.#pids.values()).filter(
(pid: number) => this.#processByPid.get(pid)?.stopped
(pid: pid) => this.#processByPid.get(pid)?.stopped
)
for (const pid of terminatedProcesses) {
this.delete(pid)
Expand All @@ -92,37 +99,57 @@ export class ChildProcessTracker {

private async monitor() {
this.cleanUp()
ChildProcessTracker.logger.debug(`Active running processes size: ${this.#pids.size}`)
this.logger.debug(`Active running processes size: ${this.#pids.size}`)

for (const pid of this.#pids.values()) {
await this.checkProcessUsage(pid)
}
}

private async checkProcessUsage(pid: number): Promise<void> {
const doesExceedThreshold = (resource: keyof ProcessStats, value: number) => {
const threshold = ChildProcessTracker.thresholds[resource]
return value > threshold
}
const warn = (resource: keyof ProcessStats, value: number) => {
telemetry.ide_childProcessWarning.run((span) => {
this.logger.warn(`Process ${this.getProcessAsStr(pid)} exceeded ${resource} threshold: ${value}`)
span.record({ systemResource: resource, childProcess: this.getProcessAsStr(pid) })
})
}

if (!this.#pids.has(pid)) {
ChildProcessTracker.logger.warn(`Missing process with id ${pid}`)
this.logger.warn(`Missing process with pid ${pid}`)
return
}
const stats = this.getUsage(pid)
if (stats) {
ChildProcessTracker.logger.debug(`Process ${pid} usage: %O`, stats)
if (stats.memory > ChildProcessTracker.thresholds.memory) {
ChildProcessTracker.logger.warn(`Process ${pid} exceeded memory threshold: ${stats.memory}`)
}
if (stats.cpu > ChildProcessTracker.thresholds.cpu) {
ChildProcessTracker.logger.warn(`Process ${pid} exceeded cpu threshold: ${stats.cpu}`)
if (!stats) {
this.logger.warn(`Failed to get process stats for process with pid ${pid}`)
return
}
for (const resource of Object.keys(ChildProcessTracker.thresholds) as (keyof ProcessStats)[]) {
if (doesExceedThreshold(resource, stats[resource])) {
warn(resource as keyof ProcessStats, stats[resource])
}
}
}

public getProcessAsStr(pid: pid): string {
try {
return this.#processByPid.get(pid)!.toString()
} catch (e) {
this.logger.warn(`Missing process with pid ${pid}`)
return `pid: ${pid}`
}
}

public add(childProcess: ChildProcess) {
const pid = childProcess.pid()
this.#processByPid.set(pid, childProcess)
this.#pids.start(pid)
}

public delete(childProcessId: number) {
public delete(childProcessId: pid) {
this.#processByPid.delete(childProcessId)
this.#pids.delete(childProcessId)
}
Expand All @@ -143,12 +170,38 @@ export class ChildProcessTracker {
this.#processByPid.clear()
}

public async getAllUsage(): Promise<Map<pid, ProcessStats>> {
return new Map(
await Promise.all(
Array.from(this.#pids).map(async (pid: pid) => {
const stats = this.getUsage(pid)
return [pid, stats] as const
})
)
)
}

public async logAllUsage(): Promise<void> {
await telemetry.ide_logActiveProcesses.run(async (span) => {
span.record({ size: this.size })
if (this.size === 0) {
this.logger.info('No Active Subprocesses')
return
}
const usage = await this.getAllUsage()
const logMsg = Array.from(usage.entries()).reduce((acc, [pid, stats]) => {
return acc + `Process ${pid}: ${stats.cpu}% cpu, ${stats.memory} MB of memory\n`
}, '')
this.logger.info(`Active Subprocesses (${this.size} active)\n${logMsg}`)
})
}

public getUsage(pid: number): ProcessStats {
try {
// isWin() leads to circular dependency.
return process.platform === 'win32' ? getWindowsUsage() : getUnixUsage()
} catch (e) {
ChildProcessTracker.logger.warn(`Failed to get process stats for ${pid}: ${e}`)
this.logger.warn(`Failed to get process stats for ${pid}: ${e}`)
return { cpu: 0, memory: 0 }
}

Expand All @@ -172,7 +225,7 @@ export class ChildProcessTracker {

return {
cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage,
memory: memoryBytes,
memory: memoryBytes / (1024 * 1024),
}
}

Expand All @@ -186,20 +239,18 @@ export class ChildProcessTracker {

return {
cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage,
memory: memoryBytes,
memory: memoryBytes / (1024 * 1024),
}
}
}
}

/**
* Convenience class to manage a child process
* To use:
* - instantiate
* - call and await run to get the results (pass or fail)
*/
export interface ProcessStats {
memory: number
cpu: number
}
export class ChildProcess {
static #runningProcesses = new ChildProcessTracker()
static #runningProcesses = ChildProcessTracker.instance
static stopTimeout = 3000
#childProcess: proc.ChildProcess | undefined
#processErrors: Error[] = []
Expand Down
Loading
Loading