Source file trace.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
(** Runs a program under Intel Processor Trace in Snapshot mode *)
open! Core
open! Async
open! Import
let supports_fzf =
Lazy.from_fun (fun () ->
match Core_unix.fork () with
| `In_the_child ->
Core_unix.close Core_unix.stdout;
Core_unix.exec ~prog:"fzf" ~argv:[ "fzf"; "--version" ] ~use_path:true ()
|> never_returns
| `In_the_parent pid ->
let exit_or_signal = Core_unix.waitpid pid in
(match Core_unix.Exit_or_signal.or_error exit_or_signal with
| Error _ -> false
| Ok () -> true))
;;
let create_elf ~executable ~(when_to_snapshot : When_to_snapshot.t) =
let elf = Elf.create executable in
match when_to_snapshot, elf with
| Application_calls_a_function _, None ->
Deferred.Or_error.errorf
"As far as magic-trace can tell, that executable doesn't have a symbol table. Was \
the binary built without debug info?"
| Magic_trace_or_the_application_terminates, _ | _, Some _ -> return (Ok elf)
;;
let check_for_processor_trace_support () =
match Core_unix.access "/sys/bus/event_source/devices/intel_pt" [ `Exists ] with
| Ok () -> Ok ()
| Error _ ->
Or_error.error_string
"Error: This machine doesn't support Intel Processor Trace, which is a hardware \
feature essential for magic-trace to work.\n\
This may be because it's a virtual machine or it's a physical machine that isn't \
new enough or uses an AMD processor.\n\
Try again on a physical Intel machine."
;;
let debug_flag flag = if Env_vars.debug then flag else Command.Param.return false
let debug_print_perf_commands =
let open Command.Param in
flag "-z-print-perf-commands" no_arg ~doc:"Prints perf commands when they're executed."
|> debug_flag
;;
let write_trace_from_events
~print_events
~trace_mode
?debug_info
writer
hits
decode_result
=
let { Decode_result.events; close_result } = decode_result in
let%bind.Deferred earliest_time =
let%map.Deferred _wait_for_first = Pipe.values_available events in
match Pipe.peek events with
| Some (Ok earliest) -> earliest.time
| None | Some (Error _) -> Time_ns.Span.zero
in
let trace =
let base_time =
Time_ns.add (Boot_time.time_ns_of_boot_in_perf_time ()) earliest_time
in
Tracing.Trace.Expert.create ~base_time:(Some base_time) writer
in
let events =
if print_events
then
Pipe.map events ~f:(fun (event : Event.t) ->
Core.print_s ~mach:() (Event.sexp_of_t event);
event)
else events
in
let writer =
Trace_writer.create
~trace_mode
~debug_info
~earliest_time
~hits
~annotate_inferred_start_times:Env_vars.debug
trace
in
let process_event ev = Trace_writer.write_event writer ev in
let%bind () = Pipe.iter_without_pushback events ~f:process_event in
Trace_writer.end_of_trace writer;
Tracing.Trace.close trace;
close_result
;;
module Make_commands (Backend : Backend_intf.S) = struct
module Decode_opts = struct
type t =
{ output_config : Tracing_tool_output.t
; decode_opts : Backend.Decode_opts.t
; print_events : bool
}
end
module Hits_file = struct
type t = (string * Breakpoint.Hit.t) list [@@deriving sexp]
let filename ~record_dir = record_dir ^/ "hits.sexp"
end
let decode_to_trace
~elf
~trace_mode
~debug_print_perf_commands
~record_dir
~perf_map
{ Decode_opts.output_config; decode_opts; print_events }
=
Core.eprintf "[ Decoding, this takes a while... ]\n%!";
Tracing_tool_output.write_and_maybe_view output_config ~f:(fun writer ->
let open Deferred.Or_error.Let_syntax in
let hits =
In_channel.read_all (Hits_file.filename ~record_dir)
|> Sexp.of_string
|> [%of_sexp: Hits_file.t]
in
let debug_info = Option.map elf ~f:Elf.addr_table in
let%bind decode_result =
Backend.decode_events
decode_opts
~debug_print_perf_commands
~record_dir
~perf_map
in
let%bind () =
write_trace_from_events
?debug_info
~trace_mode
~print_events
writer
hits
decode_result
in
return ())
;;
module Record_opts = struct
type t =
{ backend_opts : Backend.Record_opts.t
; multi_snapshot : bool
; when_to_snapshot : When_to_snapshot.t
; record_dir : string
; executable : string
; trace_mode : Trace_mode.t
; timer_resolution : Timer_resolution.t
}
end
module Attachment = struct
type t =
{ recording : Backend.Recording.t
; done_ivar : unit Ivar.t
; breakpoint_done : unit Deferred.t
; finalize_recording : unit -> unit
}
end
let attach (opts : Record_opts.t) ~elf ~debug_print_perf_commands ~subcommand pid =
let%bind.Deferred.Or_error () =
check_for_processor_trace_support () |> Deferred.return
in
let%bind.Deferred.Or_error snap_loc =
match elf with
| None -> return (Ok None)
| Some elf ->
(match opts.when_to_snapshot with
| Magic_trace_or_the_application_terminates -> return (Ok None)
| Application_calls_a_function which_function ->
let%bind.Deferred.Or_error snap_sym =
match which_function with
| Use_fzf_to_select_one ->
let all_symbols = Elf.all_symbols elf in
if force supports_fzf
then (
match%bind.Deferred.Or_error Fzf.pick_one (Assoc all_symbols) with
| None -> Deferred.Or_error.error_string "No symbol selected"
| Some symbol -> return (Ok symbol))
else
Deferred.Or_error.error_string
"magic-trace could show you a fuzzy-finding selector here if \"fzf\" \
were in your PATH, but it is not."
| User_selected symbol_name ->
(match Elf.find_symbol elf symbol_name with
| None ->
Deferred.Or_error.errorf "Snapshot symbol not found: %s" symbol_name
| Some symbol -> return (Ok symbol))
in
let snap_loc = Elf.symbol_stop_info elf pid snap_sym in
return (Ok (Some snap_loc)))
in
let%map.Deferred.Or_error recording =
Backend.Recording.attach_and_record
opts.backend_opts
~debug_print_perf_commands
~subcommand
~when_to_snapshot:opts.when_to_snapshot
~trace_mode:opts.trace_mode
~timer_resolution:opts.timer_resolution
~record_dir:opts.record_dir
pid
in
let done_ivar = Ivar.create () in
let snapshot_taken = ref false in
let take_snapshot ~source =
Backend.Recording.maybe_take_snapshot recording ~source;
snapshot_taken := true;
Core.eprintf "[ Snapshot taken. ]\n%!";
if not opts.multi_snapshot then Ivar.fill_if_empty done_ivar ()
in
let hits = ref [] in
let finalize_recording () =
if not !snapshot_taken then take_snapshot ~source:`ctrl_c;
Out_channel.write_all
(Hits_file.filename ~record_dir:opts.record_dir)
~data:([%sexp (!hits : Hits_file.t)] |> Sexp.to_string)
in
let take_snapshot_on_hit hit =
hits := hit :: !hits;
take_snapshot ~source:`function_call
in
let breakpoint_done =
match snap_loc with
| None -> Deferred.unit
| Some { Elf.Stop_info.name; addr; _ } ->
Core.eprintf "[ Attaching to %s @ 0x%016Lx ]\n%!" name addr;
let single_hit = not opts.multi_snapshot in
let bp = Breakpoint.breakpoint_fd pid ~addr ~single_hit in
let bp = Or_error.ok_exn bp in
let fd =
Async_unix.Fd.create
Async_unix.Fd.Kind.File
(Breakpoint.fd bp)
(Info.of_string "perf breakpoint")
in
let rec read_evs snapshot_enabled =
match Breakpoint.next_hit bp with
| Some hit ->
if snapshot_enabled then take_snapshot_on_hit (name, hit);
read_evs false
| None -> ()
in
let interrupt = Ivar.read done_ivar in
let%map.Deferred res =
Async_unix.Fd.interruptible_every_ready_to
fd
`Read
~interrupt
(fun () -> read_evs true)
()
in
(match res with
| `Interrupted -> Breakpoint.destroy bp
| `Bad_fd | `Closed | `Unsupported -> failwith "failed to wait on breakpoint")
in
{ Attachment.recording; done_ivar; breakpoint_done; finalize_recording }
;;
let detach { Attachment.recording; done_ivar; breakpoint_done; finalize_recording } =
Ivar.fill_if_empty done_ivar ();
let%bind () = breakpoint_done in
finalize_recording ();
let%bind.Deferred.Or_error () = Backend.Recording.finish_recording recording in
Core.eprintf "[ Finished recording. ]\n%!";
return (Ok ())
;;
let run_and_record record_opts ~elf ~debug_print_perf_commands ~prog ~argv =
let open Deferred.Or_error.Let_syntax in
let pid = Ptrace.fork_exec_stopped ~prog ~argv () in
let%bind attachment =
attach record_opts ~elf ~debug_print_perf_commands ~subcommand:Run pid
in
Ptrace.resume pid;
let exited_ivar = Ivar.create () in
Async_unix.Signal.handle
~stop:(Ivar.read exited_ivar)
Async_unix.Signal.terminating
~f:(fun signal ->
try
UnixLabels.kill ~pid:(Pid.to_int pid) ~signal:(Signal_unix.to_system_int signal)
with
| Core_unix.Unix_error (_, (_ : string), (_ : string)) ->
());
let%bind.Deferred (_ : Core_unix.Exit_or_signal.t) = Async_unix.Unix.waitpid pid in
Ivar.fill exited_ivar ();
let%bind () = detach attachment in
return pid
;;
let attach_and_record record_opts ~elf ~debug_print_perf_commands pid =
let%bind.Deferred.Or_error attachment =
attach record_opts ~elf ~debug_print_perf_commands ~subcommand:Attach pid
in
let { Attachment.done_ivar; _ } = attachment in
let stop = Ivar.read done_ivar in
Async_unix.Signal.handle ~stop [ Signal.int ] ~f:(fun (_ : Signal.t) ->
Core.eprintf "[ Got signal, detaching... ]\n%!";
Ivar.fill_if_empty done_ivar ());
Core.eprintf "[ Attached. Press Ctrl-C to stop recording. ]\n%!";
let%bind () = stop in
detach attachment
;;
let record_dir_flag mode =
let open Command.Param in
flag
"-working-directory"
(mode Filename_unix.arg_type)
~doc:
"DIR Where to store intermediate files (including raw perf.data files). If not \
provided, magic-trace stores them in a subdirectory of $TMPDIR and deletes them \
when it's done. If provided, files will be stored in the given directory, \
creating the directory if necessary, and magic-trace will not delete the \
directory when it's done."
;;
let record_flags =
let%map_open.Command record_dir = record_dir_flag optional
and when_to_snapshot = When_to_snapshot.param
and multi_snapshot =
flag
"-multi-snapshot"
no_arg
~doc:
"Take a snapshot every time the trigger is hit, instead of only the first \
time. This flag has two caveats:\n\
(1) There's an ~8us performance hit every time the trigger symbol is hit. If \
snapshots trigger frequently, your application's performance may be \
materially impacted.\n\
(2) Each snapshot linearly increases the size of the trace file. Large trace \
files may crash the trace viewer."
and trace_mode = Trace_mode.param
and timer_resolution = Timer_resolution.param
and backend_opts = Backend.Record_opts.param in
fun ~executable ~f ->
let record_dir, cleanup =
match record_dir with
| Some dir ->
if not (Sys_unix.is_directory_exn dir) then Core_unix.mkdir dir;
dir, false
| None -> Filename_unix.temp_dir "magic_trace" "", true
in
Monitor.protect
~finally:(fun () ->
if cleanup then Shell.rm ~r:() ~f:() record_dir;
Deferred.unit)
(fun () ->
f
{ Record_opts.backend_opts
; multi_snapshot
; when_to_snapshot
; record_dir
; executable
; trace_mode
; timer_resolution
})
;;
let decode_flags =
let%map_open.Command output_config = Tracing_tool_output.param
and print_events =
flag "-z-print-events" no_arg ~doc:"Prints decoded [Event.t]s." |> debug_flag
and decode_opts = Backend.Decode_opts.param in
{ Decode_opts.output_config; decode_opts; print_events }
;;
let run_command =
Command.async_or_error
~summary:"Runs a command and traces it."
~readme:(fun () ->
"=== examples ===\n\n\
# Run a process, snapshotting at ^C or exit\n\
magic-trace run ./program -- arg1 arg2\n\n\
# Run and trace all threads of a process, not just the main one, snapshotting \
at ^C or exit\n\
magic-trace run -multi-thread ./program -- arg1 arg2\n\n\
# Run a process, tracing its entire execution (only practical for short-lived \
processes)\n\
magic-trace run -full-execution ./program\n")
(let%map_open.Command record_opt_fn = record_flags
and decode_opts = decode_flags
and debug_print_perf_commands = debug_print_perf_commands
and prog = anon ("COMMAND" %: string)
and argv =
flag "--" escape ~doc:"ARGS Arguments for the command. Ignored by magic-trace."
in
fun () ->
let open Deferred.Or_error.Let_syntax in
let executable =
match Shell.which prog with
| Some path -> path
| None -> failwithf "Can't find executable for %s" prog ()
in
record_opt_fn ~executable ~f:(fun opts ->
let elf = Elf.create opts.executable in
let%bind pid =
let argv = prog :: List.concat (Option.to_list argv) in
run_and_record opts ~elf ~debug_print_perf_commands ~prog ~argv
in
let%bind.Deferred perf_map =
Perf_map.load (Perf_map.default_filename ~pid)
in
decode_to_trace
~elf
~trace_mode:opts.trace_mode
~debug_print_perf_commands
~record_dir:opts.record_dir
~perf_map
decode_opts))
;;
let select_pid () =
if force supports_fzf
then (
let deselect_pid_args pid =
let pid = Pid.to_string pid in
[ "--ppid"; pid; "-p"; pid; "--deselect" ]
in
let process_lines =
[ [ "x"; "-w"; "--no-headers" ]
; [ "-o"; "pid,args" ]
; (if Core_unix.geteuid () = 0 then deselect_pid_args (Pid.of_int 2) else [])
]
|> List.concat
|> Shell.run_lines "ps"
in
let%bind.Deferred.Or_error sel_line =
Fzf.pick_one (Fzf.Pick_from.Inputs process_lines)
in
let pid =
let%bind.Option sel_line = sel_line in
let sel_line = String.lstrip sel_line in
let%map.Option first_part = String.split ~on:' ' sel_line |> List.hd in
Pid.of_string first_part
in
match pid with
| Some s -> Deferred.return (Ok s)
| None -> Deferred.Or_error.error_string "No pid selected")
else
Deferred.Or_error.error_string
"The [-pid] argument is mandatory. magic-trace could show you a fuzzy-finding \
selector here if \"fzf\" were in your PATH, but it is not."
;;
let attach_command =
Command.async_or_error
~summary:"Traces a running process."
~readme:(fun () ->
"=== examples ===\n\n\
# Fuzzy-find to select a running process to trace the main thread of, \
snapshotting at ^C or exit\n\
magic-trace attach\n\n\
# Fuzzy-find to select a running process and symbol to trigger on, snapshotting \
the next time the symbol is called\n\
magic-trace attach -trigger ?\n")
(let%map_open.Command record_opt_fn = record_flags
and decode_opts = decode_flags
and debug_print_perf_commands = debug_print_perf_commands
and pid =
flag
"-pid"
(optional int)
~aliases:[ "-p" ]
~doc:
"PID Process to attach to. Required if you don't have the \"fzf\" \
application available in your PATH."
in
fun () ->
let open Deferred.Or_error.Let_syntax in
let%bind pid =
match pid with
| Some pid -> return (Pid.of_int pid)
| None -> select_pid ()
in
let executable = Core_unix.readlink [%string "/proc/%{pid#Pid}/exe"] in
record_opt_fn ~executable ~f:(fun opts ->
let { Record_opts.executable; when_to_snapshot; _ } = opts in
let%bind elf = create_elf ~executable ~when_to_snapshot in
let%bind () = attach_and_record opts ~elf ~debug_print_perf_commands pid in
let%bind.Deferred perf_map =
Perf_map.load (Perf_map.default_filename ~pid)
in
decode_to_trace
~elf
~trace_mode:opts.trace_mode
~debug_print_perf_commands
~record_dir:opts.record_dir
~perf_map
decode_opts))
;;
let decode_command =
Command.async_or_error
~summary:"Converts perf-script output to a trace. (expert)"
(let%map_open.Command record_dir = record_dir_flag required
and trace_mode = Trace_mode.param
and decode_opts = decode_flags
and executable =
flag
"-executable"
(required Filename_unix.arg_type)
~doc:"FILE Executable to extract debug symbols from."
and perf_map_file =
flag
"-perf-map-file"
(optional Filename_unix.arg_type)
~doc:"FILE for JITs, path to a perf map file, in /tmp/perf-PID.map"
and debug_print_perf_commands = debug_print_perf_commands in
fun () ->
let elf = Elf.create executable in
let%bind perf_map =
match perf_map_file with
| None -> return None
| Some file -> Perf_map.load file
in
decode_to_trace
~elf
~trace_mode
~debug_print_perf_commands
~record_dir
~perf_map
decode_opts)
;;
let commands =
[ "run", run_command; "attach", attach_command; "decode", decode_command ]
;;
end
module Perf_tool_commands = Make_commands (Perf_tool_backend)
let command =
let commands = Perf_tool_commands.commands in
Command.group ~summary:"Magical tracing based on Intel Processor Trace" commands
;;
module For_testing = struct
let write_trace_from_events = write_trace_from_events ~print_events:false
end