Skip to content

Utilization Monitor: TypeScript reimplementation #225

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

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
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
8 changes: 8 additions & 0 deletions WebUI/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { updateIntelWorkflows } from './subprocesses/updateIntelWorkflows.ts'
import getPort, { portNumbers } from 'get-port'
import { getMediaDir } from './util.ts'

import { runGpuid, runPowerShellScript, setWindow } from './subprocesses/collectorService'

// }
// The built directory structure
//
Expand Down Expand Up @@ -172,6 +174,12 @@ async function createWindow() {
if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' }
})

// Start metrics collection service
setWindow(win)
runGpuid()
runPowerShellScript()

return win
}

Expand Down
5 changes: 5 additions & 0 deletions WebUI/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('serviceSetUpProgress', (_event, value) => callback(value)),
onServiceInfoUpdate: (callback: (service: ApiServiceInformation) => void) =>
ipcRenderer.on('serviceInfoUpdate', (_event, value) => callback(value)),
onMetrics: (
callback: (
metrics: { [key: string]: number },
gpuInfo: { adapter: string, luid: string, sharedMemory: string, dedicatedMemory: string }[]
) => void) => ipcRenderer.on('metrics', (_event, metrics, gpuInfo) => callback(metrics, gpuInfo)),
})
135 changes: 135 additions & 0 deletions WebUI/electron/subprocesses/collectorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { spawn } from 'child_process'
import path from 'path'

import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const collectorPath = path.join(__dirname, '../../public/tools/collector.ps1')
const gpuidPath = path.join(__dirname, '../../public/tools/gpuid.exe')

let gpuInfo: { adapter: string; luid: string; sharedMemory: string; dedicatedMemory: string }[] = []

export function runGpuid() {
console.log('Running gpuid...')
const ps = spawn(gpuidPath)

ps.stdout.on('data', (data) => {
const output = data.toString()
processGPUIdOutput(output)
})

ps.stderr.on('data', (data) => {
console.error('Error:', data.toString())
})

ps.on('close', (code) => {
console.log(`GPUId tool exited with code ${code}`)
})
}

export function runPowerShellScript() {
console.log('Running PowerShell script...')
const ps = spawn('powershell.exe', [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-File',
collectorPath,
])

ps.stdout.on('data', (data) => {
const output = data.toString()
processOutput(output)
})

ps.stderr.on('data', (data) => {
console.error('Error:', data.toString())
})

ps.on('close', (code) => {
console.log(`PowerShell script exited with code ${code}`)
runPowerShellScript() // Restart the process
})
}

export function processGPUIdOutput(output: string) {
const lines = output.split('\n')
gpuInfo = []
let currentAdapter: {
adapter: string
luid: string
sharedMemory: string
dedicatedMemory: string
} | null = null

lines.forEach((line) => {
if (line.startsWith('Found adapter:')) {
if (currentAdapter) {
gpuInfo.push(currentAdapter)
}
currentAdapter = {
adapter: line.replace('Found adapter: ', '').trim(),
luid: '',
sharedMemory: '',
dedicatedMemory: '',
}
} else if (currentAdapter) {
const [key, value] = line.split(':')
switch (key.trim()) {
case 'Adapter LUID':
currentAdapter.luid = value.trim().split(' ')[0]
break
case 'Adapter Shared Memory':
currentAdapter.sharedMemory = value.trim().split(' ')[0]
break
case 'Adapter Dedicated Memory':
currentAdapter.dedicatedMemory = value.trim().split(' ')[0]
break
}
}
})

if (currentAdapter) {
gpuInfo.push(currentAdapter)
}

// Log the GPU info
console.log('GPU Info:', gpuInfo)
}

import { BrowserWindow } from 'electron'
let win: BrowserWindow | null = null

export function setWindow(window: BrowserWindow) {
win = window
}

function processOutput(output: string) {
// Split output by new line
const lines = output.split('\n')

// Process each line
lines.forEach((line) => {
// Initialize metrics object
let metrics: { [key: string]: number } = {}

Check failure on line 113 in WebUI/electron/subprocesses/collectorService.ts

View workflow job for this annotation

GitHub Actions / lint-ts

'metrics' is never reassigned. Use 'const' instead

// Split line by space
const segments = line.split(' ')

// Check that the first segment is the "metrics" keyword
if (segments[0] !== 'metrics') {
return
}

// Process each metric
segments[1].split(',').forEach((metric) => {
const [key, value] = metric.split('=')
metrics[key] = parseFloat(value)
})

// Add the timestamp
metrics['epoch'] = parseInt(segments[2], 10)

// Send the metrics and GPU info to the renderer
win?.webContents.send('metrics', { metrics, gpuInfo })
})
}
31 changes: 31 additions & 0 deletions WebUI/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions WebUI/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"unplugin-auto-import": "^0.18.6",
"uuid": "^11.0.5",
"vue": "^3.5.12",
"vue-chartjs": "^5.3.2",
"zod": "^3.24.1"
},
"devDependencies": {
Expand Down
159 changes: 159 additions & 0 deletions WebUI/public/tools/collector.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Constants

# Sample Interval is the time in seconds between each sample
$SampleInterval = 1

# Max Number of Samples is the maximum number of samples to collect
$MaxNumberOfSamples = 60

# Dictionary of GPUs
$gpus = @{}

# Get GPU Adapter Memory and GPU Engine Counter Sets
$gpuAdapterMemory = Get-counter -ListSet "GPU Adapter Memory"
$gpuEngines = Get-counter -ListSet "GPU Engine"

# Populate GPUs Dictionary
$Pattern = "\((.*?)\)"
foreach ($Match in [regex]::Matches($gpuAdapterMemory.PathsWithInstances, $Pattern)) {

# Extract GPU Id
$id = $Match.Groups[1].Value

# Initialize GPU Memory and Engine Types
$gpus[$id] = @{
"MemoryTypes" = [System.Collections.Generic.List[string]]::new()
"EngineTypes" = [System.Collections.Generic.List[string]]::new()
}

# Set Memory Types
$gpus[$id]["MemoryTypes"].Add("Shared Usage") | Out-Null
$gpus[$id]["MemoryTypes"].Add("Dedicated Usage") | Out-Null
$gpus[$id]["MemoryTypes"].Add("Total Committed") | Out-Null

# Set Engine Types
$_gpuEngines = $gpuEngines.PathsWithInstances | Where-Object {$_ -match $id}
$Pattern = "_engtype_(.*?)\)"
foreach ($Match in [regex]::Matches($_gpuEngines, $Pattern)) {
if ($Match.Groups[1].Value -ne "") {
if (-not $gpus[$id]["EngineTypes"].Contains($Match.Groups[1].Value)) {
$gpus[$id]["EngineTypes"].Add($Match.Groups[1].Value) | Out-Null
}
}
}
}

# Initialize Counter Set
$CounterSet = [System.Collections.Generic.List[string]]::new()

# Add Processor Utilization Counters
$CounterSet.Add("\Processor Information(_Total)\% Processor Time")

# Add Memory Utilization Counters
$CounterSet.Add("\Memory\% Committed Bytes In Use")

# Add GPU Adapter Memory and GPU Engine Counters
foreach ($id in $gpus.Keys) {

# Add GPU Adapter Memory Counters
foreach ($memoryType in $gpus[$id]["MemoryTypes"]) {
$CounterSet.Add("\GPU Adapter Memory(${id})\$memoryType")
}

# Add GPU Engine Counters
foreach ($engineType in $gpus[$id]["EngineTypes"]) {
$CounterSet.Add("\GPU Engine(*_${id}_*_engtype_${engineType})\Utilization Percentage")
}
}

# Debug Counter Set
foreach ($counter in $CounterSet) {
Write-Debug $counter
}

# Query Counters
Get-Counter `
-Counter $CounterSet `
-MaxSamples $MaxNumberOfSamples `
-SampleInterval $SampleInterval `
-ErrorAction Stop | Foreach-Object {

# Initialize Metrics Dictionary
$metrics = @{}

# Set Timestamp
$metrics["timestamp"] = [math]::Round(
(Get-Date $_.Timestamp).ToUniversalTime().Subtract(
(Get-Date "1970-01-01T00:00:00Z")
).TotalSeconds
)

# Set Processor Utilization
$metrics["cpu-utilization"] = [math]::Round(
($_.CounterSamples |
Where-Object -Property Path -Match ".*processor information.*"
).CookedValue,
2
)

# Set Memory Utilization
$metrics["memory-utilization"] = [math]::Round(
($_.CounterSamples |
Where-Object -Property Path -Match ".*committed bytes in use.*"
).CookedValue,
2
)

# Set GPU Adapter Memory and GPU Engine Utilization
foreach ($id in $gpus.Keys) {

# Filter Counter Samples
$counterSamples = $_.CounterSamples | Where-Object -Property Path -Match ".*${id}.*"

# Set Memory Utilization
foreach ($memoryType in $gpus[$id]["MemoryTypes"]) {

# Gather value
$cookedValue = ($counterSamples |
Where-Object -Property Path -Match ".*$memoryType.*"
).CookedValue

# Normalize name
$normalizedMemoryType = ($memoryType -replace " ", "-").ToLower()

# Save value
$metrics["gpu-${id}-memory-${normalizedMemoryType}"] = [math]::Round($cookedValue / 1GB, 2)
}

# Set Engine Utilization
foreach ($engineType in $gpus[$id]["EngineTypes"]) {

# Gather values
$cookedValue = ($counterSamples |
Where-Object -Property Path -Match ".*engtype_${engineType}"
).CookedValue

# Sum values
$sum = ($cookedValue | Measure-Object -Sum).Sum

# Normalize name
$normalizedEngineType = ($engineType -replace " ", "-").ToLower()

# Save sum
$metrics["gpu-${id}-engine-${normalizedEngineType}"] = [math]::Round($sum, 2)
}
}

# Prepare Reporting Data
$output = "metrics "
$metrics.GetEnumerator() | Sort-Object Key | ForEach-Object {
if ($_.Key -ne "timestamp") {
$output += "$($_.Key)=$($_.Value),"
}
}
$output = $output.TrimEnd(",")
$output += " $($metrics["timestamp"])"

# Report Data
Write-Output $output
}
Binary file added WebUI/public/tools/gpuid.exe
Binary file not shown.
Loading
Loading