[go: nahoru, domu]

Add the framework for the ResourceAttribution query API

This adds the QueryBuilder and ScopedResourceUsageQuery interface
classes. They register with the query scheduler but don't send any
queries yet.

R=fdoray

Bug: 1471683
Change-Id: I639b461c7ba4f63bc4cd2187971747579c3b7520
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5009744
Commit-Queue: Joe Mason <joenotcharles@google.com>
Reviewed-by: Francois Pierre Doray <fdoray@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1222097}
diff --git a/components/performance_manager/BUILD.gn b/components/performance_manager/BUILD.gn
index 2223617..9620239 100644
--- a/components/performance_manager/BUILD.gn
+++ b/components/performance_manager/BUILD.gn
@@ -169,8 +169,10 @@
     "public/resource_attribution/frame_context.h",
     "public/resource_attribution/page_context.h",
     "public/resource_attribution/process_context.h",
+    "public/resource_attribution/queries.h",
     "public/resource_attribution/query_results.h",
     "public/resource_attribution/resource_contexts.h",
+    "public/resource_attribution/resource_types.h",
     "public/resource_attribution/scoped_cpu_query.h",
     "public/resource_attribution/type_helpers.h",
     "public/resource_attribution/worker_context.h",
@@ -194,6 +196,9 @@
     "resource_attribution/graph_change.h",
     "resource_attribution/page_context.cc",
     "resource_attribution/process_context.cc",
+    "resource_attribution/queries.cc",
+    "resource_attribution/query_params.cc",
+    "resource_attribution/query_params.h",
     "resource_attribution/query_scheduler.cc",
     "resource_attribution/query_scheduler.h",
     "resource_attribution/scoped_cpu_query.cc",
@@ -355,6 +360,7 @@
     "resource_attribution/frame_context_unittest.cc",
     "resource_attribution/page_context_unittest.cc",
     "resource_attribution/process_context_unittest.cc",
+    "resource_attribution/queries_unittest.cc",
     "resource_attribution/query_scheduler_unittest.cc",
     "resource_attribution/resource_contexts_unittest.cc",
     "resource_attribution/type_helpers_unittest.cc",
diff --git a/components/performance_manager/public/resource_attribution/queries.h b/components/performance_manager/public/resource_attribution/queries.h
new file mode 100644
index 0000000..b67c52c8
--- /dev/null
+++ b/components/performance_manager/public/resource_attribution/queries.h
@@ -0,0 +1,163 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_QUERIES_H_
+#define COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_QUERIES_H_
+
+#include <memory>
+
+#include "base/gtest_prod_util.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/observer_list_threadsafe.h"
+#include "base/sequence_checker.h"
+#include "base/types/pass_key.h"
+#include "base/types/variant_util.h"
+#include "components/performance_manager/public/resource_attribution/query_results.h"
+#include "components/performance_manager/public/resource_attribution/resource_contexts.h"
+#include "components/performance_manager/public/resource_attribution/resource_types.h"
+#include "components/performance_manager/public/resource_attribution/type_helpers.h"
+
+namespace performance_manager::resource_attribution {
+
+namespace internal {
+struct QueryParams;
+}
+
+class QueryBuilder;
+
+// An observer that's notified by ScopedResourceUsageQuery whenever new results
+// are available.
+class QueryResultObserver {
+ public:
+  virtual ~QueryResultObserver() = default;
+  virtual void OnResourceUsageUpdated(const QueryResultMap& results) = 0;
+};
+
+// Repeatedly makes resource attribution queries on a schedule as long as it's
+// in scope.
+// TODO(crbug.com/1471683): Unfinished. This registers on create and delete,
+// which may have important side effects, but doesn't make any queries yet.
+class ScopedResourceUsageQuery {
+ public:
+  ~ScopedResourceUsageQuery();
+
+  // Move-only.
+  ScopedResourceUsageQuery(ScopedResourceUsageQuery&&);
+  ScopedResourceUsageQuery& operator=(ScopedResourceUsageQuery&&);
+  ScopedResourceUsageQuery(const ScopedResourceUsageQuery&) = delete;
+  ScopedResourceUsageQuery& operator=(const ScopedResourceUsageQuery&) = delete;
+
+  // Adds an observer that will be notified on the calling sequence. Can be
+  // called from any sequence.
+  void AddObserver(QueryResultObserver* observer);
+
+  // Removes an observer. Must be called from the same sequence as
+  // AddObserver().
+  void RemoveObserver(QueryResultObserver* observer);
+
+  // Restricted implementation methods:
+
+  // Gives tests access to validate the implementation.
+  internal::QueryParams* GetParamsForTesting() const;
+
+  // Private constructor for QueryBuilder. Use QueryBuilder::CreateScopedQuery()
+  // to create a query.
+  ScopedResourceUsageQuery(base::PassKey<QueryBuilder>,
+                           std::unique_ptr<internal::QueryParams> params);
+
+ private:
+  using ObserverList = base::ObserverListThreadSafe<
+      QueryResultObserver,
+      base::RemoveObserverPolicy::kAddingSequenceOnly>;
+
+  FRIEND_TEST_ALL_PREFIXES(ScopedResourceUsageQueryTest, Movable);
+  FRIEND_TEST_ALL_PREFIXES(ScopedResourceUsageQueryTest, Observers);
+
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  // Parameters passed from the QueryBuilder.
+  std::unique_ptr<internal::QueryParams> params_
+      GUARDED_BY_CONTEXT(sequence_checker_);
+
+  scoped_refptr<ObserverList> observer_list_ =
+      base::MakeRefCounted<ObserverList>();
+};
+
+// Creates a query to request resource usage measurements on a schedule.
+//
+// Use CreateScopedQuery() to return an object that makes repeated measurements
+// as long as it's in scope, or QueryOnce() to take a single measurement.
+//
+// Example usage:
+//
+//   // To invoke `callback` with the CPU usage of all processes.
+//   QueryBuilder()
+//       .AddAllContextsOfType<ProcessContext>()
+//       .AddResourceType(ResourceType::kCPUTime)
+//       .QueryOnce(callback);
+//
+// QueryBuilder is move-only to prevent accidentally copying large state. Use
+// Clone() to make an explicit copy.
+//
+// TODO(crbug.com/1471683): Unfinished. This collects parameters but doesn't
+// make any queries yet.
+class QueryBuilder {
+ public:
+  QueryBuilder();
+  ~QueryBuilder();
+
+  // Move-only.
+  QueryBuilder(QueryBuilder&&);
+  QueryBuilder& operator=(QueryBuilder&&);
+  QueryBuilder(const QueryBuilder&) = delete;
+  QueryBuilder& operator=(const QueryBuilder&) = delete;
+
+  // Adds `context` to the list of resource contexts to query.
+  QueryBuilder& AddResourceContext(const ResourceContext& context);
+
+  // Adds all resource contexts of type ContextType to the list of resource
+  // contexts to query. Whenever the query causes a resource measurement, all
+  // resource contexts of the given type that exist at that moment will be
+  // measured.
+  template <typename ContextType,
+            internal::EnableIfIsVariantAlternative<ContextType,
+                                                   ResourceContext> = true>
+  QueryBuilder& AddAllContextsOfType() {
+    return AddAllContextsWithTypeIndex(
+        base::VariantIndexOfType<ResourceContext, ContextType>());
+  }
+
+  // Add `type` to the lists of resources to query.
+  QueryBuilder& AddResourceType(ResourceType resource_type);
+
+  // Returns a scoped object that will repeatedly run the query and notify
+  // observers with the results. Once this is called the QueryBuilder becomes
+  // invalid.
+  ScopedResourceUsageQuery CreateScopedQuery();
+
+  // Makes a copy of the QueryBuilder to use as a base for similar queries.
+  QueryBuilder Clone() const;
+
+  // Restricted implementation methods:
+
+  // Gives tests access to validate the implementation.
+  internal::QueryParams* GetParamsForTesting() const;
+
+ private:
+  // Private constructor for Clone().
+  explicit QueryBuilder(std::unique_ptr<internal::QueryParams> params);
+
+  // Implementation of AddAllContextsOfType().
+  QueryBuilder& AddAllContextsWithTypeIndex(size_t index);
+
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  // Parameters built up by the builder.
+  std::unique_ptr<internal::QueryParams> params_
+      GUARDED_BY_CONTEXT(sequence_checker_);
+};
+
+}  // namespace performance_manager::resource_attribution
+
+#endif  // COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_QUERIES_H_
diff --git a/components/performance_manager/public/resource_attribution/query_results.h b/components/performance_manager/public/resource_attribution/query_results.h
index 432c7b5..81d9949 100644
--- a/components/performance_manager/public/resource_attribution/query_results.h
+++ b/components/performance_manager/public/resource_attribution/query_results.h
@@ -83,6 +83,31 @@
   return internal::GetFromVariantVector<T>(results);
 }
 
+inline bool operator==(const ResultMetadata& a, const ResultMetadata& b) {
+  static_assert(sizeof(ResultMetadata) ==
+                    sizeof(decltype(ResultMetadata::measurement_time)),
+                "update operator== when changing ResultMetadata");
+  return a.measurement_time == b.measurement_time;
+}
+
+inline bool operator!=(const ResultMetadata& a, const ResultMetadata& b) {
+  return !(a == b);
+}
+
+inline bool operator==(const CPUTimeResult& a, const CPUTimeResult& b) {
+  static_assert(sizeof(CPUTimeResult) ==
+                    sizeof(decltype(CPUTimeResult::metadata)) +
+                        sizeof(decltype(CPUTimeResult::start_time)) +
+                        sizeof(decltype(CPUTimeResult::cumulative_cpu)),
+                "update operator== when changing CPUTimeResult");
+  return a.metadata == b.metadata && a.start_time == b.start_time &&
+         a.cumulative_cpu == b.cumulative_cpu;
+}
+
+inline bool operator!=(const CPUTimeResult& a, const CPUTimeResult& b) {
+  return !(a == b);
+}
+
 }  // namespace performance_manager::resource_attribution
 
 #endif  // COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_QUERY_RESULTS_H_
diff --git a/components/performance_manager/public/resource_attribution/resource_types.h b/components/performance_manager/public/resource_attribution/resource_types.h
new file mode 100644
index 0000000..eaa9c31a
--- /dev/null
+++ b/components/performance_manager/public/resource_attribution/resource_types.h
@@ -0,0 +1,23 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_RESOURCE_TYPES_H_
+#define COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_RESOURCE_TYPES_H_
+
+#include "base/containers/enum_set.h"
+
+namespace performance_manager::resource_attribution {
+
+// Types of resources that Resource Attribution can measure.
+enum class ResourceType {
+  // CPU usage, measured in time spent on CPU.
+  kCPUTime,
+};
+
+using ResourceTypeSet =
+    base::EnumSet<ResourceType, ResourceType::kCPUTime, ResourceType::kCPUTime>;
+
+}  // namespace performance_manager::resource_attribution
+
+#endif  // COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_RESOURCE_ATTRIBUTION_RESOURCE_TYPES_H_
diff --git a/components/performance_manager/resource_attribution/queries.cc b/components/performance_manager/resource_attribution/queries.cc
new file mode 100644
index 0000000..ccca88b
--- /dev/null
+++ b/components/performance_manager/resource_attribution/queries.cc
@@ -0,0 +1,132 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/performance_manager/public/resource_attribution/queries.h"
+
+#include <bitset>
+#include <set>
+#include <utility>
+
+#include "base/check.h"
+#include "base/containers/enum_set.h"
+#include "base/functional/bind.h"
+#include "components/performance_manager/resource_attribution/query_params.h"
+#include "components/performance_manager/resource_attribution/query_scheduler.h"
+
+namespace performance_manager::resource_attribution {
+
+namespace {
+
+using QueryParams = internal::QueryParams;
+
+void AddScopedQueryToScheduler(QueryParams* query_params,
+                               QueryScheduler* scheduler) {
+  scheduler->AddScopedQuery(query_params);
+}
+
+void RemoveScopedQueryFromScheduler(std::unique_ptr<QueryParams> query_params,
+                                    QueryScheduler* scheduler) {
+  scheduler->RemoveScopedQuery(std::move(query_params));
+}
+
+}  // namespace
+
+ScopedResourceUsageQuery::~ScopedResourceUsageQuery() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!params_) {
+    // `params_` was moved to another ScopedResourceUsageQuery.
+    return;
+  }
+  // Notify the scheduler this query no longer exists. Sends the QueryParams to
+  // the scheduler to delete to be sure they're valid until the scheduler reads
+  // them.
+  QueryScheduler::CallOnGraphWithScheduler(
+      base::BindOnce(&RemoveScopedQueryFromScheduler, std::move(params_)));
+}
+
+ScopedResourceUsageQuery::ScopedResourceUsageQuery(ScopedResourceUsageQuery&&) =
+    default;
+
+ScopedResourceUsageQuery& ScopedResourceUsageQuery::operator=(
+    ScopedResourceUsageQuery&&) = default;
+
+void ScopedResourceUsageQuery::AddObserver(QueryResultObserver* observer) {
+  // ObserverListThreadSafe can be called on any sequence.
+  observer_list_->AddObserver(observer);
+}
+
+void ScopedResourceUsageQuery::RemoveObserver(QueryResultObserver* observer) {
+  // Must be called on the same sequence as AddObserver. ObserverListThreadSafe
+  // will validate this.
+  observer_list_->RemoveObserver(observer);
+}
+
+internal::QueryParams* ScopedResourceUsageQuery::GetParamsForTesting() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return params_.get();
+}
+
+ScopedResourceUsageQuery::ScopedResourceUsageQuery(
+    base::PassKey<QueryBuilder>,
+    std::unique_ptr<QueryParams> params)
+    : params_(std::move(params)) {
+  // Notify the scheduler this query exists.
+  QueryScheduler::CallOnGraphWithScheduler(
+      base::BindOnce(&AddScopedQueryToScheduler, params_.get()));
+}
+
+QueryBuilder::QueryBuilder() : params_(std::make_unique<QueryParams>()) {}
+
+QueryBuilder::~QueryBuilder() = default;
+
+QueryBuilder::QueryBuilder(QueryBuilder&&) = default;
+
+QueryBuilder& QueryBuilder::operator=(QueryBuilder&&) = default;
+
+QueryBuilder& QueryBuilder::AddResourceContext(const ResourceContext& context) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(params_);
+  params_->resource_contexts.insert(context);
+  return *this;
+}
+
+QueryBuilder& QueryBuilder::AddResourceType(ResourceType resource_type) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(params_);
+  params_->resource_types.Put(resource_type);
+  return *this;
+}
+
+ScopedResourceUsageQuery QueryBuilder::CreateScopedQuery() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  // Pass ownership of `params_` to the scoped query, to avoid copying the
+  // parameter contents.
+  return ScopedResourceUsageQuery(base::PassKey<QueryBuilder>(),
+                                  std::move(params_));
+}
+
+QueryBuilder QueryBuilder::Clone() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  // Clone the parameter contents to a newly-allocated QueryParams with the copy
+  // constructor.
+  auto cloned_params = std::make_unique<QueryParams>(*params_);
+  return QueryBuilder(std::move(cloned_params));
+}
+
+internal::QueryParams* QueryBuilder::GetParamsForTesting() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  return params_.get();
+}
+
+QueryBuilder::QueryBuilder(std::unique_ptr<internal::QueryParams> params)
+    : params_(std::move(params)) {}
+
+QueryBuilder& QueryBuilder::AddAllContextsWithTypeIndex(size_t index) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(params_);
+  params_->all_context_types.set(index);
+  return *this;
+}
+
+}  // namespace performance_manager::resource_attribution
diff --git a/components/performance_manager/resource_attribution/queries_unittest.cc b/components/performance_manager/resource_attribution/queries_unittest.cc
new file mode 100644
index 0000000..0dfbff8
--- /dev/null
+++ b/components/performance_manager/resource_attribution/queries_unittest.cc
@@ -0,0 +1,310 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/performance_manager/public/resource_attribution/queries.h"
+
+#include <bitset>
+#include <map>
+#include <set>
+#include <type_traits>
+#include <utility>
+
+#include "base/barrier_closure.h"
+#include "base/containers/enum_set.h"
+#include "base/dcheck_is_on.h"
+#include "base/functional/callback.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/observer_list_threadsafe.h"
+#include "base/run_loop.h"
+#include "base/time/time.h"
+#include "components/performance_manager/embedder/graph_features.h"
+#include "components/performance_manager/public/graph/graph.h"
+#include "components/performance_manager/public/graph/page_node.h"
+#include "components/performance_manager/public/graph/process_node.h"
+#include "components/performance_manager/public/resource_attribution/query_results.h"
+#include "components/performance_manager/public/resource_attribution/resource_contexts.h"
+#include "components/performance_manager/public/resource_attribution/resource_types.h"
+#include "components/performance_manager/resource_attribution/cpu_measurement_monitor.h"
+#include "components/performance_manager/resource_attribution/query_params.h"
+#include "components/performance_manager/resource_attribution/query_scheduler.h"
+#include "components/performance_manager/test_support/graph_test_harness.h"
+#include "components/performance_manager/test_support/mock_graphs.h"
+#include "components/performance_manager/test_support/performance_manager_test_harness.h"
+#include "components/performance_manager/test_support/run_in_graph.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/navigation_simulator.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+#include "url/gurl.h"
+
+namespace performance_manager::resource_attribution {
+
+namespace {
+
+using QueryParams = internal::QueryParams;
+
+class LenientMockQueryResultObserver : public QueryResultObserver {
+ public:
+  MOCK_METHOD(void,
+              OnResourceUsageUpdated,
+              (const QueryResultMap& results),
+              (override));
+};
+using MockQueryResultObserver =
+    ::testing::StrictMock<LenientMockQueryResultObserver>;
+
+// Test QueryBuilder using mock graphs.
+using QueryBuilderTest = GraphTestHarness;
+
+// Test ScopedResourceUsageQuery with PerformanceManagerTestHarness to test its
+// interactions on the PM sequence.
+class ScopedResourceUsageQueryTest : public PerformanceManagerTestHarness {
+ protected:
+  using Super = PerformanceManagerTestHarness;
+
+  void SetUp() override {
+    GetGraphFeatures().EnableResourceAttributionScheduler();
+    Super::SetUp();
+
+    // Navigate to an initial page.
+    SetContents(CreateTestWebContents());
+    content::RenderFrameHost* rfh =
+        content::NavigationSimulator::NavigateAndCommitFromBrowser(
+            web_contents(), GURL("https://a.com/"));
+    ASSERT_TRUE(rfh);
+    main_frame_context = FrameContext::FromRenderFrameHost(rfh);
+    ASSERT_TRUE(main_frame_context.has_value());
+  }
+
+  // A ResourceContext for the main frame.
+  absl::optional<FrameContext> main_frame_context;
+};
+
+}  // namespace
+
+TEST_F(QueryBuilderTest, Params) {
+  MockSinglePageInSingleProcessGraph mock_graph(graph());
+
+  QueryBuilder builder;
+  ASSERT_TRUE(builder.GetParamsForTesting());
+  EXPECT_EQ(*builder.GetParamsForTesting(), QueryParams{});
+
+  QueryBuilder& builder_ref =
+      builder.AddResourceContext(mock_graph.page->GetResourceContext())
+          .AddResourceContext(mock_graph.process->GetResourceContext())
+          .AddAllContextsOfType<FrameContext>()
+          .AddAllContextsOfType<WorkerContext>()
+          .AddResourceType(ResourceType::kCPUTime);
+  EXPECT_EQ(builder.GetParamsForTesting(), builder_ref.GetParamsForTesting());
+
+  constexpr size_t kFrameContextBit = 0;
+  constexpr size_t kWorkerContextBit = 3;
+  static_assert(
+      std::is_same_v<
+          absl::variant_alternative_t<kFrameContextBit, ResourceContext>,
+          FrameContext>,
+      "FrameContext is no longer index 0 in the ResourceContext variant, "
+      "please update the test");
+  static_assert(
+      std::is_same_v<
+          absl::variant_alternative_t<kWorkerContextBit, ResourceContext>,
+          WorkerContext>,
+      "WorkerContext is no longer index 3 in the ResourceContext variant, "
+      "please update the test");
+
+  QueryParams expected_params;
+  expected_params.resource_contexts = {
+      mock_graph.page->GetResourceContext(),
+      mock_graph.process->GetResourceContext()};
+  expected_params.all_context_types.set(kFrameContextBit);
+  expected_params.all_context_types.set(kWorkerContextBit);
+  expected_params.resource_types = {ResourceType::kCPUTime};
+
+  EXPECT_EQ(*builder.GetParamsForTesting(), expected_params);
+
+  // Creating a ScopedQuery invalidates the builder.
+  auto scoped_query = builder.CreateScopedQuery();
+  EXPECT_FALSE(builder.GetParamsForTesting());
+  ASSERT_TRUE(scoped_query.GetParamsForTesting());
+  EXPECT_EQ(*scoped_query.GetParamsForTesting(), expected_params);
+}
+
+TEST_F(QueryBuilderTest, Clone) {
+  MockSinglePageInSingleProcessGraph mock_graph(graph());
+  QueryBuilder builder;
+  builder.AddResourceContext(mock_graph.page->GetResourceContext())
+      .AddAllContextsOfType<FrameContext>()
+      .AddResourceType(ResourceType::kCPUTime);
+  QueryBuilder cloned_builder = builder.Clone();
+
+  ASSERT_TRUE(builder.GetParamsForTesting());
+  ASSERT_TRUE(cloned_builder.GetParamsForTesting());
+  EXPECT_EQ(*builder.GetParamsForTesting(),
+            *cloned_builder.GetParamsForTesting());
+
+  // Cloned builder can be modified independently.
+  builder.AddResourceContext(mock_graph.process->GetResourceContext());
+  cloned_builder.AddResourceContext(mock_graph.frame->GetResourceContext());
+
+  const std::set<ResourceContext> expected_contexts{
+      mock_graph.page->GetResourceContext(),
+      mock_graph.process->GetResourceContext()};
+  EXPECT_EQ(builder.GetParamsForTesting()->resource_contexts,
+            expected_contexts);
+  const std::set<ResourceContext> expected_cloned_contexts{
+      mock_graph.page->GetResourceContext(),
+      mock_graph.frame->GetResourceContext()};
+  EXPECT_EQ(cloned_builder.GetParamsForTesting()->resource_contexts,
+            expected_cloned_contexts);
+}
+
+TEST_F(ScopedResourceUsageQueryTest, AddRemoveScopedQuery) {
+  QueryScheduler* scheduler = nullptr;
+  RunInGraph([&](Graph* graph) {
+    scheduler = QueryScheduler::GetFromGraph(graph);
+    ASSERT_TRUE(scheduler);
+    EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+  // Abort the whole test if the scheduler wasn't found.
+  ASSERT_TRUE(scheduler);
+
+  absl::optional<ScopedResourceUsageQuery> scoped_query =
+      QueryBuilder()
+          .AddResourceType(ResourceType::kCPUTime)
+          .CreateScopedQuery();
+  RunInGraph([&] {
+    EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+  scoped_query.reset();
+  RunInGraph([&] {
+    EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+}
+
+TEST_F(ScopedResourceUsageQueryTest, Movable) {
+  QueryScheduler* scheduler = nullptr;
+  RunInGraph([&](Graph* graph) {
+    scheduler = QueryScheduler::GetFromGraph(graph);
+    ASSERT_TRUE(scheduler);
+    EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+  // Abort the whole test if the scheduler wasn't found.
+  ASSERT_TRUE(scheduler);
+
+  absl::optional<ScopedResourceUsageQuery> outer_query;
+  {
+    ScopedResourceUsageQuery inner_query =
+        QueryBuilder()
+            .AddResourceType(ResourceType::kCPUTime)
+            .CreateScopedQuery();
+    RunInGraph([&] {
+      EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+    });
+
+    auto* params = inner_query.GetParamsForTesting();
+    EXPECT_TRUE(params);
+    scoped_refptr<ScopedResourceUsageQuery::ObserverList> observer_list =
+        inner_query.observer_list_;
+    EXPECT_TRUE(observer_list);
+
+    outer_query = std::move(inner_query);
+
+    // Moving invalidates the original query.
+    EXPECT_FALSE(inner_query.GetParamsForTesting());
+    EXPECT_EQ(outer_query->GetParamsForTesting(), params);
+
+    // There shouldn't be duplicate observers, to prevent extra notifications.
+    EXPECT_FALSE(inner_query.observer_list_);
+    EXPECT_EQ(outer_query->observer_list_, observer_list);
+  }
+
+  // `inner_query` should not notify the scheduler when it goes out of scope.
+  RunInGraph([&] {
+    EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+  outer_query.reset();
+  RunInGraph([&] {
+    EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  });
+}
+
+TEST_F(ScopedResourceUsageQueryTest, Observers) {
+  ScopedResourceUsageQuery scoped_query =
+      QueryBuilder()
+          .AddResourceContext(main_frame_context.value())
+          .AddResourceType(ResourceType::kCPUTime)
+          .CreateScopedQuery();
+
+  const QueryResultMap test_results{
+      {main_frame_context.value(),
+       {CPUTimeResult{.cumulative_cpu = base::Minutes(1)}}},
+  };
+
+  // Safely do nothing when no observers are registered.
+  Graph* graph_ptr = nullptr;
+  RunInGraph([&](Graph* graph) {
+    graph_ptr = graph;
+    // TODO(crbug.com/1471683): QueryScheduler should be notifying the
+    // observers.
+    scoped_query.observer_list_->Notify(
+        FROM_HERE, &QueryResultObserver::OnResourceUsageUpdated, test_results);
+  });
+  ASSERT_TRUE(graph_ptr);
+
+  // Observer can be notified from the graph sequence when installed on any
+  // thread.
+  MockQueryResultObserver main_thread_observer;
+  MockQueryResultObserver graph_sequence_observer;
+  scoped_query.AddObserver(&main_thread_observer);
+  RunInGraph([&] { scoped_query.AddObserver(&graph_sequence_observer); });
+
+  auto check_graph_sequence = [&](bool expect_on_graph_sequence) {
+#if DCHECK_IS_ON()
+    EXPECT_EQ(graph_ptr->IsOnGraphSequence(), expect_on_graph_sequence);
+#endif
+  };
+
+  // Quit the RunLoop when both observers receive results.
+  base::RunLoop run_loop;
+  auto quit_closure = base::BarrierClosure(2, run_loop.QuitClosure());
+  EXPECT_CALL(main_thread_observer, OnResourceUsageUpdated(test_results))
+      .WillOnce([&] {
+        check_graph_sequence(false);
+        quit_closure.Run();
+      });
+  EXPECT_CALL(graph_sequence_observer, OnResourceUsageUpdated(test_results))
+      .WillOnce([&] {
+        check_graph_sequence(true);
+        quit_closure.Run();
+      });
+
+  RunInGraph([&] {
+    scoped_query.observer_list_->Notify(
+        FROM_HERE, &QueryResultObserver::OnResourceUsageUpdated, test_results);
+  });
+
+  // Wait for all notifications.
+  run_loop.Run();
+}
+
+TEST_F(ScopedResourceUsageQueryTest, GraphTeardown) {
+  // ScopedResourceUsageQuery registers with the QueryScheduler on creation and
+  // unregisters on destruction. Make sure it's safe for it to outlive the
+  // scheduler, which is deleted during graph teardown.
+  absl::optional<ScopedResourceUsageQuery> scoped_query =
+      QueryBuilder()
+          .AddResourceContext(main_frame_context.value())
+          .AddResourceType(ResourceType::kCPUTime)
+          .CreateScopedQuery();
+
+  TearDownNow();
+
+  // The test passes as long as this doesn't crash.
+  scoped_query.reset();
+}
+
+}  // namespace performance_manager::resource_attribution
diff --git a/components/performance_manager/resource_attribution/query_params.cc b/components/performance_manager/resource_attribution/query_params.cc
new file mode 100644
index 0000000..a0d32546
--- /dev/null
+++ b/components/performance_manager/resource_attribution/query_params.cc
@@ -0,0 +1,17 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/performance_manager/resource_attribution/query_params.h"
+
+namespace performance_manager::resource_attribution::internal {
+
+QueryParams::QueryParams() = default;
+
+QueryParams::~QueryParams() = default;
+
+QueryParams::QueryParams(const QueryParams& other) = default;
+
+QueryParams& QueryParams::operator=(const QueryParams& other) = default;
+
+}  // namespace performance_manager::resource_attribution::internal
diff --git a/components/performance_manager/resource_attribution/query_params.h b/components/performance_manager/resource_attribution/query_params.h
new file mode 100644
index 0000000..bf8ceb0
--- /dev/null
+++ b/components/performance_manager/resource_attribution/query_params.h
@@ -0,0 +1,56 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_PERFORMANCE_MANAGER_RESOURCE_ATTRIBUTION_QUERY_PARAMS_H_
+#define COMPONENTS_PERFORMANCE_MANAGER_RESOURCE_ATTRIBUTION_QUERY_PARAMS_H_
+
+#include <bitset>
+#include <set>
+
+#include "components/performance_manager/public/resource_attribution/resource_contexts.h"
+#include "components/performance_manager/public/resource_attribution/resource_types.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace performance_manager::resource_attribution::internal {
+
+struct QueryParams {
+  // Context types that can be added with AddAllContextsOfType, based on their
+  // index in the ResourceContexts variant.
+  using ContextTypeSet =
+      std::bitset<absl::variant_size<ResourceContext>::value>;
+
+  QueryParams();
+  ~QueryParams();
+
+  QueryParams(const QueryParams& other);
+  QueryParams& operator=(const QueryParams& other);
+
+  // Individual resource contexts to measure.
+  std::set<ResourceContext> resource_contexts;
+
+  // For each of these context types, all contexts that exist will be measured.
+  ContextTypeSet all_context_types;
+
+  // Resource types to measure.
+  ResourceTypeSet resource_types;
+};
+
+inline bool operator==(const QueryParams& a, const QueryParams& b) {
+  static_assert(sizeof(QueryParams) ==
+                    sizeof(decltype(QueryParams::resource_contexts)) +
+                        sizeof(decltype(QueryParams::all_context_types)) +
+                        sizeof(decltype(QueryParams::resource_types)),
+                "update operator== when changing QueryParams");
+  return a.resource_contexts == b.resource_contexts &&
+         a.all_context_types == b.all_context_types &&
+         a.resource_types == b.resource_types;
+}
+
+inline bool operator!=(const QueryParams& a, const QueryParams& b) {
+  return !(a == b);
+}
+
+}  // namespace performance_manager::resource_attribution::internal
+
+#endif  // COMPONENTS_PERFORMANCE_MANAGER_RESOURCE_ATTRIBUTION_QUERY_PARAMS_H_
diff --git a/components/performance_manager/resource_attribution/query_scheduler.cc b/components/performance_manager/resource_attribution/query_scheduler.cc
index de3167a..1779244 100644
--- a/components/performance_manager/resource_attribution/query_scheduler.cc
+++ b/components/performance_manager/resource_attribution/query_scheduler.cc
@@ -4,13 +4,30 @@
 
 #include "components/performance_manager/resource_attribution/query_scheduler.h"
 
+#include <utility>
+
 #include "base/check_op.h"
+#include "base/containers/enum_set.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/task/task_runner.h"
+#include "components/performance_manager/public/performance_manager.h"
+#include "components/performance_manager/resource_attribution/query_params.h"
 
 namespace performance_manager::resource_attribution {
 
+namespace {
+
+using QueryParams = internal::QueryParams;
+
+QueryScheduler* GetSchedulerFromGraph(Graph* graph) {
+  auto* scheduler = QueryScheduler::GetFromGraph(graph);
+  CHECK(scheduler);
+  return scheduler;
+}
+
+}  // namespace
+
 QueryScheduler::QueryScheduler() = default;
 
 QueryScheduler::~QueryScheduler() = default;
@@ -19,6 +36,15 @@
   return weak_factory_.GetWeakPtr();
 }
 
+// static
+void QueryScheduler::CallOnGraphWithScheduler(
+    base::OnceCallback<void(QueryScheduler*)> callback,
+    const base::Location& location) {
+  PerformanceManager::CallOnGraph(
+      location,
+      base::BindOnce(&GetSchedulerFromGraph).Then(std::move(callback)));
+}
+
 void QueryScheduler::AddCPUQuery() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   CHECK_NE(graph_, nullptr);
@@ -42,6 +68,28 @@
   }
 }
 
+void QueryScheduler::AddScopedQuery(QueryParams* query_params) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(query_params);
+  // TODO(crbug.com/1471683): Associate a notifier with the params so that when
+  // a scheduled measurement is done, the correct ScopedResourceUsageQuery can
+  // be notified.
+  if (query_params->resource_types.Has(ResourceType::kCPUTime)) {
+    AddCPUQuery();
+  }
+}
+
+void QueryScheduler::RemoveScopedQuery(
+    std::unique_ptr<QueryParams> query_params) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(query_params);
+  // TODO(crbug.com/1471683): Forget the notifier associated with the params.
+  if (query_params->resource_types.Has(ResourceType::kCPUTime)) {
+    RemoveCPUQuery();
+  }
+  // `query_params` goes out of scope and is deleted here.
+}
+
 void QueryScheduler::RequestCPUResults(
     base::OnceCallback<void(const QueryResultMap&)> callback,
     scoped_refptr<base::TaskRunner> task_runner) {
diff --git a/components/performance_manager/resource_attribution/query_scheduler.h b/components/performance_manager/resource_attribution/query_scheduler.h
index 034f481..505d248 100644
--- a/components/performance_manager/resource_attribution/query_scheduler.h
+++ b/components/performance_manager/resource_attribution/query_scheduler.h
@@ -5,7 +5,10 @@
 #ifndef COMPONENTS_PERFORMANCE_MANAGER_RESOURCE_ATTRIBUTION_QUERY_SCHEDULER_H_
 #define COMPONENTS_PERFORMANCE_MANAGER_RESOURCE_ATTRIBUTION_QUERY_SCHEDULER_H_
 
+#include <memory>
+
 #include "base/functional/callback_forward.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
@@ -21,7 +24,9 @@
 
 namespace performance_manager::resource_attribution {
 
-class CPUMeasurementMonitor;
+namespace internal {
+struct QueryParams;
+}
 
 // QueryScheduler keeps track of all queries for a particular resource type and
 // owns the machinery that performs measurements.
@@ -36,18 +41,36 @@
 
   base::WeakPtr<QueryScheduler> GetWeakPtr();
 
-  // CPU measurement accessors.
+  // Invokes `callback` on the PM sequence with a pointer to the registered
+  // QueryScheduler.
+  static void CallOnGraphWithScheduler(
+      base::OnceCallback<void(QueryScheduler*)> callback,
+      const base::Location& location = base::Location::Current());
 
   // Increases the CPU query count. `cpu_monitor_` will start monitoring CPU
   // usage when the count > 0.
+  // TODO(crbug.com/1471683): Make this private. It should only be called by
+  // AddScopedQuery().
   void AddCPUQuery();
 
   // Decreases the CPU query count. `cpu_monitor_` will stop monitoring CPU
   // usage when the count == 0.
+  // TODO(crbug.com/1471683): Make this private. It should only be called by
+  // RemoveScopedQuery().
   void RemoveCPUQuery();
 
+  // Adds a scoped query for `query_params`. Increases the query count for all
+  // resource types and contexts referenced in `query_params`.
+  void AddScopedQuery(internal::QueryParams* query_params);
+
+  // Decreases the query count for all resource types and contexts referenced in
+  // `query_params` and deletes `query_params`.
+  void RemoveScopedQuery(std::unique_ptr<internal::QueryParams> query_params);
+
   // Requests the latest CPU measurements from `cpu_monitor_`, and posts them
   // to `callback` on `task_runner`. Asserts that the CPU query count > 0.
+  // TODO(crbug.com/1471683): Replace with a general RequestResults that handles
+  // any QueryParams.
   void RequestCPUResults(
       base::OnceCallback<void(const QueryResultMap&)> callback,
       scoped_refptr<base::TaskRunner> task_runner);
diff --git a/components/performance_manager/resource_attribution/query_scheduler_unittest.cc b/components/performance_manager/resource_attribution/query_scheduler_unittest.cc
index 7989689..571df16 100644
--- a/components/performance_manager/resource_attribution/query_scheduler_unittest.cc
+++ b/components/performance_manager/resource_attribution/query_scheduler_unittest.cc
@@ -5,20 +5,33 @@
 #include "components/performance_manager/resource_attribution/query_scheduler.h"
 
 #include <memory>
+#include <utility>
 
+#include "base/containers/enum_set.h"
+#include "base/dcheck_is_on.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/functional/callback_helpers.h"
+#include "base/memory/weak_ptr.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
 #include "components/performance_manager/embedder/graph_features.h"
+#include "components/performance_manager/public/graph/graph.h"
+#include "components/performance_manager/public/graph/process_node.h"
+#include "components/performance_manager/public/resource_attribution/cpu_measurement_delegate.h"
+#include "components/performance_manager/public/resource_attribution/process_context.h"
 #include "components/performance_manager/public/resource_attribution/query_results.h"
+#include "components/performance_manager/public/resource_attribution/resource_types.h"
 #include "components/performance_manager/public/resource_attribution/scoped_cpu_query.h"
+#include "components/performance_manager/resource_attribution/cpu_measurement_monitor.h"
+#include "components/performance_manager/resource_attribution/query_params.h"
 #include "components/performance_manager/test_support/graph_test_harness.h"
 #include "components/performance_manager/test_support/mock_graphs.h"
+#include "components/performance_manager/test_support/performance_manager_test_harness.h"
 #include "components/performance_manager/test_support/resource_attribution/simulated_cpu_measurement_delegate.h"
+#include "components/performance_manager/test_support/run_in_graph.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -28,6 +41,13 @@
 
 using ::testing::Contains;
 using ::testing::Key;
+using QueryParams = internal::QueryParams;
+
+std::unique_ptr<QueryParams> CreateQueryParams(ResourceTypeSet resource_types) {
+  auto params = std::make_unique<QueryParams>();
+  params->resource_types = std::move(resource_types);
+  return params;
+}
 
 // Waits for a result from `query` and tests that it matches `matcher`.
 void ExpectQueryResult(ScopedCPUQuery* query, auto matcher) {
@@ -57,6 +77,8 @@
   SimulatedCPUMeasurementDelegateFactory delegate_factory_;
 };
 
+using QuerySchedulerPMTest = PerformanceManagerTestHarness;
+
 TEST_F(QuerySchedulerTest, CPUQueries) {
   MockSinglePageInSingleProcessGraph mock_graph(graph());
 
@@ -85,6 +107,33 @@
   EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
 }
 
+TEST_F(QuerySchedulerTest, ScopedQueries) {
+  auto* scheduler = QueryScheduler::GetFromGraph(graph());
+  ASSERT_TRUE(scheduler);
+
+  // Query without kCPUTime should not start CPU monitoring.
+  auto no_cpu_params = CreateQueryParams({});
+  scheduler->AddScopedQuery(no_cpu_params.get());
+  EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+
+  // First kCPUTime query should start monitoring.
+  auto cpu_params1 = CreateQueryParams({ResourceType::kCPUTime});
+  scheduler->AddScopedQuery(cpu_params1.get());
+  EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+
+  // Removing non-CPU query should not affect CPU monitoring.
+  scheduler->RemoveScopedQuery(std::move(no_cpu_params));
+  EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+
+  // CPU monitoring should not stop until the last CPU query is deleted.
+  auto cpu_params2 = CreateQueryParams({ResourceType::kCPUTime});
+  scheduler->AddScopedQuery(cpu_params2.get());
+  scheduler->RemoveScopedQuery(std::move(cpu_params1));
+  EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+  scheduler->RemoveScopedQuery(std::move(cpu_params2));
+  EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
+}
+
 TEST_F(QuerySchedulerTest, GraphTeardown) {
   // Make sure queries that still exist when the scheduler is deleted during
   // graph teardown safely return no data.
@@ -109,4 +158,26 @@
       base::ScopedClosureRunner(run_loop.QuitClosure())));
 }
 
+TEST_F(QuerySchedulerPMTest, CallOnGraphWithScheduler) {
+  QueryScheduler* scheduler_ptr = nullptr;
+  Graph* graph_ptr = nullptr;
+  RunInGraph([&](Graph* graph) {
+    auto scheduler = std::make_unique<QueryScheduler>();
+    scheduler_ptr = scheduler.get();
+    graph_ptr = graph;
+    graph->PassToGraph(std::move(scheduler));
+  });
+  ASSERT_TRUE(scheduler_ptr);
+  ASSERT_TRUE(graph_ptr);
+  base::RunLoop run_loop;
+  QueryScheduler::CallOnGraphWithScheduler(
+      base::BindLambdaForTesting([&](QueryScheduler* scheduler) {
+#if DCHECK_IS_ON()
+        EXPECT_TRUE(graph_ptr->IsOnGraphSequence());
+#endif
+        EXPECT_EQ(scheduler, scheduler_ptr);
+      }).Then(run_loop.QuitClosure()));
+  run_loop.Run();
+}
+
 }  // namespace performance_manager::resource_attribution