mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-21 22:50:13 -06:00
264 lines
6.6 KiB
TypeScript
264 lines
6.6 KiB
TypeScript
import Debug from 'debug'
|
|
import la from 'lazy-ass'
|
|
import _ from 'lodash'
|
|
import si from 'systeminformation'
|
|
import { concatStream } from '@packages/network'
|
|
|
|
const browsers = require('../browsers')
|
|
const plugins = require('../plugins')
|
|
|
|
type Group = 'browser' | 'cypress' | 'launchpad' | 'plugin' | 'ffmpeg' | 'electron-shared' | 'other'
|
|
type Process = si.Systeminformation.ProcessesProcessData & {
|
|
group?: Group
|
|
}
|
|
|
|
const debug = Debug('cypress:server:util:process_profiler')
|
|
const debugVerbose = Debug('cypress-verbose:server:util:process_profiler')
|
|
|
|
const interval = Number(process.env.CYPRESS_PROCESS_PROFILER_INTERVAL) || 10000
|
|
let started = false
|
|
|
|
let groupsOverTime = {}
|
|
|
|
export const _reset = () => {
|
|
groupsOverTime = {}
|
|
}
|
|
|
|
const formatPidDisplay = (groupedProcesses) => {
|
|
const pids = _.map(groupedProcesses, 'pid')
|
|
const maxArrayLength = 6
|
|
|
|
let display = pids.slice(0, maxArrayLength).join(', ')
|
|
|
|
if (pids.length > maxArrayLength) {
|
|
display += ` ... ${pids.length - maxArrayLength} more items`
|
|
}
|
|
|
|
return display
|
|
}
|
|
|
|
export const _groupCyProcesses = ({ list }: si.Systeminformation.ProcessesData) => {
|
|
const cyProcesses: Process[] = []
|
|
const thisProcess: Process = _.find(list, { pid: process.pid })!
|
|
|
|
la(thisProcess, 'expected to find current pid in process list', process.pid)
|
|
|
|
const isParentProcessInGroup = (proc: Process, group: Group) => {
|
|
return _.chain(cyProcesses).filter({ group }).map('pid').includes(proc.parentPid).value()
|
|
}
|
|
|
|
// is this a browser process launched to run Cypress tests?
|
|
const isBrowserProcess = (proc: Process): boolean => {
|
|
const instance = browsers.getBrowserInstance()
|
|
// electron will return a list of pids, since it's not a hierarchy
|
|
const pids: number[] = instance?.allPids ? instance.allPids : [instance?.pid]
|
|
|
|
return (pids.includes(proc.pid))
|
|
|| isParentProcessInGroup(proc, 'browser')
|
|
}
|
|
|
|
const isPluginProcess = (proc: Process): boolean => {
|
|
return proc.pid === plugins.getPluginPid()
|
|
|| isParentProcessInGroup(proc, 'plugin')
|
|
}
|
|
|
|
// is this the renderer for the launchpad?
|
|
const isDesktopGuiProcess = (proc: Process): boolean => {
|
|
return proc.params?.includes('--type=renderer')
|
|
&& !isBrowserProcess(proc)
|
|
}
|
|
|
|
// these processes may be shared between the AUT and launchpad.
|
|
// rather than treat them as part of the `browser` in `run` mode and have
|
|
// their usage in `open` mode be ambiguous, just put them in their own group
|
|
const isElectronSharedProcess = (proc: Process): boolean => {
|
|
const isType = (type) => {
|
|
return proc.params?.includes(`--type=${type}`)
|
|
}
|
|
|
|
return isType('broker')
|
|
|| isType('gpu-process')
|
|
|| isType('utility')
|
|
|| isType('zygote')
|
|
}
|
|
|
|
const isFfmpegProcess = (proc: Process): boolean => {
|
|
return proc.parentPid === thisProcess.pid
|
|
&& /ffmpeg/i.test(proc.name)
|
|
}
|
|
|
|
const getProcessGroup = (proc: Process): Group => {
|
|
if (proc === thisProcess) {
|
|
return 'cypress'
|
|
}
|
|
|
|
if (isBrowserProcess(proc)) {
|
|
return 'browser'
|
|
}
|
|
|
|
if (isPluginProcess(proc)) {
|
|
return 'plugin'
|
|
}
|
|
|
|
if (isDesktopGuiProcess(proc)) {
|
|
return 'launchpad'
|
|
}
|
|
|
|
if (isFfmpegProcess(proc)) {
|
|
return 'ffmpeg'
|
|
}
|
|
|
|
if (isElectronSharedProcess(proc)) {
|
|
return 'electron-shared'
|
|
}
|
|
|
|
return 'other'
|
|
}
|
|
|
|
const classifyProcess = (proc: Process) => {
|
|
const classify = (group: Group) => {
|
|
proc.group = group
|
|
cyProcesses.push(proc)
|
|
|
|
// queue all children
|
|
_.chain(list)
|
|
.filter({ parentPid: proc.pid })
|
|
.map(classifyProcess)
|
|
.value()
|
|
}
|
|
|
|
classify(getProcessGroup(proc))
|
|
}
|
|
|
|
classifyProcess(thisProcess)
|
|
|
|
return cyProcesses
|
|
}
|
|
|
|
export const _renameBrowserGroup = (processes: Process[]) => {
|
|
const instance = browsers.getBrowserInstance()
|
|
const displayName = _.get(instance, 'browser.displayName')
|
|
|
|
processes.forEach((proc) => {
|
|
if (!displayName) {
|
|
return
|
|
}
|
|
|
|
if (proc.group === 'browser') {
|
|
proc.group = displayName
|
|
}
|
|
})
|
|
|
|
return processes
|
|
}
|
|
|
|
export const _aggregateGroups = (processes: Process[]) => {
|
|
debugVerbose('all Cypress-launched processes: %s', require('util').inspect(processes))
|
|
|
|
const groupTotals = _.chain(processes)
|
|
.groupBy('group')
|
|
.mapValues((groupedProcesses, group) => {
|
|
return {
|
|
group,
|
|
processCount: groupedProcesses.length,
|
|
pids: formatPidDisplay(groupedProcesses),
|
|
cpuPercent: _.sumBy(groupedProcesses, 'cpu'),
|
|
memRssMb: _.sumBy(groupedProcesses, 'memRss') / 1024,
|
|
}
|
|
})
|
|
.values()
|
|
.sortBy('memRssMb')
|
|
.reverse()
|
|
.value()
|
|
|
|
groupTotals.push(_.reduce(groupTotals, (acc, val) => {
|
|
acc.processCount += val.processCount
|
|
acc.cpuPercent += val.cpuPercent
|
|
acc.memRssMb += val.memRssMb
|
|
|
|
return acc
|
|
}, { group: 'TOTAL', processCount: 0, pids: '-', cpuPercent: 0, memRssMb: 0 }))
|
|
|
|
groupTotals.forEach((total) => {
|
|
if (!groupsOverTime[total.group]) {
|
|
groupsOverTime[total.group] = []
|
|
}
|
|
|
|
const measurements = groupsOverTime[total.group]
|
|
|
|
measurements.push(total)
|
|
|
|
_.merge(total, {
|
|
meanCpuPercent: _.meanBy(measurements, 'cpuPercent'),
|
|
meanMemRssMb: _.meanBy(measurements, 'memRssMb'),
|
|
maxMemRssMb: _.max(_.map(measurements, _.property('memRssMb'))),
|
|
})
|
|
|
|
_.forEach(total, (v, k) => {
|
|
// round all numbers to 100ths precision
|
|
if (_.isNumber(v)) {
|
|
total[k] = _.round(v, 2)
|
|
}
|
|
})
|
|
})
|
|
|
|
return groupTotals
|
|
}
|
|
|
|
export const _printGroupedProcesses = (groupTotals) => {
|
|
const consoleBuffer = concatStream((buf) => {
|
|
// get rid of trailing newline
|
|
debug(String(buf).trim())
|
|
})
|
|
|
|
// eslint-disable-next-line no-console
|
|
const buffedConsole = new console.Console(consoleBuffer)
|
|
|
|
buffedConsole.log('current & mean memory and CPU usage by process group:')
|
|
buffedConsole.table(groupTotals, [
|
|
'group',
|
|
'processCount',
|
|
'pids',
|
|
'cpuPercent',
|
|
'meanCpuPercent',
|
|
'memRssMb',
|
|
'meanMemRssMb',
|
|
'maxMemRssMb',
|
|
])
|
|
|
|
consoleBuffer.end()
|
|
}
|
|
|
|
function _checkProcesses () {
|
|
return si.processes()
|
|
.then(_groupCyProcesses)
|
|
.then(_renameBrowserGroup)
|
|
.then(_aggregateGroups)
|
|
.then(_printGroupedProcesses)
|
|
.then(_scheduleProcessCheck)
|
|
.catch((err) => {
|
|
debug('error running process profiler: %o', err)
|
|
})
|
|
}
|
|
|
|
function _scheduleProcessCheck () {
|
|
// not setinterval, since checkProcesses is asynchronous
|
|
setTimeout(_checkProcesses, interval)
|
|
}
|
|
|
|
export function start () {
|
|
if (!debug.enabled && !debugVerbose.enabled) {
|
|
debug('process profiler not enabled')
|
|
|
|
return
|
|
}
|
|
|
|
if (started) {
|
|
return
|
|
}
|
|
|
|
_checkProcesses().catch(() => {})
|
|
|
|
started = true
|
|
}
|