From efba2a4afc72ae703e80db561d6d7d941cc8b668 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Sat, 29 Jun 2019 17:41:46 -0700 Subject: [PATCH] objectcache: optimize MemcachedPeclBagOStuff::*Multi() write methods Make use of Memcached::OPT_NO_BLOCK and Memcached::OPT_BUFFER_WRITES at selective points to avoid slow internal blocking for loops in the PECL extension methods. As this mode makes the return values less meaningful, this is only triggered only if WRITE_BACKGROUND is provided (it only catches some glaring format or size errors that the client runs into). Leave changeTTLMulti() to the default as the above approach would not make it faster, since the diver does not buffer for that command. Expand mctest.php to include *Multi() methods and also break down the single key method timings by method. Change-Id: I11fc58e1a2296778b2526a0b2f3103fef1baba0c --- includes/libs/objectcache/BagOStuff.php | 21 +- .../objectcache/MemcachedPeclBagOStuff.php | 210 ++++++++++++++---- maintenance/mctest.php | 189 +++++++++++++--- 3 files changed, 344 insertions(+), 76 deletions(-) diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index d13626ae1b..43621b69e2 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -97,14 +97,15 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar /** @var int[] Map of (ATTR_* class constant => QOS_* class constant) */ protected $attrMap = []; - /** Bitfield constants for get()/getMulti() */ - const READ_LATEST = 1; // use latest data for replicated stores - const READ_VERIFIED = 2; // promise that caller can tell when keys are stale - /** Bitfield constants for set()/merge() */ - const WRITE_SYNC = 4; // synchronously write to all locations for replicated stores - const WRITE_CACHE_ONLY = 8; // Only change state of the in-memory cache - const WRITE_ALLOW_SEGMENTS = 16; // Allow partitioning of the value if it is large - const WRITE_PRUNE_SEGMENTS = 32; // Delete all partition segments of the value + /** Bitfield constants for get()/getMulti(); these are only advisory */ + const READ_LATEST = 1; // if supported, avoid reading stale data due to replication + const READ_VERIFIED = 2; // promise that the caller handles detection of staleness + /** Bitfield constants for set()/merge(); these are only advisory */ + const WRITE_SYNC = 4; // if supported, block until the write is fully replicated + const WRITE_CACHE_ONLY = 8; // only change state of the in-memory cache + const WRITE_ALLOW_SEGMENTS = 16; // allow partitioning of the value if it is large + const WRITE_PRUNE_SEGMENTS = 32; // delete all the segments if the value is partitioned + const WRITE_BACKGROUND = 64; // if supported, /** @var string Component to use for key construction of blob segment keys */ const SEGMENT_COMPONENT = 'segment'; @@ -720,6 +721,8 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar * * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O * + * WRITE_BACKGROUND can be used for bulk insertion where the response is not vital + * * @param mixed[] $data Map of (key => value) * @param int $exptime Either an interval in seconds or a unix timestamp for expiry * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) @@ -754,6 +757,8 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar * * This does not support WRITE_ALLOW_SEGMENTS to avoid excessive read I/O * + * WRITE_BACKGROUND can be used for bulk deletion where the response is not vital + * * @param string[] $keys List of keys * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php index f6721ce49d..0c814db4a2 100644 --- a/includes/libs/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPeclBagOStuff.php @@ -28,7 +28,25 @@ */ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { /** @var Memcached */ - protected $client; + protected $syncClient; + /** @var Memcached|null */ + protected $asyncClient; + + /** @var bool Whether the non-buffering client is locked from use */ + protected $syncClientIsBuffering = false; + /** @var bool Whether the non-buffering client should be flushed before use */ + protected $hasUnflushedChanges = false; + + /** @var array Memcached options */ + private static $OPTS_SYNC_WRITES = [ + Memcached::OPT_NO_BLOCK => false, // async I/O (using TCP buffers) + Memcached::OPT_BUFFER_WRITES => false // libmemcached buffers + ]; + /** @var array Memcached options */ + private static $OPTS_ASYNC_WRITES = [ + Memcached::OPT_NO_BLOCK => true, // async I/O (using TCP buffers) + Memcached::OPT_BUFFER_WRITES => true // libmemcached buffers + ]; /** * Available parameters are: @@ -63,15 +81,22 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { // The Memcached object is essentially shared for each pool ID. // We can only reuse a pool ID if we keep the config consistent. $connectionPoolId = md5( serialize( $params ) ); - $client = new Memcached( $connectionPoolId ); - $this->initializeClient( $client, $params ); + $syncClient = new Memcached( "$connectionPoolId-sync" ); + // Avoid clobbering the main thread-shared Memcached instance + $asyncClient = new Memcached( "$connectionPoolId-async" ); } else { - $client = new Memcached; - $this->initializeClient( $client, $params ); + $syncClient = new Memcached(); + $asyncClient = null; } - $this->client = $client; + $this->initializeClient( $syncClient, $params, self::$OPTS_SYNC_WRITES ); + if ( $asyncClient ) { + $this->initializeClient( $asyncClient, $params, self::$OPTS_ASYNC_WRITES ); + } + // Set the main client and any dedicated one for buffered writes + $this->syncClient = $syncClient; + $this->asyncClient = $asyncClient; // The compression threshold is an undocumented php.ini option for some // reason. There's probably not much harm in setting it globally, for // compatibility with the settings for the PHP client. @@ -84,9 +109,10 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { * * @param Memcached $client * @param array $params + * @param array $options Base options for Memcached::setOptions() * @throws RuntimeException */ - private function initializeClient( Memcached $client, array $params ) { + private function initializeClient( Memcached $client, array $params, array $options ) { if ( $client->getServerList() ) { $this->logger->debug( __METHOD__ . ": pre-initialized client instance." ); @@ -95,7 +121,9 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { $this->logger->debug( __METHOD__ . ": initializing new client instance." ); - $options = [ + $options += [ + Memcached::OPT_NO_BLOCK => false, + Memcached::OPT_BUFFER_WRITES => false, // Network protocol (ASCII or binary) Memcached::OPT_BINARY_PROTOCOL => $params['use_binary_protocol'], // Set various network timeouts @@ -150,10 +178,12 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { protected function doGet( $key, $flags = 0, &$casToken = null ) { $this->debug( "get($key)" ); + + $client = $this->acquireSyncClient(); if ( defined( Memcached::class . '::GET_EXTENDED' ) ) { // v3.0.0 /** @noinspection PhpUndefinedClassConstantInspection */ $flags = Memcached::GET_EXTENDED; - $res = $this->client->get( $this->validateKeyEncoding( $key ), null, $flags ); + $res = $client->get( $this->validateKeyEncoding( $key ), null, $flags ); if ( is_array( $res ) ) { $result = $res['value']; $casToken = $res['cas']; @@ -162,62 +192,77 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { $casToken = null; } } else { - $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken ); + $result = $client->get( $this->validateKeyEncoding( $key ), null, $casToken ); } - $result = $this->checkResult( $key, $result ); - return $result; + + return $this->checkResult( $key, $result ); } protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "set($key)" ); - $result = $this->client->set( + + $client = $this->acquireSyncClient(); + $result = $client->set( $this->validateKeyEncoding( $key ), $value, $this->fixExpiry( $exptime ) ); - if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) { + + return ( $result === false && $client->getResultCode() === Memcached::RES_NOTSTORED ) // "Not stored" is always used as the mcrouter response with AllAsyncRoute - return true; - } - return $this->checkResult( $key, $result ); + ? true + : $this->checkResult( $key, $result ); } protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "cas($key)" ); - $result = $this->client->cas( $casToken, $this->validateKeyEncoding( $key ), - $value, $this->fixExpiry( $exptime ) ); + + $result = $this->acquireSyncClient()->cas( + $casToken, + $this->validateKeyEncoding( $key ), + $value, $this->fixExpiry( $exptime ) + ); + return $this->checkResult( $key, $result ); } protected function doDelete( $key, $flags = 0 ) { $this->debug( "delete($key)" ); - $result = $this->client->delete( $this->validateKeyEncoding( $key ) ); - if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) { + + $client = $this->acquireSyncClient(); + $result = $client->delete( $this->validateKeyEncoding( $key ) ); + + return ( $result === false && $client->getResultCode() === Memcached::RES_NOTFOUND ) // "Not found" is counted as success in our interface - return true; - } - return $this->checkResult( $key, $result ); + ? true + : $this->checkResult( $key, $result ); } public function add( $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "add($key)" ); - $result = $this->client->add( + + $result = $this->acquireSyncClient()->add( $this->validateKeyEncoding( $key ), $value, $this->fixExpiry( $exptime ) ); + return $this->checkResult( $key, $result ); } public function incr( $key, $value = 1 ) { $this->debug( "incr($key)" ); - $result = $this->client->increment( $key, $value ); + + $result = $this->acquireSyncClient()->increment( $key, $value ); + return $this->checkResult( $key, $result ); } public function decr( $key, $value = 1 ) { $this->debug( "decr($key)" ); - $result = $this->client->decrement( $key, $value ); + + $result = $this->acquireSyncClient()->decrement( $key, $value ); + return $this->checkResult( $key, $result ); } @@ -236,22 +281,25 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { if ( $result !== false ) { return $result; } - switch ( $this->client->getResultCode() ) { + + $client = $this->syncClient; + switch ( $client->getResultCode() ) { case Memcached::RES_SUCCESS: break; case Memcached::RES_DATA_EXISTS: case Memcached::RES_NOTSTORED: case Memcached::RES_NOTFOUND: - $this->debug( "result: " . $this->client->getResultMessage() ); + $this->debug( "result: " . $client->getResultMessage() ); break; default: - $msg = $this->client->getResultMessage(); + $msg = $client->getResultMessage(); $logCtx = []; if ( $key !== false ) { - $server = $this->client->getServerByKey( $key ); + $server = $client->getServerByKey( $key ); $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}"; $logCtx['memcached-key'] = $key; - $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg"; + $msg = "Memcached error for key \"{memcached-key}\" " . + "on server \"{memcached-server}\": $msg"; } else { $msg = "Memcached error: $msg"; } @@ -263,41 +311,71 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { public function doGetMulti( array $keys, $flags = 0 ) { $this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' ); + foreach ( $keys as $key ) { $this->validateKeyEncoding( $key ); } - $result = $this->client->getMulti( $keys ) ?: []; + + // The PECL implementation uses "gets" which works as well as a pipeline + $result = $this->acquireSyncClient()->getMulti( $keys ) ?: []; + return $this->checkResult( false, $result ); } public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) { $this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' ); + + $exptime = $this->fixExpiry( $exptime ); foreach ( array_keys( $data ) as $key ) { $this->validateKeyEncoding( $key ); } - $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) ); + + // The PECL implementation is a naïve for-loop so use async I/O to pipeline; + // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852 + if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) { + $client = $this->acquireAsyncClient(); + $result = $client->setMulti( $data, $exptime ); + $this->releaseAsyncClient( $client ); + } else { + $result = $this->acquireSyncClient()->setMulti( $data, $exptime ); + } + return $this->checkResult( false, $result ); } public function doDeleteMulti( array $keys, $flags = 0 ) { $this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' ); + foreach ( $keys as $key ) { $this->validateKeyEncoding( $key ); } - $result = $this->client->deleteMulti( $keys ) ?: []; - $ok = true; - foreach ( $result as $code ) { + + // The PECL implementation is a naïve for-loop so use async I/O to pipeline; + // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852 + if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) { + $client = $this->acquireAsyncClient(); + $resultArray = $client->deleteMulti( $keys ) ?: []; + $this->releaseAsyncClient( $client ); + } else { + $resultArray = $this->acquireSyncClient()->deleteMulti( $keys ) ?: []; + } + + $result = true; + foreach ( $resultArray as $code ) { if ( !in_array( $code, [ true, Memcached::RES_NOTFOUND ], true ) ) { // "Not found" is counted as success in our interface - $ok = false; + $result = false; } } - return $this->checkResult( false, $ok ); + + return $this->checkResult( false, $result ); } protected function doChangeTTL( $key, $exptime, $flags ) { $this->debug( "touch($key)" ); - $result = $this->client->touch( $key, $exptime ); + + $result = $this->acquireSyncClient()->touch( $key, $this->fixExpiry( $exptime ) ); + return $this->checkResult( $key, $result ); } @@ -306,7 +384,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return $value; } - $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER ); + $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER ); if ( $serializer === Memcached::SERIALIZER_PHP ) { return serialize( $value ); } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) { @@ -321,7 +399,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { return (int)$value; } - $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER ); + $serializer = $this->syncClient->getOption( Memcached::OPT_SERIALIZER ); if ( $serializer === Memcached::SERIALIZER_PHP ) { return unserialize( $value ); } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) { @@ -330,4 +408,52 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { throw new UnexpectedValueException( __METHOD__ . ": got serializer '$serializer'." ); } + + /** + * @return Memcached + */ + private function acquireSyncClient() { + if ( $this->syncClientIsBuffering ) { + throw new RuntimeException( "The main (unbuffered I/O) client is locked" ); + } + + if ( $this->hasUnflushedChanges ) { + // Force a synchronous flush of async writes so that their changes are visible + $this->syncClient->fetch(); + if ( $this->asyncClient ) { + $this->asyncClient->fetch(); + } + $this->hasUnflushedChanges = false; + } + + return $this->syncClient; + } + + /** + * @return Memcached + */ + private function acquireAsyncClient() { + if ( $this->asyncClient ) { + return $this->asyncClient; // dedicated buffering instance + } + + // Modify the main instance to temporarily buffer writes + $this->syncClientIsBuffering = true; + $this->syncClient->setOptions( self::$OPTS_ASYNC_WRITES ); + + return $this->syncClient; + } + + /** + * @param Memcached $client + */ + private function releaseAsyncClient( $client ) { + $this->hasUnflushedChanges = true; + + if ( !$this->asyncClient ) { + // This is the main instance; make it stop buffering writes again + $client->setOptions( self::$OPTS_SYNC_WRITES ); + $this->syncClientIsBuffering = false; + } + } } diff --git a/maintenance/mctest.php b/maintenance/mctest.php index 513edf3911..9548d6b688 100644 --- a/maintenance/mctest.php +++ b/maintenance/mctest.php @@ -37,6 +37,7 @@ class McTest extends Maintenance { . " memcached server and shows a report" ); $this->addOption( 'i', 'Number of iterations', false, true ); $this->addOption( 'cache', 'Use servers from this $wgObjectCaches store', false, true ); + $this->addOption( 'driver', 'Either "php" or "pecl"', false, true ); $this->addArg( 'server[:port]', 'Memcached server to test, with optional port', false ); } @@ -66,41 +67,177 @@ class McTest extends Maintenance { # find out the longest server string to nicely align output later on $maxSrvLen = $servers ? max( array_map( 'strlen', $servers ) ) : 0; + $type = $this->getOption( 'driver', 'php' ); + if ( $type === 'php' ) { + $class = MemcachedPhpBagOStuff::class; + } elseif ( $type === 'pecl' ) { + $class = MemcachedPeclBagOStuff::class; + } else { + $this->fatalError( "Invalid driver type '$type'" ); + } + foreach ( $servers as $server ) { - $this->output( - str_pad( $server, $maxSrvLen ), - $server # output channel - ); + $this->output( str_pad( $server, $maxSrvLen ) . "\n" ); - $mcc = new MemcachedClient( [ - 'persistant' => true, + /** @var BagOStuff $mcc */ + $mcc = new $class( [ + 'servers' => [ $server ], + 'persistent' => true, 'timeout' => $wgMemCachedTimeout ] ); - $mcc->set_servers( [ $server ] ); - $set = 0; - $incr = 0; - $get = 0; - $time_start = microtime( true ); - for ( $i = 1; $i <= $iterations; $i++ ) { - if ( $mcc->set( "test$i", $i ) ) { - $set++; - } + + $this->benchmarkSingleKeyOps( $mcc, $iterations ); + $this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ); + $this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ); + } + } + + /** + * @param BagOStuff $mcc + * @param int $iterations + */ + private function benchmarkSingleKeyOps( $mcc, $iterations ) { + $add = 0; + $set = 0; + $incr = 0; + $get = 0; + $delete = 0; + + $keys = []; + for ( $i = 1; $i <= $iterations; $i++ ) { + $keys[] = "test$i"; + } + + // Clear out any old values + $mcc->deleteMulti( $keys ); + + $time_start = microtime( true ); + foreach ( $keys as $key ) { + if ( $mcc->add( $key, $i ) ) { + $add++; } - for ( $i = 1; $i <= $iterations; $i++ ) { - if ( !is_null( $mcc->incr( "test$i", $i ) ) ) { - $incr++; - } + } + $addMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + foreach ( $keys as $key ) { + if ( $mcc->set( $key, $i ) ) { + $set++; + } + } + $setMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + foreach ( $keys as $key ) { + if ( !is_null( $mcc->incr( $key, $i ) ) ) { + $incr++; } - for ( $i = 1; $i <= $iterations; $i++ ) { - $value = $mcc->get( "test$i" ); - if ( $value == $i * 2 ) { - $get++; - } + } + $incrMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + foreach ( $keys as $key ) { + $value = $mcc->get( $key ); + if ( $value == $i * 2 ) { + $get++; } - $exectime = microtime( true ) - $time_start; + } + $getMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); - $this->output( " set: $set incr: $incr get: $get time: $exectime", $server ); + $time_start = microtime( true ); + foreach ( $keys as $key ) { + if ( $mcc->delete( $key ) ) { + $delete++; + } } + $delMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $this->output( + " add: $add/$iterations {$addMs}ms " . + "set: $set/$iterations {$setMs}ms " . + "incr: $incr/$iterations {$incrMs}ms " . + "get: $get/$iterations ({$getMs}ms) " . + "delete: $delete/$iterations ({$delMs}ms)\n" + ); + } + + /** + * @param BagOStuff $mcc + * @param int $iterations + */ + private function benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ) { + $keysByValue = []; + for ( $i = 1; $i <= $iterations; $i++ ) { + $keysByValue["test$i"] = 'S' . str_pad( $i, 2048 ); + } + $keyList = array_keys( $keysByValue ); + + $time_start = microtime( true ); + $mSetOk = $mcc->setMulti( $keysByValue ) ? 'S' : 'F'; + $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + $found = $mcc->getMulti( $keyList ); + $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + $mGetOk = 0; + foreach ( $found as $key => $value ) { + $mGetOk += ( $value === $keysByValue[$key] ); + } + + $time_start = microtime( true ); + $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600 ) ? 'S' : 'F'; + $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + $mDelOk = $mcc->deleteMulti( $keyList ) ? 'S' : 'F'; + $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $this->output( + " setMulti (IB): $mSetOk {$mSetMs}ms " . + "getMulti (IB): $mGetOk/$iterations {$mGetMs}ms " . + "changeTTLMulti (IB): $mChangeTTLOk {$mChangeTTTMs}ms " . + "deleteMulti (IB): $mDelOk {$mDelMs}ms\n" + ); + } + + /** + * @param BagOStuff $mcc + * @param int $iterations + */ + private function benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ) { + $flags = $mcc::WRITE_BACKGROUND; + $keysByValue = []; + for ( $i = 1; $i <= $iterations; $i++ ) { + $keysByValue["test$i"] = 'A' . str_pad( $i, 2048 ); + } + $keyList = array_keys( $keysByValue ); + + $time_start = microtime( true ); + $mSetOk = $mcc->setMulti( $keysByValue, 0, $flags ) ? 'S' : 'F'; + $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + $found = $mcc->getMulti( $keyList ); + $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + $mGetOk = 0; + foreach ( $found as $key => $value ) { + $mGetOk += ( $value === $keysByValue[$key] ); + } + + $time_start = microtime( true ); + $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600, $flags ) ? 'S' : 'F'; + $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $time_start = microtime( true ); + $mDelOk = $mcc->deleteMulti( $keyList, $flags ) ? 'S' : 'F'; + $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); + + $this->output( + " setMulti (DB): $mSetOk {$mSetMs}ms " . + "getMulti (DB): $mGetOk/$iterations {$mGetMs}ms " . + "changeTTLMulti (DB): $mChangeTTLOk {$mChangeTTTMs}ms " . + "deleteMulti (DB): $mDelOk {$mDelMs}ms\n" + ); } } -- 2.20.1