元件通常不會一直執行工作。大多數元件都是以非同步方式編寫,也就是說,它們通常會等待下一個 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 載入器連線中保留狀態,且只用於指標和當機上傳。因此,我們已調整這項功能,讓它在閒置時停止運作。
已知限制
檢查:如果元件透過檢查功能發布診斷資訊,這些資訊會在元件停止時遭到捨棄。https://fxbug.dev/339076913 會追蹤檢查資料,即使在元件停止後也一樣。
掛起的 get 方法:如果元件是掛起的 get FIDL 方法的伺服器或用戶端,則很難保留該連線,因為 FIDL 繫結無法儲存及還原有關進行中呼叫的資訊。您可以將該 FIDL 方法轉換為事件和單向 ACK。
目錄:如果元件提供目錄通訊協定,則很難保留該連線,因為目錄通常是由 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
轉換為在檔案系統中沒有工作時,自動解除繫結傳出目錄伺服器端點的 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
函式會在背景工作完成後才傳回,這會反過來讓 active_guard
保留在 Item::Request
的範圍內,防止 ServiceFs
停止。
將託管的控制項和狀態傳送至架構
連線閒置且程式庫已提供未繫結的伺服器端點後,下一個步驟就是將這些句柄託管,也就是將這些句柄傳送至元件架構以便保管。
無狀態通訊協定
部分 FIDL 連線不會攜帶狀態。無論是透過相同連線還是個別連線傳送,每項要求都會以相同方式運作。您可以按照下列步驟處理這些連線:
在元件資訊清單中宣告能力 (如果尚未宣告的話)。如果這個通訊協定連線是從其他連線衍生,且通常不會從傳出目錄提供,您可能需要宣告這項能力。
宣告能力時新增
delivery: "on_readable"
。您需要將元件新增至tools/cmc/build/restricted_features/BUILD.gn
的delivery_type
可見度清單。接著,架構會監控新連線要求的伺服器端點上可讀取的信號,並在有待處理訊息時,將伺服器端點連線至提供者元件。範例: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
。
具狀態的通訊協定和其他重要狀態
fuchsia.process.lifecycle/Lifecycle.OnEscrow
事件會採用另一個引數,即 escrowed_dictionary client_end:fuchsia.component.sandbox.Dictionary
,這是 Dictionary
物件的參照。字典是可能儲存資料或功能的鍵/值對應項目。
您可以使用架構中的
fuchsia.component.sandbox.CapabilityStore
,並在Factory
通訊協定上呼叫DictionaryCreate
,藉此建立DictionaryRef
:use: [ { protocol: "fuchsia.component.sandbox.CapabilityStore", from: "framework", } ]
let capability_store = fuchsia_component::client::connect_to_protocol::< fidl_fuchsia_component_sandbox::CapabilityStoreMarker, >() .unwrap(); let id_generator = sandbox::CapabilityIdGenerator::new(); let dictionary_id = id_generator.next(); capability_store.dictionary_create(dictionary_id).await?.map_err(to_err)?;
您可以對
Dictionary
呼叫Insert
,將某些資料 (例如位元組向量) 新增至Dictionary
。如需其他方法,請參閱fuchsia.component.sandbox
FIDL 程式庫說明文件:let bytes = vec![1, 2, 3]; let data_id = id_generator.next(); capability_store .import(data_id, fsandbox::Capability::Data(fsandbox::Data::Bytes(bytes))) .await? .map_err(to_err)?; capability_store .dictionary_insert( dictionary_id, &fsandbox::DictionaryItem { key: "my_data".to_string(), value: data_id }, ) .await? .map_err(to_err)?; let fsandbox::Capability::Dictionary(dictionary_ref) = capability_store.export(dictionary_id).await?.map_err(to_err)? else { panic!("Bad export"); };
在結束前,請在
send_on_escrow
中傳送Dictionary
用戶端端點:lifecycle_control_handle.send_on_escrow(flifecycle::LifecycleOnEscrowRequest { outgoing_dir: outgoing_dir, escrowed_dictionary: Some(dictionary_ref), ..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 dict_id = id_generator.next(); capability_store .import( dict_id, fsandbox::Capability::Dictionary(fsandbox::DictionaryRef { token: dictionary.into() }), ) .await? .map_err(to_err)?; let capability_id = id_generator.next(); capability_store .dictionary_remove( dict_id, "my_data", Some(&fsandbox::WrappedNewCapabilityId { id: capability_id }), ) .await? .map_err(to_err)?; let fsandbox::Capability::Data(data) = capability_store.export(capability_id).await?.map_err(to_err)? else { return Err(anyhow!("Bad capability type from dictionary")); }; // Do something with the data...
Dictionary
物件支援多種項目資料類型。如果元件的狀態小於 fuchsia.component.sandbox/MAX_DATA_LENGTH
,建議您儲存 fuchsia.component.sandbox/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
中。您可以將這些事件與其他錯誤記錄做關聯,以便調查是否因不當停止元件而導致錯誤。