final changes in pdfs closed xml

final changes in pdfs closed xml

Here’s a complete evaluation of the branch:


Summary

Branch: RefactorPdfXlReports — one commit d6a75cccf
Goal: Reduce API container memory footprint by caching assets and removing expensive per-cell operations in ClosedXML.

There are three distinct workstreams, plus peripheral cleanups:


1. QuestPDF Asset Caching (11 files)

What changed: Header images, footer images, and QR codes were being loaded inside ComposeHeader()/ComposeFooter() — which meant File.ReadAllBytes() every time a page header/footer rendered. QR code generation (QRCodeGeneratorQRCodeDataPngByteQRCode) was also recreated per page.

Fix: Moved all these into field initializers — loaded once at construction time:

// Before: in ComposeHeader() — loaded per page
byte[] imageData = File.ReadAllBytes(Model.HeaderImg);

// After: field initializer — loaded once
private readonly byte[]? _headerImgBytes = 
    !string.IsNullOrEmpty(_InvoicePrintData.HeaderImg) && File.Exists(_InvoicePrintData.HeaderImg) 
        ? File.ReadAllBytes(_InvoicePrintData.HeaderImg) : null;

Similarly for QR codes — extracted to static helper GenerateQrCodeImage() that returns pre-rendered PNG bytes, stored in a field.

Affected: Invoice, Dncn, EwayInvoice, Grn, Indent, JV, JobWork, JobWorkMisc, Payslip, SalesOrder, Voucher, CrewPayslip (12 document classes)

Memory impact: High — prevents repeated disk I/O and QR code generation (which allocates intermediate objects per page). The null-conditional guards also prevent crashes when HeaderImg is empty/missing.


2. ClosedXML — AdjustToContents Optimization (3 files)

This is the core memory fix from the audit. AdjustToContents() calls text measurement on every cell, and by default ClosedXML uses SkiaSharp font rendering — which loads actual font files and does precise glyph measurement, consuming significant memory per worksheet.

2a. New: ClosedXmlGraphicEngine + ClosedXmlFastEngineScope

A custom IXLGraphicEngine registered in Program.cs that wraps the default engine:

  • Normal mode (scope disabled): delegates to the default engine (full precision)
  • Fast mode (scope enabled via using (ClosedXmlFastEngineScope.Enable())): returns approximate text metrics based on character count × a heuristic:
Method Fast mode formula
GetTextHeight fontSize × 1.3 × (dpi / 96)
GetTextWidth text.Length × 7.5 × (fontSize/11) × (dpi/96) (8.6 if bold)
GetMaxDigitWidth Same as width per digit

This completely bypasses SkiaSharp/SixLabors font loading — no font file is ever read for measurement. The values are good enough for column auto-sizing.

2b. Style batching — CrewKraReport & LeadSummaryReport

Before: NumberFormat.Format = "0.00" was applied per-cell inside loops (potentially thousands of style objects created):

worksheet.Cell(i, columnvalIndex + k).Style.NumberFormat.Format = "0.00";

After: A single range-level format applied once after the loop:

worksheet.Range(5, 3, i, columnvalIndex + monthsList.Count + 2).Style.NumberFormat.Format = "0.00";

Why this matters: Each individual cell.Style.NumberFormat creates a new style object. AdjustToContents then needs to inspect every cell’s style. By applying a range format once, ClosedXML shares the style object across all cells in that range, drastically reducing internal style tracking overhead.

2c. Registration in Program.cs

ClosedXML.Excel.LoadOptions.DefaultGraphicEngine = new ClosedXmlGraphicEngine();

3. QuestPDF_PrintLabel Refactor

  • Removed the second constructor parameter _mfgtrndata — it was an unused ListModel that shadowed per-loop data
  • Removed mutable class stateComposeContent now receives its data as parameters (QrPrint listModel, byte[] qrCodeImage) instead of depending on a field that was overwritten in a loop
  • QR generation moved to static helper — pre-rendered per label at the call site in the loop, not re-created inside ComposeContent

Memory impact: Moderate — eliminates unnecessary allocations from the unused field and the QR code objects inside the composition path (which ran for every label in a multi-label loop).


4. Docker & Monitoring

Change What
docker-compose.yml / docker-compose-aws.yml Removed memory: ${API_MEM_LIMIT:-1152m} limits under deploy.resources.limits
scripts/monitor_memory.sh (new) Logs docker stats + host memory to a CSV every run — for verifying the footprint improvement

The 1152m limit was removed — presumably these optimizations now keep the container under whatever threshold was causing OOM kills, or the limit was causing OOM kills because 1152m was too tight on burst.


Overall Assessment

Change Memory impact Risk
Image/QR caching (field initializers) High — eliminates repeated allocations per-page Low — images already loaded, just earlier
ClosedXmlGraphicEngine fast mode High — eliminates font loading entirely for AdjustToContents Medium — column widths become approximate, could cause text overflow in cells
Range-level NumberFormat batching Medium — fewer style objects tracked by ClosedXML Low — same format, same cells
PrintLabel constructor cleanup Low Low
Removing docker memory limit N/A (operational) Medium — removes hard cap, relies on optimizations

Ponytail note: The fast graphic engine’s text.Length × 7.5 heuristic is good enough for AdjustToContents — the column widths just need to be “wide enough,” not pixel-perfect. If any cells show text clipping, AdjustToContents with a wider default or a “fast mode with padding” multiplier (e.g. 7.5 → 8.0) would handle it. Not worth adding unless someone reports clipping.