blob: 29da5277305a9997e4c17d5e40cb4b82517deac8 [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "gather_vmos.h"
#include <zircon/process.h>
#include <algorithm>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "dockyard_proxy_fake.h"
#include "info_resource.h"
using ::testing::_;
using ::testing::IsNull;
using ::testing::Le;
using ::testing::NiceMock;
using ::testing::NotNull;
using ::testing::ReturnRef;
class MockTaskTree : public harvester::TaskTree {
public:
MOCK_METHOD(void, Gather, (), (override));
MOCK_METHOD(const std::vector<Task>&, Jobs, (), (const, override));
MOCK_METHOD(const std::vector<Task>&, Processes, (), (const, override));
MOCK_METHOD(const std::vector<Task>&, Threads, (), (const, override));
};
class MockOS : public harvester::OS {
public:
MOCK_METHOD(zx_duration_t, HighResolutionNow, (), (override));
MOCK_METHOD(zx_status_t, GetInfo,
(zx_handle_t parent, unsigned int children_kind, void* out_buffer,
size_t buffer_size, size_t* actual, size_t* avail),
(override));
};
class GatherVmosTest : public ::testing::Test {
public:
void SetUp() override {
ASSERT_EQ(harvester::GetInfoResource(&info_resource_), ZX_OK);
}
zx_info_vmo_t MakeVmo(zx_koid_t vmo_koid, size_t size_bytes,
size_t committed_bytes, const char* name) {
return MakeVmoWithParent(vmo_koid, 0, size_bytes, committed_bytes, name);
}
zx_info_vmo_t MakeVmoWithParent(zx_koid_t vmo_koid, size_t parent_vmo_koid,
size_t size_bytes, size_t committed_bytes,
const char* name) {
zx_info_vmo_t vmo = {
.koid = vmo_koid,
.size_bytes = size_bytes,
.parent_koid = parent_vmo_koid,
.committed_bytes = committed_bytes,
};
strlcpy(vmo.name, name, sizeof(vmo.name));
return vmo;
}
zx_status_t GetVmoCount(zx_handle_t parent, unsigned int children_kind,
void* out_buffer, size_t buffer_size, size_t* actual,
size_t* avail) {
if (process_handle_to_vmos_.count(parent) == 0) {
ADD_FAILURE() << "Warning: unexpected handle " << parent;
return ZX_ERR_BAD_HANDLE;
}
const std::vector<zx_info_vmo_t>& vmos = process_handle_to_vmos_.at(parent);
*avail = vmos.size();
return ZX_OK;
}
zx_status_t GetVmoInfo(zx_handle_t parent, unsigned int children_kind,
void* out_buffer, size_t buffer_size, size_t* actual,
size_t* avail) {
size_t capacity = buffer_size / sizeof(zx_info_vmo_t);
if (process_handle_to_vmos_.count(parent) == 0) {
ADD_FAILURE() << "Warning: unexpected handle " << parent;
return ZX_ERR_BAD_HANDLE;
}
const std::vector<zx_info_vmo_t>& vmos = process_handle_to_vmos_.at(parent);
*actual = std::min(vmos.size(), capacity);
*avail = vmos.size();
memcpy(out_buffer, (void*)vmos.data(), *actual * sizeof(zx_info_vmo_t));
return ZX_OK;
}
// Get a dockyard path for |koid| with the given |suffix| key.
std::string KoidPath(zx_koid_t koid, const std::string& suffix) {
std::ostringstream out;
out << "koid:" << koid << ":" << suffix;
return out.str();
}
uint64_t GetValueForPath(std::string path) {
uint64_t value;
EXPECT_TRUE(dockyard_proxy_.CheckValueSent(path, &value));
return value;
}
protected:
// Mocks.
NiceMock<MockTaskTree> task_tree_;
NiceMock<MockOS> os_;
// Test data.
harvester::DockyardProxyFake dockyard_proxy_;
std::vector<harvester::TaskTree::Task> processes_;
std::map<zx_handle_t, std::vector<zx_info_vmo_t>> process_handle_to_vmos_;
zx_handle_t info_resource_;
};
TEST_F(GatherVmosTest, NoRootedVmos) {
harvester::GatherVmos gatherer(info_resource_, &dockyard_proxy_, task_tree_,
&os_);
// Build a task tree of:
//
// 1 (Root job)
// / \
// 2 5
// / \
// 3 4
//
// Where everything but 1 is a process.
processes_ = {
// These tuples are {handle, koid, parent koid}.
// The top level parent 1 is hidden because it's not a process.
{2, 2, 1},
{3, 3, 2},
{4, 4, 2},
{5, 5, 1},
};
ON_CALL(task_tree_, Processes()).WillByDefault(ReturnRef(processes_));
// Add one VMO for each process, none of which are rooted.
for (auto& process : processes_) {
zx_info_vmo_t vmo = MakeVmo(100 + process.koid, 4096, 4096, "scudo");
process_handle_to_vmos_[process.handle] = {vmo};
}
ON_CALL(os_, GetInfo(_, _, IsNull(), _, _, _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoCount));
ON_CALL(os_, GetInfo(_, _, NotNull(), _, NotNull(), _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoInfo));
gatherer.Gather();
uint64_t test_value;
for (auto& process : processes_) {
EXPECT_TRUE(dockyard_proxy_.CheckValueSent(
KoidPath(process.koid, "vmo_Sysmem-core"), &test_value));
EXPECT_EQ(test_value, 0UL);
EXPECT_TRUE(dockyard_proxy_.CheckValueSent(
KoidPath(process.koid, "vmo_Sysmem-contig-core"), &test_value));
EXPECT_EQ(test_value, 0UL);
}
}
TEST_F(GatherVmosTest, RootedVmos_WithNestedDescendants) {
harvester::GatherVmos gatherer(info_resource_, &dockyard_proxy_, task_tree_,
&os_);
// Build a task tree of:
//
// 1 (Root job)
// / \
// 2 5
// / \
// 3 4
//
// Where everything but 1 is a process.
processes_ = {
// These tuples are {handle, koid, parent koid}.
// The top level parent 1 is hidden because it's not a process.
{2, 2, 1},
{3, 3, 2},
{4, 4, 2},
{5, 5, 1},
};
ON_CALL(task_tree_, Processes()).WillByDefault(ReturnRef(processes_));
// <koid:5> will hold the rooted VMO.
zx_info_vmo_t root_vmo = MakeVmo(105, 4096 * 4, 4096 * 4, "Sysmem-core");
process_handle_to_vmos_[5] = {root_vmo};
// <koid:2> will take one page to hand out later.
zx_info_vmo_t intermediate_vmo =
MakeVmoWithParent(102, root_vmo.koid, 4096, 4096, "Sysmem-core");
zx_info_vmo_t nonrooted_vmo = MakeVmo(202, 4096 * 2, 4096 * 2, "scudo");
process_handle_to_vmos_[2] = {intermediate_vmo, nonrooted_vmo};
// <koid:4> will take the page from <koid:2>. Even though the VMO has been
// assigned a slightly different name, GatherVmos will still find it using the
// parent/child relationship.
zx_info_vmo_t child_vmo = MakeVmoWithParent(104, intermediate_vmo.koid, 4096,
4096, "Sysmem-core-child");
process_handle_to_vmos_[4] = {child_vmo};
// <koid:3> gets a non-rooted page.
zx_info_vmo_t nonrooted_child_vmo =
MakeVmoWithParent(203, nonrooted_vmo.koid, 4096, 4096, "scudo");
process_handle_to_vmos_[3] = {nonrooted_child_vmo};
ON_CALL(os_, GetInfo(_, _, IsNull(), _, _, _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoCount));
ON_CALL(os_, GetInfo(_, _, NotNull(), _, NotNull(), _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoInfo));
gatherer.Gather();
// Check that <koid:1> has nothing sent since it's a job.
uint64_t test_value;
EXPECT_FALSE(dockyard_proxy_.CheckValueSent(KoidPath(1, "vmo_Sysmem-core"),
&test_value));
// Check that scudo information is not sent (it is not rooted memory).
EXPECT_FALSE(
dockyard_proxy_.CheckValueSent(KoidPath(1, "vmo_scudo"), &test_value));
EXPECT_FALSE(
dockyard_proxy_.CheckValueSent(KoidPath(1, "vmo_scudo"), &test_value));
// <koid:5> should retain 3 rooted pages, <koid:2> and <koid:3> none, and
// <koid:4> one page.
EXPECT_EQ(GetValueForPath(KoidPath(2, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(3, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(4, "vmo_Sysmem-core")), 4096UL);
EXPECT_EQ(GetValueForPath(KoidPath(5, "vmo_Sysmem-core")), 12288UL);
// No processes should have memory from a *different* sysmem VMO.
for (auto& process : processes_) {
EXPECT_EQ(GetValueForPath(KoidPath(process.koid, "vmo_Sysmem-contig-core")),
0UL);
}
}
TEST_F(GatherVmosTest, TracksChangesOverTime) {
harvester::GatherVmos gatherer(info_resource_, &dockyard_proxy_, task_tree_,
&os_);
// Build a list of processes all under root job 0.
processes_ = {
// These tuples are {handle, koid, parent koid}.
// The top level parent 0 is hidden because it's not a process.
{1, 1, 0}, {2, 2, 0}, {3, 3, 0}, {4, 4, 0}, {5, 5, 0}, {6, 6, 0},
};
ON_CALL(task_tree_, Processes()).WillByDefault(ReturnRef(processes_));
// On first (full) scan, no processes have VMOs.
for (auto& process : processes_) {
process_handle_to_vmos_[process.handle] = {};
}
ON_CALL(os_, GetInfo(_, _, IsNull(), _, _, _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoCount));
ON_CALL(os_, GetInfo(_, _, NotNull(), _, NotNull(), _))
.WillByDefault(Invoke(this, &GatherVmosTest::GetVmoInfo));
// Processes to be scanned: all.
gatherer.Gather();
// No processes should have sysmem VMOs.
for (auto& process : processes_) {
EXPECT_EQ(GetValueForPath(KoidPath(process.koid, "vmo_Sysmem-core")), 0UL);
}
// Give <koid:2> a rooted VMO.
zx_info_vmo_t core_vmo = MakeVmo(102, 4096, 4096, "Sysmem-core");
process_handle_to_vmos_[2] = {core_vmo};
// Kill <koid:4>.
processes_.erase(processes_.begin() + 3);
// Scan queue: [4, 5, 6, 1, 2, 3]. Gather() will ignore the dead <koid:4> and
// pull 3 processes [5, 6, 1].
gatherer.Gather();
// Only the first 3 processes should be checked; <koid:2>'s new sysmem VMO
// should not be detected yet.
for (auto& process : processes_) {
EXPECT_EQ(GetValueForPath(KoidPath(process.koid, "vmo_Sysmem-core")), 0UL);
}
// Add a new <koid:7> with a rooted VMO.
processes_.push_back({7, 7, 0});
zx_info_vmo_t contig_vmo = MakeVmo(107, 4096, 4096, "Sysmem-contig-core");
process_handle_to_vmos_[7] = {contig_vmo};
// Scan queue: [2, 3, 5, 6, 1, 7]. Gather will see <koid:2>'s VMO. New
// processes are always scanned.
gatherer.Gather();
EXPECT_EQ(GetValueForPath(KoidPath(1, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(2, "vmo_Sysmem-core")), 4096UL);
EXPECT_EQ(GetValueForPath(KoidPath(3, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(5, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(6, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(7, "vmo_Sysmem-core")), 0UL);
EXPECT_EQ(GetValueForPath(KoidPath(7, "vmo_Sysmem-contig-core")), 4096UL);
}