blob: b07d0fedbfef0614c41da897fbff962d5c819342 [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/trace_event/malloc_dump_provider.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "base/allocator/buildflags.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif
#if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
#include "base/memory/advanced_memory_safety_checks.h"
#include "base/trace_event/process_memory_dump.h"
#include "partition_alloc/partition_root.h"
#endif // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
namespace base::trace_event {
#if BUILDFLAG(IS_WIN)
namespace {
class ScopedTestHeap {
public:
ScopedTestHeap() : handle_(::HeapCreate(0, 0, 0)) { CHECK(handle_); }
~ScopedTestHeap() { CHECK(::HeapDestroy(handle_)); }
ScopedTestHeap(const ScopedTestHeap&) = delete;
ScopedTestHeap& operator=(const ScopedTestHeap&) = delete;
HANDLE handle() { return handle_; }
private:
HANDLE handle_;
};
// Above the historical HeapAlloc->VirtualAlloc threshold (~512 KB), so the
// allocation is guaranteed to appear as an orphan busy entry.
constexpr size_t kLargeAllocBytes = 2 * 1024 * 1024;
} // namespace
TEST(MallocDumpProviderTest, WinHeapInfo_EmptyHeap) {
ScopedTestHeap heap;
auto info = internal::WinHeapInfo::FromHandleForTesting(heap.handle());
EXPECT_EQ(info.allocated_size, 0u);
EXPECT_EQ(info.block_count, 0u);
// A fresh heap has at least one reserved region.
EXPECT_GT(info.committed_size + info.uncommitted_size, 0u);
}
TEST(MallocDumpProviderTest, WinHeapInfo_SmallAllocStaysInRegion) {
ScopedTestHeap heap;
void* p = ::HeapAlloc(heap.handle(), 0, 64);
ASSERT_TRUE(p);
auto info = internal::WinHeapInfo::FromHandleForTesting(heap.handle());
EXPECT_GE(info.allocated_size, 64u);
EXPECT_GE(info.block_count, 1u);
// The block lives inside a region whose committed bytes already include it,
// so committed_size must dominate allocated_size.
EXPECT_GE(info.committed_size, info.allocated_size);
::HeapFree(heap.handle(), 0, p);
}
TEST(MallocDumpProviderTest, WinHeapInfo_LargeAllocBecomesOrphanBusy) {
ScopedTestHeap heap;
void* p = ::HeapAlloc(heap.handle(), 0, kLargeAllocBytes);
ASSERT_TRUE(p);
auto info = internal::WinHeapInfo::FromHandleForTesting(heap.handle());
EXPECT_GE(info.allocated_size, kLargeAllocBytes);
EXPECT_GE(info.block_count, 1u);
// Regression assertion for the orphan-busy-entry fix: committed_size must
// grow with the large allocation. Before the fix, large blocks lived
// outside any PROCESS_HEAP_REGION and were not counted as committed, so
// committed_size would have been (much) less than allocated_size.
EXPECT_GE(info.committed_size, info.allocated_size);
::HeapFree(heap.handle(), 0, p);
}
#endif // BUILDFLAG(IS_WIN)
#if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
namespace {
constexpr std::string_view kAllocatedObjectsPrefix =
"malloc/allocated_objects/";
const MemoryAllocatorDump* FindAllocatorDump(const ProcessMemoryDump& pmd,
std::string_view name) {
auto it = pmd.allocator_dumps().find(std::string(name));
return it == pmd.allocator_dumps().cend() ? nullptr : it->second.get();
}
std::optional<uint64_t> GetScalarEntry(const MemoryAllocatorDump& dump,
std::string_view name,
std::string_view units) {
for (const auto& entry : dump.entries()) {
if (entry.name == name) {
CHECK_EQ(MemoryAllocatorDump::Entry::EntryType::kUint64,
entry.entry_type);
CHECK_EQ(units, entry.units);
return entry.value_uint64;
}
}
return std::nullopt;
}
std::optional<uint64_t> GetBytesEntry(const MemoryAllocatorDump& dump,
std::string_view name) {
return GetScalarEntry(dump, name, MemoryAllocatorDump::kUnitsBytes);
}
} // namespace
// malloc/partitions reports the resident footprint of PartitionAlloc. The
// objects allocated out of it are reported by its per-bucket children.
TEST(MallocDumpProviderTest, PartitionsDumpReportsFootprint) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* partitions_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitions);
ASSERT_TRUE(partitions_dump);
std::optional<uint64_t> size =
GetBytesEntry(*partitions_dump, MemoryAllocatorDump::kNameSize);
std::optional<uint64_t> virtual_size =
GetBytesEntry(*partitions_dump, "virtual_size");
std::optional<uint64_t> allocated_objects_size =
GetBytesEntry(*partitions_dump, "allocated_objects_size");
std::optional<uint64_t> committed_size =
GetBytesEntry(*partitions_dump, "virtual_committed_size");
std::optional<uint64_t> wasted = GetBytesEntry(*partitions_dump, "wasted");
std::optional<uint64_t> fragmentation =
GetScalarEntry(*partitions_dump, "fragmentation", "percent");
ASSERT_TRUE(size.has_value());
ASSERT_TRUE(virtual_size.has_value());
ASSERT_TRUE(allocated_objects_size.has_value());
ASSERT_TRUE(committed_size.has_value());
ASSERT_TRUE(wasted.has_value());
ASSERT_TRUE(fragmentation.has_value());
// virtual_size is address space, which cannot be smaller than the resident
// bytes mapped into it.
EXPECT_GE(*virtual_size, *size);
EXPECT_LE(*wasted, *committed_size);
EXPECT_LE(*fragmentation, 100u);
// wasted and fragmentation have the same meaning as in each partition's own
// dump, so the totals are the sum of the partitions' values.
const std::string partition_prefix =
std::string(MallocDumpProvider::kPartitions) + "/";
uint64_t partitions_committed_size = 0;
uint64_t partitions_wasted = 0;
for (const auto& [name, dump] : pmd.allocator_dumps()) {
if (!name.starts_with(partition_prefix) ||
name.find('/', partition_prefix.size()) != std::string::npos) {
continue;
}
std::optional<uint64_t> partition_committed_size =
GetBytesEntry(*dump, "virtual_committed_size");
std::optional<uint64_t> partition_wasted = GetBytesEntry(*dump, "wasted");
ASSERT_TRUE(partition_committed_size.has_value()) << name;
ASSERT_TRUE(partition_wasted.has_value()) << name;
partitions_committed_size += *partition_committed_size;
partitions_wasted += *partition_wasted;
}
EXPECT_EQ(*committed_size, partitions_committed_size);
EXPECT_EQ(*wasted, partitions_wasted);
EXPECT_EQ(*fragmentation,
*committed_size == 0 ? 0 : 100 * *wasted / *committed_size);
}
// The objects are attributed by the per-bucket suballocations, so the system
// allocator pool itself no longer owns malloc/partitions. It must own nothing:
// an allocator dump can only own a single target, which is what kept the other
// backends from being attributed this way.
TEST(MallocDumpProviderTest, SystemAllocatorPoolOwnsNothing) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* allocated_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kAllocatedObjects);
const MemoryAllocatorDump* partitions_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitionsAllocatedObjects);
ASSERT_TRUE(allocated_objects_dump);
ASSERT_TRUE(partitions_objects_dump);
const auto& edges = pmd.allocator_dumps_edges();
EXPECT_TRUE(edges.find(allocated_objects_dump->guid()) == edges.cend());
const MemoryAllocatorDump* partitions_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitions);
ASSERT_TRUE(partitions_dump);
EXPECT_EQ(
GetBytesEntry(*partitions_objects_dump, MemoryAllocatorDump::kNameSize),
GetBytesEntry(*partitions_dump, "allocated_objects_size"));
}
// Each bucket's live objects are suballocated from the bucket they live in.
TEST(MallocDumpProviderTest, PartitionBucketsSuballocateTheirObjects) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* bucket_objects_dump = nullptr;
for (const auto& [name, dump] : pmd.allocator_dumps()) {
if (name.starts_with(kAllocatedObjectsPrefix) &&
name.find("/buckets/") != std::string::npos) {
bucket_objects_dump = dump.get();
break;
}
}
ASSERT_TRUE(bucket_objects_dump)
<< "no per-bucket allocated_objects dump was reported";
// The dump name is the bucket's own name re-rooted under allocated_objects,
// so the bucket it belongs to is recovered by dropping that prefix.
const std::string bucket_name =
"malloc/" + bucket_objects_dump->absolute_name().substr(
kAllocatedObjectsPrefix.size());
const MemoryAllocatorDump* suballocation_dump = FindAllocatorDump(
pmd, bucket_name + "/__" + bucket_objects_dump->guid().ToString());
ASSERT_TRUE(suballocation_dump)
<< "no suballocation of " << bucket_name << " was reported";
const auto& edges = pmd.allocator_dumps_edges();
auto edge = edges.find(bucket_objects_dump->guid());
ASSERT_TRUE(edge != edges.cend());
EXPECT_EQ(edge->second.target.ToUint64(),
suballocation_dump->guid().ToUint64());
}
// In background mode no bucket is dumped, so the per-bucket suballocations
// which normally attribute the objects do not exist. Without an edge of its
// own, malloc/allocated_objects/partitions and malloc/partitions would both be
// counted in full under malloc, inflating the malloc-wide total by the size of
// the live objects.
TEST(MallocDumpProviderTest, PartitionsObjectsOwnPartitionsInBackgroundDumps) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kBackground};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* partitions_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitions);
const MemoryAllocatorDump* partitions_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitionsAllocatedObjects);
ASSERT_TRUE(partitions_dump);
ASSERT_TRUE(partitions_objects_dump);
const auto& edges = pmd.allocator_dumps_edges();
auto edge = edges.find(partitions_objects_dump->guid());
ASSERT_TRUE(edge != edges.cend());
EXPECT_EQ(edge->second.target.ToUint64(), partitions_dump->guid().ToUint64());
}
// At the levels of detail which dump the buckets, the per-bucket
// suballocations attribute the objects, so the edge above would subtract them
// a second time.
TEST(MallocDumpProviderTest, PartitionsObjectsOwnNothingInDetailedDumps) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* partitions_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kPartitionsAllocatedObjects);
ASSERT_TRUE(partitions_objects_dump);
const auto& edges = pmd.allocator_dumps_edges();
EXPECT_TRUE(edges.find(partitions_objects_dump->guid()) == edges.cend());
}
#endif // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
// The malloc/win_heap dump is only created when PartitionAlloc is the malloc
// implementation. Without it, ReportWinHeapStats folds the WinHeap numbers
// into the malloc totals and is passed no dump to populate.
#if BUILDFLAG(IS_WIN) && PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
namespace {
constexpr char kWinHeapWasteDumpName[] =
"malloc/win_heap/metadata_fragmentation_caches";
constexpr char kMallocWasteDumpName[] = "malloc/metadata_fragmentation_caches";
} // namespace
// malloc/win_heap reports the resident footprint of the heap. The objects
// allocated out of it are reported by malloc/allocated_objects/win_heap.
TEST(MallocDumpProviderTest, WinHeapDumpReportsFootprint) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* win_heap_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kWinHeap);
const MemoryAllocatorDump* win_heap_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kWinHeapAllocatedObjects);
ASSERT_TRUE(win_heap_dump);
ASSERT_TRUE(win_heap_objects_dump);
std::optional<uint64_t> size =
GetBytesEntry(*win_heap_dump, MemoryAllocatorDump::kNameSize);
std::optional<uint64_t> committed_size =
GetBytesEntry(*win_heap_dump, "virtual_committed_size");
std::optional<uint64_t> virtual_size =
GetBytesEntry(*win_heap_dump, "virtual_size");
std::optional<uint64_t> allocated_size =
GetBytesEntry(*win_heap_objects_dump, MemoryAllocatorDump::kNameSize);
std::optional<uint64_t> wasted = GetBytesEntry(*win_heap_dump, "wasted");
std::optional<uint64_t> fragmentation =
GetScalarEntry(*win_heap_dump, "fragmentation", "percent");
ASSERT_TRUE(size.has_value());
ASSERT_TRUE(committed_size.has_value());
ASSERT_TRUE(virtual_size.has_value());
ASSERT_TRUE(allocated_size.has_value());
ASSERT_TRUE(wasted.has_value());
ASSERT_TRUE(fragmentation.has_value());
// Resident size is approximated with the committed heap size.
EXPECT_EQ(*size, *committed_size);
// virtual_size is committed + uncommitted, and the committed bytes of a
// region already include the blocks allocated inside it.
EXPECT_GE(*virtual_size, *committed_size);
EXPECT_GE(*committed_size, *allocated_size);
// The committed bytes are either handed out to a live allocation or wasted.
EXPECT_EQ(*committed_size, *allocated_size + *wasted);
EXPECT_LE(*fragmentation, 100u);
}
// The wasted bytes are reported under malloc/win_heap so that its children
// account for the whole committed heap, and are excluded from the malloc-wide
// malloc/metadata_fragmentation_caches to avoid counting them twice.
TEST(MallocDumpProviderTest, WinHeapWasteReportedUnderHeapDump) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* win_heap_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kWinHeap);
ASSERT_TRUE(win_heap_dump);
std::optional<uint64_t> wasted = GetBytesEntry(*win_heap_dump, "wasted");
ASSERT_TRUE(wasted.has_value());
// Every backend on Windows reports its own waste, so nothing is left for the
// malloc-wide waste dump.
EXPECT_FALSE(FindAllocatorDump(pmd, kMallocWasteDumpName));
const MemoryAllocatorDump* waste_dump =
FindAllocatorDump(pmd, kWinHeapWasteDumpName);
if (*wasted == 0) {
// A heap whose committed bytes are all handed out gets no waste dump.
EXPECT_FALSE(waste_dump);
return;
}
ASSERT_TRUE(waste_dump);
EXPECT_EQ(GetBytesEntry(*waste_dump, MemoryAllocatorDump::kNameSize), wasted);
}
// The objects allocated out of the WinHeap are accounted for under the system
// allocator pool, and reported as suballocated from malloc/win_heap so that the
// heap dump does not account for them a second time.
TEST(MallocDumpProviderTest, WinHeapAllocatedObjectsAreSuballocatedFromHeap) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kDetailed};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
const MemoryAllocatorDump* win_heap_objects_dump =
FindAllocatorDump(pmd, MallocDumpProvider::kWinHeapAllocatedObjects);
ASSERT_TRUE(win_heap_objects_dump);
EXPECT_TRUE(
GetBytesEntry(*win_heap_objects_dump, MemoryAllocatorDump::kNameSize)
.has_value());
EXPECT_TRUE(GetScalarEntry(*win_heap_objects_dump,
MemoryAllocatorDump::kNameObjectCount,
MemoryAllocatorDump::kUnitsObjects)
.has_value());
// AddSuballocation() names the child after the owner's guid and leaves it
// without a size of its own: the UI groups nodes named this way under a
// synthetic "suballocations" entry of the parent, and takes their size from
// the owner.
const std::string suballocation_name =
std::string(MallocDumpProvider::kWinHeap) + "/__" +
win_heap_objects_dump->guid().ToString();
const MemoryAllocatorDump* suballocation_dump =
FindAllocatorDump(pmd, suballocation_name);
ASSERT_TRUE(suballocation_dump);
EXPECT_FALSE(
GetBytesEntry(*suballocation_dump, MemoryAllocatorDump::kNameSize)
.has_value());
const auto& edges = pmd.allocator_dumps_edges();
auto edge = edges.find(win_heap_objects_dump->guid());
ASSERT_TRUE(edge != edges.cend());
EXPECT_EQ(edge->second.target.ToUint64(),
suballocation_dump->guid().ToUint64());
}
// Walking the heap is too expensive for the lighter levels of detail, so no
// dumps are created for them at all.
TEST(MallocDumpProviderTest, WinHeapDumpsOmittedBelowDetailedLevel) {
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kBackground};
ProcessMemoryDump pmd(dump_args);
ASSERT_TRUE(mdp->OnMemoryDump(dump_args, &pmd));
EXPECT_FALSE(FindAllocatorDump(pmd, MallocDumpProvider::kWinHeap));
EXPECT_FALSE(
FindAllocatorDump(pmd, MallocDumpProvider::kWinHeapAllocatedObjects));
EXPECT_FALSE(FindAllocatorDump(pmd, kWinHeapWasteDumpName));
}
#endif // BUILDFLAG(IS_WIN) && PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
#if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
namespace {
class NormalTestClass1 {
public:
NormalTestClass1() = default;
uint8_t unused_padding[15];
};
class LeakedTestClass1 {
LEAKED_SANITIZED_OBJECT();
public:
LeakedTestClass1() = default;
uint8_t unused_padding[15];
};
class LeakedTestClass2 {
LEAKED_SANITIZED_OBJECT();
public:
LeakedTestClass2() = default;
uint8_t unused_padding[2047];
};
std::pair<bool, size_t> GetIntendedLeakSize() {
constexpr std::string_view kIntendedLeakSize = "intended_leak_size";
constexpr std::string_view kAllocatorDumpName = "malloc/partitions/leaked";
std::unique_ptr<MallocDumpProvider> mdp =
MallocDumpProvider::CreateForTesting();
const MemoryDumpArgs dump_args = {MemoryDumpLevelOfDetail::kBackground};
ProcessMemoryDump pmd(dump_args);
mdp->OnMemoryDump(dump_args, &pmd);
auto iterator = pmd.allocator_dumps().find(std::string(kAllocatorDumpName));
if (pmd.allocator_dumps().cend() == iterator) {
return std::make_pair(false, 0u);
}
for (const auto& entry : iterator->second->entries()) {
if (entry.name == kIntendedLeakSize) {
CHECK_EQ(MemoryAllocatorDump::Entry::EntryType::kUint64,
entry.entry_type);
CHECK_EQ(MemoryAllocatorDump::kUnitsBytes, entry.units);
return std::make_pair(true, entry.value_uint64);
}
}
return std::make_pair(false, 0u);
}
} // namespace
TEST(MallocDumpProviderTest, DumpIntendedLeakedSize) {
// To avoid flakiness, firstly we will measure current `intended_leak_size`.
// The flakiness will be caused by `safety_checks_unittests` because the tests
// leak some objects at free().
size_t expected_intended_leak_size;
expected_intended_leak_size = GetIntendedLeakSize().second;
const auto* leaked_security_object_root =
base::internal::LeakedSecurityObjectAllocator();
ASSERT_NE(leaked_security_object_root, nullptr);
// Allocate and deallocate normal object. This doesn't cause any memory leaks.
{
std::unique_ptr<NormalTestClass1> normal_obj1 =
std::make_unique<NormalTestClass1>();
ASSERT_NE(normal_obj1, nullptr);
EXPECT_NE(
leaked_security_object_root,
partition_alloc::PartitionRoot::GetRootFromAddress(normal_obj1.get()));
}
{
auto intended_leak_size = GetIntendedLeakSize();
EXPECT_TRUE(intended_leak_size.first);
EXPECT_EQ(expected_intended_leak_size, intended_leak_size.second);
}
// Allocate and deallocate leaked security object. This will cause memory
// leak.
{
std::unique_ptr<LeakedTestClass1> leaked_obj1 =
std::make_unique<LeakedTestClass1>();
ASSERT_NE(leaked_obj1, nullptr);
EXPECT_EQ(
leaked_security_object_root,
partition_alloc::PartitionRoot::GetRootFromAddress(leaked_obj1.get()));
// `intended_leaked_size` is calculated based on `slot_size`.
expected_intended_leak_size +=
leaked_security_object_root->GetSlotSizeForTesting(leaked_obj1.get());
}
{
auto intended_leak_size = GetIntendedLeakSize();
EXPECT_TRUE(intended_leak_size.first);
EXPECT_EQ(expected_intended_leak_size, intended_leak_size.second);
}
{
std::unique_ptr<LeakedTestClass2> leaked_obj2 =
std::make_unique<LeakedTestClass2>();
ASSERT_NE(leaked_obj2, nullptr);
EXPECT_EQ(
leaked_security_object_root,
partition_alloc::PartitionRoot::GetRootFromAddress(leaked_obj2.get()));
expected_intended_leak_size +=
leaked_security_object_root->GetSlotSizeForTesting(leaked_obj2.get());
}
{
auto intended_leak_size = GetIntendedLeakSize();
EXPECT_TRUE(intended_leak_size.first);
EXPECT_EQ(expected_intended_leak_size, intended_leak_size.second);
}
}
#endif // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
} // namespace base::trace_event