Bacon / BaconQrCode

QR Code Generator for PHP
BSD 2-Clause "Simplified" License
1.83k stars 208 forks source link

Performance issue in BaconQrCode\Encoder #99

Closed odan closed 2 years ago

odan commented 2 years ago

Hi!

I'm using the sprain/php-swiss-qr-bill library to generate QR-ESRs. Generating a single QR-ESR takes ~5 seconds (or more).

Then I started the profiler and debugged it manually until I found that only the BaconQrCode\Encoder::chooseMaskPattern needed nearly all of this time. The time for PDF rendering etc. is nothing compared to this single method.

https://github.com/Bacon/BaconQrCode/blob/master/src/Encoder/Encoder.php#L233-L252

My question, is there a possibility to pass "some arguments" to make it faster to generate the QR code?

DASPRiD commented 2 years ago

5 seconds sounds like an awful lot of time for generating a single QR-Code. Can you provide a test-code (BaconQrCode native) to reproduce that time? All my tests generate QR-Codes in a fraction of that time.

odan commented 2 years ago

Thanks for the feedback. I will try to prepare a test-code and paste it here.

odan commented 2 years ago

Ok, here is a minimal working example to reproduce the performance issue:

use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Encoder\Encoder;
// ...

$text = str_repeat('A', 270);
$baconErrorCorrectionLevel = ErrorCorrectionLevel::valueOf('L');
$encoding = 'UTF-8';

$baconQrCode = Encoder::encode($text, $baconErrorCorrectionLevel, $encoding);

The Encoder::encode method takes ~10 seconds for 270 characters with Xdebug enabled. We use this also to generate the code coverage.

Time: 10.16 seconds, Memory: 10.00MB

With Xdebug disabled, it takes ~500 ms.

Time: 519 ms, Memory: 10.00MB

This is still relative slow.

Now let's take a look at the profiler and sort by number of calls.

image

If I counted correctly, there are over ~129.000 ! internal method calls to process this one QR code, which could be the cause of this performance issue.

Within the Encoder::encode method, this line with self::chooseMaskPattern(...) requires nearly all the time for the calculation, the other parts are fast.

$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);

https://github.com/Bacon/BaconQrCode/blob/2.0.6/src/Encoder/Encoder.php#L135

DASPRiD commented 2 years ago

Thanks, that's a pretty in-depth analysis! One thing which stands out to me is that over half the time is spent on MaskUtil::getDataMaskBit(). This is particularly interesting because it's just a very simple switch statement with some bit masking.

This can't come from the overhead of function calls in PHP itself, as other functions are called more often and take way less time. Do you have any clue what might make this specific function so expensive?

odan commented 2 years ago

Sorry, after trying the whole day, I have no clue what's going on there. I tried to find a concrete bottleneck, but I found at least two methods calls.

  1. MatrixUtil::embedDataBits
  2. $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);

The time sum of these methods is by far the largest.

image

So my conclusion (guess) is that it's the sheer mass of bit operations inside plenty of nested foreach and for loops. I have tried to optimize some functions but without a real performance gain. So I close this issue for future reference.

DASPRiD commented 2 years ago

Yeah, PHP is not really suited for this kind of stuff. Thanks though for investing time into this!

One option to speed things up would be to utilize qrencode binary if installed on the system, in which case it could use that to generate an ASCII QR-Code, parse that into a matrix array and then continue as usual.

This would only be helpful though if you have access to the server running your PHP code to install that package.