Skip to content

Commit abdcbed

Browse files
ondrejmirtesclaude
andcommitted
Keep the symbols found in directories between runs, checked by stat
OptimizedDirectorySourceLocatorFactory had two ways of finding the symbols of a directory. Without the turbo extension, or with workers that are not forked, they were cached, and the cache was checked by hashing every file. With the extension and forked workers, every file was scanned natively on every run instead, because hashing cost about as much as the scan - 0.7s on Drupal core, paid by every run that analyses at least one file. There is one way now. The cache entry of a directory keeps each file's stat signature next to its hash (see FileStatSignatures), and a file whose signature matches is not even hashed. When the signature cannot vouch for the file, the hash decides, as before. The arena records that shared the file hashes and the symbol maps between workers that are not forked are gone - the cache entry itself is still shared through the arena by Cache::load(). The cache now also keeps the files that declare no symbols, which used to be scanned again on every run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
1 parent 866f50d commit abdcbed

4 files changed

Lines changed: 268 additions & 416 deletions

File tree

‎src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php‎

Lines changed: 4 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use PHPStan\BetterReflection\Reflector\Reflector;
1515
use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
1616
use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
17-
use PHPStan\Cache\ArenaCache;
1817
use PHPStan\Cache\Cache;
1918
use PHPStan\File\CouldNotReadFileException;
2019
use PHPStan\File\FileContentHasher;
@@ -25,31 +24,13 @@
2524
use function array_key_exists;
2625
use function array_values;
2726
use function current;
28-
use function is_array;
29-
use function is_string;
3027
use function sprintf;
3128
use function strtolower;
3229

3330
final class OptimizedDirectorySourceLocator implements SourceLocator
3431
{
3532

36-
/** @var array<string, string|false> */
37-
private array $arenaClassLookups = [];
38-
39-
/** @var array<string, array<int, string>|false> */
40-
private array $arenaFunctionLookups = [];
41-
42-
/** @var array<string, string|false> */
43-
private array $arenaConstantLookups = [];
44-
45-
private bool $hydratedFromArena = false;
46-
4733
/**
48-
* With $arenaKeyPrefix set, the maps start empty and names resolve lazily
49-
* from the run's shared arena (published by whichever process built this
50-
* directory's index first), so the worker materializes only the names it
51-
* touches instead of the whole index.
52-
*
5334
* @param array<string, string> $classToFile
5435
* @param array<string, array<int, string>> $functionToFiles
5536
* @param array<string, string> $constantToFile
@@ -62,14 +43,13 @@ public function __construct(
6243
private array $classToFile,
6344
private array $functionToFiles,
6445
private array $constantToFile,
65-
private ?string $arenaKeyPrefix = null,
6646
private bool $awaitingBatchedScan = false,
6747
)
6848
{
6949
}
7050

7151
/**
72-
* Fills in the symbol maps of a locator created for a batched scan.
52+
* Fills in the symbol maps of a locator created ahead of the scan.
7353
*
7454
* The factory hands these out before the scan that produces their contents
7555
* has run, so that one scan can cover every directory at once
@@ -248,111 +228,20 @@ private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode
248228

249229
private function findFileByClass(string $className): ?string
250230
{
251-
if (array_key_exists($className, $this->classToFile)) {
252-
return $this->classToFile[$className];
253-
}
254-
255-
if ($this->arenaKeyPrefix === null) {
256-
return null;
257-
}
258-
259-
if (array_key_exists($className, $this->arenaClassLookups)) {
260-
$file = $this->arenaClassLookups[$className];
261-
} else {
262-
$file = ArenaCache::lookupHash($this->arenaKeyPrefix . '-classes', $className);
263-
if (!is_string($file)) {
264-
$file = false;
265-
}
266-
$this->arenaClassLookups[$className] = $file;
267-
}
268-
269-
return $file === false ? null : $file;
231+
return $this->classToFile[$className] ?? null;
270232
}
271233

272234
private function findFileByConstant(string $constantName): ?string
273235
{
274-
if (array_key_exists($constantName, $this->constantToFile)) {
275-
return $this->constantToFile[$constantName];
276-
}
277-
278-
if ($this->arenaKeyPrefix === null) {
279-
return null;
280-
}
281-
282-
if (array_key_exists($constantName, $this->arenaConstantLookups)) {
283-
$file = $this->arenaConstantLookups[$constantName];
284-
} else {
285-
$file = ArenaCache::lookupHash($this->arenaKeyPrefix . '-constants', $constantName);
286-
if (!is_string($file)) {
287-
$file = false;
288-
}
289-
$this->arenaConstantLookups[$constantName] = $file;
290-
}
291-
292-
return $file === false ? null : $file;
236+
return $this->constantToFile[$constantName] ?? null;
293237
}
294238

295239
/**
296240
* @return string[]
297241
*/
298242
private function findFilesByFunction(string $functionName): array
299243
{
300-
if (array_key_exists($functionName, $this->functionToFiles)) {
301-
return $this->functionToFiles[$functionName];
302-
}
303-
304-
if ($this->arenaKeyPrefix === null) {
305-
return [];
306-
}
307-
308-
if (array_key_exists($functionName, $this->arenaFunctionLookups)) {
309-
$files = $this->arenaFunctionLookups[$functionName];
310-
} else {
311-
/** @var array<int, string>|mixed $files */
312-
$files = ArenaCache::lookupHash($this->arenaKeyPrefix . '-functions', $functionName);
313-
if (!is_array($files)) {
314-
$files = false;
315-
}
316-
$this->arenaFunctionLookups[$functionName] = $files;
317-
}
318-
319-
return $files === false ? [] : $files;
320-
}
321-
322-
/**
323-
* Enumeration needs the full maps: hydrates them from the arena records
324-
* in their publication order, which equals the insertion order a locally
325-
* built index would have. A null (a corrupt record — impossible with an
326-
* intact arena, the factory gated on all three records) leaves a map
327-
* empty rather than failing the run.
328-
*/
329-
private function hydrateSymbolsFromArena(): void
330-
{
331-
if ($this->arenaKeyPrefix === null || $this->hydratedFromArena) {
332-
return;
333-
}
334-
335-
$this->hydratedFromArena = true;
336-
337-
/** @var array<string, string>|null $classes */
338-
$classes = ArenaCache::lookupHashAll($this->arenaKeyPrefix . '-classes');
339-
if ($classes !== null) {
340-
$this->classToFile = $classes;
341-
}
342-
343-
/** @var array<string, array<int, string>>|null $functions */
344-
$functions = ArenaCache::lookupHashAll($this->arenaKeyPrefix . '-functions');
345-
if ($functions !== null) {
346-
$this->functionToFiles = $functions;
347-
}
348-
349-
/** @var array<string, string>|null $constants */
350-
$constants = ArenaCache::lookupHashAll($this->arenaKeyPrefix . '-constants');
351-
if ($constants === null) {
352-
return;
353-
}
354-
355-
$this->constantToFile = $constants;
244+
return $this->functionToFiles[$functionName] ?? [];
356245
}
357246

358247
/**
@@ -365,8 +254,6 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id
365254
throw new ShouldNotHappenException('Symbols were looked up in a directory whose batched scan has not been flushed yet.');
366255
}
367256

368-
$this->hydrateSymbolsFromArena();
369-
370257
$reflections = [];
371258
if ($identifierType->isClass()) {
372259
foreach ($this->classToFile as $file) {

0 commit comments

Comments
 (0)