2929use function str_contains ;
3030use function str_starts_with ;
3131use function substr ;
32- use function time ;
3332use function unlink ;
3433use function unserialize ;
3534use const DIRECTORY_SEPARATOR ;
4948 * Reading the directories is what a walk costs - on a tree the size of Drupal core (28k directories)
5049 * about a second, every run, in the main process and again in the worker that builds the symbol
5150 * index. What a directory contains changes only when an entry is added, removed or renamed in it,
52- * and each of those updates the directory's mtime and ctime, so the listings are kept in tmpDir and
53- * a directory whose stat still matches is not read again. ctime cannot be set back by touch or by
54- * extracting an archive, and a directory modified in the very second its listing is read is not
55- * kept: a second change within that same second would leave its stat unchanged. The same technique
56- * is behind git's untracked cache.
51+ * so the listings are kept in tmpDir and a directory whose stat signature still matches (see
52+ * FileStatSignatures) is not read again. The same technique is behind git's untracked cache.
5753 *
5854 * The walk yields what Symfony Finder yields for files()->name()->followLinks() with its default
5955 * ignores - dot files and VCS directories left out, symlinks followed, files in readdir order - and
6460final class DirectoryWalker
6561{
6662
67- private const LISTINGS_FORMAT = 'directoryListings-v1 ' ;
63+ private const LISTINGS_FORMAT = 'directoryListings-v2 ' ;
6864
6965 /** The directories Finder's ignoreVCS() leaves out; the ones starting with a dot are left out anyway. */
7066 private const VCS_DIRECTORIES = ['_svn ' => true , 'CVS ' => true , '_darcs ' => true ];
@@ -73,12 +69,12 @@ final class DirectoryWalker
7369 private array $ cachedWalks = [];
7470
7571 /**
76- * Directory path => [mtime, ctime, inode , entries]. The entries are the directory's names in
72+ * Directory path => [stat signature , entries]. The entries are the directory's names in
7773 * readdir order, each prefixed by d (directory), f (anything else - Finder's files() only leaves
7874 * out directories, so a broken symlink is a file too) or l (symlink - resolved on every walk,
7975 * because its target can change without this directory changing), joined by NUL.
8076 *
81- * @var array<string, array{int, int, int , string}>|null
77+ * @var array<string, array{string , string}>|null
8278 */
8379 private ?array $ listings = null ;
8480
@@ -88,6 +84,7 @@ final class DirectoryWalker
8884 * @param string $tmpDir where the listings are kept between runs, nowhere when empty
8985 */
9086 public function __construct (
87+ private FileStatSignatures $ fileStatSignatures ,
9188 #[AutowiredParameter]
9289 private string $ tmpDir = '' ,
9390 )
@@ -155,7 +152,7 @@ private function walkWithListings(string $directory, array $fileExtensions): ?ar
155152
156153 $ files = [];
157154 $ visited = [];
158- if (!$ this ->walkDirectory ($ directory , Glob ::toRegex ('*.{ ' . implode (', ' , $ fileExtensions ) . '} ' ), time (), $ files , $ visited )) {
155+ if (!$ this ->walkDirectory ($ directory , Glob ::toRegex ('*.{ ' . implode (', ' , $ fileExtensions ) . '} ' ), $ this -> fileStatSignatures -> begin (), $ files , $ visited )) {
159156 return null ;
160157 }
161158
@@ -168,25 +165,26 @@ private function walkWithListings(string $directory, array $fileExtensions): ?ar
168165 * @param list<string> $files
169166 * @param array<string, true> $visited
170167 */
171- private function walkDirectory (string $ directory , string $ pattern , int $ now , array &$ files , array &$ visited ): bool
168+ private function walkDirectory (string $ directory , string $ pattern , FileStatSignatureReader $ signatures , array &$ files , array &$ visited ): bool
172169 {
173170 $ stat = @stat ($ directory );
174171 if ($ stat === false ) {
175172 return false ;
176173 }
177174
178175 $ visited [$ directory ] = true ;
176+ $ signature = $ signatures ->fromStat ($ stat );
179177 $ listing = $ this ->listings [$ directory ] ?? null ;
180- if ($ listing !== null && $ listing[ 0 ] === $ stat [ ' mtime ' ] && $ listing [1 ] === $ stat [ ' ctime ' ] && $ listing [ 2 ] === $ stat [ ' ino ' ] ) {
181- $ entries = $ listing [3 ];
178+ if ($ signature !== null && $ listing !== null && $ listing [0 ] === $ signature ) {
179+ $ entries = $ listing [1 ];
182180 } else {
183181 $ entries = $ this ->readDirectory ($ directory );
184182 if ($ entries === null ) {
185183 return false ;
186184 }
187185
188- if ($ stat [ ' mtime ' ] < $ now && $ stat [ ' ctime ' ] < $ now ) {
189- $ this ->listings [$ directory ] = [$ stat [ ' mtime ' ], $ stat [ ' ctime ' ], $ stat [ ' ino ' ] , $ entries ];
186+ if ($ signature !== null ) {
187+ $ this ->listings [$ directory ] = [$ signature , $ entries ];
190188 } else {
191189 unset($ this ->listings [$ directory ]);
192190 }
@@ -213,7 +211,7 @@ private function walkDirectory(string $directory, string $pattern, int $now, arr
213211 }
214212
215213 if ($ type === 'd ' ) {
216- if (!$ this ->walkDirectory ($ path , $ pattern , $ now , $ files , $ visited )) {
214+ if (!$ this ->walkDirectory ($ path , $ pattern , $ signatures , $ files , $ visited )) {
217215 return false ;
218216 }
219217
@@ -297,7 +295,7 @@ private function loadListings(): void
297295 return ;
298296 }
299297
300- /** @var array<string, array{int, int, int , string}> $listings */
298+ /** @var array<string, array{string , string}> $listings */
301299 $ listings = $ data ['listings ' ];
302300 $ this ->listings = $ listings ;
303301 }
0 commit comments