2010-10-19 18:25:42 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* @author Trevor Parscal
|
|
|
|
|
* @author Roan Kattouw
|
|
|
|
|
*/
|
|
|
|
|
|
2018-03-22 03:34:40 +00:00
|
|
|
/**
|
|
|
|
|
* Module for ResourceLoader initialization.
|
|
|
|
|
*
|
|
|
|
|
* See also <https://www.mediawiki.org/wiki/ResourceLoader/Features#Startup_Module>
|
|
|
|
|
*
|
|
|
|
|
* The startup module, as being called only from ResourceLoaderClientHtml, has
|
|
|
|
|
* the ability to vary based extra query parameters, in addition to those
|
|
|
|
|
* from ResourceLoaderContext:
|
|
|
|
|
*
|
2018-03-28 00:57:06 +00:00
|
|
|
* - target: Only register modules in the client intended for this target.
|
2018-03-22 03:34:40 +00:00
|
|
|
* Default: "desktop".
|
|
|
|
|
* See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
|
2018-03-28 00:57:06 +00:00
|
|
|
*
|
|
|
|
|
* - safemode: Only register modules that have ORIGIN_CORE as their origin.
|
|
|
|
|
* This effectively disables ORIGIN_USER modules. (T185303)
|
|
|
|
|
* See also: OutputPage::disallowUserJs()
|
2019-09-14 04:32:54 +00:00
|
|
|
*
|
|
|
|
|
* @ingroup ResourceLoader
|
|
|
|
|
* @internal
|
2018-03-22 03:34:40 +00:00
|
|
|
*/
|
2010-10-19 18:25:42 +00:00
|
|
|
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
|
2016-02-17 09:09:32 +00:00
|
|
|
protected $targets = [ 'desktop', 'mobile' ];
|
2010-10-19 18:25:42 +00:00
|
|
|
|
2019-08-14 19:39:01 +00:00
|
|
|
private $groupIds = [
|
|
|
|
|
// These reserved numbers MUST start at 0 and not skip any. These are preset
|
2020-02-09 20:36:12 +00:00
|
|
|
// for forward compatibility so that they can be safely referenced by mediawiki.js,
|
2019-08-14 19:39:01 +00:00
|
|
|
// even when the code is cached and the order of registrations (and implicit
|
|
|
|
|
// group ids) changes between versions of the software.
|
|
|
|
|
'user' => 0,
|
|
|
|
|
'private' => 1,
|
|
|
|
|
];
|
|
|
|
|
|
2013-07-07 22:51:15 +00:00
|
|
|
/**
|
|
|
|
|
* Recursively get all explicit and implicit dependencies for to the given module.
|
|
|
|
|
*
|
|
|
|
|
* @param array $registryData
|
|
|
|
|
* @param string $moduleName
|
2019-05-22 18:29:32 +00:00
|
|
|
* @param string[] $handled Internal parameter for recursion. (Optional)
|
2013-07-07 22:51:15 +00:00
|
|
|
* @return array
|
2019-05-22 18:29:32 +00:00
|
|
|
* @throws ResourceLoaderCircularDependencyError
|
2013-07-07 22:51:15 +00:00
|
|
|
*/
|
2019-05-22 18:29:32 +00:00
|
|
|
protected static function getImplicitDependencies(
|
|
|
|
|
array $registryData,
|
2019-10-08 21:10:04 +00:00
|
|
|
string $moduleName,
|
2019-05-22 18:29:32 +00:00
|
|
|
array $handled = []
|
2021-07-22 03:11:47 +00:00
|
|
|
): array {
|
2016-02-17 09:09:32 +00:00
|
|
|
static $dependencyCache = [];
|
2013-07-07 22:51:15 +00:00
|
|
|
|
2019-05-22 18:29:32 +00:00
|
|
|
// No modules will be added or changed server-side after this point,
|
|
|
|
|
// so we can safely cache parts of the tree for re-use.
|
2013-07-07 22:51:15 +00:00
|
|
|
if ( !isset( $dependencyCache[$moduleName] ) ) {
|
|
|
|
|
if ( !isset( $registryData[$moduleName] ) ) {
|
2019-05-22 18:29:32 +00:00
|
|
|
// Unknown module names are allowed here, this is only an optimisation.
|
|
|
|
|
// Checks for illegal and unknown dependencies happen as PHPUnit structure tests,
|
|
|
|
|
// and also client-side at run-time.
|
|
|
|
|
$flat = [];
|
2013-07-07 22:51:15 +00:00
|
|
|
} else {
|
|
|
|
|
$data = $registryData[$moduleName];
|
2019-05-22 18:29:32 +00:00
|
|
|
$flat = $data['dependencies'];
|
2013-07-07 22:51:15 +00:00
|
|
|
|
2019-05-22 18:29:32 +00:00
|
|
|
// Prevent recursion
|
|
|
|
|
$handled[] = $moduleName;
|
2013-07-07 22:51:15 +00:00
|
|
|
foreach ( $data['dependencies'] as $dependency ) {
|
2019-05-22 18:29:32 +00:00
|
|
|
if ( in_array( $dependency, $handled, true ) ) {
|
|
|
|
|
// If we encounter a circular dependency, then stop the optimiser and leave the
|
|
|
|
|
// original dependencies array unmodified. Circular dependencies are not
|
|
|
|
|
// supported in ResourceLoader. Awareness of them exists here so that we can
|
|
|
|
|
// optimise the registry when it isn't broken, and otherwise transport the
|
|
|
|
|
// registry unchanged. The client will handle this further.
|
|
|
|
|
throw new ResourceLoaderCircularDependencyError();
|
|
|
|
|
} else {
|
|
|
|
|
// Recursively add the dependencies of the dependencies
|
|
|
|
|
$flat = array_merge(
|
|
|
|
|
$flat,
|
|
|
|
|
self::getImplicitDependencies( $registryData, $dependency, $handled )
|
|
|
|
|
);
|
|
|
|
|
}
|
2013-07-07 22:51:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-05-22 18:29:32 +00:00
|
|
|
|
|
|
|
|
$dependencyCache[$moduleName] = $flat;
|
2013-07-07 22:51:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $dependencyCache[$moduleName];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-10-25 00:18:24 +00:00
|
|
|
* Optimize the dependency tree in $this->modules.
|
2013-07-07 22:51:15 +00:00
|
|
|
*
|
|
|
|
|
* The optimization basically works like this:
|
2018-05-19 20:46:54 +00:00
|
|
|
* Given we have module A with the dependencies B and C
|
|
|
|
|
* and module B with the dependency C.
|
|
|
|
|
* Now we don't have to tell the client to explicitly fetch module
|
|
|
|
|
* C as that's already included in module B.
|
2013-07-07 22:51:15 +00:00
|
|
|
*
|
2014-10-25 00:18:24 +00:00
|
|
|
* This way we can reasonably reduce the amount of module registration
|
2013-07-07 22:51:15 +00:00
|
|
|
* data send to the client.
|
|
|
|
|
*
|
2019-10-12 15:13:38 +00:00
|
|
|
* @param array[] &$registryData Modules keyed by name with properties:
|
resourceloader: Replace timestamp system with version hashing
Modules now track their version via getVersionHash() instead of getModifiedTime().
== Background ==
While some resources have observeable timestamps (e.g. files stored on disk),
many other resources do not. E.g. config variables, and module definitions.
For static file modules, one can e.g. revert one of more files in a module to a
previous version and not affect the max timestamp.
Wiki modules include pages only if they exist. The user module supports common.js
and skin.js. By default neither exists. If a user has both, and then the
less-recently modified one is deleted, the max-timestamp remains unchanged.
For client-side caching, batch requests use "Math.max" on the relevant timestamps.
Again, if a module changes but another module is more recent (e.g. out-of-order
deployment, or out-of-order discovery), the change would not result in a cache miss.
More scenarios can be found in the associated Phabricator tasks.
== Version hash ==
Previously we virtually mapped these variables to a timestamp by storing the current
time alongside a hash of the value in ObjectCache. Considering the number of
possible request contexts (wikis * modules * users * skins * languages) this doesn't
work well. It results in needless cache invalidation when the first time observation
is purged due to LRU algorithms. It also has other minor bugs leading to fewer
cache hits.
All modules automatically get the benefits of version hashing with this change.
The old getDefinitionMtime() and getHashMtime() have been replaced with dummies
that return 1. These functions are often called from getModifiedTime() in subclasses.
For backward-compatibility, their respective values (definition summary and hash)
are now included in getVersionHash directly.
As examples, the following modules have been updated to use getVersionHash directly.
Other modules still work fine and can be updated later.
* ResourceLoaderFileModule
* ResourceLoaderEditToolbarModule
* ResourceLoaderStartUpModule
* ResourceLoaderWikiModule
The presence of hashes in place of timestamps increases the startup module size on
a default MediaWiki install from 4.4k to 5.8k (after gzip and minification).
== ETag ==
Since timestamps are no longer tracked, we need a different way to implement caching
for cache proxies (e.g. Varnish) and web browsers. Previously we used the
Last-Modified header (in combination with Cache-Control and Expires).
Instead of Last-Modified (and If-Modified-Since), we use ETag (and If-None-Match).
Entity tags (new in HTTP/1.1) are much stricter than Last-Modified by default.
They instruct browsers to allow usage of partial Range requests. Since our responses
are dynamically generated, we need to use the Weak version of ETag.
While this sounds bad, it's no different than Last-Modified. As reassured by
RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3> the
specified behaviour behind Last-Modified follows the same "Weak" caching logic as
Entity tags. It's just that entity tags are capable of a stricter mode (whereas
Last-Modified is inherently weak).
== File cache ==
If $wgUseFileCache is enabled, ResourceLoader uses ResourceFileCache to cache
load.php responses. While the blind TTL handling (during the allowed expiry period)
is still maxage/timestamp based, tryRespondNotModified() now requires the caller to
know the expected ETag.
For this to work, the FileCache handling had to be moved from the top of
ResoureLoader::respond() to after the expected ETag is computed.
This also allows us to remove the duplicate tryRespondNotModified() handling since
that's is already handled by ResourceLoader::respond() meanwhile.
== Misc ==
* Remove redundant modifiedTime cache in ResourceLoaderFileModule.
* Change bugzilla references to Phabricator.
* Centralised inclusion of wgCacheEpoch using getDefinitionSummary. Previously this
logic was duplicated in each place the modified timestamp was used.
* It's easy to forget calling the parent class in getDefinitionSummary().
Previously this method only tracked 'class' by default. As such, various
extensions hardcoded that one value instead of calling the parent and extending
the array. To better prevent this in the future, getVersionHash() now asserts
that the '_cacheEpoch' property made it through.
* tests: Don't use getDefinitionSummary() as an API.
Fix ResourceLoaderWikiModuleTest to call getPages properly.
* In tests, the default timestamp used to be 1388534400000 (which is the unix time
of 20140101000000; the unit tests' CacheEpoch). The new version hash of these
modules is "XyCC+PSK", which is the base64 encoded prefix of the SHA1 digest of:
'{"_class":"ResourceLoaderTestModule","_cacheEpoch":"20140101000000"}'
* Add sha1.js library for client-side hash generation.
Compared various different implementations for code size (after minfication/gzip),
and speed (when used for short hexidecimal strings).
https://jsperf.com/sha1-implementations
- CryptoJS <https://code.google.com/p/crypto-js/#SHA-1> (min+gzip: 2.5k)
http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha1.js
Chrome: 45k, Firefox: 89k, Safari: 92k
- jsSHA <https://github.com/Caligatio/jsSHA>
https://github.com/Caligatio/jsSHA/blob/3c1d4f2e/src/sha1.js (min+gzip: 1.8k)
Chrome: 65k, Firefox: 53k, Safari: 69k
- phpjs-sha1 <https://github.com/kvz/phpjs> (RL min+gzip: 0.8k)
https://github.com/kvz/phpjs/blob/1eaab15d/functions/strings/sha1.js
Chrome: 200k, Firefox: 280k, Safari: 78k
Modern browsers implement the HTML5 Crypto API. However, this API is asynchronous,
only enabled when on HTTPS in Chromium, and is quite low-level. It requires boilerplate
code to actually use with TextEncoder, ArrayBuffer and Uint32Array. Due this being
needed in the module loader, we'd have to load the fallback regardless. Considering
this is not used in a critical path for performance, it's not worth shipping two
implementations for this optimisation.
May also resolve:
* T44094
* T90411
* T94810
Bug: T94074
Change-Id: Ibb292d2416839327d1807a66c78fd96dac0637d0
2015-04-29 22:53:24 +00:00
|
|
|
* - string 'version'
|
2013-07-07 22:51:15 +00:00
|
|
|
* - array 'dependencies'
|
|
|
|
|
* - string|null 'group'
|
|
|
|
|
* - string 'source'
|
2019-10-12 15:13:38 +00:00
|
|
|
* @phan-param array<string,array{version:string,dependencies:array,group:?string,source:string}> &$registryData
|
2013-07-07 22:51:15 +00:00
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
public static function compileUnresolvedDependencies( array &$registryData ): void {
|
2013-07-07 22:51:15 +00:00
|
|
|
foreach ( $registryData as $name => &$data ) {
|
|
|
|
|
$dependencies = $data['dependencies'];
|
2019-05-22 18:29:32 +00:00
|
|
|
try {
|
|
|
|
|
foreach ( $data['dependencies'] as $dependency ) {
|
|
|
|
|
$implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
|
|
|
|
|
$dependencies = array_diff( $dependencies, $implicitDependencies );
|
|
|
|
|
}
|
|
|
|
|
} catch ( ResourceLoaderCircularDependencyError $err ) {
|
|
|
|
|
// Leave unchanged
|
|
|
|
|
$dependencies = $data['dependencies'];
|
2013-07-07 22:51:15 +00:00
|
|
|
}
|
2019-05-22 18:29:32 +00:00
|
|
|
|
2013-07-07 22:51:15 +00:00
|
|
|
// Rebuild keys
|
|
|
|
|
$data['dependencies'] = array_values( $dependencies );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-10-19 18:25:42 +00:00
|
|
|
/**
|
2014-03-07 18:31:05 +00:00
|
|
|
* Get registration code for all modules.
|
2010-10-19 18:25:42 +00:00
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param ResourceLoaderContext $context
|
2014-03-07 18:31:05 +00:00
|
|
|
* @return string JavaScript code for registering all modules with the client loader
|
2010-10-19 18:25:42 +00:00
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
public function getModuleRegistrations( ResourceLoaderContext $context ): string {
|
* Made Resources.php return a pure-data array instead of an ugly mix of data and code. This allows the class code to be lazy-loaded with the autoloader, for a performance advantage especially on non-APC installs. And using the convention where if the class is omitted, ResourceLoaderFileModule is assumed, the registration code becomes shorter and simpler.
* Modified ResourceLoader to lazy-initialise module objects, for a further performance advantage.
* Deleted ResourceLoader::getModules(), provided getModuleNames() instead. Although the startup module needs this functionality, it's slow to generate, so to avoid misuse, it's better to provide a foolproof fast interface and let the startup module do the slow thing itself.
* Modified ResourceLoader::register() to optionally accept an info array instead of an object.
* Added $wgResourceModules, allowing extensions to efficiently define their own resource loader modules. The trouble with hooks is that they contain code, and code is slow. We've been through all this before with i18n. Hooks are useful as a performance tool only if you call them very rarely.
* Moved ResourceLoader settings to their own section in DefaultSettings.php
* Added options to ResourceLoaderFileModule equivalent to the $localBasePath and $remoteBasePath parameters, to allow it to be instantiated via the new array style. Also added remoteExtPath, which allows modules to be registered before $wgExtensionAssetsPath is known.
* Added OutputPage::getResourceLoader(), mostly for debugging.
* The time saving at the moment is about 5ms per request with no extensions, which is significant already with 6 load.php requests for a cold cache page view. This is a much more scalable interface; the relative saving will grow as more extensions are added which use this interface, especially for non-APC installs.
Although the interface is backwards compatible, extension updates will follow in a subsequent commit.
2010-11-19 10:41:06 +00:00
|
|
|
$resourceLoader = $context->getResourceLoader();
|
2018-03-22 03:35:16 +00:00
|
|
|
// Future developers: Use WebRequest::getRawVal() instead getVal().
|
|
|
|
|
// The getVal() method performs slow Language+UTF logic. (f303bb9360)
|
|
|
|
|
$target = $context->getRequest()->getRawVal( 'target', 'desktop' );
|
2018-03-28 00:57:06 +00:00
|
|
|
$safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
|
2015-11-17 03:55:19 +00:00
|
|
|
// Bypass target filter if this request is Special:JavaScriptTest.
|
|
|
|
|
// To prevent misuse in production, this is only allowed if testing is enabled server-side.
|
2015-06-04 20:12:23 +00:00
|
|
|
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
|
2011-07-26 21:10:34 +00:00
|
|
|
|
2014-03-07 18:31:05 +00:00
|
|
|
$out = '';
|
resourceloader: Don't let module exception break startup
When getScript (or some other method used in a module response)
throws an error, only that module fails (by outputting mw.loader.state
instead of mw.loader.implement). Other modules will work.
This has always been the case and is working fine. For example,
"load.php?modules=foo|bar", where 'foo' throws, will return:
```js
/* exception message: .. */
mw.loader.implement('bar', ..)
mw.loader.state('foo', 'error')
```
The problem, however, is that during the generation of the startup
module, we iterate over all other modules. In 2011, the
getVersionHash method (then: getModifiedTime) was fairly simple
and unlikely to throw errors.
Nowadays, some modules use enableModuleContentVersion which will
involve the same code path as for regular module responses.
The try/catch in ResourceLoader::makeModuleResponse() suffices
for the case of loading modules other than startup. But when
loading the startup module, and an exception happens in getVersionHash,
then the entire startup response is replaced with an exception comment.
Example case:
* A file not existing for a FileModule subclass that uses
enableModuleContentVersion.
* A database error from a data module, like CiteDataModule or
CNChoiceData.
Changes:
* Ensure E-Tag is still useful while an error happens in production
because we respond with 200 OK and one error isn't the same as
another.
Fixed by try/catch in getCombinedVersion.
* Ensure start manifest isn't disrupted by one broken module.
Fixed by try/catch in StartupModule::getModuleRegistrations().
Tests:
* testMakeModuleResponseError: The case that already worked fined.
* testMakeModuleResponseStartupError: The case fixed in this commit.
* testGetCombinedVersion: The case fixed in this commit for E-Tag.
Bug: T152266
Change-Id: Ice4ede5ea594bf3fa591134bc9382bd9c24e2f39
2016-12-03 00:48:14 +00:00
|
|
|
$states = [];
|
2016-02-17 09:09:32 +00:00
|
|
|
$registryData = [];
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
$moduleNames = $resourceLoader->getModuleNames();
|
|
|
|
|
|
|
|
|
|
// Preload with a batch so that the below calls to getVersionHash() for each module
|
|
|
|
|
// don't require on-demand loading of more information.
|
|
|
|
|
try {
|
|
|
|
|
$resourceLoader->preloadModuleInfo( $moduleNames, $context );
|
|
|
|
|
} catch ( Exception $e ) {
|
|
|
|
|
// Don't fail the request (T152266)
|
|
|
|
|
// Also print the error in the main output
|
|
|
|
|
$resourceLoader->outputErrorAndLog( $e,
|
|
|
|
|
'Preloading module info from startup failed: {exception}',
|
|
|
|
|
[ 'exception' => $e ]
|
|
|
|
|
);
|
|
|
|
|
}
|
2011-07-26 21:10:34 +00:00
|
|
|
|
2014-03-07 18:31:05 +00:00
|
|
|
// Get registry data
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
foreach ( $moduleNames as $name ) {
|
* Made Resources.php return a pure-data array instead of an ugly mix of data and code. This allows the class code to be lazy-loaded with the autoloader, for a performance advantage especially on non-APC installs. And using the convention where if the class is omitted, ResourceLoaderFileModule is assumed, the registration code becomes shorter and simpler.
* Modified ResourceLoader to lazy-initialise module objects, for a further performance advantage.
* Deleted ResourceLoader::getModules(), provided getModuleNames() instead. Although the startup module needs this functionality, it's slow to generate, so to avoid misuse, it's better to provide a foolproof fast interface and let the startup module do the slow thing itself.
* Modified ResourceLoader::register() to optionally accept an info array instead of an object.
* Added $wgResourceModules, allowing extensions to efficiently define their own resource loader modules. The trouble with hooks is that they contain code, and code is slow. We've been through all this before with i18n. Hooks are useful as a performance tool only if you call them very rarely.
* Moved ResourceLoader settings to their own section in DefaultSettings.php
* Added options to ResourceLoaderFileModule equivalent to the $localBasePath and $remoteBasePath parameters, to allow it to be instantiated via the new array style. Also added remoteExtPath, which allows modules to be registered before $wgExtensionAssetsPath is known.
* Added OutputPage::getResourceLoader(), mostly for debugging.
* The time saving at the moment is about 5ms per request with no extensions, which is significant already with 6 load.php requests for a cold cache page view. This is a much more scalable interface; the relative saving will grow as more extensions are added which use this interface, especially for non-APC installs.
Although the interface is backwards compatible, extension updates will follow in a subsequent commit.
2010-11-19 10:41:06 +00:00
|
|
|
$module = $resourceLoader->getModule( $name );
|
2012-10-11 22:37:59 +00:00
|
|
|
$moduleTargets = $module->getTargets();
|
2018-03-28 00:57:06 +00:00
|
|
|
if (
|
|
|
|
|
( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
|
|
|
|
|
|| ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
|
|
|
|
|
) {
|
2012-10-18 12:19:59 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
resourceloader: Remove support for raw modules
Being a raw module means that when it is requested from load.php with
"only=scripts" set, then the output is *not* wrapped in an
'mw.loader.implement' closure *and* there no 'mw.loader.state()' appendix.
Instead, it is served "raw".
Before 2018, the modules 'mediawiki' and 'jquery' were raw modules.
They were needed before the client could define 'mw.loader.implement', and
could never be valid dependencies. Module 'mediawiki' merged to 'startup',
and 'jquery' became a regular module (T192623). Based on the architecture
of modules being deliverable bundles, it doesn't make sense for there to
ever be raw modules again. Anything that 'startup' needs should be bundled
with it. Anything else is a regular module.
On top of that, we never actually needed this feature because specifying
the 'only=scripts' and 'raw=1' parameters does the same thing.
The only special bit about marking modules (not requests) as "raw" was that
it allowed the client to forget to specify "raw=1" and the server would
automatically omit the 'mw.loader.state()' appendix based on whether the
module is marked as raw. As of Ie4564ec8e26ad53f2, the two remaining use
cases for raw responses now specify the 'raw=1' request parameter, and we
can get rid of the "raw module" feature and all the complexity around it.
== Startup module
In the startup module there was an interesting use of isRaw() that has
little to do with the above. The "ATTENTION" warning there applies to the
startup module only, not raw modules in general. This is now fixed by
explicitly checking for StartupModule.
Above that warning, it talked about saving bytes, which was an optimisation
given that "raw" modules don't communicate with mw.loader, they also don't
need to be registered there because even if mw.loader would try to load
them, the server would never inform mw.loader about the module having
arrived. There are now no longer any such modules.
Bug: T201483
Change-Id: I8839036e7b2b76919b6cd3aa42ccfde4d1247899
2019-06-13 18:41:56 +00:00
|
|
|
if ( $module instanceof ResourceLoaderStartUpModule ) {
|
|
|
|
|
// Don't register 'startup' to the client because loading it lazily or depending
|
|
|
|
|
// on it doesn't make sense, because the startup module *is* the client.
|
|
|
|
|
// Registering would be a waste of bandwidth and memory and risks somehow causing
|
|
|
|
|
// it to load a second time.
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
|
|
|
|
|
// ATTENTION: Because of the line below, this is not going to cause infinite recursion.
|
|
|
|
|
// Think carefully before making changes to this code!
|
|
|
|
|
// The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
|
|
|
|
|
// For StartUpModule (this module) the hash is computed based on the manifest content,
|
|
|
|
|
// which is the very thing we are computing right here. As such, this must skip iterating
|
|
|
|
|
// over 'startup' itself.
|
2014-08-28 20:16:03 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
resourceloader: Don't let module exception break startup
When getScript (or some other method used in a module response)
throws an error, only that module fails (by outputting mw.loader.state
instead of mw.loader.implement). Other modules will work.
This has always been the case and is working fine. For example,
"load.php?modules=foo|bar", where 'foo' throws, will return:
```js
/* exception message: .. */
mw.loader.implement('bar', ..)
mw.loader.state('foo', 'error')
```
The problem, however, is that during the generation of the startup
module, we iterate over all other modules. In 2011, the
getVersionHash method (then: getModifiedTime) was fairly simple
and unlikely to throw errors.
Nowadays, some modules use enableModuleContentVersion which will
involve the same code path as for regular module responses.
The try/catch in ResourceLoader::makeModuleResponse() suffices
for the case of loading modules other than startup. But when
loading the startup module, and an exception happens in getVersionHash,
then the entire startup response is replaced with an exception comment.
Example case:
* A file not existing for a FileModule subclass that uses
enableModuleContentVersion.
* A database error from a data module, like CiteDataModule or
CNChoiceData.
Changes:
* Ensure E-Tag is still useful while an error happens in production
because we respond with 200 OK and one error isn't the same as
another.
Fixed by try/catch in getCombinedVersion.
* Ensure start manifest isn't disrupted by one broken module.
Fixed by try/catch in StartupModule::getModuleRegistrations().
Tests:
* testMakeModuleResponseError: The case that already worked fined.
* testMakeModuleResponseStartupError: The case fixed in this commit.
* testGetCombinedVersion: The case fixed in this commit for E-Tag.
Bug: T152266
Change-Id: Ice4ede5ea594bf3fa591134bc9382bd9c24e2f39
2016-12-03 00:48:14 +00:00
|
|
|
try {
|
|
|
|
|
$versionHash = $module->getVersionHash( $context );
|
|
|
|
|
} catch ( Exception $e ) {
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
// Don't fail the request (T152266)
|
|
|
|
|
// Also print the error in the main output
|
|
|
|
|
$resourceLoader->outputErrorAndLog( $e,
|
resourceloader: Don't let module exception break startup
When getScript (or some other method used in a module response)
throws an error, only that module fails (by outputting mw.loader.state
instead of mw.loader.implement). Other modules will work.
This has always been the case and is working fine. For example,
"load.php?modules=foo|bar", where 'foo' throws, will return:
```js
/* exception message: .. */
mw.loader.implement('bar', ..)
mw.loader.state('foo', 'error')
```
The problem, however, is that during the generation of the startup
module, we iterate over all other modules. In 2011, the
getVersionHash method (then: getModifiedTime) was fairly simple
and unlikely to throw errors.
Nowadays, some modules use enableModuleContentVersion which will
involve the same code path as for regular module responses.
The try/catch in ResourceLoader::makeModuleResponse() suffices
for the case of loading modules other than startup. But when
loading the startup module, and an exception happens in getVersionHash,
then the entire startup response is replaced with an exception comment.
Example case:
* A file not existing for a FileModule subclass that uses
enableModuleContentVersion.
* A database error from a data module, like CiteDataModule or
CNChoiceData.
Changes:
* Ensure E-Tag is still useful while an error happens in production
because we respond with 200 OK and one error isn't the same as
another.
Fixed by try/catch in getCombinedVersion.
* Ensure start manifest isn't disrupted by one broken module.
Fixed by try/catch in StartupModule::getModuleRegistrations().
Tests:
* testMakeModuleResponseError: The case that already worked fined.
* testMakeModuleResponseStartupError: The case fixed in this commit.
* testGetCombinedVersion: The case fixed in this commit for E-Tag.
Bug: T152266
Change-Id: Ice4ede5ea594bf3fa591134bc9382bd9c24e2f39
2016-12-03 00:48:14 +00:00
|
|
|
'Calculating version for "{module}" failed: {exception}',
|
|
|
|
|
[
|
|
|
|
|
'module' => $name,
|
|
|
|
|
'exception' => $e,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
$versionHash = '';
|
|
|
|
|
$states[$name] = 'error';
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-29 16:15:23 +00:00
|
|
|
if ( $versionHash !== '' && strlen( $versionHash ) !== ResourceLoader::HASH_LENGTH ) {
|
2019-08-01 00:19:45 +00:00
|
|
|
$e = new RuntimeException( "Badly formatted module version hash" );
|
|
|
|
|
$resourceLoader->outputErrorAndLog( $e,
|
|
|
|
|
"Module '{module}' produced an invalid version hash: '{version}'.",
|
2016-02-17 09:09:32 +00:00
|
|
|
[
|
2015-11-10 03:12:24 +00:00
|
|
|
'module' => $name,
|
|
|
|
|
'version' => $versionHash,
|
2016-02-17 09:09:32 +00:00
|
|
|
]
|
2015-11-10 03:12:24 +00:00
|
|
|
);
|
resourceloader: Replace timestamp system with version hashing
Modules now track their version via getVersionHash() instead of getModifiedTime().
== Background ==
While some resources have observeable timestamps (e.g. files stored on disk),
many other resources do not. E.g. config variables, and module definitions.
For static file modules, one can e.g. revert one of more files in a module to a
previous version and not affect the max timestamp.
Wiki modules include pages only if they exist. The user module supports common.js
and skin.js. By default neither exists. If a user has both, and then the
less-recently modified one is deleted, the max-timestamp remains unchanged.
For client-side caching, batch requests use "Math.max" on the relevant timestamps.
Again, if a module changes but another module is more recent (e.g. out-of-order
deployment, or out-of-order discovery), the change would not result in a cache miss.
More scenarios can be found in the associated Phabricator tasks.
== Version hash ==
Previously we virtually mapped these variables to a timestamp by storing the current
time alongside a hash of the value in ObjectCache. Considering the number of
possible request contexts (wikis * modules * users * skins * languages) this doesn't
work well. It results in needless cache invalidation when the first time observation
is purged due to LRU algorithms. It also has other minor bugs leading to fewer
cache hits.
All modules automatically get the benefits of version hashing with this change.
The old getDefinitionMtime() and getHashMtime() have been replaced with dummies
that return 1. These functions are often called from getModifiedTime() in subclasses.
For backward-compatibility, their respective values (definition summary and hash)
are now included in getVersionHash directly.
As examples, the following modules have been updated to use getVersionHash directly.
Other modules still work fine and can be updated later.
* ResourceLoaderFileModule
* ResourceLoaderEditToolbarModule
* ResourceLoaderStartUpModule
* ResourceLoaderWikiModule
The presence of hashes in place of timestamps increases the startup module size on
a default MediaWiki install from 4.4k to 5.8k (after gzip and minification).
== ETag ==
Since timestamps are no longer tracked, we need a different way to implement caching
for cache proxies (e.g. Varnish) and web browsers. Previously we used the
Last-Modified header (in combination with Cache-Control and Expires).
Instead of Last-Modified (and If-Modified-Since), we use ETag (and If-None-Match).
Entity tags (new in HTTP/1.1) are much stricter than Last-Modified by default.
They instruct browsers to allow usage of partial Range requests. Since our responses
are dynamically generated, we need to use the Weak version of ETag.
While this sounds bad, it's no different than Last-Modified. As reassured by
RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3> the
specified behaviour behind Last-Modified follows the same "Weak" caching logic as
Entity tags. It's just that entity tags are capable of a stricter mode (whereas
Last-Modified is inherently weak).
== File cache ==
If $wgUseFileCache is enabled, ResourceLoader uses ResourceFileCache to cache
load.php responses. While the blind TTL handling (during the allowed expiry period)
is still maxage/timestamp based, tryRespondNotModified() now requires the caller to
know the expected ETag.
For this to work, the FileCache handling had to be moved from the top of
ResoureLoader::respond() to after the expected ETag is computed.
This also allows us to remove the duplicate tryRespondNotModified() handling since
that's is already handled by ResourceLoader::respond() meanwhile.
== Misc ==
* Remove redundant modifiedTime cache in ResourceLoaderFileModule.
* Change bugzilla references to Phabricator.
* Centralised inclusion of wgCacheEpoch using getDefinitionSummary. Previously this
logic was duplicated in each place the modified timestamp was used.
* It's easy to forget calling the parent class in getDefinitionSummary().
Previously this method only tracked 'class' by default. As such, various
extensions hardcoded that one value instead of calling the parent and extending
the array. To better prevent this in the future, getVersionHash() now asserts
that the '_cacheEpoch' property made it through.
* tests: Don't use getDefinitionSummary() as an API.
Fix ResourceLoaderWikiModuleTest to call getPages properly.
* In tests, the default timestamp used to be 1388534400000 (which is the unix time
of 20140101000000; the unit tests' CacheEpoch). The new version hash of these
modules is "XyCC+PSK", which is the base64 encoded prefix of the SHA1 digest of:
'{"_class":"ResourceLoaderTestModule","_cacheEpoch":"20140101000000"}'
* Add sha1.js library for client-side hash generation.
Compared various different implementations for code size (after minfication/gzip),
and speed (when used for short hexidecimal strings).
https://jsperf.com/sha1-implementations
- CryptoJS <https://code.google.com/p/crypto-js/#SHA-1> (min+gzip: 2.5k)
http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha1.js
Chrome: 45k, Firefox: 89k, Safari: 92k
- jsSHA <https://github.com/Caligatio/jsSHA>
https://github.com/Caligatio/jsSHA/blob/3c1d4f2e/src/sha1.js (min+gzip: 1.8k)
Chrome: 65k, Firefox: 53k, Safari: 69k
- phpjs-sha1 <https://github.com/kvz/phpjs> (RL min+gzip: 0.8k)
https://github.com/kvz/phpjs/blob/1eaab15d/functions/strings/sha1.js
Chrome: 200k, Firefox: 280k, Safari: 78k
Modern browsers implement the HTML5 Crypto API. However, this API is asynchronous,
only enabled when on HTTPS in Chromium, and is quite low-level. It requires boilerplate
code to actually use with TextEncoder, ArrayBuffer and Uint32Array. Due this being
needed in the module loader, we'd have to load the fallback regardless. Considering
this is not used in a critical path for performance, it's not worth shipping two
implementations for this optimisation.
May also resolve:
* T44094
* T90411
* T94810
Bug: T94074
Change-Id: Ibb292d2416839327d1807a66c78fd96dac0637d0
2015-04-29 22:53:24 +00:00
|
|
|
// Module implementation either broken or deviated from ResourceLoader::makeHash
|
|
|
|
|
// Asserted by tests/phpunit/structure/ResourcesTest.
|
|
|
|
|
$versionHash = ResourceLoader::makeHash( $versionHash );
|
|
|
|
|
}
|
2014-03-07 18:31:05 +00:00
|
|
|
|
2014-04-30 21:06:51 +00:00
|
|
|
$skipFunction = $module->getSkipFunction();
|
2019-09-09 15:50:13 +00:00
|
|
|
if ( $skipFunction !== null && !$context->getDebug() ) {
|
2015-10-01 18:05:08 +00:00
|
|
|
$skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
|
2014-04-30 21:06:51 +00:00
|
|
|
}
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$registryData[$name] = [
|
resourceloader: Replace timestamp system with version hashing
Modules now track their version via getVersionHash() instead of getModifiedTime().
== Background ==
While some resources have observeable timestamps (e.g. files stored on disk),
many other resources do not. E.g. config variables, and module definitions.
For static file modules, one can e.g. revert one of more files in a module to a
previous version and not affect the max timestamp.
Wiki modules include pages only if they exist. The user module supports common.js
and skin.js. By default neither exists. If a user has both, and then the
less-recently modified one is deleted, the max-timestamp remains unchanged.
For client-side caching, batch requests use "Math.max" on the relevant timestamps.
Again, if a module changes but another module is more recent (e.g. out-of-order
deployment, or out-of-order discovery), the change would not result in a cache miss.
More scenarios can be found in the associated Phabricator tasks.
== Version hash ==
Previously we virtually mapped these variables to a timestamp by storing the current
time alongside a hash of the value in ObjectCache. Considering the number of
possible request contexts (wikis * modules * users * skins * languages) this doesn't
work well. It results in needless cache invalidation when the first time observation
is purged due to LRU algorithms. It also has other minor bugs leading to fewer
cache hits.
All modules automatically get the benefits of version hashing with this change.
The old getDefinitionMtime() and getHashMtime() have been replaced with dummies
that return 1. These functions are often called from getModifiedTime() in subclasses.
For backward-compatibility, their respective values (definition summary and hash)
are now included in getVersionHash directly.
As examples, the following modules have been updated to use getVersionHash directly.
Other modules still work fine and can be updated later.
* ResourceLoaderFileModule
* ResourceLoaderEditToolbarModule
* ResourceLoaderStartUpModule
* ResourceLoaderWikiModule
The presence of hashes in place of timestamps increases the startup module size on
a default MediaWiki install from 4.4k to 5.8k (after gzip and minification).
== ETag ==
Since timestamps are no longer tracked, we need a different way to implement caching
for cache proxies (e.g. Varnish) and web browsers. Previously we used the
Last-Modified header (in combination with Cache-Control and Expires).
Instead of Last-Modified (and If-Modified-Since), we use ETag (and If-None-Match).
Entity tags (new in HTTP/1.1) are much stricter than Last-Modified by default.
They instruct browsers to allow usage of partial Range requests. Since our responses
are dynamically generated, we need to use the Weak version of ETag.
While this sounds bad, it's no different than Last-Modified. As reassured by
RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3> the
specified behaviour behind Last-Modified follows the same "Weak" caching logic as
Entity tags. It's just that entity tags are capable of a stricter mode (whereas
Last-Modified is inherently weak).
== File cache ==
If $wgUseFileCache is enabled, ResourceLoader uses ResourceFileCache to cache
load.php responses. While the blind TTL handling (during the allowed expiry period)
is still maxage/timestamp based, tryRespondNotModified() now requires the caller to
know the expected ETag.
For this to work, the FileCache handling had to be moved from the top of
ResoureLoader::respond() to after the expected ETag is computed.
This also allows us to remove the duplicate tryRespondNotModified() handling since
that's is already handled by ResourceLoader::respond() meanwhile.
== Misc ==
* Remove redundant modifiedTime cache in ResourceLoaderFileModule.
* Change bugzilla references to Phabricator.
* Centralised inclusion of wgCacheEpoch using getDefinitionSummary. Previously this
logic was duplicated in each place the modified timestamp was used.
* It's easy to forget calling the parent class in getDefinitionSummary().
Previously this method only tracked 'class' by default. As such, various
extensions hardcoded that one value instead of calling the parent and extending
the array. To better prevent this in the future, getVersionHash() now asserts
that the '_cacheEpoch' property made it through.
* tests: Don't use getDefinitionSummary() as an API.
Fix ResourceLoaderWikiModuleTest to call getPages properly.
* In tests, the default timestamp used to be 1388534400000 (which is the unix time
of 20140101000000; the unit tests' CacheEpoch). The new version hash of these
modules is "XyCC+PSK", which is the base64 encoded prefix of the SHA1 digest of:
'{"_class":"ResourceLoaderTestModule","_cacheEpoch":"20140101000000"}'
* Add sha1.js library for client-side hash generation.
Compared various different implementations for code size (after minfication/gzip),
and speed (when used for short hexidecimal strings).
https://jsperf.com/sha1-implementations
- CryptoJS <https://code.google.com/p/crypto-js/#SHA-1> (min+gzip: 2.5k)
http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha1.js
Chrome: 45k, Firefox: 89k, Safari: 92k
- jsSHA <https://github.com/Caligatio/jsSHA>
https://github.com/Caligatio/jsSHA/blob/3c1d4f2e/src/sha1.js (min+gzip: 1.8k)
Chrome: 65k, Firefox: 53k, Safari: 69k
- phpjs-sha1 <https://github.com/kvz/phpjs> (RL min+gzip: 0.8k)
https://github.com/kvz/phpjs/blob/1eaab15d/functions/strings/sha1.js
Chrome: 200k, Firefox: 280k, Safari: 78k
Modern browsers implement the HTML5 Crypto API. However, this API is asynchronous,
only enabled when on HTTPS in Chromium, and is quite low-level. It requires boilerplate
code to actually use with TextEncoder, ArrayBuffer and Uint32Array. Due this being
needed in the module loader, we'd have to load the fallback regardless. Considering
this is not used in a critical path for performance, it's not worth shipping two
implementations for this optimisation.
May also resolve:
* T44094
* T90411
* T94810
Bug: T94074
Change-Id: Ibb292d2416839327d1807a66c78fd96dac0637d0
2015-04-29 22:53:24 +00:00
|
|
|
'version' => $versionHash,
|
2015-04-08 21:34:08 +00:00
|
|
|
'dependencies' => $module->getDependencies( $context ),
|
2021-01-23 07:33:38 +00:00
|
|
|
'es6' => $module->requiresES6(),
|
2019-08-14 19:39:01 +00:00
|
|
|
'group' => $this->getGroupId( $module->getGroup() ),
|
2014-03-07 18:31:05 +00:00
|
|
|
'source' => $module->getSource(),
|
2014-04-30 21:06:51 +00:00
|
|
|
'skip' => $skipFunction,
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2014-03-07 18:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2013-07-07 22:51:15 +00:00
|
|
|
self::compileUnresolvedDependencies( $registryData );
|
|
|
|
|
|
2014-03-07 18:31:05 +00:00
|
|
|
// Register sources
|
2019-09-09 15:50:13 +00:00
|
|
|
$out .= ResourceLoader::makeLoaderSourcesScript( $context, $resourceLoader->getSources() );
|
2014-03-07 18:31:05 +00:00
|
|
|
|
2015-10-19 23:04:23 +00:00
|
|
|
// Figure out the different call signatures for mw.loader.register
|
2016-02-17 09:09:32 +00:00
|
|
|
$registrations = [];
|
2014-03-07 18:31:05 +00:00
|
|
|
foreach ( $registryData as $name => $data ) {
|
resourceloader: Replace timestamp system with version hashing
Modules now track their version via getVersionHash() instead of getModifiedTime().
== Background ==
While some resources have observeable timestamps (e.g. files stored on disk),
many other resources do not. E.g. config variables, and module definitions.
For static file modules, one can e.g. revert one of more files in a module to a
previous version and not affect the max timestamp.
Wiki modules include pages only if they exist. The user module supports common.js
and skin.js. By default neither exists. If a user has both, and then the
less-recently modified one is deleted, the max-timestamp remains unchanged.
For client-side caching, batch requests use "Math.max" on the relevant timestamps.
Again, if a module changes but another module is more recent (e.g. out-of-order
deployment, or out-of-order discovery), the change would not result in a cache miss.
More scenarios can be found in the associated Phabricator tasks.
== Version hash ==
Previously we virtually mapped these variables to a timestamp by storing the current
time alongside a hash of the value in ObjectCache. Considering the number of
possible request contexts (wikis * modules * users * skins * languages) this doesn't
work well. It results in needless cache invalidation when the first time observation
is purged due to LRU algorithms. It also has other minor bugs leading to fewer
cache hits.
All modules automatically get the benefits of version hashing with this change.
The old getDefinitionMtime() and getHashMtime() have been replaced with dummies
that return 1. These functions are often called from getModifiedTime() in subclasses.
For backward-compatibility, their respective values (definition summary and hash)
are now included in getVersionHash directly.
As examples, the following modules have been updated to use getVersionHash directly.
Other modules still work fine and can be updated later.
* ResourceLoaderFileModule
* ResourceLoaderEditToolbarModule
* ResourceLoaderStartUpModule
* ResourceLoaderWikiModule
The presence of hashes in place of timestamps increases the startup module size on
a default MediaWiki install from 4.4k to 5.8k (after gzip and minification).
== ETag ==
Since timestamps are no longer tracked, we need a different way to implement caching
for cache proxies (e.g. Varnish) and web browsers. Previously we used the
Last-Modified header (in combination with Cache-Control and Expires).
Instead of Last-Modified (and If-Modified-Since), we use ETag (and If-None-Match).
Entity tags (new in HTTP/1.1) are much stricter than Last-Modified by default.
They instruct browsers to allow usage of partial Range requests. Since our responses
are dynamically generated, we need to use the Weak version of ETag.
While this sounds bad, it's no different than Last-Modified. As reassured by
RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3> the
specified behaviour behind Last-Modified follows the same "Weak" caching logic as
Entity tags. It's just that entity tags are capable of a stricter mode (whereas
Last-Modified is inherently weak).
== File cache ==
If $wgUseFileCache is enabled, ResourceLoader uses ResourceFileCache to cache
load.php responses. While the blind TTL handling (during the allowed expiry period)
is still maxage/timestamp based, tryRespondNotModified() now requires the caller to
know the expected ETag.
For this to work, the FileCache handling had to be moved from the top of
ResoureLoader::respond() to after the expected ETag is computed.
This also allows us to remove the duplicate tryRespondNotModified() handling since
that's is already handled by ResourceLoader::respond() meanwhile.
== Misc ==
* Remove redundant modifiedTime cache in ResourceLoaderFileModule.
* Change bugzilla references to Phabricator.
* Centralised inclusion of wgCacheEpoch using getDefinitionSummary. Previously this
logic was duplicated in each place the modified timestamp was used.
* It's easy to forget calling the parent class in getDefinitionSummary().
Previously this method only tracked 'class' by default. As such, various
extensions hardcoded that one value instead of calling the parent and extending
the array. To better prevent this in the future, getVersionHash() now asserts
that the '_cacheEpoch' property made it through.
* tests: Don't use getDefinitionSummary() as an API.
Fix ResourceLoaderWikiModuleTest to call getPages properly.
* In tests, the default timestamp used to be 1388534400000 (which is the unix time
of 20140101000000; the unit tests' CacheEpoch). The new version hash of these
modules is "XyCC+PSK", which is the base64 encoded prefix of the SHA1 digest of:
'{"_class":"ResourceLoaderTestModule","_cacheEpoch":"20140101000000"}'
* Add sha1.js library for client-side hash generation.
Compared various different implementations for code size (after minfication/gzip),
and speed (when used for short hexidecimal strings).
https://jsperf.com/sha1-implementations
- CryptoJS <https://code.google.com/p/crypto-js/#SHA-1> (min+gzip: 2.5k)
http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha1.js
Chrome: 45k, Firefox: 89k, Safari: 92k
- jsSHA <https://github.com/Caligatio/jsSHA>
https://github.com/Caligatio/jsSHA/blob/3c1d4f2e/src/sha1.js (min+gzip: 1.8k)
Chrome: 65k, Firefox: 53k, Safari: 69k
- phpjs-sha1 <https://github.com/kvz/phpjs> (RL min+gzip: 0.8k)
https://github.com/kvz/phpjs/blob/1eaab15d/functions/strings/sha1.js
Chrome: 200k, Firefox: 280k, Safari: 78k
Modern browsers implement the HTML5 Crypto API. However, this API is asynchronous,
only enabled when on HTTPS in Chromium, and is quite low-level. It requires boilerplate
code to actually use with TextEncoder, ArrayBuffer and Uint32Array. Due this being
needed in the module loader, we'd have to load the fallback regardless. Considering
this is not used in a critical path for performance, it's not worth shipping two
implementations for this optimisation.
May also resolve:
* T44094
* T90411
* T94810
Bug: T94074
Change-Id: Ibb292d2416839327d1807a66c78fd96dac0637d0
2015-04-29 22:53:24 +00:00
|
|
|
// Call mw.loader.register(name, version, dependencies, group, source, skip)
|
2016-02-17 09:09:32 +00:00
|
|
|
$registrations[] = [
|
2014-12-09 00:29:19 +00:00
|
|
|
$name,
|
2021-01-23 07:33:38 +00:00
|
|
|
// HACK: signify ES6 with a ! added at the end of the version
|
|
|
|
|
// This avoids having to add another register() parameter, and generating
|
|
|
|
|
// a bunch of nulls for ES6-only modules
|
|
|
|
|
$data['version'] . ( $data['es6'] ? '!' : '' ),
|
2014-12-09 00:29:19 +00:00
|
|
|
$data['dependencies'],
|
|
|
|
|
$data['group'],
|
|
|
|
|
// Swap default (local) for null
|
|
|
|
|
$data['source'] === 'local' ? null : $data['source'],
|
|
|
|
|
$data['skip']
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2010-10-19 18:25:42 +00:00
|
|
|
}
|
2014-03-07 18:31:05 +00:00
|
|
|
|
|
|
|
|
// Register modules
|
2019-09-09 15:50:13 +00:00
|
|
|
$out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $context, $registrations );
|
2011-06-17 16:05:05 +00:00
|
|
|
|
resourceloader: Don't let module exception break startup
When getScript (or some other method used in a module response)
throws an error, only that module fails (by outputting mw.loader.state
instead of mw.loader.implement). Other modules will work.
This has always been the case and is working fine. For example,
"load.php?modules=foo|bar", where 'foo' throws, will return:
```js
/* exception message: .. */
mw.loader.implement('bar', ..)
mw.loader.state('foo', 'error')
```
The problem, however, is that during the generation of the startup
module, we iterate over all other modules. In 2011, the
getVersionHash method (then: getModifiedTime) was fairly simple
and unlikely to throw errors.
Nowadays, some modules use enableModuleContentVersion which will
involve the same code path as for regular module responses.
The try/catch in ResourceLoader::makeModuleResponse() suffices
for the case of loading modules other than startup. But when
loading the startup module, and an exception happens in getVersionHash,
then the entire startup response is replaced with an exception comment.
Example case:
* A file not existing for a FileModule subclass that uses
enableModuleContentVersion.
* A database error from a data module, like CiteDataModule or
CNChoiceData.
Changes:
* Ensure E-Tag is still useful while an error happens in production
because we respond with 200 OK and one error isn't the same as
another.
Fixed by try/catch in getCombinedVersion.
* Ensure start manifest isn't disrupted by one broken module.
Fixed by try/catch in StartupModule::getModuleRegistrations().
Tests:
* testMakeModuleResponseError: The case that already worked fined.
* testMakeModuleResponseStartupError: The case fixed in this commit.
* testGetCombinedVersion: The case fixed in this commit for E-Tag.
Bug: T152266
Change-Id: Ice4ede5ea594bf3fa591134bc9382bd9c24e2f39
2016-12-03 00:48:14 +00:00
|
|
|
if ( $states ) {
|
2019-09-09 15:50:13 +00:00
|
|
|
$out .= "\n" . ResourceLoader::makeLoaderStateScript( $context, $states );
|
resourceloader: Don't let module exception break startup
When getScript (or some other method used in a module response)
throws an error, only that module fails (by outputting mw.loader.state
instead of mw.loader.implement). Other modules will work.
This has always been the case and is working fine. For example,
"load.php?modules=foo|bar", where 'foo' throws, will return:
```js
/* exception message: .. */
mw.loader.implement('bar', ..)
mw.loader.state('foo', 'error')
```
The problem, however, is that during the generation of the startup
module, we iterate over all other modules. In 2011, the
getVersionHash method (then: getModifiedTime) was fairly simple
and unlikely to throw errors.
Nowadays, some modules use enableModuleContentVersion which will
involve the same code path as for regular module responses.
The try/catch in ResourceLoader::makeModuleResponse() suffices
for the case of loading modules other than startup. But when
loading the startup module, and an exception happens in getVersionHash,
then the entire startup response is replaced with an exception comment.
Example case:
* A file not existing for a FileModule subclass that uses
enableModuleContentVersion.
* A database error from a data module, like CiteDataModule or
CNChoiceData.
Changes:
* Ensure E-Tag is still useful while an error happens in production
because we respond with 200 OK and one error isn't the same as
another.
Fixed by try/catch in getCombinedVersion.
* Ensure start manifest isn't disrupted by one broken module.
Fixed by try/catch in StartupModule::getModuleRegistrations().
Tests:
* testMakeModuleResponseError: The case that already worked fined.
* testMakeModuleResponseStartupError: The case fixed in this commit.
* testGetCombinedVersion: The case fixed in this commit for E-Tag.
Bug: T152266
Change-Id: Ice4ede5ea594bf3fa591134bc9382bd9c24e2f39
2016-12-03 00:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2010-10-19 18:25:42 +00:00
|
|
|
return $out;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-22 03:11:47 +00:00
|
|
|
private function getGroupId( $groupName ): ?int {
|
2019-08-14 19:39:01 +00:00
|
|
|
if ( $groupName === null ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !array_key_exists( $groupName, $this->groupIds ) ) {
|
|
|
|
|
$this->groupIds[$groupName] = count( $this->groupIds );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->groupIds[$groupName];
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-15 20:20:14 +00:00
|
|
|
/**
|
|
|
|
|
* Base modules implicitly available to all modules.
|
|
|
|
|
*
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function getBaseModules(): array {
|
2019-10-08 21:10:04 +00:00
|
|
|
return [ 'jquery', 'mediawiki.base' ];
|
2015-08-05 22:42:26 +00:00
|
|
|
}
|
|
|
|
|
|
2019-06-29 07:47:57 +00:00
|
|
|
/**
|
|
|
|
|
* Get the localStorage key for the entire module store. The key references
|
|
|
|
|
* $wgDBname to prevent clashes between wikis under the same web domain.
|
|
|
|
|
*
|
|
|
|
|
* @return string localStorage item key for JavaScript
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function getStoreKey(): string {
|
2019-06-29 07:47:57 +00:00
|
|
|
return 'MediaWikiModuleStore:' . $this->getConfig()->get( 'DBname' );
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-04 20:30:24 +00:00
|
|
|
/**
|
|
|
|
|
* @see $wgResourceLoaderMaxQueryLength
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function getMaxQueryLength(): int {
|
2020-05-04 20:30:24 +00:00
|
|
|
$len = $this->getConfig()->get( 'ResourceLoaderMaxQueryLength' );
|
|
|
|
|
// - Ignore -1, which in MW 1.34 and earlier was used to mean "unlimited".
|
|
|
|
|
// - Ignore invalid values, e.g. non-int or other negative values.
|
|
|
|
|
if ( $len === false || $len < 0 ) {
|
|
|
|
|
// Default
|
|
|
|
|
$len = 2000;
|
|
|
|
|
}
|
|
|
|
|
return $len;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-28 13:06:42 +00:00
|
|
|
/**
|
|
|
|
|
* Get the key on which the JavaScript module cache (mw.loader.store) will vary.
|
|
|
|
|
*
|
|
|
|
|
* @param ResourceLoaderContext $context
|
|
|
|
|
* @return string String of concatenated vary conditions
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function getStoreVary( ResourceLoaderContext $context ): string {
|
2019-06-28 13:06:42 +00:00
|
|
|
return implode( ':', [
|
|
|
|
|
$context->getSkin(),
|
|
|
|
|
$this->getConfig()->get( 'ResourceLoaderStorageVersion' ),
|
|
|
|
|
$context->getLanguage(),
|
|
|
|
|
] );
|
|
|
|
|
}
|
|
|
|
|
|
2012-05-11 19:16:29 +00:00
|
|
|
/**
|
2014-03-07 18:31:05 +00:00
|
|
|
* @param ResourceLoaderContext $context
|
2017-04-03 08:24:41 +00:00
|
|
|
* @return string JavaScript code
|
2011-05-21 17:45:20 +00:00
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
public function getScript( ResourceLoaderContext $context ): string {
|
2014-08-07 10:25:56 +00:00
|
|
|
global $IP;
|
2019-03-06 15:45:55 +00:00
|
|
|
$conf = $this->getConfig();
|
|
|
|
|
|
2015-07-27 22:47:05 +00:00
|
|
|
if ( $context->getOnly() !== 'scripts' ) {
|
2019-12-17 11:56:53 +00:00
|
|
|
return '/* Requires only=scripts */';
|
2015-07-27 22:47:05 +00:00
|
|
|
}
|
2010-10-19 18:25:42 +00:00
|
|
|
|
resourceloader: Combine base modules and page modules requests
This commit implements step 4 and step 5 of the plan outlined at T192623.
Before this task began, the typical JavaScript execution flow was:
* HTML triggers request for startup module (js req 1).
* Startup module contains registry, site config, and triggers
a request for the base modules (js req 2).
* After the base modules arrive (which define jQuery and mw.loader),
the startup module invokes a callback that processes RLQ,
which is what will request modules for this page (js req 3).
In past weeks, we have:
* Made mediawiki.js independent of jQuery.
* Spun off 'mediawiki.base' from mediawiki.js – for everything
that wasn't needed for defining `mw.loader`.
* Moved mediawiki.js from the base module request to being embedded
as part of startup.js.
The concept of dependencies is native to ResourceLoader, and thanks to the
use of closures in mw.loader.implement() responses, we can download any
number of interdependant modules in a single request (or parallel requests).
Then, when a response arrives, mw.loader takes care to pause or resume
execution as-needed. It is normal for ResourceLoader to batch several modules
together, including their dependencies.
As such, we can eliminate one of the two roundtrips required before a
page can request modules. Specifically, we can eliminate "js req 2" (above),
by making the two remaining base modules ("jquery" and "mediawiki.base") an
implied dependency for all other modules, which ResourceLoader will naturally
fetch and execute in the right order as part of the batch request.
Bug: T192623
Change-Id: I17cd13dffebd6ae476044d8d038dc3974a1fa176
2018-07-12 20:09:28 +00:00
|
|
|
$startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
|
2018-06-15 20:20:14 +00:00
|
|
|
|
2021-06-11 15:11:37 +00:00
|
|
|
// The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html.
|
2018-06-15 20:20:14 +00:00
|
|
|
$mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
|
2021-08-19 07:54:30 +00:00
|
|
|
file_get_contents( "$IP/resources/src/startup/mediawiki.loader.js" ) .
|
2018-06-15 20:20:14 +00:00
|
|
|
file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
|
2019-03-06 15:45:55 +00:00
|
|
|
if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
|
resourceloader: Implement mw.inspect 'time' report
When enabling $wgResourceLoaderEnableJSProfiler, mw.loader gets instrumented
with the following timing values for each of the modules loaded on the page:
* 'total' - This measures the time spent in mw.loader#execute(), and
represents the initialisation of the module's implementation, including
the registration of messages, templates, and the execution of the 'script'
closure received from load.php.
* 'script' – This measures only the subset of time spent in the internal
runScript() function, and represents just the execution of the module's
JavaScript code as received through mw.loader.implement() from load.php.
For user scripts and site scripts, this measures the call to domEval
(formerly known as "globalEval").
* 'execute' - This measures the self time of mw.loader#execute(), which is
effectively `total - script`.
To view the report, enable the feature, then run `mw.inspect( 'time' )` from
the browser console, which will render a table with the initialisation
overhead from each module used on the page.
Bug: T133646
Change-Id: I68d1193b62c93c97cf09b7d344c896afb437c5ac
2018-07-10 00:28:55 +00:00
|
|
|
$mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
|
|
|
|
|
}
|
2011-03-20 17:15:51 +00:00
|
|
|
|
resourceloader: Combine base modules and page modules requests
This commit implements step 4 and step 5 of the plan outlined at T192623.
Before this task began, the typical JavaScript execution flow was:
* HTML triggers request for startup module (js req 1).
* Startup module contains registry, site config, and triggers
a request for the base modules (js req 2).
* After the base modules arrive (which define jQuery and mw.loader),
the startup module invokes a callback that processes RLQ,
which is what will request modules for this page (js req 3).
In past weeks, we have:
* Made mediawiki.js independent of jQuery.
* Spun off 'mediawiki.base' from mediawiki.js – for everything
that wasn't needed for defining `mw.loader`.
* Moved mediawiki.js from the base module request to being embedded
as part of startup.js.
The concept of dependencies is native to ResourceLoader, and thanks to the
use of closures in mw.loader.implement() responses, we can download any
number of interdependant modules in a single request (or parallel requests).
Then, when a response arrives, mw.loader takes care to pause or resume
execution as-needed. It is normal for ResourceLoader to batch several modules
together, including their dependencies.
As such, we can eliminate one of the two roundtrips required before a
page can request modules. Specifically, we can eliminate "js req 2" (above),
by making the two remaining base modules ("jquery" and "mediawiki.base") an
implied dependency for all other modules, which ResourceLoader will naturally
fetch and execute in the right order as part of the batch request.
Bug: T192623
Change-Id: I17cd13dffebd6ae476044d8d038dc3974a1fa176
2018-07-12 20:09:28 +00:00
|
|
|
// Perform replacements for mediawiki.js
|
resourceloader: Implement mw.inspect 'time' report
When enabling $wgResourceLoaderEnableJSProfiler, mw.loader gets instrumented
with the following timing values for each of the modules loaded on the page:
* 'total' - This measures the time spent in mw.loader#execute(), and
represents the initialisation of the module's implementation, including
the registration of messages, templates, and the execution of the 'script'
closure received from load.php.
* 'script' – This measures only the subset of time spent in the internal
runScript() function, and represents just the execution of the module's
JavaScript code as received through mw.loader.implement() from load.php.
For user scripts and site scripts, this measures the call to domEval
(formerly known as "globalEval").
* 'execute' - This measures the self time of mw.loader#execute(), which is
effectively `total - script`.
To view the report, enable the feature, then run `mw.inspect( 'time' )` from
the browser console, which will render a table with the initialisation
overhead from each module used on the page.
Bug: T133646
Change-Id: I68d1193b62c93c97cf09b7d344c896afb437c5ac
2018-07-10 00:28:55 +00:00
|
|
|
$mwLoaderPairs = [
|
resourceloader: Fix load.mock.php query parameter corruption in tests
=== Observe the bug
1. Run Special:JavaScriptTest
(add ?module=mediawiki.loader to run only the relevant tests)
2. In the Network panel, check the JS requests to load.mock.php?…
3. Without this patch, they are like:
"load.mock.php?1234?lang=en&modules=…&…"
With this patch, they are like:
"load.mock.php?lang=en&modules=…&…"
The question mark is only valid as the start of the query string,
not as divider between them. This means without this patch, the
"lang" parameter is simply ignored because it becomes part
of the key "1234?lang" with value "en".
=== What
The mock server doesn't do anything with "lang". And given that
RL sorts its query parameters for optimum cache-hit rate, the
corrupted parameter is always "lang", as its sorts before
"module" or "version", which our mock server does utilize.
As part of server-side compression of the startup module (d13e5b75),
we filter redundant base parameters that match the default. For
RLContext, this is `{ debug: false, lang: qqx, skin: fallback }`.
As such, if one were to mock the localisation backend with
uselang=qqx internally, the "lang" parameter will not need to be
sent, and thus the above bug will start corrupting the "modules"
paramater instead, which our test suite correctly detects as being
very badly broken.
=== Why
mediawiki.loader.test.js used QUnit.fixurl() as paranoid way to
avoid accidental caching. This blindly adds "?<random>" to the
url. Upstream QUnit assumes the URL will be a simple file on disk,
not expecting existing query parameters.
=== Fix
* Removing the call to QUnit.fixurl(). It was set by me years ago.
But, there is no reason to believe a browser would cache this
anyway. Plus, the file hardly ever changes. Just in case,
set a no-cache header on the server side instead.
* Relatedly, the export of $VARS.reqBase is an associative array in
PHP and becomes an object in JSON. Make sure this works even if
the PHP array is empty, by casting to an object. Otherwise, it
becomes `[]` instead of `{}` given an PHP php array is ambiguous
in terms of whether it is meant as hashtable or list.
Bug: T250045
Change-Id: I3b8ff427577af9df3f1c26500ecf3646973ad34c
2019-10-27 22:54:34 +00:00
|
|
|
// This should always be an object, even if the base vars are empty
|
|
|
|
|
// (such as when using the default lang/skin).
|
|
|
|
|
'$VARS.reqBase' => $context->encodeJson( (object)$context->getReqBase() ),
|
2019-09-09 15:50:13 +00:00
|
|
|
'$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
|
2020-05-04 20:30:24 +00:00
|
|
|
'$VARS.maxQueryLength' => $context->encodeJson( $this->getMaxQueryLength() ),
|
2019-07-25 16:41:06 +00:00
|
|
|
// The client-side module cache can be disabled by site configuration.
|
|
|
|
|
// It is also always disabled in debug mode.
|
2021-09-06 22:13:35 +00:00
|
|
|
'$VARS.storeDisabled' => $context->encodeJson(
|
|
|
|
|
!$conf->get( 'ResourceLoaderStorageEnabled' ) || $context->getDebug()
|
2019-07-25 16:41:06 +00:00
|
|
|
),
|
2019-09-09 15:50:13 +00:00
|
|
|
'$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
|
|
|
|
|
'$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
|
|
|
|
|
'$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
|
|
|
|
|
'$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
|
resourceloader: exclude mw.loader test code in production
The line exposing the defineFallbacks() method for tests,
`mw.redefineFallbacksForTest = window.QUnit && defineFallbacks;`,
is run unconditionally on all requests. However, this is only used
for testing in via Special:JavaScriptTest, which is only available
if $wgEnableJavaScriptTest is true, which for production sites is
not the case.* Thus, we don't even need to include the line. Replace
it with a $CODE substitution ResourceLoaderStartUpModule.
Additionally, move it a bit further up in the code to be grouped with
where defineFallbacks() is defined.
Should be a no-op in terms of functionality.
* Technically, we only need it if JavaScript tests are enabled,
and we are on Special:JavaScriptTest, but that doesn't seem
easy to check and isn't as important of an optimization.
Change-Id: I2f0d9443a118e713cc7016c4bc0102fdd2071ec0
2021-08-31 02:18:34 +00:00
|
|
|
// Only expose private mw.redefineFallbacksForTest in test mode.
|
|
|
|
|
'$CODE.maybeRedefineFallbacksForTest();' => $conf->get( 'EnableJavaScriptTest' ) ?
|
|
|
|
|
'mw.redefineFallbacksForTest = defineFallbacks;' :
|
|
|
|
|
'',
|
resourceloader: Implement mw.inspect 'time' report
When enabling $wgResourceLoaderEnableJSProfiler, mw.loader gets instrumented
with the following timing values for each of the modules loaded on the page:
* 'total' - This measures the time spent in mw.loader#execute(), and
represents the initialisation of the module's implementation, including
the registration of messages, templates, and the execution of the 'script'
closure received from load.php.
* 'script' – This measures only the subset of time spent in the internal
runScript() function, and represents just the execution of the module's
JavaScript code as received through mw.loader.implement() from load.php.
For user scripts and site scripts, this measures the call to domEval
(formerly known as "globalEval").
* 'execute' - This measures the self time of mw.loader#execute(), which is
effectively `total - script`.
To view the report, enable the feature, then run `mw.inspect( 'time' )` from
the browser console, which will render a table with the initialisation
overhead from each module used on the page.
Bug: T133646
Change-Id: I68d1193b62c93c97cf09b7d344c896afb437c5ac
2018-07-10 00:28:55 +00:00
|
|
|
];
|
|
|
|
|
$profilerStubs = [
|
|
|
|
|
'$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
|
|
|
|
|
'$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
|
|
|
|
|
'$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
|
|
|
|
|
'$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
|
|
|
|
|
];
|
2021-04-19 23:12:25 +00:00
|
|
|
$debugStubs = [
|
|
|
|
|
'$CODE.consoleLog();' => 'console.log.apply( console, arguments );',
|
|
|
|
|
];
|
|
|
|
|
// When profiling is enabled, insert the calls. When disabled (by default), insert nothing.
|
|
|
|
|
$mwLoaderPairs += $conf->get( 'ResourceLoaderEnableJSProfiler' )
|
|
|
|
|
? $profilerStubs
|
|
|
|
|
: array_fill_keys( array_keys( $profilerStubs ), '' );
|
|
|
|
|
$mwLoaderPairs += $context->getDebug()
|
|
|
|
|
? $debugStubs
|
|
|
|
|
: array_fill_keys( array_keys( $debugStubs ), '' );
|
resourceloader: Implement mw.inspect 'time' report
When enabling $wgResourceLoaderEnableJSProfiler, mw.loader gets instrumented
with the following timing values for each of the modules loaded on the page:
* 'total' - This measures the time spent in mw.loader#execute(), and
represents the initialisation of the module's implementation, including
the registration of messages, templates, and the execution of the 'script'
closure received from load.php.
* 'script' – This measures only the subset of time spent in the internal
runScript() function, and represents just the execution of the module's
JavaScript code as received through mw.loader.implement() from load.php.
For user scripts and site scripts, this measures the call to domEval
(formerly known as "globalEval").
* 'execute' - This measures the self time of mw.loader#execute(), which is
effectively `total - script`.
To view the report, enable the feature, then run `mw.inspect( 'time' )` from
the browser console, which will render a table with the initialisation
overhead from each module used on the page.
Bug: T133646
Change-Id: I68d1193b62c93c97cf09b7d344c896afb437c5ac
2018-07-10 00:28:55 +00:00
|
|
|
$mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
|
resourceloader: Combine base modules and page modules requests
This commit implements step 4 and step 5 of the plan outlined at T192623.
Before this task began, the typical JavaScript execution flow was:
* HTML triggers request for startup module (js req 1).
* Startup module contains registry, site config, and triggers
a request for the base modules (js req 2).
* After the base modules arrive (which define jQuery and mw.loader),
the startup module invokes a callback that processes RLQ,
which is what will request modules for this page (js req 3).
In past weeks, we have:
* Made mediawiki.js independent of jQuery.
* Spun off 'mediawiki.base' from mediawiki.js – for everything
that wasn't needed for defining `mw.loader`.
* Moved mediawiki.js from the base module request to being embedded
as part of startup.js.
The concept of dependencies is native to ResourceLoader, and thanks to the
use of closures in mw.loader.implement() responses, we can download any
number of interdependant modules in a single request (or parallel requests).
Then, when a response arrives, mw.loader takes care to pause or resume
execution as-needed. It is normal for ResourceLoader to batch several modules
together, including their dependencies.
As such, we can eliminate one of the two roundtrips required before a
page can request modules. Specifically, we can eliminate "js req 2" (above),
by making the two remaining base modules ("jquery" and "mediawiki.base") an
implied dependency for all other modules, which ResourceLoader will naturally
fetch and execute in the right order as part of the batch request.
Bug: T192623
Change-Id: I17cd13dffebd6ae476044d8d038dc3974a1fa176
2018-07-12 20:09:28 +00:00
|
|
|
|
2018-08-16 19:48:59 +00:00
|
|
|
// Perform string replacements for startup.js
|
|
|
|
|
$pairs = [
|
|
|
|
|
// Raw JavaScript code (not JSON)
|
|
|
|
|
'$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
|
|
|
|
|
'$CODE.defineLoader();' => $mwLoaderCode,
|
|
|
|
|
];
|
resourceloader: Combine base modules and page modules requests
This commit implements step 4 and step 5 of the plan outlined at T192623.
Before this task began, the typical JavaScript execution flow was:
* HTML triggers request for startup module (js req 1).
* Startup module contains registry, site config, and triggers
a request for the base modules (js req 2).
* After the base modules arrive (which define jQuery and mw.loader),
the startup module invokes a callback that processes RLQ,
which is what will request modules for this page (js req 3).
In past weeks, we have:
* Made mediawiki.js independent of jQuery.
* Spun off 'mediawiki.base' from mediawiki.js – for everything
that wasn't needed for defining `mw.loader`.
* Moved mediawiki.js from the base module request to being embedded
as part of startup.js.
The concept of dependencies is native to ResourceLoader, and thanks to the
use of closures in mw.loader.implement() responses, we can download any
number of interdependant modules in a single request (or parallel requests).
Then, when a response arrives, mw.loader takes care to pause or resume
execution as-needed. It is normal for ResourceLoader to batch several modules
together, including their dependencies.
As such, we can eliminate one of the two roundtrips required before a
page can request modules. Specifically, we can eliminate "js req 2" (above),
by making the two remaining base modules ("jquery" and "mediawiki.base") an
implied dependency for all other modules, which ResourceLoader will naturally
fetch and execute in the right order as part of the batch request.
Bug: T192623
Change-Id: I17cd13dffebd6ae476044d8d038dc3974a1fa176
2018-07-12 20:09:28 +00:00
|
|
|
$startupCode = strtr( $startupCode, $pairs );
|
2015-07-27 22:47:05 +00:00
|
|
|
|
resourceloader: Combine base modules and page modules requests
This commit implements step 4 and step 5 of the plan outlined at T192623.
Before this task began, the typical JavaScript execution flow was:
* HTML triggers request for startup module (js req 1).
* Startup module contains registry, site config, and triggers
a request for the base modules (js req 2).
* After the base modules arrive (which define jQuery and mw.loader),
the startup module invokes a callback that processes RLQ,
which is what will request modules for this page (js req 3).
In past weeks, we have:
* Made mediawiki.js independent of jQuery.
* Spun off 'mediawiki.base' from mediawiki.js – for everything
that wasn't needed for defining `mw.loader`.
* Moved mediawiki.js from the base module request to being embedded
as part of startup.js.
The concept of dependencies is native to ResourceLoader, and thanks to the
use of closures in mw.loader.implement() responses, we can download any
number of interdependant modules in a single request (or parallel requests).
Then, when a response arrives, mw.loader takes care to pause or resume
execution as-needed. It is normal for ResourceLoader to batch several modules
together, including their dependencies.
As such, we can eliminate one of the two roundtrips required before a
page can request modules. Specifically, we can eliminate "js req 2" (above),
by making the two remaining base modules ("jquery" and "mediawiki.base") an
implied dependency for all other modules, which ResourceLoader will naturally
fetch and execute in the right order as part of the batch request.
Bug: T192623
Change-Id: I17cd13dffebd6ae476044d8d038dc3974a1fa176
2018-07-12 20:09:28 +00:00
|
|
|
return $startupCode;
|
2010-10-19 18:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
2011-10-14 08:06:54 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
public function supportsURLLoading(): bool {
|
Fix the fixme on r88053: dependency handling was broken in debug mode in certain cases. More specifically, if A is a file module that depends on B, B is a wiki module that depends on C and C is a file module, the loading order is CBA (correct) in production mode but was BCA (wrong) in debug mode. Fixed this by URL-ifying scripts and styles for those modules in debug mode, as I said to on CR. What this means is that the initial debug=true request for a module will now always return arrays of URLs, never the JS or CSS itself. This was already the case for file modules (which returned arrays of URLs to the raw files), but not for other modules (which returned the JS and CSS itself). So for non-file modules, load.php?modules=foo&debug=true now returns some JS that instructs the loader to fetch the module's JS from load.php?modules=foo&debug=true&only=scripts and the CSS from ...&only=styles .
* Removed the magic behavior where ResourceLoaderModule::getScripts() and getStyles() could return an array of URLs where the documentation said they should return a JS/CSS string. Because I didn't restructure the calling code too much, the old magical behavior should still work.
* Instead, move this behavior to getScriptURLsForDebug() and getStyleURLsForDebug(). The default implementation constructs a single URL for a load.php request for the module with debug=true&only=scripts (or styles). The URL building code duplicates some things from OutputPage::makeResourceLoaderLink(), I'll clean that up later. ResourceLoaderFileModule overrides this method to return URLs to the raw files, using code that I removed from getScripts()/getStyles()
* Add ResourceLoaderModule::supportsURLLoading(), which returns true by default but may return false to indicate that a module does not support loading via a URL. This is needed to respect $this->debugRaw in ResourceLoaderFileModule (set to true for jquery and mediawiki), and obviously for the startup module as well, because we get bootstrapping problems otherwise (can't call mw.loader.implement() when the code for mw.loader isn't loaded yet)
2011-09-13 17:13:53 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2011-05-21 17:45:20 +00:00
|
|
|
/**
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
* @return bool
|
2014-03-20 06:04:48 +00:00
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
public function enableModuleContentVersion(): bool {
|
resourceloader: Use 'enableModuleContentVersion' for startup module
This significantly simplifies the getVersionHash implementation for
StartupModule, and fixes a couple of bugs.
Previously, the startup module's E-Tag was determined by the
'getDefinitionSummary' method, which combined the E-Tag values
from all registered modules, plus what we thought is all information
used by 'getScript' (config vars, embedded script files, list
of base modules, ...)
However, this were various things part of the manifest that it
forgot about, including:
* Changes to the list of dependencies of a module.
* Changes to the name of module.
* Changes to the cache group of module.
* Adding or removing a foreign module source (mw.loader.addSource).
These are all quite rare, and when they do change, they usually
also involve a change that *was* tracked already. But, sometimes
they don't and that's when bugs happened.
Instead of the tracking array of getDefinitionSummary, we now
use the 'enableModuleContentVersion' option for StartupModule,
which simply calls the actual getScript() method and hashes that.
Of note: When an exception happens with the version computation of
any individual module, we catch it, log it, and continue with the
rest. Previously, the first time such error was discovered at
run-time would be in the getCombinedVersion() call from
StartupModule::getAllModuleHashes(). That public getCombinedVersion()
method of ResourceLoader had the benefit of also outputting details
of that exception in the HTTP response output. In order to keep that
behaviour, I made outputErrorAndLog() public so that StartupModule
can call it directly now. This is covered by
ResourceLoaderTest::testMakeModuleResponseStartupError.
Bug: T201686
Change-Id: I8e8d3a2cd2ccd68d2d78e988bcdd0d77fbcbf1d4
2018-08-30 02:52:39 +00:00
|
|
|
// Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
|
|
|
|
|
// and hash it to determine the version (as used by E-Tag HTTP response header).
|
|
|
|
|
return true;
|
2014-03-20 06:04:48 +00:00
|
|
|
}
|
2011-06-16 21:20:05 +00:00
|
|
|
}
|