In a PHP profiler's call graph, what is the difference between inclusive and exclusive time, and which one tells you what to optimize?
answer
- self time versus total time
- children are counted in one only
- follow the inclusive path downward
- call counts reveal loops of queries
- equal for a function that calls nothing
basics
~20 sInclusive time includes everything a function called; exclusive (self) time is its own body only. Follow inclusive time down to find the costly branch, then exclusive time and call counts to find the code to change.
solid answer
~40 s**Inclusive** time is everything from a function's entry to its exit, callees included, so the front controller always has close to 100 %. **Exclusive** (self) time subtracts the callees and leaves only the function's own work. Neither alone is enough. Sort by inclusive and walk down from the entry point to find which branch holds the cost: controller, then service, then repository. Then sort by exclusive to find hot leaves, such as a `preg_match()` called 40,000 times or `PDOStatement::execute()`. Check **call counts** as well: a function with modest per-call time but 3,000 calls usually means a loop that should be a batch. Internal functions show up as leaves, and a large exclusive time on `curl_exec()` or `PDOStatement::execute()` is waiting, not computation. In a flame graph, a frame's width is its inclusive time.
go deeper
Remember the definitions: inclusive counts the callees, exclusive is the function's own body. A function that calls nothing has equal inclusive and exclusive time.
Walk a call graph out loud: inclusive downwards to the costly branch, exclusive to the hot leaf, call counts to spot loops. Distinguish waiting leaves from computing leaves.
Show that you read profiles for structure: a query-per-row loop hides in call counts and inclusive time, not in any single slow line. Mention recursion as a source of misleading inclusive totals.
Talk about making profiles comparable across releases, so a team can diff inclusive time and call counts per endpoint instead of reading each profile from scratch.
## Two numbers per function A profiler that records a PHP request builds a **call graph**: a node for each function (user functions and internal ones like `json_encode()` or `PDOStatement::execute()`), and an edge for each caller-to-callee relationship, annotated with call counts and times. For every node it reports two timings: - **Inclusive time** (also called total or cumulative time): the time between entering the function and leaving it, **including** all the functions it called. - **Exclusive time** (also called self time): inclusive time **minus** the inclusive time of its callees; the time spent executing the function's own statements. For a function that calls nothing, the two are equal. For the front controller `index.php`, inclusive time is essentially the whole request while exclusive time is tiny. ## A worked example | Function | Calls | Inclusive | Exclusive | |---|---|---|---| | `{main}` | 1 | 1,000 ms | 10 ms | | `ProductController::show` | 1 | 990 ms | 10 ms | | `ReviewRepository::forProduct` | 1 | 820 ms | 40 ms | | `PDOStatement::execute` | 241 | 780 ms | 780 ms | | `TemplateRenderer::render` | 1 | 160 ms | 100 ms | | `htmlspecialchars` | 12,400 | 60 ms | 60 ms | Reading it: 1. Walk **inclusive** time down from the top: `{main}` to `ProductController::show` to `ReviewRepository::forProduct`, which holds 820 of 1,000 ms. That is the expensive branch. 2. Look at the **exclusive** time inside that branch: almost all of it is in `PDOStatement::execute`. 3. Look at the **call count**: 241 executions for one page. The repository runs a query per review, or per related row, inside a loop. The fix is one batched query, not faster PHP. 4. Note what not to chase: `htmlspecialchars` has 12,400 calls but only 60 ms. It looks busy in a call-count view and is not the problem. ## Why you need both - Sorting only by **inclusive** time puts wrappers at the top: `{main}`, the kernel, middleware, the controller. They are on the path, but they are not doing the work. - Sorting only by **exclusive** time finds hot leaves, but hides a structural problem: a service method whose own code is cheap but which calls a costly function too often has low exclusive time and high inclusive time. - **Call counts** convert "this is slow" into "this runs too often", which is the most common shape of a PHP performance bug. ## Internal functions and waiting In PHP profiles, the leaves are usually internal functions written in C. Their exclusive time has two very different meanings: - **Computation**: `preg_match()`, `json_encode()`, `usort()`, `password_hash()`. The time is CPU, and reducing calls or input size helps. - **Waiting**: `PDOStatement::execute()`, `curl_exec()`, `file_get_contents()` on a URL, `fread()` on a socket. The time is the other side's latency; the PHP call is just where the process was blocked. Knowing which is which decides whether you optimize code, reduce round-trips, or look at the database or remote service. ## Recursion and flame graphs - **Recursion** complicates inclusive time: when `render()` calls `render()`, a naive sum counts nested time more than once. Profilers handle this differently, so be careful with recursive templates and tree walks. - A **flame graph** draws each stack as a tower of boxes: a box's **width** is its inclusive time, and the part of a box not covered by boxes above it is its exclusive time. Wide plateaus at the top are hot leaves; a wide box with a narrow top is a function that is expensive because of what it calls. - A **call graph** view (nodes and arrows) shows the same data plus call counts, which a flame graph merges away. ## A reading checklist When you open a profile of a slow PHP request, go through the same questions each time: 1. What is the total, and does it match the latency you measured with timers? 2. Which child of the entry point holds most of the inclusive time? 3. Inside that branch, which leaves have the most exclusive time, and are they waiting or computing? 4. Which functions have call counts that grow with the data rather than staying fixed per request? 5. Which of these changed compared with a known-good profile? Start from inclusive time to find where, use exclusive time to find what, and use call counts to find how often.
- A profile shows PDOStatement::execute() with the highest exclusive time. Does that mean PDO is slow?No. `execute()` sends the query and blocks until the database answers, so its exclusive time is mostly the database's latency plus the network round-trip. Check the call count and the SQL: many small executions suggest a query-per-row loop; one long execution suggests a slow query plan. PDO's own overhead is rarely the cause.
- Why can a function with low exclusive time still be the best thing to optimize?Because its decisions drive the cost of its callees. A repository method that loops over 200 rows and queries once per row has little self time, but its inclusive time contains all 200 queries. Changing that method to issue one batched query removes most of the cost, even though no single line inside it was slow.
saying these in an interview costs you the question
- Optimizes the function at the top of the inclusive list, such as the front controller.
- Reads high exclusive time in PDOStatement::execute() as proof that PHP code is slow.
- Ignores call counts and looks only at time columns.
- Thinks exclusive time includes time spent in the functions it calls.
- Adds up the inclusive times of nested functions and expects the request total.