Index: trunk/phase3/includes/resourceloader/ResourceLoader.php |
— | — | @@ -353,8 +353,16 @@ |
354 | 354 | * @param $context ResourceLoaderContext: Context in which a response should be formed |
355 | 355 | */ |
356 | 356 | public function respond( ResourceLoaderContext $context ) { |
357 | | - global $wgCacheEpoch; |
| 357 | + global $wgCacheEpoch, $wgUseFileCache; |
358 | 358 | |
| 359 | + // Use file cache if enabled and available... |
| 360 | + if ( $wgUseFileCache ) { |
| 361 | + $fileCache = ResourceFileCache::newFromContext( $context ); |
| 362 | + if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) { |
| 363 | + return; // output handled |
| 364 | + } |
| 365 | + } |
| 366 | + |
359 | 367 | // Buffer output to catch warnings. Normally we'd use ob_clean() on the |
360 | 368 | // top-level output buffer to clear warnings, but that breaks when ob_gzhandler |
361 | 369 | // is used: ob_clean() will clear the GZIP header in that case and it won't come |
— | — | @@ -432,6 +440,18 @@ |
433 | 441 | ob_end_clean(); |
434 | 442 | echo $response; |
435 | 443 | |
| 444 | + // Save response to file cache unless there are private modules or errors |
| 445 | + if ( isset( $fileCache ) && !$private && !$exceptions && !$missing ) { |
| 446 | + // Cache single modules...and other requests if there are enough hits |
| 447 | + if ( ResourceFileCache::useFileCache( $context ) ) { |
| 448 | + if ( $fileCache->isCacheWorthy() ) { |
| 449 | + $fileCache->saveText( $response ); |
| 450 | + } else { |
| 451 | + $fileCache->incrMissesRecent( $context->getRequest() ); |
| 452 | + } |
| 453 | + } |
| 454 | + } |
| 455 | + |
436 | 456 | wfProfileOut( __METHOD__ ); |
437 | 457 | } |
438 | 458 | |
— | — | @@ -520,6 +540,52 @@ |
521 | 541 | } |
522 | 542 | |
523 | 543 | /** |
| 544 | + * Send out code for a response from file cache if possible |
| 545 | + * |
| 546 | + * @param $fileCache ObjectFileCache: Cache object for this request URL |
| 547 | + * @param $context ResourceLoaderContext: Context in which to generate a response |
| 548 | + * @return bool If this found a cache file and handled the response |
| 549 | + */ |
| 550 | + protected function tryRespondFromFileCache( |
| 551 | + ResourceFileCache $fileCache, ResourceLoaderContext $context |
| 552 | + ) { |
| 553 | + global $wgResourceLoaderMaxage; |
| 554 | + // Buffer output to catch warnings. |
| 555 | + ob_start(); |
| 556 | + // Get the maximum age the cache can be |
| 557 | + $maxage = is_null( $context->getVersion() ) |
| 558 | + ? $wgResourceLoaderMaxage['unversioned']['server'] |
| 559 | + : $wgResourceLoaderMaxage['versioned']['server']; |
| 560 | + // Minimum timestamp the cache file must have |
| 561 | + $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) ); |
| 562 | + if ( !$good ) { |
| 563 | + try { // RL always hits the DB on file cache miss... |
| 564 | + wfGetDB( DB_SLAVE ); |
| 565 | + } catch( DBConnectionError $e ) { // ...check if we need to fallback to cache |
| 566 | + $good = $fileCache->isCacheGood(); // cache existence check |
| 567 | + } |
| 568 | + } |
| 569 | + if ( $good ) { |
| 570 | + $ts = $fileCache->cacheTimestamp(); |
| 571 | + // Send content type and cache headers |
| 572 | + $this->sendResponseHeaders( $context, $ts, false ); |
| 573 | + // If there's an If-Modified-Since header, respond with a 304 appropriately |
| 574 | + if ( $this->tryRespondLastModified( $context, $ts ) ) { |
| 575 | + return; // output handled (buffers cleared) |
| 576 | + } |
| 577 | + $response = $fileCache->fetchText(); |
| 578 | + // Remove the output buffer and output the response |
| 579 | + ob_end_clean(); |
| 580 | + echo $response . "\n/* Cached {$ts} */"; |
| 581 | + return true; // cache hit |
| 582 | + } |
| 583 | + // Clear buffer |
| 584 | + ob_end_clean(); |
| 585 | + |
| 586 | + return false; // cache miss |
| 587 | + } |
| 588 | + |
| 589 | + /** |
524 | 590 | * Generates code for a response |
525 | 591 | * |
526 | 592 | * @param $context ResourceLoaderContext: Context in which to generate a response |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -373,6 +373,7 @@ |
374 | 374 | 'LinkCache' => 'includes/cache/LinkCache.php', |
375 | 375 | 'MessageCache' => 'includes/cache/MessageCache.php', |
376 | 376 | 'ObjectFileCache' => 'includes/cache/ObjectFileCache.php', |
| 377 | + 'ResourceFileCache' => 'includes/cache/ResourceFileCache.php', |
377 | 378 | 'SquidUpdate' => 'includes/cache/SquidUpdate.php', |
378 | 379 | 'TitleDependency' => 'includes/cache/CacheDependency.php', |
379 | 380 | 'TitleListDependency' => 'includes/cache/CacheDependency.php', |
Index: trunk/phase3/includes/cache/ObjectFileCache.php |
— | — | @@ -4,7 +4,7 @@ |
5 | 5 | * @file |
6 | 6 | * @ingroup Cache |
7 | 7 | */ |
8 | | -class ObjectFileCache extends FileCacheBase { |
| 8 | +abstract class ObjectFileCache extends FileCacheBase { |
9 | 9 | /** |
10 | 10 | * Construct an ObjectFileCache from a key and a type |
11 | 11 | * @param $key string |
— | — | @@ -14,38 +14,18 @@ |
15 | 15 | public static function newFromKey( $key, $type ) { |
16 | 16 | $cache = new self(); |
17 | 17 | |
18 | | - $allowedTypes = self::cacheableTypes(); |
19 | | - if ( !isset( $allowedTypes[$type] ) ) { |
20 | | - throw new MWException( "Invalid filecache type given." ); |
21 | | - } |
22 | 18 | $cache->mKey = (string)$key; |
23 | 19 | $cache->mType = (string)$type; |
24 | | - $cache->mExt = $allowedTypes[$cache->mType]; |
| 20 | + $cache->mExt = 'cache'; |
25 | 21 | |
26 | 22 | return $cache; |
27 | 23 | } |
28 | 24 | |
29 | 25 | /** |
30 | | - * Get the type => extension mapping |
31 | | - * @return array |
32 | | - */ |
33 | | - protected static function cacheableTypes() { |
34 | | - return array( 'resources-js' => 'js', 'resources-css' => 'css' ); |
35 | | - } |
36 | | - |
37 | | - /** |
38 | 26 | * Get the base file cache directory |
39 | 27 | * @return string |
40 | 28 | */ |
41 | 29 | protected function cacheDirectory() { |
42 | | - global $wgCacheDirectory, $wgFileCacheDirectory, $wgFileCacheDepth; |
43 | | - if ( $wgFileCacheDirectory ) { |
44 | | - $dir = $wgFileCacheDirectory; |
45 | | - } elseif ( $wgCacheDirectory ) { |
46 | | - $dir = "$wgCacheDirectory/object"; |
47 | | - } else { |
48 | | - throw new MWException( 'Please set $wgCacheDirectory in LocalSettings.php if you wish to use the HTML file cache' ); |
49 | | - } |
50 | | - return $dir; |
| 30 | + return $this->baseCacheDirectory() . '/object'; |
51 | 31 | } |
52 | 32 | } |
Index: trunk/phase3/includes/cache/ResourceFileCache.php |
— | — | @@ -0,0 +1,84 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Contain the ResourceFileCache class |
| 5 | + * @file |
| 6 | + * @ingroup Cache |
| 7 | + */ |
| 8 | +class ResourceFileCache extends FileCacheBase { |
| 9 | + protected $mCacheWorthy; |
| 10 | + |
| 11 | + /* @TODO: configurable? */ |
| 12 | + const MISS_THRESHOLD = 360; // 6/min * 60 min |
| 13 | + |
| 14 | + /** |
| 15 | + * Construct an ResourceFileCache from a context |
| 16 | + * @param $context ResourceLoaderContext |
| 17 | + * @return ResourceFileCache |
| 18 | + */ |
| 19 | + public static function newFromContext( ResourceLoaderContext $context ) { |
| 20 | + $cache = new self(); |
| 21 | + |
| 22 | + if ( $context->getOnly() === 'styles' ) { |
| 23 | + $cache->mType = $cache->mExt = 'css'; |
| 24 | + } else { |
| 25 | + $cache->mType = $cache->mExt = 'js'; |
| 26 | + } |
| 27 | + $modules = array_unique( $context->getModules() ); // remove duplicates |
| 28 | + sort( $modules ); // normalize the order (permutation => combination) |
| 29 | + $cache->mKey = sha1( $context->getHash() . implode( '|', $modules ) ); |
| 30 | + if ( count( $modules ) == 1 ) { |
| 31 | + $cache->mCacheWorthy = true; // won't take up much space |
| 32 | + } |
| 33 | + |
| 34 | + return $cache; |
| 35 | + } |
| 36 | + |
| 37 | + /** |
| 38 | + * Check if an RL request can be cached. |
| 39 | + * Caller is responsible for checking if any modules are private. |
| 40 | + * @param $context ResourceLoaderContext |
| 41 | + * @return bool |
| 42 | + */ |
| 43 | + public static function useFileCache( ResourceLoaderContext $context ) { |
| 44 | + global $wgUseFileCache, $wgDefaultSkin, $wgLanguageCode; |
| 45 | + if ( !$wgUseFileCache ) { |
| 46 | + return false; |
| 47 | + } |
| 48 | + // Get all query values |
| 49 | + $queryVals = $context->getRequest()->getValues(); |
| 50 | + foreach ( $queryVals as $query => $val ) { |
| 51 | + if ( $query === 'modules' || $query === '*' ) { // &* added as IE fix |
| 52 | + continue; |
| 53 | + } elseif ( $query === 'skin' && $val === $wgDefaultSkin ) { |
| 54 | + continue; |
| 55 | + } elseif ( $query === 'lang' && $val === $wgLanguageCode ) { |
| 56 | + continue; |
| 57 | + } elseif ( $query === 'only' && in_array( $val, array( 'styles', 'scripts' ) ) ) { |
| 58 | + continue; |
| 59 | + } elseif ( $query === 'debug' && $val === 'false' ) { |
| 60 | + continue; |
| 61 | + } |
| 62 | + return false; |
| 63 | + } |
| 64 | + return true; // cacheable |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Get the base file cache directory |
| 69 | + * @return string |
| 70 | + */ |
| 71 | + protected function cacheDirectory() { |
| 72 | + return $this->baseCacheDirectory() . '/resources'; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Recent cache misses |
| 77 | + * @return bool |
| 78 | + */ |
| 79 | + public function isCacheWorthy() { |
| 80 | + if ( $this->mCacheWorthy === null ) { |
| 81 | + $this->mCacheWorthy = ( $this->getMissesRecent() >= self::MISS_THRESHOLD ); |
| 82 | + } |
| 83 | + return $this->mCacheWorthy; |
| 84 | + } |
| 85 | +} |
Property changes on: trunk/phase3/includes/cache/ResourceFileCache.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 86 | + native |
Index: trunk/phase3/includes/cache/FileCacheBase.php |
— | — | @@ -6,19 +6,37 @@ |
7 | 7 | */ |
8 | 8 | abstract class FileCacheBase { |
9 | 9 | protected $mKey; |
10 | | - protected $mType; |
11 | | - protected $mExt; |
| 10 | + protected $mType = 'object'; |
| 11 | + protected $mExt = 'cache'; |
12 | 12 | protected $mFilePath; |
13 | 13 | protected $mUseGzip; |
14 | 14 | |
| 15 | + /* @TODO: configurable? */ |
| 16 | + const MISS_FACTOR = 10; // log 1 every MISS_FACTOR cache misses |
| 17 | + |
15 | 18 | protected function __construct() { |
16 | 19 | global $wgUseGzip; |
17 | 20 | |
18 | 21 | $this->mUseGzip = (bool)$wgUseGzip; |
19 | | - $this->mExt = 'cache'; |
20 | 22 | } |
21 | 23 | |
22 | 24 | /** |
| 25 | + * Get the base file cache directory |
| 26 | + * @return string |
| 27 | + */ |
| 28 | + final protected function baseCacheDirectory() { |
| 29 | + global $wgCacheDirectory, $wgFileCacheDirectory, $wgFileCacheDepth; |
| 30 | + if ( $wgFileCacheDirectory ) { |
| 31 | + $dir = $wgFileCacheDirectory; |
| 32 | + } elseif ( $wgCacheDirectory ) { |
| 33 | + $dir = $wgCacheDirectory; |
| 34 | + } else { |
| 35 | + throw new MWException( 'Please set $wgCacheDirectory in LocalSettings.php if you wish to use the HTML file cache' ); |
| 36 | + } |
| 37 | + return $dir; |
| 38 | + } |
| 39 | + |
| 40 | + /** |
23 | 41 | * Get the base cache directory (not speficic to this file) |
24 | 42 | * @return string |
25 | 43 | */ |
— | — | @@ -34,7 +52,8 @@ |
35 | 53 | } |
36 | 54 | |
37 | 55 | $dir = $this->cacheDirectory(); |
38 | | - $subDirs = $this->mType . '/' . $this->hashSubdirectory(); // includes '/' |
| 56 | + # Build directories (methods include the trailing "/") |
| 57 | + $subDirs = $this->typeSubdirectory() . $this->hashSubdirectory(); |
39 | 58 | # Avoid extension confusion |
40 | 59 | $key = str_replace( '.', '%2E', urlencode( $this->mKey ) ); |
41 | 60 | # Build the full file path |
— | — | @@ -112,6 +131,7 @@ |
113 | 132 | */ |
114 | 133 | public function saveText( $text ) { |
115 | 134 | global $wgUseFileCache; |
| 135 | + |
116 | 136 | if ( !$wgUseFileCache ) { |
117 | 137 | return false; |
118 | 138 | } |
— | — | @@ -121,7 +141,7 @@ |
122 | 142 | } |
123 | 143 | |
124 | 144 | $this->checkCacheDirs(); // build parent dir |
125 | | - if ( !file_put_contents( $this->cachePath(), $text ) ) { |
| 145 | + if ( !file_put_contents( $this->cachePath(), $text, LOCK_EX ) ) { |
126 | 146 | return false; |
127 | 147 | } |
128 | 148 | |
— | — | @@ -140,21 +160,23 @@ |
141 | 161 | |
142 | 162 | /** |
143 | 163 | * Create parent directors of $this->cachePath() |
144 | | - * @TODO: why call wfMkdirParents() twice? |
145 | 164 | * @return void |
146 | 165 | */ |
147 | 166 | protected function checkCacheDirs() { |
148 | | - $filename = $this->cachePath(); |
149 | | - $mydir2 = substr( $filename, 0, strrpos( $filename, '/') ); # subdirectory level 2 |
150 | | - $mydir1 = substr( $mydir2, 0, strrpos( $mydir2, '/') ); # subdirectory level 1 |
| 167 | + wfMkdirParents( dirname( $this->cachePath() ), null, __METHOD__ ); |
| 168 | + } |
151 | 169 | |
152 | | - wfMkdirParents( $mydir1, null, __METHOD__ ); |
153 | | - wfMkdirParents( $mydir2, null, __METHOD__ ); |
| 170 | + /** |
| 171 | + * Get the cache type subdirectory (with trailing slash) or the empty string |
| 172 | + * @return string |
| 173 | + */ |
| 174 | + protected function typeSubdirectory() { |
| 175 | + return $this->mType . '/'; |
154 | 176 | } |
155 | 177 | |
156 | 178 | /** |
157 | | - * Return relative multi-level hash subdirectory with the trailing |
158 | | - * slash or the empty string if $wgFileCacheDepth is off |
| 179 | + * Return relative multi-level hash subdirectory (with trailing slash) |
| 180 | + * or the empty string if not $wgFileCacheDepth |
159 | 181 | * @return string |
160 | 182 | */ |
161 | 183 | protected function hashSubdirectory() { |
— | — | @@ -170,4 +192,55 @@ |
171 | 193 | |
172 | 194 | return $subdir; |
173 | 195 | } |
| 196 | + |
| 197 | + /** |
| 198 | + * Roughly increments the cache misses in the last hour by unique visitors |
| 199 | + * @param $request WebRequest |
| 200 | + * @return void |
| 201 | + */ |
| 202 | + public function incrMissesRecent( WebRequest $request ) { |
| 203 | + global $wgMemc; |
| 204 | + if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) { |
| 205 | + # Get an large IP range that should include the user |
| 206 | + # even if that person's IP address changes... |
| 207 | + $ip = $request->getIP(); |
| 208 | + if ( !IP::isValid( $ip ) ) { |
| 209 | + return; |
| 210 | + } |
| 211 | + $ip = IP::isIPv6( $ip ) |
| 212 | + ? IP::sanitizeRange( "$ip/64" ) |
| 213 | + : IP::sanitizeRange( "$ip/16" ); |
| 214 | + |
| 215 | + # Bail out if a request already came from this range... |
| 216 | + $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip ); |
| 217 | + if ( $wgMemc->get( $key ) ) { |
| 218 | + return; // possibly the same user |
| 219 | + } |
| 220 | + $wgMemc->set( $key, 1, 3600 ); |
| 221 | + |
| 222 | + # Increment the number of cache misses... |
| 223 | + $key = $this->cacheMissKey(); |
| 224 | + if ( $wgMemc->get( $key ) === false ) { |
| 225 | + $wgMemc->set( $key, 1, 3600 ); |
| 226 | + } else { |
| 227 | + $wgMemc->incr( $key ); |
| 228 | + } |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * Roughly gets the cache misses in the last hour by unique visitors |
| 234 | + * @return int |
| 235 | + */ |
| 236 | + public function getMissesRecent() { |
| 237 | + global $wgMemc; |
| 238 | + return self::MISS_FACTOR * $wgMemc->get( $this->cacheMissKey() ); |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * @return string |
| 243 | + */ |
| 244 | + protected function cacheMissKey() { |
| 245 | + return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey ); |
| 246 | + } |
174 | 247 | } |
Index: trunk/phase3/includes/cache/HTMLFileCache.php |
— | — | @@ -35,6 +35,7 @@ |
36 | 36 | |
37 | 37 | /** |
38 | 38 | * Get the base file cache directory |
| 39 | + * Note: avoids baseCacheDirectory() for b/c to not skip existing cache |
39 | 40 | * @return string |
40 | 41 | */ |
41 | 42 | protected function cacheDirectory() { |
— | — | @@ -50,6 +51,18 @@ |
51 | 52 | } |
52 | 53 | |
53 | 54 | /** |
| 55 | + * Get the cache type subdirectory (with the trailing slash) or the empty string |
| 56 | + * @return string |
| 57 | + */ |
| 58 | + protected function typeSubdirectory() { |
| 59 | + if ( $this->mType === 'view' ) { |
| 60 | + return ''; // b/c to not skip existing cache |
| 61 | + } else { |
| 62 | + return $this->mType . '/'; |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + /** |
54 | 67 | * Check if pages can be cached for this request/user |
55 | 68 | * @param $context IContextSource |
56 | 69 | * @return bool |
— | — | @@ -71,9 +84,8 @@ |
72 | 85 | // Below are header setting params |
73 | 86 | } elseif ( $query == 'maxage' || $query == 'smaxage' ) { |
74 | 87 | continue; |
75 | | - } else { |
76 | | - return false; |
77 | 88 | } |
| 89 | + return false; |
78 | 90 | } |
79 | 91 | $user = $context->getUser(); |
80 | 92 | // Check for non-standard user language; this covers uselang, |