Skip to main content

Counter System

Overview

Chronon provides per-unit counters via the Counter class. Each unit instance declares its own counters as members, and they are automatically registered with the observation system when the unit's context is attached.

Counter (Per-Unit)

Declare counters as unit members for automatic per-instance tracking:

class ALUUnit : public TickableUnit, public ObservableUnit {
// Per-instance counters - each ALU has its own
Counter ops_{this, "ops", "Operations executed", "ops"};
Counter stalls_{this, "stalls", "Stall cycles", "cycles"};

public:
void tick() override {
++ops_; // ~2-3ns
stalls_ += 5; // Add 5
}
};

Output (CSV)

Pivoted format (default, csv_format: pivoted): Rows are dump cycles, columns are counters — compact and written incrementally:

cycle,alu0.ops,alu1.ops,alu2.ops
10000,2500,2400,2550
20000,5100,4900,5050
30000,7650,7300,7600

Each periodic dump appends one row, so the file can be monitored during simulation (like log files). Column names are discovered from the first dump and written as the CSV header.

Long format (csv_format: long): One row per (cycle, unit, counter, value) — traditional streaming format:

cycle,unit_name,counter_name,value
10000,alu0,ops,2500
10000,alu1,ops,2400
10000,alu2,ops,2550

Delta Semantics

Counter values are per-interval deltas — each row shows only the count for that dump interval. Counters reset to zero at the source after each periodic dump snapshot.

cycle,alu0.ops,alu1.ops
10000,2500,2400
20000,2600,2500
30000,2550,2400

This makes interval-local rate metrics (hit rate, IPC, stall rate) directly computable from a single row without post-processing.

Counter API

class Counter {
public:
Counter(ObservableUnit* owner,
std::string_view name,
std::string_view description = "",
std::string_view unit = "");

Counter& operator++() noexcept;
Counter& operator+=(uint64_t delta) noexcept;

uint64_t get() const noexcept;
void reset() noexcept;
};

Hierarchical Naming

Counter paths include the unit's full tree hierarchy:

# With unit.setTreeNode(node)
cpu0.alu0.ops
cpu0.alu1.ops
cpu1.alu0.ops

Note: Hierarchical naming depends on the unit being attached to a TreeNode hierarchy via setTreeNode(). Without this, counter names use the unit's local name only.

Storage Architecture

SimpleCounter

Optimized for Chronon's single-threaded-per-unit model:

struct SimpleCounter {
uint64_t value = 0;
uint64_t epoch_base = 0; // For rollback

void increment(uint64_t delta = 1) noexcept { value += delta; }
uint64_t get() const noexcept { return value; }

void commitEpoch() noexcept; // epoch_base = value
void rollbackEpoch() noexcept; // value = epoch_base
};

FixedCounterStorage

Dynamic array that grows as counters are added:

class FixedCounterStorage {
public:
// Constructor starts empty; grows via addCounter()
explicit FixedCounterStorage(std::string name);

CounterId addCounter(const std::string& name,
const std::string& description = "",
const std::string& unit = "");

SimpleCounter& getUnchecked(CounterId id) noexcept; // ~1-2ns

void commitAllEpochs();
void rollbackAllEpochs();
void resetAll();
};

Sizing Behavior:

  • Starts empty, grows as Counter members call addCounter() during context attachment
  • Enables O(1) getUnchecked() access without bounds checking

Memory Comparison

Memory usage scales with the number of counters per unit:

ArchitecturePer-Unit (17 counters)9 Units (17 counters each)
Old (Dense)64 KB576 KB
New (Sparse)272 bytes2.4 KB
Savings99.6%99.6%

Formula: Per-unit memory = N counters × 16 bytes

Registration-Based Pull Model

Counters register with ObservationManager at initialization:

1. Unit Construction
└─► Counter members created (pending)

2. Context Attachment
└─► Counter::onContextAttached(ctx)
└─► ctx.counters().addCounter(name, desc, unit)

3. Counter Snapshots
└─► manager.dumpCounterSnapshots(cycle)
└─► Read from registered addresses (lock-free)

Epoch Operations

For lookahead/speculative execution:

SimpleCounter counter;

counter.increment(100);
counter.commitEpoch(); // epoch_base = 100

// Speculative execution
counter.increment(50); // value = 150

// Rollback on misprediction
counter.rollbackEpoch(); // value = 100

// Or commit on success
counter.commitEpoch(); // epoch_base = 150

Derived Counters

Derived counters compute values from raw counters at CSV dump time. They add zero overhead to the simulation hot path — all computation happens in the ObservationBackend thread.

Declaration

class Fetch : public TickableUnit, public ObservableUnit {
Counter hits_{this, "hits", "Cache hits"};
Counter misses_{this, "misses", "Cache misses"};
Counter retired_{this, "retired", "Instructions retired"};
Counter cycles_{this, "cycles", "Active cycles"};

// Convenience formula: a / (a + b)
DerivedCounter hit_rate_{this, "hit_rate", "Cache hit rate",
{hits_, misses_}, DerivedFormula::Ratio};

// Custom lambda — any computation
DerivedCounter ipc_{this, "ipc", "Instructions per cycle",
{retired_, cycles_},
[](std::span<const uint64_t> v) {
return v[1] > 0 ? double(v[0]) / v[1] : 0.0;
}};

// Multi-source with arbitrary formula
DerivedCounter branch_mpki_{this, "branch_mpki", "Branch MPKI",
{mispred_, retired_}, DerivedFormula::PerKilo};
};

Available Convenience Formulas

FormulaComputationUse Case
DerivedFormula::Ratioa / (a + b)Hit rates, miss rates
DerivedFormula::Dividea / bIPC, throughput
DerivedFormula::PerKiloa * 1000 / bMPKI metrics

Custom lambdas receive a std::span<const uint64_t> with the per-interval delta values of each source counter (in declaration order) and return a double.

CSV Output

Pivoted format:

cycle,fetch.hits,fetch.misses,fetch.hit_rate,fetch.ipc
10000,850,150,0.850000,1.234567
20000,900,100,0.900000,1.345678

Long format:

cycle,unit,counter_name,value
10000,fetch,hits,850
10000,fetch,misses,150
10000,fetch,hit_rate,0.850000

Edge Cases

CaseBehavior
Division by zero (all sources = 0)Depends on lambda; convenience formulas return 0.0
Source counter not in first dump batchDerived counter skipped with stderr warning
Any number of source countersSupported (not limited to 2)

Backend Semantics

Derived values are computed from per-interval deltas (same as raw counters). In pivoted mode, the derived columns appear after all raw counter columns. The computation function is called once per CSV row flush.

Complete Example

#include "chronon/Chronon.hpp"
using namespace chronon;

class ALUUnit : public TickableUnit, public ObservableUnit {
Counter ops_{this, "ops", "Operations", "ops"};
uint32_t id_;

public:
ALUUnit(uint32_t id)
: TickableUnit("alu" + std::to_string(id)), id_(id) {}

void tick() override {
++ops_; // Per-instance
}

uint64_t getOps() const { return ops_.get(); }
};

int main() {
TickSimulation sim;
auto* alu0 = sim.createUnit<ALUUnit>(0);
auto* alu1 = sim.createUnit<ALUUnit>(1);

// ... setup observation contexts ...

sim.run(1000000);

std::cout << "ALU0 ops: " << alu0->getOps() << "\n";
std::cout << "ALU1 ops: " << alu1->getOps() << "\n";
}