0

Add memory usage metric for OCR and Main Content Extraction services.

Memory usage of OCR and Main Content Extraction services are monitored
and the max usage per process instance is recorded.
Older metrics on memory state before OCR service are removed since the
new metrics better indicate the usage and can be used in future to stop
service initialization when enough memory is not available.

OBSOLETE_HISTOGRAMS=Accessibility.OCR.Service.MemoryBefore.{status}.Available is replaced by Accessibility.{ServiceName}.Service.MaxMemoryLoad
OBSOLETE_HISTOGRAMS=Accessibility.OCR.Service.MemoryBefore.{status}.Pressure is replaced by Accessibility.{ServiceName}.Service.MaxMemoryLoad
OBSOLETE_HISTOGRAMS=Accessibility.OCR.Service.MemoryBefore.{status}.Total is replaced by Accessibility.{ServiceName}.Service.MaxMemoryLoad

AX-Relotes: n/a
Bug: 369722831
Change-Id: I23bfe03ef6f3db5d4739c38d04398ee272ce8534
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6552375
Commit-Queue: Ramin Halavati <rhalavati@chromium.org>
Reviewed-by: Mark Schillaci <mschillaci@google.com>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: Kyungjun Lee <kyungjunlee@google.com>
Cr-Commit-Position: refs/heads/main@{#1464563}
This commit is contained in:
Ramin Halavati
2025-05-22 22:08:33 -07:00
committed by Chromium LUCI CQ
parent 0d0b8701cf
commit db6d35a43d
12 changed files with 243 additions and 80 deletions

@ -46,6 +46,8 @@ source_set("screen_ai_install_state") {
source_set("screen_ai_service_router_factory") {
sources = [
"resource_monitor.cc",
"resource_monitor.h",
"screen_ai_service_handler_base.cc",
"screen_ai_service_handler_base.h",
"screen_ai_service_handler_main_content_extraction.cc",
@ -62,6 +64,7 @@ source_set("screen_ai_service_router_factory") {
":screen_ai_install_state",
"//chrome/browser/profiles:profile",
"//components/keyed_service/core",
"//components/performance_manager:performance_manager",
"//content/public/browser",
"//services/screen_ai/public/cpp:utilities",
"//services/screen_ai/public/mojom:factory",

@ -5,5 +5,6 @@ include_rules = [
"+components/keyed_service/content",
"+components/prefs",
"+components/component_updater",
"+components/performance_manager",
"+content/public/browser",
]

@ -44,6 +44,12 @@ namespace {
constexpr base::TimeDelta kServiceIdleCheckingDelay = base::Seconds(3);
// LINT.ThenChange(//services/screen_ai/screen_ai_service_impl.cc:kIdleCheckingDelay)
#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !BUILDFLAG(USE_FAKE_SCREEN_AI)
// LINT.IfChange(kResourceMeasurementInterval)
constexpr base::TimeDelta kResourceMeasurementInterval = base::Seconds(1);
// LINT.ThenChange(//chrome/browser/screen_ai/resource_monitor.cc:kSampleInterval)
#endif
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
@ -758,8 +764,11 @@ IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
PerformOCRMultipleClientsNoWaitBetween) {
base::HistogramTester histograms;
base::ScopedAllowBlockingForTesting allow_blocking;
base::TimeTicks start_time = base::TimeTicks::Now();
// Create multiple OCR clients.
scoped_refptr<OpticalCharacterRecognizer> ocr_clients[kTestFilenamesCount];
{
@ -814,6 +823,32 @@ IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
auto& results = future.Get<mojom::VisualAnnotationPtr>();
EXPECT_TRUE(results->lines.size());
}
// Disconnect all clients.
for (auto& ocr : ocr_clients) {
ocr.reset();
}
// Wait for the service to shutdown and store metrics.
screen_ai::ScreenAIServiceRouter* router =
ScreenAIServiceRouterFactory::GetForBrowserContext(browser()->profile());
base::test::TestFuture<void> future;
WaitForDisconnecting(router, future.GetCallback(), /*remaining_tries=*/2);
ASSERT_TRUE(future.Wait());
ASSERT_FALSE(router->IsProcessRunningForTesting(
screen_ai::ScreenAIServiceRouter::Service::kOCR));
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
// Memory use and lifetime measurements should be recorded only once since all
// tasks were done with the same process.
histograms.ExpectTotalCount("Accessibility.OCR.Service.LifeTime", 1);
// Since this metric is recorded at long intervals, ensure the test has been
// running long enough to record it.
if (base::TimeTicks::Now() - start_time > kResourceMeasurementInterval * 2) {
histograms.ExpectTotalCount("Accessibility.OCR.Service.MaxMemoryLoad", 1);
}
}
#endif // BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !

@ -0,0 +1,82 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/screen_ai/resource_monitor.h"
#include <algorithm>
#include "base/memory/ptr_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/performance_manager/public/resource_attribution/query_results.h"
#include "content/public/browser/browser_child_process_host.h"
#include "content/public/browser/browser_child_process_host_iterator.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/common/process_type.h"
namespace {
// LINT.IfChange(kSampleInterval)
constexpr base::TimeDelta kSampleInterval = base::Seconds(1);
// LINT.ThenChange(//chrome/browser/screen_ai/optical_character_recognizer_browsertest.cc:kResourceMeasurementInterval)
} // namespace
namespace screen_ai {
// static
std::unique_ptr<ResourceMonitor> ResourceMonitor::CreateForProcess(
std::string_view process_name) {
std::u16string u16name = base::UTF8ToUTF16(process_name);
content::BrowserChildProcessHost* process_host = nullptr;
content::BrowserChildProcessHostIterator iter(content::PROCESS_TYPE_UTILITY);
while (!iter.Done()) {
if (iter.GetData().name == u16name) {
process_host =
content::BrowserChildProcessHost::FromID(iter.GetData().id);
break;
}
++iter;
}
if (!process_host) {
return nullptr;
}
auto process_context =
resource_attribution::ProcessContext::FromBrowserChildProcessHost(
process_host);
return process_context
? base::WrapUnique(new ResourceMonitor(process_context.value()))
: nullptr;
}
ResourceMonitor::ResourceMonitor(
resource_attribution::ProcessContext& process_context)
: scoped_query_(resource_attribution::QueryBuilder()
.AddResourceType(
resource_attribution::ResourceType::kMemorySummary)
.AddResourceContext(process_context)
.CreateScopedQuery()) {
query_observation_.Observe(&scoped_query_);
scoped_query_.Start(kSampleInterval);
}
ResourceMonitor::~ResourceMonitor() = default;
void ResourceMonitor::OnResourceUsageUpdated(
const resource_attribution::QueryResultMap& results) {
if (results.size() == 0) {
// Service process couldn't be measured.
return;
}
CHECK_EQ(results.size(), 1ul);
const resource_attribution::QueryResults& result = results.begin()->second;
const std::optional<resource_attribution::MemorySummaryResult>& memory =
result.memory_summary_result;
if (memory.has_value()) {
max_resident_memory_kb_ =
std::max(max_resident_memory_kb_, memory->resident_set_size_kb);
}
}
} // namespace screen_ai

@ -0,0 +1,45 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_SCREEN_AI_RESOURCE_MONITOR_H_
#define CHROME_BROWSER_SCREEN_AI_RESOURCE_MONITOR_H_
#include <string_view>
#include "components/performance_manager/public/resource_attribution/process_context.h"
#include "components/performance_manager/public/resource_attribution/queries.h"
namespace screen_ai {
class ResourceMonitor : public resource_attribution::QueryResultObserver {
public:
static std::unique_ptr<ResourceMonitor> CreateForProcess(
std::string_view process_name);
~ResourceMonitor() override;
ResourceMonitor(const ResourceMonitor&) = delete;
ResourceMonitor& operator=(const ResourceMonitor&) = delete;
// QueryResultObserver:
void OnResourceUsageUpdated(
const resource_attribution::QueryResultMap& results) override;
uint64_t get_max_resident_memory_kb() const {
return max_resident_memory_kb_;
}
private:
explicit ResourceMonitor(
resource_attribution::ProcessContext& process_context);
resource_attribution::ScopedResourceUsageQuery scoped_query_;
resource_attribution::ScopedQueryObservation query_observation_{this};
uint64_t max_resident_memory_kb_ = 0;
};
} // namespace screen_ai
#endif // CHROME_BROWSER_SCREEN_AI_RESOURCE_MONITOR_H_

@ -14,6 +14,8 @@
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
@ -178,6 +180,18 @@ void ScreenAIServiceHandlerBase::OnScreenAIServiceDisconnected() {
screen_ai_service_factory_.reset();
CallPendingStatusRequests(false);
if (resource_monitor_) {
if (resource_monitor_->get_max_resident_memory_kb()) {
base::UmaHistogramMemoryMB(
GetMetricFullName("MaxMemoryLoad"),
resource_monitor_->get_max_resident_memory_kb() / 1000);
}
resource_monitor_.reset();
base::UmaHistogramMediumTimes(GetMetricFullName("LifeTime"),
base::TimeTicks::Now() - service_start_time_);
}
screen_ai_service_shutdown_handler_.reset();
if (shutdown_handler_data_.shutdown_message_received) {
if (shutdown_handler_data_.crash_count) {
@ -185,7 +199,6 @@ void ScreenAIServiceHandlerBase::OnScreenAIServiceDisconnected() {
shutdown_handler_data_.crash_count);
}
shutdown_handler_data_.crash_count = 0;
OnDisconnected(/*crashed=*/false);
return;
}
@ -201,7 +214,6 @@ void ScreenAIServiceHandlerBase::OnScreenAIServiceDisconnected() {
weak_ptr_factory_.GetWeakPtr()),
suspense_time);
VLOG(0) << "Service suspended due to crash for: " << suspense_time;
OnDisconnected(/*crashed=*/true);
}
void ScreenAIServiceHandlerBase::CallPendingStatusRequests(bool successful) {
@ -238,8 +250,6 @@ void ScreenAIServiceHandlerBase::LaunchIfNotRunning() {
return;
}
PerformPrelaunchSteps();
base::FilePath binary_path = state_instance->get_component_binary_path();
#if BUILDFLAG(IS_WIN)
std::vector<base::FilePath> preload_libraries = {binary_path};
@ -249,10 +259,11 @@ void ScreenAIServiceHandlerBase::LaunchIfNotRunning() {
binary_path.MaybeAsASCII().c_str())};
#endif // BUILDFLAG(IS_WIN)
std::string process_name = base::StrCat({GetServiceName(), " Service"});
content::ServiceProcessHost::Launch(
screen_ai_service_factory_.BindNewPipeAndPassReceiver(),
content::ServiceProcessHost::Options()
.WithDisplayName(GetServiceName() + " Service")
.WithDisplayName(process_name)
#if BUILDFLAG(IS_WIN)
.WithPreloadedLibraries(
preload_libraries,
@ -260,6 +271,9 @@ void ScreenAIServiceHandlerBase::LaunchIfNotRunning() {
#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
.WithExtraCommandLineSwitches(extra_switches)
#endif // BUILDFLAG(IS_WIN)
.WithProcessCallback(
base::BindOnce(&ScreenAIServiceHandlerBase::OnServiceLaunched,
weak_ptr_factory_.GetWeakPtr(), process_name))
.Pass());
shutdown_handler_data_.shutdown_message_received = false;
@ -271,6 +285,25 @@ void ScreenAIServiceHandlerBase::LaunchIfNotRunning() {
weak_ptr_factory_.GetWeakPtr()));
}
void ScreenAIServiceHandlerBase::OnServiceLaunched(
const std::string& process_name,
const base::Process& process) {
// Post task to ensure that `resource_monitor_` is created after the service
// process host is registered.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ScreenAIServiceHandlerBase::CreateResourceMonitor,
weak_ptr_factory_.GetWeakPtr(), process_name));
service_start_time_ = base::TimeTicks::Now();
}
void ScreenAIServiceHandlerBase::CreateResourceMonitor(
const std::string& process_name) {
CHECK(!resource_monitor_);
resource_monitor_ = ResourceMonitor::CreateForProcess(process_name);
CHECK(resource_monitor_);
}
void ScreenAIServiceHandlerBase::InitializeServiceIfNeeded() {
std::optional<bool> service_state = GetServiceState();
if (service_state) {

@ -7,14 +7,20 @@
#include <optional>
#include <set>
#include <string_view>
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "chrome/browser/screen_ai/resource_monitor.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/screen_ai/public/mojom/screen_ai_factory.mojom.h"
namespace base {
class Process;
}
namespace screen_ai {
using ServiceStateCallback = base::OnceCallback<void(bool)>;
@ -70,6 +76,13 @@ class ScreenAIServiceHandlerBase
// Launches the service if it's not already launched.
void LaunchIfNotRunning();
// Called after service is launched.
void OnServiceLaunched(const std::string& process_name,
const base::Process& process);
// Creates resource monitor to track CPU and memory load.
void CreateResourceMonitor(const std::string& process_name);
// True if service is already initialized, false if it is disabled, and
// nullopt if not known.
std::optional<bool> GetServiceState();
@ -90,18 +103,19 @@ class ScreenAIServiceHandlerBase
virtual bool IsConnectionBound() const = 0;
virtual bool IsServiceEnabled() const = 0;
virtual void ResetConnection() = 0;
virtual void OnDisconnected(bool crashed) = 0;
virtual void PerformPrelaunchSteps() = 0;
// Pending requests to receive service state for each service type.
std::vector<ServiceStateCallback> pending_state_requests_;
std::unique_ptr<ResourceMonitor> resource_monitor_;
struct ShutdownHandlerData {
bool shutdown_message_received = false;
bool suspended = false;
int crash_count = 0;
} shutdown_handler_data_;
base::TimeTicks service_start_time_;
mojo::Receiver<screen_ai::mojom::ScreenAIServiceShutdownHandler>
screen_ai_service_shutdown_handler_;

@ -38,8 +38,6 @@ class ScreenAIServiceHandlerMainContentExtraction
bool IsConnectionBound() const override;
bool IsServiceEnabled() const override;
void ResetConnection() override;
void OnDisconnected(bool crashed) override {}
void PerformPrelaunchSteps() override {}
void InitializeService(
base::TimeTicks request_start_time,

@ -11,7 +11,6 @@
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/memory_pressure_monitor.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
@ -59,19 +58,6 @@ void ScreenAIServiceHandlerOCR::ResetConnection() {
service_.reset();
}
void ScreenAIServiceHandlerOCR::OnDisconnected(bool crashed) {
std::string prefix = GetMetricFullName("MemoryBefore.");
prefix += crashed ? "Crash." : "Shutdown.";
if (memory_stats_before_launch_.pressure_available) {
base::UmaHistogramEnumeration(prefix + "Pressure",
memory_stats_before_launch_.pressure_level);
}
base::UmaHistogramCounts100000(prefix + "Total",
memory_stats_before_launch_.total_memory);
base::UmaHistogramCounts100000(prefix + "Available",
memory_stats_before_launch_.available_memory);
}
void ScreenAIServiceHandlerOCR::BindService(
mojo::PendingReceiver<mojom::ScreenAIAnnotator> receiver) {
InitializeServiceIfNeeded();
@ -81,23 +67,6 @@ void ScreenAIServiceHandlerOCR::BindService(
}
}
void ScreenAIServiceHandlerOCR::PerformPrelaunchSteps() {
// Keep memory stats for metrics after shutdown or crash.
memory_stats_before_launch_.total_memory =
base::SysInfo::AmountOfPhysicalMemoryMB();
memory_stats_before_launch_.available_memory = static_cast<int>(
base::SysInfo::AmountOfAvailablePhysicalMemory() / (1024 * 1024));
const auto* const memory_monitor = base::MemoryPressureMonitor::Get();
if (memory_monitor) {
memory_stats_before_launch_.pressure_available = true;
memory_stats_before_launch_.pressure_level =
memory_monitor->GetCurrentPressureLevel();
} else {
memory_stats_before_launch_.pressure_available = false;
}
}
void ScreenAIServiceHandlerOCR::LoadModelFilesAndInitialize(
base::TimeTicks request_start_time) {
base::ThreadPool::PostTaskAndReplyWithResult(

@ -8,7 +8,6 @@
#include <optional>
#include <set>
#include "base/memory/memory_pressure_listener.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "chrome/browser/screen_ai/screen_ai_service_handler_base.h"
@ -37,20 +36,11 @@ class ScreenAIServiceHandlerOCR : public ScreenAIServiceHandlerBase {
bool IsConnectionBound() const override;
bool IsServiceEnabled() const override;
void ResetConnection() override;
void OnDisconnected(bool crashed) override;
void PerformPrelaunchSteps() override;
void InitializeService(base::TimeTicks request_start_time,
mojo::PendingReceiver<mojom::OCRService> receiver,
std::unique_ptr<ComponentFiles> model_files);
struct MemoryStatsBeforeLaunch {
int total_memory; // in MB.
int available_memory; // in MB.
bool pressure_available;
base::MemoryPressureListener::MemoryPressureLevel pressure_level;
} memory_stats_before_launch_;
mojo::Remote<mojom::OCRService> service_;
base::WeakPtrFactory<ScreenAIServiceHandlerOCR> weak_ptr_factory_{this};

@ -25,8 +25,6 @@ class TestScreenAIServiceHandler : public ScreenAIServiceHandlerBase {
bool IsConnectionBound() const override { return service_connected_; }
bool IsServiceEnabled() const override { return true; }
void ResetConnection() override { service_connected_ = false; }
void OnDisconnected(bool crashed) override { service_connected_ = false; }
void PerformPrelaunchSteps() override {}
private:
bool service_connected_ = false;

@ -2203,34 +2203,6 @@ even if they fit the naming pattern. -->
</summary>
</histogram>
<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Available"
units="MB" expires_after="2026-03-01">
<owner>rhalavati@chromium.org</owner>
<owner>chrome-a11y-core@google.com</owner>
<summary>
Available memory before launching OCR service, when service {status}.
</summary>
<token key="status" variants="TerminationStatus"/>
</histogram>
<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Pressure"
enum="MemoryPressureLevel" expires_after="2026-03-01">
<owner>rhalavati@chromium.org</owner>
<owner>chrome-a11y-core@google.com</owner>
<summary>
Memory pressure level before launching OCR service, when service {status}.
</summary>
<token key="status" variants="TerminationStatus"/>
</histogram>
<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Total"
units="MB" expires_after="2026-03-01">
<owner>rhalavati@chromium.org</owner>
<owner>chrome-a11y-core@google.com</owner>
<summary>Total memory when OCR service {status}.</summary>
<token key="status" variants="TerminationStatus"/>
</histogram>
<histogram name="Accessibility.OOBEStartupSoundDelay" units="ms"
expires_after="never">
<!-- expires-never: Core metric for monitoring OOBE accessibility status. -->
@ -3688,6 +3660,29 @@ even if they fit the naming pattern. -->
<token key="ServiceName" variants="ScreenAIServices"/>
</histogram>
<histogram name="Accessibility.{ServiceName}.Service.LifeTime" units="ms"
expires_after="2026-03-06">
<owner>rhalavati@chromium.org</owner>
<owner>chrome-a11y-core@google.com</owner>
<summary>
Records the duration of {ServiceName} service. The metric is stored after
the process shuts down or crashes.
</summary>
<token key="ServiceName" variants="ScreenAIServices"/>
</histogram>
<histogram name="Accessibility.{ServiceName}.Service.MaxMemoryLoad" units="MB"
expires_after="2026-03-06">
<owner>rhalavati@chromium.org</owner>
<owner>chrome-a11y-core@google.com</owner>
<summary>
Records the maximum memory usage of {ServiceName} service. The metric is
stored after the process shuts down or crashes and up to 1GB and only if the
process runs for more than one second.
</summary>
<token key="ServiceName" variants="ScreenAIServices"/>
</histogram>
<histogram name="DomDistiller.AdaBoostModel.NegativeScore" units="count"
expires_after="2025-10-26">
<owner>wylieb@google.com</owner>