2

I have a process that takes an array of HTML strings and builds up a PDF page by page using the following approach:

let printPageRenderer = ReportPrintPageRenderer(withTemplate: self.template)

var pageIndex = 0
data.forEach { (htmlPage) in
     let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlPage)

     printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: pageIndex)
     pageIndex += 1
}

let pdfData = drawPDFUsingPrintPageRendererNoImages(printPageRenderer: printPageRenderer)

Where the drawPDFUsing... method used the standard approach I have seen described many times:

let data = NSMutableData()

UIGraphicsBeginPDFContextToData(data, CGRect.init(x: 0, y: 0, width: template.pageWidth, height: template.pageHeight), nil)

printPageRenderer.prepare(forDrawingPages: NSMakeRange(0, printPageRenderer.numberOfPages))

let bounds = UIGraphicsGetPDFContextBounds()

for i in 0...((printPageRenderer.numberOfPages - 1 < 0) ? 0 : printPageRenderer.numberOfPages - 1) {
      UIGraphicsBeginPDFPage()
       printPageRenderer.drawPage(at: i, in: bounds)
}

UIGraphicsEndPDFContext()

return data

The whole process works perfectly for until I reach PDF documents that are about 130 pages long. In the simulator, I can print documents that reach 250+ pages without issue. However, for the same data on an actual device (iPhone x or iPad pro), the data.forEach loop runs until the pageIndex hits about 130 and then it fails with the following information at this line:

let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlPage)
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1aa2b0330)
warning: could not execute support code to read Objective-C class data in the process. This may reduce the quality of type information available.

No other exceptions are generated

Memory usage is still OK and while it has spiked, it does not seem too excessive given what I am doing. In addition, it increases like this when running in the simulator and generally only peaks around 300-400MB.

enter image description here

I have tried the following to get around the issue, all without success:

  1. Wrap various parts of the process in autoreleasepool
  2. Make sure it was not my HTML data strings by randomising the data as it is processed. No matter the order it always fails after 130ish records
  3. Switch to iOS13
  4. Change the process to write out each page as a separate PDF to disk. No matter how I do it, it still always fails after 130 pages are created

I am at a loss as it what is going on and am out of ideas as to how to fix this. Any ideas or help would be appreciated.

As per a suggestion in comments, here is the trace from running bt in the debugger:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x1aa326a14)
    frame #0: 0x00000001aa326a14 WebCore`bmalloc::IsoAllocator<bmalloc::IsoConfig<88u> >::allocateSlow(bool) + 252
    frame #1: 0x00000001aaaf7f48 WebCore`WebCore::RenderText::createTextBox() + 28
    frame #2: 0x00000001aab06e90 WebCore`WebCore::RenderTextLineBoxes::createAndAppendLineBox(WebCore::RenderText&) + 40
    frame #3: 0x00000001aa9e2c50 WebCore`WebCore::RenderBlockFlow::constructLine(WebCore::BidiRunList<WebCore::BidiRun>&, WebCore::LineInfo const&) + 344
    frame #4: 0x00000001aa9e5944 WebCore`WebCore::RenderBlockFlow::createLineBoxesFromBidiRuns(unsigned int, WebCore::BidiRunList<WebCore::BidiRun>&, WebCore::InlineIterator const&, WebCore::LineInfo&, WebCore::VerticalPositionCache&, WebCore::BidiRun*, WTF::Vector<WebCore::WordMeasurement, 64ul, WTF::CrashOnOverflow, 16ul>&) + 100
    frame #5: 0x00000001aa9e825c WebCore`WebCore::RenderBlockFlow::layoutRunsAndFloatsInRange(WebCore::LineLayoutState&, WebCore::BidiResolverWithIsolate<WebCore::InlineIterator, WebCore::BidiRun, WebCore::BidiIsolatedRun>&, WebCore::InlineIterator const&, WebCore::BidiStatus const&, unsigned int) + 4576
    frame #6: 0x00000001aa9e5e2c WebCore`WebCore::RenderBlockFlow::layoutRunsAndFloats(WebCore::LineLayoutState&, bool) + 920
    frame #7: 0x00000001aa9ea03c WebCore`WebCore::RenderBlockFlow::layoutLineBoxes(bool, WebCore::LayoutUnit&, WebCore::LayoutUnit&) + 1800
    frame #8: 0x00000001aa9cd5cc WebCore`WebCore::RenderBlockFlow::layoutBlock(bool, WebCore::LayoutUnit) + 1056
    frame #9: 0x00000001aaadad84 WebCore`WebCore::RenderTableCell::layout() + 196
    frame #10: 0x00000001aaae8794 WebCore`WebCore::RenderTableRow::layout() + 252
    frame #11: 0x00000001aaaea914 WebCore`WebCore::RenderTableSection::layout() + 844
    frame #12: 0x00000001aaacf744 WebCore`WebCore::RenderTable::layout() + 1916
    frame #13: 0x00000001aa9cf578 WebCore`WebCore::RenderBlockFlow::layoutBlockChild(WebCore::RenderBox&, WebCore::RenderBlockFlow::MarginInfo&, WebCore::LayoutUnit&, 
.....

If I strip it back to barest essentials and don't even try to create the PDF:

func exportHTMLContentToPDFNoImages(reportName name:String, fromHTMLData data: [String]) throws -> URL? {
        data.shuffled().forEach { (htmlPage) in
            autoreleasepool { () -> Void in
                let _ = UIMarkupTextPrintFormatter(markupText: htmlPage)
            }
        }
        return nil
    }

It still crashes after the 130th pass through the loop. Replacing the htmlPage with some simple html like

<html><body>Hi</body></html>

works just fine. So there must be something in the table I am creating that is causing the issue.

UPDATE

I was able to reduce this to a very simple example that to me demonstrated it was an bug in iOS. I also ended up reporting it to apple. Luckily, it looks like this bug has been fixed in 13.1, so there is a path forward for the future, even if there are issues with iOS 12.

robowen5mac
  • 866
  • 8
  • 17
  • Are you able to reproduce the problem when attached to the device from Xcode? If so, can you provide a stack trace? If Xcode isn't stopping when the exception happens, make sure you have *Exception Breakpoints* enabled. See https://stackoverflow.com/a/17802723/196964 – Doug Richardson Aug 29 '19 at 16:45
  • The info above is from the device attached to Xcode. I don’t get a stack trace or anything useful. Just an automatic break into the debugger with that line highlighted and the first error message. The debug console has the second message. It’s really not much info unfortunately. – robowen5mac Aug 29 '19 at 16:48
  • What about if you open the lldb debugger and run `bt` to get the stack trace? https://stackoverflow.com/a/26770913/196964 – Doug Richardson Aug 29 '19 at 16:49
  • Thanks...didn't know about that one. I updated my answer. Still not sure what is going on, but maybe you will – robowen5mac Aug 29 '19 at 17:53

1 Answers1

0

Either you're running out of memory or the heap has become corrupted. My guess is that, in spite of your memory usage screenshot, you are running out of memory.

This EXC_BREAKPOINT occurs in WebKit's bmalloc::IsoAllocator.

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x1aa326a14)
    frame #0: 0x00000001aa326a14 WebCore`bmalloc::IsoAllocator<bmalloc::IsoConfig<88u> >::allocateSlow(bool) + 252
    frame #1: 0x00000001aaaf7f48 WebCore`WebCore::RenderText::createTextBox() + 28

Here is the line that is raising the exception (note this is from WebKit trunk, which may not be the same as the version running on your phone):


template<typename Config>
BNO_INLINE void* IsoAllocator<Config>::allocateSlow(bool abortOnFailure)
{
    ...

    return m_freeList.allocate<Config>([] () { BCRASH(); return nullptr; });
}

m_freeList.allocate runs what's in the lambda (the part in the parenthesis) if it fails to allocate from it's own list.

BCRASH raises the exception you are seeing. From BAssert.h:

// Crash with a SIGTRAP i.e EXC_BREAKPOINT.
// We are not using __builtin_trap because it is only guaranteed to abort, but not necessarily
// trigger a SIGTRAP. Instead, we use inline asm to ensure that we trigger the SIGTRAP.
#define BCRASH() do { \
        BBreakpointTrap(); \
        __builtin_unreachable(); \
    } while (false)

This allocation appears to be part of WebKit rendering your PDF, but it's likely getting pressure from your growing NSMutableData.

Try using UIGraphicsBeginPDFContextToFile instead of UIGraphicsBeginPDFContextToData and see if the crash still occurs. It is possible the implementation of UIGraphicsBeginPDFContextToFile writes pages to disk instead of putting the entire result in memory all at once like you're doing.

Once you switch to UIGraphicsBeginPDFContextToFile, you can use the PDF as a file or, if you need is loaded into an NSData, use this method to use a memory mapped file:

let data = try Data(contentsOf: URL(fileURLWithPath: newPDFPath), options: .mappedIfSafe)
Doug Richardson
  • 10,483
  • 6
  • 51
  • 77
  • I definitely agree that a memory problem makes sense, however, I don't think its something I can control. I can strip the method back to something as simple as this and it still crashes in exactly the same way. It seems as though the memory in that particular method id not being handled well. data.forEach { (htmlPage) in autoreleasepool { () -> Void in let _ = UIMarkupTextPrintFormatter(markupText: htmlPage) } } – robowen5mac Aug 29 '19 at 19:31
  • Did you try using UIGraphicsBeginPDFContextToFile to see if the crash still occurred? – Doug Richardson Aug 29 '19 at 19:38
  • Sorry, I may not be understanding your suggestion completely. In my comment above, the only code I am calling is what is shown in my comment. I don't even try to write out the PDF or call and UIGraphicsBeing methods. I only loop over the html data and call the UIMarkupTextPrintFormatter method. As a result, if there is a problem with memory I think it is solely due to that method. Am I not understanding? – robowen5mac Aug 29 '19 at 19:40
  • Your 2nd code block in your question calls `UIGraphicsBeginPDFContextToData`. I'm suggesting you replace with with `UIGraphicsBeginPDFContextToFile` to see if the crash still occurs. Once it is written to a file, you can use it as a file instead of an NSMutableData or, if you do actually need it in a data object, load it into an NSData from disk using the mmap option. I'll update the answer to clarify this. – Doug Richardson Aug 29 '19 at 19:44
  • I get that, but in the most recent testing I am doing, that method is never called. I will update my problem – robowen5mac Aug 29 '19 at 19:45
  • Oh, the exception occurs BEFORE the code I'm telling you to swap out is called? – Doug Richardson Aug 29 '19 at 19:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/198681/discussion-between-doug-richardson-and-robowen5mac). – Doug Richardson Aug 29 '19 at 19:50