在组件处于空闲状态时将其停止

组件通常不会始终运行。大多数组件都是以 是异步的,这意味着它们通常会等待下一个 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 是不包含 状态,仅用于指标和崩溃 上传。因此,我们对其进行了调整,使其在经过相应配置后在空闲时停止。

已知限制

  • 检查:如果组件通过检查功能发布诊断信息, 当组件停止运行时,这些信息就会被舍弃。 https://fxbug.dev/339076913 跟踪保留 检查数据。

  • 挂起获取:如果您的组件是挂起获取的服务器或客户端 FIDL 方法,则很难保持这种联系,因为 FIDL 绑定无法保存和恢复 正在进行的通话。您可以将该 FIDL 方法转换为事件,然后将 单向确认

  • 目录:如果您的组件提供目录协议,该组件将 很难保持这种连接,因为 由 VFS 库提供VFS 库目前尚未公开返回 底层通道和相关状态(例如定位指针)。

所有这些都有充分的理由。您可以联系 与您的应用场景联系

检测空闲性

停止空闲组件的第一步是增强该组件的代码 了解设备是否已进入空闲状态,这意味着:

  • FIDL 连接处于空闲状态:组件通常会声明多个 FIDL 协议功能之后,客户端就会连接到这些协议, 需要它。这些连接不应具有需要 组件的注意力。

  • 传出目录处于空闲状态:组件提供用于 发布其传出功能该网页上不应存在 表示对此组件的功能请求, 到传出目录(除了由外部 IP 地址建立的 component_manager

  • 其他后台业务逻辑:例如,如果某个组件发出 在后台进行网络请求以响应 FIDL 方法时, 否则就会将该组件视为空闲状态。 该组件在请求过程中停止操作可能是不安全的。

我们都有可用于检测每种情况下的空闲情况的 Rust 库。 https://fxbug.dev/332342122 跟踪相同的 功能。

检测空闲 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 停止。

由框架托管句柄和状态

当连接处于空闲状态且库为您提供了未绑定的服务器时 下一步是托管这些标识名,也就是说,将其发送到 组件框架的安全性

无状态协议

某些 FIDL 连接不具有状态。每个请求的运作方式都完全相同 无论是通过同一连接发送,还是通过不同连接发送。 对于此类连接,您可以按以下步骤操作:

  • 如果尚未在组件清单中声明该功能,请进行声明。您可能需要 声明相应功能(如果此协议连接派生自另一个 连接,并且通常不会从传出目录提供。

  • 在声明 capability 时添加了 delivery: "on_readable"。您需要添加 将你的组件添加到 delivery_type 可见性列表中 tools/cmc/build/restricted_features/BUILD.gn。框架 然后,系统会监控 新的连接请求,并将服务器端点连接到提供商 组件。示例:

    capabilities: [
        {
            protocol: "fuchsia.net.http.Loader",
            delivery: "on_readable",
        },
    ],
    
  • 为该功能添加来自 self 的使用声明,以便程序可以 从其传入的命名空间连接到该实例您可以在以下位置安装该功能 /escrow 目录,以便将其与您的 组件。示例:

    {
        protocol: "fuchsia.net.http.Loader",
        from: "self",
        path: "/escrow/fuchsia.net.http.Loader",
    },
    
  • 从传入的命名空间连接到 capability,传递未绑定的 来自 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::StalledServiceFs 返回的这个地址),因为服务器 端点是一个入口点,所有其他连接均可从该入口点 组件。对于 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 (mut lifecycle_request_stream, lifecycle_control_handle) =
        lifecycle.into_stream_and_control_handle().unwrap();
    
    // Later, when `ServiceFs` has stalled and we have an `outgoing_dir`.
    let outgoing_dir = Some(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.Factory 创建 Dictionary ,并在 Factory 协议上调用 CreateDictionary

    use: [
        {
            protocol: "fuchsia.component.sandbox.Factory",
            from: "framework",
        }
    ]
    
    let factory =
        fuchsia_component::client::connect_to_protocol::<
            fidl_fuchsia_component_sandbox::FactoryMarker
        >().unwrap();
    let dictionary = factory.create_dictionary().await?;
    
  • 您可以通过调用Dictionary 针对 Dictionary FIDL 连接的 Insert。请参阅 fuchsia.component.sandbox FIDL 库文档,适用于 其他方法:

    let bytes = vec![...];
    let data = fidl_fuchsia_component_sandbox::Data::Bytes(bytes);
    let dictionary = dictionary.into_proxy().unwrap();
    dictionary
        .insert(
            "my_data",
            fidl_fuchsia_component_sandbox::Capability::Data(data)
        )
        .await??;
    
  • 在退出之前,请在 send_on_escrow 中发送 Dictionary 客户端端点:

    lifecycle
        .control_handle()
        .send_on_escrow(flifecycle::LifecycleOnEscrowRequest {
            outgoing_dir: Some(outgoing_dir),
            escrowed_dictionary: Some(dictionary.into_channel().unwrap().into_zx_channel().into()),
            ..Default::default()
        })?;
    
  • 下次启动时,您可以从启动句柄获取此字典:

    if let Some(dictionary) = fuchsia_runtime::take_startup_handle(
        HandleInfo::new(HandleType::EscrowedDictionary, 0)
    ) {
        let dictionary = dictionary.into_proxy()?;
        let capability = dictionary.get("my_data").await??;
        match capability {
            fidl_fuchsia_component_sandbox::Capability::Data(
                fidl_fuchsia_component_sandbox::Data::Bytes(data)
            ) => {
                // Do something with the data...
            },
            capability @ _ => warn!("unexpected {capability:?}"),
        }
    }
    

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。您可以在此 dashboard,以便微调空闲超时。

获取反馈概况时, 字段,初始组件和最新组件执行的时间戳将是 可在选择器 <component_manager>:root/lifecycle/early<component_manager>:root/lifecycle/late。您可以将这些 事件和其他错误日志,以帮助调查错误是否由 而是由组件停止不当导致的