Optimisation solution for long text frame chains

Previous topic - Next topic

phoebe

Hi everyone,

My first time posting, and I hope this is the right way to go about it.

I started using Scribus with scripting as part of an automation solution and immediately hit the problem that has been mentioned here previously about the linear slow-down when working with long chains of text frames. Each new frame takes longer than the last.

I did some tests to establish that it wasn't related to the page count. Nope, no problem there, even at 500 poges. Then I tried dividing the long text I was using into 4 parts and starting each in a new frame not linked to the previous chunk. That worked as expected. Now the delays were localised into four smaller ramps instead of one long one. Append the text, however, to a previous frame when starting a new spill and the larger linear growth returns.

This seemed likely related to some sort of recursive behaviour. I figured that each time a new frame was added, all the previous ones were marked as needing an update, too, just to be safe. But I'm working on a deterministic layout solution so once a page has been done, it's almost certainly done, minus some potential reflow on the current page when linking to a new page due to widow orphan control or similar. So I just need to find a way to stop the reflow except for the last page when linking its frame to a new one.

Then I remembered, PageMaker had a little thing in its scripting to pause reflow. I think InD might have it too, but I don't recall. I checked the code in Scribus and there definitely isn't anything, so I downloaded the 1.7 source, added it to the scripting API and obviously added a few bits to the main source here and there to implement. Rebuilt on my Mac, and it works perfectly.

With reflow paused the actual chain time over 106 pages is 24x faster. The final page takes 1.86s with reflow on, and 0.06s with it turned off. Average time per page reduces from 0.95s to 0.04s. The solution is 3.3x faster overall, but that's because importing is a massive bottleneck, and I don't yet understand why, so I've separated import time from the results. Also, this was just a txt file. I haven't tried other formats yet, although the solution I'm working on will be using docbook.xlm.

I don't know how to submit this for consideration, and I'm not much of a programmer. I've had a lot of experience with system design, etc, and lots of scripting, but not much c++. I had to use AI to help point me to the right places in the code, so this has not had a proper look over. It just happens to work in my testing, and the changes are very low footprint.

You can use this in a script simply by calling scribus.setReflow(0) to pause reflow, and scribus.setReflow(1) to turn it back on. And it will always turn reflow back on when script execution ends, even if it's in an error or crash. Turning reflow back on during a script will be useful as well to give pages a chance to settle in again, I presume, and to get on with other layout tasks. While that will then trigger a full update to all frames (it does this deliberately anyway, when its state is changed), this only happens once instead of repeatedly, so the time improvements remain.

Super simple, really effective, not sure how to share it except here.

Hope it helps,
Phoebe

Test 1 - Increasing time for each new frame
============================================================
Run: 2026-04-23 12:31:34
Mode: CONTINUOUS
Reflow: enabled (no setReflow)
Trim: 152.4 x 228.6 mm (6" x 9")
Frame: 107.0 x 177.8 mm
Input: ulyssess_small.txt (269582 chars) x 1 iterations

Total pages: 107
Total time: 136.85s
  Import time: 35.20s
  Chain time:  101.58s
  Avg per chain page: 0.9493s
  Fastest chain: page 1 (0.0000s)
  Slowest chain: page 105 (1.8610s)

Sampled pages (10 of 107):
  Page   1: 0.0000s  #
  Page  13: 0.2034s  ##
  Page  25: 0.4450s  ####
  Page  36: 0.5861s  #####
  Page  48: 1.0333s  ##########
  Page  60: 1.0533s  ##########
  Page  72: 1.2571s  ############
  Page  83: 1.3777s  #############
  Page  95: 1.6915s  ################
  Page 107: 1.8068s  ##################

Test 2 - Reflow paused, adding frames to a chain, even a quite long one, always proceeds at the same pace
============================================================
Run: 2026-04-23 12:32:48
Mode: CONTINUOUS
Reflow: paused during build
Trim: 152.4 x 228.6 mm (6" x 9")
Frame: 107.0 x 177.8 mm
Input: ulyssess_small.txt (269582 chars) x 1 iterations

Total pages: 107
Total time: 41.31s
  Import time: 37.02s
  Chain time:  4.22s
  Avg per chain page: 0.0394s
  Fastest chain: page 1 (0.0000s)
  Slowest chain: page 2 (0.0612s)

Sampled pages (10 of 107):
  Page   1: 0.0000s  #
  Page  13: 0.0351s  #
  Page  25: 0.0372s  #
  Page  36: 0.0570s  #
  Page  48: 0.0435s  #
  Page  60: 0.0498s  #
  Page  72: 0.0443s  #
  Page  83: 0.0452s  #
  Page  95: 0.0340s  #
  Page 107: 0.0208s  #

Path file
From: DLS Project
Subject: [PATCH] Add setReflow() API to fix text frame chain performance degradation

Add a `setReflow(enabled)` Python API to let scripts pause
backward invalidation and chain walking during linked frame construction,
reducing the per-page cost.

Reflow is automatically re-enabled when a script finishes (even on crash)
via the finishScriptRun() cleanup path.

Benchmark: 107-page single-chain document (ulyssess_small.txt, 270K chars)
  Before: 101.58s chain time, avg 0.95s/page, slowest page 1.86s (page 105)
  After:    4.22s chain time, avg 0.04s/page, slowest page 0.06s (page 2)
  Total build: 136.85s -> 41.31s (3.3x faster)
  Import time: unchanged (~35s)

diff --git a/scribus/pageitem.cpp b/scribus/pageitem.cpp
index 64e91e7350..fe11cc3d33 100644
--- a/scribus/pageitem.cpp
+++ b/scribus/pageitem.cpp
@@ -1176,11 +1176,14 @@ void PageItem::link(PageItem* nxt, bool addPARSEP)
  }
  }
  invalid = true;
- PageItem* prev = this;
- while (prev->m_backBox && !prev->m_backBox->frameOverflows())
+ if (m_Doc->m_reflowEnabled)
  {
- prev->m_backBox->invalid = true;
- prev = prev->m_backBox;
+ PageItem* prev = this;
+ while (prev->m_backBox && !prev->m_backBox->frameOverflows())
+ {
+ prev->m_backBox->invalid = true;
+ prev = prev->m_backBox;
+ }
  }
  while (nxt)
  {
diff --git a/scribus/pageitem_textframe.cpp b/scribus/pageitem_textframe.cpp
index 3b48782545..b5b9e815a0 100644
--- a/scribus/pageitem_textframe.cpp
+++ b/scribus/pageitem_textframe.cpp
@@ -1177,20 +1177,28 @@ void PageItem_TextFrame::layout()
 // printBacktrace(24);
  if (m_backBox != nullptr)
  {
-// qDebug("textframe: len=%d, going back", itemText.length());
  PageItem_TextFrame* firstInvalid = nullptr;
- PageItem_TextFrame* prevInChain  = dynamic_cast<PageItem_TextFrame*>(m_backBox);
- while (prevInChain)
+ if (m_Doc->m_reflowEnabled)
  {
- if (prevInChain->invalid)
- firstInvalid = prevInChain;
- prevInChain = dynamic_cast<PageItem_TextFrame*>(prevInChain->m_backBox);
+ PageItem_TextFrame* prevInChain  = dynamic_cast<PageItem_TextFrame*>(m_backBox);
+ while (prevInChain)
+ {
+ if (prevInChain->invalid)
+ firstInvalid = prevInChain;
+ prevInChain = dynamic_cast<PageItem_TextFrame*>(prevInChain->m_backBox);
+ }
+ PageItem_TextFrame* nextInChain = firstInvalid;
+ while (nextInChain && (nextInChain != this))
+ {
+ nextInChain->layout();
+ nextInChain = dynamic_cast<PageItem_TextFrame*>(nextInChain->m_nextBox);
+ }
  }
- PageItem_TextFrame* nextInChain = firstInvalid;
- while (nextInChain && (nextInChain != this))
+ else if (invalid)
  {
- nextInChain->layout();
- nextInChain = dynamic_cast<PageItem_TextFrame*>(nextInChain->m_nextBox);
+ PageItem_TextFrame* prevFrame = dynamic_cast<PageItem_TextFrame*>(m_backBox);
+ if (prevFrame)
+ firstChar = prevFrame->m_maxChars;
  }
  // #9592 : warning, BackBox->layout() may not layout BackBox next box
  if (!invalid)
diff --git a/scribus/plugins/scriptplugin/scriptercore.cpp b/scribus/plugins/scriptplugin/scriptercore.cpp
index 92948e67b7..f253ea6b4a 100644
--- a/scribus/plugins/scriptplugin/scriptercore.cpp
+++ b/scribus/plugins/scriptplugin/scriptercore.cpp
@@ -162,6 +162,12 @@ void ScripterCore::finishScriptRun()
  if (!mainWin->HaveDoc)
  return;
 
+ if (!mainWin->doc->m_reflowEnabled)
+ {
+ mainWin->doc->m_reflowEnabled = true;
+ mainWin->doc->invalidateAll();
+ }
+
  mainWin->propertiesPalette->setDoc(mainWin->doc);
  mainWin->contentPalette->setDoc(mainWin->doc);
  mainWin->marksManager->setDoc(mainWin->doc);
diff --git a/scribus/plugins/scriptplugin/scriptplugin.cpp b/scribus/plugins/scriptplugin/scriptplugin.cpp
--- a/scribus/plugins/scriptplugin/scriptplugin.cpp
+++ b/scribus/plugins/scriptplugin/scriptplugin.cpp
@@ -*,* +*,* @@
+ { "setReflow", scribus_setreflow, METH_VARARGS, tr(scribus_setreflow__doc__)},
diff --git a/scribus/scribusdoc.h b/scribus/scribusdoc.h
index 1ce4404326..ac78a8b005 100644
--- a/scribus/scribusdoc.h
+++ b/scribus/scribusdoc.h
@@ -1480,6 +1480,8 @@ public:
  int TotalItems {0};
  PrintOptions Print_Options;
  bool RePos {false};
+ bool m_reflowEnabled {true};
+
  struct BookMa {
  QString Title;
  QString Text;

MrB

Hi Phoebe.

Thanks for the post. The best place to submit code changes and sample documents, scripts etc is on bugs.scribus.net (it's a better interface for uploads etc). Then when we resolve, we automatically get changelog updates and all of that, plus we want bug numbers to refer to. This will need review, in particular by @jghali. In any case, we can continue the conversation here for sure.

thanks
Craig