From 24d1ef4868ba15eccaebb26ef651877d94c83e2a Mon Sep 17 00:00:00 2001 From: "alph@chromium.org" Date: Mon, 29 Sep 2014 12:59:54 +0000 Subject: [PATCH] Initial implementation of GetStackSample sampling profiler API. The patch is based on https://codereview.chromium.org/578163002/#ps20001 made by gholap@chromium.org LOG=N BUG=v8:3490 R=bmeurer@chromium.org, yurys@chromium.org Review URL: https://codereview.chromium.org/596533002 git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@24285 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- include/v8.h | 36 ++++++ src/api.cc | 25 ++-- src/globals.h | 16 --- src/sampler.cc | 69 ++++++----- src/sampler.h | 16 ++- src/vm-state.h | 5 + test/cctest/cctest.gyp | 1 + test/cctest/test-api.cc | 2 +- test/cctest/test-sampler-api.cc | 245 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 353 insertions(+), 62 deletions(-) create mode 100644 test/cctest/test-sampler-api.cc diff --git a/include/v8.h b/include/v8.h index 63c6762..4d5d713 100644 --- a/include/v8.h +++ b/include/v8.h @@ -1415,6 +1415,27 @@ class V8_EXPORT StackFrame { }; +// A StateTag represents a possible state of the VM. +enum StateTag { JS, GC, COMPILER, OTHER, EXTERNAL, IDLE }; + + +// A RegisterState represents the current state of registers used +// by the sampling profiler API. +struct RegisterState { + RegisterState() : pc(NULL), sp(NULL), fp(NULL) {} + void* pc; // Instruction pointer. + void* sp; // Stack pointer. + void* fp; // Frame pointer. +}; + + +// The output structure filled up by GetStackSample API function. +struct SampleInfo { + size_t frames_count; + StateTag vm_state; +}; + + /** * A JSON Parser. */ @@ -4560,6 +4581,21 @@ class V8_EXPORT Isolate { void GetHeapStatistics(HeapStatistics* heap_statistics); /** + * Get a call stack sample from the isolate. + * \param state Execution state. + * \param frames Caller allocated buffer to store stack frames. + * \param frames_limit Maximum number of frames to capture. The buffer must + * be large enough to hold the number of frames. + * \param sample_info The sample info is filled up by the function + * provides number of actual captured stack frames and + * the current VM state. + * \note GetStackSample should only be called when the JS thread is paused or + * interrupted. Otherwise the behavior is undefined. + */ + void GetStackSample(const RegisterState& state, void** frames, + size_t frames_limit, SampleInfo* sample_info); + + /** * Adjusts the amount of registered external memory. Used to give V8 an * indication of the amount of externally allocated memory that is kept alive * by JavaScript objects. V8 uses this to decide when to perform global diff --git a/src/api.cc b/src/api.cc index 0fbdf7b..63ea457 100644 --- a/src/api.cc +++ b/src/api.cc @@ -40,6 +40,7 @@ #include "src/prototype.h" #include "src/runtime/runtime.h" #include "src/runtime-profiler.h" +#include "src/sampler.h" #include "src/scanner-character-streams.h" #include "src/simulator.h" #include "src/snapshot.h" @@ -51,9 +52,9 @@ #define LOG_API(isolate, expr) LOG(isolate, ApiEntryCall(expr)) -#define ENTER_V8(isolate) \ - DCHECK((isolate)->IsInitialized()); \ - i::VMState __state__((isolate)) +#define ENTER_V8(isolate) \ + DCHECK((isolate)->IsInitialized()); \ + i::VMState __state__((isolate)) namespace v8 { @@ -6702,6 +6703,14 @@ void Isolate::GetHeapStatistics(HeapStatistics* heap_statistics) { } +void Isolate::GetStackSample(const RegisterState& state, void** frames, + size_t frames_limit, SampleInfo* sample_info) { + i::Isolate* isolate = reinterpret_cast(this); + i::TickSample::GetStackSample(isolate, state, frames, frames_limit, + sample_info); +} + + void Isolate::SetEventLogger(LogEventCallback that) { // Do not overwrite the event logger if we want to log explicitly. if (i::FLAG_log_timer_events) return; @@ -7199,13 +7208,13 @@ const CpuProfile* CpuProfiler::StopCpuProfiling(Handle title) { void CpuProfiler::SetIdle(bool is_idle) { i::Isolate* isolate = reinterpret_cast(this)->isolate(); - i::StateTag state = isolate->current_vm_state(); - DCHECK(state == i::EXTERNAL || state == i::IDLE); + v8::StateTag state = isolate->current_vm_state(); + DCHECK(state == v8::EXTERNAL || state == v8::IDLE); if (isolate->js_entry_sp() != NULL) return; if (is_idle) { - isolate->set_current_vm_state(i::IDLE); - } else if (state == i::IDLE) { - isolate->set_current_vm_state(i::EXTERNAL); + isolate->set_current_vm_state(v8::IDLE); + } else if (state == v8::IDLE) { + isolate->set_current_vm_state(v8::EXTERNAL); } } diff --git a/src/globals.h b/src/globals.h index 609ab88..d42caf7 100644 --- a/src/globals.h +++ b/src/globals.h @@ -547,22 +547,6 @@ struct AccessorDescriptor { }; -// Logging and profiling. A StateTag represents a possible state of -// the VM. The logger maintains a stack of these. Creating a VMState -// object enters a state by pushing on the stack, and destroying a -// VMState object leaves a state by popping the current state from the -// stack. - -enum StateTag { - JS, - GC, - COMPILER, - OTHER, - EXTERNAL, - IDLE -}; - - // ----------------------------------------------------------------------------- // Macros diff --git a/src/sampler.cc b/src/sampler.cc index 394efeb..4633712 100644 --- a/src/sampler.cc +++ b/src/sampler.cc @@ -226,13 +226,13 @@ class Sampler::PlatformData : public PlatformDataCommon { #if defined(USE_SIMULATOR) class SimulatorHelper { public: - inline bool Init(Sampler* sampler, Isolate* isolate) { + inline bool Init(Isolate* isolate) { simulator_ = isolate->thread_local_top()->simulator_; // Check if there is active simulator. return simulator_ != NULL; } - inline void FillRegisters(RegisterState* state) { + inline void FillRegisters(v8::RegisterState* state) { #if V8_TARGET_ARCH_ARM state->pc = reinterpret_cast
(simulator_->get_pc()); state->sp = reinterpret_cast
(simulator_->get_register( @@ -241,22 +241,16 @@ class SimulatorHelper { Simulator::r11)); #elif V8_TARGET_ARCH_ARM64 if (simulator_->sp() == 0 || simulator_->fp() == 0) { - // It possible that the simulator is interrupted while it is updating + // It's possible that the simulator is interrupted while it is updating // the sp or fp register. ARM64 simulator does this in two steps: - // first setting it to zero and then setting it to the new value. + // first setting it to zero and then setting it to a new value. // Bailout if sp/fp doesn't contain the new value. return; } state->pc = reinterpret_cast
(simulator_->pc()); state->sp = reinterpret_cast
(simulator_->sp()); state->fp = reinterpret_cast
(simulator_->fp()); -#elif V8_TARGET_ARCH_MIPS - state->pc = reinterpret_cast
(simulator_->get_pc()); - state->sp = reinterpret_cast
(simulator_->get_register( - Simulator::sp)); - state->fp = reinterpret_cast
(simulator_->get_register( - Simulator::fp)); -#elif V8_TARGET_ARCH_MIPS64 +#elif V8_TARGET_ARCH_MIPS || V8_TARGET_ARCH_MIPS64 state->pc = reinterpret_cast
(simulator_->get_pc()); state->sp = reinterpret_cast
(simulator_->get_register( Simulator::sp)); @@ -353,11 +347,11 @@ void SignalHandler::HandleProfilerSignal(int signal, siginfo_t* info, Sampler* sampler = isolate->logger()->sampler(); if (sampler == NULL) return; - RegisterState state; + v8::RegisterState state; #if defined(USE_SIMULATOR) SimulatorHelper helper; - if (!helper.Init(sampler, isolate)) return; + if (!helper.Init(isolate)) return; helper.FillRegisters(&state); // It possible that the simulator is interrupted while it is updating // the sp or fp register. ARM64 simulator does this in two steps: @@ -577,20 +571,17 @@ SamplerThread* SamplerThread::instance_ = NULL; // StackTracer implementation // DISABLE_ASAN void TickSample::Init(Isolate* isolate, - const RegisterState& regs) { + const v8::RegisterState& regs) { DCHECK(isolate->IsInitialized()); timestamp = base::TimeTicks::HighResolutionNow(); - pc = regs.pc; + pc = reinterpret_cast
(regs.pc); state = isolate->current_vm_state(); // Avoid collecting traces while doing GC. if (state == GC) return; Address js_entry_sp = isolate->js_entry_sp(); - if (js_entry_sp == 0) { - // Not executing JS now. - return; - } + if (js_entry_sp == 0) return; // Not executing JS now. ExternalCallbackScope* scope = isolate->external_callback_scope(); Address handler = Isolate::handler(isolate->thread_local_top()); @@ -603,18 +594,40 @@ DISABLE_ASAN void TickSample::Init(Isolate* isolate, } else { // Sample potential return address value for frameless invocation of // stubs (we'll figure out later, if this value makes sense). - tos = Memory::Address_at(regs.sp); + tos = Memory::Address_at(reinterpret_cast
(regs.sp)); has_external_callback = false; } - SafeStackFrameIterator it(isolate, regs.fp, regs.sp, js_entry_sp); + SafeStackFrameIterator it(isolate, reinterpret_cast
(regs.fp), + reinterpret_cast
(regs.sp), js_entry_sp); top_frame_type = it.top_frame_type(); - unsigned i = 0; - while (!it.done() && i < TickSample::kMaxFramesCount) { - stack[i++] = it.frame()->pc(); + + SampleInfo info; + GetStackSample(isolate, regs, reinterpret_cast(&stack[0]), + kMaxFramesCount, &info); + frames_count = static_cast(info.frames_count); +} + + +void TickSample::GetStackSample(Isolate* isolate, const v8::RegisterState& regs, + void** frames, size_t frames_limit, + v8::SampleInfo* sample_info) { + DCHECK(isolate->IsInitialized()); + sample_info->frames_count = 0; + sample_info->vm_state = isolate->current_vm_state(); + if (sample_info->vm_state == GC) return; + + Address js_entry_sp = isolate->js_entry_sp(); + if (js_entry_sp == 0) return; // Not executing JS now. + + SafeStackFrameIterator it(isolate, reinterpret_cast
(regs.fp), + reinterpret_cast
(regs.sp), js_entry_sp); + size_t i = 0; + while (!it.done() && i < frames_limit) { + frames[i++] = it.frame()->pc(); it.Advance(); } - frames_count = i; + sample_info->frames_count = i; } @@ -682,7 +695,7 @@ void Sampler::DecreaseProfilingDepth() { } -void Sampler::SampleStack(const RegisterState& state) { +void Sampler::SampleStack(const v8::RegisterState& state) { TickSample* sample = isolate_->cpu_profiler()->StartTickSample(); TickSample sample_obj; if (sample == NULL) sample = &sample_obj; @@ -714,7 +727,7 @@ void Sampler::DoSample() { #if defined(USE_SIMULATOR) SimulatorHelper helper; - if (!helper.Init(this, isolate())) return; + if (!helper.Init(isolate())) return; #endif const DWORD kSuspendFailed = static_cast(-1); @@ -725,7 +738,7 @@ void Sampler::DoSample() { memset(&context, 0, sizeof(context)); context.ContextFlags = CONTEXT_FULL; if (GetThreadContext(profiled_thread, &context) != 0) { - RegisterState state; + v8::RegisterState state; #if defined(USE_SIMULATOR) helper.FillRegisters(&state); #else diff --git a/src/sampler.h b/src/sampler.h index c3dce4e..7b84af2 100644 --- a/src/sampler.h +++ b/src/sampler.h @@ -5,6 +5,8 @@ #ifndef V8_SAMPLER_H_ #define V8_SAMPLER_H_ +#include "include/v8.h" + #include "src/base/atomicops.h" #include "src/frames.h" #include "src/globals.h" @@ -21,13 +23,6 @@ class Isolate; // (if used for profiling) the program counter and stack pointer for // the thread that created it. -struct RegisterState { - RegisterState() : pc(NULL), sp(NULL), fp(NULL) {} - Address pc; // Instruction pointer. - Address sp; // Stack pointer. - Address fp; // Frame pointer. -}; - // TickSample captures the information collected for each sample. struct TickSample { TickSample() @@ -37,7 +32,10 @@ struct TickSample { frames_count(0), has_external_callback(false), top_frame_type(StackFrame::NONE) {} - void Init(Isolate* isolate, const RegisterState& state); + void Init(Isolate* isolate, const v8::RegisterState& state); + static void GetStackSample(Isolate* isolate, const v8::RegisterState& state, + void** frames, size_t frames_limit, + v8::SampleInfo* sample_info); StateTag state; // The state of the VM. Address pc; // Instruction pointer. union { @@ -67,7 +65,7 @@ class Sampler { int interval() const { return interval_; } // Performs stack sampling. - void SampleStack(const RegisterState& regs); + void SampleStack(const v8::RegisterState& regs); // Start and stop sampler. void Start(); diff --git a/src/vm-state.h b/src/vm-state.h index a72180c..9838b87 100644 --- a/src/vm-state.h +++ b/src/vm-state.h @@ -11,6 +11,11 @@ namespace v8 { namespace internal { +// Logging and profiling. A StateTag represents a possible state of +// the VM. The logger maintains a stack of these. Creating a VMState +// object enters a state by pushing on the stack, and destroying a +// VMState object leaves a state by popping the current state from the +// stack. template class VMState BASE_EMBEDDED { public: diff --git a/test/cctest/cctest.gyp b/test/cctest/cctest.gyp index f993d26..74acbf0 100644 --- a/test/cctest/cctest.gyp +++ b/test/cctest/cctest.gyp @@ -143,6 +143,7 @@ 'test-regexp.cc', 'test-reloc-info.cc', 'test-representation.cc', + 'test-sampler-api.cc', 'test-serialize.cc', 'test-spaces.cc', 'test-strings.cc', diff --git a/test/cctest/test-api.cc b/test/cctest/test-api.cc index 0330ac8..bf2ef23 100644 --- a/test/cctest/test-api.cc +++ b/test/cctest/test-api.cc @@ -981,7 +981,7 @@ static void CheckReturnValue(const T& t, i::Address callback) { // If CPU profiler is active check that when API callback is invoked // VMState is set to EXTERNAL. if (isolate->cpu_profiler()->is_profiling()) { - CHECK_EQ(i::EXTERNAL, isolate->current_vm_state()); + CHECK_EQ(v8::EXTERNAL, isolate->current_vm_state()); CHECK(isolate->external_callback_scope()); CHECK_EQ(callback, isolate->external_callback_scope()->callback()); } diff --git a/test/cctest/test-sampler-api.cc b/test/cctest/test-sampler-api.cc new file mode 100644 index 0000000..2f6f92e --- /dev/null +++ b/test/cctest/test-sampler-api.cc @@ -0,0 +1,245 @@ +// Copyright 2014 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tests the sampling API in include/v8.h + +#include +#include +#include "include/v8.h" +#include "src/simulator.h" +#include "test/cctest/cctest.h" + +namespace { + +class Sample { + public: + enum { kFramesLimit = 255 }; + + Sample() {} + + typedef const void* const* const_iterator; + const_iterator begin() const { return data_.start(); } + const_iterator end() const { return &data_[data_.length()]; } + + int size() const { return data_.length(); } + v8::internal::Vector& data() { return data_; } + + private: + v8::internal::EmbeddedVector data_; +}; + + +#if defined(USE_SIMULATOR) +class SimulatorHelper { + public: + inline bool Init(v8::Isolate* isolate) { + simulator_ = reinterpret_cast(isolate) + ->thread_local_top() + ->simulator_; + // Check if there is active simulator. + return simulator_ != NULL; + } + + inline void FillRegisters(v8::RegisterState* state) { +#if V8_TARGET_ARCH_ARM + state->pc = reinterpret_cast(simulator_->get_pc()); + state->sp = reinterpret_cast( + simulator_->get_register(v8::internal::Simulator::sp)); + state->fp = reinterpret_cast( + simulator_->get_register(v8::internal::Simulator::r11)); +#elif V8_TARGET_ARCH_ARM64 + if (simulator_->sp() == 0 || simulator_->fp() == 0) { + // It's possible that the simulator is interrupted while it is updating + // the sp or fp register. ARM64 simulator does this in two steps: + // first setting it to zero and then setting it to a new value. + // Bailout if sp/fp doesn't contain the new value. + return; + } + state->pc = reinterpret_cast(simulator_->pc()); + state->sp = reinterpret_cast(simulator_->sp()); + state->fp = reinterpret_cast(simulator_->fp()); +#elif V8_TARGET_ARCH_MIPS || V8_TARGET_ARCH_MIPS64 + state->pc = reinterpret_cast(simulator_->get_pc()); + state->sp = reinterpret_cast( + simulator_->get_register(v8::internal::Simulator::sp)); + state->fp = reinterpret_cast( + simulator_->get_register(v8::internal::Simulator::fp)); +#endif + } + + private: + v8::internal::Simulator* simulator_; +}; +#endif // USE_SIMULATOR + + +class SamplingTestHelper { + public: + struct CodeEventEntry { + std::string name; + const void* code_start; + size_t code_len; + }; + typedef std::map CodeEntries; + + explicit SamplingTestHelper(const std::string& test_function) + : sample_is_taken_(false), isolate_(CcTest::isolate()) { + DCHECK_EQ(NULL, instance_); + instance_ = this; + v8::HandleScope scope(isolate_); + v8::Handle global = v8::ObjectTemplate::New(isolate_); + global->Set(v8::String::NewFromUtf8(isolate_, "CollectSample"), + v8::FunctionTemplate::New(isolate_, CollectSample)); + LocalContext env(isolate_, NULL, global); + isolate_->SetJitCodeEventHandler(v8::kJitCodeEventDefault, + JitCodeEventHandler); + v8::Script::Compile( + v8::String::NewFromUtf8(isolate_, test_function.c_str()))->Run(); + } + + ~SamplingTestHelper() { + isolate_->SetJitCodeEventHandler(v8::kJitCodeEventDefault, NULL); + instance_ = NULL; + } + + Sample& sample() { return sample_; } + + const CodeEventEntry* FindEventEntry(const void* address) { + CodeEntries::const_iterator it = code_entries_.upper_bound(address); + if (it == code_entries_.begin()) return NULL; + const CodeEventEntry& entry = (--it)->second; + const void* code_end = + static_cast(entry.code_start) + entry.code_len; + return address < code_end ? &entry : NULL; + } + + private: + static void CollectSample(const v8::FunctionCallbackInfo& args) { + instance_->DoCollectSample(); + } + + static void JitCodeEventHandler(const v8::JitCodeEvent* event) { + instance_->DoJitCodeEventHandler(event); + } + + // The JavaScript calls this function when on full stack depth. + void DoCollectSample() { + v8::RegisterState state; +#if defined(USE_SIMULATOR) + SimulatorHelper simulator_helper; + if (!simulator_helper.Init(isolate_)) return; + simulator_helper.FillRegisters(&state); +#else + state.pc = NULL; + state.fp = &state; + state.sp = &state; +#endif + v8::SampleInfo info; + isolate_->GetStackSample(state, sample_.data().start(), + static_cast(sample_.size()), &info); + size_t frames_count = info.frames_count; + CHECK_LE(frames_count, static_cast(sample_.size())); + sample_.data().Truncate(static_cast(frames_count)); + sample_is_taken_ = true; + } + + void DoJitCodeEventHandler(const v8::JitCodeEvent* event) { + if (sample_is_taken_) return; + switch (event->type) { + case v8::JitCodeEvent::CODE_ADDED: { + CodeEventEntry entry; + entry.name = std::string(event->name.str, event->name.len); + entry.code_start = event->code_start; + entry.code_len = event->code_len; + code_entries_.insert(std::make_pair(entry.code_start, entry)); + break; + } + case v8::JitCodeEvent::CODE_MOVED: { + CodeEntries::iterator it = code_entries_.find(event->code_start); + CHECK(it != code_entries_.end()); + code_entries_.erase(it); + CodeEventEntry entry; + entry.name = std::string(event->name.str, event->name.len); + entry.code_start = event->new_code_start; + entry.code_len = event->code_len; + code_entries_.insert(std::make_pair(entry.code_start, entry)); + break; + } + case v8::JitCodeEvent::CODE_REMOVED: + code_entries_.erase(event->code_start); + break; + default: + break; + } + } + + Sample sample_; + bool sample_is_taken_; + v8::Isolate* isolate_; + CodeEntries code_entries_; + + static SamplingTestHelper* instance_; +}; + +SamplingTestHelper* SamplingTestHelper::instance_; + +} // namespace + + +// A JavaScript function which takes stack depth +// (minimum value 2) as an argument. +// When at the bottom of the recursion, +// the JavaScript code calls into C++ test code, +// waiting for the sampler to take a sample. +static const char* test_function = + "function func(depth) {" + " if (depth == 2) CollectSample();" + " else return func(depth - 1);" + "}"; + + +TEST(StackDepthIsConsistent) { + SamplingTestHelper helper(std::string(test_function) + "func(8);"); + CHECK_EQ(8, helper.sample().size()); +} + + +TEST(StackDepthDoesNotExceedMaxValue) { + SamplingTestHelper helper(std::string(test_function) + "func(300);"); + CHECK_EQ(Sample::kFramesLimit, helper.sample().size()); +} + + +// The captured sample should have three pc values. +// They should fall in the range where the compiled code resides. +// The expected stack is: +// bottom of stack [{anon script}, outer, inner] top of stack +// ^ ^ ^ +// sample.stack indices 2 1 0 +TEST(StackFramesConsistent) { + // Note: The arguments.callee stuff is there so that the + // functions are not optimized away. + const char* test_script = + "function test_sampler_api_inner() {" + " CollectSample();" + " return arguments.callee.toString();" + "}" + "function test_sampler_api_outer() {" + " return test_sampler_api_inner() + arguments.callee.toString();" + "}" + "test_sampler_api_outer();"; + + SamplingTestHelper helper(test_script); + Sample& sample = helper.sample(); + CHECK_EQ(3, sample.size()); + + const SamplingTestHelper::CodeEventEntry* entry; + entry = helper.FindEventEntry(sample.begin()[0]); + CHECK_NE(NULL, entry); + CHECK(std::string::npos != entry->name.find("test_sampler_api_inner")); + + entry = helper.FindEventEntry(sample.begin()[1]); + CHECK_NE(NULL, entry); + CHECK(std::string::npos != entry->name.find("test_sampler_api_outer")); +} -- 2.7.4