mediawiki-skins-Vector/tests/phpunit/structure/PerformanceBudgetTest.php
Michael Große b32116de30 Add styles performance budget for GrowthExperiments extension
We have CI failing on this test for GrowthExperiments with the styles
budget being exceeded. On changes that do not touch production code at
all.

I do not know how this can happen in the first place, or whether fixing
it this way is sensible, but we need to unblock our CI.

Change-Id: I178cb7ff6f111a11e9b6060960548e65d1c7d3c0
2024-06-14 13:01:43 +02:00

270 lines
8.9 KiB
PHP

<?php
namespace MediaWiki\Skins\Vector\Tests\Structure;
use ExtensionRegistry;
use HashBagOStuff;
use MediaWiki\Context\DerivativeContext;
use MediaWiki\Context\RequestContext;
use MediaWiki\Output\OutputPage;
use MediaWiki\Request\FauxRequest;
use MediaWiki\ResourceLoader\Context;
use MediaWiki\ResourceLoader\Module;
use MediaWikiIntegrationTestCase;
use Wikimedia\DependencyStore\KeyValueDependencyStore;
/**
* @group Database
*/
class PerformanceBudgetTest extends MediaWikiIntegrationTestCase {
/**
* Get the maximum size of modules in bytes as defined in bundlesize.config.json
*
* @param string $skinName
*
* @return array
*/
protected function getMaxSize( $skinName ) {
$configFile = dirname( __DIR__, 3 ) . '/bundlesize.config.json';
$bundleSizeConfig = json_decode( file_get_contents( $configFile ), true );
return $bundleSizeConfig[ 'total' ][ $skinName ] ?? [];
}
/**
* Calculates the size of a module
*
* @param string $moduleName
* @param string $skinName
*
* @return float|int
* @throws \Wikimedia\RequestTimeout\TimeoutException
* @throws MediaWiki\Config\ConfigException
*/
protected function getContentTransferSize( $moduleName, $skinName ) {
// Calculate Size
$resourceLoader = $this->getServiceContainer()->getResourceLoader();
$resourceLoader->setDependencyStore( new KeyValueDependencyStore( new HashBagOStuff() ) );
$request = new FauxRequest(
[
'lang' => 'en',
'modules' => $moduleName,
'skin' => $skinName,
]
);
$context = new Context( $resourceLoader, $request );
$module = $resourceLoader->getModule( $moduleName );
$contentContext = new \MediaWiki\ResourceLoader\DerivativeContext( $context );
$contentContext->setOnly(
$module->getType() === Module::LOAD_STYLES
? Module::TYPE_STYLES
: Module::TYPE_COMBINED
);
// Create a module response for the given module and calculate the size
$content = $resourceLoader->makeModuleResponse( $contentContext, [ $moduleName => $module ] );
$contentTransferSize = strlen( gzencode( $content, 9 ) );
// Adjustments for core modules [T343407]
$contentTransferSize -= 17;
return $contentTransferSize;
}
/**
* Prepares a skin for testing, assigning context and output page
*
* @param string $skinName
*
* @return \Skin
* @throws \SkinException
*/
protected function prepareSkin( string $skinName ): \Skin {
$skinFactory = $this->getServiceContainer()->getSkinFactory();
$skin = $skinFactory->makeSkin( $skinName );
$title = $this->getExistingTestPage()->getTitle();
$context = new DerivativeContext( RequestContext::getMain() );
$context->setTitle( $title );
$context->setSkin( $skin );
$outputPage = new OutputPage( $context );
$context->setOutput( $outputPage );
$skin->setContext( $context );
$outputPage->setTitle( $title );
$outputPage->output( true );
return $skin;
}
/**
* Converts a string to bytes
*
* @param string|int|float $size
*
* @return float|int
*/
private function getSizeInBytes( $size ) {
if ( is_string( $size ) ) {
if ( strpos( $size, 'KB' ) !== false || strpos( $size, 'kB' ) !== false ) {
$size = (float)str_replace( [ 'KB', 'kB', ' KB', ' kB' ], '', $size );
$size = $size * 1024;
} elseif ( strpos( $size, 'B' ) !== false ) {
$size = (float)str_replace( [ ' B', 'B' ], '', $size );
}
}
return $size;
}
/**
* Get the list of skins and their maximum size
*
* @return array
*/
public function provideSkinsForModulesSize() {
$allowedSkins = [ 'vector-2022', 'vector' ];
$skins = [];
foreach ( $allowedSkins as $skinName ) {
$maxSizes = $this->getMaxSize( $skinName );
if ( empty( $maxSizes ) ) {
continue;
}
$skins[ $skinName ] = [ $skinName, $maxSizes ];
}
return $skins;
}
/**
* @param string $key either styles or scripts.
* @return bool
*/
private function getAdditionalBudgetForInstalledExtensions( $key = 'styles' ) {
/**
* @todo: In future this should be defined in the extensions themselves via bundlesize.config.json
*/
$additional = [
'scripts' => [
// ext.echo.centralauth
'Echo' => 2000,
// ext.checkUser.clientHints
'CheckUser' => 1000,
// ext.popups
'Popups' => 2000,
// ext.flaggedRevs.advanced
'FlaggedRevs' => 2000,
// ext.growthExperiments.SuggestedEditSession
// Note this seems pretty high compared to other numbers,
// so we may want to work with Growth team to optimize this down.
'GrowthExperiments' => 7500,
// ext.wikimediaEvents
// Note this seems pretty high compared to other numbers,
// so we may want to optimize this in future when we attempt to add new schemas.
'WikimediaEvents' => 11000,
// wikibase.vector.searchClient and ext.wikimediaevents.wikibase and wikibase.client.data-bridge.init
'Wikibase' => 2000,
// ext.visualEditor.desktopArticleTarget.init and ext.visualEditor.targetLoader
// Note this seems pretty high compared to other numbers,
// so we may want to work with VisualEditor to optimize this down.
'VisualEditor' => 14000,
// ext.eventLogging
'EventLogging' => 1000,
// ext.uls.interface
'UniversalLanguageSelector' => 4000,
// ext.wikilambda.languageselector
'WikiLambda' => 4000,
],
'styles' => [
'FlaggedRevs' => 1000,
'GrowthExperiments' => 1000,
],
];
$total = 0;
// The flagged revisions extension is only loaded in certain environments.
// Since the environment is based on FlaggedRevs not being installed, extra budget is allocated.
// More context: https://phabricator.wikimedia.org/T360102#9632324
foreach ( $additional[ $key ] ?? [] as $extension => $budget ) {
if ( ExtensionRegistry::getInstance()->isLoaded( $extension ) ) {
$total += $budget;
}
}
return $total;
}
/**
* Tests the size of modules in allowed skins
*
* @param string $skinName
* @param array $maxSizes
*
* @dataProvider provideSkinsForModulesSize
* @coversNothing
*
* @return void
* @throws \Wikimedia\RequestTimeout\TimeoutException
* @throws MediaWiki\Config\ConfigException
*/
public function testTotalModulesSize( $skinName, $maxSizes ) {
$skin = $this->prepareSkin( $skinName );
$moduleStyles = $skin->getOutput()->getModuleStyles();
$size = 0;
foreach ( $moduleStyles as $moduleName ) {
$size += $this->getContentTransferSize( $moduleName, $skinName );
}
$stylesMaxSize = $this->getSizeInBytes( $maxSizes[ 'styles' ] ) +
$this->getAdditionalBudgetForInstalledExtensions( 'styles' );
$message = $this->createMessage( $skinName, $size, $stylesMaxSize, $moduleStyles );
$this->assertLessThanOrEqual( $stylesMaxSize, $size, $message );
$modulesScripts = $skin->getOutput()->getModules();
$size = 0;
foreach ( $modulesScripts as $moduleName ) {
$size += $this->getContentTransferSize( $moduleName, $skinName );
}
$scriptsMaxSize = $this->getSizeInBytes( $maxSizes[ 'scripts' ] ) +
$this->getAdditionalBudgetForInstalledExtensions( 'scripts' );
$message = $this->createMessage( $skinName, $size, $scriptsMaxSize, $modulesScripts, true );
$this->assertLessThanOrEqual( $scriptsMaxSize, $size, $message );
}
/**
* Creates a message for the assertion
*
* @param string $skinName
* @param int|float $size
* @param int|float $maxSize
* @param array $modules
* @param bool $scripts
*
* @return string
*/
private function createMessage( $skinName, $size, $maxSize, $modules, $scripts = false ) {
$debugInformation = "[PLEASE DO NOT SKIP THIS TEST. If this is blocking a deploy this might " .
"signal a potential performance regression with the desktop site." .
"Instead of skipping the test you can increase the value in bundlesize.config.json " .
"and create a ticket to investigate this error. If the error is > 1kb please tag " .
"this as a train blocker." .
"Please tag the ticket #Web-Team-Backlog.\n\n" .
"The following modules are enabled on page load:\n" .
implode( "\n", $modules );
$moduleType = $scripts ? 'scripts' : 'styles';
return "T346813: Performance budget for $moduleType in skin" .
" $skinName on main article namespace has been exceeded." .
" Total size of $moduleType modules is " . $this->bytesToKbsRoundup( $size ) . " Kbs is greater" .
" than the current budget size of " . $this->bytesToKbsRoundup( $maxSize ) . " Kbs" .
" (see Vector/bundlesize.config.json).\n" .
"If you are adding code on page load, please reduce $moduleType that you are loading on page load" .
" or talk to the web team about increasing the budget.\n" .
"If you are not adding code, and this seems to be an error, " .
"it is possible that something running without CI has bypassed this check and we " .
"can address this separately." .
"Please reach out to the web team to discuss this via Phabricator or #talk-to-web." .
"$debugInformation";
}
/**
* Converts bytes to Kbs and rounds up to the nearest 0.1
*
* @param int|float $sizeInBytes
*
* @return float
*/
private function bytesToKbsRoundup( $sizeInBytes ) {
return ceil( ( $sizeInBytes * 10 ) / 1024 ) / 10;
}
}