1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #ifndef ProfileBufferChunkManagerWithLocalLimit_h
8 #define ProfileBufferChunkManagerWithLocalLimit_h
10 #include "BaseProfiler.h"
11 #include "mozilla/BaseProfilerDetail.h"
12 #include "mozilla/ProfileBufferChunkManager.h"
13 #include "mozilla/ProfileBufferControlledChunkManager.h"
17 // Manages the Chunks for this process in a thread-safe manner, with a maximum
20 // "Unreleased" chunks are not owned here, only "released" chunks can be
21 // destroyed or recycled when reaching the memory limit, so it is theoretically
22 // possible to break that limit, if:
23 // - The user of this class doesn't release their chunks, AND/OR
24 // - The limit is too small (e.g., smaller than 2 or 3 chunks, which should be
25 // the usual number of unreleased chunks in flight).
26 // In this case, it just means that we will use more memory than allowed,
27 // potentially risking OOMs. Hopefully this shouldn't happen in real code,
28 // assuming that the user is doing the right thing and releasing chunks ASAP,
29 // and that the memory limit is reasonably large.
30 class ProfileBufferChunkManagerWithLocalLimit final
31 : public ProfileBufferChunkManager
,
32 public ProfileBufferControlledChunkManager
{
34 using Length
= ProfileBufferChunk::Length
;
36 // MaxTotalBytes: Maximum number of bytes allocated in all local Chunks.
37 // ChunkMinBufferBytes: Minimum number of user-available bytes in each Chunk.
38 // Note that Chunks use a bit more memory for their header.
39 explicit ProfileBufferChunkManagerWithLocalLimit(size_t aMaxTotalBytes
,
40 Length aChunkMinBufferBytes
)
41 : mMaxTotalBytes(aMaxTotalBytes
),
42 mChunkMinBufferBytes(aChunkMinBufferBytes
) {}
44 ~ProfileBufferChunkManagerWithLocalLimit() {
45 if (mUpdateCallback
) {
46 // Signal the end of this callback.
47 std::move(mUpdateCallback
)(Update(nullptr));
51 [[nodiscard
]] size_t MaxTotalSize() const final
{
52 // `mMaxTotalBytes` is `const` so there is no need to lock the mutex.
53 return mMaxTotalBytes
;
56 [[nodiscard
]] UniquePtr
<ProfileBufferChunk
> GetChunk() final
{
57 AUTO_PROFILER_STATS(Local_GetChunk
);
58 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
59 return GetChunk(lock
);
62 void RequestChunk(std::function
<void(UniquePtr
<ProfileBufferChunk
>)>&&
63 aChunkReceiver
) final
{
64 AUTO_PROFILER_STATS(Local_RequestChunk
);
65 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
67 // We already have a chunk receiver, meaning a request is pending.
70 // Store the chunk receiver. This indicates that a request is pending, and
71 // it will be handled in the next `FulfillChunkRequests()` call.
72 mChunkReceiver
= std::move(aChunkReceiver
);
75 void FulfillChunkRequests() final
{
76 AUTO_PROFILER_STATS(Local_FulfillChunkRequests
);
77 std::function
<void(UniquePtr
<ProfileBufferChunk
>)> chunkReceiver
;
78 UniquePtr
<ProfileBufferChunk
> chunk
;
80 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
81 if (!mChunkReceiver
) {
82 // No receiver means no pending request, we're done.
85 // Otherwise there is a request, extract the receiver to call below.
86 std::swap(chunkReceiver
, mChunkReceiver
);
87 MOZ_ASSERT(!mChunkReceiver
, "mChunkReceiver should have been emptied");
88 // And allocate the requested chunk. This may fail, it's fine, we're
89 // letting the receiver know about it.
90 AUTO_PROFILER_STATS(Local_FulfillChunkRequests_GetChunk
);
91 chunk
= GetChunk(lock
);
93 // Invoke callback outside of lock, so that it can use other chunk manager
94 // functions if needed.
95 // Note that this means there could be a race, where another request happens
96 // now and even gets fulfilled before this one is! It should be rare, and
97 // shouldn't be a problem anyway, the user will still get their requested
98 // chunks, new/recycled chunks look the same so their order doesn't matter.
99 MOZ_ASSERT(!!chunkReceiver
, "chunkReceiver shouldn't be empty here");
100 std::move(chunkReceiver
)(std::move(chunk
));
103 void ReleaseChunks(UniquePtr
<ProfileBufferChunk
> aChunks
) final
{
104 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
105 MOZ_ASSERT(mUser
, "Not registered yet");
106 // Keep a pointer to the first newly-released chunk, so we can use it to
107 // prepare an update (after `aChunks` is moved-from).
108 const ProfileBufferChunk
* const newlyReleasedChunks
= aChunks
.get();
109 // Compute the size of all provided chunks.
111 for (const ProfileBufferChunk
* chunk
= newlyReleasedChunks
; chunk
;
112 chunk
= chunk
->GetNext()) {
113 bytes
+= chunk
->BufferBytes();
114 MOZ_ASSERT(!chunk
->ChunkHeader().mDoneTimeStamp
.IsNull(),
115 "All released chunks should have a 'Done' timestamp");
117 !chunk
->GetNext() || (chunk
->ChunkHeader().mDoneTimeStamp
<
118 chunk
->GetNext()->ChunkHeader().mDoneTimeStamp
),
119 "Released chunk groups must have increasing timestamps");
121 // Transfer the chunks size from the unreleased bucket to the released one.
122 mUnreleasedBufferBytes
-= bytes
;
123 if (!mReleasedChunks
) {
124 // No other released chunks at the moment, we're starting the list.
125 MOZ_ASSERT(mReleasedBufferBytes
== 0);
126 mReleasedBufferBytes
= bytes
;
127 mReleasedChunks
= std::move(aChunks
);
129 // Add to the end of the released chunks list (oldest first, most recent
131 MOZ_ASSERT(mReleasedChunks
->Last()->ChunkHeader().mDoneTimeStamp
<
132 aChunks
->ChunkHeader().mDoneTimeStamp
,
133 "Chunks must be released in increasing timestamps");
134 mReleasedBufferBytes
+= bytes
;
135 mReleasedChunks
->SetLast(std::move(aChunks
));
138 if (mUpdateCallback
) {
139 mUpdateCallback(Update(mUnreleasedBufferBytes
, mReleasedBufferBytes
,
140 mReleasedChunks
.get(), newlyReleasedChunks
));
144 void SetChunkDestroyedCallback(
145 std::function
<void(const ProfileBufferChunk
&)>&& aChunkDestroyedCallback
)
147 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
148 MOZ_ASSERT(mUser
, "Not registered yet");
149 mChunkDestroyedCallback
= std::move(aChunkDestroyedCallback
);
152 [[nodiscard
]] UniquePtr
<ProfileBufferChunk
> GetExtantReleasedChunks() final
{
153 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
154 MOZ_ASSERT(mUser
, "Not registered yet");
155 mReleasedBufferBytes
= 0;
156 if (mUpdateCallback
) {
157 mUpdateCallback(Update(mUnreleasedBufferBytes
, 0, nullptr, nullptr));
159 return std::move(mReleasedChunks
);
162 void ForgetUnreleasedChunks() final
{
163 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
164 MOZ_ASSERT(mUser
, "Not registered yet");
165 mUnreleasedBufferBytes
= 0;
166 if (mUpdateCallback
) {
168 Update(0, mReleasedBufferBytes
, mReleasedChunks
.get(), nullptr));
172 [[nodiscard
]] size_t SizeOfExcludingThis(
173 MallocSizeOf aMallocSizeOf
) const final
{
174 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
175 return SizeOfExcludingThis(aMallocSizeOf
, lock
);
178 [[nodiscard
]] size_t SizeOfIncludingThis(
179 MallocSizeOf aMallocSizeOf
) const final
{
180 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
181 MOZ_ASSERT(mUser
, "Not registered yet");
182 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf
, lock
);
185 void SetUpdateCallback(UpdateCallback
&& aUpdateCallback
) final
{
186 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
187 if (mUpdateCallback
) {
188 // Signal the end of the previous callback.
189 std::move(mUpdateCallback
)(Update(nullptr));
191 mUpdateCallback
= std::move(aUpdateCallback
);
192 if (mUpdateCallback
) {
193 mUpdateCallback(Update(mUnreleasedBufferBytes
, mReleasedBufferBytes
,
194 mReleasedChunks
.get(), nullptr));
198 void DestroyChunksAtOrBefore(TimeStamp aDoneTimeStamp
) final
{
199 MOZ_ASSERT(!aDoneTimeStamp
.IsNull());
200 baseprofiler::detail::BaseProfilerAutoLock
lock(mMutex
);
202 if (!mReleasedChunks
) {
203 // We don't own any released chunks (anymore), we're done.
206 if (mReleasedChunks
->ChunkHeader().mDoneTimeStamp
> aDoneTimeStamp
) {
207 // The current chunk is strictly after the given timestamp, we're done.
210 // We've found a chunk at or before the timestamp, discard it.
211 DiscardOldestReleasedChunk(lock
);
216 const ProfileBufferChunk
* PeekExtantReleasedChunksAndLock() final
{
218 MOZ_ASSERT(mUser
, "Not registered yet");
219 return mReleasedChunks
.get();
221 void UnlockAfterPeekExtantReleasedChunks() final
{ mMutex
.Unlock(); }
224 void MaybeRecycleChunk(
225 UniquePtr
<ProfileBufferChunk
>&& chunk
,
226 const baseprofiler::detail::BaseProfilerAutoLock
& aLock
) {
227 // Try to recycle big-enough chunks. (All chunks should have the same size,
228 // but it's a cheap test and may allow future adjustments based on actual
230 if (chunk
->BufferBytes() >= mChunkMinBufferBytes
) {
231 // We keep up to two recycled chunks at any time.
232 if (!mRecycledChunks
) {
233 mRecycledChunks
= std::move(chunk
);
234 } else if (!mRecycledChunks
->GetNext()) {
235 mRecycledChunks
->InsertNext(std::move(chunk
));
240 UniquePtr
<ProfileBufferChunk
> TakeRecycledChunk(
241 const baseprofiler::detail::BaseProfilerAutoLock
& aLock
) {
242 UniquePtr
<ProfileBufferChunk
> recycled
;
243 if (mRecycledChunks
) {
244 recycled
= std::exchange(mRecycledChunks
, mRecycledChunks
->ReleaseNext());
245 recycled
->MarkRecycled();
250 void DiscardOldestReleasedChunk(
251 const baseprofiler::detail::BaseProfilerAutoLock
& aLock
) {
252 MOZ_ASSERT(!!mReleasedChunks
);
253 UniquePtr
<ProfileBufferChunk
> oldest
=
254 std::exchange(mReleasedChunks
, mReleasedChunks
->ReleaseNext());
255 mReleasedBufferBytes
-= oldest
->BufferBytes();
256 if (mChunkDestroyedCallback
) {
257 // Inform the user that we're going to destroy this chunk.
258 mChunkDestroyedCallback(*oldest
);
260 MaybeRecycleChunk(std::move(oldest
), aLock
);
263 [[nodiscard
]] UniquePtr
<ProfileBufferChunk
> GetChunk(
264 const baseprofiler::detail::BaseProfilerAutoLock
& aLock
) {
265 MOZ_ASSERT(mUser
, "Not registered yet");
266 // After this function, the total memory consumption will be the sum of:
267 // - Bytes from released (i.e., full) chunks,
268 // - Bytes from unreleased (still in use) chunks,
269 // - Bytes from the chunk we want to create/recycle. (Note that we don't
270 // count the extra bytes of chunk header, and of extra allocation ability,
271 // for the new chunk, as it's assumed to be negligible compared to the
272 // total memory limit.)
273 // If this total is higher than the local limit, we'll want to destroy
274 // the oldest released chunks until we're under the limit; if any, we may
275 // recycle one of them to avoid a deallocation followed by an allocation.
276 while (mReleasedBufferBytes
+ mUnreleasedBufferBytes
+
277 mChunkMinBufferBytes
>=
280 // We have reached the local limit, discard the oldest released chunk.
281 DiscardOldestReleasedChunk(aLock
);
284 // Extract the recycled chunk, if any.
285 UniquePtr
<ProfileBufferChunk
> chunk
= TakeRecycledChunk(aLock
);
288 // No recycled chunk -> Create a chunk now. (This could still fail.)
289 chunk
= ProfileBufferChunk::Create(mChunkMinBufferBytes
);
293 // We do have a chunk (recycled or new), record its size as "unreleased".
294 mUnreleasedBufferBytes
+= chunk
->BufferBytes();
296 if (mUpdateCallback
) {
297 mUpdateCallback(Update(mUnreleasedBufferBytes
, mReleasedBufferBytes
,
298 mReleasedChunks
.get(), nullptr));
305 [[nodiscard
]] size_t SizeOfExcludingThis(
306 MallocSizeOf aMallocSizeOf
,
307 const baseprofiler::detail::BaseProfilerAutoLock
&) const {
308 MOZ_ASSERT(mUser
, "Not registered yet");
310 if (mReleasedChunks
) {
311 size
+= mReleasedChunks
->SizeOfIncludingThis(aMallocSizeOf
);
313 if (mRecycledChunks
) {
314 size
+= mRecycledChunks
->SizeOfIncludingThis(aMallocSizeOf
);
316 // Note: Missing size of std::function external resources (if any).
320 // Maxumum number of bytes that should be used by all unreleased and released
321 // chunks. Note that only released chunks can be destroyed here, so it is the
322 // responsibility of the user to properly release their chunks when possible.
323 const size_t mMaxTotalBytes
;
325 // Minimum number of bytes that new chunks should be able to store.
326 // Used when calling `ProfileBufferChunk::Create()`.
327 const Length mChunkMinBufferBytes
;
329 // Mutex guarding the following members.
330 mutable baseprofiler::detail::BaseProfilerMutex mMutex
;
332 // Number of bytes currently held in chunks that have been given away (through
333 // `GetChunk` or `RequestChunk`) and not released yet.
334 size_t mUnreleasedBufferBytes
= 0;
336 // Number of bytes currently held in chunks that have been released and stored
337 // in `mReleasedChunks` below.
338 size_t mReleasedBufferBytes
= 0;
340 // List of all released chunks. The oldest one should be at the start of the
341 // list, and may be destroyed or recycled when the memory limit is reached.
342 UniquePtr
<ProfileBufferChunk
> mReleasedChunks
;
344 // This may hold chunks that were released then slated for destruction, they
345 // will be reused next time an allocation would have been needed.
346 UniquePtr
<ProfileBufferChunk
> mRecycledChunks
;
348 // Optional callback used to notify the user when a chunk is about to be
349 // destroyed or recycled. (The data content is always destroyed, but the chunk
350 // container may be reused.)
351 std::function
<void(const ProfileBufferChunk
&)> mChunkDestroyedCallback
;
353 // Callback set from `RequestChunk()`, until it is serviced in
354 // `FulfillChunkRequests()`. There can only be one request in flight.
355 std::function
<void(UniquePtr
<ProfileBufferChunk
>)> mChunkReceiver
;
357 UpdateCallback mUpdateCallback
;
360 } // namespace mozilla
362 #endif // ProfileBufferChunkManagerWithLocalLimit_h