123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491open!CoreincludeParser_intf(* Stores thread kernel objects as a tuple of (pid, tid) *)moduleThread_kernel_object=structincludeTuple.Make(Int)(Int)includeTuple.Hashable(Int)(Int)endmoduleString_index=IntmoduleThread_index=IntmoduleEvent_arg=structtypevalue=|StringofString_index.t|Intofint|Floatoffloat[@@derivingsexp_of,compare]typet=String_index.t*value[@@derivingsexp_of,compare]endmoduleEvent=structtypet={timestamp:Time_ns.Span.t;thread:Thread_index.t;category:String_index.t;name:String_index.t;arguments:Event_arg.tlist;event_type:Event_type.t}[@@derivingsexp_of,compare]endmoduleRecord=structtypet=|EventofEvent.t|Interned_stringof{index:String_index.t;value:string}|Interned_threadof{index:Thread_index.t;value:Thread.t}|Process_name_changeof{name:String_index.t;pid:int}|Thread_name_changeof{name:String_index.t;pid:int;tid:int}|Tick_initializationof{ticks_per_second:int;base_time:Time_ns.Option.t}[@@derivingsexp_of,compare]endtypet={iobuf:(read,Iobuf.seek)Iobuf.t;cur_record:(read,Iobuf.seek)Iobuf.t;mutablecurrent_provider:intoption;provider_name_by_id:stringInt.Table.t;mutableticks_per_second:int;mutablebase_tick:int;mutablebase_time:Time_ns.Option.t;thread_table:Thread.tInt.Table.t;string_table:stringInt.Table.t;process_names:stringInt.Table.t;thread_names:stringThread_kernel_object.Table.t;warnings:Warnings.t}[@@derivingfields]letcreateiobuf={iobuf;cur_record=Iobuf.create~len:0;current_provider=None;provider_name_by_id=Int.Table.create();ticks_per_second=1_000_000_000;base_tick=0;base_time=Time_ns.Option.none;thread_table=Int.Table.create();string_table=Int.Table.create();process_names=Int.Table.create();thread_names=Thread_kernel_object.Table.create();warnings={num_unparsed_records=0;num_unparsed_args=0}};;exceptionTicks_too_largeexceptionInvalid_tick_rateexceptionInvalid_recordexceptionString_not_foundexceptionThread_not_foundletconsume_int32_exniobuf=ifIobuf.lengthiobuf<4thenraiseInvalid_recordelseIobuf.Consume.int32_leiobuf;;(* Many things don't use the most significant bit of their word so we can safely use a
normal OCaml 63 bit int to parse them. *)letconsume_int64_exniobuf=ifIobuf.lengthiobuf<8thenraiseInvalid_recordelseIobuf.Consume.int64_le_trunciobuf;;letconsume_int64_t_exniobuf=ifIobuf.lengthiobuf<8thenraiseInvalid_recordelseIobuf.Consume.int64_t_leiobuf;;letconsume_tail_padded_string_exniobuf~len=ifIobuf.lengthiobuf<lenthenraiseInvalid_recordelseIobuf.Consume.tail_padded_fixed_string~padding:Char.min_value~leniobuf;;letadvance_iobuf_exniobuf~by:len=ifIobuf.lengthiobuf<lenthenraiseInvalid_recordelseIobuf.advanceiobuflen;;let[@inline]extract_fieldword~pos~size=(wordlsrpos)land((1lslsize)-1)(* Because the format guarantees aligned 64-bit words, some things need to be padded to
8 bytes. This is an efficient expression for doing that. *)letpadding_to_wordx=-xland(8-1)(* Method for converting a tick count to nanoseconds taken from the Perfetto source code.
Raises [Ticks_too_large] if the result doesn't fit in an int63.
This implements a kind of elementary school long multiplication to handle larger
values without overflowing in the intermediate steps or losing precision. We do
this complicated method instead of just using floats because it's nice if our
tools don't lose precision if a ticks value is an absolute [Time_ns.t], even if
those traces won't work perfectly in the Perfetto web UI. *)letticks_to_nsticks~ticks_per_sec=letticks_hi=tickslsr32inletticks_lo=ticksland((1lsl32)-1)inletns_per_sec=1_000_000_000in(* Calculating [result_hi] can overflow, so we check for that case. *)letresult_hi=ticks_hi*((ns_per_seclsl32)/ticks_per_sec)inifticks_hi<>0&&result_hi/ticks_hi<>(ns_per_seclsl32)/ticks_per_secthenraiseTicks_too_large;(* Calculating [result_lo] can't overflow since [ticks_lo * ns_per_sec] is less than
2^62. *)letresult_lo=ticks_lo*ns_per_sec/ticks_per_secin(* Adding [result_lo + result_hi] can overflow. *)letresult=result_lo+result_hiinifresult<0thenraiseTicks_too_large;result;;letevent_tick_to_spanttick=letticks_elapsed=tick-t.base_tickinletticks_ns=ticks_to_nsticks_elapsed~ticks_per_sec:t.ticks_per_secondinTime_ns.Span.of_int_nsticks_ns;;letlookup_string_exnt~index=ifindex=0then""else(tryInt.Table.find_exnt.string_tableindexwith|_->raiseString_not_found);;letlookup_thread_exnt~index=tryInt.Table.find_exnt.thread_tableindexwith|_->raiseThread_not_found;;(* Extracts a 16-bit string index. Will raise if the string index isn't in the string
table or if attempting to read an inline string reference.
Since inline string references have their highest bit set to 1 and use the lower 15
bits to indicate the length of the string stream, the value will always be >= 32768.
Values >= 32768 will never be in the string table because in [parse_string_record],
we only write strings to indices [1, 32767]. *)let[@inline]extract_string_indextword~pos=letindex=extract_fieldword~pos~size:16in(* raise an exception if the string is not in the string table *)lookup_string_exnt~index|>(ignore:string->unit);index;;(* Extracts an 8-bit thread index. Will raise if the thread index isn't in the
thread table. *)let[@inline]extract_thread_indextword~pos=letindex=extract_fieldword~pos~size:8in(* raise an exception if the thread is not in the thread table *)lookup_thread_exnt~index|>(ignore:Thread.t->unit);index;;let[@inline]consume_tickt=letticks=consume_int64_exnt.cur_recordin(* We raise [Ticks_too_large] in case a bug (e.g. overflow) caused the ticks value to
become negative when converted to a 63-bit OCaml int. *)ifticks<0thenraiseTicks_too_large;ticks;;letparse_metadata_recordt=letheader=consume_int64_exnt.cur_recordinletmtype=extract_fieldheader~pos:16~size:4inmatchmtypewith|1(* Provider info metadata *)->letprovider_id=extract_fieldheader~pos:20~size:32inletname_len=extract_fieldheader~pos:52~size:8inletpadding=padding_to_wordname_leninletprovider_name=consume_tail_padded_string_exnt.cur_record~len:(name_len+padding)inInt.Table.sett.provider_name_by_id~key:provider_id~data:provider_name;t.current_provider<-Someprovider_id|2(* Provider section metadata *)->letprovider_id=extract_fieldheader~pos:20~size:32int.current_provider<-Someprovider_id|4(* Trace info metadata *)->lettrace_info_type=extract_fieldheader~pos:20~size:4inlettrace_info=extract_fieldheader~pos:24~size:32in(* Check for magic number record *)ifnot(trace_info_type=0&&trace_info=0x16547846)thent.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1|_->(* Unsupported metadata type *)t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;;letparse_initialization_recordt=letheader=consume_int64_exnt.cur_recordinletrsize=extract_fieldheader~pos:4~size:12inletticks_per_second=consume_int64_exnt.cur_recordinifticks_per_second<=0thenraiseInvalid_tick_rate;t.ticks_per_second<-ticks_per_second;(* By default, initialization records have size = 2. This checks for the extended
initialization record that has two extra words. *)ifrsize=4then(letbase_tick=consume_ticktinletbase_time_in_ns=consume_int64_exnt.cur_recordinletbase_time=Time_ns.of_int_ns_since_epochbase_time_in_ns|>Time_ns.Option.someint.base_tick<-base_tick;t.base_time<-base_time;Record.Tick_initialization{ticks_per_second;base_time})elseRecord.Tick_initialization{ticks_per_second;base_time=Time_ns.Option.none};;(* Reads a zero-padded string and stores it at the associated 15-bit index (from 1 to
32767) in the string table. *)letparse_string_recordt=letheader=consume_int64_exnt.cur_recordinletstring_index=extract_fieldheader~pos:16~size:15in(* Index 0 is used to denote the empty string. The spec mandates that string records
which attempt to define it anyways be ignored. *)ifstring_index=0thenNoneelse(letstr_len=extract_fieldheader~pos:32~size:15inletpadding=padding_to_wordstr_leninletinterned_string=consume_tail_padded_string_exnt.cur_record~len:(str_len+padding)inInt.Table.sett.string_table~key:string_index~data:interned_string;Some(Record.Interned_string{index=string_index;value=interned_string}));;(* Reads a PID and TID and stores them at the associated 8-bit index (from 1 to 255) in
the thread table. *)letparse_thread_recordt=letheader=consume_int64_exnt.cur_recordinletthread_index=extract_fieldheader~pos:16~size:8in(* Index 0 is reserved for inline thread refs, sets to it must be ignored. *)ifthread_index=0thenNoneelse(letprocess_koid=consume_int64_exnt.cur_recordinletthread_koid=consume_int64_exnt.cur_recordinletthread={Thread.pid=process_koid;tid=thread_koid;process_name=Int.Table.findt.process_namesprocess_koid;thread_name=Thread_kernel_object.Table.findt.thread_names(process_koid,thread_koid)}inInt.Table.sett.thread_table~key:thread_index~data:thread;Some(Record.Interned_thread{index=thread_index;value=thread}));;letrecparse_args?(args=[])t~num_args=ifnum_args=0thenList.revargselse(letheader_low_word=consume_int32_exnt.cur_recordinletarg_type=extract_fieldheader_low_word~pos:0~size:4inletrsize=extract_fieldheader_low_word~pos:4~size:12inletarg_name=extract_string_indextheader_low_word~pos:16in(* The Fuchsia spec says the upper 32-bits of the header are reserved for future
extensions, and should just be ignored if they aren't used. *)letheader_high_word=consume_int32_exnt.cur_recordinlet(args:Event_arg.tlist)=matcharg_typewith(* arg_type 0 is a null argument with no value. We never write these so we just
collapse them into an Int with value zero. *)|0|1->(arg_name,Intheader_high_word)::args|3->letvalue=consume_int64_exnt.cur_recordin(arg_name,Intvalue)::args|5->letvalue_as_int64=consume_int64_t_exnt.cur_recordinletvalue=Int64.float_of_bitsvalue_as_int64in(arg_name,Floatvalue)::args|6->letvalue=extract_string_indextheader_high_word~pos:0in(arg_name,Stringvalue)::args|_->(* Advance [rsize - 1] words to the next argument after reading the header word. *)advance_iobuf_exnt.cur_record~by:(8*(rsize-1));(* Unsupported argument types: unsigned integers, pointers, kernel IDs *)t.warnings.num_unparsed_args<-t.warnings.num_unparsed_args+1;argsinparse_argst~num_args:(num_args-1)~args);;letparse_kernel_object_recordt=letheader=consume_int64_exnt.cur_recordinletobj_type=extract_fieldheader~pos:16~size:8inletname=extract_string_indextheader~pos:24inletname_str=lookup_string_exnt~index:nameinletnum_args=extract_fieldheader~pos:40~size:4inmatchobj_typewith|1(* process *)->letkoid=consume_int64_exnt.cur_recordinInt.Table.sett.process_names~key:koid~data:name_str;(* Update the name of any matching process in the process table. *)Int.Table.itert.thread_table~f:(funthread->ifthread.pid=koidthenthread.process_name<-Somename_str);ifnum_args>0thent.warnings.num_unparsed_args<-t.warnings.num_unparsed_args+num_args;Some(Record.Process_name_change{name;pid=koid})|2(* thread *)->letkoid=consume_int64_exnt.cur_recordinifnum_args>0then((* We expect the first arg to be a koid argument named "process". *)letarg_header=consume_int32_exnt.cur_recordinletarg_type=extract_fieldarg_header~pos:0~size:4inletarg_name_ref=extract_string_indextarg_header~pos:16inletarg_name=lookup_string_exnt~index:arg_name_refinifarg_type=8&&String.(=)arg_name"process"then(consume_int32_exnt.cur_record|>(ignore:int->unit);letprocess_koid=consume_int64_exnt.cur_recordinThread_kernel_object.Table.sett.thread_names~key:(process_koid,koid)~data:name_str;(* Update the name of any matching thread in the thread table. *)Int.Table.itert.thread_table~f:(funthread->ifthread.pid=process_koid&&thread.tid=koidthenthread.thread_name<-Somename_str);(* Mark any remaining arguments as unparsed. *)t.warnings.num_unparsed_args<-t.warnings.num_unparsed_args+(num_args-1);Some(Record.Thread_name_change{name;pid=process_koid;tid=koid}))else(t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;None))else(t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;None)|_->(* The record contains an unsupported kernel object type. *)t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;None;;letparse_event_recordt=(* Parse the header in two 32-bit pieces so that we can avoid allocating despite it
being possible the most significant bit is one. *)letheader_lower=consume_int32_exnt.cur_recordinletev_type=extract_fieldheader_lower~pos:16~size:4inletnum_args=extract_fieldheader_lower~pos:20~size:4inletthread=extract_thread_indextheader_lower~pos:24inletheader_upper=consume_int32_exnt.cur_recordinletcategory=extract_string_indextheader_upper~pos:0inletname=extract_string_indextheader_upper~pos:16inlettimestamp_tick=consume_ticktinletargs=parse_argst~num_argsinletevent_type:Event_type.toption=matchev_typewith|0->SomeInstant|1->letcounter_id=consume_int64_exnt.cur_recordinSome(Counter{id=counter_id})|2->SomeDuration_begin|3->SomeDuration_end|4->letend_time_tick=consume_int64_exnt.cur_recordinSome(Duration_complete{end_time=event_tick_to_spantend_time_tick})|8->letflow_correlation_id=consume_int64_exnt.cur_recordinSome(Flow_begin{flow_correlation_id})|9->letflow_correlation_id=consume_int64_exnt.cur_recordinSome(Flow_step{flow_correlation_id})|10->letflow_correlation_id=consume_int64_exnt.cur_recordinSome(Flow_end{flow_correlation_id})(* Unsupported event type: Async begin, instant or end *)|_->Noneinmatchevent_typewith|Someevent_type->lettimestamp=event_tick_to_spanttimestamp_tickinletevent={Event.timestamp;thread;category;name;arguments=args;event_type}inSome(Record.Eventevent)|None->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;None;;(* This function advances through the trace until it finds a Fuschia record matching one
of the records types defined in [Record.t]. *)letrecparse_until_next_external_recordt=ifIobuf.lengtht.iobuf<8thenraiseEnd_of_file;letheader=Iobuf.Peek.int64_le_trunct.iobuf~pos:0inletrtype=extract_fieldheader~pos:0~size:4inletrsize=(* large blob records use a larger length field *)ifrtype=15thenextract_fieldheader~pos:4~size:32elseextract_fieldheader~pos:4~size:12inletrlen=8*rsizein(* We raise an exception if the current record is split across two iobufs. Subsequent
calls to parse will attempt to parse this record again. *)ifIobuf.lengtht.iobuf<rlenthenraiseEnd_of_file;Iobuf.Expert.set_bounds_and_buffer_sub~pos:0~len:rlen~src:t.iobuf~dst:t.cur_record;(* Because this happens before parsing, errors thrown when parsing will cause subsequent
calls to parse to begin with the next record, allowing skipping invalid records. *)Iobuf.advancet.iobufrlen;letrecord=matchrtypewith|0(* Metadata record *)->parse_metadata_recordt;None|1(* Initialization record *)->Some(parse_initialization_recordt)|2(* String record *)->parse_string_recordt|3(* Thread record *)->parse_thread_recordt|4(* Event record *)->parse_event_recordt|7(* Kernel object record *)->parse_kernel_object_recordt|_(* Unsupported record type *)->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Noneinmatchrecordwith|Somerecord->record|None->parse_until_next_external_recordt;;letparse_nextt=tryletrecord=parse_until_next_external_recordtinResult.returnrecordwith|End_of_file->Result.failParse_error.No_more_words|Ticks_too_large->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Result.failParse_error.Timestamp_too_large|Invalid_tick_rate->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Result.failParse_error.Invalid_tick_initialization|Invalid_record->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Result.failParse_error.Invalid_size_on_record|String_not_found->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Result.failParse_error.Invalid_string_ref|Thread_not_found->t.warnings.num_unparsed_records<-t.warnings.num_unparsed_records+1;Result.failParse_error.Invalid_thread_ref;;