Prechádzať zdrojové kódy

:sparkles: add PHPBench to run benchmarks

smiley 1 rok pred
rodič
commit
f31eff72f5

+ 1 - 0
.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',

+ 148 - 0
benchmark/BenchmarkAbstract.php

@@ -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 - 0
benchmark/DecoderBenchmark.php

@@ -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 - 0
benchmark/MaskPatternBenchmark.php

@@ -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 - 0
benchmark/OutputBenchmark.php

@@ -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 - 0
benchmark/QRCodeBenchmark.php

@@ -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 - 0
benchmark/QRDataBenchmark.php

@@ -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 - 0
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;
+}

+ 138 - 0
benchmark/generate-html.php

@@ -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 - 0
benchmark/generate-markdown.php

@@ -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 - 0
benchmark/parse-common.php

@@ -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 - 0
benchmark/parse-result.php

@@ -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 - 0
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"
 	},

+ 19 - 0
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
+		}
+	}
+}

+ 0 - 48
tests/Performance/PerformanceTest.php

@@ -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);
-	}
-
-}

+ 0 - 62
tests/Performance/maskpattern.php

@@ -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));

+ 0 - 80
tests/Performance/output.php

@@ -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));

+ 0 - 84
tests/Performance/qrcode.php

@@ -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));

+ 0 - 92
tests/Performance/qrdata.php

@@ -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));