From 2285d5139599b2587e79edf71d5e5486639e95c0 Mon Sep 17 00:00:00 2001 From: Ahmed Elzeiny Date: Thu, 28 Dec 2023 14:37:33 -0800 Subject: [PATCH] Implement Cobalt wrappers for v8::CpuProfiler (#2037) Implement Cobalt wrappers for v8::CpuProfiler Provides a baseline cobalt-side implementation of the JS Self-Profiling API (https://wicg.github.io/js-self-profiling/). Adds Profiler::Profiler, which wraps a profile handle from a v8::CpuProfiler, and relevant IDL bindings. b/314179829 Co-authored-by: Ahmed Elzeiny --- cobalt/browser/BUILD.gn | 1 + cobalt/browser/idl_files.gni | 8 + cobalt/build/cobalt_configuration.py | 1 + cobalt/js_profiler/BUILD.gn | 53 ++++++ cobalt/js_profiler/js_profiler_test.cc | 115 ++++++++++++ cobalt/js_profiler/profiler.cc | 158 ++++++++++++++++ cobalt/js_profiler/profiler.h | 85 +++++++++ cobalt/js_profiler/profiler.idl | 28 +++ cobalt/js_profiler/profiler_frame.idl | 22 +++ cobalt/js_profiler/profiler_init_options.idl | 20 ++ cobalt/js_profiler/profiler_sample.idl | 20 ++ cobalt/js_profiler/profiler_stack.idl | 20 ++ cobalt/js_profiler/profiler_trace.idl | 22 +++ cobalt/js_profiler/profiler_trace_builder.cc | 172 ++++++++++++++++++ cobalt/js_profiler/profiler_trace_builder.h | 82 +++++++++ cobalt/js_profiler/profiler_trace_wrapper.h | 48 +++++ cobalt/js_profiler/profiler_trace_wrapper.idl | 24 +++ 17 files changed, 879 insertions(+) create mode 100644 cobalt/js_profiler/BUILD.gn create mode 100644 cobalt/js_profiler/js_profiler_test.cc create mode 100644 cobalt/js_profiler/profiler.cc create mode 100644 cobalt/js_profiler/profiler.h create mode 100644 cobalt/js_profiler/profiler.idl create mode 100644 cobalt/js_profiler/profiler_frame.idl create mode 100644 cobalt/js_profiler/profiler_init_options.idl create mode 100644 cobalt/js_profiler/profiler_sample.idl create mode 100644 cobalt/js_profiler/profiler_stack.idl create mode 100644 cobalt/js_profiler/profiler_trace.idl create mode 100644 cobalt/js_profiler/profiler_trace_builder.cc create mode 100644 cobalt/js_profiler/profiler_trace_builder.h create mode 100644 cobalt/js_profiler/profiler_trace_wrapper.h create mode 100644 cobalt/js_profiler/profiler_trace_wrapper.idl diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn index 7e742bc005ed..4a5ed73b1c99 100644 --- a/cobalt/browser/BUILD.gn +++ b/cobalt/browser/BUILD.gn @@ -168,6 +168,7 @@ static_library("browser") { "//cobalt/fetch", "//cobalt/h5vcc", "//cobalt/input", + "//cobalt/js_profiler", "//cobalt/layout", "//cobalt/loader", "//cobalt/math", diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni index ca2b572ee9c7..7066cc76735a 100644 --- a/cobalt/browser/idl_files.gni +++ b/cobalt/browser/idl_files.gni @@ -254,6 +254,9 @@ source_idl_files = [ "//cobalt/xhr/xml_http_request.idl", "//cobalt/xhr/xml_http_request_event_target.idl", "//cobalt/xhr/xml_http_request_upload.idl", + + "//cobalt/js_profiler/profiler.idl", + "//cobalt/js_profiler/profiler_trace_wrapper.idl", ] if (!is_gold) { @@ -312,6 +315,11 @@ generated_header_idl_files = [ "//cobalt/encoding/text_decode_options.idl", "//cobalt/encoding/text_decoder_options.idl", "//cobalt/encoding/text_encoder_encode_into_result.idl", + "//cobalt/js_profiler/profiler_frame.idl", + "//cobalt/js_profiler/profiler_init_options.idl", + "//cobalt/js_profiler/profiler_sample.idl", + "//cobalt/js_profiler/profiler_stack.idl", + "//cobalt/js_profiler/profiler_trace.idl", "//cobalt/h5vcc/h5vcc_crash_type.idl", "//cobalt/h5vcc/h5vcc_metric_type.idl", "//cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl", diff --git a/cobalt/build/cobalt_configuration.py b/cobalt/build/cobalt_configuration.py index e5d04f669b3a..570839905b23 100644 --- a/cobalt/build/cobalt_configuration.py +++ b/cobalt/build/cobalt_configuration.py @@ -142,6 +142,7 @@ def GetTestTargets(self): 'persistent_settings_test', 'png_utils_test', 'poem_unittests', + 'js_profiler_test', 'renderer_test', 'render_tree_test', 'scroll_engine_tests', diff --git a/cobalt/js_profiler/BUILD.gn b/cobalt/js_profiler/BUILD.gn new file mode 100644 index 000000000000..2a025d16f9e3 --- /dev/null +++ b/cobalt/js_profiler/BUILD.gn @@ -0,0 +1,53 @@ +# Copyright 2023 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. + +static_library("js_profiler") { + sources = [ + "profiler.cc", + "profiler.h", + "profiler_trace_builder.cc", + "profiler_trace_builder.h", + "profiler_trace_wrapper.h", + ] + + deps = [ + "//cobalt/base", + "//cobalt/browser:generated_bindings", + "//cobalt/browser:generated_types", + "//cobalt/dom", + "//cobalt/script", + "//cobalt/script/v8c:engine", + "//cobalt/web", + "//third_party/chromium/media:media", + "//third_party/v8:cppgc", + ] +} + +target(gtest_target_type, "js_profiler_test") { + testonly = true + + sources = [ "js_profiler_test.cc" ] + + deps = [ + ":js_profiler", + "//cobalt/dom", + "//cobalt/dom/testing:dom_testing", + "//cobalt/dom/testing:dom_testing", + "//cobalt/script", + "//cobalt/test:run_all_unittests", + "//cobalt/web:dom_exception", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/cobalt/js_profiler/js_profiler_test.cc b/cobalt/js_profiler/js_profiler_test.cc new file mode 100644 index 000000000000..98bb584a206b --- /dev/null +++ b/cobalt/js_profiler/js_profiler_test.cc @@ -0,0 +1,115 @@ +// Copyright 2023 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 "base/callback.h" +#include "base/memory/ref_counted.h" +#include "cobalt/dom/testing/stub_environment_settings.h" +#include "cobalt/dom/testing/stub_window.h" +#include "cobalt/dom/testing/test_with_javascript.h" +#include "cobalt/js_profiler/profiler.h" +#include "cobalt/js_profiler/profiler_trace_wrapper.h" +#include "cobalt/script/testing/mock_exception_state.h" +#include "cobalt/web/dom_exception.h" +#include "cobalt/web/environment_settings_helper.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::Return; +using ::testing::StrictMock; + +namespace cobalt { +namespace js_profiler { + +class ProfilerTest : public dom::testing::TestWithJavaScript { + public: + ProfilerTest() {} + + protected: + dom::testing::StubWindow window_; + StrictMock exception_state_; +}; + +TEST_F(ProfilerTest, ProfilerStop) { + 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_)); + + auto promise = profiler_->Stop(window_.environment_settings()); + EXPECT_EQ(profiler_->stopped(), true); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kPending); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kFulfilled); +} + +TEST_F(ProfilerTest, ProfilerAlreadyStopped) { + v8::HandleScope scope(web::get_isolate(window_.environment_settings())); + ProfilerInitOptions init_options; + init_options.set_sample_interval(10); + init_options.set_max_buffer_size(0); + + scoped_refptr profiler_(new Profiler( + window_.environment_settings(), init_options, &exception_state_)); + + auto promise = profiler_->Stop(window_.environment_settings()); + EXPECT_EQ(profiler_->stopped(), true); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kPending); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kFulfilled); + auto promise2 = profiler_->Stop(window_.environment_settings()); + EXPECT_TRUE(promise2->State() == cobalt::script::PromiseState::kRejected); +} + +TEST_F(ProfilerTest, ProfilerZeroSampleInterval) { + v8::HandleScope scope(web::get_isolate(window_.environment_settings())); + ProfilerInitOptions init_options; + init_options.set_sample_interval(0); + init_options.set_max_buffer_size(0); + + scoped_refptr profiler_(new Profiler( + window_.environment_settings(), init_options, &exception_state_)); + EXPECT_EQ(profiler_->sample_interval(), 10); + auto promise = profiler_->Stop(window_.environment_settings()); + EXPECT_EQ(profiler_->stopped(), true); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kPending); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kFulfilled); +} + +TEST_F(ProfilerTest, ProfilerOutRangeSampleInterval) { + v8::HandleScope scope(web::get_isolate(window_.environment_settings())); + ProfilerInitOptions init_options; + init_options.set_sample_interval(-1); + init_options.set_max_buffer_size(0); + + scoped_refptr profiler_(new Profiler( + window_.environment_settings(), init_options, &exception_state_)); + EXPECT_EQ(profiler_->sample_interval(), 10); + auto promise = profiler_->Stop(window_.environment_settings()); + EXPECT_EQ(profiler_->stopped(), true); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kPending); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(promise->State() == cobalt::script::PromiseState::kFulfilled); +} + +TEST_F(ProfilerTest, ProfilerJSCode) { + std::string result; + EXPECT_TRUE(EvaluateScript("Profiler", &result)); + EXPECT_EQ(result, "function Profiler() { [native code] }"); +} +} // namespace js_profiler +} // namespace cobalt diff --git a/cobalt/js_profiler/profiler.cc b/cobalt/js_profiler/profiler.cc new file mode 100644 index 000000000000..c380aed7c53f --- /dev/null +++ b/cobalt/js_profiler/profiler.cc @@ -0,0 +1,158 @@ +// Copyright 2023 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.h" + +#include +#include +#include +#include +#include + +#include "cobalt/base/polymorphic_downcast.h" +#include "cobalt/js_profiler/profiler_trace_builder.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(); + + const base::TimeDelta sample_interval = + base::Milliseconds(options.sample_interval()); + + int64_t sample_interval_us = sample_interval.InMicroseconds(); + + if (sample_interval_us < 0 || + sample_interval_us > std::numeric_limits::max()) { + sample_interval_us = 0; + } + + int effective_sample_interval_ms = + static_cast(sample_interval.InMilliseconds()); + if (effective_sample_interval_ms % kBaseSampleIntervalMs != 0 || + effective_sample_interval_ms == 0) { + effective_sample_interval_ms += + (kBaseSampleIntervalMs - + effective_sample_interval_ms % kBaseSampleIntervalMs); + } + sample_interval_ = effective_sample_interval_ms; + + auto isolate = web::get_isolate(settings); + + auto status = ImplProfilingStart( + profiler_id_, + v8::CpuProfilingOptions(v8::kLeafNodeLineNumbers, + options.max_buffer_size(), sample_interval_us), + settings); + + if (status == v8::CpuProfilingStatus::kAlreadyStarted) { + web::DOMException::Raise(web::DOMException::kInvalidStateErr, + "Profiler Already started", exception_state); + } else if (status == v8::CpuProfilingStatus::kErrorTooManyProfilers) { + web::DOMException::Raise(web::DOMException::kInvalidStateErr, + "Too Many Profilers", exception_state); + } +} + +Profiler::~Profiler() { + if (cpu_profiler_) { + cpu_profiler_->Dispose(); + cpu_profiler_ = nullptr; + } +} + +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); +} + +Profiler::ProfilerTracePromise Profiler::Stop( + script::EnvironmentSettings* environment_settings) { + script::HandlePromiseWrappable promise = + web::get_script_value_factory(environment_settings) + ->CreateInterfacePromise>(); + if (!stopped()) { + stopped_ = true; + auto* global_wrappable = web::get_global_wrappable(environment_settings); + auto* context = web::get_context(environment_settings); + std::unique_ptr promise_reference( + new script::ValuePromiseWrappable::Reference(global_wrappable, + promise)); + + context->message_loop()->task_runner()->PostTask( + FROM_HERE, + base::BindOnce(&Profiler::PerformStop, base::Unretained(this), + environment_settings, std::move(promise_reference), + std::move(time_origin_), std::move(profiler_id_))); + } else { + promise->Reject(new web::DOMException(web::DOMException::kInvalidStateErr, + "Profiler already stopped.")); + } + return promise; +} + +} // namespace js_profiler +} // namespace cobalt diff --git a/cobalt/js_profiler/profiler.h b/cobalt/js_profiler/profiler.h new file mode 100644 index 000000000000..a6da1e77e63d --- /dev/null +++ b/cobalt/js_profiler/profiler.h @@ -0,0 +1,85 @@ +// Copyright 2023 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_H_ +#define COBALT_JS_PROFILER_PROFILER_H_ + +#include +#include + +#include "cobalt/dom/performance_high_resolution_time.h" +#include "cobalt/js_profiler/profiler_init_options.h" +#include "cobalt/js_profiler/profiler_trace.h" +#include "cobalt/script/promise.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 { + public: + using ProfilerTracePromise = script::HandlePromiseWrappable; + + Profiler(script::EnvironmentSettings* settings, ProfilerInitOptions options, + script::ExceptionState* exception_state); + ~Profiler(); + + ProfilerTracePromise Stop(script::EnvironmentSettings* environment_settings); + + bool stopped() const { return stopped_; } + + dom::DOMHighResTimeStamp sample_interval() const { return sample_interval_; } + + 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, + 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_; +}; + +class ProfilerMaxSamplesDelegate : public v8::DiscardedSamplesDelegate { + public: + explicit ProfilerMaxSamplesDelegate(Profiler* profiler) + : profiler_(profiler) {} + void Notify() override { + if (profiler_.Get()) { + profiler_->DispatchEvent(new web::Event("samplebufferfull")); + } + } + + private: + cppgc::WeakMember profiler_; +}; + +} // namespace js_profiler +} // namespace cobalt +#endif // COBALT_JS_PROFILER_PROFILER_H_ diff --git a/cobalt/js_profiler/profiler.idl b/cobalt/js_profiler/profiler.idl new file mode 100644 index 000000000000..6f3793374219 --- /dev/null +++ b/cobalt/js_profiler/profiler.idl @@ -0,0 +1,28 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#the-profiler-interface + +[ + Exposed=Window, + Constructor(ProfilerInitOptions options), + ConstructorCallWith=EnvironmentSettings, + RaisesException = Constructor, +] +interface Profiler : EventTarget { + readonly attribute DOMHighResTimeStamp sampleInterval; + readonly attribute boolean stopped; + + [CallWith=EnvironmentSettings] Promise stop(); +}; diff --git a/cobalt/js_profiler/profiler_frame.idl b/cobalt/js_profiler/profiler_frame.idl new file mode 100644 index 000000000000..84c5e6b56ddc --- /dev/null +++ b/cobalt/js_profiler/profiler_frame.idl @@ -0,0 +1,22 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#the-profilerframe-dictionary + +dictionary ProfilerFrame { + required DOMString name; + unsigned long long resourceId; + unsigned long long line; + unsigned long long column; +}; diff --git a/cobalt/js_profiler/profiler_init_options.idl b/cobalt/js_profiler/profiler_init_options.idl new file mode 100644 index 000000000000..7086803878d3 --- /dev/null +++ b/cobalt/js_profiler/profiler_init_options.idl @@ -0,0 +1,20 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#dom-profilerinitoptions + +dictionary ProfilerInitOptions { + required DOMHighResTimeStamp sampleInterval; + required unsigned long maxBufferSize; +}; diff --git a/cobalt/js_profiler/profiler_sample.idl b/cobalt/js_profiler/profiler_sample.idl new file mode 100644 index 000000000000..0b50acc217d7 --- /dev/null +++ b/cobalt/js_profiler/profiler_sample.idl @@ -0,0 +1,20 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#the-profilersample-dictionary + +dictionary ProfilerSample { + required DOMHighResTimeStamp timestamp; + unsigned long long stackId; +}; diff --git a/cobalt/js_profiler/profiler_stack.idl b/cobalt/js_profiler/profiler_stack.idl new file mode 100644 index 000000000000..073bf006ffae --- /dev/null +++ b/cobalt/js_profiler/profiler_stack.idl @@ -0,0 +1,20 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#the-profilerstack-dictionary + +dictionary ProfilerStack { + unsigned long long parentId; + required unsigned long long frameId; +}; diff --git a/cobalt/js_profiler/profiler_trace.idl b/cobalt/js_profiler/profiler_trace.idl new file mode 100644 index 000000000000..199f33908549 --- /dev/null +++ b/cobalt/js_profiler/profiler_trace.idl @@ -0,0 +1,22 @@ +// Copyright 2023 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. + +// https://wicg.github.io/js-self-profiling/#the-profilertrace-dictionary + +dictionary ProfilerTrace { + required sequence resources; + required sequence frames; + required sequence stacks; + required sequence samples; +}; diff --git a/cobalt/js_profiler/profiler_trace_builder.cc b/cobalt/js_profiler/profiler_trace_builder.cc new file mode 100644 index 000000000000..5f8c06d05b55 --- /dev/null +++ b/cobalt/js_profiler/profiler_trace_builder.cc @@ -0,0 +1,172 @@ +// Copyright 2023 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_trace_builder.h" + +#include "base/time/time.h" +#include "cobalt/dom/performance.h" +#include "cobalt/js_profiler/profiler_frame.h" +#include "cobalt/js_profiler/profiler_sample.h" +#include "cobalt/js_profiler/profiler_stack.h" +#include "cobalt/js_profiler/profiler_trace.h" +#include "v8/include/v8.h" + +namespace cobalt { +namespace js_profiler { + +ProfilerTrace ProfilerTraceBuilder::FromProfile(const v8::CpuProfile* profile, + base::TimeTicks time_origin) { + ProfilerTraceBuilder builder(time_origin); + if (profile) { + for (int i = 0; i < profile->GetSamplesCount(); i++) { + const auto* node = profile->GetSample(i); + auto timestamp = base::TimeTicks() + + base::Microseconds(profile->GetSampleTimestamp(i)); + builder.AddSample(node, timestamp); + } + } + return builder.GetTrace(); +} + +ProfilerTraceBuilder::ProfilerTraceBuilder(base::TimeTicks time_origin) + : time_origin_(time_origin) {} + +void ProfilerTraceBuilder::AddSample(const v8::CpuProfileNode* node, + base::TimeTicks timestamp) { + ProfilerSample sample; + + auto relative_timestamp = + dom::Performance::MonotonicTimeToDOMHighResTimeStamp(time_origin_, + timestamp); + + sample.set_timestamp(relative_timestamp); + absl::optional stack_id = GetOrInsertStackId(node); + if (stack_id.has_value()) sample.set_stack_id(stack_id.value()); + + samples_.push_back(sample); +} + +absl::optional ProfilerTraceBuilder::GetOrInsertStackId( + const v8::CpuProfileNode* node) { + if (!node) return absl::nullopt; + + if (!ShouldIncludeStackFrame(node)) + return GetOrInsertStackId(node->GetParent()); + + auto existing_stack_id = node_to_stack_map_.find(node); + if (existing_stack_id != node_to_stack_map_.end()) { + // If we found a stack entry for this node ID, the subpath to the root + // already exists in the trace, and we may coalesce. + return existing_stack_id->second; + } + + ProfilerStack stack; + uint64_t frame_id = GetOrInsertFrameId(node); + stack.set_frame_id(frame_id); + absl::optional parent_stack_id = GetOrInsertStackId(node->GetParent()); + if (parent_stack_id.has_value()) stack.set_parent_id(parent_stack_id.value()); + + uint64_t stack_id = stacks_.size(); + stacks_.push_back(stack); + node_to_stack_map_[node] = stack_id; + return stack_id; +} + +uint64_t ProfilerTraceBuilder::GetOrInsertFrameId( + const v8::CpuProfileNode* node) { + auto existing_frame_id = node_to_frame_map_.find(node); + + if (existing_frame_id != node_to_frame_map_.end()) + return existing_frame_id->second; + + ProfilerFrame frame; + std::string function_name(node->GetFunctionNameStr()); + frame.set_name(function_name); + if (*node->GetScriptResourceNameStr() != '\0') { + uint64_t resource_id = + GetOrInsertResourceId(node->GetScriptResourceNameStr()); + frame.set_resource_id(resource_id); + } + if (node->GetLineNumber() != v8::CpuProfileNode::kNoLineNumberInfo) + frame.set_line(node->GetLineNumber()); + if (node->GetColumnNumber() != v8::CpuProfileNode::kNoColumnNumberInfo) + frame.set_column(node->GetColumnNumber()); + + uint64_t frame_id = frames_.size(); + frames_.push_back(frame); + node_to_frame_map_[node] = frame_id; + + return frame_id; +} + +uint64_t ProfilerTraceBuilder::GetOrInsertResourceId( + const char* resource_name) { + auto existing_resource_id = resource_indices_.find(resource_name); + + if (existing_resource_id != resource_indices_.end()) + return existing_resource_id->second; + + uint64_t resource_id = resources_.size(); + resources_.push_back(resource_name); + + resource_indices_[resource_name] = resource_id; + + return resource_id; +} + +ProfilerTrace ProfilerTraceBuilder::GetTrace() const { + ProfilerTrace trace; + trace.set_resources(resources_); + trace.set_frames(frames_); + trace.set_stacks(stacks_); + trace.set_samples(samples_); + return trace; +} + +bool ProfilerTraceBuilder::ShouldIncludeStackFrame( + const v8::CpuProfileNode* node) { + DCHECK(node); + + // Omit V8 metadata frames. + const v8::CpuProfileNode::SourceType source_type = node->GetSourceType(); + if (source_type != v8::CpuProfileNode::kScript && + source_type != v8::CpuProfileNode::kBuiltin && + source_type != v8::CpuProfileNode::kCallback) { + return false; + } + + // Attempt to attribute each stack frame to a script. + // - For JS functions, this is their own script. + // - For builtins, this is the first attributable caller script. + const v8::CpuProfileNode* resource_node = node; + if (source_type != v8::CpuProfileNode::kScript) { + while (resource_node && + resource_node->GetScriptId() == v8::UnboundScript::kNoScriptId) { + resource_node = resource_node->GetParent(); + } + } + if (!resource_node) return false; + + int script_id = resource_node->GetScriptId(); + + // If we already tested whether or not this script was cross-origin, return + // the cached results. + auto it = script_same_origin_cache_.find(script_id); + if (it != script_same_origin_cache_.end()) return it->second; + // insert in pair script_same_origin_cache_ (script_id, true) + script_same_origin_cache_[script_id] = true; + return true; +} +} // namespace js_profiler +} // namespace cobalt diff --git a/cobalt/js_profiler/profiler_trace_builder.h b/cobalt/js_profiler/profiler_trace_builder.h new file mode 100644 index 000000000000..adf88bf4cf46 --- /dev/null +++ b/cobalt/js_profiler/profiler_trace_builder.h @@ -0,0 +1,82 @@ +// Copyright 2023 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_TRACE_BUILDER_H_ +#define COBALT_JS_PROFILER_PROFILER_TRACE_BUILDER_H_ + +#include +#include + +#include "base/time/time.h" +#include "cobalt/script/sequence.h" +#include "third_party/chromium/media/cobalt/third_party/abseil-cpp/absl/types/optional.h" +#include "v8/include/v8-profiler.h" + +namespace cobalt { +namespace js_profiler { + +class ProfilerFrame; +class ProfilerSample; +class ProfilerStack; +class ProfilerTrace; + +class ProfilerTraceBuilder { + public: + static ProfilerTrace FromProfile(const v8::CpuProfile* profile, + base::TimeTicks time_origin); + + explicit ProfilerTraceBuilder(base::TimeTicks time_origin); + + ProfilerTraceBuilder(const ProfilerTraceBuilder&) = delete; + ProfilerTraceBuilder& operator=(const ProfilerTraceBuilder&) = delete; + + private: + // Adds a stack sample from V8 to the trace, performing necessary filtering + // and coalescing. + void AddSample(const v8::CpuProfileNode* node, base::TimeTicks timestamp); + + // Obtains the stack ID of the substack with the given node as its leaf, + // performing origin-based filtering. + absl::optional GetOrInsertStackId(const v8::CpuProfileNode* node); + + // Obtains the frame ID of the stack frame represented by the given node. + uint64_t GetOrInsertFrameId(const v8::CpuProfileNode* node); + + // Obtains the resource ID for the given resource name. + uint64_t GetOrInsertResourceId(const char* resource_name); + + ProfilerTrace GetTrace() const; + + // Discards metadata frames and performs an origin check on the given stack + // frame, returning true if it either has the same origin as the profiler, or + // if it should be shared cross origin. + bool ShouldIncludeStackFrame(const v8::CpuProfileNode* node); + + base::TimeTicks time_origin_; + + script::Sequence resources_; + script::Sequence frames_; + script::Sequence stacks_; + script::Sequence samples_; + + // Maps V8-managed resource strings to their indices in the resources table. + std::map resource_indices_; + std::map node_to_stack_map_; + std::map node_to_frame_map_; + + std::map script_same_origin_cache_; +}; +} // namespace js_profiler +} // namespace cobalt +#endif // COBALT_JS_PROFILER_PROFILER_TRACE_BUILDER_H_ diff --git a/cobalt/js_profiler/profiler_trace_wrapper.h b/cobalt/js_profiler/profiler_trace_wrapper.h new file mode 100644 index 000000000000..99685273bcb4 --- /dev/null +++ b/cobalt/js_profiler/profiler_trace_wrapper.h @@ -0,0 +1,48 @@ +// Copyright 2023 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_TRACE_WRAPPER_H_ +#define COBALT_JS_PROFILER_PROFILER_TRACE_WRAPPER_H_ + +#include + +#include "cobalt/js_profiler/profiler_trace.h" + +namespace cobalt { +namespace js_profiler { +class ProfilerTraceWrapper : public script::Wrappable { + public: + DEFINE_WRAPPABLE_TYPE(ProfilerTraceWrapper); + explicit ProfilerTraceWrapper(ProfilerTrace trace) { + resources_ = trace.resources(); + frames_ = trace.frames(); + stacks_ = trace.stacks(); + samples_ = trace.samples(); + } + ProfilerTraceWrapper() {} + script::Sequence resources() const { return resources_; } + script::Sequence frames() const { return frames_; } + script::Sequence stacks() const { return stacks_; } + script::Sequence samples() const { return samples_; } + + private: + script::Sequence resources_; + script::Sequence frames_; + script::Sequence stacks_; + script::Sequence samples_; +}; +} // namespace js_profiler +} // namespace cobalt + +#endif // COBALT_JS_PROFILER_PROFILER_TRACE_WRAPPER_H_ diff --git a/cobalt/js_profiler/profiler_trace_wrapper.idl b/cobalt/js_profiler/profiler_trace_wrapper.idl new file mode 100644 index 000000000000..b564ab2d97f0 --- /dev/null +++ b/cobalt/js_profiler/profiler_trace_wrapper.idl @@ -0,0 +1,24 @@ +// Copyright 2023 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. + +// Custom, not in spec. Interface wrapper for ProfilerTrace to make +// compatible with Promises implementation in Cobalt. + +[Constructor(optional ProfilerTrace trace)] +interface ProfilerTraceWrapper { + readonly attribute sequence resources; + readonly attribute sequence frames; + readonly attribute sequence stacks; + readonly attribute sequence samples; +};