Bug 1632750 - Make ProfileBufferChunkManagerWithLocalLimit a ProfileBufferControlledChunkManager - r=canaltinova

Differential Revision: https://phabricator.services.mozilla.com/D72363
This commit is contained in:
Gerald Squelart 2020-05-08 03:34:28 +00:00
parent 2cf1bef331
commit 3292ac76d9
2 changed files with 288 additions and 2 deletions

View File

@ -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<ProfileBufferChunk> 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<ProfileBufferChunk> 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<void(UniquePtr<ProfileBufferChunk>)> mChunkReceiver;
UpdateCallback mUpdateCallback;
};
} // namespace mozilla

View File

@ -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<ProfileBufferChunk> 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<ProfileBufferChunk> 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<uintptr_t>(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<ProfileBufferChunk> newChunk;
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> 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<uintptr_t>(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();