[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