# Measures what dictation apps cost on Windows: memory (Task Manager's # "Memory" column, i.e. private working set) and CPU, summed over each app's # process and all its child processes (WebView2 or Electron helpers), sampled # over time. The Windows twin of resources.swift; run the same scenarios. # # powershell -ExecutionPolicy Bypass -File dev\bench\resources.ps1 -App vokalu,"Wispr Flow" -Seconds 120 -Label idle # # -App takes process names without .exe (as in Task Manager's Details tab). # CPU is in % of one core, like the Mac version. Writes a CSV and a summary to # dev\bench\out\ (git-ignored). param( [Parameter(Mandatory)][string[]]$App, [double]$Seconds = 60, [double]$Interval = 1, [string]$Label = "run" ) # With -File, PowerShell passes "vokalu,Wispr Flow" as one string; split it. $App = @($App | ForEach-Object { $_ -split "," } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) function Get-Tree([string]$name, $all = (Get-CimInstance Win32_Process)) { $roots = $all | Where-Object { $_.Name -ieq "$name.exe" } if (-not $roots) { return @() } $ids = [System.Collections.Generic.HashSet[uint32]]::new() foreach ($r in $roots) { [void]$ids.Add($r.ProcessId) } # Children of children (WebView2 and Electron start their own helpers). do { $added = $false foreach ($p in $all) { if ($ids.Contains($p.ParentProcessId) -and $ids.Add($p.ProcessId)) { $added = $true } } } while ($added) return $all | Where-Object { $ids.Contains($_.ProcessId) } } function Get-PrivateWorkingSet { $map = @{} foreach ($p in Get-CimInstance Win32_PerfRawData_PerfProc_Process) { $map[[uint32]$p.IDProcess] = [uint64]$p.WorkingSetPrivate } return $map } foreach ($a in $App) { if (-not (Get-Tree $a)) { Write-Error "$a isn't running. Open it first."; exit 1 } } $outDir = Join-Path $PSScriptRoot "out" New-Item -ItemType Directory -Force -Path $outDir | Out-Null $stamp = (Get-Date -Format "yyyy-MM-ddTHH-mm-ss") $csv = [System.Collections.Generic.List[string]]::new() $csv.Add("time_s,app,processes,memory_mb,cpu_percent") $cores = [Environment]::ProcessorCount Write-Host "Measuring $($App -join ' and ') for $Seconds s ($Label)..." $last = @{}; $samples = @{}; $peak = @{}; $peakBreakdown = @{} foreach ($a in $App) { $samples[$a] = [System.Collections.Generic.List[object]]::new(); $peak[$a] = 0 } $start = Get-Date; $t0 = $start # Listing processes takes about half a second, so the process trees are # refreshed every 5 s rather than every sample (new helpers are picked up then). $trees = @{}; $treesAt = [datetime]::MinValue while ($true) { $ws = Get-PrivateWorkingSet $running = @{}; foreach ($p in Get-Process) { $running[[uint32]$p.Id] = $p } if (((Get-Date) - $treesAt).TotalSeconds -ge 5) { $procs = Get-CimInstance Win32_Process foreach ($a in $App) { $trees[$a] = Get-Tree $a $procs } $treesAt = Get-Date } foreach ($a in $App) { $tree = $trees[$a] $mem = [uint64]0; $cpuDelta = 0.0; $now = @{}; $breakdown = @() foreach ($p in $tree) { $proc = $running[[uint32]$p.ProcessId] if (-not $proc) { continue } $m = if ($ws.ContainsKey([uint32]$p.ProcessId)) { $ws[[uint32]$p.ProcessId] } else { [uint64]$proc.WorkingSet64 } $mem += $m $cpu = $proc.TotalProcessorTime.TotalMilliseconds $now[$p.ProcessId] = $cpu $breakdown += [pscustomobject]@{ Name = $p.Name; Bytes = $m } if ($last.ContainsKey($a) -and $last[$a].ContainsKey($p.ProcessId)) { $cpuDelta += [math]::Max(0, $cpu - $last[$a][$p.ProcessId]) } } $elapsedMs = ((Get-Date) - $t0).TotalMilliseconds if ($last.ContainsKey($a)) { $cpuPct = $cpuDelta / $elapsedMs * 100 $samples[$a].Add([pscustomobject]@{ Mem = $mem; Cpu = $cpuPct; Count = @($tree).Count }) $csv.Add(("{0:F1},{1},{2},{3:F1},{4:F2}" -f ((Get-Date) - $start).TotalSeconds, $a, @($tree).Count, ($mem / 1MB), $cpuPct)) } if ($mem -gt $peak[$a]) { $peak[$a] = $mem; $peakBreakdown[$a] = $breakdown | Sort-Object Bytes -Descending } $last[$a] = $now } $t0 = Get-Date if (((Get-Date) - $start).TotalSeconds -ge $Seconds) { break } Start-Sleep -Milliseconds ([int]($Interval * 1000)) } $os = (Get-CimInstance Win32_OperatingSystem).Caption $report = "Resource use, $Label, $Seconds s, $env:COMPUTERNAME, $os ($cores logical cores)`n`n" foreach ($a in $App) { $s = $samples[$a] if ($s.Count -eq 0) { continue } $avgMem = ($s | Measure-Object Mem -Average).Average $avgCpu = ($s | Measure-Object Cpu -Average).Average $maxCpu = ($s | Measure-Object Cpu -Maximum).Maximum $exe = (Get-Process -Name $a -ErrorAction SilentlyContinue | Select-Object -First 1).Path $disk = if ($exe) { (Get-ChildItem (Split-Path $exe) -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum } else { 0 } $report += "$a`n" $report += (" memory: average {0:F0} MB, peak {1:F0} MB`n" -f ($avgMem / 1MB), ($peak[$a] / 1MB)) $report += (" CPU: average {0:F1}%, peak {1:F1}% (of one core)`n" -f $avgCpu, $maxCpu) $report += " processes: $(($s | Measure-Object Count -Maximum).Maximum)`n" $report += (" on disk: {0:F0} MB (install folder)`n" -f ($disk / 1MB)) $report += " at peak: " + (($peakBreakdown[$a] | Select-Object -First 6 | ForEach-Object { "{0} {1:F0} MB" -f $_.Name, ($_.Bytes / 1MB) }) -join ", ") + "`n`n" } Write-Host $report $base = Join-Path $outDir "$stamp-$Label" $csv | Set-Content -Encoding UTF8 "$base.csv" $report | Set-Content -Encoding UTF8 "$base.txt" Write-Host "Saved: $base.txt and .csv"