Akita Basics 5: Tracing
The purpose of running a simulation is to acquire performance metrics. Akita uses a tracing system to allow users to extract desired performance metrics from the simulator. The tracing system is mainly composed of two parts: the simulation annotating API and the tracers. Tracing is implemented at gitlab.com/akita/util/tracing.
Annotating API
### Tasks
The core concept in the tracing system is “Task”, which describes a multi-cycle action performed by a component. The execution of an instruction, a cache access, and a DRAM transaction are all examples of tasks. A task is represented using the following struct definition.
```go
type Task struct {
ID string `json:”id”`
ParentID string `json:”parent_id”`
Kind string `json:”kind”`
What string `json:”what”`
Where string `json:”where”`
StartTime akita.VTimeInSec `json:”start_time”`
EndTime akita.VTimeInSec `json:”end_time”`
Steps []TaskStep `json:”steps”`
Detail interface{} `json:”-”`
}
```
The fields of a task are straightforward. Each task has a unique `ID`. A user can use `akita.GetIDGenerator().Generate()` to acquire a unique ID (unique within the simulation run, consistent across multiple runes). Each task should also have a Parent task, identified by the `ParentID` field. For example, if a cache access task is generated from the execution of an instruction, then the instruction task is the parent task of the cache access task. `Kind` and `What` denote the action of the task. `Where` is the name of the component that carries out the task. `StartTime` and `EndTime` represent when the task is started and completed. Also, users are allowed to attach any extra information in the `Detailed` field. We will talk about the `Steps` field later.
When a developer implements a component, the developer needs to annotate the component using the annotating APIs. When the task starts, the `StartTask` function needs to be called. When the task finishes, the `EndTask` function needs to be called. Here are the function signatures for `StartTask` and `EndTask`. Most of the fields are straightforward. The only exception is the field `domain`, which represents where the task happens. A domain is typically an Akita component.
```go
func StartTask(
id string,
parentID string,
now akita.VTimeInSec,
domain NamedHookable,
kind string,
what string,
detail interface{},
)
func EndTask(
id string,
now akita.VTimeInSec,
domain NamedHookable,
)
```
### Request-Based Tasks
A received request is a task to complete. Therefore, Akita provides convenient APIs to record request-based tasks.
Suppose we have a core connecting to an L1 cache. When the core wants to send a read request to the L1 cache, it creates the request in the current cycle. We say this is the cycle that the request is **Initiated**. The core then sends the message through a interconnect to the L1 cache. The request may also be buffered at the L1 cache for a while before it can be processed. We say the cycle that the L1 cache starts to process the request is the cycle that the request is **Received.** After a few cycles, the L1 cache may have the data ready and can send the response back to the core. We say this is the cycle that the request is **Completed.** Finally, it takes a few extra cycles for the core to receive the response. We say the cycle that the core receives the response to be the cycle that the request is **Finalized**.
Following this terminologies, Akita provides 4 APIs:
```go
func TraceReqInitiate(msg akita.Msg, now akita.VTimeInSec, domain NamedHookable, taskParentID string)
func TraceReqReceive(msg akita.Msg, now akita.VTimeInSec, domain NamedHookable)
func TraceReqComplete(msg akita.Msg, now akita.VTimeInSec, domain NamedHookable)
func TraceReqFinalize(msg akita.Msg, now akita.VTimeInSec, domain NamedHookable)
```
These 4 APIs create and complete 2 tasks. The `TraceReqInitiate` API and the `TraceReqFinalize` API creates a task with the `req_out` kind. The `TraceReqReceive` and the `TraceReqComplete` creates a task with the `req_in` kind. The `req_in` task is a subtask of the `req_out` task. The ID of the `req_in`task can be found using a combination of the request message and the domain, by calling this function
```go
func MsgIDAtReceiver(msg akita.Msg, domain NamedHookable) string
```
### Task Steps
In many cases, tracing at the granularity of tasks is not detailed enough. For example, if a cache access is a task recorded without any extra information, we will have no idea if it is a cache hit or a cache miss. Tagging in the simulator code can solve this problem. The tagging API is also simple:
```go
func AddTaskStep(id string, now akita.VTimeInSec, domain NamedHookable, what string)
```
## Tracer
Tracers are a type of structs that can handle the annotating trace API calls. The interface of a tracer is very simple, as it simply defines the action when a task starts, when a task ends, and when we annotate the task steps.
```go
type Tracer interface {
StartTask(task Task)
StepTask(task Task)
EndTask(task Task)
}
```
Although the Tracer interface is very simple, tracers can be versatile. For example, Akita provides a `BusyTimeTracer` which can measure the time that a component is running at least one component, which can be used to measure performance and measure hardware utilization. `AverageTimeTracer` can calculate the average time of tasks. We use this tracer to measure the average request handling time for caches, understanding how efficient the caches are. Akita also provides `StepCountTracer`, which we use to count the number of cache hits and misses. Finally, we provide a `MySQLTracer` and a `MongoDBTracer`, which can store all the tasks into a MySQL or a MongoDB database. Most of the tracers can configure a
Users can select from which component to collect trace. The API to attach a tracer is `func CollectTrace(domain NamedHookable, tracer Tracer)`.
Users can select from which component to collect trace. The API to attach a tracer is func CollectTrace(domain NamedHookable, tracer Tracer)
.