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/. */
6 
7 #ifndef ProfileBufferChunk_h
8 #define ProfileBufferChunk_h
9 
10 #include "mozilla/MemoryReporting.h"
11 #include "mozilla/ProfileBufferIndex.h"
12 #include "mozilla/Span.h"
13 #include "mozilla/TimeStamp.h"
14 #include "mozilla/UniquePtr.h"
15 
16 #if defined(MOZ_MEMORY)
17 #  include "mozmemory.h"
18 #endif
19 
20 #include <algorithm>
21 #include <limits>
22 #include <type_traits>
23 
24 #ifdef DEBUG
25 #  include <cstdio>
26 #endif
27 
28 namespace mozilla {
29 
30 // Represents a single chunk of memory, with a link to the next chunk (or null).
31 //
32 // A chunk is made of an internal header (which contains a public part) followed
33 // by user-accessible bytes.
34 //
35 // +---------------+---------+----------------------------------------------+
36 // | public Header | private |         memory containing user blocks        |
37 // +---------------+---------+----------------------------------------------+
38 //                           <---------------BufferBytes()------------------>
39 // <------------------------------ChunkBytes()------------------------------>
40 //
41 // The chunk can reserve "blocks", but doesn't know the internal contents of
42 // each block, it only knows where the first one starts, and where the last one
43 // ends (which is where the next one will begin, if not already out of range).
44 // It is up to the user to add structure to each block so that they can be
45 // distinguished when later read.
46 //
47 // +---------------+---------+----------------------------------------------+
48 // | public Header | private |      [1st block]...[last full block]         |
49 // +---------------+---------+----------------------------------------------+
50 //  ChunkHeader().mOffsetFirstBlock ^                             ^
51 //                           ChunkHeader().mOffsetPastLastBlock --'
52 //
53 // It is possible to attempt to reserve more than the remaining space, in which
54 // case only what is available is returned. The caller is responsible for using
55 // another chunk, reserving a block "tail" in it, and using both parts to
56 // constitute a full block. (This initial tail may be empty in some chunks.)
57 //
58 // +---------------+---------+----------------------------------------------+
59 // | public Header | private | tail][1st block]...[last full block][head... |
60 // +---------------+---------+----------------------------------------------+
61 //  ChunkHeader().mOffsetFirstBlock ^                                       ^
62 //                                     ChunkHeader().mOffsetPastLastBlock --'
63 //
64 // Each Chunk has an internal state (checked in DEBUG builds) that directs how
65 // to use it during creation, initialization, use, end of life, recycling, and
66 // destruction. See `State` below for details.
67 // In particular:
68 // - `ReserveInitialBlockAsTail()` must be called before the first `Reserve()`
69 //   after construction or recycling, even with a size of 0 (no actual tail),
70 // - `MarkDone()` and `MarkRecycled()` must be called as appropriate.
71 class ProfileBufferChunk {
72  public:
73   using Byte = uint8_t;
74   using Length = uint32_t;
75 
76   using SpanOfBytes = Span<Byte>;
77 
78   // Hint about the size of the metadata (public and private headers).
79   // `Create()` below takes the minimum *buffer* size, so the minimum total
80   // Chunk size is at least `SizeofChunkMetadata() + aMinBufferBytes`.
SizeofChunkMetadata()81   [[nodiscard]] static constexpr Length SizeofChunkMetadata() {
82     return static_cast<Length>(sizeof(InternalHeader));
83   }
84 
85   // Allocate space for a chunk with a given minimum size, and construct it.
86   // The actual size may be higher, to match the actual space taken in the
87   // memory pool.
Create(Length aMinBufferBytes)88   [[nodiscard]] static UniquePtr<ProfileBufferChunk> Create(
89       Length aMinBufferBytes) {
90     // We need at least one byte, to cover the always-present `mBuffer` byte.
91     aMinBufferBytes = std::max(aMinBufferBytes, Length(1));
92     // Trivial struct with the same alignment as `ProfileBufferChunk`, and size
93     // equal to that alignment, because typically the sizeof of an object is
94     // a multiple of its alignment.
95     struct alignas(alignof(InternalHeader)) ChunkStruct {
96       Byte c[alignof(InternalHeader)];
97     };
98     static_assert(std::is_trivial_v<ChunkStruct>,
99                   "ChunkStruct must be trivial to avoid any construction");
100     // Allocate an array of that struct, enough to contain the expected
101     // `ProfileBufferChunk` (with its header+buffer).
102     size_t count = (sizeof(InternalHeader) + aMinBufferBytes +
103                     (alignof(InternalHeader) - 1)) /
104                    alignof(InternalHeader);
105 #if defined(MOZ_MEMORY)
106     // Potentially expand the array to use more of the effective allocation.
107     count = (malloc_good_size(count * sizeof(ChunkStruct)) +
108              (sizeof(ChunkStruct) - 1)) /
109             sizeof(ChunkStruct);
110 #endif
111     auto chunkStorage = MakeUnique<ChunkStruct[]>(count);
112     MOZ_ASSERT(reinterpret_cast<uintptr_t>(chunkStorage.get()) %
113                    alignof(InternalHeader) ==
114                0);
115     // After the allocation, compute the actual chunk size (including header).
116     const size_t chunkBytes = count * sizeof(ChunkStruct);
117     MOZ_ASSERT(chunkBytes >= sizeof(ProfileBufferChunk),
118                "Not enough space to construct a ProfileBufferChunk");
119     MOZ_ASSERT(chunkBytes <=
120                static_cast<size_t>(std::numeric_limits<Length>::max()));
121     // Compute the size of the user-accessible buffer inside the chunk.
122     const Length bufferBytes =
123         static_cast<Length>(chunkBytes - sizeof(InternalHeader));
124     MOZ_ASSERT(bufferBytes >= aMinBufferBytes,
125                "Not enough space for minimum buffer size");
126     // Construct the header at the beginning of the allocated array, with the
127     // known buffer size.
128     new (chunkStorage.get()) ProfileBufferChunk(bufferBytes);
129     // We now have a proper `ProfileBufferChunk` object, create the appropriate
130     // UniquePtr for it.
131     UniquePtr<ProfileBufferChunk> chunk{
132         reinterpret_cast<ProfileBufferChunk*>(chunkStorage.release())};
133     MOZ_ASSERT(
134         size_t(reinterpret_cast<const char*>(
135                    &chunk.get()->BufferSpan()[bufferBytes - 1]) -
136                reinterpret_cast<const char*>(chunk.get())) == chunkBytes - 1,
137         "Buffer span spills out of chunk allocation");
138     return chunk;
139   }
140 
141 #ifdef DEBUG
~ProfileBufferChunk()142   ~ProfileBufferChunk() {
143     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse);
144     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full);
145     MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created ||
146                mInternalHeader.mState == InternalHeader::State::Done ||
147                mInternalHeader.mState == InternalHeader::State::Recycled);
148   }
149 #endif
150 
151   // Must be called with the first block tail (may be empty), which will be
152   // skipped if the reader starts with this ProfileBufferChunk.
ReserveInitialBlockAsTail(Length aTailSize)153   [[nodiscard]] SpanOfBytes ReserveInitialBlockAsTail(Length aTailSize) {
154 #ifdef DEBUG
155     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse);
156     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full);
157     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done);
158     MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created ||
159                mInternalHeader.mState == InternalHeader::State::Recycled);
160     mInternalHeader.mState = InternalHeader::State::InUse;
161 #endif
162     mInternalHeader.mHeader.mOffsetFirstBlock = aTailSize;
163     mInternalHeader.mHeader.mOffsetPastLastBlock = aTailSize;
164     return SpanOfBytes(&mBuffer, aTailSize);
165   }
166 
167   struct ReserveReturn {
168     SpanOfBytes mSpan;
169     ProfileBufferBlockIndex mBlockRangeIndex;
170   };
171 
172   // Reserve a block of up to `aBlockSize` bytes, and return a Span to it, and
173   // its starting index. The actual size may be smaller, if the block cannot fit
174   // in the remaining space.
ReserveBlock(Length aBlockSize)175   [[nodiscard]] ReserveReturn ReserveBlock(Length aBlockSize) {
176     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Created);
177     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full);
178     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done);
179     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Recycled);
180     MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::InUse);
181     MOZ_ASSERT(RangeStart() != 0,
182                "Expected valid range start before first Reserve()");
183     const Length blockOffset = mInternalHeader.mHeader.mOffsetPastLastBlock;
184     Length reservedSize = aBlockSize;
185     if (MOZ_UNLIKELY(aBlockSize >= RemainingBytes())) {
186       reservedSize = RemainingBytes();
187 #ifdef DEBUG
188       mInternalHeader.mState = InternalHeader::State::Full;
189 #endif
190     }
191     mInternalHeader.mHeader.mOffsetPastLastBlock += reservedSize;
192     mInternalHeader.mHeader.mBlockCount += 1;
193     return {SpanOfBytes(&mBuffer + blockOffset, reservedSize),
194             ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
195                 mInternalHeader.mHeader.mRangeStart + blockOffset)};
196   }
197 
198   // When a chunk will not be used to store more blocks (because it is full, or
199   // because the profiler will not add more data), it should be marked "done".
200   // Access to its content is still allowed.
MarkDone()201   void MarkDone() {
202 #ifdef DEBUG
203     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Created);
204     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done);
205     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Recycled);
206     MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::InUse ||
207                mInternalHeader.mState == InternalHeader::State::Full);
208     mInternalHeader.mState = InternalHeader::State::Done;
209 #endif
210     mInternalHeader.mHeader.mDoneTimeStamp = TimeStamp::Now();
211   }
212 
213   // A "Done" chunk may be recycled, to avoid allocating a new one.
MarkRecycled()214   void MarkRecycled() {
215 #ifdef DEBUG
216     // We also allow Created and already-Recycled chunks to be recycled, this
217     // way it's easier to recycle chunks when their state is not easily
218     // trackable.
219     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse);
220     MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full);
221     MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created ||
222                mInternalHeader.mState == InternalHeader::State::Done ||
223                mInternalHeader.mState == InternalHeader::State::Recycled);
224     mInternalHeader.mState = InternalHeader::State::Recycled;
225 #endif
226     // Reset all header fields, in case this recycled chunk gets read.
227     mInternalHeader.mHeader.Reset();
228   }
229 
230   // Public header, meant to uniquely identify a chunk, it may be shared with
231   // other processes to coordinate global memory handling.
232   struct Header {
HeaderHeader233     explicit Header(Length aBufferBytes) : mBufferBytes(aBufferBytes) {}
234 
235     // Reset all members to their as-new values (apart from the buffer size,
236     // which cannot change), ready for re-use.
ResetHeader237     void Reset() {
238       mOffsetFirstBlock = 0;
239       mOffsetPastLastBlock = 0;
240       mDoneTimeStamp = TimeStamp{};
241       mBlockCount = 0;
242       mRangeStart = 0;
243       mProcessId = 0;
244     }
245 
246     // Note: Part of the ordering of members below is to avoid unnecessary
247     // padding.
248 
249     // Members managed by the ProfileBufferChunk.
250 
251     // Offset of the first block (past the initial tail block, which may be 0).
252     Length mOffsetFirstBlock = 0;
253     // Offset past the last byte of the last reserved block
254     // It may be past mBufferBytes when last block continues in the next
255     // ProfileBufferChunk. It may be before mBufferBytes if ProfileBufferChunk
256     // is marked "Done" before the end is reached.
257     Length mOffsetPastLastBlock = 0;
258     // Timestamp when buffer is "Done" (which happens when the last block is
259     // written). This will be used to find and discard the oldest
260     // ProfileBufferChunk.
261     TimeStamp mDoneTimeStamp;
262     // Number of bytes in the buffer, set once at construction time.
263     const Length mBufferBytes;
264     // Number of reserved blocks (including final one even if partial, but
265     // excluding initial tail).
266     Length mBlockCount = 0;
267 
268     // Meta-data set by the user.
269 
270     // Index of the first byte of this ProfileBufferChunk, relative to all
271     // Chunks for this process. Index 0 is reserved as nullptr-like index,
272     // mRangeStart should be set to a non-0 value before the first `Reserve()`.
273     ProfileBufferIndex mRangeStart = 0;
274     // Process writing to this ProfileBufferChunk.
275     int mProcessId = 0;
276 
277     // A bit of spare space (necessary here because of the alignment due to
278     // other members), may be later repurposed for extra data.
279     const int mPADDING = 0;
280   };
281 
ChunkHeader()282   [[nodiscard]] const Header& ChunkHeader() const {
283     return mInternalHeader.mHeader;
284   }
285 
BufferBytes()286   [[nodiscard]] Length BufferBytes() const {
287     return ChunkHeader().mBufferBytes;
288   }
289 
290   // Total size of the chunk (buffer + header).
ChunkBytes()291   [[nodiscard]] Length ChunkBytes() const {
292     return static_cast<Length>(sizeof(InternalHeader)) + BufferBytes();
293   }
294 
295   // Size of external resources, in this case all the following chunks.
SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)296   [[nodiscard]] size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
297     const ProfileBufferChunk* const next = GetNext();
298     return next ? next->SizeOfIncludingThis(aMallocSizeOf) : 0;
299   }
300 
301   // Size of this chunk and all following ones.
SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)302   [[nodiscard]] size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
303     // Just in case `aMallocSizeOf` falls back on just `sizeof`, make sure we
304     // account for at least the actual Chunk requested allocation size.
305     return std::max<size_t>(aMallocSizeOf(this), ChunkBytes()) +
306            SizeOfExcludingThis(aMallocSizeOf);
307   }
308 
RemainingBytes()309   [[nodiscard]] Length RemainingBytes() const {
310     return BufferBytes() - OffsetPastLastBlock();
311   }
312 
OffsetFirstBlock()313   [[nodiscard]] Length OffsetFirstBlock() const {
314     return ChunkHeader().mOffsetFirstBlock;
315   }
316 
OffsetPastLastBlock()317   [[nodiscard]] Length OffsetPastLastBlock() const {
318     return ChunkHeader().mOffsetPastLastBlock;
319   }
320 
BlockCount()321   [[nodiscard]] Length BlockCount() const { return ChunkHeader().mBlockCount; }
322 
ProcessId()323   [[nodiscard]] int ProcessId() const { return ChunkHeader().mProcessId; }
324 
SetProcessId(int aProcessId)325   void SetProcessId(int aProcessId) {
326     mInternalHeader.mHeader.mProcessId = aProcessId;
327   }
328 
329   // Global range index at the start of this Chunk.
RangeStart()330   [[nodiscard]] ProfileBufferIndex RangeStart() const {
331     return ChunkHeader().mRangeStart;
332   }
333 
SetRangeStart(ProfileBufferIndex aRangeStart)334   void SetRangeStart(ProfileBufferIndex aRangeStart) {
335     mInternalHeader.mHeader.mRangeStart = aRangeStart;
336   }
337 
338   // Get a read-only Span to the buffer. It is up to the caller to decypher the
339   // contents, based on known offsets and the internal block structure.
BufferSpan()340   [[nodiscard]] Span<const Byte> BufferSpan() const {
341     return Span<const Byte>(&mBuffer, BufferBytes());
342   }
343 
ByteAt(Length aOffset)344   [[nodiscard]] Byte ByteAt(Length aOffset) const {
345     MOZ_ASSERT(aOffset < OffsetPastLastBlock());
346     return *(&mBuffer + aOffset);
347   }
348 
GetNext()349   [[nodiscard]] ProfileBufferChunk* GetNext() {
350     return mInternalHeader.mNext.get();
351   }
GetNext()352   [[nodiscard]] const ProfileBufferChunk* GetNext() const {
353     return mInternalHeader.mNext.get();
354   }
355 
ReleaseNext()356   [[nodiscard]] UniquePtr<ProfileBufferChunk> ReleaseNext() {
357     return std::move(mInternalHeader.mNext);
358   }
359 
InsertNext(UniquePtr<ProfileBufferChunk> && aChunk)360   void InsertNext(UniquePtr<ProfileBufferChunk>&& aChunk) {
361     if (!aChunk) {
362       return;
363     }
364     aChunk->SetLast(ReleaseNext());
365     mInternalHeader.mNext = std::move(aChunk);
366   }
367 
368   // Find the last chunk in this chain (it may be `this`).
Last()369   [[nodiscard]] ProfileBufferChunk* Last() {
370     ProfileBufferChunk* chunk = this;
371     for (;;) {
372       ProfileBufferChunk* next = chunk->GetNext();
373       if (!next) {
374         return chunk;
375       }
376       chunk = next;
377     }
378   }
Last()379   [[nodiscard]] const ProfileBufferChunk* Last() const {
380     const ProfileBufferChunk* chunk = this;
381     for (;;) {
382       const ProfileBufferChunk* next = chunk->GetNext();
383       if (!next) {
384         return chunk;
385       }
386       chunk = next;
387     }
388   }
389 
SetLast(UniquePtr<ProfileBufferChunk> && aChunk)390   void SetLast(UniquePtr<ProfileBufferChunk>&& aChunk) {
391     if (!aChunk) {
392       return;
393     }
394     Last()->mInternalHeader.mNext = std::move(aChunk);
395   }
396 
397   // Join two possibly-null chunk lists.
Join(UniquePtr<ProfileBufferChunk> && aFirst,UniquePtr<ProfileBufferChunk> && aLast)398   [[nodiscard]] static UniquePtr<ProfileBufferChunk> Join(
399       UniquePtr<ProfileBufferChunk>&& aFirst,
400       UniquePtr<ProfileBufferChunk>&& aLast) {
401     if (aFirst) {
402       aFirst->SetLast(std::move(aLast));
403       return std::move(aFirst);
404     }
405     return std::move(aLast);
406   }
407 
408 #ifdef DEBUG
409   void Dump(std::FILE* aFile = stdout) const {
410     fprintf(aFile,
411             "Chunk[%p] chunkSize=%u bufferSize=%u state=%s rangeStart=%u "
412             "firstBlockOffset=%u offsetPastLastBlock=%u blockCount=%u",
413             this, unsigned(ChunkBytes()), unsigned(BufferBytes()),
414             mInternalHeader.StateString(), unsigned(RangeStart()),
415             unsigned(OffsetFirstBlock()), unsigned(OffsetPastLastBlock()),
416             unsigned(BlockCount()));
417     const auto len = OffsetPastLastBlock();
418     constexpr unsigned columns = 16;
419     unsigned char ascii[columns + 1];
420     ascii[columns] = '\0';
421     for (Length i = 0; i < len; ++i) {
422       if (i % columns == 0) {
423         fprintf(aFile, "\n  %4u=0x%03x:", unsigned(i), unsigned(i));
424         for (unsigned a = 0; a < columns; ++a) {
425           ascii[a] = ' ';
426         }
427       }
428       unsigned char sep = ' ';
429       if (i == OffsetFirstBlock()) {
430         if (i == OffsetPastLastBlock()) {
431           sep = '#';
432         } else {
433           sep = '[';
434         }
435       } else if (i == OffsetPastLastBlock()) {
436         sep = ']';
437       }
438       unsigned char c = *(&mBuffer + i);
439       fprintf(aFile, "%c%02x", sep, c);
440 
441       if (i == len - 1) {
442         if (i + 1 == OffsetPastLastBlock()) {
443           // Special case when last block ends right at the end.
444           fprintf(aFile, "]");
445         } else {
446           fprintf(aFile, " ");
447         }
448       } else if (i % columns == columns - 1) {
449         fprintf(aFile, " ");
450       }
451 
452       ascii[i % columns] = (c >= ' ' && c <= '~') ? c : '.';
453 
454       if (i % columns == columns - 1) {
455         fprintf(aFile, " %s", ascii);
456       }
457     }
458 
459     if (len % columns < columns - 1) {
460       for (Length i = len % columns; i < columns; ++i) {
461         fprintf(aFile, "   ");
462       }
463       fprintf(aFile, " %s", ascii);
464     }
465 
466     fprintf(aFile, "\n");
467   }
468 #endif  // DEBUG
469 
470  private:
471   // ProfileBufferChunk constructor. Use static `Create()` to allocate and
472   // construct a ProfileBufferChunk.
ProfileBufferChunk(Length aBufferBytes)473   explicit ProfileBufferChunk(Length aBufferBytes)
474       : mInternalHeader(aBufferBytes) {}
475 
476   // This internal header starts with the public `Header`, and adds some data
477   // only necessary for local handling.
478   // This encapsulation is also necessary to perform placement-new in
479   // `Create()`.
480   struct InternalHeader {
InternalHeaderInternalHeader481     explicit InternalHeader(Length aBufferBytes) : mHeader(aBufferBytes) {}
482 
483     Header mHeader;
484     UniquePtr<ProfileBufferChunk> mNext;
485 
486 #ifdef DEBUG
487     enum class State {
488       Created,  // Self-set. Just constructed, waiting for initial block tail.
489       InUse,    // Ready to accept blocks.
490       Full,     // Self-set. Blocks reach the end (or further).
491       Done,     // Blocks won't be added anymore.
492       Recycled  // Still full of data, but expecting an initial block tail.
493     };
494 
495     State mState = State::Created;
496     // Transition table: (X=unexpected)
497     // Method          \  State   Created  InUse    Full     Done     Recycled
498     // ReserveInitialBlockAsTail   InUse     X       X        X        InUse
499     // Reserve                       X   InUse/Full  X        X          X
500     // MarkDone                      X     Done     Done      X          X
501     // MarkRecycled                  X       X       X      Recycled     X
502     // destructor                    ok      X       X        ok         ok
503 
StateStringInternalHeader504     const char* StateString() const {
505       switch (mState) {
506         case State::Created:
507           return "Created";
508         case State::InUse:
509           return "InUse";
510         case State::Full:
511           return "Full";
512         case State::Done:
513           return "Done";
514         case State::Recycled:
515           return "Recycled";
516         default:
517           return "?";
518       }
519     }
520 #else  // DEBUG
StateStringInternalHeader521     const char* StateString() const { return "(non-DEBUG)"; }
522 #endif
523   };
524 
525   InternalHeader mInternalHeader;
526 
527   // KEEP THIS LAST!
528   // First byte of the buffer. Note that ProfileBufferChunk::Create allocates a
529   // bigger block, such that `mBuffer` is the first of `mBufferBytes` available
530   // bytes.
531   // The initialization is not strictly needed, because bytes should only be
532   // read after they have been written and `mOffsetPastLastBlock` has been
533   // updated. However:
534   // - Reviewbot complains that it's not initialized.
535   // - It's cheap to initialize one byte.
536   // - In the worst case (reading does happen), zero is not a valid entry size
537   //   and should get caught in entry readers.
538   Byte mBuffer = '\0';
539 };
540 
541 }  // namespace mozilla
542 
543 #endif  // ProfileBufferChunk_h
544