From adb4325d79a34ce5625c29ba506533adc713e719 Mon Sep 17 00:00:00 2001 From: Pengfei Date: Wed, 18 Aug 2021 20:40:53 +0800 Subject: [PATCH] Add a rate limiter to Qt's progress dialog To work around a macOS specific issue where theh progress bar is not updated. With this the previous workaround for other OSes also isn't needed. --- src/frontend/CMakeLists.txt | 2 + src/frontend/helpers/multi_job.cpp | 133 +++++++++--------- src/frontend/helpers/multi_job.h | 111 +++++++-------- .../helpers/rate_limited_progress_dialog.cpp | 34 +++++ .../helpers/rate_limited_progress_dialog.h | 21 +++ src/frontend/helpers/simple_job.cpp | 30 ++-- src/frontend/import_dialog.cpp | 87 +++++------- 7 files changed, 229 insertions(+), 189 deletions(-) create mode 100644 src/frontend/helpers/rate_limited_progress_dialog.cpp create mode 100644 src/frontend/helpers/rate_limited_progress_dialog.h diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 00632fe..562b07a 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -15,6 +15,8 @@ add_executable(threeSD helpers/frontend_common.h helpers/multi_job.cpp helpers/multi_job.h + helpers/rate_limited_progress_dialog.cpp + helpers/rate_limited_progress_dialog.h helpers/simple_job.cpp helpers/simple_job.h cia_build_dialog.cpp diff --git a/src/frontend/helpers/multi_job.cpp b/src/frontend/helpers/multi_job.cpp index 99c4da4..09d1ac3 100644 --- a/src/frontend/helpers/multi_job.cpp +++ b/src/frontend/helpers/multi_job.cpp @@ -1,66 +1,67 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "frontend/helpers/multi_job.h" - -MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, - std::vector contents_, ExecuteFunc execute_func_, - AbortFunc abort_func_) - : QThread(parent), importer(importer_), contents(std::move(contents_)), - execute_func(std::move(execute_func_)), abort_func(abort_func_) {} - -MultiJob::~MultiJob() = default; - -void MultiJob::run() { - u64 total_size = 0; - for (const auto& content : contents) { - total_size += content.maximum_size; - } - - std::size_t count = 0; - int eta = -1; - - const auto initial_time = std::chrono::steady_clock::now(); - const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) { - if (size_imported < 10 * 1024 * 1024) { // 10M Threshold - return; - } - using namespace std::chrono; - const u64 time_elapsed = - duration_cast(steady_clock::now() - initial_time).count(); - eta = - static_cast(time_elapsed * (total_size - size_imported) / (size_imported) / 1000); - }; - const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size, - u64 total_imported_size, u64 /*total_size*/) { - UpdateETA(total_imported_size); - emit ProgressUpdated(current_imported_size, total_imported_size, eta); - }; - - Common::ProgressCallbackWrapper wrapper{total_size}; - for (const auto& content : contents) { - emit NextContent(count + 1, content, eta); - if (!execute_func(importer, content, wrapper.Wrap(Callback))) { - if (!cancelled) { - failed_contents.emplace_back(content); - } - } - count++; - - if (cancelled) { - break; - } - } - emit Completed(); -} - -void MultiJob::Cancel() { - cancelled.store(true); - abort_func(importer); -} - -std::vector MultiJob::GetFailedContents() const { - return failed_contents; -} +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "frontend/helpers/multi_job.h" + +MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, + std::vector contents_, ExecuteFunc execute_func_, + AbortFunc abort_func_) + : QThread(parent), importer(importer_), contents(std::move(contents_)), + execute_func(std::move(execute_func_)), abort_func(abort_func_) {} + +MultiJob::~MultiJob() = default; + +void MultiJob::run() { + u64 total_size = 0; + for (const auto& content : contents) { + total_size += content.maximum_size; + } + + std::size_t count = 0; + int eta = -1; + + const auto initial_time = std::chrono::steady_clock::now(); + const auto UpdateETA = [total_size, &eta, initial_time](u64 size_imported) { + if (size_imported < 10 * 1024 * 1024) { // 10M Threshold + return; + } + using namespace std::chrono; + const u64 time_elapsed = + duration_cast(steady_clock::now() - initial_time).count(); + eta = + static_cast(time_elapsed * (total_size - size_imported) / (size_imported) / 1000); + }; + const auto Callback = [this, &eta, &UpdateETA](u64 current_imported_size, + u64 total_imported_size, u64 /*total_size*/) { + UpdateETA(total_imported_size); + emit ProgressUpdated(current_imported_size, total_imported_size, eta); + }; + + Common::ProgressCallbackWrapper wrapper{total_size}; + for (const auto& content : contents) { + emit NextContent(count + 1, wrapper.current_done_size + wrapper.current_pending_size, + content, eta); + if (!execute_func(importer, content, wrapper.Wrap(Callback))) { + if (!cancelled) { + failed_contents.emplace_back(content); + } + } + count++; + + if (cancelled) { + break; + } + } + emit Completed(); +} + +void MultiJob::Cancel() { + cancelled.store(true); + abort_func(importer); +} + +std::vector MultiJob::GetFailedContents() const { + return failed_contents; +} diff --git a/src/frontend/helpers/multi_job.h b/src/frontend/helpers/multi_job.h index da4363f..1e602bc 100644 --- a/src/frontend/helpers/multi_job.h +++ b/src/frontend/helpers/multi_job.h @@ -1,55 +1,56 @@ -// Copyright 2019 threeSD Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "common/progress_callback.h" -#include "core/importer.h" - -class MultiJob : public QThread { - Q_OBJECT - -public: - using ExecuteFunc = std::function; - using AbortFunc = std::function; - - explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, - std::vector contents, ExecuteFunc execute_func, - AbortFunc abort_func); - ~MultiJob() override; - - void run() override; - void Cancel(); - - std::vector GetFailedContents() const; - -signals: - /** - * Called when progress is updated on the current content. - * @param current_imported_size Imported size of the current content. - * @param total_imported_size Total imported size taking all previous contents into - * consideration. - * @param eta ETA in seconds, 0 when not determined. - */ - void ProgressUpdated(u64 current_imported_size, u64 total_imported_size, int eta); - - /// Dumping of a content has been finished, go on to the next. Called at start as well. - void NextContent(std::size_t count, const Core::ContentSpecifier& next_content, int eta); - - void Completed(); - -private: - std::atomic_bool cancelled{false}; - Core::SDMCImporter& importer; - std::vector contents; - std::vector failed_contents; - ExecuteFunc execute_func; - AbortFunc abort_func; -}; - -Q_DECLARE_METATYPE(Core::ContentSpecifier) +// Copyright 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/progress_callback.h" +#include "core/importer.h" + +class MultiJob : public QThread { + Q_OBJECT + +public: + using ExecuteFunc = std::function; + using AbortFunc = std::function; + + explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, + std::vector contents, ExecuteFunc execute_func, + AbortFunc abort_func); + ~MultiJob() override; + + void run() override; + void Cancel(); + + std::vector GetFailedContents() const; + +signals: + /** + * Called when progress is updated on the current content. + * @param current_imported_size Imported size of the current content. + * @param total_imported_size Total imported size taking all previous contents into + * consideration. + * @param eta ETA in seconds, 0 when not determined. + */ + void ProgressUpdated(u64 current_imported_size, u64 total_imported_size, int eta); + + /// Dumping of a content has been finished, go on to the next. Called at start as well. + void NextContent(std::size_t count, u64 total_imported_size, + const Core::ContentSpecifier& next_content, int eta); + + void Completed(); + +private: + std::atomic_bool cancelled{false}; + Core::SDMCImporter& importer; + std::vector contents; + std::vector failed_contents; + ExecuteFunc execute_func; + AbortFunc abort_func; +}; + +Q_DECLARE_METATYPE(Core::ContentSpecifier) diff --git a/src/frontend/helpers/rate_limited_progress_dialog.cpp b/src/frontend/helpers/rate_limited_progress_dialog.cpp new file mode 100644 index 0000000..f3edc00 --- /dev/null +++ b/src/frontend/helpers/rate_limited_progress_dialog.cpp @@ -0,0 +1,34 @@ +// Copyright 2021 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "frontend/helpers/rate_limited_progress_dialog.h" + +RateLimitedProgressDialog::RateLimitedProgressDialog(const QString& label_text, + const QString& cancel_button_text, int minimum, + int maximum, QWidget* parent) + : QProgressDialog(label_text, cancel_button_text, minimum, maximum, parent) { + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + setWindowModality(Qt::WindowModal); + setMinimumDuration(0); + setValue(0); +} + +RateLimitedProgressDialog::~RateLimitedProgressDialog() = default; + +void RateLimitedProgressDialog::Update(int progress, const QString& label_text) { + if (progress == maximum()) { // always set the maximum + setValue(progress); + return; + } + + const auto current_time = std::chrono::steady_clock::now(); + if (current_time - last_update_time < MinimumInterval) { + return; + } + + setValue(progress); + setLabelText(label_text); + last_update_time = current_time; +} diff --git a/src/frontend/helpers/rate_limited_progress_dialog.h b/src/frontend/helpers/rate_limited_progress_dialog.h new file mode 100644 index 0000000..1120b78 --- /dev/null +++ b/src/frontend/helpers/rate_limited_progress_dialog.h @@ -0,0 +1,21 @@ +// Copyright 2021 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class RateLimitedProgressDialog : public QProgressDialog { +public: + explicit RateLimitedProgressDialog(const QString& label_text, const QString& cancel_button_text, + int minimum, int maximum, QWidget* parent = nullptr); + ~RateLimitedProgressDialog() override; + + void Update(int progress, const QString& label_text); + +private: + std::chrono::steady_clock::time_point last_update_time = std::chrono::steady_clock::now(); + static constexpr auto MinimumInterval = std::chrono::milliseconds{100}; +}; diff --git a/src/frontend/helpers/simple_job.cpp b/src/frontend/helpers/simple_job.cpp index e33b132..26b49e5 100644 --- a/src/frontend/helpers/simple_job.cpp +++ b/src/frontend/helpers/simple_job.cpp @@ -3,9 +3,8 @@ // Refer to the license.txt file included. #include -#include -#include #include "frontend/helpers/frontend_common.h" +#include "frontend/helpers/rate_limited_progress_dialog.h" #include "frontend/helpers/simple_job.h" SimpleJob::SimpleJob(QObject* parent, ExecuteFunc execute_, AbortFunc abort_) @@ -30,34 +29,25 @@ void SimpleJob::Cancel() { } void SimpleJob::StartWithProgressDialog(QWidget* widget) { - // We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's - // event handling. - auto* bar = new QProgressBar(widget); - bar->setRange(0, 100); - bar->setValue(0); - - auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget); - dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); - dialog->setWindowModality(Qt::WindowModal); - dialog->setBar(bar); - dialog->setMinimumDuration(0); - - connect(this, &SimpleJob::ProgressUpdated, this, [bar, dialog](u64 current, u64 total) { + auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, widget); + connect(this, &SimpleJob::ProgressUpdated, this, [dialog](u64 current, u64 total) { + if (dialog->wasCanceled()) { + return; + } // Try to map total to int range // This is equal to ceil(total / INT_MAX) const u64 multiplier = (total + std::numeric_limits::max() - 1) / std::numeric_limits::max(); - bar->setMaximum(static_cast(total / multiplier)); - bar->setValue(static_cast(current / multiplier)); - dialog->setLabelText( - tr("%1 / %2").arg(ReadableByteSize(current)).arg(ReadableByteSize(total))); + dialog->setMaximum(static_cast(total / multiplier)); + dialog->Update(static_cast(current / multiplier), + tr("%1 / %2").arg(ReadableByteSize(current), ReadableByteSize(total))); }); connect(this, &SimpleJob::ErrorOccured, this, [widget, dialog] { QMessageBox::critical(widget, tr("threeSD"), tr("Operation failed. Please refer to the log.")); dialog->hide(); }); - connect(this, &SimpleJob::Completed, this, [dialog] { dialog->setValue(dialog->maximum()); }); + connect(this, &SimpleJob::Completed, dialog, &QProgressDialog::hide); connect(dialog, &QProgressDialog::canceled, this, &SimpleJob::Cancel); start(); diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index 724c0b0..d2c6c3c 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -12,8 +12,6 @@ #include #include #include -#include -#include #include #include #include "common/assert.h" @@ -23,6 +21,7 @@ #include "frontend/cia_build_dialog.h" #include "frontend/helpers/frontend_common.h" #include "frontend/helpers/multi_job.h" +#include "frontend/helpers/rate_limited_progress_dialog.h" #include "frontend/helpers/simple_job.h" #include "frontend/import_dialog.h" #include "frontend/title_info_dialog.h" @@ -128,12 +127,9 @@ void ImportDialog::SetContentSizes(int previous_width, int previous_height) { } void ImportDialog::RelistContent() { - auto* dialog = new QProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); - dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); - dialog->setWindowModality(Qt::WindowModal); + auto* dialog = + new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); dialog->setCancelButton(nullptr); - dialog->setMinimumDuration(0); - dialog->setValue(0); using FutureWatcher = QFutureWatcher; auto* future_watcher = new FutureWatcher(this); @@ -601,48 +597,47 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total label->setWordWrap(true); label->setFixedWidth(600); - // We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's - // event handling. - auto* bar = new QProgressBar(this); - bar->setRange(0, static_cast(total_size / multiplier)); - bar->setValue(0); - - auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, this); - dialog->setWindowFlags(dialog->windowFlags() & (~Qt::WindowContextHelpButtonHint)); - dialog->setWindowModality(Qt::WindowModal); - dialog->setBar(bar); + auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, + static_cast(total_size / multiplier), this); dialog->setLabel(label); - dialog->setMinimumDuration(0); connect(job, &MultiJob::NextContent, this, - [this, dialog, total_count](std::size_t count, - const Core::ContentSpecifier& next_content, int eta) { - dialog->setLabelText( - tr("

(%1/%2) %3 (%4)

 

%5

") - .arg(count) - .arg(total_count) - .arg(GetContentName(next_content)) - .arg(GetContentTypeName(next_content.type)) - .arg(FormatETA(eta))); + [this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size, + const Core::ContentSpecifier& next_content, + int eta) { + if (dialog->wasCanceled()) { + return; + } + dialog->Update(static_cast(total_imported_size / multiplier), + tr("

(%1/%2) %3 (%4)

 

%5

") + .arg(count) + .arg(total_count) + .arg(GetContentName(next_content)) + .arg(GetContentTypeName(next_content.type)) + .arg(FormatETA(eta))); current_content = next_content; current_count = count; }); - connect(job, &MultiJob::ProgressUpdated, this, - [this, bar, dialog, multiplier, total_count](u64 current_imported_size, - u64 total_imported_size, int eta) { - bar->setValue(static_cast(total_imported_size / multiplier)); - dialog->setLabelText(tr("

(%1/%2) %3 (%4)

%5 " - "/ %6

%7

") - .arg(current_count) - .arg(total_count) - .arg(GetContentName(current_content)) - .arg(GetContentTypeName(current_content.type)) - .arg(ReadableByteSize(current_imported_size)) - .arg(ReadableByteSize(current_content.maximum_size)) - .arg(FormatETA(eta))); - }); + connect( + job, &MultiJob::ProgressUpdated, this, + [this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size, + int eta) { + if (dialog->wasCanceled()) { + return; + } + dialog->Update( + static_cast(total_imported_size / multiplier), + tr("

(%1/%2) %3 (%4)

%5 / %6

%7

") + .arg(current_count) + .arg(total_count) + .arg(GetContentName(current_content)) + .arg(GetContentTypeName(current_content.type)) + .arg(ReadableByteSize(current_imported_size)) + .arg(ReadableByteSize(current_content.maximum_size)) + .arg(FormatETA(eta))); + }); connect(job, &MultiJob::Completed, this, [this, dialog, job] { - dialog->setValue(dialog->maximum()); + dialog->hide(); const auto failed_contents = job->GetFailedContents(); if (failed_contents.empty()) { @@ -662,13 +657,9 @@ void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total }); connect(dialog, &QProgressDialog::canceled, this, [this, job] { // Add yet-another-ProgressDialog to indicate cancel progress - auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); - cancel_dialog->setWindowFlags(cancel_dialog->windowFlags() & - (~Qt::WindowContextHelpButtonHint)); - cancel_dialog->setWindowModality(Qt::WindowModal); + auto* cancel_dialog = + new RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); cancel_dialog->setCancelButton(nullptr); - cancel_dialog->setMinimumDuration(0); - cancel_dialog->setValue(0); connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); job->Cancel(); });