diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 53436d2cd1e..c9d71d0a9d6 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -708,6 +708,8 @@ if ($hassiteconfig) { $ADMIN->add('modules', new admin_category('cache', new lang_string('caching', 'cache'))); $ADMIN->add('cache', new admin_externalpage('cacheconfig', new lang_string('cacheconfig', 'cache'), $CFG->wwwroot .'/cache/admin.php')); $ADMIN->add('cache', new admin_externalpage('cachetestperformance', new lang_string('testperformance', 'cache'), $CFG->wwwroot . '/cache/testperformance.php')); + $ADMIN->add('cache', new admin_externalpage('cacheusage', + new lang_string('cacheusage', 'cache'), $CFG->wwwroot . '/cache/usage.php')); $ADMIN->add('cache', new admin_category('cachestores', new lang_string('cachestores', 'cache'))); $ADMIN->locate('cachestores')->set_sorting(true); foreach (core_component::get_plugin_list('cachestore') as $plugin => $path) { diff --git a/cache/classes/administration_helper.php b/cache/classes/administration_helper.php index 2ebd3dc773c..6006af43332 100644 --- a/cache/classes/administration_helper.php +++ b/cache/classes/administration_helper.php @@ -404,4 +404,28 @@ abstract class administration_helper extends cache_helper { * @return string the HTML for the page. */ abstract public function generate_admin_page(\core_cache\output\renderer $renderer): string; + + /** + * Gets usage information about the whole cache system. + * + * This is a slow function and should only be used on an admin information page. + * + * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For + * each store, the following fields are available: + * + * - name (store name) + * - class (e.g. cachestore_redis) + * - supported (true if we have any information) + * - items (number of items stored) + * - mean (mean size of item) + * - sd (standard deviation for item sizes) + * - margin (margin of error for mean at 95% confidence) + * - storetotal (total usage for store if known, otherwise null) + * + * The storetotal field will be the same for every cache that uses the same store. + * + * @param int $samplekeys Number of keys to sample when checking size of large caches + * @return array Details of cache usage + */ + abstract public function get_usage(int $samplekeys): array; } diff --git a/cache/classes/local/administration_display_helper.php b/cache/classes/local/administration_display_helper.php index 84fa0b6a804..4ddad4c1357 100644 --- a/cache/classes/local/administration_display_helper.php +++ b/cache/classes/local/administration_display_helper.php @@ -792,4 +792,87 @@ class administration_display_helper extends \core_cache\administration_helper { return $html; } + + /** + * Gets usage information about the whole cache system. + * + * This is a slow function and should only be used on an admin information page. + * + * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For + * each store, the following fields are available: + * + * - name (store name) + * - class (e.g. cachestore_redis) + * - supported (true if we have any information) + * - items (number of items stored) + * - mean (mean size of item) + * - sd (standard deviation for item sizes) + * - margin (margin of error for mean at 95% confidence) + * - storetotal (total usage for store if known, otherwise null) + * + * The storetotal field will be the same for every cache that uses the same store. + * + * @param int $samplekeys Number of keys to sample when checking size of large caches + * @return array Details of cache usage + */ + public function get_usage(int $samplekeys): array { + $results = []; + + $factory = cache_factory::instance(); + + // Check the caches we already have an instance of, so we don't make another one... + $got = $factory->get_caches_in_use(); + $gotid = []; + foreach ($got as $longid => $unused) { + // The IDs here can be of the form cacheid/morestuff if there are parameters in the + // cache. Any entry for a cacheid is good enough to consider that we don't need to make + // another entry ourselves, so we remove the extra bits and track the basic cache id. + $gotid[preg_replace('~^([^/]+/[^/]+)/.*$~', '$1', $longid)] = true; + } + + $storetotals = []; + + $config = $factory->create_config_instance(); + foreach ($config->get_definitions() as $configdetails) { + if (!array_key_exists($configdetails['component'] . '/' . $configdetails['area'], $gotid)) { + // Where possible (if it doesn't need identifiers), make an instance of the cache, otherwise + // we can't get the store instances for it (and it won't show up in the list). + if (empty($configdetails['requireidentifiers'])) { + \cache::make($configdetails['component'], $configdetails['area']); + } + } + $definition = $factory->create_definition($configdetails['component'], $configdetails['area']); + $stores = $factory->get_store_instances_in_use($definition); + + // Create object for results about this cache definition. + $currentresult = (object)['cacheid' => $definition->get_id(), 'stores' => []]; + $results[$currentresult->cacheid] = $currentresult; + + /** @var cache_store $store */ + foreach ($stores as $store) { + // Skip static cache. + if ($store instanceof \cachestore_static) { + continue; + } + + // Get cache size details from store. + $currentstore = $store->cache_size_details($samplekeys); + + // Add in basic information about store. + $currentstore->name = $store->my_name(); + $currentstore->class = get_class($store); + + // Add in store total. + if (!array_key_exists($currentstore->name, $storetotals)) { + $storetotals[$currentstore->name] = $store->store_total_size(); + } + $currentstore->storetotal = $storetotals[$currentstore->name]; + + $currentresult->stores[] = $currentstore; + } + } + + ksort($results); + return $results; + } } diff --git a/cache/classes/output/renderer.php b/cache/classes/output/renderer.php index 5dcd67fd640..4a524fda645 100644 --- a/cache/classes/output/renderer.php +++ b/cache/classes/output/renderer.php @@ -413,4 +413,138 @@ class renderer extends \plugin_renderer_base { $html .= html_writer::end_div(); return $html; } + + /** + * Creates the two tables which display on the usage page. + * + * @param array $usage Usage information (from cache_helper::usage) + * @return array Array of 2 tables (main and summary table) + * @throws \coding_exception + */ + public function usage_tables(array $usage): array { + $table = new \html_table(); + $table->id = 'usage_main'; + $table->head = [ + get_string('definition', 'cache'), + get_string('storename', 'cache'), + get_string('plugin', 'cache'), + get_string('usage_items', 'cache'), + get_string('usage_mean', 'cache'), + get_string('usage_sd', 'cache'), + get_string('usage_total', 'cache'), + get_string('usage_totalmargin', 'cache')]; + $table->align = [ + 'left', 'left', 'left', + 'right', 'right', 'right', 'right', 'right' + ]; + $table->data = []; + + $summarytable = new \html_table(); + $summarytable->id = 'usage_summary'; + $summarytable->head = [ + get_string('storename', 'cache'), + get_string('plugin', 'cache'), + get_string('usage_total', 'cache'), + get_string('usage_realtotal', 'cache') + ]; + $summarytable->align = [ + 'left', 'left', + 'right', 'right', + ]; + $summarytable->data = []; + $summarytable->attributes['class'] = 'generaltable w-auto'; + $storetotals = []; + + // We will highlight all cells that are more than 2% of total size, so work that out first. + $total = 0; + foreach ($usage as $definition) { + foreach ($definition->stores as $storedata) { + $total += $storedata->items * $storedata->mean; + } + } + $highlightover = round($total / 50); + + foreach ($usage as $definition) { + foreach ($definition->stores as $storedata) { + $row = []; + $row[] = s($definition->cacheid); + $row[] = s($storedata->name); + $row[] = s($storedata->class); + if (!$storedata->supported) { + // We don't have data for this store because it isn't searchable. + $row[] = '-'; + } else { + $row[] = $storedata->items; + } + if ($storedata->items) { + $row[] = display_size(round($storedata->mean)); + if ($storedata->items > 1) { + $row[] = display_size(round($storedata->sd)); + } else { + $row[] = ''; + } + $cellsize = round($storedata->items * $storedata->mean); + $row[] = display_size($cellsize, 1, 'MB'); + + if (!array_key_exists($storedata->name, $storetotals)) { + $storetotals[$storedata->name] = (object)[ + 'plugin' => $storedata->class, + 'total' => 0, + 'storetotal' => $storedata->storetotal, + ]; + } + $storetotals[$storedata->name]->total += $cellsize; + } else { + $row[] = ''; + $row[] = ''; + $cellsize = 0; + $row[] = ''; + } + if ($storedata->margin) { + // Plus or minus. + $row[] = '±' . display_size($storedata->margin * $storedata->items, 1, 'MB'); + } else { + $row[] = ''; + } + $htmlrow = new \html_table_row($row); + if ($cellsize > $highlightover) { + $htmlrow->attributes = ['class' => 'table-warning']; + } + $table->data[] = $htmlrow; + } + } + + ksort($storetotals); + + foreach ($storetotals as $storename => $storedetails) { + $row = [s($storename), s($storedetails->plugin)]; + $row[] = display_size($storedetails->total, 1, 'MB'); + if ($storedetails->storetotal !== null) { + $row[] = display_size($storedetails->storetotal, 1, 'MB'); + } else { + $row[] = '-'; + } + $summarytable->data[] = $row; + } + + return [$table, $summarytable]; + } + + /** + * Renders the usage page. + * + * @param \html_table $maintable Main table + * @param \html_table $summarytable Summary table + * @param \moodleform $samplesform Form to select number of samples + * @return string HTML for page + */ + public function usage_page(\html_table $maintable, \html_table $summarytable, \moodleform $samplesform): string { + $data = [ + 'maintable' => \html_writer::table($maintable), + 'summarytable' => \html_writer::table($summarytable), + 'samplesform' => $samplesform->render() + ]; + + return $this->render_from_template('core_cache/usage', $data); + } } diff --git a/cache/classes/output/usage_samples_form.php b/cache/classes/output/usage_samples_form.php new file mode 100644 index 00000000000..d13cdcdfb9c --- /dev/null +++ b/cache/classes/output/usage_samples_form.php @@ -0,0 +1,58 @@ +. + +/** + * Form for usage page to select number of samples. + * + * @package core_cache + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_cache\output; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Form for usage page to select number of samples. + * + * @package core_cache + */ +class usage_samples_form extends \moodleform { + /** + * Constructor sets form up to use GET request to current page. + */ + public function __construct() { + parent::__construct(null, null, 'get'); + } + + /** + * Adds controls to form. + */ + protected function definition() { + $mform = $this->_form; + + $radioarray = []; + foreach ([50, 100, 200, 500, 1000] as $samples) { + $radioarray[] = $mform->createElement('radio', 'samples', '', $samples, $samples); + } + $mform->setDefault('samples', 50); + $mform->addGroup($radioarray, 'samplesradios', get_string('usage_samples', 'cache'), [' '], false); + $mform->addElement('submit', 'submit', get_string('update')); + } +} diff --git a/cache/classes/store.php b/cache/classes/store.php index a2cfe3e6dce..6ed6c5e3aba 100644 --- a/cache/classes/store.php +++ b/cache/classes/store.php @@ -381,6 +381,123 @@ abstract class cache_store implements cache_store_interface { return array(); } + /** + * Estimates the storage size used within this cache if the given value is stored with the + * given key. + * + * This function is not exactly accurate; it does not necessarily take into account all the + * overheads involved. It is only intended to give a good idea of the relative size of + * different caches. + * + * The default implementation serializes both key and value and sums the lengths (as a rough + * estimate which is probably good enough for everything unless the cache offers compression). + * + * @param mixed $key Key + * @param mixed $value Value + * @return int Size in bytes + */ + public function estimate_stored_size($key, $value): int { + return strlen(serialize($key)) + strlen(serialize($value)); + } + + /** + * Gets the amount of memory/storage currently used by this cache store if known. + * + * This value should be obtained quickly from the store itself, if available. + * + * This is the total memory usage of the entire store, not for ther specific cache in question. + * + * Where not supported (default), will always return null. + * + * @return int|null Amount of memory used in bytes or null + */ + public function store_total_size(): ?int { + return null; + } + + /** + * Gets the amount of memory used by this specific cache within the store, if known. + * + * This function may be slow and should not be called in normal usage, only for administration + * pages. The value is usually an estimate, and may not be available at all. + * + * When estimating, a number of sample items will be used for the estimate. If set to 50 + * (default), then this function will retrieve 50 random items and use that to estimate the + * total size. + * + * The return value has the following fields: + * - supported (true if any other values are completed) + * - items (number of items) + * - mean (mean size of one item in bytes) + * - sd (standard deviation of item size in bytes, based on sample) + * - margin (95% confidence margin for mean - will be 0 if exactly computed) + * + * @param int $samplekeys Number of samples to use + * @return stdClass Object with information about the store size + */ + public function cache_size_details(int $samplekeys = 50): stdClass { + $result = (object)[ + 'supported' => false, + 'items' => 0, + 'mean' => 0, + 'sd' => 0, + 'margin' => 0 + ]; + + // If this cache isn't searchable, we don't know the answer. + if (!$this->is_searchable()) { + return $result; + } + $result->supported = true; + + // Get all the keys for the cache. + $keys = $this->find_all(); + $result->items = count($keys); + + // Don't do anything else if there are no items. + if ($result->items === 0) { + return $result; + } + + // Select N random keys. + $exact = false; + if ($result->items <= $samplekeys) { + $samples = $keys; + $exact = true; + } else { + $indexes = array_rand($keys, $samplekeys); + $samples = []; + foreach ($indexes as $index) { + $samples[] = $keys[$index]; + } + } + + // Get the random items from cache and estimate the size of each. + $sizes = []; + foreach ($samples as $samplekey) { + $value = $this->get($samplekey); + $sizes[] = $this->estimate_stored_size($samplekey, $value); + } + $number = count($sizes); + + // Calculate the mean and standard deviation. + $result->mean = array_sum($sizes) / $number; + $squarediff = 0; + foreach ($sizes as $size) { + $squarediff += ($size - $result->mean) ** 2; + } + $squarediff /= $number; + $result->sd = sqrt($squarediff); + + // If it's not exact, also calculate the confidence interval. + if (!$exact) { + // 95% confidence has a Z value of 1.96. + $result->margin = (1.96 * $result->sd) / sqrt($number); + } + + return $result; + } + /** * Returns true if this cache store instance is both suitable for testing, and ready for testing. * diff --git a/cache/stores/file/lib.php b/cache/stores/file/lib.php index 0c08710d551..c8c90a8b442 100644 --- a/cache/stores/file/lib.php +++ b/cache/stores/file/lib.php @@ -784,4 +784,57 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i } return $return; } + + /** + * Gets total size for the directory used by the cache store. + * + * @return int Total size in bytes + */ + public function store_total_size(): ?int { + return get_directory_size($this->filestorepath); + } + + /** + * Gets total size for a specific cache. + * + * With the file cache we can just look at the directory listing without having to + * actually load any files, so the $samplekeys parameter is ignored. + * + * @param int $samplekeys Unused + * @return stdClass Cache details + */ + public function cache_size_details(int $samplekeys = 50): stdClass { + $result = (object)[ + 'supported' => true, + 'items' => 0, + 'mean' => 0, + 'sd' => 0, + 'margin' => 0 + ]; + + // Find all the files in this cache. + $this->ensure_path_exists(); + $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); + if ($files === false || count($files) === 0) { + return $result; + } + + // Get the sizes and count of files. + $sizes = []; + foreach ($files as $file) { + $result->items++; + $sizes[] = filesize($file); + } + + // Work out mean and standard deviation. + $total = array_sum($sizes); + $result->mean = $total / $result->items; + $squarediff = 0; + foreach ($sizes as $size) { + $squarediff += ($size - $result->mean) ** 2; + } + $squarediff /= $result->items; + $result->sd = sqrt($squarediff); + return $result; + } } diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index 796370c3168..5045907f005 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -572,7 +572,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ $count = 0; $batches = 0; $timebefore = microtime(true); - $memorybefore = $this->get_used_memory(); + $memorybefore = $this->store_total_size(); do { $keys = $this->redis->zRangeByScore($this->hash . self::TTL_SUFFIX, 0, $limit, ['limit' => [0, self::TTL_EXPIRE_BATCH]]); @@ -580,7 +580,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ $count += count($keys); $batches++; } while (count($keys) === self::TTL_EXPIRE_BATCH); - $memoryafter = $this->get_used_memory(); + $memoryafter = $this->store_total_size(); $timeafter = microtime(true); $result = ['keys' => $count, 'batches' => $batches, 'time' => $timeafter - $timebefore]; @@ -623,12 +623,29 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_ } } + /** + * Estimates the stored size, taking into account whether compression is turned on. + * + * @param mixed $key Key name + * @param mixed $value Value + * @return int Approximate stored size + */ + public function estimate_stored_size($key, $value): int { + if ($this->compressor == self::COMPRESSOR_NONE) { + // If uncompressed, use default estimate. + return parent::estimate_stored_size($key, $value); + } else { + // If compressed, compress value. + return strlen($this->serialize($key)) + strlen($this->compress($value)); + } + } + /** * Gets Redis reported memory usage. * * @return int|null Memory used by Redis or null if we don't know */ - protected function get_used_memory(): ?int { + public function store_total_size(): ?int { $details = $this->redis->info('MEMORY'); if (empty($details['used_memory'])) { return null; diff --git a/cache/templates/usage.mustache b/cache/templates/usage.mustache new file mode 100644 index 00000000000..88a5b8facac --- /dev/null +++ b/cache/templates/usage.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_cache/usage + + The cache usage page + + Context variables required for this template: + * maintable - Main table HTML + * summarytable - Summary table HTML + * samplesform - Form to choose number of samples + + Example context (json): + { + "maintable": "", + "summarytable": "", + "samplesform": "" + } +}} + +

{{# str }} cacheusage, cache {{/ str }}

+ +
+

{{# str }} cachestores, cache {{/ str }}

+ {{{summarytable}}} +
+
+

{{# str }} details {{/ str }}

+ {{{maintable}}} +
+ +{{{samplesform}}} diff --git a/cache/tests/administration_helper_test.php b/cache/tests/administration_helper_test.php index 39418717000..b3a2e2d03d5 100644 --- a/cache/tests/administration_helper_test.php +++ b/cache/tests/administration_helper_test.php @@ -235,4 +235,39 @@ class core_cache_administration_helper_testcase extends advanced_testcase { $result = cache_helper::hash_key('test/test', $definition); $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test/test'), $result); } + + /** + * Tests the get_usage function. + */ + public function test_get_usage(): void { + // Create a test cache definition and put items in it. + $instance = cache_config_testing::instance(true); + $instance->phpunit_add_definition('phpunit/test', [ + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'test', + 'simplekeys' => true + ]); + $cache = cache::make('phpunit', 'test'); + for ($i = 0; $i < 100; $i++) { + $cache->set('key' . $i, str_repeat('x', $i)); + } + + $factory = cache_factory::instance(); + $adminhelper = $factory->get_administration_display_helper(); + + $usage = $adminhelper->get_usage(10)['phpunit/test']; + $this->assertEquals('phpunit/test', $usage->cacheid); + $this->assertCount(1, $usage->stores); + $store = $usage->stores[0]; + $this->assertEquals('default_application', $store->name); + $this->assertEquals('cachestore_file', $store->class); + $this->assertEquals(true, $store->supported); + $this->assertEquals(100, $store->items); + + // As file store checks all items, the values should be exact. + $this->assertEqualsWithDelta(57.4, $store->mean, 0.1); + $this->assertEqualsWithDelta(29.0, $store->sd, 0.1); + $this->assertEquals(0, $store->margin); + } } diff --git a/cache/tests/behat/usage.feature b/cache/tests/behat/usage.feature new file mode 100644 index 00000000000..645d6ead91d --- /dev/null +++ b/cache/tests/behat/usage.feature @@ -0,0 +1,35 @@ +@core @core_cache +Feature: Display usage information for cache + In order to investigate performance problems with caching + As an administrator + I need to be able to monitor the size of items in the cache + + Background: + Given 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" + And I navigate to "Plugins > Caching > Cache usage" in site administration + + # Check one row of the summary table. The actual total is currently 3.6MB so it's likely to + # continue to be in the MB range. + Then "default_application" row "Plugin" column of "usage_summary" table should contain "cachestore_file" + And "default_application" row "Estimated total" column of "usage_summary" table should contain "MB" + And "default_application" row "Actual usage (if known)" column of "usage_summary" table should contain "MB" + + # And one row of the main table. The totals are fixed to use the MB unit. + And "core/config" row "Store name" column of "usage_main" table should contain "default_application" + And "core/config" row "Plugin" column of "usage_main" table should contain "cachestore_file" + And "core/config" row "Estimated total" column of "usage_main" table should contain "MB" + + Scenario: Sample option works + When I am on the "C1" "Course" page logged in as "admin" + And I navigate to "Plugins > Caching > Cache usage" in site administration + And I set the field "samples" to "1000" + And I press "Update" + + Then the field "samples" matches value "1000" + And "usage_summary" "table" should exist + And "usage_main" "table" should exist diff --git a/cache/tests/store_test.php b/cache/tests/store_test.php new file mode 100644 index 00000000000..34e9169b6c6 --- /dev/null +++ b/cache/tests/store_test.php @@ -0,0 +1,79 @@ +. + +namespace core_cache; + +/** + * Unit tests for cache_store functionality. + * + * @package core_cache + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class store_test extends \advanced_testcase { + + /** + * Tests the default implementation of cache_size_details, which does some + * complicated statistics. + */ + public function test_cache_size_details(): void { + // Fill a store with 100 entries of varying size. + $store = self::create_static_store(); + for ($i = 0; $i < 100; $i++) { + $store->set('key' . $i, str_repeat('x', $i)); + } + + // Do the statistics for 10 random picks. + $details = $store->cache_size_details(10); + $this->assertTrue($details->supported); + $this->assertEquals(100, $details->items); + + // Min/max possible means if it picks the smallest/largest 10. + $this->assertGreaterThan(22, $details->mean); + $this->assertLessThan(115, $details->mean); + // Min/max possible SD. + $this->assertLessThan(49, $details->sd); + $this->assertGreaterThan(2.8, $details->sd); + // Lowest possible confidence margin is about 1.74. + $this->assertGreaterThan(1.7, $details->margin); + + // Repeat the statistics for a pick of all 100 entries (exact). + $details = $store->cache_size_details(100); + $this->assertTrue($details->supported); + $this->assertEquals(100, $details->items); + $this->assertEqualsWithDelta(69.3, $details->mean, 0.1); + $this->assertEqualsWithDelta(29.2, $details->sd, 0.1); + $this->assertEquals(0, $details->margin); + } + + /** + * Creates a static store for testing. + * + * @return \cachestore_static Store + */ + protected static function create_static_store(): \cachestore_static { + require_once(__DIR__ . '/../stores/static/lib.php'); + $store = new \cachestore_static('frog'); + $definition = \cache_definition::load('zombie', [ + 'mode' => \cache_store::MODE_REQUEST, + 'component' => 'phpunit', + 'area' => 'store_test', + 'simplekeys' => true + ]); + $store->initialise($definition); + return $store; + } +} diff --git a/cache/upgrade.txt b/cache/upgrade.txt index 1890d306076..04e7225544c 100644 --- a/cache/upgrade.txt +++ b/cache/upgrade.txt @@ -1,6 +1,11 @@ This files describes API changes in /cache/stores/* - cache store plugins. Information provided here is intended especially for developers. +=== 4.0 === +* The cache_store class now has functions cache_size_details(), store_total_size(), and + estimate_stored_size(), related to size used by the cache. These can be overridden by a cache + store to provide better information for the new cache usage admin page. + === 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/cache/usage.php b/cache/usage.php new file mode 100644 index 00000000000..6309e901589 --- /dev/null +++ b/cache/usage.php @@ -0,0 +1,50 @@ +. + +/** + * Show current cache usage (number of items, size of caches). + * + * @package core_cache + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); + +require_once($CFG->dirroot . '/lib/adminlib.php'); + +admin_externalpage_setup('cacheusage'); +$adminhelper = cache_factory::instance()->get_administration_display_helper(); +raise_memory_limit(MEMORY_EXTRA); + +$samples = optional_param('samples', 50, PARAM_INT); + +// Just for safety reasons, stop people choosing a stupid number. +if ($samples > 1000) { + $samples = 1000; +} + +// Get the actual data. +$usage = $adminhelper->get_usage($samples); + +// Set up the renderer and organise data to render. +$renderer = $PAGE->get_renderer('core_cache'); +[$table, $summarytable] = $renderer->usage_tables($usage); +$form = new \core_cache\output\usage_samples_form(); + +echo $OUTPUT->header(); +echo $renderer->usage_page($table, $summarytable, $form); +echo $OUTPUT->footer(); diff --git a/lang/en/cache.php b/lang/en/cache.php index d4eac728c9e..e87e5c11a3d 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -93,6 +93,7 @@ $string['cachedef_yuimodules'] = 'YUI Module definitions'; $string['cachedef_gradesetting'] = 'Course grade setting'; $string['cachelock_file_default'] = 'Default file locking'; $string['cachestores'] = 'Cache stores'; +$string['cacheusage'] = 'Cache usage'; $string['canuselocalstore'] = 'Can use local store'; $string['component'] = 'Component'; $string['confirmlockdeletion'] = 'Confirm lock deletion'; @@ -203,6 +204,13 @@ $string['tested'] = 'Tested'; $string['testperformance'] = 'Test performance'; $string['unsupportedmode'] = 'Unsupported mode'; $string['untestable'] = 'Untestable'; +$string['usage_items'] = 'Items'; +$string['usage_mean'] = 'Mean item size'; +$string['usage_samples'] = 'Items sampled per cache'; +$string['usage_sd'] = 'Std. dev.'; +$string['usage_total'] = 'Estimated total'; +$string['usage_totalmargin'] = 'Error margin (95%)'; +$string['usage_realtotal'] = 'Actual usage (if known)'; $string['userinputsharingkey'] = 'Custom key for sharing'; $string['userinputsharingkey_help'] = 'Enter your own private key here. When you set up other stores on other sites you wish to share data with make sure you set the exact same key there.';