package tracing

  1. Overview
  2. Docs

Write a trace in the Fuchsia Trace Format using a high level API. This allows you to convert your data to a trace and view it on an interactive timeline in Perfetto.

This module provides a wrapper around Tracing_zero.Writer.t which makes it easier to write traces with less steps and more normal OCaml types.

However, unlike Tracing_zero.Writer.t, this module allocates and doesn't pay close attention to its performance. It should still be fast enough for most offline converters though.

See app/tracing/bin/dominodb_converter.ml for an example of something using this API to visualize some performance data as a trace.

Viewing traces

You can view traces in the Perfetto web UI available at https://ui.perfetto.dev/

Use the "Open trace file" menu item on the left to select a trace file to view.

String interning

All strings in this API with the exception of Arg.String event arguments are interned in a lasting way such that every further use will just refer to the string by an identifier rather than putting it in the file again. This means long names and categories are fine and won't bloat the file too much if you create lots of events with them.

However, there's currently a limit of around 32k interned strings shared between names, categories and Arg.Interned arguments. If you exceed that limit the library will assert. We expect nearly every use case won't come close to this limit, but if you have a use case that does let us know and we can add logic to evict old strings from the interning slots for re-use.

Times: relative and absolute

Time instants are provided as a Time_ns.Span.t from the start of the trace. If you have absolute times you should subtract the time of the start of your trace or the start of the day the trace was taken (see dominodb_converter.ml in app/tracing for code). Then provide base_time to create so that tools which need absolute time can use it to for example merge traces while aligning time properly.

Avoid converting a Time_ns.t to a span since epoch, because although the format uses 64-bit nanoseconds, the Perfetto trace viewer visualizes using Javascript doubles so large timestamps like this will cause weird imprecision in where events are drawn due to rounding of large numbers.

type t
val create_for_file : base_time:Core.Time_ns.t option -> filename:string -> t

Open a file to write trace events to in the Fuchsia Trace Format, suggested extension is .fxt.

If base_time is provided, a time initialization record will be written which records what absolute time corresponds to Time_ns.Span.zero.

val close : t -> unit

Signifies that all writing is done, any further writing will throw an exception.

Just calls close on the underlying writer.

val translate_time : t -> Core.Time_ns.t -> Core.Time_ns.Span.t

Translate an absolute time to trace time. If base_time was provided it subtracts that, asserting if the time is before the base time. Otherwise it measures relative to unix epoch. If you already have times relative to a trace start time there's no need to use this.

val allocate_pid : t -> name:string -> int

The format uses pids and tids to represent processes and threads, and the wrapper can allocate pids for processes and set the process name for you.

Processes are sorted by pid in the Perfetto UI, and PIDs are allocated in order.

module Thread : sig ... end
val allocate_thread : t -> pid:int -> name:string -> Thread.t

Allocate a tid and name a new the thread for a process.

Threads are sorted by alphabetical order within a process in the Perfetto UI.

module Arg : sig ... end

Named arguments of various simple types can be attached to events and will show up in the bottom details panel when an event is selected in Perfetto.

type 'a event_writer = t -> args:Arg.t list -> thread:Thread.t -> category:string -> name:string -> time:Core.Time_ns.Span.t -> 'a

The name shows up as the main label of the event in UIs, and the category is another string that can be used to classify the event, and is visible in Perfetto when an event is clicked on.

val write_instant : unit event_writer

An event with a time but no duration

Note: instant events currently are not visible in the Perfetto UI.

val write_duration_instant : unit event_writer

Same as write_instant except guaranteed to be visible in visualization UIs and usable with flow events.

Currently this uses write_duration_complete with an identical begin and end time, which shows up in Perfetto as chevron pointing at that time. In the future if we change Perfetto to be able to show instant events we may search for usages of this function which don't use flow events and replace them with write_instant.

val write_counter : unit event_writer

A counters event uses its arguments to specify "counters" which may be represented by trace viewers as a chart over time. Its arguments must be Arg.Int or Arg.Float and there should be at least one.

The spec says that multiple arguments in the same counter event should be representable as a stacked area chart, and otherwise you should use multiple counter events. At the moment Perfetto doesn't implement this and instead ignores the counter ID and represents each argument as a separate line chart.

Counter events are grouped into a chart labeled "<event name>:<argument name>" per thread that name pair is used on.

val write_duration_begin : unit event_writer

Begin a duration slice which will be finished with a matching end event.

A "duration slice" starts and ends at different times but has a single name and category. It shows up as a bar between the start and end time in UIs.

Duration slices within a thread should be nested properly such that if a duration slice starts within another slice then it must end before that slice ends.

val write_duration_end : unit event_writer

End a duration slice, should be properly nested and with matching name/category

val write_duration_complete : (time_end:Core.Time_ns.Span.t -> unit) event_writer

Create a duration slice where the start and end are known up front.

Takes 3*8 bytes instead of 2*2*8 bytes for separate begin/end events, saving 8 bytes per slice

val create_flow : t -> Flow.t

Flows are chains of duration slices which get connected by arrows when selected in a trace viewer UI. A flow is composed of "steps", each of which must be written at a time contained in a duration slice, and when one of those slices is selected in the UI it will display arrows connecting the slices of each step in order like this:

      [    my_slice_1     ] ---> [ my_slice_2 ] -\   [slice_not_in_flow_2 ]
      [      slice_not_in_flow       ]            \-----------> [ my_slice_3 ]

Flows have identity and can consist of multiple steps, which allows selecting one duration slice with an associated flow to show the arrows for all steps of that flow. Perfetto also allows the user to navigate through the steps of a flow with the square bracket keys.

val write_flow_step : t -> Flow.t -> thread:Thread.t -> time:Core.Time_ns.Span.t -> unit

Add a step to a flow, which will connect to the duration event enclosing the provided time on that thread. Specifically there needs to be a duration slice on the given thread with a start time less than or equal to time and an end time greater than or equal to time and the flow event will be associated with the latest-starting (i.e most deeply nested) such enclosing slice.

val finish_flow : t -> Flow.t -> unit

All flows must be "finished" before the end of the trace and after writing all steps.

The representation of trace events in the file format requires us to hold one flow step in memory until either the next flow step or we finish adding steps, in order to know what kind of event to write out. Finishing a flow writes out any last buffered step.

module Expert : sig ... end