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

PHPOffice / PhpSpreadsheet / 22981566997

12 Mar 2026 12:49AM UTC coverage: 96.844% (-0.06%) from 96.904%
22981566997

Pull #4834

github

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

182 of 218 new or added lines in 6 files covered. (83.49%)

14 existing lines in 1 file now uncovered.

47874 of 49434 relevant lines covered (96.84%)

382.61 hits per line

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

88.24
/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

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

14
    private BackendInterface $backend;
15

16
    private ?int $maxWorkers;
17

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

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

36
        if ($taskCount <= 1) {
20✔
37
            return $this->executeSequential($tasks, $worker);
2✔
38
        }
39

40
        $workerCount = $this->resolveWorkerCount($taskCount);
18✔
41

42
        if ($workerCount < 2) {
18✔
43
            return $this->executeSequential($tasks, $worker);
1✔
44
        }
45

46
        return $this->backend->execute($tasks, $worker, $workerCount);
17✔
47
    }
48

49
    /**
50
     * @param list<mixed> $tasks
51
     *
52
     * @return list<mixed>
53
     */
54
    private function executeSequential(array $tasks, Closure $worker): array
3✔
55
    {
56
        $sequential = new SequentialBackend();
3✔
57

58
        return $sequential->execute($tasks, $worker, 1);
3✔
59
    }
60

61
    private function resolveWorkerCount(int $taskCount): int
18✔
62
    {
63
        if ($this->maxWorkers !== null) {
18✔
64
            return min($this->maxWorkers, $taskCount);
13✔
65
        }
66

67
        // Auto-detect: min(cpuCount - 1, taskCount, cap)
68
        $cpuCount = CpuDetector::detectCpuCount();
5✔
69
        $available = max(1, $cpuCount - 1);
5✔
70

71
        $workerCount = min($available, $taskCount, self::MAX_WORKERS_CAP);
5✔
72

73
        // Memory safety check for pcntl_fork
74
        if ($this->backend instanceof PcntlBackend) {
5✔
75
            $workerCount = $this->applyMemoryLimit($workerCount);
3✔
76
        }
77

78
        return $workerCount;
5✔
79
    }
80

81
    private function applyMemoryLimit(int $workerCount): int
3✔
82
    {
83
        $limit = self::getMemoryLimitBytes();
3✔
84
        if ($limit <= 0) {
3✔
NEW
85
            return $workerCount; // No limit set
×
86
        }
87

88
        $currentUsage = memory_get_usage(true);
3✔
89
        // Estimate ~30% of current usage per forked child as dirty page overhead
90
        $estimatedPerChild = (int) ($currentUsage * 0.3);
3✔
91

92
        if ($estimatedPerChild <= 0) {
3✔
NEW
93
            return $workerCount;
×
94
        }
95

96
        $headroom = $limit - $currentUsage;
3✔
97
        $maxSafe = (int) ($headroom / $estimatedPerChild);
3✔
98

99
        return min($workerCount, max(1, $maxSafe));
3✔
100
    }
101

102
    /**
103
     * Parse memory_limit INI value into bytes.
104
     *
105
     * @internal
106
     */
107
    public static function getMemoryLimitBytes(): int
4✔
108
    {
109
        $limit = ini_get('memory_limit');
4✔
110
        if ($limit === '' || $limit === '-1') {
4✔
NEW
111
            return 0; // No limit
×
112
        }
113

114
        $value = (int) $limit;
4✔
115
        $unit = strtolower(substr($limit, -1));
4✔
116

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

128
        return $value;
4✔
129
    }
130

131
    private static function detectBackend(): BackendInterface
5✔
132
    {
133
        if (PcntlBackend::isAvailable()) {
5✔
134
            return new PcntlBackend();
5✔
135
        }
136

NEW
137
        return new SequentialBackend();
×
138
    }
139
}
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