php-src has 4,487 critical-band functions out of 27,810 analyzed, and the top five by activity-weighted risk are all in the active tier, meaning I’m looking at live regression risk rather than a cleanup backlog. The leader, reflection_method_invoke in ext/reflection/php_reflection.c, has an activity-weighted risk score of 3.73 and 120 touches in the last year, and it was changed 0 days ago, so it is both enormously branchy and in motion right now. I would start there, then look at the two OpenSSL stream functions in ext/openssl/xp_ssl.c, which show up together because they share a file that changed 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
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
reflection_method_invoke | ext/reflection/php_reflection.c | 3.7 | 480 | 5 | 293 |
php_openssl_sockop_set_option | ext/openssl/xp_ssl.c | 3.7 | 69 | 7 | 36 |
zend_compile_args_ex | Zend/zend_compile.c | 3.6 | 67 | 6 | 29 |
php_openssl_sockop_io | ext/openssl/xp_ssl.c | 3.6 | 56 | 5 | 16 |
zend_jit_fetch_obj | ext/opcache/jit/zend_jit_ir.c | 3.6 | 116 | 6 | 77 |
Codemod / Tooling Files in Results
The libgd functions in context (_gdImagePngCtxWithOptions, gdImageTrueColorToPaletteBody, bmp_read_direct, _gdImageGifAnimAddCtx, gdft_draw_bitmap) live under ext/gd/libgd/, which is a bundled copy of the third-party libgd image library. They score highly on structural complexity but are quiet, and none appear in the Top 5. If you want to exclude them from your own analysis, use { "exclude": ["ext/gd/libgd/"] } in .hotspotsrc.json.
27,810 functions analyzed
Across the repository, 15,596 functions sit in active files and 12,214 in quiet ones. The distinction matters for how I read the list below: all five top hotspots are active, so none of them can be described as dormant debt. Each is complex and was modified within the last day or so. The context-only functions I mention at the end are the opposite case.
reflection_method_invoke — php_reflection.c
This is the shared implementation behind method invocation through the Reflection API, and the excerpt shows why it has so many paths: it branches on abstract methods, a variadic versus non-variadic parameter-parsing mode, static versus instance methods, instance-of checks, and a special case for Closure::__invoke(). A cyclomatic complexity of 480 is far beyond the 100+ I treat as extreme, and a fan-out of 293 means it calls nearly three hundred distinct functions, so a change here can ripple in ways that are hard to predict. Nesting depth is only 5, which suggests the complexity comes from breadth, many sequential decisions and calls, more than from deep nesting. The file has 144 total commits, a bug-fix rate of 14.6%, no reverts and no bug-linked commits, and 6 distinct authors in the last 90 days. The historical signals are mild; the risk here is structural size combined with 120 touches in the last year.
My recommendation is to split along the decisions the excerpt already makes. Extract the receiver validation (abstract check, static versus instance handling, instance-of check, closure signature check) into a helper that returns the resolved object and class, and keep the invocation itself in the original function. That separates the many error paths from the call path and gives each error path its own test target.
php_openssl_sockop_set_option — xp_ssl.c
This is the stream option handler for SSL sockets, and the excerpt shows a large switch over stream option constants. The first case, which builds the metadata array, has an inner switch on the TLS protocol version, conditional compilation for TLS 1.3, 1.2, 1.1, SSL3 and ALPN, and a nested early-data status switch. Preprocessor conditionals multiply the variants that actually compile, so the true number of configurations is larger than the cyclomatic complexity of 69 suggests. Nesting depth of 7 is well past the point where I stop trusting my own reading of control flow, and a fan-out of 36 adds coupling to the OpenSSL API surface.
The file has 85 total commits and a bug-fix rate of 29.4%, the highest among the five, with no reverts. It also has 9 distinct authors in the last 90 days and was changed 0 days ago. I would treat that bug-fix rate as a reason to review carefully, not as evidence that this function is defective. The most concrete action is to pull each case of the outer switch into its own static function, starting with the metadata case, which is self-contained and contains most of the depth.
zend_compile_args_ex — zend_compile.c
This compiles the argument list of a call into opcodes, and the excerpt shows it tracking several state flags at once: argument unpacking, named arguments, undefined-argument possibility, extra named arguments collected into a variadic, and partial application. The compile-time errors it raises, such as unpacking after named arguments or combining partial application with unpacking, are language-level rules encoded as branches. The excerpt also contains a TODO about tracking out-of-order named arguments, which tells me the function is still absorbing new language features. It has 84 touches in the last year, was last changed 1 day ago, and has 18 distinct authors in the last 90 days, the widest ownership spread in the top five.
This is the file with a recorded revert: 1 revert across 108 total commits, with a bug-fix rate of 24.1%. One revert is a small signal, but combined with a cyclomatic complexity of 67 and nesting of 6 it supports giving this function careful review. Because several people are editing it, I would extract the per-argument handling (unpack, named, positional) into separate helpers so concurrent changes touch different functions, and so the flag state they share becomes explicit parameters or a small struct.
php_openssl_sockop_io — xp_ssl.c
This is the read and write path for SSL streams. From the excerpt, it handles buffered-data supplemental reads, an INT_MAX clamp to prevent overflow in OpenSSL, blocking mode switching, timeout tracking with gettimeofday, a main IO loop, and a renegotiation rate-limiting shutdown. The control flow mixes socket mode state, timing and error handling in one loop, which is where I would expect the cyclomatic complexity of 56 to come from. Fan-out is a relatively modest 16, so the risk is more about reasoning through the loop than about coupling.
It shares file-level history with php_openssl_sockop_set_option: 54 touches in the last year, 85 total commits, a bug-fix rate of 29.4%, and 9 distinct authors in the last 90 days. In a C function that manages socket blocking state and restores it on several exit paths, I would specifically check that every early return puts the socket back into its original mode. A reasonable refactor is to separate the timeout bookkeeping into a small helper that computes remaining time, so the loop body only deals with the SSL call and its result codes.
zend_jit_fetch_obj — zend_jit_ir.c
This function generates JIT intermediate representation for object property fetches. The signature alone takes seventeen parameters, covering operand info, class entries, trace state, and result addressing, and the body branches on whether the JIT is in hot-trace mode, whether the access is on $this, whether the operand may be a reference, undefined or non-object, and the specific fetch opcode. A cyclomatic complexity of 116 and a fan-out of 77 put it in the extreme range on both axes. The file has 154 total commits, the most of any file in the top five, with 64 touches in the last year, a bug-fix rate of 24.0%, no reverts and 16 distinct authors in the last 90 days. It was last changed 1 day ago.
Seventeen parameters are a design signal in themselves: several are probably always passed together and could be grouped into a context struct for the operand and for the property access. I would do that first, since it shrinks every call site and every helper signature, and then split the trace-mode and non-trace-mode paths into separate functions.
Context: a quiet cluster in libgd
For contrast, the next-highest functions in the data are all in the bundled libgd code under ext/gd/libgd/. They are critical-band but quiet, each with 8 touches in the last year and 113 days since last change, with activity-weighted risk scores between 1.73 and 1.81. That is structural debt with high blast radius when next touched, not live risk, and it scores roughly half of what the top five do.
Where to start
I would start with reflection_method_invoke, because its cyclomatic complexity of 480 and 120 touches in the last year make it the largest single review burden in the list, and it changed today. Next I would take zend_compile_args_ex, where 18 distinct authors in 90 days and a recorded revert argue for smaller, separately owned helpers. Then I would handle the two OpenSSL stream functions together, since they share a file and a 29.4% bug-fix rate, and finish with zend_jit_fetch_obj, where grouping the seventeen parameters is the cheapest first step.
Reproduce This Analysis
git clone https://github.com/php/php-src
cd php-src
git checkout 088f299f1e577174aa0f94c26feb898ea4662d52
hotspots analyze . --mode snapshot --force
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 →