Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry pick PR #2454: Stabilize JavaScript Profiler in Cobalt #2820

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/venv/*
_certs/
.coverage
compile_commands.json
1 change: 1 addition & 0 deletions cobalt/base/tokens.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ namespace base {
MacroOpWithNameOnly(resourcetimingbufferfull) \
MacroOpWithNameOnly(result) \
MacroOpWithNameOnly(resume) \
MacroOpWithNameOnly(samplebufferfull) \
MacroOpWithNameOnly(scroll) \
MacroOpWithNameOnly(securitypolicyviolation) \
MacroOpWithNameOnly(seeked) \
Expand Down
2 changes: 2 additions & 0 deletions cobalt/js_profiler/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions cobalt/js_profiler/js_profiler_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<script::testing::MockExceptionState> exception_state_;
Expand Down Expand Up @@ -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> 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> profiler_(new Profiler(
window_.environment_settings(), init_options, &exception_state_));

profiler_->Cancel();
EXPECT_EQ(profiler_->stopped(), true);
}

} // namespace js_profiler
} // namespace cobalt
113 changes: 48 additions & 65 deletions cobalt/js_profiler/profiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,27 @@

#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 "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<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();
: 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());
Expand All @@ -66,21 +48,19 @@ Profiler::Profiler(script::EnvironmentSettings* settings,

int effective_sample_interval_ms =
static_cast<int>(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,
Expand All @@ -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<SampleBufferFullCallbackReference>(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<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);
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<scoped_refptr<ProfilerTraceWrapper>>();
Expand All @@ -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,
Expand All @@ -154,5 +119,23 @@ Profiler::ProfilerTracePromise Profiler::Stop(
return promise;
}

void Profiler::PerformStop(
ProfilerGroup* profiler_group,
std::unique_ptr<script::ValuePromiseWrappable::Reference> 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<ProfilerTraceWrapper> 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
59 changes: 35 additions & 24 deletions cobalt/js_profiler/profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,67 +17,78 @@

#include <memory>
#include <string>
#include <vector>

#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<void()> SampleBufferFullCallback;
typedef script::ScriptValue<SampleBufferFullCallback>
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<script::ValuePromiseWrappable::Reference>
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<std::unique_ptr<SampleBufferFullCallbackReference>> 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> profiler_;
DISALLOW_COPY_AND_ASSIGN(Profiler);
};

} // namespace js_profiler
Expand Down
9 changes: 8 additions & 1 deletion cobalt/js_profiler/profiler.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProfilerTraceWrapper> stop();
};

callback SampleBufferFullCallback = void();
Loading
Loading