Trace Event Format summary CLI
5.7 kB
147 lines
1(*---------------------------------------------------------------------------
2 Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6module Event = Catapult.Event
7module Phase = Catapult.Phase
8module Slice = Catapult.Slice
9
10let slices s = Slice.of_trace (Catapult.of_string_exn s)
11let names l = List.map (fun (s : Slice.t) -> s.name) l
12let durs l = List.map (fun (s : Slice.t) -> s.dur) l
13
14(* Spec, "Complete Events": ts + dur describe the whole interval. *)
15let test_complete () =
16 let l = slices {|[{"name":"f","ph":"X","ts":10,"dur":5,"pid":1,"tid":1}]|} in
17 Alcotest.(check (list string)) "names" [ "f" ] (names l);
18 Alcotest.(check (list (option (float 0.)))) "durs" [ Some 5. ] (durs l)
19
20(* Spec, "Duration Events": B/E must be properly nested per thread; an E closes
21 the most recent open B of its thread. *)
22let test_duration_nesting () =
23 let l =
24 slices
25 {|[{"name":"outer","ph":"B","ts":0,"pid":1,"tid":1},
26 {"name":"inner","ph":"B","ts":10,"pid":1,"tid":1},
27 {"ph":"E","ts":20,"pid":1,"tid":1},
28 {"ph":"E","ts":40,"pid":1,"tid":1}]|}
29 in
30 Alcotest.(check (list string)) "names" [ "outer"; "inner" ] (names l);
31 Alcotest.(check (list (option (float 0.))))
32 "durs" [ Some 40.; Some 10. ] (durs l)
33
34(* Duration pairing is per (pid, tid): same-name events on two threads must not
35 cross-match. *)
36let test_duration_per_thread () =
37 let l =
38 slices
39 {|[{"name":"w","ph":"B","ts":0,"pid":1,"tid":1},
40 {"name":"w","ph":"B","ts":5,"pid":1,"tid":2},
41 {"ph":"E","ts":7,"pid":1,"tid":2},
42 {"ph":"E","ts":30,"pid":1,"tid":1}]|}
43 in
44 Alcotest.(check (list (option (float 0.))))
45 "durs" [ Some 30.; Some 2. ] (durs l)
46
47(* A B without an E is an unterminated slice, not a dropped one: crashed
48 producers are the array format's normal case. A stray E is dropped. *)
49let test_duration_unmatched () =
50 let l =
51 slices
52 {|[{"ph":"E","ts":1,"pid":1,"tid":1},
53 {"name":"open","ph":"B","ts":5,"pid":1,"tid":1}]|}
54 in
55 Alcotest.(check (list string)) "names" [ "open" ] (names l);
56 Alcotest.(check (list (option (float 0.)))) "durs" [ None ] (durs l)
57
58(* B and E args are concatenated, opening side first. *)
59let test_duration_args_merge () =
60 let l =
61 slices
62 {|[{"name":"f","ph":"B","ts":0,"pid":1,"tid":1,"args":{"in":1}},
63 {"ph":"E","ts":1,"pid":1,"tid":1,"args":{"out":2}}]|}
64 in
65 match l with
66 | [ s ] ->
67 Alcotest.(check (list string))
68 "arg keys" [ "in"; "out" ]
69 (List.map fst s.Slice.args)
70 | _ -> Alcotest.fail "expected one slice"
71
72(* Spec, "Async Events": b/e pair by id, crossing other spans freely. The same
73 id can be reused sequentially. *)
74let test_async_interleaved () =
75 let l =
76 slices
77 {|[{"name":"req","ph":"b","ts":0,"id":1,"pid":1,"tid":1},
78 {"name":"req","ph":"b","ts":10,"id":2,"pid":1,"tid":1},
79 {"name":"req","ph":"e","ts":15,"id":1,"pid":1,"tid":1},
80 {"name":"req","ph":"e","ts":40,"id":2,"pid":1,"tid":1},
81 {"name":"req","ph":"b","ts":50,"id":1,"pid":1,"tid":1},
82 {"name":"req","ph":"e","ts":51,"id":1,"pid":1,"tid":1}]|}
83 in
84 Alcotest.(check (list (option (float 0.))))
85 "durs"
86 [ Some 15.; Some 30.; Some 1. ]
87 (durs l)
88
89(* Async pairing requires the same name: two ids may collide across different
90 span declarations. *)
91let test_async_name_keyed () =
92 let l =
93 slices
94 {|[{"name":"a","ph":"b","ts":0,"id":7,"pid":1},
95 {"name":"b","ph":"b","ts":1,"id":7,"pid":1},
96 {"name":"a","ph":"e","ts":2,"id":7,"pid":1},
97 {"name":"b","ph":"e","ts":4,"id":7,"pid":1}]|}
98 in
99 Alcotest.(check (list (option (float 0.))))
100 "durs" [ Some 2.; Some 3. ] (durs l)
101
102(* Async instants are zero-duration slices; unterminated async begins surface
103 with dur None. *)
104let test_async_instant_and_open () =
105 let l =
106 slices
107 {|[{"name":"n","ph":"n","ts":3,"id":1,"pid":1},
108 {"name":"open","ph":"b","ts":5,"id":2,"pid":1}]|}
109 in
110 Alcotest.(check (list (option (float 0.)))) "durs" [ Some 0.; None ] (durs l)
111
112(* Events sort by timestamp before pairing; the format does not promise file
113 order. *)
114let test_unsorted_input () =
115 let l =
116 slices
117 {|[{"ph":"E","ts":9,"pid":1,"tid":1},
118 {"name":"f","ph":"B","ts":2,"pid":1,"tid":1}]|}
119 in
120 Alcotest.(check (list (option (float 0.)))) "durs" [ Some 7. ] (durs l)
121
122(* Non-interval phases produce no slice. *)
123let test_non_slices () =
124 let l =
125 slices
126 {|[{"name":"i","ph":"i","ts":1,"pid":1},
127 {"name":"c","ph":"C","ts":2,"pid":1,"args":{"v":1}},
128 {"name":"process_name","ph":"M","pid":1,"args":{"name":"p"}}]|}
129 in
130 Alcotest.(check (list string)) "no slices" [] (names l)
131
132let suite =
133 ( "slice",
134 [
135 Alcotest.test_case "complete (spec)" `Quick test_complete;
136 Alcotest.test_case "duration nesting (spec)" `Quick test_duration_nesting;
137 Alcotest.test_case "duration per thread" `Quick test_duration_per_thread;
138 Alcotest.test_case "duration unmatched" `Quick test_duration_unmatched;
139 Alcotest.test_case "duration args merge" `Quick test_duration_args_merge;
140 Alcotest.test_case "async interleaved (spec)" `Quick
141 test_async_interleaved;
142 Alcotest.test_case "async name keyed" `Quick test_async_name_keyed;
143 Alcotest.test_case "async instant and open" `Quick
144 test_async_instant_and_open;
145 Alcotest.test_case "unsorted input" `Quick test_unsorted_input;
146 Alcotest.test_case "non-slices" `Quick test_non_slices;
147 ] )