Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions include/silk/util/pmc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#pragma once

#include <cstdint>
#include <vector>

namespace silk
{

/**
* Measure hardware/software performance counters over a measured window.
*
* Opens a per-CPU event group on every CPU in the calling process's affinity
* mask (so it respects taskset/cgroup pinning). Each group counts, on its CPU:
*
* - CPU cycles (PERF_TYPE_HARDWARE / PERF_COUNT_HW_CPU_CYCLES)
* - instructions (PERF_TYPE_HARDWARE / PERF_COUNT_HW_INSTRUCTIONS)
* - context switches (PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CONTEXT_SWITCHES)
*
* Per-CPU scope (pid == -1) is what makes this honest for io_uring work: it
* captures kernel-side and async-offload (io-wq / sqpoll) cost that ran on the
* CPU, which a per-thread counter on the submitting thread would miss. The
* tradeoff is that it also counts anything else scheduled on those CPUs, so it
* is meant for benchmark box.
*
* The three events form one group (single group leader) so they are co-scheduled
* and the cycles/instructions ratio is internally consistent. Scaling for counter
* multiplexing is applied on read via TIME_ENABLED / TIME_RUNNING.
*
* Requires kernel.perf_event_paranoid <= 0 (per-CPU CPU-event access) or
* CAP_PERFMON. On failure the object stays stable: read() returns {valid=false}
* and the bracketing calls are no-ops, so callers can wire it in unconditionally.
*
* Lifecycle: open() -> [reset() enable() ... disable()] -> read() -> close().
* Counters start disabled after open().
*/
class Pmc
{
public:
struct Counts
{
uint64_t cycles = 0;
uint64_t instructions = 0;
uint64_t contextSwitches = 0;
/** False if open() failed (no permission / no PMU); other fields are 0. */
bool valid = false;
/** True if any CPU group was multiplexed and values were scaled up. */
bool scaled = false;
};

Pmc() noexcept = default;
~Pmc() noexcept { close(); }

Pmc(const Pmc &) = delete;
Pmc & operator=(const Pmc &) = delete;

/**
* Open per-CPU groups across the process affinity mask. Returns 0 on success,
* otherwise an errno (commonly EACCES when perf_event_paranoid is too high).
* On failure the object is inert and all other methods are safe no-ops.
*/
int open() noexcept;

/** Zero all counters (group-wide). No-op if not open. */
void reset() noexcept;

/** Start counting (group-wide). No-op if not open. */
void enable() noexcept;

/** Stop counting (group-wide). No-op if not open. */
void disable() noexcept;

/** Sum scaled counts across every CPU group. Returns {valid=false} if not open. */
Counts read() noexcept;

/** Close all file descriptors. Idempotent. */
void close() noexcept;

/** True if at least one per-CPU group is open. */
bool valid() const noexcept { return !groups.empty(); }

private:
struct CpuGroup
{
int cpu = -1;
int leaderFd = -1; // cycles; group leader
int instrFd = -1;
int csFd = -1;
};

std::vector<CpuGroup> groups;
};

} // namespace silk
27 changes: 27 additions & 0 deletions src/perf/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <silk/util/assert.h>
#include <silk/util/perf.h>
#include <silk/util/platform.h>
#include <silk/util/pmc.h>
#include <silk/util/tsc.h>

#include <algorithm>
Expand Down Expand Up @@ -146,6 +147,32 @@ void printCounters() noexcept
printf(" }\n");
}

void printPmc(const silk::Pmc::Counts & counts, uint64_t totalIos) noexcept
{
printf(" \"pmc\": {\n");
printf(" \"valid\": %s", counts.valid ? "true" : "false");
if (counts.valid)
{
double ipc = counts.cycles ? static_cast<double>(counts.instructions) / static_cast<double>(counts.cycles) : 0.0;
double cyclesPerIo = totalIos ? static_cast<double>(counts.cycles) / static_cast<double>(totalIos) : 0.0;
double instrPerIo = totalIos ? static_cast<double>(counts.instructions) / static_cast<double>(totalIos) : 0.0;

printf(",\n");
printf(" \"scaled\": %s,\n", counts.scaled ? "true" : "false");
printf(" \"cycles\": %lu,\n", counts.cycles);
printf(" \"instructions\": %lu,\n", counts.instructions);
printf(" \"context_switches\": %lu,\n", counts.contextSwitches);
printf(" \"ipc\": %.4f,\n", ipc);
printf(" \"cycles_per_io\": %.1f,\n", cyclesPerIo);
printf(" \"instructions_per_io\": %.1f\n", instrPerIo);
}
else
{
printf("\n");
}
printf(" }\n");
}

void printSchedulerLatency() noexcept
{
printf(" \"scheduler_latency\": {\n");
Expand Down
10 changes: 10 additions & 0 deletions src/perf/common.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <silk/util/pmc.h>

#include <cerrno>
#include <cstdint>
#include <random>
Expand Down Expand Up @@ -37,6 +39,14 @@ void printSchedulerLatency() noexcept;
*/
void printCounters() noexcept;

/**
* Print the "pmc" JSON section: aggregate hardware/software counts over the
* measured window, plus per-IO and ratio derivations (totalIos is the number
* of completed, post-warmup IOs the run measured).
* Outputs no trailing comma. Emits "valid": false when counters could not be opened.
*/
void printPmc(const silk::Pmc::Counts & counts, uint64_t totalIos) noexcept;

/**
* Parse a size string with optional k/m/g suffix (case-insensitive) into bytes.
* Examples: "4k" -> 4096, "1g" -> 1073741824, "512" -> 512.
Expand Down
25 changes: 23 additions & 2 deletions src/perf/file-perf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ struct ClientConfig
bool printCounters = false;
// use registered buffers (IORING_OP_READ_FIXED / IORING_OP_WRITE_FIXED).
bool fixedBuffers = false;
// collect per-CPU hardware/software PMCs over the measurement window.
bool pmc = false;
};

class Benchmark
Expand Down Expand Up @@ -346,7 +348,7 @@ int Benchmark::workerFiberMain(WorkerFiberParams * params) noexcept
return 0;
}

static void printJson(std::vector<uint64_t> & latNs, const ClientConfig & cfg)
static void printJson(std::vector<uint64_t> & latNs, const ClientConfig & cfg, const silk::Pmc::Counts & pmcCounts)
{
uint64_t total = latNs.size();
double durationS = static_cast<double>(cfg.durationNs) / 1e9;
Expand All @@ -365,6 +367,11 @@ static void printJson(std::vector<uint64_t> & latNs, const ClientConfig & cfg)
printf(" \"rps\": %.1f,\n", rps);
printf(" \"bw_bytes\": %.0f,\n", bwBytesS);
printLatencyUs(latNs);
if (cfg.pmc)
{
printf(",");
printPmc(pmcCounts, total);
}
if (cfg.printCounters)
{
printf(",");
Expand Down Expand Up @@ -406,6 +413,7 @@ int main(int argc, char ** argv)
("direct", "use O_DIRECT (bypass page cache)", cxxopts::value<bool>(cfg.direct))
("fixed-buffers", "use registered buffers (IORING_OP_READ_FIXED / WRITE_FIXED)", cxxopts::value<bool>(cfg.fixedBuffers))
("print-counters", "enable per-CPU profiler and include counters in the JSON report", cxxopts::value<bool>(cfg.printCounters))
("pmc", "collect per-CPU HW/SW perf counters (cycles, instructions, ctx-switches)", cxxopts::value<bool>(cfg.pmc))
("v,verbose", "enable debug logging", cxxopts::value<bool>(verbose))
;
// clang-format on
Expand Down Expand Up @@ -490,10 +498,23 @@ int main(int argc, char ** argv)
signalled = sigwaitFor(mask, cfg.warmupNs);
}

// Bracket the measurement window with per-CPU PMCs, matching the
// post-warmup latency-collection window (see warmupEndCycles).
silk::Pmc pmc;
silk::Pmc::Counts pmcCounts;
if (cfg.pmc)
{
pmc.open();
}

if (!signalled)
{
pmc.reset();
pmc.enable();
SILK_INFO("measuring for %s...", formatDuration(cfg.durationNs).c_str());
sigwaitFor(mask, cfg.durationNs);
pmc.disable();
pmcCounts = pmc.read();
}

pthread_sigmask(SIG_UNBLOCK, &mask, nullptr);
Expand All @@ -502,7 +523,7 @@ int main(int argc, char ** argv)
benchmark.stop();

std::vector<uint64_t> allLat = benchmark.collectLatencies();
printJson(allLat, cfg);
printJson(allLat, cfg, pmcCounts);

silk::FiberScheduler::destroy();
silk::destroy();
Expand Down
Loading