MouseManager.stopListeners never removes the wheel listener — destroyed Game stays retained through the canvas
Maintainer thường phản hồi trong vòng 2 ngày
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 78/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- javascript
- Lĩnh vực
- game-dev
Hướng nghiên cứu
Bắt đầu trong src/input/mouse/MouseManager.js bằng cách so sánh startListeners với stopListeners, tập trung vào việc đăng ký wheel và phần xử lý contextmenu liên quan. Kiểm tra đường đi của quá trình hủy với canvas của game được giữ lại và xác nhận rằng game.destroy(true) không để lại listener wheel của Phaser nào được gắn vào canvas.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Version
- Phaser Version: 4.1.0 (also present in 3.87.0 and in current
mastersrc/input/mouse/MouseManager.js) - Operating system: any (verified Windows 11)
- Browser: any (verified Chromium 138, headed + headless)
Description
MouseManager.startListeners attaches six listeners to the input target (the game
canvas): mousemove, mousedown, mouseup, mouseover, mouseout, and wheel.
MouseManager.stopListeners removes the first five, but there is no
removeEventListener('wheel', …) anywhere in the class — the wheel listener
stays on the canvas forever, including after game.destroy(true).
The onMouseWheel closure captures _this (the MouseManager) and manager (the
InputManager), and through them the entire Phaser.Game — every scene, texture,
and renderer pipeline. So after destroy, the dead canvas carries a hidden strong
reference to the whole engine graph.
When nothing else references the canvas, the canvas + listener + Game all become
unreachable together and this is invisible. But any surviving reference to the
canvas element (DevTools element inspection, a stray element handle in test
tooling, an app that keeps the canvas for a screenshot/share feature, a detached
node captured by an extension) now retains a full Game instance — measured at
~8 MB per create/destroy cycle in our app. We root-caused a "heap climbs 11 MB →
380 MB over 40 game visits" report to exactly this: one complete Phaser Game
graph pinned per cycle, with the retainer path running through the canvas's
wheel EventListener.
Retainer path from a Chrome heap snapshot (destroyed game, canvas still
referenced):
HTMLCanvasElement → EventListener (wheel) → V8EventListener → onMouseWheel closure
→ context: MouseManager / InputManager → Phaser.Game → SceneManager, TextureManager, …
Every other input surface is balanced (TouchManager, KeyboardManager, and
GamepadPlugin all remove what they add), which suggests wheel was simply missed
when it was added to startListeners.
Example Test Code
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
<script>
const game = new Phaser.Game({ type: Phaser.AUTO, width: 640, height: 480 });
setTimeout(() => {
// Any surviving reference to the canvas — DevTools inspection, test
// tooling, an app-level screenshot cache — plays this role in real apps.
window.deadCanvas = game.canvas;
game.destroy(true);
setTimeout(() => {
// In the DevTools console:
// getEventListeners(window.deadCanvas)
// → { wheel: [ { listener: onMouseWheel, … } ] } ← still attached
//
// Take a heap snapshot → filter "Game" → the destroyed Phaser.Game is
// still reachable, retained via that wheel listener's closure.
console.log('destroyed — inspect getEventListeners(window.deadCanvas)');
}, 500);
}, 1000);
</script>
Expected: after game.destroy(true) the canvas has no Phaser listeners, so a
retained canvas costs a canvas.
Actual: the canvas keeps a wheel listener whose closure retains the entire
destroyed Game.
Suggested fix
In stopListeners (src/input/mouse/MouseManager.js), alongside the other target
removals:
target.removeEventListener('wheel', this.onMouseWheel);
removeEventListener ignores the passive flag when matching, so the single
call covers both the { passive: false } and passive registration branches.
Related, much smaller: disableContextMenu adds an anonymous contextmenu
listener that can never be removed. Its closure captures nothing, so it only
leaks the listener itself — but storing the handler and removing it in
stopListeners would make destroy fully clean.
Workaround (app side)
Before calling game.destroy(true):
game.canvas.removeEventListener('wheel', game.input.mouse.onMouseWheel);
Found while profiling arcade-game open/exit cycles on spacedread.tv (heap
snapshot + reverse-edge retainer analysis).
- Ngôn ngữ chính
- JavaScript
- Star
- 40.5k
- Fork
- 7.2k
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của phaserjs/phaser
-
Type definition for TilemapLayer.getTileAt is wrongCó thể đã có người làm @hkbughk đã nhận 9 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Shader.preDestroy leaks its ShaderQuad's vertex buffer and VAO on every create/destroy cycle (4.2.1)Có thể đã có người làm @k1epg01ng129-eng đã nhận 7 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Black opaque background on transparent Phaser.Text textures in Phaser 4.2.1 on older iOS WebGLĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 2 ngày
-
flipY renders tile incorrectlyCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
Maintainer thường phản hồi trong vòng 2 ngày
-
WebGL stroke of a Polygon drops a vertex when its origin-shifted position equals the previous raw vertexCó thể đã có người làm @ICOM725 đã nhận 46 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
phaserjs/phaser#7361 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
Tất cả issue của phaserjs/phaser
Issue tương tự
-
Place search spins forever when nominatim failsCó thể đã có người làm @jgt87 đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
anvaka/city-roads#213 ·
-
mobile UX
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 69/100
jellyfin/jellyfin-web#8685 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
SchemaStore/schemastore#6535 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement good first issue helia storage tier-1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
QMeshPy/distributed-quantum#53 ·
Maintainer thường phản hồi trong vòng 1 ngày