Phaser's tween builder and WebGL renderer carry the highest activity risk — 5 functions

Analysis of phaserjs/phaser at commit 1b8977a finds 119 critical functions across 1,897 total, with five fire-quadrant hotspots — including GetValueOp, GraphicsWebGLRenderer, and IsometricCullTiles — all touched within the last day and carrying activity-weighted risk scores above 17.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk19.31Low
Hottest FunctionGetValueOp

Antipatterns Detected

complex_branching5deeply_nested5exit_heavy5long_function4god_function3hub_function2

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is complex branching and why does it matter in Phaser?

Complex branching is when a function contains a large number of independent decision points — `if`, `else if`, `switch` cases, ternaries, and short-circuit operators — each of which creates a separate execution path through the code. Cyclomatic complexity is the formal count of those paths: a value of 10 is moderate, 30 is high, and 60 like `GraphicsWebGLRenderer` means there are 60 distinct paths that tests would need to cover to achieve full branch coverage. In a JavaScript game engine like Phaser, where rendering functions must handle dozens of drawing primitives and tween builders must parse many input formats, complex branching tends to accumulate naturally — but it makes functions hard to reason about when changing them and expensive to test thoroughly. All five of the top hotspots in this analysis flag complex branching, which means the highest-risk code in the repo is also the hardest to cover with tests.

How do I reduce cyclomatic complexity in JavaScript?

The most effective technique is extract-method refactoring: identify each logically distinct branch or case and move it into its own named function with a clear input/output contract. A cyclomatic complexity above 15 is a reliable signal to start splitting; above 30, it warrants immediate attention before the next change lands. For `GetValueOp` specifically, each input type — number, array, string operator — is already a natural extraction boundary; pulling each into its own factory function would bring the per-unit complexity below 10 with minimal risk. For a large `switch`-based dispatcher like `GraphicsWebGLRenderer`, extracting each case handler is the right move, and making shared mutable state explicit as a context object passed between handlers prevents the subtle bugs that come from closing over variables across many branches.

Is Phaser actively maintained?

The data points to active development. All five of the top hotspots are in the fire quadrant — meaning they are both structurally complex and recently modified — and every one of them was touched within the last 30 days, with all five showing days_since_changed of 1. There are 345 functions in the fire quadrant across the codebase and zero in the debt quadrant, which means there is no dormant complexity sitting untouched: every high-complexity function has also seen recent commits. Active development and structural complexity are not mutually exclusive — the fire-quadrant concentration reflects a codebase that is being actively extended, which is precisely why the structural risk in these five functions deserves attention now rather than later.

How do I reproduce this analysis?

The analysis was run against phaserjs/phaser at commit `1b8977a`. To reproduce it, install the Hotspots CLI from https://github.com/hotspots-dev/hotspots, check out that commit with `git checkout 1b8977a`, then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without any additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — with recent commit frequency, so functions that are both hard to understand and actively being changed score the highest. A function with very high cyclomatic complexity that has not been touched in two years scores lower than one with moderate complexity touched every week, because the dormant function has lower near-term regression risk: no one is introducing bugs into it right now. This prioritization helps focus refactoring effort where it reduces the probability of bugs being introduced in the current development cycle, not just where the code looks complicated in the abstract. All five functions analyzed here have an activity-weighted risk score above 17, and all five were modified within the last day — that combination is why they represent live risk rather than a cleanup backlog.

At commit 1b8977a, Phaser has 1,897 analyzed functions, 119 of them rated critical — and every single one of the top five hotspots is in the fire quadrant, meaning they are both structurally complex and actively changing right now. GetValueOp leads with an activity-weighted risk score of 19.31, touched 1 time in the last 30 days and modified just 1 day ago, making it a live regression risk rather than a cleanup backlog item. I would start there, then move to PCTDecode (19.26) and GraphicsWebGLRenderer (17.68) — three functions that collectively account for some of the heaviest branching and nesting complexity in the codebase.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
GetValueOpsrc/tweens/builders/GetValueOp.js19.334914
PCTDecodesrc/textures/parsers/PCTDecode.js19.329921
GetValuesrc/utils/object/GetValue.js17.91866
GraphicsWebGLRenderersrc/gameobjects/graphics/GraphicsWebGLRenderer.js17.760518
IsometricCullTilessrc/tilemaps/components/IsometricCullTiles.js17.14172

Large Repo Analysis

phaser is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Codemod / Tooling Files in Results

The ownKeys function appears in two files under scripts/tsgen/bin/Parser.js and publish.js. These are TypeScript declaration generation scripts, tooling that lives outside the game engine itself and is not part of the runtime bundle. Their elevated scores reflect scripting complexity in the build toolchain, not in the engine code. To exclude them from future analyses, add { "exclude": ["scripts/"] } to .hotspotsrc.json in the repository root.

Phaser is one of the most widely used open-source 2D game frameworks in JavaScript, and at nearly 1,900 analyzed functions the surface area is substantial. Before getting into individual functions, it helps to see how risk is distributed across the codebase.

Triage Band Distribution
Fire345Watch1552

1,897 functions analyzed

The quadrant picture is striking: 345 functions are in the fire quadrant and zero are in the debt quadrant. Every structurally complex function in this repo has also been recently touched. There is no dormant complexity to defer — it is all live.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
God Function×3God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Hub Function×2Hub Function
Many other functions call this one — a change here ripples widely through callers.

Across the five hotspots, every single one flags complex branching, deep nesting, and multiple exit paths. Three are tagged as god functions. That combination — many paths through the function, deep nesting that makes those paths hard to trace, and multiple return points that make coverage hard to verify — is the signature of code that is expensive to change safely.


GetValueOp — src/tweens/builders/GetValueOp.js

GetValueOp
src/tweens/builders/GetValueOp.js
19.31
critical
CC 34
ND 9
FO 14
touches/30d 1

GetValueOp sits at the heart of Phaser’s tween system. Its job, as the source excerpt makes clear, is to interpret the many forms a tween property value can take — a plain number, an array of keyframes, a string operator like '+=400' or 'random(10, 100)' — and return the appropriate getEnd, getStart, and getActive callbacks for the tween engine to invoke at runtime.

The problem is that this single function has to handle every one of those input shapes through a deeply nested chain of typeof checks, Array.isArray guards, substring parsing, and brace-matching logic. That produces a cyclomatic complexity of 34 — more than three times the threshold where splitting becomes worthwhile — nested to a maximum depth of 9. Nesting at that level means reasoning about the innermost branches requires mentally tracking at least nine enclosing conditions simultaneously. The 14 fan-out calls compound this: changes here can propagate to 14 distinct callees, and in JavaScript’s dynamic dispatch environment that number likely understates the true coupling.

The exit-heavy and god-function patterns both fire here. Multiple early returns, combined with the function’s role as a universal interpreter for tween property syntax, mean that adding or modifying any one input shape risks disturbing the others.

The most direct refactoring move is to extract each input type into its own named handler — a numberValueOp, arrayValueOp, stringOperatorValueOp — and reduce GetValueOp to a dispatcher that delegates to whichever handler matches. That would bring cyclomatic complexity per unit well below 10, make each path independently testable, and contain the blast radius of future tween API changes.


PCTDecode — src/textures/parsers/PCTDecode.js

PCTDecode
src/textures/parsers/PCTDecode.js
19.26
critical
CC 29
ND 9
FO 21
touches/30d 1

PCTDecode is a full text-format parser for Phaser’s PCT texture atlas format, and the source excerpt shows just how much it takes on in one function: header validation, version parsing, CRLF normalization, a multi-pass line loop, pending-block state management, folder expansion, and frame coordinate calculation — all interleaved in a single body.

At CC 29 and nesting depth 9, the branching is about as deep as GetValueOp, but the fan-out here is even higher at 21. That is the broadest coupling of any function in this analysis. In a JavaScript parser that relies on string manipulation utilities, index math, and format-specific helpers, 21 distinct callees means a change to any one of those helpers — or to the PCT format spec itself — creates a wide surface of potential breakage inside this function.

The pending-block state variable is a specific concern visible in the excerpt: it creates implicit ordering dependencies between iterations of the line loop. Code that depends on the value of a variable set in a previous loop iteration is notoriously hard to reason about under change.

Fan-Out 21
threshold: 15

I would recommend splitting PCTDecode into at least three pieces: a header-and-version validator, a line tokenizer/normalizer, and a frame-layout resolver that consumes the tokenized output. The pending-block pattern should become an explicit state machine or a two-pass approach rather than a flag threaded through a single loop. That decomposition would also make the CRLF normalization trivially unit-testable in isolation.


GetValue — src/utils/object/GetValue.js

GetValue
src/utils/object/GetValue.js
17.85
critical
CC 18
ND 6
FO 6
touches/30d 1

GetValue is a utility for deep property lookup with a fallback. The source excerpt shows it handles at least four distinct cases: null/numeric source short-circuit, direct hasOwnProperty hit on a primary source, direct hit on an alternate source, and dotted-key traversal across both sources simultaneously — with its own early-exit logic for each found/not-found combination at each depth level.

The hub-function pattern fires here, which is the right read: GetValue is almost certainly called throughout the codebase as the standard way to pull configuration values out of nested objects. That makes it a high-blast-radius function even at a moderate fan-out of 6, because the fan-in (the number of places that call it) is almost certainly very large.

Cyclomatic Complexity 18
threshold: 10

CC 18 with nesting depth 6 is the result of trying to handle both the single-source and dual-source traversal cases in one body, with a hand-rolled dot-notation parser layered on top. A concrete first step: separate the dotted-key traversal into its own GetDeepValue(source, keys, defaultValue) helper, then let GetValue call it. That alone would cut the cyclomatic complexity roughly in half and make the dotted-key logic independently testable — important given that edge cases in deep property access (missing intermediate keys, prototype-chain collisions) are subtle.


GraphicsWebGLRenderer — src/gameobjects/graphics/GraphicsWebGLRenderer.js

GraphicsWebGLRenderer
src/gameobjects/graphics/GraphicsWebGLRenderer.js
17.68
critical
CC 60
ND 5
FO 18
touches/30d 1

GraphicsWebGLRenderer has the highest raw cyclomatic complexity in this set at 60 — six times the moderate threshold. The source excerpt shows why: it is a command-buffer interpreter. It walks an array of drawing commands and dispatches each through a large switch statement, with separate path logic for BEGIN_PATH, CLOSE_PATH, FILL_PATH, and presumably many more drawing primitives. Each case is its own mini-state machine that modifies shared variables like path, lastPath, currentMatrix, and pathOpen.

Cyclomatic Complexity 60
threshold: 10

The nesting depth of 5 is actually lower than the other hotspots, which reflects the fact that the switch keeps individual cases flatter than chained if-else trees. But at CC 60, the sheer number of independent execution paths — each a potential test case — is the dominant concern. Fan-out of 18 means the renderer is directly invoking 18 distinct callees, including render node dispatch calls like customRenderNodes.FillPath || defaultRenderNodes.FillPath that introduce runtime polymorphism on top of the static coupling count.

This function was touched 1 time in the last 30 days and modified just 1 day ago. Any engineer adding a new drawing primitive or modifying path-close behavior right now is doing so inside a 60-path function with shared mutable state. The god-function and long-function patterns both apply.

The appropriate refactoring here is to extract each switch case — or at minimum each logical group of cases — into its own handler function, and to make the shared mutable state (path, currentMatrix, etc.) explicit as a context object passed between handlers rather than closed-over variables. That would reduce the per-handler cyclomatic complexity to single digits and make individual drawing primitives testable without constructing a full command buffer.


IsometricCullTiles — src/tilemaps/components/IsometricCullTiles.js

IsometricCullTiles
src/tilemaps/components/IsometricCullTiles.js
17.14
critical
CC 41
ND 7
FO 2
touches/30d 1

IsometricCullTiles determines which tiles in an isometric tilemap layer are visible within the camera frustum and need to be rendered. The source excerpt shows it handles four traversal orders — right-down, left-down, right-up, and presumably left-up — each implemented as its own pair of nested for loops with identical inner filter logic checking tile existence, index, visibility, alpha, and isometric bounds.

CC 41 with nesting depth 7 is the result of that repetition. The traversal-order branching at the top level, each branch containing double-nested loops with multi-condition guards, creates a large number of independent execution paths. What makes this function stand out from the others is its extremely low fan-out of 2. The complexity here is almost entirely internal — a self-contained maze of loops and conditionals rather than a coordination hub.

Cyclomatic Complexity 41
threshold: 10

The exit-heavy pattern applies to the inner loop: each tile goes through a series of guard conditions with continue statements before being pushed to the output array. That logic is identical across all four traversal orders, which is both the problem and the solution — the inner filter should be extracted into a shouldIncludeTile(tile, x, y, layer, camera, skipCull) predicate, and then each traversal order becomes a simple loop body that calls it. That extraction alone would drop the cyclomatic complexity substantially and make the culling predicate independently testable for edge cases like zero-alpha tiles or out-of-bounds indices.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
deeply_nested5
exit_heavy5
long_function4
god_function3
hub_function2

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/phaserjs/phaser
cd phaser
git checkout 1b8977a62fe2e7b632a57fa1c42a4377e0f6e364
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses