Source code

Revision control

Other Tools

/* -*- 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/. */
#include "BaseProfiler.h"
#include "mozilla/Attributes.h"
#include "mozilla/BaseProfileJSONWriter.h"
#ifdef MOZ_GECKO_PROFILER
# include "mozilla/BaseProfilerMarkerTypes.h"
# include "mozilla/BlocksRingBuffer.h"
# include "mozilla/leb128iterator.h"
# include "mozilla/ModuloBuffer.h"
# include "mozilla/mozalloc.h"
# include "mozilla/PowerOfTwo.h"
# include "mozilla/ProfileBufferChunk.h"
# include "mozilla/ProfileBufferChunkManagerSingle.h"
# include "mozilla/ProfileBufferChunkManagerWithLocalLimit.h"
# include "mozilla/ProfileBufferControlledChunkManager.h"
# include "mozilla/ProfileChunkedBuffer.h"
# include "mozilla/Vector.h"
#endif // MOZ_GECKO_PROFILER
#if defined(_MSC_VER) || defined(__MINGW32__)
# include <windows.h>
# include <mmsystem.h>
# include <process.h>
#else
# include <errno.h>
# include <string.h>
# include <time.h>
# include <unistd.h>
#endif
#include <algorithm>
#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <type_traits>
#include <utility>
void TestProfilerUtils() {
printf("TestProfilerUtils...\n");
{
using mozilla::baseprofiler::BaseProfilerProcessId;
using Number = BaseProfilerProcessId::NumberType;
static constexpr Number scMaxNumber = std::numeric_limits<Number>::max();
static_assert(
BaseProfilerProcessId{}.ToNumber() == 0,
"These tests assume that the unspecified process id number is 0; "
"if this fails, please update these tests accordingly");
static_assert(!BaseProfilerProcessId{}.IsSpecified());
static_assert(!BaseProfilerProcessId::FromNumber(0).IsSpecified());
static_assert(BaseProfilerProcessId::FromNumber(1).IsSpecified());
static_assert(BaseProfilerProcessId::FromNumber(123).IsSpecified());
static_assert(BaseProfilerProcessId::FromNumber(scMaxNumber).IsSpecified());
static_assert(BaseProfilerProcessId::FromNumber(Number(1)).ToNumber() ==
Number(1));
static_assert(BaseProfilerProcessId::FromNumber(Number(123)).ToNumber() ==
Number(123));
static_assert(BaseProfilerProcessId::FromNumber(scMaxNumber).ToNumber() ==
scMaxNumber);
static_assert(BaseProfilerProcessId{} == BaseProfilerProcessId{});
static_assert(BaseProfilerProcessId::FromNumber(Number(123)) ==
BaseProfilerProcessId::FromNumber(Number(123)));
static_assert(BaseProfilerProcessId{} !=
BaseProfilerProcessId::FromNumber(Number(123)));
static_assert(BaseProfilerProcessId::FromNumber(Number(123)) !=
BaseProfilerProcessId{});
static_assert(BaseProfilerProcessId::FromNumber(Number(123)) !=
BaseProfilerProcessId::FromNumber(scMaxNumber));
static_assert(BaseProfilerProcessId::FromNumber(scMaxNumber) !=
BaseProfilerProcessId::FromNumber(Number(123)));
// Verify trivial-copyability by memcpy'ing to&from same-size storage.
static_assert(std::is_trivially_copyable_v<BaseProfilerProcessId>);
BaseProfilerProcessId pid;
MOZ_RELEASE_ASSERT(!pid.IsSpecified());
Number pidStorage;
static_assert(sizeof(pidStorage) == sizeof(pid));
// Copy from BaseProfilerProcessId to storage. Note: We cannot assume that
// this is equal to what ToNumber() gives us. All we can do is verify that
// copying from storage back to BaseProfilerProcessId works as expected.
std::memcpy(&pidStorage, &pid, sizeof(pidStorage));
BaseProfilerProcessId pid2 = BaseProfilerProcessId::FromNumber(2);
MOZ_RELEASE_ASSERT(pid2.IsSpecified());
std::memcpy(&pid2, &pidStorage, sizeof(pid));
MOZ_RELEASE_ASSERT(!pid2.IsSpecified());
pid = BaseProfilerProcessId::FromNumber(123);
std::memcpy(&pidStorage, &pid, sizeof(pidStorage));
pid2 = BaseProfilerProcessId{};
MOZ_RELEASE_ASSERT(!pid2.IsSpecified());
std::memcpy(&pid2, &pidStorage, sizeof(pid));
MOZ_RELEASE_ASSERT(pid2.IsSpecified());
MOZ_RELEASE_ASSERT(pid2.ToNumber() == 123);
// No conversions to/from numbers.
static_assert(!std::is_constructible_v<BaseProfilerProcessId, Number>);
static_assert(!std::is_assignable_v<BaseProfilerProcessId, Number>);
static_assert(!std::is_constructible_v<Number, BaseProfilerProcessId>);
static_assert(!std::is_assignable_v<Number, BaseProfilerProcessId>);
static_assert(
std::is_same_v<
decltype(mozilla::baseprofiler::profiler_current_process_id()),
BaseProfilerProcessId>);
MOZ_RELEASE_ASSERT(
mozilla::baseprofiler::profiler_current_process_id().IsSpecified());
}
{
mozilla::baseprofiler::profiler_init_main_thread_id();
using mozilla::baseprofiler::BaseProfilerThreadId;
using Number = BaseProfilerThreadId::NumberType;
static constexpr Number scMaxNumber = std::numeric_limits<Number>::max();
static_assert(
BaseProfilerThreadId{}.ToNumber() == 0,
"These tests assume that the unspecified thread id number is 0; "
"if this fails, please update these tests accordingly");
static_assert(!BaseProfilerThreadId{}.IsSpecified());
static_assert(!BaseProfilerThreadId::FromNumber(0).IsSpecified());
static_assert(BaseProfilerThreadId::FromNumber(1).IsSpecified());
static_assert(BaseProfilerThreadId::FromNumber(123).IsSpecified());
static_assert(BaseProfilerThreadId::FromNumber(scMaxNumber).IsSpecified());
static_assert(BaseProfilerThreadId::FromNumber(Number(1)).ToNumber() ==
Number(1));
static_assert(BaseProfilerThreadId::FromNumber(Number(123)).ToNumber() ==
Number(123));
static_assert(BaseProfilerThreadId::FromNumber(scMaxNumber).ToNumber() ==
scMaxNumber);
static_assert(BaseProfilerThreadId{} == BaseProfilerThreadId{});
static_assert(BaseProfilerThreadId::FromNumber(Number(123)) ==
BaseProfilerThreadId::FromNumber(Number(123)));
static_assert(BaseProfilerThreadId{} !=
BaseProfilerThreadId::FromNumber(Number(123)));
static_assert(BaseProfilerThreadId::FromNumber(Number(123)) !=
BaseProfilerThreadId{});
static_assert(BaseProfilerThreadId::FromNumber(Number(123)) !=
BaseProfilerThreadId::FromNumber(scMaxNumber));
static_assert(BaseProfilerThreadId::FromNumber(scMaxNumber) !=
BaseProfilerThreadId::FromNumber(Number(123)));
// Verify trivial-copyability by memcpy'ing to&from same-size storage.
static_assert(std::is_trivially_copyable_v<BaseProfilerThreadId>);
BaseProfilerThreadId tid;
MOZ_RELEASE_ASSERT(!tid.IsSpecified());
Number tidStorage;
static_assert(sizeof(tidStorage) == sizeof(tid));
// Copy from BaseProfilerThreadId to storage. Note: We cannot assume that
// this is equal to what ToNumber() gives us. All we can do is verify that
// copying from storage back to BaseProfilerThreadId works as expected.
std::memcpy(&tidStorage, &tid, sizeof(tidStorage));
BaseProfilerThreadId tid2 = BaseProfilerThreadId::FromNumber(2);
MOZ_RELEASE_ASSERT(tid2.IsSpecified());
std::memcpy(&tid2, &tidStorage, sizeof(tid));
MOZ_RELEASE_ASSERT(!tid2.IsSpecified());
tid = BaseProfilerThreadId::FromNumber(Number(123));
std::memcpy(&tidStorage, &tid, sizeof(tidStorage));
tid2 = BaseProfilerThreadId{};
MOZ_RELEASE_ASSERT(!tid2.IsSpecified());
std::memcpy(&tid2, &tidStorage, sizeof(tid));
MOZ_RELEASE_ASSERT(tid2.IsSpecified());
MOZ_RELEASE_ASSERT(tid2.ToNumber() == Number(123));
// No conversions to/from numbers.
static_assert(!std::is_constructible_v<BaseProfilerThreadId, Number>);
static_assert(!std::is_assignable_v<BaseProfilerThreadId, Number>);
static_assert(!std::is_constructible_v<Number, BaseProfilerThreadId>);
static_assert(!std::is_assignable_v<Number, BaseProfilerThreadId>);
static_assert(std::is_same_v<
decltype(mozilla::baseprofiler::profiler_current_thread_id()),
BaseProfilerThreadId>);
BaseProfilerThreadId mainTestThreadId =
mozilla::baseprofiler::profiler_current_thread_id();
MOZ_RELEASE_ASSERT(mainTestThreadId.IsSpecified());
BaseProfilerThreadId mainThreadId =
mozilla::baseprofiler::profiler_main_thread_id();
MOZ_RELEASE_ASSERT(mainThreadId.IsSpecified());
MOZ_RELEASE_ASSERT(mainThreadId == mainTestThreadId,
"Test should run on the main thread");
MOZ_RELEASE_ASSERT(mozilla::baseprofiler::profiler_is_main_thread());
std::thread testThread([&]() {
const BaseProfilerThreadId testThreadId =
mozilla::baseprofiler::profiler_current_thread_id();
MOZ_RELEASE_ASSERT(testThreadId.IsSpecified());
MOZ_RELEASE_ASSERT(testThreadId != mainThreadId);
MOZ_RELEASE_ASSERT(!mozilla::baseprofiler::profiler_is_main_thread());
});
testThread.join();
}
// No conversions between processes and threads.
static_assert(
!std::is_constructible_v<mozilla::baseprofiler::BaseProfilerThreadId,
mozilla::baseprofiler::BaseProfilerProcessId>);
static_assert(
!std::is_assignable_v<mozilla::baseprofiler::BaseProfilerThreadId,
mozilla::baseprofiler::BaseProfilerProcessId>);
static_assert(
!std::is_constructible_v<mozilla::baseprofiler::BaseProfilerProcessId,
mozilla::baseprofiler::BaseProfilerThreadId>);
static_assert(
!std::is_assignable_v<mozilla::baseprofiler::BaseProfilerProcessId,
mozilla::baseprofiler::BaseProfilerThreadId>);
printf("TestProfilerUtils done\n");
}
#ifdef MOZ_GECKO_PROFILER
MOZ_MAYBE_UNUSED static void SleepMilli(unsigned aMilliseconds) {
# if defined(_MSC_VER) || defined(__MINGW32__)
Sleep(aMilliseconds);
# else
struct timespec ts = {/* .tv_sec */ static_cast<time_t>(aMilliseconds / 1000),
/* ts.tv_nsec */ long(aMilliseconds % 1000) * 1000000};
struct timespec tr = {0, 0};
while (nanosleep(&ts, &tr)) {
if (errno == EINTR) {
ts = tr;
} else {
printf("nanosleep() -> %s\n", strerror(errno));
exit(1);
}
}
# endif
}
MOZ_MAYBE_UNUSED static void WaitUntilTimeStampChanges(
const mozilla::TimeStamp& aTimeStampToCompare = mozilla::TimeStamp::Now()) {
while (aTimeStampToCompare == mozilla::TimeStamp::Now()) {
SleepMilli(1);
}
}
using namespace mozilla;
void TestPowerOfTwoMask() {
printf("TestPowerOfTwoMask...\n");
static_assert(MakePowerOfTwoMask<uint32_t, 0>().MaskValue() == 0);
constexpr PowerOfTwoMask<uint32_t> c0 = MakePowerOfTwoMask<uint32_t, 0>();
MOZ_RELEASE_ASSERT(c0.MaskValue() == 0);
static_assert(MakePowerOfTwoMask<uint32_t, 0xFFu>().MaskValue() == 0xFFu);
constexpr PowerOfTwoMask<uint32_t> cFF =
MakePowerOfTwoMask<uint32_t, 0xFFu>();
MOZ_RELEASE_ASSERT(cFF.MaskValue() == 0xFFu);
static_assert(MakePowerOfTwoMask<uint32_t, 0xFFFFFFFFu>().MaskValue() ==
0xFFFFFFFFu);
constexpr PowerOfTwoMask<uint32_t> cFFFFFFFF =
MakePowerOfTwoMask<uint32_t, 0xFFFFFFFFu>();
MOZ_RELEASE_ASSERT(cFFFFFFFF.MaskValue() == 0xFFFFFFFFu);
struct TestDataU32 {
uint32_t mInput;
uint32_t mMask;
};
// clang-format off
TestDataU32 tests[] = {
{ 0, 0 },
{ 1, 1 },
{ 2, 3 },
{ 3, 3 },
{ 4, 7 },
{ 5, 7 },
{ (1u << 31) - 1, (1u << 31) - 1 },
{ (1u << 31), uint32_t(-1) },
{ (1u << 31) + 1, uint32_t(-1) },
{ uint32_t(-1), uint32_t(-1) }
};
// clang-format on
for (const TestDataU32& test : tests) {
PowerOfTwoMask<uint32_t> p2m(test.mInput);
MOZ_RELEASE_ASSERT(p2m.MaskValue() == test.mMask);
for (const TestDataU32& inner : tests) {
if (p2m.MaskValue() != uint32_t(-1)) {
MOZ_RELEASE_ASSERT((inner.mInput % p2m) ==
(inner.mInput % (p2m.MaskValue() + 1)));
}
MOZ_RELEASE_ASSERT((inner.mInput & p2m) == (inner.mInput % p2m));
MOZ_RELEASE_ASSERT((p2m & inner.mInput) == (inner.mInput & p2m));
}
}
printf("TestPowerOfTwoMask done\n");
}
void TestPowerOfTwo() {
printf("TestPowerOfTwo...\n");
static_assert(MakePowerOfTwo<uint32_t, 1>().Value() == 1);
constexpr PowerOfTwo<uint32_t> c1 = MakePowerOfTwo<uint32_t, 1>();
MOZ_RELEASE_ASSERT(c1.Value() == 1);
static_assert(MakePowerOfTwo<uint32_t, 1>().Mask().MaskValue() == 0);
static_assert(MakePowerOfTwo<uint32_t, 128>().Value() == 128);
constexpr PowerOfTwo<uint32_t> c128 = MakePowerOfTwo<uint32_t, 128>();
MOZ_RELEASE_ASSERT(c128.Value() == 128);
static_assert(MakePowerOfTwo<uint32_t, 128>().Mask().MaskValue() == 127);
static_assert(MakePowerOfTwo<uint32_t, 0x80000000u>().Value() == 0x80000000u);
constexpr PowerOfTwo<uint32_t> cMax = MakePowerOfTwo<uint32_t, 0x80000000u>();
MOZ_RELEASE_ASSERT(cMax.Value() == 0x80000000u);
static_assert(MakePowerOfTwo<uint32_t, 0x80000000u>().Mask().MaskValue() ==
0x7FFFFFFFu);
struct TestDataU32 {
uint32_t mInput;
uint32_t mValue;
uint32_t mMask;
};
// clang-format off
TestDataU32 tests[] = {
{ 0, 1, 0 },
{ 1, 1, 0 },
{ 2, 2, 1 },
{ 3, 4, 3 },
{ 4, 4, 3 },
{ 5, 8, 7 },
{ (1u << 31) - 1, (1u << 31), (1u << 31) - 1 },
{ (1u << 31), (1u << 31), (1u << 31) - 1 },
{ (1u << 31) + 1, (1u << 31), (1u << 31) - 1 },
{ uint32_t(-1), (1u << 31), (1u << 31) - 1 }
};
// clang-format on
for (const TestDataU32& test : tests) {
PowerOfTwo<uint32_t> p2(test.mInput);
MOZ_RELEASE_ASSERT(p2.Value() == test.mValue);
MOZ_RELEASE_ASSERT(p2.MaskValue() == test.mMask);
PowerOfTwoMask<uint32_t> p2m = p2.Mask();
MOZ_RELEASE_ASSERT(p2m.MaskValue() == test.mMask);
for (const TestDataU32& inner : tests) {
MOZ_RELEASE_ASSERT((inner.mInput % p2) == (inner.mInput % p2.Value()));
}
}
printf("TestPowerOfTwo done\n");
}
void TestLEB128() {
printf("TestLEB128...\n");
MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint8_t>() == 2);
MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint16_t>() == 3);
MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint32_t>() == 5);
MOZ_RELEASE_ASSERT(ULEB128MaxSize<uint64_t>() == 10);
struct TestDataU64 {
uint64_t mValue;
unsigned mSize;
const char* mBytes;
};
// clang-format off
TestDataU64 tests[] = {
// Small numbers should keep their normal byte representation.
{ 0u, 1, "\0" },
{ 1u, 1, "\x01" },
// 0111 1111 (127, or 0x7F) is the highest number that fits into a single
// LEB128 byte. It gets encoded as 0111 1111, note the most significant bit
// is off.
{ 0x7Fu, 1, "\x7F" },
// Next number: 128, or 0x80.
// Original data representation: 1000 0000
// Broken up into groups of 7: 1 0000000
// Padded with 0 (msB) or 1 (lsB): 00000001 10000000
// Byte representation: 0x01 0x80
// Little endian order: -> 0x80 0x01
{ 0x80u, 2, "\x80\x01" },
// Next: 129, or 0x81 (showing that we don't lose low bits.)
// Original data representation: 1000 0001
// Broken up into groups of 7: 1 0000001
// Padded with 0 (msB) or 1 (lsB): 00000001 10000001
// Byte representation: 0x01 0x81
// Little endian order: -> 0x81 0x01
{ 0x81u, 2, "\x81\x01" },
// Highest 8-bit number: 255, or 0xFF.
// Original data representation: 1111 1111
// Broken up into groups of 7: 1 1111111
// Padded with 0 (msB) or 1 (lsB): 00000001 11111111
// Byte representation: 0x01 0xFF
// Little endian order: -> 0xFF 0x01
{ 0xFFu, 2, "\xFF\x01" },
// Next: 256, or 0x100.
// Original data representation: 1 0000 0000
// Broken up into groups of 7: 10 0000000
// Padded with 0 (msB) or 1 (lsB): 00000010 10000000
// Byte representation: 0x10 0x80
// Little endian order: -> 0x80 0x02
{ 0x100u, 2, "\x80\x02" },
// Highest 32-bit number: 0xFFFFFFFF (8 bytes, all bits set).
// Original: 1111 1111 1111 1111 1111 1111 1111 1111
// Groups: 1111 1111111 1111111 1111111 1111111
// Padded: 00001111 11111111 11111111 11111111 11111111
// Bytes: 0x0F 0xFF 0xFF 0xFF 0xFF
// Little Endian: -> 0xFF 0xFF 0xFF 0xFF 0x0F
{ 0xFFFFFFFFu, 5, "\xFF\xFF\xFF\xFF\x0F" },
// Highest 64-bit number: 0xFFFFFFFFFFFFFFFF (16 bytes, all bits set).
// 64 bits, that's 9 groups of 7 bits, plus 1 (most significant) bit.
{ 0xFFFFFFFFFFFFFFFFu, 10, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01" }
};
// clang-format on
for (const TestDataU64& test : tests) {
MOZ_RELEASE_ASSERT(ULEB128Size(test.mValue) == test.mSize);
// Prepare a buffer that can accomodate the largest-possible LEB128.
uint8_t buffer[ULEB128MaxSize<uint64_t>()];
// Use a pointer into the buffer as iterator.
uint8_t* p = buffer;
// And write the LEB128.
WriteULEB128(test.mValue, p);
// Pointer (iterator) should have advanced just past the expected LEB128
// size.
MOZ_RELEASE_ASSERT(p == buffer + test.mSize);
// Check expected bytes.
for (unsigned i = 0; i < test.mSize; ++i) {
MOZ_RELEASE_ASSERT(buffer[i] == uint8_t(test.mBytes[i]));
}
// Move pointer (iterator) back to start of buffer.
p = buffer;
// And read the LEB128 we wrote above.
uint64_t read = ReadULEB128<uint64_t>(p);
// Pointer (iterator) should have also advanced just past the expected
// LEB128 size.
MOZ_RELEASE_ASSERT(p == buffer + test.mSize);
// And check the read value.
MOZ_RELEASE_ASSERT(read == test.mValue);
// Testing ULEB128 reader.
ULEB128Reader<uint64_t> reader;
MOZ_RELEASE_ASSERT(!reader.IsComplete());
// Move pointer back to start of buffer.
p = buffer;
for (;;) {
// Read a byte and feed it to the reader.
if (reader.FeedByteIsComplete(*p++)) {
break;
}
// Not complete yet, we shouldn't have reached the end pointer.
MOZ_RELEASE_ASSERT(!reader.IsComplete());
MOZ_RELEASE_ASSERT(p < buffer + test.mSize);
}
MOZ_RELEASE_ASSERT(reader.IsComplete());
// Pointer should have advanced just past the expected LEB128 size.
MOZ_RELEASE_ASSERT(p == buffer + test.mSize);
// And check the read value.
MOZ_RELEASE_ASSERT(reader.Value() == test.mValue);
// And again after a Reset.
reader.Reset();
MOZ_RELEASE_ASSERT(!reader.IsComplete());
p = buffer;
for (;;) {
if (reader.FeedByteIsComplete(*p++)) {
break;
}
MOZ_RELEASE_ASSERT(!reader.IsComplete());
MOZ_RELEASE_ASSERT(p < buffer + test.mSize);
}
MOZ_RELEASE_ASSERT(reader.IsComplete());
MOZ_RELEASE_ASSERT(p == buffer + test.mSize);
MOZ_RELEASE_ASSERT(reader.Value() == test.mValue);
}
printf("TestLEB128 done\n");
}
struct StringWriteFunc : public JSONWriteFunc {
std::string mString;
void Write(const mozilla::Span<const char>& aStr) override {
mString.append(aStr.data(), aStr.size());
}
};
void CheckJSON(mozilla::baseprofiler::SpliceableJSONWriter& aWriter,
const char* aExpected, int aLine) {
const std::string& actual =
static_cast<StringWriteFunc*>(aWriter.WriteFunc())->mString;
if (strcmp(aExpected, actual.c_str()) != 0) {
fprintf(stderr,
"---- EXPECTED ---- (line %d)\n<<<%s>>>\n"
"---- ACTUAL ----\n<<<%s>>>\n",
aLine, aExpected, actual.c_str());
MOZ_RELEASE_ASSERT(false, "expected and actual output don't match");
}
}
void TestJSONTimeOutput() {
printf("TestJSONTimeOutput...\n");
# define TEST(in, out) \
do { \
mozilla::baseprofiler::SpliceableJSONWriter writer( \
mozilla::MakeUnique<StringWriteFunc>()); \
writer.Start(mozilla::JSONWriter::SingleLineStyle); \
writer.TimeDoubleMsProperty("time_ms", (in)); \
writer.End(); \
CheckJSON(writer, "{\"time_ms\": " out "}\n", __LINE__); \
} while (false);
TEST(0, "0");
TEST(0.000'000'1, "0");
TEST(0.000'000'4, "0");
TEST(0.000'000'499, "0");
TEST(0.000'000'5, "0.000001");
TEST(0.000'001, "0.000001");
TEST(0.000'01, "0.00001");
TEST(0.000'1, "0.0001");
TEST(0.001, "0.001");
TEST(0.01, "0.01");
TEST(0.1, "0.1");
TEST(1, "1");
TEST(2, "2");
TEST(10, "10");
TEST(100, "100");
TEST(1'000, "1000");
TEST(10'000, "10000");
TEST(100'000, "100000");
TEST(1'000'000, "1000000");
// 2^53-2 ns in ms. 2^53-1 is the highest integer value representable in
// double, -1 again because we're adding 0.5 before truncating.
// That's 104 days, after which the nanosecond precision would decrease.
TEST(9'007'199'254.740'990, "9007199254.74099");
TEST(-0.000'000'1, "0");
TEST(-0.000'000'4, "0");
TEST(-0.000'000'499, "0");
TEST(-0.000'000'5, "-0.000001");
TEST(-0.000'001, "-0.000001");
TEST(-0.000'01, "-0.00001");
TEST(-0.000'1, "-0.0001");
TEST(-0.001, "-0.001");
TEST(-0.01, "-0.01");
TEST(-0.1, "-0.1");
TEST(-1, "-1");
TEST(-2, "-2");
TEST(-10, "-10");
TEST(-100, "-100");
TEST(-1'000, "-1000");
TEST(-10'000, "-10000");
TEST(-100'000, "-100000");
TEST(-1'000'000, "-1000000");
TEST(-9'007'199'254.740'990, "-9007199254.74099");
# undef TEST
printf("TestJSONTimeOutput done\n");
}
template <uint8_t byte, uint8_t... tail>
constexpr bool TestConstexprULEB128Reader(ULEB128Reader<uint64_t>& aReader) {
if (aReader.IsComplete()) {
return false;
}
const bool isComplete = aReader.FeedByteIsComplete(byte);
if (aReader.IsComplete() != isComplete) {
return false;
}
if constexpr (sizeof...(tail) == 0) {
return isComplete;
} else {
if (isComplete) {
return false;
}
return TestConstexprULEB128Reader<tail...>(aReader);
}
}
template <uint64_t expected, uint8_t... bytes>
constexpr bool TestConstexprULEB128Reader() {
ULEB128Reader<uint64_t> reader;
if (!TestConstexprULEB128Reader<bytes...>(reader)) {
return false;
}
if (!reader.IsComplete()) {
return false;
}
if (reader.Value() != expected) {
return false;
}
reader.Reset();
if (!TestConstexprULEB128Reader<bytes...>(reader)) {
return false;
}
if (!reader.IsComplete()) {
return false;
}
if (reader.Value() != expected) {
return false;
}
return true;
}
static_assert(TestConstexprULEB128Reader<0x0u, 0x0u>());
static_assert(!TestConstexprULEB128Reader<0x0u, 0x0u, 0x0u>());
static_assert(TestConstexprULEB128Reader<0x1u, 0x1u>());
static_assert(TestConstexprULEB128Reader<0x7Fu, 0x7Fu>());
static_assert(TestConstexprULEB128Reader<0x80u, 0x80u, 0x01u>());
static_assert(!TestConstexprULEB128Reader<0x80u, 0x80u>());
static_assert(!TestConstexprULEB128Reader<0x80u, 0x01u>());
static_assert(TestConstexprULEB128Reader<0x81u, 0x81u, 0x01u>());
static_assert(TestConstexprULEB128Reader<0xFFu, 0xFFu, 0x01u>());
static_assert(TestConstexprULEB128Reader<0x100u, 0x80u, 0x02u>());
static_assert(TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu,
0xFFu, 0x0Fu>());
static_assert(
!TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu>());
static_assert(!TestConstexprULEB128Reader<0xFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu,
0xFFu, 0xFFu, 0x0Fu>());
static_assert(
TestConstexprULEB128Reader<0xFFFFFFFFFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu,
0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0x01u>());
static_assert(
!TestConstexprULEB128Reader<0xFFFFFFFFFFFFFFFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu,
0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu>());
static void TestChunk() {
printf("TestChunk...\n");
static_assert(!std::is_default_constructible_v<ProfileBufferChunk>,
"ProfileBufferChunk should not be default-constructible");
static_assert(
!std::is_constructible_v<ProfileBufferChunk, ProfileBufferChunk::Length>,
"ProfileBufferChunk should not be constructible from Length");
static_assert(
sizeof(ProfileBufferChunk::Header) ==
sizeof(ProfileBufferChunk::Header::mOffsetFirstBlock) +
sizeof(ProfileBufferChunk::Header::mOffsetPastLastBlock) +
sizeof(ProfileBufferChunk::Header::mDoneTimeStamp) +
sizeof(ProfileBufferChunk::Header::mBufferBytes) +
sizeof(ProfileBufferChunk::Header::mBlockCount) +
sizeof(ProfileBufferChunk::Header::mRangeStart) +
sizeof(ProfileBufferChunk::Header::mProcessId) +
sizeof(ProfileBufferChunk::Header::mPADDING),
"ProfileBufferChunk::Header may have unwanted padding, please review");
// Note: The above static_assert is an attempt at keeping
// ProfileBufferChunk::Header tightly packed, but some changes could make this
// impossible to achieve (most probably due to alignment) -- Just do your
// best!
constexpr ProfileBufferChunk::Length TestLen = 1000;
// Basic allocations of different sizes.
for (ProfileBufferChunk::Length len = 0; len <= TestLen; ++len) {
auto chunk = ProfileBufferChunk::Create(len);
static_assert(
std::is_same_v<decltype(chunk), UniquePtr<ProfileBufferChunk>>,
"ProfileBufferChunk::Create() should return a "
"UniquePtr<ProfileBufferChunk>");
MOZ_RELEASE_ASSERT(!!chunk, "OOM!?");
MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= len);
MOZ_RELEASE_ASSERT(chunk->ChunkBytes() >=
len + ProfileBufferChunk::SizeofChunkMetadata());
MOZ_RELEASE_ASSERT(chunk->RemainingBytes() == chunk->BufferBytes());
MOZ_RELEASE_ASSERT(chunk->OffsetFirstBlock() == 0);
MOZ_RELEASE_ASSERT(chunk->OffsetPastLastBlock() == 0);
MOZ_RELEASE_ASSERT(chunk->BlockCount() == 0);
MOZ_RELEASE_ASSERT(chunk->ProcessId() == 0);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0);
MOZ_RELEASE_ASSERT(chunk->BufferSpan().LengthBytes() ==
chunk->BufferBytes());
MOZ_RELEASE_ASSERT(!chunk->GetNext());
MOZ_RELEASE_ASSERT(!chunk->ReleaseNext());
MOZ_RELEASE_ASSERT(chunk->Last() == chunk.get());
}
// Allocate the main test Chunk.
auto chunkA = ProfileBufferChunk::Create(TestLen);
MOZ_RELEASE_ASSERT(!!chunkA, "OOM!?");
MOZ_RELEASE_ASSERT(chunkA->BufferBytes() >= TestLen);
MOZ_RELEASE_ASSERT(chunkA->ChunkBytes() >=
TestLen + ProfileBufferChunk::SizeofChunkMetadata());
MOZ_RELEASE_ASSERT(!chunkA->GetNext());
MOZ_RELEASE_ASSERT(!chunkA->ReleaseNext());
constexpr ProfileBufferIndex chunkARangeStart = 12345;
chunkA->SetRangeStart(chunkARangeStart);
MOZ_RELEASE_ASSERT(chunkA->RangeStart() == chunkARangeStart);
// Get a read-only span over its buffer.
auto bufferA = chunkA->BufferSpan();
static_assert(
std::is_same_v<decltype(bufferA), Span<const ProfileBufferChunk::Byte>>,
"BufferSpan() should return a Span<const Byte>");
MOZ_RELEASE_ASSERT(bufferA.LengthBytes() == chunkA->BufferBytes());
// Add the initial tail block.
constexpr ProfileBufferChunk::Length initTailLen = 10;
auto initTail = chunkA->ReserveInitialBlockAsTail(initTailLen);
static_assert(
std::is_same_v<decltype(initTail), Span<ProfileBufferChunk::Byte>>,
"ReserveInitialBlockAsTail() should return a Span<Byte>");
MOZ_RELEASE_ASSERT(initTail.LengthBytes() == initTailLen);
MOZ_RELEASE_ASSERT(initTail.Elements() == bufferA.Elements());
MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen);
MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == initTailLen);
// Add the first complete block.
constexpr ProfileBufferChunk::Length block1Len = 20;
auto block1 = chunkA->ReserveBlock(block1Len);
static_assert(
std::is_same_v<decltype(block1), ProfileBufferChunk::ReserveReturn>,
"ReserveBlock() should return a ReserveReturn");
MOZ_RELEASE_ASSERT(block1.mBlockRangeIndex.ConvertToProfileBufferIndex() ==
chunkARangeStart + initTailLen);
MOZ_RELEASE_ASSERT(block1.mSpan.LengthBytes() == block1Len);
MOZ_RELEASE_ASSERT(block1.mSpan.Elements() ==
bufferA.Elements() + initTailLen);
MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen);
MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == initTailLen + block1Len);
MOZ_RELEASE_ASSERT(chunkA->RemainingBytes() != 0);
// Add another block to over-fill the ProfileBufferChunk.
const ProfileBufferChunk::Length remaining =
chunkA->BufferBytes() - (initTailLen + block1Len);
constexpr ProfileBufferChunk::Length overfill = 30;
const ProfileBufferChunk::Length block2Len = remaining + overfill;
ProfileBufferChunk::ReserveReturn block2 = chunkA->ReserveBlock(block2Len);
MOZ_RELEASE_ASSERT(block2.mBlockRangeIndex.ConvertToProfileBufferIndex() ==
chunkARangeStart + initTailLen + block1Len);
MOZ_RELEASE_ASSERT(block2.mSpan.LengthBytes() == remaining);
MOZ_RELEASE_ASSERT(block2.mSpan.Elements() ==
bufferA.Elements() + initTailLen + block1Len);
MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == initTailLen);
MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == chunkA->BufferBytes());
MOZ_RELEASE_ASSERT(chunkA->RemainingBytes() == 0);
// Block must be marked "done" before it can be recycled.
chunkA->MarkDone();
// It must be marked "recycled" before data can be added to it again.
chunkA->MarkRecycled();
// Add an empty initial tail block.
Span<ProfileBufferChunk::Byte> initTail2 =
chunkA->ReserveInitialBlockAsTail(0);
MOZ_RELEASE_ASSERT(initTail2.LengthBytes() == 0);
MOZ_RELEASE_ASSERT(initTail2.Elements() == bufferA.Elements());
MOZ_RELEASE_ASSERT(chunkA->OffsetFirstBlock() == 0);
MOZ_RELEASE_ASSERT(chunkA->OffsetPastLastBlock() == 0);
// Block must be marked "done" before it can be destroyed.
chunkA->MarkDone();
chunkA->SetProcessId(123);
MOZ_RELEASE_ASSERT(chunkA->ProcessId() == 123);
printf("TestChunk done\n");
}
static void TestChunkManagerSingle() {
printf("TestChunkManagerSingle...\n");
// Construct a ProfileBufferChunkManagerSingle for one chunk of size >=1000.
constexpr ProfileBufferChunk::Length ChunkMinBufferBytes = 1000;
ProfileBufferChunkManagerSingle cms{ChunkMinBufferBytes};
// Reference to base class, to exercize virtual methods.
ProfileBufferChunkManager& cm = cms;
# ifdef DEBUG
const char* chunkManagerRegisterer = "TestChunkManagerSingle";
cm.RegisteredWith(chunkManagerRegisterer);
# endif // DEBUG
const auto maxTotalSize = cm.MaxTotalSize();
MOZ_RELEASE_ASSERT(maxTotalSize >= ChunkMinBufferBytes);
cm.SetChunkDestroyedCallback([](const ProfileBufferChunk&) {
MOZ_RELEASE_ASSERT(
false,
"ProfileBufferChunkManagerSingle should never destroy its one chunk");
});
UniquePtr<ProfileBufferChunk> extantReleasedChunks =
cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// First request.
UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk();
MOZ_RELEASE_ASSERT(!!chunk, "First chunk request should always work");
MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= ChunkMinBufferBytes,
"Unexpected chunk size");
MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk");
// Keep address, for later checks.
const uintptr_t chunkAddress = reinterpret_cast<uintptr_t>(chunk.get());
extantReleasedChunks = cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// Second request.
MOZ_RELEASE_ASSERT(!cm.GetChunk(), "Second chunk request should always fail");
extantReleasedChunks = cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// Add some data to the chunk (to verify recycling later on).
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0);
chunk->SetRangeStart(100);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 100);
Unused << chunk->ReserveInitialBlockAsTail(1);
Unused << chunk->ReserveBlock(2);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 1);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 1 + 2);
// Release the first chunk.
chunk->MarkDone();
cm.ReleaseChunk(std::move(chunk));
MOZ_RELEASE_ASSERT(!chunk, "chunk UniquePtr should have been moved-from");
// Request after release.
MOZ_RELEASE_ASSERT(!cm.GetChunk(),
"Chunk request after release should also fail");
// Check released chunk.
extantReleasedChunks = cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!!extantReleasedChunks,
"Could not retrieve released chunk");
MOZ_RELEASE_ASSERT(!extantReleasedChunks->GetNext(),
"There should only be one released chunk");
MOZ_RELEASE_ASSERT(
reinterpret_cast<uintptr_t>(extantReleasedChunks.get()) == chunkAddress,
"Released chunk should be first requested one");
MOZ_RELEASE_ASSERT(!cm.GetExtantReleasedChunks(),
"Unexpected extra released chunk(s)");
// Another request after release.
MOZ_RELEASE_ASSERT(!cm.GetChunk(),
"Chunk request after release should also fail");
MOZ_RELEASE_ASSERT(
cm.MaxTotalSize() == maxTotalSize,
"MaxTotalSize() should not change after requests&releases");
// Reset the chunk manager. (Single-only non-virtual function.)
cms.Reset(std::move(extantReleasedChunks));
MOZ_RELEASE_ASSERT(!extantReleasedChunks,
"Released chunk UniquePtr should have been moved-from");
MOZ_RELEASE_ASSERT(
cm.MaxTotalSize() == maxTotalSize,
"MaxTotalSize() should not change when resetting with the same chunk");
// 2nd round, first request. Theoretically async, but this implementation just
// immediately runs the callback.
bool ran = false;
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) {
ran = true;
MOZ_RELEASE_ASSERT(!!aChunk);
chunk = std::move(aChunk);
});
MOZ_RELEASE_ASSERT(ran, "RequestChunk callback not called immediately");
ran = false;
cm.FulfillChunkRequests();
MOZ_RELEASE_ASSERT(!ran, "FulfillChunkRequests should not have any effects");
MOZ_RELEASE_ASSERT(!!chunk, "First chunk request should always work");
MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= ChunkMinBufferBytes,
"Unexpected chunk size");
MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk");
MOZ_RELEASE_ASSERT(reinterpret_cast<uintptr_t>(chunk.get()) == chunkAddress,
"Requested chunk should be first requested one");
// Verify that chunk is empty and usable.
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0);
chunk->SetRangeStart(200);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 200);
Unused << chunk->ReserveInitialBlockAsTail(3);
Unused << chunk->ReserveBlock(4);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 3);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 3 + 4);
// Second request.
ran = false;
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) {
ran = true;
MOZ_RELEASE_ASSERT(!aChunk, "Second chunk request should always fail");
});
MOZ_RELEASE_ASSERT(ran, "RequestChunk callback not called");
// This one does nothing.
cm.ForgetUnreleasedChunks();
// Don't forget to mark chunk "Done" before letting it die.
chunk->MarkDone();
chunk = nullptr;
// Create a tiny chunk and reset the chunk manager with it.
chunk = ProfileBufferChunk::Create(1);
MOZ_RELEASE_ASSERT(!!chunk);
auto tinyChunkSize = chunk->BufferBytes();
MOZ_RELEASE_ASSERT(tinyChunkSize >= 1);
MOZ_RELEASE_ASSERT(tinyChunkSize < ChunkMinBufferBytes);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0);
chunk->SetRangeStart(300);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 300);
cms.Reset(std::move(chunk));
MOZ_RELEASE_ASSERT(!chunk, "chunk UniquePtr should have been moved-from");
MOZ_RELEASE_ASSERT(cm.MaxTotalSize() == tinyChunkSize,
"MaxTotalSize() should match the new chunk size");
chunk = cm.GetChunk();
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0, "Got non-recycled chunk");
// Enough testing! Clean-up.
Unused << chunk->ReserveInitialBlockAsTail(0);
chunk->MarkDone();
cm.ForgetUnreleasedChunks();
# ifdef DEBUG
cm.DeregisteredFrom(chunkManagerRegisterer);
# endif // DEBUG
printf("TestChunkManagerSingle done\n");
}
static void TestChunkManagerWithLocalLimit() {
printf("TestChunkManagerWithLocalLimit...\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 base class, to exercize virtual methods.
ProfileBufferChunkManager& cm = cmll;
# ifdef DEBUG
const char* chunkManagerRegisterer = "TestChunkManagerWithLocalLimit";
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();
}
});
UniquePtr<ProfileBufferChunk> extantReleasedChunks =
cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// First request.
UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk();
MOZ_RELEASE_ASSERT(!!chunk,
"First chunk immediate request should always work");
const auto chunkActualBufferBytes = chunk->BufferBytes();
MOZ_RELEASE_ASSERT(chunkActualBufferBytes >= ChunkMinBufferBytes,
"Unexpected chunk size");
MOZ_RELEASE_ASSERT(!chunk->GetNext(), "There should only be one chunk");
// Keep address, for later checks.
const uintptr_t chunk1Address = reinterpret_cast<uintptr_t>(chunk.get());
extantReleasedChunks = cm.GetExtantReleasedChunks();
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// Verify that ReleaseChunk accepts zero chunks.
cm.ReleaseChunk(nullptr);
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Unexpected released chunk(s)");
// 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);
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.
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 0);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 0);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == 0);
const ProfileBufferIndex index = 1 + i * chunkActualBufferBytes;
chunk->SetRangeStart(index);
MOZ_RELEASE_ASSERT(chunk->RangeStart() == index);
Unused << chunk->ReserveInitialBlockAsTail(1);
Unused << chunk->ReserveBlock(2);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetFirstBlock == 1);
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mOffsetPastLastBlock == 1 + 2);
// Request a new chunk.
bool ran = false;
UniquePtr<ProfileBufferChunk> newChunk;
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) {
ran = true;
newChunk = std::move(aChunk);
});
MOZ_RELEASE_ASSERT(
!ran, "RequestChunk should not immediately fulfill the request");
cm.FulfillChunkRequests();
MOZ_RELEASE_ASSERT(ran, "FulfillChunkRequests should invoke the callback");
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");
// Mark previous chunk done and release it.
WaitUntilTimeStampChanges(); // Force "done" timestamp to change.
chunk->MarkDone();
cm.ReleaseChunk(std::move(chunk));
// And cycle to the new chunk.
chunk = std::move(newChunk);
if (reinterpret_cast<uintptr_t>(chunk.get()) == chunk1Address) {
++chunk1ReuseCount;
}
}
// Expect all rollovers except 1 to destroy chunks.
MOZ_RELEASE_ASSERT(destroyedChunks >= (Rollovers - 1) * MaxTotalBytes /
chunkActualBufferBytes,
"Not enough destroyed chunks");
MOZ_RELEASE_ASSERT(destroyedBytes == destroyedChunks * chunkActualBufferBytes,
"Mismatched destroyed chunks and bytes");
MOZ_RELEASE_ASSERT(chunk1ReuseCount >= (Rollovers - 1),
"Not enough reuse of the first chunks");
// Check that chunk manager is reentrant from request callback.
bool ran = false;
bool ranInner = false;
UniquePtr<ProfileBufferChunk> newChunk;
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) {
ran = true;
MOZ_RELEASE_ASSERT(!!aChunk, "Chunk request should always work");
Unused << aChunk->ReserveInitialBlockAsTail(0);
WaitUntilTimeStampChanges(); // Force "done" timestamp to change.
aChunk->MarkDone();
UniquePtr<ProfileBufferChunk> anotherChunk = cm.GetChunk();
MOZ_RELEASE_ASSERT(!!anotherChunk);
Unused << anotherChunk->ReserveInitialBlockAsTail(0);
WaitUntilTimeStampChanges(); // Force "done" timestamp to change.
anotherChunk->MarkDone();
cm.RequestChunk([&](UniquePtr<ProfileBufferChunk> aChunk) {
ranInner = true;
MOZ_RELEASE_ASSERT(!!aChunk, "Chunk request should always work");
Unused << aChunk->ReserveInitialBlockAsTail(0);
WaitUntilTimeStampChanges(); // Force "done" timestamp to change.
aChunk->MarkDone();
});
MOZ_RELEASE_ASSERT(
!ranInner, "RequestChunk should not immediately fulfill the request");
});
MOZ_RELEASE_ASSERT(!ran,
"RequestChunk should not immediately fulfill the request");
MOZ_RELEASE_ASSERT(
!ranInner,
"RequestChunk should not immediately fulfill the inner request");
cm.FulfillChunkRequests();
MOZ_RELEASE_ASSERT(ran, "FulfillChunkRequests should invoke the callback");
MOZ_RELEASE_ASSERT(!ranInner,
"FulfillChunkRequests should not immediately fulfill "
"the inner request");
cm.FulfillChunkRequests();
MOZ_RELEASE_ASSERT(
ran, "2nd FulfillChunkRequests should invoke the inner request callback");
// Enough testing! Clean-up.
Unused << chunk->ReserveInitialBlockAsTail(0);
WaitUntilTimeStampChanges(); // Force "done" timestamp to change.
chunk->MarkDone();
cm.ForgetUnreleasedChunks();
// Special testing of the release algorithm, to make sure released chunks get
// sorted.
constexpr unsigned RandomReleaseChunkLoop = 100;
// Build a vector of chunks, and mark them "done", ready to be released.
Vector<UniquePtr<ProfileBufferChunk>> chunksToRelease;
MOZ_RELEASE_ASSERT(chunksToRelease.reserve(RandomReleaseChunkLoop));
Vector<TimeStamp> chunksTimeStamps;
MOZ_RELEASE_ASSERT(chunksTimeStamps.reserve(RandomReleaseChunkLoop));
for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) {
UniquePtr<ProfileBufferChunk> chunk = cm.GetChunk();
MOZ_RELEASE_ASSERT(chunk);
Unused << chunk->ReserveInitialBlockAsTail(0);
chunk->MarkDone();
MOZ_RELEASE_ASSERT(!chunk->ChunkHeader().mDoneTimeStamp.IsNull());
chunksTimeStamps.infallibleEmplaceBack(chunk->ChunkHeader().mDoneTimeStamp);
chunksToRelease.infallibleEmplaceBack(std::move(chunk));
if (i % 10 == 0) {
// "Done" timestamps should *usually* increase, let's make extra sure some
// timestamps are actually different.
WaitUntilTimeStampChanges();
}
}
// Shuffle the list.
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(chunksToRelease.begin(), chunksToRelease.end(), generator);
// And release chunks one by one, checking that the list of released chunks
// is always sorted.
printf("TestChunkManagerWithLocalLimit - Shuffle test timestamps:");
for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) {
printf(" %f", (chunksToRelease[i]->ChunkHeader().mDoneTimeStamp -
TimeStamp::ProcessCreation())
.ToMicroseconds());
cm.ReleaseChunk(std::move(chunksToRelease[i]));
cm.PeekExtantReleasedChunks([i](const ProfileBufferChunk* releasedChunks) {
MOZ_RELEASE_ASSERT(releasedChunks);
unsigned releasedChunkCount = 1;
for (;;) {
const ProfileBufferChunk* nextChunk = releasedChunks->GetNext();
if (!nextChunk) {
break;
}
++releasedChunkCount;
MOZ_RELEASE_ASSERT(releasedChunks->ChunkHeader().mDoneTimeStamp <=
nextChunk->ChunkHeader().mDoneTimeStamp);
releasedChunks = nextChunk;
}
MOZ_RELEASE_ASSERT(releasedChunkCount == i + 1);
});
}
printf("\n");
// Finally, the whole list of released chunks should have the exact same
// timestamps as the initial list of "done" chunks.
extantReleasedChunks = cm.GetExtantReleasedChunks();
for (unsigned i = 0; i < RandomReleaseChunkLoop; ++i) {
MOZ_RELEASE_ASSERT(extantReleasedChunks, "Not enough released chunks");
MOZ_RELEASE_ASSERT(extantReleasedChunks->ChunkHeader().mDoneTimeStamp ==
chunksTimeStamps[i]);
Unused << std::exchange(extantReleasedChunks,
extantReleasedChunks->ReleaseNext());
}
MOZ_RELEASE_ASSERT(!extantReleasedChunks, "Too many released chunks");
# ifdef DEBUG
cm.DeregisteredFrom(chunkManagerRegisterer);
# endif // DEBUG
printf("TestChunkManagerWithLocalLimit done\n");
}
static bool IsSameMetadata(
const ProfileBufferControlledChunkManager::ChunkMetadata& a1,
const ProfileBufferControlledChunkManager::ChunkMetadata& a2) {
return a1.mDoneTimeStamp == a2.mDoneTimeStamp &&
a1.mBufferBytes == a2.mBufferBytes;
};
static bool IsSameUpdate(
const ProfileBufferControlledChunkManager::Update& a1,
const ProfileBufferControlledChunkManager::Update& a2) {
// Final and not-an-update don't carry other data, so we can test these two
// states first.
if (a1.IsFinal() || a2.IsFinal()) {
return a1.IsFinal() && a2.IsFinal();
}
if (a1.IsNotUpdate() || a2.IsNotUpdate()) {
return a1.IsNotUpdate() && a2.IsNotUpdate();
}
// Here, both are "normal" udpates, check member variables:
if (a1.UnreleasedBytes() != a2.UnreleasedBytes()) {
return false;
}
if (a1.ReleasedBytes() != a2.ReleasedBytes()) {
return false;
}
if (a1.OldestDoneTimeStamp() != a2.OldestDoneTimeStamp()) {
return false;
}
if (a1.NewlyReleasedChunksRef().size() !=
a2.NewlyReleasedChunksRef().size()) {
return false;
}
for (unsigned i = 0; i < a1.NewlyReleasedChunksRef().size(); ++i) {
if (!IsSameMetadata(a1.NewlyReleasedChunksRef()[i],
a2.NewlyReleasedChunksRef()[i])) {
return false;
}
}
return true;
}
static void TestControlledChunkManagerUpdate() {
printf("TestControlledChunkManagerUpdate...\n");
using Update = ProfileBufferControlledChunkManager::Update;
// Default construction.
Update update1;
MOZ_RELEASE_ASSERT(update1.IsNotUpdate());
MOZ_RELEASE_ASSERT(!update1.IsFinal());
// Clear an already-cleared update.
update1.Clear();
MOZ_RELEASE_ASSERT(update1.IsNotUpdate());
MOZ_RELEASE_ASSERT(!update1.IsFinal());
// Final construction with nullptr.
const Update final(nullptr);
MOZ_RELEASE_ASSERT(final.IsFinal());
MOZ_RELEASE_ASSERT(!final.IsNotUpdate());
// Copy final to cleared.
update1 = final;
MOZ_RELEASE_ASSERT(update1.IsFinal());
MOZ_RELEASE_ASSERT(!update1.IsNotUpdate());
// Copy final to final.
update1 = final;
MOZ_RELEASE_ASSERT(update1.IsFinal());
MOZ_RELEASE_ASSERT(!update1.IsNotUpdate());
// Clear a final update.
update1.Clear();
MOZ_RELEASE_ASSERT(update1.IsNotUpdate());
MOZ_RELEASE_ASSERT(!update1.IsFinal());
// Move final to cleared.
update1 = Update(nullptr);
MOZ_RELEASE_ASSERT(update1.IsFinal());
MOZ_RELEASE_ASSERT(!update1.IsNotUpdate());
// Move final to final.
update1 = Update(nullptr);
MOZ_RELEASE_ASSERT(update1.IsFinal());
MOZ_RELEASE_ASSERT(!update1.IsNotUpdate());
// Move from not-an-update (effectively same as Clear).
update1 = Update();
MOZ_RELEASE_ASSERT(update1.IsNotUpdate());
MOZ_RELEASE_ASSERT(!update1.IsFinal());
auto CreateBiggerChunkAfter = [](const ProfileBufferChunk& aChunkToBeat) {
while (TimeStamp::Now() <= aChunkToBeat.ChunkHeader().mDoneTimeStamp) {
::SleepMilli(1);
}
auto chunk = ProfileBufferChunk::Create(aChunkToBeat.BufferBytes() * 2);
MOZ_RELEASE_ASSERT(!!chunk);
MOZ_RELEASE_ASSERT(chunk->BufferBytes() >= aChunkToBeat.BufferBytes() * 2);
Unused << chunk->ReserveInitialBlockAsTail(0);
chunk->MarkDone();
MOZ_RELEASE_ASSERT(chunk->ChunkHeader().mDoneTimeStamp >
aChunkToBeat.ChunkHeader().mDoneTimeStamp);
return chunk;
};
update1 = Update(1, 2, nullptr, nullptr);
// Create initial update with 2 released chunks and 1 unreleased chunk.
auto released = ProfileBufferChunk::Create(10);
ProfileBufferChunk* c1 = released.get();
Unused << c1->ReserveInitialBlockAsTail(0);
c1->MarkDone();
released->SetLast(CreateBiggerChunkAfter(*c1));
ProfileBufferChunk* c2 = c1->GetNext();
auto unreleased = CreateBiggerChunkAfter(*c2);
ProfileBufferChunk* c3 = unreleased.get();
Update update2(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), c1,
c1);
MOZ_RELEASE_ASSERT(IsSameUpdate(
update2,
Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()},
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}})));
// Check every field, this time only, after that we'll trust that the
// `SameUpdate` test will be enough.
MOZ_RELEASE_ASSERT(!update2.IsNotUpdate());
MOZ_RELEASE_ASSERT(!update2.IsFinal());
MOZ_RELEASE_ASSERT(update2.UnreleasedBytes() == c3->BufferBytes());
MOZ_RELEASE_ASSERT(update2.ReleasedBytes() ==
c1->BufferBytes() + c2->BufferBytes());
MOZ_RELEASE_ASSERT(update2.OldestDoneTimeStamp() ==
c1->ChunkHeader().mDoneTimeStamp);
MOZ_RELEASE_ASSERT(update2.NewlyReleasedChunksRef().size() == 2);
MOZ_RELEASE_ASSERT(
IsSameMetadata(update2.NewlyReleasedChunksRef()[0],
{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()}));
MOZ_RELEASE_ASSERT(
IsSameMetadata(update2.NewlyReleasedChunksRef()[1],
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}));
// Fold into not-an-update.
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()},
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}})));
// Pretend nothing happened.
update2 = Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(), c1,
nullptr);
MOZ_RELEASE_ASSERT(IsSameUpdate(
update2, Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp, {})));
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c3->BufferBytes(), c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()},
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}})));
// Pretend there's a new unreleased chunk.
c3->SetLast(CreateBiggerChunkAfter(*c3));
ProfileBufferChunk* c4 = c3->GetNext();
update2 = Update(c3->BufferBytes() + c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes(), c1, nullptr);
MOZ_RELEASE_ASSERT(
IsSameUpdate(update2, Update(c3->BufferBytes() + c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp, {})));
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c3->BufferBytes() + c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()},
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()}})));
// Pretend the first unreleased chunk c3 has been released.
released->SetLast(std::exchange(unreleased, unreleased->ReleaseNext()));
update2 =
Update(c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(), c1, c3);
MOZ_RELEASE_ASSERT(IsSameUpdate(
update2,
Update(c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}})));
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c4->BufferBytes(),
c1->BufferBytes() + c2->BufferBytes() + c3->BufferBytes(),
c1->ChunkHeader().mDoneTimeStamp,
{{c1->ChunkHeader().mDoneTimeStamp, c1->BufferBytes()},
{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()},
{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}})));
// Pretend c1 has been destroyed, so the oldest timestamp is now at c2.
released = released->ReleaseNext();
c1 = nullptr;
update2 = Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(), c2,
nullptr);
MOZ_RELEASE_ASSERT(IsSameUpdate(
update2, Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(),
c2->ChunkHeader().mDoneTimeStamp, {})));
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c4->BufferBytes(), c2->BufferBytes() + c3->BufferBytes(),
c2->ChunkHeader().mDoneTimeStamp,
{{c2->ChunkHeader().mDoneTimeStamp, c2->BufferBytes()},
{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()}})));
// Pretend c2 has been recycled to make unreleased c5, and c4 has been
// released.
auto recycled = std::exchange(released, released->ReleaseNext());
recycled->MarkRecycled();
Unused << recycled->ReserveInitialBlockAsTail(0);
recycled->MarkDone();
released->SetLast(std::move(unreleased));
unreleased = std::move(recycled);
ProfileBufferChunk* c5 = c2;
c2 = nullptr;
update2 =
Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(), c3, c4);
MOZ_RELEASE_ASSERT(IsSameUpdate(
update2,
Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(),
c3->ChunkHeader().mDoneTimeStamp,
{{c4->ChunkHeader().mDoneTimeStamp, c4->BufferBytes()}})));
update1.Fold(std::move(update2));
MOZ_RELEASE_ASSERT(IsSameUpdate(
update1,
Update(c5->BufferBytes(), c3->BufferBytes() + c4->BufferBytes(),
c3->ChunkHeader().mDoneTimeStamp,
{{c3->ChunkHeader().mDoneTimeStamp, c3->BufferBytes()},
{c4->ChunkHeader().mDoneTimeStamp, c4->BufferBytes()}})));
// And send a final update.
update1.Fold(Update(nullptr));
MOZ_RELEASE_ASSERT(update1.IsFinal());
MOZ_RELEASE_ASSERT(!update1.IsNotUpdate());
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,