Skip to content

Commit bcb6423

Browse files
committed
add first test case
1 parent 9d40215 commit bcb6423

File tree

3 files changed

+76
-34
lines changed

3 files changed

+76
-34
lines changed

packages/core/src/shared/utilities/pollingSet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class PollingSet<T> extends Set<T> {
4444
this.clearTimer()
4545
}
4646
}
47-
47+
// TODO(hkobew): Overwrite the add method instead of adding seperate method. If we add item to set, timer should always start.
4848
public start(id: T): void {
4949
this.add(id)
5050
this.pollTimer = this.pollTimer ?? globals.clock.setInterval(() => this.poll(), this.interval)

packages/core/src/shared/utilities/processUtils.ts

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -62,45 +62,53 @@ export interface ChildProcessResult {
6262
signal?: string
6363
}
6464

65+
interface Tracker<T> {
66+
add(item: T): void
67+
delete(item: T): void
68+
has(item: T): boolean
69+
size(): number
70+
cleanUp(): void
71+
monitor(): void | Promise<void>
72+
}
73+
6574
export const eof = Symbol('EOF')
6675

67-
class ChildProcessTracker extends Map<number, ChildProcess> {
68-
static pollingInterval: number = 1000
76+
export class ChildProcessTracker implements Tracker<ChildProcess> {
77+
static pollingInterval: number = 10000
6978
static thresholds: { memory: number; cpu: number; time: number } = {
7079
memory: 100 * 1024 * 1024, // 100 MB
7180
cpu: 50,
7281
time: 30 * 1000, // 30 seconds
7382
}
74-
75-
#processPoller: PollingSet<number>
83+
#processByPid: Map<number, ChildProcess> = new Map<number, ChildProcess>()
84+
#pids: PollingSet<number>
7685

7786
public constructor() {
78-
super()
79-
this.#processPoller = new PollingSet(ChildProcessTracker.pollingInterval, () => this.monitorProcesses())
87+
this.#pids = new PollingSet(ChildProcessTracker.pollingInterval, () => this.monitor())
8088
getLogger().debug(`ChildProcessTracker created with polling interval: ${ChildProcessTracker.pollingInterval}`)
8189
}
8290

83-
private cleanUpProcesses() {
84-
const terminatedProcesses = Array.from(this.#processPoller.values()).filter(
85-
(pid: number) => !this.has(pid) || this.get(pid)?.stopped
91+
public cleanUp() {
92+
const terminatedProcesses = Array.from(this.#pids.values()).filter(
93+
(pid: number) => !this.#pids.has(pid) || this.#processByPid.get(pid)?.stopped
8694
)
8795
for (const pid of terminatedProcesses) {
88-
this.#processPoller.delete(pid)
96+
this.#pids.delete(pid)
8997
}
9098
}
9199

92-
public async monitorProcesses() {
93-
this.cleanUpProcesses()
94-
getLogger().debug(`Active running processes size: ${this.#processPoller.size}`)
100+
public async monitor() {
101+
this.cleanUp()
102+
getLogger().debug(`Active running processes size: ${this.#pids.size}`)
95103

96-
for (const pid of this.#processPoller.values()) {
97-
await this.monitorProcess(pid)
104+
for (const pid of this.#pids.values()) {
105+
await this.checkProcessUsage(pid)
98106
}
99107
}
100108

101-
private async monitorProcess(pid: number) {
102-
if (this.has(pid)) {
103-
const stats = await pidusage(pid)
109+
private async checkProcessUsage(pid: number): Promise<void> {
110+
if (this.#pids.has(pid)) {
111+
const stats = await this.getUsage(pid)
104112
getLogger().debug(`stats for ${pid}: %O`, stats)
105113
if (stats.memory > ChildProcessTracker.thresholds.memory) {
106114
getLogger().warn(`Process ${pid} exceeded memory threshold: ${stats.memory}`)
@@ -116,20 +124,33 @@ class ChildProcessTracker extends Map<number, ChildProcess> {
116124
}
117125
}
118126

119-
public override set(key: number, value: ChildProcess): this {
120-
this.#processPoller.start(key)
121-
super.set(key, value)
122-
return this
127+
public add(childProcess: ChildProcess) {
128+
const pid = childProcess.pid()
129+
this.#processByPid.set(pid, childProcess)
130+
this.#pids.start(pid)
131+
}
132+
133+
public delete(childProcess: ChildProcess) {
134+
const pid = childProcess.pid()
135+
this.#processByPid.delete(pid)
136+
this.#pids.delete(pid)
123137
}
124138

125-
public override delete(key: number): boolean {
126-
this.#processPoller.delete(key)
127-
return super.delete(key)
139+
public size() {
140+
return this.#pids.size
128141
}
129142

130-
public override clear(): void {
131-
this.#processPoller.clear()
132-
super.clear()
143+
public has(childProcess: ChildProcess) {
144+
return this.#pids.has(childProcess.pid())
145+
}
146+
147+
private async getUsage(pid: number): Promise<{ memory: number; cpu: number; elapsed: number }> {
148+
const stats = await pidusage(pid)
149+
return {
150+
memory: stats.memory,
151+
cpu: stats.cpu,
152+
elapsed: stats.elapsed,
153+
}
133154
}
134155
}
135156

@@ -140,7 +161,7 @@ class ChildProcessTracker extends Map<number, ChildProcess> {
140161
* - call and await run to get the results (pass or fail)
141162
*/
142163
export class ChildProcess {
143-
static #runningProcesses: ChildProcessTracker = new ChildProcessTracker()
164+
static #runningProcesses: Tracker<ChildProcess> = new ChildProcessTracker()
144165
#childProcess: proc.ChildProcess | undefined
145166
#processErrors: Error[] = []
146167
#processResult: ChildProcessResult | undefined
@@ -213,7 +234,7 @@ export class ChildProcess {
213234
const args = this.#args.concat(options.extraArgs ?? [])
214235

215236
const debugDetail = this.#log.logLevelEnabled('debug')
216-
? ` (running processes: ${ChildProcess.#runningProcesses.size})`
237+
? ` (running processes: ${ChildProcess.#runningProcesses.size()})`
217238
: ''
218239
this.#log.info(`Command: ${this.toString(options.logging === 'noparams')}${debugDetail}`)
219240

@@ -381,7 +402,7 @@ export class ChildProcess {
381402
if (pid === undefined) {
382403
return
383404
}
384-
ChildProcess.#runningProcesses.set(pid, this)
405+
ChildProcess.#runningProcesses.add(this)
385406

386407
const timeoutListener = options?.timeout?.token.onCancellationRequested(({ agent }) => {
387408
const message = agent === 'user' ? 'Cancelled: ' : 'Timed out: '
@@ -391,7 +412,7 @@ export class ChildProcess {
391412

392413
const dispose = () => {
393414
timeoutListener?.dispose()
394-
ChildProcess.#runningProcesses.delete(pid)
415+
ChildProcess.#runningProcesses.delete(this)
395416
}
396417

397418
process.on('exit', dispose)

packages/core/src/test/shared/utilities/processUtils.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import assert from 'assert'
77
import * as os from 'os'
88
import * as path from 'path'
99
import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../../shared/filesystemUtilities'
10-
import { ChildProcess, eof } from '../../../shared/utilities/processUtils'
10+
import { ChildProcess, ChildProcessTracker, eof } from '../../../shared/utilities/processUtils'
1111
import { sleep } from '../../../shared/utilities/timeoutUtils'
1212
import { Timeout, waitUntil } from '../../../shared/utilities/timeoutUtils'
1313
import { fs } from '../../../shared'
14+
import * as FakeTimers from '@sinonjs/fake-timers'
15+
import { installFakeClock } from '../../testUtil'
1416

1517
describe('ChildProcess', async function () {
1618
let tempFolder: string
@@ -350,3 +352,22 @@ describe('ChildProcess', async function () {
350352
await writeShellFile(filename, file)
351353
}
352354
})
355+
356+
describe('ChildProcessTracker', function () {
357+
let tracker: ChildProcessTracker
358+
let clock: FakeTimers.InstalledClock
359+
before(function () {
360+
clock = installFakeClock()
361+
tracker = new ChildProcessTracker()
362+
})
363+
364+
it('removes stopped processes every X seconds', async function () {
365+
const childProcess = new ChildProcess('echo', ['hi'])
366+
await childProcess.run()
367+
tracker.add(childProcess)
368+
childProcess.stop()
369+
assert.strictEqual(tracker.has(childProcess), true)
370+
await clock.tickAsync(ChildProcessTracker.pollingInterval)
371+
assert.strictEqual(tracker.has(childProcess), false)
372+
})
373+
})

0 commit comments

Comments
 (0)