元件通常不會一直運作。大多數元件都是以非同步方式編寫,也就是說,這些元件通常會等待下一個 FIDL 訊息送達。但這些元件仍會佔用記憶體。本指南說明如何調整元件,使其在閒置時停止主動運作並釋出資源。
總覽
預計變更內容如下:
您將對元件的程式碼進行一些變更,以便決定何時停止。您的元件會在停止前保留狀態和控制代碼。保存這類資料的過程稱為「託管」。
元件的用戶端不會知道元件已停止運作。 以這種方式停止元件,不會中斷元件與元件的 FIDL 連線。
Fuchsia 提供程式庫,可讓您監控 FIDL 連線和傳出目錄連線何時閒置,並在發生這種情況時將這些連線還原為控制代碼。
元件架構提供 API,讓元件儲存控制代碼和資料,並在下次執行時 (通常是在控制代碼可讀取或收到新的能力要求時) 擷取這些項目。我們會在下一節詳細說明這些項目的運作方式。
Fuchsia 快照和 Cobalt 資訊主頁會包含實用的生命週期指標。
哪些元件適合?
建議您查看具有下列特性的元件:
流量暴增。這個元件可以啟動及處理這些流量,然後在完成時返回停止狀態。開機和更新路徑中的許多元件只在這些時間需要,否則會閒置並浪費 RAM,例如
core/system-update/system-updater
。不要過於有狀態。您可以在元件停止前保留狀態。在限制條件中,我們可以編寫程式碼來保存所有重要狀態。在實務上,我們會在節省記憶體與保存必要狀態的複雜度之間做出取捨。
記憶體用量偏高。使用
ffx profile memory
查看元件的記憶體用量。舉例來說,它會顯示使用私有記憶體732 KiB
的一般系統上的console-launcher.cm
。私有記憶體是指僅由該元件參照的記憶體,因此我們保證停止該元件時,至少會釋放該記憶體量。請參閱「測量記憶體用量」。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 會追蹤檢查資料的保留情形,即使元件停止運作也一樣。
暫止的取得作業:如果元件是暫止的取得作業 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
轉換為自動取消傳出目錄伺服器端點的項目 (如果檔案系統中沒有工作)。詳情請參閱 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
停止。
將控點和狀態託管給架構
連線閒置且程式庫提供未繫結的伺服器端點後,下一步就是託管這些控制代碼,也就是將控制代碼傳送至元件架構以確保安全。
無狀態通訊協定
注意:通訊協定必須標示為
@discoverable
,才能支援託管。 這樣一來,元件就能在從閒置狀態喚醒後重新連線至管道。
部分 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 事件,將傳出目錄傳送至架構:
在元件
.cml
中新增lifecycle: { stop_event: "notify" }
: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
建立DictionaryRef
,並在Factory
通訊協定上呼叫DictionaryCreate
: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
FIDL 連線呼叫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
。您可以將這些事件與其他錯誤記錄相互參照,協助調查錯誤是否是由於元件停止運作不當所致。