mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 00:42:54 +02:00
MDL-72619 core_cache: Provide admin page to view cache size estimates
This commit is contained in:
parent
b8291d04f4
commit
1b94bb8c20
@ -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) {
|
||||
|
24
cache/classes/administration_helper.php
vendored
24
cache/classes/administration_helper.php
vendored
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
134
cache/classes/output/renderer.php
vendored
134
cache/classes/output/renderer.php
vendored
@ -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);
|
||||
}
|
||||
}
|
||||
|
58
cache/classes/output/usage_samples_form.php
vendored
Normal file
58
cache/classes/output/usage_samples_form.php
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
}
|
117
cache/classes/store.php
vendored
117
cache/classes/store.php
vendored
@ -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.
|
||||
*
|
||||
|
53
cache/stores/file/lib.php
vendored
53
cache/stores/file/lib.php
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
23
cache/stores/redis/lib.php
vendored
23
cache/stores/redis/lib.php
vendored
@ -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;
|
||||
|
46
cache/templates/usage.mustache
vendored
Normal file
46
cache/templates/usage.mustache
vendored
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@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": ""
|
||||
}
|
||||
}}
|
||||
|
||||
<h2>{{# str }} cacheusage, cache {{/ str }}</h2>
|
||||
|
||||
<div class="mt-6 mb-6">
|
||||
<h3>{{# str }} cachestores, cache {{/ str }}</h3>
|
||||
{{{summarytable}}}
|
||||
</div>
|
||||
<div class="mt-6 mb-6">
|
||||
<h3>{{# str }} details {{/ str }}</h3>
|
||||
{{{maintable}}}
|
||||
</div>
|
||||
|
||||
{{{samplesform}}}
|
35
cache/tests/administration_helper_test.php
vendored
35
cache/tests/administration_helper_test.php
vendored
@ -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);
|
||||
}
|
||||
}
|
||||
|
35
cache/tests/behat/usage.feature
vendored
Normal file
35
cache/tests/behat/usage.feature
vendored
Normal file
@ -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
|
79
cache/tests/store_test.php
vendored
Normal file
79
cache/tests/store_test.php
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
5
cache/upgrade.txt
vendored
5
cache/upgrade.txt
vendored
@ -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
|
||||
|
50
cache/usage.php
vendored
Normal file
50
cache/usage.php
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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();
|
@ -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.';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user