You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
525 lines
18 KiB
525 lines
18 KiB
/*
|
|
* Copyright (C) 2020 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
|
|
#include <chrono>
|
|
#include <list>
|
|
#include <map>
|
|
#include <random>
|
|
#include <regex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "perfetto/base/build_config.h"
|
|
#include "perfetto/base/compiler.h"
|
|
#include "perfetto/ext/base/ctrl_c_handler.h"
|
|
#include "perfetto/ext/base/file_utils.h"
|
|
#include "perfetto/ext/base/scoped_file.h"
|
|
#include "perfetto/ext/base/subprocess.h"
|
|
#include "perfetto/ext/base/temp_file.h"
|
|
#include "perfetto/ext/base/utils.h"
|
|
#include "perfetto/protozero/proto_utils.h"
|
|
#include "perfetto/tracing.h"
|
|
#include "perfetto/tracing/core/forward_decls.h"
|
|
#include "perfetto/tracing/core/trace_config.h"
|
|
#include "src/base/test/utils.h"
|
|
|
|
#include "protos/perfetto/config/stress_test_config.gen.h"
|
|
#include "protos/perfetto/trace/test_event.pbzero.h"
|
|
#include "protos/perfetto/trace/trace_packet.pbzero.h"
|
|
|
|
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
|
|
#include <Windows.h>
|
|
#else
|
|
#include <signal.h>
|
|
#endif
|
|
|
|
// Generated by gen_configs_blob.py. It defines the kStressTestConfigs array,
|
|
// which contains a proto-encoded StressTestConfig message for each .cfg file
|
|
// listed in /test/stress_test/configs/BUILD.gn.
|
|
#include "test/stress_test/configs/stress_test_config_blobs.h"
|
|
|
|
namespace perfetto {
|
|
namespace {
|
|
|
|
// TODO(primiano): We need a base::File to get around the awkwardness of
|
|
// files on Windows being a mix of int and HANDLE (and open() vs CreateFile())
|
|
// in our codebase.
|
|
base::ScopedPlatformHandle OpenLogFile(const std::string& path) {
|
|
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
|
|
return base::ScopedPlatformHandle(::CreateFileA(
|
|
path.c_str(), GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_DELETE | FILE_SHARE_READ, nullptr, CREATE_ALWAYS, 0, nullptr));
|
|
#else
|
|
return base::OpenFile(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
|
|
#endif
|
|
}
|
|
|
|
using StressTestConfig = protos::gen::StressTestConfig;
|
|
|
|
struct SigHandlerCtx {
|
|
std::atomic<bool> aborted{};
|
|
std::vector<int> pids_to_kill;
|
|
};
|
|
SigHandlerCtx* g_sig;
|
|
|
|
struct TestResult {
|
|
const char* cfg_name = nullptr;
|
|
StressTestConfig cfg;
|
|
uint32_t run_time_ms = 0;
|
|
uint32_t trace_size_kb = 0;
|
|
uint32_t num_packets = 0;
|
|
uint32_t num_threads = 0;
|
|
uint32_t num_errors = 0;
|
|
base::Subprocess::ResourceUsage svc_rusage;
|
|
base::Subprocess::ResourceUsage prod_rusage;
|
|
};
|
|
|
|
struct ParsedTraceStats {
|
|
struct WriterThread {
|
|
uint64_t packets_seen = 0;
|
|
bool last_seen = false;
|
|
uint32_t last_seq = 0;
|
|
uint64_t seq_errors = 0;
|
|
uint64_t counter_errors = 0;
|
|
std::minstd_rand0 rnd_engine;
|
|
};
|
|
|
|
// One for each trusted_packet_sequence_id.
|
|
std::map<uint32_t, WriterThread> threads;
|
|
};
|
|
|
|
class TestHarness {
|
|
public:
|
|
TestHarness();
|
|
void RunConfig(const char* cfg_name, const StressTestConfig&, bool verbose);
|
|
const std::list<TestResult>& test_results() const { return test_results_; }
|
|
|
|
private:
|
|
void ReadbackTrace(const std::string&, ParsedTraceStats*);
|
|
void ParseTracePacket(const uint8_t*, size_t, ParsedTraceStats* ctx);
|
|
void AddFailure(const char* fmt, ...) PERFETTO_PRINTF_FORMAT(2, 3);
|
|
|
|
std::vector<std::string> env_;
|
|
std::list<TestResult> test_results_;
|
|
std::string results_dir_;
|
|
base::ScopedFile error_log_;
|
|
};
|
|
|
|
TestHarness::TestHarness() {
|
|
results_dir_ = base::GetSysTempDir() + "/perfetto-stress-test";
|
|
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
|
|
base::ignore_result(system(("rmdir \"" + results_dir_ + "\" /s /q").c_str()));
|
|
#else
|
|
base::ignore_result(system(("rm -r -- \"" + results_dir_ + "\"").c_str()));
|
|
#endif
|
|
PERFETTO_CHECK(base::Mkdir(results_dir_));
|
|
PERFETTO_LOG("Saving test results in %s", results_dir_.c_str());
|
|
}
|
|
|
|
void TestHarness::AddFailure(const char* fmt, ...) {
|
|
++test_results_.back().num_errors;
|
|
|
|
char log_msg[512];
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
int res = vsnprintf(log_msg, sizeof(log_msg), fmt, args);
|
|
va_end(args);
|
|
|
|
PERFETTO_ELOG("FAIL: %s", log_msg);
|
|
|
|
if (res > 0 && static_cast<size_t>(res) < sizeof(log_msg) - 2) {
|
|
log_msg[res++] = '\n';
|
|
log_msg[res++] = '\0';
|
|
}
|
|
|
|
base::WriteAll(*error_log_, log_msg, static_cast<size_t>(res));
|
|
}
|
|
|
|
void TestHarness::RunConfig(const char* cfg_name,
|
|
const StressTestConfig& cfg,
|
|
bool verbose) {
|
|
test_results_.emplace_back();
|
|
TestResult& test_result = test_results_.back();
|
|
test_result.cfg_name = cfg_name;
|
|
test_result.cfg = cfg;
|
|
g_sig->pids_to_kill.clear();
|
|
|
|
auto result_dir = results_dir_ + "/" + cfg_name;
|
|
PERFETTO_CHECK(base::Mkdir(result_dir));
|
|
error_log_ = base::OpenFile(result_dir + "/errors.log",
|
|
O_RDWR | O_CREAT | O_TRUNC, 0644);
|
|
|
|
PERFETTO_ILOG("Starting \"%s\" - %s", cfg_name, result_dir.c_str());
|
|
|
|
env_.emplace_back("PERFETTO_PRODUCER_SOCK_NAME=" + result_dir +
|
|
"/producer.sock");
|
|
env_.emplace_back("PERFETTO_CONSUMER_SOCK_NAME=" + result_dir +
|
|
"/consumer.sock");
|
|
std::string bin_dir = base::GetCurExecutableDir();
|
|
|
|
// Start the service.
|
|
base::Subprocess traced({bin_dir + "/traced"});
|
|
traced.args.env = env_;
|
|
if (!verbose) {
|
|
traced.args.out_fd = OpenLogFile(result_dir + "/traced.log");
|
|
traced.args.stderr_mode = traced.args.stdout_mode = base::Subprocess::kFd;
|
|
}
|
|
traced.Start();
|
|
g_sig->pids_to_kill.emplace_back(traced.pid());
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
PERFETTO_CHECK(traced.Poll() == base::Subprocess::kRunning);
|
|
|
|
// Start the producer processes.
|
|
std::list<base::Subprocess> producers;
|
|
for (uint32_t i = 0; i < cfg.num_processes(); ++i) {
|
|
producers.emplace_back(base::Subprocess({bin_dir + "/stress_producer"}));
|
|
auto& producer = producers.back();
|
|
producer.args.input = cfg.SerializeAsString();
|
|
if (!verbose) {
|
|
producer.args.out_fd =
|
|
OpenLogFile(result_dir + "/producer." + std::to_string(i) + ".log");
|
|
producer.args.stderr_mode = producer.args.stdout_mode =
|
|
base::Subprocess::kFd;
|
|
}
|
|
producer.args.env = env_;
|
|
producer.Start();
|
|
g_sig->pids_to_kill.emplace_back(producer.pid());
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
for (auto& producer : producers)
|
|
PERFETTO_CHECK(producer.Poll() == base::Subprocess::kRunning);
|
|
|
|
auto trace_file_path = result_dir + "/trace";
|
|
base::Subprocess consumer(
|
|
{bin_dir + "/perfetto", "-c", "-", "-o", trace_file_path.c_str()});
|
|
consumer.args.env = env_;
|
|
consumer.args.input = cfg.trace_config().SerializeAsString();
|
|
if (!verbose) {
|
|
consumer.args.out_fd = OpenLogFile(result_dir + "/perfetto.log");
|
|
consumer.args.stderr_mode = consumer.args.stdout_mode =
|
|
base::Subprocess::kFd;
|
|
}
|
|
remove(trace_file_path.c_str());
|
|
consumer.Start();
|
|
int64_t t_start = base::GetBootTimeNs().count();
|
|
g_sig->pids_to_kill.emplace_back(consumer.pid());
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
PERFETTO_CHECK(consumer.Poll() == base::Subprocess::kRunning);
|
|
|
|
if (!consumer.Wait(
|
|
static_cast<int>(cfg.trace_config().duration_ms() + 30000))) {
|
|
AddFailure("Consumer didn't quit in time");
|
|
consumer.KillAndWaitForTermination();
|
|
}
|
|
|
|
int64_t t_end = base::GetBootTimeNs().count();
|
|
|
|
for (auto& producer : producers) {
|
|
producer.KillAndWaitForTermination();
|
|
test_result.prod_rusage = producer.posix_rusage(); // Only keep last one
|
|
}
|
|
producers.clear();
|
|
traced.KillAndWaitForTermination();
|
|
|
|
test_result.svc_rusage = traced.posix_rusage();
|
|
test_result.run_time_ms = static_cast<uint32_t>((t_end - t_start) / 1000000);
|
|
|
|
// Verify
|
|
// TODO(primiano): read back the TraceStats and check them as well.
|
|
ParsedTraceStats ctx;
|
|
ReadbackTrace(trace_file_path, &ctx);
|
|
auto exp_thd = cfg.num_processes() * cfg.num_threads();
|
|
if (ctx.threads.size() != exp_thd) {
|
|
AddFailure("Trace threads mismatch. Expected %u threads, got %zu", exp_thd,
|
|
ctx.threads.size());
|
|
}
|
|
for (const auto& it : ctx.threads) {
|
|
uint32_t seq_id = it.first;
|
|
const auto& thd = it.second;
|
|
if (!thd.last_seen) {
|
|
AddFailure("Last packet not seen for sequence %u", seq_id);
|
|
}
|
|
if (thd.seq_errors > 0) {
|
|
AddFailure("Sequence %u had %" PRIu64 " packets out of sync", seq_id,
|
|
thd.seq_errors);
|
|
}
|
|
if (thd.counter_errors > 0) {
|
|
AddFailure("Sequence %u had %" PRIu64 " packets counter errors", seq_id,
|
|
thd.counter_errors);
|
|
}
|
|
}
|
|
|
|
error_log_.reset();
|
|
PERFETTO_ILOG("Completed \"%s\"", cfg_name);
|
|
}
|
|
|
|
void TestHarness::ReadbackTrace(const std::string& trace_file_path,
|
|
ParsedTraceStats* ctx) {
|
|
TestResult& test_result = test_results_.back();
|
|
using namespace protozero::proto_utils;
|
|
auto fd = base::OpenFile(trace_file_path.c_str(), O_RDONLY);
|
|
if (!fd)
|
|
return AddFailure("Trace file does not exist");
|
|
const off_t file_size = lseek(*fd, 0, SEEK_END);
|
|
lseek(*fd, 0, SEEK_SET);
|
|
if (file_size <= 0)
|
|
return AddFailure("Trace file is empty");
|
|
|
|
test_result.trace_size_kb = static_cast<uint32_t>(file_size / 1000);
|
|
std::string trace_data;
|
|
PERFETTO_CHECK(base::ReadFileDescriptor(*fd, &trace_data));
|
|
const auto* const start = reinterpret_cast<const uint8_t*>(trace_data.data());
|
|
const uint8_t* const end = start + file_size;
|
|
|
|
constexpr uint8_t kTracePacketTag = MakeTagLengthDelimited(1);
|
|
|
|
for (auto* ptr = start; (end - ptr) > 2;) {
|
|
const uint8_t* tokenizer_start = ptr;
|
|
if (*(ptr++) != kTracePacketTag) {
|
|
return AddFailure("Tokenizer failure at offset %zd", ptr - start);
|
|
}
|
|
uint64_t packet_size = 0;
|
|
ptr = ParseVarInt(ptr, end, &packet_size);
|
|
const uint8_t* const packet_start = ptr;
|
|
ptr += packet_size;
|
|
if ((ptr - tokenizer_start) < 2 || ptr > end)
|
|
return AddFailure("Got invalid packet size %" PRIu64 " at offset %zd",
|
|
packet_size,
|
|
static_cast<ssize_t>(packet_start - start));
|
|
ParseTracePacket(packet_start, static_cast<size_t>(packet_size), ctx);
|
|
}
|
|
test_result.num_threads = static_cast<uint32_t>(ctx->threads.size());
|
|
}
|
|
|
|
void TestHarness::ParseTracePacket(const uint8_t* start,
|
|
size_t size,
|
|
ParsedTraceStats* ctx) {
|
|
TestResult& test_result = test_results_.back();
|
|
protos::pbzero::TracePacket::Decoder packet(start, size);
|
|
if (!packet.has_for_testing())
|
|
return;
|
|
|
|
++test_result.num_packets;
|
|
const uint32_t seq_id = packet.trusted_packet_sequence_id();
|
|
|
|
protos::pbzero::TestEvent::Decoder te(packet.for_testing());
|
|
auto t_it = ctx->threads.find(seq_id);
|
|
bool is_first_packet = false;
|
|
if (t_it == ctx->threads.end()) {
|
|
is_first_packet = true;
|
|
t_it = ctx->threads.emplace(seq_id, ParsedTraceStats::WriterThread()).first;
|
|
}
|
|
ParsedTraceStats::WriterThread& thd = t_it->second;
|
|
|
|
++thd.packets_seen;
|
|
if (te.is_last()) {
|
|
if (thd.last_seen) {
|
|
return AddFailure(
|
|
"last_seen=true happened more than once for sequence %u", seq_id);
|
|
} else {
|
|
thd.last_seen = true;
|
|
}
|
|
}
|
|
if (is_first_packet) {
|
|
thd.rnd_engine = std::minstd_rand0(te.seq_value());
|
|
} else {
|
|
const uint32_t expected = static_cast<uint32_t>(thd.rnd_engine());
|
|
if (te.seq_value() != expected) {
|
|
thd.rnd_engine = std::minstd_rand0(te.seq_value()); // Resync the engine.
|
|
++thd.seq_errors;
|
|
return AddFailure(
|
|
"TestEvent seq mismatch for sequence %u. Expected %u got %u", seq_id,
|
|
expected, te.seq_value());
|
|
}
|
|
if (te.counter() != thd.packets_seen) {
|
|
return AddFailure(
|
|
"TestEvent counter mismatch for sequence %u. Expected %" PRIu64
|
|
" got %" PRIu64,
|
|
seq_id, thd.packets_seen, te.counter());
|
|
}
|
|
}
|
|
|
|
if (!te.has_payload()) {
|
|
return AddFailure("TestEvent %u for sequence %u has no payload",
|
|
te.seq_value(), seq_id);
|
|
}
|
|
|
|
// Check the validity of the payload. The payload might be nested. If that is
|
|
// the case, we need to check all levels.
|
|
protozero::ConstBytes payload_bounds = te.payload();
|
|
for (uint32_t depth = 0, last_depth = 0;; depth++) {
|
|
if (depth > 100) {
|
|
return AddFailure("Unexpectedly deep depth for event %u, sequence %u",
|
|
te.seq_value(), seq_id);
|
|
}
|
|
protos::pbzero::TestEvent::TestPayload::Decoder payload(payload_bounds);
|
|
const uint32_t rem_depth = payload.remaining_nesting_depth();
|
|
|
|
// The payload is a repeated field and must have exactly two instances.
|
|
// The writer splits it always in two halves of identical size.
|
|
int num_payload_pieces = 0;
|
|
size_t last_size = 0;
|
|
for (auto it = payload.str(); it; ++it, ++num_payload_pieces) {
|
|
protozero::ConstChars payload_str = *it;
|
|
last_size = last_size ? last_size : payload_str.size;
|
|
if (payload_str.size != last_size) {
|
|
return AddFailure(
|
|
"Asymmetrical payload at depth %u, event id %u, sequence %u. "
|
|
"%zu != %zu",
|
|
depth, te.seq_value(), seq_id, last_size, payload_str.size);
|
|
}
|
|
// Check that the payload content matches the expected sequence.
|
|
for (size_t i = 0; i < payload_str.size; i++) {
|
|
char exp = static_cast<char>(33 + ((te.seq_value() + i) % 64));
|
|
if (payload_str.data[i] != exp) {
|
|
return AddFailure(
|
|
"Payload mismatch at %zu, depth %u, event id %u, sequence %u. "
|
|
"Expected: 0x%x, Actual: 0x%x",
|
|
i, depth, te.seq_value(), seq_id, exp, payload_str.data[i]);
|
|
}
|
|
}
|
|
}
|
|
if (num_payload_pieces != 2) {
|
|
return AddFailure(
|
|
"Broken payload at depth %u, event id %u, sequence %u. "
|
|
"Expecting 2 repeated str fields, got %d",
|
|
depth, te.seq_value(), seq_id, num_payload_pieces);
|
|
}
|
|
|
|
if (depth > 0 && rem_depth != last_depth - 1) {
|
|
return AddFailure(
|
|
"Unexpected nesting level (expected: %u, actual: %u) at depth %u, "
|
|
"event id %u, sequence %u",
|
|
rem_depth, last_depth - 1, depth, te.seq_value(), seq_id);
|
|
}
|
|
|
|
last_depth = rem_depth;
|
|
if (rem_depth == 0)
|
|
break;
|
|
if (payload.has_nested()) {
|
|
payload_bounds = *payload.nested();
|
|
} else {
|
|
payload_bounds = {nullptr, 0};
|
|
}
|
|
}
|
|
}
|
|
|
|
void CtrlCHandler() {
|
|
g_sig->aborted.store(true);
|
|
for (auto it = g_sig->pids_to_kill.rbegin(); it != g_sig->pids_to_kill.rend();
|
|
it++) {
|
|
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
|
|
base::ScopedPlatformHandle proc_handle(
|
|
::OpenProcess(PROCESS_TERMINATE, false, *it));
|
|
::TerminateProcess(*proc_handle, STATUS_CONTROL_C_EXIT);
|
|
#else
|
|
kill(*it, SIGKILL);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void StressTestMain(int argc, char** argv) {
|
|
TestHarness th;
|
|
std::regex filter;
|
|
bool has_filter = false;
|
|
|
|
bool verbose = false;
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (!strcmp(argv[i], "-v")) {
|
|
verbose = true;
|
|
} else {
|
|
filter = std::regex(argv[i], std::regex::ECMAScript | std::regex::icase);
|
|
has_filter = true;
|
|
}
|
|
}
|
|
|
|
g_sig = new SigHandlerCtx();
|
|
base::InstallCtrCHandler(&CtrlCHandler);
|
|
|
|
for (size_t i = 0; i < base::ArraySize(kStressTestConfigs) && !g_sig->aborted;
|
|
++i) {
|
|
const auto& cfg_blob = kStressTestConfigs[i];
|
|
StressTestConfig cfg;
|
|
std::cmatch ignored;
|
|
if (has_filter && !std::regex_search(cfg_blob.name, ignored, filter)) {
|
|
continue;
|
|
}
|
|
PERFETTO_CHECK(cfg.ParseFromArray(cfg_blob.data, cfg_blob.size));
|
|
th.RunConfig(cfg_blob.name, cfg, verbose);
|
|
}
|
|
|
|
for (const auto& tres : th.test_results()) {
|
|
const auto& cfg = tres.cfg;
|
|
printf("===============================================================\n");
|
|
printf("Config: %s\n", tres.cfg_name);
|
|
printf("===============================================================\n");
|
|
printf("%-20s %-10s %-10s\n", "Metric", "Expected", "Actual");
|
|
printf("%-20s %-10s %-10s\n", "------", "--------", "------");
|
|
printf("%-20s %-10d %-10d\n", "#Errors", 0, tres.num_errors);
|
|
printf("%-20s %-10d %-10d \n", "Duration [ms]",
|
|
cfg.trace_config().duration_ms(), tres.run_time_ms);
|
|
|
|
uint32_t exp_threads = cfg.num_processes() * cfg.num_threads();
|
|
printf("%-20s %-10u %-10u\n", "Num threads", exp_threads, tres.num_threads);
|
|
|
|
double dur_s = cfg.trace_config().duration_ms() / 1e3;
|
|
double exp_per_thread = cfg.steady_state_timings().rate_mean() * dur_s;
|
|
if (cfg.burst_period_ms()) {
|
|
double burst_rate = 1.0 * cfg.burst_duration_ms() / cfg.burst_period_ms();
|
|
exp_per_thread *= 1.0 - burst_rate;
|
|
exp_per_thread += burst_rate * cfg.burst_timings().rate_mean() * dur_s;
|
|
}
|
|
if (cfg.max_events())
|
|
exp_per_thread = std::min(exp_per_thread, 1.0 * cfg.max_events());
|
|
double exp_packets = std::round(exp_per_thread * exp_threads);
|
|
printf("%-20s %-10.0f %-10d\n", "Num packets", exp_packets,
|
|
tres.num_packets);
|
|
|
|
double exp_size_kb = exp_packets * (cfg.nesting() + 1) *
|
|
(cfg.steady_state_timings().payload_mean() + 40) /
|
|
1000;
|
|
printf("%-20s ~%-9.0f %-10d\n", "Trace size [KB]", exp_size_kb,
|
|
tres.trace_size_kb);
|
|
|
|
double exp_rss_mb = cfg.trace_config().buffers()[0].size_kb() / 1000;
|
|
printf("%-20s (max) %-4.0f %-10d\n", "Svc RSS [MB]", exp_rss_mb,
|
|
tres.svc_rusage.max_rss_kb / 1000);
|
|
printf("%-20s %-10s %-10d\n", "Svc CPU [ms]", "---",
|
|
tres.svc_rusage.cpu_time_ms());
|
|
printf("%-20s %-10s %d / %d\n", "Svc #ctxswitch", "---",
|
|
tres.svc_rusage.invol_ctx_switch, tres.svc_rusage.vol_ctx_switch);
|
|
|
|
printf("%-20s %-10s %-10d\n", "Prod RSS [MB]", "---",
|
|
tres.prod_rusage.max_rss_kb / 1000);
|
|
printf("%-20s %-10s %-10d\n", "Prod CPU [ms]", "---",
|
|
tres.prod_rusage.cpu_time_ms());
|
|
printf("%-20s %-10s %d / %d\n", "Prod #ctxswitch", "---",
|
|
tres.prod_rusage.invol_ctx_switch, tres.prod_rusage.vol_ctx_switch);
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace perfetto
|
|
|
|
int main(int argc, char** argv) {
|
|
perfetto::StressTestMain(argc, argv);
|
|
}
|