At commit c24ae61, ray-project/ray contains 26,578 analyzed functions, of which 2,261 are in the critical band and 7,221 fall into the ‘fire’ quadrant — high complexity and high recent activity simultaneously. The top-ranked function, from_config in rllib/utils/from_config.py, carries an activity-weighted risk score of 19.78 and was touched 1 time in the last 30 days just 3 days ago, meaning any engineer editing it today is working inside a cyclomatic complexity of 75 with 28 distinct callees and no margin for misreading a branch. I would start there, and then move immediately to _get_or_compile in the compiled DAG layer, where a cyclomatic complexity of 116 makes it the single most structurally dangerous function in this scan.
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
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
from_config | rllib/utils/from_config.py | 19.8 | 75 | 5 | 28 |
_get_or_compile | python/ray/dag/compiled_dag_node.py | 19.4 | 116 | 6 | 55 |
get_multi_agent_setup | rllib/algorithms/algorithm_config.py | 19.4 | 102 | 7 | 21 |
convert_to_numpy | python/ray/data/_internal/numpy_support.py | 19.3 | 27 | 4 | 14 |
confirm | python/ray/autoscaler/_private/cli_logger.py | 19.2 | 52 | 7 | 22 |
Large Repo Analysis
ray 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.
Repository-wide triage
Before narrowing to individual functions, it helps to see where the structural risk concentrates across the codebase.
26,578 functions analyzed
Every function hotspots flagged lands in either ‘fire’ or ‘watch’ — there are no dormant-complexity debt functions and no fully inactive low-risk functions in this snapshot. That means the structural risk in ray is not hiding in untouched corners: it is co-located with active development across the board. The 7,221 fire-quadrant functions represent code that is both hard to reason about and currently moving.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.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.Cyclic Hub×3Cyclic Hub
Participates in a call cycle with other high-traffic functions, creating circular dependency risk.Hub Function×2Hub Function
Many other functions call this one — a change here ripples widely through callers.
All five top hotspots share the god_function, long_function, complex_branching, and exit_heavy patterns simultaneously. That cluster is a specific structural signature: functions that have absorbed too many responsibilities over time, express them through dense branching trees, and have grown too long to refactor in a single sitting. In Python, where duck typing means fan-out coupling is resolved at runtime rather than at the call site, the hub_function and cyclic_hub patterns on two and three of these functions respectively amplify the blast radius further — a caller passing the wrong duck-typed argument may not surface the failure until deep inside one of these control-flow trees.
from_config — from_config.py
This function is RLlib’s universal object factory. The source excerpt makes its scope clear: it accepts a config that can be a dict, a string, a filename, a Python callable, a pre-instantiated object, or None — and it must resolve each of those cases into a constructed object using a class-level __type_registry__. That polymorphic dispatch responsibility is precisely what drives a cyclomatic complexity of 75. Each accepted input shape adds branches; each constructor-hint form adds more; error handling and deepcopy fallback paths add still more. The result is tagged with complex_branching, deeply_nested, exit_heavy, god_function, and long_function — every pattern in this scan — as well as cyclic_hub and hub_function, meaning it both calls 28 distinct functions and sits at the center of a call graph that routes through it repeatedly.
In Python, fan-out of 28 is more dangerous than it looks on paper. Because arguments are duck-typed, the branch that resolves type_ from a string key in __type_registry__ versus from a module path versus from a callable all exercise different subsets of those 28 callees, and a static reader cannot tell which path a given caller will actually take at runtime. The function was touched 3 days ago, making this a live regression risk, not backlog.
Recommendation: Extract each constructor-hint resolution strategy into its own private method — one for dict configs, one for string/filename resolution, one for callable hints, one for pre-instantiated passthrough. The type_ resolution block visible in the excerpt is a natural extraction seam. This would reduce the CC by distributing branches across smaller functions and make each path independently testable. The cyclic_hub pattern suggests some callers may be calling from_config for only one or two of these paths — a dispatch table or strategy object would let those callers call narrower, cheaper functions directly.
_get_or_compile — compiled_dag_node.py
This is the highest raw cyclomatic complexity in the entire scan at 116, and a fan-out of 55 makes it the broadest coupling point as well. From the docstring and excerpt, _get_or_compile handles DAG compilation: it allocates inter-actor communication channels in a breadth-first traversal of the DAG, wires up reader/writer relationships for each node, and sets up the submitter and output-fetcher handles that subsequent invocations will reuse. The function is designed to be idempotent — it caches results and returns early if already compiled — but the compilation path itself is a deeply nested BFS loop that branches on node type (ClassMethodNode, InputNode, MultiOutputNode), on whether a downstream node is a class method output, on whether a reader handle has already been seen, and on channel type.
A CC of 116 means there are at least 116 independent execution paths through this function. Each path is a required test case; most are probably not covered. The nesting depth of 6 means the deepest logic — the per-reader channel-type decisions visible in the excerpt — sits inside six levels of conditionals and loops, making it extremely difficult to reason about in isolation. Fan-out of 55 means a change here can touch over half of the distinct callees in the compiled DAG subsystem.
The function was touched 3 days ago. Given that Compiled DAG is one of Ray’s newer high-performance execution paths, active iteration on a function this complex is a meaningful regression risk.
Recommendation: The BFS traversal and the per-node channel allocation are two distinct concerns sharing the same function body. I would extract the channel-allocation logic for ClassMethodNode into a _allocate_method_channels helper, and the reader-deduplication and MultiOutputNode handling into a _resolve_readers helper. This would lower the effective CC of _get_or_compile to roughly the BFS scaffolding, and let the channel-type branching be tested as a unit.
get_multi_agent_setup — algorithm_config.py
This method compiles a fully-resolved multi-agent policy configuration from potentially incomplete inputs: it infers observation and action spaces from an env, a spaces dict, or stored config values; it resolves policy classes; it handles remote actor envs via ray.get; it handles gymnasium.vector.Env; it validates that agents mapping to the same policy ID don’t have conflicting spaces; and it constructs the is_policy_to_train callable. The docstring’s Raises section alone lists two distinct ValueError paths. A cyclomatic complexity of 102 and a maximum nesting depth of 7 reflect that responsibility honestly — each space-inference fallback adds a branch, and each agent-to-policy validation adds another nested check.
The nesting depth of 7 is the deepest in the top five, meaning some conditional logic sits seven levels deep inside the function. At that depth, the implicit state a reader must track to understand a single branch — which env type, which space source, which policy class fallback has been selected — spans most of the function’s local variable space. Exit-heavy is also flagged, meaning there are multiple early-return paths that short-circuit the setup under different conditions, each of which requires its own test to verify correctness.
Recommendation: The space-inference logic — trying env first, then the spaces dict, then stored config — is a well-bounded sub-problem that could be extracted into a _infer_spaces_for_policy method. Separating that from the policy-class resolution and the is_policy_to_train closure construction would make each sub-task testable in isolation and bring the CC of the outer method to a manageable range. The deeply_nested pattern here is particularly addressable with early-return guard clauses: flatten the outermost env-type branches by returning a partial result from a helper rather than nesting the rest of the function inside them.
convert_to_numpy — numpy_support.py
This function is Ray Data’s entry point for coercing arbitrary UDF column output into numpy arrays. The source excerpt shows a layered decision tree: if the input is already an ndarray, return immediately; if it’s a list of one array, use np.expand_dims as an optimization; if it’s a list of datetimes, delegate to a datetime converter; otherwise attempt a general conversion that handles array-like objects (e.g. torch.Tensor), inspects element shapes for heterogeneity, and falls back to a ragged ndarray representation for mixed-shape or byte-string data.
A CC of 27 is moderate rather than extreme, but the hub_function pattern is the more telling signal here: 14 distinct callees means this function touches a wide cross-section of the numpy and array-protocol surface, and in Python, each of those callees may behave differently depending on the runtime type of column_values. The exit_heavy pattern reflects the multiple early returns — at least three distinct return paths before the main conversion block, plus an exception re-raise. Each exit path is a coverage obligation, and the exception path in particular (Failed to convert column values) silently swallows the shape-inspection state before re-raising, which could make debugging conversion failures harder than it needs to be.
This function was touched 3 days ago. Ray Data’s map_batches API is a high-traffic code path, so any regression in type-coercion behavior would have broad user-facing impact.
Recommendation: Extract the ragged-vs-uniform shape detection logic — the loop that populates shapes and has_object — into a _classify_column_elements helper that returns a typed result. This would reduce the nesting in the main conversion block, make the shape-inspection logic independently testable, and separate the classification concern from the numpy construction concern. Given the hub_function classification, I’d also verify that callers aren’t relying on the specific exception message format, since that coupling would make future changes to the error path more expensive.
confirm — cli_logger.py
At first glance, a confirmation dialog function with a CC of 52 and a nesting depth of 7 looks like a mismatch between name and complexity. The source excerpt explains why: confirm is handling at least four distinct execution environments — non-interactive mode (raises immediately), --yes auto-confirm, Unix stdin readline with optional timeout, and Windows (where select is unavailable and the function must busy-poll msvcrt.kbhit() with a manual timer loop). The Windows path alone, visible in the excerpt, adds several nested branches just to handle character-by-character input detection and timeout logic. Layered on top of this are the _abort, _default, and _timeout_s control parameters, each of which adds conditional paths.
A nesting depth of 7 in a CLI logger function is a strong signal that platform branching and timeout logic have accumulated inside a function whose callers expect a simple bool return. The god_function pattern confirms it has grown beyond its original scope. Fan-out of 22 means it reaches into formatting utilities, platform detection, stdin, and possibly terminal libraries, all of which are implicitly coupled through the function’s kwargs forwarding.
Recommendation: The Windows polling loop is the most tractable extraction target — it can become a _read_with_timeout_windows helper that returns a string, symmetric with the Unix readline path. Pulling both platform-specific input-reading strategies out of confirm would flatten the nesting from 7 to roughly 4, reduce the CC meaningfully, and make each platform path independently testable without needing to mock the entire confirmation flow. The _abort and _default path logic could then be handled at the top of confirm via guard clauses rather than nested conditionals.
Context functions worth watching
Several functions from Ray Data also appeared in the broader scan in the ‘watch’ quadrant — _create_pyarrow_compute_udf in expressions.py, and max, min, sum in pandas_block.py. All were touched 3 days ago and carry activity-weighted risk scores around 10–11. Their structural complexity is low (CC of 3–4), so they are not refactoring priorities, but their recent activity in the same timeframe as convert_to_numpy suggests the Ray Data layer is in an active development push. If convert_to_numpy is being changed, reviewing whether those watch-quadrant neighbors are also in scope for the same PR is worth the five minutes.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 5 |
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
deeply_nested | 4 |
cyclic_hub | 3 |
hub_function | 2 |
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/ray-project/ray
cd ray
git checkout c24ae61b20b9242ee091ca21fd69f26d8a8bd391
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 →