summaryrefslogtreecommitdiffstats
path: root/src/async
diff options
context:
space:
mode:
Diffstat (limited to 'src/async')
-rw-r--r--src/async/CMakeLists.txt13
-rw-r--r--src/async/async.cpp67
-rw-r--r--src/async/async.h37
-rw-r--r--src/async/background-progress.h41
-rw-r--r--src/async/channel.h184
-rw-r--r--src/async/progress-splitter.h73
-rw-r--r--src/async/progress.h155
7 files changed, 570 insertions, 0 deletions
diff --git a/src/async/CMakeLists.txt b/src/async/CMakeLists.txt
new file mode 100644
index 0000000..c6bafb6
--- /dev/null
+++ b/src/async/CMakeLists.txt
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set(async_SRC
+ async.cpp
+
+ async.h
+ channel.h
+ background-progress.h
+ progress.h
+ progress-splitter.h
+)
+
+add_inkscape_source("${async_SRC}")
diff --git a/src/async/async.cpp b/src/async/async.cpp
new file mode 100644
index 0000000..7bcebbb
--- /dev/null
+++ b/src/async/async.cpp
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <vector>
+#include <algorithm>
+#include <mutex>
+#include <chrono>
+#include "async.h"
+#include "util/statics.h"
+
+namespace {
+
+// Todo: Replace when C++ gets an .is_ready().
+bool is_ready(std::future<void> const &future)
+{
+ return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
+}
+
+// Holds on to asyncs and waits for them to finish at program exit.
+class AsyncBin
+{
+public:
+ static auto const &get()
+ {
+ /*
+ * Using Static<AsyncBin> to ensure destruction before main() exits, so that lifetimes
+ * of background threads are synchronized with the destruction of statics.
+ */
+ static Inkscape::Util::Static<AsyncBin const> instance;
+ return instance.get();
+ }
+
+ void add(std::future<void> &&future) const
+ {
+ auto g = std::lock_guard(mutables);
+ futures.erase(
+ std::remove_if(futures.begin(), futures.end(), [] (auto const &future) {
+ return is_ready(future);
+ }),
+ futures.end());
+ futures.emplace_back(std::move(future));
+ }
+
+ ~AsyncBin() { drain(); }
+
+private:
+ mutable std::mutex mutables;
+ mutable std::vector<std::future<void>> futures;
+
+ auto grab() const
+ {
+ auto g = std::lock_guard(mutables);
+ return std::move(futures);
+ }
+
+ void drain() const { while (!grab().empty()) {} }
+};
+
+} // namespace
+
+namespace Inkscape {
+namespace Async {
+namespace detail {
+
+void extend(std::future<void> &&future) { AsyncBin::get().add(std::move(future)); }
+
+} // namespace detail
+} // namespace Async
+} // namespace Inkscape
diff --git a/src/async/async.h b/src/async/async.h
new file mode 100644
index 0000000..f4e3003
--- /dev/null
+++ b/src/async/async.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file Async
+ * Fire-and-forget asyncs without UB at program exit.
+ *
+ * This file provides asyncs whose futures do not block on destruction, while
+ * ensuring program exit is delayed until all such asyncs have terminated, in
+ * order to ensure clean termination of asyncs and avoid undefined behaivour.
+ *
+ * Related: https://open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3451.pdf
+ */
+#ifndef INKSCAPE_ASYNC_H
+#define INKSCAPE_ASYNC_H
+
+#include <future>
+#include <utility>
+
+namespace Inkscape {
+namespace Async {
+namespace detail {
+
+void extend(std::future<void> &&future);
+
+} // namespace detail
+
+/**
+ * Launch an async which will delay program exit until its termination.
+ */
+template <typename F>
+inline void fire_and_forget(F &&f)
+{
+ detail::extend(std::async(std::launch::async, std::forward<F>(f)));
+}
+
+} // namespace Async
+} // namespace Inkscape
+
+#endif // INKSCAPE_ASYNC_H
diff --git a/src/async/background-progress.h b/src/async/background-progress.h
new file mode 100644
index 0000000..49fe3d7
--- /dev/null
+++ b/src/async/background-progress.h
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file Background-progress
+ * A Progress object that reports progress thread-safely over a Channel.
+ */
+#ifndef INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H
+#define INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H
+
+#include <functional>
+#include "channel.h"
+#include "progress.h"
+
+namespace Inkscape {
+namespace Async {
+
+template <typename... T>
+class BackgroundProgress final
+ : public Progress<T...>
+{
+public:
+ /**
+ * Construct a Progress object which becomes cancelled as soon as \a channel is closed,
+ * and reports progress by calling \a onprogress over \a channel.
+ *
+ * The result can only be used within the lifetime of \a channel.
+ */
+ BackgroundProgress(Channel::Source &channel, std::function<void(T...)> &onprogress)
+ : channel(&channel)
+ , onprogress(std::move(onprogress)) {}
+
+private:
+ Channel::Source *channel;
+ std::function<void(T...)> onprogress;
+
+ bool _keepgoing() const override { return channel->operator bool(); }
+ bool _report(T const &... progress) override { return channel->run(std::bind(onprogress, progress...)); }
+};
+
+} // namespace Async
+} // namespace Inkscape
+
+#endif // INKSCAPE_ASYNC_BACKGROUND_PROGRESS_H
diff --git a/src/async/channel.h b/src/async/channel.h
new file mode 100644
index 0000000..d58841e
--- /dev/null
+++ b/src/async/channel.h
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file Channel
+ * Thread-safe communication channel for asyncs.
+ */
+#ifndef INKSCAPE_ASYNC_CHANNEL_H
+#define INKSCAPE_ASYNC_CHANNEL_H
+
+#include <memory>
+#include <optional>
+#include <mutex>
+#include <utility>
+#include <glibmm/dispatcher.h>
+#include "util/funclog.h"
+
+namespace Inkscape {
+namespace Async {
+namespace Channel {
+namespace detail {
+
+class Shared final
+ : public std::enable_shared_from_this<Shared>
+{
+public:
+ Shared()
+ {
+ dispatcher->connect([this] {
+ auto ref = shared_from_this();
+ grab().exec_while([this] { return is_open; });
+ });
+ }
+
+ Shared(Shared const &) = delete;
+ Shared &operator=(Shared const &) = delete;
+
+ operator bool() const
+ {
+ auto g = std::lock_guard(mutables);
+ return is_open;
+ }
+
+ template <typename F>
+ bool run(F &&f) const
+ {
+ auto g = std::lock_guard(mutables);
+ if (!is_open) return false;
+ if (funclog.empty()) dispatcher->emit();
+ funclog.emplace(std::forward<F>(f));
+ return true;
+ }
+
+ void close()
+ {
+ disconnect_source();
+ dispatcher.reset();
+ funclog.clear();
+ }
+
+private:
+ mutable std::mutex mutables;
+ mutable std::optional<Glib::Dispatcher> dispatcher = std::make_optional<Glib::Dispatcher>();
+ mutable Util::FuncLog funclog;
+ bool is_open = true;
+
+ Util::FuncLog grab() const
+ {
+ auto g = std::lock_guard(mutables);
+ return std::move(funclog);
+ }
+
+ void disconnect_source()
+ {
+ auto g = std::lock_guard(mutables);
+ is_open = false;
+ }
+};
+
+struct Create;
+
+} // namespace detail
+
+class Source final
+{
+public:
+ Source() = default;
+ Source(Source const &) = delete;
+ Source &operator=(Source const &) = delete;
+ Source(Source &&) = default;
+ Source &operator=(Source &&) = default;
+
+ /**
+ * Check whether the channel is still open.
+ */
+ explicit operator bool() const { return shared && shared->operator bool(); }
+
+ /**
+ * Attempt to run a function on the main loop that the Channel was created in. This will
+ * either succeed and execute or destroy the function in the main loop's thread, or fail and
+ * leave it untouched.
+ *
+ * \return Whether the Channel is still open at the time of calling.
+ *
+ * Note that a return value of true doesn't indicate whether the function will actually run,
+ * because the Channel could be closed in the meantime. If it does run, it is guaranteed the
+ * Dest object still exists and \a close() has not been called on it.
+ */
+ template <typename F>
+ bool run(F &&f) const { return shared && shared->run(std::forward<F>(f)); }
+
+ /**
+ * Close the channel. No more functions submitted through run() will be run.
+ */
+ void close() { shared.reset(); }
+
+private:
+ std::shared_ptr<detail::Shared const> shared;
+ explicit Source(std::shared_ptr<detail::Shared> shared_) : shared(std::move(shared_)) {}
+ friend struct detail::Create;
+};
+
+class Dest final
+{
+public:
+ Dest() = default;
+ Dest(Dest const &) = delete;
+ Dest &operator=(Dest const &) = delete;
+ Dest(Dest &&) = default;
+ Dest &operator=(Dest &&) = default;
+ ~Dest() { close(); }
+
+ /**
+ * Close the channel. No further functions submitted by the other end will be run, and it will
+ * be notified of closure whenever it checks.
+ */
+ void close() { if (shared) { shared->close(); shared.reset(); } }
+
+ /**
+ * Check whether \a close() has already been called, or if the channel was never opened.
+ *
+ * Note: This does not check whether the corresponding \a close() method of Source has been
+ * called. In fact, this condition is meaningless without further synchronization. If you need
+ * to know whether the Source has closed, you can have it manually send this information
+ * over the Channel instead.
+ */
+ explicit operator bool() const { return (bool)shared; }
+
+private:
+ std::shared_ptr<detail::Shared> shared;
+ explicit Dest(std::shared_ptr<detail::Shared> shared_) : shared(std::move(shared_)) {}
+ friend struct detail::Create;
+};
+
+namespace detail {
+
+struct Create
+{
+ Create() = delete;
+
+ static auto create()
+ {
+ auto shared = std::make_shared<detail::Shared>();
+ auto src = Source(shared);
+ auto dst = Dest(std::move(shared));
+ return std::make_pair(std::move(src), std::move(dst));
+ }
+};
+
+} // namespace detail
+
+/**
+ * Create a linked Source - Destination pair forming a thread-safe communication channel.
+ *
+ * As long as the channel is still open, the Source can use it to run commands in the main loop of
+ * the creation thread and check if the channel is still open. Destructing either end closes the channel.
+ */
+inline std::pair<Source, Dest> create()
+{
+ return detail::Create::create();
+}
+
+} // namespace Channel
+} // namespace Async
+} // namespace Inkscape
+
+#endif // INKSCAPE_ASYNC_CHANNEL_H
diff --git a/src/async/progress-splitter.h b/src/async/progress-splitter.h
new file mode 100644
index 0000000..9666611
--- /dev/null
+++ b/src/async/progress-splitter.h
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file Progress-splitter
+ * Dynamically split a Progress into several sub-tasks.
+ */
+#ifndef INKSCAPE_ASYNC_PROGRESS_SPLITTER_H
+#define INKSCAPE_ASYNC_PROGRESS_SPLITTER_H
+
+#include <vector>
+#include <optional>
+#include "progress.h"
+
+namespace Inkscape {
+namespace Async {
+
+/**
+ * A RAII object for splitting a Progress into a dynamically-determined collection of sub-tasks.
+ */
+template <typename T, typename... S>
+class ProgressSplitter
+{
+public:
+ /// Construct a progress splitter for a given task.
+ ProgressSplitter(Progress<T, S...> &parent) : parent(&parent) {}
+
+ /// Add a SubProgress which makes progress \a amount.
+ ProgressSplitter &add(std::optional<SubProgress<T, S...>> &progress, T amount)
+ {
+ entries.push_back({ &progress, amount });
+ return *this;
+ }
+
+ /// Convenience method to enable a "fluent interface". Calls \a add if \a condition is true.
+ ProgressSplitter &add_if(std::optional<SubProgress<T, S...>> &progress, T amount, bool condition)
+ {
+ return condition ? add(progress, amount) : *this;
+ }
+
+ /// Assign to each added SubProgress its portion of the total progress.
+ ~ProgressSplitter() { apportion(); }
+
+private:
+ struct Entry
+ {
+ std::optional<SubProgress<T, S...>> *progress;
+ T amount;
+ };
+
+ Progress<T, S...> *parent;
+ std::vector<Entry> entries;
+
+ void apportion() noexcept
+ {
+ if (entries.empty()) {
+ return;
+ }
+
+ T total = 0;
+ for (auto const &e : entries) {
+ total += e.amount;
+ }
+
+ T from = 0;
+ for (auto &e : entries) {
+ e.progress->emplace(*parent, from / total, e.amount / total);
+ from += e.amount;
+ }
+ }
+};
+
+} // namespace Async
+} // namespace Inkscape
+
+#endif // INKSCAPE_ASYNC_PROGRESS_SPLITTER_H
diff --git a/src/async/progress.h b/src/async/progress.h
new file mode 100644
index 0000000..120f03b
--- /dev/null
+++ b/src/async/progress.h
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file Progress
+ * Interface for reporting progress and checking cancellation.
+ */
+#ifndef INKSCAPE_ASYNC_PROGRESS_H
+#define INKSCAPE_ASYNC_PROGRESS_H
+
+#include <chrono>
+
+namespace Inkscape {
+namespace Async {
+
+class CancelledException {};
+
+/**
+ * An interface for tasks to report progress and check for cancellation.
+ * Not supported:
+ * - Error reporting - use exceptions!
+ * - Thread-safety - overrides should provide this if needed using e.g. BackgroundProgress.
+ */
+template <typename... T>
+class Progress
+{
+public:
+ /// Report a progress value, returning false if cancelled.
+ bool report(T const &... progress) { return _report(progress...); }
+
+ /// Report a progress value, throwing CancelledException if cancelled.
+ void report_or_throw(T const &... progress) { if (!_report(progress...)) throw CancelledException(); }
+
+ /// Return whether not cancelled.
+ bool keepgoing() const { return _keepgoing(); }
+
+ /// Throw CancelledException if cancelled.
+ void throw_if_cancelled() const { if (!_keepgoing()) throw CancelledException(); }
+
+ /// Convenience function - same as check().
+ operator bool() const { return _keepgoing(); }
+
+protected:
+ ~Progress() = default;
+ virtual bool _keepgoing() const = 0;
+ virtual bool _report(T const &... progress) = 0;
+};
+
+/**
+ * A Progress object representing a sub-task of another Progress.
+ */
+template <typename T, typename... S>
+class SubProgress final
+ : public Progress<T, S...>
+{
+public:
+ /// Construct a progress object for a sub-task.
+ SubProgress(Progress<T, S...> &parent, T from, T amount)
+ {
+ if (auto p = dynamic_cast<SubProgress*>(&parent)) {
+ _root = p->_root;
+ _from = p->_from + p->_amount * from;
+ _amount = p->_amount * amount;
+ } else {
+ _root = &parent;
+ _from = from;
+ _amount = amount;
+ }
+ }
+
+private:
+ Progress<T, S...> *_root;
+ T _from, _amount;
+
+ bool _keepgoing() const override { return _root->keepgoing(); }
+ bool _report(T const &progress, S const &... aux) override { return _root->report(_from + _amount * progress, aux...); }
+};
+
+/**
+ * A Progress object that throttles reports to a given step size.
+ */
+template <typename T, typename... S>
+class ProgressStepThrottler final
+ : public Progress<T, S...>
+{
+public:
+ ProgressStepThrottler(Progress<T, S...> &parent, T step)
+ : parent(&parent), step(step) {}
+
+private:
+ Progress<T, S...> *parent;
+ T step;
+ T last = 0;
+
+ bool _keepgoing() const override { return parent->keepgoing(); }
+
+ bool _report(T const &progress, S const &... aux) override
+ {
+ if (progress - last < step) {
+ return parent->keepgoing();
+ } else {
+ last = progress;
+ return parent->report(progress, aux...);
+ }
+ }
+};
+
+/**
+ * A Progress object that throttles reports to a given time interval.
+ */
+template <typename... T>
+class ProgressTimeThrottler final
+ : public Progress<T...>
+{
+ using clock = std::chrono::steady_clock;
+ using time_point = clock::time_point;
+
+public:
+ using duration = clock::duration;
+
+ ProgressTimeThrottler(Progress<T...> &parent, duration interval)
+ : parent(&parent), interval(interval) {}
+
+private:
+ Progress<T...> *parent;
+ duration interval;
+ time_point last = clock::now();
+
+ bool _keepgoing() const override { return parent->keepgoing(); }
+
+ bool _report(T const &... progress) override
+ {
+ auto now = clock::now();
+ if (now - last < interval) {
+ return parent->keepgoing();
+ } else {
+ last = now;
+ return parent->report(progress...);
+ }
+ }
+};
+
+/**
+ * A dummy Progress object that never reports cancellation.
+ */
+template <typename... T>
+class ProgressAlways final
+ : public Progress<T...>
+{
+private:
+ bool _keepgoing() const override { return true; }
+ bool _report(T const &...) override { return true; }
+};
+
+} // namespace Async
+} // namespace Inkscape
+
+#endif // INKSCAPE_ASYNC_PROGRESS_H