Skip to content

Instantly share code, notes, and snippets.

@zviryatko
Last active October 1, 2026 10:24
Show Gist options
  • Select an option

  • Save zviryatko/996fa488b983f66fb2a951d8bf8bc1b9 to your computer and use it in GitHub Desktop.

Select an option

Save zviryatko/996fa488b983f66fb2a951d8bf8bc1b9 to your computer and use it in GitHub Desktop.
Reusable Drupal site:install profiling skill, php-spx instrumentation patch, and configurable runner

Drupal installation profiling with php-spx

A reusable skill, Bash runner, and temporary core patch for profiling Drupal's installer one task iteration at a time.

Save all three companion files in one directory, such as .agents/skills/profile-site-install/, and follow SKILL.md.

Requires Bash, patch, Drush, and CLI PHP with php-spx. Run inside your PHP environment from the Drupal project root; no particular container platform, database backend, fixture module, or configuration directory is assumed. The patch was authored for Drupal 11; applicability is checked before execution.

DRUPAL_ROOT=web bash .agents/skills/profile-site-install/profile-site-install.sh -- \
  vendor/bin/drush --uri=https://disposable.example \
  site:install --existing-config -y

Select a disposable database/site: the supplied installer command replaces it. Set DRUPAL_ROOT for nonstandard webroots and configure spx.data_dir through CLI PHP's SPX configuration. Reports can be inspected with SPX's UI or an optional analysis integration. The runner restores the original installer on exit and preserves the command's failure status.

Install SPX

For an Alpine image using distribution PHP packages, add the following to your Dockerfile. Set PHP_VER to match the image's PHP package suffix and use repositories that provide the matching SPX package. This approach is for Alpine-packaged PHP, not the separate PHP build in official php:* images.

ARG PHP_VER=85
RUN apk add --no-cache bash patch git php${PHP_VER}-spx \
    && mkdir -p /var/tmp/spx \
    && printf '%s\n' 'spx.data_dir="/var/tmp/spx"' \
       > /etc/php${PHP_VER}/conf.d/99-spx-profiling.ini

Give the user running Drush write access to /var/tmp/spx (for example, chown <runtime-user>:<runtime-group> /var/tmp/spx during the image build). Mount that directory as a volume if reports should survive container replacement. The package normally enables the extension; check before adding a duplicate extension=spx.so entry.

For other PHP installations, follow the upstream SPX installation instructions using the development tools for the same PHP binary that runs Drush. Enable extension=spx.so if the installer has not already done so, and set spx.data_dir in a CLI-loaded INI file. Find that location with php --ini.

Verify inside the PHP runtime:

php --ri spx
php -r 'var_dump(extension_loaded("spx"), ini_get("spx.data_dir"));'

This CLI workflow needs no spx.http_* settings or exposed SPX web endpoint. The runner supplies the profiling environment variables for each invocation.

Install optional spx-mcp

php-spx-mcp requires PHP 8.3+ and zlib; zstd is optional for .txt.zst reports. It currently supports SPX v0.5 report format. Install outside the Drupal bind mount, with Composer available:

RUN git clone --depth 1 https://github.com/NoiseByNorthwest/php-spx-mcp.git /opt/php-spx-mcp \
    && cd /opt/php-spx-mcp \
    && composer install --no-dev --no-interaction --optimize-autoloader

Use your Composer executable (composer2 on some images). For repeatable builds, check out a chosen commit before installation. The runtime user needs read access to the installed server and reports.

Add a stdio server to your MCP client's configuration (JSON-style example):

{
  "mcpServers": {
    "spx-mcp": {
      "command": "php",
      "args": ["/opt/php-spx-mcp/bin/server.php"],
      "env": {"SPX_DATA_DIR": "/var/tmp/spx"}
    }
  }
}

For Docker Compose, replace that server entry with:

{
  "command": "docker",
  "args": [
    "compose", "-f", "/absolute/path/to/compose.yaml",
    "exec", "-T", "-e", "SPX_DATA_DIR=/var/tmp/spx",
    "php", "php", "/opt/php-spx-mcp/bin/server.php"
  ]
}

The first php after the environment argument is your Compose service name; replace it as needed. The second is the PHP executable. -T preserves stdio transport. Both processes must see the same reports: MCP's SPX_DATA_DIR points to PHP's spx.data_dir, using paths valid in their respective environments. Restart/reconnect the MCP client, run a profile, then call find_reports.

Index: core/includes/install.core.inc
===================================================================
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -561,6 +561,10 @@ function install_run_tasks(&$install_state, ?callable $callback = NULL) {
$task = array_shift($tasks_to_perform);
$install_state['active_task'] = $task_name;
$original_parameters = $install_state['parameters'];
+ if (function_exists('spx_profiler_start')) {
+ spx_profiler_start();
+ spx_profiler_full_report_set_custom_metadata_str(json_encode(['task' => $task_name]));
+ }
$output = install_run_task($task, $install_state);
// Ensure the maintenance theme is initialized. If the install task has
// rebuilt the container the active theme will not be set. This can occur if
@@ -587,6 +591,9 @@ function install_run_tasks(&$install_state, ?callable $callback = NULL) {
// the URL parameters have changed, or an end to the page request was
// specifically called for.
$finished = empty($tasks_to_perform) || ($install_state['interactive'] && (isset($output) || $install_state['parameters_changed'] || $install_state['stop_page_request']));
+ if (function_exists('spx_profiler_stop')) {
+ spx_profiler_stop();
+ }
} while (!$finished);
return $output;
}
#!/usr/bin/env bash
set -euo pipefail
# Run inside the PHP environment (host or container), from the project root.
# Usage: [DRUPAL_ROOT=web] bash /path/profile-site-install.sh -- \
# vendor/bin/drush --uri=https://disposable.example site:install --existing-config -y
# The supplied install command replaces the selected site's database.
# SPX must be installed and enabled for CLI PHP. Set spx.data_dir in PHP's
# configuration to select the report destination.
if [[ ${1:-} != -- || $# -lt 2 ]]; then
echo 'Usage: [DRUPAL_ROOT=web] profile-site-install.sh -- <drush command and install arguments>' >&2
exit 2
fi
shift
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
patch_file="$script_dir/install-core-spx-steps.patch"
if [[ -z ${DRUPAL_ROOT:-} ]]; then
roots=()
for candidate in web docroot .; do
[[ ! -f "$candidate/core/includes/install.core.inc" ]] || roots+=("$candidate")
done
if [[ ${#roots[@]} != 1 ]]; then
echo 'Set DRUPAL_ROOT to the Drupal webroot (directory containing core).' >&2
exit 2
fi
DRUPAL_ROOT=${roots[0]}
fi
core_dir=$(cd -- "$DRUPAL_ROOT/core" && pwd)
[[ -f "$core_dir/includes/install.core.inc" && -f "$patch_file" ]] || {
echo 'Missing installer or companion patch.' >&2
exit 2
}
# An unsuccessful forward dry-run can mean incompatible core, not just an
# already-applied patch. Stop in either case without starting an install.
patch --batch --forward --dry-run -d "$core_dir" -p2 < "$patch_file"
# Restore the exact original installer, including on a partially failed apply.
# Run only one profiling/install process at a time in this checkout.
backup=$(mktemp)
cp -p -- "$core_dir/includes/install.core.inc" "$backup"
cleanup() {
result=$?
trap - EXIT
if cp -p -- "$backup" "$core_dir/includes/install.core.inc"; then
rm -f -- "$backup"
else
echo "Could not restore installer; backup retained at $backup" >&2
result=1
fi
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
patch --batch --forward -d "$core_dir" -p2 < "$patch_file"
SPX_ENABLED=1 SPX_AUTO_START=0 SPX_REPORT=full "$@"
echo 'Install finished. Inspect reports in the CLI PHP spx.data_dir directory.'
name profile-site-install
description Profile Drupal site installation task by task with php-spx, identify config import or translation hotspots, and measure installation performance patches.

Profile Drupal installation with SPX

Use the companion runner and instrumentation patch. Keep them alongside this file. The patch brackets iterations of install_run_tasks() with manual SPX start/stop calls and records the task name in JSON custom metadata. Batch tasks may produce multiple reports with the same name; aggregate them when comparing whole-task costs.

Prepare the environment

For extension setup and optional MCP wiring, follow the companion installation instructions.

Identify the project's Drupal webroot, Drush command, intended site URI, database, install profile, and config sync settings. Use an explicitly selected disposable site/database: site:install replaces its database. Preserve existing authorization; do not infer permission to reinstall another site.

Run in the PHP runtime that has Drush and php-spx available. This can be the host or a container; use that project's normal shell/exec workflow. The runner does not depend on Docker, a specific service name, SQLite, fixtures, or a specific config directory. All file paths must be visible inside that runtime.

Check that SPX is available in CLI PHP, and inspect its spx.data_dir setting (e.g. php --ri spx). Configure that directory to be writable and accessible for analysis. The instrumentation was authored against Drupal 11; the runner checks patch applicability before invoking the command. Adapt the patch for other core versions if needed rather than forcing a failed patch.

Run

From the Drupal project root, supply the complete command after --:

DRUPAL_ROOT=web bash /path/to/profile-site-install/profile-site-install.sh -- \
  vendor/bin/drush --uri=https://disposable.example \
  site:install --existing-config -y

Use the project's actual URI and installation arguments. For an installation without existing configuration, pass the chosen profile and omit --existing-config. The runner detects a single webroot at web, docroot, or . when DRUPAL_ROOT is unset. It preserves the current working directory for Drush and sets SPX_ENABLED=1, SPX_AUTO_START=0, and SPX_REPORT=full. Manual profiling requires SPX_AUTO_START=0.

The runner applies the patch temporarily and restores the original installer on normal exit, command failure, or a handled signal. It refuses an already patched or incompatible installer. Run one install at a time in the checkout; a forced kill or runtime shutdown can prevent cleanup, so inspect the installer after such an interruption. Do not commit instrumentation into managed core.

Inspect reports

Find the new spx-full-*.json / .txt.gz report pairs in spx.data_dir. Use the SPX UI or an available report-analysis integration. If spx-mcp is configured, its report discovery, metadata, flat profile, callers, and call-graph tools can help; it is optional and is not bundled here.

Read custom_metadata_str for {"task":"<task name>"} rather than identifying the task by command line, which is shared by the reports. Verify timing units with the installed viewer/integration. Start with task totals, then inspect exclusive and inclusive function time and call counts for the expensive tasks. Compare config import, translation import, and other tasks before choosing a hotspot to optimize.

Measure improvements

Use unprofiled runs of the same Drush installation command for before/after wall-clock measurements. Disable SPX (including automatic profiling) and remove the instrumentation for these runs. Keep the PHP runtime, database backend, configuration, install arguments, translation sources, and cache conditions consistent. Restore the same disposable baseline before each run using the project's established procedure; do not assume a database filename or delete files by wildcard.

Repeat baseline and patched runs to assess variability. Keep unrelated fixture imports and post-install build steps out of the timed command. Report measured results separately from profiler observations and distinguish a core/contrib performance patch from this temporary instrumentation patch.

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