MediaWiki
REL1_20
|
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 $dbr = wfGetDB( DB_SLAVE ); 00052 self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ ); 00053 } 00054 00055 self::$loaded = true; 00056 } 00057 00061 static function loadAndLazyInit() { 00062 wfDebug( __METHOD__ . ": reading site_stats from slave\n" ); 00063 $row = self::doLoad( wfGetDB( DB_SLAVE ) ); 00064 00065 if( !self::isSane( $row ) ) { 00066 // Might have just been initialized during this request? Underflow? 00067 wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" ); 00068 $row = self::doLoad( wfGetDB( DB_MASTER ) ); 00069 } 00070 00071 if( !self::isSane( $row ) ) { 00072 // Normally the site_stats table is initialized at install time. 00073 // Some manual construction scenarios may leave the table empty or 00074 // broken, however, for instance when importing from a dump into a 00075 // clean schema with mwdumper. 00076 wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" ); 00077 00078 SiteStatsInit::doAllAndCommit( wfGetDB( DB_SLAVE ) ); 00079 00080 $row = self::doLoad( wfGetDB( DB_MASTER ) ); 00081 } 00082 00083 if( !self::isSane( $row ) ) { 00084 wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" ); 00085 } 00086 return $row; 00087 } 00088 00093 static function doLoad( $db ) { 00094 return $db->selectRow( 'site_stats', '*', false, __METHOD__ ); 00095 } 00096 00100 static function views() { 00101 self::load(); 00102 return self::$row->ss_total_views; 00103 } 00104 00108 static function edits() { 00109 self::load(); 00110 return self::$row->ss_total_edits; 00111 } 00112 00116 static function articles() { 00117 self::load(); 00118 return self::$row->ss_good_articles; 00119 } 00120 00124 static function pages() { 00125 self::load(); 00126 return self::$row->ss_total_pages; 00127 } 00128 00132 static function users() { 00133 self::load(); 00134 return self::$row->ss_users; 00135 } 00136 00140 static function activeUsers() { 00141 self::load(); 00142 return self::$row->ss_active_users; 00143 } 00144 00148 static function images() { 00149 self::load(); 00150 return self::$row->ss_images; 00151 } 00152 00158 static function numberingroup( $group ) { 00159 if ( !isset( self::$groupMemberCounts[$group] ) ) { 00160 global $wgMemc; 00161 $key = wfMemcKey( 'SiteStats', 'groupcounts', $group ); 00162 $hit = $wgMemc->get( $key ); 00163 if ( !$hit ) { 00164 $dbr = wfGetDB( DB_SLAVE ); 00165 $hit = $dbr->selectField( 00166 'user_groups', 00167 'COUNT(*)', 00168 array( 'ug_group' => $group ), 00169 __METHOD__ 00170 ); 00171 $wgMemc->set( $key, $hit, 3600 ); 00172 } 00173 self::$groupMemberCounts[$group] = $hit; 00174 } 00175 return self::$groupMemberCounts[$group]; 00176 } 00177 00181 static function jobs() { 00182 if ( !isset( self::$jobs ) ) { 00183 $dbr = wfGetDB( DB_SLAVE ); 00184 self::$jobs = $dbr->estimateRowCount( 'job' ); 00185 /* Zero rows still do single row read for row that doesn't exist, but people are annoyed by that */ 00186 if ( self::$jobs == 1 ) { 00187 self::$jobs = 0; 00188 } 00189 } 00190 return self::$jobs; 00191 } 00192 00198 static function pagesInNs( $ns ) { 00199 wfProfileIn( __METHOD__ ); 00200 if( !isset( self::$pageCount[$ns] ) ) { 00201 $dbr = wfGetDB( DB_SLAVE ); 00202 self::$pageCount[$ns] = (int)$dbr->selectField( 00203 'page', 00204 'COUNT(*)', 00205 array( 'page_namespace' => $ns ), 00206 __METHOD__ 00207 ); 00208 } 00209 wfProfileOut( __METHOD__ ); 00210 return self::$pageCount[$ns]; 00211 } 00212 00220 private static function isSane( $row ) { 00221 if( 00222 $row === false 00223 || $row->ss_total_pages < $row->ss_good_articles 00224 || $row->ss_total_edits < $row->ss_total_pages 00225 ) { 00226 return false; 00227 } 00228 // Now check for underflow/overflow 00229 foreach( array( 'total_views', 'total_edits', 'good_articles', 00230 'total_pages', 'users', 'images' ) as $member ) { 00231 if( 00232 $row->{"ss_$member"} > 2000000000 00233 || $row->{"ss_$member"} < 0 00234 ) { 00235 return false; 00236 } 00237 } 00238 return true; 00239 } 00240 } 00241 00245 class SiteStatsUpdate implements DeferrableUpdate { 00246 protected $views = 0; 00247 protected $edits = 0; 00248 protected $pages = 0; 00249 protected $articles = 0; 00250 protected $users = 0; 00251 protected $images = 0; 00252 00253 // @TODO: deprecate this constructor 00254 function __construct( $views, $edits, $good, $pages = 0, $users = 0 ) { 00255 $this->views = $views; 00256 $this->edits = $edits; 00257 $this->articles = $good; 00258 $this->pages = $pages; 00259 $this->users = $users; 00260 } 00261 00266 public static function factory( array $deltas ) { 00267 $update = new self( 0, 0, 0 ); 00268 00269 $fields = array( 'views', 'edits', 'pages', 'articles', 'users', 'images' ); 00270 foreach ( $fields as $field ) { 00271 if ( isset( $deltas[$field] ) && $deltas[$field] ) { 00272 $update->$field = $deltas[$field]; 00273 } 00274 } 00275 00276 return $update; 00277 } 00278 00279 public function doUpdate() { 00280 global $wgSiteStatsAsyncFactor; 00281 00282 $rate = $wgSiteStatsAsyncFactor; // convenience 00283 // If set to do so, only do actual DB updates 1 every $rate times. 00284 // The other times, just update "pending delta" values in memcached. 00285 if ( $rate && ( $rate < 0 || mt_rand( 0, $rate - 1 ) != 0 ) ) { 00286 $this->doUpdatePendingDeltas(); 00287 } else { 00288 $dbw = wfGetDB( DB_MASTER ); 00289 00290 $lockKey = wfMemcKey( 'site_stats' ); // prepend wiki ID 00291 if ( $rate ) { 00292 // Lock the table so we don't have double DB/memcached updates 00293 if ( !$dbw->lockIsFree( $lockKey, __METHOD__ ) 00294 || !$dbw->lock( $lockKey, __METHOD__, 1 ) // 1 sec timeout 00295 ) { 00296 $this->doUpdatePendingDeltas(); 00297 return; 00298 } 00299 $pd = $this->getPendingDeltas(); 00300 // Piggy-back the async deltas onto those of this stats update.... 00301 $this->views += ( $pd['ss_total_views']['+'] - $pd['ss_total_views']['-'] ); 00302 $this->edits += ( $pd['ss_total_edits']['+'] - $pd['ss_total_edits']['-'] ); 00303 $this->articles += ( $pd['ss_good_articles']['+'] - $pd['ss_good_articles']['-'] ); 00304 $this->pages += ( $pd['ss_total_pages']['+'] - $pd['ss_total_pages']['-'] ); 00305 $this->users += ( $pd['ss_users']['+'] - $pd['ss_users']['-'] ); 00306 $this->images += ( $pd['ss_images']['+'] - $pd['ss_images']['-'] ); 00307 } 00308 00309 // Need a separate transaction because this a global lock 00310 $dbw->begin( __METHOD__ ); 00311 00312 // Build up an SQL query of deltas and apply them... 00313 $updates = ''; 00314 $this->appendUpdate( $updates, 'ss_total_views', $this->views ); 00315 $this->appendUpdate( $updates, 'ss_total_edits', $this->edits ); 00316 $this->appendUpdate( $updates, 'ss_good_articles', $this->articles ); 00317 $this->appendUpdate( $updates, 'ss_total_pages', $this->pages ); 00318 $this->appendUpdate( $updates, 'ss_users', $this->users ); 00319 $this->appendUpdate( $updates, 'ss_images', $this->images ); 00320 if ( $updates != '' ) { 00321 $dbw->update( 'site_stats', array( $updates ), array(), __METHOD__ ); 00322 } 00323 00324 if ( $rate ) { 00325 // Decrement the async deltas now that we applied them 00326 $this->removePendingDeltas( $pd ); 00327 // Commit the updates and unlock the table 00328 $dbw->unlock( $lockKey, __METHOD__ ); 00329 } 00330 00331 $dbw->commit( __METHOD__ ); 00332 } 00333 } 00334 00339 public static function cacheUpdate( $dbw ) { 00340 global $wgActiveUserDays; 00341 $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow' ) ); 00342 # Get non-bot users than did some recent action other than making accounts. 00343 # If account creation is included, the number gets inflated ~20+ fold on enwiki. 00344 $activeUsers = $dbr->selectField( 00345 'recentchanges', 00346 'COUNT( DISTINCT rc_user_text )', 00347 array( 00348 'rc_user != 0', 00349 'rc_bot' => 0, 00350 'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL', 00351 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ) ), 00352 ), 00353 __METHOD__ 00354 ); 00355 $dbw->update( 00356 'site_stats', 00357 array( 'ss_active_users' => intval( $activeUsers ) ), 00358 array( 'ss_row_id' => 1 ), 00359 __METHOD__ 00360 ); 00361 return $activeUsers; 00362 } 00363 00364 protected function doUpdatePendingDeltas() { 00365 $this->adjustPending( 'ss_total_views', $this->views ); 00366 $this->adjustPending( 'ss_total_edits', $this->edits ); 00367 $this->adjustPending( 'ss_good_articles', $this->articles ); 00368 $this->adjustPending( 'ss_total_pages', $this->pages ); 00369 $this->adjustPending( 'ss_users', $this->users ); 00370 $this->adjustPending( 'ss_images', $this->images ); 00371 } 00372 00378 protected function appendUpdate( &$sql, $field, $delta ) { 00379 if ( $delta ) { 00380 if ( $sql ) { 00381 $sql .= ','; 00382 } 00383 if ( $delta < 0 ) { 00384 $sql .= "$field=$field-" . abs( $delta ); 00385 } else { 00386 $sql .= "$field=$field+" . abs( $delta ); 00387 } 00388 } 00389 } 00390 00396 private function getTypeCacheKey( $type, $sign ) { 00397 return wfMemcKey( 'sitestatsupdate', 'pendingdelta', $type, $sign ); 00398 } 00399 00407 protected function adjustPending( $type, $delta ) { 00408 global $wgMemc; 00409 00410 if ( $delta < 0 ) { // decrement 00411 $key = $this->getTypeCacheKey( $type, '-' ); 00412 } else { // increment 00413 $key = $this->getTypeCacheKey( $type, '+' ); 00414 } 00415 00416 $magnitude = abs( $delta ); 00417 if ( !$wgMemc->incr( $key, $magnitude ) ) { // not there? 00418 if ( !$wgMemc->add( $key, $magnitude ) ) { // race? 00419 $wgMemc->incr( $key, $magnitude ); 00420 } 00421 } 00422 } 00423 00429 protected function getPendingDeltas() { 00430 global $wgMemc; 00431 00432 $pending = array(); 00433 foreach ( array( 'ss_total_views', 'ss_total_edits', 00434 'ss_good_articles', 'ss_total_pages', 'ss_users', 'ss_images' ) as $type ) 00435 { 00436 // Get pending increments and pending decrements 00437 $pending[$type]['+'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '+' ) ); 00438 $pending[$type]['-'] = (int)$wgMemc->get( $this->getTypeCacheKey( $type, '-' ) ); 00439 } 00440 00441 return $pending; 00442 } 00443 00449 protected function removePendingDeltas( array $pd ) { 00450 global $wgMemc; 00451 00452 foreach ( $pd as $type => $deltas ) { 00453 foreach ( $deltas as $sign => $magnitude ) { 00454 // Lower the pending counter now that we applied these changes 00455 $wgMemc->decr( $this->getTypeCacheKey( $type, $sign ), $magnitude ); 00456 } 00457 } 00458 } 00459 } 00460 00464 class SiteStatsInit { 00465 00466 // Database connection 00467 private $db; 00468 00469 // Various stats 00470 private $mEdits, $mArticles, $mPages, $mUsers, $mViews, $mFiles = 0; 00471 00478 public function __construct( $database = false ) { 00479 if ( $database instanceof DatabaseBase ) { 00480 $this->db = $database; 00481 } else { 00482 $this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE ); 00483 } 00484 } 00485 00490 public function edits() { 00491 $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ ); 00492 $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ ); 00493 return $this->mEdits; 00494 } 00495 00500 public function articles() { 00501 global $wgArticleCountMethod; 00502 00503 $tables = array( 'page' ); 00504 $conds = array( 00505 'page_namespace' => MWNamespace::getContentNamespaces(), 00506 'page_is_redirect' => 0, 00507 ); 00508 00509 if ( $wgArticleCountMethod == 'link' ) { 00510 $tables[] = 'pagelinks'; 00511 $conds[] = 'pl_from=page_id'; 00512 } elseif ( $wgArticleCountMethod == 'comma' ) { 00513 // To make a correct check for this, we would need, for each page, 00514 // to load the text, maybe uncompress it, maybe decode it and then 00515 // check if there's one comma. 00516 // But one thing we are sure is that if the page is empty, it can't 00517 // contain a comma :) 00518 $conds[] = 'page_len > 0'; 00519 } 00520 00521 $this->mArticles = $this->db->selectField( $tables, 'COUNT(DISTINCT page_id)', 00522 $conds, __METHOD__ ); 00523 return $this->mArticles; 00524 } 00525 00530 public function pages() { 00531 $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ ); 00532 return $this->mPages; 00533 } 00534 00539 public function users() { 00540 $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ ); 00541 return $this->mUsers; 00542 } 00543 00548 public function views() { 00549 $this->mViews = $this->db->selectField( 'page', 'SUM(page_counter)', '', __METHOD__ ); 00550 return $this->mViews; 00551 } 00552 00557 public function files() { 00558 $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ ); 00559 return $this->mFiles; 00560 } 00561 00574 public static function doAllAndCommit( $database, array $options = array() ) { 00575 $options += array( 'update' => false, 'views' => true, 'activeUsers' => false ); 00576 00577 // Grab the object and count everything 00578 $counter = new SiteStatsInit( $database ); 00579 00580 $counter->edits(); 00581 $counter->articles(); 00582 $counter->pages(); 00583 $counter->users(); 00584 $counter->files(); 00585 00586 // Only do views if we don't want to not count them 00587 if( $options['views'] ) { 00588 $counter->views(); 00589 } 00590 00591 // Update/refresh 00592 if( $options['update'] ) { 00593 $counter->update(); 00594 } else { 00595 $counter->refresh(); 00596 } 00597 00598 // Count active users if need be 00599 if( $options['activeUsers'] ) { 00600 SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) ); 00601 } 00602 } 00603 00607 public function update() { 00608 list( $values, $conds ) = $this->getDbParams(); 00609 $dbw = wfGetDB( DB_MASTER ); 00610 $dbw->update( 'site_stats', $values, $conds, __METHOD__ ); 00611 } 00612 00617 public function refresh() { 00618 list( $values, $conds, $views ) = $this->getDbParams(); 00619 $dbw = wfGetDB( DB_MASTER ); 00620 $dbw->delete( 'site_stats', $conds, __METHOD__ ); 00621 $dbw->insert( 'site_stats', array_merge( $values, $conds, $views ), __METHOD__ ); 00622 } 00623 00628 private function getDbParams() { 00629 $values = array( 00630 'ss_total_edits' => $this->mEdits, 00631 'ss_good_articles' => $this->mArticles, 00632 'ss_total_pages' => $this->mPages, 00633 'ss_users' => $this->mUsers, 00634 'ss_images' => $this->mFiles 00635 ); 00636 $conds = array( 'ss_row_id' => 1 ); 00637 $views = array( 'ss_total_views' => $this->mViews ); 00638 return array( $values, $conds, $views ); 00639 } 00640 }