Skip to content

Conversation

@TimothyMakkison
Copy link
Contributor

@TimothyMakkison TimothyMakkison commented Oct 1, 2025

WIP

TODO

  • Benchmarks
  • Handle array length exceeding scanLinesLength

Summary by CodeRabbit

  • Refactor
    • Optimized PNG QR code generation to use pooled memory for scanline processing, reducing allocations and improving performance on modern runtimes.
    • Behavior, visual output, and public usage remain unchanged for end users.
    • Internal interfaces adjusted to support pooled buffers; no changes required from consumers.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 1, 2025

📝 Walkthrough

Walkthrough

Replaces per-call byte[] allocations with ArrayPool-backed buffers in PngByteQRCode for supported targets; changes DrawScanlines to return ArraySegment, updates WriteScanlines/Deflate/Adler32 to accept ArraySegment, and returns rented buffers after use.

Changes

Cohort / File(s) Summary
Memory pooling & buffer flow
QRCoder/PngByteQRCode.cs
- Added conditional using System.Buffers for `NETCOREAPP2_1_OR_GREATER
Method signatures & callers
QRCoder/PngByteQRCode.cs
- DrawScanlines signature changed from byte[] to ArraySegment<byte> (now private)
- WriteScanlines signature changed to accept ArraySegment<byte> instead of byte[] (exported/internal API change)
- Deflate overload updated to accept ArraySegment<byte> and write using its Array and Count
- Adler32 calls updated to use scanlines.Array and scanlines.Count
Allocation/cleanup adjustments
QRCoder/PngByteQRCode.cs
- GetGraphic variants obtain scanlines as ArraySegment<byte>, pass them to writers, and ensure rented buffers are returned to ArrayPool when applicable
- Preserved fallback heap allocation and cleanup when ArrayPool is not available

Sequence Diagram(s)

sequenceDiagram
    participant Caller as GetGraphic(...)
    participant Drawer as DrawScanlines
    participant Writer as WriteScanlines
    participant Deflater as Deflate
    participant Adler as Adler32

    rect #EEF6FF
      Caller->>Drawer: Request scanlines (pixelsPerModule, drawQuietZones)
      note right of Drawer: Rent buffer from ArrayPool when supported\nReturn ArraySegment<byte>
    end

    rect #F6FFF0
      Caller->>Writer: Pass ArraySegment<byte> scanlines
      Writer->>Deflater: Deflate(scanlines.Array, scanlines.Count)
      Writer->>Adler: Adler32(scanlines.Array, 0, scanlines.Count)
    end

    rect #FFF6EE
      Writer->>Caller: Complete writing
      Caller->>Drawer: Return rented buffer to ArrayPool (if rented)
      note right of Drawer: Fallback: heap allocation — no pool return needed
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly conveys the primary enhancement—modifying PngByteQRCode to utilize ArrayPool.Rent for avoiding byte[] allocations—which directly reflects the core intent of the changeset without extraneous detail. It is a clear, single sentence that will be meaningful to anyone reviewing the project’s history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
QRCoder/PngByteQRCode.cs (1)

93-136: ArrayPool usage is correct; fix formatting issues.

The implementation correctly handles the case where ArrayPool.Rent returns a larger array than requested:

  • Line 102 clears only the required scanLinesLength bytes
  • Line 135 returns an ArraySegment<byte> that exposes only the valid portion
  • All array access within the method stays within bounds

This addresses the concern raised in the past review comment about array length.

However, the pipeline is failing due to whitespace/indentation issues on lines 100, 103, and 105.

Apply this diff to fix the formatting:

         var bytesPerScanline = (matrixSize * pixelsPerModule + 7) / 8 + 1; // A monochrome scanline is one byte for filter type then one bit per pixel.
         var scanLinesLength = bytesPerScanline * matrixSize * pixelsPerModule;
-        #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
         var scanlines = ArrayPool<byte>.Shared.Rent(scanLinesLength);
         Array.Clear(scanlines, 0, scanLinesLength);
-        #else
+#else
         var scanlines = new byte[scanLinesLength];
-        #endif
+#endif
 
         for (var y = 0; y < matrixSize; y++)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 446aa0c and d6ffc67.

📒 Files selected for processing (1)
  • QRCoder/PngByteQRCode.cs (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/PngByteQRCode.cs (1)
QRCoder/QRCodeGenerator.cs (1)
  • ArraySegment (652-715)
🪛 GitHub Actions: Format (Pull Request)
QRCoder/PngByteQRCode.cs

[error] 80-80: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 82-82: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 83-83: WHITESPACE: Fix whitespace formatting. Replace 8 characters with '\s\s\s\s\s\s\s\s'.


[error] 100-100: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 103-103: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 104-104: WHITESPACE: Fix whitespace formatting. Replace 8 characters with '\s\s\s\s\s\s\s\s'.


[error] 105-105: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 106-106: WHITESPACE: Fix whitespace formatting. Replace 9 characters with '\n\s\s\s\s\s\s\s\s'.


[error] 80-80: IDE0055: Fix formatting

🪛 GitHub Check: format
QRCoder/PngByteQRCode.cs

[failure] 82-82:
Fix formatting


[failure] 80-80:
Fix formatting


[failure] 82-82:
Fix formatting


[failure] 80-80:
Fix formatting


[failure] 105-105:
Fix formatting


[failure] 103-103:
Fix formatting


[failure] 100-100:
Fix formatting


[failure] 105-105:
Fix formatting


[failure] 103-103:
Fix formatting


[failure] 100-100:
Fix formatting

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
QRCoder/PngByteQRCode.cs (5)

1-3: LGTM!

The conditional compilation directive correctly targets platforms where ArrayPool<T> is available.


42-46: LGTM!

The ArrayPool rent-use-return pattern is correctly implemented. The rented buffer is properly returned after use.


269-269: LGTM!

The signature change from byte[] to ArraySegment<byte> is necessary to support pooled buffers. Since PngBuilder is a private sealed class, this is not a breaking change.


324-328: LGTM with same offset assumption.

The Deflate method correctly accepts and unpacks ArraySegment<byte>. The same assumption about offset being 0 applies here as well (see comment on lines 288-289).


288-289: Adler32 offset assumption verified
Both DrawScanlines (PngByteQRCode.cs:135) and related code (QRCodeGenerator.cs:702) instantiate ArraySegment<byte> with offset 0, so passing 0 to Adler32 is correct.

@Shane32
Copy link
Owner

Shane32 commented Oct 1, 2025

FYI, we use 'squash and merge', so no need to force push if you don't want to. The commit history will show a single commit for the entire PR regardless.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
QRCoder/PngByteQRCode.cs (2)

78-82: Fix formatting issues.

The ArrayPool pattern is correct, but the pipeline is failing due to whitespace/indentation on the preprocessor directives.

Apply this diff to fix the formatting:

-        var scanLines = DrawScanlines(pixelsPerModule, drawQuietZones);
-        png.WriteScanlines(scanLines);
-        #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
-        ArrayPool<byte>.Shared.Return(scanLines.Array!);
-        #endif
+        var scanLines = DrawScanlines(pixelsPerModule, drawQuietZones);
+        png.WriteScanlines(scanLines);
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
+        ArrayPool<byte>.Shared.Return(scanLines.Array!);
+#endif

42-46: Fix formatting issues.

The ArrayPool pattern is correct, but the pipeline is failing due to whitespace/indentation on the preprocessor directives.

Apply this diff to fix the formatting:

-        var scanLines = DrawScanlines(pixelsPerModule, drawQuietZones);
-        png.WriteScanlines(scanLines);
-        #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
-        ArrayPool<byte>.Shared.Return(scanLines.Array!);
-        #endif
+        var scanLines = DrawScanlines(pixelsPerModule, drawQuietZones);
+        png.WriteScanlines(scanLines);
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
+        ArrayPool<byte>.Shared.Return(scanLines.Array!);
+#endif
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6ffc67 and ea16006.

📒 Files selected for processing (1)
  • QRCoder/PngByteQRCode.cs (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/PngByteQRCode.cs (1)
QRCoder/QRCodeGenerator.cs (1)
  • ArraySegment (652-715)
🪛 GitHub Actions: Format (Pull Request)
QRCoder/PngByteQRCode.cs

[error] 100-100: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 103-103: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 104-104: WHITESPACE: Fix whitespace formatting. Replace 8 characters with '\s\s\s\s\s\s\s\s'.


[error] 105-105: WHITESPACE: Fix whitespace formatting. Delete 8 characters.


[error] 106-106: WHITESPACE: Fix whitespace formatting. Replace 9 characters with '\n\s\s\s\s\s\s\s\s'.


[error] 100-100: IDE0055: Fix formatting

🪛 GitHub Check: format
QRCoder/PngByteQRCode.cs

[failure] 100-100:
Fix formatting


[failure] 105-105:
Fix formatting


[failure] 103-103:
Fix formatting


[failure] 100-100:
Fix formatting


[failure] 105-105:
Fix formatting


[failure] 103-103:
Fix formatting


[failure] 100-100:
Fix formatting


[failure] 105-105:
Fix formatting


[failure] 103-103:
Fix formatting


[failure] 100-100:
Fix formatting

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
QRCoder/PngByteQRCode.cs (4)

1-3: LGTM!

The conditional using directive correctly gates ArrayPool support for compatible target frameworks.


107-136: LGTM!

The loop logic correctly bounds all array accesses, and the returned ArraySegment<byte> properly encapsulates the exact data length regardless of the underlying array size.


269-292: LGTM!

The signature change to ArraySegment<byte> correctly propagates through the call chain. Since PngBuilder is a private sealed class, this is not a breaking change. The null-forgiving operator on scanlines.Array! is safe because both allocation paths guarantee a non-null array.


324-328: LGTM!

The Deflate method correctly accepts and uses ArraySegment<byte>, properly accessing the underlying array and count. The null-forgiving operator is safe given the guaranteed non-null array from the allocation paths.

@TimothyMakkison
Copy link
Contributor Author

FYI, we use 'squash and merge', so no need to force push if you don't want to. The commit history will show a single commit for the entire PR regardless.

Sorry force of habit, good to know

@TimothyMakkison
Copy link
Contributor Author

TimothyMakkison commented Oct 1, 2025

Okay, did my TODO list, I've used ArraySegment<byte> to make it harder for a longer rented array to break things 🤞I think Array.Clear is needed, could be wrong, looks like lines 124 or 131 may rely on empty arrays. Looks like its has decent memory savings, I have a couple of other ideas.

Have you considered using a formatter like csharpier for this project? Iirc Mapperly has it to automatically format code as its pushed, would save code rabbit from rescanning the commit

@TimothyMakkison TimothyMakkison marked this pull request as ready for review October 1, 2025 16:12
@Shane32
Copy link
Owner

Shane32 commented Oct 1, 2025

Have you considered using a formatter like csharpier for this project? Iirc Mapperly has it to automatically format code as its pushed, would save code rabbit from rescanning the commit

We have it set up to check that dotnet format matches. Most of the time this means you just press Ctrl-K Ctrl-D from within VS and it will reformat the page correctly. Exceptions are when there's a .NET warning like "trim excess usings" which are not updated by Ctrl-K Ctrl-D.

I don't want to use another formatter, like prettier or csharpier, which isn't compatible with the way that VS formats things by default. I have not checked out how to integrate an automatic formatting step into the push process for .NET code bases, although I've done that with my react projects, and it's certainly nice to have.

@Shane32
Copy link
Owner

Shane32 commented Oct 1, 2025

I don't care too much about Coderabbit's checks; I noticed it was reading the build check outputs here and providing a suggestion based on that. Coderabbit is nice sometimes but can also be a bit verbose (and sometimes wrong). In PR #611 just now it correctly caught a bug unrelated to the PR 👍 . And it's always nice that it writes a PR summary.

@TimothyMakkison TimothyMakkison marked this pull request as draft October 1, 2025 18:52
@TimothyMakkison
Copy link
Contributor Author

TimothyMakkison commented Oct 1, 2025

Reopening, I realised that my benchmarks weren't measuring PNG QR code performance, so I wanted to verify that the changes were signficant. Here are the actual results using GFoidl's profiling code #595.

Before

image

After

image

@TimothyMakkison TimothyMakkison marked this pull request as ready for review October 1, 2025 19:05
@Shane32
Copy link
Owner

Shane32 commented Oct 1, 2025

I copied BitmapByteQRCode.cs to a new similar file for PNG files. It tests the PNG creation code only.

Before


BenchmarkDotNet v0.13.12, Windows 11 (10.0.26100.6584)
Unknown processor
.NET SDK 8.0.414
  [Host]     : .NET 8.0.20 (8.0.2025.41914), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.20 (8.0.2025.41914), X64 RyuJIT AVX2


Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
RenderPngByteQRCodeSmall 42.75 μs 0.578 μs 0.541 μs 0.6714 - - 12.64 KB
RenderPngByteQRCodeMedium 124.77 μs 1.971 μs 1.843 μs 1.8311 0.1221 - 35.31 KB
RenderPngByteQRCodeBig 1,799.79 μs 16.810 μs 15.724 μs 123.0469 123.0469 123.0469 420.5 KB
RenderPngByteQRCodeHuge 37,235.56 μs 503.120 μs 470.619 μs 800.0000 800.0000 800.0000 10617 KB

After


BenchmarkDotNet v0.13.12, Windows 11 (10.0.26100.6584)
Unknown processor
.NET SDK 8.0.414
  [Host]     : .NET 8.0.20 (8.0.2025.41914), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.20 (8.0.2025.41914), X64 RyuJIT AVX2


Method Mean Error StdDev Gen0 Allocated
RenderPngByteQRCodeSmall 42.18 μs 0.398 μs 0.353 μs 0.0610 1.85 KB
RenderPngByteQRCodeMedium 122.63 μs 1.830 μs 1.712 μs 0.2441 5.14 KB
RenderPngByteQRCodeBig 1,697.70 μs 20.408 μs 19.090 μs - 34.89 KB
RenderPngByteQRCodeHuge 33,768.16 μs 490.598 μs 458.905 μs - 1040.23 KB

@Shane32 Shane32 changed the title feat: use ArrayPool.Rent to prevent byte[] allocation PngByteQRCode: use ArrayPool.Rent to prevent byte[] allocation Oct 1, 2025
@Shane32 Shane32 merged commit ec52e48 into Shane32:master Oct 1, 2025
12 checks passed
@Shane32 Shane32 added this to the 1.7.0 milestone Oct 2, 2025
@Shane32 Shane32 added the performance Performance related enhancements or benchmarks label Oct 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance related enhancements or benchmarks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants