birt icon indicating copy to clipboard operation
birt copied to clipboard

Infinite loop when generating a PDF with a table with fixed height and page-break-inside: avoid

Open neilpelow opened this issue 2 months ago • 3 comments

1. The Problem: What's Happening?

An infinite loop can occur during PDF generation under a specific set of conditions:

  • A report design contains a table with a fixed height.
  • A row within that table has the page break property set to page-break-inside: avoid.
  • The content of that single row is too tall to fit on the current page.

When these conditions are met, the BIRT layout manager gets stuck. It tries to place the row, sees that it overflows the page, but is forbidden from splitting it due to the avoid property. It fails to make any progress, retrying the same failed operation endlessly until the print job times out.

2. Root Cause Analysis

The infinite loop is not caused by a single bug but by a harmful interaction between several methods in the layout engine. When a row is too tall to fit on an empty page and cannot be split, the engine fails to advance its state, leading to a loop.

Key Contributing Components:

  • PDFTableBandLM.addToRoot: This method is the first point of failure. It correctly identifies that the tall row overflows the page, but it refuses to add it, even to an empty page. It simply returns false.
  • PDFBlockStackingLM.addArea: This method calls addToRoot but critically ignores its false return value. Because it doesn't know addToRoot failed, it doesn't trigger a page break or advance the layout cursor. The state remains unchanged.
  • RowArea.split: When the layout engine forcefully tries to split the content to make progress, this method is called. However, if the row is marked with page-break-inside: avoid, it returns SUCCEED_WITH_NULL, which essentially means "I succeeded by doing nothing." This response doesn't signal an error or a need for a page break.

How the Loop Occurs: The retry mechanism is driven by BlockContainerArea.split.

  • The layout engine attempts to place the unsplittable, oversized row. addArea calls addToRoot, which returns false (no progress).
  • The engine detects an overflow and forcefully calls split to try and break the content across pages.
  • RowArea.split respects the avoid property and returns SUCCEED_WITH_NULL without actually splitting anything (no progress).
  • The BlockContainerArea sees this "successful" null split and does not change its internal state.
  • The parent caller sees that the content still doesn't fit and triggers the same auto-page-break logic again, returning to step
  • This cycle repeats indefinitely.

neilpelow avatar Oct 13 '25 16:10 neilpelow

Nice and detailed analysis.

This is a duplicate of #2306.

The main cause are conflicting requirements of the report.

Can you provide a PR?

Otherwise, the solution to avoid this error is to just not have conflicting requirements in the report.

hvbtup avatar Oct 14 '25 08:10 hvbtup

Since there still are conflicting requirements, at least one of the requirements cannot be fulfilled. So, what happens now, with your change in effect, ...

  • in your original test case?
  • in more typical cases, when there is no height specified for anything, but the table row eg contains another table or grid which has page-break-inside: avoid set and, when the set of rows of the inner table doesn't fit into the page?

hvbtup avatar Oct 23 '25 06:10 hvbtup

@hvbtup

FYI, I think it would be best to comment about the pull request details on the pull request.

https://github.com/eclipse-birt/birt/pull/2324

I.e., does the specific change have the potential for harmful side-effects?

If your comments about the PR are on the issue, then the PR is may well get merged without taking your reviews into account because reviews normally happen on the PR.

merks avatar Oct 23 '25 07:10 merks