diff --git a/README.md b/README.md index be78192f..74e5fd52 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ General information on how to do additional services and some additional example * [redis](docker-compose-services/redis) * [redis-commander](docker-compose-services/redis-commander) * [Varnish](docker-compose-services/varnish) +* [XHGui](docker-compose-services/xhgui) ## .ddev/web-build/Dockerfile examples to customize web container diff --git a/docker-compose-services/xhgui/README.md b/docker-compose-services/xhgui/README.md new file mode 100644 index 00000000..68ee1222 --- /dev/null +++ b/docker-compose-services/xhgui/README.md @@ -0,0 +1,96 @@ +# XHGui + +This container provides a [XHGui](https://github.com/perftools/xhgui) container for your project so you can collect performance information +provided by `xhprof`. + +![A screenshoot of XHGui](assets/xhgui-screenshot.png) + + +## Warning + +This recipe has a dev environment on mind. +Profiling production environments with this recipe is probably not a good idea. + +## Installation + +### Your DDEV config +You need the `xhprof` php module. The easiest way is adding +``` +webimage_extra_packages: [php7.4-xhprof] +``` +to your `.ddev/config.yaml` file. + +### The containers + +* Copy the `docker-compose.xhgui.yml` file to your `.ddev` folder. +* Copy the `xhgui` folder to your `.ddev` folder. +* Copy the `xhgui-mongodb` folder to your `.ddev` folder. +* (Optional) Copy the `commands/xhprof` file to your `.ddev/web/commands` folder, so you can easily start/stop `xhprof`. This probably won't be needed with next ddev release, where you can just do `ddev xhprof on` as you are already used to with xdebug. + +### Your application + +Your application needs to have a profiler set up. + +If your application uses composer, you can install it with + +``` +ddev composer require perftools/php-profiler (maybe you want to use --dev) +``` + +For other usecases, see the WordPress section as an example. + +You need to place some code for initializing the profiling as soon as possible in the +bootstrap of your application. + +In the `examples` folder you will find the collector initialization +and the config for that collector. + +#### Drupal 8+ based projects + +An easy way of doing this in Drupal, is copying those two files in the `examples` folder to your +`sites/default` folder, and append to your `settings.ddev.php`. +``` +require_once __DIR__ . '/xhgui.collector.php'; +``` + +If you want to stop profiling, you can just comment/remove that line. +Take into account that with the default configuration, every time you +`ddev start`, DDEV will recreate this file. You can remove the #ddev-generated at the top of the file if you want to avoid that. + +#### WordPress projects + +Download latest version of `perftools/php-profiler` (this has been validated with the current latest release, 0.18.0). +If you use [bedrock](https://roots.io/bedrock/), just use the composer command from the previous section. + +If you use vanilla WordPress: + +``` +wget https://github.com/perftools/php-profiler/archive/refs/tags/0.18.0.tar.gz +tar -xvf 0.18.0.tar.gz +``` + +Copy those two files in the `examples` folder of **this** repo (not the `php-profiler` you just downloaded) to your +WordPress folder, and append to your `wp-config-ddev.php`: + +``` +require_once __DIR__ . '/php-profiler-0.18.0/autoload.php'; +require_once __DIR__ . '/xhgui.collector.php'; +``` + +If you want to stop profiling, you can just comment/remove those lines. +Take into account that with the default configuration, every time you +`ddev start`, DDEV will recreate this file. You can remove the #ddev-generated at the top of the file if you want to avoid that. + +### Service initialization + +Start (or restart) DDEV to have the service initialized when you are ready: `ddev start` +Remember, `settings.ddev.php` or `wp-config-ddev.php` might be rewritten and you need to do changes there. + +### Accessing the service + +By default, xhgui will be available at http://``:8282. Note that it's http only. + + +**Contributed by [@penyaskito](https://github.com/penyaskito)** + +**Help and feedback from** [@randyfay](https://github.com/randyfay), [@e0ipso](https://github.com/e0ipso), [@andypost](https://github.com/andypost) diff --git a/docker-compose-services/xhgui/assets/xhgui-screenshot.png b/docker-compose-services/xhgui/assets/xhgui-screenshot.png new file mode 100644 index 00000000..0597019a Binary files /dev/null and b/docker-compose-services/xhgui/assets/xhgui-screenshot.png differ diff --git a/docker-compose-services/xhgui/commands/xhprof b/docker-compose-services/xhgui/commands/xhprof new file mode 100755 index 00000000..1d3345e9 --- /dev/null +++ b/docker-compose-services/xhgui/commands/xhprof @@ -0,0 +1,43 @@ +#!/bin/bash + +## Description: Enable or disable xhprof +## Usage: xhprof on|off|enable|disable|true|false|status +## Example: "ddev xhprof" (default is "on"), "ddev xhprof off", "ddev xhprof on", "ddev xhprof status" + +enable_xhprof() { + phpenmod -v $DDEV_PHP_VERSION xhprof; + killall -HUP php-fpm 2>/dev/null || true; + echo "Enabled xhprof"; +} + +disable_xhprof() { + phpdismod -v $DDEV_PHP_VERSION xhprof; + killall -HUP php-fpm 2>/dev/null || true; + echo "Disabled xhprof"; +} + +if [ $# -eq 0 ]; then + enable_xhprof + exit +fi + +case $1 in +on | true | enable) + enable_xhprof + ;; +off | false | disable) + disable_xhprof + ;; +status) + status=$(php -m | grep 'xhprof') + if [ "${status}" = "xhprof" ]; then + result="xhprof is enabled" + else + result="xhprof is disabled" + fi + echo $result + ;; +*) + echo "Invalid argument: $1" + ;; +esac diff --git a/docker-compose-services/xhgui/docker-compose.xhgui.yml b/docker-compose-services/xhgui/docker-compose.xhgui.yml new file mode 100644 index 00000000..519daa16 --- /dev/null +++ b/docker-compose-services/xhgui/docker-compose.xhgui.yml @@ -0,0 +1,41 @@ +version: '3.7' + +services: + xhgui: + container_name: ddev-${DDEV_SITENAME}-xhgui + hostname: ${DDEV_SITENAME}-xhgui + image: xhgui/xhgui + ports: + - 8282:80 + links: + - xhgui-mongo + environment: + - XHGUI_MONGO_HOSTNAME=xhgui-mongo + - XHGUI_MONGO_DATABASE=xhprof + labels: + com.ddev.site-name: ${DDEV_SITENAME} + com.ddev.approot: $DDEV_APPROOT + volumes: + - ./xhgui/nginx.default.conf:/etc/nginx/conf.d/default.conf + - ./xhgui/xhgui.config.php:/var/www/xhgui/config/config.php + xhgui-mongo: + container_name: ddev-${DDEV_SITENAME}-xhgui-mongo + # xhgui doesn't work with mongodb >= 3.6 + image: percona/percona-server-mongodb:3.4 + # (case sensitive) engine: mmapv1, rocksdb, wiredTiger, inMemory + command: --storageEngine=wiredTiger + environment: + - MONGO_INITDB_DATABASE=xhprof + volumes: + - ./xhgui-mongo/mongo.init.d:/docker-entrypoint-initdb.d + - xhgui-mongo:/data/db + ports: + - "27017:27017" + web: + links: + - xhgui + depends_on: + - xhgui +volumes: + xhgui-source: + xhgui-mongo: diff --git a/docker-compose-services/xhgui/examples/xhgui.collector.config.php b/docker-compose-services/xhgui/examples/xhgui.collector.config.php new file mode 100644 index 00000000..b31b7819 --- /dev/null +++ b/docker-compose-services/xhgui/examples/xhgui.collector.config.php @@ -0,0 +1,45 @@ + true, + 'mode' => 'development', + + 'save.handler' => Profiler::SAVER_STACK, + 'save.handler.stack' => [ + 'savers' => [ + Profiler::SAVER_UPLOAD, + Profiler::SAVER_FILE, + ], + // if saveAll=false, break the chain on successful save + 'saveAll' => false, + ], + 'save.handler.file' => [ + // Appends jsonlines formatted data to this path + 'filename' => '/tmp/xhgui.data.jsonl', + ], + 'save.handler.upload' => [ + 'url' => 'http://xhgui/run/import', + // The timeout option is in seconds and defaults to 3 if unspecified. + 'timeout' => 3, + // the token must match 'upload.token' config in XHGui + 'token' => getenv('XHGUI_UPLOAD_TOKEN', 'token'), + ], + // Profile all requests. + 'profiler.enable' => function() { + return true; + }, + + 'profiler.simple_url' => function($url) { + return preg_replace('/\=\d+/', '', $url); + }, + + 'profiler.simple_url' => function($url) { + return str_replace(getenv('DDEV_HOSTNAME'), '', $url); + } + +]; diff --git a/docker-compose-services/xhgui/examples/xhgui.collector.php b/docker-compose-services/xhgui/examples/xhgui.collector.php new file mode 100644 index 00000000..749e7341 --- /dev/null +++ b/docker-compose-services/xhgui/examples/xhgui.collector.php @@ -0,0 +1,19 @@ +start(); +} catch (Exception $e) { + // throw away or log error about profiling instantiation failure +} diff --git a/docker-compose-services/xhgui/xhgui-mongo/mongo.init.d b/docker-compose-services/xhgui/xhgui-mongo/mongo.init.d new file mode 100644 index 00000000..c386fca3 --- /dev/null +++ b/docker-compose-services/xhgui/xhgui-mongo/mongo.init.d @@ -0,0 +1,5 @@ +db.results.ensureIndex( { 'meta.SERVER.REQUEST_TIME' : -1 } ); +db.results.ensureIndex( { 'profile.main().wt' : -1 } ); +db.results.ensureIndex( { 'profile.main().mu' : -1 } ); +db.results.ensureIndex( { 'profile.main().cpu' : -1 } ); +db.results.ensureIndex( { 'meta.url' : 1 } ); diff --git a/docker-compose-services/xhgui/xhgui/nginx.default.conf b/docker-compose-services/xhgui/xhgui/nginx.default.conf new file mode 100644 index 00000000..84d5cf68 --- /dev/null +++ b/docker-compose-services/xhgui/xhgui/nginx.default.conf @@ -0,0 +1,20 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name xhgui; + # root directive should be global + root /var/www/xhgui/webroot/; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location ~ \.php$ { + try_files $uri =404; + include /etc/nginx/fastcgi_params; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } +} diff --git a/docker-compose-services/xhgui/xhgui/xhgui.config.php b/docker-compose-services/xhgui/xhgui/xhgui.config.php new file mode 100644 index 00000000..8f094f76 --- /dev/null +++ b/docker-compose-services/xhgui/xhgui/xhgui.config.php @@ -0,0 +1,63 @@ + 'production', + + // Can be either mongodb or file. + /* + 'save.handler' => 'file', + 'save.handler.filename' => dirname(__DIR__) . '/cache/' . 'xhgui.data.' . microtime(true) . '_' . substr(md5($url), 0, 6), + */ + 'save.handler' => 'mongodb', + + // Needed for file save handler. Beware of file locking. You can adjust this file path + // to reduce locking problems (eg uniqid, time ...) + //'save.handler.filename' => __DIR__.'/../data/xhgui_'.date('Ymd').'.dat', + // Database options for MongoDB. + 'mongodb' => [ + // 'hostname' and 'port' are used to build DSN for MongoClient + 'hostname' => getenv('XHGUI_MONGO_HOSTNAME') ?: 'xhgui-mongo', + 'port' => getenv('XHGUI_MONGO_PORT') ?: 27017, + // The database name + 'database' => getenv('XHGUI_MONGO_DATABASE') ?: 'xhprof', + // Additional options for the MongoClient constructor, + // for example 'username', 'password', or 'replicaSet'. + // See . + 'options' => [], + // An array of options for the MongoDB driver. + // Options include setting connection context options for SSL or logging callbacks. + // See . + 'driverOptions' => [], + ], + + 'run.view.filter.names' => [ + 'Zend*', + 'Composer*', + ], + // If defined, add imports via upload (/run/import) must pass token parameter with this value + 'upload.token' => getenv('XHGUI_UPLOAD_TOKEN') ?: '', + + // Add this path prefix to all links and resources + // If this is not defined, auto-detection will try to find it itself + 'path.prefix' => null, + + // Setup timezone for date formatting + // Example: 'UTC', 'Europe/Tallinn' + // If left empty, php default will be used (php.ini or compiled in default) + 'timezone' => '', + + // Date format used when browsing XHGui pages. + // + // Must be a format supported by the PHP date() function. + // See . + 'date.format' => 'M jS H:i:s', + + // The number of items to show in "Top lists" with functions + // using the most time or memory resources, on XHGui Run pages. + 'detail.count' => 6, + + // The number of items to show per page, on XHGui list pages. + 'page.limit' => 25, +];