在元件處於閒置狀態時停止元件

元件通常不會一直執行工作。大多數元件都是以非同步方式編寫,也就是說,它們通常會等待下一個 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.gndelivery_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 中。您可以將這些事件與其他錯誤記錄做關聯,以便調查是否因不當停止元件而導致錯誤。