[LyX/master] Parallelize GUI load of images.
Pavel Sanda
sanda at lyx.org
Sat Jul 11 02:01:25 UTC 2026
commit e0dd8db6ade8472fe3017a0b573617fd24d6d781
Author: Pavel Sanda <sanda at lyx.org>
Date: Sat Jul 11 02:20:55 2026 +0200
Parallelize GUI load of images.
Load takes quite long on figure-heavy reports, underutilizes usual multicore
CPUs and its more of us, who scroll through the document just to load all
images; that's absurd.
Two improvements:
- default parallelize up to 4 threads if available on the machine
- new LFUN buffer-load-graphics [all] [<THREADS>|max] which triggers
load machinery for the whole document ("all" = including childern);
optionally you can explicitely setup number of <THREADS> or give it
max, so it uses all cores on the current machine.
https://www.mail-archive.com/lyx-devel@lists.lyx.org/msg218082.html
https://www.mail-archive.com/lyx-devel@lists.lyx.org/msg226451.html
Draft by Claude, review by me. As he concludes:
"Nice bit of archaeology as a bookend: lazy image loading itself - the behavior
buffer-load-graphics now works around - was introduced on 2002-02-27 (commit
607ad8d3a7, "lazy loading (don't try and load the image until a request to draw
it is received)"), fixing ticket #37 "InsetGraphics previews render
synchronously". Twenty-four years from "make it lazy" to "let me force it
eager, in parallel".
Assisted-by: Claude Opus 4.8
---
lib/RELEASE-NOTES | 8 ++++++
src/Buffer.cpp | 33 ++++++++++++++++++++++++
src/FuncCode.h | 1 +
src/LyXAction.cpp | 15 +++++++++++
src/insets/InsetGraphics.cpp | 7 ++++++
src/insets/InsetGraphics.h | 2 ++
src/insets/RenderGraphic.cpp | 12 +++++++++
src/insets/RenderGraphic.h | 2 ++
src/support/ForkedCalls.cpp | 60 ++++++++++++++++++++++++++------------------
src/support/ForkedCalls.h | 3 +++
10 files changed, 119 insertions(+), 24 deletions(-)
diff --git a/lib/RELEASE-NOTES b/lib/RELEASE-NOTES
index f8a223afab..b6fe35c956 100644
--- a/lib/RELEASE-NOTES
+++ b/lib/RELEASE-NOTES
@@ -5,6 +5,10 @@
!!Documents compilation process and images conversion
+- Conversion of images for on-screen display now runs up to 4 conversion
+ processes in parallel. The limit can be changed via the optional
+ argument of the buffer-load-graphics lfun.
+
!!!The following pref variables were added in 2.:
@@ -22,6 +26,10 @@
!!!The following new LyX functions have been introduced in 2.6:
+- buffer-load-graphics: triggers preview conversion for every image in
+ the current buffer at once, instead of waiting for each image to be
+ scrolled into view.
+
!!!The following LyX functions have been changed in 2.6:
- The function newpage-insert has been renamed to textbreak-insert
diff --git a/src/Buffer.cpp b/src/Buffer.cpp
index 8122a05843..9c301c36d9 100644
--- a/src/Buffer.cpp
+++ b/src/Buffer.cpp
@@ -70,6 +70,7 @@
#include "xml.h"
#include "insets/InsetBranch.h"
+#include "insets/InsetGraphics.h"
#include "insets/InsetInclude.h"
#include "insets/InsetText.h"
@@ -94,6 +95,7 @@
#include "support/FileName.h"
#include "support/FileNameList.h"
#include "support/filetools.h"
+#include "support/ForkedCalls.h"
#include "support/gettext.h"
#include "support/gzstream.h"
#include "support/Lexer.h"
@@ -2888,6 +2890,10 @@ bool Buffer::getStatus(FuncRequest const & cmd, FuncStatus & flag) const
enable = (d->preview_file_).exists() && !(d->preview_file_).isFileEmpty();
break;
+ case LFUN_BUFFER_LOAD_GRAPHICS:
+ enable = true;
+ break;
+
case LFUN_CHANGES_TRACK:
flag.setEnabled(true);
flag.setOnOff(params().track_changes);
@@ -3163,6 +3169,33 @@ void Buffer::dispatch(FuncRequest const & func, DispatchResult & dr)
dr.setMessage(_("Error viewing the output file."));
break;
+ case LFUN_BUFFER_LOAD_GRAPHICS: {
+ ListOfBuffers bufs;
+ for (string const & arg :
+ getVectorFromString(to_utf8(func.argument()), " ")) {
+ if (arg == "all") //include all childern
+ bufs = getDescendants();
+ else if (arg == "max") //use all available cores
+ ForkedCallQueue::setMaxRunning(0);
+ else
+ ForkedCallQueue::setMaxRunning(convert<int>(arg));
+ }
+ bufs.push_front(this);
+ int counted = 0;
+ for (Buffer * b : bufs) {
+ InsetIterator it = begin(b->inset());
+ InsetIterator const itend = end(b->inset());
+ for (; it != itend; ++it) {
+ if (it->lyxCode() == GRAPHICS_CODE) {
+ static_cast<InsetGraphics const &>(*it).preload();
+ ++counted;
+ }
+ }
+ }
+ //dr.setMessage(bformat(_("Queued %1$d image(s) for preview."), counted));
+ break;
+ }
+
case LFUN_CHANGES_TRACK:
if (params().save_transient_properties)
undo().recordUndoBufferParams();
diff --git a/src/FuncCode.h b/src/FuncCode.h
index 0a56683779..5c3259dd68 100644
--- a/src/FuncCode.h
+++ b/src/FuncCode.h
@@ -461,6 +461,7 @@ enum FuncCode
LFUN_SEPARATOR_INSERT, // ef 20140502
LFUN_SERVER_GET_STATISTICS, // brokenclock 20141010
LFUN_BUFFER_VIEW_CACHE, // skostysh 20150401
+ LFUN_BUFFER_LOAD_GRAPHICS, // ps 20260623
// 360
LFUN_BUFFER_MOVE_NEXT, // skostysh 20150408
LFUN_BUFFER_MOVE_PREVIOUS, // skostysh 20150408
diff --git a/src/LyXAction.cpp b/src/LyXAction.cpp
index 0e0826ead4..390312bfa8 100644
--- a/src/LyXAction.cpp
+++ b/src/LyXAction.cpp
@@ -904,6 +904,21 @@ void LyXAction::init()
*/
{ LFUN_BUFFER_VIEW_CACHE, "buffer-view-cache", ReadOnly, Buffer },
+/*!
+ * \var lyx::FuncCode lyx::LFUN_BUFFER_LOAD_GRAPHICS
+ * \li Action: Triggers image preview for every image in the current buffer,
+ instead of waiting for each image to be scrolled into view.
+ * \li Syntax: buffer-load-graphics [all] [<THREADS>|max]
+ * \li Params: all: also process all child documents; by default only the
+ current buffer is processed.
+ <THREADS>: fix number os threads for conversion, or 'max' to use
+ all cores available on the system.\n
+ The setting persists until changed again.
+ * \li Origin: ps, 23 Jun 2026
+ * \endvar
+ */
+ { LFUN_BUFFER_LOAD_GRAPHICS, "buffer-load-graphics", ReadOnly, Buffer },
+
/*!
* \var lyx::FuncCode lyx::LFUN_BUFFER_WRITE
* \li Action: Saves the current buffer.
diff --git a/src/insets/InsetGraphics.cpp b/src/insets/InsetGraphics.cpp
index 0ca24e0488..c087533905 100644
--- a/src/insets/InsetGraphics.cpp
+++ b/src/insets/InsetGraphics.cpp
@@ -1157,6 +1157,13 @@ InsetGraphicsParams const & InsetGraphics::params() const
}
+void InsetGraphics::preload() const
+{
+ if (graphic_)
+ graphic_->preload();
+}
+
+
void InsetGraphics::editGraphics(InsetGraphicsParams const & p) const
{
theFormats().edit(buffer(), p.filename,
diff --git a/src/insets/InsetGraphics.h b/src/insets/InsetGraphics.h
index 95289832cb..7f7cb2e81d 100644
--- a/src/insets/InsetGraphics.h
+++ b/src/insets/InsetGraphics.h
@@ -91,6 +91,8 @@ public:
docstring layoutName() const override { return from_ascii("Graphics"); }
/// Get the inset parameters, used by the GUIndependent dialog.
InsetGraphicsParams const & params() const;
+ /// Force preview generation now (skip the wait-for-paint).
+ void preload() const;
///
int topOffset(BufferView const *) const override { return 0; }
diff --git a/src/insets/RenderGraphic.cpp b/src/insets/RenderGraphic.cpp
index 67d44e1bb4..5f88372953 100644
--- a/src/insets/RenderGraphic.cpp
+++ b/src/insets/RenderGraphic.cpp
@@ -51,6 +51,7 @@ RenderBase * RenderGraphic::clone(Inset const * inset) const
return new RenderGraphic(*this, inset);
}
+
void RenderGraphic::reload() const
{
loader_.reload();
@@ -132,6 +133,17 @@ bool readyToDisplay(graphics::Loader const & loader)
} // namespace
+void RenderGraphic::preload() const
+{
+ if (!displayGraphic(params_))
+ return;
+ // ErrorConverting is included so we retry failed conversions.
+ if (loader_.status() == graphics::WaitingToLoad
+ || loader_.status() == graphics::ErrorConverting)
+ loader_.startLoading();
+}
+
+
void RenderGraphic::metrics(MetricsInfo & mi, Dimension & dim) const
{
if (displayGraphic(params_)) {
diff --git a/src/insets/RenderGraphic.h b/src/insets/RenderGraphic.h
index fc631a0c7a..2b175008a3 100644
--- a/src/insets/RenderGraphic.h
+++ b/src/insets/RenderGraphic.h
@@ -35,6 +35,8 @@ public:
/// Refresh the info about which file to display and how to display it.
void update(graphics::Params const & params);
+ /// Trigger conversion, no-op if the image is already loading, loaded.
+ void preload() const;
/// Reloads the image if necessary
void reload() const;
diff --git a/src/support/ForkedCalls.cpp b/src/support/ForkedCalls.cpp
index 054290d0dd..c81c8af868 100644
--- a/src/support/ForkedCalls.cpp
+++ b/src/support/ForkedCalls.cpp
@@ -20,11 +20,13 @@
#include "support/lyxlib.h"
#include "support/Timeout.h"
+#include <algorithm>
#include <cerrno>
#include <cstring>
#include <list>
#include <queue>
#include <sstream>
+#include <thread>
#include <utility>
#include <vector>
@@ -443,18 +445,23 @@ typedef pair<string, ForkedCall::sigPtr> Process;
/// in-progress queue
static queue<Process> callQueue_;
-/// flag whether queue is running
-static bool running_ = false;
+/// Maximum number of forked children allowed to run concurrently.
+/// Graphics conversion is the only client of this queue.
+/// Default: min(4, #cores), serial if the hardware cannot be queried.
+/// Adjustable at runtime via setMaxRunning().
+static int max_running_ =
+ max(1, min(4, int(std::thread::hardware_concurrency())));
+
+/// Number of children currently running.
+static int running_count_ = 0;
///
-void startCaller();
-///
-void stopCaller();
+void callNext();
///
void callback(pid_t, int);
-/** Add a process to the queue. Processes are forked sequentially
- * only one is running at a time.
+/** Add a process to the queue. Up to max_running_ processes are
+ * forked in parallel; the remainder wait for a slot to free up.
* Connect to the returned signal and you'll be informed when
* the process has ended.
*/
@@ -463,8 +470,10 @@ ForkedCall::sigPtr add(string const & process)
ForkedCall::sigPtr ptr;
ptr.reset(new ForkedCall::sig);
callQueue_.push(Process(process, ptr));
- if (!running_)
- startCaller();
+ if (running_count_ == 0)
+ LYXERR(Debug::GRAPHICS, "ForkedCallQueue: waking up");
+ while (running_count_ < max_running_ && !callQueue_.empty())
+ callNext();
return ptr;
}
@@ -477,6 +486,7 @@ void callNext()
callQueue_.pop();
// Bind our chain caller
pro.second->connect(callback);
+ ++running_count_;
ForkedCall call;
//If we fail to fork the process, then emit the signal
//to tell the outside world that it failed.
@@ -487,31 +497,33 @@ void callNext()
void callback(pid_t, int)
{
- if (callQueue_.empty())
- stopCaller();
- else
+ --running_count_;
+ while (running_count_ < max_running_ && !callQueue_.empty())
callNext();
+ if (running_count_ == 0 && callQueue_.empty())
+ LYXERR(Debug::GRAPHICS, "ForkedCallQueue: I'm going to sleep");
}
-void startCaller()
-{
- LYXERR(Debug::GRAPHICS, "ForkedCallQueue: waking up");
- running_ = true ;
- callNext();
-}
-
-
-void stopCaller()
+void setMaxRunning(int n)
{
- running_ = false ;
- LYXERR(Debug::GRAPHICS, "ForkedCallQueue: I'm going to sleep");
+ if (n < 1) { //All cores available on the system.
+ int const hw = int(std::thread::hardware_concurrency());
+ if (hw < 1) // Keep the current setting if the hardware cannot be queried.
+ return;
+ n = hw;
+ }
+ max_running_ = n;
+ LYXERR(Debug::GRAPHICS, "ForkedCallQueue: max parallel processes set to " << n);
+ // If the cap was raised while jobs were waiting, top up the pool.
+ while (running_count_ < max_running_ && !callQueue_.empty())
+ callNext();
}
bool running()
{
- return running_;
+ return running_count_ > 0;
}
} // namespace ForkedCallQueue
diff --git a/src/support/ForkedCalls.h b/src/support/ForkedCalls.h
index 84aeac9679..6c41ca9d9b 100644
--- a/src/support/ForkedCalls.h
+++ b/src/support/ForkedCalls.h
@@ -196,6 +196,9 @@ private:
namespace ForkedCallQueue {
ForkedCall::sigPtr add(std::string const & process);
+/// Set the maximum number of processes forked in parallel;
+/// n < 1 means all cores available on the system.
+void setMaxRunning(int n);
/// Query whether the queue is running a forked process now.
bool running();
More information about the lyx-cvs
mailing list