diff --git a/.phan/config.php b/.phan/config.php
index cb6554b66..82154d3ff 100644
--- a/.phan/config.php
+++ b/.phan/config.php
@@ -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',
diff --git a/benchmark/BenchmarkAbstract.php b/benchmark/BenchmarkAbstract.php
new file mode 100644
index 000000000..5cfebdb6d
--- /dev/null
+++ b/benchmark/BenchmarkAbstract.php
@@ -0,0 +1,148 @@
+
+ * @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];
+ }
+ }
+
+}
diff --git a/benchmark/DecoderBenchmark.php b/benchmark/DecoderBenchmark.php
new file mode 100644
index 000000000..44b74ef7d
--- /dev/null
+++ b/benchmark/DecoderBenchmark.php
@@ -0,0 +1,93 @@
+
+ * @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
+ }
+ }
+
+}
diff --git a/benchmark/MaskPatternBenchmark.php b/benchmark/MaskPatternBenchmark.php
new file mode 100644
index 000000000..752fb395f
--- /dev/null
+++ b/benchmark/MaskPatternBenchmark.php
@@ -0,0 +1,41 @@
+
+ * @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);
+ }
+
+}
diff --git a/benchmark/OutputBenchmark.php b/benchmark/OutputBenchmark.php
new file mode 100644
index 000000000..3b6c883b5
--- /dev/null
+++ b/benchmark/OutputBenchmark.php
@@ -0,0 +1,92 @@
+
+ * @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();
+ }
+
+}
diff --git a/benchmark/QRCodeBenchmark.php b/benchmark/QRCodeBenchmark.php
new file mode 100644
index 000000000..d55023a97
--- /dev/null
+++ b/benchmark/QRCodeBenchmark.php
@@ -0,0 +1,40 @@
+
+ * @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();
+ }
+
+}
diff --git a/benchmark/QRDataBenchmark.php b/benchmark/QRDataBenchmark.php
new file mode 100644
index 000000000..158158987
--- /dev/null
+++ b/benchmark/QRDataBenchmark.php
@@ -0,0 +1,76 @@
+
+ * @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());
+ }
+
+}
diff --git a/benchmark/benchmark.css b/benchmark/benchmark.css
new file mode 100644
index 000000000..b9e2f13e8
--- /dev/null
+++ b/benchmark/benchmark.css
@@ -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;
+}
diff --git a/benchmark/generate-html.php b/benchmark/generate-html.php
new file mode 100644
index 000000000..fe8024993
--- /dev/null
+++ b/benchmark/generate-html.php
@@ -0,0 +1,138 @@
+
+ * @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 = '
+
+
+
+
+
+%s results
+
+';
+
+$htmlFoot = '
+';
+
+$html = [];
+
+// General information/overview
+
+$env = $json['env'];
+$suite = $json['suite'];
+
+$html['index'][] = sprintf($htmlHead, 'Benchmark');
+$html['index'][] = 'Benchmark results
';
+$html['index'][] = 'Environment
';
+$html['index'][] = '';
+$html['index'][] = '| Name | Value |
';
+$html['index'][] = '';
+
+$html['index'][] = sprintf('| date | %s %s |
', $suite['date'], $suite['time']);
+$html['index'][] = sprintf('| environment | %s %s, %s |
', $env['uname_os'], $env['uname_version'], $env['uname_machine']);
+$html['index'][] = sprintf('| tag | %s |
', 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('| %s | %s |
', $field, $env[$field]);
+}
+
+$html['index'][] = '
';
+
+// list indiviidual reports
+$html['index'][] = 'Reports
';
+
+$list = [];
+
+foreach(array_keys($json['benchmark']) as $benchmark){
+
+ // add a file & header
+ $html[$benchmark][] = sprintf($htmlHead, $benchmark);
+ $html[$benchmark][] = sprintf('%s
', $benchmark);
+ $html[$benchmark][] = sprintf('%s', $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('%1$s
', $subject);
+ $html[$benchmark][] = sprintf('Revs: %s, Iterations: %s
', $subj['revs'], $subj['iterations']);
+ $html[$benchmark][] = parseVariants($subj['variants']);
+ }
+
+ // close document
+ $html[$benchmark][] = 'back to overview';
+ $html[$benchmark][] = $htmlFoot;
+}
+
+// create overview list
+
+$html['index'][] = '';
+
+foreach($list as $benchmark => $subjects){
+ // list item
+ $html['index'][] = sprintf('- %1$s
', $benchmark);
+
+ foreach($subjects as $subject){
+ // list sub-item
+ $html['index'][] = sprintf('- %2$s
', $benchmark, $subject);
+ }
+
+ $html['index'][] = '
';
+}
+
+$html['index'][] = '
';
+
+// 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));
+}
diff --git a/benchmark/generate-markdown.php b/benchmark/generate-markdown.php
new file mode 100644
index 000000000..479cb0394
--- /dev/null
+++ b/benchmark/generate-markdown.php
@@ -0,0 +1,107 @@
+
+ * @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));
+}
diff --git a/benchmark/parse-common.php b/benchmark/parse-common.php
new file mode 100644
index 000000000..eacbc2357
--- /dev/null
+++ b/benchmark/parse-common.php
@@ -0,0 +1,78 @@
+
+ * @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('%s | ', $mode), $modeKeys);
+ $eccHeaders = array_map(fn($ecc) => sprintf('%s | ', $ecc), $eccKeys);
+
+ $table = [''];
+ $table[] = sprintf(' | %s
', implode('', $modeHeaders));
+ $table[] = sprintf('| Version | %s
', str_repeat(implode('', $eccHeaders), count($modeKeys)));
+ $table[] = '';
+
+ foreach($data as $version => $modes){
+
+ $results = [];
+
+ foreach($modes as $eccLevels){
+ foreach($eccLevels as [$time_avg, $mem_peak]){
+ $results[] = sprintf('%01.3f | ', $time_avg);
+ }
+ }
+
+ $table[] = sprintf('| %s | %s
', $version, implode('', $results));
+
+ }
+
+ $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),
+ ];
+}
diff --git a/benchmark/parse-result.php b/benchmark/parse-result.php
new file mode 100644
index 000000000..931e97031
--- /dev/null
+++ b/benchmark/parse-result.php
@@ -0,0 +1,131 @@
+
+ * @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)));
diff --git a/composer.json b/composer.json
index acb10e812..e1191710e 100644
--- a/composer.json
+++ b/composer.json
@@ -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"
},
diff --git a/phpbench.json b/phpbench.json
new file mode 100644
index 000000000..dcd98e74b
--- /dev/null
+++ b/phpbench.json
@@ -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
+ }
+ }
+}
diff --git a/tests/Performance/PerformanceTest.php b/tests/Performance/PerformanceTest.php
deleted file mode 100644
index 9fc9137f0..000000000
--- a/tests/Performance/PerformanceTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
-
- * @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);
- }
-
-}
diff --git a/tests/Performance/maskpattern.php b/tests/Performance/maskpattern.php
deleted file mode 100644
index 55295b79a..000000000
--- a/tests/Performance/maskpattern.php
+++ /dev/null
@@ -1,62 +0,0 @@
-
- * @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));
diff --git a/tests/Performance/output.php b/tests/Performance/output.php
deleted file mode 100644
index 98a0d9fe8..000000000
--- a/tests/Performance/output.php
+++ /dev/null
@@ -1,80 +0,0 @@
-
- * @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));
diff --git a/tests/Performance/qrcode.php b/tests/Performance/qrcode.php
deleted file mode 100644
index e08e76121..000000000
--- a/tests/Performance/qrcode.php
+++ /dev/null
@@ -1,84 +0,0 @@
-
- * @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));
diff --git a/tests/Performance/qrdata.php b/tests/Performance/qrdata.php
deleted file mode 100644
index 615baa8ca..000000000
--- a/tests/Performance/qrdata.php
+++ /dev/null
@@ -1,92 +0,0 @@
-
- * @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));