Simulator users usually want to export meaningful information from simulations for further analysis. In other simulators, data exporting is typically supported by adding variables and print commands to the component code. However, mixing the data-collecting code with component logic bloats the code and makes the software hard to maintain. Even worse, simulators tend to use Singletons to aggregate data (because all the instances can report data to a centralized location). We consider that Singletons significantly harm the flexibility of the code, making it difficult to adapt to users’ needs. To solve this problem and to ensure both flexibility and elegant code, we employ the Observer Pattern. Specifically, we introduce the Hooks and Tracers to decouple the hardware logic and the need to collect data from the simulator.

2.4.1 Hooks

A hook is a piece of software that can be called when certain actions happen. Such actions can be the start of an event handling or when an element is popped from a buffer. Everything related to Hooks is implemented in sim/hook.go.

Any element that can accept hooks is called a hookable. Developers need to invoke the hooks at certain locations in the hookable code. For example, the event-driven simulation engine is a hookable. And the following hook-invoking code is used before the event-driven simulation engine triggers an event.

hookCtx := HookCtx{
	Domain: e,
	Pos:    HookPosBeforeEvent,
	Item:   evt,
}
e.InvokeHook(hookCtx)

Central to the example is the InvokeHook function. This function is provided in the HookableBase struct. So any hookable that can simply embed the HookableBase struct to avoid reimplementing the methods related to hookables in every struct. When the InvokeHook function is called, all the hooks that are attached to the current hookable is invoked.

The InvokeHook method needs an argument, named as HookCtx (hook context). The hook context is a struct with three fields, including the domain, the position (pos) and the item. The domain is the hookable. The item is the object of the action. In this example, since the hook is about the triggering of an event, the item is the event. Finally, we need a position field to describe what is happening.

The position field needs an variable with type HookPos. Developers should define all the possible hook positions as package-level, exported, variables. For example, when defining event-handling related positions, we use the following code. These two lines of code do not belong to any function.

// HookPosBeforeEvent is a hook position that triggers before handling an event.
var HookPosBeforeEvent = &HookPos{Name: "BeforeEvent"}

// HookPosAfterEvent is a hook position that triggers after handling an event.
var HookPosAfterEvent = &HookPos{Name: "AfterEvent"}

It is also easy to implement a hook. A hook only needs to define a function named Func, which takes the HookCtx as an argument. The Func can decide what to do according to the Domain, Pos, or Item. For example, the code below is a simplified version of an EventLogger, which simply dumps all the events triggered before each event is handled.

// EventLogger is an hook that prints the event information
type EventLogger struct {
	...
}

// Func writes the event information into the logger
func (h *EventLogger) Func(ctx HookCtx) {
	if ctx.Pos != HookPosBeforeEvent {
		return
	}

	evt, ok := ctx.Item.(Event)
	if !ok {
		return
	}

	
	fmt.Printf(...)
}

In this example, we can see that the hook first checks the ctx.Pos. It only cares about the HookPosBeforeEvent position. Also, it doublechecks if the item is an event. It both criterions meet, the hook prints the event information. Note that the hookable may invoke hooks for many different reasons and at various positions. Therefore, it is the hook’s responsibility to determine if an action need to be taken.

The configuration code needs to associate the desired hook to hookables, deciding what data to collect. To attach a hook to an hookable, we can simply call the AcceptHook method of the hookable. For example, to attach the EventLogger to an engine, we can use the following code.

var engine sim.Engine
engine = sim.NewSerialEngine()

engine.AcceptHook(sim.NewEventLogger(log.New(os.Stdout, "", 0)))

2.4.1 Basic Concepts of Tracing

Hook provides the fundamental mechanism to collect data. However, it is a bit too flexible. Users and developers may need a more structured guidance on what data to collect. Therefore, we build the tracing system on top of the hooking system.

// Yifan stopped here.

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