第 25 章 使用執行時期事件進行執行時期追蹤

本章介紹執行時期事件追蹤系統,該系統能夠以極低的開銷從 OCaml 執行時期持續提取效能資訊。該系統和介面是底層的,且與執行時期的實作緊密耦合,目的是讓終端使用者依賴工具來消耗和視覺化感興趣的資料。

發出的資料包括

可以宣告和消耗其他事件,為 OCaml 函式庫提供更高等級的監控功能。它們被稱為自訂事件

1 概述

執行時期事件系統發出三種主要類型的事件

跨度
在一段時間內發生的事件。例如,當 OCaml 垃圾收集器中開始進行次要收集時,執行時期事件追蹤系統會發出一個跨度事件,當收集完成時結束。跨度可以包含其他跨度,例如,在次要收集開始後且結束前可能會發出其他跨度事件。
生命週期事件
在某個時間點發生的事件。例如,當網域終止時,會發出相應的生命週期事件。
計數器
包含某些感興趣數量測量的事件。例如,在上次次要垃圾收集期間從次要堆積晉升到主要堆積的字數,會以計數器事件發出。

執行時期事件追蹤系統旨在用於不同的情境

自我監控
OCaml 程式和函式庫可以安裝自己的回呼函式來監聽執行時期事件並以程式方式對其做出反應,例如,將事件匯出到磁碟或透過網路。
外部監控
外部處理程序可以消耗 OCaml 程式的執行時期事件,該程式的執行時期追蹤系統已透過設定相應的環境變數啟用。

執行時期事件追蹤系統會將事件記錄到環形緩衝區。因此,舊事件會被新事件覆寫。消費者可以持續消耗事件,或選擇僅在某些情況下執行此操作,例如,如果特定查詢或操作的完成時間比預期的長。

2 架構

執行時期追蹤系統在概念上包含兩個部分:1) 發出事件的事件來源和 2) 攝取和傳輸這些事件的事件傳輸。

2.1 事件來源

事件來源是發出事件的點。在 OCaml 執行時期系統中,有多個此類來源。這些來源進一步分為兩組:1) 始終可用的事件來源和 2) 僅在檢測執行時期可用的事件來源。檢測執行時期中的事件來源主要引起 OCaml 執行時期和垃圾收集器開發人員的興趣,目前僅包含主要堆積分配大小計數器事件。

事件來源發出的完整事件集及其文件,可以在 模組 Runtime_events 中找到。

2.2 事件傳輸

系統的事件傳輸部分會攝取事件來源發出的事件,並使這些事件可供消費者使用。

環形緩衝區

事件使用稱為環形緩衝區的資料結構傳輸。此資料結構包含兩個指向線性後備陣列的指標,尾部指標指向可以寫入新事件的位置,而頭部指標指向緩衝區中可以讀取的最新事件。當後備陣列中沒有足夠的空間來寫入新事件時,頭部指標會向前移動,並且最舊的事件會被新事件覆寫。

執行時期事件中使用的環形緩衝區實作,一次最多只能由一個生產者寫入,但可以由多個消費者同時讀取,而無需生產者的協調。每個執行中的網域都有一個唯一的環形緩衝區,並且在網域終止時,環形緩衝區可能會重複用於新產生的網域。僅當啟用執行時期事件時才會分配環形緩衝區(包括主網域)。環形緩衝區本身儲存在記憶體對應的檔案中,該檔案以處理程序識別碼作為名稱,並以副檔名 .events 儲存,這使它們可以從主 OCaml 處理程序外部讀取。如需詳細資訊,請參閱 Runtime_events

消耗 API

執行時期事件追蹤系統提供 OCaml 和 C API,這些 API 是基於游標的,並由輪詢驅動。消耗事件的高階過程如下所示

  1. 透過 Runtime_events.create_cursor 為目前處理程序或外部處理程序(由路徑和 PID 指定)建立游標。
  2. 呼叫 Runtime_events.Callbacks.create 來註冊接收事件的回呼函式。
  3. 使用上一步中建立的回呼函式,透過 Runtime_events.read_poll 輪詢游標。對於環形緩衝區中的每個相符事件,都會呼叫提供的回呼函式。除了發出的事件之外,回呼函式還會獲得發射器網域的 ID 和發射時間戳記。

3 用法

3.1 使用 OCaml API

我們先從一個簡單的範例開始,該範例會印出執行時期事件追蹤系統發出的事件名稱、開始時間和結束時間

let runtime_begin _ ts phase =
    Printf.printf "Begin\t%s\t%Ld\n"
        (Runtime_events.runtime_phase_name phase)
        (Runtime_events.Timestamp.to_int64 ts)

let runtime_end _ ts phase =
    Printf.printf "End\t%s\t%Ld\n"
        (Runtime_events.runtime_phase_name phase)
        (Runtime_events.Timestamp.to_int64 ts)

let () =
    Runtime_events.start ();
    let cursor = Runtime_events.create_cursor None in
    let callbacks = Runtime_events.Callbacks.create ~runtime_begin ~runtime_end ()
    in
    while true do
        let list_ref = ref [] in (* for later fake GC work *)
        for _ = 1 to 100 do
            (* here we do some fake GC work *)
            list_ref := [];
            for _ = 1 to 10 do
                list_ref := (Sys.opaque_identity(ref 42)) :: !list_ref
            done;
            Gc.full_major ();
        done;
        ignore(Runtime_events.read_poll cursor callbacks None);
        Unix.sleep 1
    done

下一步是使用 runtime_events 函式庫編譯和連結程式。這可以透過以下方式完成

       ocamlopt -I +runtime_events -I +unix unix.cmxa runtime_events.cmxa
            example.ml -o example

當使用 dune 建置系統時,此範例可以透過以下方式建置

(executable
 (name example)
 (modules example)
 (libraries unix runtime_events))

執行範例的已編譯二進位檔會產生類似以下的輸出

Begin   explicit_gc_full_major  24086187297852
Begin   stw_leader      24086187298594
Begin   minor   24086187299404
Begin   minor_global_roots      24086187299807
End     minor_global_roots      24086187331461
Begin   minor_remembered_set    24086187331631
Begin   minor_finalizers_oldify 24086187544312
End     minor_finalizers_oldify 24086187544704
Begin   minor_remembered_set_promote    24086187544879
End     minor_remembered_set_promote    24086187606414
End     minor_remembered_set    24086187606584
Begin   minor_finalizers_admin  24086187606854
End     minor_finalizers_admin  24086187607152
Begin   minor_local_roots       24086187607329
Begin   minor_local_roots_promote       24086187609699
End     minor_local_roots_promote       24086187610539
End     minor_local_roots       24086187610709
End     minor   24086187611746
Begin   minor_clear     24086187612238
End     minor_clear     24086187612580
End     stw_leader      24086187613209
...

這是一個自我監控的範例,其中程式明確地開始監聽執行時期事件並監控自身。

對於外部監控,程式不需要知道執行時期事件的存在。執行時期事件可以透過環境變數 OCAML_RUNTIME_EVENTS_START 控制,該變數設定後,會導致執行時期追蹤系統在程式初始化時啟動。

我們可以從先前的範例中移除 Runtime_events.start ();,而是改為如下呼叫程式來產生相同的結果

OCAML_RUNTIME_EVENTS_START=1 ./example

環境變數

環境變數可用於控制執行時期事件追蹤系統的不同方面。可以使用以下環境變數

執行時期事件環形緩衝區的大小可以透過 OCAMLRUNPARAM 設定,如需更多資訊,請參閱 15.2 節。

使用檢測執行時期建置

某些事件僅由檢測執行時期發出。若要接收它們,OCaml 程式需要針對檢測執行時期進行編譯和連結。對於我們稍早的範例程式,可以透過以下方式達成

ocamlopt -runtime-variant i -I +runtime_events -I +unix unix.cmxa runtime_events.cmxa example.ml -o example

對於 dune

(executable
 (name example)
 (modules example)
 (flags "-runtime-variant=i")
 (libraries unix runtime_events))

3.2 使用工具

對事件的程式存取主要用於可觀測性函式庫和終端使用者工具的撰寫者。彈性的 API 可讓使用者將執行時期事件中的效能資料用於記錄和監控目的。

在本節中,我們將介紹 runtime_events_tools 套件中的數個公用程式,這些公用程式提供了從執行時期事件中提取和彙總資料的簡單方法。尤其是追蹤公用程式,會產生與 OCaml 4.12 至 4.14 中可用的先前的「eventlog」檢測系統相似的資料。

首先,在 OCaml 5.0+ opam 切換中安裝 runtime_events_tools

opam install runtime_events_tools

這應該會在您的路徑中安裝 olly 工具。您現在可以使用 trace 子指令為使用 OCaml 5.0+ 編譯的程式產生執行時期追蹤

olly trace trace.json 'your_program.exe .. args ..'

執行時期追蹤資料將以 json 追蹤事件格式產生到 trace.json。然後可以將其載入 Chrome 追蹤檢視器或 Perfetto 中,以視覺化收集到的追蹤。

測量 GC 延遲

olly 公用程式還包含一個 latency 子指令,該指令會消耗執行時期事件資料,並在程式完成時發出暫停持續時間的可剖析直方圖摘要。可以如下執行

olly latency 'your_program.exe .. args ..'

這應該會產生類似以下的輸出

GC latency profile:
#[Mean (ms):	2.46,	 Stddev (ms):	3.87]
#[Min (ms):	0.01,	 max (ms):	9.17]

Percentile 	 Latency (ms)
25.0000 	 0.01
50.0000 	 0.23
60.0000 	 0.23
70.0000 	 0.45
75.0000 	 0.45
80.0000 	 0.45
85.0000 	 0.45
90.0000 	 9.17
95.0000 	 9.17
96.0000 	 9.17
97.0000 	 9.17
98.0000 	 9.17
99.0000 	 9.17
99.9000 	 9.17
99.9900 	 9.17
99.9990 	 9.17
99.9999 	 9.17
100.0000 	 9.17

4 自訂事件

4.1 概述

執行時期事件系統支援定義自訂事件。這些事件以全域唯一的名稱識別,並帶有內建類型(unitintspan)和使用者定義類型的酬載發射。為了理解如何操作自訂事件,了解它們如何傳輸和儲存非常重要:它們的表示方式包含一個名稱字串(實際上,是所有自訂名稱陣列的索引)和任意位元組序列。自訂事件類型可以透過 Runtime_events.Type.register 函數,提供往返位元組序列的編碼和解碼函數來註冊

定義新的自訂事件(無論其酬載是內建類型還是自訂類型),都是透過另一個註冊函數 Runtime_events.User.register 完成的。此函數會記錄自訂事件的名稱、其類型和標籤之間的關聯。然後,在發射或使用自訂事件時會使用標籤;它充當事件名稱的簡寫。

一旦事件被註冊,該事件的值會使用 Runtime_events.User.write 發射。

總而言之,使用者要使用自訂類型發射和使用自訂事件,需要

  1. 註冊自訂類型
  2. 使用新的建構子擴展 Runtime_events.User.tag 變體
  3. 註冊新的自訂事件,將自訂事件名稱、新標籤和新的自訂類型綁定在一起
  4. 使用 Runtime_events.User.write 發射事件實例
  5. 指示一個回呼,該回呼應接收所有自訂類型的事件,使用 Runtime_events.Callbacks.add_user_eventRuntime_events.Callbacks.t 物件中註冊它,以便在輪詢時使用(請參閱上方第 ‍25.3.1 節)。
  6. (可選,如果存在以相同自訂類型註冊的不同事件:) 在該回呼中,使用事件標籤進行模式匹配,以便區分相同自訂類型的不同事件標籤。

請注意,如果發射器和使用者是不同的程式,則兩者都必須執行步驟 1 到 3 來註冊自訂事件和自訂事件類型(如果有的話)。請注意,標籤值在兩個程式中不需要相同;唯一應該匹配的值是名稱。

未註冊的使用者定義類型事件目前會被忽略。作為後備方案,可以使用未註冊的內建類型自訂事件,但它們都會被標記為 UNK(未知)。

因此,當收到事件時,使用者有三種情況

請注意,事件使用者將回呼綁定到事件類型,因此它們可以作為通用監聽器,解釋來自未註冊事件的酬載。因為這僅適用於內建類型的事件,所以成對發射事件會很有用:一個自訂類型的事件和一個內建類型的事件,以便在與通用追蹤工具保持相容的同時,設計一個專用的使用者。

4.2 追蹤自訂事件:範例

以下程式使用 Runtime_events.UserRuntime_events.Type 模組宣告兩個自訂事件,分別提供 spanint 值。tag 可擴展變體擴展了 CustomSpanCustomInt

type Runtime_events.User.tag += CustomSpan | CustomInt

let count_span =
    Runtime_events.User.register "count.span" CustomSpan
        Runtime_events.Type.span

let count_value =
    Runtime_events.User.register "count.value" CustomInt
        Runtime_events.Type.int

let count () =
    Runtime_events.User.write count_span Begin;
    for i = 1 to 5 do
        Runtime_events.User.write count_value i
    done;
    Runtime_events.User.write count_span End

let () =
    Runtime_events.start ();
    for _ = 1 to 3 do
        count ()
    done

在使用者端,可以使用提供的事件標籤和類型來匹配相關事件。為了完整起見,我們假設使用者是不同的程式,並重複定義新的標籤建構子,以及註冊自訂事件。

type Runtime_events.User.tag += CustomSpan | CustomInt

let count_span =
    Runtime_events.User.register "count.span" CustomSpan
        Runtime_events.Type.span

let count_value =
    Runtime_events.User.register "count.value" CustomInt
        Runtime_events.Type.int

let span_event_handler domain_id ts event value =
    (* we're only interested in our CustomSpan event *)
    match Runtime_events.User.tag event, value with
    | CustomSpan, Runtime_events.Type.Begin -> Printf.printf "> count begin\n"
    | CustomSpan, End -> Printf.printf "< count end\n"
    | _ -> ()

let int_event_handler domain_id ts event value =
    (* we're only interested in our CustomInt event *)
    match Runtime_events.User.tag event with
    | CustomInt -> Printf.printf "| count %d\n" value
    | _ -> ()

let () =
    let open Runtime_events in
    let cursor = create_cursor None in
    let callbacks =
        Callbacks.create ()
        |> Callbacks.add_user_event Type.span span_event_handler
        |> Callbacks.add_user_event Type.int int_event_handler
    in
    for _ = 0 to 100 do
        ignore(read_poll cursor callbacks None)
    done

給出以下輸出

> count begin
| count 1
| count 2
| count 3
| count 4
| count 5
< count end
> count begin
| count 1
| count 2
[...]