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

PHPOffice / PhpSpreadsheet / 22974928839

11 Mar 2026 09:17PM UTC coverage: 96.852% (-0.05%) from 96.904%
22974928839

Pull #4834

github

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

179 of 211 new or added lines in 7 files covered. (84.83%)

17 existing lines in 1 file now uncovered.

47871 of 49427 relevant lines covered (96.85%)

382.9 hits per line

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

88.89
/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)
459✔
20
    {
21
        $this->backend = $backend ?? self::detectBackend();
459✔
22
        $this->maxWorkers = $maxWorkers;
459✔
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
459✔
34
    {
35
        $taskCount = count($tasks);
459✔
36

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

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

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

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

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

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

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

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

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

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

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

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

85
        return $workerCount;
3✔
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
    /**
110
     * Parse memory_limit INI value into bytes.
111
     *
112
     * @internal
113
     */
114
    public static function getMemoryLimitBytes(): int
3✔
115
    {
116
        $limit = ini_get('memory_limit');
3✔
117
        if ($limit === '' || $limit === '-1') {
3✔
NEW
118
            return 0; // No limit
×
119
        }
120

121
        $value = (int) $limit;
3✔
122
        $unit = strtolower(substr($limit, -1));
3✔
123

124
        switch ($unit) {
125
            case 'g':
3✔
NEW
126
                $value *= 1024;
×
127
                // no break
128
            case 'm':
3✔
129
                $value *= 1024;
3✔
130
                // no break
NEW
131
            case 'k':
×
132
                $value *= 1024;
3✔
133
        }
134

135
        return $value;
3✔
136
    }
137

138
    private static function detectBackend(): BackendInterface
446✔
139
    {
140
        if (PcntlBackend::isAvailable()) {
446✔
141
            return new PcntlBackend();
446✔
142
        }
143

NEW
144
        return new SequentialBackend();
×
145
    }
146
}
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