Files
Electron-Websocket-Fix/BUILD-GUIDE.md
T
bigjakk e5b411cee5 Initial release: patched Electron builds fixing WebSocket starvation
Fixes a Chromium regression where continuous mouse input (e.g., shooting
in FPS games) starves WebSocket/Worker message dispatch when
--disable-frame-rate-limit is active.

Includes:
- Patch diff for main_thread_scheduler_impl.cc
- Automated CDP stress test for verification
- Full build-from-source guide

Pre-built binaries available in Releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:12:39 -08:00

25 KiB

Building Patched Electron: WebSocket/Input Priority Fix

This guide documents how to build a patched Electron binary that fixes a Chromium regression where continuous mouse input (e.g., shooting in an FPS game) starves WebSocket and Worker message dispatch when --disable-frame-rate-limit is active.

Problem

When an Electron app uses --disable-frame-rate-limit and --disable-gpu-vsync (common in competitive gaming), continuous mouse input with left-click held down causes WebSocket messages to freeze for 100-300ms+ at a time. This is because Chromium's Blink main thread scheduler gives input and compositor tasks the highest priority, and with no frame rate limit, back-to-back BeginFrame tasks create a tight loop that permanently starves normal-priority tasks (WebSocket onmessage, Worker postMessage).

Root Cause

Three factors combine:

  1. Input tasks at kHighestPriority -- In ComputePriority(), input tasks get priority level 1 (highest). WebSocket/Worker tasks get kNormalPriority (level 7).

  2. No cross-priority anti-starvation -- task_queue_selector.cc simply picks from the highest active priority queue. It only prevents starvation within the same priority level, not across levels.

  3. Compositor priority boost during input -- When mouse is held + moving (UseCase::kMainThreadCustomInputHandling), the compositor queue gets boosted to kHighestPriority. With --disable-frame-rate-limit, the BackToBackBeginFrameSource posts SEND_BEGIN_MAIN_FRAME at zero delay, creating an infinite loop of highest-priority tasks.

The Fix

Two changes in main_thread_scheduler_impl.cc:

  1. Lower input task priority from kHighestPriority to kNormalPriority
  2. Cap compositor priority to kNormalPriority

Test results (12-second automated stress test with continuous mouse input):

Build p99 latency max latency Messages >50ms
Unpatched ~97ms ~308ms 8.6%
Patched ~34ms ~38ms 0%

Prerequisites

  • Windows 11 (10 may work, untested)
  • 250GB+ free disk space (source is ~30GB, build output ~41GB)
  • 16GB+ RAM (64GB recommended)
  • Visual Studio 2022 Build Tools with:
    • "Desktop development with C++" workload
    • C++ ATL for latest build tools
    • Windows 11 SDK (10.0.26100.0)
  • Git with long path support
  • Node.js LTS (v20+)
  • Python 3.11+

Step 0: Environment Setup

Install depot_tools

cd C:\
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

Install Visual Studio 2022 Build Tools

Download from https://visualstudio.microsoft.com/downloads/ or use winget:

winget install "Microsoft.VisualStudio.2022.BuildTools" --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools;includeRecommended --add Microsoft.VisualStudio.Component.VC.ATL --add Microsoft.VisualStudio.Component.Windows11SDK.26100"

If you already have Build Tools but need the SDK:

winget install "Microsoft.WindowsSDK.10.0.26100"

Install Python 3.12

winget install Python.Python.3.12

After installing, disable the Windows Store Python aliases:

  • Settings > Apps > Advanced app settings > App execution aliases
  • Turn OFF "python.exe" and "python3.exe" App Installers

Or manually rename the stubs:

mv "$LOCALAPPDATA/Microsoft/WindowsApps/python.exe" "$LOCALAPPDATA/Microsoft/WindowsApps/python.exe.bak"
mv "$LOCALAPPDATA/Microsoft/WindowsApps/python3.exe" "$LOCALAPPDATA/Microsoft/WindowsApps/python3.exe.bak"

Also create a python3.exe copy if it doesn't exist:

cp "$LOCALAPPDATA/Programs/Python/Python312/python.exe" "$LOCALAPPDATA/Programs/Python/Python312/python3.exe"

Configure Git

git config --global core.longpaths true
git config --global core.autocrlf false
git config --global core.filemode false
git config --global core.fscache true
git config --global core.preloadindex true
git config --global branch.autosetuprebase always

Set Environment Variables

Add to your shell profile or set as system environment variables:

export DEPOT_TOOLS_WIN_TOOLCHAIN=0
export GIT_CACHE_PATH="C:\\git_cache"
export vs2022_install="C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools"

Configure PATH

Ensure this order (earlier = higher priority):

C:\Users\<you>\AppData\Local\Programs\Python\Python312
C:\Users\<you>\AppData\Local\Programs\Python\Python312\Scripts
C:\depot_tools
C:\Program Files\nodejs

Add exclusions for build directories to avoid massive slowdown:

Add-MpPreference -ExclusionPath "C:\electron"
Add-MpPreference -ExclusionPath "C:\depot_tools"

Install @electron/build-tools

npm install -g @electron/build-tools

Create git cache directory

mkdir C:\git_cache

Step 1: Initialize and Sync Source

Initialize Electron source

mkdir C:\electron && cd C:\electron
e init --root=C:\electron krunker-patch --import release

This creates the directory structure and .gclient file.

Sync source (downloads ~30GB)

e sync

This takes 1-3 hours depending on network speed. It downloads Chromium, Node.js, and all dependencies.


Step 2: Apply the Patch

The file to modify is:

C:\electron\electron\src\third_party\blink\renderer\platform\scheduler\main_thread\main_thread_scheduler_impl.cc

Patch 1: Input Priority (in ComputePriority() function)

Find this code (around line 2757):

case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput:
  return TaskPriority::kHighestPriority;

Replace with:

case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput:
  // Lowered from kHighestPriority to kNormalPriority to prevent input
  // tasks from starving WebSocket/Worker message dispatch when
  // --disable-frame-rate-limit is active. Without cross-priority
  // anti-starvation in the task queue selector, ANY priority above
  // kNormalPriority causes starvation during continuous mouse input.
  // Testing shows kNormalPriority actually improves both frame rate
  // and input throughput vs kHighestPriority, because it prevents the
  // input->compositor priority cascade from monopolizing the thread.
  return TaskPriority::kNormalPriority;

Patch 2: Compositor Priority Cap (in ComputeCompositorPriority() function)

Find the ComputeCompositorPriority() function (around line 2823). Replace the entire function body. The original looks like:

TaskPriority MainThreadSchedulerImpl::ComputeCompositorPriority() const {
  std::optional<TaskPriority> targeted_main_frame_priority =
      ComputeCompositorPriorityForMainFrame();
  std::optional<TaskPriority> use_case_priority =
      ComputeCompositorPriorityFromUseCase();
  if (!targeted_main_frame_priority && !use_case_priority) {
    return TaskPriority::kNormalPriority;
  } else if (!use_case_priority) {
    return *targeted_main_frame_priority;
  } else if (!targeted_main_frame_priority) {
    return *use_case_priority;
  }

  // Both are set, so some reconciliation is needed.
  CHECK(targeted_main_frame_priority && use_case_priority);
  // If either votes for the highest priority, use that to simplify the
  // remaining case.
  if (*targeted_main_frame_priority == TaskPriority::kHighestPriority ||
      *use_case_priority == TaskPriority::kHighestPriority) {
    return TaskPriority::kHighestPriority;
  }
  // Otherwise, this must be a combination of UseCase::kCompositorGesture and
  // rendering starvation since all other use cases set the priority to highest.
  CHECK(current_use_case() == UseCase::kCompositorGesture &&
        (main_thread_only().main_frame_prioritization_state ==
             RenderingPrioritizationState::kRenderingStarved ||
         main_thread_only().main_frame_prioritization_state ==
             RenderingPrioritizationState::kRenderingStarvedByRenderBlocking));
  CHECK_LE(*targeted_main_frame_priority, *use_case_priority);
  return *targeted_main_frame_priority;
}

Replace with:

TaskPriority MainThreadSchedulerImpl::ComputeCompositorPriority() const {
  std::optional<TaskPriority> targeted_main_frame_priority =
      ComputeCompositorPriorityForMainFrame();
  std::optional<TaskPriority> use_case_priority =
      ComputeCompositorPriorityFromUseCase();

  TaskPriority result;
  if (!targeted_main_frame_priority && !use_case_priority) {
    result = TaskPriority::kNormalPriority;
  } else if (!use_case_priority) {
    result = *targeted_main_frame_priority;
  } else if (!targeted_main_frame_priority) {
    result = *use_case_priority;
  } else {
    // Both are set -- take the higher priority (lower numeric value).
    result = std::min(*targeted_main_frame_priority, *use_case_priority);
  }

  // Cap compositor priority to kNormalPriority. Without this cap,
  // back-to-back BeginFrame tasks at kHighestPriority (triggered by
  // continuous mouse input + --disable-frame-rate-limit) create a tight
  // compositor loop that permanently starves kNormalPriority tasks
  // (WebSocket onmessage, Worker postMessage). The task queue selector
  // has no cross-priority anti-starvation, so any priority above kNormal
  // causes indefinite deferral of lower-priority work. Rendering starvation
  // detection in ComputeCompositorPriorityForMainFrame() is sufficient to
  // protect against actual frame drops when compositor priority is capped.
  return std::max(result, TaskPriority::kNormalPriority);
}

Using the diff file

Alternatively, if you have the .diff file, apply it from the Chromium src root:

cd C:\electron\electron\src
git apply /path/to/ws-priority-patch.diff

Verify the patch

cd C:\electron\electron\src
grep -n "kNormalPriority" third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc | grep -E "(kInput|std::max)"

You should see the kInput case returning kNormalPriority and the std::max cap at the end of ComputeCompositorPriority().


Step 3: Configure the Release Build

Set up args.gn

mkdir -p C:\electron\electron\src\out\Release

Create/edit C:\electron\electron\src\out\Release\args.gn:

import("//electron/build/args/release.gn")
is_official_build = true
use_remoteexec = false
use_reclient = false

Generate build files

cd C:\electron\electron\src
buildtools/win/gn.exe gen out/Release

You should see: Done. Made XXXXX targets from XXXX files

Clean stale state (if needed)

If you see an error about Siso state files:

buildtools/win/gn.exe clean out/Release
buildtools/win/gn.exe gen out/Release

Step 4: Build

cd C:\electron\electron\src
ninja -C out/Release electron

This is a full rebuild -- expect 6-10+ hours depending on CPU cores and speed. On a 24-core machine with 64GB RAM it takes approximately 8-9 hours (~45,000 build steps).

Build the distribution zip

After the main build completes:

ninja -C out/Release electron:electron_dist_zip

The dist zip will be at C:\electron\electron\src\out\Release\dist.zip (~137MB).


Step 5: Using the Patched Electron

Option A: Direct binary

Run your app directly with the built electron:

C:\electron\electron\src\out\Release\electron.exe /path/to/your/app

Option B: Replace in node_modules

Extract dist.zip and replace the Electron binary in your project:

# Find your project's electron installation
ls node_modules/electron/dist/

# Back up the original
mv node_modules/electron/dist node_modules/electron/dist-original

# Extract patched version
mkdir node_modules/electron/dist
cd node_modules/electron/dist
unzip /path/to/dist.zip

Option C: electron-builder / electron-forge

Point your build tool to the custom Electron zip:

electron-builder (package.json):

{
  "build": {
    "electronDist": "path/to/extracted/dist",
    "electronVersion": "40.6.1"
  }
}

Or set the environment variable:

export ELECTRON_CUSTOM_DIR=path/to/extracted/dist

electron-forge (forge.config.js):

module.exports = {
  packagerConfig: {
    electronZipDir: 'path/to/extracted/dist'
  }
};

Required Chromium flags

Your Electron app should use these flags for unlimited FPS:

app.commandLine.appendSwitch('disable-frame-rate-limit');
app.commandLine.appendSwitch('disable-gpu-vsync');

Verification: Automated Stress Test

Test files

Create a directory (e.g., C:\electron\test-app) with these files:

package.json:

{
  "name": "ws-starvation-test",
  "version": "1.0.0",
  "main": "cdp-test.js",
  "dependencies": {
    "ws": "^8.0.0"
  }
}

Run npm install in the test directory.

stress.html:

<!DOCTYPE html>
<html>
<head><title>WS Starvation Stress Test</title></head>
<body style="margin:0; overflow:hidden; background:#111;">
<canvas id="c" style="width:100vw;height:100vh;display:block;"></canvas>
<div id="hud" style="position:fixed;top:10px;left:10px;color:#0f0;font:14px monospace;background:rgba(0,0,0,0.8);padding:10px;z-index:10;pointer-events:none;"></div>
<script>
    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d');
    const hud = document.getElementById('hud');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    let mx = canvas.width/2, my = canvas.height/2;
    let mouseDown = false;
    let moveCount = 0;
    let frameCount = 0;

    document.addEventListener('mousedown', (e) => {
        mouseDown = true;
        mx = e.clientX; my = e.clientY;
        ctx.fillStyle = '#ff0';
        ctx.beginPath();
        ctx.arc(mx, my, 30, 0, Math.PI*2);
        ctx.fill();
    });
    document.addEventListener('mouseup', () => { mouseDown = false; });
    document.addEventListener('mousemove', (e) => {
        mx = e.clientX; my = e.clientY;
        moveCount++;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.strokeStyle = mouseDown ? '#f00' : '#0f0';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(mx - 20, my); ctx.lineTo(mx + 20, my);
        ctx.moveTo(mx, my - 20); ctx.lineTo(mx, my + 20);
        ctx.stroke();
        if (mouseDown) {
            for (let i = 0; i < 5; i++) {
                const hue = Math.floor(Math.random()*60);
                ctx.fillStyle = 'hsl(' + hue + ', 100%, 50%)';
                ctx.fillRect(mx + Math.random()*60-30, my + Math.random()*60-30, 3, 3);
            }
        }
    });

    function frame() {
        frameCount++;
        requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);

    let wsLatencies = [];
    const ws = new WebSocket('ws://' + location.host);
    ws.onmessage = function(e) {
        const now = performance.now();
        if (ws._lastReceive) {
            wsLatencies.push(now - ws._lastReceive);
        }
        ws._lastReceive = now;
    };

    setInterval(function() {
        if (wsLatencies.length < 2) return;
        const recent = wsLatencies.slice(-300);
        const sorted = recent.slice().sort(function(a,b) { return a - b; });
        const avg = recent.reduce(function(a,b) { return a+b; }, 0) / recent.length;
        const p95 = sorted[Math.floor(sorted.length * 0.95)];
        const p99 = sorted[Math.floor(sorted.length * 0.99)];
        const max = sorted[sorted.length - 1];
        const over50 = recent.filter(function(x) { return x > 50; }).length;
        hud.textContent =
            'Mouse: ' + (mouseDown ? 'SHOOTING' : 'idle') + ' (' + moveCount + ' moves)\n' +
            'Frames: ' + frameCount + '\n' +
            'WS: avg=' + avg.toFixed(1) + ' p95=' + p95.toFixed(1) + ' p99=' + p99.toFixed(1) + ' max=' + max.toFixed(1) + '\n' +
            'WS >50ms: ' + over50 + '/' + recent.length + ' (' + (over50/recent.length*100).toFixed(1) + '%)';
    }, 250);
</script>
</body>
</html>

cdp-test.js:

const { app, BrowserWindow } = require('electron');
const http = require('http');
const fs = require('fs');
const path = require('path');
const { WebSocketServer } = require('ws');

const PORT = parseInt(process.argv[2] || '8085');
const LABEL = process.argv[3] || 'TEST';
const TEST_DURATION_MS = 12000;
const WARMUP_MS = 3000;

app.commandLine.appendSwitch('disable-frame-rate-limit');
app.commandLine.appendSwitch('disable-gpu-vsync');

// Server
const server = http.createServer((req, res) => {
    const filePath = path.join(__dirname, 'stress.html');
    fs.readFile(filePath, (err, data) => {
        if (err) { res.writeHead(404); res.end(); return; }
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data);
    });
});
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
    console.log('[' + LABEL + '] WS client connected');
    const interval = setInterval(() => {
        if (ws.readyState === ws.OPEN) {
            ws.send(JSON.stringify({ t: performance.now() }));
        }
    }, 16);
    ws.on('close', () => clearInterval(interval));
});
server.listen(PORT, () => console.log('[' + LABEL + '] Server on port ' + PORT));

app.whenReady().then(async () => {
    const win = new BrowserWindow({
        width: 900, height: 700, show: true,
        webPreferences: { nodeIntegration: false, contextIsolation: true }
    });

    await win.loadURL('http://localhost:' + PORT);
    const wc = win.webContents;

    // Attach to debugger for CDP input dispatch
    wc.debugger.attach('1.3');
    console.log('[' + LABEL + '] CDP debugger attached');

    await new Promise(r => setTimeout(r, WARMUP_MS));
    await wc.executeJavaScript('wsLatencies = []; void 0;');

    console.log('[' + LABEL + '] Starting ' + (TEST_DURATION_MS/1000) + 's stress test...');

    const startTime = Date.now();
    let eventCount = 0;
    let angle = 0;
    const centerX = 450, centerY = 350;

    // Mouse down via CDP
    await wc.debugger.sendCommand('Input.dispatchMouseEvent', {
        type: 'mousePressed',
        x: centerX, y: centerY,
        button: 'left',
        clickCount: 1
    });
    eventCount++;

    // Flood with mouseMoved events via CDP
    const flood = async () => {
        while (Date.now() - startTime < TEST_DURATION_MS) {
            const promises = [];
            for (let i = 0; i < 10; i++) {
                angle += 0.03;
                const radius = 100 + Math.sin(angle * 0.5) * 60;
                const x = Math.floor(centerX + Math.cos(angle) * radius);
                const y = Math.floor(centerY + Math.sin(angle) * radius);
                promises.push(
                    wc.debugger.sendCommand('Input.dispatchMouseEvent', {
                        type: 'mouseMoved',
                        x: x, y: y,
                        button: 'left',
                        buttons: 1
                    }).catch(() => {})
                );
                eventCount++;
            }
            await Promise.all(promises);
        }
    };

    await flood();

    // Release
    await wc.debugger.sendCommand('Input.dispatchMouseEvent', {
        type: 'mouseReleased',
        x: centerX, y: centerY,
        button: 'left'
    }).catch(() => {});

    await new Promise(r => setTimeout(r, 2000));

    // Collect results
    const result = await wc.executeJavaScript(
        '(function() {' +
        '  if (!wsLatencies || wsLatencies.length === 0) return JSON.stringify({ error: "no data" });' +
        '  var sorted = wsLatencies.slice().sort(function(a,b){return a-b});' +
        '  var len = sorted.length;' +
        '  var avg = wsLatencies.reduce(function(a,b){return a+b},0) / len;' +
        '  var p50 = sorted[Math.floor(len*0.50)];' +
        '  var p75 = sorted[Math.floor(len*0.75)];' +
        '  var p90 = sorted[Math.floor(len*0.90)];' +
        '  var p95 = sorted[Math.floor(len*0.95)];' +
        '  var p99 = sorted[Math.floor(len*0.99)];' +
        '  var max = sorted[len-1];' +
        '  var min = sorted[0];' +
        '  var over50 = wsLatencies.filter(function(x){return x>50}).length;' +
        '  var over100 = wsLatencies.filter(function(x){return x>100}).length;' +
        '  var over200 = wsLatencies.filter(function(x){return x>200}).length;' +
        '  var over500 = wsLatencies.filter(function(x){return x>500}).length;' +
        '  var mouseEvents = typeof moveCount !== "undefined" ? moveCount : -1;' +
        '  var frames = typeof frameCount !== "undefined" ? frameCount : -1;' +
        '  return JSON.stringify({avg:avg,min:min,p50:p50,p75:p75,p90:p90,p95:p95,p99:p99,max:max,total:len,over50:over50,over100:over100,over200:over200,over500:over500,mouseEvents:mouseEvents,frames:frames});' +
        '})()'
    );

    wc.debugger.detach();

    const d = JSON.parse(result);
    if (d.error) {
        console.log('[' + LABEL + '] ERROR: ' + d.error);
        process.exit(2);
    }

    console.log('\n[' + LABEL + '] ============ RESULTS ============');
    console.log('[' + LABEL + '] CDP events sent: ' + eventCount);
    console.log('[' + LABEL + '] Mouse events received by renderer: ' + d.mouseEvents);
    console.log('[' + LABEL + '] RAF frames rendered: ' + d.frames);
    console.log('[' + LABEL + '] WS samples collected: ' + d.total);
    console.log('[' + LABEL + ']');
    console.log('[' + LABEL + '] WS inter-message latency (ms):');
    console.log('[' + LABEL + ']   min:  ' + d.min.toFixed(1));
    console.log('[' + LABEL + ']   avg:  ' + d.avg.toFixed(1));
    console.log('[' + LABEL + ']   p50:  ' + d.p50.toFixed(1));
    console.log('[' + LABEL + ']   p75:  ' + d.p75.toFixed(1));
    console.log('[' + LABEL + ']   p90:  ' + d.p90.toFixed(1));
    console.log('[' + LABEL + ']   p95:  ' + d.p95.toFixed(1));
    console.log('[' + LABEL + ']   p99:  ' + d.p99.toFixed(1));
    console.log('[' + LABEL + ']   max:  ' + d.max.toFixed(1));
    console.log('[' + LABEL + ']');
    console.log('[' + LABEL + '] Threshold violations:');
    console.log('[' + LABEL + ']   >50ms:  ' + d.over50 + ' / ' + d.total + ' (' + (d.over50/d.total*100).toFixed(1) + '%)');
    console.log('[' + LABEL + ']   >100ms: ' + d.over100 + ' / ' + d.total + ' (' + (d.over100/d.total*100).toFixed(1) + '%)');
    console.log('[' + LABEL + ']   >200ms: ' + d.over200 + ' / ' + d.total + ' (' + (d.over200/d.total*100).toFixed(1) + '%)');
    console.log('[' + LABEL + ']   >500ms: ' + d.over500 + ' / ' + d.total + ' (' + (d.over500/d.total*100).toFixed(1) + '%)');
    console.log('[' + LABEL + '] ==================================');

    process.exit(0);
});

Running the test

# Test the patched build
C:\electron\electron\src\out\Release\electron.exe cdp-test.js 8085 PATCHED

# Compare against a stock Electron (download from https://github.com/electron/electron/releases)
path/to/stock/electron.exe cdp-test.js 8086 BASELINE

Expected results:

  • Unpatched: p99 ~80-100ms, max ~200-300ms, 5-9% of messages >50ms
  • Patched: p99 ~30-35ms, max ~35-40ms, 0% of messages >50ms

Important: The test uses CDP Input.dispatchMouseEvent to simulate mouse input. This goes through the full Chromium input pipeline and reliably triggers the starvation. Electron's sendInputEvent API does NOT trigger it (bypasses compositor thread input handler).


Rebuilding for a Different Electron Version

To build the patch against a different Electron version (e.g., upgrading to a newer stable release):

cd C:\electron\electron\src\electron

# List available stable versions
git tag --list 'v*' --sort=-version:refname | grep -v -E '(nightly|alpha|beta)' | head -10

# Check out the desired version
git checkout v40.6.1

# Sync dependencies (30-60+ minutes)
cd C:\electron\electron\src
gclient sync --with_branch_heads --with_tags

# Re-apply the patch (line numbers may differ between versions)
# Edit main_thread_scheduler_impl.cc as described in Step 2
# Or try: git apply ws-priority-patch.diff

# Clean, generate, and build
buildtools/win/gn.exe clean out/Release
buildtools/win/gn.exe gen out/Release
ninja -C out/Release electron
ninja -C out/Release electron:electron_dist_zip

Note: The patch modifies Chromium source (not Electron source), so line numbers may shift between versions. The function names and structure should remain the same across Chromium versions. Search for PrioritisationType::kInput and ComputeCompositorPriority() to find the right locations.


Patch File

The raw diff is saved at ws-priority-patch.diff alongside this guide. It was generated against Chromium 147.x (Electron v42 nightly) but the same logic applies to all recent versions.


Troubleshooting

"Python was not found" during build

Disable Windows Store Python aliases (see Step 0). Ensure real Python 3.12 is in PATH before C:\Users\<you>\AppData\Local\Microsoft\WindowsApps.

"Siso state file" error when running ninja

Run buildtools/win/gn.exe clean out/Release then gn gen again.

"gn not found"

Use the full path: buildtools/win/gn.exe from the Chromium src directory.

Build fails with missing Windows SDK

Install SDK 10.0.26100.0: winget install "Microsoft.WindowsSDK.10.0.26100"

gclient sync fails with SSH errors

The sync uses Git cache. If SSH keys aren't set up for GitHub, the repos should still sync via HTTPS through the cache. If errors persist, check GIT_CACHE_PATH is set correctly.

Patch doesn't apply cleanly to a different version

Apply manually -- search for PrioritisationType::kInput returning kHighestPriority and change it to kNormalPriority. Then find ComputeCompositorPriority() and add the std::max cap. The surrounding code structure should be recognizable even if line numbers differ.