Add ASCII renderer optimizations by Shane32 · Pull Request #652 · Shane32/QRCoder

@Shane32

@Shane32 Shane32 commented

Oct 7, 2025

edited by coderabbitai bot

Loading

Summary by CodeRabbit

  • Refactor
    • Optimized ASCII QR code generation for faster performance and lower memory usage.
    • Improved throughput for smaller QR sizes with reduced per-line allocations.
    • Preserves identical output and behavior; no public API changes.

@Shane32

@coderabbitai

📝 Walkthrough

Walkthrough

Replaces dynamic list with a pre-sized string[] for QR lines, adds a HAS_SPAN-guarded fast path using stack-allocated Span<char> when conditions allow, and updates the fallback to reuse a StringBuilder per line while computing capacities upfront and early-returning from the span path. (50 words)

Changes

Cohort / File(s) Summary
ASCII QR line construction optimization
QRCoder/ASCIIQRCode.cs
Replaced List<string> with pre-sized string[]; added HAS_SPAN fast path using stack-allocated Span<char> when dark/white token lengths match and capacity is small; precomputed sideLength and lineCapacity; fallback uses a reusable StringBuilder per line and writes directly into the array; early return from span path.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant ASCIIQRCode as ASCIIQRCode.Generate
  participant SpanPath as HAS_SPAN Fast Path
  participant Builder as StringBuilder Fallback

  Caller->>ASCIIQRCode: Generate ASCII QR lines
  ASCIIQRCode->>ASCIIQRCode: compute sideLength & lineCapacity
  alt HAS_SPAN enabled && token lengths equal && small capacity
    ASCIIQRCode->>SpanPath: stackalloc Span<char>, fill each line directly
    SpanPath-->>ASCIIQRCode: populated string[] lines
    ASCIIQRCode-->>Caller: return string[] lines
  else Fallback
    ASCIIQRCode->>Builder: reuse/reset builder per line, append blocks
    Builder-->>ASCIIQRCode: write resulting string into pre-sized array
    ASCIIQRCode-->>Caller: return string[] lines
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • gfoidl

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately summarizes the main change in the pull request by indicating optimizations to the ASCII renderer component. It is concise, specific, and directly reflects the performance enhancements made in ASCIIQRCode.cs. It omits extraneous detail and avoids vague terms, making it easily understood by teammates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ascii_opt

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.

coderabbitai[bot]

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
QRCoder/ASCIIQRCode.cs (1)

71-74: Cache spans outside the hot loop

AsSpan() gets re-evaluated for every module even though the underlying strings never change. Caching var darkSpan = darkColorString.AsSpan(); / var whiteSpan = whiteSpaceString.AsSpan(); once before the loops lets you reuse them in the inner loop and avoids repeated method calls in this perf-sensitive path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 317c587 and 9590d2e.

📒 Files selected for processing (1)
  • QRCoder/ASCIIQRCode.cs (2 hunks)
⏰ 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). (5)
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: additional-tests

gfoidl

@Shane32 @gfoidl

Co-authored-by: Günther Foidl <gue@korporal.at>

coderabbitai[bot]

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
QRCoder/ASCIIQRCode.cs (1)

59-81: Verify the lineCapacity < 510 threshold.

The 510 limit leaves a 2-char safety buffer in the 512-char stackalloc, but this might unnecessarily exclude cases where lineCapacity is 510-512. Unless there's a specific reason for this buffer, consider using <= 512 to maximize the fast path coverage.

If the 2-char buffer is intentional, adding a brief comment explaining why would improve maintainability:

+        // Limit to 510 to leave buffer space for safety/alignment
         if (darkColorString.Length == whiteSpaceString.Length && lineCapacity < 510)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9590d2e and cef1680.

📒 Files selected for processing (1)
  • QRCoder/ASCIIQRCode.cs (2 hunks)
⏰ 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). (6)
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: additional-tests
🔇 Additional comments (3)
QRCoder/ASCIIQRCode.cs (3)

57-58: LGTM! Pre-allocation eliminates List overhead.

Pre-computing lineCapacity and switching to a fixed-size array removes dynamic growth costs and enables the span optimization below.


82-82: LGTM! StringBuilder capacity optimization.

Initializing the StringBuilder with lineCapacity and reusing it across iterations (with reset on line 94) eliminates repeated buffer allocations in the fallback path.


93-96: LGTM! Efficient array population and return.

Direct array indexing (line 93) and StringBuilder reset via Length = 0 (line 94) are efficient choices. The direct return of the pre-allocated array (line 96) completes the optimization, with GetGraphic (line 35) handling the join operation as needed.

This was referenced

Oct 9, 2025

This was referenced

Nov 25, 2025

This was referenced

Dec 5, 2025