Commit e3615997 authored by alexpott's avatar alexpott

Issue #2395143 by amateescu, Fabianx, Berdir, beejeebus, dashaforbes,...

Issue #2395143 by amateescu, Fabianx, Berdir, beejeebus, dashaforbes, alexpott, larowlan, znerol, dawehner, catch, neclimdul, yched, fgm, effulgentsia: YAML parsing is very slow, cache it with FileCache
parent 038dee46
......@@ -8,6 +8,7 @@
namespace Drupal\Component\Discovery;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\FileCache\FileCacheFactory;
/**
* Provides discovery for YAML files within a given set of directories.
......@@ -47,10 +48,27 @@ public function __construct($name, array $directories) {
*/
public function findAll() {
$all = array();
foreach ($this->findFiles() as $provider => $file) {
// If a file is empty or its contents are commented out, return an empty
// array instead of NULL for type consistency.
$all[$provider] = Yaml::decode(file_get_contents($file)) ?: [];
$files = $this->findFiles();
$provider_by_files = array_flip($files);
$file_cache = FileCacheFactory::get('yaml_discovery:' . $this->name);
// Try to load from the file cache first.
foreach ($file_cache->getMultiple($files) as $file => $data) {
$all[$provider_by_files[$file]] = $data;
unset($provider_by_files[$file]);
}
// If there are files left that were not returned from the cache, load and
// parse them now. This list was flipped above and is keyed by filename.
if ($provider_by_files) {
foreach ($provider_by_files as $file => $provider) {
// If a file is empty or its contents are commented out, return an empty
// array instead of NULL for type consistency.
$all[$provider] = Yaml::decode(file_get_contents($file)) ?: [];
$file_cache->set($file, $all[$provider]);
}
}
return $all;
......
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\ApcuFileCacheBackend.
*/
namespace Drupal\Component\FileCache;
/**
* APCu backend for the file cache.
*/
class ApcuFileCacheBackend implements FileCacheBackendInterface {
/**
* {@inheritdoc}
*/
public function fetch(array $cids) {
return apc_fetch($cids);
}
/**
* {@inheritdoc}
*/
public function store($cid, $data) {
apc_store($cid, $data);
}
/**
* {@inheritdoc}
*/
public function delete($cid) {
apc_delete($cid);
}
}
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCache.
*/
namespace Drupal\Component\FileCache;
/**
* Allows to cache data based on file modification dates.
*/
class FileCache implements FileCacheInterface {
/**
* Prefix that is used for cache entries.
*
* @var string
*/
protected $prefix;
/**
* Static cache that contains already loaded cache entries.
*
* @var array
*/
protected static $cached = [];
/**
* The collection identifier of this cache.
*
* @var string
*/
protected $collection;
/**
* The cache backend backing this FileCache object.
*
* @var \Drupal\Component\FileCache\FileCacheBackendInterface
*/
protected $cache;
/**
* Constructs a FileCache object.
*
* @param string $prefix
* The cache prefix.
* @param string $collection
* A collection identifier to ensure that the same files could be cached for
* different purposes without clashing.
* @param string|null $cache_backend_class
* (optional) The class that should be used as cache backend.
* @param array $cache_backend_configuration
* (optional) The configuration for the backend class.
*/
public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) {
if (empty($prefix)) {
throw new \InvalidArgumentException('Required prefix configuration is missing');
}
$this->prefix = $prefix;
$this->collection = $collection;
if (isset($cache_backend_class)) {
$this->cache = new $cache_backend_class($cache_backend_configuration);
}
}
/**
* {@inheritdoc}
*/
public function get($filepath) {
$filepaths = [$filepath];
$cached = $this->getMultiple($filepaths);
return isset($cached[$filepath]) ? $cached[$filepath] : NULL;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $filepaths) {
$file_data = [];
$remaining_cids = [];
// First load from the static cache what we can.
foreach ($filepaths as $filepath) {
if (!file_exists($filepath)) {
continue;
}
$realpath = realpath($filepath);
// If the file exists but realpath returns nothing, it is using a stream
// wrapper, those are not supported.
if (empty($realpath)) {
continue;
}
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
if (isset(static::$cached[$cid]) && static::$cached[$cid]['mtime'] == filemtime($filepath)) {
$file_data[$filepath] = static::$cached[$cid]['data'];
}
else {
// Collect a list of cache IDs that we still need to fetch from cache
// backend.
$remaining_cids[$cid] = $filepath;
}
}
// If there are any cache IDs left to fetch from the cache backend.
if ($remaining_cids && $this->cache) {
$cache_results = $this->cache->fetch(array_keys($remaining_cids)) ?: [];
foreach ($cache_results as $cid => $cached) {
$filepath = $remaining_cids[$cid];
if ($cached['mtime'] == filemtime($filepath)) {
$file_data[$cached['filepath']] = $cached['data'];
static::$cached[$cid] = $cached;
}
}
}
return $file_data;
}
/**
* {@inheritdoc}
*/
public function set($filepath, $data) {
$realpath = realpath($filepath);
$cached = [
'mtime' => filemtime($filepath),
'filepath' => $filepath,
'data' => $data,
];
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
static::$cached[$cid] = $cached;
if ($this->cache) {
$this->cache->store($cid, $cached);
}
}
/**
* {@inheritdoc}
*/
public function delete($filepath) {
$realpath = realpath($filepath);
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
unset(static::$cached[$cid]);
if ($this->cache) {
$this->cache->delete($cid);
}
}
}
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCacheBackendInterface.
*/
namespace Drupal\Component\FileCache;
/**
* Defines an interface inspired by APCu for FileCache backends.
*/
interface FileCacheBackendInterface {
/**
* Fetches data from the cache backend.
*
* @param array $cids
* The cache IDs to fetch.
*
* @return array
* An array containing cache entries keyed by cache ID.
*/
public function fetch(array $cids);
/**
* Stores data into a cache backend.
*
* @param string $cid
* The cache ID to store data to.
* @param mixed $data
* The data to store.
*/
public function store($cid, $data);
/**
* Deletes data from a cache backend.
*
* @param string $cid
* The cache ID to delete.
*/
public function delete($cid);
}
<?php
/**
* @file
* Contains Drupal\Component\FileCache\FileCacheFactory.
*/
namespace Drupal\Component\FileCache;
/**
* Creates a FileCache object.
*/
class FileCacheFactory {
/**
* The configuration used to create FileCache objects.
*
* @var array $configuration
*/
protected static $configuration;
/**
* The cache prefix.
*
* @var string
*/
protected static $prefix;
/**
* Instantiates a FileCache object for a given collection identifier.
*
* @param string $collection
* The collection identifier for this FileCache.
* @param array $default_configuration
* (optional) The default configuration for this FileCache collection. This
* can be used to e.g. specify default usage of a FileCache class.
*
* @return \Drupal\Component\FileCache\FileCacheInterface
* The initialized FileCache object.
*/
public static function get($collection, $default_configuration = []) {
$default_configuration += [
'class' => '\Drupal\Component\FileCache\FileCache',
'collection' => $collection,
'cache_backend_class' => NULL,
'cache_backend_configuration' => [],
];
$configuration = [];
if (isset(static::$configuration[$collection])) {
$configuration = static::$configuration[$collection];
}
elseif (isset(static::$configuration['default'])) {
$configuration = static::$configuration['default'];
}
// Add defaults to the configuration.
$configuration = $configuration + $default_configuration;
$class = $configuration['class'];
return new $class(static::getPrefix(), $configuration['collection'], $configuration['cache_backend_class'], $configuration['cache_backend_configuration']);
}
/**
* Gets the configuration used for constructing future file cache objects.
*
* @return array
* The configuration that is used.
*/
public static function getConfiguration() {
return static::$configuration;
}
/**
* Sets the configuration to use for constructing future file cache objects.
*
* @param array $configuration
* The configuration to use.
*/
public static function setConfiguration($configuration) {
static::$configuration = $configuration;
}
/**
* Returns the cache prefix.
*
* @return string
* The cache prefix.
*/
public static function getPrefix() {
return static::$prefix;
}
/**
* Sets the cache prefix that should be used.
*
* Should be set to a secure, unique key to prevent cache pollution by a
* third party.
*
* @param string $prefix
* The cache prefix.
*/
public static function setPrefix($prefix) {
static::$prefix = $prefix;
}
}
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCacheInterface.
*/
namespace Drupal\Component\FileCache;
/**
* Interface for objects that allow caching file data.
*
* Parsing YAML, annotations or similar data out of files can be a
* time-consuming process, especially since those files usually don't change
* and identical data is parsed over and over again.
*
* File cache is a self-contained caching layer for such processing, that relies
* on the file modification to ensure that cached data is still up to date and
* does not need to be invalidated externally.
*/
interface FileCacheInterface {
/**
* Gets data based on a filename.
*
* @param string $filepath
* Path of the file that the cached data is based on.
*
* @return mixed|null
* The data that was persisted with set() or NULL if there is no data
* or the file has been modified.
*/
public function get($filepath);
/**
* Gets data based on filenames.
*
* @param string[] $filepaths
* List of file paths used as cache identifiers.
*
* @return array
* List of cached data keyed by the passed in file paths.
*/
public function getMultiple(array $filepaths);
/**
* Stores data based on a filename.
*
* @param string $filepath
* Path of the file that the cached data is based on.
* @param mixed $data
* The data that should be cached.
*/
public function set($filepath, $data);
/**
* Deletes data from the cache.
*
* @param string $filepath
* Path of the file that the cached data is based on.
*/
public function delete($filepath);
}
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\NullFileCache.
*/
namespace Drupal\Component\FileCache;
/**
* Null implementation for the file cache.
*/
class NullFileCache implements FileCacheInterface {
/**
* Constructs a FileCache object.
*
* @param string $prefix
* A prefix that is used as a prefix, should be set to a secure, unique key
* to prevent cache pollution by a third party.
* @param string $collection
* A collection identifier to ensure that the same files could be cached for
* different purposes without clashing.
* @param string|null $cache_backend_class
* (optional) The class that should be used as cache backend.
* @param array $cache_backend_configuration
* (optional) The configuration for the backend class.
*/
public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) {
}
/**
* {@inheritdoc}
*/
public function get($filepath) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $filepaths) {
return [];
}
/**
* {@inheritdoc}
*/
public function set($filepath, $data) {
}
/**
* {@inheritdoc}
*/
public function delete($filepath) {
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\DependencyInjection;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Yaml;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -34,30 +35,22 @@ class YamlFileLoader
{
/**
* Statically cached yaml files.
*
* Especially during tests, YAML files are re-parsed often.
*
* @var array
* @var \Drupal\Core\DependencyInjection\ContainerBuilder $container
*/
static protected $yaml = array();
protected $container;
/**
* @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
* File cache object.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected $container;
protected $fileCache;
public function __construct(ContainerBuilder $container)
{
$this->container = $container;
}
/**
* Resets the internal cache. This method is mostly useful for tests.
*/
public static function reset()
public function __construct(ContainerBuilder $container)
{
static::$yaml = array();
$this->container = $container;
$this->fileCache = FileCacheFactory::get('container_yaml_loader');
}
/**
......@@ -67,10 +60,14 @@ public static function reset()
*/
public function load($file)
{
if (!isset(static::$yaml[$file])) {
static::$yaml[$file] = $this->loadFile($file);
// Load from the file cache, fall back to loading the file.
// @todo Refactor this to cache parsed definition objects in
// https://www.drupal.org/node/2464053
$content = $this->fileCache->get($file);
if (!$content) {
$content = $this->loadFile($file);
$this->fileCache->set($file, $content);
}
$content = static::$yaml[$file];
// Not supported.
//$this->container->addResource(new FileResource($path));
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\ProxyBuilder\ProxyDumper;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Timer;
......@@ -411,6 +412,28 @@ public function boot() {
if (!$this->sitePath) {
throw new \Exception('Kernel does not have site path set before calling boot()');
}
// Initialize the FileCacheFactory component. We have to do it here instead
// of in \Drupal\Component\FileCache\FileCacheFactory because we can not use
// the Settings object in a component.
$configuration = Settings::get('file_cache');
// Provide a default configuration, if not set.
if (!isset($configuration['default'])) {
$configuration['default'] = [
'class' => '\Drupal\Component\FileCache\FileCache',
'cache_backend_class' => NULL,
'cache_backend_configuration' => [],
];
// @todo Use extension_loaded('apcu') for non-testbot
// https://www.drupal.org/node/2447753.
if (function_exists('apc_fetch')) {
$configuration['default']['cache_backend_class'] = '\Drupal\Component\FileCache\ApcuFileCacheBackend';
}
}
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
// Initialize the container.
$this->initializeContainer();
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Extension;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
use Drupal\Core\Site\Settings;
......@@ -80,6 +81,13 @@ class ExtensionDiscovery {
*/
protected $root;
/**
* The file cache object.
*
* @var \Drupal\Component\FileCache\FileCacheInterface
*/
protected $fileCache;
/**
* Constructs a new ExtensionDiscovery object.
*
......@@ -88,6 +96,7 @@ class ExtensionDiscovery {
*/
public function __construct($root) {
$this->root = $root;
$this->fileCache = FileCacheFactory::get('extension_discovery');
}
/**
......@@ -406,6 +415,12 @@ protected function scanDirectory($dir, $include_tests) {
if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) {
continue;
}
if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
$files[$cached_extension->getType()][$key] = $cached_extension;
continue;
}
// Determine extension type from info file.
$type = FALSE;
$file = $fileinfo->openFile('r');
......@@ -441,6 +456,7 @@ protected function scanDirectory($dir, $include_tests) {
$extension->origin = $dir;
$files[$type][$key] = $extension;
$this->fileCache->set($fileinfo->getPathName(), $extension);
}
return $files;
}
......
......@@ -7,6 +7,7 @@
namespace Drupal\simpletest;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Crypt;
......@@ -1082,8 +1083,9 @@ protected function setContainerParameter($name, $value) {
$services['parameters'][$name] = $value;
file_put_contents($filename, Yaml::encode($services));
// Clear the YML file cache.
YamlFileLoader::reset();
// Ensure that the cache is deleted for the yaml file loader.
$file_cache = FileCacheFactory::get('container_yaml_loader');
$file_cache->delete($filename);
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Tests\Component\FileCache\FileCacheFactoryTest.
*/
namespace Drupal\Tests\Component\FileCache;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Component\FileCache\FileCacheFactory
* @group FileCache
*/
class FileCacheFactoryTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$settings = [
'collection' => 'test-23',
'cache_backend_class' => '\Drupal\Tests\Component\FileCache\StaticFileCacheBackend',
'cache_backend_configuration' => [
'bin' => 'dog',
],
];
$configuration = FileCacheFactory::getConfiguration();
if (!$configuration) {
$configuration = [];
}
$configuration += [ 'test_foo_settings' => $settings ];
FileCacheFactory::setConfiguration($configuration);
FileCacheFactory::setPrefix('prefix');
}