Show progress while loading reads the image sizes
Before decoding, loadImages reads every header to size the batch against the free memory. That pass was serial, silent and could not be cancelled, so the loading window sat at "0 of N". LoadProgress gains onSized, the headers are read in parallel on the same cursor-driven workers as the decodes, and a cancel stops the pass. The window's bar runs twice, "Checking sizes" then "Decoding", and the CLI prints a sizing line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+57
-37
@@ -164,14 +164,55 @@ std::size_t imageFilePixels(const std::string& path) {
|
||||
std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
||||
std::vector<LoadFailure>& failures,
|
||||
const LoadProgress* progress) {
|
||||
const auto cancelled = [progress] {
|
||||
return progress != nullptr && progress->cancel != nullptr &&
|
||||
progress->cancel->load(std::memory_order_relaxed);
|
||||
};
|
||||
// Runs `work(i)` for every path on `workers` threads. Threads take the next
|
||||
// path from a shared cursor rather than a fixed slice, so one huge or slow
|
||||
// file cannot stall the files behind it and progress arrives at a steady
|
||||
// rate. A cancel stops every thread before its next path.
|
||||
const auto forEachPath = [&](std::size_t workers, const auto& work) {
|
||||
std::atomic<std::size_t> cursor{0};
|
||||
const auto run = [&] {
|
||||
for (;;) {
|
||||
if (cancelled()) return;
|
||||
const std::size_t i = cursor.fetch_add(1, std::memory_order_relaxed);
|
||||
if (i >= paths.size()) return;
|
||||
work(i);
|
||||
}
|
||||
};
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers > 0 ? workers - 1 : 0);
|
||||
for (std::size_t w = 1; w < workers; ++w) threads.emplace_back(run);
|
||||
run();
|
||||
for (auto& thread : threads) thread.join();
|
||||
};
|
||||
|
||||
unsigned hardware = std::thread::hardware_concurrency();
|
||||
if (hardware == 0) hardware = 1;
|
||||
std::size_t workers = std::min<std::size_t>(hardware, std::max<std::size_t>(paths.size(), 1));
|
||||
|
||||
// What the batch holds once decoded, and what one decode needs on top
|
||||
// while it runs: the pixbuf (with alpha, at worst) and its rotated copy.
|
||||
// Reading a header can mean a round trip to a slow disk or a network
|
||||
// share, so the headers are read in parallel too, and reported.
|
||||
std::vector<std::size_t> pixels(paths.size(), 0);
|
||||
std::atomic<std::size_t> sized{0};
|
||||
forEachPath(workers, [&](std::size_t i) {
|
||||
pixels[i] = imageFilePixels(paths[i]);
|
||||
if (progress != nullptr && progress->onSized) {
|
||||
progress->onSized(sized.fetch_add(1, std::memory_order_relaxed) + 1, paths.size(),
|
||||
paths[i]);
|
||||
}
|
||||
});
|
||||
if (cancelled()) return {};
|
||||
|
||||
std::size_t kept = 0;
|
||||
std::size_t largest = 0;
|
||||
for (const auto& path : paths) {
|
||||
const std::size_t pixels = imageFilePixels(path);
|
||||
kept += pixels * 3;
|
||||
largest = std::max(largest, pixels * 8);
|
||||
for (const std::size_t count : pixels) {
|
||||
kept += count * 3;
|
||||
largest = std::max(largest, count * 8);
|
||||
}
|
||||
const std::string what = paths.size() == 1
|
||||
? std::string("Loading this image")
|
||||
@@ -180,45 +221,24 @@ std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
||||
const std::size_t spare = usableMemory() - std::min(usableMemory(), kept);
|
||||
|
||||
// Decoding is the dominant cost of a headless run, and the files are
|
||||
// independent, so they are decoded in parallel. Threads take the next path
|
||||
// from a shared cursor rather than a fixed slice, so one huge file cannot
|
||||
// stall the files behind it and progress arrives at a steady rate. Slots
|
||||
// keep the result in the order the paths were given.
|
||||
// independent, so they are decoded in parallel. Slots keep the result in
|
||||
// the order the paths were given.
|
||||
std::vector<Image> slots(paths.size());
|
||||
std::vector<std::string> errors(paths.size());
|
||||
std::atomic<std::size_t> cursor{0};
|
||||
std::atomic<std::size_t> done{0};
|
||||
|
||||
unsigned hardware = std::thread::hardware_concurrency();
|
||||
if (hardware == 0) hardware = 1;
|
||||
std::size_t workers = std::min<std::size_t>(hardware, std::max<std::size_t>(paths.size(), 1));
|
||||
if (largest > 0) workers = std::clamp<std::size_t>(spare / largest, 1, workers);
|
||||
|
||||
const auto decode = [&] {
|
||||
for (;;) {
|
||||
if (progress != nullptr && progress->cancel != nullptr &&
|
||||
progress->cancel->load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
const std::size_t i = cursor.fetch_add(1, std::memory_order_relaxed);
|
||||
if (i >= paths.size()) return;
|
||||
try {
|
||||
slots[i] = Image::load(paths[i]);
|
||||
} catch (const std::exception& error) {
|
||||
errors[i] = error.what();
|
||||
}
|
||||
if (progress != nullptr && progress->onFile) {
|
||||
progress->onFile(done.fetch_add(1, std::memory_order_relaxed) + 1, paths.size(),
|
||||
paths[i]);
|
||||
}
|
||||
forEachPath(workers, [&](std::size_t i) {
|
||||
try {
|
||||
slots[i] = Image::load(paths[i]);
|
||||
} catch (const std::exception& error) {
|
||||
errors[i] = error.what();
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers > 0 ? workers - 1 : 0);
|
||||
for (std::size_t w = 1; w < workers; ++w) threads.emplace_back(decode);
|
||||
decode();
|
||||
for (auto& thread : threads) thread.join();
|
||||
if (progress != nullptr && progress->onFile) {
|
||||
progress->onFile(done.fetch_add(1, std::memory_order_relaxed) + 1, paths.size(),
|
||||
paths[i]);
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<Image> images;
|
||||
images.reserve(paths.size());
|
||||
|
||||
+5
-2
@@ -61,9 +61,12 @@ struct LoadFailure {
|
||||
};
|
||||
|
||||
// Lets a caller follow a long batch decode and abort it. Both members are
|
||||
// touched from the decoding threads, so `onFile` must be thread-safe and must
|
||||
// not block: the GUI only bumps a counter from it and wakes the main thread.
|
||||
// touched from the decoding threads, so `onSized` and `onFile` must be
|
||||
// thread-safe and must not block: the GUI only bumps a counter from them and
|
||||
// wakes the main thread. `onSized` follows the first pass, which reads every
|
||||
// header to size the batch; `onFile` follows the decodes after it.
|
||||
struct LoadProgress {
|
||||
std::function<void(std::size_t done, std::size_t total, const std::string& path)> onSized;
|
||||
std::function<void(std::size_t done, std::size_t total, const std::string& path)> onFile;
|
||||
std::atomic<bool>* cancel = nullptr;
|
||||
};
|
||||
|
||||
+19
-3
@@ -1388,6 +1388,7 @@ void EffectorWindow::addPaths(const std::vector<std::string>& paths) {
|
||||
|
||||
loading_ = true;
|
||||
loadCancel_.store(false, std::memory_order_relaxed);
|
||||
loadSized_.store(0, std::memory_order_relaxed);
|
||||
loadDone_.store(0, std::memory_order_relaxed);
|
||||
loadTotal_ = paths.size();
|
||||
{
|
||||
@@ -1409,6 +1410,14 @@ void EffectorWindow::addPaths(const std::vector<std::string>& paths) {
|
||||
loader_ = std::thread([this, paths] {
|
||||
LoadProgress progress;
|
||||
progress.cancel = &loadCancel_;
|
||||
progress.onSized = [this](std::size_t sized, std::size_t, const std::string& path) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(loadMutex_);
|
||||
loadCurrent_ = path;
|
||||
}
|
||||
loadSized_.store(sized, std::memory_order_relaxed);
|
||||
loadProgress_.emit();
|
||||
};
|
||||
progress.onFile = [this](std::size_t done, std::size_t, const std::string& path) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(loadMutex_);
|
||||
@@ -1438,15 +1447,22 @@ void EffectorWindow::addPaths(const std::vector<std::string>& paths) {
|
||||
}
|
||||
|
||||
void EffectorWindow::onLoadProgress() {
|
||||
if (loadCancel_.load(std::memory_order_relaxed)) return; // keep "Cancelling…" up
|
||||
const std::size_t sized = loadSized_.load(std::memory_order_relaxed);
|
||||
const std::size_t done = loadDone_.load(std::memory_order_relaxed);
|
||||
std::string current;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(loadMutex_);
|
||||
current = loadCurrent_;
|
||||
}
|
||||
// The headers are all read before the first decode starts, so the bar
|
||||
// runs twice: once sizing the batch, then once decoding it.
|
||||
if (loadBar_ != nullptr && loadTotal_ > 0) {
|
||||
loadBar_->set_fraction(static_cast<double>(done) / static_cast<double>(loadTotal_));
|
||||
loadBar_->set_text(std::to_string(done) + " of " + std::to_string(loadTotal_));
|
||||
const bool sizing = sized < loadTotal_ && done == 0;
|
||||
const std::size_t count = sizing ? sized : done;
|
||||
loadBar_->set_fraction(static_cast<double>(count) / static_cast<double>(loadTotal_));
|
||||
loadBar_->set_text((sizing ? "Checking sizes: " : "Decoding: ") + std::to_string(count) +
|
||||
" of " + std::to_string(loadTotal_));
|
||||
}
|
||||
if (loadFileLabel_ != nullptr && !current.empty()) {
|
||||
loadFileLabel_->set_text(std::filesystem::path(current).filename().string());
|
||||
@@ -1552,7 +1568,7 @@ void EffectorWindow::showLoadWindow() {
|
||||
|
||||
auto* bar = Gtk::make_managed<Gtk::ProgressBar>();
|
||||
bar->set_show_text(true);
|
||||
bar->set_text("0 of " + std::to_string(loadTotal_));
|
||||
bar->set_text("Checking sizes: 0 of " + std::to_string(loadTotal_));
|
||||
box->append(*bar);
|
||||
loadBar_ = bar;
|
||||
|
||||
|
||||
+2
-1
@@ -70,7 +70,7 @@ private:
|
||||
void onImagesChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
|
||||
const Glib::RefPtr<Gtk::FileDialog>& dialog);
|
||||
void addPaths(const std::vector<std::string>& paths); // decodes in the background
|
||||
void onLoadProgress(); // dispatcher: one file finished decoding
|
||||
void onLoadProgress(); // dispatcher: one file sized or decoded
|
||||
void onLoadFinished(); // dispatcher: the batch is done or was cancelled
|
||||
void showLoadWindow();
|
||||
void closeLoadWindow();
|
||||
@@ -219,6 +219,7 @@ private:
|
||||
Glib::Dispatcher loadProgress_;
|
||||
Glib::Dispatcher loadFinished_;
|
||||
std::atomic<bool> loadCancel_{false};
|
||||
std::atomic<std::size_t> loadSized_{0}; // headers read (sizing the batch)
|
||||
std::atomic<std::size_t> loadDone_{0};
|
||||
std::size_t loadTotal_ = 0;
|
||||
std::mutex loadMutex_;
|
||||
|
||||
@@ -92,6 +92,11 @@ int renderHeadless(const effector::Options& options) {
|
||||
std::mutex progressMutex;
|
||||
const bool showProgress = isatty(fileno(stderr)) != 0 && options.images.size() > 1;
|
||||
if (showProgress) {
|
||||
progress.onSized = [&](std::size_t done, std::size_t total, const std::string& path) {
|
||||
std::lock_guard<std::mutex> lock(progressMutex);
|
||||
std::cerr << "\r\033[Ksizing " << done << "/" << total << " "
|
||||
<< std::filesystem::path(path).filename().string() << std::flush;
|
||||
};
|
||||
progress.onFile = [&](std::size_t done, std::size_t total, const std::string& path) {
|
||||
std::lock_guard<std::mutex> lock(progressMutex);
|
||||
std::cerr << "\r\033[Kreading " << done << "/" << total << " "
|
||||
|
||||
Reference in New Issue
Block a user