Skip to content

Commit

Permalink
* Made Resources.php return a pure-data array instead of an ugly mix …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
Tim Starling committed Nov 19, 2010
1 parent ec2acb2 commit dac5084
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 251 deletions.
95 changes: 59 additions & 36 deletions includes/DefaultSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -1646,42 +1646,6 @@
*/
$wgClockSkewFudge = 5;

/**
* Maximum time in seconds to cache resources served by the resource loader
*/
$wgResourceLoaderMaxage = array(
'versioned' => array(
// Squid/Varnish but also any other public proxy cache between the client and MediaWiki
'server' => 30 * 24 * 60 * 60, // 30 days
// On the client side (e.g. in the browser cache).
'client' => 30 * 24 * 60 * 60, // 30 days
),
'unversioned' => array(
'server' => 5 * 60, // 5 minutes
'client' => 5 * 60, // 5 minutes
),
);

/**
* Whether to embed private modules inline with HTML output or to bypass
* caching and check the user parameter against $wgUser to prevent
* unauthorized access to private modules.
*/
$wgResourceLoaderInlinePrivateModules = true;

/**
* The default debug mode (on/off) for of ResourceLoader requests. This will still
* be overridden when the debug URL parameter is used.
*/
$wgResourceLoaderDebug = false;

/**
* Enable embedding of certain resources using Edge Side Includes. This will
* improve performance but only works if there is something in front of the
* web server (e..g a Squid or Varnish server) configured to process the ESI.
*/
$wgResourceLoaderUseESI = false;

/** @} */ # end of cache settings

/************************************************************************//**
Expand Down Expand Up @@ -2319,6 +2283,65 @@

/** @} */ # End of output format settings }

/*************************************************************************//**
* @name Resource loader settings
* @{
*/

/**
* Client-side resource modules. Extensions should add their module definitions
* here.
*
* Example:
* $wgResourceModules['ext.myExtension'] = array(
* 'scripts' => 'myExtension.js',
* 'styles' => 'myExtension.css',
* 'dependencies' => array( 'jquery.cookie', 'jquery.tabIndex' ),
* 'localBasePath' => dirname( __FILE ),
* 'remoteExtPath' => 'MyExtension',
* );
*/
$wgResourceModules = array();

/**
* Maximum time in seconds to cache resources served by the resource loader
*/
$wgResourceLoaderMaxage = array(
'versioned' => array(
// Squid/Varnish but also any other public proxy cache between the client and MediaWiki
'server' => 30 * 24 * 60 * 60, // 30 days
// On the client side (e.g. in the browser cache).
'client' => 30 * 24 * 60 * 60, // 30 days
),
'unversioned' => array(
'server' => 5 * 60, // 5 minutes
'client' => 5 * 60, // 5 minutes
),
);

/**
* Whether to embed private modules inline with HTML output or to bypass
* caching and check the user parameter against $wgUser to prevent
* unauthorized access to private modules.
*/
$wgResourceLoaderInlinePrivateModules = true;

/**
* The default debug mode (on/off) for of ResourceLoader requests. This will still
* be overridden when the debug URL parameter is used.
*/
$wgResourceLoaderDebug = false;

/**
* Enable embedding of certain resources using Edge Side Includes. This will
* improve performance but only works if there is something in front of the
* web server (e..g a Squid or Varnish server) configured to process the ESI.
*/
$wgResourceLoaderUseESI = false;

/** @} */ # End of resource loader settings }


/*************************************************************************//**
* @name Page title and interwiki link settings
* @{
Expand Down
24 changes: 16 additions & 8 deletions includes/OutputPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,16 @@ public function headElement( Skin $sk, $includeStyle = true ) {
return $ret;
}

/**
* Get a ResourceLoader object associated with this OutputPage
*/
public function getResourceLoader() {
if ( is_null( $this->mResourceLoader ) ) {
$this->mResourceLoader = new ResourceLoader();
}
return $this->mResourceLoader;
}

/**
* TODO: Document
* @param $skin Skin
Expand All @@ -2301,9 +2311,6 @@ protected function makeResourceLoaderLink( Skin $skin, $modules, $only, $useESI
global $wgUser, $wgLang, $wgLoadScript, $wgResourceLoaderUseESI,
$wgResourceLoaderInlinePrivateModules;
// Lazy-load ResourceLoader
if ( is_null( $this->mResourceLoader ) ) {
$this->mResourceLoader = new ResourceLoader();
}
// TODO: Should this be a static function of ResourceLoader instead?
// TODO: Divide off modules starting with "user", and add the user parameter to them
$query = array(
Expand Down Expand Up @@ -2335,8 +2342,9 @@ protected function makeResourceLoaderLink( Skin $skin, $modules, $only, $useESI

// Create keyed-by-group list of module objects from modules list
$groups = array();
$resourceLoader = $this->getResourceLoader();
foreach ( (array) $modules as $name ) {
$module = $this->mResourceLoader->getModule( $name );
$module = $resourceLoader->getModule( $name );
$group = $module->getGroup();
if ( !isset( $groups[$group] ) ) {
$groups[$group] = array();
Expand All @@ -2352,15 +2360,15 @@ protected function makeResourceLoaderLink( Skin $skin, $modules, $only, $useESI
}
// Support inlining of private modules if configured as such
if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) {
$context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) );
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
if ( $only == 'styles' ) {
$links .= Html::inlineStyle(
$this->mResourceLoader->makeModuleResponse( $context, $modules )
$resourceLoader->makeModuleResponse( $context, $modules )
);
} else {
$links .= Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
$this->mResourceLoader->makeModuleResponse( $context, $modules )
$resourceLoader->makeModuleResponse( $context, $modules )
)
);
}
Expand All @@ -2371,7 +2379,7 @@ protected function makeResourceLoaderLink( Skin $skin, $modules, $only, $useESI
// we can ensure cache misses on change
if ( $group === 'user' || $group === 'site' ) {
// Create a fake request based on the one we are about to make so modules return correct times
$context = new ResourceLoaderContext( $this->mResourceLoader, new FauxRequest( $query ) );
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
// Get the maximum timestamp
$timestamp = 1;
foreach ( $modules as $module ) {
Expand Down
92 changes: 60 additions & 32 deletions includes/resourceloader/ResourceLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class ResourceLoader {

/** Array: List of module name/ResourceLoaderModule object pairs */
protected $modules = array();
/** Associative array mapping module name to info associative array */
protected $moduleInfos = array();

/* Protected Methods */

Expand Down Expand Up @@ -67,22 +69,22 @@ protected function preloadModuleInfo( array $modules, ResourceLoaderContext $con
// Set modules' dependecies
$modulesWithDeps = array();
foreach ( $res as $row ) {
$this->modules[$row->md_module]->setFileDependencies( $skin,
$this->getModule( $row->md_module )->setFileDependencies( $skin,
FormatJson::decode( $row->md_deps, true )
);
$modulesWithDeps[] = $row->md_module;
}

// Register the absence of a dependency row too
foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
$this->modules[$name]->setFileDependencies( $skin, array() );
$this->getModule( $name )->setFileDependencies( $skin, array() );
}

// Get message blob mtimes. Only do this for modules with messages
$modulesWithMessages = array();
$modulesWithoutMessages = array();
foreach ( $modules as $name ) {
if ( count( $this->modules[$name]->getMessages() ) ) {
if ( count( $this->getModule( $name )->getMessages() ) ) {
$modulesWithMessages[] = $name;
} else {
$modulesWithoutMessages[] = $name;
Expand All @@ -95,11 +97,11 @@ protected function preloadModuleInfo( array $modules, ResourceLoaderContext $con
), __METHOD__
);
foreach ( $res as $row ) {
$this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp );
$this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, $row->mr_timestamp );
}
}
foreach ( $modulesWithoutMessages as $name ) {
$this->modules[$name]->setMsgBlobMtime( $lang, 0 );
$this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
}
}

Expand Down Expand Up @@ -172,14 +174,15 @@ protected function filter( $filter, $data ) {
* Registers core modules and runs registration hooks.
*/
public function __construct() {
global $IP;
global $IP, $wgResourceModules;

wfProfileIn( __METHOD__ );

// Register core modules
$this->register( include( "$IP/resources/Resources.php" ) );
// Register extension modules
wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
$this->register( $wgResourceModules );

wfProfileOut( __METHOD__ );
}
Expand All @@ -188,57 +191,60 @@ public function __construct() {
* Registers a module with the ResourceLoader system.
*
* @param $name Mixed: Name of module as a string or List of name/object pairs as an array
* @param $object ResourceLoaderModule: Module object (optional when using
* multiple-registration calling style)
* @param $info Module info array. For backwards compatibility with 1.17alpha,
* this may also be a ResourceLoaderModule object. Optional when using
* multiple-registration calling style.
* @throws MWException: If a duplicate module registration is attempted
* @throws MWException: If something other than a ResourceLoaderModule is being registered
* @return Boolean: False if there were any errors, in which case one or more modules were not
* registered
*/
public function register( $name, ResourceLoaderModule $object = null ) {

public function register( $name, $info = null ) {
wfProfileIn( __METHOD__ );

// Allow multiple modules to be registered in one call
if ( is_array( $name ) && !isset( $object ) ) {
if ( is_array( $name ) ) {
foreach ( $name as $key => $value ) {
$this->register( $key, $value );
}

wfProfileOut( __METHOD__ );

return;
}

// Disallow duplicate registrations
if ( isset( $this->modules[$name] ) ) {
if ( isset( $this->moduleInfos[$name] ) ) {
// A module has already been registered by this name
throw new MWException(
'ResourceLoader duplicate registration error. ' .
'Another module has already been registered as ' . $name
);
}

// Validate the input (type hinting lets null through)
if ( !( $object instanceof ResourceLoaderModule ) ) {
throw new MWException( 'ResourceLoader invalid module error. ' .
'Instances of ResourceLoaderModule expected.' );
// Attach module
if ( is_object( $info ) ) {
// Old calling convention
// Validate the input
if ( !( $info instanceof ResourceLoaderModule ) ) {
throw new MWException( 'ResourceLoader invalid module error. ' .
'Instances of ResourceLoaderModule expected.' );
}

$this->moduleInfos[$name] = array( 'object' => $info );
$this->modules[$name] = $info;
} else {
// New calling convention
$this->moduleInfos[$name] = $info;
}

// Attach module
$this->modules[$name] = $object;
$object->setName( $name );

wfProfileOut( __METHOD__ );
}

/**
* Gets a map of all modules and their options
/**
* Get a list of module names
*
* @return Array: List of modules keyed by module name
* @return Array: List of module names
*/
public function getModules() {
return $this->modules;
public function getModuleNames() {
return array_keys( $this->moduleInfos );
}

/**
Expand All @@ -248,7 +254,29 @@ public function getModules() {
* @return Mixed: ResourceLoaderModule if module has been registered, null otherwise
*/
public function getModule( $name ) {
return isset( $this->modules[$name] ) ? $this->modules[$name] : null;
if ( !isset( $this->modules[$name] ) ) {
if ( !isset( $this->moduleInfos[$name] ) ) {
// No such module
return null;
}
// Construct the requested object
$info = $this->moduleInfos[$name];
if ( isset( $info['object'] ) ) {
// Object given in info array
$object = $info['object'];
} else {
if ( !isset( $info['class'] ) ) {
$class = 'ResourceLoaderFileModule';
} else {
$class = $info['class'];
}
$object = new $class( $info );
}
$object->setName( $name );
$this->modules[$name] = $object;
}

return $this->modules[$name];
}

/**
Expand All @@ -265,8 +293,8 @@ public function respond( ResourceLoaderContext $context ) {
$modules = array();
$missing = array();
foreach ( $context->getModules() as $name ) {
if ( isset( $this->modules[$name] ) ) {
$modules[$name] = $this->modules[$name];
if ( isset( $this->moduleInfos[$name] ) ) {
$modules[$name] = $this->getModule( $name );
} else {
$missing[] = $name;
}
Expand Down Expand Up @@ -376,7 +404,7 @@ public function makeModuleResponse( ResourceLoaderContext $context,
if ( $context->shouldIncludeStyles() ) {
$styles = $module->getStyles( $context );
// Flip CSS on a per-module basis
if ( $styles && $this->modules[$name]->getFlip( $context ) ) {
if ( $styles && $module->getFlip( $context ) ) {
foreach ( $styles as $media => $style ) {
$styles[$media] = $this->filter( 'flip-css', $style );
}
Expand Down
Loading

0 comments on commit dac5084

Please sign in to comment.