diff --git a/cache/classes/helper.php b/cache/classes/helper.php index 50643fe7f80..db78844626c 100644 --- a/cache/classes/helper.php +++ b/cache/classes/helper.php @@ -381,6 +381,7 @@ class cache_helper { 'hits' => 0, 'misses' => 0, 'sets' => 0, + 'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED, ) ) ); @@ -390,6 +391,7 @@ class cache_helper { 'hits' => 0, 'misses' => 0, 'sets' => 0, + 'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED, ); } } @@ -429,8 +431,9 @@ class cache_helper { * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the * actual cache_definition object now. * @param int $hits The number of hits to record (by default 1) + * @param int $readbytes Number of bytes read from the cache or cache_store::IO_BYTES_NOT_SUPPORTED */ - public static function record_cache_hit($store, $definition, $hits = 1) { + public static function record_cache_hit($store, $definition, int $hits = 1, int $readbytes = cache_store::IO_BYTES_NOT_SUPPORTED): void { $storeclass = ''; if ($store instanceof cache_store) { $storeclass = get_class($store); @@ -439,6 +442,13 @@ class cache_helper { list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition); self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode); self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits; + if ($readbytes !== cache_store::IO_BYTES_NOT_SUPPORTED) { + if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { + self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $readbytes; + } else { + self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $readbytes; + } + } } /** @@ -479,8 +489,10 @@ class cache_helper { * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the * actual cache_definition object now. * @param int $sets The number of sets to record (by default 1) + * @param int $writebytes Number of bytes written to the cache or cache_store::IO_BYTES_NOT_SUPPORTED */ - public static function record_cache_set($store, $definition, $sets = 1) { + public static function record_cache_set($store, $definition, int $sets = 1, + int $writebytes = cache_store::IO_BYTES_NOT_SUPPORTED) { $storeclass = ''; if ($store instanceof cache_store) { $storeclass = get_class($store); @@ -489,6 +501,13 @@ class cache_helper { list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition); self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode); self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets; + if ($writebytes !== cache_store::IO_BYTES_NOT_SUPPORTED) { + if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { + self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $writebytes; + } else { + self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $writebytes; + } + } } /** diff --git a/cache/classes/loaders.php b/cache/classes/loaders.php index a557a3e9b69..bf987abda70 100644 --- a/cache/classes/loaders.php +++ b/cache/classes/loaders.php @@ -445,7 +445,8 @@ class cache implements cache_loader { } $setaftervalidation = ($result !== false); } else if ($this->perfdebug) { - cache_helper::record_cache_hit($this->store, $this->definition); + $readbytes = $this->store->get_last_io_bytes(); + cache_helper::record_cache_hit($this->store, $this->definition, 1, $readbytes); } // 5. Validate strictness. if ($strictness === MUST_EXIST && $result === false) { @@ -492,6 +493,7 @@ class cache implements cache_loader { $resultpersist = array(); $resultstore = array(); $keystofind = array(); + $readbytes = cache_store::IO_BYTES_NOT_SUPPORTED; // First up check the persist cache for each key. $isusingpersist = $this->use_static_acceleration(); @@ -515,6 +517,9 @@ class cache implements cache_loader { // Next assuming we didn't find all of the keys in the persist cache try loading them from the store. if (count($keystofind)) { $resultstore = $this->store->get_many(array_keys($keystofind)); + if ($this->perfdebug) { + $readbytes = $this->store->get_last_io_bytes(); + } // Process each item in the result to "unwrap" it. foreach ($resultstore as $key => $value) { if ($value instanceof cache_ttl_wrapper) { @@ -599,7 +604,7 @@ class cache implements cache_loader { $hits++; } } - cache_helper::record_cache_hit($this->store, $this->definition, $hits); + cache_helper::record_cache_hit($this->store, $this->definition, $hits, $readbytes); cache_helper::record_cache_miss($this->store, $this->definition, $misses); } @@ -625,9 +630,6 @@ class cache implements cache_loader { * @return bool True on success, false otherwise. */ public function set($key, $data) { - if ($this->perfdebug) { - cache_helper::record_cache_set($this->store, $this->definition); - } if ($this->loader !== false) { // We have a loader available set it there as well. // We have to let the loader do its own parsing of data as it may be unique. @@ -654,7 +656,12 @@ class cache implements cache_loader { } $parsedkey = $this->parse_key($key); - return $this->store->set($parsedkey, $data); + $success = $this->store->set($parsedkey, $data); + if ($this->perfdebug) { + cache_helper::record_cache_set($this->store, $this->definition, 1, + $this->store->get_last_io_bytes()); + } + return $success; } /** @@ -781,7 +788,8 @@ class cache implements cache_loader { } $successfullyset = $this->store->set_many($data); if ($this->perfdebug && $successfullyset) { - cache_helper::record_cache_set($this->store, $this->definition, $successfullyset); + cache_helper::record_cache_set($this->store, $this->definition, $successfullyset, + $this->store->get_last_io_bytes()); } return $successfullyset; } @@ -1845,6 +1853,9 @@ class cache_session extends cache { if ($result instanceof cache_cached_object) { $result = $result->restore_object(); } + if ($this->perfdebug) { + $readbytes = $this->get_store()->get_last_io_bytes(); + } } // 4. Load if from the loader/datasource if we don't already have it. if ($result === false) { @@ -1864,7 +1875,7 @@ class cache_session extends cache { $this->set($key, $result); } } else if ($this->perfdebug) { - cache_helper::record_cache_hit($this->get_store(), $this->get_definition()); + cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), 1, $readbytes); } // 5. Validate strictness. if ($strictness === MUST_EXIST && $result === false) { @@ -1907,9 +1918,6 @@ class cache_session extends cache { // We have to let the loader do its own parsing of data as it may be unique. $loader->set($key, $data); } - if ($this->perfdebug) { - cache_helper::record_cache_set($this->get_store(), $this->get_definition()); - } if (is_object($data) && $data instanceof cacheable_object) { $data = new cache_cached_object($data); } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($data)) { @@ -1923,7 +1931,12 @@ class cache_session extends cache { if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { $data = new cache_ttl_wrapper($data, $this->get_definition()->get_ttl()); } - return $this->get_store()->set($this->parse_key($key), $data); + $success = $this->get_store()->set($this->parse_key($key), $data); + if ($this->perfdebug) { + cache_helper::record_cache_set($this->get_store(), $this->get_definition(), 1, + $this->get_store()->get_last_io_bytes()); + } + return $success; } /** @@ -1971,6 +1984,9 @@ class cache_session extends cache { $keymap[$parsedkey] = $key; } $result = $this->get_store()->get_many($parsedkeys); + if ($this->perfdebug) { + $readbytes = $this->get_store()->get_last_io_bytes(); + } $return = array(); $missingkeys = array(); $hasmissingkeys = false; @@ -2038,7 +2054,7 @@ class cache_session extends cache { $hits++; } } - cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits); + cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits, $readbytes); cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses); } return $return; @@ -2116,7 +2132,8 @@ class cache_session extends cache { } $successfullyset = $this->get_store()->set_many($data); if ($this->perfdebug && $successfullyset) { - cache_helper::record_cache_set($this->get_store(), $this->get_definition(), $successfullyset); + cache_helper::record_cache_set($this->get_store(), $this->get_definition(), $successfullyset, + $this->get_store()->get_last_io_bytes()); } return $successfullyset; } diff --git a/cache/classes/store.php b/cache/classes/store.php index a2cfe3e6dce..0a2dd473acb 100644 --- a/cache/classes/store.php +++ b/cache/classes/store.php @@ -149,6 +149,11 @@ abstract class cache_store implements cache_store_interface { */ const STATIC_ACCEL = '** static accel. **'; + /** + * Returned from get_last_io_bytes if this cache store doesn't support counting bytes read/sent. + */ + const IO_BYTES_NOT_SUPPORTED = -1; + /** * Constructs an instance of the cache store. * @@ -392,4 +397,23 @@ abstract class cache_store implements cache_store_interface { public static function ready_to_be_used_for_testing() { return false; } + + /** + * Gets the number of bytes read from or written to cache as a result of the last action. + * + * This includes calls to the functions get(), get_many(), set(), and set_many(). The number + * is reset by calling any of these functions. + * + * This should be the actual number of bytes of the value read from or written to cache, + * giving an impression of the network or other load. It will not be exactly the same amount + * as netowrk traffic because of protocol overhead, key text, etc. + * + * If not supported, returns IO_BYTES_NOT_SUPPORTED. + * + * @return int Bytes read (or 0 if none/not supported) + * @since Moodle 4.0 + */ + public function get_last_io_bytes(): int { + return self::IO_BYTES_NOT_SUPPORTED; + } } diff --git a/cache/stores/file/lib.php b/cache/stores/file/lib.php index 0c08710d551..8f140528e4d 100644 --- a/cache/stores/file/lib.php +++ b/cache/stores/file/lib.php @@ -101,6 +101,13 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i */ protected $definition; + /** + * Bytes read or written by last call to set()/get() or set_many()/get_many(). + * + * @var int + */ + protected $lastiobytes = 0; + /** * A reference to the global $CFG object. * @@ -334,6 +341,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i * @return mixed The data that was associated with the key, or false if the key did not exist. */ public function get($key) { + $this->lastiobytes = 0; $filename = $key.'.cache'; $file = $this->file_path_for_key($key); $ttl = $this->definition->get_ttl(); @@ -369,6 +377,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i } while (!feof($handle)); // Unlock it. flock($handle, LOCK_UN); + $this->lastiobytes = strlen($data); // Return it unserialised. return $this->prep_data_after_read($data); } @@ -384,12 +393,25 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i */ public function get_many($keys) { $result = array(); + $total = 0; foreach ($keys as $key) { $result[$key] = $this->get($key); + $total += $this->lastiobytes; } + $this->lastiobytes = $total; return $result; } + /** + * Gets bytes read by last get() or get_many(), or written by set() or set_many(). + * + * @return int Bytes read or written + * @since Moodle 4.0 + */ + public function get_last_io_bytes(): int { + return $this->lastiobytes; + } + /** * Deletes an item from the cache store. * @@ -434,7 +456,9 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i $this->ensure_path_exists(); $filename = $key.'.cache'; $file = $this->file_path_for_key($key, true); - $result = $this->write_file($file, $this->prep_data_before_save($data)); + $serialized = $this->prep_data_before_save($data); + $this->lastiobytes = strlen($serialized); + $result = $this->write_file($file, $serialized); if (!$result) { // Couldn't write the file. return false; @@ -482,11 +506,14 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i */ public function set_many(array $keyvaluearray) { $count = 0; + $totaliobytes = 0; foreach ($keyvaluearray as $pair) { if ($this->set($pair['key'], $pair['value'])) { + $totaliobytes += $this->lastiobytes; $count++; } } + $this->lastiobytes = $totaliobytes; return $count; } diff --git a/cache/stores/file/tests/file_test.php b/cache/stores/file/tests/file_test.php index 887c5454f7c..18c5f46a148 100644 --- a/cache/stores/file/tests/file_test.php +++ b/cache/stores/file/tests/file_test.php @@ -74,4 +74,31 @@ class cachestore_file_test extends cachestore_tests { $cache->get('testing'); } -} \ No newline at end of file + + /** + * Tests the get_last_read byte count. + */ + public function test_get_last_io_bytes(): void { + $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test'); + $store = new \cachestore_file('Test'); + $store->initialise($definition); + + $store->set('foo', 'bar'); + $store->set('frog', 'ribbit'); + $store->get('foo'); + // It's not 3 bytes, because the data is stored serialized. + $this->assertEquals(10, $store->get_last_io_bytes()); + $store->get('frog'); + $this->assertEquals(13, $store->get_last_io_bytes()); + $store->get_many(['foo', 'frog']); + $this->assertEquals(23, $store->get_last_io_bytes()); + + $store->set('foo', 'goo'); + $this->assertEquals(10, $store->get_last_io_bytes()); + $store->set_many([ + ['key' => 'foo', 'value' => 'bar'], + ['key' => 'frog', 'value' => 'jump'] + ]); + $this->assertEquals(21, $store->get_last_io_bytes()); + } +} diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index 796370c3168..87152bbd925 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -112,6 +112,13 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ */ protected $compressor = self::COMPRESSOR_NONE; + /** + * Bytes read or written by last call to set()/get() or set_many()/get_many(). + * + * @var int + */ + protected $lastiobytes = 0; + /** * Determines if the requirements for this type of store are met. * @@ -290,6 +297,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ return $value; } + // When using compression, values are always strings, so strlen will work. + $this->lastiobytes = strlen($value); + return $this->uncompress($value); } @@ -306,13 +316,34 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ return $values; } + $this->lastiobytes = 0; foreach ($values as &$value) { + $this->lastiobytes += strlen($value); $value = $this->uncompress($value); } return $values; } + /** + * Gets the number of bytes read from or written to cache as a result of the last action. + * + * If compression is not enabled, this function always returns IO_BYTES_NOT_SUPPORTED. The reason is that + * when compression is not enabled, data sent to the cache is not serialized, and we would + * need to serialize it to compute the size, which would have a significant performance cost. + * + * @return int Bytes read or written + * @since Moodle 4.0 + */ + public function get_last_io_bytes(): int { + if ($this->compressor != self::COMPRESSOR_NONE) { + return $this->lastiobytes; + } else { + // Not supported unless compression is on. + return parent::get_last_io_bytes(); + } + } + /** * Set the value of a key. * @@ -323,6 +354,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ public function set($key, $value) { if ($this->compressor != self::COMPRESSOR_NONE) { $value = $this->compress($value); + $this->lastiobytes = strlen($value); } if ($this->redis->hSet($this->hash, $key, $value) === false) { @@ -354,10 +386,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ $now = self::get_time(); } + $this->lastiobytes = 0; foreach ($keyvaluearray as $pair) { $key = $pair['key']; if ($this->compressor != self::COMPRESSOR_NONE) { $pairs[$key] = $this->compress($pair['value']); + $this->lastiobytes += strlen($pairs[$key]); } else { $pairs[$key] = $pair['value']; } diff --git a/cache/stores/redis/tests/redis_test.php b/cache/stores/redis/tests/redis_test.php index a4bd9f79425..db1fbd0073f 100644 --- a/cache/stores/redis/tests/redis_test.php +++ b/cache/stores/redis/tests/redis_test.php @@ -71,12 +71,15 @@ class cachestore_redis_test extends cachestore_tests { /** * Creates the required cachestore for the tests to run against Redis. * + * @param array $extraconfig Extra configuration options for Redis instance, if any * @return cachestore_redis */ - protected function create_cachestore_redis() { + protected function create_cachestore_redis(array $extraconfig = []): cachestore_redis { /** @var cache_definition $definition */ $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test'); - $store = new cachestore_redis('Test', cachestore_redis::unit_test_configuration()); + $configuration = array_merge(cachestore_redis::unit_test_configuration(), $extraconfig); + + $store = new cachestore_redis('Test', $configuration); $store->initialise($definition); $this->store = $store; @@ -123,4 +126,45 @@ class cachestore_redis_test extends cachestore_tests { $this->assertFalse($store->release_lock('lock', '321')); $this->assertTrue($store->release_lock('lock', '123')); } + + /** + * Tests the get_last_io_bytes function when not using compression (just returns unknown). + */ + public function test_get_last_io_bytes(): void { + $store = $this->create_cachestore_redis(); + + $store->set('foo', [1, 2, 3, 4]); + $this->assertEquals(\cache_store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes()); + $store->get('foo'); + $this->assertEquals(\cache_store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes()); + } + + /** + * Tests the get_last_io_bytes byte count when using compression. + */ + public function test_get_last_io_bytes_compressed(): void { + $store = $this->create_cachestore_redis(['compressor' => cachestore_redis::COMPRESSOR_PHP_GZIP]); + + $alphabet = 'abcdefghijklmnopqrstuvwxyz'; + + $store->set('small', $alphabet); + $store->set('large', str_repeat($alphabet, 10)); + + $store->get('small'); + // Interesting 'compression'. + $this->assertEquals(54, $store->get_last_io_bytes()); + $store->get('large'); + // This one is actually smaller than uncompressed value! + $this->assertEquals(57, $store->get_last_io_bytes()); + $store->get_many(['small', 'large']); + $this->assertEquals(111, $store->get_last_io_bytes()); + + $store->set('small', str_repeat($alphabet, 2)); + $this->assertEquals(56, $store->get_last_io_bytes()); + $store->set_many([ + ['key' => 'small', 'value' => $alphabet], + ['key' => 'large', 'value' => str_repeat($alphabet, 10)] + ]); + $this->assertEquals(111, $store->get_last_io_bytes()); + } } diff --git a/cache/tests/behat/perf_info.feature b/cache/tests/behat/perf_info.feature new file mode 100644 index 00000000000..6e81d293a26 --- /dev/null +++ b/cache/tests/behat/perf_info.feature @@ -0,0 +1,21 @@ +@core @core_cache +Feature: Display cache information in performance info + In order to investigate performance problems with caching + As an administrator + I need to be able to see cache information in perfinfo + + Background: + Given the following config values are set as admin: + | perfdebug | 15 | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + + Scenario: Cache performance info displays + When I am on the "C1" "Course" page logged in as "admin" + # Confirm that the first cache info table is visible by checking an arbitrary row. + Then I should see "default_application" in the "core/databasemeta" "table_row" + # Don't specify the exact size as it may vary. + And I should see "KB" in the "core/databasemeta" "table_row" + # Confirm that the second cache info table is visible. + And I should see "default_application" in the "cachestore_file" "table_row" diff --git a/cache/upgrade.txt b/cache/upgrade.txt index 1890d306076..86ce4d3d0d7 100644 --- a/cache/upgrade.txt +++ b/cache/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in /cache/stores/* - cache store plugins. Information provided here is intended especially for developers. +=== 4.0 === +* Cache stores may implement new optional function cache_store::get_last_io_bytes() to provide + information about the size of data transferred (shown in footer if performance info enabled). + === 3.10 === * The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations. * The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 1533c1d8029..232546e76f0 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -9562,9 +9562,9 @@ function get_performance_info() { $table = new html_table(); $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; - $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S']; + $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O']; $table->data = []; - $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right']; + $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right']; $text = 'Caches used (hits/misses/sets): '; $hits = 0; @@ -9595,9 +9595,11 @@ function get_performance_info() { $table->align[] = 'right'; $table->align[] = 'right'; $table->align[] = 'right'; + $table->align[] = 'right'; $table->head[] = 'H'; $table->head[] = 'M'; $table->head[] = 'S'; + $table->head[] = 'I/O'; } ksort($stats); @@ -9657,6 +9659,22 @@ function get_performance_info() { $cell = new html_table_cell($data['sets']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; + + if ($data['hits'] || $data['sets']) { + if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { + $size = '-'; + } else { + $size = display_size($data['iobytes'], 1, 'KB'); + if ($data['iobytes'] >= 10 * 1024) { + $cachestoreclass = ' bg-warning text-dark'; + } + } + } else { + $size = ''; + } + $cell = new html_table_cell($size); + $cell->attributes = ['class' => $cachestoreclass]; + $row[] = $cell; } $storec++; } @@ -9665,6 +9683,7 @@ function get_performance_info() { $row[] = ''; $row[] = ''; $row[] = ''; + $row[] = ''; } $text .= '} '; @@ -9675,11 +9694,11 @@ function get_performance_info() { // Now lets also show sub totals for each cache store. $storetotals = []; - $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0]; + $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; foreach ($stats as $definition => $details) { foreach ($details['stores'] as $store => $data) { if (!array_key_exists($store, $storetotals)) { - $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0]; + $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; } $storetotals[$store]['class'] = $data['class']; $storetotals[$store]['hits'] += $data['hits']; @@ -9688,14 +9707,18 @@ function get_performance_info() { $storetotal['hits'] += $data['hits']; $storetotal['misses'] += $data['misses']; $storetotal['sets'] += $data['sets']; + if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) { + $storetotals[$store]['iobytes'] += $data['iobytes']; + $storetotal['iobytes'] += $data['iobytes']; + } } } $table = new html_table(); $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; - $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S']; + $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O']; $table->data = []; - $table->align = ['left', 'left', 'right', 'right', 'right']; + $table->align = ['left', 'left', 'right', 'right', 'right', 'right']; ksort($storetotals); @@ -9723,14 +9746,34 @@ function get_performance_info() { $cell = new html_table_cell($data['sets']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; + if ($data['hits'] || $data['sets']) { + if ($data['iobytes']) { + $size = display_size($data['iobytes'], 1, 'KB'); + } else { + $size = '-'; + } + } else { + $size = ''; + } + $cell = new html_table_cell($size); + $cell->attributes = ['class' => $cachestoreclass]; + $row[] = $cell; $table->data[] = $row; } + if (!empty($storetotal['iobytes'])) { + $size = display_size($storetotal['iobytes'], 1, 'KB'); + } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) { + $size = '-'; + } else { + $size = ''; + } $row = [ get_string('total'), '', $storetotal['hits'], $storetotal['misses'], $storetotal['sets'], + $size, ]; $table->data[] = $row;