1515use phpDocumentor \Guides \Nodes \DocumentTree \ExternalEntryNode ;
1616use Psr \Log \LoggerInterface ;
1717use SplPriorityQueue ;
18+ use T3Docs \GuidesExtension \Compiler \Cache \IncrementalBuildCache ;
19+ use T3Docs \GuidesExtension \Util \CpuDetector ;
20+ use T3Docs \GuidesExtension \Util \ProcessManager ;
1821
1922use function array_chunk ;
2023use function count ;
2326use function function_exists ;
2427use function iterator_to_array ;
2528use function pcntl_fork ;
26- use function pcntl_waitpid ;
27- use function pcntl_wexitstatus ;
28- use function pcntl_wifexited ;
2929use function serialize ;
3030use function sprintf ;
31- use function sys_get_temp_dir ;
32- use function tempnam ;
33- use function unlink ;
3431use function unserialize ;
3532
3633/**
@@ -85,6 +82,7 @@ public function __construct(
8582 private readonly Compiler $ sequentialCompiler ,
8683 iterable $ passes ,
8784 NodeTransformerFactory $ nodeTransformerFactory ,
85+ private readonly ?IncrementalBuildCache $ incrementalCache = null ,
8886 private readonly ?LoggerInterface $ logger = null ,
8987 ?int $ workerCount = null ,
9088 ) {
@@ -218,7 +216,7 @@ private function runCollectionPhase(array $documents, CompilerContext $compilerC
218216 continue ;
219217 }
220218
221- $ tempFile = tempnam ( sys_get_temp_dir (), 'compile_collect_ ' . $ workerId . '_ ' );
219+ $ tempFile = ProcessManager:: createSecureTempFile ( 'compile_collect_ ' . $ workerId . '_ ' );
222220 if ($ tempFile === false ) {
223221 $ this ->logger ?->error('Failed to create temp file, falling back to sequential ' );
224222 return [$ this ->runSequentially ($ documents , $ compilerContext ), []];
@@ -230,7 +228,7 @@ private function runCollectionPhase(array $documents, CompilerContext $compilerC
230228 if ($ pid === -1 ) {
231229 $ this ->logger ?->error('pcntl_fork failed, falling back to sequential ' );
232230 foreach ($ tempFiles as $ tf ) {
233- @ unlink ($ tf );
231+ ProcessManager:: cleanupTempFile ($ tf );
234232 }
235233 return [$ this ->runSequentially ($ documents , $ compilerContext ), []];
236234 }
@@ -244,18 +242,14 @@ private function runCollectionPhase(array $documents, CompilerContext $compilerC
244242 $ childPids [$ workerId ] = $ pid ;
245243 }
246244
247- // Wait for children and collect results
245+ // Wait for children with timeout and collect results
246+ $ waitResult = ProcessManager::waitForChildrenWithTimeout ($ childPids );
248247 $ allDocuments = [];
249248 $ allResults = [];
250249
251250 foreach ($ childPids as $ workerId => $ pid ) {
252- $ status = 0 ;
253- pcntl_waitpid ($ pid , $ status );
254-
255- // pcntl_waitpid always sets $status to an int
256- assert (is_int ($ status ));
257-
258- if (pcntl_wifexited ($ status ) && pcntl_wexitstatus ($ status ) === 0 ) {
251+ // Only read results from successful workers
252+ if (in_array ($ workerId , $ waitResult ['successes ' ], true )) {
259253 $ serialized = file_get_contents ($ tempFiles [$ workerId ]);
260254 if ($ serialized !== false && $ serialized !== '' ) {
261255 $ data = unserialize ($ serialized );
@@ -274,7 +268,14 @@ private function runCollectionPhase(array $documents, CompilerContext $compilerC
274268 }
275269 }
276270
277- @unlink ($ tempFiles [$ workerId ]);
271+ ProcessManager::cleanupTempFile ($ tempFiles [$ workerId ]);
272+ }
273+
274+ // Log any failures
275+ if ($ waitResult ['failures ' ] !== []) {
276+ foreach ($ waitResult ['failures ' ] as $ workerId => $ reason ) {
277+ $ this ->logger ?->warning(sprintf ('Collection worker %d failed: %s ' , $ workerId , $ reason ));
278+ }
278279 }
279280
280281 // Preserve document order
@@ -468,7 +469,7 @@ private function runResolutionPhase(array $documents, CompilerContext $compilerC
468469 continue ;
469470 }
470471
471- $ tempFile = tempnam ( sys_get_temp_dir (), 'compile_resolve_ ' . $ workerId . '_ ' );
472+ $ tempFile = ProcessManager:: createSecureTempFile ( 'compile_resolve_ ' . $ workerId . '_ ' );
472473 if ($ tempFile === false ) {
473474 return $ this ->runResolutionSequentially ($ documents , $ compilerContext );
474475 }
@@ -478,7 +479,7 @@ private function runResolutionPhase(array $documents, CompilerContext $compilerC
478479
479480 if ($ pid === -1 ) {
480481 foreach ($ tempFiles as $ tf ) {
481- @ unlink ($ tf );
482+ ProcessManager:: cleanupTempFile ($ tf );
482483 }
483484 return $ this ->runResolutionSequentially ($ documents , $ compilerContext );
484485 }
@@ -491,30 +492,61 @@ private function runResolutionPhase(array $documents, CompilerContext $compilerC
491492 $ childPids [$ workerId ] = $ pid ;
492493 }
493494
494- // Collect results
495+ // Wait for children with timeout and collect results
496+ $ waitResult = ProcessManager::waitForChildrenWithTimeout ($ childPids );
495497 $ allDocuments = [];
496- foreach ($ childPids as $ workerId => $ pid ) {
497- $ status = 0 ;
498- pcntl_waitpid ($ pid , $ status );
498+ $ cacheStates = [];
499499
500- // pcntl_waitpid always sets $status to an int
501- assert (is_int ($ status ));
502-
503- if (pcntl_wifexited ($ status ) && pcntl_wexitstatus ($ status ) === 0 ) {
500+ foreach ($ childPids as $ workerId => $ pid ) {
501+ // Only read results from successful workers
502+ if (in_array ($ workerId , $ waitResult ['successes ' ], true )) {
504503 $ serialized = file_get_contents ($ tempFiles [$ workerId ]);
505504 if ($ serialized !== false && $ serialized !== '' ) {
506- $ batchResult = unserialize ($ serialized );
507- if (is_array ($ batchResult )) {
508- foreach ($ batchResult as $ doc ) {
509- if ($ doc instanceof DocumentNode) {
510- $ allDocuments [$ doc ->getFilePath ()] = $ doc ;
505+ $ data = unserialize ($ serialized );
506+ if (is_array ($ data )) {
507+ // New format with cache state
508+ if (isset ($ data ['documents ' ])) {
509+ foreach ($ data ['documents ' ] as $ doc ) {
510+ if ($ doc instanceof DocumentNode) {
511+ $ allDocuments [$ doc ->getFilePath ()] = $ doc ;
512+ }
513+ }
514+ // Collect cache state for merging
515+ if (isset ($ data ['cacheState ' ]) && is_array ($ data ['cacheState ' ])) {
516+ $ cacheStates [] = $ data ['cacheState ' ];
517+ }
518+ } else {
519+ // Legacy format (just documents array)
520+ foreach ($ data as $ doc ) {
521+ if ($ doc instanceof DocumentNode) {
522+ $ allDocuments [$ doc ->getFilePath ()] = $ doc ;
523+ }
511524 }
512525 }
513526 }
514527 }
515528 }
516529
517- @unlink ($ tempFiles [$ workerId ]);
530+ ProcessManager::cleanupTempFile ($ tempFiles [$ workerId ]);
531+ }
532+
533+ // Log any failures
534+ if ($ waitResult ['failures ' ] !== []) {
535+ foreach ($ waitResult ['failures ' ] as $ workerId => $ reason ) {
536+ $ this ->logger ?->warning(sprintf ('Resolution worker %d failed: %s ' , $ workerId , $ reason ));
537+ }
538+ }
539+
540+ // Merge cache states from all children
541+ if ($ this ->incrementalCache !== null && $ cacheStates !== []) {
542+ foreach ($ cacheStates as $ state ) {
543+ $ this ->incrementalCache ->mergeState ($ state );
544+ }
545+ $ this ->logger ?->debug(sprintf (
546+ 'Merged cache states from %d workers, now have %d exports ' ,
547+ count ($ cacheStates ),
548+ count ($ this ->incrementalCache ->getAllExports ())
549+ ));
518550 }
519551
520552 // Preserve order
@@ -541,7 +573,13 @@ private function processResolutionBatch(
541573 $ batch = $ pass ->run ($ batch , $ compilerContext );
542574 }
543575
544- file_put_contents ($ tempFile , serialize ($ batch ));
576+ // Serialize documents and cache state (if cache is available)
577+ $ data = [
578+ 'documents ' => $ batch ,
579+ 'cacheState ' => $ this ->incrementalCache ?->extractState() ?? [],
580+ ];
581+
582+ file_put_contents ($ tempFile , serialize ($ data ));
545583 }
546584
547585 /**
@@ -698,25 +736,7 @@ private function shouldFork(int $documentCount): bool
698736
699737 private function detectCpuCount (): int
700738 {
701- if (is_file ('/proc/cpuinfo ' )) {
702- $ cpuinfo = file_get_contents ('/proc/cpuinfo ' );
703- if ($ cpuinfo !== false ) {
704- $ count = substr_count ($ cpuinfo , 'processor ' );
705- if ($ count > 0 ) {
706- return min ($ count , 8 );
707- }
708- }
709- }
710-
711- $ nproc = @shell_exec ('nproc 2>/dev/null ' );
712- if ($ nproc !== null && $ nproc !== false ) {
713- $ count = (int ) trim ($ nproc );
714- if ($ count > 0 ) {
715- return min ($ count , 8 );
716- }
717- }
718-
719- return 4 ;
739+ return CpuDetector::detectCores ();
720740 }
721741
722742 public function setParallelEnabled (bool $ enabled ): void
0 commit comments