Skip to content

Restart with OPcache's file cache when WP_CLI_OPCACHE is set - #6439

Open
swissspidy wants to merge 4 commits into
mainfrom
claude/opcache-restart
Open

swissspidy wants to merge 4 commits into
mainfrom
claude/opcache-restart

Conversation

@swissspidy

@swissspidy swissspidy commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Each wp run compiles WP-CLI, WordPress and all active plugins again. PHP's own default for opcache.enable_cli is 0, though distributions, Docker images and hosts may turn it on. But even with it on, OPcache keeps compiled code in shared memory, which only lasts for one process. Only the file cache (opcache.file_cache) keeps it across runs, and it isn't set by default.

With WP_CLI_OPCACHE=1, WP-CLI now restarts PHP once with the file cache enabled. Compiled code is then stored on disk and reused by the next run.

This is the idea behind PHPStan always running with OPcache (PHPStan 2.3). It's opt-in for now, as a prototype.

How it works

  • Turning it on: set WP_CLI_OPCACHE as an environment variable, or in the env: section of a config file:

    env:
      WP_CLI_OPCACHE: 1
  • New bootstrap step: RestartWithOpcache runs after the config is loaded and the logger and formatter are set up. That's before the shutdown handler, the root check, --require files and packages, so those only run in the restarted process.

  • composer/xdebug-handler does the restart (new dependency, ^3.0.3). The bundle already ships it via composer/composer. It:

    • writes a temporary php.ini with the current settings, including -d options and WP_CLI_PHP_ARGS;
    • passes STDIN, STDOUT, TTYs and the exit code through;
    • handles Windows and signals.

    OpcacheRestarter extends it: requiresRestart() decides when to restart, and restart() adds the OPcache options.

  • Only the classes the restart needs are loaded before restarting (Composer\XdebugHandler, Composer\Pcre), through WP-CLI's own autoloader. Loading the Composer autoloader there would also include every bundled command's registration file in a process that's about to be replaced. With it, the parent took ~280 ms before restarting; now it's ~15 ms.

  • When it restarts: whenever the file cache isn't in use yet. That includes PHP setups that already have opcache.enable_cli=1 without a file cache.

  • Skipped when:

    • WP_CLI_OPCACHE isn't set, or OPcache isn't loaded;
    • opcache.enable=0 (it defaults to 1, so this is a deliberate opt-out, and nothing would be cached);
    • the file cache is already configured, e.g. in php.ini;
    • Xdebug is active;
    • the cache directory isn't writable, or proc_open() is disabled.

    WP-CLI then runs as before.

  • Subprocesses: processes WP-CLI launches itself (launch_self(), runcommand() with launch) restart in turn. The restarted process clears xdebug-handler's restart marker, so they don't mistake themselves for a restarted process.

OPcache settings

The restarted process keeps the user's other settings. That includes OPcache settings from php.ini that had no effect while OPcache was off for the CLI, for example ones tuned for PHP-FPM. So these are always set:

Setting Why
opcache.enable_cli=1 Turns OPcache on for the CLI.
opcache.file_cache=<cache dir>/opcache WP_CLI_CACHE_DIR (environment or config) or ~/.wp-cli/cache. opcache/ is created with 0700: a cache directory other users can write to would let them inject compiled code.
opcache.file_cache_only=1 No shared memory. It's faster for one short-lived process (see below), and a preload script configured for the web server (opcache.preload) isn't run in the CLI. JIT needs shared memory, so it's off; it made no difference for WP-CLI here.
opcache.save_comments=1 Commands are defined by their doc comments. With 0, CommandFactory falls back to re-reading each command's source file, and plugins that read doc comments through reflection would break.
opcache.validate_timestamps=1 Edited files must be compiled again, e.g. after wp plugin update. With 0, edited files were still picked up in my tests, so this is defensive.

opcache.memory_consumption, opcache.max_accelerated_files and other shared-memory settings don't apply with file_cache_only. opcache.blacklist_filename is kept from php.ini.

Results

hyperfine, 20 runs after 3 warmups, PHP 8.3, nightly bundle run from source, on a site with WooCommerce, Yoast, Elementor, ACF and CF7. OPcache is loaded with PHP's defaults, so opcache.enable_cli=0:

Without WP_CLI_OPCACHE=1
option get siteurl 1,053 ms 450 ms
option get siteurl --skip-plugins --skip-themes 297 ms 137 ms
cli version 52 ms 81 ms
  • Cost of the restart: a second PHP startup, about 25 ms here. Commands that don't load WordPress, like cli version or help, are therefore slower with it. That's one reason to keep it opt-in, or to only restart for commands that load WordPress.
  • Phar: OPcache caches files inside the phar too. With the nightly phar and OPcache options passed directly, option get siteurl went from 1.19 s to 0.57 s. I haven't run this PR's restart from a phar build.

OPcache modes for option get siteurl --skip-plugins --skip-themes, with the options passed directly (no restart), 15 runs:

Mode Time
OPcache off for the CLI 278 ms
opcache.enable_cli=1 only (shared memory) 602 ms
Shared memory + file cache 116 ms
Shared memory + file cache + JIT 112 ms
file_cache_only=1 (what the restart uses) 94 ms

So setups that already have opcache.enable_cli=1 without a file cache gain the most: every run pays for OPcache's optimizer and keeps nothing, which is twice as slow as having it off.

Open questions

  • Opt-in or default: the gain is large for anything that loads WordPress. A default would need a cheap way to skip the restart for commands that don't load WordPress.
  • Earlier restart: restarting before the bootstrap, in php/wp-cli.php, would save another ~10 ms of the parent's work (compiling utils.php and class-wp-cli.php). But config files wouldn't be read yet, and it would need its own handling of the cache directory and autoloading.
  • WP_CLI_EARLY_REQUIRE files run before the bootstrap, so they run in both processes.
  • wp cli info could report whether the file cache is in use.
  • Cache cleanup: OPcache separates the cache by PHP build, but nothing removes old builds' directories yet. wp cli cache clear would delete them along with the rest of the cache.

Tests

  • New scenario in features/bootstrap.feature:
    • With WP_CLI_OPCACHE=1, it checks that the restarted process uses the file cache in <cache dir>/opcache, that STDIN and a memory_limit from WP_CLI_PHP_ARGS reach it, and that its exit code (3) comes back.
    • Without the variable, it checks that nothing changes.
    • With opcache.save_comments=0 and opcache.validate_timestamps=0 in the PHP settings, the restarted process still has both on.
    • With opcache.enable=0, there's no restart.
    • It then turns the restart on through wp-cli.yml.
    • If OPcache isn't loaded, the file-cache checks are skipped (OPcache's extension name contains a space, so @require-extension-* can't be used). It fails without the new step, and the save_comments check fails without the forced settings.
  • Manual checks:
    • a runcommand() subprocess with launch restarts too;
    • with -d opcache.enable_cli=1 already set, it still restarts and ends up with the file cache;
    • the cache directory is created with 0700;
    • with file_cache_only, a configured opcache.preload script doesn't run, while it does with shared memory.
  • Feature suites: bootstrap, runner, cli-info, cli and help pass, apart from 3 scenarios that also fail on main in this environment (Composer installs blocked by the network, and early root detection while running as root).
  • Static checks: PHPUnit passes apart from the 2 FileCacheTest failures that also happen on main when run as root. PHPCS is clean, and PHPStan reports no new errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D26yjkN2BiqCXT6p6o1WqS

Summary by CodeRabbit

  • New Features
    • WP-CLI can now use PHP’s OPcache file cache when enabled with WP_CLI_OPCACHE, including when the setting is provided through wp-cli.yml.
    • If OPcache or a usable cache directory is unavailable, WP-CLI continues without the OPcache restart. Existing behavior remains unchanged when the feature is not enabled.

OPcache is off for the CLI by default, so every run compiles WP-CLI,
WordPress and all active plugins again. With WP_CLI_OPCACHE set, WP-CLI
restarts PHP once with OPcache's file cache enabled, in a directory in
WP-CLI's cache, so compiled code is reused between runs.

The restart uses composer/xdebug-handler, which keeps the current PHP
settings and passes STDIN, STDOUT and the exit code through. It is
skipped when OPcache isn't available, its file cache is already used, or
Xdebug is active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D26yjkN2BiqCXT6p6o1WqS
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5eaf74cf-a348-4d00-b603-7d586f2b17e7
Commits

Reviewing files that changed from the base of the PR and between a1735b0 and a27d95a.

Files selected for processing: 5
  • composer.json
  • features/bootstrap.feature
  • php/WP_CLI/Bootstrap/RestartWithOpcache.php
  • php/WP_CLI/OpcacheRestarter.php
  • php/bootstrap.php

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Walkthrough

WP-CLI now supports restarting PHP with OPcache’s file cache when WP_CLI_OPCACHE is enabled and the required runtime and cache-directory checks pass. Bootstrap tests cover environment and configuration settings, OPcache availability, and PHP arguments.

Changes: OPcache file-cache restart

Layer / File(s) Summary
Configure the OPcache restart
composer.json, php/WP_CLI/OpcacheRestarter.php
Adds composer/xdebug-handler and an OpcacheRestarter that adds CLI OPcache settings before delegating the restart. It skips restarting when Xdebug is active or the CLI file cache is already active.
Activate and test the restart
php/WP_CLI/Bootstrap/RestartWithOpcache.php, php/bootstrap.php, features/bootstrap.feature
Adds a bootstrap step that checks the feature flag, OPcache availability, and cache-directory access. Registers the step and adds scenarios for OPcache behavior and configuration.

Priority: Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Bootstrap as WP-CLI bootstrap
  participant Step as RestartWithOpcache
  participant Restarter as OpcacheRestarter
  participant Handler as XdebugHandler
  Bootstrap->>Step: process(BootstrapState)
  Step->>Step: Check flag, OPcache, and cache directory
  Step->>Restarter: check(cache directory)
  Restarter->>Handler: Restart with OPcache CLI settings
Loading

Suggested labels: scope:framework, scope:testing

Suggested reviewers: schlessera

Merge Risk: Minimal · up to a27d9

No actionable merge-blocking issue is established. The change is ready for normal checks, though the phar restart path has not been verified.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: restarting PHP with OPcache's file cache when WP_CLI_OPCACHE is enabled.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 3 functions across 3 files. (2 skipped: 2 unsupported.)

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The step runs after the config is loaded, so a project can turn on the
OPcache restart in its wp-cli.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D26yjkN2BiqCXT6p6o1WqS
The restarted process keeps the settings from php.ini, including OPcache
settings that had no effect while OPcache was off for the CLI. Always
keep doc comments, which commands are defined by, and validate file
timestamps. Don't restart when opcache.enable is turned off, as nothing
would be cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D26yjkN2BiqCXT6p6o1WqS
@swissspidy
swissspidy marked this pull request as ready for review October 28, 2026 20:35
Copilot AI balanced review requested due to automatic review settings October 28, 2026 20:35
@swissspidy
swissspidy requested a review from a team as a code owner October 28, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants