blob: f08b8507d0a8a85f013e79971ca905098a2ff9c8 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/resource_coordinator/public/cpp/memory_instrumentation/os_metrics.h"
#include <set>
#include <vector>
#include "base/files/file_util.h"
#include "base/process/process_handle.h"
#include "base/process/process_metrics.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#if defined(OS_MAC)
#include <libgen.h>
#include <mach-o/dyld.h>
#endif
#if defined(OS_WIN)
#include <base/strings/sys_string_conversions.h>
#include <windows.h>
#endif
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
#include <sys/mman.h>
#endif
namespace memory_instrumentation {
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
namespace {
const char kTestSmaps1[] =
"00400000-004be000 r-xp 00000000 fc:01 1234 /file/1\n"
"Size: 760 kB\n"
"Rss: 296 kB\n"
"Pss: 162 kB\n"
"Shared_Clean: 228 kB\n"
"Shared_Dirty: 0 kB\n"
"Private_Clean: 0 kB\n"
"Private_Dirty: 68 kB\n"
"Referenced: 296 kB\n"
"Anonymous: 68 kB\n"
"AnonHugePages: 0 kB\n"
"Swap: 4 kB\n"
"KernelPageSize: 4 kB\n"
"MMUPageSize: 4 kB\n"
"Locked: 0 kB\n"
"VmFlags: rd ex mr mw me dw sd\n"
"ff000000-ff800000 -w-p 00001080 fc:01 0 /file/name with space\n"
"Size: 0 kB\n"
"Rss: 192 kB\n"
"Pss: 128 kB\n"
"Shared_Clean: 120 kB\n"
"Shared_Dirty: 4 kB\n"
"Private_Clean: 60 kB\n"
"Private_Dirty: 8 kB\n"
"Referenced: 296 kB\n"
"Anonymous: 0 kB\n"
"AnonHugePages: 0 kB\n"
"Swap: 0 kB\n"
"KernelPageSize: 4 kB\n"
"MMUPageSize: 4 kB\n"
"Locked: 1 kB\n"
"VmFlags: rd ex mr mw me dw sd";
const char kTestSmaps2[] =
// An invalid region, with zero size and overlapping with the last one
// (See crbug.com/461237).
"7fe7ce79c000-7fe7ce79c000 ---p 00000000 00:00 0 \n"
"Size: 4 kB\n"
"Rss: 0 kB\n"
"Pss: 0 kB\n"
"Shared_Clean: 0 kB\n"
"Shared_Dirty: 0 kB\n"
"Private_Clean: 0 kB\n"
"Private_Dirty: 0 kB\n"
"Referenced: 0 kB\n"
"Anonymous: 0 kB\n"
"AnonHugePages: 0 kB\n"
"Swap: 0 kB\n"
"KernelPageSize: 4 kB\n"
"MMUPageSize: 4 kB\n"
"Locked: 0 kB\n"
"VmFlags: rd ex mr mw me dw sd\n"
// A invalid region with its range going backwards.
"00400000-00200000 ---p 00000000 00:00 0 \n"
"Size: 4 kB\n"
"Rss: 0 kB\n"
"Pss: 0 kB\n"
"Shared_Clean: 0 kB\n"
"Shared_Dirty: 0 kB\n"
"Private_Clean: 0 kB\n"
"Private_Dirty: 0 kB\n"
"Referenced: 0 kB\n"
"Anonymous: 0 kB\n"
"AnonHugePages: 0 kB\n"
"Swap: 0 kB\n"
"KernelPageSize: 4 kB\n"
"MMUPageSize: 4 kB\n"
"Locked: 0 kB\n"
"VmFlags: rd ex mr mw me dw sd\n"
// A good anonymous region at the end.
"7fe7ce79c000-7fe7ce7a8000 ---p 00000000 00:00 0 \n"
"Size: 48 kB\n"
"Rss: 40 kB\n"
"Pss: 32 kB\n"
"Shared_Clean: 16 kB\n"
"Shared_Dirty: 12 kB\n"
"Private_Clean: 8 kB\n"
"Private_Dirty: 4 kB\n"
"Referenced: 40 kB\n"
"Anonymous: 16 kB\n"
"AnonHugePages: 0 kB\n"
"Swap: 0 kB\n"
"KernelPageSize: 4 kB\n"
"MMUPageSize: 4 kB\n"
"Locked: 11 kB\n"
"VmFlags: rd wr mr mw me ac sd\n";
void CreateTempFileWithContents(const char* contents, base::ScopedFILE* file) {
base::FilePath temp_path;
*file = CreateAndOpenTemporaryStream(&temp_path);
ASSERT_TRUE(*file);
ASSERT_TRUE(base::WriteFileDescriptor(fileno(file->get()), contents,
strlen(contents)));
}
} // namespace
#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
TEST(OSMetricsTest, GivesNonZeroResults) {
base::ProcessId pid = base::kNullProcessId;
mojom::RawOSMemDump dump;
dump.platform_private_footprint = mojom::PlatformPrivateFootprint::New();
EXPECT_TRUE(OSMetrics::FillOSMemoryDump(pid, &dump));
EXPECT_TRUE(dump.platform_private_footprint);
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID) || \
defined(OS_FUCHSIA)
EXPECT_GT(dump.platform_private_footprint->rss_anon_bytes, 0u);
#elif defined(OS_WIN)
EXPECT_GT(dump.platform_private_footprint->private_bytes, 0u);
#elif defined(OS_MAC)
EXPECT_GT(dump.platform_private_footprint->internal_bytes, 0u);
#endif
}
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
TEST(OSMetricsTest, ParseProcSmaps) {
const uint32_t kProtR = mojom::VmRegion::kProtectionFlagsRead;
const uint32_t kProtW = mojom::VmRegion::kProtectionFlagsWrite;
const uint32_t kProtX = mojom::VmRegion::kProtectionFlagsExec;
// Emulate an empty /proc/self/smaps.
base::ScopedFILE empty_file(OpenFile(base::FilePath("/dev/null"), "r"));
ASSERT_TRUE(empty_file.get());
OSMetrics::SetProcSmapsForTesting(empty_file.get());
auto no_maps = OSMetrics::GetProcessMemoryMaps(base::kNullProcessId);
ASSERT_TRUE(no_maps.empty());
// Parse the 1st smaps file.
base::ScopedFILE temp_file1;
CreateTempFileWithContents(kTestSmaps1, &temp_file1);
OSMetrics::SetProcSmapsForTesting(temp_file1.get());
auto maps_1 = OSMetrics::GetProcessMemoryMaps(base::kNullProcessId);
ASSERT_EQ(2UL, maps_1.size());
EXPECT_EQ(0x00400000UL, maps_1[0]->start_address);
EXPECT_EQ(0x004be000UL - 0x00400000UL, maps_1[0]->size_in_bytes);
EXPECT_EQ(kProtR | kProtX, maps_1[0]->protection_flags);
EXPECT_EQ("/file/1", maps_1[0]->mapped_file);
EXPECT_EQ(162 * 1024UL, maps_1[0]->byte_stats_proportional_resident);
EXPECT_EQ(228 * 1024UL, maps_1[0]->byte_stats_shared_clean_resident);
EXPECT_EQ(0UL, maps_1[0]->byte_stats_shared_dirty_resident);
EXPECT_EQ(0UL, maps_1[0]->byte_stats_private_clean_resident);
EXPECT_EQ(68 * 1024UL, maps_1[0]->byte_stats_private_dirty_resident);
EXPECT_EQ(4 * 1024UL, maps_1[0]->byte_stats_swapped);
EXPECT_EQ(0 * 1024UL, maps_1[0]->byte_locked);
EXPECT_EQ(0xff000000UL, maps_1[1]->start_address);
EXPECT_EQ(0xff800000UL - 0xff000000UL, maps_1[1]->size_in_bytes);
EXPECT_EQ(kProtW, maps_1[1]->protection_flags);
EXPECT_EQ("/file/name with space", maps_1[1]->mapped_file);
EXPECT_EQ(128 * 1024UL, maps_1[1]->byte_stats_proportional_resident);
EXPECT_EQ(120 * 1024UL, maps_1[1]->byte_stats_shared_clean_resident);
EXPECT_EQ(4 * 1024UL, maps_1[1]->byte_stats_shared_dirty_resident);
EXPECT_EQ(60 * 1024UL, maps_1[1]->byte_stats_private_clean_resident);
EXPECT_EQ(8 * 1024UL, maps_1[1]->byte_stats_private_dirty_resident);
EXPECT_EQ(0 * 1024UL, maps_1[1]->byte_stats_swapped);
EXPECT_EQ(1 * 1024UL, maps_1[1]->byte_locked);
// Parse the 2nd smaps file.
base::ScopedFILE temp_file2;
CreateTempFileWithContents(kTestSmaps2, &temp_file2);
OSMetrics::SetProcSmapsForTesting(temp_file2.get());
auto maps_2 = OSMetrics::GetProcessMemoryMaps(base::kNullProcessId);
ASSERT_EQ(1UL, maps_2.size());
EXPECT_EQ(0x7fe7ce79c000UL, maps_2[0]->start_address);
EXPECT_EQ(0x7fe7ce7a8000UL - 0x7fe7ce79c000UL, maps_2[0]->size_in_bytes);
EXPECT_EQ(0U, maps_2[0]->protection_flags);
EXPECT_EQ("", maps_2[0]->mapped_file);
EXPECT_EQ(32 * 1024UL, maps_2[0]->byte_stats_proportional_resident);
EXPECT_EQ(16 * 1024UL, maps_2[0]->byte_stats_shared_clean_resident);
EXPECT_EQ(12 * 1024UL, maps_2[0]->byte_stats_shared_dirty_resident);
EXPECT_EQ(8 * 1024UL, maps_2[0]->byte_stats_private_clean_resident);
EXPECT_EQ(4 * 1024UL, maps_2[0]->byte_stats_private_dirty_resident);
EXPECT_EQ(0 * 1024UL, maps_2[0]->byte_stats_swapped);
EXPECT_EQ(11 * 1024UL, maps_2[0]->byte_locked);
}
TEST(OSMetricsTest, GetMappedAndResidentPages) {
const size_t kPages = 16;
const size_t kPageSize = base::GetPageSize();
const size_t kLength = kPages * kPageSize;
// mmap guarantees addr is aligned with kPagesize.
void* addr = mmap(NULL, kLength, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(MAP_FAILED, addr) << "mmap() failed";
std::set<size_t> pages;
uint8_t* array = static_cast<uint8_t*>(addr);
for (unsigned int i = 0; i < kPages / 2; ++i) {
int page = rand() % kPages;
int offset = rand() % kPageSize;
*static_cast<volatile uint8_t*>(array + page * kPageSize + offset) =
rand() % 256;
pages.insert(page);
}
size_t start_address = reinterpret_cast<size_t>(addr);
std::vector<uint8_t> accessed_pages_bitmap;
OSMetrics::MappedAndResidentPagesDumpState state =
OSMetrics::GetMappedAndResidentPages(
start_address, start_address + kLength, &accessed_pages_bitmap);
ASSERT_EQ(munmap(addr, kLength), 0);
if (state == OSMetrics::MappedAndResidentPagesDumpState::kAccessPagemapDenied)
return;
EXPECT_EQ(state == OSMetrics::MappedAndResidentPagesDumpState::kSuccess,
true);
std::set<size_t> accessed_pages_set;
for (size_t i = 0; i < accessed_pages_bitmap.size(); i++) {
for (int j = 0; j < 8; j++)
if (accessed_pages_bitmap[i] & (1 << j))
accessed_pages_set.insert(i * 8 + j);
}
EXPECT_EQ(pages == accessed_pages_set, true);
}
#endif // defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
#if defined(OS_WIN)
void DummyFunction() {}
TEST(OSMetricsTest, TestWinModuleReading) {
auto maps = OSMetrics::GetProcessMemoryMaps(base::kNullProcessId);
wchar_t module_name[MAX_PATH];
DWORD result = GetModuleFileName(nullptr, module_name, MAX_PATH);
ASSERT_TRUE(result);
std::string executable_name = base::SysWideToNativeMB(module_name);
HMODULE module_containing_dummy = nullptr;
uintptr_t dummy_function_address =
reinterpret_cast<uintptr_t>(&DummyFunction);
result = GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
reinterpret_cast<LPCWSTR>(dummy_function_address),
&module_containing_dummy);
ASSERT_TRUE(result);
result = GetModuleFileName(nullptr, module_name, MAX_PATH);
ASSERT_TRUE(result);
std::string module_containing_dummy_name =
base::SysWideToNativeMB(module_name);
bool found_executable = false;
bool found_region_with_dummy = false;
for (const mojom::VmRegionPtr& region : maps) {
// We add a region just for byte_stats_proportional_resident which
// is empty other than that one stat.
if (region->byte_stats_proportional_resident > 0) {
EXPECT_EQ(0u, region->start_address);
EXPECT_EQ(0u, region->size_in_bytes);
continue;
}
EXPECT_NE(0u, region->start_address);
EXPECT_NE(0u, region->size_in_bytes);
if (region->mapped_file.find(executable_name) != std::string::npos)
found_executable = true;
if (dummy_function_address >= region->start_address &&
dummy_function_address <
region->start_address + region->size_in_bytes) {
found_region_with_dummy = true;
EXPECT_EQ(module_containing_dummy_name, region->mapped_file);
}
}
EXPECT_TRUE(found_executable);
EXPECT_TRUE(found_region_with_dummy);
}
#endif // defined(OS_WIN)
#if defined(OS_MAC)
namespace {
void CheckMachORegions(const std::vector<mojom::VmRegionPtr>& maps) {
uint32_t size = 100;
char full_path[size];
int result = _NSGetExecutablePath(full_path, &size);
ASSERT_EQ(0, result);
std::string name = basename(full_path);
bool found_appkit = false;
bool found_components_unittests = false;
for (const mojom::VmRegionPtr& region : maps) {
EXPECT_NE(0u, region->start_address);
EXPECT_NE(0u, region->size_in_bytes);
EXPECT_LT(region->size_in_bytes, 1ull << 32);
uint32_t required_protection_flags = mojom::VmRegion::kProtectionFlagsRead |
mojom::VmRegion::kProtectionFlagsExec;
if (region->mapped_file.find(name) != std::string::npos &&
region->protection_flags == required_protection_flags) {
found_components_unittests = true;
}
if (region->mapped_file.find("AppKit") != std::string::npos) {
found_appkit = true;
}
}
EXPECT_TRUE(found_components_unittests);
EXPECT_TRUE(found_appkit);
}
} // namespace
// Test failing on Mac ASan 64: https://crbug.com/852690
TEST(OSMetricsTest, DISABLED_TestMachOReading) {
auto maps = OSMetrics::GetProcessMemoryMaps(base::kNullProcessId);
CheckMachORegions(maps);
maps = OSMetrics::GetProcessModules(base::kNullProcessId);
CheckMachORegions(maps);
}
#endif // defined(OS_MAC)
} // namespace memory_instrumentation