summaryrefslogtreecommitdiffstats
path: root/xpcom/threads/StateWatching.h
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /xpcom/threads/StateWatching.h
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xpcom/threads/StateWatching.h')
-rw-r--r--xpcom/threads/StateWatching.h302
1 files changed, 302 insertions, 0 deletions
diff --git a/xpcom/threads/StateWatching.h b/xpcom/threads/StateWatching.h
new file mode 100644
index 0000000000..3da0c63bfe
--- /dev/null
+++ b/xpcom/threads/StateWatching.h
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(StateWatching_h_)
+# define StateWatching_h_
+
+# include <cstddef>
+# include <new>
+# include <utility>
+# include "mozilla/AbstractThread.h"
+# include "mozilla/Assertions.h"
+# include "mozilla/Logging.h"
+# include "mozilla/RefPtr.h"
+# include "nsISupports.h"
+# include "nsTArray.h"
+# include "nsThreadUtils.h"
+
+/*
+ * The state-watching machinery automates the process of responding to changes
+ * in various pieces of state.
+ *
+ * A standard programming pattern is as follows:
+ *
+ * mFoo = ...;
+ * NotifyStuffChanged();
+ * ...
+ * mBar = ...;
+ * NotifyStuffChanged();
+ *
+ * This pattern is error-prone and difficult to audit because it requires the
+ * programmer to manually trigger the update routine. This can be especially
+ * problematic when the update routine depends on numerous pieces of state, and
+ * when that state is modified across a variety of helper methods. In these
+ * cases the responsibility for invoking the routine is often unclear, causing
+ * developers to scatter calls to it like pixie dust. This can result in
+ * duplicate invocations (which is wasteful) and missing invocations in corner-
+ * cases (which is a source of bugs).
+ *
+ * This file provides a set of primitives that automatically handle updates and
+ * allow the programmers to explicitly construct a graph of state dependencies.
+ * When used correctly, it eliminates the guess-work and wasted cycles described
+ * above.
+ *
+ * There are two basic pieces:
+ * (1) Objects that can be watched for updates. These inherit WatchTarget.
+ * (2) Objects that receive objects and trigger processing. These inherit
+ * AbstractWatcher. In the current machinery, these exist only internally
+ * within the WatchManager, though that could change.
+ *
+ * Note that none of this machinery is thread-safe - it must all happen on the
+ * same owning thread. To solve multi-threaded use-cases, use state mirroring
+ * and watch the mirrored value.
+ *
+ * Given that semantics may change and comments tend to go out of date, we
+ * deliberately don't provide usage examples here. Grep around to find them.
+ */
+
+namespace mozilla {
+
+extern LazyLogModule gStateWatchingLog;
+
+# define WATCH_LOG(x, ...) \
+ MOZ_LOG(gStateWatchingLog, LogLevel::Debug, (x, ##__VA_ARGS__))
+
+/*
+ * AbstractWatcher is a superclass from which all watchers must inherit.
+ */
+class AbstractWatcher {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AbstractWatcher)
+ AbstractWatcher() : mDestroyed(false) {}
+ bool IsDestroyed() { return mDestroyed; }
+ virtual void Notify() = 0;
+
+ protected:
+ virtual ~AbstractWatcher() { MOZ_ASSERT(mDestroyed); }
+ bool mDestroyed;
+};
+
+/*
+ * WatchTarget is a superclass from which all watchable things must inherit.
+ * Unlike AbstractWatcher, it is a fully-implemented Mix-in, and the subclass
+ * needs only to invoke NotifyWatchers when something changes.
+ *
+ * The functionality that this class provides is not threadsafe, and should only
+ * be used on the thread that owns that WatchTarget.
+ */
+class WatchTarget {
+ public:
+ explicit WatchTarget(const char* aName) : mName(aName) {}
+
+ void AddWatcher(AbstractWatcher* aWatcher) {
+ MOZ_ASSERT(!mWatchers.Contains(aWatcher));
+ mWatchers.AppendElement(aWatcher);
+ }
+
+ void RemoveWatcher(AbstractWatcher* aWatcher) {
+ MOZ_ASSERT(mWatchers.Contains(aWatcher));
+ mWatchers.RemoveElement(aWatcher);
+ }
+
+ protected:
+ void NotifyWatchers() {
+ WATCH_LOG("%s[%p] notifying watchers\n", mName, this);
+ PruneWatchers();
+ for (size_t i = 0; i < mWatchers.Length(); ++i) {
+ mWatchers[i]->Notify();
+ }
+ }
+
+ private:
+ // We don't have Watchers explicitly unregister themselves when they die,
+ // because then they'd need back-references to all the WatchTargets they're
+ // subscribed to, and WatchTargets aren't reference-counted. So instead we
+ // just prune dead ones at appropriate times, which works just fine.
+ void PruneWatchers() {
+ mWatchers.RemoveElementsBy(
+ [](const auto& watcher) { return watcher->IsDestroyed(); });
+ }
+
+ nsTArray<RefPtr<AbstractWatcher>> mWatchers;
+
+ protected:
+ const char* mName;
+};
+
+/*
+ * Watchable is a wrapper class that turns any primitive into a WatchTarget.
+ */
+template <typename T>
+class Watchable : public WatchTarget {
+ public:
+ Watchable(const T& aInitialValue, const char* aName)
+ : WatchTarget(aName), mValue(aInitialValue) {}
+
+ const T& Ref() const { return mValue; }
+ operator const T&() const { return Ref(); }
+ Watchable& operator=(const T& aNewValue) {
+ if (aNewValue != mValue) {
+ mValue = aNewValue;
+ NotifyWatchers();
+ }
+
+ return *this;
+ }
+
+ private:
+ Watchable(const Watchable& aOther) = delete;
+ Watchable& operator=(const Watchable& aOther) = delete;
+
+ T mValue;
+};
+
+// Manager class for state-watching. Declare one of these in any class for which
+// you want to invoke method callbacks.
+//
+// Internally, WatchManager maintains one AbstractWatcher per callback method.
+// Consumers invoke Watch/Unwatch on a particular (WatchTarget, Callback) tuple.
+// This causes an AbstractWatcher for |Callback| to be instantiated if it
+// doesn't already exist, and registers it with |WatchTarget|.
+//
+// Using Direct Tasks on the TailDispatcher, WatchManager ensures that we fire
+// watch callbacks no more than once per task, once all other operations for
+// that task have been completed.
+//
+// WatchManager<OwnerType> is intended to be declared as a member of |OwnerType|
+// objects. Given that, it and its owned objects can't hold permanent strong
+// refs to the owner, since that would keep the owner alive indefinitely.
+// Instead, it _only_ holds strong refs while waiting for Direct Tasks to fire.
+// This ensures that everything is kept alive just long enough.
+template <typename OwnerType>
+class WatchManager {
+ public:
+ typedef void (OwnerType::*CallbackMethod)();
+ explicit WatchManager(OwnerType* aOwner, AbstractThread* aOwnerThread)
+ : mOwner(aOwner), mOwnerThread(aOwnerThread) {}
+
+ ~WatchManager() {
+ if (!IsShutdown()) {
+ Shutdown();
+ }
+ }
+
+ bool IsShutdown() const { return !mOwner; }
+
+ // Shutdown needs to happen on mOwnerThread. If the WatchManager will be
+ // destroyed on a different thread, Shutdown() must be called manually.
+ void Shutdown() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ for (auto& watcher : mWatchers) {
+ watcher->Destroy();
+ }
+ mWatchers.Clear();
+ mOwner = nullptr;
+ }
+
+ void Watch(WatchTarget& aTarget, CallbackMethod aMethod) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ aTarget.AddWatcher(&EnsureWatcher(aMethod));
+ }
+
+ void Unwatch(WatchTarget& aTarget, CallbackMethod aMethod) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ PerCallbackWatcher* watcher = GetWatcher(aMethod);
+ MOZ_ASSERT(watcher);
+ aTarget.RemoveWatcher(watcher);
+ }
+
+ void ManualNotify(CallbackMethod aMethod) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ PerCallbackWatcher* watcher = GetWatcher(aMethod);
+ MOZ_ASSERT(watcher);
+ watcher->Notify();
+ }
+
+ private:
+ class PerCallbackWatcher : public AbstractWatcher {
+ public:
+ PerCallbackWatcher(OwnerType* aOwner, AbstractThread* aOwnerThread,
+ CallbackMethod aMethod)
+ : mOwner(aOwner),
+ mOwnerThread(aOwnerThread),
+ mCallbackMethod(aMethod) {}
+
+ void Destroy() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ mDestroyed = true;
+ mOwner = nullptr;
+ }
+
+ void Notify() override {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ MOZ_DIAGNOSTIC_ASSERT(mOwner,
+ "mOwner is only null after destruction, "
+ "at which point we shouldn't be notified");
+ if (mNotificationPending) {
+ // We've already got a notification job in the pipe.
+ return;
+ }
+ mNotificationPending = true;
+
+ // Queue up our notification jobs to run in a stable state.
+ AbstractThread::DispatchDirectTask(
+ NS_NewRunnableFunction("WatchManager::PerCallbackWatcher::Notify",
+ [self = RefPtr<PerCallbackWatcher>(this),
+ owner = RefPtr<OwnerType>(mOwner)]() {
+ if (!self->mDestroyed) {
+ ((*owner).*(self->mCallbackMethod))();
+ }
+ self->mNotificationPending = false;
+ }));
+ }
+
+ bool CallbackMethodIs(CallbackMethod aMethod) const {
+ return mCallbackMethod == aMethod;
+ }
+
+ private:
+ ~PerCallbackWatcher() = default;
+
+ OwnerType* mOwner; // Never null.
+ bool mNotificationPending = false;
+ RefPtr<AbstractThread> mOwnerThread;
+ CallbackMethod mCallbackMethod;
+ };
+
+ PerCallbackWatcher* GetWatcher(CallbackMethod aMethod) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ for (auto& watcher : mWatchers) {
+ if (watcher->CallbackMethodIs(aMethod)) {
+ return watcher;
+ }
+ }
+ return nullptr;
+ }
+
+ PerCallbackWatcher& EnsureWatcher(CallbackMethod aMethod) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ PerCallbackWatcher* watcher = GetWatcher(aMethod);
+ if (watcher) {
+ return *watcher;
+ }
+ watcher = mWatchers
+ .AppendElement(MakeAndAddRef<PerCallbackWatcher>(
+ mOwner, mOwnerThread, aMethod))
+ ->get();
+ return *watcher;
+ }
+
+ nsTArray<RefPtr<PerCallbackWatcher>> mWatchers;
+ OwnerType* mOwner;
+ RefPtr<AbstractThread> mOwnerThread;
+};
+
+# undef WATCH_LOG
+
+} // namespace mozilla
+
+#endif