add PHPBench to run benchmarks

This commit is contained in:
smiley
2024-04-30 21:15:17 +02:00
parent e633382b58
commit f31eff72f5
19 changed files with 1034 additions and 366 deletions
+1
View File
@@ -25,6 +25,7 @@ return [
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'benchmark',
'examples',
'src',
'tests',
+148
View File
@@ -0,0 +1,148 @@
<?php
/**
* Class BenchmarkAbstract
*
* @created 23.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCodeTest\QRMaxLengthTrait;
use PhpBench\Attributes\{Iterations, ParamProviders, Revs, Warmup};
use Generator, RuntimeException;
use function extension_loaded, is_dir, mb_substr, mkdir, sprintf, str_repeat, str_replace;
/**
* The abstract benchmark with common methods
*/
#[Iterations(3)]
#[Warmup(3)]
#[Revs(100)]
#[ParamProviders(['versionProvider', 'eccLevelProvider', 'dataModeProvider'])]
abstract class BenchmarkAbstract{
use QRMaxLengthTrait;
protected const BUILDDIR = __DIR__.'/../.build/phpbench/';
protected const ECC_LEVELS = [EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H];
protected const DATAMODES = Mode::INTERFACES;
protected array $dataModeData;
protected string $testData;
protected QROptions $options;
protected QRMatrix $matrix;
// properties from data providers
protected Version $version;
protected EccLevel $eccLevel;
protected int $mode;
protected string $modeFQCN;
/**
*
*/
public function __construct(){
foreach(['gd', 'imagick'] as $ext){
if(!extension_loaded($ext)){
throw new RuntimeException(sprintf('ext-%s not loaded', $ext));
}
}
if(!is_dir(self::BUILDDIR)){
mkdir(directory: self::BUILDDIR, recursive: true);
}
$this->dataModeData = $this->generateDataModeData();
}
/**
* Generates test data strings for each mode
*/
protected function generateDataModeData():array{
return [
Mode::NUMBER => str_repeat('0123456789', 750),
Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100),
Mode::KANJI => str_repeat('漂う花の香り', 350),
Mode::HANZI => str_repeat('无可奈何燃花作香', 250),
Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100),
];
}
/**
* Generates a test max-length data string for the given version, ecc level and data mode
*/
protected function getData(Version $version, EccLevel $eccLevel, int $mode):string{
$maxLength = self::getMaxLengthForMode($mode, $version, $eccLevel);
if($mode === Mode::KANJI || $mode === Mode::HANZI){
return mb_substr($this->dataModeData[$mode], 0, $maxLength);
}
return mb_substr($this->dataModeData[$mode], 0, $maxLength, '8bit');
}
/**
* Initializes a QROptions instance and assigns it to its temp property
*/
protected function initQROptions(array $options):void{
$this->options = new QROptions($options);
}
/**
* Initializes a QRMatrix instance and assigns it to its temp property
*/
public function initMatrix():void{
$this->matrix = (new QRCode($this->options))
->addByteSegment($this->testData)
->getQRMatrix()
;
}
/**
* Generates a test data string and assigns it to its temp property
*/
public function generateTestData():void{
$this->testData = $this->getData($this->version, $this->eccLevel, $this->mode);
}
/**
* Assigns the parameter array from the providers to properties and enforces the types
*/
public function assignParams(array $params):void{
foreach($params as $k => $v){
$this->{$k} = $v;
}
}
public function versionProvider():Generator{
for($v = 1; $v <= 40; $v++){
$version = new Version($v);
yield (string)$version => ['version' => $version];
}
}
public function eccLevelProvider():Generator{
foreach(static::ECC_LEVELS as $ecc){
$eccLevel = new EccLevel($ecc);
yield (string)$eccLevel => ['eccLevel' => $eccLevel];
}
}
public function dataModeProvider():Generator{
foreach(static::DATAMODES as $mode => $modeFQCN){
$name = str_replace('chillerlan\\QRCode\\Data\\', '', $modeFQCN);
yield $name => ['mode' => $mode, 'modeFQCN' => $modeFQCN];
}
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* Class DecoderBenchmark
*
* @created 26.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\Common\{GDLuminanceSource, IMagickLuminanceSource, Mode};
use chillerlan\QRCode\Data\Byte;
use chillerlan\QRCode\Decoder\{Decoder, DecoderResult};
use chillerlan\QRCode\Output\QRGdImagePNG;
use chillerlan\QRCode\QRCodeException;
use PhpBench\Attributes\{AfterMethods, BeforeMethods, Subject};
use RuntimeException;
/**
* Tests the performance of the QR Code reader/decoder
*/
final class DecoderBenchmark extends BenchmarkAbstract{
protected const DATAMODES = [Mode::BYTE => Byte::class];
private string $imageBlob;
private DecoderResult $result;
public function initOptions():void{
$options = [
'version' => $this->version->getVersionNumber(),
'eccLevel' => $this->eccLevel->getLevel(),
'scale' => 2,
'imageTransparent' => false,
'outputBase64' => false,
];
$this->initQROptions($options);
}
public function generateImageBlob():void{
$this->imageBlob = (new QRGdImagePNG($this->options, $this->matrix))->dump();
}
public function checkReaderResult():void{
if($this->result->data !== $this->testData){
throw new RuntimeException('invalid reader result');
}
}
/**
* Tests the performance of the GD reader
*/
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])]
#[AfterMethods(['checkReaderResult'])]
public function GDLuminanceSource():void{
// in rare cases the reader will be unable to detect and throw,
// but we don't want the performance test to yell about it
// @see QRCodeReaderTestAbstract::testReadData()
try{
$this->result = (new Decoder($this->options))
->decode(GDLuminanceSource::fromBlob($this->imageBlob, $this->options));
}
catch(QRCodeException){
// noop
}
}
/**
* Tests the performance of the ImageMagick reader
*/
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])]
#[AfterMethods(['checkReaderResult'])]
public function IMagickLuminanceSource():void{
$this->options->readerUseImagickIfAvailable = true;
try{
$this->result = (new Decoder($this->options))
->decode(IMagickLuminanceSource::fromBlob($this->imageBlob, $this->options));
}
catch(QRCodeException){
// noop
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* Class MaskPatternBenchmark
*
* @created 23.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\Common\{MaskPattern, Mode};
use chillerlan\QRCode\Data\Byte;
use PhpBench\Attributes\{BeforeMethods, Subject};
/**
* Tests the performance of the mask pattern penalty testing
*/
final class MaskPatternBenchmark extends BenchmarkAbstract{
protected const DATAMODES = [Mode::BYTE => Byte::class];
public function initOptions():void{
$options = [
'version' => $this->version->getVersionNumber(),
'eccLevel' => $this->eccLevel->getLevel(),
];
$this->initQROptions($options);
}
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])]
public function getBestPattern():void{
MaskPattern::getBestPattern($this->matrix);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* Class OutputBenchmark
*
* @created 23.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\Common\{EccLevel, Mode};
use chillerlan\QRCode\Data\Byte;
use chillerlan\QRCode\Output\{
QREps, QRFpdf, QRGdImageJPEG, QRGdImagePNG, QRGdImageWEBP, QRImagick, QRMarkupSVG, QRStringJSON
};
use PhpBench\Attributes\{BeforeMethods, Subject};
/**
* Tests the performance of the built-in output classes
*/
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])]
final class OutputBenchmark extends BenchmarkAbstract{
protected const ECC_LEVELS = [EccLevel::H];
protected const DATAMODES = [Mode::BYTE => Byte::class];
public function initOptions():void{
$options = [
'version' => $this->version->getVersionNumber(),
'eccLevel' => $this->eccLevel->getLevel(),
'connectPaths' => true,
'drawLightModules' => true,
'drawCircularModules' => true,
'gdImageUseUpscale' => false, // set to false to allow proper comparison
];
$this->initQROptions($options);
}
#[Subject]
public function QREps():void{
(new QREps($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRFpdf():void{
(new QRFpdf($this->options, $this->matrix))->dump();
}
/**
* for some reason imageavif() is extremely slow, ~50x slower than imagepng()
*/
# #[Subject]
# public function QRGdImageAVIF():void{
# (new QRGdImageAVIF($this->options, $this->matrix))->dump();
# }
#[Subject]
public function QRGdImageJPEG():void{
(new QRGdImageJPEG($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRGdImagePNG():void{
(new QRGdImagePNG($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRGdImageWEBP():void{
(new QRGdImageWEBP($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRImagick():void{
(new QRImagick($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRMarkupSVG():void{
(new QRMarkupSVG($this->options, $this->matrix))->dump();
}
#[Subject]
public function QRStringJSON():void{
(new QRStringJSON($this->options, $this->matrix))->dump();
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* Class QRCodeBenchmark
*
* @created 23.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\Output\QRStringJSON;
use chillerlan\QRCode\QRCode;
use PhpBench\Attributes\{BeforeMethods, Subject};
/**
* Tests the overall performance of the QRCode class
*/
final class QRCodeBenchmark extends BenchmarkAbstract{
public function initOptions():void{
$options = [
'outputInterface' => QRStringJSON::class,
'version' => $this->version->getVersionNumber(),
'eccLevel' => $this->eccLevel->getLevel(),
];
$this->initQROptions($options);
}
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions'])]
public function render():void{
(new QRCode($this->options))->addSegment(new $this->modeFQCN($this->testData))->render();
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Class QRDataBenchmark
*
* @created 23.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use chillerlan\QRCode\Common\BitBuffer;
use chillerlan\QRCode\Data\QRData;
use PhpBench\Attributes\{BeforeMethods, Subject};
/**
* Tests the QRMatrix write performance
*/
final class QRDataBenchmark extends BenchmarkAbstract{
private QRData $qrData;
private BitBuffer $bitBuffer;
public function initOptions():void{
$options = [
'version' => $this->version->getVersionNumber(),
'eccLevel' => $this->eccLevel->getLevel(),
];
$this->initQROptions($options);
}
public function initQRData():void{
$this->qrData = new QRData($this->options, [new $this->modeFQCN($this->testData)]);
}
public function initBitBuffer():void{
$this->bitBuffer = $this->qrData->getBitBuffer();
$this->bitBuffer->read(4); // read data mode indicator
}
/**
* Tests the performance of QRData invovcation, includes QRData::writeBitBuffer()
*/
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions'])]
public function invocation():void{
/** @phan-suppress-next-line PhanNoopNew */
new QRData($this->options, [new $this->modeFQCN($this->testData)]);
}
/**
* Tests the performance of QRData::writeMatrix(), includes QRMatrix::writeCodewords() and the ReedSolomonEncoder
*/
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initQRData'])]
public function writeMatrix():void{
$this->qrData->writeMatrix();
}
/**
* Tests the performance of QRDataModeInterface::decodeSegment()
*
* we need to clone the BitBuffer instance here because its internal state is modified during decoding
*/
#[Subject]
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initQRData', 'initBitBuffer'])]
public function decodeSegment():void{
/** @noinspection PhpUndefinedMethodInspection */
$this->modeFQCN::decodeSegment(clone $this->bitBuffer, $this->version->getVersionNumber());
}
}
+64
View File
@@ -0,0 +1,64 @@
body {
font-size: 16px;
line-height: 1.25em;
font-family: "Verdana", sans-serif;
color: #000;
}
code{
font-family: "Consolas", monospace;
}
table {
border-collapse: collapse;
}
td{
padding: 0.25em 0.5em;
}
thead > tr:nth-child(1) > th:nth-child(1n+2),
thead > tr:nth-child(even) > th:nth-child(4n+2),
tbody > tr > td:nth-child(4n+2) {
border-left: 2px solid #999;
}
tr:nth-child(odd),
thead > tr > th:nth-child(even),
thead > tr:nth-child(2) > th:first-child {
background: rgba(0, 0, 0, 0.1);
}
tbody > tr > td:first-child {
background: rgba(0, 0, 0, 0.1);
font-weight: bold;
}
tbody > tr > td:nth-child(1n+2) {
text-align: right;
}
thead > tr:nth-child(2) > th:nth-child(4n+2),
tbody > tr > td:nth-child(4n+2) {
background: rgba(0, 255, 0, 0.1);
}
thead > tr:nth-child(2) > th:nth-child(4n+3),
tbody > tr > td:nth-child(4n+3) {
background: rgba(255, 255, 0, 0.1);
}
thead > tr:nth-child(2) > th:nth-child(4n+4),
tbody > tr > td:nth-child(4n+4) {
background: rgba(0, 0, 255, 0.1);
}
thead > tr:nth-child(2) > th:nth-child(4n+5),
tbody > tr > td:nth-child(4n+5) {
background: rgba(255, 0, 0, 0.1);
}
a.return{
display: block;
margin: 1em 0;
}
+138
View File
@@ -0,0 +1,138 @@
<?php
/**
* Generates a benchmark report in HTML
*
* @created 30.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*
* @phan-file-suppress PhanTypeArraySuspiciousNullable
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use RuntimeException;
use function array_keys;
use function copy;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function htmlspecialchars;
use function implode;
use function is_bool;
use function is_dir;
use function json_decode;
use function mkdir;
use function sprintf;
require_once __DIR__.'/parse-common.php';
if(!file_exists(FILE.'.json')){
throw new RuntimeException('invalid benchmark report');
}
$json = json_decode(file_get_contents(FILE.'.json'), true);
$htmlHead = '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="./benchmark.css">
<title>%s results</title>
</head>
<body>';
$htmlFoot = '</body>
</html>';
$html = [];
// General information/overview
$env = $json['env'];
$suite = $json['suite'];
$html['index'][] = sprintf($htmlHead, 'Benchmark');
$html['index'][] = '<h1>Benchmark results</h1>';
$html['index'][] = '<h2>Environment</h2>';
$html['index'][] = '<table><thead>';
$html['index'][] = '<tr><th>Name</th><th>Value</th></tr>';
$html['index'][] = '</thead><tbody>';
$html['index'][] = sprintf('<tr><td>date</td><td style="text-align: left;">%s %s</td></tr>', $suite['date'], $suite['time']);
$html['index'][] = sprintf('<tr><td>environment</td><td style="text-align: left;">%s %s, %s</td></tr>', $env['uname_os'], $env['uname_version'], $env['uname_machine']);
$html['index'][] = sprintf('<tr><td>tag</td><td style="text-align: left;">%s</td></tr>', htmlspecialchars($suite['tag']));
foreach(['php_version', 'php_ini', 'php_extensions', 'php_xdebug', 'opcache_extension_loaded', 'opcache_enabled'] as $field){
// prettify the boolean values
if(is_bool($env[$field])){
$env[$field] = ($env[$field]) ? '✓' : '✗';
}
$html['index'][] = sprintf('<tr><td>%s</td><td style="text-align: left;">%s</td></tr>', $field, $env[$field]);
}
$html['index'][] = '</tbody></table>';
// list indiviidual reports
$html['index'][] = '<h2>Reports</h2>';
$list = [];
foreach(array_keys($json['benchmark']) as $benchmark){
// add a file & header
$html[$benchmark][] = sprintf($htmlHead, $benchmark);
$html[$benchmark][] = sprintf('<h1>%s</h1>', $benchmark);
$html[$benchmark][] = sprintf('<code>%s</code>', $json['benchmark'][$benchmark]['class']);
foreach(array_keys($json['benchmark'][$benchmark]['subjects']) as $subject){
// list item
$list[$benchmark][] = $subject;
$subj = $json['benchmark'][$benchmark]['subjects'][$subject];
$html[$benchmark][] = sprintf('<h2 id="%1$s">%1$s</h2>', $subject);
$html[$benchmark][] = sprintf('<div>Revs: %s, Iterations: %s</div>', $subj['revs'], $subj['iterations']);
$html[$benchmark][] = parseVariants($subj['variants']);
}
// close document
$html[$benchmark][] = '<a class="return" href="./index.html">back to overview</a>';
$html[$benchmark][] = $htmlFoot;
}
// create overview list
$html['index'][] = '<ul>';
foreach($list as $benchmark => $subjects){
// list item
$html['index'][] = sprintf('<li><a href="./%1$s.html">%1$s</a><ul>', $benchmark);
foreach($subjects as $subject){
// list sub-item
$html['index'][] = sprintf('<li><a href="./%1$s.html#%2$s">%2$s</a></li>', $benchmark, $subject);
}
$html['index'][] = '</ul></li>';
}
$html['index'][] = '</ul>';
// close document
$html['index'][] = $htmlFoot;
if(!is_dir(BUILDDIR.'/html/')){
mkdir(BUILDDIR.'/html/');
}
copy('./benchmark.css', BUILDDIR.'/html/benchmark.css');
foreach($html as $file => $content){
file_put_contents(BUILDDIR.'/html/'.$file.'.html', implode("\n", $content));
}
+107
View File
@@ -0,0 +1,107 @@
<?php
/**
* Generates a benchmark report in Markdown
*
* @created 27.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*
* @phan-file-suppress PhanTypeArraySuspiciousNullable
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use RuntimeException;
use function array_keys;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function implode;
use function is_bool;
use function is_dir;
use function json_decode;
use function mkdir;
use function sprintf;
use function strtolower;
require_once __DIR__.'/parse-common.php';
if(!file_exists(FILE.'.json')){
throw new RuntimeException('invalid benchmark report');
}
$json = json_decode(file_get_contents(FILE.'.json'), true);
$markdown = [];
// General information/overview
$env = $json['env'];
$suite = $json['suite'];
$markdown['Readme'][] = "# Benchmark results\n";
$markdown['Readme'][] = "## Environment\n";
$markdown['Readme'][] = '| Name | Value |';
$markdown['Readme'][] = '|------|-------|';
$markdown['Readme'][] = sprintf('| date | %s %s |', $suite['date'], $suite['time']);
$markdown['Readme'][] = sprintf('| environment | %s %s, %s |', $env['uname_os'], $env['uname_version'], $env['uname_machine']);
$markdown['Readme'][] = sprintf('| tag | %s |', $suite['tag']);
foreach(['php_version', 'php_ini', 'php_extensions', 'php_xdebug', 'opcache_extension_loaded', 'opcache_enabled'] as $field){
// prettify the boolean values
if(is_bool($env[$field])){
$env[$field] = ($env[$field]) ? '✓' : '✗';
}
$markdown['Readme'][] = sprintf('| %s | %s |', $field, $env[$field]);
}
// list indiviidual reports
$markdown['Readme'][] = '';
$markdown['Readme'][] = '## Reports';
$markdown['Readme'][] = '';
$list = [];
foreach(array_keys($json['benchmark']) as $benchmark){
// add a file & header
$markdown[$benchmark][] = sprintf("# %s\n", $benchmark);
$markdown[$benchmark][] = sprintf("`%s`\n", $json['benchmark'][$benchmark]['class']);
foreach(array_keys($json['benchmark'][$benchmark]['subjects']) as $subject){
// list item
$list[$benchmark][] = $subject;
$subj = $json['benchmark'][$benchmark]['subjects'][$subject];
$markdown[$benchmark][] = sprintf("## %s\n", $subject);
$markdown[$benchmark][] = sprintf("**Revs: %s, Iterations: %s**\n", $subj['revs'], $subj['iterations']);
$markdown[$benchmark][] = parseVariants($subj['variants']);
$markdown[$benchmark][] = '';
}
$markdown[$benchmark][] = '[back to overview](./Benchmark.md)';
}
// create overview list
foreach($list as $benchmark => $subjects){
// list item
$markdown['Readme'][] = sprintf('- [%1$s](./%1$s.md)', $benchmark);
foreach($subjects as $subject){
// list sub-item
$markdown['Readme'][] = sprintf(' - [%2$s](./%1$s.md#%3$s)', $benchmark, $subject, strtolower($subject));
}
}
if(!is_dir(BUILDDIR.'/markdown/')){
mkdir(BUILDDIR.'/markdown/');
}
foreach($markdown as $file => $content){
file_put_contents(BUILDDIR.'/markdown/'.$file.'.md', implode("\n", $content));
}
+78
View File
@@ -0,0 +1,78 @@
<?php
/**
* @created 27.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use function array_column;
use function array_key_first;
use function array_keys;
use function array_map;
use function array_sum;
use function explode;
use function implode;
use function intdiv;
use function rsort;
use function sprintf;
use function str_repeat;
use const SORT_NUMERIC;
const BUILDDIR = __DIR__.'/../.build/phpbench';
const FILE = BUILDDIR.'/benchmark'; // without extension
function parseVariants(array $variants):string{
$data = [];
foreach($variants as $variant){
[$version, $ecc, $mode] = explode(',', $variant['name']);
$data[(int)$version][$mode][$ecc] = parseVariantResults($variant['results']);
}
$v = $data[array_key_first($data)];
$modeKeys = array_keys($v);
$eccKeys = array_keys($v[$modeKeys[0]]);
$modeHeaders = array_map(fn($mode) => sprintf('<th colspan="4">%s</th>', $mode), $modeKeys);
$eccHeaders = array_map(fn($ecc) => sprintf('<th>%s</th>', $ecc), $eccKeys);
$table = ['<table><thead>'];
$table[] = sprintf('<tr><th></th>%s</tr>', implode('', $modeHeaders));
$table[] = sprintf('<tr><th>Version</th>%s</tr>', str_repeat(implode('', $eccHeaders), count($modeKeys)));
$table[] = '</thead><tbody>';
foreach($data as $version => $modes){
$results = [];
foreach($modes as $eccLevels){
foreach($eccLevels as [$time_avg, $mem_peak]){
$results[] = sprintf('<td>%01.3f</td>', $time_avg);
}
}
$table[] = sprintf('<tr><td>%s</td>%s</tr>', $version, implode('', $results));
}
$table[] = '</tbody></table>';
return implode("\n", $table);
}
function parseVariantResults(array $results):array{
$iterations = count($results);
$mem_peak = array_column($results, 'mem_peak');
rsort($mem_peak, SORT_NUMERIC);
return [
(array_sum(array_column($results, 'time_avg')) / $iterations / 1000),
intdiv($mem_peak[0], 1024),
];
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* Parses the CSV result into more handy JSON
*
* @created 26.04.2024
* @author smiley <smiley@chillerlan.net>
* @copyright 2024 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\QRCodeBenchmark;
use function array_combine;
use function array_map;
use function array_shift;
use function explode;
use function file_get_contents;
use function file_put_contents;
use function floatval;
use function intval;
use function json_encode;
use function str_replace;
use function str_starts_with;
use function trim;
use const JSON_PRESERVE_ZERO_FRACTION;
use const JSON_PRETTY_PRINT;
require_once __DIR__.'/parse-common.php';
const SEPARATOR = '|';
function parseLine(string $line):array{
return array_map(fn(string $str):string => trim($str, "\ \n\r\t\v\0\""), explode(SEPARATOR, $line));
}
$csv = explode("\n", trim(file_get_contents(FILE.'.csv')));
$header = parseLine(array_shift($csv));
$parsed = array_map(fn(string $line):array => array_combine($header, parseLine($line)), $csv);
$json = ['env' => [], 'suite' => [], 'benchmark' => []];
foreach($parsed as $i => $result){
// booleans
foreach(['has_baseline', 'env_php_xdebug', 'env_opcache_extension_loaded', 'env_opcache_enabled'] as $bool){
$result[$bool] = (bool)$result[$bool];
}
// integers
foreach(['variant_index', 'variant_revs', 'variant_iterations', 'iteration_index', 'result_time_net', 'result_time_revs', 'result_mem_peak', 'result_mem_real', 'result_mem_final'] as $int){
$result[$int] = intval($result[$int]);
}
// floats
foreach(['env_sampler_nothing', 'env_sampler_md5', 'env_sampler_file_rw', 'result_time_avg', 'result_comp_z_value', 'result_comp_deviation'] as $float){
$result[$float] = floatval($result[$float]);
}
// arrays
foreach(['subject_groups', 'variant_params'] as $array){
$val = trim($result[$array], '"[]');
if($val === ''){
$result[$array] = [];
continue;
}
$val = array_map('trim', explode(',', $val));
$result[$array] = $val;
}
// rename some things to avoid bloat
$result['subject_revs'] = $result['variant_revs'];
$result['subject_iterations'] = $result['variant_iterations'];
$result['result_index'] = $result['iteration_index'];
unset($result['variant_revs'], $result['variant_iterations'], $result['iteration_index'], $result['result_time_revs']);
// add the class name
$json['benchmark'][$result['benchmark_name']]['class'] = $result['benchmark_class'];
foreach($result as $k => $v){
// the environment info is only needed once
if(str_starts_with($k, 'env_')){
if($i === 0){
$json['env'][str_replace('env_', '', $k)] = $v;
}
continue;
}
// suite info is needed only once
if(str_starts_with($k, 'suite_')){
if($i === 0){
$json['suite'][str_replace('suite_', '', $k)] = $v;
}
continue;
}
// test subject info once per test subject
if(str_starts_with($k, 'subject_')){
// skip the name as it is the key
if($k === 'subject_name'){
continue;
}
$json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']][str_replace('subject_', '', $k)] = $v;
}
// add variants
if(str_starts_with($k, 'variant_')){
$json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']]['variants'][$result['variant_index']][str_replace('variant_', '', $k)] = $v;
}
// add benchmark results per variant
if(str_starts_with($k, 'result_')){
$json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']]['variants'][$result['variant_index']]['results'][$result['result_index']][str_replace('result_', '', $k)] = $v;
}
}
}
file_put_contents(FILE.'.json', json_encode($json, (JSON_PRESERVE_ZERO_FRACTION|JSON_PRETTY_PRINT)));
+6
View File
@@ -52,6 +52,7 @@
},
"require-dev": {
"chillerlan/php-authenticator": "^5.1",
"phpbench/phpbench": "^1.2.15",
"phan/phan": "^5.4",
"phpunit/phpunit": "^11.0",
"phpmd/phpmd": "^2.15",
@@ -70,10 +71,15 @@
},
"autoload-dev": {
"psr-4": {
"chillerlan\\QRCodeBenchmark\\": "benchmark/",
"chillerlan\\QRCodeTest\\": "tests/"
}
},
"scripts": {
"phpbench":[
"Composer\\Config::disableProcessTimeout",
"@php vendor/bin/phpbench run"
],
"phpunit": "@php vendor/bin/phpunit",
"phan": "@php vendor/bin/phan"
},
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "vendor/phpbench/phpbench/phpbench.schema.json",
"core.extensions": [
"PhpBench\\Extensions\\XDebug\\XDebugExtension"
],
"xdebug.command_handler_output_dir": ".build/phpbench/xdebug-profile",
"runner.bootstrap": "vendor/autoload.php",
"runner.path": "benchmark",
"runner.file_pattern": "*Benchmark.php",
"console.ansi": true,
"report.outputs": {
"csv": {
"extends": "delimited",
"delimiter": "|",
"file": ".build/phpbench/benchmark.csv",
"header": true
}
}
}
-48
View File
@@ -1,48 +0,0 @@
<?php
/**
* Class PerformanceTest
*
* @created 16.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Performance;
use Closure;
use function hrtime;
/**
*
*/
class PerformanceTest{
protected int $runs;
protected int $total = 0;
public function __construct(int $runs = 1000){
$this->runs = $runs;
}
public function run(Closure $subject):self{
$this->total = 0;
for($i = 0; $i < $this->runs; $i++){
$start = hrtime(true);
$subject();
$end = hrtime(true);
$this->total += ($end - $start);
}
return $this;
}
public function getResult():float{
return ($this->total / $this->runs / 1000000);
}
}
-62
View File
@@ -1,62 +0,0 @@
<?php
/**
* Tests the performance of the mask pattern penalty testing
*
* @created 18.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Performance;
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Mode, Version};
use chillerlan\QRCodeTest\QRMaxLengthTrait;
use Generator;
use function file_put_contents;
use function json_encode;
use function printf;
use function sprintf;
use function str_repeat;
use function substr;
use const JSON_PRETTY_PRINT;
require_once __DIR__.'/../../vendor/autoload.php';
// excerpt from QRCodeReaderTestAbstract
$generator = new class () {
use QRMaxLengthTrait;
public function dataProvider():Generator{
$str = str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100);
$eccLevel = new EccLevel(EccLevel::H);
for($v = 1; $v <= 40; $v++){
$version = new Version($v);
yield sprintf('version %2s%s', $version, $eccLevel) => [
$version->getVersionNumber(),
$eccLevel->getLevel(),
substr($str, 0, self::getMaxLengthForMode(Mode::BYTE, $version, $eccLevel)),
];
}
}
};
$test = new PerformanceTest(100);
$json = [];
foreach($generator->dataProvider() as $key => [$version, $eccLevel, $data]){
$qrcode = new QRCode(new QROptions(['version' => $version, 'eccLevel' => $eccLevel]));
$qrcode->addByteSegment($data);
$matrix = $qrcode->getQRMatrix();
$test->run(fn() => MaskPattern::getBestPattern($matrix));
printf("%s: %01.3fms\n", $key, $test->getResult());
$json[$version] = $test->getResult();
}
file_put_contents(__DIR__.'/performance_maskpattern.json', json_encode($json, JSON_PRETTY_PRINT));
-80
View File
@@ -1,80 +0,0 @@
<?php
/**
* Tests the performance of the built-in output classes
*
* @created 16.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Performance;
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
use chillerlan\QRCode\Output\QROutputInterface;
use chillerlan\QRCodeTest\QRMaxLengthTrait;
use Generator;
use function file_put_contents;
use function json_encode;
use function printf;
use function sprintf;
use function str_repeat;
use function str_replace;
use function substr;
use const JSON_PRETTY_PRINT;
require_once __DIR__.'/../../vendor/autoload.php';
// excerpt from QRCodeReaderTestAbstract
$generator = new class () {
use QRMaxLengthTrait;
public function dataProvider():Generator{
$str = str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100);
$eccLevel = new EccLevel(EccLevel::L);
for($v = 5; $v <= 40; $v += 5){
$version = new Version($v);
foreach(QROutputInterface::MODES as $FQN){
$name = str_replace('chillerlan\\QRCode\\Output\\', '', $FQN);
yield sprintf('version %2s: %-14s', $version, $name) => [
$version->getVersionNumber(),
$FQN,
substr($str, 0, self::getMaxLengthForMode(Mode::BYTE, $version, $eccLevel)),
$name,
];
}
}
}
};
$test = new PerformanceTest(100);
$json = [];
foreach($generator->dataProvider() as $key => [$version, $FQN, $data, $name]){
$options = new QROptions([
'version' => $version,
'outputInterface' => $FQN,
'connectPaths' => true,
'drawLightModules' => true,
'drawCircularModules' => true,
'gdImageUseUpscale' => false, // set to false to allow proper comparison
]);
$qrcode = new QRCode($options);
$qrcode->addByteSegment($data);
$matrix = $qrcode->getQRMatrix();
$test->run(fn() => (new $FQN($options, $matrix))->dump());
printf("%s: %8.3fms\n", $key, $test->getResult());
$json[$name][$version] = $test->getResult();
}
file_put_contents(__DIR__.'/performance_output.json', json_encode($json, JSON_PRETTY_PRINT));
-84
View File
@@ -1,84 +0,0 @@
<?php
/**
* Tests the overall performance of the QRCode class
*
* @created 19.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Performance;
use chillerlan\QRCode\{QRCode, QROptions};
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
use chillerlan\QRCode\Output\QRStringJSON;
use chillerlan\QRCodeTest\QRMaxLengthTrait;
use Generator;
use function file_put_contents;
use function json_encode;
use function mb_substr;
use function printf;
use function sprintf;
use function str_repeat;
use function str_replace;
use const JSON_PRETTY_PRINT;
require_once __DIR__.'/../../vendor/autoload.php';
// excerpt from QRCodeReaderTestAbstract
$generator = new class () {
use QRMaxLengthTrait;
public function dataProvider():Generator{
$dataModeData = [
Mode::NUMBER => str_repeat('0123456789', 750),
Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100),
Mode::KANJI => str_repeat('漂う花の香り', 350),
Mode::HANZI => str_repeat('无可奈何燃花作香', 250),
Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100),
];
foreach(Mode::INTERFACES as $dataMode => $dataModeInterface){
$dataModeName = str_replace('chillerlan\\QRCode\\Data\\', '', $dataModeInterface);
for($v = 1; $v <= 40; $v++){
$version = new Version($v);
foreach([EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H] as $ecc){
$eccLevel = new EccLevel($ecc);
yield sprintf('version %2s%s (%s)', $version, $eccLevel, $dataModeName) => [
$version->getVersionNumber(),
$eccLevel,
$dataModeInterface,
$dataModeName,
mb_substr($dataModeData[$dataMode], 0, self::getMaxLengthForMode($dataMode, $version, $eccLevel)),
];
}
}
}
}
};
$test = new PerformanceTest(100);
$json = [];
foreach($generator->dataProvider() as $key => [$version, $eccLevel, $dataModeInterface, $dataModeName, $data]){
$options = new QROptions([
'outputInterface' => QRStringJSON::class,
'version' => $version,
'eccLevel' => $eccLevel->getLevel(),
]);
$test->run(fn() => (new QRCode($options))->addSegment(new $dataModeInterface($data))->render());
printf("%s: %01.3fms\n", $key, $test->getResult());
$json[$dataModeName][(string)$eccLevel][$version] = $test->getResult();
}
file_put_contents(__DIR__.'/performance_qrcode.json', json_encode($json, JSON_PRETTY_PRINT));
-92
View File
@@ -1,92 +0,0 @@
<?php
/**
* Tests the QRMatrix write performance
*
* @created 16.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCodeTest\Performance;
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
use chillerlan\QRCode\Data\QRData;
use chillerlan\QRCode\QROptions;
use chillerlan\QRCodeTest\QRMaxLengthTrait;
use Generator;
use function file_put_contents;
use function json_encode;
use function printf;
use function sprintf;
use function str_repeat;
use function str_replace;
use const JSON_PRETTY_PRINT;
require_once __DIR__.'/../../vendor/autoload.php';
// excerpt from QRCodeReaderTestAbstract
$generator = new class () {
use QRMaxLengthTrait;
public function dataProvider():Generator{
$dataModeData = [
Mode::NUMBER => str_repeat('0123456789', 750),
Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100),
Mode::KANJI => str_repeat('漂う花の香り', 350),
Mode::HANZI => str_repeat('无可奈何燃花作香', 250),
Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100),
];
foreach(Mode::INTERFACES as $dataMode => $dataModeInterface){
$dataModeName = str_replace('chillerlan\\QRCode\\Data\\', '', $dataModeInterface);
for($v = 1; $v <= 40; $v++){
$version = new Version($v);
foreach([EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H] as $ecc){
$eccLevel = new EccLevel($ecc);
yield sprintf('version %2s%s (%s)', $version, $eccLevel, $dataModeName) => [
$version->getVersionNumber(),
$eccLevel,
$dataModeInterface,
$dataModeName,
mb_substr($dataModeData[$dataMode], 0, self::getMaxLengthForMode($dataMode, $version, $eccLevel)),
];
}
}
}
}
};
$test = new PerformanceTest(100);
$json = [];
foreach($generator->dataProvider() as $key => [$version, $eccLevel, $dataModeInterface, $dataModeName, $data]){
// invovcation tests the performance of QRData::writeBitBuffer()
$test->run(fn() => new QRData(new QROptions(['version' => $version, 'eccLevel' => $eccLevel->getLevel()]), [new $dataModeInterface($data)]));
printf('%s encode: % 6.3fms', $key, $test->getResult());
$json[$dataModeName][(string)$eccLevel]['encode'][$version] = $test->getResult();
// writeMatrix includes QRMatrix::writeCodewords() and the ReedSolomonEncoder
$qrdata = new QRData(new QROptions(['version' => $version, 'eccLevel' => $eccLevel->getLevel()]), [new $dataModeInterface($data)]);
$test->run(fn() => $qrdata->writeMatrix());
printf(', write matrix: % 6.3fms', $test->getResult());
$json[$dataModeName][(string)$eccLevel]['write'][$version] = $test->getResult();
$bitBuffer = $qrdata->getBitBuffer();
$bitBuffer->read(4); // read data mode indicator
$test->run(fn() => $dataModeInterface::decodeSegment(clone $bitBuffer, $version));
printf(", decode: % 6.3fms\n", $test->getResult());
$json[$dataModeName][(string)$eccLevel]['decode'][$version] = $test->getResult();
}
file_put_contents(__DIR__.'/performance_qrdata.json', json_encode($json, JSON_PRETTY_PRINT));