memory allocation pdfquest optimization round2

memory allocation pdfquest optimization round2

Here’s my full audit of the plan:


Plan Audit: ✅ Mostly Correct, 3 Gaps, 1 Overstatement

What the Plan Gets Right

Claim Verdict Details
12 QuestPDF files listed All exist Plus 4 more unlisted (QuestPDF_ChequePrint.cs, QuestPDF_ConfirmationOfBalance.cs, QuestPDF_PrintLabel.cs, QuestPDF_CrewBill.cs)
Images read inside deferred lifecycle methods 16 active calls all inside ComposeHeader/ComposeFooter/ComposeContent — never cached
QR code generators never disposed Zero using blocks across 4 files (Invoice, Dncn, EwayInvoice, PrintLabel)
AdjustToContents = 536 calls Confirmed — 536 in API, 0 in Web. This is a major concern on Linux/Docker
ToArray() not a problem ✅ Only 1 call in scaffold WeatherForecastController
new byte[16] is fine ✅ Only explicit byte array allocation, AES IV — irrelevant

What the Plan Gets Wrong or Misses

❌ Gap 1: Zero Pagination Everywhere — Biggest Omission

The plan mentions ToList() but downplays it. There is no .Take() or .Skip() anywhere in the entire API project. Every Dapper query is fully materialized into List<T> via ToList() — hundreds of calls. For large reports (GstReport, SalesAnalysis, ArAp), this loads entire datasets into memory. This is likely the #1 contributor to your server memory issues, not cell-by-cell styling.

❌ Gap 2: QuestPDF_PrintLabel.cs Missed

QuestPDF_PrintLabel.cs:83-93 has the same undisposed QR code pattern (QRCodeGenerator, QRCodeData, PngByteQRCode — zero using blocks) but is not listed in Component 1.

❌ Gap 3: QuestPDF_ConfirmationOfBalance.cs Missed

Has using QRCoder import but the QR code is unused — dead import that should be removed. Also has DB calls inside Compose() and loops over parties (amplifying image reads).

⚠️ Overstatement: Cell-by-cell styling

The plan frames this as a significant issue, but the reality:

  • 181 cell-by-cell .Style calls vs 3,850 range-based .Style calls — only 4.5% are cell-by-cell
  • Both CrewMstController and CrmLeadReportController already use range-based styling for the vast majority of their formatting
  • The 6 cell-by-cell instances in CrewMstController and 2 in CrmLeadReportController are number format inside loops — extracting them to ranges has marginal memory benefit
  • This is low priority relative to pagination and AdjustToContents

⚠️ Partial: AdjustToContents solution is incomplete

The plan says “set explicit column widths where possible” — but 536 calls across 88+ controllers means you need to identify the heaviest hit sheets (ItemMst: 30, CrewMst: 25, GstReport: 21, AccountMst: 17) and prioritize those. Suggesting “where possible” without prioritizing is vague.

Additional Observations (Not in Plan)

1. .Result blocking inside Compose — Several QuestPDF files (QuestPDF_Voucher.cs, QuestPDF_ChequePrint.cs, QuestPDF_ConfirmationOfBalance.cs, QuestPDF_Payslip.cs) make synchronous blocking calls via .Result on async DB methods inside Compose(). This causes thread-pool starvation under load and is a separate memory/stress issue.

2. Better pattern exists — The ERP footer logo uses .Image("path/erplogo.png") (string path), letting QuestPDF handle loading/caching internally. The plan should consider whether header/footer images could switch to this same string-path pattern instead of caching byte[] fields — simpler, and QuestPDF may already optimize it.

3. Image re-read amplification — Files with loops (Invoice loops 1-4 copies, Voucher loops voucher data, ConfirmationOfBalance loops parties, Payslip loops employees) re-read images on every iteration. Caching in the constructor (as the plan proposes) fixes this, but it should also fix the .Result blocking for DB calls inside those same loops.


Pros & Cons of Recommended Changes

Change Pros Cons
Cache images in constructor Eliminates redundant disk I/O + LOH byte[] allocations per page pass. Simple, targeted change. Adds 2 private fields per file. Minimal complexity.
QR code using + caching Fixes undisposed IDisposable resources (potential handle leak). Caching reduces allocations. Need to ensure QR content doesn’t change per-page (it doesn’t — it’s invoice-specific).
Range-based styling Reduces ClosedXML style objects Low impact given 94% already range-based. Not worth the effort for current scope.
Explicit column widths Eliminates 536 AdjustToContents calls — big perf win on Linux Need to determine good widths for each sheet; trial-and-error
Pagination (not in plan) Biggest memory impact — prevents loading entire datasets Requires architecture changes (page params, count queries, UI)

What I’d Recommend Changing in the Plan

  1. Elevate pagination to Component 4 — this is likely the largest memory consumer. Even basic OFFSET/FETCH in the heaviest report repositories (GstReport, SalesAnalysis, ArAp, CrewMst) would dramatically reduce per-request memory.
  2. Lower cell-by-cell styling priority — 181 instances out of 4,031 style calls (4.5%) doesn’t justify dedicated effort. Tuck it into a “while-you’re-there” note.
  3. Add QuestPDF_PrintLabel.cs to QR code fix group.
  4. Add a note about .Result blocking in QuestPDF files — it’s a separate issue but affects server responsiveness under load.
  5. Prioritize the top-5 AdjustToContents offenders (ItemMst: 30, CrewMst: 25, GstReport: 21, AccountMst: 17, PurchaseAnalysis: 17) rather than the vague “where possible” language.

Bottom line: The plan is ~85% solid. The caching/QR fixes are well-targeted. The biggest missed opportunity is zero pagination — that’s likely where your memory is actually going. Want me to adjust the plan before we implement?