Skip to content

Bar: info panel gets OPcache status without the scripts table - #623

Open
adrianbj wants to merge 1 commit into
nette:masterfrom
adrianbj:info-panel-opcache-without-scripts
Open

Bar: info panel gets OPcache status without the scripts table#623
adrianbj wants to merge 1 commit into
nette:masterfrom
adrianbj:info-panel-opcache-without-scripts

Conversation

@adrianbj

Copy link
Copy Markdown
Contributor

Problem

info.panel.php calls opcache_get_status() with the default $include_scripts = true, which builds a 7-field entry for every script in the opcode cache. The cache belongs to the PHP process pool rather than the application, so on a machine serving several sites every vhost inflates the cost. That whole array is then used for a single membership test against get_included_files().

Measured on PHP 8.5.9 with 1365 cached scripts, on a request that included 232 files:

opcache_get_status()       →  156.92 ms
opcache_get_status(false)  →    0.06 ms

In that app it was ~157 ms of a ~250 ms request — more than half the time spent rendering the entire Bar, inside a panel that is usually never opened. It grows with the size of the cache, so the busier the server, the worse it gets.

(For context, the ReflectionClass loop for Classes + interfaces + traits in the same file, which looks like the more obvious suspect, measured 0.1 ms.)

Change

opcache_is_script_cached() answers exactly the question the row asks — "was this file served from the cache?" — with one hash lookup per included file, so $include_scripts can be false. jit, memory_usage, interned_strings_usage and opcache_statistics are all still returned, so the rest of the panel is untouched.

The rendered row does not change. Both implementations reported 100 % cached (231 of 232 files) on the same request. To check they agree when something genuinely isn't cached, I included a file that had just been written, so opcache.file_update_protection skipped it: old and new code both reported 99.1 %, and both identified the same two uncached files. The two path sets are directly comparable — $opcache['scripts'] keys and get_included_files() are both resolved absolute paths.

Side effect: the row no longer depends on $opcache['scripts'] existing at all, which was the subject of #271.

Notes

  • opcache_is_script_cached() has been available since PHP 5.5.11, well below Tracy's floor.
  • Both functions are gated by the same opcache.restrict_api setting, so they succeed or fail together; the $opcache && guard keeps the previous behaviour of showing when the API is restricted or OPcache is off. The extra function_exists() only covers a build that has the status function but not the per-script one.
  • No test added: the value is environment-dependent, so there is nothing stable to assert. Happy to add one if you'd like a particular shape.
  • The timings above come from a single machine (macOS, PHP 8.5.9, 1365 cached scripts). The absolute figure scales with cache size; the ratio is the point.

opcache_get_status() defaults to $include_scripts=true, which builds an
entry for every script in the opcode cache - a cache-wide list, so every
vhost sharing the PHP pool inflates it. The result was used only for a
membership test against get_included_files().

opcache_is_script_cached() answers that question per file with one hash
lookup, so $include_scripts can be false. Measured with 1365 cached
scripts on PHP 8.5: 156.92ms -> 0.06ms, with the row reporting the same
"100 % cached". jit, memory_usage, interned_strings_usage and
opcache_statistics are all still returned, so nothing else changes.
adrianbj added a commit to adrianbj/TracyDebugger that referenced this pull request Aug 18, 2026
…dies

Measured on PHP 8.5 / Tracy 2.12 with a warm opcode cache, a near-empty
front-end page: 250ms and 954KB of response, of which 205ms was spent in
Bar::renderPanels() and ~740KB was bar markup. Same page now: ~150ms and
317KB. A page with Tracy disabled is 70ms, so panel rendering was ~80% of
what Tracy added.

Bundled Tracy (all four version trees, marked "TracyDebugger patch:"):

- info panel: @opcache_get_status() materialises per-script stats for the
  whole opcode cache - cache-wide, so every vhost sharing the PHP pool
  inflates it. 156.92ms of the request with 1365 cached scripts. The row
  only needs "was each of this request's files cached", which
  opcache_is_script_cached() answers in 0.06ms with identical output
  (100% cached, 231 of 232 files, and both agree file-for-file when
  file_update_protection leaves some uncached). Submitted upstream as
  nette/tracy#623.
- asset responses carried Cache-Control: max-age=864000 next to a stale
  Expires: Thu, 19 Nov 1981 left by PHP's session cache limiter. Tracy
  already removed Pragma there; remove Expires too, and both in the
  deferred-content branch.

Console panel:

- the PW API autocomplete list (~200KB of JSON with descriptions) was
  inlined into every page whether or not the console was opened. It is
  now fetched from a tracyAutocomplete endpoint on editor init, with the
  cache key in the URL and an ETag. Only the current page's fields stay
  inline. Rebuilding the list also fixes an index reset that let the 108
  procedural functions overwrite the first 108 API variable entries.
- 92.8KB of the panel's JS was identical on every page load. Moved to
  scripts/console-panel.js, loaded via tracyJSLoader and cached per module
  version; the 10 values that were interpolated through the JS now travel
  on the tracyConsole config object. Panel body: 102,064B -> 14,746B.

Deferred panel bodies:

- panels listed in TracyDebugger::$deferrablePanels render a loading shell
  and fetch their body from the tracyPanelContent endpoint when the tab is
  first opened. Only panels whose content does not depend on the request
  that rendered the bar qualify. ProcessWire Info (saves 12.9ms and 45KB),
  ProcessWire Logs (66KB), Tracy Logs (68KB) and Methods Info (7.5KB) -
  the log panels gather entries in getTab() for the tab count, so those two
  save markup but not the gathering.
- deliberately not deferred: Debug Mode and Request Info (queries, timers,
  pages loaded, page/permission state all belong to this request), Panel
  Selector (tracyPanelsOnce is a single-use cookie the original request
  consumes), Tracy Exceptions (builds its own links from REQUEST_URI).
  Those need shutdown-time rendering, i.e. nette/tracy#613.
- the shell keeps the panel's own h1, because Tracy's Panel.init() captures
  h1 elements as drag handles at first open; only .tracy-inner is swapped.

README: note that the bar compresses 645KB -> 76KB and PW ships no
compression config.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant