Files
PhpSpreadsheet/tests/Benchmark/SpreadsheetCopyBenchmarkTest.php
kemo e9e467de10 Revert copy() to serialize, keep benchmark and docblocks
Drop the clone-based copy() since benchmark results vary across PHP
versions and platforms. Keep the benchmark test (renamed to end in Test)
in a separate testsuite so users can assess which approach suits their
environment. Add docblock @see references on copy() and __clone().
2026-03-25 16:52:09 +01:00

206 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetBenchmarks;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('benchmark')]
class SpreadsheetCopyBenchmarkTest extends TestCase
{
private ?Spreadsheet $spreadsheet = null;
protected function tearDown(): void
{
if ($this->spreadsheet !== null) {
$this->spreadsheet->disconnectWorksheets();
$this->spreadsheet = null;
}
}
/**
* Build a non-trivial spreadsheet with 1000+ cells across multiple sheets,
* including data values, formulas, and styles.
*/
private function createPopulatedSpreadsheet(): Spreadsheet
{
$spreadsheet = new Spreadsheet();
// Sheet 1: Data values with styles
$dataSheet = $spreadsheet->getActiveSheet();
$dataSheet->setTitle('Data');
$columns = ['A', 'B', 'C', 'D', 'E'];
for ($row = 1; $row <= 200; ++$row) {
foreach ($columns as $colIdx => $col) {
$dataSheet->getCell("{$col}{$row}")
->setValue("Data R{$row}C" . ($colIdx + 1));
}
// Apply styles to first column
$dataSheet->getStyle("A{$row}")->applyFromArray([
'font' => [
'bold' => $row % 2 === 0,
'color' => ['argb' => 'FF003366'],
'size' => 11,
],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => $row % 3 === 0 ? 'FFFFF2CC' : 'FFD9E2F3'],
],
'borders' => [
'bottom' => [
'borderStyle' => Border::BORDER_THIN,
'color' => ['argb' => Color::COLOR_BLACK],
],
],
]);
}
// Sheet 2: Numeric data
$numericSheet = $spreadsheet->createSheet();
$numericSheet->setTitle('Numbers');
for ($row = 1; $row <= 200; ++$row) {
foreach ($columns as $colIdx => $col) {
$numericSheet->getCell("{$col}{$row}")
->setValue($row * ($colIdx + 1) * 1.5);
}
}
// Sheet 3: Formulas referencing Sheet 2
$formulaSheet = $spreadsheet->createSheet();
$formulaSheet->setTitle('Formulas');
for ($row = 1; $row <= 200; ++$row) {
$formulaSheet->getCell("A{$row}")
->setValue("=Numbers!A{$row}+Numbers!B{$row}");
$formulaSheet->getCell("B{$row}")
->setValue("=Numbers!C{$row}*2");
$formulaSheet->getCell("C{$row}")
->setValue("=SUM(Numbers!A{$row}:Numbers!E{$row})");
}
// Sheet 4: Mixed content with column widths
$mixedSheet = $spreadsheet->createSheet();
$mixedSheet->setTitle('Mixed');
for ($row = 1; $row <= 100; ++$row) {
$mixedSheet->getCell("A{$row}")->setValue("Label {$row}");
$mixedSheet->getCell("B{$row}")->setValue($row * 100);
$mixedSheet->getCell("C{$row}")->setValue($row % 2 === 0);
$mixedSheet->getCell("D{$row}")->setValue(date('Y-m-d', strtotime("2024-01-01 +{$row} days")));
}
$mixedSheet->getColumnDimension('A')->setWidth(20);
$mixedSheet->getColumnDimension('B')->setWidth(15);
return $spreadsheet;
}
/**
* Compare clone vs serialize/unserialize for copying a Spreadsheet.
*
* Performance varies across PHP versions and platforms — run this
* benchmark in your own environment to determine which is faster:
*
* vendor/bin/phpunit --group benchmark --filter SpreadsheetCopyBenchmarkTest --stderr
*/
public function testCloneVsSerializeBenchmark(): void
{
$this->spreadsheet = $this->createPopulatedSpreadsheet();
// Count total cells to confirm we have 1000+
$totalCells = 0;
foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
$totalCells += count($sheet->getCellCollection()->getCoordinates());
}
self::assertGreaterThanOrEqual(1000, $totalCells, 'Spreadsheet must have at least 1000 cells');
// Warm up (first run may include autoloading overhead)
$warmup = clone $this->spreadsheet;
$warmup->disconnectWorksheets();
unset($warmup);
/** @var Spreadsheet $warmup */
$warmup = unserialize(serialize($this->spreadsheet));
$warmup->disconnectWorksheets();
unset($warmup);
$iterations = 5;
// Benchmark clone
gc_collect_cycles();
$cloneMemBefore = memory_get_usage(true);
$cloneStart = hrtime(true);
$clones = [];
for ($i = 0; $i < $iterations; ++$i) {
$clones[] = clone $this->spreadsheet;
}
$cloneEnd = hrtime(true);
$cloneMemAfter = memory_get_usage(true);
foreach ($clones as $c) {
$c->disconnectWorksheets();
}
unset($clones);
gc_collect_cycles();
$cloneTimeMs = ($cloneEnd - $cloneStart) / 1_000_000;
$cloneAvgMs = $cloneTimeMs / $iterations;
$cloneMemDeltaMb = ($cloneMemAfter - $cloneMemBefore) / 1024 / 1024;
// Benchmark serialize/unserialize
gc_collect_cycles();
$serializeMemBefore = memory_get_usage(true);
$serializeStart = hrtime(true);
$serialized = [];
for ($i = 0; $i < $iterations; ++$i) {
$serialized[] = unserialize(serialize($this->spreadsheet));
}
$serializeEnd = hrtime(true);
$serializeMemAfter = memory_get_usage(true);
foreach ($serialized as $s) {
/** @var Spreadsheet $s */
$s->disconnectWorksheets();
}
unset($serialized);
gc_collect_cycles();
$serializeTimeMs = ($serializeEnd - $serializeStart) / 1_000_000;
$serializeAvgMs = $serializeTimeMs / $iterations;
$serializeMemDeltaMb = ($serializeMemAfter - $serializeMemBefore) / 1024 / 1024;
// Output results for manual comparison
fwrite(STDERR, "\n");
fwrite(STDERR, "=== Spreadsheet Copy Benchmark ({$totalCells} cells, {$iterations} iterations) ===\n");
fwrite(STDERR, sprintf(" PHP version: %s (%s)\n", PHP_VERSION, PHP_OS));
fwrite(STDERR, sprintf(" clone: %.2f ms avg (%.2f ms total)\n", $cloneAvgMs, $cloneTimeMs));
fwrite(STDERR, sprintf(" serialize/unserialize: %.2f ms avg (%.2f ms total)\n", $serializeAvgMs, $serializeTimeMs));
fwrite(STDERR, sprintf(" Clone memory delta: %.2f MB\n", $cloneMemDeltaMb));
fwrite(STDERR, sprintf(" Serialize memory delta: %.2f MB\n", $serializeMemDeltaMb));
fwrite(STDERR, "\n");
// Assert both approaches produce spreadsheets with the expected cell count
$cloneCopy = clone $this->spreadsheet;
$cloneCells = 0;
foreach ($cloneCopy->getWorksheetIterator() as $sheet) {
$cloneCells += count($sheet->getCellCollection()->getCoordinates());
}
self::assertSame($totalCells, $cloneCells, 'Clone must preserve all cells');
$cloneCopy->disconnectWorksheets();
/** @var Spreadsheet $serializeCopy */
$serializeCopy = unserialize(serialize($this->spreadsheet));
$serializeCells = 0;
foreach ($serializeCopy->getWorksheetIterator() as $sheet) {
$serializeCells += count($sheet->getCellCollection()->getCoordinates());
}
self::assertSame($totalCells, $serializeCells, 'Serialize copy must preserve all cells');
$serializeCopy->disconnectWorksheets();
}
}