MediaWiki
master
|
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 // Need a separate transaction because this a global lock 00298 $dbw->begin( __METHOD__ ); 00299 00300 $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID 00301 if ( $rate ) { 00302 // Lock the table so we don't have double DB/memcached updates 00303 if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) 00304 || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout 00305 ) { 00306 $this->doUpdatePendingDeltas(); 00307 return; 00308 } 00309 $pd = $this->getPendingDeltas(); 00310 // Piggy-back the async deltas onto those of this stats update.... 00311 $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] ); 00312 $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] ); 00313 $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] ); 00314 $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] ); 00315 $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] ); 00316 $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] ); 00317 } 00318 00319 // Build up an SQL query of deltas and apply them... 00320 $updates = ''; 00321 $this->appendUpdate( $updates, 'ss_total_views', $this->views ); 00322 $this->appendUpdate( $updates, 'ss_total_edits', $this->edits ); 00323 $this->appendUpdate( $updates, 'ss_good_articles', $this->articles ); 00324 $this->appendUpdate( $updates, 'ss_total_pages', $this->pages ); 00325 $this->appendUpdate( $updates, 'ss_users', $this->users ); 00326 $this->appendUpdate( $updates, 'ss_images', $this->images ); 00327 if ( $updates != '' ) { 00328 $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ ); 00329 } 00330 00331 if ( $rate ) { 00332 // Decrement the async deltas now that we applied them 00333 $this->removePendingDeltas( $pd ); 00334 // Commit the updates and unlock the table 00335 $dbw->unlock( $lockKey, __METHOD__ ); 00336 } 00337 00338 $dbw->commit( __METHOD__ ); 00339 } 00340 } 00341 00346 public static function cacheUpdate( $dbw ) { 00347 global $wgActiveUserDays; 00348 $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow' ) ); 00349 # Get non-bot users than did some recent action other than making accounts. 00350 # If account creation is included, the number gets inflated ~20+ fold on enwiki. 00351 $activeUsers = $dbr->selectField( 00352 'recentchanges', 00353 'COUNT( DISTINCT rc_user_text )', 00354 array( 00355 'rc_user != 0', 00356 'rc_bot' => 0, 00357 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL', 00358 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ), 00359 ), 00360 __METHOD__ 00361 ); 00362 $dbw->update( 00363 'site_stats', 00364 array( 'ss_active_users' => intval( $activeUsers ) ), 00365 array( 'ss_row_id' => 1 ), 00366 __METHOD__ 00367 ); 00368 return $activeUsers; 00369 } 00370 00371 protected function doUpdatePendingDeltas() { 00372 $this->adjustPending( 'ss_total_views', $this->views ); 00373 $this->adjustPending( 'ss_total_edits', $this->edits ); 00374 $this->adjustPending( 'ss_good_articles', $this->articles ); 00375 $this->adjustPending( 'ss_total_pages', $this->pages ); 00376 $this->adjustPending( 'ss_users', $this->users ); 00377 $this->adjustPending( 'ss_images', $this->images ); 00378 } 00379 00385 protected function appendUpdate( &$sql, $field, $delta ) { 00386 if ( $delta ) { 00387 if ( $sql ) { 00388 $sql .= ','; 00389 } 00390 if ( $delta < 0 ) { 00391 $sql .= "$field=$field-" . abs( $delta ); 00392 } else { 00393 $sql .= "$field=$field+" . abs( $delta ); 00394 } 00395 } 00396 } 00397 00403 private function getTypeCacheKey( $type, $sign ) { 00404 return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign ); 00405 } 00406 00414 protected function adjustPending( $type, $delta ) { 00415 global $wgMemc; 00416 00417 if ( $delta < 0 ) { // decrement 00418 $key = $this->getTypeCacheKey( $type, '-' ); 00419 } else { // increment 00420 $key = $this->getTypeCacheKey( $type, '+' ); 00421 } 00422 00423 $magnitude = abs( $delta ); 00424 if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there? 00425 if ( !$wgMemc->add( $key, $magnitude ) ) { // race? 00426 $wgMemc->incr( $key, $magnitude ); 00427 } 00428 } 00429 } 00430 00436 protected function getPendingDeltas() { 00437 global $wgMemc; 00438 00439 $pending = array(); 00440 foreach ( array( 'ss_total_views', 'ss_total_edits', 00441 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type ) 00442 { 00443 // Get pending increments and pending decrements 00444 $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) ); 00445 $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) ); 00446 } 00447 00448 return $pending; 00449 } 00450 00456 protected function removePendingDeltas( array $pd ) { 00457 global $wgMemc; 00458 00459 foreach ( $pd as $type => $deltas ) { 00460 foreach ( $deltas as $sign => $magnitude ) { 00461 // Lower the pending counter now that we applied these changes 00462 $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude ); 00463 } 00464 } 00465 } 00466 } 00467 00471 class SiteStatsInit { 00472 00473 // Database connection 00474 private $db; 00475 00476 // Various stats 00477 private $mEdits, $mArticles, $mPages, $mUsers, $mViews, $mFiles = 0; 00478 00485 public function __construct( $database = false ) { 00486 if ( $database instanceof DatabaseBase ) { 00487 $this->db = $database; 00488 } else { 00489 $this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE ); 00490 } 00491 } 00492 00497 public function edits() { 00498 $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ ); 00499 $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ ); 00500 return $this->mEdits; 00501 } 00502 00507 public function articles() { 00508 global $wgArticleCountMethod; 00509 00510 $tables = array( 'page' ); 00511 $conds = array( 00512 'page_namespace' => MWNamespace::getContentNamespaces(), 00513 'page_is_redirect' => 0, 00514 ); 00515 00516 if ( $wgArticleCountMethod == 'link' ) { 00517 $tables[] = 'pagelinks'; 00518 $conds[] = 'pl_from=page_id'; 00519 } elseif ( $wgArticleCountMethod == 'comma' ) { 00520 // To make a correct check for this, we would need, for each page, 00521 // to load the text, maybe uncompress it, maybe decode it and then 00522 // check if there's one comma. 00523 // But one thing we are sure is that if the page is empty, it can't 00524 // contain a comma :) 00525 $conds[] = 'page_len > 0'; 00526 } 00527 00528 $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)', 00529 $conds, __METHOD__ ); 00530 return $this->mArticles; 00531 } 00532 00537 public function pages() { 00538 $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ ); 00539 return $this->mPages; 00540 } 00541 00546 public function users() { 00547 $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ ); 00548 return $this->mUsers; 00549 } 00550 00555 public function views() { 00556 $this->mViews = $this->db->selectField( 'page', 'SUM(page_counter)', '', __METHOD__ ); 00557 return $this->mViews; 00558 } 00559 00564 public function files() { 00565 $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ ); 00566 return $this->mFiles; 00567 } 00568 00581 public static function doAllAndCommit( $database, array $options = array() ) { 00582 $options += array( 'update' => false, 'views' => true, 'activeUsers' => false ); 00583 00584 // Grab the object and count everything 00585 $counter = new SiteStatsInit( $database ); 00586 00587 $counter->edits(); 00588 $counter->articles(); 00589 $counter->pages(); 00590 $counter->users(); 00591 $counter->files(); 00592 00593 // Only do views if we don't want to not count them 00594 if( $options['views'] ) { 00595 $counter->views(); 00596 } 00597 00598 // Update/refresh 00599 if( $options['update'] ) { 00600 $counter->update(); 00601 } else { 00602 $counter->refresh(); 00603 } 00604 00605 // Count active users if need be 00606 if( $options['activeUsers'] ) { 00607 SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) ); 00608 } 00609 } 00610 00614 public function update() { 00615 list( $values, $conds ) = $this->getDbParams(); 00616 $dbw = wfGetDB( DB_MASTER ); 00617 $dbw->update( 'site_stats', $values, $conds, __METHOD__ ); 00618 } 00619 00624 public function refresh() { 00625 list( $values, $conds, $views ) = $this->getDbParams(); 00626 $dbw = wfGetDB( DB_MASTER ); 00627 $dbw->delete( 'site_stats', $conds, __METHOD__ ); 00628 $dbw->insert( 'site_stats', array_merge( $values, $conds, $views ), __METHOD__ ); 00629 } 00630 00635 private function getDbParams() { 00636 $values = array( 00637 'ss_total_edits' => $this->mEdits, 00638 'ss_good_articles' => $this->mArticles, 00639 'ss_total_pages' => $this->mPages, 00640 'ss_users' => $this->mUsers, 00641 'ss_images' => $this->mFiles 00642 ); 00643 $conds = array( 'ss_row_id' => 1 ); 00644 $views = array( 'ss_total_views' => $this->mViews ); 00645 return array( $values, $conds, $views ); 00646 } 00647 }