The measurement pass

A few dozen requests, one at a time, from inside your own site. It is not a load test, and that is the design rather than a shortcut.

Why sequential

The capacity model needs service demand — how long one request occupies a worker with nothing else in the way — and then does the extrapolation itself, rigorously. Two things follow:

  • Measuring under load would give a contaminated number. A request measured while forty others are queueing already includes queueing delay, and the model would then queue it a second time. The error compounds in the direction that matters.
  • A load generator on the machine it is testing competes for the very workers it is counting. Run it from the same box and you measure a site that is several workers down; run it from elsewhere and you have set up infrastructure nobody has at the moment they need the answer.

So requests go out one at a time, and the site never notices. A pass is safe to run against production in a way a load test never is — which matters, because production is the only machine whose answer anybody wanted.

What it measures

QuantityHow
Wall timeInstrumentation inside the request, not the client’s stopwatch
CPU timegetrusage() — real process CPU, not a leftover
Database time and query countYii’s profiler, in a separate pass
Peak memorymemory_get_peak_usage()
Whether PHP ran at allAbsence of the instrumentation header

CPU is measured, not inferred

It would be easy to call everything that is not database time “CPU”. It would also be badly wrong for the case that matters most: a request that spends two seconds blocked on a payment gateway has consumed almost no CPU, and a model told otherwise will confidently recommend more cores that would not help by a single request per second.

So CPU comes from getrusage(), and what is left after real CPU and real database time is reported as what it is — blocked on something else. That figure is the most dangerous cost a request can carry, because it consumes a worker while consuming nothing else, and it is why a server can look idle at the exact moment the site stops responding.

Two passes, because the profiler is not free

Yii renders the raw SQL of every statement to use as its profiling label. That is real work, charged to the request being measured, and on a page running two hundred queries it is not a rounding error.

So Heat measures twice:

  • Timing pass — profiling off. Gives wall, CPU and memory.
  • Composition pass — profiling on. Gives query count and database time.

Database timings are taken around execution itself, so they are unaffected by which pass produced them, and the two compose without needing to be reconciled. A tool whose measurement inflates the thing it measures is the exact failure this exists to avoid.

How the cache shows up

Heat requests each page twice over: once with a query parameter no cache has seen, and once exactly as a visitor would get it.

If the second one comes back 200 with no instrumentation header, PHP never ran — a reverse proxy, a static cache or a CDN edge answered, and your origin was never involved. That request costs no worker, no connection and no query, and the model treats it that way. Blitz, Varnish, Cloudflare and friends all show up correctly with no configuration.

A scenario counts as cache-served only when most of its requests were. Half a cache is not a cache, and modelling it as free is the optimistic lie that makes a launch fail.

The floor

Heat also prices a Craft request that does nothing at all — routed, booted and answered without rendering a template or running an element query. It is usually the most surprising line in the report, and it is load-bearing: the editor and queue probes measure work that arrives as a request but is not one, so the floor is what makes their cost commensurable with a page’s.

The logged-in path

The most important scenario and the hardest to measure honestly. It cannot be faked by adding a cookie, because what makes it expensive is real: no shared full-page cache is usable, permissions get evaluated, and element queries carry the user’s status.

So Heat creates a throwaway account, signs in through the ordinary login action like any visitor, measures, and deletes the account. There is deliberately no privileged shortcut — an endpoint that logs a client in as a given user is a session-forging endpoint sitting in a plugin, the kind of convenience that turns into an advisory. Going through the front door costs two extra requests and nothing else.

Editor and queue load (Pro)

Neither can be measured by fetching a URL: one needs a control-panel session and a real entry to modify, the other does not involve HTTP at all. Both are measured in-process, doing the real work, with the same meter the request instrumentation uses.

The editor probe duplicates an entry that actually exists on your site, so the save carries the Matrix blocks, the relations, the search index terms, the revision and the invalidation of everything that referenced it. A blank entry saves quickly and proves nothing. The copy is disabled, kept out of any structure, and hard-deleted immediately.

It is a proxy, and the report says so: a duplicate writes new rows where a re-save updates existing ones, and it skips the diffing an editor’s changes would trigger. It is the closest thing to an editor save that does not require mutating content the site is actually serving.

How the samples become one number

The trimmed mean, not the median. Capacity is throughput, and throughput arithmetic wants the mean: if a request occupies a worker for a mean of 200ms, the pool completes five a second, however lumpy the distribution is. A median would understate exactly the site whose problem is that one page in twenty is dreadful.

But a raw mean lets one GC pause, a neighbouring container or a laptop deciding to index something move the answer by twenty percent — so the extremes are trimmed from both ends first. An implausibly fast sample is as much an artifact as a slow one.

Safety rails

  • Refuses to run on production unless explicitly allowed.
  • Only ever requests this installation’s own sites, compared on host and port — never a prefix match.
  • Hard caps on total requests and wall-clock time.
  • Aborts past an error-rate threshold: a pass that keeps going once the site starts erroring is no longer measuring the site, it is measuring the error page.
  • Reports why requests failed, tallied by reason, not just how many.

Using the results

Measured costs replace the estimates automatically and everywhere — there is no switch to forget to flip. A pass that has run is a pass whose numbers are in use. To go back:

php craft heat/measure/forget