From ea496cc529fba57f8bdb7eebbf1b03c6f76dbe95 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 17 Jan 2024 12:01:49 +0100 Subject: [PATCH 1/3] CachedParser: use LRU cache --- conf/config.neon | 2 +- src/Parser/CachedParser.php | 62 +++++------------- src/Parser/LRUCache.php | 76 +++++++++++++++++++++++ tests/PHPStan/Parser/CachedParserTest.php | 11 +--- 4 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 src/Parser/LRUCache.php diff --git a/conf/config.neon b/conf/config.neon index 1620a1d49e..30c4e62250 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1857,7 +1857,7 @@ services: class: PHPStan\Parser\CachedParser arguments: originalParser: @pathRoutingParser - cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + cacheCapacity: %cache.nodesByStringCountMax% autowired: false phpParserDecorator: diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index 225375678c..f232b4459b 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -4,24 +4,22 @@ use PhpParser\Node; use PHPStan\File\FileReader; -use function array_slice; class CachedParser implements Parser { - /** @var array*/ - private array $cachedNodesByString = []; - - private int $cachedNodesByStringCount = 0; + /** @var LRUCache*/ + private LRUCache $cache; /** @var array */ private array $parsedByString = []; public function __construct( private Parser $originalParser, - private int $cachedNodesByStringCountMax, + int $cacheCapacity, ) { + $this->cache = new LRUCache($cacheCapacity); } /** @@ -30,25 +28,13 @@ public function __construct( */ public function parseFile(string $file): array { - if ($this->cachedNodesByStringCountMax !== 0 && $this->cachedNodesByStringCount >= $this->cachedNodesByStringCountMax) { - $this->cachedNodesByString = array_slice( - $this->cachedNodesByString, - 1, - null, - true, - ); - - --$this->cachedNodesByStringCount; - } - $sourceCode = FileReader::read($file); - if (!isset($this->cachedNodesByString[$sourceCode]) || isset($this->parsedByString[$sourceCode])) { - $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseFile($file); - $this->cachedNodesByStringCount++; + if (!$this->cache->has($sourceCode) || isset($this->parsedByString[$sourceCode])) { + $this->cache->put($sourceCode, $this->originalParser->parseFile($file)); unset($this->parsedByString[$sourceCode]); } - return $this->cachedNodesByString[$sourceCode]; + return $this->cache->get($sourceCode); } /** @@ -56,42 +42,22 @@ public function parseFile(string $file): array */ public function parseString(string $sourceCode): array { - if ($this->cachedNodesByStringCountMax !== 0 && $this->cachedNodesByStringCount >= $this->cachedNodesByStringCountMax) { - $this->cachedNodesByString = array_slice( - $this->cachedNodesByString, - 1, - null, - true, - ); - - --$this->cachedNodesByStringCount; - } - - if (!isset($this->cachedNodesByString[$sourceCode])) { - $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseString($sourceCode); - $this->cachedNodesByStringCount++; + if (!$this->cache->has($sourceCode)) { + $this->cache->put($sourceCode, $this->originalParser->parseString($sourceCode)); $this->parsedByString[$sourceCode] = true; } - return $this->cachedNodesByString[$sourceCode]; - } - - public function getCachedNodesByStringCount(): int - { - return $this->cachedNodesByStringCount; + return $this->cache->get($sourceCode); } - public function getCachedNodesByStringCountMax(): int + public function getCacheCapacity(): int { - return $this->cachedNodesByStringCountMax; + return $this->cache->getCapacity(); } - /** - * @return array - */ - public function getCachedNodesByString(): array + public function getCachedItemsCount(): int { - return $this->cachedNodesByString; + return $this->cache->getCount(); } } diff --git a/src/Parser/LRUCache.php b/src/Parser/LRUCache.php new file mode 100644 index 0000000000..7278735bc8 --- /dev/null +++ b/src/Parser/LRUCache.php @@ -0,0 +1,76 @@ + */ + private array $cache; + + /** @var SplDoublyLinkedList */ + private SplDoublyLinkedList $list; + + public function __construct(private int $capacity) + { + $this->cache = []; + $this->list = new SplDoublyLinkedList(); + } + + /** + * @return TValue + */ + public function get(string $key) + { + if (!isset($this->cache[$key])) { + throw new LogicException(sprintf('Key %s was not found in the cache, use ->has() first', $key)); + } + + $this->list->rewind(); + while ($this->list->current() !== $key) { + $this->list->next(); + } + $this->list->rewind(); + $this->list->unshift($this->list->pop()); + + return $this->cache[$key]; + } + + /** + * @param TValue $value + */ + public function put(string $key, $value): void + { + if (count($this->cache) >= $this->capacity) { + $removedKey = $this->list->pop(); + unset($this->cache[$removedKey]); + } + + $this->list->unshift($key); + $this->cache[$key] = $value; + } + + public function has(string $key): bool + { + return isset($this->cache[$key]); + } + + public function getCount(): int + { + return count($this->cache); + } + + public function getCapacity(): int + { + return $this->capacity; + } + +} diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index 76bb9e215d..a369cbb1f4 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -28,7 +28,7 @@ public function testParseFileClearCache( $this->assertEquals( $cachedNodesByStringCountMax, - $parser->getCachedNodesByStringCountMax() + $parser->getCacheCapacity() ); // Add strings to cache @@ -36,14 +36,9 @@ public function testParseFileClearCache( $parser->parseString('string' . $i); } - $this->assertEquals( - $cachedNodesByStringCountExpected, - $parser->getCachedNodesByStringCount() - ); - - $this->assertCount( + $this->assertSame( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByString() + $parser->getCachedItemsCount() ); } From fd162d3cabaa31c347970ee36d86599d5c2d4ac2 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 17 Jan 2024 12:09:35 +0100 Subject: [PATCH 2/3] Fix unlimited cache --- src/Parser/CachedParser.php | 13 +++--- src/Parser/LRUCache.php | 3 +- src/Parser/ParserCache.php | 25 +++++++++++ src/Parser/UnlimitedCache.php | 54 +++++++++++++++++++++++ tests/PHPStan/Parser/CachedParserTest.php | 10 ++--- 5 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 src/Parser/ParserCache.php create mode 100644 src/Parser/UnlimitedCache.php diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index f232b4459b..ecd5df9e1e 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -8,8 +8,8 @@ class CachedParser implements Parser { - /** @var LRUCache*/ - private LRUCache $cache; + /** @var ParserCache*/ + private ParserCache $cache; /** @var array */ private array $parsedByString = []; @@ -19,7 +19,9 @@ public function __construct( int $cacheCapacity, ) { - $this->cache = new LRUCache($cacheCapacity); + $this->cache = $cacheCapacity === 0 + ? new UnlimitedCache() + : new LRUCache($cacheCapacity); } /** @@ -50,11 +52,6 @@ public function parseString(string $sourceCode): array return $this->cache->get($sourceCode); } - public function getCacheCapacity(): int - { - return $this->cache->getCapacity(); - } - public function getCachedItemsCount(): int { return $this->cache->getCount(); diff --git a/src/Parser/LRUCache.php b/src/Parser/LRUCache.php index 7278735bc8..5d0a9fa5a9 100644 --- a/src/Parser/LRUCache.php +++ b/src/Parser/LRUCache.php @@ -9,8 +9,9 @@ /** * @template TValue + * @implements ParserCache */ -class LRUCache +class LRUCache implements ParserCache { /** @var array */ diff --git a/src/Parser/ParserCache.php b/src/Parser/ParserCache.php new file mode 100644 index 0000000000..77e34a70c7 --- /dev/null +++ b/src/Parser/ParserCache.php @@ -0,0 +1,25 @@ + + */ +class UnlimitedCache implements ParserCache +{ + + /** @var array */ + private array $cache; + + public function __construct() + { + $this->cache = []; + } + + /** + * @return TValue + */ + public function get(string $key) + { + if (!isset($this->cache[$key])) { + throw new LogicException(sprintf('Key %s was not found in the cache, use ->has() first', $key)); + } + + return $this->cache[$key]; + } + + /** + * @param TValue $value + */ + public function put(string $key, $value): void + { + $this->cache[$key] = $value; + } + + public function has(string $key): bool + { + return isset($this->cache[$key]); + } + + public function getCount(): int + { + return count($this->cache); + } + +} diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index a369cbb1f4..5178aecd8f 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -26,11 +26,6 @@ public function testParseFileClearCache( $cachedNodesByStringCountMax ); - $this->assertEquals( - $cachedNodesByStringCountMax, - $parser->getCacheCapacity() - ); - // Add strings to cache for ($i = 0; $i <= $cachedNodesByStringCountMax; $i++) { $parser->parseString('string' . $i); @@ -53,6 +48,11 @@ public function dataParseFileClearCache(): \Generator 'cachedNodesByStringCountMax' => 51, 'cachedNodesByStringCountExpected' => 51, ]; + + yield 'unlimited' => [ + 'cachedNodesByStringCountMax' => 0, + 'cachedNodesByStringCountExpected' => 1, + ]; } private function getParserMock(): Parser&MockObject From 7e0be5f7982dc5f1fafbb044c47ea7b6036702ff Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 17 Jan 2024 15:02:54 +0100 Subject: [PATCH 3/3] Fix LRUCache, add test --- src/Parser/LRUCache.php | 27 +++++++++------------ tests/PHPStan/Parser/LRUCacheTest.php | 35 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 tests/PHPStan/Parser/LRUCacheTest.php diff --git a/src/Parser/LRUCache.php b/src/Parser/LRUCache.php index 5d0a9fa5a9..08fc4913a0 100644 --- a/src/Parser/LRUCache.php +++ b/src/Parser/LRUCache.php @@ -3,7 +3,8 @@ namespace PHPStan\Parser; use LogicException; -use SplDoublyLinkedList; +use function array_key_first; +use function array_keys; use function count; use function sprintf; @@ -17,13 +18,9 @@ class LRUCache implements ParserCache /** @var array */ private array $cache; - /** @var SplDoublyLinkedList */ - private SplDoublyLinkedList $list; - public function __construct(private int $capacity) { $this->cache = []; - $this->list = new SplDoublyLinkedList(); } /** @@ -35,12 +32,9 @@ public function get(string $key) throw new LogicException(sprintf('Key %s was not found in the cache, use ->has() first', $key)); } - $this->list->rewind(); - while ($this->list->current() !== $key) { - $this->list->next(); - } - $this->list->rewind(); - $this->list->unshift($this->list->pop()); + $value = $this->cache[$key]; + unset($this->cache[$key]); + $this->cache[$key] = $value; return $this->cache[$key]; } @@ -51,11 +45,9 @@ public function get(string $key) public function put(string $key, $value): void { if (count($this->cache) >= $this->capacity) { - $removedKey = $this->list->pop(); - unset($this->cache[$removedKey]); + unset($this->cache[array_key_first($this->cache)]); } - $this->list->unshift($key); $this->cache[$key] = $value; } @@ -69,9 +61,12 @@ public function getCount(): int return count($this->cache); } - public function getCapacity(): int + /** + * @return list + */ + public function getKeys(): array { - return $this->capacity; + return array_keys($this->cache); } } diff --git a/tests/PHPStan/Parser/LRUCacheTest.php b/tests/PHPStan/Parser/LRUCacheTest.php new file mode 100644 index 0000000000..3ec24a6879 --- /dev/null +++ b/tests/PHPStan/Parser/LRUCacheTest.php @@ -0,0 +1,35 @@ +put('1', '1'); + $cache->put('2', '2'); + $cache->put('3', '3'); + self::assertSame([1, 2, 3], $cache->getKeys()); + + $cache->get('2'); + self::assertSame([1, 3, 2], $cache->getKeys()); + + $cache->put('4', '4'); + self::assertSame([3, 2, 4], $cache->getKeys()); + + $cache->get('2'); + self::assertSame([3, 4, 2], $cache->getKeys()); + + $cache->get('2'); + self::assertSame([3, 4, 2], $cache->getKeys()); + + $cache->put('5', '5'); + self::assertSame([4, 2, 5], $cache->getKeys()); + } + +}