Skip to content

Commit 812f37f

Browse files
Performance Improvement: Oscilloscope Throttling (#5741)
* chore(i18n): auto-update JSON files from updated PO files * perf: refactor oscilloscope loop to prevent double-scheduling and sync with global idle state * perf: sync activity event handlers with master and maintain idle throttling * Apply Prettier formatting to satisfy CI * chore: remove unrelated workflow and locale changes from PR --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 5abf38d commit 812f37f

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

js/activity.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,6 +2983,60 @@ class Activity {
29832983
}
29842984
};
29852985

2986+
/**
2987+
* Initialize an idle watcher that throttles the application's framerate
2988+
* when the application is inactive and no music is playing.
2989+
* This significantly reduces CPU usage and improves battery life.
2990+
*/
2991+
this._initIdleWatcher = () => {
2992+
const IDLE_THRESHOLD = 5000; // 5 seconds
2993+
const ACTIVE_FPS = 60;
2994+
const IDLE_FPS = 1;
2995+
2996+
let lastActivity = Date.now();
2997+
this.isAppIdle = false;
2998+
2999+
// Wake up function - restores full framerate
3000+
const resetIdleTimer = () => {
3001+
lastActivity = Date.now();
3002+
if (this.isAppIdle) {
3003+
this.isAppIdle = false;
3004+
createjs.Ticker.framerate = ACTIVE_FPS;
3005+
// Force immediate redraw for responsiveness
3006+
if (this.stage) this.stage.update();
3007+
}
3008+
};
3009+
3010+
// Track user activity
3011+
window.addEventListener("mousemove", resetIdleTimer);
3012+
window.addEventListener("mousedown", resetIdleTimer);
3013+
window.addEventListener("keydown", resetIdleTimer);
3014+
window.addEventListener("touchstart", resetIdleTimer);
3015+
window.addEventListener("wheel", resetIdleTimer);
3016+
3017+
// Periodic check for idle state
3018+
setInterval(() => {
3019+
// Check if music/code is playing
3020+
const isMusicPlaying = this.logo?._alreadyRunning || false;
3021+
3022+
if (!isMusicPlaying && Date.now() - lastActivity > IDLE_THRESHOLD) {
3023+
if (!this.isAppIdle) {
3024+
this.isAppIdle = true;
3025+
createjs.Ticker.framerate = IDLE_FPS;
3026+
console.log("⚡ Idle mode: Throttling to 1 FPS to save battery");
3027+
}
3028+
} else if (this.isAppIdle && isMusicPlaying) {
3029+
// Music started playing - wake up immediately
3030+
resetIdleTimer();
3031+
}
3032+
}, 1000);
3033+
3034+
// Expose activity instance for external checks
3035+
if (typeof window !== "undefined") {
3036+
window.activity = this;
3037+
}
3038+
};
3039+
29863040
/**
29873041
* Renders an error message with appropriate artwork.
29883042
* @param {string} name - The name specifying the SVG to be rendered.

js/widgets/oscilloscope.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ class Oscilloscope {
4747
// RAF lifecycle control
4848
this._running = false;
4949
this._rafId = null;
50+
this._timeoutId = null;
51+
this._isIdle = false;
5052
this.draw = this.draw.bind(this);
5153

54+
this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
55+
document.addEventListener("visibilitychange", this._handleVisibilityChange);
56+
5257
this.pitchAnalysers = {};
5358
this._canvasState = {};
5459
this.drawVisualIDs = {};
@@ -109,6 +114,11 @@ class Oscilloscope {
109114
this._rafId = null;
110115
}
111116

117+
if (this._timeoutId !== null) {
118+
clearTimeout(this._timeoutId);
119+
this._timeoutId = null;
120+
}
121+
112122
// Backward compatibility: if any per-turtle RAF ids were stored, cancel them too.
113123
for (const id of Object.values(this.drawVisualIDs || {})) {
114124
if (id !== null && id !== undefined) {
@@ -123,9 +133,55 @@ class Oscilloscope {
123133
this.draw();
124134
}
125135

136+
_throttle() {
137+
if (!this._running) return;
138+
139+
// Cancel any active RAF
140+
if (this._rafId !== null) {
141+
cancelAnimationFrame(this._rafId);
142+
this._rafId = null;
143+
}
144+
145+
// Enter idle mode
146+
this._isIdle = true;
147+
148+
// Start timeout scheduler if not already running
149+
if (this._timeoutId === null) {
150+
this._timeoutId = setTimeout(this.draw, 1000);
151+
}
152+
}
153+
154+
_wakeUp() {
155+
if (!this._running) return;
156+
157+
// Cancel any active timeout
158+
if (this._timeoutId !== null) {
159+
clearTimeout(this._timeoutId);
160+
this._timeoutId = null;
161+
}
162+
163+
// Exit idle mode
164+
this._isIdle = false;
165+
166+
// Restart RAF scheduler if not already running
167+
if (this._rafId === null) {
168+
this.draw();
169+
}
170+
}
171+
172+
_handleVisibilityChange() {
173+
if (document.visibilityState === "visible") {
174+
this._wakeUp();
175+
} else {
176+
this._throttle();
177+
}
178+
}
179+
126180
close() {
127181
this._stopAnimation();
128182

183+
document.removeEventListener("visibilitychange", this._handleVisibilityChange);
184+
129185
this.drawVisualIDs = {};
130186
this._canvasState = {};
131187
this.pitchAnalysers = {};
@@ -214,9 +270,22 @@ class Oscilloscope {
214270
draw() {
215271
if (!this._running) return;
216272

273+
// Render the current frame
217274
this._renderFrame();
218275

219-
this._rafId = requestAnimationFrame(this.draw);
276+
// Schedule next frame based on idle state
277+
if (
278+
this._isIdle ||
279+
(this.activity && this.activity.isAppIdle) ||
280+
document.visibilityState === "hidden" ||
281+
this.widgetWindow._rolled
282+
) {
283+
// Use setTimeout for idle mode (1 FPS)
284+
this._timeoutId = setTimeout(this.draw, 1000);
285+
} else {
286+
// Use RAF for active mode (~60 FPS)
287+
this._rafId = requestAnimationFrame(this.draw);
288+
}
220289
}
221290

222291
/* ---------------- Resize ---------------- */

0 commit comments

Comments
 (0)