Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 28 additions & 1 deletion core/aws-lsp-core/src/util/processUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as os from 'os'
import * as path from 'path'
import * as sinon from 'sinon'
import { chmod } from 'fs/promises'
import { ChildProcess, ChildProcessResult, ChildProcessTracker, eof } from './processUtils'
import { ChildProcess, ChildProcessResult, ChildProcessTracker, eof, parseCimProcessStats } from './processUtils'
import { waitUntil } from './timeoutUtils'
import { TestFolder } from '../test/testFolder'
import { TestFeatures } from '@aws/language-server-runtimes/testing'
Expand Down Expand Up @@ -395,6 +395,33 @@ describe('ChildProcessTracker', function () {
})
})

describe('parseCimProcessStats', function () {
it('parses a single-object JSON result', function () {
const output = '{\r\n "PercentProcessorTime": 42,\r\n "WorkingSet": 157286400\r\n}\r\n'
assert.deepStrictEqual(parseCimProcessStats(output), { cpu: 42, memory: 157286400 })
})

it('uses the first entry of an array result', function () {
const output =
'[{"PercentProcessorTime": 7, "WorkingSet": 1048576}, {"PercentProcessorTime": 99, "WorkingSet": 2097152}]'
assert.deepStrictEqual(parseCimProcessStats(output), { cpu: 7, memory: 1048576 })
})

it('returns zero usage for empty output (process already exited)', function () {
assert.deepStrictEqual(parseCimProcessStats(''), { cpu: 0, memory: 0 })
assert.deepStrictEqual(parseCimProcessStats('\r\n'), { cpu: 0, memory: 0 })
})

it('returns zero usage when properties are missing', function () {
assert.deepStrictEqual(parseCimProcessStats('{}'), { cpu: 0, memory: 0 })
assert.deepStrictEqual(parseCimProcessStats('null'), { cpu: 0, memory: 0 })
})

it('throws on malformed output so the caller can log it', function () {
assert.throws(() => parseCimProcessStats('not json'))
})
})

async function startTestProcess(
tempFolder: TestFolder,
logger: Features['logging'],
Expand Down
66 changes: 41 additions & 25 deletions core/aws-lsp-core/src/util/processUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ export interface ProcessStats {
memory: number
cpu: number
}

/**
* Parses the JSON emitted by the `Get-CimInstance ... | ConvertTo-Json` process-stats query.
* Empty output (process already exited) yields zero usage. Exported for testing.
*/
export function parseCimProcessStats(output: string): ProcessStats {
const trimmed = output.trim()
if (!trimmed) {
return { cpu: 0, memory: 0 }
}
const parsed = JSON.parse(trimmed)
// ConvertTo-Json emits a bare object for a single match and an array for multiple.
const stats = Array.isArray(parsed) ? parsed[0] : parsed
const cpu = Number(stats?.PercentProcessorTime)
const memory = Number(stats?.WorkingSet)
return {
cpu: isNaN(cpu) ? 0 : cpu,
memory: isNaN(memory) ? 0 : memory,
}
}
export class ChildProcessTracker {
static #instance: ChildProcessTracker
static readonly pollingInterval: number = 10000 // Check usage every 10 seconds
Expand Down Expand Up @@ -120,7 +140,7 @@ export class ChildProcessTracker {
this.logging.warn(`Missing process with id ${pid}`)
return
}
const stats = this.getUsage(pid)
const stats = await this.getUsage(pid)
if (stats) {
this.logIfExceeds(pid, stats)
}
Expand Down Expand Up @@ -162,36 +182,32 @@ export class ChildProcessTracker {
this.#processByPid.clear()
}

private getUsage(pid: number): ProcessStats {
private async getUsage(pid: number): Promise<ProcessStats> {
try {
return process.platform === 'win32' ? getWindowsUsage() : getUnixUsage()
return process.platform === 'win32' ? await getWindowsUsage() : getUnixUsage()
} catch (e) {
this.logging.warn(`Failed to get process stats for ${pid}: ${e}`)
return { cpu: 0, memory: 0 }
}

function getWindowsUsage() {
const cpuOutput = proc
.execFileSync('wmic', [
'path',
'Win32_PerfFormattedData_PerfProc_Process',
'where',
`IDProcess=${pid}`,
'get',
'PercentProcessorTime',
])
.toString()
const memOutput = proc
.execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize'])
.toString()

const cpuPercentage = parseFloat(cpuOutput.split('\n')[1])
const memoryBytes = parseInt(memOutput.split('\n')[1]) * 1024

return {
cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage,
memory: memoryBytes,
}
// wmic was removed from current Windows 11 builds; query CIM through PowerShell instead.
// A single Win32_PerfFormattedData query provides both cpu percentage and working set,
// and ConvertTo-Json keeps the output locale-independent.
async function getWindowsUsage(): Promise<ProcessStats> {
const output = await new Promise<string>((resolve, reject) => {
proc.execFile(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
`Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -Filter 'IDProcess=${pid}' | Select-Object PercentProcessorTime,WorkingSet | ConvertTo-Json`,
],
{ windowsHide: true },
(error, stdout) => (error ? reject(error) : resolve(stdout))
)
})
return parseCimProcessStats(output)
}

function getUnixUsage() {
Expand Down