Skip to content

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

Open
adrianbj wants to merge 21 commits into
nette:masterfrom
adrianbj:info-panel-opcache-without-scripts
Open

adrianbj wants to merge 21 commits 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.

dg and others added 21 commits August 18, 2026 13:15
Single helper replacing e.target everywhere; with shadow DOM coming,
composedPath()[0] is the element the event actually originated from.
Infrastructure for distributing styles as constructed stylesheets;
nothing consumes it yet.
CSS is no longer injected into the host page's document.head. Styles are
distributed as constructed stylesheets (adoptedStyleSheets) built from
the Tracy.css registry, so host-page styling cannot be affected by Tracy
and vice versa. This also makes Tracy styles work under CSP without
'unsafe-inline' style-src.

- Bar: <tracy-bar> shadow host, panels live inside a <tracy-div id=tracy-debug>
  wrapper kept for backward compatibility of third-party panel CSS; the
  panel popup window adopts the stylesheets explicitly in toWindow()
- BlueScreen: <tracy-bs> shadow host; the standalone error page keeps a
  document-level <style> so it renders without JavaScript
- standalone dumps: <tracy-dump> custom element with shadow root; upgrade is
  deferred while the parser is still constructing children
The shadow boundary now provides the isolation, so the #tracy-debug and
#tracy-bs selector prefixes and most of reset.css are no longer needed.
Layers are named tracy-base and tracy-components so they cannot collide
with the host page's own cascade layers (e.g. Tailwind).
setPointerCapture() replaces the document-level mousemove/mouseup +
touchmove/touchend pairs, works for touch input (which passive touchmove
listeners could not preventDefault), guarantees drag end delivery when
the pointer leaves the window, and removes the module-global dragging
flag. options.start is no longer invoked twice per drag.
…ssets

renderTemplate() no longer reads and minifies CSS/JS assets for templates
that do not embed them (agent markdown, AJAX content) and the expensive
object-graph traversal in findGeneratorsAndFibers() is computed once per
exception and reused by all render targets. The agent console block is no
longer emitted into .html files written by renderToFile().
…ustom properties

Adds 'auto' theme support driven by prefers-color-scheme. The .tracy-light
and .tracy-dark classes emitted by the Renderer keep working.
- BlueScreen panel callbacks may return a 'text' key with a markdown
  section included in renderAgent() output and .md log files
- new AgentPanel interface formalizes getAgentInfo() for Bar panels,
  method_exists() discovery still works
- Bar::renderAgent() isolates a throwing panel instead of aborting
- info panel provides a System Info section for agents
- redirect queue entries are logged to console in chronological order
…ception reports

- every logged exception appends a JSON Lines record (time, level, hash,
  message, report filename, source) usable by log aggregators and AI agents
- Logger::$retention ('30 days') deletes old .html/.md exception reports,
  checked opportunistically at most once a day
The header offers a button that copies the complete markdown report
(same content renderAgent() produces) to the clipboard - ready to paste
into an issue tracker or an AI assistant. The payload is embedded as an
inert JSON script inside the shadow root; the button appears only when
the Clipboard API is available.
Re-stamps request time and output-buffer level, clears timers, barDump
data, the live dump snapshot and per-request strategy state, and flushes
FileSession so deferred content of the previous request is persisted.
Intended for FrankenPHP worker mode, RoadRunner and Swoole.
- Bar lives in the top layer via the Popover API, no more z-index battles
  with host-page overlays and modal dialogs (with graceful fallback)
- toggles are keyboard-accessible (tabindex + Enter/Space activation)
- collapsed content uses hidden='until-found' so browser find-in-page
  reaches it and auto-expands the match
- Logger::$notifiers[] hooks for one-line Slack/Sentry/webhook integrations
- mailers receive the exception report filename as a third argument
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.
adrianbj added a commit to adrianbj/TracyDebugger that referenced this pull request Oct 22, 2026
…e (plus the OPcache patch) (#119)

* Remove Tracy's asset-response Expires/Pragma headers from TracyDebugger instead of patching core

PHP's session cache limiter queues "Expires: Thu, 19 Nov 1981" and
"Pragma: no-cache" before Tracy serves ?_tracy_bar= assets, which
contradicted the Cache-Control Tracy sends with them. TracyDebugger now
removes both headers itself just before Debugger::enable() on those
requests, so the local patch in tracy-2.12.x DeferredContent.php is
dropped (the bundle injection patch there remains for now).

* Restore upstream's BlueScreen footer emoji in tracy-2.12.x

The emoji was removed because a 4-byte character broke deferred content
stored in a utf8 (mb3) DB session. Tracy 2.10+ stores deferred content
with FileSession, and TracyDebugger only switches to NativeSession when
use_php_session is on and SessionHandlerDB is not installed, so the
content never reaches a DB session. Tested with SessionHandlerDB on a
utf8mb3 sessions table and use_php_session on: the AJAX BlueScreen
arrives intact.

* Add TracyDebugger's bar JS through Debugger::$customJsFiles instead of patched core properties

Tracy caches the bar bundle by URL for 10 days, but TracyDebugger's
$customJsStr/$customCssStr/$customBodyStr content depended on the request
(tracyHidden/tracyShow cookies, $showPanels, the session), so a browser
kept whichever variant it fetched first. The same URL gave a different
bundle with and without the tracyHidden cookie.

- the JS is built in $barJs and added as a content-hashed file in the
  TracyDebugger cache dir (barJsFile()), via the official customJsFiles,
  for every bundled Tracy version
- the tracyHidden/tracyShow checks read document.cookie in the browser
- the Dumps Recorder localStorage key is cleared when that panel isn't
  in the bar, instead of checking the session and $showPanels
- font-awesome is loaded for users who could open the File Editor (or
  when PW Info panel custom links are set), not based on $showPanels
- the font-awesome <link> and the show button are created as elements,
  not via innerHTML

tracy-2.12.x drops the $customCssStr/$customJsStr/$customBodyStr
properties from Debugger.php, and DeferredContent.php is now upstream's.

* Remove stale bar JS cache files when barJsFile() writes a new one

Each change to what feeds TracyDebugger's bar JS (config, module
version, a new front end/admin or privilege variant) left another
bar-<md5>.js in the cache dir, and nothing removed them. Writing a new
file now also deletes other bar-*.js files more than a day old; a
variant still in use is simply written again by its next request.

* Narrow Tracy's hover reset from TracyDebugger's main.js instead of patching reset.css

Upstream's "tracy-div:not(a b) *:hover { color: inherit; background:
transparent }" beats any lower-specificity color/background rule on
hover, so with it the Console and File Editor's Ace editor lost its
theme background and colors while hovered, as did keyboard-shortcut
boxes, filter-box counters, Viewports headings and Validator
highlights (62 elements with every front-end panel open). The colors
come from the user's Ace theme, so overriding them in styles.css
doesn't work.

main.js now finds that rule in each <style class="tracy-debug"> sheet
and sets its selector to "tracy-div:not(a b) > *:hover", the selector
the core patch used, on load and after Tracy.BlueScreen.loadAjax.
tracy-2.12.x's reset.css is upstream's again.

* Upgrade the tracy-2.12.x core to Tracy 2.12.2

tracy-2.12.x is now the v2.12.2 release as exported by git archive
(which also adds phpstan-stubs.php, previously left out) plus the three
remaining TracyDebugger patches, re-applied unchanged:
- Debugger::$disableShutdownHandler (Console)
- the Info panel's OPcache row (nette/tracy#623)
- the dump location below the dump, with its full path

2.12.2 adds PHP 8.6 support and hides /*sensitive{*/ regions in the
BlueScreen's markdown output; no API TracyDebugger uses changed. The bar
bundle URL now carries v=2.12.2, so browsers fetch a fresh bundle.

* Move dump locations below the dump from TracyDebugger's main.js instead of patching Dumper/Renderer.php

Tracy's global dump() and bdump() render without going through
TracyDebugger's PHP (Tracy's functions.php is loaded first), so the
layout is now changed client-side. main.js wraps Tracy.Dumper.init, which
runs for the page, panels, popups, AJAX BlueScreens, the Console and the
Dumps Recorder: each <a class="tracy-dump-location"> moves out of its
<pre> into a div.tracy-dump-location-container after it, without the pin
emoji, followed by the file's path in the markup Helpers::editorLink()
gives. The path shown has Debugger::$editorMapping applied, read from the
link's editor URL by matching it against the URL templates TracyDebugger
sets Debugger::$editor to (window.TracyDebuggerEditorTemplates, in the
bar JS).

Tested on pwtest with dump(), bdump() and bd(), for an external editor
and the online editor: the markup is identical to the patched core's.
tracy-2.12.x's Renderer.php is upstream's again.

* Skip Tracy's shutdown handler for the Console from TracyDebugger instead of patching Debugger.php

As suggested in nette/tracy#279: TracyDebugger registers a shutdown
function just before Debugger::enable(), so it runs before Tracy's.
When CodeProcessor sets TracyDebugger::$skipTracyShutdownHandler (as
before, when use_php_session is 1 or on Tracy 2.7), it keeps
error_get_last() in TracyDebugger::$consoleLastError for
tracyConsoleShutdownHandler, clears it, and turns the bar off, so
Tracy's handler has nothing to log or render. This works the same for
every bundled Tracy version.

Tested on pwtest by running a fatal (time limit) and a clean Console run
over curl, with and without X-Tracy-Ajax, with use_php_session 1 and the
default: the response, status files, deferred AJAX content and log files
match the patched core. Without the patch or this, Tracy logged each
fatal as an exception file.

tracy-2.12.x's Debugger.php is upstream's again; the copy now differs
from the v2.12.2 release only in the Info panel's OPcache row.

* Shorten the bar bundle's browser cache from 10 days to an hour

Tracy serves ?_tracy_bar=js with Cache-Control: max-age=864000 under a
URL that only changes with Tracy's version, so a TracyDebugger update
(main.js, styles.css, ...) or a settings change that feeds the bundle
(bar position, server type colors, ...) reached browsers up to 10 days
late unless they hard-reloaded. On those requests a
header_register_callback() now replaces it with max-age=3600 just before
the headers go out. The bundle body, the content(-ajax) responses and
normal pages are unchanged.

* Always add dumps-polling.js and exception-loader.js to the bar bundle

Both were added only when their panel (Dumps Recorder, Tracy Exceptions)
was in $showPanels, which varies with the sticky/once panel cookies, so
the bundle a browser cached decided whether they were there: with the
Dumps Recorder switched on after a page had cached a bundle without it,
the panel never polled. They are now always in the bundle, which is the
same for every panel selection, and dumps-polling.js only polls when the
Dumps Recorder panel is in the bar.

* Use !empty() for the PW Info panel links check that gates font-awesome

Since the font-awesome check no longer depends on $showPanels, the
count($this->data['customPWInfoPanelLinks']) in it runs for every user
without File Editor access, and count() throws a TypeError on PHP 8 if a
cleared setting was saved as ''.

* Fix the Console error handler logging @-suppressed errors on PHP 8

tracyConsoleErrorHandler skipped suppressed errors with
error_reporting() === 0, but since PHP 8 the @ operator lowers
error_reporting() to the fatal levels instead of 0, so every suppressed
warning in Console code (and inside libraries it calls, e.g. Tracy's
@http_response_code()) was shown as a red error block and logged. It now
tests the error's own level against error_reporting(), as TD's dump
error handler already does.

* Bump to 5.0.73.

* Run TracyDebugger's bar setup from Tracy's init hooks instead of polling every animation frame

Several scripts waited for an element by calling requestAnimationFrame
until it appeared, and some never appear: on every page with the bar,
bsZIndex (waiting for a BlueScreen) and the File Editor's
initializeEditor (waiting for its panel to be opened) ran 60 times a
second for as long as the page was open, and a panel popped out into
its own window ran seven such loops, since it has no bar.

main.js adds tracyDebuggerOnBar() and tracyDebuggerOnPanelInit(), which
run callbacks after Tracy.Debug.init and DebugPanel.prototype.init (also
if they throw partway). The hide link, the logo changes, the Dumps
Recorder state clean-up, dumps polling and the File Editor's recently
opened list use them; the show button gets its click handler when it is
created; and the BlueScreen's z-index is a CSS rule, so it applies to
every BlueScreen rather than the first one. Measured on pwtest: 0 loops
on a page (was 2) and in a popup (was 7).

* Keep barJsFile() from deleting a variant in use, and handle a failed write

The clean-up deleted any bar-*.js older than a day, including a variant
still in use whose file was written long ago, so a concurrent bundle
request could build without TracyDebugger's bar JS and browsers would
cache that. A file in use is now written again at most hourly, which
keeps its mtime recent, so only variants nothing has asked for in a day
are removed. A failed write (unwritable cache dir, full disk) removes its
.tmp file, logs an error and returns null, and the caller then adds no
file rather than one Tracy can't read.

* Only build the bar JS file on ?_tracy_bar= requests

Tracy reads Debugger::$customJsFiles only when it serves the bar bundle,
but every page view with the bar hashed TracyDebugger's bar JS and
checked (sometimes wrote) its cache file.

* Console: report fatal errors raised under @, and show @-suppressed errors in scream mode

tracyConsoleShutdownHandler still returned early on the pre-PHP-8
error_reporting() === 0 check, which on PHP 7 hid a fatal error raised
inside an @ expression; a fatal error is now always reported. And the
error handler's @ check now gives way to Tracy's scream mode
(forceScream), whose purpose is to show those errors.

This branch has not been deployed

No deployments
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.

2 participants