| // Copyright 2017 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 "components/browser_watcher/stability_report_extractor.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/debug/activity_tracker.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/memory_mapped_file.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/persistent_memory_allocator.h" |
| #include "base/stl_util.h" |
| #include "base/time/time.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/crashpad/crashpad/client/crash_report_database.h" |
| |
| namespace browser_watcher { |
| |
| using base::debug::ActivityData; |
| using base::debug::ActivityTrackerMemoryAllocator; |
| using base::debug::ActivityUserData; |
| using base::debug::GlobalActivityTracker; |
| using base::debug::ThreadActivityTracker; |
| using base::File; |
| using base::FilePath; |
| using base::FilePersistentMemoryAllocator; |
| using base::MemoryMappedFile; |
| using base::PersistentMemoryAllocator; |
| using base::WrapUnique; |
| |
| namespace { |
| |
| // The tracker creates some data entries internally. |
| const size_t kInternalProcessDatums = 1; |
| |
| // Parameters for the activity tracking. |
| const size_t kFileSize = 64 << 10; // 64 KiB |
| const int kStackDepth = 6; |
| const uint64_t kAllocatorId = 0; |
| const char kAllocatorName[] = "PostmortemReportCollectorCollectionTest"; |
| const uint64_t kTaskSequenceNum = 42; |
| const uintptr_t kTaskOrigin = 1000U; |
| const uintptr_t kLockAddress = 1001U; |
| const uintptr_t kEventAddress = 1002U; |
| const int kThreadId = 43; |
| const int kProcessId = 44; |
| const int kAnotherThreadId = 45; |
| const uint32_t kGenericId = 46U; |
| const int32_t kGenericData = 47; |
| |
| } // namespace |
| |
| // Sets up a file backed thread tracker for direct access. A |
| // GlobalActivityTracker is not created, meaning there is no risk of |
| // the instrumentation interfering with the file's content. |
| class StabilityReportExtractorThreadTrackerTest : public testing::Test { |
| public: |
| // Create a proper debug file. |
| void SetUp() override { |
| testing::Test::SetUp(); |
| |
| // Create a file backed allocator. |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| debug_file_path_ = temp_dir_.GetPath().AppendASCII("debug_file.pma"); |
| allocator_ = CreateAllocator(); |
| ASSERT_NE(nullptr, allocator_); |
| |
| size_t tracker_mem_size = |
| ThreadActivityTracker::SizeForStackDepth(kStackDepth); |
| ASSERT_GT(kFileSize, tracker_mem_size); |
| |
| // Create a tracker. |
| tracker_ = CreateTracker(allocator_.get(), tracker_mem_size); |
| ASSERT_NE(nullptr, tracker_); |
| ASSERT_TRUE(tracker_->IsValid()); |
| } |
| |
| std::unique_ptr<PersistentMemoryAllocator> CreateAllocator() { |
| // Create the memory mapped file. |
| std::unique_ptr<MemoryMappedFile> mmfile(new MemoryMappedFile()); |
| bool success = mmfile->Initialize( |
| File(debug_file_path_, File::FLAG_CREATE | File::FLAG_READ | |
| File::FLAG_WRITE | File::FLAG_SHARE_DELETE), |
| {0, static_cast<int64_t>(kFileSize)}, |
| MemoryMappedFile::READ_WRITE_EXTEND); |
| if (!success || !mmfile->IsValid()) |
| return nullptr; |
| |
| // Create a persistent memory allocator. |
| if (!FilePersistentMemoryAllocator::IsFileAcceptable(*mmfile, true)) |
| return nullptr; |
| return WrapUnique(new FilePersistentMemoryAllocator( |
| std::move(mmfile), kFileSize, kAllocatorId, kAllocatorName, false)); |
| } |
| |
| std::unique_ptr<ThreadActivityTracker> CreateTracker( |
| PersistentMemoryAllocator* allocator, |
| size_t tracker_mem_size) { |
| // Allocate a block of memory for the tracker to use. |
| PersistentMemoryAllocator::Reference mem_reference = allocator->Allocate( |
| tracker_mem_size, GlobalActivityTracker::kTypeIdActivityTracker); |
| if (mem_reference == 0U) |
| return nullptr; |
| |
| // Get the memory's base address. |
| void* mem_base = allocator->GetAsArray<char>( |
| mem_reference, GlobalActivityTracker::kTypeIdActivityTracker, |
| PersistentMemoryAllocator::kSizeAny); |
| if (mem_base == nullptr) |
| return nullptr; |
| |
| // Make the allocation iterable so it can be found by other processes. |
| allocator->MakeIterable(mem_reference); |
| |
| return WrapUnique(new ThreadActivityTracker(mem_base, tracker_mem_size)); |
| } |
| |
| void PerformBasicReportValidation(const StabilityReport& report) { |
| // One report with one thread that has the expected name and id. |
| ASSERT_EQ(1, report.process_states_size()); |
| const ProcessState& process_state = report.process_states(0); |
| EXPECT_EQ(base::GetCurrentProcId(), process_state.process_id()); |
| ASSERT_EQ(1, process_state.threads_size()); |
| const ThreadState& thread_state = process_state.threads(0); |
| EXPECT_EQ(base::PlatformThread::GetName(), thread_state.thread_name()); |
| #if defined(OS_WIN) |
| EXPECT_EQ(base::PlatformThread::CurrentId(), thread_state.thread_id()); |
| #elif defined(OS_POSIX) |
| EXPECT_EQ(base::PlatformThread::CurrentHandle().platform_handle(), |
| thread_state.thread_id()); |
| #endif |
| } |
| |
| const FilePath& debug_file_path() const { return debug_file_path_; } |
| |
| protected: |
| base::ScopedTempDir temp_dir_; |
| FilePath debug_file_path_; |
| |
| std::unique_ptr<PersistentMemoryAllocator> allocator_; |
| std::unique_ptr<ThreadActivityTracker> tracker_; |
| }; |
| |
| TEST_F(StabilityReportExtractorThreadTrackerTest, CollectSuccess) { |
| // Create some activity data. |
| tracker_->PushActivity(reinterpret_cast<void*>(kTaskOrigin), |
| base::debug::Activity::ACT_TASK_RUN, |
| ActivityData::ForTask(kTaskSequenceNum)); |
| tracker_->PushActivity( |
| nullptr, base::debug::Activity::ACT_LOCK_ACQUIRE, |
| ActivityData::ForLock(reinterpret_cast<void*>(kLockAddress))); |
| ThreadActivityTracker::ActivityId activity_id = tracker_->PushActivity( |
| nullptr, base::debug::Activity::ACT_EVENT_WAIT, |
| ActivityData::ForEvent(reinterpret_cast<void*>(kEventAddress))); |
| tracker_->PushActivity(nullptr, base::debug::Activity::ACT_THREAD_JOIN, |
| ActivityData::ForThread(kThreadId)); |
| tracker_->PushActivity(nullptr, base::debug::Activity::ACT_PROCESS_WAIT, |
| ActivityData::ForProcess(kProcessId)); |
| tracker_->PushActivity(nullptr, base::debug::Activity::ACT_GENERIC, |
| ActivityData::ForGeneric(kGenericId, kGenericData)); |
| // Note: this exceeds the activity stack's capacity. |
| tracker_->PushActivity(nullptr, base::debug::Activity::ACT_THREAD_JOIN, |
| ActivityData::ForThread(kAnotherThreadId)); |
| |
| // Add some user data. |
| ActivityTrackerMemoryAllocator user_data_allocator( |
| allocator_.get(), GlobalActivityTracker::kTypeIdUserDataRecord, |
| GlobalActivityTracker::kTypeIdUserDataRecordFree, 1024U, 10U, false); |
| std::unique_ptr<ActivityUserData> user_data = |
| tracker_->GetUserData(activity_id, &user_data_allocator); |
| user_data->SetInt("some_int", 42); |
| |
| // Validate collection returns the expected report. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| |
| // Validate the report. |
| ASSERT_NO_FATAL_FAILURE(PerformBasicReportValidation(report)); |
| const ThreadState& thread_state = report.process_states(0).threads(0); |
| |
| EXPECT_EQ(7, thread_state.activity_count()); |
| ASSERT_EQ(6, thread_state.activities_size()); |
| { |
| const Activity& activity = thread_state.activities(0); |
| EXPECT_EQ(Activity::ACT_TASK_RUN, activity.type()); |
| EXPECT_EQ(kTaskOrigin, activity.origin_address()); |
| EXPECT_EQ(kTaskSequenceNum, activity.task_sequence_id()); |
| EXPECT_EQ(0U, activity.user_data().size()); |
| } |
| { |
| const Activity& activity = thread_state.activities(1); |
| EXPECT_EQ(Activity::ACT_LOCK_ACQUIRE, activity.type()); |
| EXPECT_EQ(kLockAddress, activity.lock_address()); |
| EXPECT_EQ(0U, activity.user_data().size()); |
| } |
| { |
| const Activity& activity = thread_state.activities(2); |
| EXPECT_EQ(Activity::ACT_EVENT_WAIT, activity.type()); |
| EXPECT_EQ(kEventAddress, activity.event_address()); |
| ASSERT_EQ(1U, activity.user_data().size()); |
| ASSERT_TRUE(base::ContainsKey(activity.user_data(), "some_int")); |
| EXPECT_EQ(TypedValue::kSignedValue, |
| activity.user_data().at("some_int").value_case()); |
| EXPECT_EQ(42, activity.user_data().at("some_int").signed_value()); |
| } |
| { |
| const Activity& activity = thread_state.activities(3); |
| EXPECT_EQ(Activity::ACT_THREAD_JOIN, activity.type()); |
| EXPECT_EQ(kThreadId, activity.thread_id()); |
| EXPECT_EQ(0U, activity.user_data().size()); |
| } |
| { |
| const Activity& activity = thread_state.activities(4); |
| EXPECT_EQ(Activity::ACT_PROCESS_WAIT, activity.type()); |
| EXPECT_EQ(kProcessId, activity.process_id()); |
| EXPECT_EQ(0U, activity.user_data().size()); |
| } |
| { |
| const Activity& activity = thread_state.activities(5); |
| EXPECT_EQ(Activity::ACT_GENERIC, activity.type()); |
| EXPECT_EQ(kGenericId, activity.generic_id()); |
| EXPECT_EQ(kGenericData, activity.generic_data()); |
| EXPECT_EQ(0U, activity.user_data().size()); |
| } |
| } |
| |
| TEST_F(StabilityReportExtractorThreadTrackerTest, CollectException) { |
| const void* expected_pc = reinterpret_cast<void*>(0xCAFE); |
| const void* expected_address = nullptr; |
| const uint32_t expected_code = 42U; |
| |
| // Record an exception. |
| const int64_t timestamp = base::Time::Now().ToInternalValue(); |
| tracker_->RecordExceptionActivity(expected_pc, expected_address, |
| base::debug::Activity::ACT_EXCEPTION, |
| ActivityData::ForException(expected_code)); |
| |
| // Collect report and validate. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| |
| // Validate the presence of the exception. |
| ASSERT_NO_FATAL_FAILURE(PerformBasicReportValidation(report)); |
| const ThreadState& thread_state = report.process_states(0).threads(0); |
| ASSERT_TRUE(thread_state.has_exception()); |
| const Exception& exception = thread_state.exception(); |
| EXPECT_EQ(expected_code, exception.code()); |
| EXPECT_EQ(expected_pc, reinterpret_cast<void*>(exception.program_counter())); |
| EXPECT_EQ(expected_address, |
| reinterpret_cast<void*>(exception.exception_address())); |
| const int64_t tolerance_us = 1000ULL; |
| EXPECT_LE(std::abs(timestamp - exception.time()), tolerance_us); |
| } |
| |
| TEST_F(StabilityReportExtractorThreadTrackerTest, CollectNoException) { |
| // Record something. |
| tracker_->PushActivity(reinterpret_cast<void*>(kTaskOrigin), |
| base::debug::Activity::ACT_TASK_RUN, |
| ActivityData::ForTask(kTaskSequenceNum)); |
| |
| // Collect report and validate there is no exception. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| ASSERT_NO_FATAL_FAILURE(PerformBasicReportValidation(report)); |
| const ThreadState& thread_state = report.process_states(0).threads(0); |
| ASSERT_FALSE(thread_state.has_exception()); |
| } |
| |
| // Tests stability report extraction. |
| class StabilityReportExtractorTest : public testing::Test { |
| public: |
| const int kMemorySize = 1 << 20; // 1MiB |
| |
| StabilityReportExtractorTest() {} |
| ~StabilityReportExtractorTest() override { |
| GlobalActivityTracker* global_tracker = GlobalActivityTracker::Get(); |
| if (global_tracker) { |
| global_tracker->ReleaseTrackerForCurrentThreadForTesting(); |
| delete global_tracker; |
| } |
| } |
| |
| void SetUp() override { |
| testing::Test::SetUp(); |
| |
| // Set up a debug file path. |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| debug_file_path_ = temp_dir_.GetPath().AppendASCII("debug.pma"); |
| } |
| |
| const FilePath& debug_file_path() { return debug_file_path_; } |
| |
| protected: |
| base::ScopedTempDir temp_dir_; |
| FilePath debug_file_path_; |
| }; |
| |
| TEST_F(StabilityReportExtractorTest, LogCollection) { |
| // Record some log messages. |
| GlobalActivityTracker::CreateWithFile(debug_file_path(), kMemorySize, 0ULL, |
| "", 3); |
| GlobalActivityTracker::Get()->RecordLogMessage("hello world"); |
| GlobalActivityTracker::Get()->RecordLogMessage("foo bar"); |
| |
| // Collect the stability report. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| |
| // Validate the report's log content. |
| ASSERT_EQ(2, report.log_messages_size()); |
| ASSERT_EQ("hello world", report.log_messages(0)); |
| ASSERT_EQ("foo bar", report.log_messages(1)); |
| } |
| |
| TEST_F(StabilityReportExtractorTest, ProcessUserDataCollection) { |
| const char string1[] = "foo"; |
| const char string2[] = "bar"; |
| |
| // Record some process user data. |
| GlobalActivityTracker::CreateWithFile(debug_file_path(), kMemorySize, 0ULL, |
| "", 3); |
| ActivityUserData& process_data = GlobalActivityTracker::Get()->process_data(); |
| ActivityUserData::Snapshot snapshot; |
| ASSERT_TRUE(process_data.CreateSnapshot(&snapshot)); |
| ASSERT_EQ(kInternalProcessDatums, snapshot.size()); |
| process_data.Set("raw", "foo", 3); |
| process_data.SetString("string", "bar"); |
| process_data.SetChar("char", '9'); |
| process_data.SetInt("int", -9999); |
| process_data.SetUint("uint", 9999); |
| process_data.SetBool("bool", true); |
| process_data.SetReference("ref", string1, strlen(string1)); |
| process_data.SetStringReference("sref", string2); |
| |
| // Collect the stability report. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| |
| // We expect a single process. |
| ASSERT_EQ(1, report.process_states_size()); |
| |
| // Validate the report contains the process' data. |
| const auto& collected_data = report.process_states(0).data(); |
| ASSERT_EQ(kInternalProcessDatums + 8U, collected_data.size()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "raw")); |
| EXPECT_EQ(TypedValue::kBytesValue, collected_data.at("raw").value_case()); |
| EXPECT_EQ("foo", collected_data.at("raw").bytes_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "string")); |
| EXPECT_EQ(TypedValue::kStringValue, collected_data.at("string").value_case()); |
| EXPECT_EQ("bar", collected_data.at("string").string_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "char")); |
| EXPECT_EQ(TypedValue::kCharValue, collected_data.at("char").value_case()); |
| EXPECT_EQ("9", collected_data.at("char").char_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "int")); |
| EXPECT_EQ(TypedValue::kSignedValue, collected_data.at("int").value_case()); |
| EXPECT_EQ(-9999, collected_data.at("int").signed_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "uint")); |
| EXPECT_EQ(TypedValue::kUnsignedValue, collected_data.at("uint").value_case()); |
| EXPECT_EQ(9999U, collected_data.at("uint").unsigned_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "bool")); |
| EXPECT_EQ(TypedValue::kBoolValue, collected_data.at("bool").value_case()); |
| EXPECT_TRUE(collected_data.at("bool").bool_value()); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "ref")); |
| EXPECT_EQ(TypedValue::kBytesReference, collected_data.at("ref").value_case()); |
| const TypedValue::Reference& ref = collected_data.at("ref").bytes_reference(); |
| EXPECT_EQ(reinterpret_cast<uintptr_t>(string1), ref.address()); |
| EXPECT_EQ(strlen(string1), static_cast<uint64_t>(ref.size())); |
| |
| ASSERT_TRUE(base::ContainsKey(collected_data, "sref")); |
| EXPECT_EQ(TypedValue::kStringReference, |
| collected_data.at("sref").value_case()); |
| const TypedValue::Reference& sref = |
| collected_data.at("sref").string_reference(); |
| EXPECT_EQ(reinterpret_cast<uintptr_t>(string2), sref.address()); |
| EXPECT_EQ(strlen(string2), static_cast<uint64_t>(sref.size())); |
| } |
| |
| TEST_F(StabilityReportExtractorTest, FieldTrialCollection) { |
| // Record some data. |
| GlobalActivityTracker::CreateWithFile(debug_file_path(), kMemorySize, 0ULL, |
| "", 3); |
| ActivityUserData& process_data = GlobalActivityTracker::Get()->process_data(); |
| process_data.SetString("string", "bar"); |
| process_data.SetString("FieldTrial.string", "bar"); |
| process_data.SetString("FieldTrial.foo", "bar"); |
| |
| // Collect the stability report. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| ASSERT_EQ(1, report.process_states_size()); |
| |
| // Validate the report's experiment and global data. |
| ASSERT_EQ(2, report.field_trials_size()); |
| EXPECT_NE(0U, report.field_trials(0).name_id()); |
| EXPECT_NE(0U, report.field_trials(0).group_id()); |
| EXPECT_NE(0U, report.field_trials(1).name_id()); |
| EXPECT_EQ(report.field_trials(0).group_id(), |
| report.field_trials(1).group_id()); |
| |
| // Expect 1 key/value pair. |
| const auto& collected_data = report.process_states(0).data(); |
| EXPECT_EQ(kInternalProcessDatums + 1U, collected_data.size()); |
| EXPECT_TRUE(base::ContainsKey(collected_data, "string")); |
| } |
| |
| TEST_F(StabilityReportExtractorTest, ModuleCollection) { |
| // Record some module information. |
| GlobalActivityTracker::CreateWithFile(debug_file_path(), kMemorySize, 0ULL, |
| "", 3); |
| |
| base::debug::GlobalActivityTracker::ModuleInfo module_info = {}; |
| module_info.is_loaded = true; |
| module_info.address = 0x123456; |
| module_info.load_time = 1111LL; |
| module_info.size = 0x2d000; |
| module_info.timestamp = 0xCAFECAFE; |
| module_info.age = 1; |
| crashpad::UUID debug_uuid; |
| debug_uuid.InitializeFromString("11223344-5566-7788-abcd-0123456789ab"); |
| memcpy(module_info.identifier, &debug_uuid, sizeof(module_info.identifier)); |
| module_info.file = "foo"; |
| module_info.debug_file = "bar"; |
| |
| GlobalActivityTracker::Get()->RecordModuleInfo(module_info); |
| |
| // Collect the stability report. |
| StabilityReport report; |
| ASSERT_EQ(SUCCESS, Extract(debug_file_path(), &report)); |
| |
| // Validate the report's modules content. |
| ASSERT_EQ(1, report.process_states_size()); |
| const ProcessState& process_state = report.process_states(0); |
| ASSERT_EQ(1, process_state.modules_size()); |
| |
| const CodeModule collected_module = process_state.modules(0); |
| EXPECT_EQ(module_info.address, |
| static_cast<uintptr_t>(collected_module.base_address())); |
| EXPECT_EQ(module_info.size, static_cast<size_t>(collected_module.size())); |
| EXPECT_EQ(module_info.file, collected_module.code_file()); |
| EXPECT_EQ("CAFECAFE2d000", collected_module.code_identifier()); |
| EXPECT_EQ(module_info.debug_file, collected_module.debug_file()); |
| EXPECT_EQ("1122334455667788ABCD0123456789AB1", |
| collected_module.debug_identifier()); |
| EXPECT_EQ("", collected_module.version()); |
| EXPECT_EQ(0LL, collected_module.shrink_down_delta()); |
| EXPECT_EQ(!module_info.is_loaded, collected_module.is_unloaded()); |
| } |
| |
| } // namespace browser_watcher |