Commit 31bd2006 authored by catch's avatar catch
Browse files

Issue #1944674 by damiankloip, dawehner: Improve performance of ViewsDataCache.

parent 8c21dcb6
......@@ -52,6 +52,8 @@ protected function setUp() {
'bundle' => 'contact_message',
);
field_create_instance($instance);
$this->container->get('views.views_data')->clear();
}
/**
......
......@@ -68,6 +68,8 @@ function setUp() {
);
$this->nodes[] = $this->drupalCreateNode($edit);
}
$this->container->get('views.views_data')->clear();
}
/**
......
......@@ -69,6 +69,8 @@ protected function setUp() {
$this->nodes[$i] = $this->drupalCreateNode($edit);
}
$this->container->get('views.views_data')->clear();
}
/**
......
......@@ -21,6 +21,13 @@ class ViewsDataTest extends ViewUnitTestBase {
*/
protected $viewsDataCache;
/**
* Stores a count for hook_views_data being invoked.
*
* @var int
*/
protected $count = 0;
public static function getInfo() {
return array(
'name' => 'Table Data',
......@@ -33,6 +40,7 @@ protected function setUp() {
parent::setUp();
$this->viewsDataCache = $this->container->get('views.views_data');
$this->state = $this->container->get('state');
}
/**
......@@ -42,35 +50,98 @@ protected function setUp() {
*/
public function testViewsFetchData() {
$table_name = 'views_test_data';
$expected_data = $this->viewsData();
$data = $this->viewsDataCache->get($table_name);
$this->assertEqual($data, $expected_data[$table_name], 'Make sure fetching views data by table works as expected.');
$data = $this->viewsDataCache->get($this->randomName());
$this->assertTrue(empty($data), 'Make sure fetching views data for an invalid table returns empty.');
$data = $this->viewsDataCache->get();
$this->assertTrue(isset($data[$table_name]), 'Make sure the views_test_data info appears in the total views data.');
$this->assertEqual($data[$table_name], $expected_data[$table_name], 'Make sure the views_test_data has the expected values.');
// Verify that views_test_data_views_data() has only been called once.
$state = \Drupal::service('state');
$count = $state->get('views_test_data_views_data_count');
$random_table_name = $this->randomName();
// Invoke expected data directly from hook_views_data implementations.
$expected_data = $this->container->get('module_handler')->invokeAll('views_data');
// Verify that views_test_data_views_data() has only been called once after
// calling clear().
$this->startCount();
$this->viewsDataCache->get();
// Test views data has been invoked.
$this->assertCountIncrement();
// Clear the storage/cache.
$this->viewsDataCache->clear();
// Get the data again.
$this->viewsDataCache->get();
$this->viewsDataCache->get($table_name);
// Verify that view_test_data_views_data() has run again.
$this->assertEqual($count + 1, $state->get('views_test_data_views_data_count'));
$this->viewsDataCache->get($random_table_name);
// Verify that view_test_data_views_data() has run once.
$this->assertCountIncrement();
// Get the data again.
$this->viewsDataCache->get($table_name);
// Also request all table data.
$this->viewsDataCache->get();
$this->viewsDataCache->get($table_name);
$this->viewsDataCache->get($random_table_name);
// Verify that view_test_data_views_data() has not run again.
$this->assertEqual($count + 1, $state->get('views_test_data_views_data_count'));
$this->assertCountIncrement(FALSE);
// Clear the views data, and test all table data.
$this->viewsDataCache->clear();
$this->startCount();
$data = $this->viewsDataCache->get();
$this->assertEqual($data, $expected_data, 'Make sure fetching all views data by works as expected.');
// Views data should be invoked once.
$this->assertCountIncrement();
// Calling get() again, the count for this table should stay the same.
$data = $this->viewsDataCache->get();
$this->assertEqual($data, $expected_data, 'Make sure fetching all cached views data works as expected.');
$this->assertCountIncrement(FALSE);
// Clear the views data, and test data for a specific table.
$this->viewsDataCache->clear();
$this->startCount();
$data = $this->viewsDataCache->get($table_name);
$this->assertEqual($data, $expected_data[$table_name], 'Make sure fetching views data by table works as expected.');
// Views data should be invoked once.
$this->assertCountIncrement();
// Calling get() again, the count for this table should stay the same.
$data = $this->viewsDataCache->get($table_name);
$this->assertEqual($data, $expected_data[$table_name], 'Make sure fetching cached views data by table works as expected.');
$this->assertCountIncrement(FALSE);
// Test that this data is present if all views data is returned.
$data = $this->viewsDataCache->get();
$this->assertTrue(isset($data[$table_name]), 'Make sure the views_test_data info appears in the total views data.');
$this->assertEqual($data[$table_name], $expected_data[$table_name], 'Make sure the views_test_data has the expected values.');
// Clear the views data, and test data for an invalid table.
$this->viewsDataCache->clear();
$this->startCount();
// All views data should be requested on the first try.
$data = $this->viewsDataCache->get($random_table_name);
$this->assertEqual($data, array(), 'Make sure fetching views data for an invalid table returns an empty array.');
$this->assertCountIncrement();
// Test no data is rebuilt when requesting an invalid table again.
$data = $this->viewsDataCache->get($random_table_name);
$this->assertEqual($data, array(), 'Make sure fetching views data for an invalid table returns an empty array.');
$this->assertCountIncrement(FALSE);
}
/**
* Starts a count for hook_views_data being invoked.
*/
protected function startCount() {
$count = $this->state->get('views_test_data_views_data_count');
$this->count = isset($count) ? $count : 0;
}
/**
* Asserts that the count for hook_views_data either equal or has increased.
*
* @param bool $equal
* Whether to assert that the count should be equal. Defaults to FALSE.
*/
protected function assertCountIncrement($increment = TRUE) {
if ($increment) {
// If an incremented count is expected, increment this now.
$this->count++;
$message = 'hook_views_data has been invoked.';
}
else {
$message = 'hook_views_data has not been invoked';
}
$this->assertEqual($this->count, $this->state->get('views_test_data_views_data_count'), $message);
}
/**
......
......@@ -38,111 +38,98 @@ class ViewsDataCache implements DestructableInterface {
protected $storage = array();
/**
* The configuration factory object.
* An array of requested tables.
*
* @var \Drupal\Core\Config\ConfigFactory
* @var array
*/
protected $config;
protected $requestedTables = array();
/**
* The current language code.
* Whether the data has been fully loaded in this request.
*
* @var string
* @var bool
*/
protected $langcode;
protected $fullyLoaded = FALSE;
/**
* Whether the data has been fully loaded in this request.
* Whether views data has been rebuilt. This is set when getData() doesn't
* return anything from cache.
*
* @var bool
*/
protected $fullyLoaded = FALSE;
protected $rebuildAll = FALSE;
/**
* Whether or not to skip data caching and rebuild data each time.
*
* @var bool
*/
protected $skipCache;
protected $skipCache = FALSE;
/**
* Whether the cache should be rebuilt. This is set when getData() is called.
* The current language code.
*
* @var bool
* @var string
*/
protected $rebuildCache;
protected $langcode;
public function __construct(CacheBackendInterface $cache_backend, ConfigFactory $config) {
$this->config = $config;
$this->cacheBackend = $cache_backend;
$this->langcode = language(LANGUAGE_TYPE_INTERFACE)->langcode;
$this->skipCache = $this->config->get('views.settings')->get('skip_cache');
$this->skipCache = $config->get('views.settings')->get('skip_cache');
}
/**
* Gets cached data for a particular key, or rebuilds if necessary.
* Gets data for a particular table, or all tables.
*
* @param string|null $key
* The key of the cache entry to retrieve. Defaults to NULL.
* The key of the cache entry to retrieve. Defaults to NULL, this will
* return all table data.
*
* @return array $data
* The cached data.
* An array of table data.
*/
public function get($key = NULL) {
if ($key) {
$from_cache = FALSE;
if (!isset($this->storage[$key])) {
// Prepare a cache ID.
$cid = $this->baseCid . ':' . $key;
$data = $this->cacheGet($cid);
if (!empty($data->data)) {
if ($data = $this->cacheGet($cid)) {
$this->storage[$key] = $data->data;
$from_cache = TRUE;
}
else {
// No cache entry, rebuild.
// If there is no cached entry and data is not already fully loaded,
// rebuild. This will stop requests for invalid tables calling getData.
elseif (!$this->fullyLoaded) {
$this->storage = $this->getData();
$this->fullyLoaded = TRUE;
}
}
if (isset($this->storage[$key])) {
if (!$from_cache) {
// Add this table to a list of requested tables, as it's table cache
// entry was not found.
array_push($this->requestedTables, $key);
}
return $this->storage[$key];
}
// If the key is invalid, return an empty array.
return array();
}
else {
if (!$this->fullyLoaded) {
$data = $this->cacheGet($this->baseCid);
if (!empty($data->data)) {
$this->storage = $data->data;
}
else {
$this->storage = $this->getData();
}
$this->fullyLoaded = TRUE;
$this->storage = $this->getData();
}
}
return $this->storage;
}
/**
* Sets the data in the cache backend for a cache key.
*
* @param string $key
* The cache key to set.
* @param mixed $value
* The value to set for this key.
*/
public function set($key, $value) {
if ($this->skipCache) {
return FALSE;
}
$key .= ':' . $this->langcode;
$this->cacheBackend->set($key, $value);
}
/**
* Gets data from the cache backend.
*
......@@ -158,26 +145,47 @@ protected function cacheGet($cid) {
return FALSE;
}
$cid .= ':' . $this->langcode;
return $this->cacheBackend->get($this->prepareCid($cid));
}
return $this->cacheBackend->get($cid);
/**
* Prepares the cache ID by appending a language code.
*
* @param string $cid
* The cache ID to prepare.
*
* @return string
* The prepared cache ID.
*/
protected function prepareCid($cid) {
return $cid . ':' . $this->langcode;
}
/**
* Gets all data invoked by hook_views_data().
*
* This is requested from the cache before being rebuilt.
*
* @return array
* An array of all data.
*/
protected function getData() {
$data = module_invoke_all('views_data');
drupal_alter('views_data', $data);
$this->fullyLoaded = TRUE;
$this->processEntityTypes($data);
if ($data = $this->cacheGet($this->baseCid)) {
return $data->data;
}
else {
$data = module_invoke_all('views_data');
drupal_alter('views_data', $data);
$this->processEntityTypes($data);
$this->rebuildCache = TRUE;
// Set as TRUE, so all table data will be cached.
$this->rebuildAll = TRUE;
return $data;
return $data;
}
}
/**
......@@ -245,13 +253,16 @@ public function fetchBaseTables() {
* Implements \Drupal\Core\DestructableInterface::destruct().
*/
public function destruct() {
if ($this->rebuildCache && !empty($this->storage)) {
// Keep a record with all data.
$this->set($this->baseCid, $this->storage);
// Save data in seperate cache entries.
foreach ($this->storage as $table => $data) {
if (!empty($this->storage) && !$this->skipCache) {
if ($this->rebuildAll) {
// Keep a record with all data.
$this->cacheBackend->set($this->prepareCid($this->baseCid), $this->storage);
}
// Save data in seperate, per table cache entries.
foreach ($this->requestedTables as $table) {
$cid = $this->baseCid . ':' . $table;
$this->set($cid, $data);
$this->cacheBackend->set($this->prepareCid($cid), $this->storage[$table]);
}
}
}
......@@ -261,6 +272,7 @@ public function destruct() {
*/
public function clear() {
$this->storage = array();
$this->fullyLoaded = FALSE;
$this->cacheBackend->deleteAll();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment