probe β€” Non-Intrusive Instrumentation

An observer-pattern framework for attaching monitoring, profiling, and analysis tools to simulation components without modifying their source code.

📚 What is this & why does it exist?

πŸ’‘ Think of it like medical monitoring…

Imagine a patient (System & Standard Library Β· gem5 Wiki" class="xref">SimObject) in a hospital. ProbePoint is a sensor port on the patient's body β€” a pulse oximeter clip, blood pressure cuff, or EKG lead. ProbeListener is the monitoring device plugged into that sensor. The patient doesn't need surgery to add monitoring β€” you just clip on a new sensor. Similarly, gem5 components expose probe points that external listeners can attach to without changing the component's core logic.

Architecture Overview

The probe framework implements the Observer design pattern. SimObject subclasses register named ProbePoint objects during initialization. Separate ProbeListener objects (often from different SimObjects) attach to these points to receive notifications when interesting events occur β€” without the producer needing to know about the consumer.

graph TB
    subgraph "Producer (e.g., CPU)"
      CPU["BaseCPU"]
      PP1["ProbePoint<InstPtr>\n'Commit'"]
      PP2["ProbePoint<Addr>\n'DataAccess'"]
      CPU --> PP1
      CPU --> PP2
    end

    subgraph "Framework"
      PM["ProbeManager\n(registry)"]
    end

    subgraph "Consumers"
      PL1["ProbeListener\n(SimpleCache stats)"]
      PL2["ProbeListener\n(PMU counter)"]
      PL3["ProbeListener\n(Trace logger)"]
    end

    PP1 -->|"notify"| PL1
    PP1 -->|"notify"| PL2
    PP2 -->|"notify"| PL3
    PM --> PP1
    PM --> PP2
    

ProbePoint

A ProbePoint is a named notification source templated on the type of data it emits. When the producing component wants to signal an event, it calls notify() on the probe point, which fans out to all registered listeners.

ProbePointArg Class

// Type-erased base for probe point registration
class ProbePoint
{
  protected:
    std::string name;
    ProbeManager *manager;

  public:
    ProbePoint(ProbeManager *manager, const std::string &name);
    virtual ~ProbePoint();

    const std::string &getName() const { return name; }
};

// Typed probe point with argument
template<typename Arg>
class ProbePointArg : public ProbePoint
{
    std::vector<ProbeListener *> listeners;

  public:
    ProbePointArg(ProbeManager *manager, const std::string &name);

    // Fire notification to all listeners
    void notify(const Arg &val) {
        for (auto *l : listeners)
            static_cast<ProbeListenerArg<Arg> *>(l)->notify(val);
    }

    void addListener(ProbeListener *l);
    void removeListener(ProbeListener *l);
    bool hasListeners() const { return !listeners.empty(); }
};

ProbeListener

A ProbeListener attaches to a specific probe point and receives callbacks when events occur. The listener specifies which SimObject and probe point name to listen to.

ProbeListenerArg Class

class ProbeListener
{
  protected:
    ProbeManager *manager;
    std::string pointName;

  public:
    ProbeListener(ProbeManager *pm, const std::string &name);
    virtual ~ProbeListener();
};

template<typename Arg>
class ProbeListenerArg : public ProbeListener
{
  public:
    ProbeListenerArg(ProbeManager *pm, const std::string &name,
                     std::function<void(const Arg &)> callback);

    virtual void notify(const Arg &val) {
        callback(val);
    }

  private:
    std::function<void(const Arg &)> callback;
};

ProbeManager

Each SimObject has a ProbeManager that serves as a registry for its probe points. Listeners use the manager to find and connect to named probe points. The manager handles the lifecycle of point-listener connections.

class ProbeManager
{
    SimObject *object;
    std::vector<ProbePoint *> points;

  public:
    ProbeManager(SimObject *obj);

    // Register a new probe point
    void addPoint(ProbePoint *point);

    // Find probe point by name
    ProbePoint *getPoint(const std::string &name) const;

    // Connect listener to named point
    bool addListener(const std::string &pointName,
                     ProbeListener *listener);

    bool removeListener(const std::string &pointName,
                        ProbeListener *listener);
};

Standard Probe Points

gem5 defines several standard probe points across its components. These provide a consistent instrumentation interface for common events.

Probe PointSource ComponentArgument TypeDescription
CommitCPUStaticInstPtrFired when an instruction commits
FetchCPUStaticInstPtrFired when an instruction is fetched
DataAccessCPUPacketPtrFired on data memory access
MissCache Hierarchy | gem5 Wiki" href="../../mem_doc/cache/index.html">CachePacketPtrFired on cache miss
HitCachePacketPtrFired on cache hit
FillCachePacketPtrFired when a cache line is filled
EvictionCacheAddrFired when a cache line is evicted
TLBMissTLBAddrFired on TLB miss
PwrStateChangeClockedObjectPowerStateFired on power state transition

Usage Patterns

Registering Probe Points (Producer)

// In a CPU's header file
class MyCPU : public BaseCPU
{
    ProbePointArg<StaticInstPtr> *ppCommit;
    ProbePointArg<PacketPtr> *ppDataAccess;

  public:
    void regProbePoints() override {
        ppCommit = new ProbePointArg<StaticInstPtr>(
            getProbeManager(), "Commit");
        ppDataAccess = new ProbePointArg<PacketPtr>(
            getProbeManager(), "DataAccess");
    }

    // In execute logic:
    void commitInst(StaticInstPtr inst) {
        // ... normal commit logic ...
        ppCommit->notify(inst);  // Fire probe
    }
};

Attaching Listeners (Consumer)

// A PMU-like listener object
class MyProfiler : public SimObject
{
    uint64_t commitCount = 0;
    ProbeListenerArg<StaticInstPtr> *listener;

  public:
    void regProbeListeners() override {
        // Attach to CPU's "Commit" probe point
        listener = new ProbeListenerArg<StaticInstPtr>(
            cpu->getProbeManager(), "Commit",
            [this](const StaticInstPtr &inst) {
                commitCount++;
                if (inst->isMemRef())
                    memAccessCount++;
            });
    }
};

Conditional Probing

// Check if anyone is listening before doing expensive work
void MyCPU::commitInst(StaticInstPtr inst) {
    // ... normal commit logic ...

    if (ppCommit->hasListeners()) {
        // Only construct the notification data if someone cares
        ppCommit->notify(inst);
    }
}

Built-in Probe Listeners

gem5 ships with several pre-built probe listener implementations for common analysis tasks:

PMU (Performance Monitoring Unit)

The AArch32, AArch64, TrustZone security extensions, SVE/SVE2 vector processing, PMU, and hardware transactional memory." href="../../arch_doc/arm/index.html">ARM PMU implementation uses probe points to count architectural events (instructions retired, cache misses, branch mispredictions) without modifying the CPU pipeline code.

Stack Distance Calculator

Attaches to cache access probe points to compute reuse distance distributions β€” essential for cache sizing analysis.

Memory Trace Recorder

Records all memory accesses to a trace file by listening to DataAccess probe points, enabling offline analysis.

πŸ” Narrated Walkthrough: Adding a Custom Probe Listener

  1. Identify the Probe β€” Determine which SimObject exposes the event you want to monitor. Check the component's regProbePoints() method for available probe point names.
  2. Create a Listener Class β€” Define a new SimObject subclass (or use an existing one) that will receive probe notifications. Override regProbeListeners().
  3. Instantiate ProbeListenerArg β€” In regProbeListeners(), create a ProbeListenerArg with the target's ProbeManager, the point name, and your callback function.
  4. Handle Notifications β€” Your callback receives the typed argument whenever the probe fires. Update counters, log data, or trigger other actions.
  5. Python Configuration β€” Instantiate your listener in the Python config and pass it a reference to the SimObject you want to monitor.
  6. Zero Overhead β€” If no listener is attached, the notify() call is essentially a no-op (empty vector iteration), so unmonitored components pay minimal cost.
sequenceDiagram
    participant Init as Initialization
    participant CPU as CPU Component
    participant PM as ProbeManager
    participant PP as ProbePoint
    participant PL as ProbeListener
    participant User as User Callback

    Init->>CPU: regProbePoints()
    CPU->>PP: new ProbePointArg("Commit")
    CPU->>PM: addPoint(ppCommit)
    Init->>PL: regProbeListeners()
    PL->>PM: addListener("Commit", listener)
    PM->>PP: addListener(listener)

    Note over CPU,User: Simulation Running

    CPU->>PP: notify(inst)
    PP->>PL: notify(inst)
    PL->>User: callback(inst)
    User->>User: Update statistics
    

Design Principles

Key Components Reference

ComponentHeaderPurpose
ProbePointprobe/probe.hhType-erased base for named notification sources
ProbePointArg<T>probe/probe.hhTyped probe point that fires notifications with argument
ProbeListenerprobe/probe.hhBase class for probe event receivers
ProbeListenerArg<T>probe/probe.hhTyped listener with callback function
ProbeManagerprobe/probe.hhPer-SimObject registry of probe points

Glossary

TermDefinition
Observer PatternDesign pattern where subjects notify observers of state changes without tight coupling
Probe PointA named notification source on a SimObject that fires when specific events occur
Probe ListenerAn observer that attaches to a probe point and receives event callbacks
Non-IntrusiveMonitoring that can be added/removed without modifying the monitored component's source code
PMUPerformance Monitoring Unit β€” hardware counter system modeled using probe listeners
Reuse DistanceNumber of unique addresses accessed between two accesses to the same address β€” key cache metric