1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/renderer/v8_unwinder.h"
11 #include "base/functional/bind.h"
12 #include "base/functional/callback.h"
13 #include "base/location.h"
14 #include "base/profiler/module_cache.h"
15 #include "base/profiler/stack_sampling_profiler_test_util.h"
16 #include "base/synchronization/waitable_event.h"
17 #include "base/test/bind.h"
18 #include "base/test/task_environment.h"
19 #include "build/build_config.h"
20 #include "gin/public/isolate_holder.h"
21 #include "testing/gmock/include/gmock/gmock.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 #include "v8/include/v8.h"
27 using ::testing::AllOf;
28 using ::testing::AnyOf;
29 using ::testing::Contains;
31 using ::testing::Field;
32 using ::testing::NotNull;
33 using ::testing::Pointee;
34 using ::testing::Property;
36 v8::Local<v8::String> ToV8String(const char* str) {
37 return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), str)
41 v8::Local<v8::Object> CreatePointerHolder(const void* ptr) {
42 v8::Local<v8::ObjectTemplate> object_template =
43 v8::ObjectTemplate::New(v8::Isolate::GetCurrent());
44 object_template->SetInternalFieldCount(1);
45 v8::Local<v8::Object> holder =
47 ->NewInstance(v8::Isolate::GetCurrent()->GetCurrentContext())
49 holder->SetAlignedPointerInInternalField(0, const_cast<void*>(ptr));
54 T* GetPointerFromHolder(v8::Local<v8::Object> holder) {
55 return reinterpret_cast<T*>(holder->GetAlignedPointerFromInternalField(0));
58 // Sets up the environment necessary to execute V8 code.
59 class ScopedV8Environment {
62 : isolate_holder_(task_environment_.GetMainThreadTaskRunner(),
63 gin::IsolateHolder::IsolateType::kBlinkMainThread) {
65 v8::HandleScope handle_scope(isolate());
66 context_.Reset(isolate(), v8::Context::New(isolate()));
67 v8::Local<v8::Context>::New(isolate(), context_)->Enter();
70 ~ScopedV8Environment() {
72 v8::HandleScope handle_scope(isolate());
73 v8::Local<v8::Context>::New(isolate(), context_)->Exit();
79 v8::Isolate* isolate() { return isolate_holder_.isolate(); }
82 base::test::TaskEnvironment task_environment_;
83 gin::IsolateHolder isolate_holder_;
84 v8::Persistent<v8::Context> context_;
87 // C++ function to be invoked from V8 which calls back into the provided closure
88 // pointer (passed via a holder object) to wait for a stack sample to be taken.
89 void WaitForSampleNative(const v8::FunctionCallbackInfo<v8::Value>& info) {
90 base::OnceClosure* wait_for_sample =
91 GetPointerFromHolder<base::OnceClosure>(info[0].As<v8::Object>());
93 std::move(*wait_for_sample).Run();
96 // Causes a stack sample to be taken after setting up a call stack from C++ to
97 // JavaScript and back into C++.
98 base::FunctionAddressRange CallThroughV8(
99 const base::RepeatingCallback<void(v8::Isolate*)>& report_isolate,
100 base::OnceClosure wait_for_sample) {
101 const void* start_program_counter = base::GetProgramCounter();
103 if (wait_for_sample) {
104 // Set up V8 runtime environment.
105 // Allows use of natives (functions starting with '%') within JavaScript
106 // code, which allows us to control compilation of the JavaScript function
108 // TODO(wittman): The flag should be set only for the duration of this test
109 // but the V8 API currently doesn't support this. http://crbug.com/v8/9210
110 // covers adding the necessary functionality to V8.
111 v8::V8::SetFlagsFromString("--allow-natives-syntax");
112 ScopedV8Environment v8_environment;
113 v8::Isolate* isolate = v8_environment.isolate();
114 report_isolate.Run(isolate);
115 v8::HandleScope handle_scope(isolate);
116 v8::Local<v8::Context> context = isolate->GetCurrentContext();
118 // Define a V8 function WaitForSampleNative() backed by the C++ function
119 // WaitForSampleNative().
120 v8::Local<v8::FunctionTemplate> js_wait_for_sample_native_template =
121 v8::FunctionTemplate::New(isolate, WaitForSampleNative);
122 v8::Local<v8::Function> js_wait_for_sample_native =
123 js_wait_for_sample_native_template->GetFunction(context)
125 js_wait_for_sample_native->SetName(ToV8String("WaitForSampleNative"));
127 ->Set(context, ToV8String("WaitForSampleNative"),
128 js_wait_for_sample_native)
131 // Run a script to create the V8 function waitForSample() that invokes
132 // WaitForSampleNative(), and a function that ensures that waitForSample()
133 // gets compiled. waitForSample() just passes the holder object for the
134 // pointer to the wait_for_sample Closure back into the C++ code. We ensure
135 // that the function is compiled to test walking through both builtin and
136 // runtime-generated code.
137 const char kWaitForSampleJs[] = R"(
138 function waitForSample(closure_pointer_holder) {
139 if (closure_pointer_holder)
140 WaitForSampleNative(closure_pointer_holder);
143 // Set up the function to be compiled rather than interpreted.
144 function compileWaitForSample(closure_pointer_holder) {
145 %PrepareFunctionForOptimization(waitForSample);
146 waitForSample(closure_pointer_holder);
147 waitForSample(closure_pointer_holder);
148 %OptimizeFunctionOnNextCall(waitForSample);
151 v8::Local<v8::Script> script =
152 v8::Script::Compile(context, ToV8String(kWaitForSampleJs))
154 script->Run(context).ToLocalChecked();
156 // Run compileWaitForSample(), using a null closure pointer to avoid
158 v8::Local<v8::Function> js_compile_wait_for_sample =
159 v8::Local<v8::Function>::Cast(
161 ->Get(context, ToV8String("compileWaitForSample"))
163 v8::Local<v8::Value> argv[] = {CreatePointerHolder(nullptr)};
164 js_compile_wait_for_sample
165 ->Call(context, v8::Undefined(isolate), std::size(argv), argv)
168 // Run waitForSample() with the real closure pointer.
169 argv[0] = CreatePointerHolder(&wait_for_sample);
170 v8::Local<v8::Function> js_wait_for_sample = v8::Local<v8::Function>::Cast(
172 ->Get(context, ToV8String("waitForSample"))
175 ->Call(context, v8::Undefined(isolate), std::size(argv), argv)
179 // Volatile to prevent a tail call to GetProgramCounter().
180 const void* volatile end_program_counter = base::GetProgramCounter();
181 return {start_program_counter, end_program_counter};
184 class UpdateModulesTestUnwinder : public V8Unwinder {
186 explicit UpdateModulesTestUnwinder(v8::Isolate* isolate)
187 : V8Unwinder(isolate) {}
189 void SetCodePages(std::vector<v8::MemoryRange> code_pages) {
190 code_pages_to_provide_ = code_pages;
194 size_t CopyCodePages(size_t capacity, v8::MemoryRange* code_pages) override {
195 std::copy_n(code_pages_to_provide_.begin(),
196 std::min(capacity, code_pages_to_provide_.size()), code_pages);
197 return code_pages_to_provide_.size();
201 std::vector<v8::MemoryRange> code_pages_to_provide_;
204 v8::MemoryRange GetEmbeddedCodeRange(v8::Isolate* isolate) {
205 v8::MemoryRange range;
206 isolate->GetEmbeddedCodeRange(&range.start, &range.length_in_bytes);
212 TEST(V8UnwinderTest, EmbeddedCodeRangeModule) {
213 ScopedV8Environment v8_environment;
214 V8Unwinder unwinder(v8_environment.isolate());
215 base::ModuleCache module_cache;
217 unwinder.Initialize(&module_cache);
219 v8::MemoryRange embedded_code_range;
220 v8_environment.isolate()->GetEmbeddedCodeRange(
221 &embedded_code_range.start, &embedded_code_range.length_in_bytes);
223 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(
224 reinterpret_cast<uintptr_t>(embedded_code_range.start));
225 ASSERT_NE(nullptr, module);
226 EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId());
229 TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnUpdate) {
230 ScopedV8Environment v8_environment;
231 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
232 base::ModuleCache module_cache;
234 unwinder.Initialize(&module_cache);
236 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
237 GetEmbeddedCodeRange(v8_environment.isolate())});
239 unwinder.OnStackCapture();
240 unwinder.UpdateModules();
242 v8::MemoryRange embedded_code_range;
243 v8_environment.isolate()->GetEmbeddedCodeRange(
244 &embedded_code_range.start, &embedded_code_range.length_in_bytes);
246 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(
247 reinterpret_cast<uintptr_t>(embedded_code_range.start));
248 ASSERT_NE(nullptr, module);
249 EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId());
252 // Checks that the embedded code range is preserved even if it wasn't included
253 // in the code pages due to insufficient capacity.
254 TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnOverCapacityUpdate) {
255 ScopedV8Environment v8_environment;
256 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
257 base::ModuleCache module_cache;
259 unwinder.Initialize(&module_cache);
261 const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
262 std::vector<v8::MemoryRange> code_pages;
263 code_pages.reserve(kDefaultCapacity + 1);
264 for (int i = 0; i < kDefaultCapacity + 1; ++i)
265 code_pages.push_back({reinterpret_cast<void*>(i + 1), 1});
266 unwinder.SetCodePages(code_pages);
268 unwinder.OnStackCapture();
269 unwinder.UpdateModules();
271 v8::MemoryRange embedded_code_range;
272 v8_environment.isolate()->GetEmbeddedCodeRange(
273 &embedded_code_range.start, &embedded_code_range.length_in_bytes);
275 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(
276 reinterpret_cast<uintptr_t>(embedded_code_range.start));
277 ASSERT_NE(nullptr, module);
278 EXPECT_EQ(V8Unwinder::kV8EmbeddedCodeRangeBuildId, module->GetId());
281 TEST(V8UnwinderTest, UpdateModules_ModuleAdded) {
282 ScopedV8Environment v8_environment;
283 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
284 base::ModuleCache module_cache;
286 unwinder.Initialize(&module_cache);
287 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
288 GetEmbeddedCodeRange(v8_environment.isolate())});
289 unwinder.OnStackCapture();
290 unwinder.UpdateModules();
292 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1);
293 ASSERT_NE(nullptr, module);
294 EXPECT_EQ(1u, module->GetBaseAddress());
295 EXPECT_EQ(10u, module->GetSize());
296 EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId());
297 EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII());
300 // Check that modules added before the last module are propagated to the
301 // ModuleCache. This case takes a different code path in the implementation.
302 TEST(V8UnwinderTest, UpdateModules_ModuleAddedBeforeLast) {
303 ScopedV8Environment v8_environment;
304 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
305 base::ModuleCache module_cache;
307 unwinder.Initialize(&module_cache);
309 unwinder.SetCodePages({{reinterpret_cast<void*>(100), 10},
310 GetEmbeddedCodeRange(v8_environment.isolate())});
311 unwinder.OnStackCapture();
312 unwinder.UpdateModules();
314 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
315 {reinterpret_cast<void*>(100), 10},
316 GetEmbeddedCodeRange(v8_environment.isolate())});
317 unwinder.OnStackCapture();
318 unwinder.UpdateModules();
320 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1);
321 ASSERT_NE(nullptr, module);
322 EXPECT_EQ(1u, module->GetBaseAddress());
323 EXPECT_EQ(10u, module->GetSize());
324 EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId());
325 EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII());
328 TEST(V8UnwinderTest, UpdateModules_ModuleRetained) {
329 ScopedV8Environment v8_environment;
330 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
331 base::ModuleCache module_cache;
333 unwinder.Initialize(&module_cache);
335 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
336 GetEmbeddedCodeRange(v8_environment.isolate())});
337 unwinder.OnStackCapture();
338 unwinder.UpdateModules();
340 // Code pages remain the same for this stack capture.
341 unwinder.OnStackCapture();
342 unwinder.UpdateModules();
344 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1);
345 ASSERT_NE(nullptr, module);
346 EXPECT_EQ(1u, module->GetBaseAddress());
347 EXPECT_EQ(10u, module->GetSize());
348 EXPECT_EQ(V8Unwinder::kV8CodeRangeBuildId, module->GetId());
349 EXPECT_EQ("V8 Code Range", module->GetDebugBasename().MaybeAsASCII());
352 TEST(V8UnwinderTest, UpdateModules_ModuleRetainedWithDifferentSize) {
353 ScopedV8Environment v8_environment;
354 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
355 base::ModuleCache module_cache;
357 unwinder.Initialize(&module_cache);
359 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
360 GetEmbeddedCodeRange(v8_environment.isolate())});
361 unwinder.OnStackCapture();
362 unwinder.UpdateModules();
364 // Code pages remain the same for this stack capture.
365 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 20},
366 GetEmbeddedCodeRange(v8_environment.isolate())});
367 unwinder.OnStackCapture();
368 unwinder.UpdateModules();
370 const base::ModuleCache::Module* module =
371 module_cache.GetModuleForAddress(11);
372 ASSERT_NE(nullptr, module);
373 EXPECT_EQ(1u, module->GetBaseAddress());
374 EXPECT_EQ(20u, module->GetSize());
377 TEST(V8UnwinderTest, UpdateModules_ModuleRemoved) {
378 ScopedV8Environment v8_environment;
379 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
380 base::ModuleCache module_cache;
382 unwinder.Initialize(&module_cache);
384 unwinder.SetCodePages({{{reinterpret_cast<void*>(1), 10},
385 GetEmbeddedCodeRange(v8_environment.isolate())}});
386 unwinder.OnStackCapture();
387 unwinder.UpdateModules();
389 unwinder.SetCodePages({GetEmbeddedCodeRange(v8_environment.isolate())});
390 unwinder.OnStackCapture();
391 unwinder.UpdateModules();
393 EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(1));
396 // Check that modules removed before the last module are propagated to the
397 // ModuleCache. This case takes a different code path in the implementation.
398 TEST(V8UnwinderTest, UpdateModules_ModuleRemovedBeforeLast) {
399 ScopedV8Environment v8_environment;
400 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
401 base::ModuleCache module_cache;
403 unwinder.Initialize(&module_cache);
405 unwinder.SetCodePages({{{reinterpret_cast<void*>(1), 10},
406 {reinterpret_cast<void*>(100), 10},
407 GetEmbeddedCodeRange(v8_environment.isolate())}});
408 unwinder.OnStackCapture();
409 unwinder.UpdateModules();
411 unwinder.SetCodePages({{reinterpret_cast<void*>(100), 10},
412 GetEmbeddedCodeRange(v8_environment.isolate())});
413 unwinder.OnStackCapture();
414 unwinder.UpdateModules();
416 EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(1));
419 TEST(V8UnwinderTest, UpdateModules_CapacityExceeded) {
420 ScopedV8Environment v8_environment;
421 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
422 base::ModuleCache module_cache;
424 unwinder.Initialize(&module_cache);
426 const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
428 std::vector<v8::MemoryRange> code_pages;
429 // Create kDefaultCapacity + 2 code pages, with the last being the embedded
431 code_pages.reserve(kDefaultCapacity + 2);
432 for (int i = 0; i < kDefaultCapacity + 1; ++i)
433 code_pages.push_back({reinterpret_cast<void*>(i + 1), 1});
434 code_pages.push_back(GetEmbeddedCodeRange(v8_environment.isolate()));
436 // The first sample should successfully create modules up to the default
438 unwinder.SetCodePages(code_pages);
439 unwinder.OnStackCapture();
440 unwinder.UpdateModules();
442 EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity));
443 EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1));
445 // The capacity should be expanded by the second sample.
446 unwinder.SetCodePages(code_pages);
447 unwinder.OnStackCapture();
448 unwinder.UpdateModules();
450 EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity));
451 EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1));
454 // Checks that the implementation can handle the capacity being exceeded by a
456 TEST(V8UnwinderTest, UpdateModules_CapacitySubstantiallyExceeded) {
457 ScopedV8Environment v8_environment;
458 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
459 base::ModuleCache module_cache;
461 unwinder.Initialize(&module_cache);
463 const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
464 const int kCodePages = kDefaultCapacity * 3;
465 std::vector<v8::MemoryRange> code_pages;
466 code_pages.reserve(kCodePages);
467 // Create kCodePages with the last being the embedded code page.
468 for (int i = 0; i < kCodePages - 1; ++i)
469 code_pages.push_back({reinterpret_cast<void*>(i + 1), 1});
470 code_pages.push_back(GetEmbeddedCodeRange(v8_environment.isolate()));
472 // The first sample should successfully create modules up to the default
474 unwinder.SetCodePages(code_pages);
475 unwinder.OnStackCapture();
476 unwinder.UpdateModules();
478 EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity));
479 EXPECT_EQ(nullptr, module_cache.GetModuleForAddress(kDefaultCapacity + 1));
481 // The capacity should be expanded by the second sample to handle all the
482 // available modules.
483 unwinder.SetCodePages(code_pages);
484 unwinder.OnStackCapture();
485 unwinder.UpdateModules();
487 EXPECT_NE(nullptr, module_cache.GetModuleForAddress(kCodePages - 1));
490 TEST(V8UnwinderTest, CanUnwindFrom_V8Module) {
491 ScopedV8Environment v8_environment;
492 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
493 base::ModuleCache module_cache;
495 unwinder.Initialize(&module_cache);
497 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
498 GetEmbeddedCodeRange(v8_environment.isolate())});
499 unwinder.OnStackCapture();
500 unwinder.UpdateModules();
502 const base::ModuleCache::Module* module = module_cache.GetModuleForAddress(1);
503 ASSERT_NE(nullptr, module);
505 EXPECT_TRUE(unwinder.CanUnwindFrom({1, module}));
508 TEST(V8UnwinderTest, CanUnwindFrom_OtherModule) {
509 ScopedV8Environment v8_environment;
510 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
511 base::ModuleCache module_cache;
513 unwinder.Initialize(&module_cache);
515 unwinder.SetCodePages({GetEmbeddedCodeRange(v8_environment.isolate())});
516 unwinder.OnStackCapture();
517 unwinder.UpdateModules();
519 auto other_module = std::make_unique<base::TestModule>(1, 10);
520 const base::ModuleCache::Module* other_module_ptr = other_module.get();
521 module_cache.AddCustomNativeModule(std::move(other_module));
523 EXPECT_FALSE(unwinder.CanUnwindFrom({1, other_module_ptr}));
526 TEST(V8UnwinderTest, CanUnwindFrom_NullModule) {
527 ScopedV8Environment v8_environment;
528 UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
529 base::ModuleCache module_cache;
531 unwinder.Initialize(&module_cache);
533 // Insert a non-native module to potentially exercise the Module comparator.
534 unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
535 GetEmbeddedCodeRange(v8_environment.isolate())});
536 unwinder.OnStackCapture();
537 unwinder.UpdateModules();
539 EXPECT_FALSE(unwinder.CanUnwindFrom({20, nullptr}));
542 // Checks that unwinding from C++ through JavaScript and back into C++ succeeds.
543 #if (BUILDFLAG(IS_WIN) && defined(ARCH_CPU_64_BITS)) || BUILDFLAG(IS_MAC) || \
544 (BUILDFLAG(IS_ANDROID) && defined(ARCH_CPU_ARMEL))
545 #define MAYBE_UnwindThroughV8Frames UnwindThroughV8Frames
547 #define MAYBE_UnwindThroughV8Frames DISABLED_UnwindThroughV8Frames
549 TEST(V8UnwinderTest, MAYBE_UnwindThroughV8Frames) {
550 v8::Isolate* isolate = nullptr;
551 base::WaitableEvent isolate_available;
553 const auto set_isolate = [&](v8::Isolate* isolate_state) {
554 isolate = isolate_state;
555 isolate_available.Signal();
558 const auto create_v8_unwinder = [&]() -> std::unique_ptr<base::Unwinder> {
559 isolate_available.Wait();
560 return std::make_unique<V8Unwinder>(isolate);
563 base::UnwindScenario scenario(base::BindRepeating(
564 &CallThroughV8, base::BindLambdaForTesting(set_isolate)));
565 base::ModuleCache module_cache;
567 std::vector<base::Frame> sample = SampleScenario(
568 &scenario, &module_cache, base::BindLambdaForTesting(create_v8_unwinder));
570 // The stack should contain a full unwind.
571 ExpectStackContains(sample, {scenario.GetWaitForSampleAddressRange(),
572 scenario.GetSetupFunctionAddressRange(),
573 scenario.GetOuterFunctionAddressRange()});
575 // The stack should contain a frame from a JavaScript module.
578 "module", &base::Frame::module,
581 "module.id", &base::ModuleCache::Module::GetId,
582 AnyOf(Eq(V8Unwinder::kV8EmbeddedCodeRangeBuildId),
583 Eq(V8Unwinder::kV8CodeRangeBuildId))))))));