blob: 99310c3ceb2a1b86525e75a7c61eba0f31b9fbd7 [file] [log] [blame]
// Copyright 2025 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 <stdio.h>
#include <unistd.h>
#include <iomanip>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <thread>
#include "src/performance/memory/thrasher/lib.h"
// Forward declaration from lib.h to resolve build issue
std::string to_hex_string(const std::vector<uint8_t>& bytes);
#include <fidl/fuchsia.fxfs/cpp/wire.h>
#include <fidl/fuchsia.fxfs/cpp/wire_messaging.h>
#include <lib/async-testing/test_loop.h>
#include <lib/async/default.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fidl/cpp/wire/server.h>
#include <lib/sync/completion.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/sys/component/cpp/testing/realm_builder_types.h>
#include <lib/sys/cpp/testing/component_context_provider.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/clock.h>
#include <lib/zx/vmo.h>
#include <gtest/gtest.h>
#include <src/lib/testing/loop_fixture/real_loop_fixture.h>
namespace ffxfs = fuchsia_fxfs;
using component_testing::ChildRef;
using component_testing::LocalComponentFactory;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::RealmBuilder;
using component_testing::Route;
// Helper function to convert hex string to bytes
std::vector<uint8_t> hex_to_bytes(const std::string& hex) {
std::vector<uint8_t> bytes;
for (unsigned int i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
uint8_t byte = (uint8_t)strtol(byteString.c_str(), NULL, 16);
bytes.push_back(byte);
}
return bytes;
}
struct MockBlobInfo {
std::string merkle;
uint64_t size;
};
enum class MockGetVmoErrorType {
kNone,
kReplyError, // Call completer.ReplyError()
kFidlError, // Close channel to simulate transport error
kInvalidHandle, // ReplySuccess with an invalid VMO
kZeroSizeVmo, // ReplySuccess with a zero-size VMO
kChannelClose // Call completer.Close()
};
class MockBlobReaderImpl : public component_testing::LocalComponentImpl {
public:
explicit MockBlobReaderImpl(async_dispatcher_t* dispatcher, MockBlobReaderImpl** self_ptr)
: dispatcher_(dispatcher) {
*self_ptr = this;
}
void OnStart() override {
ASSERT_EQ(ZX_OK, outgoing()->AddProtocol<ffxfs::BlobReader>(bindings_.CreateHandler(
&server_, dispatcher_, fidl::kIgnoreBindingClosure)));
}
// Methods to control the mock
void set_mock_blobs(const std::vector<MockBlobInfo>& blobs) { server_.set_mock_blobs(blobs); }
void set_error_simulation(MockGetVmoErrorType type, zx_status_t error = ZX_OK) {
server_.set_error_simulation(type, error);
}
void set_manual_completion(bool manual) { server_.set_manual_completion(manual); }
bool has_pending_request() const { return server_.has_pending_request(); }
void ReplyToPendingRequest(zx::vmo vmo) { server_.ReplyToPendingRequest(std::move(vmo)); }
size_t get_call_count() const { return server_.get_call_count(); }
void set_close_channel_after_first_call(bool close) {
server_.set_close_channel_after_first_call(close);
}
private:
// Inner class to implement the FIDL protocol
// Inner class to implement the fidl::WireServer<ffxfs::BlobReader> protocol.
// This mock allows simulating various responses and errors from the BlobReader service.
class Server : public fidl::WireServer<ffxfs::BlobReader> {
public:
void GetVmo(GetVmoRequestView request, GetVmoCompleter::Sync& completer) override {
if (call_count_ > 0 && close_channel_after_first_call_) {
completer.Close(ZX_ERR_PEER_CLOSED);
return;
}
call_count_++;
// Handle manual completion mode for tests that need to control the reply timing.
if (manual_completion_) {
pending_completer_ = completer.ToAsync();
return;
}
switch (error_type_) {
case MockGetVmoErrorType::kReplyError:
completer.ReplyError(force_error_);
return;
case MockGetVmoErrorType::kFidlError:
completer.Close(ZX_ERR_INTERNAL); // Simulate transport error
return;
case MockGetVmoErrorType::kChannelClose:
completer.Close(force_error_); // Or a specific error like ZX_ERR_PEER_CLOSED
return;
case MockGetVmoErrorType::kInvalidHandle:
completer.ReplySuccess(zx::vmo()); // Send invalid handle
return;
case MockGetVmoErrorType::kZeroSizeVmo: {
zx::vmo vmo;
zx_status_t status = zx::vmo::create(0, 0, &vmo);
if (status != ZX_OK) {
completer.ReplyError(status);
} else {
completer.ReplySuccess(std::move(vmo));
}
return;
}
case MockGetVmoErrorType::kNone:
// Proceed to blob lookup logic if no error simulation is active.
break;
}
std::string merkle_root = to_hex_string(fidl::VectorView<uint8_t>::FromExternal(
request->blob_hash.data(), request->blob_hash.size()));
for (const auto& blob : mock_blobs_) {
if (blob.merkle == merkle_root) {
zx::vmo parent_vmo;
zx_status_t status = zx::vmo::create(blob.size, 0, &parent_vmo);
if (status != ZX_OK) {
completer.ReplyError(status);
return;
}
// Write some data to the parent VMO to ensure it has pages.
std::vector<uint8_t> data(blob.size, 0xAA);
status = parent_vmo.write(data.data(), 0, blob.size);
if (status != ZX_OK) {
completer.ReplyError(status);
return;
}
uint64_t vmo_size;
status = parent_vmo.get_size(&vmo_size);
if (status != ZX_OK) {
completer.ReplyError(status);
return;
}
status = parent_vmo.op_range(ZX_VMO_OP_COMMIT, 0, vmo_size, nullptr, 0);
if (status != ZX_OK) {
completer.ReplyError(status);
return;
}
// Create a reference child VMO to return, simulating Fxfs behavior.
// Size must be 0 for ZX_VMO_CHILD_REFERENCE (b/393402141).
zx::vmo child_vmo;
status = parent_vmo.create_child(ZX_VMO_CHILD_REFERENCE | ZX_VMO_CHILD_NO_WRITE, 0, 0,
&child_vmo);
if (status != ZX_OK) {
completer.ReplyError(status);
return;
}
// We must keep the parent VMO alive for the child to be valid if it's a reference?
// Actually, for REFERENCE, if the parent dies, the child might be invalid or empty?
// Let's keep it alive in the mock for now to be safe, or maybe it's fine if it dies
// if it's just a reference to pages?
// Re-reading vmo-reference.cc: "Closing the parent VMO will not affect the reference."
// Wait, is that true?
// "The reference VMO will continue to point to the same pages even if the parent VMO is
// closed." Let's assume it's fine to let parent_vmo go out of scope here if it works like
// that. Actually, if it's a reference to *pages*, and parent goes away, who owns the
// pages? "ZX_VMO_CHILD_REFERENCE creates a VMO that shares the same pages as the parent
// VMO." If parent is closed, the pages might be freed if nothing else holds them. Let's
// keep parent VMOs alive in the mock just in case.
parent_vmos_.push_back(std::move(parent_vmo));
completer.ReplySuccess(std::move(child_vmo));
return;
}
}
completer.ReplyError(ZX_ERR_NOT_FOUND); // Default if merkle root not found in mock_blobs_
}
void set_mock_blobs(const std::vector<MockBlobInfo>& blobs) { mock_blobs_ = blobs; }
void set_error_simulation(MockGetVmoErrorType type, zx_status_t error = ZX_OK) {
error_type_ = type;
force_error_ = error;
}
void set_manual_completion(bool manual) { manual_completion_ = manual; }
bool has_pending_request() const { return pending_completer_.has_value(); }
void ReplyToPendingRequest(zx::vmo vmo) {
ASSERT_TRUE(pending_completer_.has_value());
pending_completer_->ReplySuccess(std::move(vmo));
pending_completer_.reset();
}
size_t get_call_count() const { return call_count_; }
void set_close_channel_after_first_call(bool close) { close_channel_after_first_call_ = close; }
private:
using GetVmoRequestView = fidl::WireServer<ffxfs::BlobReader>::GetVmoRequestView;
using GetVmoCompleter = fidl::WireServer<ffxfs::BlobReader>::GetVmoCompleter;
std::string to_hex_string(const fidl::VectorView<uint8_t>& vec) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
for (uint8_t byte : vec) {
ss << std::setw(2) << static_cast<int>(byte);
}
return ss.str();
}
std::vector<MockBlobInfo> mock_blobs_;
MockGetVmoErrorType error_type_ = MockGetVmoErrorType::kNone;
zx_status_t force_error_ = ZX_OK;
size_t call_count_ = 0;
bool manual_completion_ = false;
std::optional<GetVmoCompleter::Async> pending_completer_;
std::vector<zx::vmo> parent_vmos_;
bool close_channel_after_first_call_ = false;
};
async_dispatcher_t* dispatcher_;
Server server_;
fidl::ServerBindingGroup<ffxfs::BlobReader> bindings_;
};
namespace fio = fuchsia_io;
class ThrasherTest : public gtest::RealLoopFixture {
protected:
MockBlobReaderImpl* mock_blob_reader_ptr_ =
nullptr; // Pointer to the mock instance, set by the factory.
std::unique_ptr<component_testing::RealmRoot> realm_; // Manages the test realm.
// Sets up the test realm using RealmBuilder.
// Adds the MockBlobReaderImpl as a local component.
// Optionally routes the fuchsia.fxfs.BlobReader protocol from the mock to the realm's parent.
fidl::ClientEnd<ffxfs::BlobReader> CreateRealm(bool route_blob_reader = true) {
auto realm_builder = component_testing::RealmBuilder::Create();
mock_blob_reader_ptr_ = nullptr;
realm_builder.AddLocalChild(
"mock_blob_reader",
[&]() -> std::unique_ptr<component_testing::LocalComponentImpl> {
auto mock =
std::make_unique<MockBlobReaderImpl>(this->dispatcher(), &mock_blob_reader_ptr_);
return mock;
},
component_testing::ChildOptions{.startup_mode = component_testing::StartupMode::EAGER});
if (route_blob_reader) {
realm_builder.AddRoute(
component_testing::Route{.capabilities = {component_testing::Protocol{
fidl::DiscoverableProtocolName<ffxfs::BlobReader>}},
.source = component_testing::ChildRef{"mock_blob_reader"},
.targets = {component_testing::ParentRef()}});
}
realm_builder.AddRoute(component_testing::Route{
.capabilities = {component_testing::Protocol{"fuchsia.logger.LogSink"}},
.source = component_testing::ParentRef(),
.targets = {component_testing::ChildRef{"mock_blob_reader"}}});
realm_ =
std::make_unique<component_testing::RealmRoot>(realm_builder.Build(this->dispatcher()));
// Connect to the service within the realm
auto client_end = realm_->component().Connect<ffxfs::BlobReader>();
if (!client_end.is_ok()) {
FX_LOGS(ERROR) << "CreateRealm: Failed to connect to BlobReader from realm: "
<< client_end.status_string();
return {};
}
// Wait for the mock to be created by the AddLocalChild lambda.
RunLoopUntil([&] { return mock_blob_reader_ptr_ != nullptr; });
return std::move(client_end.value());
}
// Tears down the test realm, ensuring components are stopped and resources are freed.
// The event loop is run to allow asynchronous teardown tasks to complete.
void TearDown() override {
if (realm_) {
bool complete = false;
realm_->Teardown([&](fit::result<fuchsia::component::Error> result) { complete = true; });
RunLoopUntil([&]() { return complete; });
}
gtest::RealLoopFixture::TearDown();
}
// Tears down the test realm, ensuring components are stopped and resources are freed.
// The event loop is run to allow asynchronous teardown tasks to complete.
};
TEST_F(ThrasherTest, ParentVmoAccountingTest) {
const size_t kPageSize = zx_system_get_page_size();
const size_t kVmoSize = kPageSize * 4;
zx::vmo parent_vmo;
ASSERT_EQ(zx::vmo::create(kVmoSize, 0, &parent_vmo), ZX_OK);
// Commit pages to the parent VMO.
ASSERT_EQ(parent_vmo.op_range(ZX_VMO_OP_COMMIT, 0, kVmoSize, nullptr, 0), ZX_OK);
// Verify parent VMO has committed bytes.
zx_info_vmo_t parent_info;
ASSERT_EQ(parent_vmo.get_info(ZX_INFO_VMO, &parent_info, sizeof(parent_info), nullptr, nullptr),
ZX_OK);
EXPECT_EQ(parent_info.committed_bytes, kVmoSize);
EXPECT_EQ(parent_info.parent_koid, 0u); // It's a root VMO
// Create a reference child VMO.
zx::vmo child_vmo;
// Size must be 0 for ZX_VMO_CHILD_REFERENCE (b/393402141)
ASSERT_EQ(
parent_vmo.create_child(ZX_VMO_CHILD_REFERENCE | ZX_VMO_CHILD_NO_WRITE, 0, 0, &child_vmo),
ZX_OK);
// Verify child VMO reports 0 committed bytes via ZX_INFO_VMO.
zx_info_vmo_t child_info;
ASSERT_EQ(child_vmo.get_info(ZX_INFO_VMO, &child_info, sizeof(child_info), nullptr, nullptr),
ZX_OK);
EXPECT_EQ(child_info.committed_bytes, 0u);
EXPECT_EQ(child_info.parent_koid, parent_info.koid);
// In this test we have handles to both, so we can verify the accounting works if we use the
// parent. This confirms the strategy: if we can find the parent, we can get the bytes.
// Call log_vmos to verify our new accounting logic.
// It should be able to find parent_vmo because we hold a handle to it in this process.
std::vector<zx::vmo> vmos_to_log;
zx::vmo child_dup;
ASSERT_EQ(child_vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &child_dup), ZX_OK);
vmos_to_log.push_back(std::move(child_dup));
LogVmos(vmos_to_log, true);
// Map the child VMO and check ZX_INFO_PROCESS_MAPS.
uintptr_t mapped_addr;
ASSERT_EQ(zx::vmar::root_self()->map(ZX_VM_PERM_READ, 0, child_vmo, 0, kVmoSize, &mapped_addr),
ZX_OK);
// Touch the pages to be sure.
volatile uint8_t* ptr = reinterpret_cast<uint8_t*>(mapped_addr);
for (size_t i = 0; i < kVmoSize; i += kPageSize) {
(void)ptr[i];
}
zx_info_handle_basic_t child_basic_info;
ASSERT_EQ(child_vmo.get_info(ZX_INFO_HANDLE_BASIC, &child_basic_info, sizeof(child_basic_info),
nullptr, nullptr),
ZX_OK);
zx_koid_t child_koid = child_basic_info.koid;
size_t actual = 0;
size_t avail = 0;
ASSERT_EQ(zx::process::self()->get_info(ZX_INFO_PROCESS_MAPS, nullptr, 0, &actual, &avail),
ZX_OK);
std::vector<zx_info_maps_t> maps(avail);
ASSERT_EQ(zx::process::self()->get_info(ZX_INFO_PROCESS_MAPS, maps.data(),
avail * sizeof(zx_info_maps_t), &actual, &avail),
ZX_OK);
bool found_mapping = false;
for (size_t i = 0; i < actual; ++i) {
if (maps[i].type == ZX_INFO_MAPS_TYPE_MAPPING && maps[i].u.mapping.vmo_koid == child_koid) {
found_mapping = true;
// Expect 0 committed bytes for reference child mapping too, based on previous findings.
EXPECT_EQ(maps[i].u.mapping.committed_bytes, 0u);
break;
}
}
EXPECT_TRUE(found_mapping);
ASSERT_EQ(zx::vmar::root_self()->unmap(mapped_addr, kVmoSize), ZX_OK);
}
void RunThrashTest(std::shared_ptr<Thrasher> thrasher, async::Loop& loop) {
bool init_done = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
while (!init_done) {
loop.RunUntilIdle();
}
ASSERT_EQ(init_status, ZX_OK);
// Callbacks are now passed to Start()
loop.Run();
}
void RunThrashTestWithConfig(
fit::function<std::shared_ptr<Thrasher>(ThrashConfig config)> create_fn) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
bool done = false;
std::vector<zx::vmo> vmos;
ThrashConfig config = {
.bursts_per_second = 100,
.run_for_seconds = 1,
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = loop.dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> result) {
vmos = std::move(result);
done = true;
loop.Quit();
});
auto thrasher = create_fn(std::move(config));
ASSERT_NE(thrasher, nullptr);
bool init_done = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
while (!init_done) {
loop.RunUntilIdle();
}
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
loop.Run();
ASSERT_TRUE(done);
ASSERT_FALSE(vmos.empty());
for (const auto& vmo : vmos) {
ASSERT_TRUE(vmo.is_valid());
}
}
TEST_F(ThrasherTest, AddLocalChildCallbackTest) {
auto client_end = CreateRealm();
ASSERT_TRUE(client_end.is_valid()) << "Failed to connect to BlobReader";
// Bind to an async client to make a call to force startup
fidl::WireClient<ffxfs::BlobReader> client(std::move(client_end), this->dispatcher());
uint8_t hash[32] = {0};
fidl::Array<uint8_t, 32> fidl_hash;
std::copy(std::begin(hash), std::end(hash), fidl_hash.begin());
bool call_complete = false;
client->GetVmo(fidl_hash).Then(
[&](fidl::WireUnownedResult<ffxfs::BlobReader::GetVmo>& result) { call_complete = true; });
// Run the loop to allow the call to go out and the component to start.
RunLoopUntil([&] { return mock_blob_reader_ptr_ != nullptr; });
EXPECT_NE(mock_blob_reader_ptr_, nullptr) << "mock_blob_reader_ptr_ was not set";
// Unbind the client before destroying the realm to avoid hanging.
client = {};
}
TEST_F(ThrasherTest, AnonThrasherInvalidSizeTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
ThrashConfig config = {
.dispatcher = loop.dispatcher(),
};
auto thrasher = CreateAnonThrasher(std::move(config), 0);
ASSERT_NE(thrasher, nullptr);
bool init_done = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
RunLoopUntil([&] { return init_done; });
EXPECT_EQ(init_status, ZX_ERR_INVALID_ARGS);
}
TEST(Thrasher, SmokeTest) {
RunThrashTestWithConfig(
[](ThrashConfig config) { return CreateAnonThrasher(std::move(config), 1024 * 1024); });
}
TEST_F(ThrasherTest, MmapThrasherNonExistentFileTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
ThrashConfig config = {
.dispatcher = loop.dispatcher(),
};
auto thrasher = CreateMmapThrasher(std::move(config), "/pkg/data/non_existent_file");
ASSERT_NE(thrasher, nullptr);
bool init_done = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
RunLoopUntil([&] { return init_done; });
EXPECT_EQ(init_status, ZX_ERR_IO);
}
TEST(Thrasher, MappedFileSmokeTest) {
RunThrashTestWithConfig([](ThrashConfig config) {
return CreateMmapThrasher(std::move(config), "/pkg/data/test_data");
});
}
TEST(Thrasher, MultiThreadedAnonymous) {
RunThrashTestWithConfig([](ThrashConfig config) {
config.num_threads = 4;
return CreateAnonThrasher(std::move(config), 1024 * 1024);
});
}
TEST(Thrasher, MultiThreadedMappedFile) {
RunThrashTestWithConfig([](ThrashConfig config) {
config.num_threads = 4;
return CreateMmapThrasher(std::move(config), "/pkg/data/test_data");
});
}
TEST_F(ThrasherTest, DirThrasherNonExistentDirTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
ThrashConfig config = {
.dispatcher = loop.dispatcher(),
};
auto thrasher = CreateDirThrasher(std::move(config), "/pkg/data/non_existent_dir");
ASSERT_NE(thrasher, nullptr);
bool init_done = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
RunLoopUntil([&] { return init_done; });
EXPECT_EQ(init_status, ZX_OK); // opendir failing on a path is not an init error for DirThrasher
// Start should also complete fine, but with no work done.
bool callback_called = false;
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
loop.Quit();
});
thrasher->Start(thrash_callback, nullptr);
loop.Run();
EXPECT_TRUE(callback_called);
}
TEST_F(ThrasherTest, DirThrasherEmptyDirTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
ThrashConfig config = {
.dispatcher = loop.dispatcher(),
};
auto thrasher = CreateDirThrasher(std::move(config), "/pkg/data/empty_dir");
ASSERT_NE(thrasher, nullptr);
bool init_done = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_status = status;
init_done = true;
});
RunLoopUntil([&] { return init_done; });
EXPECT_EQ(init_status, ZX_OK);
// Start should also complete fine, but with no work done.
bool callback_called = false;
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
loop.Quit();
});
thrasher->Start(thrash_callback, nullptr);
loop.Run();
EXPECT_TRUE(callback_called);
}
TEST(Thrasher, DirectoryTest) {
RunThrashTestWithConfig([](ThrashConfig config) {
return CreateDirThrasher(std::move(config), "/pkg/data/test_dir");
});
}
// This test is no longer valid as the check is in the Thrasher class constructor/init.
// We test initialization failure in blob tests.
// Test case for when BlobReader returns an error
TEST_F(ThrasherTest, ThrashBlobsErrorTest) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
// Set the mock to return an error
mock_blob_reader_ptr_->set_error_simulation(MockGetVmoErrorType::kReplyError, ZX_ERR_INTERNAL);
std::vector<MockBlobInfo> mock_blobs = {
{"1111111111111111111111111111111111111111111111111111111111111111", 1000},
};
mock_blob_reader_ptr_->set_mock_blobs(mock_blobs);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_OK); // Initialization succeeds even if GetVmo will fail.
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
EXPECT_TRUE(callback_called);
}
// Test case for when BlobReader succeeds
TEST_F(ThrasherTest, ThrashBlobsSuccessTest) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
std::vector<MockBlobInfo> mock_blobs = {
{"2222222222222222222222222222222222222222222222222222222222222222", 2000},
{"3333333333333333333333333333333333333333333333333333333333333333", 3000},
};
mock_blob_reader_ptr_->set_mock_blobs(mock_blobs);
ASSERT_TRUE(client_end.is_valid());
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
ASSERT_EQ(vmos.size(), 2u);
uint64_t size;
vmos[0].get_size(&size);
EXPECT_EQ(size, 4096u);
vmos[1].get_size(&size);
EXPECT_EQ(size, 4096u);
QuitLoop();
});
std::vector<std::string> merkle_roots = {
{"2222222222222222222222222222222222222222222222222222222222222222"},
{"3333333333333333333333333333333333333333333333333333333333333333"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
}
TEST(Thrasher, LogVmosTest) {
std::vector<zx::vmo> vmos;
// Test with empty vector
LogVmos(vmos, true);
// Test with valid VMOs
zx::vmo vmo1;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo1), ZX_OK);
vmos.push_back(std::move(vmo1));
zx::vmo vmo2;
ASSERT_EQ(zx::vmo::create(8192, 0, &vmo2), ZX_OK);
vmos.push_back(std::move(vmo2));
LogVmos(vmos, true);
// Test with an invalid VMO
vmos.clear();
zx::vmo invalid_vmo;
vmos.push_back(std::move(invalid_vmo));
LogVmos(vmos, true);
}
// Test case for an invalid merkle root
TEST_F(ThrasherTest, ThrashBlobsInvalidMerkleTest) {
auto client_end = CreateRealm();
ASSERT_NE(realm_, nullptr);
// Run the loop to allow the component to start and the factory lambda to execute.
RunLoopUntilIdle();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
// No blobs set in the mock, so any merkle will be not found.
mock_blob_reader_ptr_->set_mock_blobs({});
std::vector<std::string> merkle_roots = {
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"};
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
std::cerr << "[" << zx_clock_get_monotonic()
<< "] ThrashBlobsInvalidMerkleTest: RunLoopUntil returned" << std::endl;
EXPECT_TRUE(callback_called);
}
// Test case for VMO error during GetVmo
TEST_F(ThrasherTest, ThrashBlobsVmoGetSizeErrorTest) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
ASSERT_TRUE(client_end.is_valid());
mock_blob_reader_ptr_->set_error_simulation(MockGetVmoErrorType::kZeroSizeVmo);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
EXPECT_TRUE(vmos.empty());
callback_called = true;
QuitLoop();
});
std::vector<MockBlobInfo> mock_blobs = {
{"4444444444444444444444444444444444444444444444444444444444444444", 1000},
};
mock_blob_reader_ptr_->set_mock_blobs(mock_blobs);
std::vector<std::string> merkle_roots = {
{"4444444444444444444444444444444444444444444444444444444444444444"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
EXPECT_TRUE(callback_called);
}
// Test case for BlobThrasher with bursts_per_second = 0
TEST_F(ThrasherTest, ThrashBlobsInvalidConfigBursts) {
auto client_end = CreateRealm();
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.bursts_per_second = 0,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
std::vector<std::string> merkle_roots = {
"1111111111111111111111111111111111111111111111111111111111111111"};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
bool init_called = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_ERR_INVALID_ARGS);
}
// Test case for BlobThrasher with num_threads = 0
TEST_F(ThrasherTest, ThrashBlobsInvalidConfigThreads) {
auto client_end = CreateRealm();
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 0,
.dispatcher = dispatcher(),
};
std::vector<std::string> merkle_roots = {
"1111111111111111111111111111111111111111111111111111111111111111"};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
bool init_called = false;
zx_status_t init_status = ZX_OK;
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_ERR_INVALID_ARGS);
}
// Test case for a merkle root that is not 64 characters long
TEST_F(ThrasherTest, ThrashBlobsShortMerkleTest) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
std::vector<std::string> merkle_roots = {"short_merkle"};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
EXPECT_TRUE(callback_called);
}
// Test case for when an invalid client handle is passed to the thrashing manager.
TEST_F(ThrasherTest, ThrashBlobsInvalidClientHandle) {
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
std::vector<std::string> merkle_roots = {
"1111111111111111111111111111111111111111111111111111111111111111"};
auto thrasher = CreateBlobThrasherWithClient(
std::move(config), fidl::ClientEnd<ffxfs::BlobReader>(), merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
if (status != ZX_OK) {
QuitLoop();
}
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_ERR_BAD_HANDLE);
// Don't call Start() if Initialize failed.
// The Initialize callback should have called QuitLoop().
}
// Test case for when no merkle roots are provided to the thrashing manager.
TEST_F(ThrasherTest, ThrashBlobsNoMerkleRoots) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
std::vector<std::string> merkle_roots = {};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
EXPECT_TRUE(callback_called);
}
// Test case for FIDL error during GetVmo
TEST_F(ThrasherTest, ThrashBlobsGetVmoFidlErrorTest) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
mock_blob_reader_ptr_->set_error_simulation(MockGetVmoErrorType::kFidlError);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ASSERT_TRUE(client_end.is_valid());
ThrashConfig config = {
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
EXPECT_TRUE(vmos.empty());
callback_called = true;
QuitLoop();
});
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
if (status != ZX_OK) {
QuitLoop();
}
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_ERR_INTERNAL);
EXPECT_FALSE(callback_called);
}
class ThrasherUnroutedTest : public gtest::RealLoopFixture {
protected:
std::unique_ptr<component_testing::RealmRoot> realm_;
MockBlobReaderImpl* mock_blob_reader_ptr_ = nullptr;
fidl::ClientEnd<ffxfs::BlobReader> CreateRealm() {
auto realm_builder = component_testing::RealmBuilder::Create();
// We need a mock implementation even if we don't route it, just to have a valid component to
// add. We don't need to capture the pointer since we won't be interacting with it.
realm_builder.AddLocalChild(
"mock_blob_reader",
[&]() -> std::unique_ptr<component_testing::LocalComponentImpl> {
return std::make_unique<MockBlobReaderImpl>(this->dispatcher(), &mock_blob_reader_ptr_);
},
component_testing::ChildOptions{.startup_mode = component_testing::StartupMode::EAGER});
// Deliberately NOT adding a route for BlobReader from mock_blob_reader to Parent.
realm_builder.AddRoute(component_testing::Route{
.capabilities = {component_testing::Protocol{"fuchsia.logger.LogSink"}},
.source = component_testing::ParentRef(),
.targets = {component_testing::ChildRef{"mock_blob_reader"}}});
realm_ =
std::make_unique<component_testing::RealmRoot>(realm_builder.Build(this->dispatcher()));
// Connect to the service within the realm - this should succeed in getting a channel,
// but the channel will be closed by component manager when it tries to route it.
auto client_end = realm_->component().Connect<ffxfs::BlobReader>();
if (!client_end.is_ok()) {
return {};
}
return std::move(client_end.value());
}
void TearDown() override {
if (realm_) {
bool complete = false;
realm_->Teardown([&](fit::result<fuchsia::component::Error> result) { complete = true; });
RunLoopUntil([&]() { return complete; });
}
gtest::RealLoopFixture::TearDown();
}
};
TEST_F(ThrasherTest, ThrashBlobsUnroutedTest) {
auto client_end = CreateRealm(false);
ASSERT_TRUE(client_end.is_valid());
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_TRUE(vmos.empty());
QuitLoop();
});
auto shared_callback = thrash_callback;
// Try with multiple blobs to ensure we only log the error once (verified manually by looking at
// logs)
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"},
{"2222222222222222222222222222222222222222222222222222222222222222"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
if (status != ZX_OK) {
QuitLoop();
}
});
RunLoopUntil([&] { return init_called; });
EXPECT_EQ(init_status, ZX_ERR_NOT_FOUND);
EXPECT_FALSE(callback_called);
// Verify that GetVmo was called exactly once (for the probe).
// This test doesn't have a mock_blob_reader_ptr_ to check call count.
// The mock is in a child realm and not directly accessible.
}
TEST_F(ThrasherTest, ConnectionErrorStopsSubsequentCalls) {
auto client_end = CreateRealm();
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
// Simulate a connection error (e.g., peer closed) after the first call.
mock_blob_reader_ptr_->set_error_simulation(MockGetVmoErrorType::kNone);
mock_blob_reader_ptr_->set_close_channel_after_first_call(true);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_EQ(vmos.size(), 1u);
QuitLoop();
});
auto shared_callback = thrash_callback;
std::vector<MockBlobInfo> mock_blobs = {
{"1111111111111111111111111111111111111111111111111111111111111111", 1000},
{"2222222222222222222222222222222222222222222222222222222222222222", 2000},
};
mock_blob_reader_ptr_->set_mock_blobs(mock_blobs);
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"},
{"2222222222222222222222222222222222222222222222222222222222222222"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
if (status != ZX_OK) {
QuitLoop(); // Should not happen in this test
}
});
RunLoopUntil([&]() { return init_called; });
EXPECT_EQ(init_status, ZX_OK); // Initial probe succeeds.
thrasher->Start(thrash_callback, nullptr);
RunLoopUntil([&]() { return callback_called; });
EXPECT_TRUE(callback_called);
// Verify that GetVmo was called exactly once.
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 1u);
}
TEST_F(ThrasherTest, ThrashBlobsSequentialTest) {
auto client_end = CreateRealm();
mock_blob_reader_ptr_->set_manual_completion(true);
sync_completion_t completion;
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
EXPECT_EQ(vmos.size(), 2u);
callback_called = true;
sync_completion_signal(&completion);
});
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"},
{"2222222222222222222222222222222222222222222222222222222222222222"}};
auto thrasher = CreateBlobThrasherWithClient(std::move(config), std::move(client_end),
merkle_roots, 1024 * 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
// Wait for the first request during Initialize
RunLoopUntil([&]() { return mock_blob_reader_ptr_->has_pending_request(); });
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 1u);
// Reply to the first request
zx::vmo vmo1;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo1), ZX_OK);
mock_blob_reader_ptr_->ReplyToPendingRequest(std::move(vmo1));
// Wait for the second request
RunLoopUntil([&]() { return mock_blob_reader_ptr_->has_pending_request(); });
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 2u);
// Reply to the second request
zx::vmo vmo2;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo2), ZX_OK);
mock_blob_reader_ptr_->ReplyToPendingRequest(std::move(vmo2));
// Now Initialize should complete
RunLoopUntil([&]() { return init_called; });
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
// Wait for finish
RunLoopUntil([&] { return callback_called; });
// Wait for the async task to signal completion
ASSERT_EQ(sync_completion_wait(&completion, ZX_TIME_INFINITE), ZX_OK);
}
TEST_F(ThrasherTest, ThrashBlobsMemoryLimitTest) {
auto client_end = CreateRealm();
mock_blob_reader_ptr_->set_manual_completion(true);
bool callback_called = false;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
callback_called = true;
EXPECT_EQ(vmos.size(), 2u); // Should have fetched two blobs (one over limit)
QuitLoop();
});
std::vector<std::string> merkle_roots = {
{"1111111111111111111111111111111111111111111111111111111111111111"},
{"2222222222222222222222222222222222222222222222222222222222222222"},
{"3333333333333333333333333333333333333333333333333333333333333333"}};
auto thrasher =
CreateBlobThrasherWithClient(std::move(config), std::move(client_end), merkle_roots, 5000);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
// Wait for first request during Initialize
RunLoopUntil([&]() { return mock_blob_reader_ptr_->has_pending_request(); });
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 1u);
// Reply with a 4096 byte VMO. This should succeed (total 4096 <= 5000).
zx::vmo vmo1;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo1), ZX_OK);
mock_blob_reader_ptr_->ReplyToPendingRequest(std::move(vmo1));
// Wait for second request. This should happen because we allow going over the limit once.
RunLoopUntil([&]() { return mock_blob_reader_ptr_->has_pending_request(); });
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 2u);
// Reply with another 4096 byte VMO. This should succeed, but trigger the limit (total 8192 >
// 5000).
zx::vmo vmo2;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo2), ZX_OK);
mock_blob_reader_ptr_->ReplyToPendingRequest(std::move(vmo2));
// The third request should NOT happen because we are now over the limit.
// Initialize should complete.
RunLoopUntil([&]() { return init_called; });
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, nullptr);
// The third request should NOT happen because we are now over the limit.
// Instead, AllGetVmosDone should be called, leading to the callback.
RunLoopUntil([&]() { return callback_called || mock_blob_reader_ptr_->has_pending_request(); });
if (mock_blob_reader_ptr_->has_pending_request()) {
// If we got here, it means the limit failed to stop the third request.
// Reply to unblock and fail.
zx::vmo vmo3;
ASSERT_EQ(zx::vmo::create(4096, 0, &vmo3), ZX_OK);
mock_blob_reader_ptr_->ReplyToPendingRequest(std::move(vmo3));
RunLoopUntil([&]() { return callback_called; });
}
EXPECT_TRUE(callback_called);
// We expect 2 calls because the third is skipped due to the memory limit.
EXPECT_EQ(mock_blob_reader_ptr_->get_call_count(), 2u);
}
TEST_F(ThrasherTest, ParentVmoAccountingNoParentHandleTest) {
const size_t kVmoSize = zx_system_get_page_size();
zx::vmo parent_vmo;
ASSERT_EQ(zx::vmo::create(kVmoSize, 0, &parent_vmo), ZX_OK);
// Commit pages in parent.
ASSERT_EQ(parent_vmo.op_range(ZX_VMO_OP_COMMIT, 0, kVmoSize, nullptr, 0), ZX_OK);
// Get parent KOID.
zx_info_handle_basic_t parent_basic_info;
ASSERT_EQ(parent_vmo.get_info(ZX_INFO_HANDLE_BASIC, &parent_basic_info, sizeof(parent_basic_info),
nullptr, nullptr),
ZX_OK);
zx_koid_t parent_koid = parent_basic_info.koid;
// Create a reference child VMO.
zx::vmo child_vmo;
// Size must be 0 for ZX_VMO_CHILD_REFERENCE (b/393402141)
ASSERT_EQ(
parent_vmo.create_child(ZX_VMO_CHILD_REFERENCE | ZX_VMO_CHILD_NO_WRITE, 0, 0, &child_vmo),
ZX_OK);
// Close parent handle.
parent_vmo.reset();
// Call log_vmos to verify it fails gracefully when parent is missing.
std::vector<zx::vmo> vmos_to_log;
zx::vmo child_dup;
ASSERT_EQ(child_vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &child_dup), ZX_OK);
vmos_to_log.push_back(std::move(child_dup));
LogVmos(vmos_to_log, true);
// Now check ZX_INFO_PROCESS_VMOS to see if we can still find the parent.
size_t actual = 0;
size_t avail = 0;
ASSERT_EQ(zx::process::self()->get_info(ZX_INFO_PROCESS_VMOS, nullptr, 0, &actual, &avail),
ZX_OK);
// The number of VMOs can fluctuate, so we might need a loop or a larger buffer.
// For this test, a reasonably large buffer should suffice.
std::vector<zx_info_vmo_t> vmos(avail + 100);
ASSERT_EQ(zx::process::self()->get_info(ZX_INFO_PROCESS_VMOS, vmos.data(),
vmos.size() * sizeof(zx_info_vmo_t), &actual, &avail),
ZX_OK);
bool found_parent = false;
for (size_t i = 0; i < actual; ++i) {
if (vmos[i].koid == parent_koid) {
found_parent = true;
// If we found it, check if it reports committed bytes.
EXPECT_GT(vmos[i].committed_bytes, 0u);
break;
}
}
// We expect NOT to find it if we don't have a handle or mapping.
if (found_parent) {
} else {
}
// This isn't strictly a pass/fail for the *test*, but a discovery step.
// But let's assert our expectation to document it.
}
TEST(Thrasher, StatusReportingTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
bool done = false;
int status_updates = 0;
uint64_t total_touches_delta = 0;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
// Run for at least 2 seconds to get at least one status update (assuming 1s interval)
.run_for_seconds = 2,
.num_threads = 4,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = loop.dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo>) {
done = true;
loop.Quit();
});
auto status_callback = std::make_shared<StatusCallback>([&](const ThrashStatus& status) {
status_updates++;
total_touches_delta += status.touches_delta;
EXPECT_EQ(status.thrasher_type, "anon");
EXPECT_GT(status.total_memory_bytes, 0u);
EXPECT_GT(status.time_delta.to_msecs(), 0);
EXPECT_GE(status.total_touches, status.touches_delta);
EXPECT_GE(status.distinct_pages_delta, 0u);
EXPECT_GT(status.total_time.to_msecs(), 0);
});
auto thrasher = CreateAnonThrasher(std::move(config), 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
while (!init_called) {
loop.RunUntilIdle();
}
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, status_callback);
loop.Run();
ASSERT_TRUE(done);
// We expect at least one status update if it runs for 2 seconds and updates every 1s.
EXPECT_GT(status_updates, 0);
EXPECT_GT(total_touches_delta, 0u);
}
TEST(Thrasher, StatusReportingIntervalTest) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
bool done = false;
int status_updates = 0;
bool init_called = false;
zx_status_t init_status = ZX_ERR_INTERNAL;
ThrashConfig config = {
.bursts_per_second = 1000,
.run_for_seconds = 1,
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = loop.dispatcher(),
// Set a short interval to get multiple updates in 1 second
.status_interval_ms = 200,
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo>) {
done = true;
loop.Quit();
});
auto status_callback = std::make_shared<StatusCallback>([&](const ThrashStatus& status) {
status_updates++;
// Allow some jitter, but it should be close to 200ms
EXPECT_NEAR(static_cast<double>(status.time_delta.to_msecs()), 200.0, 100.0);
EXPECT_GT(status.total_time.to_msecs(), 0);
EXPECT_GE(status.distinct_pages_delta, 0u);
});
auto thrasher = CreateAnonThrasher(std::move(config), 1024 * 1024);
ASSERT_NE(thrasher, nullptr);
thrasher->Initialize([&](zx_status_t status) {
init_called = true;
init_status = status;
});
while (!init_called) {
loop.RunUntilIdle();
}
ASSERT_EQ(init_status, ZX_OK);
thrasher->Start(thrash_callback, status_callback);
loop.Run();
ASSERT_TRUE(done);
// In 1 second with 200ms interval, we expect roughly 4-5 updates.
// Allow some slack for thread scheduling.
EXPECT_GE(status_updates, 3);
}
TEST_F(ThrasherTest, AnonAndBlobConcurrent) {
auto client_end = CreateRealm();
ASSERT_TRUE(client_end.is_valid());
ASSERT_NE(mock_blob_reader_ptr_, nullptr);
std::vector<MockBlobInfo> mock_blobs = {
{"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", 4096},
};
mock_blob_reader_ptr_->set_mock_blobs(mock_blobs);
std::vector<std::string> merkle_roots = {
{"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}};
std::atomic<int> completed_thrashers = 0;
const int num_thrashers = 2;
std::vector<zx::vmo> collected_vmos;
std::mutex vmo_mutex;
ThrashConfig config = {
.bursts_per_second = 100,
.run_for_seconds = 5, // Short run time for testing
.num_threads = 1,
.pages_per_read = 1,
.consecutive_pages_per_read = 1,
.dispatcher = dispatcher(),
};
auto thrash_callback = std::make_shared<ThrashCallback>([&](std::vector<zx::vmo> vmos) {
std::lock_guard<std::mutex> lock(vmo_mutex);
for (auto& vmo : vmos) {
collected_vmos.push_back(std::move(vmo));
}
if (++completed_thrashers == num_thrashers) {
QuitLoop();
}
});
auto anon_thrasher = CreateAnonThrasher(config, 1024 * 1024);
auto blob_thrasher =
CreateBlobThrasherWithClient(config, std::move(client_end), merkle_roots, 10 * 1024 * 1024);
ASSERT_NE(anon_thrasher, nullptr);
ASSERT_NE(blob_thrasher, nullptr);
std::atomic<int> pending_inits = 2;
std::atomic<bool> init_failed = false;
zx_status_t anon_status = ZX_ERR_INTERNAL;
zx_status_t blob_status = ZX_ERR_INTERNAL;
anon_thrasher->Initialize([&](zx_status_t status) {
anon_status = status;
if (status != ZX_OK)
init_failed.store(true);
pending_inits--;
});
blob_thrasher->Initialize([&](zx_status_t status) {
blob_status = status;
if (status != ZX_OK)
init_failed.store(true);
pending_inits--;
});
RunLoopUntil([&]() { return pending_inits == 0; });
ASSERT_FALSE(init_failed.load());
ASSERT_EQ(anon_status, ZX_OK);
ASSERT_EQ(blob_status, ZX_OK);
anon_thrasher->Start(thrash_callback, nullptr);
blob_thrasher->Start(thrash_callback, nullptr);
// This is expected to time out currently
RunLoopWithTimeout(zx::sec(10));
EXPECT_EQ(completed_thrashers, num_thrashers) << "Test timed out, possible hang";
} // namespace ffxfs