blob: 67b897fd5b5662783f27707a712fc17ea59c98ec [file] [log] [blame]
// Copyright 2021 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 <lib/async-loop/cpp/loop.h>
#include <lib/driver/testing/cpp/driver_runtime.h>
#include <lib/fdf/cpp/env.h>
#include <lib/sync/cpp/completion.h>
#include <perftest/perftest.h>
#include "device_interface.h"
#include "network_device_shim.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "test_session.h"
#define ZX_ASSERT_OK(status, msg) \
ZX_ASSERT_MSG((status) == ZX_OK, msg " %s", zx_status_get_string(status))
namespace network {
class FakeDeviceImpl : public ddk::NetworkPortProtocol<FakeDeviceImpl>,
public ddk::NetworkDeviceImplProtocol<FakeDeviceImpl> {
public:
static constexpr uint16_t kDepth = 256;
static constexpr uint16_t kPortId = 1;
static constexpr uint32_t kMtu = 1500;
static constexpr uint8_t kRxFrameTypes[] = {
static_cast<uint8_t>(netdev::wire::FrameType::kEthernet),
};
static constexpr frame_type_support_t kTxFrameTypes[] = {
{.type = static_cast<uint8_t>(netdev::wire::FrameType::kEthernet)},
};
FakeDeviceImpl(perftest::RepeatState* state) : perftest_state_(state) {}
void NetworkDeviceImplInit(const network_device_ifc_protocol_t* iface,
network_device_impl_init_callback callback, void* cookie) {
iface_ = ddk::NetworkDeviceIfcProtocolClient(iface);
using Context = std::tuple<network_device_impl_init_callback, void*>;
std::unique_ptr context = std::make_unique<Context>(callback, cookie);
iface_.AddPort(
kPortId, this, &network_port_protocol_ops_,
[](void* ctx, zx_status_t status) {
std::unique_ptr<Context> context(static_cast<Context*>(ctx));
auto [callback, cookie] = *context;
ZX_ASSERT_OK(status, "AddPort failed");
callback(cookie, status);
},
context.release());
}
void NetworkDeviceImplStart(network_device_impl_start_callback callback, void* cookie) {
callback(cookie, ZX_OK);
}
void NetworkDeviceImplStop(network_device_impl_stop_callback callback, void* cookie) {
callback(cookie);
}
void NetworkDeviceImplGetInfo(device_impl_info_t* out_info) {
*out_info = {
.tx_depth = kDepth,
.rx_depth = kDepth,
.rx_threshold = kDepth,
.max_buffer_length = ZX_PAGE_SIZE / 2,
.buffer_alignment = ZX_PAGE_SIZE,
};
}
void NetworkDeviceImplQueueTx(const tx_buffer_t* buf_list, size_t buf_count) {
ZX_ASSERT_MSG(buf_count <= kDepth, "received %ld tx buffers (depth = %d)", buf_count, kDepth);
// NB: This may be called on a thread different than the test thread. To guarantee this doesn't
// happen concurrently with other perftest actions, the latency test must make sure that no
// descriptors belong to the device upon each test iteration.
perftest_state_->NextStep();
std::array<tx_result_t, kDepth> result;
auto iter = result.begin();
for (auto& buff : cpp20::span(buf_list, buf_count)) {
*iter++ = {
.id = buff.id,
.status = ZX_OK,
};
}
iface_.CompleteTx(&*result.begin(), buf_count);
}
void NetworkDeviceImplQueueRxSpace(const rx_space_buffer_t* buf_list, size_t buf_count) {
ZX_ASSERT_MSG(buf_count <= kDepth, "received %ld tx buffers (depth = %d)", buf_count, kDepth);
// NB: This may be called on a thread different than the test thread. To guarantee this doesn't
// happen concurrently with other perftest actions, the latency test must make sure that no
// descriptors belong to the device upon each test iteration.
perftest_state_->NextStep();
std::array<rx_buffer_t, kDepth> result;
std::array<rx_buffer_part_t, kDepth> parts;
auto result_iter = result.begin();
auto part_iter = parts.begin();
for (auto& buff : cpp20::span(buf_list, buf_count)) {
auto& part = *part_iter++;
part = {
.id = buff.id,
// Any length different than zero will cause the buffer to reach the session, it's
// irrelevant for the performance test.
.length = 1024,
};
*result_iter++ = {
.meta =
{
.port = kPortId,
.frame_type = static_cast<uint8_t>(netdev::wire::FrameType::kEthernet),
},
.data_list = &part,
.data_count = 1};
}
iface_.CompleteRx(&*result.begin(), buf_count);
}
ddk::NetworkDeviceImplProtocolClient client() {
network_device_impl_protocol_t proto = {
.ops = &network_device_impl_protocol_ops_,
.ctx = this,
};
return ddk::NetworkDeviceImplProtocolClient(&proto);
}
void NetworkDeviceImplPrepareVmo(uint8_t vmo_id, zx::vmo vmo,
network_device_impl_prepare_vmo_callback callback,
void* cookie) {
callback(cookie, ZX_OK);
}
void NetworkDeviceImplReleaseVmo(uint8_t vmo_id) {}
void NetworkDeviceImplSetSnoop(bool snoop) { ZX_PANIC("unexpected call to SetSnoop(%d)", snoop); }
void NetworkPortGetInfo(port_base_info_t* out_info) {
*out_info = {
.port_class = static_cast<uint8_t>(netdev::wire::DeviceClass::kEthernet),
.rx_types_list = kRxFrameTypes,
.rx_types_count = std::size(kRxFrameTypes),
.tx_types_list = kTxFrameTypes,
.tx_types_count = std::size(kTxFrameTypes),
};
}
void NetworkPortGetStatus(port_status_t* out_status) {
*out_status = {
.flags = static_cast<uint32_t>(netdev::wire::StatusFlags::kOnline),
.mtu = kMtu,
};
}
void NetworkPortSetActive(bool active) {}
void NetworkPortGetMac(mac_addr_protocol_t** out_mac_ifc) { *out_mac_ifc = &mac_addr_proto_; }
void NetworkPortRemoved() {}
private:
ddk::NetworkDeviceIfcProtocolClient iface_;
mac_addr_protocol_t mac_addr_proto_{};
perftest::RepeatState* const perftest_state_;
};
} // namespace network
// NB: BaseTestSession is laid out to make the contract clear, we avoid declaring variables with it
// to avoid dynamic dispatch.
class BaseTestSession : public network::testing::TestSession {
public:
virtual ~BaseTestSession() = default;
virtual zx_status_t SendDescriptors(const uint16_t* descriptors, size_t count,
size_t* actual) = 0;
virtual zx_status_t FetchDescriptors(uint16_t* descriptors, size_t count, size_t* actual) = 0;
virtual const zx::fifo& test_fifo() = 0;
};
class TxTestSession : public BaseTestSession {
public:
zx_status_t SendDescriptors(const uint16_t* descriptors, size_t count, size_t* actual) override {
return SendTx(descriptors, count, actual);
}
zx_status_t FetchDescriptors(uint16_t* descriptors, size_t count, size_t* actual) override {
return FetchTx(descriptors, count, actual);
}
const zx::fifo& test_fifo() override { return tx_fifo(); }
};
class RxTestSession : public BaseTestSession {
public:
zx_status_t SendDescriptors(const uint16_t* descriptors, size_t count, size_t* actual) override {
return SendRx(descriptors, count, actual);
}
zx_status_t FetchDescriptors(uint16_t* descriptors, size_t count, size_t* actual) override {
return FetchRx(descriptors, count, actual);
}
const zx::fifo& test_fifo() override { return rx_fifo(); }
};
// LatencyTest measures the round trip latency between a client and a device using an in-process
// fake network device.
//
// The total round trip latency is the time taken for the client to send a batch of packet buffers
// to the device and get them back. This breaks down as follows:
// - The outbound latency is the time between writing to the FIFO and observing the buffers
// reaching the device.
// - The return latency is the time it takes from the device fulfilling those buffers and the
// client observing them be returned on the FIFO.
//
// The template parameter determines which FIFO and, hence, which path (rx/tx), we're measuring the
// total latency on. Another variation on the test is the number of buffers offered and returned in
// a single batch (limited to the device's FIFO depth).
template <class Session>
bool LatencyTest(perftest::RepeatState* state, const uint16_t buffer_count) {
ZX_ASSERT_MSG(buffer_count <= network::FakeDeviceImpl::kDepth,
"can't measure latency with more buffers (%d) than device depth (%d)", buffer_count,
network::FakeDeviceImpl::kDepth);
zx::result dispatchers = network::OwnedDeviceInterfaceDispatchers::Create();
ZX_ASSERT_OK(dispatchers.status_value(), "failed to create dispatchers");
zx::result shim_dispatchers = network::OwnedShimDispatchers::Create();
ZX_ASSERT_OK(shim_dispatchers.status_value(), "failed to create shim dispatchers");
network::FakeDeviceImpl impl(state);
std::unique_ptr shim =
std::make_unique<network::NetworkDeviceShim>(impl.client(), shim_dispatchers->Unowned());
zx::result device_status =
network::internal::DeviceInterface::Create(dispatchers->Unowned(), std::move(shim));
ZX_ASSERT_OK(device_status.status_value(), "failed to create device");
std::unique_ptr device = std::move(device_status.value());
auto device_endpoints = fidl::Endpoints<network::netdev::Device>::Create();
ZX_ASSERT_OK(device->Bind(std::move(device_endpoints.server)), "failed to bind to device");
auto port_endpoints = fidl::Endpoints<network::netdev::Port>::Create();
ZX_ASSERT_OK(device->BindPort(network::FakeDeviceImpl::kPortId, std::move(port_endpoints.server)),
"failed to bind port");
fidl::WireSyncClient port{(std::move(port_endpoints.client))};
fidl::WireResult port_info_result = port->GetInfo();
ZX_ASSERT_OK(port_info_result.status(), "failed to get port info");
const network::netdev::wire::PortInfo& port_info = port_info_result->info;
ZX_ASSERT_MSG(port_info.has_id(), "port id missing");
const network::netdev::wire::PortId& port_id = port_info.id();
Session session;
fidl::WireSyncClient client{std::move(device_endpoints.client)};
zx_status_t status =
session.Open(client, "session", network::netdev::wire::SessionFlags::kPrimary, buffer_count);
ZX_ASSERT_OK(status, "failed to open session");
status = session.AttachPort(port_id, {network::netdev::wire::FrameType::kEthernet});
ZX_ASSERT_OK(status, "failed to attach port");
std::array<uint16_t, network::FakeDeviceImpl::kDepth> write_descriptors, returned_descriptors;
for (uint16_t i = 0; i < buffer_count; i++) {
buffer_descriptor_t& descriptor = session.ResetDescriptor(i);
// Tx tests need to set the port id here.
descriptor.port_id = {
.base = port_id.base,
.salt = port_id.salt,
};
write_descriptors[i] = i;
}
state->DeclareStep("outbound");
state->DeclareStep("return");
while (state->KeepRunning()) {
size_t actual;
status = session.SendDescriptors(&*write_descriptors.begin(), buffer_count, &actual);
ZX_ASSERT_OK(status, "failed to send descriptors");
ZX_ASSERT_MSG(actual == buffer_count, "partial FIFO write %ld/%d", actual, buffer_count);
status = session.test_fifo().wait_one(ZX_FIFO_READABLE, zx::time::infinite(), nullptr);
ZX_ASSERT_OK(status, "wait FIFO readable");
status = session.FetchDescriptors(&*returned_descriptors.begin(), buffer_count, &actual);
ZX_ASSERT_OK(status, "failed to fetch descriptors");
// Guarantee that all descriptors we sent come back to us, so the device can't be making any
// work in its background threads.
ZX_ASSERT_MSG(actual == buffer_count, "unexpected partial FIFO batch read %ld/%d", actual,
buffer_count);
}
sync_completion_t completion;
device->Teardown([&completion]() { sync_completion_signal(&completion); });
status = sync_completion_wait(&completion, zx::duration::infinite().get());
ZX_ASSERT_OK(status, "sync_completion_wait(_, _) failed ");
dispatchers->ShutdownSync();
shim_dispatchers->ShutdownSync();
return true;
}
void RegisterTests() {
constexpr uint16_t kBatchSizes[] = {1, 8, 16, 64, 256};
for (auto& batch_size : kBatchSizes) {
perftest::RegisterTest(fxl::StringPrintf("Latency/Rx/%d", batch_size).c_str(),
LatencyTest<RxTestSession>, batch_size);
perftest::RegisterTest(fxl::StringPrintf("Latency/Tx/%d", batch_size).c_str(),
LatencyTest<TxTestSession>, batch_size);
}
}
PERFTEST_CTOR(RegisterTests)
int main(int argc, char** argv) {
fdf_testing::DriverRuntime runtime;
constexpr char kTestSuiteName[] = "fuchsia.network.device";
return perftest::PerfTestMain(argc, argv, kTestSuiteName);
}