Skip to content

Instantly share code, notes, and snippets.

@SunjunKim
Last active December 5, 2025 17:23
Show Gist options
  • Select an option

  • Save SunjunKim/947b18b38b72c22ad02f0dba8f05aa91 to your computer and use it in GitHub Desktop.

Select an option

Save SunjunKim/947b18b38b72c22ad02f0dba8f05aa91 to your computer and use it in GitHub Desktop.
A web page to measure the mouse polling rate
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Raw Mouse Event Capture Test</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: #fff;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #aaa;
margin-bottom: 30px;
}
.status {
text-align: center;
padding: 15px;
background: #2a2a2a;
border-radius: 8px;
margin-bottom: 20px;
font-size: 18px;
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.stat-box {
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: #aaa;
margin-bottom: 10px;
}
.stat-value {
font-size: 36px;
font-weight: bold;
color: #4CAF50;
}
.warning {
background: linear-gradient(to right, #f44336, #e91e63);
padding: 15px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
display: none;
}
.warning.show {
display: block;
}
.instructions {
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
line-height: 1.6;
}
.instructions h2 {
margin-top: 0;
}
.instructions ol {
margin-left: 20px;
}
.click-area {
background: #2a2a2a;
padding: 40px;
border-radius: 8px;
text-align: center;
cursor: pointer;
border: 2px dashed #555;
margin-bottom: 20px;
transition: all 0.3s;
}
.click-area:hover {
border-color: #4CAF50;
background: #333;
}
.click-area.locked {
border-color: #4CAF50;
background: #1a3a1a;
}
.event-log {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
margin-top: 20px;
}
.event-log-item {
padding: 2px 0;
border-bottom: 1px solid #2a2a2a;
}
.event-log-item:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Raw Mouse Event Capture Test</h1>
<p class="subtitle">Capture mouse events as raw as possible</p>
<div class="warning" id="powerWarning">
⚠️ Connect power to your device for best results
</div>
<div class="status" id="status">Click anywhere to start capturing mouse events</div>
<div class="stats">
<div class="stat-box">
<div class="stat-label">Realtime Rate</div>
<div class="stat-value" id="realtimeRate">0 Hz</div>
</div>
<div class="stat-box">
<div class="stat-label">Maximum Rate</div>
<div class="stat-value" id="maxRate">0 Hz</div>
</div>
</div>
<div class="click-area" id="clickArea">
Click here to lock pointer and start capturing
</div>
<div class="instructions">
<h2>Instructions</h2>
<ol>
<li>Click in the area above to lock your pointer</li>
<li>Move your mouse with varying speeds</li>
<li>Watch the realtime polling rate</li>
<li>Click again (or press ESC) to unlock</li>
</ol>
<p><strong>Note:</strong> This uses raw input with <code>unadjustedMovement: true</code> to bypass OS acceleration and capture events as raw as possible.</p>
</div>
<div class="event-log" id="eventLog">
<div style="color: #666;">Event log will appear here...</div>
</div>
</div>
<script>
let isMeasuring = false;
let pollCount = 0;
let realtimeRate = 0;
let maxRate = 0;
let firstFrame = true;
let eventLogCount = 0;
const maxLogItems = 50;
// Accumulate movements between 250ms reports
let accumulatedMovementX = 0;
let accumulatedMovementY = 0;
let accumulatedEventCount = 0;
let eventTimestamps = []; // Track timestamps for interval calculation
// Check power connection
let isPowerConnected = true;
// On laptops or mobile devices, mouse or trackpad polling rates may be lower when running on battery to save power,
// resulting in less accurate measurements. This code checks if the device is plugged in and warns the user if not.
if (navigator.getBattery) {
navigator.getBattery().then((battery) => {
isPowerConnected = battery.charging;
updatePowerWarning();
battery.addEventListener("chargingchange", () => {
isPowerConnected = battery.charging;
updatePowerWarning();
});
});
}
function updatePowerWarning() {
const warning = document.getElementById("powerWarning");
if (!isPowerConnected) {
warning.classList.add("show");
} else {
warning.classList.remove("show");
}
}
// Update polling rate every 250ms and report accumulated movements
setInterval(() => {
if (isMeasuring) {
// Report accumulated movements every 250ms
if (accumulatedEventCount > 0) {
// Calculate mean interval and temporal jitter
let meanInterval = 0;
let temporalJitter = 0;
if (eventTimestamps.length > 1) {
// Calculate intervals between consecutive events
const intervals = [];
for (let i = 1; i < eventTimestamps.length; i++) {
intervals.push(eventTimestamps[i] - eventTimestamps[i - 1]);
}
// Calculate mean interval
const sum = intervals.reduce((a, b) => a + b, 0);
meanInterval = sum / intervals.length;
// Calculate temporal jitter (standard deviation of intervals)
if (intervals.length > 1) {
const variance = intervals.reduce((acc, interval) => {
return acc + Math.pow(interval - meanInterval, 2);
}, 0) / intervals.length;
temporalJitter = Math.sqrt(variance);
}
}
// Calculate realtimeRate as the inverse of meanInterval (convert ms to Hz)
realtimeRate = (meanInterval > 0) ? (1000 / meanInterval) : 0;
document.getElementById("realtimeRate").textContent = realtimeRate.toFixed(1) + " Hz";
if (!firstFrame && realtimeRate > maxRate) {
maxRate = realtimeRate;
document.getElementById("maxRate").textContent = maxRate.toFixed(1) + " Hz";
}
logEvent(`${accumulatedEventCount} events`, {
movementX: accumulatedMovementX,
movementY: accumulatedMovementY,
meanInterval: meanInterval,
temporalJitter: temporalJitter
});
// Reset accumulators
accumulatedMovementX = 0;
accumulatedMovementY = 0;
accumulatedEventCount = 0;
eventTimestamps = [];
}
firstFrame = false;
pollCount = 0;
}
}, 250);
// Raw mouse event handler - captures all events including coalesced ones
function handlePointerMove(e) {
if (!isMeasuring) return;
// Use getCoalescedEvents() to capture ALL events, including those that were coalesced
// This is crucial for accurate polling rate measurement
let eventCount = 1;
if (e.getCoalescedEvents && e.getCoalescedEvents().length > 1) {
eventCount = e.getCoalescedEvents().length;
pollCount += eventCount;
// Accumulate each coalesced event's movement
const coalescedEvents = e.getCoalescedEvents();
coalescedEvents.forEach(event => {
accumulatedMovementX += event.movementX;
accumulatedMovementY += event.movementY;
accumulatedEventCount++;
// Use the event's timestamp (coalesced events have their own timeStamp)
// Use nullish coalescing (??) instead of || to avoid treating 0 as falsy
eventTimestamps.push(event.timeStamp ?? performance.now());
});
} else {
pollCount++;
// Accumulate single event movement
accumulatedMovementX += e.movementX;
accumulatedMovementY += e.movementY;
accumulatedEventCount++;
// Record timestamp for interval calculation
// Use nullish coalescing (??) instead of || to avoid treating 0 as falsy
eventTimestamps.push(e.timeStamp ?? performance.now());
}
}
// Log events (reported every 250ms)
function logEvent(type, data) {
const log = document.getElementById("eventLog");
const item = document.createElement("div");
item.className = "event-log-item";
let logText = `${type} | X:${data.movementX} | Y:${data.movementY}`;
if (data.meanInterval !== undefined && data.temporalJitter !== undefined) {
logText += ` | Mean Interval: ${data.meanInterval.toFixed(3)}ms | Jitter (SD): ${data.temporalJitter.toFixed(3)}ms`;
}
item.textContent = logText;
log.insertBefore(item, log.firstChild);
// Keep only last N items
while (log.children.length > maxLogItems) {
log.removeChild(log.lastChild);
}
}
let pointerMoveListenerActive = false;
function togglePointerMoveListener(active) {
if (active && !pointerMoveListenerActive) {
document.addEventListener("pointermove", handlePointerMove, {
passive: true,
capture: true
});
pointerMoveListenerActive = true;
} else if (!active && pointerMoveListenerActive) {
document.removeEventListener("pointermove", handlePointerMove, {
passive: true,
capture: true
});
pointerMoveListenerActive = false;
}
}
// Handle pointer lock changes
document.addEventListener("pointerlockchange", () => {
const clickArea = document.getElementById("clickArea");
const status = document.getElementById("status");
if (document.pointerLockElement === document.body) {
console.log("Pointer locked - starting measurement");
status.textContent = "Keep moving your mouse";
clickArea.classList.add("locked");
clickArea.textContent = "Pointer locked - Move your mouse";
isMeasuring = true;
maxRate = 0;
firstFrame = true;
pollCount = 0;
accumulatedMovementX = 0;
accumulatedMovementY = 0;
accumulatedEventCount = 0;
eventTimestamps = [];
document.getElementById("maxRate").textContent = "0 Hz";
togglePointerMoveListener(true); // Only start capturing events now
} else {
console.log("Pointer unlocked - stopping measurement");
status.textContent = "Click in the highlighted area to start capturing mouse events";
clickArea.classList.remove("locked");
clickArea.textContent = "Click here to lock pointer and start capturing";
isMeasuring = false;
togglePointerMoveListener(false); // Stop listening
}
}, false);
// Exit pointer lock on mouse up
document.addEventListener("mouseup", () => {
if (document.pointerLockElement) {
document.exitPointerLock();
}
});
// Request pointer lock only when click happens in clickArea
const clickArea = document.getElementById("clickArea");
clickArea.addEventListener("click", (e) => {
// Don't lock if clicking on links or inputs
if (e.target.tagName?.toLowerCase() === "a" ||
e.target.tagName?.toLowerCase() === "input") {
return;
}
if (!document.pointerLockElement) {
// Request pointer lock with unadjustedMovement: true for raw input
// This bypasses OS-level mouse acceleration and pointer precision
document.body.requestPointerLock({
unadjustedMovement: true
}).catch(err => {
console.error("Error requesting pointer lock:", err);
alert("Could not lock pointer. Make sure you're using a modern browser and allow pointer lock.");
});
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment