Merge pull request #36 from c-cube/simon/fuchsia-via-subscriber-2025-05-02

full refactoring of TEF and fuchsia backends
This commit is contained in:
Simon Cruanes 2025-05-08 09:44:58 -04:00 committed by GitHub
commit e6b17c5536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 1585 additions and 1304 deletions

View file

@ -51,6 +51,11 @@ jobs:
- run: opam install hmap
- run: opam exec -- dune build '@install' -p trace,trace-tef,trace-fuchsia
- run: opam install picos_aux
if: matrix.ocaml-compiler != '4.08.x' && matrix.ocaml-compiler != '4.12.x'
- run: opam exec -- dune build '@install' -p trace,trace-tef,trace-fuchsia
if: matrix.ocaml-compiler != '4.08.x' && matrix.ocaml-compiler != '4.12.x'
- run: opam install mtime
- run: opam exec -- dune build '@install' -p trace,trace-tef,trace-fuchsia

View file

@ -1,23 +1,34 @@
open Trace_fuchsia_write
open Trace_fuchsia
open Trace_fuchsia.Writer
module B = Benchmark
let pf = Printf.printf
let encode_1_span (out : Output.t) () =
Event.Duration_complete.encode out ~name:"span" ~t_ref:(Thread_ref.Ref 5)
~time_ns:100_000L ~end_time_ns:5_000_000L ~args:[] ()
let encode_1000_span (bufs : Buf_chain.t) () =
for _i = 1 to 1000 do
Event.Duration_complete.encode bufs ~name:"span" ~t_ref:(Thread_ref.Ref 5)
~time_ns:100_000L ~end_time_ns:5_000_000L ~args:[] ()
done;
Buf_chain.ready_all_non_empty bufs;
Buf_chain.pop_ready bufs ~f:ignore;
()
let encode_3_span (out : Output.t) () =
Event.Duration_complete.encode out ~name:"outer" ~t_ref:(Thread_ref.Ref 5)
~time_ns:100_000L ~end_time_ns:5_000_000L ~args:[] ();
Event.Duration_complete.encode out ~name:"inner" ~t_ref:(Thread_ref.Ref 5)
~time_ns:180_000L ~end_time_ns:4_500_000L ~args:[] ();
Event.Instant.encode out ~name:"hello" ~time_ns:1_234_567L
~t_ref:(Thread_ref.Ref 5)
~args:[ "x", `Int 42 ]
()
let encode_300_span (bufs : Buf_chain.t) () =
for _i = 1 to 100 do
Event.Duration_complete.encode bufs ~name:"outer" ~t_ref:(Thread_ref.Ref 5)
~time_ns:100_000L ~end_time_ns:5_000_000L ~args:[] ();
Event.Duration_complete.encode bufs ~name:"inner" ~t_ref:(Thread_ref.Ref 5)
~time_ns:180_000L ~end_time_ns:4_500_000L ~args:[] ();
Event.Instant.encode bufs ~name:"hello" ~time_ns:1_234_567L
~t_ref:(Thread_ref.Ref 5)
~args:[ "x", A_int 42 ]
()
done;
Buf_chain.ready_all_non_empty bufs;
Buf_chain.pop_ready bufs ~f:ignore;
()
let time_per_iter_ns (samples : B.t list) : float =
let time_per_iter_ns n_per_iter (samples : B.t list) : float =
let n_iters = ref 0L in
let time = ref 0. in
List.iter
@ -25,34 +36,32 @@ let time_per_iter_ns (samples : B.t list) : float =
n_iters := Int64.add !n_iters s.iters;
time := !time +. s.stime +. s.utime)
samples;
!time *. 1e9 /. Int64.to_float !n_iters
!time *. 1e9 /. (Int64.to_float !n_iters *. float n_per_iter)
let () =
let buf_pool = Buf_pool.create () in
let out =
Output.create ~buf_pool
~send_buf:(fun buf -> Buf_pool.recycle buf_pool buf)
()
in
let bufs = Buf_chain.create ~sharded:false ~buf_pool () in
let samples = B.throughput1 4 ~name:"encode_1_span" (encode_1_span out) () in
let samples =
B.throughput1 4 ~name:"encode_1000_span" (encode_1000_span bufs) ()
in
B.print_gc samples;
let [ (_, samples) ] = samples [@@warning "-8"] in
let iter_per_ns = time_per_iter_ns samples in
let iter_per_ns = time_per_iter_ns 1000 samples in
pf "%.3f ns/iter\n" iter_per_ns;
()
let () =
let buf_pool = Buf_pool.create () in
let out =
Output.create ~buf_pool
~send_buf:(fun buf -> Buf_pool.recycle buf_pool buf)
()
let bufs = Buf_chain.create ~sharded:false ~buf_pool () in
let samples =
B.throughput1 4 ~name:"encode_300_span" (encode_300_span bufs) ()
in
let samples = B.throughput1 4 ~name:"encode_3_span" (encode_3_span out) () in
B.print_gc samples;
let [ (_, samples) ] = samples [@@warning "-8"] in
let iter_per_ns = time_per_iter_ns 300 samples in
pf "%.3f ns/iter\n" iter_per_ns;
()

View file

@ -20,4 +20,4 @@
(executable
(name bench_fuchsia_write)
(modules bench_fuchsia_write)
(libraries benchmark trace-fuchsia.write))
(libraries benchmark trace-fuchsia))

View file

@ -28,6 +28,7 @@
(depopts
hmap
unix
(picos_aux (>= 0.6))
(mtime
(>= 2.0)))
(tags

7
src/event/dune Normal file
View file

@ -0,0 +1,7 @@
(library
(name trace_event)
(public_name trace.event)
(synopsis "Turns subscriber callbacks into an event type")
(libraries
(re_export trace.core) (re_export trace.subscriber)))

View file

@ -1,26 +1,28 @@
open Trace_core
module Sub = Trace_subscriber
(** An event, specialized for TEF *)
(** An event with TEF/fuchsia semantics *)
type t =
| E_tick
| E_init of { time_ns: int64 }
| E_shutdown of { time_ns: int64 }
| E_message of {
tid: int;
msg: string;
time_us: float;
time_ns: int64;
data: (string * Sub.user_data) list;
}
| E_define_span of {
tid: int;
name: string;
time_us: float;
time_ns: int64;
id: span;
fun_name: string option;
data: (string * Sub.user_data) list;
}
| E_exit_span of {
id: span;
time_us: float;
time_ns: int64;
}
| E_add_data of {
id: span;
@ -29,7 +31,7 @@ type t =
| E_enter_manual_span of {
tid: int;
name: string;
time_us: float;
time_ns: int64;
id: trace_id;
flavor: Sub.flavor option;
fun_name: string option;
@ -38,7 +40,7 @@ type t =
| E_exit_manual_span of {
tid: int;
name: string;
time_us: float;
time_ns: int64;
flavor: Sub.flavor option;
data: (string * Sub.user_data) list;
id: trace_id;
@ -46,7 +48,7 @@ type t =
| E_counter of {
name: string;
tid: int;
time_us: float;
time_ns: int64;
n: float;
}
| E_name_process of { name: string }
@ -54,3 +56,8 @@ type t =
tid: int;
name: string;
}
| E_extension_event of {
tid: int;
time_ns: int64;
ext: Trace_core.extension_event;
}

53
src/event/subscriber.ml Normal file
View file

@ -0,0 +1,53 @@
open Trace_core
open Event
type event_consumer = { on_event: Event.t -> unit } [@@unboxed]
(** Callback for events *)
module Callbacks : Sub.Callbacks.S with type st = event_consumer = struct
type st = event_consumer
let on_init (self : st) ~time_ns = self.on_event (E_init { time_ns })
let on_shutdown (self : st) ~time_ns = self.on_event (E_shutdown { time_ns })
let on_name_process (self : st) ~time_ns:_ ~tid:_ ~name : unit =
self.on_event @@ E_name_process { name }
let on_name_thread (self : st) ~time_ns:_ ~tid ~name : unit =
self.on_event @@ E_name_thread { tid; name }
let[@inline] on_enter_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~data ~name span : unit =
self.on_event
@@ E_define_span { tid; name; time_ns; id = span; fun_name; data }
let on_exit_span (self : st) ~time_ns ~tid:_ span : unit =
self.on_event @@ E_exit_span { id = span; time_ns }
let on_add_data (self : st) ~data span =
if data <> [] then self.on_event @@ E_add_data { id = span; data }
let on_message (self : st) ~time_ns ~tid ~span:_ ~data msg : unit =
self.on_event @@ E_message { tid; time_ns; msg; data }
let on_counter (self : st) ~time_ns ~tid ~data:_ ~name f : unit =
self.on_event @@ E_counter { name; n = f; time_ns; tid }
let on_enter_manual_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~parent:_ ~data ~name ~flavor ~trace_id _span :
unit =
self.on_event
@@ E_enter_manual_span
{ id = trace_id; time_ns; tid; data; name; fun_name; flavor }
let on_exit_manual_span (self : st) ~time_ns ~tid ~name ~data ~flavor
~trace_id (_ : span) : unit =
self.on_event
@@ E_exit_manual_span { tid; id = trace_id; name; time_ns; data; flavor }
let on_extension_event (self : st) ~time_ns ~tid ext : unit =
self.on_event @@ E_extension_event { tid; time_ns; ext }
end
let subscriber (consumer : event_consumer) : Sub.t =
Sub.Subscriber.Sub { st = consumer; callbacks = (module Callbacks) }

View file

@ -1,62 +0,0 @@
open Common_
type out =
[ `Stdout
| `Stderr
| `File of string
]
type event =
| E_write_buf of Buf.t
| E_tick
type state = {
buf_pool: Buf_pool.t;
oc: out_channel;
events: event B_queue.t;
}
let with_out_ (out : out) f =
let oc, must_close =
match out with
| `Stdout -> stdout, false
| `Stderr -> stderr, false
| `File path -> open_out path, true
in
if must_close then (
let finally () = close_out_noerr oc in
Fun.protect ~finally (fun () -> f oc)
) else
f oc
let handle_ev (self : state) (ev : event) : unit =
match ev with
| E_tick -> flush self.oc
| E_write_buf buf ->
output self.oc buf.buf 0 buf.offset;
Buf_pool.recycle self.buf_pool buf
let bg_loop (self : state) : unit =
let continue = ref true in
while !continue do
match B_queue.pop_all self.events with
| exception B_queue.Closed -> continue := false
| evs -> List.iter (handle_ev self) evs
done
let bg_thread ~buf_pool ~out ~(events : event B_queue.t) () : unit =
let@ oc = with_out_ out in
let st = { oc; buf_pool; events } in
bg_loop st
(** Thread that simply regularly "ticks", sending events to the background
thread so it has a chance to write to the file, and call [f()] *)
let tick_thread events : unit =
try
while true do
Thread.delay 0.5;
B_queue.push events E_tick
done
with B_queue.Closed -> ()

View file

@ -8,12 +8,14 @@ type t = {
let empty : t = { buf = Bytes.empty; offset = 0 }
let create (n : int) : t =
(* multiple of 8-bytes size *)
let buf = Bytes.create (round_to_word n) in
{ buf; offset = 0 }
let[@inline] clear self = self.offset <- 0
let[@inline] available self = Bytes.length self.buf - self.offset
let[@inline] size self = self.offset
let[@inline] is_empty self = self.offset = 0
(* see below: we assume little endian *)
let () = assert (not Sys.big_endian)

140
src/fuchsia/buf_chain.ml Normal file
View file

@ -0,0 +1,140 @@
(** A set of buffers in use, and a set of ready buffers *)
open Common_
(** Buffers in use *)
type buffers =
| B_one of { mutable buf: Buf.t }
| B_many of Buf.t Lock.t array
(** mask(thread id) -> buffer. This reduces contention *)
type t = {
bufs: buffers;
has_ready: bool A.t;
ready: Buf.t Queue.t Lock.t;
(** Buffers that are full (enough) and must be written *)
buf_pool: Buf_pool.t;
}
(** A set of buffers, some of which are ready to be written *)
open struct
let shard_log = 4
let shard = 1 lsl shard_log
let shard_mask = shard - 1
end
let create ~(sharded : bool) ~(buf_pool : Buf_pool.t) () : t =
let bufs =
if sharded then (
let bufs =
Array.init shard (fun _ -> Lock.create @@ Buf_pool.alloc buf_pool)
in
B_many bufs
) else
B_one { buf = Buf_pool.alloc buf_pool }
in
{
bufs;
buf_pool;
has_ready = A.make false;
ready = Lock.create @@ Queue.create ();
}
open struct
let put_in_ready (self : t) buf : unit =
if Buf.size buf > 0 then (
let@ q = Lock.with_ self.ready in
A.set self.has_ready true;
Queue.push buf q
)
let assert_available buf ~available =
if Buf.available buf < available then (
let msg =
Printf.sprintf
"fuchsia: buffer is too small (available: %d bytes, needed: %d bytes)"
(Buf.available buf) available
in
failwith msg
)
end
(** Move all non-empty buffers to [ready] *)
let ready_all_non_empty (self : t) : unit =
let@ q = Lock.with_ self.ready in
match self.bufs with
| B_one r ->
if not (Buf.is_empty r.buf) then (
Queue.push r.buf q;
A.set self.has_ready true;
r.buf <- Buf.empty
)
| B_many bufs ->
Array.iter
(fun buf ->
Lock.update buf (fun buf ->
if Buf.size buf > 0 then (
Queue.push buf q;
A.set self.has_ready true;
Buf.empty
) else
buf))
bufs
let[@inline] has_ready self : bool = A.get self.has_ready
(** Get access to ready buffers, then clean them up automatically *)
let pop_ready (self : t) ~(f : Buf.t Queue.t -> 'a) : 'a =
let@ q = Lock.with_ self.ready in
let res = f q in
(* clear queue *)
Queue.iter (Buf_pool.recycle self.buf_pool) q;
Queue.clear q;
A.set self.has_ready false;
res
(** Maximum size available, in words, for a single message *)
let[@inline] max_size_word (_self : t) : int = fuchsia_buf_size lsr 3
(** Obtain a buffer with at least [available_word] 64-bit words *)
let with_buf (self : t) ~(available_word : int) (f : Buf.t -> 'a) : 'a =
let available = available_word lsl 3 in
match self.bufs with
| B_one r ->
if Buf.available r.buf < available_word then (
put_in_ready self r.buf;
r.buf <- Buf_pool.alloc self.buf_pool
);
assert_available r.buf ~available;
f r.buf
| B_many bufs ->
let tid = Thread.(id (self ())) in
let masked_tid = tid land shard_mask in
let buf_lock = bufs.(masked_tid) in
let@ buf = Lock.with_ buf_lock in
let buf =
if Buf.available buf < available then (
put_in_ready self buf;
let new_buf = Buf_pool.alloc self.buf_pool in
assert_available new_buf ~available;
Lock.set_while_locked buf_lock new_buf;
new_buf
) else
buf
in
f buf
(** Dispose of resources (here, recycle buffers) *)
let dispose (self : t) : unit =
match self.bufs with
| B_one r ->
Buf_pool.recycle self.buf_pool r.buf;
r.buf <- Buf.empty
| B_many bufs ->
Array.iter
(fun buf_lock ->
let@ buf = Lock.with_ buf_lock in
Buf_pool.recycle self.buf_pool buf;
Lock.set_while_locked buf_lock Buf.empty)
bufs

23
src/fuchsia/buf_pool.ml Normal file
View file

@ -0,0 +1,23 @@
open Common_
open Trace_private_util
type t = Buf.t Rpool.t
let create ?(max_size = 64) () : t =
Rpool.create ~max_size ~clear:Buf.clear
~create:(fun () -> Buf.create fuchsia_buf_size)
()
let alloc = Rpool.alloc
let[@inline] recycle self buf = if buf != Buf.empty then Rpool.recycle self buf
let with_ (self : t) f =
let x = alloc self in
try
let res = f x in
recycle self x;
res
with e ->
let bt = Printexc.get_raw_backtrace () in
recycle self x;
Printexc.raise_with_backtrace e bt

View file

@ -1,12 +1,22 @@
module A = Trace_core.Internal_.Atomic_
module FWrite = Trace_fuchsia_write
module B_queue = Trace_private_util.B_queue
module Buf = FWrite.Buf
module Buf_pool = FWrite.Buf_pool
module Output = FWrite.Output
module Sub = Trace_subscriber
let on_tracing_error =
ref (fun s -> Printf.eprintf "trace-fuchsia error: %s\n%!" s)
let ( let@ ) = ( @@ )
let spf = Printf.sprintf
let with_lock lock f =
Mutex.lock lock;
try
let res = f () in
Mutex.unlock lock;
res
with e ->
let bt = Printexc.get_raw_backtrace () in
Mutex.unlock lock;
Printexc.raise_with_backtrace e bt
(** Buffer size we use. *)
let fuchsia_buf_size = 1 lsl 16

View file

@ -6,8 +6,8 @@
(libraries
trace.core
trace.private.util
trace.subscriber
thread-local-storage
(re_export trace-fuchsia.write)
bigarray
mtime
mtime.clock.os

61
src/fuchsia/exporter.ml Normal file
View file

@ -0,0 +1,61 @@
(** An exporter, takes buffers with fuchsia events, and writes them somewhere *)
open Common_
type t = {
write_bufs: Buf.t Queue.t -> unit;
(** Takes buffers and writes them somewhere. The buffers are only valid
during this call and must not be stored. The queue must not be
modified. *)
flush: unit -> unit; (** Force write *)
close: unit -> unit; (** Close underlying resources *)
}
(** An exporter, takes buffers and writes them somewhere. This should be
thread-safe if used in a threaded environment. *)
open struct
let with_lock lock f =
Mutex.lock lock;
try
let res = f () in
Mutex.unlock lock;
res
with e ->
let bt = Printexc.get_raw_backtrace () in
Mutex.unlock lock;
Printexc.raise_with_backtrace e bt
end
(** Export to the channel
@param close_channel if true, closing the exporter will close the channel *)
let of_out_channel ~close_channel oc : t =
let lock = Mutex.create () in
let closed = ref false in
let flush () =
let@ () = with_lock lock in
flush oc
in
let close () =
let@ () = with_lock lock in
if not !closed then (
closed := true;
if close_channel then close_out_noerr oc
)
in
let write_bufs bufs =
if not (Queue.is_empty bufs) then
let@ () = with_lock lock in
Queue.iter (fun (buf : Buf.t) -> output oc buf.buf 0 buf.offset) bufs
in
{ flush; close; write_bufs }
let of_buffer (buffer : Buffer.t) : t =
let buffer = Lock.create buffer in
let write_bufs bufs =
if not (Queue.is_empty bufs) then
let@ buffer = Lock.with_ buffer in
Queue.iter
(fun (buf : Buf.t) -> Buffer.add_subbytes buffer buf.buf 0 buf.offset)
bufs
in
{ flush = ignore; close = ignore; write_bufs }

View file

@ -1,410 +0,0 @@
open Trace_core
open Common_
module TLS = Thread_local_storage
module Int_map = Map.Make (Int)
let pid = Unix.getpid ()
(** Thread-local stack of span info *)
module Span_info_stack : sig
type t
val create : unit -> t
val push :
t ->
span ->
name:string ->
start_time_ns:int64 ->
data:(string * user_data) list ->
unit
val pop : t -> int64 * string * int64 * (string * user_data) list
val find_ : t -> span -> int option
val add_data : t -> int -> (string * user_data) list -> unit
end = struct
module BA = Bigarray
module BA1 = Bigarray.Array1
type int64arr = (int64, BA.int64_elt, BA.c_layout) BA1.t
type t = {
mutable len: int;
mutable span: int64arr;
mutable start_time_ns: int64arr;
mutable name: string array;
mutable data: (string * user_data) list array;
}
let init_size_ = 1
let create () : t =
{
len = 0;
span = BA1.create BA.Int64 BA.C_layout init_size_;
start_time_ns = BA1.create BA.Int64 BA.C_layout init_size_;
name = Array.make init_size_ "";
data = Array.make init_size_ [];
}
let[@inline] cap self = Array.length self.name
let grow_ (self : t) : unit =
let new_cap = 2 * cap self in
let new_span = BA1.create BA.Int64 BA.C_layout new_cap in
BA1.blit self.span (BA1.sub new_span 0 self.len);
let new_startime_ns = BA1.create BA.Int64 BA.C_layout new_cap in
BA1.blit self.start_time_ns (BA1.sub new_startime_ns 0 self.len);
let new_name = Array.make new_cap "" in
Array.blit self.name 0 new_name 0 self.len;
let new_data = Array.make new_cap [] in
Array.blit self.data 0 new_data 0 self.len;
self.span <- new_span;
self.start_time_ns <- new_startime_ns;
self.name <- new_name;
self.data <- new_data
let push (self : t) (span : int64) ~name ~start_time_ns ~data =
if cap self = self.len then grow_ self;
BA1.set self.span self.len span;
BA1.set self.start_time_ns self.len start_time_ns;
Array.set self.name self.len name;
Array.set self.data self.len data;
self.len <- self.len + 1
let pop (self : t) =
assert (self.len > 0);
self.len <- self.len - 1;
let span = BA1.get self.span self.len in
let name = self.name.(self.len) in
let start_time_ns = BA1.get self.start_time_ns self.len in
let data = self.data.(self.len) in
(* avoid holding onto old values *)
Array.set self.name self.len "";
Array.set self.data self.len [];
span, name, start_time_ns, data
let[@inline] add_data self i d : unit =
assert (i < self.len);
self.data.(i) <- List.rev_append d self.data.(i)
exception Found of int
let[@inline] find_ (self : t) span : _ option =
try
for i = self.len - 1 downto 0 do
if Int64.equal (BA1.get self.span i) span then raise_notrace (Found i)
done;
None
with Found i -> Some i
end
type async_span_info = {
flavor: [ `Sync | `Async ] option;
name: string;
mutable data: (string * user_data) list;
}
let key_async_data : async_span_info Meta_map.key = Meta_map.Key.create ()
open struct
let state_id_ = A.make 0
(* re-raise exception with its backtrace *)
external reraise : exn -> 'a = "%reraise"
end
type per_thread_state = {
tid: int;
state_id: int; (** ID of the current collector state *)
local_span_id_gen: int A.t; (** Used for thread-local spans *)
mutable thread_ref: FWrite.Thread_ref.t;
mutable out: Output.t option;
spans: Span_info_stack.t; (** In-flight spans *)
}
type state = {
active: bool A.t;
events: Bg_thread.event B_queue.t;
span_id_gen: int A.t; (** Used for async spans *)
bg_thread: Thread.t;
buf_pool: Buf_pool.t;
next_thread_ref: int A.t; (** in [0x01 .. 0xff], to allocate thread refs *)
per_thread: per_thread_state Int_map.t A.t array;
(** the state keeps tabs on thread-local state, so it can flush writers at
the end. This is a tid-sharded array of maps. *)
}
let[@inline] mk_trace_id (self : state) : trace_id =
let n = A.fetch_and_add self.span_id_gen 1 in
let b = Bytes.create 8 in
Bytes.set_int64_le b 0 (Int64.of_int n);
Bytes.unsafe_to_string b
let key_thread_local_st : per_thread_state TLS.t = TLS.create ()
let[@inline never] mk_thread_local_st () =
let tid = Thread.id @@ Thread.self () in
let st =
{
tid;
state_id = A.get state_id_;
thread_ref = FWrite.Thread_ref.inline ~pid ~tid;
local_span_id_gen = A.make 0;
out = None;
spans = Span_info_stack.create ();
}
in
TLS.set key_thread_local_st st;
st
let[@inline] get_thread_local_st () =
match TLS.get_opt key_thread_local_st with
| Some k -> k
| None -> mk_thread_local_st ()
let out_of_st (st : state) : Output.t =
FWrite.Output.create () ~buf_pool:st.buf_pool ~send_buf:(fun buf ->
try B_queue.push st.events (E_write_buf buf) with B_queue.Closed -> ())
module C
(St : sig
val st : state
end)
() =
struct
open St
let state_id = 1 + A.fetch_and_add state_id_ 1
(** prepare the thread's state *)
let[@inline never] update_or_init_local_state (self : per_thread_state) : unit
=
(* get an output *)
let out = out_of_st st in
self.out <- Some out;
(* try to allocate a thread ref for current thread *)
let th_ref = A.fetch_and_add st.next_thread_ref 1 in
if th_ref <= 0xff then (
self.thread_ref <- FWrite.Thread_ref.ref th_ref;
FWrite.Thread_record.encode out ~as_ref:th_ref ~tid:self.tid ~pid ()
);
(* add to [st]'s list of threads *)
let shard_of_per_thread = st.per_thread.(self.tid land 0b1111) in
while
let old = A.get shard_of_per_thread in
not
(A.compare_and_set shard_of_per_thread old
(Int_map.add self.tid self old))
do
()
done;
let on_exit _ =
while
let old = A.get shard_of_per_thread in
not
(A.compare_and_set shard_of_per_thread old
(Int_map.remove self.tid old))
do
()
done;
Option.iter Output.flush self.out
in
(* after thread exits, flush output and remove from global list *)
Gc.finalise on_exit (Thread.self ());
()
(** Obtain the output for the current thread *)
let[@inline] get_thread_output () : Output.t * per_thread_state =
let tls = get_thread_local_st () in
if tls.state_id != state_id || tls.out == None then
update_or_init_local_state tls;
let out =
match tls.out with
| None -> assert false
| Some o -> o
in
out, tls
let close_per_thread (tls : per_thread_state) =
Option.iter Output.flush tls.out
(** flush all outputs *)
let flush_all_outputs_ () =
Array.iter
(fun shard ->
let tls_l = A.get shard in
Int_map.iter (fun _tid tls -> close_per_thread tls) tls_l)
st.per_thread
let shutdown () =
if A.exchange st.active false then (
flush_all_outputs_ ();
B_queue.close st.events;
(* wait for writer thread to be done. The writer thread will exit
after processing remaining events because the queue is now closed *)
Thread.join st.bg_thread
)
let enter_span ~__FUNCTION__:_ ~__FILE__:_ ~__LINE__:_ ~data name : span =
let tls = get_thread_local_st () in
let span = Int64.of_int (A.fetch_and_add tls.local_span_id_gen 1) in
let time_ns = Time.now_ns () in
Span_info_stack.push tls.spans span ~name ~data ~start_time_ns:time_ns;
span
let exit_span span : unit =
let out, tls = get_thread_output () in
let end_time_ns = Time.now_ns () in
let span', name, start_time_ns, data = Span_info_stack.pop tls.spans in
if span <> span' then
!on_tracing_error
(spf "span mismatch: top is %Ld, expected %Ld" span' span)
else
FWrite.Event.Duration_complete.encode out ~name ~t_ref:tls.thread_ref
~time_ns:start_time_ns ~end_time_ns ~args:data ()
let with_span ~__FUNCTION__:_ ~__FILE__:_ ~__LINE__:_ ~data name f =
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
let span = Int64.of_int (A.fetch_and_add tls.local_span_id_gen 1) in
Span_info_stack.push tls.spans span ~start_time_ns:time_ns ~data ~name;
let[@inline] exit () : unit =
let end_time_ns = Time.now_ns () in
let _span', _, _, data = Span_info_stack.pop tls.spans in
assert (span = _span');
FWrite.Event.Duration_complete.encode out ~name ~time_ns ~end_time_ns
~t_ref:tls.thread_ref ~args:data ()
in
try
let x = f span in
exit ();
x
with exn ->
exit ();
reraise exn
let add_data_to_span span data =
let tls = get_thread_local_st () in
match Span_info_stack.find_ tls.spans span with
| None -> !on_tracing_error (spf "unknown span %Ld" span)
| Some idx -> Span_info_stack.add_data tls.spans idx data
let enter_manual_span ~(parent : explicit_span_ctx option) ~flavor
~__FUNCTION__:_ ~__FILE__:_ ~__LINE__:_ ~data name : explicit_span =
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
(* get the id, or make a new one *)
let trace_id =
match parent with
| Some m -> m.trace_id
| None -> mk_trace_id st
in
FWrite.Event.Async_begin.encode out ~name ~args:data ~t_ref:tls.thread_ref
~time_ns ~async_id:trace_id ();
{
span = 0L;
trace_id;
meta = Meta_map.(empty |> add key_async_data { name; flavor; data = [] });
}
let exit_manual_span (es : explicit_span) : unit =
let { name; data; flavor = _ } = Meta_map.find_exn key_async_data es.meta in
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
FWrite.Event.Async_end.encode out ~name ~t_ref:tls.thread_ref ~time_ns
~args:data ~async_id:es.trace_id ()
let add_data_to_manual_span (es : explicit_span) data =
let m = Meta_map.find_exn key_async_data es.meta in
m.data <- List.rev_append data m.data
let message ?span:_ ~data msg : unit =
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
FWrite.Event.Instant.encode out ~name:msg ~time_ns ~t_ref:tls.thread_ref
~args:data ()
let counter_float ~data name f =
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
FWrite.Event.Counter.encode out ~name:"c" ~time_ns ~t_ref:tls.thread_ref
~args:((name, `Float f) :: data)
()
let counter_int ~data name i =
let out, tls = get_thread_output () in
let time_ns = Time.now_ns () in
FWrite.Event.Counter.encode out ~name:"c" ~time_ns ~t_ref:tls.thread_ref
~args:((name, `Int i) :: data)
()
let name_process name : unit =
let out, _tls = get_thread_output () in
FWrite.Kernel_object.(encode out ~name ~ty:ty_process ~kid:pid ~args:[] ())
let name_thread name : unit =
let out, tls = get_thread_output () in
FWrite.Kernel_object.(
encode out ~name ~ty:ty_thread ~kid:tls.tid
~args:[ "process", `Kid pid ]
())
let extension_event _ = ()
end
let create ~out () : collector =
let buf_pool = Buf_pool.create () in
let events = B_queue.create () in
let bg_thread =
Thread.create (Bg_thread.bg_thread ~buf_pool ~out ~events) ()
in
let st =
{
active = A.make true;
buf_pool;
bg_thread;
events;
span_id_gen = A.make 0;
next_thread_ref = A.make 1;
per_thread = Array.init 16 (fun _ -> A.make Int_map.empty);
}
in
let _tick_thread = Thread.create (fun () -> Bg_thread.tick_thread events) in
(* write header *)
let out = out_of_st st in
FWrite.Metadata.Magic_record.encode out;
FWrite.Metadata.Initialization_record.(
encode out ~ticks_per_secs:default_ticks_per_sec ());
FWrite.Metadata.Provider_info.encode out ~id:0 ~name:"ocaml-trace" ();
Output.flush out;
Output.dispose out;
let module Coll =
C
(struct
let st = st
end)
()
in
(module Coll)

View file

@ -1,3 +0,0 @@
open Trace_core
val create : out:Bg_thread.out -> unit -> collector

28
src/fuchsia/lock.ml Normal file
View file

@ -0,0 +1,28 @@
type 'a t = {
mutex: Mutex.t;
mutable content: 'a;
}
let create content : _ t = { mutex = Mutex.create (); content }
let with_ (self : _ t) f =
Mutex.lock self.mutex;
try
let x = f self.content in
Mutex.unlock self.mutex;
x
with e ->
let bt = Printexc.get_raw_backtrace () in
Mutex.unlock self.mutex;
Printexc.raise_with_backtrace e bt
let[@inline] update self f = with_ self (fun x -> self.content <- f x)
let[@inline] update_map l f =
with_ l (fun x ->
let x', y = f x in
l.content <- x';
y)
let[@inline] set_while_locked (self : 'a t) (x : 'a) = self.content <- x

10
src/fuchsia/lock.mli Normal file
View file

@ -0,0 +1,10 @@
type 'a t
(** A value protected by a mutex *)
val create : 'a -> 'a t
val with_ : 'a t -> ('a -> 'b) -> 'b
val update : 'a t -> ('a -> 'a) -> unit
val update_map : 'a t -> ('a -> 'a * 'b) -> 'b
val set_while_locked : 'a t -> 'a -> unit
(** Change the value while inside [with_] or similar. *)

172
src/fuchsia/subscriber.ml Normal file
View file

@ -0,0 +1,172 @@
open Common_
open Trace_core
module Span_tbl = Trace_subscriber.Span_tbl
let on_tracing_error = on_tracing_error
type span_info = {
tid: int;
name: string;
start_ns: int64;
mutable data: (string * Sub.user_data) list;
(* NOTE: thread safety: this is supposed to only be modified by the thread
that's running this (synchronous, stack-abiding) span. *)
}
(** Information we store about a span begin event, to emit a complete event when
we meet the corresponding span end event *)
type t = {
active: bool A.t;
pid: int;
spans: span_info Span_tbl.t;
buf_chain: Buf_chain.t;
exporter: Exporter.t;
}
(** Subscriber state *)
open struct
(** Write the buffers that are ready *)
let[@inline] write_ready_ (self : t) =
if Buf_chain.has_ready self.buf_chain then
Buf_chain.pop_ready self.buf_chain ~f:self.exporter.write_bufs
let print_non_closed_spans_warning spans =
let module Str_set = Set.Make (String) in
let spans = Span_tbl.to_list spans in
if spans <> [] then (
!on_tracing_error
@@ Printf.sprintf "warning: %d spans were not closed" (List.length spans);
let names =
List.fold_left
(fun set (_, span) -> Str_set.add span.name set)
Str_set.empty spans
in
Str_set.iter
(fun name ->
!on_tracing_error @@ Printf.sprintf " span %S was not closed" name)
names;
flush stderr
)
end
let close (self : t) : unit =
if A.exchange self.active false then (
Buf_chain.ready_all_non_empty self.buf_chain;
write_ready_ self;
self.exporter.close ();
print_non_closed_spans_warning self.spans
)
let[@inline] active self = A.get self.active
let flush (self : t) : unit =
Buf_chain.ready_all_non_empty self.buf_chain;
write_ready_ self;
self.exporter.flush ()
let create ?(buf_pool = Buf_pool.create ()) ~pid ~exporter () : t =
let buf_chain = Buf_chain.create ~sharded:true ~buf_pool () in
{ active = A.make true; buf_chain; exporter; pid; spans = Span_tbl.create () }
module Callbacks = struct
type st = t
let on_init (self : st) ~time_ns:_ =
Writer.Metadata.Magic_record.encode self.buf_chain;
Writer.Metadata.Initialization_record.(
encode self.buf_chain ~ticks_per_secs:default_ticks_per_sec ());
Writer.Metadata.Provider_info.encode self.buf_chain ~id:0
~name:"ocaml-trace" ();
(* make sure we write these immediately so they're not out of order *)
Buf_chain.ready_all_non_empty self.buf_chain;
write_ready_ self
let on_shutdown (self : st) ~time_ns:_ = close self
let on_name_process (self : st) ~time_ns:_ ~tid:_ ~name : unit =
Writer.Kernel_object.(
encode self.buf_chain ~name ~ty:ty_process ~kid:self.pid ~args:[] ());
write_ready_ self
let on_name_thread (self : st) ~time_ns:_ ~tid ~name : unit =
Writer.Kernel_object.(
encode self.buf_chain ~name ~ty:ty_thread ~kid:tid
~args:[ "process", A_kid (Int64.of_int self.pid) ]
());
write_ready_ self
(* add function name, if provided, to the metadata *)
let add_fun_name_ fun_name data : _ list =
match fun_name with
| None -> data
| Some f -> ("function", Sub.U_string f) :: data
let[@inline] on_enter_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~data ~name span : unit =
let data = add_fun_name_ fun_name data in
let info = { tid; name; start_ns = time_ns; data } in
(* save the span so we find it at exit *)
Span_tbl.add self.spans span info
let on_exit_span (self : st) ~time_ns ~tid:_ span : unit =
match Span_tbl.find_exn self.spans span with
| exception Not_found ->
!on_tracing_error (Printf.sprintf "cannot find span %Ld" span)
| { tid; name; start_ns; data } ->
Span_tbl.remove self.spans span;
Writer.(
Event.Duration_complete.encode self.buf_chain ~name
~t_ref:(Thread_ref.inline ~pid:self.pid ~tid)
~time_ns:start_ns ~end_time_ns:time_ns ~args:(args_of_user_data data)
());
write_ready_ self
let on_add_data (self : st) ~data span =
if data <> [] then (
try
let info = Span_tbl.find_exn self.spans span in
info.data <- List.rev_append data info.data
with Not_found ->
!on_tracing_error (Printf.sprintf "cannot find span %Ld" span)
)
let on_message (self : st) ~time_ns ~tid ~span:_ ~data msg : unit =
Writer.(
Event.Instant.encode self.buf_chain
~t_ref:(Thread_ref.inline ~pid:self.pid ~tid)
~name:msg ~time_ns ~args:(args_of_user_data data) ());
write_ready_ self
let on_counter (self : st) ~time_ns ~tid ~data ~name n : unit =
Writer.(
Event.Counter.encode self.buf_chain
~t_ref:(Thread_ref.inline ~pid:self.pid ~tid)
~name ~time_ns
~args:((name, A_float n) :: args_of_user_data data)
());
write_ready_ self
let on_enter_manual_span (self : st) ~__FUNCTION__:_ ~__FILE__:_ ~__LINE__:_
~time_ns ~tid ~parent:_ ~data ~name ~flavor:_ ~trace_id _span : unit =
Writer.(
Event.Async_begin.encode self.buf_chain ~name
~args:(args_of_user_data data)
~t_ref:(Thread_ref.inline ~pid:self.pid ~tid)
~time_ns ~async_id:trace_id ());
write_ready_ self
let on_exit_manual_span (self : st) ~time_ns ~tid ~name ~data ~flavor:_
~trace_id (_ : span) : unit =
Writer.(
Event.Async_end.encode self.buf_chain ~name ~args:(args_of_user_data data)
~t_ref:(Thread_ref.inline ~pid:self.pid ~tid)
~time_ns ~async_id:trace_id ());
write_ready_ self
let on_extension_event _ ~time_ns:_ ~tid:_ _ev = ()
end
let subscriber (self : t) : Sub.t =
Sub.Subscriber.Sub { st = self; callbacks = (module Callbacks) }

View file

@ -0,0 +1,20 @@
type t
(** Main subscriber state. *)
val create : ?buf_pool:Buf_pool.t -> pid:int -> exporter:Exporter.t -> unit -> t
(** Create a subscriber state. *)
val flush : t -> unit
val close : t -> unit
val active : t -> bool
module Callbacks : Trace_subscriber.Callbacks.S with type st = t
val subscriber : t -> Trace_subscriber.t
(** Subscriber that writes json into this writer *)
(**/**)
val on_tracing_error : (string -> unit) ref
(**/**)

View file

@ -1,31 +1,50 @@
open Common_
module Buf = Buf
module Buf_chain = Buf_chain
module Buf_pool = Buf_pool
module Exporter = Exporter
module Subscriber = Subscriber
module Writer = Writer
type output =
[ `Stdout
| `Stderr
| `File of string
[ `File of string
| `Exporter of Exporter.t
]
let collector = Fcollector.create
let get_out_ (out : [< output ]) : Exporter.t =
match out with
| `File path ->
let oc = open_out path in
Exporter.of_out_channel ~close_channel:true oc
| `Exporter e -> e
let subscriber ~out () : Sub.t =
let exporter = get_out_ out in
let pid =
if !Trace_subscriber.Private_.mock then
2
else
Unix.getpid ()
in
let sub = Subscriber.create ~pid ~exporter () in
Subscriber.subscriber sub
let collector ~out () = Sub.collector @@ subscriber ~out ()
let setup ?(out = `Env) () =
match out with
| `Stderr -> Trace_core.setup_collector @@ Fcollector.create ~out:`Stderr ()
| `Stdout -> Trace_core.setup_collector @@ Fcollector.create ~out:`Stdout ()
| `File path ->
Trace_core.setup_collector @@ Fcollector.create ~out:(`File path) ()
| `File path -> Trace_core.setup_collector @@ collector ~out:(`File path) ()
| `Exporter _ as out ->
let sub = subscriber ~out () in
Trace_core.setup_collector @@ Sub.collector sub
| `Env ->
(match Sys.getenv_opt "TRACE" with
| Some ("1" | "true") ->
let path = "trace.fxt" in
let c = Fcollector.create ~out:(`File path) () in
let c = collector ~out:(`File path) () in
Trace_core.setup_collector c
| Some "stdout" ->
Trace_core.setup_collector @@ Fcollector.create ~out:`Stdout ()
| Some "stderr" ->
Trace_core.setup_collector @@ Fcollector.create ~out:`Stderr ()
| Some path ->
let c = Fcollector.create ~out:(`File path) () in
let c = collector ~out:(`File path) () in
Trace_core.setup_collector c
| None -> ())
@ -33,6 +52,24 @@ let with_setup ?out () f =
setup ?out ();
Fun.protect ~finally:Trace_core.shutdown f
module Mock_ = struct
let now = ref 0
(* used to mock timing *)
let get_now_ns () : int64 =
let x = !now in
incr now;
Int64.(mul (of_int x) 1000L)
let get_tid_ () : int = 3
end
module Internal_ = struct
let mock_all_ () =
Sub.Private_.mock := true;
Sub.Private_.get_now_ns_ := Mock_.get_now_ns;
Sub.Private_.get_tid_ := Mock_.get_tid_;
()
let on_tracing_error = on_tracing_error
end

View file

@ -6,22 +6,23 @@
trace format}. This reduces the tracing overhead compared to [trace-tef],
at the expense of simplicity. *)
val collector :
out:[ `File of string | `Stderr | `Stdout ] -> unit -> Trace_core.collector
(** Make a collector that writes into the given output. See {!setup} for more
details. *)
module Buf = Buf
module Buf_chain = Buf_chain
module Buf_pool = Buf_pool
module Exporter = Exporter
module Subscriber = Subscriber
module Writer = Writer
type output =
[ `Stdout
| `Stderr
| `File of string
[ `File of string
| `Exporter of Exporter.t
]
(** Output for tracing.
- [`Stdout] will enable tracing and print events on stdout
- [`Stderr] will enable tracing and print events on stderr
- [`File "foo"] will enable tracing and print events into file named "foo"
*)
val subscriber : out:[< output ] -> unit -> Trace_subscriber.t
val collector : out:[< output ] -> unit -> Trace_core.collector
(** Make a collector that writes into the given output. See {!setup} for more
details. *)
val setup : ?out:[ output | `Env ] -> unit -> unit
(** [setup ()] installs the collector depending on [out].
@ -32,12 +33,10 @@ val setup : ?out:[ output | `Env ] -> unit -> unit
- [`Env] will enable tracing if the environment variable "TRACE" is set.
- If it's set to "1", then the file is "trace.fxt".
- If it's set to "stdout", then logging happens on stdout (since 0.2)
- If it's set to "stderr", then logging happens on stdout (since 0.2)
- Otherwise, if it's set to a non empty string, the value is taken to be the
file path into which to write. *)
val with_setup : ?out:[ output | `Env ] -> unit -> (unit -> 'a) -> 'a
val with_setup : ?out:[< output | `Env > `Env ] -> unit -> (unit -> 'a) -> 'a
(** [with_setup () f] (optionally) sets a collector up, calls [f()], and makes
sure to shutdown before exiting. *)
@ -45,6 +44,9 @@ val with_setup : ?out:[ output | `Env ] -> unit -> (unit -> 'a) -> 'a
module Internal_ : sig
val on_tracing_error : (string -> unit) ref
val mock_all_ : unit -> unit
(** use fake, deterministic timestamps, TID, PID *)
end
(**/**)

View file

@ -1,58 +0,0 @@
open struct
module A = Trace_core.Internal_.Atomic_
exception Got_buf of Buf.t
end
module List_with_len = struct
type +'a t =
| Nil
| Cons of int * 'a * 'a t
let empty : _ t = Nil
let[@inline] len = function
| Nil -> 0
| Cons (i, _, _) -> i
let[@inline] cons x self = Cons (len self + 1, x, self)
end
type t = {
max_len: int;
buf_size: int;
bufs: Buf.t List_with_len.t A.t;
}
let create ?(max_len = 64) ?(buf_size = 1 lsl 16) () : t =
let buf_size = min (1 lsl 22) (max buf_size (1 lsl 15)) in
{ max_len; buf_size; bufs = A.make List_with_len.empty }
let alloc (self : t) : Buf.t =
try
while
match A.get self.bufs with
| Nil -> false
| Cons (_, buf, tl) as old ->
if A.compare_and_set self.bufs old tl then
raise (Got_buf buf)
else
false
do
()
done;
Buf.create self.buf_size
with Got_buf b -> b
let recycle (self : t) (buf : Buf.t) : unit =
Buf.clear buf;
try
while
match A.get self.bufs with
| Cons (i, _, _) when i >= self.max_len -> raise Exit
| old ->
not (A.compare_and_set self.bufs old (List_with_len.cons buf old))
do
()
done
with Exit -> () (* do not recycle *)

View file

@ -1,9 +0,0 @@
(library
(name trace_fuchsia_write)
(public_name trace-fuchsia.write)
(synopsis "Serialization part of trace-fuchsia")
(ocamlopt_flags
:standard
;-dlambda
)
(libraries trace.core threads))

View file

@ -1,65 +0,0 @@
type t = {
mutable buf: Buf.t;
mutable send_buf: Buf.t -> unit;
buf_pool: Buf_pool.t;
}
let create ~(buf_pool : Buf_pool.t) ~send_buf () : t =
let buf_size = buf_pool.buf_size in
let buf = Buf.create buf_size in
{ buf; send_buf; buf_pool }
open struct
(* NOTE: there is a potential race condition if an output is
flushed from the main thread upon closing, while
the local thread is blissfully writing new records to it
as we're winding down the collector. This is trying to reduce
the likelyhood of a race happening. *)
let[@poll error] replace_buf_ (self : t) (new_buf : Buf.t) : Buf.t =
let old_buf = self.buf in
self.buf <- new_buf;
old_buf
let flush_ (self : t) : unit =
let new_buf = Buf_pool.alloc self.buf_pool in
let old_buf = replace_buf_ self new_buf in
self.send_buf old_buf
let[@inline never] cycle_buf (self : t) ~available : Buf.t =
flush_ self;
let buf = self.buf in
if Buf.available buf < available then (
let msg =
Printf.sprintf
"fuchsia: buffer is too small (available: %d bytes, needed: %d bytes)"
(Buf.available buf) available
in
failwith msg
);
buf
end
let[@inline] flush (self : t) : unit = if Buf.size self.buf > 0 then flush_ self
(** Maximum size available, in words, for a single message *)
let[@inline] max_size_word (self : t) : int = self.buf_pool.buf_size lsr 3
(** Obtain a buffer with at least [available] bytes *)
let[@inline] get_buf (self : t) ~(available_word : int) : Buf.t =
let available = available_word lsl 3 in
if Buf.available self.buf >= available then
self.buf
else
cycle_buf self ~available
let into_buffer ~buf_pool (buffer : Buffer.t) : t =
let send_buf (buf : Buf.t) =
Buffer.add_subbytes buffer buf.buf 0 buf.offset
in
create ~buf_pool ~send_buf ()
let dispose (self : t) : unit =
flush_ self;
Buf_pool.recycle self.buf_pool self.buf;
self.buf <- Buf.empty

View file

@ -2,14 +2,10 @@
Reference: https://fuchsia.dev/fuchsia-src/reference/tracing/trace-format *)
open Common_
module Util = Util
module Buf = Buf
module Output = Output
module Buf_pool = Buf_pool
open struct
let spf = Printf.sprintf
let[@inline] int64_of_trace_id_ (id : Trace_core.trace_id) : int64 =
if id == Trace_core.Collector.dummy_trace_id then
0L
@ -19,7 +15,27 @@ end
open Util
type user_data = Trace_core.user_data
type user_data = Sub.user_data =
| U_bool of bool
| U_float of float
| U_int of int
| U_none
| U_string of string
type arg =
| A_bool of bool
| A_float of float
| A_int of int
| A_none
| A_string of string
| A_kid of int64
(* NOTE: only works because [user_data] is a prefix of [arg] and is immutable *)
let arg_of_user_data : user_data -> arg = Obj.magic
(* NOTE: only works because [user_data] is a prefix of [arg] and is immutable *)
let args_of_user_data : (string * user_data) list -> (string * arg) list =
Obj.magic
module I64 = struct
include Int64
@ -111,8 +127,8 @@ module Metadata = struct
let value = 0x0016547846040010L
let size_word = 1
let encode (out : Output.t) =
let buf = Output.get_buf out ~available_word:size_word in
let encode (bufs : Buf_chain.t) =
let@ buf = Buf_chain.with_buf bufs ~available_word:size_word in
Buf.add_i64 buf value
end
@ -122,8 +138,8 @@ module Metadata = struct
(** Default: 1 tick = 1 ns *)
let default_ticks_per_sec = 1_000_000_000L
let encode (out : Output.t) ~ticks_per_secs () : unit =
let buf = Output.get_buf out ~available_word:size_word in
let encode (bufs : Buf_chain.t) ~ticks_per_secs () : unit =
let@ buf = Buf_chain.with_buf bufs ~available_word:size_word in
let hd = I64.(1L lor (of_int size_word lsl 4)) in
Buf.add_i64 buf hd;
Buf.add_i64 buf ticks_per_secs
@ -132,10 +148,10 @@ module Metadata = struct
module Provider_info = struct
let size_word ~name () = 1 + str_len_word name
let encode (out : Output.t) ~(id : int) ~name () : unit =
let encode (bufs : Buf_chain.t) ~(id : int) ~name () : unit =
let name = truncate_string name in
let size = size_word ~name () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
(of_int size lsl 4)
@ -152,29 +168,29 @@ module Metadata = struct
end
module Argument = struct
type 'a t = string * ([< user_data | `Kid of int ] as 'a)
type t = string * arg
let check_valid_ : _ t -> unit = function
| _, `String s -> assert (String.length s < max_str_len)
let check_valid_ : t -> unit = function
| _, A_string s -> assert (String.length s < max_str_len)
| _ -> ()
let[@inline] is_i32_ (i : int) : bool = Int32.(to_int (of_int i) = i)
let size_word (self : _ t) =
let size_word (self : t) =
let name, data = self in
match data with
| `None | `Bool _ -> 1 + str_len_word name
| `Int i when is_i32_ i -> 1 + str_len_word name
| `Int _ -> (* int64 *) 2 + str_len_word name
| `Float _ -> 2 + str_len_word name
| `String s -> 1 + str_len_word_maybe_too_big s + str_len_word name
| `Kid _ -> 2 + str_len_word name
| A_none | A_bool _ -> 1 + str_len_word name
| A_int i when is_i32_ i -> 1 + str_len_word name
| A_int _ -> (* int64 *) 2 + str_len_word name
| A_float _ -> 2 + str_len_word name
| A_string s -> 1 + str_len_word_maybe_too_big s + str_len_word name
| A_kid _ -> 2 + str_len_word name
open struct
external int_of_bool : bool -> int = "%identity"
end
let encode (buf : Buf.t) (self : _ t) : unit =
let encode (buf : Buf.t) (self : t) : unit =
let name, data = self in
let name = truncate_string name in
let size = size_word self in
@ -187,26 +203,26 @@ module Argument = struct
in
match data with
| `None ->
| A_none ->
let hd = hd_arg_size in
Buf.add_i64 buf hd;
Buf.add_string buf name
| `Int i when is_i32_ i ->
| A_int i when is_i32_ i ->
let hd = I64.(1L lor hd_arg_size lor (of_int i lsl 32)) in
Buf.add_i64 buf hd;
Buf.add_string buf name
| `Int i ->
| A_int i ->
(* int64 *)
let hd = I64.(3L lor hd_arg_size) in
Buf.add_i64 buf hd;
Buf.add_string buf name;
Buf.add_i64 buf (I64.of_int i)
| `Float f ->
| A_float f ->
let hd = I64.(5L lor hd_arg_size) in
Buf.add_i64 buf hd;
Buf.add_string buf name;
Buf.add_i64 buf (I64.bits_of_float f)
| `String s ->
| A_string s ->
let s = truncate_string s in
let hd =
I64.(
@ -216,35 +232,35 @@ module Argument = struct
Buf.add_i64 buf hd;
Buf.add_string buf name;
Buf.add_string buf s
| `Bool b ->
| A_bool b ->
let hd = I64.(9L lor hd_arg_size lor (of_int (int_of_bool b) lsl 16)) in
Buf.add_i64 buf hd;
Buf.add_string buf name
| `Kid kid ->
| A_kid kid ->
(* int64 *)
let hd = I64.(8L lor hd_arg_size) in
Buf.add_i64 buf hd;
Buf.add_string buf name;
Buf.add_i64 buf (I64.of_int kid)
Buf.add_i64 buf kid
end
module Arguments = struct
type 'a t = 'a Argument.t list
type t = Argument.t list
let[@inline] len (self : _ t) : int =
let[@inline] len (self : t) : int =
match self with
| [] -> 0
| [ _ ] -> 1
| _ :: _ :: tl -> 2 + List.length tl
let check_valid (self : _ t) =
let check_valid (self : t) =
let len = len self in
if len > 15 then
invalid_arg (spf "fuchsia: can have at most 15 args, got %d" len);
List.iter Argument.check_valid_ self;
()
let[@inline] size_word (self : _ t) =
let[@inline] size_word (self : t) =
match self with
| [] -> 0
| [ a ] -> Argument.size_word a
@ -254,7 +270,7 @@ module Arguments = struct
(Argument.size_word a + Argument.size_word b)
tl
let[@inline] encode (buf : Buf.t) (self : _ t) =
let[@inline] encode (buf : Buf.t) (self : t) =
let rec aux buf l =
match l with
| [] -> ()
@ -276,11 +292,11 @@ module Thread_record = struct
let size_word : int = 3
(** Record that [Thread_ref.ref as_ref] represents the pair [pid, tid] *)
let encode (out : Output.t) ~as_ref ~pid ~tid () : unit =
let encode (bufs : Buf_chain.t) ~as_ref ~pid ~tid () : unit =
if as_ref <= 0 || as_ref > 255 then
invalid_arg "fuchsia: thread_record: invalid ref";
let buf = Output.get_buf out ~available_word:size_word in
let@ buf = Buf_chain.with_buf bufs ~available_word:size_word in
let hd = I64.(3L lor (of_int size_word lsl 4) lor (of_int as_ref lsl 16)) in
Buf.add_i64 buf hd;
@ -296,11 +312,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args ()
: unit =
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args
() : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
(* set category = 0 *)
let hd =
@ -331,11 +347,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args + 1 (* counter id *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args ()
: unit =
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args
() : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
@ -368,11 +384,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args ()
: unit =
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args
() : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
@ -403,11 +419,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args ()
: unit =
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns ~args
() : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
@ -438,11 +454,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args + 1 (* end timestamp *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
~end_time_ns ~args () : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
(* set category = 0 *)
let hd =
@ -475,11 +491,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args + 1 (* async id *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
~(async_id : Trace_core.trace_id) ~args () : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
@ -511,11 +527,11 @@ module Event = struct
1 + Thread_ref.size_word t_ref + 1 (* timestamp *) + str_len_word name
+ Arguments.size_word args + 1 (* async id *)
let encode (out : Output.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
let encode (bufs : Buf_chain.t) ~name ~(t_ref : Thread_ref.t) ~time_ns
~(async_id : Trace_core.trace_id) ~args () : unit =
let name = truncate_string name in
let size = size_word ~name ~t_ref ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(
@ -556,10 +572,11 @@ module Kernel_object = struct
let ty_process : ty = 1
let ty_thread : ty = 2
let encode (out : Output.t) ~name ~(ty : ty) ~(kid : int) ~args () : unit =
let encode (bufs : Buf_chain.t) ~name ~(ty : ty) ~(kid : int) ~args () : unit
=
let name = truncate_string name in
let size = size_word ~name ~args () in
let buf = Output.get_buf out ~available_word:size in
let@ buf = Buf_chain.with_buf bufs ~available_word:size in
let hd =
I64.(

View file

@ -29,16 +29,16 @@ module type S = sig
type st
(** Type of the state passed to every callback. *)
val on_init : st -> time_ns:float -> unit
val on_init : st -> time_ns:int64 -> unit
(** Called when the subscriber is initialized in a collector *)
val on_shutdown : st -> time_ns:float -> unit
val on_shutdown : st -> time_ns:int64 -> unit
(** Called when the collector is shutdown *)
val on_name_thread : st -> time_ns:float -> tid:int -> name:string -> unit
val on_name_thread : st -> time_ns:int64 -> tid:int -> name:string -> unit
(** Current thread is being named *)
val on_name_process : st -> time_ns:float -> tid:int -> name:string -> unit
val on_name_process : st -> time_ns:int64 -> tid:int -> name:string -> unit
(** Current process is being named *)
val on_enter_span :
@ -46,7 +46,7 @@ module type S = sig
__FUNCTION__:string option ->
__FILE__:string ->
__LINE__:int ->
time_ns:float ->
time_ns:int64 ->
tid:int ->
data:(string * user_data) list ->
name:string ->
@ -54,7 +54,7 @@ module type S = sig
unit
(** Enter a regular (sync) span *)
val on_exit_span : st -> time_ns:float -> tid:int -> span -> unit
val on_exit_span : st -> time_ns:int64 -> tid:int -> span -> unit
(** Exit a span. This and [on_enter_span] must follow strict stack discipline
*)
@ -63,7 +63,7 @@ module type S = sig
val on_message :
st ->
time_ns:float ->
time_ns:int64 ->
tid:int ->
span:span option ->
data:(string * user_data) list ->
@ -73,7 +73,7 @@ module type S = sig
val on_counter :
st ->
time_ns:float ->
time_ns:int64 ->
tid:int ->
data:(string * user_data) list ->
name:string ->
@ -86,7 +86,7 @@ module type S = sig
__FUNCTION__:string option ->
__FILE__:string ->
__LINE__:int ->
time_ns:float ->
time_ns:int64 ->
tid:int ->
parent:span option ->
data:(string * user_data) list ->
@ -99,7 +99,7 @@ module type S = sig
val on_exit_manual_span :
st ->
time_ns:float ->
time_ns:int64 ->
tid:int ->
name:string ->
data:(string * user_data) list ->
@ -110,7 +110,7 @@ module type S = sig
(** Exit a manual span *)
val on_extension_event :
st -> time_ns:float -> tid:int -> extension_event -> unit
st -> time_ns:int64 -> tid:int -> extension_event -> unit
(** Extension event
@since 0.8 *)
end
@ -121,7 +121,16 @@ type 'st t = (module S with type st = 'st)
(** Dummy callbacks. It can be useful to reuse some of these functions in a real
subscriber that doesn't want to handle {b all} events, but only some of
them. *)
them.
To write a subscriber that only supports some callbacks, this can be handy:
{[
module My_callbacks = struct
type st = my_own_state
include Callbacks.Dummy
let on_counter (st:st) ~time_ns ~tid ~data ~name v : unit = ...
end
]} *)
module Dummy = struct
let on_init _ ~time_ns:_ = ()
let on_shutdown _ ~time_ns:_ = ()

View file

@ -1,6 +1,7 @@
(library
(name trace_subscriber)
(public_name trace.subscriber)
(private_modules time_ thread_ tbl_)
(libraries
(re_export trace.core)
(select
@ -8,6 +9,12 @@
from
(threads -> thread_.real.ml)
(-> thread_.dummy.ml))
(select
tbl_.ml
from
(picos_aux.htbl -> tbl_.picos.ml)
(threads -> tbl_.thread.ml)
(-> tbl_.basic.ml))
(select
time_.ml
from

View file

@ -0,0 +1 @@
include Tbl_

View file

@ -0,0 +1,21 @@
(** A table that can be used to remember information about spans.
This is convenient when we want to rememner information from a span begin,
when dealing with the corresponding span end.
{b NOTE}: this is thread safe when threads are enabled. *)
open Trace_core
type 'v t
val create : unit -> 'v t
val add : 'v t -> span -> 'v -> unit
val find_exn : 'v t -> span -> 'v
(** @raise Not_found if information isn't found *)
val remove : _ t -> span -> unit
(** Remove the span if present *)
val to_list : 'v t -> (span * 'v) list

View file

@ -0,0 +1,13 @@
module T = Hashtbl.Make (struct
include Int64
let hash = Hashtbl.hash
end)
type 'v t = 'v T.t
let create () : _ t = T.create 32
let find_exn = T.find
let remove = T.remove
let add = T.replace
let to_list self : _ list = T.fold (fun k v l -> (k, v) :: l) self []

7
src/subscriber/tbl_.mli Normal file
View file

@ -0,0 +1,7 @@
type 'v t
val create : unit -> 'v t
val add : 'v t -> int64 -> 'v -> unit
val find_exn : 'v t -> int64 -> 'v
val remove : _ t -> int64 -> unit
val to_list : 'v t -> (int64 * 'v) list

View file

@ -0,0 +1,18 @@
module H = Picos_aux_htbl
module Key = struct
include Int64
let hash = Hashtbl.hash
end
type 'v t = (int64, 'v) H.t
let create () : _ t = H.create ~hashed_type:(module Key) ()
let find_exn = H.find_exn
let[@inline] remove self k = ignore (H.try_remove self k : bool)
let[@inline] add self k v =
if not (H.try_add self k v) then ignore (H.try_set self k v)
let[@inline] to_list self = H.to_seq self |> List.of_seq

View file

@ -0,0 +1,38 @@
module T = Hashtbl.Make (struct
include Int64
let hash = Hashtbl.hash
end)
type 'v t = {
tbl: 'v T.t;
lock: Mutex.t;
}
let create () : _ t = { tbl = T.create 32; lock = Mutex.create () }
let find_exn self k =
Mutex.lock self.lock;
try
let v = T.find self.tbl k in
Mutex.unlock self.lock;
v
with e ->
Mutex.unlock self.lock;
raise e
let remove self k =
Mutex.lock self.lock;
T.remove self.tbl k;
Mutex.unlock self.lock
let add self k v =
Mutex.lock self.lock;
T.replace self.tbl k v;
Mutex.unlock self.lock
let to_list self : _ list =
Mutex.lock self.lock;
let l = T.fold (fun k v l -> (k, v) :: l) self.tbl [] in
Mutex.unlock self.lock;
l

View file

@ -1 +1 @@
let[@inline] get_time_ns () : float = 0.
let[@inline] get_time_ns () : int64 = 0L

View file

@ -1 +1 @@
val get_time_ns : unit -> float
val get_time_ns : unit -> int64

View file

@ -1,3 +1,3 @@
let[@inline] get_time_ns () : float =
let[@inline] get_time_ns () : int64 =
let t = Mtime_clock.now () in
Int64.to_float (Mtime.to_uint64_ns t)
Mtime.to_uint64_ns t

View file

@ -1,3 +1,3 @@
let[@inline] get_time_ns () : float =
let[@inline] get_time_ns () : int64 =
let t = Unix.gettimeofday () in
t *. 1e9
Int64.of_float (t *. 1e9)

View file

@ -1,24 +1,28 @@
open Trace_core
module Callbacks = Callbacks
module Subscriber = Subscriber
module Span_tbl = Span_tbl
include Types
type t = Subscriber.t
module Private_ = struct
let get_now_ns_ = ref None
let get_tid_ = ref None
let mock = ref false
let get_now_ns_ = ref Time_.get_time_ns
let get_tid_ = ref Thread_.get_tid
(** Now, in nanoseconds *)
let[@inline] now_ns () : float =
match !get_now_ns_ with
| Some f -> f ()
| None -> Time_.get_time_ns ()
let[@inline] now_ns () : int64 =
if !mock then
!get_now_ns_ ()
else
Time_.get_time_ns ()
let[@inline] tid_ () : int =
match !get_tid_ with
| Some f -> f ()
| None -> Thread_.get_tid ()
if !mock then
!get_tid_ ()
else
Thread_.get_tid ()
end
open struct

View file

@ -4,10 +4,14 @@
trace event. It also defines a collector that needs to be installed for the
subscriber(s) to be called.
Thanks to {!Subscriber.tee_l} it's possible to combine multiple subscribers
into a single collector.
@since 0.8 *)
module Callbacks = Callbacks
module Subscriber = Subscriber
module Span_tbl = Span_tbl
include module type of struct
include Types
@ -31,13 +35,17 @@ val collector : t -> Trace_core.collector
(**/**)
module Private_ : sig
val get_now_ns_ : (unit -> float) option ref
val mock : bool ref
(** Global mock flag. If enable, all timestamps, tid, etc should be faked. *)
val get_now_ns_ : (unit -> int64) ref
(** The callback used to get the current timestamp *)
val get_tid_ : (unit -> int) option ref
val get_tid_ : (unit -> int) ref
(** The callback used to get the current thread's id *)
val now_ns : unit -> float
val now_ns : unit -> int64
(** Get the current timestamp, or a mock version *)
end
(**/**)

View file

@ -22,7 +22,7 @@ let get_unix_socket () =
type as_client = {
trace_id: string;
socket: string;
socket: string; (** Unix socket address *)
emit_tef_at_exit: string option;
(** For parent, ask daemon to emit traces here *)
}

4
src/tef/common_.ml Normal file
View file

@ -0,0 +1,4 @@
module Sub = Trace_subscriber
module A = Trace_core.Internal_.Atomic_
let ( let@ ) = ( @@ )

0
src/tef/emit_tef.ml Normal file
View file

85
src/tef/exporter.ml Normal file
View file

@ -0,0 +1,85 @@
(** An exporter, takes JSON objects and writes them somewhere *)
open Common_
type t = {
on_json: Buffer.t -> unit;
(** Takes a buffer and writes it somewhere. The buffer is only valid
during this call and must not be stored. *)
flush: unit -> unit; (** Force write *)
close: unit -> unit; (** Close underlying resources *)
}
(** An exporter, takes JSON objects and writes them somewhere.
This should be thread-safe if used in a threaded environment. *)
open struct
let with_lock lock f =
Mutex.lock lock;
try
let res = f () in
Mutex.unlock lock;
res
with e ->
let bt = Printexc.get_raw_backtrace () in
Mutex.unlock lock;
Printexc.raise_with_backtrace e bt
end
(** Export to the channel
@param jsonl
if true, export as a JSON object per line, otherwise export as a single
big JSON array.
@param close_channel if true, closing the exporter will close the channel *)
let of_out_channel ~close_channel ~jsonl oc : t =
let lock = Mutex.create () in
let first = ref true in
let closed = ref false in
let flush () =
let@ () = with_lock lock in
flush oc
in
let close () =
let@ () = with_lock lock in
if not !closed then (
closed := true;
if not jsonl then output_char oc ']';
if close_channel then close_out_noerr oc
)
in
let on_json buf =
let@ () = with_lock lock in
if not jsonl then
if !first then (
if not jsonl then output_char oc '[';
first := false
) else
output_string oc ",\n";
Buffer.output_buffer oc buf;
if jsonl then output_char oc '\n'
in
{ flush; close; on_json }
let of_buffer ~jsonl (buf : Buffer.t) : t =
let lock = Mutex.create () in
let first = ref true in
let closed = ref false in
let close () =
let@ () = with_lock lock in
if not !closed then (
closed := true;
if not jsonl then Buffer.add_char buf ']'
)
in
let on_json json =
let@ () = with_lock lock in
if not jsonl then
if !first then (
if not jsonl then Buffer.add_char buf '[';
first := false
) else
Buffer.add_string buf ",\n";
Buffer.add_buffer buf json;
if jsonl then Buffer.add_char buf '\n'
in
{ flush = ignore; close; on_json }

175
src/tef/subscriber.ml Normal file
View file

@ -0,0 +1,175 @@
open Common_
open Trace_core
open Trace_private_util
module Span_tbl = Sub.Span_tbl
module Buf_pool = struct
type t = Buffer.t Rpool.t
let create ?(max_size = 32) ?(buf_size = 256) () : t =
Rpool.create ~max_size ~clear:Buffer.reset
~create:(fun () -> Buffer.create buf_size)
()
end
open struct
let[@inline] time_us_of_time_ns (t : int64) : float =
Int64.div t 1_000L |> Int64.to_float
let[@inline] int64_of_trace_id_ (id : Trace_core.trace_id) : int64 =
if id == Trace_core.Collector.dummy_trace_id then
0L
else
Bytes.get_int64_le (Bytes.unsafe_of_string id) 0
end
let on_tracing_error = ref (fun s -> Printf.eprintf "%s\n%!" s)
type span_info = {
tid: int;
name: string;
start_us: float;
mutable data: (string * Sub.user_data) list;
(* NOTE: thread safety: this is supposed to only be modified by the thread
that's running this (synchronous, stack-abiding) span. *)
}
(** Information we store about a span begin event, to emit a complete event when
we meet the corresponding span end event *)
type t = {
active: bool A.t;
pid: int;
spans: span_info Span_tbl.t;
buf_pool: Buf_pool.t;
exporter: Exporter.t;
}
(** Subscriber state *)
open struct
let print_non_closed_spans_warning spans =
let module Str_set = Set.Make (String) in
let spans = Span_tbl.to_list spans in
if spans <> [] then (
!on_tracing_error
@@ Printf.sprintf "trace-tef: warning: %d spans were not closed"
(List.length spans);
let names =
List.fold_left
(fun set (_, span) -> Str_set.add span.name set)
Str_set.empty spans
in
Str_set.iter
(fun name ->
!on_tracing_error @@ Printf.sprintf " span %S was not closed" name)
names;
flush stderr
)
end
let close (self : t) : unit =
if A.exchange self.active false then (
print_non_closed_spans_warning self.spans;
self.exporter.close ()
)
let[@inline] active self = A.get self.active
let[@inline] flush (self : t) : unit = self.exporter.flush ()
let create ?(buf_pool = Buf_pool.create ()) ~pid ~exporter () : t =
{ active = A.make true; exporter; buf_pool; pid; spans = Span_tbl.create () }
module Callbacks = struct
type st = t
let on_init _ ~time_ns:_ = ()
let on_shutdown (self : st) ~time_ns:_ = close self
let on_name_process (self : st) ~time_ns:_ ~tid:_ ~name : unit =
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_name_process ~pid:self.pid ~name buf;
self.exporter.on_json buf
let on_name_thread (self : st) ~time_ns:_ ~tid ~name : unit =
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_name_thread buf ~pid:self.pid ~tid ~name;
self.exporter.on_json buf
(* add function name, if provided, to the metadata *)
let add_fun_name_ fun_name data : _ list =
match fun_name with
| None -> data
| Some f -> ("function", Sub.U_string f) :: data
let[@inline] on_enter_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~data ~name span : unit =
let time_us = time_us_of_time_ns @@ time_ns in
let data = add_fun_name_ fun_name data in
let info = { tid; name; start_us = time_us; data } in
(* save the span so we find it at exit *)
Span_tbl.add self.spans span info
let on_exit_span (self : st) ~time_ns ~tid:_ span : unit =
let time_us = time_us_of_time_ns @@ time_ns in
match Span_tbl.find_exn self.spans span with
| exception Not_found ->
!on_tracing_error
(Printf.sprintf "trace-tef: error: cannot find span %Ld" span)
| { tid; name; start_us; data } ->
Span_tbl.remove self.spans span;
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_duration_event buf ~pid:self.pid ~tid ~name ~start:start_us
~end_:time_us ~args:data;
self.exporter.on_json buf
let on_add_data (self : st) ~data span =
if data <> [] then (
try
let info = Span_tbl.find_exn self.spans span in
info.data <- List.rev_append data info.data
with Not_found ->
!on_tracing_error
(Printf.sprintf "trace-tef: error: cannot find span %Ld" span)
)
let on_message (self : st) ~time_ns ~tid ~span:_ ~data msg : unit =
let time_us = time_us_of_time_ns @@ time_ns in
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_instant_event buf ~pid:self.pid ~tid ~name:msg ~ts:time_us
~args:data;
self.exporter.on_json buf
let on_counter (self : st) ~time_ns ~tid ~data:_ ~name n : unit =
let time_us = time_us_of_time_ns @@ time_ns in
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_counter buf ~pid:self.pid ~name ~tid ~ts:time_us n;
self.exporter.on_json buf
let on_enter_manual_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~parent:_ ~data ~name ~flavor ~trace_id _span :
unit =
let time_us = time_us_of_time_ns @@ time_ns in
let data = add_fun_name_ fun_name data in
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_manual_begin buf ~pid:self.pid ~tid ~name
~id:(int64_of_trace_id_ trace_id)
~ts:time_us ~args:data ~flavor;
self.exporter.on_json buf
let on_exit_manual_span (self : st) ~time_ns ~tid ~name ~data ~flavor
~trace_id (_ : span) : unit =
let time_us = time_us_of_time_ns @@ time_ns in
let@ buf = Rpool.with_ self.buf_pool in
Writer.emit_manual_end buf ~pid:self.pid ~tid ~name
~id:(int64_of_trace_id_ trace_id)
~ts:time_us ~flavor ~args:data;
self.exporter.on_json buf
let on_extension_event _ ~time_ns:_ ~tid:_ _ev = ()
end
let subscriber (self : t) : Sub.t =
Sub.Subscriber.Sub { st = self; callbacks = (module Callbacks) }

28
src/tef/subscriber.mli Normal file
View file

@ -0,0 +1,28 @@
open Common_
module Buf_pool : sig
type t
val create : ?max_size:int -> ?buf_size:int -> unit -> t
end
type t
(** Main subscriber state. *)
val create : ?buf_pool:Buf_pool.t -> pid:int -> exporter:Exporter.t -> unit -> t
(** Create a subscriber state. *)
val flush : t -> unit
val close : t -> unit
val active : t -> bool
module Callbacks : Sub.Callbacks.S with type st = t
val subscriber : t -> Sub.t
(** Subscriber that writes json into this writer *)
(**/**)
val on_tracing_error : (string -> unit) ref
(**/**)

View file

@ -1,214 +1,7 @@
open Trace_core
open Trace_private_util
open Event
module Sub = Trace_subscriber
module A = Trace_core.Internal_.Atomic_
let on_tracing_error = ref (fun s -> Printf.eprintf "trace-tef error: %s\n%!" s)
let[@inline] int64_of_trace_id_ (id : Trace_core.trace_id) : int64 =
if id == Trace_core.Collector.dummy_trace_id then
0L
else
Bytes.get_int64_le (Bytes.unsafe_of_string id) 0
module Mock_ = struct
let enabled = ref false
let now = ref 0
(* used to mock timing *)
let get_now_ns () : float =
let x = !now in
incr now;
float_of_int x *. 1000.
let get_tid_ () : int = 3
end
module Span_tbl = Hashtbl.Make (struct
include Int64
let hash : t -> int = Hashtbl.hash
end)
type span_info = {
tid: int;
name: string;
start_us: float;
mutable data: (string * Sub.user_data) list;
}
(** Writer: knows how to write entries to a file in TEF format *)
module Writer = struct
type t = {
oc: out_channel;
jsonl: bool; (** JSONL mode, one json event per line *)
mutable first: bool; (** first event? useful in json mode *)
buf: Buffer.t; (** Buffer to write into *)
must_close: bool; (** Do we have to close the underlying channel [oc]? *)
pid: int;
}
(** A writer to a [out_channel]. It writes JSON entries in an array and closes
the array at the end. *)
let create ~(mode : [ `Single | `Jsonl ]) ~out () : t =
let jsonl = mode = `Jsonl in
let oc, must_close =
match out with
| `Stdout -> stdout, false
| `Stderr -> stderr, false
| `File path -> open_out path, true
| `File_append path ->
open_out_gen [ Open_creat; Open_wronly; Open_append ] 0o644 path, true
| `Output oc -> oc, false
in
let pid =
if !Mock_.enabled then
2
else
Unix.getpid ()
in
if not jsonl then output_char oc '[';
{ oc; jsonl; first = true; pid; must_close; buf = Buffer.create 2_048 }
let close (self : t) : unit =
if self.jsonl then
output_char self.oc '\n'
else
output_char self.oc ']';
flush self.oc;
if self.must_close then close_out self.oc
let with_ ~mode ~out f =
let writer = create ~mode ~out () in
Fun.protect ~finally:(fun () -> close writer) (fun () -> f writer)
let[@inline] flush (self : t) : unit = flush self.oc
(** Emit "," if we need, and get the buffer ready *)
let emit_sep_and_start_ (self : t) =
Buffer.reset self.buf;
if self.jsonl then
Buffer.add_char self.buf '\n'
else if self.first then
self.first <- false
else
Buffer.add_string self.buf ",\n"
let char = Buffer.add_char
let raw_string = Buffer.add_string
let str_val (buf : Buffer.t) (s : string) =
char buf '"';
let encode_char c =
match c with
| '"' -> raw_string buf {|\"|}
| '\\' -> raw_string buf {|\\|}
| '\n' -> raw_string buf {|\n|}
| '\b' -> raw_string buf {|\b|}
| '\r' -> raw_string buf {|\r|}
| '\t' -> raw_string buf {|\t|}
| _ when Char.code c <= 0x1f ->
raw_string buf {|\u00|};
Printf.bprintf buf "%02x" (Char.code c)
| c -> char buf c
in
String.iter encode_char s;
char buf '"'
let pp_user_data_ (out : Buffer.t) : Sub.user_data -> unit = function
| U_none -> raw_string out "null"
| U_int i -> Printf.bprintf out "%d" i
| U_bool b -> Printf.bprintf out "%b" b
| U_string s -> str_val out s
| U_float f -> Printf.bprintf out "%g" f
(* emit args, if not empty. [ppv] is used to print values. *)
let emit_args_o_ ppv (out : Buffer.t) args : unit =
if args <> [] then (
Printf.bprintf out {json|,"args": {|json};
List.iteri
(fun i (n, value) ->
if i > 0 then raw_string out ",";
Printf.bprintf out {json|"%s":%a|json} n ppv value)
args;
char out '}'
)
let emit_duration_event ~tid ~name ~start ~end_ ~args (self : t) : unit =
let dur = end_ -. start in
let ts = start in
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"cat":"","tid": %d,"dur": %.2f,"ts": %.2f,"name":%a,"ph":"X"%a}|json}
self.pid tid dur ts str_val name
(emit_args_o_ pp_user_data_)
args;
Buffer.output_buffer self.oc self.buf
let emit_manual_begin ~tid ~name ~(id : trace_id) ~ts ~args
~(flavor : Sub.flavor option) (self : t) : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
self.pid (int64_of_trace_id_ id) tid ts str_val name
(match flavor with
| None | Some Async -> 'b'
| Some Sync -> 'B')
(emit_args_o_ pp_user_data_)
args;
Buffer.output_buffer self.oc self.buf
let emit_manual_end ~tid ~name ~(id : trace_id) ~ts
~(flavor : Sub.flavor option) ~args (self : t) : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
self.pid (int64_of_trace_id_ id) tid ts str_val name
(match flavor with
| None | Some Async -> 'e'
| Some Sync -> 'E')
(emit_args_o_ pp_user_data_)
args;
Buffer.output_buffer self.oc self.buf
let emit_instant_event ~tid ~name ~ts ~args (self : t) : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"cat":"","tid": %d,"ts": %.2f,"name":%a,"ph":"I"%a}|json}
self.pid tid ts str_val name
(emit_args_o_ pp_user_data_)
args;
Buffer.output_buffer self.oc self.buf
let emit_name_thread ~tid ~name (self : t) : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"tid": %d,"name":"thread_name","ph":"M"%a}|json} self.pid
tid
(emit_args_o_ pp_user_data_)
[ "name", U_string name ];
Buffer.output_buffer self.oc self.buf
let emit_name_process ~name (self : t) : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"name":"process_name","ph":"M"%a}|json} self.pid
(emit_args_o_ pp_user_data_)
[ "name", U_string name ];
Buffer.output_buffer self.oc self.buf
let emit_counter ~name ~tid ~ts (self : t) f : unit =
emit_sep_and_start_ self;
Printf.bprintf self.buf
{json|{"pid":%d,"tid":%d,"ts":%.2f,"name":"c","ph":"C"%a}|json} self.pid
tid ts
(emit_args_o_ pp_user_data_)
[ name, U_float f ];
Buffer.output_buffer self.oc self.buf
end
module Subscriber = Subscriber
module Exporter = Exporter
module Writer = Writer
let block_signals () =
try
@ -226,97 +19,14 @@ let block_signals () =
: _ list)
with _ -> ()
let print_non_closed_spans_warning spans =
let module Str_set = Set.Make (String) in
Printf.eprintf "trace-tef: warning: %d spans were not closed\n"
(Span_tbl.length spans);
let names = ref Str_set.empty in
Span_tbl.iter (fun _ span -> names := Str_set.add span.name !names) spans;
Str_set.iter
(fun name -> Printf.eprintf " span %S was not closed\n" name)
!names;
flush stderr
(** Background thread, takes events from the queue, puts them in context using
local state, and writes fully resolved TEF events to [out]. *)
let bg_thread ~mode ~out (events : Event.t B_queue.t) : unit =
block_signals ();
(* open a writer to [out] *)
Writer.with_ ~mode ~out @@ fun writer ->
(* local state, to keep track of span information and implicit stack context *)
let spans : span_info Span_tbl.t = Span_tbl.create 32 in
(* add function name, if provided, to the metadata *)
let add_fun_name_ fun_name data : _ list =
match fun_name with
| None -> data
| Some f -> ("function", Sub.U_string f) :: data
in
(* how to deal with an event *)
let handle_ev (ev : Event.t) : unit =
match ev with
| E_tick -> Writer.flush writer
| E_message { tid; msg; time_us; data } ->
Writer.emit_instant_event ~tid ~name:msg ~ts:time_us ~args:data writer
| E_define_span { tid; name; id; time_us; fun_name; data } ->
let data = add_fun_name_ fun_name data in
let info = { tid; name; start_us = time_us; data } in
(* save the span so we find it at exit *)
Span_tbl.add spans id info
| E_exit_span { id; time_us = stop_us } ->
(match Span_tbl.find_opt spans id with
| None -> !on_tracing_error (Printf.sprintf "cannot find span %Ld" id)
| Some { tid; name; start_us; data } ->
Span_tbl.remove spans id;
Writer.emit_duration_event ~tid ~name ~start:start_us ~end_:stop_us
~args:data writer)
| E_add_data { id; data } ->
(match Span_tbl.find_opt spans id with
| None -> !on_tracing_error (Printf.sprintf "cannot find span %Ld" id)
| Some info -> info.data <- List.rev_append data info.data)
| E_enter_manual_span { tid; time_us; name; id; data; fun_name; flavor } ->
let data = add_fun_name_ fun_name data in
Writer.emit_manual_begin ~tid ~name ~id ~ts:time_us ~args:data ~flavor
writer
| E_exit_manual_span { tid; time_us; name; id; flavor; data } ->
Writer.emit_manual_end ~tid ~name ~id ~ts:time_us ~flavor ~args:data
writer
| E_counter { tid; name; time_us; n } ->
Writer.emit_counter ~name ~tid ~ts:time_us writer n
| E_name_process { name } -> Writer.emit_name_process ~name writer
| E_name_thread { tid; name } -> Writer.emit_name_thread ~tid ~name writer
in
try
while true do
(* get all the events in the incoming blocking queue, in
one single critical section. *)
let local = B_queue.pop_all events in
List.iter handle_ev local
done
with B_queue.Closed ->
(* write a message about us closing *)
Writer.emit_instant_event ~name:"tef-worker.exit"
~tid:(Thread.id @@ Thread.self ())
~ts:(Sub.Private_.now_ns () *. 1e-3)
~args:[] writer;
(* warn if app didn't close all spans *)
if Span_tbl.length spans > 0 then print_non_closed_spans_warning spans;
()
(** Thread that simply regularly "ticks", sending events to the background
thread so it has a chance to write to the file *)
let tick_thread events : unit =
let tick_thread (sub : Subscriber.t) : unit =
block_signals ();
try
while true do
Thread.delay 0.5;
B_queue.push events E_tick
done
with B_queue.Closed -> ()
while Subscriber.active sub do
Thread.delay 0.5;
Subscriber.flush sub
done
type output =
[ `Stdout
@ -324,91 +34,45 @@ type output =
| `File of string
]
module Internal_st = struct
type t = {
active: bool A.t;
events: Event.t B_queue.t;
t_write: Thread.t;
}
end
let subscriber_ ~finally ~out ~(mode : [ `Single | `Jsonl ]) () : Sub.t =
let module M : Sub.Callbacks.S with type st = Internal_st.t = struct
type st = Internal_st.t
let on_init _ ~time_ns:_ = ()
let on_shutdown (self : st) ~time_ns:_ =
if A.exchange self.active false then (
B_queue.close self.events;
(* wait for writer thread to be done. The writer thread will exit
after processing remaining events because the queue is now closed *)
Thread.join self.t_write
)
let on_name_process (self : st) ~time_ns:_ ~tid:_ ~name : unit =
B_queue.push self.events @@ E_name_process { name }
let on_name_thread (self : st) ~time_ns:_ ~tid ~name : unit =
B_queue.push self.events @@ E_name_thread { tid; name }
let[@inline] on_enter_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~data ~name span : unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events
@@ E_define_span { tid; name; time_us; id = span; fun_name; data }
let on_exit_span (self : st) ~time_ns ~tid:_ span : unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events @@ E_exit_span { id = span; time_us }
let on_add_data (self : st) ~data span =
if data <> [] then
B_queue.push self.events @@ E_add_data { id = span; data }
let on_message (self : st) ~time_ns ~tid ~span:_ ~data msg : unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events @@ E_message { tid; time_us; msg; data }
let on_counter (self : st) ~time_ns ~tid ~data:_ ~name f : unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events @@ E_counter { name; n = f; time_us; tid }
let on_enter_manual_span (self : st) ~__FUNCTION__:fun_name ~__FILE__:_
~__LINE__:_ ~time_ns ~tid ~parent:_ ~data ~name ~flavor ~trace_id _span
: unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events
@@ E_enter_manual_span
{ id = trace_id; time_us; tid; data; name; fun_name; flavor }
let on_exit_manual_span (self : st) ~time_ns ~tid ~name ~data ~flavor
~trace_id (_ : span) : unit =
let time_us = time_ns *. 1e-3 in
B_queue.push self.events
@@ E_exit_manual_span { tid; id = trace_id; name; time_us; data; flavor }
let on_extension_event _ ~time_ns:_ ~tid:_ _ev = ()
end in
let events = B_queue.create () in
let t_write =
Thread.create
(fun () -> Fun.protect ~finally @@ fun () -> bg_thread ~mode ~out events)
()
let subscriber_ ~finally ~out ~(mode : [ `Single | `Jsonl ]) () :
Trace_subscriber.t =
let jsonl = mode = `Jsonl in
let oc, must_close =
match out with
| `Stdout -> stdout, false
| `Stderr -> stderr, false
| `File path -> open_out path, true
| `File_append path ->
open_out_gen [ Open_creat; Open_wronly; Open_append ] 0o644 path, true
| `Output oc -> oc, false
in
let pid =
if !Trace_subscriber.Private_.mock then
2
else
Unix.getpid ()
in
(* ticker thread, regularly sends a message to the writer thread.
no need to join it. *)
let _t_tick : Thread.t = Thread.create (fun () -> tick_thread events) () in
let st : Internal_st.t = { active = A.make true; events; t_write } in
Sub.Subscriber.Sub { st; callbacks = (module M) }
let exporter = Exporter.of_out_channel oc ~jsonl ~close_channel:must_close in
let exporter =
{
exporter with
close =
(fun () ->
exporter.close ();
finally ());
}
in
let sub = Subscriber.create ~pid ~exporter () in
let _t_tick : Thread.t = Thread.create tick_thread sub in
Subscriber.subscriber sub
let collector_ ~(finally : unit -> unit) ~(mode : [ `Single | `Jsonl ]) ~out ()
: collector =
let sub = subscriber_ ~finally ~mode ~out () in
Sub.collector sub
Trace_subscriber.collector sub
let[@inline] subscriber ~out () : Sub.t =
let[@inline] subscriber ~out () : Trace_subscriber.t =
subscriber_ ~finally:ignore ~mode:`Single ~out ()
let[@inline] collector ~out () : collector =
@ -436,14 +100,26 @@ let with_setup ?out () f =
setup ?out ();
Fun.protect ~finally:Trace_core.shutdown f
module Mock_ = struct
let now = ref 0
(* used to mock timing *)
let get_now_ns () : int64 =
let x = !now in
incr now;
Int64.(mul (of_int x) 1000L)
let get_tid_ () : int = 3
end
module Private_ = struct
let mock_all_ () =
Mock_.enabled := true;
Sub.Private_.get_now_ns_ := Some Mock_.get_now_ns;
Sub.Private_.get_tid_ := Some Mock_.get_tid_;
Trace_subscriber.Private_.mock := true;
Trace_subscriber.Private_.get_now_ns_ := Mock_.get_now_ns;
Trace_subscriber.Private_.get_tid_ := Mock_.get_tid_;
()
let on_tracing_error = on_tracing_error
let on_tracing_error = Subscriber.on_tracing_error
let subscriber_jsonl ~finally ~out () =
subscriber_ ~finally ~mode:`Jsonl ~out ()

View file

@ -1,3 +1,7 @@
module Subscriber = Subscriber
module Exporter = Exporter
module Writer = Writer
type output =
[ `Stdout
| `Stderr

97
src/tef/writer.ml Normal file
View file

@ -0,0 +1,97 @@
open Common_
let char = Buffer.add_char
let raw_string = Buffer.add_string
let str_val (buf : Buffer.t) (s : string) =
char buf '"';
let encode_char c =
match c with
| '"' -> raw_string buf {|\"|}
| '\\' -> raw_string buf {|\\|}
| '\n' -> raw_string buf {|\n|}
| '\b' -> raw_string buf {|\b|}
| '\r' -> raw_string buf {|\r|}
| '\t' -> raw_string buf {|\t|}
| _ when Char.code c <= 0x1f ->
raw_string buf {|\u00|};
Printf.bprintf buf "%02x" (Char.code c)
| c -> char buf c
in
String.iter encode_char s;
char buf '"'
let pp_user_data_ (out : Buffer.t) : Sub.user_data -> unit = function
| U_none -> raw_string out "null"
| U_int i -> Printf.bprintf out "%d" i
| U_bool b -> Printf.bprintf out "%b" b
| U_string s -> str_val out s
| U_float f -> Printf.bprintf out "%g" f
(* emit args, if not empty. [ppv] is used to print values. *)
let emit_args_o_ ppv (out : Buffer.t) args : unit =
if args <> [] then (
Printf.bprintf out {json|,"args": {|json};
List.iteri
(fun i (n, value) ->
if i > 0 then raw_string out ",";
Printf.bprintf out {json|"%s":%a|json} n ppv value)
args;
char out '}'
)
let emit_duration_event ~pid ~tid ~name ~start ~end_ ~args buf : unit =
let dur = end_ -. start in
let ts = start in
Printf.bprintf buf
{json|{"pid":%d,"cat":"","tid": %d,"dur": %.2f,"ts": %.2f,"name":%a,"ph":"X"%a}|json}
pid tid dur ts str_val name
(emit_args_o_ pp_user_data_)
args
let emit_manual_begin ~pid ~tid ~name ~(id : int64) ~ts ~args
~(flavor : Sub.flavor option) buf : unit =
Printf.bprintf buf
{json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
pid id tid ts str_val name
(match flavor with
| None | Some Async -> 'b'
| Some Sync -> 'B')
(emit_args_o_ pp_user_data_)
args
let emit_manual_end ~pid ~tid ~name ~(id : int64) ~ts
~(flavor : Sub.flavor option) ~args buf : unit =
Printf.bprintf buf
{json|{"pid":%d,"cat":"trace","id":%Ld,"tid": %d,"ts": %.2f,"name":%a,"ph":"%c"%a}|json}
pid id tid ts str_val name
(match flavor with
| None | Some Async -> 'e'
| Some Sync -> 'E')
(emit_args_o_ pp_user_data_)
args
let emit_instant_event ~pid ~tid ~name ~ts ~args buf : unit =
Printf.bprintf buf
{json|{"pid":%d,"cat":"","tid": %d,"ts": %.2f,"name":%a,"ph":"I"%a}|json}
pid tid ts str_val name
(emit_args_o_ pp_user_data_)
args
let emit_name_thread ~pid ~tid ~name buf : unit =
Printf.bprintf buf
{json|{"pid":%d,"tid": %d,"name":"thread_name","ph":"M"%a}|json} pid tid
(emit_args_o_ pp_user_data_)
[ "name", U_string name ]
let emit_name_process ~pid ~name buf : unit =
Printf.bprintf buf {json|{"pid":%d,"name":"process_name","ph":"M"%a}|json} pid
(emit_args_o_ pp_user_data_)
[ "name", U_string name ]
let emit_counter ~pid ~tid ~name ~ts buf f : unit =
Printf.bprintf buf
{json|{"pid":%d,"tid":%d,"ts":%.2f,"name":"c","ph":"C"%a}|json} pid tid ts
(emit_args_o_ pp_user_data_)
[ name, U_float f ]

54
src/tef/writer.mli Normal file
View file

@ -0,0 +1,54 @@
(** Write JSON events to a buffer.
This is the part of the code that knows how to emit TEF-compliant JSON from
raw event data. *)
open Common_
open Trace_core
val emit_duration_event :
pid:int ->
tid:int ->
name:string ->
start:float ->
end_:float ->
args:(string * Sub.user_data) list ->
Buffer.t ->
unit
val emit_manual_begin :
pid:int ->
tid:int ->
name:string ->
id:span ->
ts:float ->
args:(string * Sub.user_data) list ->
flavor:Sub.flavor option ->
Buffer.t ->
unit
val emit_manual_end :
pid:int ->
tid:int ->
name:string ->
id:span ->
ts:float ->
flavor:Sub.flavor option ->
args:(string * Sub.user_data) list ->
Buffer.t ->
unit
val emit_instant_event :
pid:int ->
tid:int ->
name:string ->
ts:float ->
args:(string * Sub.user_data) list ->
Buffer.t ->
unit
val emit_name_thread : pid:int -> tid:int -> name:string -> Buffer.t -> unit
val emit_name_process : pid:int -> name:string -> Buffer.t -> unit
val emit_counter :
pid:int -> tid:int -> name:string -> ts:float -> Buffer.t -> float -> unit

View file

@ -1,65 +0,0 @@
module A = Trace_core.Internal_.Atomic_
type 'a t = {
mutex: Mutex.t;
cond: Condition.t;
q: 'a Mpsc_bag.t;
mutable closed: bool;
consumer_waiting: bool A.t;
}
exception Closed
let create () : _ t =
{
mutex = Mutex.create ();
cond = Condition.create ();
q = Mpsc_bag.create ();
closed = false;
consumer_waiting = A.make false;
}
let close (self : _ t) =
Mutex.lock self.mutex;
if not self.closed then (
self.closed <- true;
Condition.broadcast self.cond (* awake waiters so they fail *)
);
Mutex.unlock self.mutex
let push (self : _ t) x : unit =
if self.closed then raise Closed;
Mpsc_bag.add self.q x;
if self.closed then raise Closed;
if A.get self.consumer_waiting then (
(* wakeup consumer *)
Mutex.lock self.mutex;
Condition.broadcast self.cond;
Mutex.unlock self.mutex
)
let rec pop_all (self : 'a t) : 'a list =
match Mpsc_bag.pop_all self.q with
| Some l -> l
| None ->
if self.closed then raise Closed;
Mutex.lock self.mutex;
A.set self.consumer_waiting true;
(* check again, a producer might have pushed an element since we
last checked. However if we still find
nothing, because this comes after [consumer_waiting:=true],
any producer arriving after that will know to wake us up. *)
(match Mpsc_bag.pop_all self.q with
| Some l ->
A.set self.consumer_waiting false;
Mutex.unlock self.mutex;
l
| None ->
if self.closed then (
Mutex.unlock self.mutex;
raise Closed
);
Condition.wait self.cond self.mutex;
A.set self.consumer_waiting false;
Mutex.unlock self.mutex;
pop_all self)

View file

@ -1,18 +0,0 @@
(** Basic Blocking Queue *)
type 'a t
val create : unit -> _ t
exception Closed
val push : 'a t -> 'a -> unit
(** [push q x] pushes [x] into [q], and returns [()].
@raise Closed if [close q] was previously called.*)
val pop_all : 'a t -> 'a list
(** [pop_all bq] returns all items presently in [bq], in the same order, and
clears [bq]. It blocks if no element is in [bq]. *)
val close : _ t -> unit
(** Close the queue, meaning there won't be any more [push] allowed. *)

View file

@ -1,32 +0,0 @@
module A = Trace_core.Internal_.Atomic_
type 'a t = { bag: 'a list A.t } [@@unboxed]
let create () =
let bag = A.make [] in
{ bag }
module Backoff = struct
type t = int
let default = 2
let once (b : t) : t =
for _i = 1 to b do
Domain_util.cpu_relax ()
done;
min (b * 2) 256
end
let rec add backoff t x =
let before = A.get t.bag in
let after = x :: before in
if not (A.compare_and_set t.bag before after) then
add (Backoff.once backoff) t x
let[@inline] add t x = add Backoff.default t x
let[@inline] pop_all t : _ list option =
match A.exchange t.bag [] with
| [] -> None
| l -> Some (List.rev l)

View file

@ -1,11 +0,0 @@
(** A multi-producer, single-consumer bag *)
type 'a t
val create : unit -> 'a t
val add : 'a t -> 'a -> unit
(** [add q x] adds [x] in the bag. *)
val pop_all : 'a t -> 'a list option
(** Return all current items in the insertion order. *)

67
src/util/rpool.ml Normal file
View file

@ -0,0 +1,67 @@
open struct
module A = Trace_core.Internal_.Atomic_
end
module List_with_len = struct
type +'a t =
| Nil
| Cons of int * 'a * 'a t
let empty : _ t = Nil
let[@inline] len = function
| Nil -> 0
| Cons (i, _, _) -> i
let[@inline] cons x self = Cons (len self + 1, x, self)
end
type 'a t = {
max_size: int;
create: unit -> 'a;
clear: 'a -> unit;
cached: 'a List_with_len.t A.t;
}
let create ~max_size ~create ~clear () : _ t =
{ max_size; create; clear; cached = A.make List_with_len.empty }
let alloc (type a) (self : a t) : a =
let module M = struct
exception Found of a
end in
try
while
match A.get self.cached with
| Nil -> false
| Cons (_, x, tl) as old ->
if A.compare_and_set self.cached old tl then
raise_notrace (M.Found x)
else
true
do
()
done;
self.create ()
with M.Found x -> x
let recycle (self : 'a t) (x : 'a) : unit =
self.clear x;
while
match A.get self.cached with
| Cons (i, _, _) when i >= self.max_size -> false (* drop buf *)
| old -> not (A.compare_and_set self.cached old (List_with_len.cons x old))
do
()
done
let with_ (self : 'a t) f =
let x = alloc self in
try
let res = f x in
recycle self x;
res
with e ->
let bt = Printexc.get_raw_backtrace () in
recycle self x;
Printexc.raise_with_backtrace e bt

10
src/util/rpool.mli Normal file
View file

@ -0,0 +1,10 @@
(** A resource pool (for buffers) *)
type 'a t
val create :
max_size:int -> create:(unit -> 'a) -> clear:('a -> unit) -> unit -> 'a t
val alloc : 'a t -> 'a
val recycle : 'a t -> 'a -> unit
val with_ : 'a t -> ('a -> 'b) -> 'b

5
test/fuchsia/dune Normal file
View file

@ -0,0 +1,5 @@
(test
(name t1)
(package trace-fuchsia)
(modules t1)
(libraries trace trace-fuchsia))

1
test/fuchsia/t1.expected Normal file

File diff suppressed because one or more lines are too long

75
test/fuchsia/t1.ml Normal file
View file

@ -0,0 +1,75 @@
let run () =
Trace.set_process_name "main";
Trace.set_thread_name "t1";
let n = ref 0 in
for _i = 1 to 50 do
Trace.with_span ~__FILE__ ~__LINE__ "outer.loop" @@ fun _sp ->
let pseudo_async_sp =
Trace.enter_manual_span ~parent:None ~__FILE__ ~__LINE__ "fake_sleep"
in
for _j = 2 to 5 do
incr n;
Trace.with_span ~__FILE__ ~__LINE__ "inner.loop" @@ fun _sp ->
Trace.messagef (fun k -> k "hello %d %d" _i _j);
Trace.message "world";
Trace.counter_int "n" !n;
Trace.add_data_to_span _sp [ "i", `Int _i ];
if _j = 2 then (
Trace.add_data_to_span _sp [ "j", `Int _j ];
let _sp =
Trace.enter_manual_span
~parent:(Some (Trace.ctx_of_span pseudo_async_sp))
~flavor:
(if _i mod 3 = 0 then
`Sync
else
`Async)
~__FILE__ ~__LINE__ "sub-sleep"
in
(* fake micro sleep *)
Thread.delay 0.005;
Trace.exit_manual_span _sp
) else if _j = 3 then (
(* pretend some task finished. Note that this is not well scoped wrt other spans. *)
Trace.add_data_to_manual_span pseudo_async_sp [ "slept", `Bool true ];
Trace.exit_manual_span pseudo_async_sp
)
done
done
let to_hex (s : string) : string =
let i_to_hex (i : int) =
if i < 10 then
Char.chr (i + Char.code '0')
else
Char.chr (i - 10 + Char.code 'a')
in
let res = Bytes.create (2 * String.length s) in
for i = 0 to String.length s - 1 do
let n = Char.code (String.get s i) in
Bytes.set res (2 * i) (i_to_hex ((n land 0xf0) lsr 4));
Bytes.set res ((2 * i) + 1) (i_to_hex (n land 0x0f))
done;
Bytes.unsafe_to_string res
let () =
Trace_fuchsia.Internal_.mock_all_ ();
let buf = Buffer.create 32 in
let exporter = Trace_fuchsia.Exporter.of_buffer buf in
Trace_fuchsia.with_setup ~out:(`Exporter exporter) () run;
exporter.close ();
let data = Buffer.contents buf in
(let oc = open_out_bin "t1.fxt" in
output_string oc data;
close_out_noerr oc);
print_endline (to_hex data);
flush stdout

View file

@ -1,4 +1,4 @@
(tests
(names t1 t2)
(package trace-fuchsia)
(libraries trace-fuchsia.write))
(libraries trace-fuchsia))

View file

@ -1,4 +1,4 @@
open Trace_fuchsia_write
open Trace_fuchsia
module Str_ = struct
open String
@ -39,14 +39,14 @@ module Str_ = struct
end
let () =
let l = List.init 100 (fun i -> Util.round_to_word i) in
let l = List.init 100 (fun i -> Writer.Util.round_to_word i) in
assert (List.for_all (fun x -> x mod 8 = 0) l)
let () =
assert (Str_ref.inline 0 = 0b0000_0000_0000_0000);
assert (Str_ref.inline 1 = 0b1000_0000_0000_0001);
assert (Str_ref.inline 6 = 0b1000_0000_0000_0110);
assert (Str_ref.inline 31999 = 0b1111_1100_1111_1111);
assert (Writer.Str_ref.inline 0 = 0b0000_0000_0000_0000);
assert (Writer.Str_ref.inline 1 = 0b1000_0000_0000_0001);
assert (Writer.Str_ref.inline 6 = 0b1000_0000_0000_0110);
assert (Writer.Str_ref.inline 31999 = 0b1111_1100_1111_1111);
()
let () =

View file

@ -1,4 +1,5 @@
open Trace_fuchsia_write
open Trace_fuchsia
open Trace_fuchsia.Writer
let pf = Printf.printf
@ -40,24 +41,27 @@ module Str_ = struct
Bytes.unsafe_to_string res
end
let with_buf_output (f : Output.t -> unit) : string =
let with_buf_chain (f : Buf_chain.t -> unit) : string =
let buf_pool = Buf_pool.create () in
let buffer = Buffer.create 32 in
let out = Output.into_buffer ~buf_pool buffer in
f out;
Output.flush out;
let buf_chain = Buf_chain.create ~sharded:true ~buf_pool () in
f buf_chain;
Buf_chain.ready_all_non_empty buf_chain;
let exp = Exporter.of_buffer buffer in
Buf_chain.pop_ready buf_chain ~f:exp.write_bufs;
Buffer.contents buffer
let () = pf "first trace\n"
let () =
let str =
with_buf_output (fun out ->
Metadata.Magic_record.encode out;
Thread_record.encode out ~as_ref:5 ~pid:1 ~tid:86 ();
Event.Instant.encode out ~name:"hello" ~time_ns:1234_5678L
with_buf_chain (fun bufs ->
Metadata.Magic_record.encode bufs;
Thread_record.encode bufs ~as_ref:5 ~pid:1 ~tid:86 ();
Event.Instant.encode bufs ~name:"hello" ~time_ns:1234_5678L
~t_ref:(Thread_ref.Ref 5)
~args:[ "x", `Int 42 ]
~args:[ "x", A_int 42 ]
())
in
pf "%s\n" (Str_.to_hex str)
@ -66,21 +70,21 @@ let () = pf "second trace\n"
let () =
let str =
with_buf_output (fun out ->
Metadata.Magic_record.encode out;
with_buf_chain (fun bufs ->
Metadata.Magic_record.encode bufs;
Metadata.Initialization_record.(
encode out ~ticks_per_secs:default_ticks_per_sec ());
Thread_record.encode out ~as_ref:5 ~pid:1 ~tid:86 ();
Metadata.Provider_info.encode out ~id:1 ~name:"ocaml-trace" ();
Event.Duration_complete.encode out ~name:"outer"
encode bufs ~ticks_per_secs:default_ticks_per_sec ());
Thread_record.encode bufs ~as_ref:5 ~pid:1 ~tid:86 ();
Metadata.Provider_info.encode bufs ~id:1 ~name:"ocaml-trace" ();
Event.Duration_complete.encode bufs ~name:"outer"
~t_ref:(Thread_ref.Ref 5) ~time_ns:100_000L ~end_time_ns:5_000_000L
~args:[] ();
Event.Duration_complete.encode out ~name:"inner"
Event.Duration_complete.encode bufs ~name:"inner"
~t_ref:(Thread_ref.Ref 5) ~time_ns:180_000L ~end_time_ns:4_500_000L
~args:[] ();
Event.Instant.encode out ~name:"hello" ~time_ns:1_234_567L
Event.Instant.encode bufs ~name:"hello" ~time_ns:1_234_567L
~t_ref:(Thread_ref.Ref 5)
~args:[ "x", `Int 42 ]
~args:[ "x", A_int 42 ]
())
in
(let oc = open_out "foo.fxt" in

View file

@ -1049,5 +1049,4 @@
{"pid":2,"cat":"","tid": 3,"ts": 1299.00,"name":"world","ph":"I"},
{"pid":2,"tid":3,"ts":1300.00,"name":"c","ph":"C","args": {"n":200}},
{"pid":2,"cat":"","tid": 3,"dur": 4.00,"ts": 1297.00,"name":"inner.loop","ph":"X","args": {"i":50}},
{"pid":2,"cat":"","tid": 3,"dur": 25.00,"ts": 1277.00,"name":"outer.loop","ph":"X"},
{"pid":2,"cat":"","tid": 1,"ts": 1304.00,"name":"tef-worker.exit","ph":"I"}]
{"pid":2,"cat":"","tid": 3,"dur": 25.00,"ts": 1277.00,"name":"outer.loop","ph":"X"}]

View file

@ -929,5 +929,4 @@
{"pid":2,"cat":"","tid": 3,"dur": 217.00,"ts": 1642.00,"name":"Dune__exe__T2.fib2","ph":"X"},
{"pid":2,"cat":"","tid": 3,"dur": 353.00,"ts": 1507.00,"name":"Dune__exe__T2.fib2","ph":"X"},
{"pid":2,"cat":"","tid": 3,"dur": 573.00,"ts": 1288.00,"name":"Dune__exe__T2.fib2","ph":"X"},
{"pid":2,"cat":"","tid": 3,"dur": 929.00,"ts": 933.00,"name":"Dune__exe__T2.fib2","ph":"X"},
{"pid":2,"cat":"","tid": 1,"ts": 1864.00,"name":"tef-worker.exit","ph":"I"}]
{"pid":2,"cat":"","tid": 3,"dur": 929.00,"ts": 933.00,"name":"Dune__exe__T2.fib2","ph":"X"}]

View file

@ -17,6 +17,7 @@ depends: [
depopts: [
"hmap"
"unix"
"picos_aux" {>= "0.6"}
"mtime" {>= "2.0"}
]
build: [