Skip to content

Commit

Permalink
Implement Cobalt wrappers for v8::CpuProfiler (#2037)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
aelzeiny and Ahmed Elzeiny committed Dec 28, 2023
1 parent a684fb0 commit 2285d51
Show file tree
Hide file tree
Showing 17 changed files with 879 additions and 0 deletions.
1 change: 1 addition & 0 deletions cobalt/browser/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ static_library("browser") {
"//cobalt/fetch",
"//cobalt/h5vcc",
"//cobalt/input",
"//cobalt/js_profiler",
"//cobalt/layout",
"//cobalt/loader",
"//cobalt/math",
Expand Down
8 changes: 8 additions & 0 deletions cobalt/browser/idl_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions cobalt/build/cobalt_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
53 changes: 53 additions & 0 deletions cobalt/js_profiler/BUILD.gn
Original file line number Diff line number Diff line change
@@ -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",
]
}
115 changes: 115 additions & 0 deletions cobalt/js_profiler/js_profiler_test.cc
Original file line number Diff line number Diff line change
@@ -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<script::testing::MockExceptionState> 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> 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> 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> 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> 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
158 changes: 158 additions & 0 deletions cobalt/js_profiler/profiler.cc
Original file line number Diff line number Diff line change
@@ -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 <iostream>
#include <limits>
#include <memory>
#include <string>
#include <utility>

#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<v8::String> 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<int>::max()) {
sample_interval_us = 0;
}

int effective_sample_interval_ms =
static_cast<int>(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<ProfilerMaxSamplesDelegate>(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<script::ValuePromiseWrappable::Reference> 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<ProfilerTraceWrapper> 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<scoped_refptr<ProfilerTraceWrapper>>();
if (!stopped()) {
stopped_ = true;
auto* global_wrappable = web::get_global_wrappable(environment_settings);
auto* context = web::get_context(environment_settings);
std::unique_ptr<script::ValuePromiseWrappable::Reference> 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
Loading

0 comments on commit 2285d51

Please sign in to comment.