A state is just a labelled interval that does not hold any extra information, but allows
quick prototyping without extending the Aftermath type system. The easiest way to use it
is to use am_state_stack
and am_buffered_event_collection
. When the state is entered it
is pushed to the stack. On exit the state is popped. Popping the
state from the stack adds a new interval with the corresponding state name into the event collection.
Intervals types that associate specific information with the interval should be modeled as separate data structure with an embedded interval and additional fields carrying the extra information (e.g., intervals representing OpenMP loop iteration periods are represented as instances of am_dsk_openmp_iteration_period).
Extending the type system will be covered later in this document.
All trace data is first accumulated in main memory before being written out to a
trace file. The root data structure that serves as an entry point to all data
held in memory is am_buffered_trace
. Trace-wide data is stored directly in a
dedicated buffer embedded into the buffered trace object. Dynamic events
(e.g., events generated by a thread) are associated to streams of events,
called event collections. In order to avoid contention on the trace-wide buffer,
each event collection has its own buffer that can be written concurrently with
buffers from other event collections.
A trace can be associated with an arbitrary number of event collections and events from the collections can be associated to nodes of arbitrary hierarchies representing the logical and physical structure of the system (e.g., tasks grouped into threads grouped into processes or e.g., cores grouped into physical packages grouped into machines).k
New event collections are created by invoking am_buffered_trace_new_collection
and are added to the trace by calling am_buffered_trace_add_collection()
.
However, before any collection is added, the data structure for event
collections must be registered once using
am_dsk_event_collection_write_to_buffer_defid()
.
Before tracing starts, the trace has to be initialized and the header including
data structures associating types from the type system with numeric IDs needs to
be written to the main, trace-wide buffer.
Afterwards, hierarchies can be initialized and added to the trace buffer. Once
all hierarchies have been established and all event collections have been
created, the actual mappings of event streams to nodes from these hierarchies
for execution intervals represented as instances of am_dsk_event_mapping
can
to be written to the trace buffer using
am_dsk_event_mapping_write_to_buffer_defid()
.
Please see the instrumented pthreads library or AfterOMPT for examples of creating and destroying traces.
Assuming an appropriate type was added to the type system, tracing events is relatively straightforward. Writing an event to the buffer of an event collection is done by calling an appropriate *_write_to_buffer_defid() function from on_disk_write_to_buffer.h. Please note that each event type must be registered once at the trace-wide buffer using the corresponding *_write_default_id_to_buffer() function.
For readers interesting more in the architecture of the Aftermath we refer to
the read me file in libaftermath-core/doc/TYPE_SYSTEM
. It explains in details
a difference between on-disk and in-memory models.
For the purpose of this tutorial it is enough to know that on-disk types are ones written to the trace file and stored on disk in the file. In-memory types, on the other hand, are the memory representation of on-disk types when they are loaded from disk to memory to be analysed, displayed, etc. Definitions of on-disk and in-memory types are stored in separate directories.
In order to allow an event of a new type to be written to trace files, the new
type has to be added to the type system. The type definitions can be found in
libaftermath-core/src/defs/aftermath/types
and on-disk types are defined in
on_disk.py
. Adding new types consists of adding a new object of type
EventFrame
for event collections events and Frame
for trace wide events.
Then the event has to be added to TypeList
at the end of the file.
To enable reading and writing data from and to on-disk representation, so it can
be processed and displayed, a corresponding in-memory representation has to be
added. This can be done by creating a new InMemoryCompoundType
object and
adding it to the TypeList
at the end of an appropriate in-memory.py
file.
After the in-memory type is created the link to the on-disk representation
has to be created. This is done by adding following lines to the appropriate
on_disk.py
file.
Within the trace can be stored either in the trace-wide array or
an event collection. Again for the more detailed description we
refer to libaftermath-core/doc/TYPE_SYSTEM
.
For frames to be stored in an event collection, the syntax is:
tags.dsk.tomem.add_per_event_collection_tags(
on_disk_type,
aftermath.types.x.y.z.in_memory.in_memory_type,
"collection_id")
And for trace-wide frames:
tags.dsk.tomem.add_per_trace_array_tags(
on_disk_type,
aftermath.type.x.y.z.in_memory.in_memory_type)
Next, some .c and .h files have to be updated so appropriate classes and functions are generated for the new type. It can be achieved with following steps:
-
Add an array definition for the in-memory type by creating a new file called
libaftermath-core/src/<type>_array.h
:#ifndef AM_<TYPE>_ARRAY_H #define AM_<TYPE>_ARRAY_H #include <aftermath/core/typed_array.h> #include <aftermath/core/in_memory.h> AM_DECL_TYPED_ARRAY_WITH_ELEMENT_DESTRUCTOR( am_<type>_array, struct am_<type>, am_<type>_destroy) #endif
If the type has no destructor then
AM_DECL_TYPED_ARRAY(am_<type>_array, struct am_<type>)
is sufficient. The destructor is required if the type is not a plain data type, e.g., it contains a string field. -
Include the new header file in
libaftermath-core/src/defs/aftermath/templates/toplevel/on_disk.tpl.c
. -
Add the array definition to the default array registry as follow in
libaftermath-core/src/default_trace_array_registry.c
:#include <aftermath/core/<type>_array.h> ... AM_DECL_DEFAULT_ARRAY_REGISTRY_FUNCTIONS(am_<type>_array)
And then in the same file add following in
am_build_default_trace_array_registry()
:AM_DEFAULT_ARRAY_REGISTRY_REGISTER(r, am_<type>_array, "am::<model>::<type>")
-
Add a symbolic link from
libaftermath-core/headers/aftermath/core/<type>_array.h
tolibaftermath-core/src/<type>_array.h
:ln -s --relative libaftermath-core/src/<group>_<type>_array.h libaftermath-core/headers/aftermath/core/<group>_<type>_array.h
-
Add
aftermath/core/<type>_array.h
tonobase_include_HEADERS
inlibaftermath-core/headers/Makefile.am
-
Add
src/<type>_array.h
tolibaftermath_core_la_SOURCES
inlibaftermath-core/Makefile.am
To enable rendering of a newly-defined type in the Aftermath GUI, a new renderer needs to be added to libaftermath-render. This can be done as follows:
-
Add new nodes in
libaftermath-render/src/dfg/nodes/timeline/layers/<group>.h
by adding:AM_RENDER_DFG_DECL_TIMELINE_LAYER_FILTER_NODE_TYPE( <group>_<type>, "<group>::<type>", "Some short description") AM_RENDER_DFG_DECL_TIMELINE_LAYER_ENABLE_CONFIGURATION_NODE_TYPE( <group>_<type>, "<group>::<type>", "Some short description") int am_render_dfg_timeline_<group>_<type>_layer_configuration_node_process( struct am_dfg_node* n);
And then following lines in
AM_DFG_BUILTIN_NODE_TYPES
:&am_render_dfg_timeline_<group>_<type>_layer_filter_node_type, &am_render_dfg_timeline_<group>_<type>_layer_configuration_node_type
-
Add an implementation for the new nodes in
libaftermath-render/src/dfg/nodes/timeline/layers/<group>.c
:AM_RENDER_DFG_IMPL_TIMELINE_LAYER_FILTER_NODE_TYPE( <group>_<type>, "<group>::<type>", struct am_timeline_<group>_<type>_layer) AM_RENDER_DFG_IMPL_TIMELINE_LAYER_ENABLE_CONFIGURATION_NODE_TYPE( <group>_<type>, "<group>::<type>")
-
Update the list of types in
libaftermath-render/src/dfg/types/builtin_types.c
:AM_RENDER_DFG_DECL_TIMELINE_LAYER_TYPE(<group>_<type>, "<group>::<type>")
And in
builtin_defs[]
add:&am_render_dfg_type_timeline_<group>_<type>_layer
-
Declare a new timeline lane renderer in
libaftermath-render/src/timeline/layers/lane/<group>/<group>.h
:struct am_timeline_render_layer_type* am_timeline_<group>_<type>_layer_instantiate_type(void);
-
Implement the lane rendering functions in
libaftermath-render/src/timeline/layers/lane/<group>/<group>.c
:static int trace_changed_<type>(struct am_timeline_render_layer* l, struct am_trace* t) { return trace_changed_per_ecoll_array(l, t); } static int renderer_changed_<type>(struct am_timeline_render_layer* l, struct am_timeline_renderer* r) { if(r->trace) return trace_changed_<type>(l, r->trace); else return 0; } static size_t calculate_index_<type>( struct am_timeline_interval_layer* l, void* arg) { struct am_<group>_<type>* m = arg; // This value determines the colour in the GUI return 0; } struct am_timeline_render_layer_type* am_timeline_<group>_<type>_layer_instantiate_type(void) { struct am_timeline_render_layer_type* t; t = am_timeline_interval_layer_instantiate_type_index_fun( "<group>::<type>", "am::<group>::<type>", sizeof(struct am_<group>_<type>), offsetof(struct am_<group>_<type>, interval), calculate_index_<type>); t->trace_changed = trace_changed_<type>; t->renderer_changed = renderer_changed_<type>; return t; }
For discrete events use the following code instead:
struct am_timeline_render_layer_type* am_timeline_<group>_<type>_acquire_layer_instantiate_type(void) { struct am_timeline_render_layer_type* t; t = am_timeline_discrete_layer_instantiate_type_index_fun( "<group>::<type>", "am::<group>::<type>", sizeof(struct am_<group>_<type>), offsetof(struct am_<group>_<type>, timestamp), calculate_index_<type>, render_event); t->trace_changed = trace_changed_<type>; t->renderer_changed = renderer_changed_<type>; return t; }
Where the
render_event
function can be freely implemented. -
Add the timeline type to
libaftermath-render/src/timeline/common_layers.c
by addingam_timeline_<group>_<type>_layer_instantiate_type
to(*inst_functions[])(void)
.
The steps below explain how to add new type to one of the existing profiles in Aftermath:
-
Add a new icon to
aftermath/share/icons/
. -
Add the path to the new icon to
ICONS
inaftermath/Makefile.am
asshare/icons/draw_<group>_<type>.svg \
. -
Add new button to
aftermath/share/profiles/<profile>/interface.amgui.in
, for example:amgui_toolbar_togglebutton { tooltip: "Some short description", icon: "@ICON_BASEDIR@/draw_<group>_<type>.svg", checked: 0u64, id: "toolbutton_draw_<group>_<type>"
And add the type to list of layers:
amgui_timeline { id: "tl1", layers: [..., "<group>_<type>", ...]
-
Update the data-flow graph in
aftermath/share/profiles/<profile>/graph.dfg
with nodes and connections of the new type.To the list of nodes, add:
am::render::timeline::layer::<group>::<type>::filter { id: XXu64 }, am::render::timeline::layer::<group>::<type>::configuration { id: YYu64 }, am::gui::toolbar_togglebutton { id: ZZu64, widget_id: "toolbutton_draw_<group>_<type>" }
To the list of list of connections, add:
[AAu64, "layers", XXu64, "in"], [XXu64, "out", YYu64, "layer"], [ZZu64, "toggled", YYu64, "enable"]
And indicate the location of the nodes as follows:
[XXu64, x1, y1], [YYu64, x2, y2], [ZZu64, x3, y3]