Krinkle has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/391162 )
Change subject: [WIP] Split profile.php from StartProfiler and add PhpAutoPrepend ...................................................................... [WIP] Split profile.php from StartProfiler and add PhpAutoPrepend Bug: T180183 Change-Id: I60cce0eb51101d9e3fed7a65890c101ddd39e7d0 --- A wmf-config/PhpAutoPrepend.php M wmf-config/StartProfiler.php A wmf-config/profiler.php 3 files changed, 282 insertions(+), 246 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/mediawiki-config refs/changes/62/391162/1 diff --git a/wmf-config/PhpAutoPrepend.php b/wmf-config/PhpAutoPrepend.php new file mode 100644 index 0000000..d29a893 --- /dev/null +++ b/wmf-config/PhpAutoPrepend.php @@ -0,0 +1,20 @@ +<?php +# WARNING: This file is publicly viewable on the web. Do not put private data here. + +/** + * Initialisation code for all PHP processes. + * + * This PHP file is configured to execute this file before the main script, + * through the `auto_prepend_file` setting. This can apply both to web + * request and CLI processes. + * + * This is executed in the same run-time as the main script, which means + * it CAN expose state, such as variables and constants. + * + * This file exposes one variable: + * - $wmfProfiler: Value intended for $wgProfiler, assigned in StartProfile.php + * + * @see https://secure.php.net/manual/en/ini.core.php#ini.auto-prepend-file + */ + +require_once __DIR__ . '/profiler.php'; diff --git a/wmf-config/StartProfiler.php b/wmf-config/StartProfiler.php index a11cecf..93fbe0c 100644 --- a/wmf-config/StartProfiler.php +++ b/wmf-config/StartProfiler.php @@ -12,250 +12,7 @@ # DefaultSettings.php, and wmf-config CommonSettings or InitialiseSettings. # ###################################################################### -/** - * File overview: - * - * 1. Parse X-Wikimedia-Header - * 2. One-off profile to stdout (via MediaWiki) - * 3. One-off profile to /tmp (from localhost) - * 4. Sampling profiler for production traffic - * 5. One-off profile to XHGui. - */ +// profiler.php defines $wmfProfiler +require_once __DIR__ . '/profiler.php'; -/** - * 1) Parse X-Wikimedia-Header - * - * If the X-Wikimedia-Header is present, parse it into an associative array. - * - * See https://wikitech.wikimedia.org/wiki/X-Wikimedia-Debug - */ -$XWD = false; -if ( isset( $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ) ) { - parse_str( preg_replace( '/; ?/', '&', $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ), $XWD ); -} - -if ( ini_get( 'hhvm.stats.enable_hot_profiler' ) ) { - /** - * 2) One-off profile to stdout - * - * MediaWiki's Profiler class can output raw profile data directly to the output - * of a web response (web), or in stdout (CLI). - * - * For web: Set X-Wikimedia-Debug (to bypass cache) and query param 'forceprofile=1'. - * For CLI: Set CLI option '--profiler=text'. - * - * See https://www.mediawiki.org/wiki/Manual:Profiling - */ - if ( - ( isset( $_GET['forceprofile'] ) && isset( $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ) ) - || PHP_SAPI === 'cli' - ) { - $wgProfiler = [ - 'class' => 'ProfilerXhprof', - 'flags' => XHPROF_FLAGS_NO_BUILTINS, - 'output' => 'text', - ]; - - /** - * 3) One-off profile to /tmp - * - * When making requests to the local server using shell access, - * setting the 'Force-Local-XHProf: 1' header will write raw profile data - * directly to a local file in /tmp/xhprof/. - * - * Note: This is only allowed for requests within the same server. - */ - } elseif ( - isset( $_SERVER['HTTP_FORCE_LOCAL_XHPROF'] ) - && isset( $_SERVER['REMOTE_ADDR'] ) - && $_SERVER['REMOTE_ADDR'] == '127.0.0.1' - && is_writable( '/tmp/xhprof' ) - ) { - xhprof_enable(); - register_shutdown_function( function () { - $prof = xhprof_disable(); - $titleFormat = "%-75s %6s %13s %13s %13s\n"; - $format = "%-75s %6d %13.3f %13.3f %13.3f%%\n"; - $out = sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%' ); - if ( empty( $prof['main()']['wt'] ) ) { - return; - } - $total = $prof['main()']['wt']; - uksort( $prof, function ( $a, $b ) use ( $prof ) { - if ( $prof[$a]['wt'] < $prof[$b]['wt'] ) { - return 1; - } elseif ( $prof[$a]['wt'] > $prof[$b]['wt'] ) { - return -1; - } else { - return 0; - } - } ); - - foreach ( $prof as $name => $info ) { - $out .= sprintf( $format, $name, $info['ct'], $info['wt'] / 1000, - $info['wt'] / $info['ct'] / 1000, - $info['wt'] / $total * 100 ); - } - file_put_contents( '/tmp/xhprof/' . date( 'Y-m-d\TH:i:s' ) . '.prof', $out ); - } ); - } -} - -/** - * 4) Sampling profiler for production traffic - * - * If Xenon is enabled, register a shutdown callback to ask HHVM for - * recently collected data. Will not end up yielding something every - * request. - * - * Based on https://github.com/wikimedia/arc-lamp - */ -if ( extension_loaded( 'xenon' ) && ini_get( 'hhvm.xenon.period' ) ) { - register_shutdown_function( function () { - $data = HH\xenon_get_data(); - - if ( empty( $data ) ) { - return; - } - - $entryPoint = basename( $_SERVER['SCRIPT_NAME'] ); - $reqMethod = '{' . $_SERVER['REQUEST_METHOD'] . '}'; - - // Collate stack samples and fold into single lines. - // This is the format expected by FlameGraph. - $stacks = []; - - foreach ( $data as $sample ) { - $stack = []; - - if ( empty( $sample['phpStack'] ) ) { - continue; - } - - foreach ( $sample['phpStack'] as $frame ) { - if ( $frame['function'] === 'include' ) { - // For file scope, just use the path as the name. - $func = $frame['file']; - } elseif ( $frame['function'] === '{closure}' && isset( $frame['line'] ) ) { - // Annotate anonymous functions with their location in the - // source code. Example: {closure:/path/to/file.php(123)} - $func = "{closure:{$frame['file']}({$frame['line']})}"; - } else { - $func = $frame['function']; - } - - if ( $func !== end( $stack ) ) { - $stack[] = $func; - } - } - - if ( count( $stack ) ) { - // The last element is usually (but not always) the full file - // path of the script name. We want things nice and consistent, - // so we pop off the path if it is there, and push the basename - // instead. - if ( strpos( end( $stack ), $entryPoint ) !== false ) { - array_pop( $stack ); - } - $stack[] = $reqMethod; - $stack[] = $entryPoint; - - $strStack = implode( ';', array_reverse( $stack ) ); - if ( !isset( $stacks[$strStack] ) ) { - $stacks[$strStack] = 0; - } - $stacks[$strStack] += 1; - } - } - - $redis = new Redis(); - if ( $redis->connect( 'mwlog1001.eqiad.wmnet', 6379, 0.1 ) ) { - foreach ( $stacks as $stack => $count ) { - $redis->publish( 'xenon', "$stack $count" ); - } - } - } ); -} - -/** - * 5) One-off profile to XHGui - * - * Set X-Wikimedia-Debug header with 'profile' attribute to instrument a web request - * with XHProf and save the profile to XHGui's MongoDB. - * - * To find the profile in XHGui, either browse "Recent", or use wgRequestId value - * from the mw.config data in the HTML web response, e.g. by running the - * `mw.config.get('wgRequestId')` snippet in JavaScript. Then look up as follows: - * - * https://performance.wikimedia.org/xhgui/?url=WShdaQpAIHwAAF9HkX4AAAAW - * - * See https://performance.wikimedia.org/xhgui/ - * See https://wikitech.wikimedia.org/wiki/X-Wikimedia-Debug#Request_profiling - */ -if ( - ini_get( 'hhvm.stats.enable_hot_profiler' ) && - // Require X-Forwarded-For to ignore non-remote requests (e.g. PyBal) - !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && - isset( $XWD['profile'] ) -) { - xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS ); - - register_postsend_function( function () use ( $XWD ) { - $data = [ 'profile' => xhprof_disable() ]; - - $sec = $_SERVER['REQUEST_TIME']; - $usec = $_SERVER['REQUEST_TIME_FLOAT'] - $sec; - - $keyWhitelist = array_flip( [ - 'HTTP_HOST', 'HTTP_X_WIKIMEDIA_DEBUG', 'REQUEST_METHOD', - 'REQUEST_START_TIME', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT', - 'SERVER_ADDR', 'SERVER_NAME', 'THREAD_TYPE', 'action' - ] ); - - // Create sanitized copies of $_SERVER, $_ENV, and $_GET: - $server = array_intersect_key( $_SERVER, $keyWhitelist ); - $env = array_intersect_key( $_ENV, $keyWhitelist ); - $get = array_intersect_key( $_GET, $keyWhitelist ); - - // Strip everything from the query string except 'action=' param: - preg_match( '/action=[^&]+/', $_SERVER['REQUEST_URI'], $matches ); - $qs = $matches ? '?' . $matches[0] : ''; - $url = $_SERVER['SCRIPT_NAME'] . $qs; - - // If profiling was explicitly requested (via X-Wikimedia-Debug) - // then include the unique request ID in the reported URL, to make - // it easy for the person debugging to find the request in Xhgui. - if ( $XWD && method_exists( 'WebRequest', 'getRequestId' ) ) { - $reqId = WebRequest::getRequestId(); - $url = '//' . $reqId . $url; - $env['UNIQUE_ID'] = $reqId; - $server['UNIQUE_ID'] = $reqId; - } - - // Include web server name (SERVER_NAME is e.g. wikipedia.org, - // SERVER_ADDR is the LVS service name, e.g. appservers.svc) - $env['HOSTNAME'] = wfHostname(); - - // Re-insert scrubbed URL as REQUEST_URL: - $server['REQUEST_URI'] = $url; - $env['REQUEST_URI'] = $url; - - $data['meta'] = [ - 'url' => $url, - 'SERVER' => $server, - 'get' => $get, - 'env' => $env, - 'simple_url' => Xhgui_Util::simpleUrl( $url ), - 'request_ts' => new MongoDate( $sec ), - 'request_ts_micro' => new MongoDate( $sec, $usec ), - 'request_date' => date( 'Y-m-d', $sec ), - ]; - - Xhgui_Saver::factory( [ - 'save.handler' => 'mongodb', - 'db.host' => 'mongodb://tungsten.eqiad.wmnet:27017', - 'db.db' => 'xhprof', - 'db.options' => [], - ] )->save( $data ); - } ); -} +$wgProfiler = $wmfProfiler; diff --git a/wmf-config/profiler.php b/wmf-config/profiler.php new file mode 100644 index 0000000..16c77fa --- /dev/null +++ b/wmf-config/profiler.php @@ -0,0 +1,259 @@ +<?php +# WARNING: This file is publicly viewable on the web. Do not put private data here. +# +# Included by PhpAutoPrepend.php before any other wmf-config or mediawiki file. +# Uses no predefined state, other than plain PHP. +# +# Exports: +# - $wmfProfiler (used by StartProfile.php) + +global $wmfProfiler; +$wmfProfiler = []; + +/** + * File overview: + * + * 1. Parse X-Wikimedia-Header + * 2. One-off profile to stdout (via MediaWiki) + * 3. One-off profile to /tmp (from localhost) + * 4. Sampling profiler for production traffic + * 5. One-off profile to XHGui. + */ + +/** + * 1) Parse X-Wikimedia-Header + * + * If the X-Wikimedia-Header is present, parse it into an associative array. + * + * See https://wikitech.wikimedia.org/wiki/X-Wikimedia-Debug + */ +$XWD = false; +if ( isset( $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ) ) { + parse_str( preg_replace( '/; ?/', '&', $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ), $XWD ); +} + +if ( ini_get( 'hhvm.stats.enable_hot_profiler' ) ) { + /** + * 2) One-off profile to stdout + * + * MediaWiki's Profiler class can output raw profile data directly to the output + * of a web response (web), or in stdout (CLI). + * + * For web: Set X-Wikimedia-Debug (to bypass cache) and query param 'forceprofile=1'. + * For CLI: Set CLI option '--profiler=text'. + * + * See https://www.mediawiki.org/wiki/Manual:Profiling + */ + if ( + ( isset( $_GET['forceprofile'] ) && isset( $_SERVER['HTTP_X_WIKIMEDIA_DEBUG'] ) ) + || PHP_SAPI === 'cli' + ) { + $wmfProfiler = [ + 'class' => 'ProfilerXhprof', + 'flags' => XHPROF_FLAGS_NO_BUILTINS, + 'output' => 'text', + ]; + + /** + * 3) One-off profile to /tmp + * + * When making requests to the local server using shell access, + * setting the 'Force-Local-XHProf: 1' header will write raw profile data + * directly to a local file in /tmp/xhprof/. + * + * Note: This is only allowed for requests within the same server. + */ + } elseif ( + isset( $_SERVER['HTTP_FORCE_LOCAL_XHPROF'] ) + && isset( $_SERVER['REMOTE_ADDR'] ) + && $_SERVER['REMOTE_ADDR'] == '127.0.0.1' + && is_writable( '/tmp/xhprof' ) + ) { + xhprof_enable(); + register_shutdown_function( function () { + $prof = xhprof_disable(); + $titleFormat = "%-75s %6s %13s %13s %13s\n"; + $format = "%-75s %6d %13.3f %13.3f %13.3f%%\n"; + $out = sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%' ); + if ( empty( $prof['main()']['wt'] ) ) { + return; + } + $total = $prof['main()']['wt']; + uksort( $prof, function ( $a, $b ) use ( $prof ) { + if ( $prof[$a]['wt'] < $prof[$b]['wt'] ) { + return 1; + } elseif ( $prof[$a]['wt'] > $prof[$b]['wt'] ) { + return -1; + } else { + return 0; + } + } ); + + foreach ( $prof as $name => $info ) { + $out .= sprintf( $format, $name, $info['ct'], $info['wt'] / 1000, + $info['wt'] / $info['ct'] / 1000, + $info['wt'] / $total * 100 ); + } + file_put_contents( '/tmp/xhprof/' . date( 'Y-m-d\TH:i:s' ) . '.prof', $out ); + } ); + } +} + +/** + * 4) Sampling profiler for production traffic + * + * If Xenon is enabled, register a shutdown callback to ask HHVM for + * recently collected data. Will not end up yielding something every + * request. + * + * Based on https://github.com/wikimedia/arc-lamp + */ +if ( extension_loaded( 'xenon' ) && ini_get( 'hhvm.xenon.period' ) ) { + register_shutdown_function( function () { + $data = HH\xenon_get_data(); + + if ( empty( $data ) ) { + return; + } + + $entryPoint = basename( $_SERVER['SCRIPT_NAME'] ); + $reqMethod = '{' . $_SERVER['REQUEST_METHOD'] . '}'; + + // Collate stack samples and fold into single lines. + // This is the format expected by FlameGraph. + $stacks = []; + + foreach ( $data as $sample ) { + $stack = []; + + if ( empty( $sample['phpStack'] ) ) { + continue; + } + + foreach ( $sample['phpStack'] as $frame ) { + if ( $frame['function'] === 'include' ) { + // For file scope, just use the path as the name. + $func = $frame['file']; + } elseif ( $frame['function'] === '{closure}' && isset( $frame['line'] ) ) { + // Annotate anonymous functions with their location in the + // source code. Example: {closure:/path/to/file.php(123)} + $func = "{closure:{$frame['file']}({$frame['line']})}"; + } else { + $func = $frame['function']; + } + + if ( $func !== end( $stack ) ) { + $stack[] = $func; + } + } + + if ( count( $stack ) ) { + // The last element is usually (but not always) the full file + // path of the script name. We want things nice and consistent, + // so we pop off the path if it is there, and push the basename + // instead. + if ( strpos( end( $stack ), $entryPoint ) !== false ) { + array_pop( $stack ); + } + $stack[] = $reqMethod; + $stack[] = $entryPoint; + + $strStack = implode( ';', array_reverse( $stack ) ); + if ( !isset( $stacks[$strStack] ) ) { + $stacks[$strStack] = 0; + } + $stacks[$strStack] += 1; + } + } + + $redis = new Redis(); + if ( $redis->connect( 'mwlog1001.eqiad.wmnet', 6379, 0.1 ) ) { + foreach ( $stacks as $stack => $count ) { + $redis->publish( 'xenon', "$stack $count" ); + } + } + } ); +} + +/** + * 5) One-off profile to XHGui + * + * Set X-Wikimedia-Debug header with 'profile' attribute to instrument a web request + * with XHProf and save the profile to XHGui's MongoDB. + * + * To find the profile in XHGui, either browse "Recent", or use wgRequestId value + * from the mw.config data in the HTML web response, e.g. by running the + * `mw.config.get('wgRequestId')` snippet in JavaScript. Then look up as follows: + * + * https://performance.wikimedia.org/xhgui/?url=WShdaQpAIHwAAF9HkX4AAAAW + * + * See https://performance.wikimedia.org/xhgui/ + * See https://wikitech.wikimedia.org/wiki/X-Wikimedia-Debug#Request_profiling + */ +if ( + ini_get( 'hhvm.stats.enable_hot_profiler' ) && + // Require X-Forwarded-For to ignore non-remote requests (e.g. PyBal) + !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && + isset( $XWD['profile'] ) +) { + xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_NO_BUILTINS ); + + register_postsend_function( function () use ( $XWD ) { + $data = [ 'profile' => xhprof_disable() ]; + + $sec = $_SERVER['REQUEST_TIME']; + $usec = $_SERVER['REQUEST_TIME_FLOAT'] - $sec; + + $keyWhitelist = array_flip( [ + 'HTTP_HOST', 'HTTP_X_WIKIMEDIA_DEBUG', 'REQUEST_METHOD', + 'REQUEST_START_TIME', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT', + 'SERVER_ADDR', 'SERVER_NAME', 'THREAD_TYPE', 'action' + ] ); + + // Create sanitized copies of $_SERVER, $_ENV, and $_GET: + $server = array_intersect_key( $_SERVER, $keyWhitelist ); + $env = array_intersect_key( $_ENV, $keyWhitelist ); + $get = array_intersect_key( $_GET, $keyWhitelist ); + + // Strip everything from the query string except 'action=' param: + preg_match( '/action=[^&]+/', $_SERVER['REQUEST_URI'], $matches ); + $qs = $matches ? '?' . $matches[0] : ''; + $url = $_SERVER['SCRIPT_NAME'] . $qs; + + // If profiling was explicitly requested (via X-Wikimedia-Debug) + // then include the unique request ID in the reported URL, to make + // it easy for the person debugging to find the request in Xhgui. + if ( $XWD && method_exists( 'WebRequest', 'getRequestId' ) ) { + $reqId = WebRequest::getRequestId(); + $url = '//' . $reqId . $url; + $env['UNIQUE_ID'] = $reqId; + $server['UNIQUE_ID'] = $reqId; + } + + // Include web server name (SERVER_NAME is e.g. wikipedia.org, + // SERVER_ADDR is the LVS service name, e.g. appservers.svc) + $env['HOSTNAME'] = wfHostname(); + + // Re-insert scrubbed URL as REQUEST_URL: + $server['REQUEST_URI'] = $url; + $env['REQUEST_URI'] = $url; + + $data['meta'] = [ + 'url' => $url, + 'SERVER' => $server, + 'get' => $get, + 'env' => $env, + 'simple_url' => Xhgui_Util::simpleUrl( $url ), + 'request_ts' => new MongoDate( $sec ), + 'request_ts_micro' => new MongoDate( $sec, $usec ), + 'request_date' => date( 'Y-m-d', $sec ), + ]; + + Xhgui_Saver::factory( [ + 'save.handler' => 'mongodb', + 'db.host' => 'mongodb://tungsten.eqiad.wmnet:27017', + 'db.db' => 'xhprof', + 'db.options' => [], + ] )->save( $data ); + } ); +} -- To view, visit https://gerrit.wikimedia.org/r/391162 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I60cce0eb51101d9e3fed7a65890c101ddd39e7d0 Gerrit-PatchSet: 1 Gerrit-Project: operations/mediawiki-config Gerrit-Branch: master Gerrit-Owner: Krinkle <krinklem...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits