[memory] Expose memory attribution in Colocated Runner
This change enables the Colocated Runner example to expose memory
attribution information, showing how nested attribution information can be
surfaced.
BUG=307580082
Change-Id: I14514aae22c6a3d890abe1da78827bce614841c8
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/974773
Reviewed-by: Yifei Teng <yifeit@google.com>
Commit-Queue: Étienne J. Membrives <etiennej@google.com>
Reviewed-by: Wez <wez@google.com>
diff --git a/examples/components/runner/colocated/BUILD.gn b/examples/components/runner/colocated/BUILD.gn
index d7e9030..de10635 100644
--- a/examples/components/runner/colocated/BUILD.gn
+++ b/examples/components/runner/colocated/BUILD.gn
@@ -22,21 +22,24 @@
edition = "2021"
deps = [
+ "//examples/components/runner/colocated/fidl:colocated_rust",
"//sdk/fidl/fuchsia.component:fuchsia.component_rust",
"//sdk/fidl/fuchsia.component.runner:fuchsia.component.runner_rust",
+ "//sdk/fidl/fuchsia.memory.attribution:fuchsia.memory.attribution_rust",
"//sdk/fidl/fuchsia.process:fuchsia.process_rust",
+ "//src/lib/fidl/rust/fidl",
"//src/lib/fuchsia",
"//src/lib/fuchsia-async",
"//src/lib/fuchsia-component",
"//src/lib/fuchsia-runtime",
- "//src/lib/mapped-vmo",
+ "//src/lib/fuchsia-sync",
"//src/lib/zircon/rust:fuchsia-zircon",
+ "//src/performance/memory/attribution",
"//src/sys/lib/runner",
"//third_party/rust_crates:anyhow",
"//third_party/rust_crates:async-lock",
"//third_party/rust_crates:async-trait",
"//third_party/rust_crates:futures",
- "//third_party/rust_crates:rand",
"//third_party/rust_crates:scopeguard",
"//third_party/rust_crates:tracing",
]
@@ -64,26 +67,14 @@
manifest = "meta/colocated-runner-example.cml"
}
-fuchsia_component("colocated-component-32mb") {
- component_name = "colocated-component-32mb"
- manifest = "meta/colocated-component-32mb.cml"
-}
-
-fuchsia_component("colocated-component-64mb") {
- component_name = "colocated-component-64mb"
- manifest = "meta/colocated-component-64mb.cml"
-}
-
-fuchsia_component("colocated-component-128mb") {
- component_name = "colocated-component-128mb"
- manifest = "meta/colocated-component-128mb.cml"
+fuchsia_component("colocated-component") {
+ component_name = "colocated-component"
+ manifest = "meta/colocated-component.cml"
}
fuchsia_package("colocated-runner-example") {
deps = [
- ":colocated-component-128mb",
- ":colocated-component-32mb",
- ":colocated-component-64mb",
+ ":colocated-component",
":colocated-runner",
":colocated-runner-example-realm",
]
@@ -92,11 +83,14 @@
fuchsia_unittest_package("colocated-runner-unittests") {
deps = [
":bin_test",
- ":colocated-component-64mb",
+ ":colocated-component",
]
}
group("hermetic_tests") {
testonly = true
- deps = [ ":colocated-runner-unittests" ]
+ deps = [
+ ":colocated-runner-unittests",
+ "integration_tests",
+ ]
}
diff --git a/examples/components/runner/colocated/README.md b/examples/components/runner/colocated/README.md
index 05977cb..05117e5 100644
--- a/examples/components/runner/colocated/README.md
+++ b/examples/components/runner/colocated/README.md
@@ -7,13 +7,14 @@
## What does this runner do
The `colocated` runner demonstrates how to attribute memory to each component
-it runs. A program run by the `colocated` runner will allocate and map a VMO of
-a user-specified size, and then fill it with randomized bytes, to cause the
-pages to be physically allocated.
+it runs. A program run by the `colocated` runner will create and hold a VMO.
+This VMO will be reported by the runner as part of the memory attribution
+protocol.
-If the program is started with a `PA_USER0` numbered handle, it will signal the
-`USER_0` signal on the peer handle once it has done filling the VMO, to indicate
-that all the backing pages have been allocated.
+If the program is started with a `PA_USER0` numbered handle, it will serve the
+`fuchsia.examples.colocated.Colocated` protocol over this channel, which can be
+used to retrieve the koid of the VMO created by the colocated component, from
+the component itself.
## Program schema
@@ -50,14 +51,11 @@
To run a colocated component, provide this URL to `ffx component run`:
```bash
-$ ffx component run /core/ffx-laboratory:colocated-runner-example/collection:1 'fuchsia-pkg://fuchsia.com/colocated-runner-example#meta/colocated-component-32mb.cm'
+$ ffx component run /core/ffx-laboratory:colocated-runner-example/collection:1 'fuchsia-pkg://fuchsia.com/colocated-runner-example#meta/colocated-component.cm'
```
-This will start a component that attempts to use 32 MiB of memory, living inside
-the address space of the `colocated` runner.
-
-You may replace `32mb` with `64mb` or `128mb` to test different sizes of memory
-usage.
+This will start a component that lives inside the address space of the
+`colocated` runner.
You may replace `collection:1` with `collection:2` etc. to start multiple
colocated components in this collection.
diff --git a/examples/components/runner/colocated/fidl/BUILD.gn b/examples/components/runner/colocated/fidl/BUILD.gn
new file mode 100644
index 0000000..0033b96
--- /dev/null
+++ b/examples/components/runner/colocated/fidl/BUILD.gn
@@ -0,0 +1,16 @@
+# Copyright 2024 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+
+fidl("colocated") {
+ # TODO(https://fxbug.dev/42180976) - The structure and location of FIDL libraries along
+ # with their names can be confusing. We should update this once we land on a
+ # decision in the linked bug.
+ name = "fuchsia.examples.colocated"
+
+ sources = [ "colocated.test.fidl" ]
+
+ public_deps = [ "//zircon/vdso/zx" ]
+}
diff --git a/examples/components/runner/colocated/fidl/colocated.test.fidl b/examples/components/runner/colocated/fidl/colocated.test.fidl
new file mode 100644
index 0000000..63a75b2
--- /dev/null
+++ b/examples/components/runner/colocated/fidl/colocated.test.fidl
@@ -0,0 +1,19 @@
+// Copyright 2024 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Library containing a simple calculator protocol.
+library fuchsia.examples.colocated;
+
+using zx;
+
+/// A protocol for reporting one's own VMO usage.
+///
+/// This protocol is used for integration testing.
+@discoverable
+open protocol Colocated {
+ /// Returns a list of VMO Koids used by the component,
+ GetVmos() -> (struct {
+ vmos vector<zx.Koid>:MAX;
+ });
+};
diff --git a/examples/components/runner/colocated/integration_tests/BUILD.gn b/examples/components/runner/colocated/integration_tests/BUILD.gn
new file mode 100644
index 0000000..a5dde7a
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/BUILD.gn
@@ -0,0 +1,58 @@
+# Copyright 2023 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+assert(is_fuchsia, "These targets are only compiled in the fuchsia toolchain.")
+
+import("//build/components.gni")
+import("//build/rust/rustc_test.gni")
+
+rustc_test("bin") {
+ name = "colocated_runner_integration_test_bin"
+ edition = "2021"
+
+ deps = [
+ "//examples/components/runner/colocated/fidl:colocated_rust",
+ "//sdk/fidl/fuchsia.component:fuchsia.component_rust",
+ "//sdk/fidl/fuchsia.component.decl:fuchsia.component.decl_rust",
+ "//sdk/fidl/fuchsia.memory.attribution:fuchsia.memory.attribution_rust",
+ "//sdk/fidl/fuchsia.process:fuchsia.process_rust",
+ "//src/lib/fuchsia",
+ "//src/lib/fuchsia-async",
+ "//src/lib/fuchsia-component-test",
+ "//src/lib/fuchsia-runtime",
+ "//src/lib/zircon/rust:fuchsia-zircon",
+ "//third_party/rust_crates:async-channel",
+ "//third_party/rust_crates:futures-util",
+ ]
+
+ sources = [ "src/lib.rs" ]
+}
+
+fuchsia_test_component("colocated_runner_integration_test") {
+ component_name = "colocated_runner_integration_test"
+ manifest = "meta/colocated_runner_integration_test.cml"
+ deps = [ ":bin" ]
+}
+
+fuchsia_component("test_realm") {
+ component_name = "test_realm"
+ manifest = "meta/test_realm.cml"
+ testonly = true
+}
+
+fuchsia_test_package("colocated-runner-integration-test") {
+ test_components = [ ":colocated_runner_integration_test" ]
+ deps = [
+ ":test_realm",
+ "//examples/components/runner/colocated:colocated-component",
+ "//examples/components/runner/colocated:colocated-runner",
+ "//src/sys/component_manager:component-manager-realm-builder-cmp",
+ "//src/sys/component_manager:elf_runner",
+ ]
+}
+
+group("integration_tests") {
+ testonly = true
+ deps = [ ":colocated-runner-integration-test" ]
+}
diff --git a/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml b/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml
new file mode 100644
index 0000000..903f09f
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/meta/colocated_runner_integration_test.cml
@@ -0,0 +1,14 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+ include: [
+ "//src/lib/fuchsia-component-test/meta/nested_component_manager.shard.cml",
+ "//src/sys/test_runners/rust/default.shard.cml",
+ "sys/component/realm_builder.shard.cml",
+ "syslog/client.shard.cml",
+ ],
+ program: {
+ binary: "bin/colocated_runner_integration_test_bin",
+ },
+}
diff --git a/examples/components/runner/colocated/integration_tests/meta/test_realm.cml b/examples/components/runner/colocated/integration_tests/meta/test_realm.cml
new file mode 100644
index 0000000..514dcf9
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/meta/test_realm.cml
@@ -0,0 +1,53 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+ include: [ "syslog/offer.shard.cml" ],
+ children: [
+ {
+ name: "elf_runner",
+ url: "#meta/elf_runner.cm",
+ },
+ {
+ name: "colocated-runner",
+ url: "#meta/colocated-runner.cm",
+ environment: "#colocated-runner-env",
+ },
+ ],
+ collections: [
+ {
+ name: "collection",
+ environment: "#colocated-env",
+ durability: "single_run",
+ },
+ ],
+ offer: [
+ {
+ protocol: "fuchsia.process.Launcher",
+ from: "parent",
+ to: "#elf_runner",
+ },
+ ],
+ environments: [
+ {
+ name: "colocated-runner-env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "elf",
+ from: "#elf_runner",
+ },
+ ],
+ },
+ {
+ name: "colocated-env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "colocated",
+ from: "#colocated-runner",
+ },
+ ],
+ },
+ ],
+}
diff --git a/examples/components/runner/colocated/integration_tests/src/lib.rs b/examples/components/runner/colocated/integration_tests/src/lib.rs
new file mode 100644
index 0000000..5e2bba2
--- /dev/null
+++ b/examples/components/runner/colocated/integration_tests/src/lib.rs
@@ -0,0 +1,263 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![cfg(test)]
+
+use fidl_fuchsia_component as fcomponent;
+use fidl_fuchsia_component_decl as fdecl;
+use fidl_fuchsia_examples_colocated as fcolocated;
+use fidl_fuchsia_memory_attribution as fattribution;
+use fidl_fuchsia_process::HandleInfo;
+use fuchsia_async as fasync;
+use fuchsia_component_test::{
+ Capability, ChildOptions, RealmBuilder, RealmBuilderParams, Ref, Route,
+};
+use fuchsia_runtime::HandleType;
+use fuchsia_zircon as zx;
+use futures_util::{future::BoxFuture, FutureExt};
+use std::{collections::HashMap, sync::Arc};
+
+/// Attribute resource given the set of resources at the root of the system,
+/// and a protocol to attribute resources under different principals.
+fn attribute_memory<'a>(
+ name: String,
+ attribution_provider: fattribution::ProviderProxy,
+ introspector: &'a fcomponent::IntrospectorProxy,
+) -> BoxFuture<'a, Node> {
+ async move {
+ // Otherwise, check if there are attribution information.
+ let attributions = attribution_provider
+ .get()
+ .await
+ .unwrap_or_else(|e| panic!("Failed to get AttributionResponse for {name}: {e}"))
+ .unwrap_or_else(|e| panic!("Failed call to AttributionResponse for {name}: {e:?}"))
+ .attributions
+ .unwrap_or_else(|| panic!("Failed memory attribution for {name}"));
+
+ let mut node = Node::new(name);
+
+ // If there are children, resources assigned to this node by its parent
+ // will be re-assigned to children if applicable.
+ let mut children = HashMap::<String, Node>::new();
+ for attribution in attributions {
+ // Recursively attribute memory in this child principal.
+ match attribution {
+ fattribution::AttributionUpdate::Add(new_principal) => {
+ let identifier =
+ get_identifier_string(new_principal.identifier, &node.name, introspector)
+ .await;
+ let child = if let Some(client) = new_principal.detailed_attribution {
+ attribute_memory(identifier, client.into_proxy().unwrap(), introspector)
+ .await
+ } else {
+ Node::new(identifier)
+ };
+ children.insert(child.name.clone(), child);
+ }
+ fattribution::AttributionUpdate::Update(updated_principal) => {
+ let identifier = get_identifier_string(
+ updated_principal.identifier,
+ &node.name,
+ introspector,
+ )
+ .await;
+
+ let child = children.get_mut(&identifier).unwrap();
+ match updated_principal.resources.unwrap() {
+ fattribution::Resources::Data(d) => {
+ child.resources = d
+ .into_iter()
+ .filter_map(|r| {
+ if let fattribution::Resource::KernelObject(koid) = r {
+ Some(koid)
+ } else {
+ None
+ }
+ })
+ .collect();
+ }
+ _ => todo!("unimplemented"),
+ };
+ }
+ fattribution::AttributionUpdate::Remove(_) => todo!(),
+ _ => panic!("Unimplemented"),
+ };
+ }
+ node.children.extend(children.into_values());
+ node
+ }
+ .boxed()
+}
+
+async fn get_identifier_string(
+ identifier: Option<fattribution::Identifier>,
+ name: &String,
+ introspector: &fcomponent::IntrospectorProxy,
+) -> String {
+ match identifier.unwrap() {
+ fattribution::Identifier::Self_(_) => todo!("self attribution not supported"),
+ fattribution::Identifier::Component(c) => introspector
+ .get_moniker(c)
+ .await
+ .expect("Inspector call failed")
+ .expect("Inspector::GetMoniker call failed"),
+ fattribution::Identifier::Part(sc) => format!("{}/{}", name, sc).to_owned(),
+ _ => todo!(),
+ }
+}
+
+#[derive(Debug)]
+struct Node {
+ name: String,
+ resources: Vec<u64>,
+ children: Vec<Node>,
+}
+
+impl Node {
+ pub fn new(identifier: String) -> Node {
+ Node { name: identifier, resources: vec![], children: vec![] }
+ }
+}
+
+#[fuchsia::test]
+async fn test_attribute_memory() {
+ // Starts a component manager and obtain its root job, so that we can simulate
+ // traversing the root job of the system, the kind done in `memory_monitor`.
+ let builder = RealmBuilder::with_params(
+ RealmBuilderParams::new().from_relative_url("#meta/test_realm.cm"),
+ )
+ .await
+ .expect("Failed to create test realm builder");
+
+ // Add a child to receive these capabilities so that we can use them in this test.
+ // - fuchsia.memory.attribution.Provider
+ // - fuchsia.kernel.RootJobForInspect
+ // - fuchsia.component.Realm
+ struct Capabilities {
+ attribution_provider: fattribution::ProviderProxy,
+ introspector: fcomponent::IntrospectorProxy,
+ realm: fcomponent::RealmProxy,
+ }
+ let (capabilities_sender, capabilities_receiver) = async_channel::unbounded::<Capabilities>();
+ let capabilities_sender = Arc::new(capabilities_sender);
+ let receiver = builder
+ .add_local_child(
+ "receiver",
+ move |handles| {
+ let capabilities_sender = capabilities_sender.clone();
+ async move {
+ capabilities_sender
+ .send(Capabilities {
+ attribution_provider: handles
+ .connect_to_protocol::<fattribution::ProviderMarker>()
+ .unwrap(),
+ introspector: handles
+ .connect_to_protocol::<fcomponent::IntrospectorMarker>()
+ .unwrap(),
+ realm: handles
+ .connect_to_protocol::<fcomponent::RealmMarker>()
+ .unwrap(),
+ })
+ .await
+ .unwrap();
+ // TODO(https://fxbug.dev/303919602): Until the component framework reliably
+ // drains capability requests when a component is stopped, we need to
+ // keep running the component.
+ std::future::pending().await
+ }
+ .boxed()
+ },
+ ChildOptions::new().eager(),
+ )
+ .await
+ .expect("Failed to add child");
+
+ builder
+ .add_route(
+ Route::new()
+ .capability(Capability::protocol_by_name("fuchsia.memory.attribution.Provider"))
+ .from(Ref::child("elf_runner"))
+ .to(&receiver),
+ )
+ .await
+ .unwrap();
+
+ builder
+ .add_route(
+ Route::new()
+ .capability(Capability::protocol_by_name("fuchsia.component.Realm"))
+ .from(Ref::framework())
+ .to(&receiver),
+ )
+ .await
+ .unwrap();
+
+ builder
+ .add_route(
+ Route::new()
+ .capability(Capability::protocol_by_name("fuchsia.component.Introspector"))
+ .from(Ref::framework())
+ .to(&receiver),
+ )
+ .await
+ .unwrap();
+
+ let _realm =
+ builder.build_in_nested_component_manager("#meta/component_manager.cm").await.unwrap();
+
+ let capabilities = capabilities_receiver.recv().await.unwrap();
+
+ // Start a colocated component.
+ let collection = fdecl::CollectionRef { name: "collection".to_string() };
+ let decl = fdecl::Child {
+ name: Some("colocated-component".to_string()),
+ url: Some("#meta/colocated-component.cm".to_string()),
+ startup: Some(fdecl::StartupMode::Lazy),
+ ..Default::default()
+ };
+ let (user0, user0_peer) = zx::Channel::create();
+ let args = fcomponent::CreateChildArgs {
+ numbered_handles: Some(vec![HandleInfo {
+ handle: user0_peer.into(),
+ id: fuchsia_runtime::HandleInfo::new(HandleType::User0, 0).as_raw(),
+ }]),
+ ..Default::default()
+ };
+ capabilities.realm.create_child(&collection, &decl, args).await.unwrap().unwrap();
+
+ let colocated_component_vmos =
+ fcolocated::ColocatedProxy::new(fasync::Channel::from_channel(user0))
+ .get_vmos()
+ .await
+ .unwrap();
+
+ assert!(!colocated_component_vmos.is_empty());
+
+ // Starting from the ELF runner, ask about the resource usage.
+ let elf_runner = attribute_memory(
+ "elf_runner.cm".to_owned(),
+ capabilities.attribution_provider,
+ &capabilities.introspector,
+ )
+ .await;
+
+ // We should get the following tree:
+ //
+ // - elf_runner.cm
+ // - colocated_runner.cm
+ // - colocated_component-64mb.cm
+ // - Some VMO
+ // - overhead
+ // - overhead
+ eprintln!("{:?}", elf_runner);
+ assert_eq!(elf_runner.children.len(), 1);
+ assert!(elf_runner.children[0].name.contains("colocated-runner"));
+ assert_eq!(elf_runner.children[0].children.len(), 1usize);
+ // Name of the colocated component
+ assert_eq!(&elf_runner.children[0].children[0].name, "collection:colocated-component");
+ let resource = &elf_runner.children[0].children[0].resources;
+ for vmo_koid in colocated_component_vmos {
+ assert!(resource.contains(&vmo_koid));
+ }
+}
diff --git a/examples/components/runner/colocated/meta/colocated-component-32mb.cml b/examples/components/runner/colocated/meta/colocated-component-32mb.cml
deleted file mode 100644
index e39fc7e..0000000
--- a/examples/components/runner/colocated/meta/colocated-component-32mb.cml
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2023 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- program: {
- runner: "colocated",
- vmo_size: "33554432",
- },
-}
diff --git a/examples/components/runner/colocated/meta/colocated-component-64mb.cml b/examples/components/runner/colocated/meta/colocated-component-64mb.cml
deleted file mode 100644
index 7efaa9b..0000000
--- a/examples/components/runner/colocated/meta/colocated-component-64mb.cml
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2023 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- program: {
- runner: "colocated",
- vmo_size: "67108864",
- },
-}
diff --git a/examples/components/runner/colocated/meta/colocated-component-128mb.cml b/examples/components/runner/colocated/meta/colocated-component.cml
similarity index 87%
rename from examples/components/runner/colocated/meta/colocated-component-128mb.cml
rename to examples/components/runner/colocated/meta/colocated-component.cml
index cde1166..ce40757 100644
--- a/examples/components/runner/colocated/meta/colocated-component-128mb.cml
+++ b/examples/components/runner/colocated/meta/colocated-component.cml
@@ -4,6 +4,5 @@
{
program: {
runner: "colocated",
- vmo_size: "134217728",
},
}
diff --git a/examples/components/runner/colocated/meta/colocated-runner.cml b/examples/components/runner/colocated/meta/colocated-runner.cml
index 5bc7277..d0a2759 100644
--- a/examples/components/runner/colocated/meta/colocated-runner.cml
+++ b/examples/components/runner/colocated/meta/colocated-runner.cml
@@ -6,6 +6,7 @@
program: {
runner: "elf",
binary: "bin/colocated_runner",
+ memory_attribution: "true",
},
capabilities: [
{ protocol: "fuchsia.component.runner.ComponentRunner" },
@@ -13,6 +14,7 @@
runner: "colocated",
path: "/svc/fuchsia.component.runner.ComponentRunner",
},
+ { protocol: "fuchsia.memory.attribution.Provider" },
],
expose: [
{
@@ -23,5 +25,9 @@
runner: "colocated",
from: "self",
},
+ {
+ protocol: "fuchsia.memory.attribution.Provider",
+ from: "self",
+ },
],
}
diff --git a/examples/components/runner/colocated/src/main.rs b/examples/components/runner/colocated/src/main.rs
index 3feb0c1..bd5e4b3 100644
--- a/examples/components/runner/colocated/src/main.rs
+++ b/examples/components/runner/colocated/src/main.rs
@@ -2,41 +2,71 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
+use attribution::{AttributionServer, AttributionServerHandle, Observer, Publisher};
+use fidl::endpoints::ControlHandle;
+use fidl::endpoints::RequestStream;
use fidl_fuchsia_component as fcomponent;
use fidl_fuchsia_component_runner as fcrunner;
+use fidl_fuchsia_memory_attribution as fattribution;
use fuchsia_async as fasync;
use fuchsia_component::server::ServiceFs;
+use fuchsia_sync::Mutex;
use fuchsia_zircon as zx;
use futures::{StreamExt, TryStreamExt};
use runner::component::{ChannelEpitaph, Controllable, Controller};
-use std::future::Future;
+use std::{
+ collections::HashMap,
+ future::Future,
+ sync::{
+ atomic::{AtomicU64, Ordering},
+ Arc,
+ },
+};
+
use tracing::{info, warn};
+use zx::{HandleBased, Koid};
mod program;
use crate::program::ColocatedProgram;
-const VMO_SIZE: &str = "vmo_size";
-
enum IncomingRequest {
Runner(fcrunner::ComponentRunnerRequestStream),
+ Memory(fattribution::ProviderRequestStream),
}
#[fuchsia::main]
async fn main() -> Result<()> {
+ let resource_tracker = Arc::new(ResourceTracker { resources: Mutex::new(Default::default()) });
+
let mut service_fs = ServiceFs::new_local();
service_fs.dir("svc").add_fidl_service(IncomingRequest::Runner);
+ service_fs.dir("svc").add_fidl_service(IncomingRequest::Memory);
service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;
+ let memory_server_handle = get_memory_server(resource_tracker.clone());
+
service_fs
- .for_each_concurrent(None, |request: IncomingRequest| async move {
+ .for_each_concurrent(None, |request: IncomingRequest| async {
match request {
IncomingRequest::Runner(stream) => {
- if let Err(err) = handle_runner_request(stream).await {
+ if let Err(err) = handle_runner_request(
+ stream,
+ resource_tracker.clone(),
+ memory_server_handle.clone(),
+ )
+ .await
+ {
warn!("Error while serving ComponentRunner: {err}");
}
}
+ IncomingRequest::Memory(stream) => {
+ let observer = memory_server_handle.new_observer(stream.control_handle());
+ if let Err(err) = handle_memory_request(stream, observer).await {
+ warn!("Error while serving AttributionProvider: {err}");
+ }
+ }
}
})
.await;
@@ -44,15 +74,24 @@
Ok(())
}
+fn get_memory_server(resource_tracker: Arc<ResourceTracker>) -> AttributionServerHandle {
+ let state_fn = Box::new(move || get_attribution(resource_tracker.clone()));
+ AttributionServer::new(state_fn)
+}
+
/// Handles `fuchsia.component.runner/ComponentRunner` requests over a FIDL connection.
-async fn handle_runner_request(mut stream: fcrunner::ComponentRunnerRequestStream) -> Result<()> {
+async fn handle_runner_request(
+ mut stream: fcrunner::ComponentRunnerRequestStream,
+ resource_tracker: Arc<ResourceTracker>,
+ memory_server_handle: AttributionServerHandle,
+) -> Result<()> {
while let Some(request) =
stream.try_next().await.context("failed to serve ComponentRunner protocol")?
{
let fcrunner::ComponentRunnerRequest::Start { start_info, controller, .. } = request;
let url = start_info.resolved_url.clone().unwrap_or_else(|| "unknown url".to_string());
info!("Colocated runner is going to start component {url}");
- match start(start_info) {
+ match start(start_info, resource_tracker.clone(), memory_server_handle.new_publisher()) {
Ok((program, on_exit)) => {
let controller = Controller::new(program, controller.into_stream().unwrap());
fasync::Task::spawn(controller.serve(on_exit)).detach();
@@ -69,16 +108,115 @@
Ok(())
}
+async fn handle_memory_request(
+ mut stream: fattribution::ProviderRequestStream,
+ subscriber: Observer,
+) -> Result<()> {
+ while let Some(request) =
+ stream.try_next().await.context("failed to serve AttributionProvider protocol")?
+ {
+ match request {
+ fattribution::ProviderRequest::Get { responder } => {
+ subscriber.next(responder);
+ }
+ fattribution::ProviderRequest::_UnknownMethod { ordinal, control_handle, .. } => {
+ warn!("Invalid request to AttributionProvider: {ordinal}");
+ control_handle.shutdown_with_epitaph(zx::Status::INVALID_ARGS);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn get_attribution(resource_tracker: Arc<ResourceTracker>) -> Vec<fattribution::AttributionUpdate> {
+ let mut children = vec![];
+ for (_, (token, koid)) in resource_tracker.resources.lock().iter() {
+ children.push(fattribution::AttributionUpdate::Add(fattribution::NewPrincipal {
+ identifier: Some(fattribution::Identifier::Component(
+ token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+ )),
+ detailed_attribution: None,
+ ..Default::default()
+ }));
+ children.push(fattribution::AttributionUpdate::Update(fattribution::UpdatedPrincipal {
+ identifier: Some(fattribution::Identifier::Component(
+ token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+ )),
+ resources: Some(fattribution::Resources::Data(vec![
+ fattribution::Resource::KernelObject(koid.raw_koid()),
+ ])),
+ ..Default::default()
+ }));
+ }
+ children
+}
+
+/// Tracks resources used by each [`ColocatedProgram`]. Since each program just allocates
+/// one VMO, we only need to track one KOID here.
+struct ResourceTracker {
+ resources: Mutex<HashMap<ProgramId, (zx::Event, Koid)>>,
+}
+
+type ProgramId = u64;
+
+static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
/// Starts a colocated component.
fn start(
start_info: fcrunner::ComponentStartInfo,
+ resource_tracker: Arc<ResourceTracker>,
+ publisher: Publisher,
) -> Result<(impl Controllable, impl Future<Output = ChannelEpitaph> + Unpin)> {
- let vmo_size = runner::get_program_string(&start_info, VMO_SIZE)
- .ok_or(anyhow!("Missing vmo_size argument in program block"))?;
- let vmo_size: u64 = vmo_size.parse().context("vmo_size is not a valid number")?;
let numbered_handles = start_info.numbered_handles.unwrap_or(vec![]);
- let program = ColocatedProgram::new(vmo_size, numbered_handles)?;
+ let program = ColocatedProgram::new(numbered_handles)?;
+ let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+ let vmo_koid = program.get_vmo_koid().raw_koid();
+ // Register this VMO.
+ let instance_token = start_info.component_instance.as_ref().unwrap();
+
+ let mut resources = resource_tracker.resources.lock();
+ resources.insert(
+ id,
+ (
+ instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+ program.get_vmo_koid(),
+ ),
+ );
+
+ let mut updates = vec![];
+ updates.push(fattribution::AttributionUpdate::Add(fattribution::NewPrincipal {
+ identifier: Some(fattribution::Identifier::Component(
+ instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+ )),
+ detailed_attribution: None,
+ ..Default::default()
+ }));
+ updates.push(fattribution::AttributionUpdate::Update(fattribution::UpdatedPrincipal {
+ identifier: Some(fattribution::Identifier::Component(
+ instance_token.duplicate_handle(fidl::Rights::SAME_RIGHTS).unwrap(),
+ )),
+ resources: Some(fattribution::Resources::Data(vec![fattribution::Resource::KernelObject(
+ vmo_koid,
+ )])),
+ ..Default::default()
+ }));
+ publisher.on_update(updates).unwrap();
let termination = program.wait_for_termination();
+ let termination_clone = program.wait_for_termination();
+ // Remove this VMO when the program has terminated.
+ let tracker = resource_tracker.clone();
+ fasync::Task::spawn(async move {
+ termination_clone.await;
+ if let Some((token, _)) = tracker.resources.lock().remove(&id) {
+ publisher
+ .on_update(vec![fattribution::AttributionUpdate::Remove(
+ fattribution::Identifier::Component(token),
+ )])
+ .unwrap();
+ }
+ })
+ .detach();
Ok((program, termination))
}
@@ -88,74 +226,59 @@
use super::*;
use fidl::endpoints::Proxy;
use fidl_fuchsia_component_decl as fdecl;
+ use fidl_fuchsia_examples_colocated as fcolocated;
use fidl_fuchsia_process::HandleInfo;
use fuchsia_runtime::HandleType;
#[fuchsia::test]
async fn test_start_stop_component() {
+ let resource_tracker =
+ Arc::new(ResourceTracker { resources: Mutex::new(Default::default()) });
let (runner, runner_stream) =
fidl::endpoints::create_proxy_and_stream::<fcrunner::ComponentRunnerMarker>().unwrap();
- let server = fasync::Task::spawn(handle_runner_request(runner_stream));
+ let memory_server = get_memory_server(resource_tracker.clone());
+ let server = fasync::Task::spawn(handle_runner_request(
+ runner_stream,
+ resource_tracker,
+ memory_server.clone(),
+ ));
- // Measure how much private RAM our own process is using.
- let usage_initial = private_ram();
-
- // Start a component using 64 MiB of RAM.
+ // Start a colocated component.
let decl = fuchsia_fs::file::read_in_namespace_to_fidl::<fdecl::Component>(
- "/pkg/meta/colocated-component-64mb.cm",
+ "/pkg/meta/colocated-component.cm",
)
.await
.unwrap();
let (controller, controller_server_end) = fidl::endpoints::create_endpoints();
- let (user0, user0_peer) = zx::EventPair::create();
+ let (user0, user0_peer) = zx::Channel::create();
let start_info = fcrunner::ComponentStartInfo {
program: decl.program.unwrap().info,
numbered_handles: Some(vec![HandleInfo {
handle: user0_peer.into(),
id: fuchsia_runtime::HandleInfo::new(HandleType::User0, 0).as_raw(),
}]),
+ component_instance: Some(zx::Event::create()),
..Default::default()
};
runner.start(start_info, controller_server_end).unwrap();
// Wait until the program has allocated 64 MiB worth of pages.
- _ = fasync::OnSignals::new(&user0, zx::Signals::USER_0).await.unwrap();
+ let colocated_component_vmos =
+ fcolocated::ColocatedProxy::new(fasync::Channel::from_channel(user0))
+ .get_vmos()
+ .await
+ .unwrap();
// Measure our private memory usage again. It should increase by roughly that much more.
- let usage_started = private_ram();
- assert!(
- usage_started > usage_initial,
- "initial: {usage_initial}, started: {usage_started}"
- );
- assert!(
- usage_started - usage_initial > 60 * 1024 * 1024,
- "initial: {usage_initial}, started: {usage_started}"
- );
+ assert!(!colocated_component_vmos.is_empty());
// Stop the component.
let controller = controller.into_proxy().unwrap();
controller.stop().unwrap();
controller.on_closed().await.unwrap();
- // Measure our private memory usage again. It should roughly go back to before.
- let usage_stopped = private_ram();
- assert!(
- usage_stopped < usage_started,
- "started: {usage_started}, stopped: {usage_stopped}"
- );
- assert!(
- usage_started - usage_stopped > 60 * 1024 * 1024,
- "started: {usage_started}, stopped: {usage_stopped}"
- );
-
// Close the connection and verify the server task ends successfully.
drop(runner);
server.await.unwrap();
}
-
- #[track_caller]
- fn private_ram() -> usize {
- let process = fuchsia_runtime::process_self();
- process.task_stats().unwrap().mem_private_bytes
- }
}
diff --git a/examples/components/runner/colocated/src/program.rs b/examples/components/runner/colocated/src/program.rs
index 72f8ee5..e3fe566 100644
--- a/examples/components/runner/colocated/src/program.rs
+++ b/examples/components/runner/colocated/src/program.rs
@@ -4,37 +4,34 @@
use async_lock::OnceCell;
use async_trait::async_trait;
+use fidl::endpoints::RequestStream;
+use fidl_fuchsia_examples_colocated as fcolocated;
use fidl_fuchsia_process::HandleInfo;
use fuchsia_async as fasync;
use fuchsia_runtime::HandleType;
use fuchsia_zircon as zx;
-use futures::{
- channel::oneshot,
- future::{BoxFuture, FutureExt},
-};
-use mapped_vmo::Mapping;
+use futures::future::{BoxFuture, FutureExt};
+use futures::TryStreamExt;
use runner::component::{ChannelEpitaph, Controllable};
-use std::{ops::DerefMut, sync::Arc};
+use std::sync::Arc;
use tracing::warn;
-use zx::Peered;
+use zx::{AsHandleRef, Koid};
/// [`ColocatedProgram `] represents an instance of a program run by the
/// colocated runner. Its state is held in this struct and its behavior
/// is run in the `task`.
pub struct ColocatedProgram {
task: Option<fasync::Task<()>>,
- filled: Option<oneshot::Receiver<Mapping>>,
terminated: Arc<OnceCell<()>>,
+ vmo_koid: Koid,
}
impl ColocatedProgram {
- pub fn new(vmo_size: u64, numbered_handles: Vec<HandleInfo>) -> Result<Self, anyhow::Error> {
- let vmo = zx::Vmo::create(vmo_size)?;
- let vmo_size = vmo.get_size()?;
- let (filled_sender, filled) = oneshot::channel();
+ pub fn new(numbered_handles: Vec<HandleInfo>) -> Result<Self, anyhow::Error> {
+ let vmo = zx::Vmo::create(1024)?;
+ let vmo_koid = vmo.get_koid()?;
+
let terminated = Arc::new(OnceCell::new());
- let fill_vmo_task =
- fasync::unblock(move || ColocatedProgram::fill_vmo(vmo, vmo_size, filled_sender));
let terminated_clone = terminated.clone();
let guard = scopeguard::guard((), move |()| {
_ = terminated_clone.set_blocking(());
@@ -44,22 +41,29 @@
// which happens when this task is dropped.
let _guard = guard;
- fill_vmo_task.await;
-
// Signal to the outside world that the pages have been allocated.
- for info in numbered_handles.into_iter().filter(|info| {
- match fuchsia_runtime::HandleInfo::try_from(info.id) {
+ let handle_info = numbered_handles
+ .into_iter()
+ .filter(|info| match fuchsia_runtime::HandleInfo::try_from(info.id) {
Ok(handle_info) => {
handle_info == fuchsia_runtime::HandleInfo::new(HandleType::User0, 0)
}
Err(_) => false,
- }
- }) {
- let handle = zx::EventPair::from(info.handle);
- match handle.signal_peer(zx::Signals::empty(), zx::Signals::USER_0) {
- Ok(()) => {}
- Err(status) => {
- warn!("Failed to signal USER0 handle: {status}");
+ })
+ .next()
+ .unwrap();
+
+ let channel = zx::Channel::from(handle_info.handle);
+ let mut request_stream = fcolocated::ColocatedRequestStream::from_channel(
+ fasync::Channel::from_channel(channel),
+ );
+ while let Some(request) = request_stream.try_next().await.unwrap() {
+ match request {
+ fcolocated::ColocatedRequest::GetVmos { responder } => {
+ responder.send(&[vmo_koid.raw_koid()]).unwrap();
+ }
+ fcolocated::ColocatedRequest::_UnknownMethod { .. } => {
+ panic!("Unknown method");
}
}
}
@@ -68,41 +72,7 @@
std::future::pending().await
};
let task = fasync::Task::spawn(task);
- Ok(Self { task: Some(task), filled: Some(filled), terminated })
- }
-
- fn fill_vmo(vmo: zx::Vmo, vmo_size: u64, filled: oneshot::Sender<Mapping>) {
- // Map the VMO into the address space.
- let vmo_size = vmo_size as usize;
- let mut mapping = Mapping::create_from_vmo(
- &vmo,
- vmo_size,
- zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE,
- )
- .unwrap();
- let buffer = mapping.deref_mut();
-
- // Fill the VMO with randomized bytes, to cause pages to be physically allocated.
- // This approach will defeat page deduplication and page compression, for ease of
- // memory usage analysis. This program should more or less use `vmo_size` bytes.
- use rand::RngCore;
- let mut rng = rand::thread_rng();
- let mut offset: usize = 0;
- const BLOCK_SIZE: usize = 512;
- let mut bytes = vec![0u8; BLOCK_SIZE];
- loop {
- rng.fill_bytes(&mut bytes);
- buffer.write_at(offset, &bytes);
- offset += BLOCK_SIZE;
- if offset > vmo_size {
- break;
- }
- }
- buffer.release_writes();
-
- // Send the mapping to the program to be kept alive. This will keep those pages
- // committed.
- _ = filled.send(mapping);
+ Ok(Self { task: Some(task), terminated, vmo_koid })
}
/// Returns a future that will resolve when the program is terminated.
@@ -114,6 +84,12 @@
}
.boxed()
}
+
+ /// Returns the koid of the program's VMO, so the runner can report its memory as attributed to
+ /// this component.
+ pub fn get_vmo_koid(&self) -> Koid {
+ self.vmo_koid
+ }
}
#[async_trait]
@@ -125,11 +101,7 @@
fn stop<'a>(&mut self) -> BoxFuture<'a, ()> {
let task = self.task.take();
- let filled = self.filled.take();
async {
- if let Some(filled) = filled {
- _ = filled.await;
- }
if let Some(task) = task {
_ = task.cancel();
}
diff --git a/src/performance/memory/attribution/src/attribution_server.rs b/src/performance/memory/attribution/src/attribution_server.rs
index 6a282a1..45a3001b 100644
--- a/src/performance/memory/attribution/src/attribution_server.rs
+++ b/src/performance/memory/attribution/src/attribution_server.rs
@@ -39,23 +39,17 @@
/// [Observer] object using [AttributionServer::new_observer].
/// [Publisher]s, created using [AttributionServer::new_publisher], should be
/// used to push attribution changes.
-pub struct AttributionServer {
- inner: Arc<Mutex<AttributionServerInner>>,
+#[derive(Clone)]
+pub struct AttributionServerHandle {
+ inner: Arc<Mutex<AttributionServer>>,
}
-impl AttributionServer {
- /// Create a new memory attribution server.
- ///
- /// `state` is a function returning the complete attribution state (not partial updates).
- pub fn new(state: Box<GetAttributionFn>) -> Self {
- Self { inner: Arc::new(Mutex::new(AttributionServerInner::new(state))) }
- }
-
+impl AttributionServerHandle {
/// Create a new [Observer] that represents a single client.
///
/// Each FIDL client connection should get its own [Observer] object.
pub fn new_observer(&self, control_handle: fattribution::ProviderControlHandle) -> Observer {
- AttributionServerInner::register(&self.inner, control_handle)
+ AttributionServer::register(&self.inner, control_handle)
}
/// Create a new [Publisher] that can push updates to observers.
@@ -68,7 +62,7 @@
/// get calls. These will be notified when the state changes or immediately the first time
/// an `Observer` registers an observation.
pub struct Observer {
- inner: Arc<Mutex<AttributionServerInner>>,
+ inner: Arc<Mutex<AttributionServer>>,
}
impl Observer {
@@ -92,7 +86,7 @@
/// A [Publisher] should be used to send updates to [Observer]s.
pub struct Publisher {
- inner: Arc<Mutex<AttributionServerInner>>,
+ inner: Arc<Mutex<AttributionServer>>,
}
impl Publisher {
@@ -109,14 +103,19 @@
}
}
-struct AttributionServerInner {
+pub struct AttributionServer {
state: Box<GetAttributionFn>,
consumer: Option<AttributionConsumer>,
}
-impl AttributionServerInner {
- pub fn new(state: Box<GetAttributionFn>) -> Self {
- Self { state, consumer: None }
+impl AttributionServer {
+ /// Create a new memory attribution server.
+ ///
+ /// `state` is a function returning the complete attribution state (not partial updates).
+ pub fn new(state: Box<GetAttributionFn>) -> AttributionServerHandle {
+ AttributionServerHandle {
+ inner: Arc::new(Mutex::new(AttributionServer { state, consumer: None })),
+ }
}
pub fn on_update(
@@ -266,7 +265,12 @@
/// Create a new [AttributionConsumer] without an `observer` and an initial `dirty`
/// value of `true`.
pub fn new(observer_control_handle: fattribution::ProviderControlHandle) -> Self {
- AttributionConsumer { first: true, pending: HashMap::new(), observer_control_handle: observer_control_handle, responder: None }
+ AttributionConsumer {
+ first: true,
+ pending: HashMap::new(),
+ observer_control_handle: observer_control_handle,
+ responder: None,
+ }
}
/// Register a new observation request. The observer will be notified immediately if
@@ -488,7 +492,6 @@
assert_matches!(result2, Err(ClientChannelClosed { status: zx::Status::BAD_STATE, .. }));
assert_matches!(result, Err(ClientChannelClosed { status: zx::Status::BAD_STATE, .. }));
-
}
/// Tests that the first get call returns the full state, not updates.
@@ -513,14 +516,14 @@
.detach();
server
- .new_publisher()
- .on_update(vec![fattribution::AttributionUpdate::Update(
- fattribution::UpdatedPrincipal {
- identifier: Some(fattribution::Identifier::Self_(fattribution::Self_)),
- ..Default::default()
- },
- )])
- .expect("Error sending the update");
+ .new_publisher()
+ .on_update(vec![fattribution::AttributionUpdate::Update(
+ fattribution::UpdatedPrincipal {
+ identifier: Some(fattribution::Identifier::Self_(fattribution::Self_)),
+ ..Default::default()
+ },
+ )])
+ .expect("Error sending the update");
// As this is the first call, we should get the full state, not the update.
let attributions =
diff --git a/src/performance/memory/attribution/src/lib.rs b/src/performance/memory/attribution/src/lib.rs
index b032c2b..d3dcc98 100644
--- a/src/performance/memory/attribution/src/lib.rs
+++ b/src/performance/memory/attribution/src/lib.rs
@@ -4,4 +4,7 @@
mod attribution_server;
-pub use attribution_server::{AttributionServer, AttributionServerObservationError};
+pub use attribution_server::{
+ AttributionServer, AttributionServerHandle, AttributionServerObservationError, Observer,
+ Publisher,
+};
diff --git a/src/sys/component_manager/src/framework/introspector.rs b/src/sys/component_manager/src/framework/introspector.rs
index 8fd9ddd..74e2be2 100644
--- a/src/sys/component_manager/src/framework/introspector.rs
+++ b/src/sys/component_manager/src/framework/introspector.rs
@@ -121,6 +121,9 @@
lazy_static! {
static ref MEMORY_MONITOR: Moniker =
Moniker::parse_str("/core/memory_monitor").unwrap();
+ /// Moniker for integration tests.
+ static ref RECEIVER: Moniker =
+ Moniker::parse_str("receiver").unwrap();
};
// TODO(https://fxbug.dev/318904493): Temporary workaround to prevent other components from
// using `Introspector` while improvements to framework capability allowlists are under way.
@@ -131,7 +134,10 @@
// realm, then exposed from `/`.
//
// All other cases are disallowed.
- if target.moniker != *MEMORY_MONITOR && !target.moniker.is_root() {
+ if target.moniker != *MEMORY_MONITOR
+ && target.moniker != *RECEIVER
+ && !target.moniker.is_root()
+ {
return Box::new(AccessDeniedCapabilityProvider {
target,
source_moniker: scope.moniker,
diff --git a/src/sys/lib/elf_runner/src/memory/reporter.rs b/src/sys/lib/elf_runner/src/memory/reporter.rs
index dfcaba8..4c5ea85 100644
--- a/src/sys/lib/elf_runner/src/memory/reporter.rs
+++ b/src/sys/lib/elf_runner/src/memory/reporter.rs
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use attribution::AttributionServer;
+use attribution::{AttributionServer, AttributionServerHandle};
use fidl::endpoints::RequestStream;
use fidl::endpoints::{ControlHandle, DiscoverableProtocolMarker};
use fidl_fuchsia_io as fio;
@@ -16,7 +16,7 @@
use crate::{component::ElfComponentInfo, ComponentSet};
pub struct MemoryReporter {
- server: AttributionServer,
+ server: AttributionServerHandle,
components: Arc<ComponentSet>,
}