diff options
author | Aaron Schulz <aschulz@wikimedia.org> | 2023-03-01 12:16:39 -0800 |
---|---|---|
committer | Krinkle <krinkle@fastmail.com> | 2023-03-23 00:08:49 +0000 |
commit | 29bab859fc8ef3680143a792acf1cf0acad22968 (patch) | |
tree | 2e28cf81b650d8b4b11d0e10516c788533edb7db | |
parent | 4de04152407da76f53291128ed0ac723782efb98 (diff) | |
download | mediawikicore-29bab859fc8ef3680143a792acf1cf0acad22968.tar.gz mediawikicore-29bab859fc8ef3680143a792acf1cf0acad22968.zip |
profiler: Add ProfilingContext class
Use this class to track the entry point and handler used for requests,
making it available for use in profiling, stats, and logging code.
This makes it possible for periodic and/or shutdown profiling callbacks
to know the basic action handler that applies to the request (if any).
Metric names can easily include this string along with MW_ENTRY_POINT
to create per-action profiling dashboards.
This info cannot otherwise be acquired from things like excimer stack
traces since the router and handler classes do not appear in the stack
during PRESEND deferred updates and variations like ApiMain/SpecialPage
"inclusion mode" would have to be detected somehow.
Bug: T330810
Change-Id: Icca5a7a343faeeb18652994c96752acb61a61fd1
-rw-r--r-- | autoload.php | 1 | ||||
-rw-r--r-- | includes/MediaWiki.php | 5 | ||||
-rw-r--r-- | includes/ResourceLoader/ResourceLoader.php | 2 | ||||
-rw-r--r-- | includes/Rest/Router.php | 2 | ||||
-rw-r--r-- | includes/api/ApiMain.php | 2 | ||||
-rw-r--r-- | includes/profiler/ProfilingContext.php | 79 | ||||
-rw-r--r-- | includes/specialpage/SpecialPageFactory.php | 2 | ||||
-rw-r--r-- | tests/phpunit/MediaWikiIntegrationTestCase.php | 2 | ||||
-rw-r--r-- | tests/phpunit/includes/profiler/ProfilingContextTest.php | 54 | ||||
-rw-r--r-- | thumb.php | 3 |
10 files changed, 151 insertions, 1 deletions
diff --git a/autoload.php b/autoload.php index f346a7426fe8..e9b654e9e191 100644 --- a/autoload.php +++ b/autoload.php @@ -1653,6 +1653,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Preferences\\SignatureValidatorFactory' => __DIR__ . '/includes/preferences/SignatureValidatorFactory.php', 'MediaWiki\\Preferences\\TimezoneFilter' => __DIR__ . '/includes/preferences/TimezoneFilter.php', 'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php', + 'MediaWiki\\Profiler\\ProfilingContext' => __DIR__ . '/includes/profiler/ProfilingContext.php', 'MediaWiki\\RenameUser\\Hook\\RenameUserAbortHook' => __DIR__ . '/includes/RenameUser/Hook/RenameUserAbortHook.php', 'MediaWiki\\RenameUser\\Hook\\RenameUserCompleteHook' => __DIR__ . '/includes/RenameUser/Hook/RenameUserCompleteHook.php', 'MediaWiki\\RenameUser\\Hook\\RenameUserPreRenameHook' => __DIR__ . '/includes/RenameUser/Hook/RenameUserPreRenameHook.php', diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index a8d47e2c3a12..cb5d99c190e1 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -26,6 +26,7 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Permissions\PermissionStatus; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Request\DerivativeRequest; use MediaWiki\Request\WebResponse; use MediaWiki\Title\Title; @@ -526,9 +527,11 @@ class MediaWiki { $t = microtime( true ); $actionName = $this->getAction(); $services = MediaWikiServices::getInstance(); - $action = $services->getActionFactory()->getAction( $actionName, $article, $this->context ); + $action = $services->getActionFactory()->getAction( $actionName, $article, $this->context ); if ( $action instanceof Action ) { + ProfilingContext::singleton()->init( MW_ENTRY_POINT, $actionName ); + // Check read permissions if ( $action->needsReadRights() && !$user->isAllowed( 'read' ) ) { throw new PermissionsError( 'read' ); diff --git a/includes/ResourceLoader/ResourceLoader.php b/includes/ResourceLoader/ResourceLoader.php index ad46ddd35c86..492fff773303 100644 --- a/includes/ResourceLoader/ResourceLoader.php +++ b/includes/ResourceLoader/ResourceLoader.php @@ -37,6 +37,7 @@ use MediaWiki\HookContainer\HookContainer; use MediaWiki\Html\Html; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Request\HeaderCallback; use MediaWiki\Title\Title; use MediaWiki\User\UserOptionsLookup; @@ -750,6 +751,7 @@ class ResourceLoader implements LoggerAwareInterface { ob_start(); $responseTime = $this->measureResponseTime(); + ProfilingContext::singleton()->init( MW_ENTRY_POINT, 'respond' ); // Find out which modules are missing and instantiate the others $modules = []; diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php index 054f2f53bc80..6f6e58480fd8 100644 --- a/includes/Rest/Router.php +++ b/includes/Rest/Router.php @@ -9,6 +9,7 @@ use MediaWiki\Config\ServiceOptions; use MediaWiki\HookContainer\HookContainer; use MediaWiki\MainConfigNames; use MediaWiki\Permissions\Authority; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; use MediaWiki\Rest\PathTemplateMatcher\PathMatcher; use MediaWiki\Rest\Reporter\ErrorReporter; @@ -493,6 +494,7 @@ class Router { * @return ResponseInterface */ private function executeHandler( $handler ): ResponseInterface { + ProfilingContext::singleton()->init( MW_ENTRY_POINT, $handler->getPath() ); // Check for basic authorization, to avoid leaking data from private wikis $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler ); if ( $authResult ) { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index f4162cfdb8f8..b06f5cfed8a1 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -28,6 +28,7 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\ParamValidator\TypeDef\UserDef; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Request\FauxRequest; use MediaWiki\Request\WebRequestUpload; use MediaWiki\Rest\HeaderParser\Origin; @@ -1886,6 +1887,7 @@ class ApiMain extends ApiBase { $this->mModule = $module; if ( !$this->mInternalMode ) { + ProfilingContext::singleton()->init( MW_ENTRY_POINT, $module->getModuleName() ); $this->setRequestExpectations( $module ); } diff --git a/includes/profiler/ProfilingContext.php b/includes/profiler/ProfilingContext.php new file mode 100644 index 000000000000..9859a7100ed7 --- /dev/null +++ b/includes/profiler/ProfilingContext.php @@ -0,0 +1,79 @@ +<?php + +namespace MediaWiki\Profiler; + +/** + * Class for tracking request-level classification information for profiling/stats/logging + * + * @note: this avoids the use of MediaWikiServices so that shutdown functions can use it + * + * @since 1.40 + */ +class ProfilingContext { + private string $entryPoint = 'unknown'; + private string $handler = 'unknown'; + + private bool $initialized = false; + + private static ?ProfilingContext $instance = null; + + public static function singleton(): ProfilingContext { + self::$instance ??= new self(); + + return self::$instance; + } + + /** + * Set entry point name and principle handler name for this request + * + * @param string $entryPoint Entry point script name (alphanumeric characters) + * @param string $handler Handler name (printable ASCII characters) + */ + public function init( string $entryPoint, string $handler ) { + // Ignore nested nesting (e.g. rest.php handlers that use ApiMain) + if ( !$this->initialized ) { + $this->initialized = true; + $this->entryPoint = $entryPoint; + $this->handler = $handler; + } + } + + /** + * @return bool Whether the context was initialized yet + */ + public function isInitialized() { + return $this->initialized; + } + + /** + * @return string Entry point name for this request (e.g. "index", "api", "load") + */ + public function getEntryPoint(): string { + return $this->entryPoint; + } + + /** + * @return string Handler name for this request (e.g. "edit", "recentchanges") + */ + public function getHandler(): string { + return $this->handler; + } + + /** + * @return string Statsd metric name prefix for this entry point and handler + */ + public function getHandlerMetricPrefix(): string { + // Replace any characters that may have a special meaning in statsd/graphite + return $this->entryPoint . '_' . strtr( + $this->handler, + [ '{' => '', '}' => '', ':' => '_', '/' => '_', '.' => '_' ] + ); + } + + /** + * @internal For testing only + */ + public static function destroySingleton() { + self::$instance = null; + } +} diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index e993d36d9d3d..3b639150a121 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -33,6 +33,7 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageReference; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Specials\SpecialActiveUsers; use MediaWiki\Specials\SpecialAllMessages; use MediaWiki\Specials\SpecialAllPages; @@ -1445,6 +1446,7 @@ class SpecialPageFactory { } if ( !$including ) { + ProfilingContext::singleton()->init( MW_ENTRY_POINT, $page->getName() ); // Narrow DB query expectations for this HTTP request $trxLimits = $context->getConfig()->get( MainConfigNames::TrxProfilerLimits ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); diff --git a/tests/phpunit/MediaWikiIntegrationTestCase.php b/tests/phpunit/MediaWikiIntegrationTestCase.php index efaecd4bbb59..646de2a64e81 100644 --- a/tests/phpunit/MediaWikiIntegrationTestCase.php +++ b/tests/phpunit/MediaWikiIntegrationTestCase.php @@ -12,6 +12,7 @@ use MediaWiki\Page\PageIdentity; use MediaWiki\Page\ProperPageIdentity; use MediaWiki\Permissions\Authority; use MediaWiki\Permissions\UltimateAuthority; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Request\FauxRequest; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\PageUpdateStatus; @@ -720,6 +721,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase { } $wgRequest = RequestContext::getMain()->getRequest(); MediaWiki\Session\SessionManager::resetCache(); + ProfilingContext::destroySingleton(); // If anything changed the content language, we need to // reset the SpecialPageFactory. diff --git a/tests/phpunit/includes/profiler/ProfilingContextTest.php b/tests/phpunit/includes/profiler/ProfilingContextTest.php new file mode 100644 index 000000000000..cf2631f297bb --- /dev/null +++ b/tests/phpunit/includes/profiler/ProfilingContextTest.php @@ -0,0 +1,54 @@ +<?php + +use MediaWiki\Profiler\ProfilingContext; + +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * @covers \MediaWiki\Profiler\ProfilingContext + */ +class ProfilingContextTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + + public static function provideEntryPointNames() { + return [ + [ 'index', 'edit', 'index_edit' ], + [ 'index', 'Recentchanges', 'index_Recentchanges' ], + [ 'api', 'upload', 'api_upload' ], + [ 'rest', '/wikibase/v1/something/{complex}/id', 'rest__wikibase_v1_something_complex_id' ] + ]; + } + + /** + * @dataProvider provideEntryPointNames + */ + public function testSetEntryPointHandler( $entryPoint, $handler, $metricName ) { + $profilerContext = new ProfilingContext(); + + $this->assertFalse( $profilerContext->isInitialized() ); + $profilerContext->init( $entryPoint, $handler ); + + $this->assertTrue( $profilerContext->isInitialized() ); + $this->assertSame( $entryPoint, $profilerContext->getEntryPoint() ); + $this->assertSame( $handler, $profilerContext->getHandler() ); + $this->assertSame( $metricName, $profilerContext->getHandlerMetricPrefix() ); + } +} diff --git a/thumb.php b/thumb.php index 888945117c08..6dc43ec3571d 100644 --- a/thumb.php +++ b/thumb.php @@ -28,6 +28,7 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\Profiler\ProfilingContext; use MediaWiki\Title\Title; use Wikimedia\AtEase\AtEase; @@ -44,6 +45,8 @@ wfThumbMain(); function wfThumbMain() { global $wgTrivialMimeDetection, $wgRequest; + ProfilingContext::singleton()->init( MW_ENTRY_POINT, 'stream' ); + // Don't use fancy MIME detection, just check the file extension for jpg/gif/png $wgTrivialMimeDetection = true; |