From f598afec85c6f6fc572fabbe38aa37bc8091a8ee Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Tue, 5 Mar 2024 10:40:59 -0800 Subject: [PATCH] Stabilize JavaScript Profiler in Cobalt (#2454) b/323983545 A major refactor was needed in order to prevent the JavaScript Profiler from SEGFAULTING due to a garbage-collected nullptr. This refactor includes the use of a `ProfilerGroup` class. The life of a cobalt::js_profiler::Profiler is extended by adding scoped_refptrs to the ProfilerGroup; thus preventing GC. Profilers live as long as their ProfilerGroup, which lives as long as the Isolate and Web Agent. One `ProfilerGroup` is bound to one `web::Agent`, and contains one `v8::Isolate`, one `v8::CpuProfiler`, and many `cobalt::js_profiler::Profiler`s. The major advantage of this refactor is that it does not crash, and is more faithful to the [original Chromium Implementation](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/profiler_group.cc;bpv=0;bpt=0). Test-On-Device: true --------- Co-authored-by: Ahmed Elzeiny (cherry picked from commit 84c616f8831fcd099e45d28c152fc00ec5323fcd) --- .gitignore | 1 + cobalt/base/tokens.h | 1 + cobalt/js_profiler/BUILD.gn | 2 + cobalt/js_profiler/js_profiler_test.cc | 41 ++++++++ cobalt/js_profiler/profiler.cc | 113 +++++++++----------- cobalt/js_profiler/profiler.h | 59 ++++++----- cobalt/js_profiler/profiler.idl | 9 +- cobalt/js_profiler/profiler_group.cc | 138 +++++++++++++++++++++++++ cobalt/js_profiler/profiler_group.h | 103 ++++++++++++++++++ cobalt/web/agent.cc | 12 +++ cobalt/web/context.h | 9 ++ cobalt/web/testing/BUILD.gn | 1 + cobalt/web/testing/stub_web_context.h | 14 +++ 13 files changed, 413 insertions(+), 90 deletions(-) create mode 100644 cobalt/js_profiler/profiler_group.cc create mode 100644 cobalt/js_profiler/profiler_group.h diff --git a/.gitignore b/.gitignore index 9a5a5caa9c01..31136b856f33 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /venv/* _certs/ .coverage +compile_commands.json diff --git a/cobalt/base/tokens.h b/cobalt/base/tokens.h index 0b82b64e8116..908ce2d0a17e 100644 --- a/cobalt/base/tokens.h +++ b/cobalt/base/tokens.h @@ -116,6 +116,7 @@ namespace base { MacroOpWithNameOnly(resourcetimingbufferfull) \ MacroOpWithNameOnly(result) \ MacroOpWithNameOnly(resume) \ + MacroOpWithNameOnly(samplebufferfull) \ MacroOpWithNameOnly(scroll) \ MacroOpWithNameOnly(securitypolicyviolation) \ MacroOpWithNameOnly(seeked) \ diff --git a/cobalt/js_profiler/BUILD.gn b/cobalt/js_profiler/BUILD.gn index 2a025d16f9e3..026c22065972 100644 --- a/cobalt/js_profiler/BUILD.gn +++ b/cobalt/js_profiler/BUILD.gn @@ -16,6 +16,8 @@ static_library("js_profiler") { sources = [ "profiler.cc", "profiler.h", + "profiler_group.cc", + "profiler_group.h", "profiler_trace_builder.cc", "profiler_trace_builder.h", "profiler_trace_wrapper.h", diff --git a/cobalt/js_profiler/js_profiler_test.cc b/cobalt/js_profiler/js_profiler_test.cc index 98bb584a206b..c2c52fa07a92 100644 --- a/cobalt/js_profiler/js_profiler_test.cc +++ b/cobalt/js_profiler/js_profiler_test.cc @@ -35,6 +35,10 @@ class ProfilerTest : public dom::testing::TestWithJavaScript { public: ProfilerTest() {} + void CollectGarbage() { + window_.web_context()->javascript_engine()->CollectGarbage(); + } + protected: dom::testing::StubWindow window_; StrictMock exception_state_; @@ -111,5 +115,42 @@ TEST_F(ProfilerTest, ProfilerJSCode) { EXPECT_TRUE(EvaluateScript("Profiler", &result)); EXPECT_EQ(result, "function Profiler() { [native code] }"); } + +TEST_F(ProfilerTest, ProfilerGroupDisposesOfCpuProfiler) { + v8::HandleScope scope(web::get_isolate(window_.environment_settings())); + ProfilerInitOptions init_options; + init_options.set_sample_interval(10); + init_options.set_max_buffer_size(1); + + auto profiler_group = ProfilerGroup::From(window_.environment_settings()); + EXPECT_FALSE(profiler_group->active()); + EXPECT_EQ(profiler_group->num_active_profilers(), 0); + + scoped_refptr profiler_(new Profiler( + window_.environment_settings(), init_options, &exception_state_)); + EXPECT_EQ(profiler_group->num_active_profilers(), 1); + EXPECT_TRUE(profiler_group->active()); + + auto promise = profiler_->Stop(window_.environment_settings()); + EXPECT_EQ(profiler_->stopped(), true); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(profiler_group->num_active_profilers(), 0); + EXPECT_FALSE(profiler_group->active()); +} + +TEST_F(ProfilerTest, ProfilerCanBeCancelled) { + v8::HandleScope scope(web::get_isolate(window_.environment_settings())); + ProfilerInitOptions init_options; + init_options.set_sample_interval(10); + init_options.set_max_buffer_size(1000); + + scoped_refptr profiler_(new Profiler( + window_.environment_settings(), init_options, &exception_state_)); + + profiler_->Cancel(); + EXPECT_EQ(profiler_->stopped(), true); +} + } // namespace js_profiler } // namespace cobalt diff --git a/cobalt/js_profiler/profiler.cc b/cobalt/js_profiler/profiler.cc index c380aed7c53f..21e4cdddb184 100644 --- a/cobalt/js_profiler/profiler.cc +++ b/cobalt/js_profiler/profiler.cc @@ -14,45 +14,27 @@ #include "cobalt/js_profiler/profiler.h" -#include #include #include #include #include -#include "cobalt/base/polymorphic_downcast.h" -#include "cobalt/js_profiler/profiler_trace_builder.h" +#include "base/logging.h" #include "cobalt/js_profiler/profiler_trace_wrapper.h" #include "cobalt/web/cache_utils.h" #include "cobalt/web/context.h" #include "cobalt/web/dom_exception.h" -#include "cobalt/web/environment_settings.h" #include "cobalt/web/environment_settings_helper.h" -namespace { -v8::Local toV8String(v8::Isolate* isolate, - const std::string& string) { - if (string.empty()) return v8::String::Empty(isolate); - return v8::String::NewFromUtf8(isolate, string.c_str(), - v8::NewStringType::kNormal, string.length()) - .ToLocalChecked(); -} -} // namespace - namespace cobalt { namespace js_profiler { -volatile uint32_t s_lastProfileId = 0; - -static constexpr int kBaseSampleIntervalMs = 10; - Profiler::Profiler(script::EnvironmentSettings* settings, ProfilerInitOptions options, script::ExceptionState* exception_state) - : cobalt::web::EventTarget(settings), - stopped_(false), - time_origin_{base::TimeTicks::Now()} { - profiler_id_ = nextProfileId(); + : stopped_(false), time_origin_{base::TimeTicks::Now()} { + profiler_group_ = ProfilerGroup::From(settings); + profiler_id_ = profiler_group_->NextProfilerId(); const base::TimeDelta sample_interval = base::Milliseconds(options.sample_interval()); @@ -66,21 +48,19 @@ Profiler::Profiler(script::EnvironmentSettings* settings, int effective_sample_interval_ms = static_cast(sample_interval.InMilliseconds()); - if (effective_sample_interval_ms % kBaseSampleIntervalMs != 0 || + if (effective_sample_interval_ms % Profiler::kBaseSampleIntervalMs != 0 || effective_sample_interval_ms == 0) { effective_sample_interval_ms += - (kBaseSampleIntervalMs - - effective_sample_interval_ms % kBaseSampleIntervalMs); + (Profiler::kBaseSampleIntervalMs - + effective_sample_interval_ms % Profiler::kBaseSampleIntervalMs); } sample_interval_ = effective_sample_interval_ms; - auto isolate = web::get_isolate(settings); - - auto status = ImplProfilingStart( - profiler_id_, + SB_LOG(INFO) << "[PROFILER] START " + profiler_id_; + auto status = profiler_group_->ProfilerStart( + this, settings, v8::CpuProfilingOptions(v8::kLeafNodeLineNumbers, - options.max_buffer_size(), sample_interval_us), - settings); + options.max_buffer_size(), sample_interval_us)); if (status == v8::CpuProfilingStatus::kAlreadyStarted) { web::DOMException::Raise(web::DOMException::kInvalidStateErr, @@ -91,46 +71,31 @@ Profiler::Profiler(script::EnvironmentSettings* settings, } } -Profiler::~Profiler() { - if (cpu_profiler_) { - cpu_profiler_->Dispose(); - cpu_profiler_ = nullptr; +void Profiler::AddEventListener( + script::EnvironmentSettings* environment_settings, const std::string& name, + const Profiler::SampleBufferFullCallbackHolder& holder) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + if (name != base::Tokens::samplebufferfull()) { + return; } + auto* global_wrappable = web::get_global_wrappable(environment_settings); + SampleBufferFullCallbackReference* token_callback = + new SampleBufferFullCallbackReference(global_wrappable, holder); + listeners_.push_back( + std::unique_ptr(token_callback)); } -v8::CpuProfilingStatus Profiler::ImplProfilingStart( - std::string profiler_id, v8::CpuProfilingOptions options, - script::EnvironmentSettings* settings) { - auto isolate = web::get_isolate(settings); - cpu_profiler_ = v8::CpuProfiler::New(isolate); - cpu_profiler_->SetSamplingInterval(kBaseSampleIntervalMs * - base::Time::kMicrosecondsPerMillisecond); - return cpu_profiler_->StartProfiling( - toV8String(isolate, profiler_id), options, - std::make_unique(this)); -} - -std::string Profiler::nextProfileId() { - s_lastProfileId++; - return "cobalt::profiler[" + std::to_string(s_lastProfileId) + "]"; -} - -void Profiler::PerformStop( - script::EnvironmentSettings* environment_settings, - std::unique_ptr promise_reference, - base::TimeTicks time_origin, std::string profiler_id) { - auto isolate = web::get_isolate(environment_settings); - auto profile = - cpu_profiler_->StopProfiling(toV8String(isolate, profiler_id_)); - auto trace = ProfilerTraceBuilder::FromProfile(profile, time_origin_); - scoped_refptr result(new ProfilerTraceWrapper(trace)); - cpu_profiler_->Dispose(); - cpu_profiler_ = nullptr; - promise_reference->value().Resolve(result); +void Profiler::DispatchSampleBufferFullEvent() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + for (auto it = listeners_.begin(); it != listeners_.end(); ++it) { + (*it)->value().Run(); + } + listeners_.clear(); } Profiler::ProfilerTracePromise Profiler::Stop( script::EnvironmentSettings* environment_settings) { + SB_LOG(INFO) << "[PROFILER] STOPPING " + profiler_id_; script::HandlePromiseWrappable promise = web::get_script_value_factory(environment_settings) ->CreateInterfacePromise>(); @@ -145,7 +110,7 @@ Profiler::ProfilerTracePromise Profiler::Stop( context->message_loop()->task_runner()->PostTask( FROM_HERE, base::BindOnce(&Profiler::PerformStop, base::Unretained(this), - environment_settings, std::move(promise_reference), + profiler_group_, std::move(promise_reference), std::move(time_origin_), std::move(profiler_id_))); } else { promise->Reject(new web::DOMException(web::DOMException::kInvalidStateErr, @@ -154,5 +119,23 @@ Profiler::ProfilerTracePromise Profiler::Stop( return promise; } +void Profiler::PerformStop( + ProfilerGroup* profiler_group, + std::unique_ptr promise_reference, + base::TimeTicks time_origin, std::string profiler_id) { + SB_LOG(INFO) << "[PROFILER] STOPPED " + profiler_id_; + auto trace = profiler_group->ProfilerStop(this); + scoped_refptr result(new ProfilerTraceWrapper(trace)); + promise_reference->value().Resolve(result); +} + +void Profiler::Cancel() { + if (!stopped_) { + stopped_ = true; + profiler_group_->ProfilerStop(this); + } + profiler_group_ = nullptr; +} + } // namespace js_profiler } // namespace cobalt diff --git a/cobalt/js_profiler/profiler.h b/cobalt/js_profiler/profiler.h index a6da1e77e63d..675f60b0a756 100644 --- a/cobalt/js_profiler/profiler.h +++ b/cobalt/js_profiler/profiler.h @@ -17,67 +17,78 @@ #include #include +#include +#include "base/threading/thread_checker.h" #include "cobalt/dom/performance_high_resolution_time.h" +#include "cobalt/js_profiler/profiler_group.h" #include "cobalt/js_profiler/profiler_init_options.h" #include "cobalt/js_profiler/profiler_trace.h" +#include "cobalt/script/callback_function.h" #include "cobalt/script/promise.h" +#include "cobalt/script/script_value.h" #include "cobalt/script/value_handle.h" #include "cobalt/script/wrappable.h" #include "cobalt/web/event_target.h" -#include "third_party/v8/include/cppgc/member.h" #include "third_party/v8/include/v8-profiler.h" namespace cobalt { namespace js_profiler { -class Profiler : public cobalt::web::EventTarget { +// Forward declaration of ProfilerGroup +class ProfilerGroup; + +// TODO(b/326337485): Profiler should be a subclass of EventTarget. +class Profiler : public script::Wrappable { public: - using ProfilerTracePromise = script::HandlePromiseWrappable; + static const int kBaseSampleIntervalMs = 10; + typedef script::HandlePromiseWrappable ProfilerTracePromise; + typedef script::CallbackFunction SampleBufferFullCallback; + typedef script::ScriptValue + SampleBufferFullCallbackHolder; + typedef SampleBufferFullCallbackHolder::Reference + SampleBufferFullCallbackReference; Profiler(script::EnvironmentSettings* settings, ProfilerInitOptions options, script::ExceptionState* exception_state); - ~Profiler(); + ~Profiler() override = default; + + void AddEventListener(script::EnvironmentSettings* environment_settings, + const std::string& name, + const SampleBufferFullCallbackHolder& listener); + + void DispatchSampleBufferFullEvent(); ProfilerTracePromise Stop(script::EnvironmentSettings* environment_settings); + void Cancel(); bool stopped() const { return stopped_; } dom::DOMHighResTimeStamp sample_interval() const { return sample_interval_; } + std::string ProfilerId() const { return profiler_id_; } + base::TimeTicks time_origin() const { return time_origin_; } DEFINE_WRAPPABLE_TYPE(Profiler); - virtual v8::CpuProfilingStatus ImplProfilingStart( - std::string profiler_id, v8::CpuProfilingOptions options, - script::EnvironmentSettings* settings); - private: - void PerformStop(script::EnvironmentSettings* environment_settings, + void PerformStop(ProfilerGroup* profiler_group, std::unique_ptr promise_reference, base::TimeTicks time_origin, std::string profiler_id); - std::string nextProfileId(); - bool stopped_; dom::DOMHighResTimeStamp sample_interval_; - v8::CpuProfiler* cpu_profiler_ = nullptr; base::TimeTicks time_origin_; std::string profiler_id_; -}; + ProfilerGroup* profiler_group_; + // All samplebufferfull listeners. Prevents GC on callbacks owned by this + // object, by binding to global-wrappable. + std::vector> listeners_; -class ProfilerMaxSamplesDelegate : public v8::DiscardedSamplesDelegate { - public: - explicit ProfilerMaxSamplesDelegate(Profiler* profiler) - : profiler_(profiler) {} - void Notify() override { - if (profiler_.Get()) { - profiler_->DispatchEvent(new web::Event("samplebufferfull")); - } - } + // Thread checker for the thread that creates this instance. + THREAD_CHECKER(thread_checker_); - private: - cppgc::WeakMember profiler_; + DISALLOW_COPY_AND_ASSIGN(Profiler); }; } // namespace js_profiler diff --git a/cobalt/js_profiler/profiler.idl b/cobalt/js_profiler/profiler.idl index 6f3793374219..0492b0039aff 100644 --- a/cobalt/js_profiler/profiler.idl +++ b/cobalt/js_profiler/profiler.idl @@ -20,9 +20,16 @@ ConstructorCallWith=EnvironmentSettings, RaisesException = Constructor, ] -interface Profiler : EventTarget { +interface Profiler { readonly attribute DOMHighResTimeStamp sampleInterval; readonly attribute boolean stopped; + // TODO(b/326337485): This function mocks but does not fully emulate the EventTarget interface. It can + // take and call many callbacks as listeners. However, note that this class does not remove listeners + // or dispatch events. Use with caution. + [CallWith=EnvironmentSettings] void addEventListener(DOMString token, SampleBufferFullCallback listener); + [CallWith=EnvironmentSettings] Promise stop(); }; + +callback SampleBufferFullCallback = void(); diff --git a/cobalt/js_profiler/profiler_group.cc b/cobalt/js_profiler/profiler_group.cc new file mode 100644 index 000000000000..e52512dc561c --- /dev/null +++ b/cobalt/js_profiler/profiler_group.cc @@ -0,0 +1,138 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "cobalt/js_profiler/profiler_group.h" + +#include "cobalt/js_profiler/profiler_trace_builder.h" +#include "cobalt/web/context.h" +#include "cobalt/web/environment_settings_helper.h" + +namespace { +v8::Local toV8String(v8::Isolate* isolate, + const std::string& string) { + if (string.empty()) return v8::String::Empty(isolate); + return v8::String::NewFromUtf8(isolate, string.c_str(), + v8::NewStringType::kNormal, string.length()) + .ToLocalChecked(); +} +} // namespace + +namespace cobalt { +namespace js_profiler { + +ProfilerGroup* ProfilerGroup::From( + script::EnvironmentSettings* environment_settings) { + web::Context* context = web::get_context(environment_settings); + if (!context->profiler_group()) { + script::GlobalEnvironment* global_env = + web::get_global_environment(environment_settings); + context->set_profiler_group( + std::make_unique(global_env->isolate())); + } + return context->profiler_group(); +} + +v8::CpuProfilingStatus ProfilerGroup::ProfilerStart( + const scoped_refptr& profiler, + script::EnvironmentSettings* settings, v8::CpuProfilingOptions options) { + if (!cpu_profiler_) { + cpu_profiler_ = v8::CpuProfiler::New(isolate_); + cpu_profiler_->SetSamplingInterval( + cobalt::js_profiler::Profiler::kBaseSampleIntervalMs * + base::Time::kMicrosecondsPerMillisecond); + } + profilers_.push_back(profiler); + num_active_profilers_++; + return cpu_profiler_->StartProfiling( + toV8String(isolate_, profiler->ProfilerId()), options, + std::make_unique(this, + profiler->ProfilerId())); +} + +ProfilerTrace ProfilerGroup::ProfilerStop(Profiler* profiler) { + auto profile = cpu_profiler_->StopProfiling( + toV8String(isolate_, profiler->ProfilerId())); + this->PopProfiler(profiler->ProfilerId()); + auto trace = + ProfilerTraceBuilder::FromProfile(profile, profiler->time_origin()); + if (profile) { + profile->Delete(); + } + if (cpu_profiler_ && num_active_profilers_ == 0) { + cpu_profiler_->Dispose(); + cpu_profiler_ = nullptr; + } + return trace; +} + +std::string ProfilerGroup::NextProfilerId() { + auto id = "cobalt::profiler[" + std::to_string(next_profiler_id_) + "]"; + next_profiler_id_++; + return id; +} + +void ProfilerGroup::DispatchSampleBufferFullEvent(std::string profiler_id) { + auto profiler = GetProfiler(profiler_id); + + if (profiler) { + profiler->DispatchSampleBufferFullEvent(); + } +} + +Profiler* ProfilerGroup::GetProfiler(std::string profiler_id) { + auto profiler = + std::find_if(profilers_.begin(), profilers_.end(), + [&profiler_id](const scoped_refptr& profiler) { + return profiler->ProfilerId() == profiler_id; + }); + if (profiler == profilers_.end()) { + return nullptr; + } + return profiler->get(); +} + +void ProfilerGroup::PopProfiler(std::string profiler_id) { + auto profiler = std::find_if(profilers_.begin(), profilers_.end(), + [&profiler_id](const Profiler* profiler) { + return profiler->ProfilerId() == profiler_id; + }); + if (profiler != profilers_.end()) { + profilers_.erase(profiler); + } + num_active_profilers_--; +} + +void ProfilerGroup::WillDestroyCurrentMessageLoop() { + while (!profilers_.empty()) { + Profiler* profiler = profilers_[0]; + DCHECK(profiler); + profiler->Cancel(); + DCHECK(profiler->stopped()); + } + + DCHECK_EQ(num_active_profilers_, 0); + if (cpu_profiler_) { + cpu_profiler_->Dispose(); + cpu_profiler_ = nullptr; + } +} + +void ProfilerMaxSamplesDelegate::Notify() { + if (profiler_group_.Get()) { + profiler_group_->DispatchSampleBufferFullEvent(profiler_id_); + } +} + +} // namespace js_profiler +} // namespace cobalt diff --git a/cobalt/js_profiler/profiler_group.h b/cobalt/js_profiler/profiler_group.h new file mode 100644 index 000000000000..a36a616c5d54 --- /dev/null +++ b/cobalt/js_profiler/profiler_group.h @@ -0,0 +1,103 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef COBALT_JS_PROFILER_PROFILER_GROUP_H_ +#define COBALT_JS_PROFILER_PROFILER_GROUP_H_ + +#include +#include +#include +#include + +#include "base/message_loop/message_loop.h" +#include "cobalt/js_profiler/profiler.h" +#include "cobalt/js_profiler/profiler_trace.h" +#include "cobalt/script/global_environment.h" +#include "third_party/v8/include/v8-profiler.h" +#include "v8/include/cppgc/member.h" +#include "v8/include/libplatform/libplatform.h" +#include "v8/include/v8-platform.h" +#include "v8/include/v8.h" + +namespace cobalt { +namespace js_profiler { + +// Forward Declaration of Profiler Class. +class Profiler; + +// A ProfilerGroup represents a set of window.Profiler JS objects sharing an +// underlying v8::CpuProfiler attached to a common isolate. +class ProfilerGroup : public base::MessageLoop::DestructionObserver { + public: + explicit ProfilerGroup(v8::Isolate* isolate) : isolate_(isolate) {} + + ~ProfilerGroup() = default; + + v8::CpuProfilingStatus ProfilerStart(const scoped_refptr& profiler, + script::EnvironmentSettings* settings, + v8::CpuProfilingOptions options); + + ProfilerTrace ProfilerStop(Profiler* profiler); + + void ProfilerCancel(Profiler* profiler); + + void DispatchSampleBufferFullEvent(std::string profiler_id); + + static ProfilerGroup* From(script::EnvironmentSettings* environment_settings); + + // Generates an unused string identifier to use for a new profiling session. + std::string NextProfilerId(); + + // From base::MessageLoop::DestructionObserver. + // All active profiling threads must be stopped before discarding this object. + void WillDestroyCurrentMessageLoop() override; + + int num_active_profilers() const { return num_active_profilers_; } + + bool active() const { return !!cpu_profiler_; } + + private: + Profiler* GetProfiler(std::string profiler_id); + void PopProfiler(std::string profiler_id); + + v8::Isolate* isolate_; + v8::CpuProfiler* cpu_profiler_ = nullptr; + + int num_active_profilers_ = 0; + int next_profiler_id_ = 0; + + // All active profilers, retained from GC. + std::vector> profilers_; +}; + +// ProfilerMaxSamplesDelegate has a notify function that is called when the +// number of collected samples exceeds the allocated buffer. It is primarily +// responsible for the "samplebufferfull" event listener on the Profiler class. +class ProfilerMaxSamplesDelegate : public v8::DiscardedSamplesDelegate { + public: + explicit ProfilerMaxSamplesDelegate(ProfilerGroup* profiler_group, + std::string profiler_id) + : profiler_group_(profiler_group), profiler_id_(profiler_id) {} + + void Notify() override; + + private: + cppgc::WeakMember profiler_group_; + std::string profiler_id_; +}; + +} // namespace js_profiler +} // namespace cobalt + +#endif // COBALT_JS_PROFILER_PROFILER_GROUP_H_ diff --git a/cobalt/web/agent.cc b/cobalt/web/agent.cc index e9f51e4fa55b..1c0c55102c79 100644 --- a/cobalt/web/agent.cc +++ b/cobalt/web/agent.cc @@ -23,6 +23,7 @@ #include "base/threading/thread_task_runner_handle.h" #include "base/trace_event/trace_event.h" #include "cobalt/base/startup_timer.h" +#include "cobalt/js_profiler/profiler_group.h" #include "cobalt/loader/fetcher_factory.h" #include "cobalt/loader/script_loader_factory.h" #include "cobalt/script/environment_settings.h" @@ -97,6 +98,9 @@ class Impl : public Context { script::ScriptRunner* script_runner() const final { return script_runner_.get(); } + js_profiler::ProfilerGroup* profiler_group() const final { + return profiler_group_.get(); + } Blob::Registry* blob_registry() const final { return blob_registry_.get(); } web::WebSettings* web_settings() const final { return web_settings_; } network::NetworkModule* network_module() const final { @@ -173,6 +177,10 @@ class Impl : public Context { return active_service_worker_; } + void set_profiler_group( + std::unique_ptr profiler_group) final { + profiler_group_ = std::move(profiler_group); + } private: // Injects a list of attributes into the Web Context's global object. @@ -218,6 +226,9 @@ class Impl : public Context { // Environment Settings object std::unique_ptr environment_settings_; + // A ProfilerGroup contains all window.Profiler objects on the isolate. + std::unique_ptr profiler_group_ = nullptr; + // The service worker registration object map. // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#environment-settings-object-service-worker-registration-object-map std::map global_environment( diff --git a/cobalt/web/context.h b/cobalt/web/context.h index b8fc450c494a..cb9a1ad62ef2 100644 --- a/cobalt/web/context.h +++ b/cobalt/web/context.h @@ -15,6 +15,7 @@ #ifndef COBALT_WEB_CONTEXT_H_ #define COBALT_WEB_CONTEXT_H_ +#include #include #include "cobalt/loader/fetcher_factory.h" @@ -39,6 +40,9 @@ class ServiceWorker; class ServiceWorkerContext; class ServiceWorkerObject; } // namespace worker +namespace js_profiler { +class ProfilerGroup; +} // namespace js_profiler namespace web { class WindowOrWorkerGlobalScope; @@ -66,6 +70,7 @@ class Context { virtual web::WebSettings* web_settings() const = 0; virtual network::NetworkModule* network_module() const = 0; virtual worker::ServiceWorkerContext* service_worker_context() const = 0; + virtual js_profiler::ProfilerGroup* profiler_group() const = 0; virtual const std::string& name() const = 0; virtual void SetupEnvironmentSettings(EnvironmentSettings* settings) = 0; @@ -110,6 +115,10 @@ class Context { active_service_worker() = 0; virtual const scoped_refptr& active_service_worker() const = 0; + + // https://wicg.github.io/js-self-profiling/ + virtual void set_profiler_group( + std::unique_ptr profiler_group) = 0; }; } // namespace web diff --git a/cobalt/web/testing/BUILD.gn b/cobalt/web/testing/BUILD.gn index 8c67e014ed08..601c3a77c43b 100644 --- a/cobalt/web/testing/BUILD.gn +++ b/cobalt/web/testing/BUILD.gn @@ -29,6 +29,7 @@ source_set("web_testing") { "//base/test:test_support", "//cobalt/base", "//cobalt/browser", + "//cobalt/js_profiler", "//cobalt/loader", "//cobalt/network", "//cobalt/script", diff --git a/cobalt/web/testing/stub_web_context.h b/cobalt/web/testing/stub_web_context.h index 487101b5a761..7c341e95679c 100644 --- a/cobalt/web/testing/stub_web_context.h +++ b/cobalt/web/testing/stub_web_context.h @@ -17,9 +17,11 @@ #include #include +#include #include "base/message_loop/message_loop.h" #include "base/test/scoped_task_environment.h" +#include "cobalt/js_profiler/profiler_group.h" #include "cobalt/loader/fetcher_factory.h" #include "cobalt/network/network_module.h" #include "cobalt/script/global_environment.h" @@ -53,6 +55,8 @@ class StubWebContext final : public Context { global_environment_ = javascript_engine_->CreateGlobalEnvironment(); blob_registry_.reset(new Blob::Registry); web_settings_.reset(new WebSettingsImpl()); + profiler_group_.reset( + new js_profiler::ProfilerGroup(global_environment_->isolate())); network_module_.reset(new network::NetworkModule()); fetcher_factory_.reset(new loader::FetcherFactory( network_module_.get(), @@ -109,6 +113,10 @@ class StubWebContext final : public Context { DCHECK(network_module_); return network_module_.get(); } + js_profiler::ProfilerGroup* profiler_group() const final { + DCHECK(profiler_group_); + return profiler_group_.get(); + } worker::ServiceWorkerContext* service_worker_context() const final { NOTREACHED(); @@ -202,6 +210,11 @@ class StubWebContext final : public Context { return service_worker_object_; } + void set_profiler_group( + std::unique_ptr profiler_group) { + profiler_group_ = std::move(profiler_group); + } + // Other private: // Name of the web instance. @@ -217,6 +230,7 @@ class StubWebContext final : public Context { std::unique_ptr web_settings_; std::unique_ptr network_module_; + std::unique_ptr profiler_group_; // Environment Settings object std::unique_ptr environment_settings_; UserAgentPlatformInfo* platform_info_ = nullptr;