元件通常不會一直運作。大多數元件都以非同步方式編寫,也就是說,它們通常會等待下一個 FIDL 訊息。不過,這些元件會占用記憶體。本指南說明如何調整元件,讓元件在閒置時主動停止並釋出資源。
總覽
並發生以下情況:
您將對元件的程式碼進行一些變更,讓元件可以決定何時停止。元件會在停止前保留狀態和控制代碼。保留這類資料稱為「託管」。
元件的用戶端不會知道元件已停止。以這種方式停止元件,不會中斷與元件的 FIDL 連線。
Fuchsia 提供的程式庫可讓您監控 FIDL 連線和傳出目錄連線何時閒置,並在發生這種情況時將這些連線轉回控制代碼。
元件架構提供 API,可讓元件儲存控制代碼和資料,並在下次執行時擷取這些資料,通常是在控制代碼可讀取或收到新的能力要求時。我們會在後續章節詳細說明運作方式。
Fuchsia 快照和 Cobalt 資訊主頁會提供實用的生命週期指標。
哪些元件適合遷移?
建議您查看具有下列特性的元件:
流量不穩定。元件可以啟動並處理這些流量,然後在完成後返回停止狀態。啟動和更新路徑中的許多元件只有在這些時候才需要,但其他時候則會閒置並浪費 RAM,例如
core/system-update/system-updater。不會過度依賴狀態。您可以在元件停止前保留狀態。在極限情況下,我們可以編寫程式碼來保留所有重要狀態。在實務上,我們會在節省記憶體和保存必要狀態的複雜度之間做出取捨。
記憶體用量偏高。使用
ffx profile memory查看元件的記憶體用量。舉例來說,在一般系統上,console-launcher.cm使用732 KiB的私人記憶體。私人記憶體是只有該元件參照的記憶體,因此我們保證在停止該元件時,至少會釋出該數量的記憶體。請參閱「評估記憶體用量」。Process name: console-launcher.cm Process koid: 2222 Private: 732 KiB PSS: 1.26 MiB (Proportional Set Size) Total: 3.07 MiB (Private + Shared unscaled)
http-client.cm 是一個範例元件,不會在 HTTP 載入器連線中保留狀態,且僅用於上傳指標和當機資料。因此,我們已調整這項功能,只要設定完成,就會在閒置時停止。
已知限制
Hanging-get:如果元件是 Hanging-get FIDL 方法的伺服器或用戶端,則很難保留該連線,因為 FIDL 繫結無法儲存及還原進行中呼叫的相關資訊。您可以將該 FIDL 方法轉換為事件和單向確認。
目錄:如果元件提供目錄通訊協定,則很難保留該連線,因為目錄通常由 VFS 程式庫提供。VFS 程式庫目前不公開取回基礎管道和相關聯狀態 (例如搜尋指標) 的方法。
只要有充分理由,這些都能獲得支援。您可以與元件架構團隊聯絡,說明您的用途。
偵測閒置狀態
如要停止閒置元件,第一步是強化該元件的程式碼,以便瞭解元件何時進入閒置狀態,也就是:
FIDL 連線處於閒置狀態:元件通常會宣告多個 FIDL 通訊協定能力,用戶端會在需要時連線至這些通訊協定。這些連線不應有需要元件處理的待處理訊息。
傳出目錄閒置:元件會提供傳出目錄,發布傳出功能。不應有代表此元件能力要求的待處理訊息,且除了
component_manager建立的連線外,不應有其他連線連至傳出目錄。其他背景商業邏輯:舉例來說,如果元件在背景發出網路要求以回應 FIDL 方法,除非該網路要求已完成,否則我們可能不會將該元件視為閒置。如果該元件在要求處理期間停止,可能會有安全疑慮。
我們有 Rust 程式庫,可偵測各種情況下的閒置狀態。https://fxbug.dev/332342122 追蹤 C++ 元件的相同功能。
偵測閒置的 FIDL 連線
您可以使用 detect_stall::until_stalled,將 Rust FIDL 要求串流轉換為在連線閒置超過指定逾時時間時,自動解除繫結 FIDL 端點的串流。您必須將元件新增至 src/lib/detect-stall/BUILD.gn 的瀏覽權限清單。詳情請參閱 API 文件和測試。http-client.cm 的使用方式如下:
async fn loader_server(
stream: net_http::LoaderRequestStream,
idle_timeout: fasync::Duration,
) -> Result<(), anyhow::Error> {
// Transforms `stream` into another stream yielding the same messages,
// but may complete prematurely when idle.
let (stream, unbind_if_stalled) = detect_stall::until_stalled(stream, idle_timeout);
// Handle the `stream` as per normal.
stream.for_each_concurrent(None, |message| {
// Match on `message`...
}).await?;
// The `unbind_if_stalled` future will resolve if the stream was idle
// for `idle_timeout` or if the stream finished. If the stream was idle,
// it will resolve with the unbound server endpoint.
//
// If the connection did not close or receive new messages within the
// timeout, send it over to component manager to wait for it on our behalf.
if let Ok(Some(server_end)) = unbind_if_stalled.await {
// Escrow the `server_end`...
}
}
偵測閒置的傳出目錄
您可以使用
fuchsia_component::server::ServiceFs::until_stalled 方法,
將 ServiceFs 轉換為在檔案系統中沒有工作時,自動解除繫結傳出目錄伺服器
端點的 。詳情請參閱 API 文件和測試。http-client.cm 的使用方式如下:
#[fuchsia::main]
pub async fn main() -> Result<(), anyhow::Error> {
// Initialize a `ServiceFs` and add services as per normal.
let mut fs = ServiceFs::new();
let _: &mut ServiceFsDir<'_, _> = fs
.take_and_serve_directory_handle()?
.dir("svc")
.add_fidl_service(HttpServices::Loader);
// Chain `.until_stalled()` before calling `.for_each_concurrent()`.
// This wraps each item in the `ServiceFs` stream into an enum of either
// a capability request, or an `Item::Stalled` message containing the
// outgoing directory server endpoint if the filesystem became idle.
fs.until_stalled(idle_timeout)
.for_each_concurrent(None, |item| async {
match item {
Item::Request(services, _active_guard) => {
let HttpServices::Loader(stream) = services;
loader_server(stream, idle_timeout).await;
}
Item::Stalled(outgoing_directory) => {
// Escrow the `outgoing_directory`...
}
}
})
.await;
}
等待其他背景商業邏輯
ServiceFs 產生 Item::Stalled 訊息後,就不會再產生更多能力要求。如果背景工作導致元件無法停止,但 ServiceFs 同時進入閒置狀態,並提早解除繫結傳出目錄端點,就可能發生問題。如要處理這類情況,您可以防止 ServiceFs 進入閒置狀態。ServiceFs 產生的 Item::Request 包含 ActiveGuard。只要範圍內有有效的防護機制,ServiceFs 就不會進入閒置狀態,並會持續產生收到的能力要求。
同樣地,您可以建立 ExecutionScope,產生與處理 FIDL 連線相關的所有背景工作,並呼叫 ExecutionScope::wait() 等待工作完成。舉例來說,http-client.cm 中的 loader_server 函式在背景工作完成前不會傳回,這會使 Item::Request 中的 active_guard 保持在範圍內,防止 ServiceFs 停止。
將控點和狀態託管給架構
連線閒置且程式庫提供未繫結的伺服器端點後,下一步是託管這些控制代碼,也就是將控制代碼傳送至元件架構妥善保管。
無狀態通訊協定
注意:通訊協定必須標示為
@discoverable,才能支援託管。這樣一來,元件從閒置狀態喚醒後,就能重新連線至管道。
部分 FIDL 連線不會傳輸狀態。無論是透過相同連線或不同連線傳送,每項要求的功能都相同。如要建立這類連結,請按照下列步驟操作:
如果尚未在元件資訊清單中宣告功能,請先完成這項操作。如果這個通訊協定連線衍生自其他連線,且通常不會從傳出目錄提供,您可能需要宣告這項能力。
宣告能力時,請新增
delivery: "on_readable"。接著,架構會監控新連線要求在伺服器端點上的可讀取信號,並在有待處理訊息時,將伺服器端點連線至供應器元件。範例:capabilities: [ { protocol: "fuchsia.net.http.Loader", delivery: "on_readable", }, ],從
self為能力新增使用宣告,讓程式可以從傳入命名空間連線到該功能。您可以在/escrow目錄中安裝能力,以便與元件使用的其他功能區分。範例:{ protocol: "fuchsia.net.http.Loader", from: "self", path: "/escrow/fuchsia.net.http.Loader", },從傳入命名空間連線至能力,並傳遞
detect_stalled::until_stalled的未繫結伺服器端點。if let Ok(Some(server_end)) = unbind_if_stalled.await { // This will open `/escrow/fuchsia.net.http.Loader` and pass the server // endpoint obtained from the idle FIDL connection. fuchsia_component::client::connect_channel_to_protocol_at::<net_http::LoaderMarker>( server_end.into(), "/escrow", )?; }
整體而言,這表示元件架構會監控閒置連線,直到連線再次可讀取,然後在發生這種情況時將該能力傳回元件。如果元件已停止,這項操作會啟動元件。
傳出目錄
我們必須使用不同的 API 來託管主要傳出目錄連線 (即 Item::Stalled 中 ServiceFs 傳回的連線),因為該伺服器端點是所有其他連線連至元件的進入點。如果是 ELF 元件,您可以透過 fuchsia.process.lifecycle/Lifecycle.OnEscrow FIDL 事件將傳出目錄傳送至架構:
將
lifecycle: { stop_event: "notify" }新增至元件.cml:program: { runner: "elf", binary: "bin/http_client", lifecycle: { stop_event: "notify" }, },取得生命週期編號控制代碼,將其轉換為 FIDL 要求串流,然後使用
send_on_escrow傳送事件:let lifecycle = fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::Lifecycle, 0)).unwrap(); let lifecycle: zx::Channel = lifecycle.into(); let lifecycle: ServerEnd<flifecycle::LifecycleMarker> = lifecycle.into(); let (lifecycle_request_stream, lifecycle_control_handle) = lifecycle.into_stream_and_control_handle(); let outgoing_dir = None; // Later, when `ServiceFs` has stalled and we have an `outgoing_dir`. lifecycle_control_handle .send_on_escrow(flifecycle::LifecycleOnEscrowRequest { outgoing_dir, ..Default::default() }) .unwrap();元件傳送
OnEscrow事件後,就無法監控更多能力要求。因此,它應該在之後立即退出。下次執行時,元件會在啟動資訊中取回與上次執行時傳送的相同outgoing_dir控制代碼。如要瞭解如何整合這些項目,請參閱
http-client。
檢查資料
元件停止後,系統就不會再提供其 Inspect VMO。不過,您可以使用架構託管凍結的 Inspect 資料寫入時複製版本。這樣一來,即使元件停止運作,診斷工具仍可讀取元件檢查樹狀結構的最後狀態。
inspect_runtime::publish 函式會傳回 PublishedInspectController。在元件停止前,您可以對這個控制器呼叫 escrow_frozen 方法。這會為架構提供元件 Inspect VMO 的凍結副本。
以下是使用範例:
// When initializing your component...
let inspector = finspect::Inspector::default();
let inspect_controller =
inspect_runtime::publish(&inspector, inspect_runtime::PublishOptions::default());
// ...
// When your component is about to stop...
if let Some(controller) = inspect_controller {
let frozen_inspect_token = controller
.escrow_frozen(inspect_runtime::EscrowOptions::default())
.await;
// The token can be stored in the escrowed dictionary if needed,
// though often just escrowing is sufficient.
}
有狀態通訊協定和其他重要狀態
fuchsia.process.lifecycle/Lifecycle.OnEscrow 事件會採用另一個引數,即 escrowed_dictionary_handle zx.Handle:EVENTPAIR,這是 Dictionary 物件的參照。字典是鍵/值對應,可保存資料或功能。
您可以使用架構中的
fuchsia.component.runtime.Capabilities建立Dictionary(例如透過 Rust 中的fuchsia_component::runtime::Dictionary::new):use: [ { protocol: "fuchsia.component.runtime.Capabilities", from: "framework", } ]let dictionary = Dictionary::new().await;您可以對
Dictionary呼叫insert,將部分資料 (例如位元組向量) 新增至Dictionary。如需其他方法,請參閱fuchsia.component.runtimeFIDL 程式庫說明文件:let bytes = vec![1, 2, 3]; let data = Data::new(DataValue::Bytes(bytes)).await; dictionary.insert("my_data", data).await;在結束前,請在
send_on_escrow中傳送Dictionary控制代碼:lifecycle_control_handle.send_on_escrow(flifecycle::LifecycleOnEscrowRequest { outgoing_dir, escrowed_dictionary_handle: Some(dictionary.handle), ..Default::default() })?;下次啟動時,您可以從啟動控制代碼取得這個字典:
let Some(dictionary) = fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::EscrowedDictionary, 0)) else { return Err(anyhow!("Couldn't find startup handle")); }; let dictionary = Dictionary::from(zx::EventPair::from(dictionary)); let Some(Capability::Data(data)) = dictionary.remove("my_data").await else { return Err(anyhow!("Bad capability type from dictionary")); }; let DataValue::Bytes(bytes) = data.get_value().await else { return Err(anyhow!("Bad data value from dictionary")); }; // Do something with the data...
Dictionary 物件支援多種項目資料類型。如果元件的狀態小於 fuchsia.component.runtime/MAX_DATA_LENGTH,建議您儲存 fuchsia.component.runtime/Data 項目,該項目可保留位元組向量。
我想等待管道可供讀取
在停止之前,如果您想安排元件架構等待管道可讀取,然後將管道傳回元件,可以使用相同的 delivery: "on_readable" 技術。這項原則也適用於元件未公開的 FIDL 通訊協定,例如服務成員。甚至支援不使用 FIDL 通訊協定的管道。舉例來說,假設元件含有 Zircon 例外狀況管道,且需要指示架構等待該管道可供讀取,然後啟動元件,則可宣告下列 .cml:
capabilities: [
{
protocol: "exception_channel",
delivery: "on_readable",
path: "/escrow/exception_channel",
},
],
use: [
{
protocol: "exception_channel",
from: "self",
path: "/escrow/exception_channel",
}
]
請注意,exception_channel 能力不會公開。這項能力由元件本身使用。元件可能會從傳入的命名空間開啟 /escrow/exception_channel,並指定要等待的管道。當該管道可讀取時,架構會在傳出目錄中開啟 /escrow/exception_channel,並視需要啟動元件。總之,您可以宣告功能,並從 self 使用這些功能,將控制代碼託管至 component_manager。
如果您需要其他類型的觸發條件,例如等待自訂信號或等待計時器,請與元件架構團隊聯絡。
測試
建議您強化現有的整合測試,一併測試元件是否能自行停止並重新啟動,而不會中斷 FIDL 連線。如果您已有整合測試,可啟動元件並傳送 FIDL 要求,則可使用元件事件比對器,驗證元件在沒有訊息時是否會停止。如需操作範例,請參閱 http-client 測試。
到達網頁和指標
如要針對特定產品最佳化這個元件,可以在元件中加入結構化設定,控管是否要設定閒置逾時,以及逾時時間長度。
元件架構會記錄元件在執行期間啟動和停止的時間,並上傳至 Cobalt。您可以在這個資訊主頁查看這些指標,微調閒置逾時設定。
擷取意見回饋快照時 (例如在欄位中遇到錯誤時),初始和最新元件執行的時間戳記會分別顯示在選取器 <component_manager>:root/lifecycle/early 和 <component_manager>:root/lifecycle/late。您可以將這些事件與其他錯誤記錄建立關聯,協助調查錯誤是否因元件不當停止而造成。