组件通常不会一直执行工作。大多数组件都是异步编写的,这意味着它们通常会等待下一个 FIDL 消息的到达。不过,这些组件会占用内存。本指南介绍了如何调整组件,使其在空闲时主动停止并释放资源。
概览
预期结果如下:
您将对组件的代码进行一些更改,以便它可以决定何时停止。您的组件将在停止之前保留其状态和处理脚本。持久化此类数据称为托管。
组件的客户端不会知道您的组件已停止。以这种方式停止组件不会破坏其与组件的 FIDL 连接。
Fuchsia 提供了一些库,可让您监控 FIDL 连接和传出目录连接何时变为空闲状态,并在发生这种情况时将这些连接转回句柄。
组件框架为您的组件提供了 API,以便其存储句柄和数据,并在下次执行时检索这些数据(通常是在句柄可读取后或收到新 capability 请求后)。我们将在后面的部分详细介绍它们的运作方式。
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 会跟踪检查数据的保留情况,即使在组件停止后也是如此。
挂起的 get 操作:如果您的组件是挂起的 get 操作的服务器或客户端,则很难保留该连接,因为 HIDL 绑定无法保存和恢复有关正在进行的调用的相关信息。您可以将该 FIDL 方法转换为事件和单向 ACK。
目录:如果您的组件提供目录协议,则很难保留该连接,因为目录通常由 VFS 库提供。VFS 库目前不提供用于检索底层通道和关联状态(例如跳转指针)的方法。
所有这些都可以通过充分的理由得到支持。您可以与组件框架团队联系,说明您的用例。
检测空闲状态
停止空闲组件的第一步是增强该组件的代码,以便了解其何时变为空闲状态,这意味着:
FIDL 连接处于空闲状态:组件通常会声明多个 FIDL 协议功能,客户端会在需要时连接到这些协议。这些连接不应包含需要组件注意的待处理消息。
出站目录处于空闲状态:某个组件提供出站目录,用于发布其出站功能。不应有代表对此组件的 capability 请求的待处理消息,并且除了由
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
消息后,便不会再生成更多 capability 请求。如果您有一些后台工作会阻止组件停止,但 ServiceFs
在此期间变为空闲状态并过早解除出站目录端点的绑定,则可能会出现问题。如需处理这些情况,您可以阻止 ServiceFs
进入空闲状态。ServiceFs
生成的 Item::Request
包含 ActiveGuard
。只要有效的守卫器在范围内,ServiceFs
就不会变为空闲状态,并会在有能力请求传入时继续让出这些请求。
同样,您可以创建 ExecutionScope
来生成与处理 FIDL 连接相关的所有后台工作,并调用 ExecutionScope::wait()
来等待这些工作完成。例如,http-client.cm
中的 loader_server
函数在后台工作完成之前不会返回,这反过来会使 Item::Request
中的 active_guard
保持在作用域内,从而防止 ServiceFs
停止。
将句柄和状态托管到框架
当连接处于空闲状态且库为您提供了未绑定的服务器端点后,下一步是托管这些句柄,也就是说,将它们发送到组件框架以进行保管。
无状态协议
某些 FIDL 连接不携带状态。无论是通过同一连接还是通过单独的连接发送,每个请求的运作方式都是相同的。您可以按照以下步骤处理这些关联:
在组件清单中声明该 capability(如果尚未声明)。如果此协议连接派生自其他连接,并且通常不是从传出目录提供,则您可能需要声明该 capability。
声明 capability 时添加
delivery: "on_readable"
。您需要将组件添加到tools/cmc/build/restricted_features/BUILD.gn
中的delivery_type
可见性列表。然后,该框架将监控新连接请求的服务器端点上的可读信号,并在有待处理的消息时将服务器端点连接到提供方组件。示例:capabilities: [ { protocol: "fuchsia.net.http.Loader", delivery: "on_readable", }, ],
为该 capability 从
self
添加使用声明,以便程序可以从其传入命名空间连接到它。您可以在/escrow
目录中安装该 capability,以将其与组件使用的其他 capability 区分开来。示例:{ 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::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
事件后,将无法监控更多 capability 请求。因此,它应该会在之后立即退出。在下次执行时,您的组件将在其启动信息中收到其在上次运行时发送的相同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
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
capability。此 capability 由组件本身使用。该组件可能会从其传入命名空间中打开 /escrow/exception_channel
,并使用要等待的通道。当该通道可读时,框架会在传出目录中打开 /escrow/exception_channel
,并根据需要启动该组件。总而言之,您可以声明功能,并在 self
中使用这些功能将句柄托管到 component_manager
。
如果您需要其他类型的触发器(例如等待自定义信号或等待计时器),请与组件框架团队联系。
测试
我们建议您增强现有的集成测试,以便同时测试您的组件是否可以在不中断 FIDL 连接的情况下自行停止并重新启动。如果您已经有一个集成测试来启动组件并向其发送 FIDL 请求,则可以使用组件事件匹配器来验证组件在没有消息时是否会停止。如需了解具体操作方法的示例,请参阅 http-client
测试。
着陆页和指标
如果您希望针对特定产品优化此组件,可以向组件添加结构化配置,以控制空闲超时是否启用以及超时时长。
组件框架会记录组件在每次执行之间启动和停止的时长,并将这些数据上传到 Cobalt。您可以在此信息中心内查看这些数据,以便微调空闲超时设置。
在获取反馈快照时(例如在现场遇到 bug 时),初始和最新组件执行时间戳将分别显示在选择器 <component_manager>:root/lifecycle/early
和 <component_manager>:root/lifecycle/late
中。您可以将这些事件与其他错误日志相关联,以便调查错误是否由组件停止不当所致。