MediaWiki
REL1_22
|
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 = array_sum( JobQueueGroup::singleton()->getQueueSizes() ); 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 ( $row === false 00230 || $row->ss_total_pages < $row->ss_good_articles 00231 || $row->ss_total_edits < $row->ss_total_pages 00232 || $row->ss_users < $row->ss_active_users 00233 ) { 00234 return false; 00235 } 00236 // Now check for underflow/overflow 00237 foreach ( array( 00238 'ss_total_views', 00239 'ss_total_edits', 00240 'ss_good_articles', 00241 'ss_total_pages', 00242 'ss_users', 00243 'ss_active_users', 00244 'ss_images', 00245 ) as $member ) { 00246 if ( $row->$member > 2000000000 || $row->$member < 0 ) { 00247 return false; 00248 } 00249 } 00250 return true; 00251 } 00252 } 00253 00257 class SiteStatsUpdate implements DeferrableUpdate { 00258 protected $views = 0; 00259 protected $edits = 0; 00260 protected $pages = 0; 00261 protected $articles = 0; 00262 protected $users = 0; 00263 protected $images = 0; 00264 00265 // @todo deprecate this constructor 00266 function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) { 00267 $this->views = $views; 00268 $this->edits = $edits; 00269 $this->articles = $good; 00270 $this->pages = $pages; 00271 $this->users = $users; 00272 } 00273 00278 public static function factory( array $deltas ) { 00279 $update = new self( 0, 0, 0 ); 00280 00281 $fields = array( 'views', 'edits', 'pages', 'articles', 'users', 'images' ); 00282 foreach ( $fields as $field ) { 00283 if ( isset( $deltas[$field] ) && $deltas[$field] ) { 00284 $update->$field = $deltas[$field]; 00285 } 00286 } 00287 00288 return $update; 00289 } 00290 00291 public function doUpdate() { 00292 global $wgSiteStatsAsyncFactor; 00293 00294 $rate = $wgSiteStatsAsyncFactor; // convenience 00295 // If set to do so, only do actual DB updates 1 every $rate times. 00296 // The other times, just update "pending delta" values in memcached. 00297 if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) { 00298 $this->doUpdatePendingDeltas(); 00299 } else { 00300 // Need a separate transaction because this a global lock 00301 wfGetDB( DB_MASTER )->onTransactionIdle( array( $this, 'tryDBUpdateInternal' ) ); 00302 } 00303 } 00304 00310 public function tryDBUpdateInternal() { 00311 global $wgSiteStatsAsyncFactor; 00312 00313 $dbw = wfGetDB( DB_MASTER ); 00314 $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID 00315 if ( $wgSiteStatsAsyncFactor ) { 00316 // Lock the table so we don't have double DB/memcached updates 00317 if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) 00318 || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout 00319 ) { 00320 $this->doUpdatePendingDeltas(); 00321 return; 00322 } 00323 $pd = $this->getPendingDeltas(); 00324 // Piggy-back the async deltas onto those of this stats update.... 00325 $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] ); 00326 $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] ); 00327 $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] ); 00328 $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] ); 00329 $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] ); 00330 $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] ); 00331 } 00332 00333 // Build up an SQL query of deltas and apply them... 00334 $updates = ''; 00335 $this->appendUpdate( $updates, 'ss_total_views', $this->views ); 00336 $this->appendUpdate( $updates, 'ss_total_edits', $this->edits ); 00337 $this->appendUpdate( $updates, 'ss_good_articles', $this->articles ); 00338 $this->appendUpdate( $updates, 'ss_total_pages', $this->pages ); 00339 $this->appendUpdate( $updates, 'ss_users', $this->users ); 00340 $this->appendUpdate( $updates, 'ss_images', $this->images ); 00341 if ( $updates != '' ) { 00342 $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ ); 00343 } 00344 00345 if ( $wgSiteStatsAsyncFactor ) { 00346 // Decrement the async deltas now that we applied them 00347 $this->removePendingDeltas( $pd ); 00348 // Commit the updates and unlock the table 00349 $dbw->unlock( $lockKey, __METHOD__ ); 00350 } 00351 } 00352 00357 public static function cacheUpdate( $dbw ) { 00358 global $wgActiveUserDays; 00359 $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow' ) ); 00360 # Get non-bot users than did some recent action other than making accounts. 00361 # If account creation is included, the number gets inflated ~20+ fold on enwiki. 00362 $activeUsers = $dbr->selectField( 00363 'recentchanges', 00364 'COUNT( DISTINCT rc_user_text )', 00365 array( 00366 'rc_user != 0', 00367 'rc_bot' => 0, 00368 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL', 00369 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays * 24 * 3600 ) ), 00370 ), 00371 __METHOD__ 00372 ); 00373 $dbw->update( 00374 'site_stats', 00375 array( 'ss_active_users' => intval( $activeUsers ) ), 00376 array( 'ss_row_id' => 1 ), 00377 __METHOD__ 00378 ); 00379 return $activeUsers; 00380 } 00381 00382 protected function doUpdatePendingDeltas() { 00383 $this->adjustPending( 'ss_total_views', $this->views ); 00384 $this->adjustPending( 'ss_total_edits', $this->edits ); 00385 $this->adjustPending( 'ss_good_articles', $this->articles ); 00386 $this->adjustPending( 'ss_total_pages', $this->pages ); 00387 $this->adjustPending( 'ss_users', $this->users ); 00388 $this->adjustPending( 'ss_images', $this->images ); 00389 } 00390 00396 protected function appendUpdate( &$sql, $field, $delta ) { 00397 if ( $delta ) { 00398 if ( $sql ) { 00399 $sql .= ','; 00400 } 00401 if ( $delta < 0 ) { 00402 $sql .= "$field=$field-" . abs( $delta ); 00403 } else { 00404 $sql .= "$field=$field+" . abs( $delta ); 00405 } 00406 } 00407 } 00408 00414 private function getTypeCacheKey( $type, $sign ) { 00415 return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign ); 00416 } 00417 00425 protected function adjustPending( $type, $delta ) { 00426 global $wgMemc; 00427 00428 if ( $delta < 0 ) { // decrement 00429 $key = $this->getTypeCacheKey( $type, '-' ); 00430 } else { // increment 00431 $key = $this->getTypeCacheKey( $type, '+' ); 00432 } 00433 00434 $magnitude = abs( $delta ); 00435 if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there? 00436 if ( !$wgMemc->add( $key, $magnitude ) ) { // race? 00437 $wgMemc->incr( $key, $magnitude ); 00438 } 00439 } 00440 } 00441 00447 protected function getPendingDeltas() { 00448 global $wgMemc; 00449 00450 $pending = array(); 00451 foreach ( array( 'ss_total_views', 'ss_total_edits', 00452 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type ) 00453 { 00454 // Get pending increments and pending decrements 00455 $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) ); 00456 $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) ); 00457 } 00458 00459 return $pending; 00460 } 00461 00467 protected function removePendingDeltas( array $pd ) { 00468 global $wgMemc; 00469 00470 foreach ( $pd as $type => $deltas ) { 00471 foreach ( $deltas as $sign => $magnitude ) { 00472 // Lower the pending counter now that we applied these changes 00473 $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude ); 00474 } 00475 } 00476 } 00477 } 00478 00482 class SiteStatsInit { 00483 00484 // Database connection 00485 private $db; 00486 00487 // Various stats 00488 private $mEdits, $mArticles, $mPages, $mUsers, $mViews, $mFiles = 0; 00489 00496 public function __construct( $database = false ) { 00497 if ( $database instanceof DatabaseBase ) { 00498 $this->db = $database; 00499 } else { 00500 $this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE ); 00501 } 00502 } 00503 00508 public function edits() { 00509 $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ ); 00510 $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ ); 00511 return $this->mEdits; 00512 } 00513 00518 public function articles() { 00519 global $wgArticleCountMethod; 00520 00521 $tables = array( 'page' ); 00522 $conds = array( 00523 'page_namespace' => MWNamespace::getContentNamespaces(), 00524 'page_is_redirect' => 0, 00525 ); 00526 00527 if ( $wgArticleCountMethod == 'link' ) { 00528 $tables[] = 'pagelinks'; 00529 $conds[] = 'pl_from=page_id'; 00530 } elseif ( $wgArticleCountMethod == 'comma' ) { 00531 // To make a correct check for this, we would need, for each page, 00532 // to load the text, maybe uncompress it, maybe decode it and then 00533 // check if there's one comma. 00534 // But one thing we are sure is that if the page is empty, it can't 00535 // contain a comma :) 00536 $conds[] = 'page_len > 0'; 00537 } 00538 00539 $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)', 00540 $conds, __METHOD__ ); 00541 return $this->mArticles; 00542 } 00543 00548 public function pages() { 00549 $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ ); 00550 return $this->mPages; 00551 } 00552 00557 public function users() { 00558 $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ ); 00559 return $this->mUsers; 00560 } 00561 00566 public function views() { 00567 $this->mViews = $this->db->selectField( 'page', 'SUM(page_counter)', '', __METHOD__ ); 00568 return $this->mViews; 00569 } 00570 00575 public function files() { 00576 $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ ); 00577 return $this->mFiles; 00578 } 00579 00592 public static function doAllAndCommit( $database, array $options = array() ) { 00593 $options += array( 'update' => false, 'views' => true, 'activeUsers' => false ); 00594 00595 // Grab the object and count everything 00596 $counter = new SiteStatsInit( $database ); 00597 00598 $counter->edits(); 00599 $counter->articles(); 00600 $counter->pages(); 00601 $counter->users(); 00602 $counter->files(); 00603 00604 // Only do views if we don't want to not count them 00605 if ( $options['views'] ) { 00606 $counter->views(); 00607 } 00608 00609 // Update/refresh 00610 if ( $options['update'] ) { 00611 $counter->update(); 00612 } else { 00613 $counter->refresh(); 00614 } 00615 00616 // Count active users if need be 00617 if ( $options['activeUsers'] ) { 00618 SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) ); 00619 } 00620 } 00621 00625 public function update() { 00626 list( $values, $conds ) = $this->getDbParams(); 00627 $dbw = wfGetDB( DB_MASTER ); 00628 $dbw->update( 'site_stats', $values, $conds, __METHOD__ ); 00629 } 00630 00635 public function refresh() { 00636 list( $values, $conds, $views ) = $this->getDbParams(); 00637 $dbw = wfGetDB( DB_MASTER ); 00638 $dbw->delete( 'site_stats', $conds, __METHOD__ ); 00639 $dbw->insert( 'site_stats', array_merge( $values, $conds, $views ), __METHOD__ ); 00640 } 00641 00646 private function getDbParams() { 00647 $values = array( 00648 'ss_total_edits' => $this->mEdits, 00649 'ss_good_articles' => $this->mArticles, 00650 'ss_total_pages' => $this->mPages, 00651 'ss_users' => $this->mUsers, 00652 'ss_images' => $this->mFiles 00653 ); 00654 $conds = array( 'ss_row_id' => 1 ); 00655 $views = array( 'ss_total_views' => $this->mViews ); 00656 return array( $values, $conds, $views ); 00657 } 00658 }