123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406open!Core(** Abstraction for a source of buffers to write trace data to.
At the moment this is used for the ability to both write to a file and to an in-memory
buffer for tests.
However I also tried to anticipate some of the structure of how the API would work
which would be required to write to a shared-memory transport involving a ring or
double-buffering system. *)moduletypeDestination=sig(** Called before writing up to [ensure_capacity] bytes of data to the destination. Any
older buffers will no longer be used after calling this function, so it's legal for
a [Destination] to re-use [Iobuf.t]s to avoid allocating.
All writers are expected to update the Iobuf's [lo], either manually or using
[Iobuf.Fill]. Data will only be consumed up to the new [lo]. *)valnext_buf:ensure_capacity:int->(read_write,Iobuf.seek,Iobuf.global)Iobuf.t(** We will no longer be writing anything. Resources should be flushed and freed. *)valclose:unit->unitendmoduletypeArg_writers=sigtypettypestring_idvalinterned_string:t->name:string_id->string_id->unit(* [Expert.update_size_for_inline_string] must be called before [inline_string] *)valinline_string:t->name:string_id->string->unitvalint32:t->name:string_id->int->unitvalint63:t->name:string_id->int->unitvalint64:t->name:string_id->int64->unitvalpointer:t->name:string_id->int64->unitvalfloat:t->name:string_id->float->unitvalbool:t->name:string_id->bool->unitendmoduleTick_translation=structtypet={ticks_per_second:int;base_ticks:int;base_time:Time_ns.t}[@@derivingglobalize]letepoch_ns={ticks_per_second=1_000_000_000;base_ticks=0;base_time=Time_ns.epoch};;end(** This is the low-level zero-alloc API for writing Fuchsia Trace Format traces. The API
pays some performance costs to check some format constraints on the events you write,
but not too many.
Most users probably want to use the [Tracing.Trace] higher-level API instead. A large
part of the purpose of this API was to figure out how to efficiently write FTF traces
without allocating. The low-overhead probe path uses a bunch of the underlying code
and approach from this interface although uses the [Expert] submodule to precompute
event headers. *)moduletypeS=sigtypetvalclose:t->unitmoduleTick_translation=Tick_translationvalmake_tick_translation:unit->local_Tick_translation.tvalwrite_tick_initialization:t->local_Tick_translation.t->unitvalmake_and_write_tick_translation:t->unitmoduleString_id:sigtypet:immediate[@@derivingequal~localize]valempty:tvalmax_number_of_temp_string_slots:intvalof_int:int->tendvalmax_interned_string_length:int(** Intern a string into the trace so that it can be referred to with very low cost.
Note that this does not check if the string has already been interned, see
[intern_string_cached].
Note that only around 32k strings can be interned this way, so use it for things
like identifiers where there won't be that many. See [set_temp_string_slot] for
things like string arguments with many possible values.
See the comment at the top of [lib/tracing/src/trace.mli] for more info on string
interning. *)valintern_string:t->string->String_id.t(** This interns a string while re-using a set of 100 reserved string IDs (by default,
the number can be overriden at writer creation). Setting the string in a slot
overwrites what was previously in that slot so any further events written in the
trace see the new value. This allows arbitrarily many unique strings to be used in a
trace, unlike [intern_string]. *)valset_temp_string_slot:t->slot:int->string->String_id.tvalget_temp_string_slot:t->slot:int->String_id.tvalnum_temp_strs:t->int(** The trace format interns the 64 bit thread and process IDs into an 8-bit thread ID
and we expose this to the user. *)moduleThread_id:sigtypet:immediatevalof_int:int->tend(** Similar to [set_temp_string_slot], interns a thread into a slot ID, overwriting any
thread which may have previously been in that slot. The number of thread slots is
very limited (0<=slot<255) so you may need to manage them carefully.
If a [pid] is the same as the [tid], Perfetto will consider that thread a "main
thread" and sort it first among the threads, contrary to its usual alphabetical
sorting by thread name. So if you don't want this to happen allocate tids such that
they're never the same as a pid.
Note that Perfetto doesn't require tids to be unique across different pids, but the
Fuchsia Trace Format spec implies they should be. I think it's safe to assume that
any tool Jane Street uses will allow per-process tids but it's still safer to make
them globally unique. *)valset_thread_slot:t->slot:int->pid:int->tid:int->Thread_id.t(** Sets the name on the collapsible process section header in the UI.
Perfetto sorts these headers by pid. *)valset_process_name:t->pid:int->name:String_id.t->unit(** Sets the name on a thread track.
Perfetto sorts threads within a process alphabetically. *)valset_thread_name:t->pid:int->tid:int->name:String_id.t->unit(** Events are written with a header which specifies how large the record is and how
many arguments it has, which means you need to pre-commit to how many arguments of
each type you will later write for an event. This is checked and will throw an
exception if you write another event or close the writer without having written the
correct arguments. *)moduleArg_types:sigtypet(** Use [none] if you aren't going to write any arguments for an event *)valnone:t(** If you're going to write arguments, provide the count of each type you'll write. *)valcreate:?int64s:int->?int32s:int->?bools:int->?floats:int->?interned_strings:int->?inlined_strings:int->unit->tend(** Most event writer functions take a common set of arguments including a commitment to
what event arguments will be added ([arg_types]), a thread the event occured on, a
[category] which is an arbitrary string classifying the event visible in UIs and
potentially used for filtering, a [name] that's the main label for the event, and a
timestamp in "ticks" which defaults to nanoseconds since the start of the trace, but
the format allows adjusting to other units like rdtsc clock cycles. *)type'aevent_writer=t->arg_types:Arg_types.t->thread:Thread_id.t->category:String_id.t->name:String_id.t->ticks:int->'a(** An event with a time but no duration. *)valwrite_instant:unitevent_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 numerical and there
should be at least one.
The counter ID is in theory for associating events that should be plotted on the
same graph but in practice Perfetto ignores it and uses the name. The
[Tracing.Trace] wrapper chooses an ID based on the name to match this. *)valwrite_counter:(counter_id:int->unit)event_writer(** Begin a duration slice which will be finished with a matching end event. *)valwrite_duration_begin:unitevent_writer(** End a duration event, should be properly nested and with matching name/category *)valwrite_duration_end:unitevent_writer(** A duration event where the start and end are known up front.
Takes 3*8 bytes instead of 2*2*8 bytes for separate events, saving 8 bytes per span *)valwrite_duration_complete:(ticks_end:int->unit)event_writer(** Begin an async slice. [async_id] disambiguates concurrent contexts. *)valwrite_async_begin:(async_id:int->unit)event_writer(** Write an event with a time but no duration associated with an async context. *)valwrite_async_instant:(async_id:int->unit)event_writer(** End an async slice. *)valwrite_async_end:(async_id:int->unit)event_writer(** Flow events connect enclosing duration events with arrows in the trace viewer.
See [Tracing.Flow.t] to make it easier to write the correct flow event type. *)(** Begins a flow, the chronologically first event in each flow must use this event.
Multiple flows with different IDs can start from one enclosing duration slice. *)valwrite_flow_begin:t->thread:Thread_id.t->ticks:int->flow_id:int->unit(** An intermediate step in the flow that's neither the first or last step. *)valwrite_flow_step:t->thread:Thread_id.t->ticks:int->flow_id:int->unit(** Close a flow with a final step. Perfetto allows the flow_id to be re-used after. *)valwrite_flow_end:t->thread:Thread_id.t->ticks:int->flow_id:int->unit(** These argument writers need to be called immediately after an event writer with
matching [Arg_types] counts for each type. *)moduleWrite_arg:Arg_writerswithtypet:=tandtypestring_id:=String_id.tmoduleExpert:sigmoduletypeDestination=Destination(** Creates writer.
[refresh_buf_at_intervals] determines whether this writer will attempt to refresh
the buffer at its freshness intervals; defaults to [true]. Freshness is checked
before an event is written and the buffer is refreshed if stale. This means that a
potentially expensive operation (refresh buffer) is done either 0 or 1 times
before each event is written. If you set [refresh_buf_at_intervals:false], you
should call [refresh_buf_if_stale] regularly to flush the latest data. *)valcreate:?num_temp_strs:int->?refresh_buf_at_intervals:bool->destination:(moduleDestination)->unit->t(** Same as [create] but without writing a FXT file header to the destination. *)valcreate_no_header:?num_temp_strs:int->?refresh_buf_at_intervals:bool->destination:(moduleDestination)->unit->t(** Switches destination for future data. Use with care; does not reset any state. *)valset_destination:t->destination:(moduleDestination)->unit(** Writes raw byte stream to the current destination. *)valwrite_bytes:t->bytes:Bytes.t->unit(** Writes raw byte stream to the current destination. *)valwrite_iobuf:t->buf:(read,Iobuf.seek,Iobuf.global)Iobuf.t->unit(** Interns a string directly to the specified slot (whereas [set_temp_string_slot]
may shift the index since certain indices are reserved for internal use). Will
raise when setting slot 1 to any string other than "process".
Useful for preserving the string ID usage of parsed traces in a way that can lead
to exact byte equality after a round trip through parsing and writing. *)valset_string_slot:t->slot:int->local_string->String_id.t(** Immediately ask the destination for a new buffer even if the current one isn't
full. This is intended for use by the probe infrastructure when a destination for
the global writer is initialized. *)valforce_switch_buffers:t->unit(** Finish all pending writes to underlying buffer.
This is currently only intended for use by tests. *)valflush:t->unit(** Refreshes the buffer when its freshness interval has elapsed. You should call this
manually iff you specified [refresh_buf_at_intervals:false] when creating the
writer.
This should be called *regularly* to flush the latest written data, which ensures:
- the buffer's consumer is able to read up-to-date data, and
- most data will not be lost if the program/writer crashes.
Additionally update tick translation to keep resulting timestamps accurate. *)valrefresh_buf_if_stale_and_update_tick_translation:t->now:Time_stamp_counter.t->unittypeheadervalglobalize_header:local_header->headervalset_name:header:local_header->name:String_id.t->local_headervalupdate_size_for_name:header:local_header->name:string->local_headervalupdate_size_for_inline_string:header:local_header->inline_string:string->local_header(** For use with [precompute_header_and_size] *)moduleEvent_type:sigtypetvalinstant:tvalcounter:tvalduration_begin:tvalduration_end:tvalduration_complete:tvalasync_begin:tvalasync_end:tvalasync_instant:tvalflow_begin:tvalflow_step:tvalflow_end:tendvalwrite_event:t->event_type:Event_type.t->extra_words:int->arg_types:Arg_types.t->thread:Thread_id.t->category:String_id.t->name:String_id.t->ticks:int->unit(** Pre-compose an event header word, for use with [write_from_header_with_tsc] *)valprecompute_header:event_type:Event_type.t->extra_words:int->arg_types:Arg_types.t->thread:Thread_id.t->category:String_id.t->name:String_id.t->local_header(** Provides a way to get the [String_id.t] of the slots that are reserved for higher
level libraries. *)valget_dyn_slot:slot:int->String_id.t(** Interns a string into one of 17 temporary slots reserved for ppx_tracing. These
slots are exclusively used to store the category, name, and names of arguments
when they cannot be interned globally. Unlike [set_temp_string_slot], strings
interned to these slots are valid *only* immediately after interning, as the slot
will be reused in subsequent probes. *)valset_dyn_slot:t->slot:int->local_string->String_id.t(** Like [set_dyn_slot] but optimised to write a 16-byte (2-word) value. It bypasses
the variable-length string interning path and writes directly into the slot. *)valset_dyn_slot_2_int64s:t->slot:int->local_Int64.t->local_Int64.t->String_id.t(** Write an event using a pre-composed header, and using less safety checking than
the normal event writing functions, intended for low-overhead probe
instrumentation. Also uses the [rdtsc] counter for the ticks field, meaning this
[Writer] must have been created with a calibrated [ticks_per_second] field to have
correct timing.
The pre-composition itself saves about 3ns per event. The omission of unnecessary
checks saves additional time. Overall in benchmarks this is about 2x faster than
using the usual [write_*] functions, saving about 6ns per event.
The only caller-visible check omission should be that it doesn't check the
corectness of arguments, which can result in invalid trace files if the arguments
written don't match the header. Users of this function must use
[Write_arg_unchecked] because it doesn't set the necessary state for checking. *)valwrite_from_header_with_tsc:t->header:local_header->unit(** Same as [write_from_header_with_tsc] but returns ticks. See
[Tracing.Writer.write_duration_instant]. *)valwrite_from_header_and_get_tsc:t->header:local_header->Time_stamp_counter.t(** Unchecked writing of the result of [write_from_header_and_get_tsc] after the
arguments. *)valwrite_tsc:t->Time_stamp_counter.t->unit(** Unchecked write of a string stream. *)valwrite_string_stream:t->local_string->unit(** Unchecked write of a correlation id to complete an async event. *)valwrite_async_id:t->int->unit(** Enables or disables mapping [String_id.t] to [string] when interning strings.
Use with caution: Enabling this will allocate upon interning strings and cost an
additional [Hashtbl.set] call. *)valset_string_map_allocate_on_intern:t->enable:bool->unit(** Returns a string represented by the corresponding [String_id.t] *)valstring_of_string_id:t->String_id.t->stringoption(** Unchecked versions of the [Write_arg] functions that can result in invalid traces. *)moduleWrite_arg_unchecked:Arg_writerswithtypet:=tandtypestring_id:=String_id.tendend