MediaWiki
REL1_21
|
00001 <?php 00026 class SiteStats { 00027 static $row, $loaded = false; 00028 static $jobs; 00029 static $pageCount = array(); 00030 static $groupMemberCounts = array(); 00031 00032 static function recache() { 00033 self::load( true ); 00034 } 00035 00039 static function load( $recache = false ) { 00040 if ( self::$loaded && !$recache ) { 00041 return; 00042 } 00043 00044 self::$row = self::loadAndLazyInit(); 00045 00046 # This code is somewhat schema-agnostic, because I'm changing it in a minor release -- TS 00047 if ( !isset( self::$row->ss_total_pages ) && self::$row->ss_total_pages == -1 ) { 00048 # Update schema 00049 $u = new SiteStatsUpdate( 0, 0, 0 ); 00050 $u->doUpdate(); 00051 self::$row = self::doLoad( wfGetDB( DB_SLAVE ) ); 00052 } 00053 00054 self::$loaded = true; 00055 } 00056 00060 static function loadAndLazyInit() { 00061 wfDebug( __METHOD__ . ": reading site_stats from slave\n" ); 00062 $row = self::doLoad( wfGetDB( DB_SLAVE ) ); 00063 00064 if( !self::isSane( $row ) ) { 00065 // Might have just been initialized during this request? Underflow? 00066 wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" ); 00067 $row = self::doLoad( wfGetDB( DB_MASTER ) ); 00068 } 00069 00070 if( !self::isSane( $row ) ) { 00071 // Normally the site_stats table is initialized at install time. 00072 // Some manual construction scenarios may leave the table empty or 00073 // broken, however, for instance when importing from a dump into a 00074 // clean schema with mwdumper. 00075 wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" ); 00076 00077 SiteStatsInit::doAllAndCommit( wfGetDB( DB_SLAVE ) ); 00078 00079 $row = self::doLoad( wfGetDB( DB_MASTER ) ); 00080 } 00081 00082 if( !self::isSane( $row ) ) { 00083 wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" ); 00084 } 00085 return $row; 00086 } 00087 00092 static function doLoad( $db ) { 00093 return $db->selectRow( 'site_stats', array( 00094 'ss_row_id', 00095 'ss_total_views', 00096 'ss_total_edits', 00097 'ss_good_articles', 00098 'ss_total_pages', 00099 'ss_users', 00100 'ss_active_users', 00101 'ss_images', 00102 ), false, __METHOD__ ); 00103 } 00104 00108 static function views() { 00109 self::load(); 00110 return self::$row->ss_total_views; 00111 } 00112 00116 static function edits() { 00117 self::load(); 00118 return self::$row->ss_total_edits; 00119 } 00120 00124 static function articles() { 00125 self::load(); 00126 return self::$row->ss_good_articles; 00127 } 00128 00132 static function pages() { 00133 self::load(); 00134 return self::$row->ss_total_pages; 00135 } 00136 00140 static function users() { 00141 self::load(); 00142 return self::$row->ss_users; 00143 } 00144 00148 static function activeUsers() { 00149 self::load(); 00150 return self::$row->ss_active_users; 00151 } 00152 00156 static function images() { 00157 self::load(); 00158 return self::$row->ss_images; 00159 } 00160 00166 static function numberingroup( $group ) { 00167 if ( !isset( self::$groupMemberCounts[$group] ) ) { 00168 global $wgMemc; 00169 $key = wfMemcKey( 'SiteStats', 'groupcounts', $group ); 00170 $hit = $wgMemc->get( $key ); 00171 if ( !$hit ) { 00172 $dbr = wfGetDB( DB_SLAVE ); 00173 $hit = $dbr->selectField( 00174 'user_groups', 00175 'COUNT(*)', 00176 array( 'ug_group' => $group ), 00177 __METHOD__ 00178 ); 00179 $wgMemc->set( $key, $hit, 3600 ); 00180 } 00181 self::$groupMemberCounts[$group] = $hit; 00182 } 00183 return self::$groupMemberCounts[$group]; 00184 } 00185 00189 static function jobs() { 00190 if ( !isset( self::$jobs ) ) { 00191 $dbr = wfGetDB( DB_SLAVE ); 00192 self::$jobs = $dbr->estimateRowCount( 'job' ); 00193 /* Zero rows still do single row read for row that doesn't exist, but people are annoyed by that */ 00194 if ( self::$jobs == 1 ) { 00195 self::$jobs = 0; 00196 } 00197 } 00198 return self::$jobs; 00199 } 00200 00206 static function pagesInNs( $ns ) { 00207 wfProfileIn( __METHOD__ ); 00208 if( !isset( self::$pageCount[$ns] ) ) { 00209 $dbr = wfGetDB( DB_SLAVE ); 00210 self::$pageCount[$ns] = (int)$dbr->selectField( 00211 'page', 00212 'COUNT(*)', 00213 array( 'page_namespace' => $ns ), 00214 __METHOD__ 00215 ); 00216 } 00217 wfProfileOut( __METHOD__ ); 00218 return self::$pageCount[$ns]; 00219 } 00220 00228 private static function isSane( $row ) { 00229 if( 00230 $row === false 00231 || $row->ss_total_pages < $row->ss_good_articles 00232 || $row->ss_total_edits < $row->ss_total_pages 00233 ) { 00234 return false; 00235 } 00236 // Now check for underflow/overflow 00237 foreach( array( 'total_views', 'total_edits', 'good_articles', 00238 'total_pages', 'users', 'images' ) as $member ) { 00239 if( 00240 $row->{"ss_$member"} > 2000000000 00241 || $row->{"ss_$member"} < 0 00242 ) { 00243 return false; 00244 } 00245 } 00246 return true; 00247 } 00248 } 00249 00253 class SiteStatsUpdate implements DeferrableUpdate { 00254 protected $views = 0; 00255 protected $edits = 0; 00256 protected $pages = 0; 00257 protected $articles = 0; 00258 protected $users = 0; 00259 protected $images = 0; 00260 00261 // @TODO: deprecate this constructor 00262 function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) { 00263 $this->views = $views; 00264 $this->edits = $edits; 00265 $this->articles = $good; 00266 $this->pages = $pages; 00267 $this->users = $users; 00268 } 00269 00274 public static function factory( array $deltas ) { 00275 $update = new self( 0, 0, 0 ); 00276 00277 $fields = array( 'views', 'edits', 'pages', 'articles', 'users', 'images' ); 00278 foreach ( $fields as $field ) { 00279 if ( isset( $deltas[$field] ) && $deltas[$field] ) { 00280 $update->$field = $deltas[$field]; 00281 } 00282 } 00283 00284 return $update; 00285 } 00286 00287 public function doUpdate() { 00288 global $wgSiteStatsAsyncFactor; 00289 00290 $rate = $wgSiteStatsAsyncFactor; // convenience 00291 // If set to do so, only do actual DB updates 1 every $rate times. 00292 // The other times, just update "pending delta" values in memcached. 00293 if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) { 00294 $this->doUpdatePendingDeltas(); 00295 } else { 00296 $dbw = wfGetDB( DB_MASTER ); 00297 00298 $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID 00299 if ( $rate ) { 00300 // Lock the table so we don't have double DB/memcached updates 00301 if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) 00302 || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout 00303 ) { 00304 $this->doUpdatePendingDeltas(); 00305 return; 00306 } 00307 $pd = $this->getPendingDeltas(); 00308 // Piggy-back the async deltas onto those of this stats update.... 00309 $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] ); 00310 $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] ); 00311 $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] ); 00312 $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] ); 00313 $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] ); 00314 $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] ); 00315 } 00316 00317 // Need a separate transaction because this a global lock 00318 $dbw->begin( __METHOD__ ); 00319 00320 // Build up an SQL query of deltas and apply them... 00321 $updates = ''; 00322 $this->appendUpdate( $updates, 'ss_total_views', $this->views ); 00323 $this->appendUpdate( $updates, 'ss_total_edits', $this->edits ); 00324 $this->appendUpdate( $updates, 'ss_good_articles', $this->articles ); 00325 $this->appendUpdate( $updates, 'ss_total_pages', $this->pages ); 00326 $this->appendUpdate( $updates, 'ss_users', $this->users ); 00327 $this->appendUpdate( $updates, 'ss_images', $this->images ); 00328 if ( $updates != '' ) { 00329 $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ ); 00330 } 00331 00332 if ( $rate ) { 00333 // Decrement the async deltas now that we applied them 00334 $this->removePendingDeltas( $pd ); 00335 // Commit the updates and unlock the table 00336 $dbw->unlock( $lockKey, __METHOD__ ); 00337 } 00338 00339 $dbw->commit( __METHOD__ ); 00340 } 00341 } 00342 00347 public static function cacheUpdate( $dbw ) { 00348 global $wgActiveUserDays; 00349 $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow' ) ); 00350 # Get non-bot users than did some recent action other than making accounts. 00351 # If account creation is included, the number gets inflated ~20+ fold on enwiki. 00352 $activeUsers = $dbr->selectField( 00353 'recentchanges', 00354 'COUNT( DISTINCT rc_user_text )', 00355 array( 00356 'rc_user != 0', 00357 'rc_bot' => 0, 00358 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL', 00359 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ), 00360 ), 00361 __METHOD__ 00362 ); 00363 $dbw->update( 00364 'site_stats', 00365 array( 'ss_active_users' => intval( $activeUsers ) ), 00366 array( 'ss_row_id' => 1 ), 00367 __METHOD__ 00368 ); 00369 return $activeUsers; 00370 } 00371 00372 protected function doUpdatePendingDeltas() { 00373 $this->adjustPending( 'ss_total_views', $this->views ); 00374 $this->adjustPending( 'ss_total_edits', $this->edits ); 00375 $this->adjustPending( 'ss_good_articles', $this->articles ); 00376 $this->adjustPending( 'ss_total_pages', $this->pages ); 00377 $this->adjustPending( 'ss_users', $this->users ); 00378 $this->adjustPending( 'ss_images', $this->images ); 00379 } 00380 00386 protected function appendUpdate( &$sql, $field, $delta ) { 00387 if ( $delta ) { 00388 if ( $sql ) { 00389 $sql .= ','; 00390 } 00391 if ( $delta < 0 ) { 00392 $sql .= "$field=$field-" . abs( $delta ); 00393 } else { 00394 $sql .= "$field=$field+" . abs( $delta ); 00395 } 00396 } 00397 } 00398 00404 private function getTypeCacheKey( $type, $sign ) { 00405 return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign ); 00406 } 00407 00415 protected function adjustPending( $type, $delta ) { 00416 global $wgMemc; 00417 00418 if ( $delta < 0 ) { // decrement 00419 $key = $this->getTypeCacheKey( $type, '-' ); 00420 } else { // increment 00421 $key = $this->getTypeCacheKey( $type, '+' ); 00422 } 00423 00424 $magnitude = abs( $delta ); 00425 if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there? 00426 if ( !$wgMemc->add( $key, $magnitude ) ) { // race? 00427 $wgMemc->incr( $key, $magnitude ); 00428 } 00429 } 00430 } 00431 00437 protected function getPendingDeltas() { 00438 global $wgMemc; 00439 00440 $pending = array(); 00441 foreach ( array( 'ss_total_views', 'ss_total_edits', 00442 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type ) 00443 { 00444 // Get pending increments and pending decrements 00445 $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) ); 00446 $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) ); 00447 } 00448 00449 return $pending; 00450 } 00451 00457 protected function removePendingDeltas( array $pd ) { 00458 global $wgMemc; 00459 00460 foreach ( $pd as $type => $deltas ) { 00461 foreach ( $deltas as $sign => $magnitude ) { 00462 // Lower the pending counter now that we applied these changes 00463 $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude ); 00464 } 00465 } 00466 } 00467 } 00468 00472 class SiteStatsInit { 00473 00474 // Database connection 00475 private $db; 00476 00477 // Various stats 00478 private $mEdits, $mArticles, $mPages, $mUsers, $mViews, $mFiles = 0; 00479 00486 public function __construct( $database = false ) { 00487 if ( $database instanceof DatabaseBase ) { 00488 $this->db = $database; 00489 } else { 00490 $this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE ); 00491 } 00492 } 00493 00498 public function edits() { 00499 $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ ); 00500 $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ ); 00501 return $this->mEdits; 00502 } 00503 00508 public function articles() { 00509 global $wgArticleCountMethod; 00510 00511 $tables = array( 'page' ); 00512 $conds = array( 00513 'page_namespace' => MWNamespace::getContentNamespaces(), 00514 'page_is_redirect' => 0, 00515 ); 00516 00517 if ( $wgArticleCountMethod == 'link' ) { 00518 $tables[] = 'pagelinks'; 00519 $conds[] = 'pl_from=page_id'; 00520 } elseif ( $wgArticleCountMethod == 'comma' ) { 00521 // To make a correct check for this, we would need, for each page, 00522 // to load the text, maybe uncompress it, maybe decode it and then 00523 // check if there's one comma. 00524 // But one thing we are sure is that if the page is empty, it can't 00525 // contain a comma :) 00526 $conds[] = 'page_len > 0'; 00527 } 00528 00529 $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)', 00530 $conds, __METHOD__ ); 00531 return $this->mArticles; 00532 } 00533 00538 public function pages() { 00539 $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ ); 00540 return $this->mPages; 00541 } 00542 00547 public function users() { 00548 $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ ); 00549 return $this->mUsers; 00550 } 00551 00556 public function views() { 00557 $this->mViews = $this->db->selectField( 'page', 'SUM(page_counter)', '', __METHOD__ ); 00558 return $this->mViews; 00559 } 00560 00565 public function files() { 00566 $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ ); 00567 return $this->mFiles; 00568 } 00569 00582 public static function doAllAndCommit( $database, array $options = array() ) { 00583 $options += array( 'update' => false, 'views' => true, 'activeUsers' => false ); 00584 00585 // Grab the object and count everything 00586 $counter = new SiteStatsInit( $database ); 00587 00588 $counter->edits(); 00589 $counter->articles(); 00590 $counter->pages(); 00591 $counter->users(); 00592 $counter->files(); 00593 00594 // Only do views if we don't want to not count them 00595 if( $options['views'] ) { 00596 $counter->views(); 00597 } 00598 00599 // Update/refresh 00600 if( $options['update'] ) { 00601 $counter->update(); 00602 } else { 00603 $counter->refresh(); 00604 } 00605 00606 // Count active users if need be 00607 if( $options['activeUsers'] ) { 00608 SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) ); 00609 } 00610 } 00611 00615 public function update() { 00616 list( $values, $conds ) = $this->getDbParams(); 00617 $dbw = wfGetDB( DB_MASTER ); 00618 $dbw->update( 'site_stats', $values, $conds, __METHOD__ ); 00619 } 00620 00625 public function refresh() { 00626 list( $values, $conds, $views ) = $this->getDbParams(); 00627 $dbw = wfGetDB( DB_MASTER ); 00628 $dbw->delete( 'site_stats', $conds, __METHOD__ ); 00629 $dbw->insert( 'site_stats', array_merge( $values, $conds, $views ), __METHOD__ ); 00630 } 00631 00636 private function getDbParams() { 00637 $values = array( 00638 'ss_total_edits' => $this->mEdits, 00639 'ss_good_articles' => $this->mArticles, 00640 'ss_total_pages' => $this->mPages, 00641 'ss_users' => $this->mUsers, 00642 'ss_images' => $this->mFiles 00643 ); 00644 $conds = array( 'ss_row_id' => 1 ); 00645 $views = array( 'ss_total_views' => $this->mViews ); 00646 return array( $values, $conds, $views ); 00647 } 00648 }