diff --git a/mozglue/baseprofiler/public/ProfileBufferChunkManagerWithLocalLimit.h b/mozglue/baseprofiler/public/ProfileBufferChunkManagerWithLocalLimit.h index 2a379014d302..8066a55c5a6d 100644 --- a/mozglue/baseprofiler/public/ProfileBufferChunkManagerWithLocalLimit.h +++ b/mozglue/baseprofiler/public/ProfileBufferChunkManagerWithLocalLimit.h @@ -10,6 +10,7 @@ #include "BaseProfiler.h" #include "mozilla/BaseProfilerDetail.h" #include "mozilla/ProfileBufferChunkManager.h" +#include "mozilla/ProfileBufferControlledChunkManager.h" namespace mozilla { @@ -27,7 +28,8 @@ namespace mozilla { // assuming that the user is doing the right thing and releasing chunks ASAP, // and that the memory limit is reasonably large. class ProfileBufferChunkManagerWithLocalLimit final - : public ProfileBufferChunkManager { + : public ProfileBufferChunkManager, + public ProfileBufferControlledChunkManager { public: using Length = ProfileBufferChunk::Length; @@ -39,6 +41,13 @@ class ProfileBufferChunkManagerWithLocalLimit final : mMaxTotalBytes(aMaxTotalBytes), mChunkMinBufferBytes(aChunkMinBufferBytes) {} + ~ProfileBufferChunkManagerWithLocalLimit() { + if (mUpdateCallback) { + // Signal the end of this callback. + std::move(mUpdateCallback)(Update(nullptr)); + } + } + [[nodiscard]] size_t MaxTotalSize() const final { // `mMaxTotalBytes` is `const` so there is no need to lock the mutex. return mMaxTotalBytes; @@ -94,11 +103,20 @@ class ProfileBufferChunkManagerWithLocalLimit final void ReleaseChunks(UniquePtr aChunks) final { baseprofiler::detail::BaseProfilerAutoLock lock(mMutex); MOZ_ASSERT(mUser, "Not registered yet"); + // Keep a pointer to the first newly-released chunk, so we can use it to + // prepare an update (after `aChunks` is moved-from). + const ProfileBufferChunk* const newlyReleasedChunks = aChunks.get(); // Compute the size of all provided chunks. size_t bytes = 0; - for (const ProfileBufferChunk* chunk = aChunks.get(); chunk; + for (const ProfileBufferChunk* chunk = newlyReleasedChunks; chunk; chunk = chunk->GetNext()) { bytes += chunk->BufferBytes(); + MOZ_ASSERT(!chunk->ChunkHeader().mDoneTimeStamp.IsNull(), + "All released chunks should have a 'Done' timestamp"); + MOZ_ASSERT( + !chunk->GetNext() || (chunk->ChunkHeader().mDoneTimeStamp < + chunk->GetNext()->ChunkHeader().mDoneTimeStamp), + "Released chunk groups must have increasing timestamps"); } // Transfer the chunks size from the unreleased bucket to the released one. mUnreleasedBufferBytes -= bytes; @@ -110,9 +128,17 @@ class ProfileBufferChunkManagerWithLocalLimit final } else { // Add to the end of the released chunks list (oldest first, most recent // last.) + MOZ_ASSERT(mReleasedChunks->Last()->ChunkHeader().mDoneTimeStamp < + aChunks->ChunkHeader().mDoneTimeStamp, + "Chunks must be released in increasing timestamps"); mReleasedBufferBytes += bytes; mReleasedChunks->SetLast(std::move(aChunks)); } + + if (mUpdateCallback) { + mUpdateCallback(Update(mUnreleasedBufferBytes, mReleasedBufferBytes, + mReleasedChunks.get(), newlyReleasedChunks)); + } } void SetChunkDestroyedCallback( @@ -127,6 +153,9 @@ class ProfileBufferChunkManagerWithLocalLimit final baseprofiler::detail::BaseProfilerAutoLock lock(mMutex); MOZ_ASSERT(mUser, "Not registered yet"); mReleasedBufferBytes = 0; + if (mUpdateCallback) { + mUpdateCallback(Update(mUnreleasedBufferBytes, 0, nullptr, nullptr)); + } return std::move(mReleasedChunks); } @@ -134,6 +163,10 @@ class ProfileBufferChunkManagerWithLocalLimit final baseprofiler::detail::BaseProfilerAutoLock lock(mMutex); MOZ_ASSERT(mUser, "Not registered yet"); mUnreleasedBufferBytes = 0; + if (mUpdateCallback) { + mUpdateCallback( + Update(0, mReleasedBufferBytes, mReleasedChunks.get(), nullptr)); + } } [[nodiscard]] size_t SizeOfExcludingThis( @@ -149,6 +182,44 @@ class ProfileBufferChunkManagerWithLocalLimit final return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf, lock); } + void SetUpdateCallback(UpdateCallback&& aUpdateCallback) final { + baseprofiler::detail::BaseProfilerAutoLock lock(mMutex); + if (mUpdateCallback) { + // Signal the end of the previous callback. + std::move(mUpdateCallback)(Update(nullptr)); + } + mUpdateCallback = std::move(aUpdateCallback); + if (mUpdateCallback) { + mUpdateCallback(Update(mUnreleasedBufferBytes, mReleasedBufferBytes, + mReleasedChunks.get(), nullptr)); + } + } + + void DestroyChunksAtOrBefore(TimeStamp aDoneTimeStamp) final { + MOZ_ASSERT(!aDoneTimeStamp.IsNull()); + baseprofiler::detail::BaseProfilerAutoLock lock(mMutex); + for (;;) { + if (!mReleasedChunks) { + // We don't own any released chunks (anymore), we're done. + break; + } + if (mReleasedChunks->ChunkHeader().mDoneTimeStamp > aDoneTimeStamp) { + // The current chunk is strictly after the given timestamp, we're done. + break; + } + // We've found a chunk at or before the timestamp. Move the released chunk + // pointer to the next one. + UniquePtr oldest = + std::exchange(mReleasedChunks, mReleasedChunks->ReleaseNext()); + mReleasedBufferBytes -= oldest->ChunkBytes(); + if (mChunkDestroyedCallback) { + // Inform the user that we're going to destroy this chunk. + mChunkDestroyedCallback(*oldest); + } + // Stop using `oldest`, this will effectively destroy it. + } + } + protected: const ProfileBufferChunk* PeekExtantReleasedChunksAndLock() final { mMutex.Lock(); @@ -205,6 +276,11 @@ class ProfileBufferChunkManagerWithLocalLimit final if (chunk) { // We do have a chunk (recycled or new), record its size as "unreleased". mUnreleasedBufferBytes += chunk->BufferBytes(); + + if (mUpdateCallback) { + mUpdateCallback(Update(mUnreleasedBufferBytes, mReleasedBufferBytes, + mReleasedChunks.get(), nullptr)); + } } return chunk; @@ -251,6 +327,8 @@ class ProfileBufferChunkManagerWithLocalLimit final // Callback set from `RequestChunk()`, until it is serviced in // `FulfillChunkRequests()`. There can only be one request in flight. std::function)> mChunkReceiver; + + UpdateCallback mUpdateCallback; }; } // namespace mozilla diff --git a/mozglue/tests/TestBaseProfiler.cpp b/mozglue/tests/TestBaseProfiler.cpp index e2d717d27258..88f914aa0ecb 100644 --- a/mozglue/tests/TestBaseProfiler.cpp +++ b/mozglue/tests/TestBaseProfiler.cpp @@ -1088,6 +1088,213 @@ static void TestControlledChunkManagerUpdate() { printf("TestControlledChunkManagerUpdate done\n"); } +static void TestControlledChunkManagerWithLocalLimit() { + printf("TestControlledChunkManagerWithLocalLimit...\n"); + + // Construct a ProfileBufferChunkManagerWithLocalLimit with chunk of minimum + // size >=100, up to 1000 bytes. + constexpr ProfileBufferChunk::Length MaxTotalBytes = 1000; + constexpr ProfileBufferChunk::Length ChunkMinBufferBytes = 100; + ProfileBufferChunkManagerWithLocalLimit cmll{MaxTotalBytes, + ChunkMinBufferBytes}; + + // Reference to chunk manager base class. + ProfileBufferChunkManager& cm = cmll; + + // Reference to controlled chunk manager base class. + ProfileBufferControlledChunkManager& ccm = cmll; + +#ifdef DEBUG + const char* chunkManagerRegisterer = + "TestControlledChunkManagerWithLocalLimit"; + cm.RegisteredWith(chunkManagerRegisterer); +#endif // DEBUG + + MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == MaxTotalBytes, + "Max total size should be exactly as given"); + + unsigned destroyedChunks = 0; + unsigned destroyedBytes = 0; + cm.SetChunkDestroyedCallback([&](const ProfileBufferChunk& aChunks) { + for (const ProfileBufferChunk* chunk = &aChunks; chunk; + chunk = chunk->GetNext()) { + destroyedChunks += 1; + destroyedBytes += chunk->BufferBytes(); + } + }); + + using Update = ProfileBufferControlledChunkManager::Update; + unsigned updateCount = 0; + ProfileBufferControlledChunkManager::Update update; + MOZ_RELEASE_ASSERT(update.IsNotUpdate()); + auto updateCallback = [&](Update&& aUpdate) { + ++updateCount; + update.Fold(std::move(aUpdate)); + }; + ccm.SetUpdateCallback(updateCallback); + MOZ_RELEASE_ASSERT(updateCount == 1, + "SetUpdateCallback should have triggered an update"); + MOZ_RELEASE_ASSERT(IsSameUpdate(update, Update(0, 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + UniquePtr extantReleasedChunks = + cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetExtantReleasedChunks should have triggered an update"); + MOZ_RELEASE_ASSERT(IsSameUpdate(update, Update(0, 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + // First request. + UniquePtr chunk = cm.GetChunk(); + MOZ_RELEASE_ASSERT(!!chunk, + "First chunk immediate request should always work"); + const auto chunkActualBufferBytes = chunk->BufferBytes(); + // Keep address, for later checks. + const uintptr_t chunk1Address = reinterpret_cast(chunk.get()); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetChunk should have triggered an update"); + MOZ_RELEASE_ASSERT( + IsSameUpdate(update, Update(chunk->BufferBytes(), 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + extantReleasedChunks = cm.GetExtantReleasedChunks(); + MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)"); + MOZ_RELEASE_ASSERT(updateCount == 1, + "GetExtantReleasedChunks should have triggered an update"); + MOZ_RELEASE_ASSERT( + IsSameUpdate(update, Update(chunk->BufferBytes(), 0, TimeStamp{}, {}))); + updateCount = 0; + update.Clear(); + + // For this test, we need to be able to get at least 2 chunks without hitting + // the limit. (If this failed, it wouldn't necessary be a problem with + // ProfileBufferChunkManagerWithLocalLimit, fiddle with constants at the top + // of this test.) + MOZ_RELEASE_ASSERT(chunkActualBufferBytes < 2 * MaxTotalBytes); + + ProfileBufferChunk::Length previousUnreleasedBytes = chunk->BufferBytes(); + ProfileBufferChunk::Length previousReleasedBytes = 0; + TimeStamp previousOldestDoneTimeStamp; + + unsigned chunk1ReuseCount = 0; + + // We will do enough loops to go through the maximum size a number of times. + const unsigned Rollovers = 3; + const unsigned Loops = Rollovers * MaxTotalBytes / chunkActualBufferBytes; + for (unsigned i = 0; i < Loops; ++i) { + // Add some data to the chunk. + const ProfileBufferIndex index = + ProfileBufferIndex(chunkActualBufferBytes) * i + 1; + chunk->SetRangeStart(index); + Unused << chunk->ReserveInitialBlockAsTail(1); + Unused << chunk->ReserveBlock(2); + + // Request a new chunk. + UniquePtr newChunk; + cm.RequestChunk([&](UniquePtr aChunk) { + newChunk = std::move(aChunk); + }); + MOZ_RELEASE_ASSERT(updateCount == 0, + "RequestChunk() shouldn't have triggered an update"); + cm.FulfillChunkRequests(); + MOZ_RELEASE_ASSERT(!!newChunk, "Chunk request should always work"); + MOZ_RELEASE_ASSERT(newChunk->BufferBytes() == chunkActualBufferBytes, + "Unexpected chunk size"); + MOZ_RELEASE_ASSERT(!newChunk->GetNext(), "There should only be one chunk"); + + MOZ_RELEASE_ASSERT(updateCount == 1, + "FulfillChunkRequests() after a request should have " + "triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == + previousUnreleasedBytes + newChunk->BufferBytes()); + previousUnreleasedBytes = update.UnreleasedBytes(); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() <= previousReleasedBytes); + previousReleasedBytes = update.ReleasedBytes(); + MOZ_RELEASE_ASSERT(previousOldestDoneTimeStamp.IsNull() || + update.OldestDoneTimeStamp() >= + previousOldestDoneTimeStamp); + previousOldestDoneTimeStamp = update.OldestDoneTimeStamp(); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().empty()); + updateCount = 0; + update.Clear(); + + // Make sure the "Done" timestamp below cannot be the same as from the + // previous loop. + const TimeStamp now = TimeStamp::NowUnfuzzed(); + while (TimeStamp::NowUnfuzzed() == now) { + ::SleepMilli(1); + } + + // Mark previous chunk done and release it. + chunk->MarkDone(); + const auto doneTimeStamp = chunk->ChunkHeader().mDoneTimeStamp; + const auto bufferBytes = chunk->BufferBytes(); + cm.ReleaseChunks(std::move(chunk)); + + MOZ_RELEASE_ASSERT(updateCount == 1, + "ReleaseChunks() should have triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == + previousUnreleasedBytes - bufferBytes); + previousUnreleasedBytes = update.UnreleasedBytes(); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() == + previousReleasedBytes + bufferBytes); + previousReleasedBytes = update.ReleasedBytes(); + MOZ_RELEASE_ASSERT(previousOldestDoneTimeStamp.IsNull() || + update.OldestDoneTimeStamp() >= + previousOldestDoneTimeStamp); + previousOldestDoneTimeStamp = update.OldestDoneTimeStamp(); + MOZ_RELEASE_ASSERT(update.OldestDoneTimeStamp() <= doneTimeStamp); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().size() == 1); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef()[0].mDoneTimeStamp == + doneTimeStamp); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef()[0].mBufferBytes == + bufferBytes); + updateCount = 0; + update.Clear(); + + // And cycle to the new chunk. + chunk = std::move(newChunk); + + if (reinterpret_cast(chunk.get()) == chunk1Address) { + ++chunk1ReuseCount; + } + } + + // Enough testing! Clean-up. + Unused << chunk->ReserveInitialBlockAsTail(0); + chunk->MarkDone(); + cm.ForgetUnreleasedChunks(); + MOZ_RELEASE_ASSERT( + updateCount == 1, + "ForgetUnreleasedChunks() should have triggered an update"); + MOZ_RELEASE_ASSERT(!update.IsFinal()); + MOZ_RELEASE_ASSERT(!update.IsNotUpdate()); + MOZ_RELEASE_ASSERT(update.UnreleasedBytes() == 0); + MOZ_RELEASE_ASSERT(update.ReleasedBytes() == previousReleasedBytes); + MOZ_RELEASE_ASSERT(update.NewlyReleasedChunksRef().empty() == 1); + updateCount = 0; + update.Clear(); + + ccm.SetUpdateCallback({}); + MOZ_RELEASE_ASSERT(updateCount == 1, + "SetUpdateCallback({}) should have triggered an update"); + MOZ_RELEASE_ASSERT(update.IsFinal()); + +#ifdef DEBUG + cm.DeregisteredFrom(chunkManagerRegisterer); +#endif // DEBUG + + printf("TestControlledChunkManagerWithLocalLimit done\n"); +} + static void TestChunkedBuffer() { printf("TestChunkedBuffer...\n"); @@ -2781,6 +2988,7 @@ void TestProfilerDependencies() { TestChunkManagerSingle(); TestChunkManagerWithLocalLimit(); TestControlledChunkManagerUpdate(); + TestControlledChunkManagerWithLocalLimit(); TestChunkedBuffer(); TestChunkedBufferSingle(); TestModuloBuffer();