• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 22973523338

11 Mar 2026 08:42PM UTC coverage: 96.827% (-0.08%) from 96.904%
22973523338

Pull #4834

github

web-flow
Merge 7cf4952f8 into a1dacfdf7
Pull Request #4834: Add parallel Xlsx writing via pcntl_fork

154 of 198 new or added lines in 7 files covered. (77.78%)

17 existing lines in 1 file now uncovered.

47846 of 49414 relevant lines covered (96.83%)

382.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.04
/src/PhpSpreadsheet/Parallel/ParallelExecutor.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Parallel;
4

5
use Closure;
6
use PhpOffice\PhpSpreadsheet\Parallel\Backend\BackendInterface;
7
use PhpOffice\PhpSpreadsheet\Parallel\Backend\PcntlBackend;
8
use PhpOffice\PhpSpreadsheet\Parallel\Backend\SequentialBackend;
9
use PhpOffice\PhpSpreadsheet\Settings;
10

11
class ParallelExecutor
12
{
13
    private const MAX_WORKERS_CAP = 8;
14

15
    private BackendInterface $backend;
16

17
    private ?int $maxWorkers;
18

19
    public function __construct(?BackendInterface $backend = null, ?int $maxWorkers = null)
457✔
20
    {
21
        $this->backend = $backend ?? self::detectBackend();
457✔
22
        $this->maxWorkers = $maxWorkers;
457✔
23
    }
24

25
    /**
26
     * Execute tasks in parallel, returning results in the same order as inputs.
27
     *
28
     * @param list<mixed> $tasks
29
     * @param Closure $worker Function receiving a single task, returning a result
30
     *
31
     * @return list<mixed>
32
     */
33
    public function map(array $tasks, Closure $worker): array
457✔
34
    {
35
        $taskCount = count($tasks);
457✔
36

37
        if ($taskCount === 0) {
457✔
38
            return [];
1✔
39
        }
40

41
        if ($taskCount === 1 || !Settings::isParallelEnabled()) {
456✔
42
            return $this->executeSequential($tasks, $worker);
443✔
43
        }
44

45
        $workerCount = $this->resolveWorkerCount($taskCount);
13✔
46

47
        if ($workerCount < 2) {
13✔
NEW
48
            return $this->executeSequential($tasks, $worker);
×
49
        }
50

51
        return $this->backend->execute($tasks, $worker, $workerCount);
13✔
52
    }
53

54
    /**
55
     * @param list<mixed> $tasks
56
     *
57
     * @return list<mixed>
58
     */
59
    private function executeSequential(array $tasks, Closure $worker): array
443✔
60
    {
61
        $sequential = new SequentialBackend();
443✔
62

63
        return $sequential->execute($tasks, $worker, 1);
443✔
64
    }
65

66
    private function resolveWorkerCount(int $taskCount): int
13✔
67
    {
68
        $configured = $this->maxWorkers ?? Settings::getMaxWorkers();
13✔
69

70
        if ($configured !== null) {
13✔
71
            return min($configured, $taskCount);
11✔
72
        }
73

74
        // Auto-detect: min(cpuCount - 1, taskCount, cap)
75
        $cpuCount = CpuDetector::detectCpuCount();
2✔
76
        $available = max(1, $cpuCount - 1);
2✔
77

78
        $workerCount = min($available, $taskCount, self::MAX_WORKERS_CAP);
2✔
79

80
        // Memory safety check for pcntl_fork
81
        if ($this->backend instanceof PcntlBackend) {
2✔
82
            $workerCount = $this->applyMemoryLimit($workerCount);
2✔
83
        }
84

85
        return $workerCount;
2✔
86
    }
87

88
    private function applyMemoryLimit(int $workerCount): int
2✔
89
    {
90
        $limit = self::getMemoryLimitBytes();
2✔
91
        if ($limit <= 0) {
2✔
NEW
92
            return $workerCount; // No limit set
×
93
        }
94

95
        $currentUsage = memory_get_usage(true);
2✔
96
        // Estimate ~30% of current usage per forked child as dirty page overhead
97
        $estimatedPerChild = (int) ($currentUsage * 0.3);
2✔
98

99
        if ($estimatedPerChild <= 0) {
2✔
NEW
100
            return $workerCount;
×
101
        }
102

103
        $headroom = $limit - $currentUsage;
2✔
104
        $maxSafe = (int) ($headroom / $estimatedPerChild);
2✔
105

106
        return min($workerCount, max(1, $maxSafe));
2✔
107
    }
108

109
    private static function getMemoryLimitBytes(): int
2✔
110
    {
111
        $limit = ini_get('memory_limit');
2✔
112
        if ($limit === '' || $limit === '-1') {
2✔
NEW
113
            return 0; // No limit
×
114
        }
115

116
        $value = (int) $limit;
2✔
117
        $unit = strtolower(substr($limit, -1));
2✔
118

119
        switch ($unit) {
120
            case 'g':
2✔
NEW
121
                $value *= 1024;
×
122
                // no break
123
            case 'm':
2✔
124
                $value *= 1024;
2✔
125
                // no break
NEW
126
            case 'k':
×
127
                $value *= 1024;
2✔
128
        }
129

130
        return $value;
2✔
131
    }
132

133
    private static function detectBackend(): BackendInterface
446✔
134
    {
135
        if (PcntlBackend::isAvailable()) {
446✔
136
            return new PcntlBackend();
446✔
137
        }
138

NEW
139
        return new SequentialBackend();
×
140
    }
141
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc