grafana/k6's react-dom test fixture carries the highest activity risk

All five top-scoring hotspots in grafana/k6 live in a single vendored-style static asset — react-dom.development.js — inside the browser module's test fixtures, each flagged critical with cyclomatic complexity ranging from 43 to 125.

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

Antipatterns Detected

complex_branching5deeply_nested5exit_heavy5god_function5long_function5

Run this on your own codebase

See if your own repo has a diffHydratedProperties-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 k6?

A god function is a single function that does so much work — calling many other functions, handling many distinct cases, and containing significant logic — that it becomes the load-bearing centre of a subsystem. In concrete terms, it means the function has high fan-out (many direct callees) and high cyclomatic complexity simultaneously, so changes to it ripple outward to many dependents and changes to any one of its callees can produce unexpected behaviour inside it. In k6's top five, `completeWork` calls 43 distinct functions while carrying 125 independent execution paths — a textbook example. The practical problem is that a god function is nearly impossible to test exhaustively in isolation, and any modification to it or its callees requires understanding the full interaction of all those paths and callees at once.

How do I reduce cyclomatic complexity in Go?

The primary technique is extract-method refactoring: identify a coherent sub-case within a large switch or if-else chain and move it to its own named function with a clear contract. A cyclomatic complexity above 15 is a reasonable trigger for considering a split; above 30 it warrants immediate action, and the functions in this analysis range from 43 to 125, well past that threshold. A concrete first step is to identify the largest `switch` arm in a function — for something like `completeWork`, each fiber-type case is a natural extraction boundary — and move it to a dedicated handler function. That single extraction can halve the visible complexity of the parent while making each case independently testable.

Is k6 actively maintained?

The quadrant data points clearly to active development: all 1,798 structurally complex functions in the repository fall into the fire quadrant, meaning they combine structural complexity with recent commit activity — there are zero debt-quadrant functions. The five top-scoring functions each carry an activity-weighted risk score above 19.93, have been touched twice in the last 30 days, and were last changed 5 days ago. The caveat worth noting is that the top five all live in a static test asset (a bundled React development build) rather than in k6's own Go code, so some of that activity is maintenance of the test infrastructure rather than the load-testing engine itself. Active development and structural complexity are not contradictory — they are the normal state of a growing project.

How do I reproduce this analysis?

The Hotspots CLI is available at https://github.com/nicholasgasior/hotspots. This analysis was run against grafana/k6 at commit `53b5727`. After checking out that commit with `git checkout 53b5727`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root to reproduce the full output. The same command works on any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk multiplies a function's structural complexity — derived from its cyclomatic complexity, maximum nesting depth, and fan-out — by a signal derived from how frequently the function has been touched in recent commits. The result is that a function with cyclomatic complexity 80 that hasn't been modified in two years scores lower than a function with cyclomatic complexity 20 that is touched every week, because the dormant function poses less near-term regression risk regardless of how complicated it looks. This prioritisation is intentional: it directs refactoring effort toward code that is both hard to reason about AND actively changing right now, where the probability of introducing a bug in the next sprint is highest. A purely structural score would send teams into old, stable code that happens to look messy but poses no immediate threat.

All five functions at the top of grafana/k6’s risk table originate from a single file: internal/js/modules/k6/browser/tests/static/react-dom.development.js. The highest scorer, diffHydratedProperties, carries an activity-weighted risk score of 20.76, combines a cyclomatic complexity of 96 with nesting 13 levels deep, and has been touched twice in the last 30 days — that combination of structural mass and recent activity puts it firmly in the “fire” quadrant, meaning it is a live regression risk right now, not a cleanup item for next quarter. k6 is Grafana’s open-source load-testing engine; across its 5,605 analysed functions, 556 are rated critical and 1,798 fall into the fire quadrant, so the risk pool is large — but the concentration at the top is striking.

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
diffHydratedPropertiesinternal/js/modules/k6/browser/tests/static/react-dom.development.js20.8961331
completeWorkinternal/js/modules/k6/browser/tests/static/react-dom.development.js20.1125743
diffPropertiesinternal/js/modules/k6/browser/tests/static/react-dom.development.js20.166817
commitDeletionEffectsOnFiberinternal/js/modules/k6/browser/tests/static/react-dom.development.js20.043913
describeNativeComponentFrameinternal/js/modules/k6/browser/tests/static/react-dom.development.js19.945816

Large Repo Analysis

k6 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

All five top hotspots live in internal/js/modules/k6/browser/tests/static/react-dom.development.js. This is a bundled development build of Facebook’s React DOM library, included as a static asset for the browser module’s integration tests. Its extreme complexity scores — cyclomatic complexity up to 125, nesting up to 13 levels — are a product of React’s internal architecture, not of k6’s own engineering. To exclude it from future Hotspots analysis, add the following to your .hotspotsrc.json: { "exclude": ["internal/js/modules/k6/browser/tests/static/"] }. If any local modifications have been made to this file, those changes should be extracted before excluding the path.

Where the risk lives

Before getting into individual functions, the distribution is worth a look.

Triage Band Distribution
Fire1798Watch3807

5,605 functions analyzed

Every function in the repository sits in either fire or watch — there are no debt-quadrant or ok-quadrant functions at all. That means nothing is both complex and dormant; wherever structural weight exists, there is also recent commit activity. The implication is that the usual “clean up the old stuff nobody touches” argument doesn’t apply here. The complex code is being actively changed.

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

Every top-five function carries all five antipattern flags simultaneously: complex branching, deep nesting, exit-heavy control flow, god-function coupling, and raw length. That’s not a coincidence — it reflects the nature of a large unmodified React development build bundled as a static test fixture. The patterns are consistent and severe.


Top 5 analysis

diffHydratedProperties — react-dom.development.js

diffHydratedProperties
internal/js/modules/k6/browser/tests/static/react-dom.development.js
20.76
critical
CC 96
ND 13
FO 31
touches/30d 2

diffHydratedProperties is responsible for reconciling DOM element properties during React’s server-side hydration pass. From the source excerpt, it opens with a large switch on the HTML tag name — dialog, iframe, video, audio, input, select, textarea, and more — attaching non-delegated event listeners for each element type before going on to compare the full property set of the incoming props against what the DOM already holds.

A cyclomatic complexity of 96 means there are 96 independent execution paths through this function. Every tag branch, every property comparison, every conditional listener attachment is a distinct path that could behave differently. Nesting reaches 13 levels deep — that’s the kind of depth where even tracing a single path by eye requires holding a stack in working memory. Fan-out of 31 means the function directly invokes 31 other functions, so a change to any one of those callees can produce a visible behaviour change here. The exit-heavy flag compounds this: multiple early-return and break paths mean test coverage requires hitting each exit independently.

Every recorded commit to this file has been tagged as a bug fix. That’s historical context, not proof of a current defect, but it does suggest this code has needed correction before.

My concrete recommendation: if this file is genuinely a vendored copy of React’s development build, exclude it from analysis (see the vendor note below) and stop treating it as first-party code. If it has been locally modified, isolate those modifications into a thin wrapper or override layer so the custom logic is visible and testable independently of the upstream bundle.


completeWork — react-dom.development.js

completeWork
internal/js/modules/k6/browser/tests/static/react-dom.development.js
20.14
critical
CC 125
ND 7
FO 43
touches/30d 2

completeWork is React’s fiber completion phase — it runs after a fiber’s children have been processed and is responsible for wrapping up work for every possible fiber type: function components, class components, host roots, portals, suspense boundaries, and more. The source excerpt shows the function opens with a switch on workInProgress.tag and then branches deeply inside each case to handle hydration state, context providers, flags like ForceClientRender, and dehydration recovery.

At CC 125, this is the most complex function in the top five — 125 independent paths. Fan-out of 43 is the highest across all five hotspots, meaning this single function directly calls 43 distinct functions. A change to anything in that call graph can produce an unexpected effect here, and with 2 touches in the last 30 days, that call graph is live. The nesting of 7, while lower than diffHydratedProperties, still crosses the threshold where reasoning about the full interaction of conditions requires careful attention.

The same recommendation applies: if this is an unmodified upstream React build, it should be excluded from analysis. If it cannot be excluded because the file is being modified, the immediate action is to document exactly which parts differ from upstream so reviewers know where to focus.


diffProperties — react-dom.development.js

diffProperties
internal/js/modules/k6/browser/tests/static/react-dom.development.js
20.1
critical
CC 66
ND 8
FO 17
touches/30d 2

diffProperties computes the update payload for a DOM element by comparing the previous and next prop sets. The source excerpt shows it starts with a tag-based switch to normalise props for input, select, and textarea elements, then iterates over the previous props to find deletions and the next props to find additions or changes. Inside those iteration loops, it branches on special prop names — STYLE, DANGEROUSLY_SET_INNER_HTML, CHILDREN, AUTOFOCUS, event listener registrations — each with its own update logic.

Cyclomatic Complexity 66
threshold: 10

A CC of 66 represents a large number of cases to test exhaustively. Nesting at 8 levels means the style-diffing loop inside the property-iteration loop inside the tag-switch produces deeply stacked conditionals. Fan-out of 17 means the function calls out broadly, including style utilities, event registration helpers, and validation functions. The exit-heavy pattern here — multiple continue statements inside the loop, plus conditional early assignment of updatePayload — means partial coverage is easy to achieve accidentally and full coverage requires deliberate scenario construction.

If this file is being kept in-tree intentionally (for example, to test the browser module’s interaction with a specific React version), pinning the version in a dependency manifest and importing the unbuilt source would be cleaner than maintaining a checked-in .development.js bundle.


commitDeletionEffectsOnFiber — react-dom.development.js

commitDeletionEffectsOnFiber
internal/js/modules/k6/browser/tests/static/react-dom.development.js
20.02
critical
CC 43
ND 9
FO 13
touches/30d 2

commitDeletionEffectsOnFiber handles the cleanup side of React’s commit phase — walking the fiber tree and applying unmount effects for each deleted node type. The source excerpt shows an outer switch on deletedFiber.tag with cases for host components, host text, dehydrated fragments, portals, function components, forwarded refs, and memo components. Each case manipulates a shared mutable stack (hostParent, hostParentIsContainer) before recursing into the subtree, then restores those values on the way back up.

Max Nesting Depth 9
threshold: 4

The nesting of 9 is the second deepest in the top five. The stack-save-recurse-restore pattern visible in the excerpt is a common source of correctness bugs when the function is modified: a new case that forgets to restore state, or a fall-through between cases (there’s already an eslint-disable-next-line-no-fallthrough comment in the excerpt), can leave the traversal stack in a corrupt state. With 2 touches in the last 30 days, this is a live regression risk, not a theoretical one.

Fan-out of 13, while lower than the other entries, still means 13 functions must behave correctly in concert. The actionable step here is the same as for the rest of the file, but if any part of this function has been locally patched, those patches should be extracted, documented, and tested in isolation before the file is excluded from future analysis.


describeNativeComponentFrame — react-dom.development.js

describeNativeComponentFrame
internal/js/modules/k6/browser/tests/static/react-dom.development.js
19.93
critical
CC 45
ND 8
FO 16
touches/30d 2

describeNativeComponentFrame generates a stack-frame string for a React component — used in development-mode error messages to show which component caused a problem. The source excerpt shows the function deliberately triggers controlled exceptions using Reflect.construct or fn.call with a Fake constructor that throws, then compares the thrown error’s stack trace against a control stack to isolate the component’s frame. It branches on whether construct mode is requested, whether Reflect is available, and handles several nested try/catch paths.

A CC of 45 for what is nominally a diagnostic utility is high. The nesting of 8 is driven by the layered try/catch/finally structure — exception-based control flow inherently adds branches that static analysis counts. Fan-out of 16 includes dispatcher manipulation (ReactCurrentDispatcher.current), log suppression, and cache lookups, giving the function a broad reach for something that produces a string. The exit-heavy flag reflects the multiple early-return paths (return '' on guard checks, return frame on cache hit) and the exception-path exits.

This function’s complexity is arguably intrinsic to what it does — deliberately triggering and catching errors to inspect stack traces is not a simple task. That makes it a good candidate for an integration test that validates the output format, rather than a refactoring target.


A note on the Go-native hotspots

Beyond the top five, several Go functions from the actual k6 codebase are worth noting for context — applySlowMo in browser/common/hooks.go, mapRequestEvent in browser/browser/request_mapping.go, and mapResponseEvent in browser/browser/response_mapping.go — all in the moderate/watch band with low structural complexity and 2 touches in the last 30 days. None of these warrant refactoring attention now, but mapRequestEvent and mapResponseEvent are worth monitoring: event-mapping functions in a browser automation context tend to grow as new event types are added, and watching their complexity trend over time is worthwhile. The throw function in internal/js/modules/k6/experimental/streams/errors.go carries the experimental_ path signal — an actively-touched error-handling utility in an experimental module is something I’d keep an eye on as that API stabilises.

Codebase Risk Distribution

All five top hotspots share the same structural patterns (complex_branching, deeply_nested, exit_heavy, god_function, long_function), which is typical of the highest-risk functions in any large codebase — they accumulate every structural signal on the way to the top. More useful context is how the risk is distributed across all 5,605 analyzed functions:

BandFunctions
Critical556
High1,242
Moderate2,693
Low1,114

Hotspot patterns 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/grafana/k6
cd k6
git checkout 53b5727d893d30a5c73e5b1a9891283786e0ec44
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