blob: 938f4f6ac340e9bb668b18731e1560ee32b5386a [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/v8_unwinder.h"
#include <algorithm>
#include <memory>
#include "base/bind.h"
#include "base/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/profiler/stack_sampling_profiler_test_util.h"
#include "base/stl_util.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_task_environment.h"
#include "build/build_config.h"
#include "gin/public/isolate_holder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8.h"
namespace {
v8::Local<v8::String> ToV8String(const char* str) {
return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), str,
v8::NewStringType::kNormal)
.ToLocalChecked();
}
v8::Local<v8::Object> CreatePointerHolder(const void* ptr) {
v8::Local<v8::ObjectTemplate> object_template =
v8::ObjectTemplate::New(v8::Isolate::GetCurrent());
object_template->SetInternalFieldCount(1);
v8::Local<v8::Object> holder =
object_template
->NewInstance(v8::Isolate::GetCurrent()->GetCurrentContext())
.ToLocalChecked();
holder->SetAlignedPointerInInternalField(0, const_cast<void*>(ptr));
return holder;
}
template <typename T>
T* GetPointerFromHolder(v8::Local<v8::Object> holder) {
return reinterpret_cast<T*>(holder->GetAlignedPointerFromInternalField(0));
}
// Sets up the environment necessary to execute V8 code.
class ScopedV8Environment {
public:
ScopedV8Environment()
: isolate_holder_(scoped_task_environment_.GetMainThreadTaskRunner(),
gin::IsolateHolder::IsolateType::kBlinkMainThread) {
isolate()->Enter();
v8::HandleScope handle_scope(isolate());
context_.Reset(isolate(), v8::Context::New(isolate()));
v8::Local<v8::Context>::New(isolate(), context_)->Enter();
}
~ScopedV8Environment() {
{
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context>::New(isolate(), context_)->Exit();
context_.Reset();
}
isolate()->Exit();
}
v8::Isolate* isolate() { return isolate_holder_.isolate(); }
private:
base::test::ScopedTaskEnvironment scoped_task_environment_;
gin::IsolateHolder isolate_holder_;
v8::Persistent<v8::Context> context_;
};
// C++ function to be invoked from V8 which calls back into the provided closure
// pointer (passed via a holder object) to wait for a stack sample to be taken.
void WaitForSampleNative(const v8::FunctionCallbackInfo<v8::Value>& info) {
const base::RepeatingClosure* wait_for_sample =
GetPointerFromHolder<const base::RepeatingClosure>(
info[0].As<v8::Object>());
if (wait_for_sample)
wait_for_sample->Run();
}
// Causes a stack sample to be taken after setting up a call stack from C++ to
// JavaScript and back into C++.
base::FunctionAddressRange CallThroughV8(
const base::RepeatingCallback<void(const v8::UnwindState&)>&
report_unwind_state,
const base::RepeatingClosure& wait_for_sample) {
const void* start_program_counter = base::GetProgramCounter();
if (wait_for_sample) {
// Set up V8 runtime environment.
// Allows use of natives (functions starting with '%') within JavaScript
// code, which allows us to control compilation of the JavaScript function
// we define.
// TODO(wittman): The flag should be set only for the duration of this test
// but the V8 API currently doesn't support this. http://crbug.com/v8/9210
// covers adding the necessary functionality to V8.
v8::V8::SetFlagsFromString("--allow-natives-syntax");
ScopedV8Environment v8_environment;
v8::Isolate* isolate = v8_environment.isolate();
report_unwind_state.Run(isolate->GetUnwindState());
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// Define a V8 function WaitForSampleNative() backed by the C++ function
// WaitForSampleNative().
v8::Local<v8::FunctionTemplate> js_wait_for_sample_native_template =
v8::FunctionTemplate::New(isolate, WaitForSampleNative);
v8::Local<v8::Function> js_wait_for_sample_native =
js_wait_for_sample_native_template->GetFunction(context)
.ToLocalChecked();
js_wait_for_sample_native->SetName(ToV8String("WaitForSampleNative"));
context->Global()
->Set(context, ToV8String("WaitForSampleNative"),
js_wait_for_sample_native)
.FromJust();
// Run a script to create the V8 function waitForSample() that invokes
// WaitForSampleNative(), and a function that ensures that waitForSample()
// gets compiled. waitForSample() just passes the holder object for the
// pointer to the wait_for_sample Closure back into the C++ code. We ensure
// that the function is compiled to test walking through both builtin and
// runtime-generated code.
const char kWaitForSampleJs[] = R"(
function waitForSample(closure_pointer_holder) {
if (closure_pointer_holder)
WaitForSampleNative(closure_pointer_holder);
}
// Set up the function to be compiled rather than interpreted.
function compileWaitForSample(closure_pointer_holder) {
%PrepareFunctionForOptimization(waitForSample);
waitForSample(closure_pointer_holder);
waitForSample(closure_pointer_holder);
%OptimizeFunctionOnNextCall(waitForSample);
}
)";
v8::Local<v8::Script> script =
v8::Script::Compile(context, ToV8String(kWaitForSampleJs))
.ToLocalChecked();
script->Run(context).ToLocalChecked();
// Run compileWaitForSample(), using a null closure pointer to avoid
// actually waiting.
v8::Local<v8::Function> js_compile_wait_for_sample =
v8::Local<v8::Function>::Cast(
context->Global()
->Get(context, ToV8String("compileWaitForSample"))
.ToLocalChecked());
v8::Local<v8::Value> argv[] = {CreatePointerHolder(nullptr)};
js_compile_wait_for_sample
->Call(context, v8::Undefined(isolate), base::size(argv), argv)
.ToLocalChecked();
// Run waitForSample() with the real closure pointer.
argv[0] = CreatePointerHolder(&wait_for_sample);
v8::Local<v8::Function> js_wait_for_sample = v8::Local<v8::Function>::Cast(
context->Global()
->Get(context, ToV8String("waitForSample"))
.ToLocalChecked());
js_wait_for_sample
->Call(context, v8::Undefined(isolate), base::size(argv), argv)
.ToLocalChecked();
}
// Volatile to prevent a tail call to GetProgramCounter().
const void* volatile end_program_counter = base::GetProgramCounter();
return {start_program_counter, end_program_counter};
}
} // namespace
// Checks that unwinding from C++ through JavaScript and back into C++ succeeds.
// NB: unwinding is only supported for 64 bit Windows and OS X.
#if (defined(OS_WIN) && defined(ARCH_CPU_64_BITS)) || \
(defined(OS_MACOSX) && !defined(OS_IOS))
#define MAYBE_UnwindThroughV8Frames UnwindThroughV8Frames
#else
#define MAYBE_UnwindThroughV8Frames DISABLED_UnwindThroughV8Frames
#endif
TEST(V8UnwinderTest, MAYBE_UnwindThroughV8Frames) {
v8::UnwindState unwind_state;
base::WaitableEvent unwind_state_available;
const auto set_unwind_state = [&](const v8::UnwindState& state) {
unwind_state = state;
unwind_state_available.Signal();
};
const auto create_v8_unwinder = [&]() -> std::unique_ptr<base::Unwinder> {
unwind_state_available.Wait();
return std::make_unique<V8Unwinder>(unwind_state);
};
base::UnwindScenario scenario(base::BindRepeating(
&CallThroughV8, base::BindLambdaForTesting(set_unwind_state)));
base::ModuleCache module_cache;
std::vector<base::Frame> sample = SampleScenario(
&scenario, &module_cache, base::BindLambdaForTesting(create_v8_unwinder));
// The stack should contain a full unwind.
ExpectStackContains(sample, {scenario.GetWaitForSampleAddressRange(),
scenario.GetSetupFunctionAddressRange(),
scenario.GetOuterFunctionAddressRange()});
// The stack should contain a frame from a JavaScript module.
auto loc =
std::find_if(sample.begin(), sample.end(), [&](const base::Frame& frame) {
return frame.module &&
(frame.module->GetId() ==
V8Unwinder::kV8EmbeddedCodeRangeBuildId ||
frame.module->GetId() == V8Unwinder::kV8CodeRangeBuildId);
});
EXPECT_NE(sample.end(), loc);
}