diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn index 7e742bc005ed..385f770abf2a 100644 --- a/cobalt/browser/BUILD.gn +++ b/cobalt/browser/BUILD.gn @@ -56,6 +56,7 @@ target(final_executable_type, "cobalt") { ":browser_switches", "//cobalt/base", "//cobalt/css_parser", + "//cobalt/profiler:js_profiler", "//net", ] data_deps = [ @@ -177,6 +178,7 @@ static_library("browser") { "//cobalt/network", "//cobalt/overlay_info", "//cobalt/persistent_storage:persistent_settings", + "//cobalt/profiler:js_profiler", "//cobalt/render_tree", "//cobalt/renderer", "//cobalt/renderer/test/png_utils", diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni index ca2b572ee9c7..234670a2eebd 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/profiler/profiler.idl", + "//cobalt/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/profiler/profiler_frame.idl", + "//cobalt/profiler/profiler_init_options.idl", + "//cobalt/profiler/profiler_sample.idl", + "//cobalt/profiler/profiler_stack.idl", + "//cobalt/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/profiler/BUILD.gn b/cobalt/profiler/BUILD.gn new file mode 100644 index 000000000000..86ed8c1a844d --- /dev/null +++ b/cobalt/profiler/BUILD.gn @@ -0,0 +1,33 @@ +# 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_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/angle:libANGLE_headers", + "//third_party/chromium/media:media", + ] +} diff --git a/cobalt/profiler/profiler.cc b/cobalt/profiler/profiler.cc new file mode 100644 index 000000000000..f58ceee5a4eb --- /dev/null +++ b/cobalt/profiler/profiler.cc @@ -0,0 +1,90 @@ +// 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/profiler/profiler.h" + +#include +#include +#include + +#include "cobalt/base/polymorphic_downcast.h" +#include "cobalt/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 cobalt { +namespace profiler { + +volatile uint32_t s_lastProfileId = 0; + + +Profiler::Profiler(script::EnvironmentSettings* settings, + ProfilerInitOptions options) + : cobalt::web::EventTarget(settings), + stopped_(false), + time_origin_{base::TimeTicks::Now()} { + auto* js_engine = web::get_context(settings)->javascript_engine(); + + profiler_id_ = nextProfileId(); + sample_interval_ = options.sample_interval(); + js_engine->StartProfiling(options.sample_interval(), + options.max_buffer_size(), profiler_id_); +} + +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 js_engine = web::get_context(environment_settings)->javascript_engine(); + auto trace = js_engine->StopProfiling(profiler_id_, time_origin_); + scoped_refptr result = new ProfilerTraceWrapper(trace); + 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 profiler +} // namespace cobalt diff --git a/cobalt/profiler/profiler.h b/cobalt/profiler/profiler.h new file mode 100644 index 000000000000..e7146a9cc353 --- /dev/null +++ b/cobalt/profiler/profiler.h @@ -0,0 +1,61 @@ +// 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_PROFILER_PROFILER_H_ +#define COBALT_PROFILER_PROFILER_H_ + +#include +#include + +#include "cobalt/dom/performance_high_resolution_time.h" +#include "cobalt/profiler/profiler_init_options.h" +#include "cobalt/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" + +namespace cobalt { +namespace profiler { + +class Profiler : public cobalt::web::EventTarget { + public: + using ProfilerTracePromise = script::HandlePromiseWrappable; + + Profiler(script::EnvironmentSettings* settings, ProfilerInitOptions options); + + ProfilerTracePromise Stop(script::EnvironmentSettings* environment_settings); + + bool stopped() const { return stopped_; } + + dom::DOMHighResTimeStamp sample_interval() const { return sample_interval_; } + + DEFINE_WRAPPABLE_TYPE(Profiler); + + 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_; + base::TimeTicks time_origin_; + std::string profiler_id_; +}; +} // namespace profiler +} // namespace cobalt +#endif // COBALT_PROFILER_PROFILER_H_ diff --git a/cobalt/profiler/profiler.idl b/cobalt/profiler/profiler.idl new file mode 100644 index 000000000000..8f8f43594bb9 --- /dev/null +++ b/cobalt/profiler/profiler.idl @@ -0,0 +1,23 @@ +// 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,] +interface Profiler : EventTarget { + readonly attribute DOMHighResTimeStamp sampleInterval; + readonly attribute boolean stopped; + + [CallWith=EnvironmentSettings] Promise stop(); +}; diff --git a/cobalt/profiler/profiler_frame.idl b/cobalt/profiler/profiler_frame.idl new file mode 100644 index 000000000000..84c5e6b56ddc --- /dev/null +++ b/cobalt/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/profiler/profiler_init_options.idl b/cobalt/profiler/profiler_init_options.idl new file mode 100644 index 000000000000..348687d2b8df --- /dev/null +++ b/cobalt/profiler/profiler_init_options.idl @@ -0,0 +1,18 @@ +// 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. + +dictionary ProfilerInitOptions { + required DOMHighResTimeStamp sampleInterval; + required unsigned long maxBufferSize; +}; diff --git a/cobalt/profiler/profiler_sample.idl b/cobalt/profiler/profiler_sample.idl new file mode 100644 index 000000000000..0b50acc217d7 --- /dev/null +++ b/cobalt/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/profiler/profiler_stack.idl b/cobalt/profiler/profiler_stack.idl new file mode 100644 index 000000000000..073bf006ffae --- /dev/null +++ b/cobalt/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/profiler/profiler_trace.idl b/cobalt/profiler/profiler_trace.idl new file mode 100644 index 000000000000..199f33908549 --- /dev/null +++ b/cobalt/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/profiler/profiler_trace_wrapper.h b/cobalt/profiler/profiler_trace_wrapper.h new file mode 100644 index 000000000000..455df1106c1b --- /dev/null +++ b/cobalt/profiler/profiler_trace_wrapper.h @@ -0,0 +1,49 @@ +// 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_PROFILER_PROFILER_TRACE_WRAPPER_H_ +#define COBALT_PROFILER_PROFILER_TRACE_WRAPPER_H_ + +#include + +#include "cobalt/profiler/profiler_trace.h" + + +namespace cobalt { +namespace 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 profiler +} // namespace cobalt + +#endif // COBALT_PROFILER_PROFILER_TRACE_WRAPPER_H_ diff --git a/cobalt/profiler/profiler_trace_wrapper.idl b/cobalt/profiler/profiler_trace_wrapper.idl new file mode 100644 index 000000000000..4e61bcbec3b4 --- /dev/null +++ b/cobalt/profiler/profiler_trace_wrapper.idl @@ -0,0 +1,21 @@ +// 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. + +[Constructor(optional ProfilerTrace trace)] +interface ProfilerTraceWrapper { + readonly attribute sequence resources; + readonly attribute sequence frames; + readonly attribute sequence stacks; + readonly attribute sequence samples; +}; diff --git a/cobalt/script/BUILD.gn b/cobalt/script/BUILD.gn index 55a307af45e4..bcece471e29b 100644 --- a/cobalt/script/BUILD.gn +++ b/cobalt/script/BUILD.gn @@ -57,6 +57,7 @@ static_library("script") { deps = [ "//cobalt/base", + "//cobalt/browser:generated_types", "//cobalt/loader:origin", "//starboard:starboard_headers_only", "//third_party/v8", diff --git a/cobalt/script/javascript_engine.h b/cobalt/script/javascript_engine.h index 55124a79294c..1390ee7b7392 100644 --- a/cobalt/script/javascript_engine.h +++ b/cobalt/script/javascript_engine.h @@ -21,6 +21,7 @@ #include "base/callback.h" #include "base/memory/ref_counted.h" #include "cobalt/base/source_location.h" +#include "cobalt/profiler/profiler_trace.h" namespace cobalt { namespace script { @@ -79,6 +80,11 @@ class JavaScriptEngine { // Update the date/time configuration in response to a change in the setting. virtual void UpdateDateTimeConfiguration() = 0; + virtual void StartProfiling(int sample_time, int max_buffer, + std::string profiler_id) = 0; + virtual profiler::ProfilerTrace StopProfiling( + std::string profiler_id, base::TimeTicks time_origin) = 0; + protected: virtual ~JavaScriptEngine() {} friend std::unique_ptr::deleter_type; diff --git a/cobalt/script/v8c/BUILD.gn b/cobalt/script/v8c/BUILD.gn index af95907d1b84..1555a53d64a2 100644 --- a/cobalt/script/v8c/BUILD.gn +++ b/cobalt/script/v8c/BUILD.gn @@ -76,6 +76,8 @@ static_library("engine") { "isolate_fellowship.cc", "isolate_fellowship.h", "native_promise.h", + "profiler_trace_builder.cc", + "profiler_trace_builder.h", "scoped_persistent.h", "switches.cc", "switches.h", @@ -128,6 +130,7 @@ static_library("engine") { deps = [ ":embed_v8c_resources_as_header_files", + "//cobalt/browser:generated_types", "//cobalt/cache", "//cobalt/configuration", "//cobalt/script", diff --git a/cobalt/script/v8c/profiler_trace_builder.cc b/cobalt/script/v8c/profiler_trace_builder.cc new file mode 100644 index 000000000000..a618a808b8b4 --- /dev/null +++ b/cobalt/script/v8c/profiler_trace_builder.cc @@ -0,0 +1,175 @@ +// 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/script/v8c/profiler_trace_builder.h" + +#include "base/time/time.h" +#include "cobalt/dom/performance.h" +#include "cobalt/profiler/profiler_frame.h" +#include "cobalt/profiler/profiler_sample.h" +#include "cobalt/profiler/profiler_stack.h" +#include "cobalt/profiler/profiler_trace.h" +#include "v8/include/v8.h" + +namespace cobalt { +namespace script { +namespace v8c { + +profiler::ProfilerTrace ProfilerTraceBuilder::FromProfile( + const v8::CpuProfile* profile, base::TimeTicks time_origin) { + ProfilerTraceBuilder* builder = new ProfilerTraceBuilder(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) { + profiler::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; + } + + profiler::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; + + profiler::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_map_.find(resource_name); + + if (existing_resource_id != resource_map_.end()) + return existing_resource_id->second; + + uint64_t resource_id = resources_.size(); + resources_.push_back(resource_name); + + resource_map_[resource_name] = resource_id; + + return resource_id; +} + +profiler::ProfilerTrace ProfilerTraceBuilder::GetTrace() const { + profiler::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 v8c +} // namespace script +} // namespace cobalt diff --git a/cobalt/script/v8c/profiler_trace_builder.h b/cobalt/script/v8c/profiler_trace_builder.h new file mode 100644 index 000000000000..80e32cb27d3f --- /dev/null +++ b/cobalt/script/v8c/profiler_trace_builder.h @@ -0,0 +1,87 @@ +// 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_SCRIPT_V8C_PROFILER_TRACE_BUILDER_H_ +#define COBALT_SCRIPT_V8C_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 profiler { + +class ProfilerFrame; +class ProfilerSample; +class ProfilerStack; +class ProfilerTrace; +} // namespace profiler + +namespace script { +namespace v8c { +class ProfilerTraceBuilder { + public: + static profiler::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); + + profiler::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_map_; + std::map node_to_stack_map_; + std::map node_to_frame_map_; + + std::map script_same_origin_cache_; +}; +} // namespace v8c +} // namespace script +} // namespace cobalt +#endif // COBALT_SCRIPT_V8C_PROFILER_TRACE_BUILDER_H_ diff --git a/cobalt/script/v8c/v8c_engine.cc b/cobalt/script/v8c/v8c_engine.cc index a039631b44da..1751cb57f186 100644 --- a/cobalt/script/v8c/v8c_engine.cc +++ b/cobalt/script/v8c/v8c_engine.cc @@ -15,6 +15,7 @@ #include "cobalt/script/v8c/v8c_engine.h" #include +#include #include #include @@ -24,13 +25,24 @@ #include "cobalt/browser/stack_size_constants.h" #include "cobalt/configuration/configuration.h" #include "cobalt/script/v8c/isolate_fellowship.h" +#include "cobalt/script/v8c/profiler_trace_builder.h" #include "cobalt/script/v8c/v8c_global_environment.h" #include "starboard/once.h" +#include "third_party/v8/include/v8-profiler.h" namespace cobalt { namespace script { namespace v8c { +static constexpr int kBaseSampleIntervalMs = 10; + +static 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 { size_t UsedHeapSize(v8::Isolate* isolate) { @@ -162,6 +174,10 @@ V8cEngine::V8cEngine(const Options& options) : options_(options) { isolate_->AddMessageListener(&ErrorMessageListener); isolate_->SetCaptureStackTraceForUncaughtExceptions(true); #endif + + cpu_profiler_ = v8::CpuProfiler::New(isolate_); + cpu_profiler_->SetSamplingInterval(kBaseSampleIntervalMs * + base::Time::kMicrosecondsPerMillisecond); } V8cEngine::~V8cEngine() { @@ -216,6 +232,33 @@ void V8cEngine::UpdateDateTimeConfiguration() { v8::Isolate::TimeZoneDetection::kRedetect); } +void V8cEngine::StartProfiling(int sample_time, int max_buffer, + std::string profiler_id) { + const base::TimeDelta sample_interval = base::Milliseconds(sample_time); + int64_t sample_interval_us = sample_interval.InMicroseconds(); + + if (sample_interval_us < 0 || + sample_interval_us > std::numeric_limits::max()) { + sample_interval_us = 0; + } + // cobalt/script/v8c + cpu_profiler_->StartProfiling( + toV8String(isolate_, profiler_id), + v8::CpuProfilingOptions(v8::kLeafNodeLineNumbers, max_buffer, + sample_interval_us)); + + profiler::ProfilerTrace trace; +} + +profiler::ProfilerTrace V8cEngine::StopProfiling(std::string profiler_id, + base::TimeTicks time_origin) { + auto* profile = + cpu_profiler_->StopProfiling(toV8String(isolate_, profiler_id)); + auto trace = ProfilerTraceBuilder::FromProfile(profile, time_origin); + return trace; +} + + } // namespace v8c // static diff --git a/cobalt/script/v8c/v8c_engine.h b/cobalt/script/v8c/v8c_engine.h index 62d164e1c133..82197d8c5aec 100644 --- a/cobalt/script/v8c/v8c_engine.h +++ b/cobalt/script/v8c/v8c_engine.h @@ -16,12 +16,15 @@ #define COBALT_SCRIPT_V8C_V8C_ENGINE_H_ #include +#include #include #include "base/threading/thread_checker.h" #include "base/timer/timer.h" +#include "cobalt/profiler/profiler_trace.h" #include "cobalt/script/javascript_engine.h" #include "cobalt/script/v8c/v8c_heap_tracer.h" +#include "third_party/v8/include/v8-profiler.h" #include "v8/include/libplatform/libplatform.h" #include "v8/include/v8.h" @@ -50,6 +53,12 @@ class V8cEngine : public JavaScriptEngine { v8::Isolate* isolate() const { return isolate_; } V8cHeapTracer* heap_tracer() const { return v8c_heap_tracer_.get(); } + void StartProfiling(int sample_time, int max_buffer, + std::string profiler_id) override; + profiler::ProfilerTrace StopProfiling(std::string profiler_id, + base::TimeTicks time_origin) override; + + private: // Where we store ourselves as embedder private data in our corresponding // |v8::Isolate|. @@ -66,6 +75,8 @@ class V8cEngine : public JavaScriptEngine { ErrorHandler error_handler_; JavaScriptEngine::Options options_; + + v8::CpuProfiler* cpu_profiler_ = nullptr; }; } // namespace v8c