[LyX/master] Parallelize GUI load of images.

Pavel Sanda <[email protected]> Sat, 11 Jul 2026 02:01:25 +0000
Newsgroups gmane.editors.lyx.cvs
Message-ID <[email protected]>
commit e0dd8db6ade8472fe3017a0b573617fd24d6d781
Author: Pavel Sanda <[email protected]>
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/[email protected]/msg218082.html
    https://www.mail-archive.com/[email protected]/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();
 
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs