blob: f33ac7cd1a8bfc2e5b6fd57f7be9b50a7c264cae [file] [log] [blame]
// 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_scheduler.h"
#include <memory>
#include <set>
#include <utility>
#include "base/byte_count.h"
#include "base/containers/enum_set.h"
#include "base/location.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/performance_manager/embedder/graph_features.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/graph/worker_node_impl.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/resource_attribution/cpu_measurement_delegate.h"
#include "components/performance_manager/public/resource_attribution/queries.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/context_collection.h"
#include "components/performance_manager/resource_attribution/cpu_measurement_monitor.h"
#include "components/performance_manager/resource_attribution/performance_manager_aliases.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/resource_attribution/gtest_util.h"
#include "components/performance_manager/test_support/resource_attribution/measurement_delegates.h"
#include "content/public/browser/browsing_instance_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace resource_attribution::internal {
namespace {
using performance_manager::features::kResourceAttributionIncludeOrigins;
using ::testing::_;
using ::testing::Bool;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;
using ::testing::WithParamInterface;
std::unique_ptr<QueryParams> CreateQueryParams(
ResourceTypeSet resource_types = {},
std::set<ResourceContext> resource_contexts = {},
std::set<ResourceContextTypeId> all_context_types = {}) {
auto params = std::make_unique<QueryParams>();
params->resource_types = std::move(resource_types);
params->contexts = ContextCollection::CreateForTesting(
std::move(resource_contexts), std::move(all_context_types));
return params;
}
// Waits for a result from `query` and tests that it matches `matcher`.
void ExpectQueryResult(
QueryScheduler* scheduler,
QueryParams* query,
auto matcher,
const base::Location& location = base::Location::Current()) {
base::RunLoop run_loop;
scheduler->RequestResults(
*query,
base::BindLambdaForTesting([&](const QueryResultMap& query_results) {
EXPECT_THAT(query_results, matcher) << location.ToString();
}).Then(run_loop.QuitClosure()));
run_loop.Run();
}
} // namespace
class ResourceAttrQuerySchedulerTest
: public performance_manager::GraphTestHarness {
protected:
using Super = performance_manager::GraphTestHarness;
ResourceAttrQuerySchedulerTest()
: scoped_feature_list_(kResourceAttributionIncludeOrigins) {}
void SetUp() override {
GetGraphFeatures().EnableResourceAttributionScheduler();
Super::SetUp();
CPUMeasurementDelegate::SetDelegateFactoryForTesting(
graph(), &cpu_delegate_factory_);
MemoryMeasurementDelegate::SetDelegateFactoryForTesting(
graph(), &memory_delegate_factory_);
}
base::test::ScopedFeatureList scoped_feature_list_;
// These must be deleted after TearDown() so that they outlive the
// CPUMeasurementMonitor and MemoryMeasurementProvider.
SimulatedCPUMeasurementDelegateFactory cpu_delegate_factory_;
FakeMemoryMeasurementDelegateFactory memory_delegate_factory_;
};
TEST_F(ResourceAttrQuerySchedulerTest, AddRemoveQueries) {
performance_manager::MockMultiplePagesWithMultipleProcessesGraph mock_graph(
graph());
// Install fake memory results for all processes.
for (const ProcessNode* node :
{mock_graph.browser_process.get(), mock_graph.process.get(),
mock_graph.other_process.get()}) {
memory_delegate_factory_.memory_summaries()[node->GetResourceContext()] =
MemoryMeasurementDelegate::MemorySummaryMeasurement{
.resident_set_size = base::KiB(1),
.private_footprint = base::KiB(2),
.private_swap = base::KiB(3),
};
}
auto* scheduler = QueryScheduler::GetFromGraph(graph());
ASSERT_TRUE(scheduler);
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
// Queries without kCPUTime should not start CPU monitoring.
auto no_resource_query =
CreateQueryParams({}, {mock_graph.process->GetResourceContext()});
auto memory_query =
CreateQueryParams({ResourceType::kMemorySummary},
{mock_graph.process->GetResourceContext()});
scheduler->AddScopedQuery(no_resource_query.get());
scheduler->AddScopedQuery(memory_query.get());
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
// First kCPUTime query should start CPU monitoring.
auto cpu_query = CreateQueryParams(
{ResourceType::kCPUTime}, {mock_graph.process->GetResourceContext()});
scheduler->AddScopedQuery(cpu_query.get());
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
auto cpu_memory_query =
CreateQueryParams({ResourceType::kCPUTime, ResourceType::kMemorySummary},
/*resource_contexts=*/{},
{ResourceContextTypeId::ForType<ProcessContext>()});
scheduler->AddScopedQuery(cpu_memory_query.get());
// Simulate a query that uses Start() to request a measurement every minute.
// The other queries in this test simulate queries that call QueryOnce() after
// a minute without calling Start().
auto repeating_query = CreateQueryParams(
{ResourceType::kCPUTime}, {mock_graph.process->GetResourceContext()});
scheduler->AddScopedQuery(repeating_query.get());
scheduler->StartRepeatingQuery(repeating_query.get());
// Only the repeating query should have a QueryId.
EXPECT_EQ(no_resource_query->GetIdForTesting(), std::nullopt);
EXPECT_EQ(memory_query->GetIdForTesting(), std::nullopt);
EXPECT_EQ(cpu_memory_query->GetIdForTesting(), std::nullopt);
std::optional<QueryId> repeating_query_id =
repeating_query->GetIdForTesting();
ASSERT_TRUE(repeating_query_id.has_value());
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsTrackingQueryForTesting(
repeating_query_id.value()));
// Allow some time to pass to measure.
task_env().FastForwardBy(base::Minutes(1));
// Only the kCPUTime queries should receive CPU results.
ExpectQueryResult(scheduler, no_resource_query.get(), IsEmpty());
ExpectQueryResult(scheduler, memory_query.get(),
ElementsAre(ResultForContextMatches<MemorySummaryResult>(
mock_graph.process->GetResourceContext(), _)));
ExpectQueryResult(scheduler, cpu_query.get(),
ElementsAre(ResultForContextMatches<CPUTimeResult>(
mock_graph.process->GetResourceContext(), _)));
ExpectQueryResult(
scheduler, cpu_memory_query.get(),
UnorderedElementsAre(
ResultForContextMatchesAll<CPUTimeResult, MemorySummaryResult>(
mock_graph.process->GetResourceContext(), _, _),
ResultForContextMatchesAll<CPUTimeResult, MemorySummaryResult>(
mock_graph.other_process->GetResourceContext(), _, _),
ResultForContextMatchesAll<CPUTimeResult, MemorySummaryResult>(
mock_graph.browser_process->GetResourceContext(), _, _)));
ExpectQueryResult(scheduler, repeating_query.get(),
ElementsAre(ResultForContextMatches<CPUTimeResult>(
mock_graph.process->GetResourceContext(), _)));
// Removing non-CPU query should not affect CPU monitoring.
scheduler->RemoveScopedQuery(std::move(no_resource_query));
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
// CPU monitoring should not stop until the last CPU query is deleted.
scheduler->RemoveScopedQuery(std::move(repeating_query));
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsTrackingQueryForTesting(
repeating_query_id.value()));
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
scheduler->RemoveScopedQuery(std::move(cpu_query));
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
scheduler->RemoveScopedQuery(std::move(cpu_memory_query));
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
}
TEST_F(ResourceAttrQuerySchedulerTest, AddRemoveNodes) {
auto* scheduler = QueryScheduler::GetFromGraph(graph());
ASSERT_TRUE(scheduler);
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
auto process1 = CreateRendererProcessNode();
auto process2 = CreateRendererProcessNode();
auto process3 = CreateRendererProcessNode();
process1->SetProcess(base::Process::Current(), base::TimeTicks::Now());
process2->SetProcess(base::Process::Current(), base::TimeTicks::Now());
process3->SetProcess(base::Process::Current(), base::TimeTicks::Now());
const auto process_context1 = process1->GetResourceContext();
const auto process_context2 = process2->GetResourceContext();
const auto process_context3 = process3->GetResourceContext();
// Create a page with several origins, to validate that
// OriginInBrowsingInstanceContext results are cleared along with the
// PageContext.
constexpr content::BrowsingInstanceId kBrowsingInstance =
content::BrowsingInstanceId::FromUnsafeValue(1);
auto page1 = CreateNode<PageNodeImpl>();
const GURL kUrl1("https://a.com");
const url::Origin kOrigin1 = url::Origin::Create(kUrl1);
auto frame1 =
CreateFrameNodeAutoId(process3.get(), page1.get(),
/*parent_frame_node=*/nullptr, kBrowsingInstance);
frame1->OnNavigationCommitted(kUrl1, kOrigin1, /*same_document=*/false,
/*is_served_from_back_forward_cache=*/false);
const GURL kUrl2("https://b.com");
const url::Origin kOrigin2 = url::Origin::Create(kUrl2);
auto frame2 =
CreateFrameNodeAutoId(process3.get(), page1.get(),
/*parent_frame_node=*/nullptr, kBrowsingInstance);
frame2->OnNavigationCommitted(kUrl2, kOrigin2, /*same_document=*/false,
/*is_served_from_back_forward_cache=*/false);
const auto page_context1 = page1->GetResourceContext();
const auto frame_context1 = frame1->GetResourceContext();
const auto frame_context2 = frame2->GetResourceContext();
const auto origin_in_page_context1 =
OriginInBrowsingInstanceContext(kOrigin1, kBrowsingInstance);
const auto origin_in_page_context2 =
OriginInBrowsingInstanceContext(kOrigin2, kBrowsingInstance);
// Also test that WorkerContexts are tracked correctly.
auto worker1 = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kDedicated,
process3.get());
const auto worker_context1 = worker1->GetResourceContext();
// Simulates a query that never calls Start(), just uses QueryOnce() to
// request results periodically.
auto non_repeating_query =
CreateQueryParams({ResourceType::kCPUTime}, /*resource_contexts=*/{},
{ResourceContextTypeId::ForType<ProcessContext>()});
scheduler->AddScopedQuery(non_repeating_query.get());
// Simulates queries that call Start() to request results for all processes
// on a schedule.
auto repeating_all_process_query =
CreateQueryParams({ResourceType::kCPUTime}, /*resource_contexts=*/{},
{ResourceContextTypeId::ForType<ProcessContext>()});
scheduler->AddScopedQuery(repeating_all_process_query.get());
scheduler->StartRepeatingQuery(repeating_all_process_query.get());
auto repeating_all_process_query2 =
CreateQueryParams({ResourceType::kCPUTime}, /*resource_contexts=*/{},
{ResourceContextTypeId::ForType<ProcessContext>()});
scheduler->AddScopedQuery(repeating_all_process_query2.get());
scheduler->StartRepeatingQuery(repeating_all_process_query2.get());
// Simulates a query that calls Start() to request results for a fixed set of
// processes on a schedule.
auto repeating_some_process_query =
CreateQueryParams({ResourceType::kCPUTime},
{process_context1, process_context2, process_context3});
scheduler->AddScopedQuery(repeating_some_process_query.get());
scheduler->StartRepeatingQuery(repeating_some_process_query.get());
EXPECT_TRUE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
EXPECT_FALSE(non_repeating_query->GetIdForTesting().has_value());
ASSERT_TRUE(repeating_all_process_query->GetIdForTesting().has_value());
ASSERT_TRUE(repeating_all_process_query2->GetIdForTesting().has_value());
ASSERT_TRUE(repeating_some_process_query->GetIdForTesting().has_value());
// Allow some time to pass to measure.
task_env().FastForwardBy(base::Minutes(1));
int i = 0;
for (QueryParams* query :
{non_repeating_query.get(), repeating_all_process_query.get(),
repeating_all_process_query2.get(),
repeating_some_process_query.get()}) {
SCOPED_TRACE(::testing::Message() << "Query " << i++);
ExpectQueryResult(
scheduler, query,
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
}
// Delete a process after the measurement. Results should still be delivered
// to the repeating queries, but not the non-repeating query.
process1.reset();
task_env().FastForwardBy(base::Minutes(1));
ExpectQueryResult(
scheduler, non_repeating_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
ExpectQueryResult(
scheduler, repeating_all_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
task_env().FastForwardBy(base::Minutes(1));
ExpectQueryResult(
scheduler, non_repeating_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
// Should not see the result for `process1` twice.
ExpectQueryResult(
scheduler, repeating_all_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
// Seeing the result for `process1` for the first time.
ExpectQueryResult(
scheduler, repeating_all_process_query2.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
ExpectQueryResult(
scheduler, repeating_some_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
task_env().FastForwardBy(base::Minutes(1));
// All queries have now seen the result for `process1`.
i = 0;
for (QueryParams* query :
{non_repeating_query.get(), repeating_all_process_query.get(),
repeating_all_process_query2.get(),
repeating_some_process_query.get()}) {
SCOPED_TRACE(::testing::Message() << "Query " << i++);
ExpectQueryResult(
scheduler, query,
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
}
auto process4 = CreateRendererProcessNode();
process4->SetProcess(base::Process::Current(), base::TimeTicks::Now());
const auto process_context4 = process4->GetResourceContext();
process2.reset();
// Create a query that measures all context types.
// Since it's created after `process2` dies it should never see its result.
auto all_context_query = CreateQueryParams(
{ResourceType::kCPUTime}, /*resource_contexts=*/{},
{
ResourceContextTypeId::ForType<FrameContext>(),
ResourceContextTypeId::ForType<PageContext>(),
ResourceContextTypeId::ForType<ProcessContext>(),
ResourceContextTypeId::ForType<WorkerContext>(),
ResourceContextTypeId::ForType<OriginInBrowsingInstanceContext>(),
});
scheduler->AddScopedQuery(all_context_query.get());
scheduler->StartRepeatingQuery(all_context_query.get());
ASSERT_TRUE(all_context_query->GetIdForTesting().has_value());
task_env().FastForwardBy(base::Minutes(1));
ExpectQueryResult(
scheduler, non_repeating_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _)));
ExpectQueryResult(
scheduler, repeating_all_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _)));
ExpectQueryResult(
scheduler, repeating_some_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
ExpectQueryResult(
scheduler, all_context_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(frame_context1, _),
ResultForContextMatches<CPUTimeResult>(frame_context2, _),
ResultForContextMatches<CPUTimeResult>(page_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _),
ResultForContextMatches<CPUTimeResult>(worker_context1, _),
ResultForContextMatches<CPUTimeResult>(origin_in_page_context1, _),
ResultForContextMatches<CPUTimeResult>(origin_in_page_context2, _)));
process4.reset();
// Frames must be removed from the page before it's deleted.
frame1.reset();
frame2.reset();
page1.reset();
worker1.reset();
task_env().FastForwardBy(base::Minutes(1));
ExpectQueryResult(scheduler, non_repeating_query.get(),
UnorderedElementsAre(ResultForContextMatches<CPUTimeResult>(
process_context3, _)));
// Already seen the response for `process2`, now sees the last response for
// `process4`.
ExpectQueryResult(
scheduler, repeating_all_process_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _)));
// Now sees the last response for both `process2` and `process4`.
ExpectQueryResult(
scheduler, repeating_all_process_query2.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context2, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _)));
// Already seen the response for `process2`, not measuring `process4`.
ExpectQueryResult(scheduler, repeating_some_process_query.get(),
UnorderedElementsAre(ResultForContextMatches<CPUTimeResult>(
process_context3, _)));
// Never measured `process2`, now sees the last response for `process4` and
// all non-process contexts.
ExpectQueryResult(
scheduler, all_context_query.get(),
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(frame_context1, _),
ResultForContextMatches<CPUTimeResult>(frame_context2, _),
ResultForContextMatches<CPUTimeResult>(page_context1, _),
ResultForContextMatches<CPUTimeResult>(process_context3, _),
ResultForContextMatches<CPUTimeResult>(process_context4, _),
ResultForContextMatches<CPUTimeResult>(worker_context1, _),
ResultForContextMatches<CPUTimeResult>(origin_in_page_context1, _),
ResultForContextMatches<CPUTimeResult>(origin_in_page_context2, _)));
task_env().FastForwardBy(base::Minutes(1));
// All queries have now seen the results for all dead contexts. Only
// `process3` is live. Note: Results for dead
// `OriginInBrowsingInstanceContext`s are retained in case they are revived.
i = 0;
for (QueryParams* query :
{non_repeating_query.get(), repeating_all_process_query.get(),
repeating_all_process_query2.get(), repeating_some_process_query.get(),
all_context_query.get()}) {
SCOPED_TRACE(::testing::Message() << "Query " << i++);
ExpectQueryResult(
scheduler, query,
UnorderedElementsAre(
ResultForContextMatches<CPUTimeResult>(process_context3, _)));
}
// Now that each query got a measurement without the dead
// `OriginInBrowsingInstanceContext`s, no results should be retained.
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 0u);
process3.reset();
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 4u);
// As repeating queries are removed, results not reported to them should be
// dropped.
scheduler->RemoveScopedQuery(std::move(repeating_all_process_query));
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 3u);
scheduler->RemoveScopedQuery(std::move(repeating_all_process_query2));
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 2u);
scheduler->RemoveScopedQuery(std::move(repeating_some_process_query));
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 1u);
scheduler->RemoveScopedQuery(std::move(all_context_query));
EXPECT_EQ(
scheduler->GetCPUMonitorForTesting().GetDeadContextCountForTesting(), 0u);
scheduler->RemoveScopedQuery(std::move(non_repeating_query));
EXPECT_FALSE(scheduler->GetCPUMonitorForTesting().IsMonitoring());
}
} // namespace resource_attribution::internal