Files
Tyler f580759337
Template tests / tests (push) Successful in 1m41s
Stop battery EcoQoS from starving frame capture during recording
Confirmed cause: on battery in a power-saving plan, Windows applies Power
Throttling (EcoQoS) to background work. StepForge records with its window
hidden, so the frame-capture worker renderer (plus the GPU and screen-capture
utility processes feeding it) get CPU-throttled. The throttled worker can't
sample the screen fast enough, so every click finds no fresh pre-click frame
and falls back to a slow post-click fresh shot — producing out-of-order,
dropped steps. It only reproduced on battery; plugged in (where EcoQoS stays
off even in eco mode) it worked. The capture log showed every click detected
but every one reporting "no frame qualified", with a ~4.6s stream startup.

Fixes:
- Chromium switches (disable-background-timer-throttling,
  disable-renderer-backgrounding, disable-backgrounding-occluded-windows) so
  Chromium stops de-prioritising and timer-throttling the hidden worker.
- New app/win-power.js opts the OS processes out of EcoQoS via
  SetProcessInformation(ProcessPowerThrottling, EXECUTION_SPEED off) and
  raises them to high priority. Applied to all live Electron processes when a
  session starts/resumes, and to the worker renderer the moment it is created
  (before it begins streaming). Best-effort, no-op off Windows.

The earlier mouse-hook EcoQoS opt-out already fixed click *detection*; this
fixes the frame-capture side that the same throttling was breaking.
2026-06-15 18:45:50 -05:00

93 lines
3.8 KiB
JavaScript

'use strict';
const { spawn } = require('node:child_process');
/**
* Opt a set of OS processes out of Windows Power Throttling (EcoQoS) and raise
* them to high priority, so the capture pipeline keeps running at full CPU
* speed while the laptop is on battery in a power-saving plan.
*
* Why this exists: during a recording StepForge hides its window, so Windows
* treats the frame-capture worker renderer (and the GPU / screen-capture
* utility processes feeding it) as background work and CPU-throttles them on
* DC power. Throttled, the worker can't sample frames fast enough — every
* click then finds no fresh pre-click frame and falls back to a slow
* post-click shot, which is what broke recordings on battery only.
*
* The Chromium command-line switches in main.js stop Chromium's own
* backgrounding; this goes one level lower and clears the OS EcoQoS flag via
* SetProcessInformation(ProcessPowerThrottling, EXECUTION_SPEED → off), which
* has no Node binding, so we drive it through a short-lived PowerShell.
*
* Best-effort and fire-and-forget: any failure (older Windows without the API,
* PowerShell blocked by policy, a process that already exited) is swallowed —
* the Chromium switches still apply and capture degrades gracefully.
*
* No-op on every non-Windows platform.
*
* @param {number[]} pids OS process ids to keep responsive.
*/
function keepProcessesResponsive(pids) {
if (process.platform !== 'win32') return;
// Only integers reach the script, so the interpolation below can't inject.
const list = [...new Set((pids || []).filter((p) => Number.isInteger(p) && p > 0))];
if (!list.length) return;
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class SFPower {
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_POWER_THROTTLING_STATE { public uint Version; public uint ControlMask; public uint StateMask; }
const int ProcessPowerThrottling = 4;
const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
const uint HIGH_PRIORITY_CLASS = 0x00000080;
const uint PROCESS_SET_INFORMATION = 0x0200;
const uint PROCESS_QUERY_INFORMATION = 0x0400;
[DllImport("kernel32.dll", SetLastError=true)]
static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool SetProcessInformation(IntPtr h, int cls, ref PROCESS_POWER_THROTTLING_STATE info, uint size);
[DllImport("kernel32.dll")]
static extern bool SetPriorityClass(IntPtr h, uint cls);
[DllImport("kernel32.dll")]
static extern bool CloseHandle(IntPtr h);
public static void Apply(uint pid) {
IntPtr h = OpenProcess(PROCESS_SET_INFORMATION | PROCESS_QUERY_INFORMATION, false, pid);
if (h == IntPtr.Zero) return;
try {
PROCESS_POWER_THROTTLING_STATE s = new PROCESS_POWER_THROTTLING_STATE();
s.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
s.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
s.StateMask = 0; // 0 => throttling off => opt out of EcoQoS
SetProcessInformation(h, ProcessPowerThrottling, ref s, (uint)Marshal.SizeOf(s));
SetPriorityClass(h, HIGH_PRIORITY_CLASS);
} finally { CloseHandle(h); }
}
}
'@
${list.map((p) => `[SFPower]::Apply(${p})`).join('\n')}
`;
try {
const child = spawn(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', ps],
{ stdio: 'ignore', windowsHide: true },
);
child.on('error', () => { /* PowerShell missing/blocked — best effort */ });
child.unref();
} catch {
// spawn itself failed; the Chromium switches remain in effect.
}
}
module.exports = { keepProcessesResponsive };