195 lines
8.9 KiB
OCaml
195 lines
8.9 KiB
OCaml
(** 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.
|
|
|
|
{2 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.
|
|
|
|
{2 String interning}
|
|
|
|
All [string]s 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.
|
|
|
|
{2 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.
|
|
*)
|
|
|
|
open! Core
|
|
|
|
type t
|
|
|
|
(** Open a file to write trace events to in the Fuchsia Trace Format, suggested extension
|
|
is [.fxt] for an uncompressed file and [.fxt.gz] for a gzip compressed one. While [.zst]
|
|
will produce a Zstandard compressed file, the perfetto viewer does not yet support it.
|
|
|
|
If [base_time] is provided, a time initialization record will be written which
|
|
records what absolute time corresponds to [Time_ns.Span.zero]. *)
|
|
val create_for_file : base_time:Time_ns.t option -> filename:string -> t
|
|
|
|
(** Signifies that all writing is done, any further writing will throw an exception.
|
|
|
|
Just calls [close] on the underlying writer. *)
|
|
val close : t -> unit
|
|
|
|
(** 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 translate_time : t -> Time_ns.t -> Time_ns.Span.t
|
|
|
|
(** 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. *)
|
|
val allocate_pid : t -> name:string -> int
|
|
|
|
module Thread : sig
|
|
type t
|
|
end
|
|
|
|
(** 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.*)
|
|
val allocate_thread : t -> pid:int -> name:string -> Thread.t
|
|
|
|
(* Wrappers for Tracing_zero.Writer events with an easier-to-use interface.
|
|
|
|
See the [Tracing_zero.Writer.t] docs for more on what these events are. *)
|
|
|
|
(** 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. *)
|
|
module Arg = Trace_intf.Event_arg
|
|
|
|
(** 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. *)
|
|
type 'a event_writer =
|
|
t
|
|
-> args:Arg.t list
|
|
-> thread:Thread.t
|
|
-> category:string
|
|
-> name:string
|
|
-> time:Time_ns.Span.t
|
|
-> 'a
|
|
|
|
(** An event with a time but no duration
|
|
|
|
Note: instant events currently are not visible in the Perfetto UI. *)
|
|
val write_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_duration_instant : 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_counter : 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_begin : unit event_writer
|
|
|
|
(** End a duration slice, should be properly nested and with matching name/category *)
|
|
val write_duration_end : 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 write_duration_complete : (time_end:Time_ns.Span.t -> unit) event_writer
|
|
|
|
(** 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:
|
|
|
|
{v
|
|
[ my_slice_1 ] ---> [ my_slice_2 ] -\ [slice_not_in_flow_2 ]
|
|
[ slice_not_in_flow ] \-----------> [ my_slice_3 ]
|
|
v}
|
|
|
|
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 create_flow : t -> Flow.t
|
|
|
|
(** 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 write_flow_step : t -> Flow.t -> thread:Thread.t -> time:Time_ns.Span.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.
|
|
*)
|
|
val finish_flow : t -> Flow.t -> unit
|
|
|
|
module Expert : sig
|
|
(** Wraps a [Tracing_zero.Writer.t] to keep track of IDs and interned strings
|
|
|
|
It's still safe to write to the writer directly after using this wrapper, as long as
|
|
you use IDs allocated with the [allocate_*] functions and are aware that this module
|
|
can overwrite strings using [set_temp_string_slot].
|
|
|
|
See [create_for_file] for explanation of [base_time]. *)
|
|
val create : base_time:Time_ns.t option -> Tracing_zero.Writer.t -> t
|
|
end
|