Menu Sidebar
Menu

Introducing wp doctor v0.1.0

I’d like to formally introduce you to wp doctor, runcommand’s second premium WP-CLI command. The command is available exclusively to runcommand gold and silver subscribers. You can apply today for access, or read on for more details.

Inspired by Pantheon’s WP Launch Check (credit where credit is due), wp doctor saves your team hours of time by codifying diagnosis procedures into a series of checks run by WP-CLI. For example, cron-count is a check to ensure WP Cron hasn’t exploded with jobs:

138d9769-d7f0-4bdc-80a9-a822c1987670

Not only does it come with dozens of default checks, wp doctor is designed for extensibility. Easily create a custom doctor.yml file to define additional checks you deem necessary for your system:

b09af583-c44a-4ebf-b97c-37224d59a9cf

If you manage more than a few WordPress installations, wp doctor is your new favorite WP-CLI command.

Fix PHP Parse error: syntax error, unexpected ‘?’ in phar:///usr/local/bin/wp/php/WP_CLI/Runner.php(941) : eval()’d code on line 1

Are you seeing this PHP parse error when trying to use WP-CLI?

PHP Parse error: syntax error, unexpected '?' in phar:///usr/local/bin/wp/php/WP_CLI/Runner.php(941) : eval()'d code on line 1

You may have a Byte Order Mark at the beginning of your wp-config.php file.

Run head -n3 wp-config.php | hexdump -C to see if there’s a ef bb bf pattern at the beginning of the file:

$ head -n3 wp-config.php | hexdump -C
00000000  ef bb bf 3c 3f 70 68 70  0d 0a 0a 0a              |...<?php....|
0000000c

The Byte Order Mark can be removed with vim:

$ vim wp-config.php
:set nobomb
:wq

Write a custom check to perform an arbitrary assertion

Because wp doctor checks are built on top of a foundational abstraction, it’s relatively straightforward for you to write your own custom check.

The basic requirement is that you create a class extending runcommand\Doctor\Checks\Check that implements a run() method. The run() must set a status and message based on whatever procedural logic

As an example, here’s an annotated custom check to assert Akismet is activated with a valid API key:

<?php

/**
 * Ensures Akismet is activated with the appropriate credentials.
 */
class Akismet_Activated extends runcommand\Doctor\Checks\Check {

	public function __construct( $options = array() ) {
		parent::__construct( $options );
		// Every check is to run on 'after_wp_load' by default.
		// You could instead use 'before_wp_load' or 'after_wp_config_load'
		$this->set_when( 'after_wp_load' );
	}

	public function run() {
		// If the Akismet isn't activated, bail early.
		if ( ! class_exists( 'Akismet' ) ) {
			$this->set_status( 'error' );
			$this->set_message( "Akismet doesn't appear to be activated." );
			return;
		}
		// Verify that the API exists.
		$api_key = Akismet::get_api_key();
		if ( empty( $api_key ) ) {
			$this->set_status( 'error' );
			$this->set_message( 'API key is missing.' );
			return;
		}
		// Verify that the API key is valid.
		$verification = Akismet::verify_key( $api_key );
		if ( 'failed' === $verification ) {
			$this->set_status( 'error' );
			$this->set_message( 'API key verification failed.' );
			return;
		}
		// Everything looks good, so report a success.
		$this->set_status( 'success' );
		$this->set_message( 'Akismet is activated with a verified API key.' );
	}

}

If the class were placed in an akismet-activated.php file, you could register it with:

plugin-akismet-activated:
  class: Akismet_Activated
  require: akismet-activated.php

Then, run the config file:

$ wp doctor check plugin-akismet-activated --config=doctor.yml
+--------------------------+--------+---------------------+
| name                     | status | message             |
+--------------------------+--------+---------------------+
| plugin-akismet-activated | error  | API key is missing. |
+--------------------------+--------+---------------------+

Use Jetpack’s Photon image resizing in local development

In order to dynamically resize an image, Photon needs to be able to fetch the image file over HTTP. Jetpack disables Photon when JETPACK_DEV_DEBUG is defined because the assumption is that your local development domain isn’t accessible from the public internet. But, if you configure Photon in your local environment, you can tell Jetpack to enable all of Photon’s features too.

Setup Photon

First, check out the Photon codebase to a location served by your VIP Quickstart or Salty WordPress Vagrant:

svn co https://code.svn.wordpress.org/photon/ photon.dev

You’ll need to set up a record within your web server (Nginx or Apache) to serve the domain. If you aren’t using dnsmasq for local development, you’ll need to edit /etc/hosts on your local machine too.

Install Gmagick

You probably don’t have Gmagick installed, which Photon uses to resize images, so you’ll need to install it. For me, this was:

apt-get install php5.6-gmagick
service php5.6-fpm restart

Add a couple of filters

The last step is to add a couple of filters to a local mu-plugin or similar:

add_action( 'jetpack_modules_loaded', function(){
include( Jetpack::get_module_path( 'photon' ) );
add_filter( 'jetpack_photon_development_mode', '__return_false' );
}, 11 );

add_filter( 'jetpack_photon_domain', function( $domain ){
return 'http://photon.dev';
});

What I’m thinking about this election season

In the interest of having my thoughts written down and expanded upon somewhere, here I go!

I voted Hillary Clinton predominately because I think Donald Trump is a whack job, unfit for President, and that Hillary better represents my social values. Going into Election Day, even though media coverage leaned strongly towards Hillary, my thinking was that I wouldn’t be surprised if Trump won. And then he did, and I was surprised, but not really.

Some people close to me are in the minority groups subject to Trump’s hate speech (LGBTQ, non-white). Other people close to me probably voted Trump, although I haven’t yet talked to them about it directly.

I’m a registered Independent, and would consider myself socially progressive, fiscally conservative, and politically libertarian. In less contentious political arenas, I voted Kate Brown for Governor of Oregon, no to Measure 97 (increased corporate income tax), and yes to Tualatin City Council term limits.

I don’t think Trump won because he’s a racist, misogynist, and bigot. I think he won for the same reasons the congressional approval rating is at 13%, voter turnout was 55% (a 20 year low), and I myself have been largely politically apathetic for the majority of my voting career. Most Americans have lost faith in the US government — they don’t think their elected politicians are effective representatives of their interests.

I don’t think most Trump voters are ignorant, hateful, etc. Given the divisive state of affairs in this country, the only path forward is respect, mutual understanding, and compassion. I’ve been sharing op-eds from Trump supporters because I think it’s important to listen to their stories. I’ve also found these articles illuminating:

This morning, my wife and I attended service at West Hills Unitarian Universalist Fellowship, my first time at church in years. Reverend Tracy gave an amazing sermon titled “Compassion and Action“. The audio will eventually be posted to that link, I think.

wp profile v0.3.0: new commands; spotlight mode; intermediate actions

wp profile is a premium WP-CLI command to help you quickly identify what’s slow with WordPress. It reports key performance metrics (execution time, query count, cache hit/miss ratio, etc.) for segments of the WordPress execution process to give you easy visibility into what code is the least performant. v0.3.0 is the third significant release.

New commands: eval, eval-file

wp profile v0.3.0 introduces two new commands for profiling arbitrary code execution: wp profile eval and wp profile eval-file. Used creatively, this is an incredibly powerful tool for better understanding your codebase. Spend 15 minutes futzing around with xDebug, or 30 seconds using wp profile.

As an example, profile a WP REST API response by creating a get-posts.php file:

<?php
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param( 'per_page', 100 );
$server = rest_get_server();
$server->dispatch( $request );

Then, profile get-posts.php by running wp profile eval-file get-posts.php:

$ wp profile eval-file get-posts.php
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| 0.9429s | 0.002s     | 5           | 96.39%      | 7599       | 285          | 0s           | 0             |
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+

Holy moly, that’s a lot of cache hits!

Identify the signal from the noise with --spotlight

In a long list of results, it can be difficult to tell the signal from the noise:

$ wp profile stage bootstrap
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook                     | callback_count | time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before  |                | 0.2536s | 0.0033s    | 1           | 40%         | 2          | 3            | 0s           | 0             |
| muplugins_loaded         | 2              | 0.0005s | 0s         | 0           | 50%         | 1          | 1            | 0s           | 0             |
| plugins_loaded:before    |                | 0.2955s | 0.0012s    | 6           | 77.63%      | 59         | 17           | 0s           | 0             |
| plugins_loaded           | 14             | 0.1572s | 0s         | 0           | 100%        | 104        | 0            | 0s           | 0             |
| setup_theme:before       |                | 0.0035s | 0s         | 0           | 100%        | 6          | 0            | 0s           | 0             |
| setup_theme              | 1              | 0.0001s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
| after_setup_theme:before |                | 0.0828s | 0s         | 0           | 100%        | 26         | 0            | 0s           | 0             |
| after_setup_theme        | 12             | 0.0038s | 0s         | 0           | 100%        | 4          | 0            | 0s           | 0             |
| init:before              |                | 0.0001s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
| init                     | 82             | 0.2954s | 0.0024s    | 7           | 96.88%      | 155        | 5            | 0s           | 0             |
| wp_loaded:before         |                | 0.0001s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
| wp_loaded                | 3              | 0.0049s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
| wp_loaded:after          |                | 0.0471s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (13)               | 114            | 1.1445s | 0.0069s    | 14          | 83.06%      | 357        | 26           | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+

The new --spotlight flag filters out the zero-ish results from the set, to help you focus on the greatest contributing factors:

$ wp profile stage bootstrap --spotlight
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook                     | callback_count | time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before  |                | 0.2253s | 0.0025s    | 1           | 40%         | 2          | 3            | 0s           | 0             |
| muplugins_loaded         | 2              | 0.0007s | 0s         | 0           | 50%         | 1          | 1            | 0s           | 0             |
| plugins_loaded:before    |                | 0.2018s | 0.0009s    | 6           | 77.63%      | 59         | 17           | 0s           | 0             |
| plugins_loaded           | 14             | 0.1239s | 0s         | 0           | 100%        | 104        | 0            | 0s           | 0             |
| after_setup_theme:before |                | 0.0598s | 0s         | 0           | 100%        | 26         | 0            | 0s           | 0             |
| init                     | 82             | 0.1751s | 0.0022s    | 7           | 96.88%      | 155        | 5            | 0s           | 0             |
| wp_loaded:after          |                | 0.0273s | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (7)                | 98             | 0.8138s | 0.0056s    | 14          | 77.42%      | 347        | 26           | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+

Profile intermediate actions

In the example immediately above, the primary actions for the stage include ‘muplugins_loaded’, ‘plugins_loaded’, and ‘init’. You can also see some intermediate actions like ‘plugins_loaded:before’ and ‘wp_loaded:after’. These intermediate actions correspond to script execution before (or after) actual WordPress hooks. They’re pseudo hooks in a sense.

Try out wp profile hook plugins_loaded:before to see which plugins take the longest time to load.


wp profile is available to runcommand gold and silver subscribers, or you can purchase a single-seat updates and support subscription for $129 per year. Existing subscribers can download the latest from their account dashboard.

Have a developer seat? You can browse the full list of resolved issues in Github.

Figure out why WordPress is slow with wp profile

wp profile is a WP-CLI command to help you quickly identify what’s slow with WordPress. It’s designed to work alongside Xdebug and New Relic because it’s easy to deploy to any server that has WP-CLI. With wp profile, you gain quick visibility into key performance metrics (execution time, query count, cache hit/miss ratio, etc.) to guide further debugging.

Dealing with a slow WordPress install you’ve never worked with before? Run wp profile stage to see metrics for each stage of the WordPress load process. Include the --url=<url> argument to mock the request as a specific URL.

$ wp profile stage --url=runcommand.io
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| stage      | time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | hook_time | hook_count | request_time | request_count |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| bootstrap  | 0.7597s | 0.0052s    | 14          | 93.21%      | 357        | 26           | 0.3328s   | 2717       | 0s           | 0             |
| main_query | 0.0131s | 0.0004s    | 3           | 94.29%      | 33         | 2            | 0.0065s   | 78         | 0s           | 0             |
| template   | 0.7041s | 0.0192s    | 147         | 92.16%      | 2350       | 200          | 0.6982s   | 6130       | 0s           | 0             |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+
| total (3)  | 1.477s  | 0.0248s    | 164         | 93.22%      | 2740       | 228          | 1.0375s   | 8925       | 0s           | 0             |
+------------+---------+------------+-------------+-------------+------------+--------------+-----------+------------+--------------+---------------+

When WordPress handles a request from a browser, it’s essentially executing as one long PHP script. wp profile stage breaks the script into three stages:

  • bootstrap is where WordPress is setting itself up, loading plugins and the main theme, and firing the init hook.
  • main_query is how WordPress transforms the request (e.g. /2016/10/21/moms-birthday/) into the primary WP_Query.
  • template is where WordPress determines which theme template to render based on the main query, and renders it.

In the example from above, bootstrap seems a bit slow, so let’s dive into it further. Run wp profile stage bootstrap to dive into higher fidelity mode of a given stage. Use the --spotlight flag to filter out the zero-ish results.

$ wp profile stage bootstrap --url=runcommand.io --spotlight
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| hook                     | callback_count | time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| muplugins_loaded:before  |                | 0.1644s | 0.0017s    | 1           | 40%         | 2          | 3            | 0s           | 0             |
| muplugins_loaded         | 2              | 0.0005s | 0s         | 0           | 50%         | 1          | 1            | 0s           | 0             |
| plugins_loaded:before    |                | 0.1771s | 0.0008s    | 6           | 77.63%      | 59         | 17           | 0s           | 0             |
| plugins_loaded           | 14             | 0.0887s | 0s         | 0           | 100%        | 104        | 0            | 0s           | 0             |
| after_setup_theme:before |                | 0.043s  | 0s         | 0           | 100%        | 26         | 0            | 0s           | 0             |
| init                     | 82             | 0.1569s | 0.0018s    | 7           | 96.88%      | 155        | 5            | 0s           | 0             |
| wp_loaded:after          |                | 0.027s  | 0s         | 0           |             | 0          | 0            | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| total (7)                | 98             | 0.6575s | 0.0043s    | 14          | 77.42%      | 347        | 26           | 0s           | 0             |
+--------------------------+----------------+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+

Each stage is further segmented by wp profile based on its primary actions. For the bootstrap stage, the primary actions include ‘muplugins_loaded’, ‘plugins_loaded’, and ‘init’. You can also see some intermediate actions like ‘plugins_loaded:before’ and ‘wp_loaded:after’. These intermediate actions correspond to script execution before (or after) actual WordPress hooks. They’re pseudo hooks in a sense.

When you’ve found a specific hook you’d like to assess, run wp profile hook <hook>. Include the --fields=<fields> argument to limit output to certain fields.

$ wp profile hook plugins_loaded --url=runcommand.io --fields=callback,time,location
+------------------------------------------------------------+---------+-----------------------------------------------------------------+
| callback                                                   | time    | location                                                        |
+------------------------------------------------------------+---------+-----------------------------------------------------------------+
| wp_maybe_load_widgets()                                    | 0.0046s | wp-includes/functions.php:3501                                  |
| wp_maybe_load_embeds()                                     | 0.0003s | wp-includes/embed.php:162                                       |
| VaultPress_Hotfixes->protect_jetpack_402_from_oembed_xss() | 0s      | vaultpress/class.vaultpress-hotfixes.php:124                    |
| _wp_customize_include()                                    | 0s      | wp-includes/theme.php:2052                                      |
| EasyRecipePlus->pluginsLoaded()                            | 0.0013s | easyrecipeplus/lib/EasyRecipePlus.php:125                       |
| Gamajo\GenesisHeaderNav\genesis_header_nav_i18n()          | 0.0007s | genesis-header-nav/genesis-header-nav.php:61                    |
| DS_Public_Post_Preview::init()                             | 0.0001s | public-post-preview/public-post-preview.php:52                  |
| wpseo_load_textdomain()                                    | 0.0004s | wordpress-seo-premium/wp-seo-main.php:222                       |
| load_yoast_notifications()                                 | 0.0016s | wordpress-seo-premium/wp-seo-main.php:381                       |
| wpseo_init()                                               | 0.0329s | wordpress-seo-premium/wp-seo-main.php:240                       |
| wpseo_premium_init()                                       | 0.0019s | wordpress-seo-premium/wp-seo-premium.php:79                     |
| wpseo_frontend_init()                                      | 0.0007s | wordpress-seo-premium/wp-seo-main.php:274                       |
| Black_Studio_TinyMCE_Plugin->load_compatibility()          | 0.0016s | black-studio-tinymce-widget/black-studio-tinymce-widget.php:206 |
| Jetpack::load_modules()                                    | 0.0564s | jetpack/class.jetpack.php:1672                                  |
+------------------------------------------------------------+---------+-----------------------------------------------------------------+
| total (14)                                                 | 0.1026s |                                                                 |
+------------------------------------------------------------+---------+-----------------------------------------------------------------+

Et voila! We’ve discovered that wpseo_init() and Jetpack::load_modules() are collectively contributing ~100ms to every page load.

With wp profile, discovering why WordPress is slow becomes the easy part.

Questions to ask when determining why a WordPress site is going down

Working on a WordPress site that’s crashing all of the time? Here are some qualifying questions you can use to determine why it might be going down:

  • When does the site go down? Is it a particular time of day that could be correlated to traffic patterns?
  • When the site goes down, what do you do to bring it back up?
  • When did the problem start happening? Is it closely related to some other change that was made to the site?
  • What have you done in previous attempts to fix the problem?
  • How are the servers configured (RAM, etc.)? Does adding more capacity remediate the problem?
  • What caching plugins are you using? Do you have other caching strategies in place (e.g. Varnish or Cloudflare)?
  • What plugins are you running? How customized is your theme?

Importantly, these qualifying questions should give you better visibility into the nature of the problem, to help guide further debugging.

Profile key performance metrics for a WP REST API response

Using the eval-file command in wp profile, you can profile key performance metrics for a WP REST API response by mocking, executing, and evaluating the request with a one-off file. These key performance metrics include execution time, query count, cache hit/miss ratio, and more.

For example, to mock a call to GET /wp/v2/posts?per_page=100, you’d create a get-posts.php file like this:

<?php
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param( 'per_page', 100 );
$server = rest_get_server();
$server->dispatch( $request );

Then, profile get-posts.php by running wp profile eval-file get-posts.php:

$ wp profile eval-file get-posts.php
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| time    | query_time | query_count | cache_ratio | cache_hits | cache_misses | request_time | request_count |
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+
| 0.9429s | 0.002s     | 5           | 96.39%      | 7599       | 285          | 0s           | 0             |
+---------+------------+-------------+-------------+------------+--------------+--------------+---------------+

Write a check for verifying contents of WordPress files

One of the check types included in wp doctor is File_Contents, or the ability to check all or a selection of WordPress files for a given regex pattern. The check type is in use by a couple of the default diagnostic checks, and you can use the File_Contents check type in your custom doctor.yml configuration file.

The File_Contents check type is the most efficient way to check WordPress files because it only iterates the filesystem once, regardless of how many file checks you’ve registered.

As an example, here are two checks using File_Contents, one which ensures sessions aren’t used by plugins and the other which ensures $_SERVER['SERVER_NAME'] isn’t used in wp-config.php:

file-sessions:
  check: File_Contents
  options:
    regex: .*(session_start|\$_SESSION).*
    only_wp_content: true
file-server-name-wp-config:
  check: File_Contents
  options:
    regex: define\(.+WP_(HOME|SITEURL).+\$_SERVER.+SERVER_NAME
    path: wp-config.php

Run together, you might see:

$ wp doctor check --config=file-contents.yml --all
+----------------------------+---------+-----------------------------------------------------------------------------------------+
| name                       | status  | message                                                                                 |
+----------------------------+---------+-----------------------------------------------------------------------------------------+
| file-sessions              | success | All 'php' files passed check for '.*(session_start|\$_SESSION).*'.                      |
| file-server-name-wp-config | success | All 'php' files passed check for 'define\(.+WP_(HOME|SITEURL).+\$_SERVER.+SERVER_NAME'. |
+----------------------------+---------+-----------------------------------------------------------------------------------------+

The File_Contents check type accepts the following options:

  • ‘regex’: Regex pattern to check against each file’s contents.
  • ‘extension’. File extension to check. Defaults to ‘php’. Separate multiple file extensions with a ‘|’.
  • ‘path’. Check a specific file path. Value should be relative to ABSPATH (e.g. ‘wp-content’ or ‘wp-config.php’).
  • ‘only_wp_content’: Only check the wp-content directory.
Newer Posts
Older Posts

Daniel Bachhuber

Proud father and husband. Principal, @handbuiltco. Maintainer, @wpcli. Sales, @rtcamp.