// Copyright 2019 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include #include #include "base/functional/bind.h" #include "base/memory/ptr_util.h" #include "base/memory/raw_ptr.h" #include "base/memory/raw_ref.h" #include "base/profiler/module_cache.h" #include "base/profiler/profile_builder.h" #include "base/profiler/stack_buffer.h" #include "base/profiler/stack_copier.h" #include "base/profiler/stack_sampler.h" #include "base/profiler/stack_sampling_profiler_test_util.h" #include "base/profiler/suspendable_thread_delegate.h" #include "base/profiler/unwinder.h" #include "base/test/metrics/histogram_tester.h" #include "build/build_config.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace base { namespace { using ::testing::ElementsAre; class TestProfileBuilder : public ProfileBuilder { public: TestProfileBuilder(ModuleCache* module_cache) : module_cache_(module_cache) {} TestProfileBuilder(const TestProfileBuilder&) = delete; TestProfileBuilder& operator=(const TestProfileBuilder&) = delete; // ProfileBuilder ModuleCache* GetModuleCache() override { return module_cache_; } void RecordMetadata( const MetadataRecorder::MetadataProvider& metadata_provider) override {} void OnSampleCompleted(std::vector frames, TimeTicks sample_timestamp) override { last_timestamp_ = sample_timestamp; } void OnProfileCompleted(TimeDelta profile_duration, TimeDelta sampling_period) override {} TimeTicks last_timestamp() { return last_timestamp_; } private: raw_ptr module_cache_; TimeTicks last_timestamp_; }; // A stack copier for use in tests that provides the expected behavior when // operating on the supplied fake stack. class TestStackCopier : public StackCopier { public: TestStackCopier(const std::vector& fake_stack, TimeTicks timestamp = TimeTicks()) : fake_stack_(fake_stack), timestamp_(timestamp) {} bool CopyStack(StackBuffer* stack_buffer, uintptr_t* stack_top, TimeTicks* timestamp, RegisterContext* thread_context, Delegate* delegate) override { std::memcpy(stack_buffer->buffer(), &(*fake_stack_)[0], fake_stack_->size() * sizeof((*fake_stack_)[0])); *stack_top = reinterpret_cast(stack_buffer->buffer() + fake_stack_->size()); // Set the stack pointer to be consistent with the copied stack. *thread_context = {}; RegisterContextStackPointer(thread_context) = reinterpret_cast(stack_buffer->buffer()); *timestamp = timestamp_; return true; } private: // Must be a reference to retain the underlying allocation from the vector // passed to the constructor. const raw_ref> fake_stack_; const TimeTicks timestamp_; }; // A StackCopier that just invokes the expected functions on the delegate. class DelegateInvokingStackCopier : public StackCopier { public: bool CopyStack(StackBuffer* stack_buffer, uintptr_t* stack_top, TimeTicks* timestamp, RegisterContext* thread_context, Delegate* delegate) override { *stack_top = reinterpret_cast(stack_buffer->buffer()) + 10; // Make msan happy. delegate->OnStackCopy(); return true; } }; // Trivial unwinder implementation for testing. class TestUnwinder : public Unwinder { public: explicit TestUnwinder(std::vector* stack_copy) : stack_copy_(stack_copy) {} bool CanUnwindFrom(const Frame& current_frame) const override { return true; } UnwindResult TryUnwind(RegisterContext* thread_context, uintptr_t stack_top, std::vector* stack) override { auto* bottom = reinterpret_cast( RegisterContextStackPointer(thread_context)); *stack_copy_ = std::vector(bottom, reinterpret_cast(stack_top)); return UnwindResult::kCompleted; } private: raw_ptr> stack_copy_; }; // Records invocations of calls to OnStackCapture()/UpdateModules(). class CallRecordingUnwinder : public Unwinder { public: void OnStackCapture() override { on_stack_capture_was_invoked_ = true; } void UpdateModules() override { update_modules_was_invoked_ = true; } bool CanUnwindFrom(const Frame& current_frame) const override { return true; } UnwindResult TryUnwind(RegisterContext* thread_context, uintptr_t stack_top, std::vector* stack) override { return UnwindResult::kUnrecognizedFrame; } bool on_stack_capture_was_invoked() const { return on_stack_capture_was_invoked_; } bool update_modules_was_invoked() const { return update_modules_was_invoked_; } private: bool on_stack_capture_was_invoked_ = false; bool update_modules_was_invoked_ = false; }; // Utility function to form a vector from a single module. std::vector> ToModuleVector( std::unique_ptr module) { return std::vector>( std::make_move_iterator(&module), std::make_move_iterator(&module + 1)); } // Injects a fake module covering the initial instruction pointer value, to // avoid asking the OS to look it up. Windows doesn't return a consistent error // code when doing so, and we DCHECK_EQ the expected error code. void InjectModuleForContextInstructionPointer( const std::vector& stack, ModuleCache* module_cache) { module_cache->AddCustomNativeModule( std::make_unique(stack[0], sizeof(uintptr_t))); } // Returns a plausible instruction pointer value for use in tests that don't // care about the instruction pointer value in the context, and hence don't need // InjectModuleForContextInstructionPointer(). uintptr_t GetTestInstructionPointer() { return reinterpret_cast(&GetTestInstructionPointer); } // An unwinder fake that replays the provided outputs. class FakeTestUnwinder : public Unwinder { public: struct Result { Result(bool can_unwind) : can_unwind(can_unwind), result(UnwindResult::kUnrecognizedFrame) {} Result(UnwindResult result, std::vector instruction_pointers) : can_unwind(true), result(result), instruction_pointers(instruction_pointers) {} bool can_unwind; UnwindResult result; std::vector instruction_pointers; }; // Construct the unwinder with the outputs. The relevant unwinder functions // are expected to be invoked at least as many times as the number of values // specified in the arrays (except for CanUnwindFrom() which will always // return true if provided an empty array. explicit FakeTestUnwinder(std::vector results) : results_(std::move(results)) {} FakeTestUnwinder(const FakeTestUnwinder&) = delete; FakeTestUnwinder& operator=(const FakeTestUnwinder&) = delete; bool CanUnwindFrom(const Frame& current_frame) const override { bool can_unwind = results_[current_unwind_].can_unwind; // NB: If CanUnwindFrom() returns false then TryUnwind() will not be // invoked, so current_unwind_ is guarantee to be incremented only once for // each result. if (!can_unwind) ++current_unwind_; return can_unwind; } UnwindResult TryUnwind(RegisterContext* thread_context, uintptr_t stack_top, std::vector* stack) override { CHECK_LT(current_unwind_, results_.size()); const Result& current_result = results_[current_unwind_]; ++current_unwind_; CHECK(current_result.can_unwind); for (const auto instruction_pointer : current_result.instruction_pointers) stack->emplace_back( instruction_pointer, module_cache()->GetModuleForAddress(instruction_pointer)); return current_result.result; } private: mutable size_t current_unwind_ = 0; std::vector results_; }; StackSampler::UnwindersFactory MakeUnwindersFactory( std::unique_ptr unwinder) { return BindOnce( [](std::unique_ptr unwinder) { std::vector> unwinders; unwinders.push_back(std::move(unwinder)); return unwinders; }, std::move(unwinder)); } base::circular_deque> MakeUnwinderCircularDeque( std::unique_ptr native_unwinder, std::unique_ptr aux_unwinder) { base::circular_deque> unwinders; if (native_unwinder) unwinders.push_front(std::move(native_unwinder)); if (aux_unwinder) unwinders.push_front(std::move(aux_unwinder)); return unwinders; } } // namespace TEST(StackSamplerTest, CopyStack) { ModuleCache module_cache; const std::vector stack = {0, 1, 2, 3, 4}; InjectModuleForContextInstructionPointer(stack, &module_cache); std::vector stack_copy; std::unique_ptr stack_sampler = StackSampler::CreateForTesting( std::make_unique(stack), MakeUnwindersFactory(std::make_unique(&stack_copy)), &module_cache); stack_sampler->Initialize(); std::unique_ptr stack_buffer = std::make_unique(stack.size() * sizeof(uintptr_t)); TestProfileBuilder profile_builder(&module_cache); stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); EXPECT_EQ(stack, stack_copy); } #if BUILDFLAG(IS_CHROMEOS) TEST(StackSamplerTest, RecordStackFramesUMAMetric) { HistogramTester histogram_tester; ModuleCache module_cache; std::vector stack; constexpr size_t UIntPtrsPerKilobyte = 1024 / sizeof(uintptr_t); // kExpectedSizeKB needs to be a fairly large number of kilobytes. The buckets // in UmaHistogramMemoryKB are big enough that small values are in the same // bucket as zero and less than zero, and testing that we added a sample in // that bucket means that the test won't fail if, for example, the // |stack_top - stack_bottom| subtraction was reversed and got a negative // value. constexpr int kExpectedSizeKB = 2048; for (uintptr_t i = 0; i <= (kExpectedSizeKB * UIntPtrsPerKilobyte) + 1; i++) { stack.push_back(i); } InjectModuleForContextInstructionPointer(stack, &module_cache); std::vector stack_copy; std::unique_ptr stack_sampler = StackSampler::CreateForTesting( std::make_unique(stack), MakeUnwindersFactory(std::make_unique(&stack_copy)), &module_cache); stack_sampler->Initialize(); std::unique_ptr stack_buffer = std::make_unique(stack.size() * sizeof(uintptr_t)); TestProfileBuilder profile_builder(&module_cache); for (uint32_t i = 0; i < StackSampler::kUMAHistogramDownsampleAmount - 1; i++) { stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); // Should have no new samples in the // Memory.StackSamplingProfiler.StackSampleSize2 histogram. histogram_tester.ExpectUniqueSample( "Memory.StackSamplingProfiler.StackSampleSize2", kExpectedSizeKB, 0); } stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); histogram_tester.ExpectUniqueSample( "Memory.StackSamplingProfiler.StackSampleSize2", kExpectedSizeKB, 1); } #endif // #if BUILDFLAG(IS_CHROMEOS) TEST(StackSamplerTest, CopyStackTimestamp) { ModuleCache module_cache; const std::vector stack = {0}; InjectModuleForContextInstructionPointer(stack, &module_cache); std::vector stack_copy; TimeTicks timestamp = TimeTicks::UnixEpoch(); std::unique_ptr stack_sampler = StackSampler::CreateForTesting( std::make_unique(stack, timestamp), MakeUnwindersFactory(std::make_unique(&stack_copy)), &module_cache); stack_sampler->Initialize(); std::unique_ptr stack_buffer = std::make_unique(stack.size() * sizeof(uintptr_t)); TestProfileBuilder profile_builder(&module_cache); stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); EXPECT_EQ(timestamp, profile_builder.last_timestamp()); } TEST(StackSamplerTest, UnwinderInvokedWhileRecordingStackFrames) { std::unique_ptr stack_buffer = std::make_unique(10); auto owned_unwinder = std::make_unique(); CallRecordingUnwinder* unwinder = owned_unwinder.get(); ModuleCache module_cache; TestProfileBuilder profile_builder(&module_cache); std::unique_ptr stack_sampler = StackSampler::CreateForTesting( std::make_unique(), MakeUnwindersFactory(std::move(owned_unwinder)), &module_cache); stack_sampler->Initialize(); stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); EXPECT_TRUE(unwinder->on_stack_capture_was_invoked()); EXPECT_TRUE(unwinder->update_modules_was_invoked()); } TEST(StackSamplerTest, AuxUnwinderInvokedWhileRecordingStackFrames) { std::unique_ptr stack_buffer = std::make_unique(10); ModuleCache module_cache; TestProfileBuilder profile_builder(&module_cache); std::unique_ptr stack_sampler = StackSampler::CreateForTesting( std::make_unique(), MakeUnwindersFactory(std::make_unique()), &module_cache); stack_sampler->Initialize(); auto owned_aux_unwinder = std::make_unique(); CallRecordingUnwinder* aux_unwinder = owned_aux_unwinder.get(); stack_sampler->AddAuxUnwinder(std::move(owned_aux_unwinder)); stack_sampler->RecordStackFrames(stack_buffer.get(), &profile_builder, PlatformThread::CurrentId()); EXPECT_TRUE(aux_unwinder->on_stack_capture_was_invoked()); EXPECT_TRUE(aux_unwinder->update_modules_was_invoked()); } TEST(StackSamplerTest, WalkStack_Completed) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = GetTestInstructionPointer(); module_cache.AddCustomNativeModule(std::make_unique(1u, 1u)); auto native_unwinder = WrapUnique(new FakeTestUnwinder({{UnwindResult::kCompleted, {1u}}})); native_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(std::move(native_unwinder), nullptr)); ASSERT_EQ(2u, stack.size()); EXPECT_EQ(1u, stack[1].instruction_pointer); } TEST(StackSamplerTest, WalkStack_Aborted) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = GetTestInstructionPointer(); module_cache.AddCustomNativeModule(std::make_unique(1u, 1u)); auto native_unwinder = WrapUnique(new FakeTestUnwinder({{UnwindResult::kAborted, {1u}}})); native_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(std::move(native_unwinder), nullptr)); ASSERT_EQ(2u, stack.size()); EXPECT_EQ(1u, stack[1].instruction_pointer); } TEST(StackSamplerTest, WalkStack_NotUnwound) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = GetTestInstructionPointer(); auto native_unwinder = WrapUnique( new FakeTestUnwinder({{UnwindResult::kUnrecognizedFrame, {}}})); native_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(std::move(native_unwinder), nullptr)); ASSERT_EQ(1u, stack.size()); } TEST(StackSamplerTest, WalkStack_AuxUnwind) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = GetTestInstructionPointer(); // Treat the context instruction pointer as being in the aux unwinder's // non-native module. module_cache.UpdateNonNativeModules( {}, ToModuleVector(std::make_unique( GetTestInstructionPointer(), 1u, false))); auto aux_unwinder = WrapUnique(new FakeTestUnwinder({{UnwindResult::kAborted, {1u}}})); aux_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(nullptr, std::move(aux_unwinder))); ASSERT_EQ(2u, stack.size()); EXPECT_EQ(GetTestInstructionPointer(), stack[0].instruction_pointer); EXPECT_EQ(1u, stack[1].instruction_pointer); } TEST(StackSamplerTest, WalkStack_AuxThenNative) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = 0u; // Treat the context instruction pointer as being in the aux unwinder's // non-native module. module_cache.UpdateNonNativeModules( {}, ToModuleVector(std::make_unique(0u, 1u, false))); // Inject a fake native module for the second frame. module_cache.AddCustomNativeModule(std::make_unique(1u, 1u)); auto aux_unwinder = WrapUnique( new FakeTestUnwinder({{UnwindResult::kUnrecognizedFrame, {1u}}, false})); aux_unwinder->Initialize(&module_cache); auto native_unwinder = WrapUnique(new FakeTestUnwinder({{UnwindResult::kCompleted, {2u}}})); native_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(std::move(native_unwinder), std::move(aux_unwinder))); ASSERT_EQ(3u, stack.size()); EXPECT_EQ(0u, stack[0].instruction_pointer); EXPECT_EQ(1u, stack[1].instruction_pointer); EXPECT_EQ(2u, stack[2].instruction_pointer); } TEST(StackSamplerTest, WalkStack_NativeThenAux) { ModuleCache module_cache; RegisterContext thread_context; RegisterContextInstructionPointer(&thread_context) = 0u; // Inject fake native modules for the instruction pointer from the context and // the third frame. module_cache.AddCustomNativeModule(std::make_unique(0u, 1u)); module_cache.AddCustomNativeModule(std::make_unique(2u, 1u)); // Treat the second frame's pointer as being in the aux unwinder's non-native // module. module_cache.UpdateNonNativeModules( {}, ToModuleVector(std::make_unique(1u, 1u, false))); auto aux_unwinder = WrapUnique(new FakeTestUnwinder( {{false}, {UnwindResult::kUnrecognizedFrame, {2u}}, {false}})); aux_unwinder->Initialize(&module_cache); auto native_unwinder = WrapUnique(new FakeTestUnwinder({{UnwindResult::kUnrecognizedFrame, {1u}}, {UnwindResult::kCompleted, {3u}}})); native_unwinder->Initialize(&module_cache); std::vector stack = StackSampler::WalkStackForTesting( &module_cache, &thread_context, 0u, MakeUnwinderCircularDeque(std::move(native_unwinder), std::move(aux_unwinder))); ASSERT_EQ(4u, stack.size()); EXPECT_EQ(0u, stack[0].instruction_pointer); EXPECT_EQ(1u, stack[1].instruction_pointer); EXPECT_EQ(2u, stack[2].instruction_pointer); EXPECT_EQ(3u, stack[3].instruction_pointer); } } // namespace base