1<?php
2/**
3 * Makes several 'set', 'incr' and 'get' requests on every memcached
4 * server and shows a report.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Maintenance
23 */
24
25require_once __DIR__ . '/Maintenance.php';
26
27/**
28 * Maintenance script that  makes several 'set', 'incr' and 'get' requests
29 * on every memcached server and shows a report.
30 *
31 * @ingroup Maintenance
32 */
33class McTest extends Maintenance {
34	public function __construct() {
35		parent::__construct();
36		$this->addDescription(
37			"Makes several operation requests on every cache server and shows a report.\n" .
38			"This tests both per-key and batched *Multi() methods as well as WRITE_BACKGROUND.\n" .
39			"\"IB\" means \"immediate blocking\" and \"DB\" means \"deferred blocking.\""
40		);
41		$this->addOption( 'i', 'Number of iterations', false, true );
42		$this->addOption( 'cache', 'Use servers from this $wgObjectCaches store', false, true );
43		$this->addOption( 'driver', 'Either "php" or "pecl"', false, true );
44		$this->addArg( 'server[:port]', 'Memcached server to test, with optional port', false );
45	}
46
47	public function execute() {
48		global $wgMainCacheType, $wgMemCachedTimeout, $wgObjectCaches;
49
50		$memcachedTypes = [ CACHE_MEMCACHED, 'memcached-php', 'memcached-pecl' ];
51
52		$cache = $this->getOption( 'cache' );
53		$iterations = $this->getOption( 'i', 100 );
54		if ( $cache ) {
55			if ( !isset( $wgObjectCaches[$cache] ) ) {
56				$this->fatalError( "MediaWiki isn't configured with a cache named '$cache'" );
57			}
58			$servers = $wgObjectCaches[$cache]['servers'];
59		} elseif ( $this->hasArg( 0 ) ) {
60			$servers = [ $this->getArg( 0 ) ];
61		} elseif ( in_array( $wgMainCacheType, $memcachedTypes, true ) ) {
62			global $wgMemCachedServers;
63			$servers = $wgMemCachedServers;
64		} elseif ( isset( $wgObjectCaches[$wgMainCacheType]['servers'] ) ) {
65			$servers = $wgObjectCaches[$wgMainCacheType]['servers'];
66		} else {
67			$this->fatalError( "MediaWiki isn't configured for Memcached usage" );
68		}
69
70		# find out the longest server string to nicely align output later on
71		$maxSrvLen = $servers ? max( array_map( 'strlen', $servers ) ) : 0;
72
73		$type = $this->getOption( 'driver', 'php' );
74		if ( $type === 'php' ) {
75			$class = MemcachedPhpBagOStuff::class;
76		} elseif ( $type === 'pecl' ) {
77			$class = MemcachedPeclBagOStuff::class;
78		} else {
79			$this->fatalError( "Invalid driver type '$type'" );
80		}
81
82		$this->output( "Warming up connections to cache servers..." );
83		$mccByServer = [];
84		foreach ( $servers as $server ) {
85			/** @var BagOStuff $mcc */
86			$mccByServer[$server] = new $class( [
87				'servers' => [ $server ],
88				'persistent' => true,
89				'allow_tcp_nagle_delay' => false,
90				'timeout' => $wgMemCachedTimeout
91			] );
92			$mccByServer[$server]->get( 'key' );
93		}
94		$this->output( "done\n" );
95		$this->output( "Single and batched operation profiling/test results:\n" );
96
97		$valueByKey = [];
98		for ( $i = 1; $i <= $iterations; $i++ ) {
99			$valueByKey["test$i"] = 'S' . str_pad( $i, 2048 );
100		}
101
102		foreach ( $mccByServer as $server => $mcc ) {
103			$this->output( str_pad( $server, $maxSrvLen ) . "\n" );
104			$this->benchmarkSingleKeyOps( $mcc, $valueByKey );
105			$this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $valueByKey );
106			$this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $valueByKey );
107		}
108	}
109
110	/**
111	 * @param BagOStuff $mcc
112	 * @param array $valueByKey
113	 */
114	private function benchmarkSingleKeyOps( BagOStuff $mcc, array $valueByKey ) {
115		$add = 0;
116		$set = 0;
117		$incr = 0;
118		$get = 0;
119		$delete = 0;
120
121		$i = count( $valueByKey );
122		$keys = array_keys( $valueByKey );
123
124		// Clear out any old values
125		$mcc->deleteMulti( $keys );
126
127		$time_start = microtime( true );
128		foreach ( $keys as $key ) {
129			if ( $mcc->add( $key, $i ) ) {
130				$add++;
131			}
132		}
133		$addMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
134
135		$time_start = microtime( true );
136		foreach ( $keys as $key ) {
137			if ( $mcc->set( $key, $i ) ) {
138				$set++;
139			}
140		}
141		$setMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
142
143		$time_start = microtime( true );
144		foreach ( $keys as $key ) {
145			if ( $mcc->incr( $key, $i ) !== null ) {
146				$incr++;
147			}
148		}
149		$incrMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
150
151		$time_start = microtime( true );
152		foreach ( $keys as $key ) {
153			$value = $mcc->get( $key );
154			if ( $value == $i * 2 ) {
155				$get++;
156			}
157		}
158		$getMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
159
160		$time_start = microtime( true );
161		foreach ( $keys as $key ) {
162			if ( $mcc->delete( $key ) ) {
163				$delete++;
164			}
165		}
166		$delMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
167
168		$this->output(
169			" add: $add/$i {$addMs}ms   " .
170			"set: $set/$i {$setMs}ms   " .
171			"incr: $incr/$i {$incrMs}ms   " .
172			"get: $get/$i ({$getMs}ms)   " .
173			"delete: $delete/$i ({$delMs}ms)\n"
174		);
175	}
176
177	/**
178	 * @param BagOStuff $mcc
179	 * @param array $valueByKey
180	 */
181	private function benchmarkMultiKeyOpsImmediateBlocking( BagOStuff $mcc, array $valueByKey ) {
182		$keys = array_keys( $valueByKey );
183		$iterations = count( $valueByKey );
184
185		$time_start = microtime( true );
186		$mSetOk = $mcc->setMulti( $valueByKey ) ? '✓' : '✗';
187		$mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
188
189		$time_start = microtime( true );
190		$found = $mcc->getMulti( $keys );
191		$mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
192		$mGetOk = 0;
193		foreach ( $found as $key => $value ) {
194			$mGetOk += ( $value === $valueByKey[$key] );
195		}
196
197		$time_start = microtime( true );
198		$mChangeTTLOk = $mcc->changeTTLMulti( $keys, 3600 ) ? '✓' : '✗';
199		$mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
200
201		$time_start = microtime( true );
202		$mDelOk = $mcc->deleteMulti( $keys ) ? '✓' : '✗';
203		$mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
204
205		$this->output(
206			" setMulti (IB): $mSetOk {$mSetMs}ms   " .
207			"getMulti (IB): $mGetOk/$iterations {$mGetMs}ms   " .
208			"changeTTLMulti (IB): $mChangeTTLOk {$mChangeTTTMs}ms   " .
209			"deleteMulti (IB): $mDelOk {$mDelMs}ms\n"
210		);
211	}
212
213	/**
214	 * @param BagOStuff $mcc
215	 * @param array $valueByKey
216	 */
217	private function benchmarkMultiKeyOpsDeferredBlocking( BagOStuff $mcc, array $valueByKey ) {
218		$keys = array_keys( $valueByKey );
219		$iterations = count( $valueByKey );
220		$flags = $mcc::WRITE_BACKGROUND;
221
222		$time_start = microtime( true );
223		$mSetOk = $mcc->setMulti( $valueByKey, 0, $flags ) ? '✓' : '✗';
224		$mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
225
226		$time_start = microtime( true );
227		$found = $mcc->getMulti( $keys );
228		$mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
229		$mGetOk = 0;
230		foreach ( $found as $key => $value ) {
231			$mGetOk += ( $value === $valueByKey[$key] );
232		}
233
234		$time_start = microtime( true );
235		$mChangeTTLOk = $mcc->changeTTLMulti( $keys, 3600, $flags ) ? '✓' : '✗';
236		$mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
237
238		$time_start = microtime( true );
239		$mDelOk = $mcc->deleteMulti( $keys, $flags ) ? '✓' : '✗';
240		$mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) );
241
242		$this->output(
243			" setMulti (DB): $mSetOk {$mSetMs}ms   " .
244			"getMulti (DB): $mGetOk/$iterations {$mGetMs}ms   " .
245			"changeTTLMulti (DB): $mChangeTTLOk {$mChangeTTTMs}ms   " .
246			"deleteMulti (DB): $mDelOk {$mDelMs}ms\n"
247		);
248	}
249}
250
251$maintClass = McTest::class;
252require_once RUN_MAINTENANCE_IF_MAIN;
253