/* -*- 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/. */ #ifndef ProgressLogger_h #define ProgressLogger_h #include "mozilla/Assertions.h" #include "mozilla/ProportionValue.h" #include "mozilla/RefCounted.h" #include "mozilla/RefPtr.h" #include // Uncomment to printf ProcessLogger updates. // #define DEBUG_PROCESSLOGGER #ifdef DEBUG_PROCESSLOGGER # include "mozilla/BaseProfilerUtils.h" # include #endif // DEBUG_PROCESSLOGGER namespace mozilla { // A `ProgressLogger` is used to update a referenced atomic `ProportionValue`, // and can recursively create a sub-logger corresponding to a subset of their // own range, but that sub-logger's updates are done in its local 0%-100% range. // The typical usage is for multi-level tasks, where each level can estimate its // own work and the work delegated to a next-level function, without knowing how // this local work relates to the higher-level total work. See // `CreateSubLoggerFromTo` for details. // Note that this implementation is single-threaded, it does not support logging // progress from multiple threads at the same time. class ProgressLogger { public: // An RefPtr'd object of this class is used as the target of all // ProgressLogger updates, and it may be shared to make these updates visible // from other code in any thread. class SharedProgress : public external::AtomicRefCounted { public: MOZ_DECLARE_REFCOUNTED_TYPENAME(SharedProgress) SharedProgress() = default; SharedProgress(const SharedProgress&) = delete; SharedProgress& operator=(const SharedProgress&) = delete; // This constant is used to indicate that an update may change the progress // value, but should not modify the previously-recorded location. static constexpr const char* NO_LOCATION_UPDATE = nullptr; // Set the current progress and location, but the previous location is not // overwritten if the new one is null or empty. // The location and then the progress are atomically "released", so that all // preceding writes on this thread will be visible to other threads reading // these values; most importantly when reaching 100% progress, the reader // can be confident that the location is final and the operation being // watched has completed. void SetProgress( ProportionValue aProgress, const char* aLocationOrNullEmptyToIgnore = NO_LOCATION_UPDATE) { if (aLocationOrNullEmptyToIgnore && *aLocationOrNullEmptyToIgnore != '\0') { mLastLocation.store(aLocationOrNullEmptyToIgnore, std::memory_order_release); } mProgress.store(aProgress, std::memory_order_release); } // Read the current progress value. Atomically "acquired", so that writes // from the thread that stored this value are all visible to the reader // here; most importantly when reaching 100%, we can be confident that the // location is final and the operation being watched has completed. [[nodiscard]] ProportionValue Progress() const { return mProgress.load(std::memory_order_acquire); } // Read the current progress value. Atomically "acquired". [[nodiscard]] const char* LastLocation() const { return mLastLocation.load(std::memory_order_acquire); } private: friend mozilla::detail::RefCounted; ~SharedProgress() = default; // Progress and last-known location. // Beware that these two values are not strongly tied: Reading one then the // other may give mismatched information; but it should be fine for // informational usage. // They are stored using atomic acquire-release ordering, to guarantee that // when read, all writes preceding these values are visible. std::atomic mProgress = ProportionValue{0.0}; std::atomic mLastLocation = nullptr; }; static constexpr const char* NO_LOCATION_UPDATE = SharedProgress::NO_LOCATION_UPDATE; ProgressLogger() = default; // Construct a top-level logger, starting at 0% and expected to end at 100%. explicit ProgressLogger( RefPtr aGlobalProgressOrNull, const char* aLocationOrNullEmptyToIgnoreAtStart = NO_LOCATION_UPDATE, const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) : ProgressLogger{std::move(aGlobalProgressOrNull), /* Start */ ProportionValue{0.0}, /* Multiplier */ ProportionValue{1.0}, aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd} {} // Don't make copies, it would be confusing! // TODO: Copies could one day be allowed to track multi-threaded work, but it // is outside the scope of this implementation; Please update if needed. ProgressLogger(const ProgressLogger&) = delete; ProgressLogger& operator&(const ProgressLogger&) = delete; // Move-construct is allowed, to return from CreateSubLoggerFromTo, and // forward straight into a function. Note that moved-from ProgressLoggers must // not be used anymore! Use `CreateSubLoggerFromTo` to pass a sub-logger to // functions. ProgressLogger(ProgressLogger&& aOther) : mGlobalProgressOrNull(std::move(aOther.mGlobalProgressOrNull)), mLocalStartInGlobalSpace(aOther.mLocalStartInGlobalSpace), mLocalToGlobalMultiplier(aOther.mLocalToGlobalMultiplier), mLocationAtDestruction(aOther.mLocationAtDestruction) { aOther.MarkMovedFrom(); #ifdef DEBUG_PROCESSLOGGER if (mGlobalProgressOrNull) { printf("[%d] Moved (staying globally at %.2f in [%.2f, %.2f])\n", int(baseprofiler::profiler_current_process_id().ToNumber()), GetGlobalProgress().ToDouble() * 100.0, mLocalStartInGlobalSpace.ToDouble() * 100.0, (mLocalStartInGlobalSpace + mLocalToGlobalMultiplier).ToDouble() * 100.0); } #endif // DEBUG_PROCESSLOGGER } // Move-assign. This may be useful when starting with a default (empty) logger // and later assigning it a progress value to start updating. ProgressLogger& operator=(ProgressLogger&& aOther) { mGlobalProgressOrNull = std::move(aOther.mGlobalProgressOrNull); mLocalStartInGlobalSpace = aOther.mLocalStartInGlobalSpace; mLocalToGlobalMultiplier = aOther.mLocalToGlobalMultiplier; mLocationAtDestruction = aOther.mLocationAtDestruction; aOther.MarkMovedFrom(); #ifdef DEBUG_PROCESSLOGGER if (mGlobalProgressOrNull) { printf("[%d] Re-assigned (globally at %.2f in [%.2f, %.2f])\n", int(baseprofiler::profiler_current_process_id().ToNumber()), GetGlobalProgress().ToDouble() * 100.0, mLocalStartInGlobalSpace.ToDouble() * 100.0, (mLocalStartInGlobalSpace + mLocalToGlobalMultiplier).ToDouble() * 100.0); } #endif // DEBUG_PROCESSLOGGER return *this; } // Destruction sets the local update value to 100% unless empty or moved-from. ~ProgressLogger() { if (!IsMovedFrom()) { #ifdef DEBUG_PROCESSLOGGER if (mGlobalProgressOrNull) { printf("[%d] Destruction:\n", int(baseprofiler::profiler_current_process_id().ToNumber())); } #endif // DEBUG_PROCESSLOGGER SetLocalProgress(ProportionValue{1.0}, mLocationAtDestruction); } } // Retrieve the current progress in the global space. May be invalid. [[nodiscard]] ProportionValue GetGlobalProgress() const { return mGlobalProgressOrNull ? mGlobalProgressOrNull->Progress() : ProportionValue::MakeInvalid(); } // Retrieve the last known global location. May be null. [[nodiscard]] const char* GetLastGlobalLocation() const { return mGlobalProgressOrNull ? mGlobalProgressOrNull->LastLocation() : nullptr; } // Set the current progress in the local space. void SetLocalProgress(ProportionValue aLocalProgress, const char* aLocationOrNullEmptyToIgnore) { MOZ_ASSERT(!IsMovedFrom()); if (mGlobalProgressOrNull && !mLocalToGlobalMultiplier.IsExactlyZero()) { mGlobalProgressOrNull->SetProgress(LocalToGlobal(aLocalProgress), aLocationOrNullEmptyToIgnore); #ifdef DEBUG_PROCESSLOGGER printf("[%d] - local %.0f%% ~ global %.2f%% \"%s\"\n", int(baseprofiler::profiler_current_process_id().ToNumber()), aLocalProgress.ToDouble() * 100.0, LocalToGlobal(aLocalProgress).ToDouble() * 100.0, aLocationOrNullEmptyToIgnore ? aLocationOrNullEmptyToIgnore : ""); #endif // DEBUG_PROCESSLOGGER } } // Create a sub-logger that will record progress in the given local range. // E.g.: `f(pl.CreateSubLoggerFromTo(0.2, "f...", 0.4, "f done"));` expects // that `f` will produce work in the local range 0.2 (when starting) to 0.4 // (when returning); `f` itself will update this provided logger from 0.0 // to 1.0 (local to that `f` function), which will effectively be converted to // 0.2-0.4 (local to the calling function). // This can cascade multiple levels, each deeper level affecting a smaller and // smaller range in the global output. [[nodiscard]] ProgressLogger CreateSubLoggerFromTo( ProportionValue aSubStartInLocalSpace, const char* aLocationOrNullEmptyToIgnoreAtStart, ProportionValue aSubEndInLocalSpace, const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) { MOZ_ASSERT(!IsMovedFrom()); if (!mGlobalProgressOrNull) { return ProgressLogger{}; } const ProportionValue subStartInGlobalSpace = LocalToGlobal(aSubStartInLocalSpace); const ProportionValue subEndInGlobalSpace = LocalToGlobal(aSubEndInLocalSpace); if (subStartInGlobalSpace.IsInvalid() || subEndInGlobalSpace.IsInvalid()) { return ProgressLogger{mGlobalProgressOrNull, /* Start */ ProportionValue::MakeInvalid(), /* Multiplier */ ProportionValue{0.0}, aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd}; } #ifdef DEBUG_PROCESSLOGGER if (mGlobalProgressOrNull) { printf("[%d] * Sub: local [%.0f%%, %.0f%%] ~ global [%.2f%%, %.2f%%]\n", int(baseprofiler::profiler_current_process_id().ToNumber()), aSubStartInLocalSpace.ToDouble() * 100.0, aSubEndInLocalSpace.ToDouble() * 100.0, subStartInGlobalSpace.ToDouble() * 100.0, subEndInGlobalSpace.ToDouble() * 100.0); } #endif // DEBUG_PROCESSLOGGER return ProgressLogger{ mGlobalProgressOrNull, /* Start */ subStartInGlobalSpace, /* Multipler */ subEndInGlobalSpace - subStartInGlobalSpace, aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd}; } // Helper with no start location. [[nodiscard]] ProgressLogger CreateSubLoggerFromTo( ProportionValue aSubStartInLocalSpace, ProportionValue aSubEndInLocalSpace, const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) { return CreateSubLoggerFromTo(aSubStartInLocalSpace, NO_LOCATION_UPDATE, aSubEndInLocalSpace, aLocationOrNullEmptyToIgnoreAtEnd); } // Helper using the current progress as start. [[nodiscard]] ProgressLogger CreateSubLoggerTo( const char* aLocationOrNullEmptyToIgnoreAtStart, ProportionValue aSubEndInLocalSpace, const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) { MOZ_ASSERT(!IsMovedFrom()); if (!mGlobalProgressOrNull) { return ProgressLogger{}; } const ProportionValue subStartInGlobalSpace = GetGlobalProgress(); const ProportionValue subEndInGlobalSpace = LocalToGlobal(aSubEndInLocalSpace); if (subStartInGlobalSpace.IsInvalid() || subEndInGlobalSpace.IsInvalid()) { return ProgressLogger{mGlobalProgressOrNull, /* Start */ ProportionValue::MakeInvalid(), /* Multiplier */ ProportionValue{0.0}, aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd}; } #ifdef DEBUG_PROCESSLOGGER if (mGlobalProgressOrNull) { printf("[%d] * Sub: local [(here), %.0f%%] ~ global [%.2f%%, %.2f%%]\n", int(baseprofiler::profiler_current_process_id().ToNumber()), aSubEndInLocalSpace.ToDouble() * 100.0, subStartInGlobalSpace.ToDouble() * 100.0, subEndInGlobalSpace.ToDouble() * 100.0); } #endif // DEBUG_PROCESSLOGGER return ProgressLogger{ mGlobalProgressOrNull, /* Start */ subStartInGlobalSpace, /* Multiplier */ subEndInGlobalSpace - subStartInGlobalSpace, aLocationOrNullEmptyToIgnoreAtStart, aLocationOrNullEmptyToIgnoreAtEnd}; } // Helper using the current progress as start, no start location. [[nodiscard]] ProgressLogger CreateSubLoggerTo( ProportionValue aSubEndInLocalSpace, const char* aLocationOrNullEmptyToIgnoreAtEnd = NO_LOCATION_UPDATE) { return CreateSubLoggerTo(NO_LOCATION_UPDATE, aSubEndInLocalSpace, aLocationOrNullEmptyToIgnoreAtEnd); } class IndexAndProgressLoggerRange; [[nodiscard]] inline IndexAndProgressLoggerRange CreateLoopSubLoggersFromTo( ProportionValue aLoopStartInLocalSpace, ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount, const char* aLocationOrNullEmptyToIgnoreAtEdges = ProgressLogger::NO_LOCATION_UPDATE); [[nodiscard]] inline IndexAndProgressLoggerRange CreateLoopSubLoggersTo( ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount, const char* aLocationOrNullEmptyToIgnoreAtEdges = ProgressLogger::NO_LOCATION_UPDATE); private: // All constructions start at the local 0%. ProgressLogger(RefPtr aGlobalProgressOrNull, ProportionValue aLocalStartInGlobalSpace, ProportionValue aLocalToGlobalMultiplier, const char* aLocationOrNullEmptyToIgnoreAtConstruction, const char* aLocationOrNullEmptyToIgnoreAtDestruction) : mGlobalProgressOrNull(std::move(aGlobalProgressOrNull)), mLocalStartInGlobalSpace(aLocalStartInGlobalSpace), mLocalToGlobalMultiplier(aLocalToGlobalMultiplier), mLocationAtDestruction(aLocationOrNullEmptyToIgnoreAtDestruction) { MOZ_ASSERT(!IsMovedFrom(), "Don't construct a moved-from object!"); SetLocalProgress(ProportionValue{0.0}, aLocationOrNullEmptyToIgnoreAtConstruction); } void MarkMovedFrom() { mLocalToGlobalMultiplier = ProportionValue::MakeInvalid(); } [[nodiscard]] bool IsMovedFrom() const { return mLocalToGlobalMultiplier.IsInvalid(); } [[nodiscard]] ProportionValue LocalToGlobal( ProportionValue aLocalProgress) const { return aLocalProgress * mLocalToGlobalMultiplier + mLocalStartInGlobalSpace; } // Global progress value to update from local changes. RefPtr mGlobalProgressOrNull; // How much to multiply and add to a local [0, 100%] value, to get the // corresponding value in the global space. // If mLocalToGlobalMultiplier is invalid, this ProgressLogger is moved-from, // functions should not be used, and destructor won't update progress. ProportionValue mLocalStartInGlobalSpace; ProportionValue mLocalToGlobalMultiplier; const char* mLocationAtDestruction = nullptr; }; // Helper class for range-for loop, e.g., with `aProgressLogger`: // for (auto [index, loopProgressLogger] : // IndexAndProgressLoggerRange{aProgressLogger, 30_pc, 50_pc, 10, // "looping..."}) { // // This will loop 10 times. // // `index` is the loop index, from 0 to 9. // // The overall loop will start at 30% and end at 50% of aProgressLogger. // // `loopProgressLogger` is the progress logger for each iteration, // // covering 1/10th of the range, therefore: [30%,32%], then [32%,34%], // // etc. until [48%,50%]. // // Progress is automatically updated before/after each loop. // } // Note that this implementation is single-threaded, it does not support logging // progress from parallel loops. class ProgressLogger::IndexAndProgressLoggerRange { public: struct IndexAndProgressLogger { uint32_t index; ProgressLogger progressLogger; }; class IndexAndProgressLoggerEndIterator { public: explicit IndexAndProgressLoggerEndIterator(uint32_t aIndex) : mIndex(aIndex) {} [[nodiscard]] uint32_t Index() const { return mIndex; } private: uint32_t mIndex; }; class IndexAndProgressLoggerIterator { public: IndexAndProgressLoggerIterator( RefPtr aGlobalProgressOrNull, ProportionValue aLoopStartInGlobalSpace, ProportionValue aLoopIncrementInGlobalSpace, const char* aLocationOrNullEmptyToIgnoreAtEdges) : mGlobalProgressOrNull(aGlobalProgressOrNull), mLoopStartInGlobalSpace(aLoopStartInGlobalSpace), mLoopIncrementInGlobalSpace(aLoopIncrementInGlobalSpace), mIndex(0u), mLocationOrNullEmptyToIgnoreAtEdges( aLocationOrNullEmptyToIgnoreAtEdges) { if (mGlobalProgressOrNull) { mGlobalProgressOrNull->SetProgress(mLoopStartInGlobalSpace, mLocationOrNullEmptyToIgnoreAtEdges); } } [[nodiscard]] IndexAndProgressLogger operator*() { return IndexAndProgressLogger{ mIndex, mGlobalProgressOrNull ? ProgressLogger{mGlobalProgressOrNull, mLoopStartInGlobalSpace, mLoopIncrementInGlobalSpace, ProgressLogger::NO_LOCATION_UPDATE, ProgressLogger::NO_LOCATION_UPDATE} : ProgressLogger{}}; } [[nodiscard]] bool operator!=( const IndexAndProgressLoggerEndIterator& aEnd) const { return mIndex != aEnd.Index(); } IndexAndProgressLoggerIterator& operator++() { ++mIndex; mLoopStartInGlobalSpace = mLoopStartInGlobalSpace + mLoopIncrementInGlobalSpace; if (mGlobalProgressOrNull) { mGlobalProgressOrNull->SetProgress(mLoopStartInGlobalSpace, mLocationOrNullEmptyToIgnoreAtEdges); } return *this; } private: RefPtr mGlobalProgressOrNull; ProportionValue mLoopStartInGlobalSpace; ProportionValue mLoopIncrementInGlobalSpace; uint32_t mIndex; const char* mLocationOrNullEmptyToIgnoreAtEdges; }; [[nodiscard]] IndexAndProgressLoggerIterator begin() { return IndexAndProgressLoggerIterator{ mGlobalProgressOrNull, mLoopStartInGlobalSpace, mLoopIncrementInGlobalSpace, mLocationOrNullEmptyToIgnoreAtEdges}; } [[nodiscard]] IndexAndProgressLoggerEndIterator end() { return IndexAndProgressLoggerEndIterator{mLoopCount}; } private: friend class ProgressLogger; IndexAndProgressLoggerRange(ProgressLogger& aProgressLogger, ProportionValue aLoopStartInGlobalSpace, ProportionValue aLoopEndInGlobalSpace, uint32_t aLoopCount, const char* aLocationOrNullEmptyToIgnoreAtEdges = ProgressLogger::NO_LOCATION_UPDATE) : mGlobalProgressOrNull(aProgressLogger.mGlobalProgressOrNull), mLoopStartInGlobalSpace(aLoopStartInGlobalSpace), mLoopIncrementInGlobalSpace( (aLoopEndInGlobalSpace - aLoopStartInGlobalSpace) / aLoopCount), mLoopCount(aLoopCount), mLocationOrNullEmptyToIgnoreAtEdges( aLocationOrNullEmptyToIgnoreAtEdges) {} RefPtr mGlobalProgressOrNull; ProportionValue mLoopStartInGlobalSpace; ProportionValue mLoopIncrementInGlobalSpace; uint32_t mLoopCount; const char* mLocationOrNullEmptyToIgnoreAtEdges; }; [[nodiscard]] ProgressLogger::IndexAndProgressLoggerRange ProgressLogger::CreateLoopSubLoggersFromTo( ProportionValue aLoopStartInLocalSpace, ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount, const char* aLocationOrNullEmptyToIgnoreAtEdges) { return IndexAndProgressLoggerRange{ *this, LocalToGlobal(aLoopStartInLocalSpace), LocalToGlobal(aLoopEndInLocalSpace), aLoopCount, aLocationOrNullEmptyToIgnoreAtEdges}; } [[nodiscard]] ProgressLogger::IndexAndProgressLoggerRange ProgressLogger::CreateLoopSubLoggersTo( ProportionValue aLoopEndInLocalSpace, uint32_t aLoopCount, const char* aLocationOrNullEmptyToIgnoreAtEdges) { return IndexAndProgressLoggerRange{ *this, GetGlobalProgress(), LocalToGlobal(aLoopEndInLocalSpace), aLoopCount, aLocationOrNullEmptyToIgnoreAtEdges}; } } // namespace mozilla #endif // ProgressLogger_h