bevy: rendering pipeline carries the highest risk — 5 functions to fix first

Five critical-band functions in bevy's rendering, glTF, and tooling layers are both structurally complex and actively changing, with the top hotspot carrying a cyclomatic complexity of 83 and two commits in the last 30 days.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk20.29Low
Hottest Functionload_gltf

Antipatterns Detected

complex_branching5god_function5long_function5deeply_nested4exit_heavy4

Run this on your own codebase

See if your own repo has a load_gltf-style hotspot — run this in any local git repo:

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

Key Points

What is a god function and why does it matter in bevy?

A god function is a single function that takes on too many responsibilities — it owns the full lifecycle of a feature rather than delegating to smaller, focused helpers. In structural terms, it shows up as a combination of high cyclomatic complexity, high fan-out (many distinct functions called), and length that defies easy review. The concrete problem is coupling: when a function calls 72 other functions, as `load_gltf` does, a behavioral change in any one of those callees can produce an observable difference inside the god function that only surfaces for a specific input combination. In bevy's codebase, all five of the highest-risk functions carry the god_function pattern, meaning the rendering pipeline and asset loading layer are each centralized in ways that make isolated testing and safe incremental change harder than they need to be.

How do I reduce cyclomatic complexity in Rust?

The primary technique is extract-method refactoring: identify each top-level conditional branch or match arm and move its body into a named function with a clear return type. In Rust, this often means converting large `match` arms into functions that return `Result` or `Option`, which also makes early-return logic explicit rather than buried. A cyclomatic complexity above 15 warrants splitting; above 30 it warrants immediate attention before adding new branches. A concrete first step for `derive_as_bind_group` (CC 83) would be to extract the `#[bindless]` attribute parser into its own `parse_bindless_attr` function — that single extraction removes the deepest nested branch from the main body and immediately reduces the path count reviewers must track.

Is bevy actively maintained?

The data shows clear active development: all 1,438 fire-quadrant functions are both structurally complex and receiving recent commits, and the top five hotspots were all touched within the last five days of the analyzed commit. `prepare_lights` alone received three commits in the last 30 days from three distinct authors. The absence of any 'debt' quadrant functions in the dataset means there are no high-complexity functions that have gone completely untouched — everything complex is being actively worked on. Active maintenance and structural complexity are not mutually exclusive; the complexity in these functions reflects the breadth of what bevy's rendering pipeline handles, not neglect.

How do I reproduce this analysis?

The analysis was run against commit `9f4ff89` of bevyengine/bevy using the Hotspots CLI, available at github.com/hotspots-dev/hotspots. After checking out that commit with `git checkout 9f4ff89`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no hotspots account or project setup is required for a local snapshot.

What does activity-weighted risk mean?

Activity-weighted risk multiplies structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — by recent commit frequency. A function with cyclomatic complexity 80 that has not been touched in two years scores considerably lower than one with cyclomatic complexity 20 that is committed to every week, because the dormant function poses lower near-term regression risk despite its structural complexity. The prioritization helps focus refactoring effort where it reduces the probability of bugs being introduced right now, rather than simply identifying code that looks complicated in the abstract. In bevy's top five hotspots, the scores range from 16.94 to 20.29 — all critical band — precisely because these functions combine genuine structural complexity with commits that landed in the last two to five days.

Across 18,642 functions in bevyengine/bevy, 293 are in the critical band — and every single one of the top five hotspots sits in the ‘fire’ quadrant, meaning high structural complexity combined with active commit churn right now. The top-ranked function by activity-weighted risk, load_gltf, carries a cyclomatic complexity of 69 and a fan-out of 72 — the highest in this dataset — giving it a risk score of 20.29. I’d start there and with derive_as_bind_group before any rendering sprint this week — these aren’t cleanup items deferred to a backlog. They are live regression risks on code that is changing today.

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
load_gltfcrates/bevy_gltf/src/loader/mod.rs20.369772
derive_as_bind_groupcrates/bevy_render/macros/src/as_bind_group.rs19.483721
assign_objects_to_clusterscrates/bevy_light/src/cluster/assign.rs19.470846
maintools/example-showcase/src/main.rs17.952533
prepare_lightscrates/bevy_pbr/src/render/light.rs16.957446

Large Repo Analysis

bevy 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.

Quadrant overview

The quadrant picture for bevy is unusually clean in one direction and alarming in another. There are no ‘debt’ or ‘ok’ functions in the dataset — everything is either actively moving or low-complexity and being watched.

Triage Band Distribution
Fire1438Watch17204

18,642 functions analyzed

That means 1,438 functions combine meaningful structural complexity with recent commit activity. The five I’m focusing on are the ones where the structural complexity is high enough — cyclomatic complexity in the 52–83 range — that any one of those commits represents real regression risk.

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

Every top hotspot carries the god_function and long_function patterns simultaneously. That combination — broad coupling through high fan-out plus sheer length — is what makes these hard to test in isolation and hard to review in a pull request. Four of the five also exhibit deeply nested control flow, and four carry multiple exit paths that add to the test-coverage burden.


load_gltf — mod.rs

load_gltf
crates/bevy_gltf/src/loader/mod.rs
20.29
critical
CC 69
ND 7
FO 72
touches/30d 1

load_gltf is the top activity-weighted risk function in the repository at 20.29. It is the async entry point for parsing an entire glTF file — buffers, textures, animations, scenes, nodes, skins — and translating that into bevy’s asset graph. The source excerpt reveals its structure: it first validates or skips validation based on settings, dispatches to registered extensions via on_root, resolves the file path, loads buffers asynchronously, computes linear texture sets, and then — conditionally compiled behind #[cfg(feature = "bevy_animation")] — walks every scene’s node tree to collect animation paths, followed by a second conditional block that processes animation channels, samplers, and keyframe data using a deeply nested reader pattern.

A fan-out of 72 is the highest in this dataset and the sharpest signal here. This function directly calls 72 distinct functions, meaning a change to any one of those callees — a buffer loader, a coordinate converter, an animation curve type — can produce a behavioral change inside load_gltf that only manifests for a specific combination of glTF features. The nesting depth of 7 compounds this: the animation channel processing visible in the excerpt nests iterator chains inside conditional feature blocks inside match arms.

Fan-out (distinct callees) 72
threshold: 20

The file was touched once in the last 30 days, five days ago, and its sole recent commit was a bug fix. With 72 call sites and 69 independent paths, even a targeted fix has a wide blast radius.

Recommendation: The most tractable first cut is to extract animation loading — everything inside the #[cfg(feature = "bevy_animation")] blocks — into a dedicated load_animations function. That single extraction removes a large conditional subtree and meaningfully reduces the path count. After that, buffer loading, texture setup, and scene graph traversal are each natural boundaries for further extraction. The goal is for load_gltf to read as an orchestration function that delegates to named sub-loaders rather than implementing all of them inline.


derive_as_bind_group — as_bind_group.rs

derive_as_bind_group
crates/bevy_render/macros/src/as_bind_group.rs
19.43
critical
CC 83
ND 7
FO 21
touches/30d 2

This is a procedural macro that derives the AsBindGroup trait — the mechanism by which materials declare their GPU resource bindings at compile time. Its job is to parse struct-level and field-level attributes and emit the token stream that wires textures, samplers, uniform buffers, and bindless resource tables into the render pipeline. Based on the source excerpt, it does all of that in a single function body: reads manifest paths, accumulates binding state vectors for both bindless and non-bindless layouts, processes a first attribute pass to detect #[bindless] and its nested modifiers (limit, index_table, range, binding), then continues into per-field binding logic.

A cyclomatic complexity of 83 means there are 83 independent paths through this macro. Each path corresponds to a different combination of attribute configurations a downstream user might write, and each is a required test case for correctness. The nesting depth of 7 — visible in the excerpt where parse_nested_meta calls contain inner parse_nested_meta calls — means a reviewer tracing any one path must hold seven levels of context simultaneously. The fan-out of 21 is moderate compared to some functions below, but in a procedural macro context it represents 21 distinct syn/quote API calls whose argument order and type expectations must all be correct for the emitted code to compile.

Cyclomatic Complexity 83
threshold: 30

Two commits have touched this file in the last 30 days, the most recent four days ago. The file’s historical commit mix shows about half of its commits have been bug-fix tagged, which is consistent with the complexity: when a macro this branchy gets a new attribute variant, adjacent paths regress.

Recommendation: Extract each top-level attribute variant (#[bindless], #[bind_group_data], field-level binding types) into its own parsing function that returns a typed intermediate representation. The main derive function should then orchestrate those parsed results into code generation rather than interleaving parsing and emission. That decomposition alone would reduce CC by routing the 83 paths into smaller, independently testable units — and would make adding the next bindless modifier a matter of touching one self-contained parser rather than extending the monolith.


assign_objects_to_clusters — assign.rs

assign_objects_to_clusters
crates/bevy_light/src/cluster/assign.rs
19.43
critical
CC 70
ND 8
FO 46
touches/30d 1

This function clusters point lights, spot lights, light probes, and decals into the tile/cluster grid used for forward+ rendering. Its signature alone — visible in the excerpt — is a declaration of scope: it accepts five separate ECS queries (views, point lights, spot lights, light probes, decals), three Local state values for persisted scratch buffers, and a global cluster settings resource. The doc comment even includes a scheduling constraint: it must run before update_point_light_frusta.

The nesting depth of 8 is the highest in this dataset and a strong refactoring signal on its own. Eight levels of nested control structures means the innermost logic — the actual cluster assignment math — is surrounded by eight layers of guard conditions, iterator adapters, and type-dispatch branches that a reader must parse before reaching the computation. The cyclomatic complexity of 70 reflects the combinatorial product of light types, visibility states, render layer membership, and GPU clustering mode. Fan-out of 46 means this function reaches broadly into the rendering infrastructure.

Max Nesting Depth 8
threshold: 4

One commit touched this file in the last 30 days, five days ago, tagged entirely as a bug fix. Single-author ownership over that period means there is limited shared context on this function’s invariants across the team.

Recommendation: The GPU/CPU clustering branch (if global_cluster_settings.gpu_clustering.is_none()) is the most natural seam: extract CPU-path collection into a collect_clusterable_objects_cpu helper and GPU-path collection into its counterpart. Within each, the per-light-type extend calls are further extraction candidates. Reducing the nesting depth from 8 to 4–5 is the primary goal — each level removed makes the scheduling constraint and the render-layer visibility logic independently reviewable.


main — main.rs

main
tools/example-showcase/src/main.rs
17.87
critical
CC 52
ND 5
FO 33
touches/30d 1

The main function of the example-showcase tool is the CLI entry point that orchestrates running bevy examples for screenshot capture and CI validation. From the excerpt, it parses arguments, validates mutual constraints between --page and --per-page, then dispatches into a large match cli.action block. The Action::Run arm alone destructures eleven fields and contains further nested match logic over combinations of stop_frame, screenshot_frame, and auto_stop_frame — each arm writing a different .ron configuration file and assembling --features flags before launching examples.

A cyclomatic complexity of 52 for a CLI main function is high. CLI argument parsing typically keeps CC low by delegating to subcommand handlers; here, dispatch, validation, file I/O, and example-launch logic are all co-located. The exit-heavy pattern — four out of five top functions share it — is particularly notable here: the excerpt shows multiple .exit() calls on error paths, meaning test coverage requires exercising each argument validation branch through a real process boundary or significant mocking.

This is a tooling file rather than a runtime crate, so its blast radius on bevy users is lower. That said, a CI system that depends on this tool for screenshot validation will feel any regression immediately.

Recommendation: Extract each Action variant’s logic into a dedicated handler function — run_examples(...), generate_showcase(...), and so on. The main function should resolve to argument parsing plus a dispatch table of at most one level of match depth. This is the standard extract-method refactoring for CLI entry points, and it would bring CC below 10 while making each action independently testable without invoking the full binary.


prepare_lights — light.rs

prepare_lights
crates/bevy_pbr/src/render/light.rs
16.94
critical
CC 57
ND 4
FO 46
touches/30d 3

prepare_lights is the render-world system that takes extracted light data and writes GPU buffers: it allocates shadow map textures, classifies point and directional lights, enforces per-view limits (with Local<bool> warning flags for when maximums are exceeded), manages RetainedViewEntity sets for live shadow mapping, and feeds the results into the clusterable object metadata and light meta resources. The signature spans three grouped parameter tuples and seven queries — a layout that reflects how much state this system must coordinate in a single pass.

This function has the highest recent activity of any hotspot here: three commits in the last 30 days, the most recent two days ago, from three distinct authors in the last 90 days. That combination — CC 57, fan-out 46, three recent committers — is the definition of live regression risk. When multiple authors are independently modifying a function with 57 execution paths, the probability that any one change invalidates an assumption held by another is non-trivial. Two of its three recent commits are tagged as bug fixes, which reinforces that pattern.

Cyclomatic Complexity 57
threshold: 30

The nesting depth of 4 is the lowest among the top five, which actually makes this the most tractable starting point for incremental decomposition: the branching is wide rather than deep, so the paths are enumerable without first unpacking layers of nesting.

Recommendation: Given three active authors, I’d prioritize this one for an explicit decomposition PR before any further feature work. Extract shadow texture allocation into allocate_shadow_textures, directional light limit enforcement into enforce_directional_light_limits, and the per-view light classification loop into classify_lights_for_view. Each extracted function reduces the shared mutable state surface that concurrent contributors need to reason about and gives the three active authors clearer ownership boundaries going forward.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
god_function5
long_function5
deeply_nested4
exit_heavy4

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.

See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function.

Reproduce This Analysis

git clone https://github.com/bevyengine/bevy
cd bevy
git checkout 9f4ff89c1a6aa49efe0ade126ed67c948121a30b
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 →

Was this useful? Let me know →

Related Analyses