123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691open!Coreopen!Asyncopen!Import(* PAGER=cat because perf spawns [less] if you get the arguments wrong, and that keeps the
parent process alive even though it just failed. That, in turn, makes magic-trace stop
responding to Ctrl+C. *)letperf_env=`Extend["PAGER","cat"]moduleRecord_opts=structtypet={multi_thread:bool;full_execution:bool;snapshot_size:Pow2_pages.toption}letparam=let%map_open.Commandmulti_thread=flag"-multi-thread"no_arg~doc:"Records every thread of an executable, instead of only the thread whose TID \
is equal to the process' PID.\n\
Warning: this flag decreases the trace's lookback period because the kernel \
divides snapshot buffer resources equally across all threads."andfull_execution=flag"-full-execution"no_arg~doc:"Record a program's full execution instead of using a snapshot ring buffer.\n\
Warning: The trace grows at a rate of hundreds of megabytes per second. The \
trace viewer may fail to load traces larger than 100M."andsnapshot_size=Pow2_pages.optional_flag"-snapshot-size"~doc:" Tunes the amount of data captured in a trace. Default: 4M if root or \
perf_event_paranoid < 0, 256K otherwise. More info: magic-trace.org/w/s"in{multi_thread;full_execution;snapshot_size};;endletperf_exit_to_or_error=function|Ok()|Error(`Signal_)->Ok()|Error(`Exit_non_zeron)->Core_unix.Exit.of_coden|>Core_unix.Exit.or_error;;(* Same as [Caml.exit] but does not run at_exit handlers *)externalsys_exit:int->'a="caml_sys_exit"letperf_fork_exec?env~prog~argv()=letpr_set_pdeathsig=Or_error.ok_exnLinux_ext.pr_set_pdeathsiginmatchCore_unix.fork()with|`In_the_child->pr_set_pdeathsigSignal.kill;never_returns(tryCore_unix.exec?env~prog~argv()with|_->sys_exit127)|`In_the_parentpid->pid;;moduleRecording=structtypet={pid:Pid.t;when_to_snapshot:[`at_exitof[`sigint|`sigusr2]|`function_call|`never]}letperf_selector_of_trace_mode:Trace_mode.t->string=function|Userspace->"u"|Kernel->"k"|Userspace_and_kernel->"uk";;letperf_intel_pt_config_of_timer_resolution:Timer_resolution.t->string=function|Low->""|Normal->"cyc=1,cyc_thresh=1,mtc_period=0"|High->"cyc=1,cyc_thresh=1,mtc_period=0,noretcomp=1"|Custom{cyc;cyc_thresh;mtc;mtc_period;noretcomp;psb_period}->letmake_configkey=function|None->None|Somevalue->Some[%string"%{key}=%{value#Int}"]in[make_config"cyc"(Option.map~f:Bool.to_intcyc);make_config"cyc_thresh"cyc_thresh;make_config"mtc"(Option.map~f:Bool.to_intmtc);make_config"mtc_period"mtc_period;make_config"noretcomp"(Option.map~f:Bool.to_intnoretcomp);make_config"psb_period"psb_period]|>List.filter_opt|>String.concat~sep:",";;letattach_and_record{Record_opts.multi_thread;full_execution;snapshot_size}~debug_print_perf_commands~(subcommand:Subcommand.t)~(when_to_snapshot:When_to_snapshot.t)~(trace_mode:Trace_mode.t)~(timer_resolution:Timer_resolution.t)~record_dirpid=let%bindcapabilities=Perf_capabilities.detect_exn()inlet%bind.Deferred.Or_error()=matchtrace_mode,Perf_capabilities.(do_intersectcapabilitieskernel_tracing)with|Userspace,_|_,true->return(Ok())|(Kernel|Userspace_and_kernel),false->ifnotEnv_vars.perf_is_privilegedthenDeferred.Or_error.error_string"magic-trace must be run as root in order to trace the kernel"elsereturn(Ok())inletperf_supports_snapshot_on_exit=Perf_capabilities.(do_intersectcapabilitiessnapshot_on_exit)in(matchwhen_to_snapshot,subcommandwith|Magic_trace_or_the_application_terminates,Run->ifnotperf_supports_snapshot_on_exitthenprintf"Warning: magic-trace will only be able to snapshot when magic-trace is \
Ctrl+C'd, not when the application it's running ends. If that application \
ends before magic-trace can snapshot it, the resulting trace will be empty. \
The ability to snapshot when an application teminates was added to perf's \
userspace tools in version 5.4. For more information, see:\n\
https://github.com/janestreet/magic-trace/wiki/Supported-platforms,-programming-languages,-and-runtimes#supported-perf-versions\n\
%!"|Application_calls_a_function_,_|_,Attach->());letthread_opts=matchmulti_threadwith|false->["--per-thread";"-t"]|true->["-p"]inletpid_opt=[Pid.to_stringpid]inletev_arg=lettimer_resolution:Timer_resolution.t=match(timer_resolution,Perf_capabilities.(do_intersectcapabilitiesconfigurable_psb_period))with|(Normal|High),false->Core.eprintf"Warning: This machine has an older generation processor, timing granularity \
will be ~1us instead of ~10ns. Consider using a newer machine.\n\
%!";Low|_,_->timer_resolutioninletintel_pt_config=perf_intel_pt_config_of_timer_resolutiontimer_resolutioninletselector=perf_selector_of_trace_modetrace_modein[%string"--event=intel_pt/%{intel_pt_config}/%{selector}"]inletkcore_opts=matchtrace_mode,Perf_capabilities.(do_intersectcapabilitieskcore)with|Userspace,_->[]|(Kernel|Userspace_and_kernel),true->["--kcore"]|(Kernel|Userspace_and_kernel),false->(* Strictly speaking, we could recreate tools/perf/perf-with-kcore.sh
here instead of bailing. But that's tricky, and upgrading to a newer
perf is easier. *)Core.eprintf"Warning: old perf version detected! perf userspace tools v5.5 contain an \
important feature, kcore, that make decoding kernel traces more reliable. In \
our experience, tracing the kernel mostly works without this feature, but you \
may run into problems if you're trying to trace through self-modifying code \
(the kernel may do this more than you think). Install a perf version >= 5.5 \
to avoid this.\n\
%!";[]inletsnapshot_size_opt=matchsnapshot_sizewith|None->[]|Somesnapshot_size->[[%string"-m,%{Pow2_pages.num_pagessnapshot_size#Int}"]]inletwhen_to_snapshot=iffull_executionthen`neverelse(matchwhen_to_snapshotwith|Magic_trace_or_the_application_terminates->ifperf_supports_snapshot_on_exitthen`at_exit`sigintelse`at_exit`sigusr2|Application_calls_a_function_->`function_call)inletsnapshot_opt=matchwhen_to_snapshotwith|`never->[]|`at_exit`sigint->["--snapshot=e"]|`function_call|`at_exit`sigusr2->["--snapshot"]inletargv=List.concat[["perf";"record";"-o";record_dir^/"perf.data";ev_arg;"--timestamp"];thread_opts;pid_opt;snapshot_opt;kcore_opts;snapshot_size_opt]inifdebug_print_perf_commandsthenCore.printf"%s\n%!"(String.concat~sep:" "argv);(* Perf prints output we don't care about and --quiet doesn't work for some reason *)letperf_pid=perf_fork_exec~env:perf_env~prog:"perf"~argv()in(* This detaches the perf process from our "process group" but not our session. This
makes it so that when Ctrl-C is sent to magic_trace in the terminal to end an attach
session, it doesn't also send SIGINT to the perf process, allowing us to send it a
SIGUSR2 first to get it to capture a snapshot before exiting. *)Core_unix.setpgid~of_:perf_pid~to_:perf_pid;let%map()=Async.Clock_ns.after(Time_ns.Span.of_ms500.0)in(* Check that the process hasn't failed after waiting, because there's no point pausing
to do recording if we've already failed. *)letres=Core_unix.wait_nohang(`Pidperf_pid)inlet%map.Or_error()=matchreswith|Some(_,exit)->perf_exit_to_or_errorexit|_->Ok()in{pid=perf_pid;when_to_snapshot};;letmaybe_take_snapshott~source=letsignal=matcht.when_to_snapshot,sourcewith(* [`never] only comes up in [-full-execution] mode. In that mode, perf always gives a
complete trace; there's no snapshotting. *)|`never,_->None(* Do not snapshot at the end of a program if the user has set up a trigger symbol. *)|`function_call,`ctrl_c->None(* This shouldn't happen unless there was a bug elsewhere. It would imply that a trigger
symbol was hit when there is no trigger symbol configured. *)|`at_exit_,`function_call->None(* Trigger symbol was hit, and we're configured to look for them. *)|`function_call,`function_call->SomeSignal.usr2(* Ctrl-C was hit, and we're configured to look for that. *)|`at_exitsignal,`ctrl_c->(* The actual signal to use varies depending on whether or not the user's version of perf
supports snapshot-at-exit. *)Some(matchsignalwith|`sigint->Signal.int|`sigusr2->Signal.usr2)inmatchsignalwith|None->()|Somesignal->Signal_unix.send_isignal(`Pidt.pid);;letfinish_recordingt=Signal_unix.send_iSignal.term(`Pidt.pid);(* This should usually be a signal exit, but we don't really care, if it didn't produce
a good perf.data file the next step will fail. *)let%map(res:Core_unix.Exit_or_signal.t)=Async_unix.Unix.waitpidt.pidinperf_exit_to_or_errorres;;endmoduleDecode_opts=structtypet=unitletparam=Command.Param.return()endmodulePerf_line=structletreport_itraces="be"letreport_fields="pid,tid,time,flags,ip,addr,sym,symoff"letsaturating_sub_i64ab=matchInt64.(to_int(a-b))with|None->Int.max_value|Someoffset->offset;;letint64_of_hex_stringstr=(* Bit hacks for fast parsing of hex strings.
*
* Note that in ASCII, ('1' | 'a' | 'A') & 0xF = 1.
*
* So for each character, take the bottom 4 bits, and add 9 if it's
* not a digit. *)letres=ref0Linfori=0toString.lengthstr-1doletopenInt64inletc=of_int(Char.to_int(String.unsafe_getstri))inres:=(!reslsl4)lor((cland0xFL)+((clsr6)lor((clsr3)land0x8L)))done;!res;;let%test_module_=(modulestructopenCoreletcheckstr=Core.print_s([%sexp_of:Int64.Hex.t](int64_of_hex_stringstr))let%expect_test"int64 hex parsing"=check"fF";[%expect{| 0xff |}];check"f0f";[%expect{| 0xf0f |}];check"fA0f";[%expect{| 0xfa0f |}];check"0";[%expect{| 0x0 |}];;end);;letok_perf_line_re=Re.Perl.re{|^ *([0-9]+)/([0-9]+) +([0-9]+).([0-9]+): +(call|return|tr strt|syscall|sysret|hw int|iret|tr end|tr strt tr end|tr end (?:call|return|syscall|sysret|iret)|jmp|jcc) +([0-9a-f]+) (.*) => +([0-9a-f]+) (.*)$|}|>Re.compile;;lettrace_error_re=Re.Posix.re{|^ instruction trace error type [0-9]+ (time ([0-9]+)\.([0-9]+) )?cpu [\-0-9]+ pid ([\-0-9]+) tid ([\-0-9]+) ip (0x[0-9a-fA-F]+|0) code [0-9+]: (.*)$|}|>Re.compile;;letsymbol_and_offset_re=Re.Posix.re{|^(.*)\+(0x[0-9a-f]+)$|}|>Re.compiletypeclassification=|Trace_error|Ok_perf_lineletclassifyline=ifString.is_prefixline~prefix:" instruction trace error"thenTrace_errorelseOk_perf_line;;letparse_time~time_hi~time_lo=lettime_lo=(* In practice, [time_lo] seems to always be 9 decimal places, but it seems good
to guard against other possibilities. *)letnum_decimal_places=String.lengthtime_loinmatchOrdering.of_int(Int.comparenum_decimal_places9)with|Less->Int.of_stringtime_lo*Int.pow10(9-num_decimal_places)|Equal->Int.of_stringtime_lo|Greater->Int.of_string(String.prefixtime_lo9)inlettime_hi=Int.of_stringtime_hiintime_lo+(time_hi*1_000_000_000)|>Time_ns.Span.of_int_ns;;lettrace_error_to_eventline:Event.Decode_error.t=matchRe.Group.all(Re.exectrace_error_reline)with|[|_;_;time_hi;time_lo;pid;tid;ip;message|]->letpid=Int.of_stringpidinlettid=Int.of_stringtidinletinstruction_pointer=ifString.(=)ip"0"thenNoneelseSome(Int64.Hex.of_stringip)inlettime=ifString.is_emptytime_hi&&String.is_emptytime_lothenTime_ns_unix.Span.Option.noneelseTime_ns_unix.Span.Option.some(parse_time~time_hi~time_lo)in{thread={pid=(ifpid=0thenNoneelseSome(Pid.of_intpid));tid=(iftid=0thenNoneelseSome(Pid.of_inttid))};instruction_pointer;message;time}|results->raise_s[%message"Regex of trace error did not match expected fields"(results:stringarray)];;letok_perf_line_to_eventline~(perf_map:Perf_map.toption):Event.Ok.t=matchRe.Group.all(Re.execok_perf_line_reline)with|[|_;pid;tid;time_hi;time_lo;kind;src_instruction_pointer;src_symbol_and_offset;dst_instruction_pointer;dst_symbol_and_offset|]->letpid=Int.of_stringpidinlettid=Int.of_stringtidinlettime=parse_time~time_hi~time_loinletsrc_instruction_pointer=int64_of_hex_stringsrc_instruction_pointerinletdst_instruction_pointer=int64_of_hex_stringdst_instruction_pointerinletparse_symbol_and_offsetstr~addr=matchRe.Group.all(Re.execsymbol_and_offset_restr)with|[|_;symbol;offset|]->Symbol.From_perfsymbol,Int.Hex.of_stringoffset|_|(exception_)->letfailed=Symbol.Unknown,0in(matchperf_mapwith|None->failed|Someperf_map->(matchPerf_map.symbolperf_map~addrwith|None->failed|Somelocation->(* It's strange that perf isn't resolving these symbols. It says on the tin that
it supports perf map files! *)letoffset=saturating_sub_i64addrlocation.start_addrinFrom_perf_maplocation,offset))inletsrc_symbol,src_symbol_offset=parse_symbol_and_offsetsrc_symbol_and_offset~addr:src_instruction_pointerinletdst_symbol,dst_symbol_offset=parse_symbol_and_offsetdst_symbol_and_offset~addr:dst_instruction_pointerinletstarts_trace,kind=matchString.chop_prefixkind~prefix:"tr strt"with|None->false,kind|Somerest->true,String.lstrip~drop:Char.is_whitespacerestinletends_trace,kind=matchString.chop_prefixkind~prefix:"tr end"with|None->false,kind|Somerest->true,String.lstrip~drop:Char.is_whitespacerestinlettrace_state_change:Trace_state_change.toption=matchstarts_trace,ends_tracewith|true,false->SomeStart|false,true->SomeEnd|false,false(* "tr strt tr end" happens when someone `go run`s ./demo/demo.go. But that
trace is pretty broken for other reasons, so it's hard to say if this is
truly necessary. Regardless, it's slightly more user friendly to show a
broken trace instead of crashing here. *)|true,true->Noneinletkind:Event.Kind.toption=matchString.stripkindwith|"call"->SomeCall|"return"->SomeReturn|"jmp"->SomeJump|"jcc"->SomeJump|"syscall"->SomeSyscall|"hw int"->SomeHardware_interrupt|"iret"->SomeIret|"sysret"->SomeSysret|""->None|_->printf"Warning: skipping unrecognized perf output: %s\n%!"line;Nonein{thread={pid=(ifpid=0thenNoneelseSome(Pid.of_intpid));tid=(iftid=0thenNoneelseSome(Pid.of_inttid))};time;trace_state_change;kind;src={instruction_pointer=src_instruction_pointer;symbol=src_symbol;symbol_offset=src_symbol_offset};dst={instruction_pointer=dst_instruction_pointer;symbol=dst_symbol;symbol_offset=dst_symbol_offset}}|results->raise_s[%message"Regex of expected perf output did not match."(results:stringarray)];;letto_eventline~perf_map:Event.t=trymatchclassifylinewith|Trace_error->Error(trace_error_to_eventline)|Ok_perf_line->Ok(ok_perf_line_to_eventline~perf_map)with|exn->raise_s[%message"BUG: exception raised while parsing perf output. Please report this to \
https://github.com/janestreet/magic-trace/issues/"(exn:exn)~perf_output:(line:string)];;let%test_module_=(modulestructopenCoreletchecks=to_events~perf_map:None|>[%sexp_of:Event.t]|>print_slet%expect_test"C symbol"=check{| 25375/25375 4509191.343298468: call 7f6fce0b71f4 __clock_gettime+0x24 => 7ffd193838e0 __vdso_clock_gettime+0x0|};[%expect{|
(Ok
((thread ((pid (25375)) (tid (25375)))) (time 52d4h33m11.343298468s)
(kind Call)
(src
((instruction_pointer 0x7f6fce0b71f4) (symbol (From_perf __clock_gettime))
(symbol_offset 0x24)))
(dst
((instruction_pointer 0x7ffd193838e0)
(symbol (From_perf __vdso_clock_gettime)) (symbol_offset 0x0))))) |}];;let%expect_test"C symbol trace start"=check{| 25375/25375 4509191.343298468: tr strt 0 [unknown] => 7f6fce0b71d0 __clock_gettime+0x0|};[%expect{|
(Ok
((thread ((pid (25375)) (tid (25375)))) (time 52d4h33m11.343298468s)
(trace_state_change Start)
(src ((instruction_pointer 0x0) (symbol Unknown) (symbol_offset 0x0)))
(dst
((instruction_pointer 0x7f6fce0b71d0) (symbol (From_perf __clock_gettime))
(symbol_offset 0x0))))) |}];;let%expect_test"C++ symbol"=check{| 7166/7166 4512623.871133092: call 9bc6db a::B<a::C, a::D<a::E>, a::F, a::F, G::H, a::I>::run+0x1eb => 9f68b0 J::K<int, std::string>+0x0|};[%expect{|
(Ok
((thread ((pid (7166)) (tid (7166)))) (time 52d5h30m23.871133092s)
(kind Call)
(src
((instruction_pointer 0x9bc6db)
(symbol
(From_perf "a::B<a::C, a::D<a::E>, a::F, a::F, G::H, a::I>::run"))
(symbol_offset 0x1eb)))
(dst
((instruction_pointer 0x9f68b0)
(symbol (From_perf "J::K<int, std::string>")) (symbol_offset 0x0))))) |}];;let%expect_test"OCaml symbol"=check{|2017001/2017001 761439.053336670: call 56234f77576b Base.Comparable.=_2352+0xb => 56234f4bc7a0 caml_apply2+0x0|};[%expect{|
(Ok
((thread ((pid (2017001)) (tid (2017001)))) (time 8d19h30m39.05333667s)
(kind Call)
(src
((instruction_pointer 0x56234f77576b)
(symbol (From_perf Base.Comparable.=_2352)) (symbol_offset 0xb)))
(dst
((instruction_pointer 0x56234f4bc7a0) (symbol (From_perf caml_apply2))
(symbol_offset 0x0))))) |}];;(* CR-someday wduff: Leaving this concrete example here for when we support this. See my
comment above as well.
{[
let%expect_test "Unknown Go symbol" =
check
{|2118573/2118573 770614.599007116: tr strt tr end 0 [unknown] => 4591e1 [unknown]|};
[%expect]
;;
]}
*)let%expect_test"manufactured example 1"=check{|2017001/2017001 761439.053336670: call 56234f77576b x => +0xb => 56234f4bc7a0 caml_apply2+0x0|};[%expect{|
(Ok
((thread ((pid (2017001)) (tid (2017001)))) (time 8d19h30m39.05333667s)
(kind Call)
(src
((instruction_pointer 0x56234f77576b) (symbol (From_perf "x => "))
(symbol_offset 0xb)))
(dst
((instruction_pointer 0x56234f4bc7a0) (symbol (From_perf caml_apply2))
(symbol_offset 0x0))))) |}];;let%expect_test"manufactured example 2"=check{|2017001/2017001 761439.053336670: call 56234f77576b x => +0xb => 56234f4bc7a0 => +0x0|};[%expect{|
(Ok
((thread ((pid (2017001)) (tid (2017001)))) (time 8d19h30m39.05333667s)
(kind Call)
(src
((instruction_pointer 0x56234f77576b) (symbol (From_perf "x => "))
(symbol_offset 0xb)))
(dst
((instruction_pointer 0x56234f4bc7a0) (symbol (From_perf "=> "))
(symbol_offset 0x0))))) |}];;let%expect_test"manufactured example 3"=check{|2017001/2017001 761439.053336670: call 56234f77576b + +0xb => 56234f4bc7a0 caml_apply2+0x0|};[%expect{|
(Ok
((thread ((pid (2017001)) (tid (2017001)))) (time 8d19h30m39.05333667s)
(kind Call)
(src
((instruction_pointer 0x56234f77576b) (symbol (From_perf "+ "))
(symbol_offset 0xb)))
(dst
((instruction_pointer 0x56234f4bc7a0) (symbol (From_perf caml_apply2))
(symbol_offset 0x0))))) |}];;let%expect_test"decode error with a timestamp"=check" instruction trace error type 1 time 47170.086912826 cpu -1 pid 293415 tid \
293415 ip 0x7ffff7327730 code 7: Overflow packet";[%expect{|
(Error
((thread ((pid (293415)) (tid (293415)))) (time (13h6m10.086912826s))
(instruction_pointer (0x7ffff7327730)) (message "Overflow packet"))) |}];;let%expect_test"decode error without a timestamp"=check" instruction trace error type 1 cpu -1 pid 293415 tid 293415 ip \
0x7ffff7327730 code 7: Overflow packet";[%expect{|
(Error
((thread ((pid (293415)) (tid (293415)))) (time ())
(instruction_pointer (0x7ffff7327730)) (message "Overflow packet"))) |}];;let%expect_test"lost trace data"=check" instruction trace error type 1 time 2651115.104731379 cpu -1 pid 1801680 tid \
1801680 ip 0 code 8: Lost trace data";[%expect{|
(Error
((thread ((pid (1801680)) (tid (1801680)))) (time (30d16h25m15.104731379s))
(instruction_pointer ()) (message "Lost trace data"))) |}];;end);;endletdecode_events()~debug_print_perf_commands~record_dir~perf_map=letargs=["script";"-i";record_dir^/"perf.data";"--ns";[%string"--itrace=%{Perf_line.report_itraces}"];"-F";Perf_line.report_fields]inifdebug_print_perf_commandsthenCore.printf"perf %s\n%!"(String.concat~sep:" "args);(* CR-someday tbrindus: this should be switched over to using [perf_fork_exec] to avoid
the [perf script] process from outliving the parent. *)let%mapperf_script_proc=Process.create_exn~env:perf_env~prog:"perf"~args()inletline_pipe=Process.stdoutperf_script_proc|>Reader.linesindon't_wait_for(Reader.transfer(Process.stderrperf_script_proc)(Writer.pipe(forceWriter.stderr)));letevents=(* Every route of filtering on streams in an async way seems to be deprecated,
including converting to pipes which says that the stream creation should be
switched to a pipe creation. Changing Async_shell is out-of-scope, and I also
can't see a reason why filter_map would lead to memory leaks. *)Pipe.mapline_pipe~f:(Perf_line.to_event~perf_map)inletclose_result=let%mapexit_or_signal=Process.waitperf_script_procinperf_exit_to_or_errorexit_or_signalinOk{Decode_result.events;close_result};;