Merge "phpcs: The final declaration must precede the visibility declaration"
[lhc/web/wiklou.git] / includes / profiler / TransactionProfiler.php
1 <?php
2 /**
3 * Transaction profiling for contention
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Profiler
22 * @author Aaron Schulz
23 */
24
25 /**
26 * Helper class that detects high-contention DB queries via profiling calls
27 *
28 * This class is meant to work with a DatabaseBase object, which manages queries
29 *
30 * @since 1.24
31 */
32 class TransactionProfiler {
33 /** @var float Seconds */
34 protected $dbLockThreshold = 3.0;
35 /** @var float Seconds */
36 protected $eventThreshold = .25;
37
38 /** @var array transaction ID => (write start time, list of DBs involved) */
39 protected $dbTrxHoldingLocks = array();
40 /** @var array transaction ID => list of (query name, start time, end time) */
41 protected $dbTrxMethodTimes = array();
42
43 /**
44 * Mark a DB as in a transaction with one or more writes pending
45 *
46 * Note that there can be multiple connections to a single DB.
47 *
48 * @param string $server DB server
49 * @param string $db DB name
50 * @param string $id ID string of transaction
51 */
52 public function transactionWritingIn( $server, $db, $id ) {
53 $name = "{$server} ({$db}) (TRX#$id)";
54 if ( isset( $this->dbTrxHoldingLocks[$name] ) ) {
55 wfDebugLog( 'DBPerformance', "Nested transaction for '$name' - out of sync." );
56 }
57 $this->dbTrxHoldingLocks[$name] = array(
58 'start' => microtime( true ),
59 'conns' => array(), // all connections involved
60 );
61 $this->dbTrxMethodTimes[$name] = array();
62
63 foreach ( $this->dbTrxHoldingLocks as $name => &$info ) {
64 // Track all DBs in transactions for this transaction
65 $info['conns'][$name] = 1;
66 }
67 }
68
69 /**
70 * Register the name and time of a method for slow DB trx detection
71 *
72 * This assumes that all queries are synchronous (non-overlapping)
73 *
74 * @param string $query Function name
75 * @param float $sTime Starting UNIX wall time
76 * @param bool $isWrite Whether this is a write query
77 */
78 public function recordQueryCompletion( $query, $sTime, $isWrite = false ) {
79 $eTime = microtime( true );
80 $elapsed = ( $eTime - $sTime );
81
82 if ( !$this->dbTrxHoldingLocks ) {
83 // Short-circuit
84 return;
85 } elseif ( !$isWrite && $elapsed < $this->eventThreshold ) {
86 // Not an important query nor slow enough
87 return;
88 }
89
90 foreach ( $this->dbTrxHoldingLocks as $name => $info ) {
91 $lastQuery = end( $this->dbTrxMethodTimes[$name] );
92 if ( $lastQuery ) {
93 // Additional query in the trx...
94 $lastEnd = $lastQuery[2];
95 if ( $sTime >= $lastEnd ) { // sanity check
96 if ( ( $sTime - $lastEnd ) > $this->eventThreshold ) {
97 // Add an entry representing the time spent doing non-queries
98 $this->dbTrxMethodTimes[$name][] = array( '...delay...', $lastEnd, $sTime );
99 }
100 $this->dbTrxMethodTimes[$name][] = array( $query, $sTime, $eTime );
101 }
102 } else {
103 // First query in the trx...
104 if ( $sTime >= $info['start'] ) { // sanity check
105 $this->dbTrxMethodTimes[$name][] = array( $query, $sTime, $eTime );
106 }
107 }
108 }
109 }
110
111 /**
112 * Mark a DB as no longer in a transaction
113 *
114 * This will check if locks are possibly held for longer than
115 * needed and log any affected transactions to a special DB log.
116 * Note that there can be multiple connections to a single DB.
117 *
118 * @param string $server DB server
119 * @param string $db DB name
120 * @param string $id ID string of transaction
121 */
122 public function transactionWritingOut( $server, $db, $id ) {
123 $name = "{$server} ({$db}) (TRX#$id)";
124 if ( !isset( $this->dbTrxMethodTimes[$name] ) ) {
125 wfDebugLog( 'DBPerformance', "Detected no transaction for '$name' - out of sync." );
126 return;
127 }
128 // Fill in the last non-query period...
129 $lastQuery = end( $this->dbTrxMethodTimes[$name] );
130 if ( $lastQuery ) {
131 $now = microtime( true );
132 $lastEnd = $lastQuery[2];
133 if ( ( $now - $lastEnd ) > $this->eventThreshold ) {
134 $this->dbTrxMethodTimes[$name][] = array( '...delay...', $lastEnd, $now );
135 }
136 }
137 // Check for any slow queries or non-query periods...
138 $slow = false;
139 foreach ( $this->dbTrxMethodTimes[$name] as $info ) {
140 $elapsed = ( $info[2] - $info[1] );
141 if ( $elapsed >= $this->dbLockThreshold ) {
142 $slow = true;
143 break;
144 }
145 }
146 if ( $slow ) {
147 $dbs = implode( ', ', array_keys( $this->dbTrxHoldingLocks[$name]['conns'] ) );
148 $msg = "Sub-optimal transaction on DB(s) [{$dbs}]:\n";
149 foreach ( $this->dbTrxMethodTimes[$name] as $i => $info ) {
150 list( $query, $sTime, $end ) = $info;
151 $msg .= sprintf( "%d\t%.6f\t%s\n", $i, ( $end - $sTime ), $query );
152 }
153 wfDebugLog( 'DBPerformance', $msg );
154 }
155 unset( $this->dbTrxHoldingLocks[$name] );
156 unset( $this->dbTrxMethodTimes[$name] );
157 }
158 }