When profiling PHP code, how does an instrumenting profiler differ from a sampling profiler, and why can instrumentation mislead you about where time goes?
answer
- hook every call versus look periodically
- overhead scales with the number of calls
- exact call counts versus statistical shares
- tiny hot functions get inflated
- which one is safe in production
basics
~20 sAn instrumenting profiler hooks every PHP function entry and exit, giving exact call counts but adding cost per call; a sampling profiler records the current call stack at a fixed interval, with low, predictable overhead but only statistical results.
solid answer
~50 sAn **instrumenting** profiler, such as Xdebug's profiler or an extension hooking the engine's observer API, records a timestamp at every function entry and exit. You get exact call counts and per-call timings, but the overhead is proportional to the number of calls, so code that makes millions of tiny calls (getters, small closures, `array_map` callbacks) slows down far more than code that waits on a database. That skews the picture: cheap functions look expensive and I/O looks relatively smaller. A **sampling** profiler interrupts at a fixed interval, for example every 10 ms, and records the PHP call stack that is running; after thousands of samples, each function's share of samples approximates its share of time. Its cost depends on the sampling rate, not the code, which makes it usable in production, but it gives no exact call counts and can miss short, rare calls.
go deeper
Know the two families: one hooks every call, one samples the stack periodically. Remember that the first is slower and more detailed.
Explain why per-call overhead inflates cheap, frequently called functions and why sampling overhead depends only on the rate. Know when exact call counts are worth the cost.
Choose a profiler for the situation: instrumentation in reproduction for counts, sampling on a fraction of production traffic for truth. Cross-check surprising findings with hrtime spans before rewriting code.
Weigh always-on sampling against on-demand profiling for a PHP estate: overhead budget, who reads the profiles, and how profiling fits the release and incident process.
## Two ways to answer "where did the time go?" A profiler attributes a program's run time to its functions. There are two basic designs, and they fail differently. **Instrumenting** (also called tracing or deterministic) profilers insert measurement at every function boundary. In PHP this happens inside the engine: an extension registers hooks that the engine calls whenever a user function or an internal function begins and ends. Xdebug's profiler works this way, and so do extensions built on the engine's **observer API** (`Zend/zend_observer.h`), the hook point PHP offers extensions for function begin and end. Each hook reads a clock, and the profiler builds a record of who called whom, how many times, and for how long. **Sampling** (statistical) profilers do not touch every call. At a fixed interval, driven by a timer, they look at what the PHP engine is currently executing and record the full call stack of PHP functions. If a function appears in 30 % of 10,000 samples, it was on the stack for about 30 % of the time. Sampling can happen inside the process (an extension with a timer) or from outside it (a separate tool reading the PHP process's memory to reconstruct the stack). ## The trade-off | Aspect | Instrumenting | Sampling | |---|---|---| | What you get | exact call counts, every call timed | statistical share of time per stack | | Overhead depends on | the number of function calls | the sampling rate | | Typical cost | large; can multiply run time for call-heavy code | small and predictable | | Short, rare functions | always captured | may be missed entirely | | Distortion | inflates functions with many cheap calls | mostly unbiased, but noisy with few samples | | Production use | development or a reproduction environment | a fraction of production traffic | ## Why instrumentation misleads The overhead of instrumentation is not spread evenly. Every call pays a fixed cost for two clock reads and bookkeeping, and that cost is attributed to the function being measured. Consider a request that: - calls a small value-object getter two million times, each call normally costing a fraction of a microsecond; - runs one database query that takes 200 ms. Under instrumentation, the getter's per-call overhead can easily exceed its real cost, so the profile shows the getter as the dominant cost and the query as a minor one. The real request was the opposite. This is the **observer effect** of profiling: the measurement changes what is measured. Some consequences: 1. **Relative rankings shift** towards call-heavy code and away from I/O and internal functions doing large units of work. 2. **Absolute times are inflated**, so a profile's total does not match production latency. 3. **Timing-sensitive behaviour changes**: timeouts and lock contention can appear or disappear under a slowed-down run. Instrumentation is still the right tool when you need **exact counts**: "how many times is `PDOStatement::execute()` called for this page?" reveals a query-per-row loop that sampling would show only as a vague share of time. ## Why sampling is statistical A sampling profiler sees only what was running at each tick. That has its own limits: - **Resolution** depends on sample count: 20 samples say little, 20,000 say a lot. Profile long enough, or aggregate many requests. - **Short, rare work can vanish**: a 2 ms function that runs once per request may not be caught in one request's samples at all. - **Choice of clock matters**: a wall-clock timer includes waiting on I/O, a CPU-time timer shows only computation. Choose based on whether the request is waiting or computing. ## Choosing in practice - Reproducing a slow page locally or in staging with production-like data: an instrumenting profiler gives exact counts and a complete call graph. - Investigating production latency: a sampling profiler on a small percentage of requests, or on requests carrying a trigger, keeps overhead bounded. - Suspected per-call overhead distortion: confirm the finding with a sampling run or with `hrtime()` spans before rewriting code. Whichever you use, record the PHP version, OPcache and JIT settings and the data set alongside the profile, because a profile taken under different conditions answers a different question. The profile is evidence, and each profiler type biases that evidence in a known direction.
- Why would you still reach for an instrumenting profiler if sampling is less distorting?Because it gives exact call counts and a complete call graph. Many PHP performance bugs are about counts: a query executed once per row, a service rebuilt on every call, a regex compiled in a loop. A sampling profile shows the time share of such code but not that it ran 4,000 times. Use instrumentation in a reproduction environment to get the counts.
- A sampling profiler shows almost nothing in PHP functions but the request is slow. What is the likely cause?The profiler is probably sampling CPU time, while the request spends its time waiting on I/O. Waiting does not consume CPU, so it produces few samples. Switch to a wall-clock sampling mode, or add `hrtime()` spans around database and HTTP calls, to see where the blocking happens.
Instrumenting is a clerk who stamps a ticket every time anyone enters or leaves a room: complete records, but a queue forms at doors used thousands of times. Sampling is a guard who glances at a floor plan every minute and notes who is where: the doors stay clear, and over a long day the notes show where people spend their time.
saying these in an interview costs you the question
- Believes an instrumenting profiler's absolute timings match production latency.
- Enables an instrumenting profiler on every production request to find a slow path.
- Thinks a sampling profiler gives exact call counts for each function.
- Assumes profiler overhead is spread evenly across all functions.
- Trusts a sampling profile built from a handful of samples.