mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-08-26 18:08:34 +00:00
224 lines
8.6 KiB
PHP
224 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace PhpOffice\PhpSpreadsheetBenchmarks;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
#[\PHPUnit\Framework\Attributes\Group('benchmark')]
|
|
/**
|
|
* @codeCoverageIgnore
|
|
*/
|
|
class CsvStreamingEncodingBenchmarkTest extends TestCase
|
|
{
|
|
/** @var string[] */
|
|
private array $tempFiles = [];
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
foreach ($this->tempFiles as $file) {
|
|
if (file_exists($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
$this->tempFiles = [];
|
|
}
|
|
|
|
/**
|
|
* Generate a CSV file in ISO-8859-1 encoding with accented characters.
|
|
*/
|
|
private function generateIso88591Csv(int $rows, int $cols = 10): string
|
|
{
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'csv_bench_iso_') . '.csv';
|
|
$this->tempFiles[] = $tempFile;
|
|
|
|
$handle = fopen($tempFile, 'wb');
|
|
self::assertNotFalse($handle);
|
|
|
|
// Accented characters in ISO-8859-1: é(0xE9), ö(0xF6), ü(0xFC), ñ(0xF1), à(0xE0), ç(0xE7)
|
|
$specialChars = ["\xE9", "\xF6", "\xFC", "\xF1", "\xE0", "\xE7"];
|
|
|
|
for ($r = 0; $r < $rows; ++$r) {
|
|
$fields = [];
|
|
for ($c = 0; $c < $cols; ++$c) {
|
|
$accent = $specialChars[($r + $c) % count($specialChars)];
|
|
$fields[] = sprintf('Cell_%d_%d_%s_data_with_special_chars_%s', $r, $c, $accent, $accent);
|
|
}
|
|
fwrite($handle, implode(',', $fields) . "\n");
|
|
}
|
|
|
|
fclose($handle);
|
|
|
|
return $tempFile;
|
|
}
|
|
|
|
/**
|
|
* Generate a CSV file in UTF-16LE encoding with BOM and accented characters.
|
|
*/
|
|
private function generateUtf16LeCsv(int $rows, int $cols = 10): string
|
|
{
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'csv_bench_u16_') . '.csv';
|
|
$this->tempFiles[] = $tempFile;
|
|
|
|
$handle = fopen($tempFile, 'wb');
|
|
self::assertNotFalse($handle);
|
|
|
|
// Write UTF-16LE BOM
|
|
fwrite($handle, "\xFF\xFE");
|
|
|
|
$specialChars = ['é', 'ö', 'ü', 'ñ', 'à', 'ç'];
|
|
|
|
for ($r = 0; $r < $rows; ++$r) {
|
|
$fields = [];
|
|
for ($c = 0; $c < $cols; ++$c) {
|
|
$accent = $specialChars[($r + $c) % count($specialChars)];
|
|
$fields[] = sprintf('Cell_%d_%d_%s_data_with_special_chars_%s', $r, $c, $accent, $accent);
|
|
}
|
|
$line = implode(',', $fields) . "\n";
|
|
// Convert UTF-8 line to UTF-16LE
|
|
$converted = mb_convert_encoding($line, 'UTF-16LE', 'UTF-8');
|
|
fwrite($handle, $converted);
|
|
}
|
|
|
|
fclose($handle);
|
|
|
|
return $tempFile;
|
|
}
|
|
|
|
/**
|
|
* Read a CSV file with the given encoding and return timing/memory stats.
|
|
*
|
|
* @return array{time_ms: float, memory_used_mb: float, rows: int}
|
|
*/
|
|
private function benchmarkRead(string $filename, string $encoding): array
|
|
{
|
|
// Force garbage collection before measurement
|
|
gc_collect_cycles();
|
|
$memoryBefore = memory_get_usage(true);
|
|
|
|
$reader = new CsvChunk();
|
|
$reader->setInputEncoding($encoding);
|
|
|
|
$startTime = hrtime(true);
|
|
$spreadsheet = $reader->load($filename);
|
|
$endTime = hrtime(true);
|
|
|
|
$memoryAfter = memory_get_usage(true);
|
|
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
$rows = $sheet->getHighestRow();
|
|
|
|
$spreadsheet->disconnectWorksheets();
|
|
|
|
return [
|
|
'time_ms' => ($endTime - $startTime) / 1_000_000,
|
|
'memory_used_mb' => ($memoryAfter - $memoryBefore) / 1024 / 1024,
|
|
'rows' => $rows,
|
|
];
|
|
}
|
|
|
|
public function testStreamingEncodingIso88591(): void
|
|
{
|
|
$filename = $this->generateIso88591Csv(5000);
|
|
$fileSize = filesize($filename);
|
|
|
|
$result = $this->benchmarkRead($filename, 'ISO-8859-1');
|
|
|
|
fwrite(STDERR, "\n");
|
|
fwrite(STDERR, "=== ISO-8859-1 Streaming Encoding Benchmark ===\n");
|
|
fwrite(STDERR, sprintf("File size: %.2f MB\n", $fileSize / 1024 / 1024));
|
|
fwrite(STDERR, sprintf("Rows read: %d\n", $result['rows']));
|
|
fwrite(STDERR, sprintf("Time: %.2f ms\n", $result['time_ms']));
|
|
fwrite(STDERR, sprintf("Memory used: %.2f MB\n", $result['memory_used_mb']));
|
|
|
|
self::assertSame(5000, $result['rows']);
|
|
}
|
|
|
|
public function testStreamingEncodingUtf16Le(): void
|
|
{
|
|
$filename = $this->generateUtf16LeCsv(5000);
|
|
$fileSize = filesize($filename);
|
|
|
|
$result = $this->benchmarkRead($filename, 'UTF-16LE');
|
|
|
|
fwrite(STDERR, "\n");
|
|
fwrite(STDERR, "=== UTF-16LE Streaming Encoding Benchmark ===\n");
|
|
fwrite(STDERR, sprintf("File size: %.2f MB\n", $fileSize / 1024 / 1024));
|
|
fwrite(STDERR, sprintf("Rows read: %d\n", $result['rows']));
|
|
fwrite(STDERR, sprintf("Time: %.2f ms\n", $result['time_ms']));
|
|
fwrite(STDERR, sprintf("Memory used: %.2f MB\n", $result['memory_used_mb']));
|
|
|
|
self::assertSame(5000, $result['rows']);
|
|
}
|
|
|
|
public function testMemoryScalingWithFileSize(): void
|
|
{
|
|
// Generate small and large ISO-8859-1 CSV files
|
|
$smallFile = $this->generateIso88591Csv(1000);
|
|
$largeFile = $this->generateIso88591Csv(5000);
|
|
|
|
$smallFileSize = filesize($smallFile);
|
|
$largeFileSize = filesize($largeFile);
|
|
|
|
$smallResult = $this->benchmarkRead($smallFile, 'ISO-8859-1');
|
|
$largeResult = $this->benchmarkRead($largeFile, 'ISO-8859-1');
|
|
|
|
$fileSizeRatio = $largeFileSize / $smallFileSize;
|
|
$smallMemory = $smallResult['memory_used_mb'];
|
|
$memoryRatio = 0;
|
|
if ($smallMemory > 0) {
|
|
$memoryRatio = $largeResult['memory_used_mb'] / $smallMemory;
|
|
}
|
|
|
|
fwrite(STDERR, "\n");
|
|
fwrite(STDERR, "=== Memory Scaling Comparison (ISO-8859-1) ===\n");
|
|
fwrite(STDERR, sprintf("Small file (1000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $smallFileSize / 1024 / 1024, $smallResult['memory_used_mb'], $smallResult['time_ms']));
|
|
fwrite(STDERR, sprintf("Large file (5000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $largeFileSize / 1024 / 1024, $largeResult['memory_used_mb'], $largeResult['time_ms']));
|
|
fwrite(STDERR, sprintf("File size ratio (large/small): %.2fx\n", $fileSizeRatio));
|
|
fwrite(STDERR, sprintf("Memory ratio (large/small): %.2fx\n", $memoryRatio));
|
|
fwrite(STDERR, sprintf("Streaming keeps memory sub-linear: %s\n", $memoryRatio < $fileSizeRatio ? 'YES' : 'NO'));
|
|
|
|
// With streaming, memory should not scale linearly with file size.
|
|
// The encoding conversion itself uses constant memory (CHUNK_SIZE),
|
|
// though the spreadsheet object will still grow with row count.
|
|
// We verify memory ratio is less than file size ratio.
|
|
self::assertLessThan(
|
|
$fileSizeRatio,
|
|
$memoryRatio,
|
|
'Peak memory should grow sub-linearly relative to file size due to streaming encoding conversion'
|
|
);
|
|
|
|
// Also generate small and large UTF-16LE files
|
|
$smallUtf16 = $this->generateUtf16LeCsv(1000);
|
|
$largeUtf16 = $this->generateUtf16LeCsv(5000);
|
|
|
|
$smallUtf16Size = filesize($smallUtf16);
|
|
$largeUtf16Size = filesize($largeUtf16);
|
|
|
|
$smallUtf16Result = $this->benchmarkRead($smallUtf16, 'UTF-16LE');
|
|
$largeUtf16Result = $this->benchmarkRead($largeUtf16, 'UTF-16LE');
|
|
|
|
$utf16FileSizeRatio = $largeUtf16Size / $smallUtf16Size;
|
|
$smallMemory = $smallUtf16Result['memory_used_mb'];
|
|
$utf16MemoryRatio = 0;
|
|
if ($smallMemory > 0) {
|
|
$utf16MemoryRatio = $largeUtf16Result['memory_used_mb'] / $smallMemory;
|
|
}
|
|
|
|
fwrite(STDERR, "\n");
|
|
fwrite(STDERR, "=== Memory Scaling Comparison (UTF-16LE) ===\n");
|
|
fwrite(STDERR, sprintf("Small file (1000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $smallUtf16Size / 1024 / 1024, $smallUtf16Result['memory_used_mb'], $smallUtf16Result['time_ms']));
|
|
fwrite(STDERR, sprintf("Large file (5000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $largeUtf16Size / 1024 / 1024, $largeUtf16Result['memory_used_mb'], $largeUtf16Result['time_ms']));
|
|
fwrite(STDERR, sprintf("File size ratio (large/small): %.2fx\n", $utf16FileSizeRatio));
|
|
fwrite(STDERR, sprintf("Memory ratio (large/small): %.2fx\n", $utf16MemoryRatio));
|
|
fwrite(STDERR, sprintf("Streaming keeps memory sub-linear: %s\n", $utf16MemoryRatio < $utf16FileSizeRatio ? 'YES' : 'NO'));
|
|
|
|
self::assertLessThan(
|
|
$utf16FileSizeRatio,
|
|
$utf16MemoryRatio,
|
|
'Peak memory should grow sub-linearly relative to file size due to streaming encoding conversion (UTF-16LE)'
|
|
);
|
|
}
|
|
}
|