| // Copyright 2016 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 "base/debug/activity_tracker.h" |
| |
| #include <algorithm> |
| #include <limits> |
| #include <utility> |
| |
| #include "base/atomic_sequence_num.h" |
| #include "base/bits.h" |
| #include "base/containers/contains.h" |
| #include "base/debug/stack_trace.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/memory_mapped_file.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/pending_task.h" |
| #include "base/pickle.h" |
| #include "base/process/process.h" |
| #include "base/process/process_handle.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/platform_thread.h" |
| #include "build/build_config.h" |
| |
| namespace base { |
| namespace debug { |
| |
| namespace { |
| |
| // The minimum depth a stack should support. |
| const int kMinStackDepth = 2; |
| |
| // The amount of memory set aside for holding arbitrary user data (key/value |
| // pairs) globally or associated with ActivityData entries. |
| const size_t kUserDataSize = 1 << 10; // 1 KiB |
| const size_t kProcessDataSize = 4 << 10; // 4 KiB |
| const size_t kMaxUserDataNameLength = |
| static_cast<size_t>(std::numeric_limits<uint8_t>::max()); |
| |
| // A constant used to indicate that module information is changing. |
| const uint32_t kModuleInformationChanging = 0x80000000; |
| |
| // The key used to record process information. |
| const char kProcessPhaseDataKey[] = "process-phase"; |
| |
| // An atomically incrementing number, used to check for recreations of objects |
| // in the same memory space. |
| AtomicSequenceNumber g_next_id; |
| |
| // Gets the next non-zero identifier. It is only unique within a process. |
| uint32_t GetNextDataId() { |
| uint32_t id; |
| while ((id = g_next_id.GetNext()) == 0) { |
| } |
| return id; |
| } |
| |
| // Gets the current process-id, either from the GlobalActivityTracker if it |
| // exists (where the PID can be defined for testing) or from the system if |
| // there isn't such. |
| int64_t GetProcessId() { |
| GlobalActivityTracker* global = GlobalActivityTracker::Get(); |
| if (global) |
| return global->process_id(); |
| return GetCurrentProcId(); |
| } |
| |
| // Finds and reuses a specific allocation or creates a new one. |
| PersistentMemoryAllocator::Reference AllocateFrom( |
| PersistentMemoryAllocator* allocator, |
| uint32_t from_type, |
| size_t size, |
| uint32_t to_type) { |
| PersistentMemoryAllocator::Iterator iter(allocator); |
| PersistentMemoryAllocator::Reference ref; |
| while ((ref = iter.GetNextOfType(from_type)) != 0) { |
| DCHECK_LE(size, allocator->GetAllocSize(ref)); |
| // This can fail if a another thread has just taken it. It is assumed that |
| // the memory is cleared during the "free" operation. |
| if (allocator->ChangeType(ref, to_type, from_type, /*clear=*/false)) |
| return ref; |
| } |
| |
| return allocator->Allocate(size, to_type); |
| } |
| |
| // Converts "tick" timing into wall time. |
| Time WallTimeFromTickTime(int64_t ticks_start, int64_t ticks, Time time_start) { |
| return time_start + TimeDelta::FromInternalValue(ticks - ticks_start); |
| } |
| |
| } // namespace |
| |
| union ThreadRef { |
| int64_t as_id; |
| #if defined(OS_WIN) |
| // On Windows, the handle itself is often a pseudo-handle with a common |
| // value meaning "this thread" and so the thread-id is used. The former |
| // can be converted to a thread-id with a system call. |
| PlatformThreadId as_tid; |
| #elif defined(OS_POSIX) || defined(OS_FUCHSIA) |
| // On Posix and Fuchsia, the handle is always a unique identifier so no |
| // conversion needs to be done. However, its value is officially opaque so |
| // there is no one correct way to convert it to a numerical identifier. |
| PlatformThreadHandle::Handle as_handle; |
| #endif |
| }; |
| |
| OwningProcess::OwningProcess() = default; |
| OwningProcess::~OwningProcess() = default; |
| |
| void OwningProcess::Release_Initialize(int64_t pid) { |
| uint32_t old_id = data_id.load(std::memory_order_acquire); |
| DCHECK_EQ(0U, old_id); |
| process_id = pid != 0 ? pid : GetProcessId(); |
| create_stamp = Time::Now().ToInternalValue(); |
| data_id.store(GetNextDataId(), std::memory_order_release); |
| } |
| |
| void OwningProcess::SetOwningProcessIdForTesting(int64_t pid, int64_t stamp) { |
| DCHECK_NE(0U, data_id); |
| process_id = pid; |
| create_stamp = stamp; |
| } |
| |
| // static |
| bool OwningProcess::GetOwningProcessId(const void* memory, |
| int64_t* out_id, |
| int64_t* out_stamp) { |
| const OwningProcess* info = reinterpret_cast<const OwningProcess*>(memory); |
| uint32_t id = info->data_id.load(std::memory_order_acquire); |
| if (id == 0) |
| return false; |
| |
| *out_id = info->process_id; |
| *out_stamp = info->create_stamp; |
| return id == info->data_id.load(std::memory_order_seq_cst); |
| } |
| |
| // It doesn't matter what is contained in this (though it will be all zeros) |
| // as only the address of it is important. |
| const ActivityData kNullActivityData = {}; |
| |
| ActivityData ActivityData::ForThread(const PlatformThreadHandle& handle) { |
| ThreadRef thread_ref; |
| thread_ref.as_id = 0; // Zero the union in case other is smaller. |
| #if defined(OS_WIN) |
| thread_ref.as_tid = ::GetThreadId(handle.platform_handle()); |
| #elif defined(OS_POSIX) |
| thread_ref.as_handle = handle.platform_handle(); |
| #endif |
| return ForThread(thread_ref.as_id); |
| } |
| |
| ActivityTrackerMemoryAllocator::ActivityTrackerMemoryAllocator( |
| PersistentMemoryAllocator* allocator, |
| uint32_t object_type, |
| uint32_t object_free_type, |
| size_t object_size, |
| size_t cache_size, |
| bool make_iterable) |
| : allocator_(allocator), |
| object_type_(object_type), |
| object_free_type_(object_free_type), |
| object_size_(object_size), |
| cache_size_(cache_size), |
| make_iterable_(make_iterable), |
| iterator_(allocator), |
| cache_values_(new Reference[cache_size]), |
| cache_used_(0) { |
| DCHECK(allocator); |
| } |
| |
| ActivityTrackerMemoryAllocator::~ActivityTrackerMemoryAllocator() = default; |
| |
| ActivityTrackerMemoryAllocator::Reference |
| ActivityTrackerMemoryAllocator::GetObjectReference() { |
| // First see if there is a cached value that can be returned. This is much |
| // faster than searching the memory system for free blocks. |
| while (cache_used_ > 0) { |
| Reference cached = cache_values_[--cache_used_]; |
| // Change the type of the cached object to the proper type and return it. |
| // If the type-change fails that means another thread has taken this from |
| // under us (via the search below) so ignore it and keep trying. Don't |
| // clear the memory because that was done when the type was made "free". |
| if (allocator_->ChangeType(cached, object_type_, object_free_type_, false)) |
| return cached; |
| } |
| |
| // Fetch the next "free" object from persistent memory. Rather than restart |
| // the iterator at the head each time and likely waste time going again |
| // through objects that aren't relevant, the iterator continues from where |
| // it last left off and is only reset when the end is reached. If the |
| // returned reference matches |last|, then it has wrapped without finding |
| // anything. |
| const Reference last = iterator_.GetLast(); |
| while (true) { |
| uint32_t type; |
| Reference found = iterator_.GetNext(&type); |
| if (found && type == object_free_type_) { |
| // Found a free object. Change it to the proper type and return it. If |
| // the type-change fails that means another thread has taken this from |
| // under us so ignore it and keep trying. |
| if (allocator_->ChangeType(found, object_type_, object_free_type_, false)) |
| return found; |
| } |
| if (found == last) { |
| // Wrapped. No desired object was found. |
| break; |
| } |
| if (!found) { |
| // Reached end; start over at the beginning. |
| iterator_.Reset(); |
| } |
| } |
| |
| // No free block was found so instead allocate a new one. |
| Reference allocated = allocator_->Allocate(object_size_, object_type_); |
| if (allocated && make_iterable_) |
| allocator_->MakeIterable(allocated); |
| return allocated; |
| } |
| |
| void ActivityTrackerMemoryAllocator::ReleaseObjectReference(Reference ref) { |
| // Mark object as free. |
| bool success = allocator_->ChangeType(ref, object_free_type_, object_type_, |
| /*clear=*/true); |
| DCHECK(success); |
| |
| // Add this reference to our "free" cache if there is space. If not, the type |
| // has still been changed to indicate that it is free so this (or another) |
| // thread can find it, albeit more slowly, using the iteration method above. |
| if (cache_used_ < cache_size_) |
| cache_values_[cache_used_++] = ref; |
| } |
| |
| // static |
| void Activity::FillFrom(Activity* activity, |
| const void* program_counter, |
| const void* origin, |
| Type type, |
| const ActivityData& data) { |
| activity->time_internal = base::TimeTicks::Now().ToInternalValue(); |
| activity->calling_address = reinterpret_cast<uintptr_t>(program_counter); |
| activity->origin_address = reinterpret_cast<uintptr_t>(origin); |
| activity->activity_type = type; |
| activity->data = data; |
| |
| #if (!defined(OS_NACL) && DCHECK_IS_ON()) || defined(ADDRESS_SANITIZER) |
| // Create a stacktrace from the current location and get the addresses for |
| // improved debuggability. |
| StackTrace stack_trace; |
| size_t stack_depth; |
| const void* const* stack_addrs = stack_trace.Addresses(&stack_depth); |
| // Copy the stack addresses, ignoring the first one (here). |
| size_t i; |
| for (i = 1; i < stack_depth && i < kActivityCallStackSize; ++i) { |
| activity->call_stack[i - 1] = reinterpret_cast<uintptr_t>(stack_addrs[i]); |
| } |
| activity->call_stack[i - 1] = 0; |
| #else |
| activity->call_stack[0] = 0; |
| #endif |
| } |
| |
| ActivityUserData::TypedValue::TypedValue() = default; |
| ActivityUserData::TypedValue::TypedValue(const TypedValue& other) = default; |
| ActivityUserData::TypedValue::~TypedValue() = default; |
| |
| StringPiece ActivityUserData::TypedValue::Get() const { |
| DCHECK_EQ(RAW_VALUE, type_); |
| return long_value_; |
| } |
| |
| StringPiece ActivityUserData::TypedValue::GetString() const { |
| DCHECK_EQ(STRING_VALUE, type_); |
| return long_value_; |
| } |
| |
| bool ActivityUserData::TypedValue::GetBool() const { |
| DCHECK_EQ(BOOL_VALUE, type_); |
| return short_value_ != 0; |
| } |
| |
| char ActivityUserData::TypedValue::GetChar() const { |
| DCHECK_EQ(CHAR_VALUE, type_); |
| return static_cast<char>(short_value_); |
| } |
| |
| int64_t ActivityUserData::TypedValue::GetInt() const { |
| DCHECK_EQ(SIGNED_VALUE, type_); |
| return static_cast<int64_t>(short_value_); |
| } |
| |
| uint64_t ActivityUserData::TypedValue::GetUint() const { |
| DCHECK_EQ(UNSIGNED_VALUE, type_); |
| return static_cast<uint64_t>(short_value_); |
| } |
| |
| StringPiece ActivityUserData::TypedValue::GetReference() const { |
| DCHECK_EQ(RAW_VALUE_REFERENCE, type_); |
| return ref_value_; |
| } |
| |
| StringPiece ActivityUserData::TypedValue::GetStringReference() const { |
| DCHECK_EQ(STRING_VALUE_REFERENCE, type_); |
| return ref_value_; |
| } |
| |
| // These are required because std::atomic is (currently) not a POD type and |
| // thus clang requires explicit out-of-line constructors and destructors even |
| // when they do nothing. |
| ActivityUserData::ValueInfo::ValueInfo() = default; |
| ActivityUserData::ValueInfo::ValueInfo(ValueInfo&&) = default; |
| ActivityUserData::ValueInfo::~ValueInfo() = default; |
| ActivityUserData::MemoryHeader::MemoryHeader() = default; |
| ActivityUserData::MemoryHeader::~MemoryHeader() = default; |
| ActivityUserData::FieldHeader::FieldHeader() = default; |
| ActivityUserData::FieldHeader::~FieldHeader() = default; |
| |
| ActivityUserData::ActivityUserData() : ActivityUserData(nullptr, 0, -1) {} |
| |
| ActivityUserData::ActivityUserData(void* memory, size_t size, int64_t pid) |
| : memory_(reinterpret_cast<char*>(memory)), |
| available_(bits::AlignDown(size, kMemoryAlignment)), |
| header_(reinterpret_cast<MemoryHeader*>(memory)), |
| orig_data_id(0), |
| orig_process_id(0), |
| orig_create_stamp(0) { |
| // It's possible that no user data is being stored. |
| if (!memory_) |
| return; |
| |
| static_assert(0 == sizeof(MemoryHeader) % kMemoryAlignment, "invalid header"); |
| DCHECK_LT(sizeof(MemoryHeader), available_); |
| if (header_->owner.data_id.load(std::memory_order_acquire) == 0) |
| header_->owner.Release_Initialize(pid); |
| memory_ += sizeof(MemoryHeader); |
| available_ -= sizeof(MemoryHeader); |
| |
| // Make a copy of identifying information for later comparison. |
| *const_cast<uint32_t*>(&orig_data_id) = |
| header_->owner.data_id.load(std::memory_order_acquire); |
| *const_cast<int64_t*>(&orig_process_id) = header_->owner.process_id; |
| *const_cast<int64_t*>(&orig_create_stamp) = header_->owner.create_stamp; |
| |
| // If there is already data present, load that. This allows the same class |
| // to be used for analysis through snapshots. |
| ImportExistingData(); |
| } |
| |
| ActivityUserData::~ActivityUserData() = default; |
| |
| bool ActivityUserData::CreateSnapshot(Snapshot* output_snapshot) const { |
| DCHECK(output_snapshot); |
| DCHECK(output_snapshot->empty()); |
| |
| // Find any new data that may have been added by an active instance of this |
| // class that is adding records. |
| ImportExistingData(); |
| |
| // Add all the values to the snapshot. |
| for (const auto& entry : values_) { |
| TypedValue value; |
| const size_t size = entry.second.size_ptr->load(std::memory_order_acquire); |
| value.type_ = entry.second.type; |
| DCHECK_GE(entry.second.extent, size); |
| |
| switch (entry.second.type) { |
| case RAW_VALUE: |
| case STRING_VALUE: |
| value.long_value_ = |
| std::string(reinterpret_cast<char*>(entry.second.memory), size); |
| break; |
| case RAW_VALUE_REFERENCE: |
| case STRING_VALUE_REFERENCE: { |
| ReferenceRecord* ref = |
| reinterpret_cast<ReferenceRecord*>(entry.second.memory); |
| value.ref_value_ = StringPiece( |
| reinterpret_cast<char*>(static_cast<uintptr_t>(ref->address)), |
| static_cast<size_t>(ref->size)); |
| } break; |
| case BOOL_VALUE: |
| case CHAR_VALUE: |
| value.short_value_ = |
| reinterpret_cast<std::atomic<char>*>(entry.second.memory) |
| ->load(std::memory_order_relaxed); |
| break; |
| case SIGNED_VALUE: |
| case UNSIGNED_VALUE: |
| value.short_value_ = |
| reinterpret_cast<std::atomic<uint64_t>*>(entry.second.memory) |
| ->load(std::memory_order_relaxed); |
| break; |
| case END_OF_VALUES: // Included for completeness purposes. |
| NOTREACHED(); |
| } |
| auto inserted = output_snapshot->emplace(std::string(entry.second.name), |
| std::move(value)); |
| DCHECK(inserted.second); // True if inserted, false if existed. |
| } |
| |
| // Another import attempt will validate that the underlying memory has not |
| // been reused for another purpose. Entries added since the first import |
| // will be ignored here but will be returned if another snapshot is created. |
| ImportExistingData(); |
| if (!memory_) { |
| output_snapshot->clear(); |
| return false; |
| } |
| |
| // Successful snapshot. |
| return true; |
| } |
| |
| const void* ActivityUserData::GetBaseAddress() const { |
| // The |memory_| pointer advances as elements are written but the |header_| |
| // value is always at the start of the block so just return that. |
| return header_; |
| } |
| |
| void ActivityUserData::SetOwningProcessIdForTesting(int64_t pid, |
| int64_t stamp) { |
| if (!header_) |
| return; |
| header_->owner.SetOwningProcessIdForTesting(pid, stamp); |
| } |
| |
| // static |
| bool ActivityUserData::GetOwningProcessId(const void* memory, |
| int64_t* out_id, |
| int64_t* out_stamp) { |
| const MemoryHeader* header = reinterpret_cast<const MemoryHeader*>(memory); |
| return OwningProcess::GetOwningProcessId(&header->owner, out_id, out_stamp); |
| } |
| |
| void* ActivityUserData::Set(StringPiece name, |
| ValueType type, |
| const void* memory, |
| size_t size) { |
| DCHECK_GE(std::numeric_limits<uint8_t>::max(), name.length()); |
| size = std::min(std::numeric_limits<uint16_t>::max() - (kMemoryAlignment - 1), |
| size); |
| |
| // It's possible that no user data is being stored. |
| if (!memory_) |
| return nullptr; |
| |
| // The storage of a name is limited so use that limit during lookup. |
| if (name.length() > kMaxUserDataNameLength) |
| name = StringPiece(name.data(), kMaxUserDataNameLength); |
| |
| ValueInfo* info; |
| auto existing = values_.find(name); |
| if (existing != values_.end()) { |
| info = &existing->second; |
| } else { |
| // The name size is limited to what can be held in a single byte but |
| // because there are not alignment constraints on strings, it's set tight |
| // against the header. Its extent (the reserved space, even if it's not |
| // all used) is calculated so that, when pressed against the header, the |
| // following field will be aligned properly. |
| size_t name_size = name.length(); |
| size_t name_extent = |
| bits::AlignUp(sizeof(FieldHeader) + name_size, kMemoryAlignment) - |
| sizeof(FieldHeader); |
| size_t value_extent = bits::AlignUp(size, kMemoryAlignment); |
| |
| // The "base size" is the size of the header and (padded) string key. Stop |
| // now if there's not room enough for even this. |
| size_t base_size = sizeof(FieldHeader) + name_extent; |
| if (base_size > available_) |
| return nullptr; |
| |
| // The "full size" is the size for storing the entire value. |
| size_t full_size = std::min(base_size + value_extent, available_); |
| |
| // If the value is actually a single byte, see if it can be stuffed at the |
| // end of the name extent rather than wasting kMemoryAlignment bytes. |
| if (size == 1 && name_extent > name_size) { |
| full_size = base_size; |
| --name_extent; |
| --base_size; |
| } |
| |
| // Truncate the stored size to the amount of available memory. Stop now if |
| // there's not any room for even part of the value. |
| if (size != 0) { |
| size = std::min(full_size - base_size, size); |
| if (size == 0) |
| return nullptr; |
| } |
| |
| // Allocate a chunk of memory. |
| FieldHeader* header = reinterpret_cast<FieldHeader*>(memory_); |
| memory_ += full_size; |
| available_ -= full_size; |
| |
| // Datafill the header and name records. Memory must be zeroed. The |type| |
| // is written last, atomically, to release all the other values. |
| DCHECK_EQ(END_OF_VALUES, header->type.load(std::memory_order_relaxed)); |
| DCHECK_EQ(0, header->value_size.load(std::memory_order_relaxed)); |
| header->name_size = static_cast<uint8_t>(name_size); |
| header->record_size = full_size; |
| char* name_memory = reinterpret_cast<char*>(header) + sizeof(FieldHeader); |
| void* value_memory = |
| reinterpret_cast<char*>(header) + sizeof(FieldHeader) + name_extent; |
| memcpy(name_memory, name.data(), name_size); |
| header->type.store(type, std::memory_order_release); |
| |
| // Create an entry in |values_| so that this field can be found and changed |
| // later on without having to allocate new entries. |
| StringPiece persistent_name(name_memory, name_size); |
| auto inserted = |
| values_.insert(std::make_pair(persistent_name, ValueInfo())); |
| DCHECK(inserted.second); // True if inserted, false if existed. |
| info = &inserted.first->second; |
| info->name = persistent_name; |
| info->memory = value_memory; |
| info->size_ptr = &header->value_size; |
| info->extent = full_size - sizeof(FieldHeader) - name_extent; |
| info->type = type; |
| } |
| |
| // Copy the value data to storage. The |size| is written last, atomically, to |
| // release the copied data. Until then, a parallel reader will just ignore |
| // records with a zero size. |
| DCHECK_EQ(type, info->type); |
| size = std::min(size, info->extent); |
| info->size_ptr->store(0, std::memory_order_seq_cst); |
| memcpy(info->memory, memory, size); |
| info->size_ptr->store(size, std::memory_order_release); |
| |
| // The address of the stored value is returned so it can be re-used by the |
| // caller, so long as it's done in an atomic way. |
| return info->memory; |
| } |
| |
| void ActivityUserData::SetReference(StringPiece name, |
| ValueType type, |
| const void* memory, |
| size_t size) { |
| ReferenceRecord rec; |
| rec.address = reinterpret_cast<uintptr_t>(memory); |
| rec.size = size; |
| Set(name, type, &rec, sizeof(rec)); |
| } |
| |
| void ActivityUserData::ImportExistingData() const { |
| // It's possible that no user data is being stored. |
| if (!memory_) |
| return; |
| |
| while (available_ > sizeof(FieldHeader)) { |
| FieldHeader* header = reinterpret_cast<FieldHeader*>(memory_); |
| ValueType type = |
| static_cast<ValueType>(header->type.load(std::memory_order_acquire)); |
| if (type == END_OF_VALUES) |
| return; |
| if (header->record_size > available_) |
| return; |
| |
| size_t value_offset = bits::AlignUp(sizeof(FieldHeader) + header->name_size, |
| kMemoryAlignment); |
| if (header->record_size == value_offset && |
| header->value_size.load(std::memory_order_relaxed) == 1) { |
| value_offset -= 1; |
| } |
| if (value_offset + header->value_size > header->record_size) |
| return; |
| |
| ValueInfo info; |
| info.name = StringPiece(memory_ + sizeof(FieldHeader), header->name_size); |
| info.type = type; |
| info.memory = memory_ + value_offset; |
| info.size_ptr = &header->value_size; |
| info.extent = header->record_size - value_offset; |
| |
| StringPiece key(info.name); |
| values_.insert(std::make_pair(key, std::move(info))); |
| |
| memory_ += header->record_size; |
| available_ -= header->record_size; |
| } |
| |
| // Check if memory has been completely reused. |
| if (header_->owner.data_id.load(std::memory_order_acquire) != orig_data_id || |
| header_->owner.process_id != orig_process_id || |
| header_->owner.create_stamp != orig_create_stamp) { |
| memory_ = nullptr; |
| values_.clear(); |
| } |
| } |
| |
| // This information is kept for every thread that is tracked. It is filled |
| // the very first time the thread is seen. All fields must be of exact sizes |
| // so there is no issue moving between 32 and 64-bit builds. |
| struct ThreadActivityTracker::Header { |
| // Defined in .h for analyzer access. Increment this if structure changes! |
| static constexpr uint32_t kPersistentTypeId = |
| GlobalActivityTracker::kTypeIdActivityTracker; |
| |
| // Expected size for 32/64-bit check. |
| static constexpr size_t kExpectedInstanceSize = |
| OwningProcess::kExpectedInstanceSize + Activity::kExpectedInstanceSize + |
| 72; |
| |
| // This information uniquely identifies a process. |
| OwningProcess owner; |
| |
| // The thread-id (thread_ref.as_id) to which this data belongs. This number |
| // is not guaranteed to mean anything but combined with the process-id from |
| // OwningProcess is unique among all active trackers. |
| ThreadRef thread_ref; |
| |
| // The start-time and start-ticks when the data was created. Each activity |
| // record has a |time_internal| value that can be converted to a "wall time" |
| // with these two values. |
| int64_t start_time; |
| int64_t start_ticks; |
| |
| // The number of Activity slots (spaces that can hold an Activity) that |
| // immediately follow this structure in memory. |
| uint32_t stack_slots; |
| |
| // Some padding to keep everything 64-bit aligned. |
| uint32_t padding; |
| |
| // The current depth of the stack. This may be greater than the number of |
| // slots. If the depth exceeds the number of slots, the newest entries |
| // won't be recorded. |
| std::atomic<uint32_t> current_depth; |
| |
| // A memory location used to indicate if changes have been made to the data |
| // that would invalidate an in-progress read of its contents. The active |
| // tracker will increment the value whenever something gets popped from the |
| // stack. A monitoring tracker can check the value before and after access |
| // to know, if it's still the same, that the contents didn't change while |
| // being copied. |
| std::atomic<uint32_t> data_version; |
| |
| // The last "exception" activity. This can't be stored on the stack because |
| // that could get popped as things unwind. |
| Activity last_exception; |
| |
| // The name of the thread (up to a maximum length). Dynamic-length names |
| // are not practical since the memory has to come from the same persistent |
| // allocator that holds this structure and to which this object has no |
| // reference. |
| char thread_name[32]; |
| }; |
| |
| ThreadActivityTracker::Snapshot::Snapshot() = default; |
| ThreadActivityTracker::Snapshot::~Snapshot() = default; |
| |
| ThreadActivityTracker::ScopedActivity::ScopedActivity( |
| ThreadActivityTracker* tracker, |
| const void* program_counter, |
| const void* origin, |
| Activity::Type type, |
| const ActivityData& data) |
| : tracker_(tracker) { |
| if (tracker_) |
| activity_id_ = tracker_->PushActivity(program_counter, origin, type, data); |
| } |
| |
| ThreadActivityTracker::ScopedActivity::~ScopedActivity() { |
| if (tracker_) |
| tracker_->PopActivity(activity_id_); |
| } |
| |
| bool ThreadActivityTracker::ScopedActivity::IsRecorded() { |
| return tracker_ && tracker_->IsRecorded(activity_id_); |
| } |
| |
| void ThreadActivityTracker::ScopedActivity::ChangeTypeAndData( |
| Activity::Type type, |
| const ActivityData& data) { |
| if (tracker_) |
| tracker_->ChangeActivity(activity_id_, type, data); |
| } |
| |
| ThreadActivityTracker::ThreadActivityTracker(void* base, size_t size) |
| : header_(static_cast<Header*>(base)), |
| stack_(reinterpret_cast<Activity*>(reinterpret_cast<char*>(base) + |
| sizeof(Header))), |
| #if DCHECK_IS_ON() |
| thread_id_(PlatformThreadRef()), |
| #endif |
| stack_slots_( |
| static_cast<uint32_t>((size - sizeof(Header)) / sizeof(Activity))) { |
| // Verify the parameters but fail gracefully if they're not valid so that |
| // production code based on external inputs will not crash. IsValid() will |
| // return false in this case. |
| if (!base || |
| // Ensure there is enough space for the header and at least a few records. |
| size < sizeof(Header) + kMinStackDepth * sizeof(Activity) || |
| // Ensure that the |stack_slots_| calculation didn't overflow. |
| (size - sizeof(Header)) / sizeof(Activity) > |
| std::numeric_limits<uint32_t>::max()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| // Ensure that the thread reference doesn't exceed the size of the ID number. |
| // This won't compile at the global scope because Header is a private struct. |
| static_assert( |
| sizeof(header_->thread_ref) == sizeof(header_->thread_ref.as_id), |
| "PlatformThreadHandle::Handle is too big to hold in 64-bit ID"); |
| |
| // Ensure that the alignment of Activity.data is properly aligned to a |
| // 64-bit boundary so there are no interoperability-issues across cpu |
| // architectures. |
| static_assert(offsetof(Activity, data) % sizeof(uint64_t) == 0, |
| "ActivityData.data is not 64-bit aligned"); |
| |
| // Provided memory should either be completely initialized or all zeros. |
| if (header_->owner.data_id.load(std::memory_order_relaxed) == 0) { |
| // This is a new file. Double-check other fields and then initialize. |
| DCHECK_EQ(0, header_->owner.process_id); |
| DCHECK_EQ(0, header_->owner.create_stamp); |
| DCHECK_EQ(0, header_->thread_ref.as_id); |
| DCHECK_EQ(0, header_->start_time); |
| DCHECK_EQ(0, header_->start_ticks); |
| DCHECK_EQ(0U, header_->stack_slots); |
| DCHECK_EQ(0U, header_->current_depth.load(std::memory_order_relaxed)); |
| DCHECK_EQ(0U, header_->data_version.load(std::memory_order_relaxed)); |
| DCHECK_EQ(0, stack_[0].time_internal); |
| DCHECK_EQ(0U, stack_[0].origin_address); |
| DCHECK_EQ(0U, stack_[0].call_stack[0]); |
| DCHECK_EQ(0U, stack_[0].data.task.sequence_id); |
| |
| #if defined(OS_WIN) |
| header_->thread_ref.as_tid = PlatformThread::CurrentId(); |
| #elif defined(OS_POSIX) || defined(OS_FUCHSIA) |
| header_->thread_ref.as_handle = |
| PlatformThread::CurrentHandle().platform_handle(); |
| #endif |
| |
| header_->start_time = base::Time::Now().ToInternalValue(); |
| header_->start_ticks = base::TimeTicks::Now().ToInternalValue(); |
| header_->stack_slots = stack_slots_; |
| strlcpy(header_->thread_name, PlatformThread::GetName(), |
| sizeof(header_->thread_name)); |
| |
| // This is done last so as to guarantee that everything above is "released" |
| // by the time this value gets written. |
| header_->owner.Release_Initialize(); |
| |
| valid_ = true; |
| DCHECK(IsValid()); |
| } else { |
| // This is a file with existing data. Perform basic consistency checks. |
| valid_ = true; |
| valid_ = IsValid(); |
| } |
| } |
| |
| ThreadActivityTracker::~ThreadActivityTracker() = default; |
| |
| ThreadActivityTracker::ActivityId ThreadActivityTracker::PushActivity( |
| const void* program_counter, |
| const void* origin, |
| Activity::Type type, |
| const ActivityData& data) { |
| // A thread-checker creates a lock to check the thread-id which means |
| // re-entry into this code if lock acquisitions are being tracked. |
| DCHECK(type == Activity::ACT_LOCK_ACQUIRE || CalledOnValidThread()); |
| |
| // Get the current depth of the stack. No access to other memory guarded |
| // by this variable is done here so a "relaxed" load is acceptable. |
| uint32_t depth = header_->current_depth.load(std::memory_order_relaxed); |
| |
| // Handle the case where the stack depth has exceeded the storage capacity. |
| // Extra entries will be lost leaving only the base of the stack. |
| if (depth >= stack_slots_) { |
| // Since no other threads modify the data, no compare/exchange is needed. |
| // Since no other memory is being modified, a "relaxed" store is acceptable. |
| header_->current_depth.store(depth + 1, std::memory_order_relaxed); |
| return depth; |
| } |
| |
| // Get a pointer to the next activity and load it. No atomicity is required |
| // here because the memory is known only to this thread. It will be made |
| // known to other threads once the depth is incremented. |
| Activity::FillFrom(&stack_[depth], program_counter, origin, type, data); |
| |
| // Save the incremented depth. Because this guards |activity| memory filled |
| // above that may be read by another thread once the recorded depth changes, |
| // a "release" store is required. |
| header_->current_depth.store(depth + 1, std::memory_order_release); |
| |
| // The current depth is used as the activity ID because it simply identifies |
| // an entry. Once an entry is pop'd, it's okay to reuse the ID. |
| return depth; |
| } |
| |
| void ThreadActivityTracker::ChangeActivity(ActivityId id, |
| Activity::Type type, |
| const ActivityData& data) { |
| DCHECK(CalledOnValidThread()); |
| DCHECK(type != Activity::ACT_NULL || &data != &kNullActivityData); |
| DCHECK_LT(id, header_->current_depth.load(std::memory_order_acquire)); |
| |
| // Update the information if it is being recorded (i.e. within slot limit). |
| if (id < stack_slots_) { |
| Activity* activity = &stack_[id]; |
| |
| if (type != Activity::ACT_NULL) { |
| DCHECK_EQ(activity->activity_type & Activity::ACT_CATEGORY_MASK, |
| type & Activity::ACT_CATEGORY_MASK); |
| activity->activity_type = type; |
| } |
| |
| if (&data != &kNullActivityData) |
| activity->data = data; |
| } |
| } |
| |
| void ThreadActivityTracker::PopActivity(ActivityId id) { |
| // Do an atomic decrement of the depth. No changes to stack entries guarded |
| // by this variable are done here so a "relaxed" operation is acceptable. |
| // |depth| will receive the value BEFORE it was modified which means the |
| // return value must also be decremented. The slot will be "free" after |
| // this call but since only a single thread can access this object, the |
| // data will remain valid until this method returns or calls outside. |
| uint32_t depth = |
| header_->current_depth.fetch_sub(1, std::memory_order_relaxed) - 1; |
| |
| // Validate that everything is running correctly. |
| DCHECK_EQ(id, depth); |
| |
| // A thread-checker creates a lock to check the thread-id which means |
| // re-entry into this code if lock acquisitions are being tracked. |
| DCHECK(stack_[depth].activity_type == Activity::ACT_LOCK_ACQUIRE || |
| CalledOnValidThread()); |
| |
| // The stack has shrunk meaning that some other thread trying to copy the |
| // contents for reporting purposes could get bad data. Increment the data |
| // version so that it con tell that things have changed. This needs to |
| // happen after the atomic |depth| operation above so a "release" store |
| // is required. |
| header_->data_version.fetch_add(1, std::memory_order_release); |
| } |
| |
| bool ThreadActivityTracker::IsRecorded(ActivityId id) { |
| return id < stack_slots_; |
| } |
| |
| std::unique_ptr<ActivityUserData> ThreadActivityTracker::GetUserData( |
| ActivityId id, |
| ActivityTrackerMemoryAllocator* allocator) { |
| // Don't allow user data for lock acquisition as recursion may occur. |
| if (stack_[id].activity_type == Activity::ACT_LOCK_ACQUIRE) { |
| NOTREACHED(); |
| return std::make_unique<ActivityUserData>(); |
| } |
| |
| // User-data is only stored for activities actually held in the stack. |
| if (id >= stack_slots_) |
| return std::make_unique<ActivityUserData>(); |
| |
| // Create and return a real UserData object. |
| return CreateUserDataForActivity(&stack_[id], allocator); |
| } |
| |
| bool ThreadActivityTracker::HasUserData(ActivityId id) { |
| // User-data is only stored for activities actually held in the stack. |
| return (id < stack_slots_ && stack_[id].user_data_ref); |
| } |
| |
| void ThreadActivityTracker::ReleaseUserData( |
| ActivityId id, |
| ActivityTrackerMemoryAllocator* allocator) { |
| // User-data is only stored for activities actually held in the stack. |
| if (id < stack_slots_ && stack_[id].user_data_ref) { |
| allocator->ReleaseObjectReference(stack_[id].user_data_ref); |
| stack_[id].user_data_ref = 0; |
| } |
| } |
| |
| void ThreadActivityTracker::RecordExceptionActivity(const void* program_counter, |
| const void* origin, |
| Activity::Type type, |
| const ActivityData& data) { |
| // A thread-checker creates a lock to check the thread-id which means |
| // re-entry into this code if lock acquisitions are being tracked. |
| DCHECK(CalledOnValidThread()); |
| |
| // Fill the reusable exception activity. |
| Activity::FillFrom(&header_->last_exception, program_counter, origin, type, |
| data); |
| |
| // The data has changed meaning that some other thread trying to copy the |
| // contents for reporting purposes could get bad data. |
| header_->data_version.fetch_add(1, std::memory_order_relaxed); |
| } |
| |
| bool ThreadActivityTracker::IsValid() const { |
| if (header_->owner.data_id.load(std::memory_order_acquire) == 0 || |
| header_->owner.process_id == 0 || header_->thread_ref.as_id == 0 || |
| header_->start_time == 0 || header_->start_ticks == 0 || |
| header_->stack_slots != stack_slots_ || |
| header_->thread_name[sizeof(header_->thread_name) - 1] != '\0') { |
| return false; |
| } |
| |
| return valid_; |
| } |
| |
| bool ThreadActivityTracker::CreateSnapshot(Snapshot* output_snapshot) const { |
| DCHECK(output_snapshot); |
| |
| // There is no "called on valid thread" check for this method as it can be |
| // called from other threads or even other processes. It is also the reason |
| // why atomic operations must be used in certain places above. |
| |
| // It's possible for the data to change while reading it in such a way that it |
| // invalidates the read. Make several attempts but don't try forever. |
| const int kMaxAttempts = 10; |
| uint32_t depth; |
| |
| // Stop here if the data isn't valid. |
| if (!IsValid()) |
| return false; |
| |
| // Allocate the maximum size for the stack so it doesn't have to be done |
| // during the time-sensitive snapshot operation. It is shrunk once the |
| // actual size is known. |
| output_snapshot->activity_stack.reserve(stack_slots_); |
| |
| for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { |
| // Remember the data IDs to ensure nothing is replaced during the snapshot |
| // operation. Use "acquire" so that all the non-atomic fields of the |
| // structure are valid (at least at the current moment in time). |
| const uint32_t starting_id = |
| header_->owner.data_id.load(std::memory_order_acquire); |
| const int64_t starting_create_stamp = header_->owner.create_stamp; |
| const int64_t starting_process_id = header_->owner.process_id; |
| const int64_t starting_thread_id = header_->thread_ref.as_id; |
| |
| // Note the current |data_version| so it's possible to detect at the end |
| // that nothing has changed since copying the data began. A "cst" operation |
| // is required to ensure it occurs before everything else. Using "cst" |
| // memory ordering is relatively expensive but this is only done during |
| // analysis so doesn't directly affect the worker threads. |
| const uint32_t pre_version = |
| header_->data_version.load(std::memory_order_seq_cst); |
| |
| // Fetching the current depth also "acquires" the contents of the stack. |
| depth = header_->current_depth.load(std::memory_order_acquire); |
| uint32_t count = std::min(depth, stack_slots_); |
| output_snapshot->activity_stack.resize(count); |
| if (count > 0) { |
| // Copy the existing contents. Memcpy is used for speed. |
| memcpy(&output_snapshot->activity_stack[0], stack_, |
| count * sizeof(Activity)); |
| } |
| |
| // Capture the last exception. |
| memcpy(&output_snapshot->last_exception, &header_->last_exception, |
| sizeof(Activity)); |
| |
| // TODO(bcwhite): Snapshot other things here. |
| |
| // Retry if something changed during the copy. A "cst" operation ensures |
| // it must happen after all the above operations. |
| if (header_->data_version.load(std::memory_order_seq_cst) != pre_version) |
| continue; |
| |
| // Stack copied. Record it's full depth. |
| output_snapshot->activity_stack_depth = depth; |
| |
| // Get the general thread information. |
| output_snapshot->thread_name = |
| std::string(header_->thread_name, sizeof(header_->thread_name) - 1); |
| output_snapshot->create_stamp = header_->owner.create_stamp; |
| output_snapshot->thread_id = header_->thread_ref.as_id; |
| output_snapshot->process_id = header_->owner.process_id; |
| |
| // All characters of the thread-name buffer were copied so as to not break |
| // if the trailing NUL were missing. Now limit the length if the actual |
| // name is shorter. |
| output_snapshot->thread_name.resize( |
| strlen(output_snapshot->thread_name.c_str())); |
| |
| // If the data ID has changed then the tracker has exited and the memory |
| // reused by a new one. Try again. |
| if (header_->owner.data_id.load(std::memory_order_seq_cst) != starting_id || |
| output_snapshot->create_stamp != starting_create_stamp || |
| output_snapshot->process_id != starting_process_id || |
| output_snapshot->thread_id != starting_thread_id) { |
| continue; |
| } |
| |
| // Only successful if the data is still valid once everything is done since |
| // it's possible for the thread to end somewhere in the middle and all its |
| // values become garbage. |
| if (!IsValid()) |
| return false; |
| |
| // Change all the timestamps in the activities from "ticks" to "wall" time. |
| const Time start_time = Time::FromInternalValue(header_->start_time); |
| const int64_t start_ticks = header_->start_ticks; |
| for (Activity& activity : output_snapshot->activity_stack) { |
| activity.time_internal = |
| WallTimeFromTickTime(start_ticks, activity.time_internal, start_time) |
| .ToInternalValue(); |
| } |
| output_snapshot->last_exception.time_internal = |
| WallTimeFromTickTime(start_ticks, |
| output_snapshot->last_exception.time_internal, |
| start_time) |
| .ToInternalValue(); |
| |
| // Success! |
| return true; |
| } |
| |
| // Too many attempts. |
| return false; |
| } |
| |
| const void* ThreadActivityTracker::GetBaseAddress() { |
| return header_; |
| } |
| |
| uint32_t ThreadActivityTracker::GetDataVersionForTesting() { |
| return header_->data_version.load(std::memory_order_relaxed); |
| } |
| |
| void ThreadActivityTracker::SetOwningProcessIdForTesting(int64_t pid, |
| int64_t stamp) { |
| header_->owner.SetOwningProcessIdForTesting(pid, stamp); |
| } |
| |
| // static |
| bool ThreadActivityTracker::GetOwningProcessId(const void* memory, |
| int64_t* out_id, |
| int64_t* out_stamp) { |
| const Header* header = reinterpret_cast<const Header*>(memory); |
| return OwningProcess::GetOwningProcessId(&header->owner, out_id, out_stamp); |
| } |
| |
| // static |
| size_t ThreadActivityTracker::SizeForStackDepth(int stack_depth) { |
| return static_cast<size_t>(stack_depth) * sizeof(Activity) + sizeof(Header); |
| } |
| |
| bool ThreadActivityTracker::CalledOnValidThread() { |
| #if DCHECK_IS_ON() |
| return thread_id_ == PlatformThreadRef(); |
| #else |
| return true; |
| #endif |
| } |
| |
| std::unique_ptr<ActivityUserData> |
| ThreadActivityTracker::CreateUserDataForActivity( |
| Activity* activity, |
| ActivityTrackerMemoryAllocator* allocator) { |
| DCHECK_EQ(0U, activity->user_data_ref); |
| |
| PersistentMemoryAllocator::Reference ref = allocator->GetObjectReference(); |
| void* memory = allocator->GetAsArray<char>(ref, kUserDataSize); |
| if (memory) { |
| std::unique_ptr<ActivityUserData> user_data = |
| std::make_unique<ActivityUserData>(memory, kUserDataSize); |
| activity->user_data_ref = ref; |
| activity->user_data_id = user_data->id(); |
| return user_data; |
| } |
| |
| // Return a dummy object that will still accept (but ignore) Set() calls. |
| return std::make_unique<ActivityUserData>(); |
| } |
| |
| // The instantiation of the GlobalActivityTracker object. |
| // The object held here will obviously not be destructed at process exit |
| // but that's best since PersistentMemoryAllocator objects (that underlie |
| // GlobalActivityTracker objects) are explicitly forbidden from doing anything |
| // essential at exit anyway due to the fact that they depend on data managed |
| // elsewhere and which could be destructed first. |
| std::atomic<GlobalActivityTracker*> GlobalActivityTracker::g_tracker_{nullptr}; |
| |
| GlobalActivityTracker::ModuleInfo::ModuleInfo() = default; |
| GlobalActivityTracker::ModuleInfo::ModuleInfo(ModuleInfo&& rhs) = default; |
| GlobalActivityTracker::ModuleInfo::ModuleInfo(const ModuleInfo& rhs) = default; |
| GlobalActivityTracker::ModuleInfo::~ModuleInfo() = default; |
| |
| GlobalActivityTracker::ModuleInfo& GlobalActivityTracker::ModuleInfo::operator=( |
| ModuleInfo&& rhs) = default; |
| GlobalActivityTracker::ModuleInfo& GlobalActivityTracker::ModuleInfo::operator=( |
| const ModuleInfo& rhs) = default; |
| |
| GlobalActivityTracker::ModuleInfoRecord::ModuleInfoRecord() = default; |
| GlobalActivityTracker::ModuleInfoRecord::~ModuleInfoRecord() = default; |
| |
| bool GlobalActivityTracker::ModuleInfoRecord::DecodeTo( |
| GlobalActivityTracker::ModuleInfo* info, |
| size_t record_size) const { |
| // Get the current "changes" indicator, acquiring all the other values. |
| uint32_t current_changes = changes.load(std::memory_order_acquire); |
| |
| // Copy out the dynamic information. |
| info->is_loaded = loaded != 0; |
| info->address = static_cast<uintptr_t>(address); |
| info->load_time = load_time; |
| |
| // Check to make sure no information changed while being read. A "seq-cst" |
| // operation is expensive but is only done during analysis and it's the only |
| // way to ensure this occurs after all the accesses above. If changes did |
| // occur then return a "not loaded" result so that |size| and |address| |
| // aren't expected to be accurate. |
| if ((current_changes & kModuleInformationChanging) != 0 || |
| changes.load(std::memory_order_seq_cst) != current_changes) { |
| info->is_loaded = false; |
| } |
| |
| // Copy out the static information. These never change so don't have to be |
| // protected by the atomic |current_changes| operations. |
| info->size = static_cast<size_t>(size); |
| info->timestamp = timestamp; |
| info->age = age; |
| memcpy(info->identifier, identifier, sizeof(info->identifier)); |
| |
| if (offsetof(ModuleInfoRecord, pickle) + pickle_size > record_size) |
| return false; |
| Pickle pickler(pickle, pickle_size); |
| PickleIterator iter(pickler); |
| return iter.ReadString(&info->file) && iter.ReadString(&info->debug_file); |
| } |
| |
| GlobalActivityTracker::ModuleInfoRecord* |
| GlobalActivityTracker::ModuleInfoRecord::CreateFrom( |
| const GlobalActivityTracker::ModuleInfo& info, |
| PersistentMemoryAllocator* allocator) { |
| Pickle pickler; |
| pickler.WriteString(info.file); |
| pickler.WriteString(info.debug_file); |
| size_t required_size = offsetof(ModuleInfoRecord, pickle) + pickler.size(); |
| ModuleInfoRecord* record = allocator->New<ModuleInfoRecord>(required_size); |
| if (!record) |
| return nullptr; |
| |
| // These fields never changes and are done before the record is made |
| // iterable so no thread protection is necessary. |
| record->size = info.size; |
| record->timestamp = info.timestamp; |
| record->age = info.age; |
| memcpy(record->identifier, info.identifier, sizeof(identifier)); |
| memcpy(record->pickle, pickler.data(), pickler.size()); |
| record->pickle_size = pickler.size(); |
| record->changes.store(0, std::memory_order_relaxed); |
| |
| // Initialize the owner info. |
| record->owner.Release_Initialize(); |
| |
| // Now set those fields that can change. |
| bool success = record->UpdateFrom(info); |
| DCHECK(success); |
| return record; |
| } |
| |
| bool GlobalActivityTracker::ModuleInfoRecord::UpdateFrom( |
| const GlobalActivityTracker::ModuleInfo& info) { |
| // Updates can occur after the record is made visible so make changes atomic. |
| // A "strong" exchange ensures no false failures. |
| uint32_t old_changes = changes.load(std::memory_order_relaxed); |
| uint32_t new_changes = old_changes | kModuleInformationChanging; |
| if ((old_changes & kModuleInformationChanging) != 0 || |
| !changes.compare_exchange_strong(old_changes, new_changes, |
| std::memory_order_acquire, |
| std::memory_order_acquire)) { |
| NOTREACHED() << "Multiple sources are updating module information."; |
| return false; |
| } |
| |
| loaded = info.is_loaded ? 1 : 0; |
| address = info.address; |
| load_time = Time::Now().ToInternalValue(); |
| |
| bool success = changes.compare_exchange_strong(new_changes, old_changes + 1, |
| std::memory_order_release, |
| std::memory_order_relaxed); |
| DCHECK(success); |
| return true; |
| } |
| |
| GlobalActivityTracker::ScopedThreadActivity::ScopedThreadActivity( |
| const void* program_counter, |
| const void* origin, |
| Activity::Type type, |
| const ActivityData& data, |
| bool lock_allowed) |
| : ThreadActivityTracker::ScopedActivity(GetOrCreateTracker(lock_allowed), |
| program_counter, |
| origin, |
| type, |
| data) {} |
| |
| GlobalActivityTracker::ScopedThreadActivity::~ScopedThreadActivity() { |
| if (tracker_ && tracker_->HasUserData(activity_id_)) { |
| GlobalActivityTracker* global = GlobalActivityTracker::Get(); |
| AutoLock lock(global->user_data_allocator_lock_); |
| tracker_->ReleaseUserData(activity_id_, &global->user_data_allocator_); |
| } |
| } |
| |
| ActivityUserData& GlobalActivityTracker::ScopedThreadActivity::user_data() { |
| if (!user_data_) { |
| if (tracker_) { |
| GlobalActivityTracker* global = GlobalActivityTracker::Get(); |
| AutoLock lock(global->user_data_allocator_lock_); |
| user_data_ = |
| tracker_->GetUserData(activity_id_, &global->user_data_allocator_); |
| } else { |
| user_data_ = std::make_unique<ActivityUserData>(); |
| } |
| } |
| return *user_data_; |
| } |
| |
| GlobalActivityTracker::ThreadSafeUserData::ThreadSafeUserData(void* memory, |
| size_t size, |
| int64_t pid) |
| : ActivityUserData(memory, size, pid) {} |
| |
| GlobalActivityTracker::ThreadSafeUserData::~ThreadSafeUserData() = default; |
| |
| void* GlobalActivityTracker::ThreadSafeUserData::Set(StringPiece name, |
| ValueType type, |
| const void* memory, |
| size_t size) { |
| AutoLock lock(data_lock_); |
| return ActivityUserData::Set(name, type, memory, size); |
| } |
| |
| GlobalActivityTracker::ManagedActivityTracker::ManagedActivityTracker( |
| PersistentMemoryAllocator::Reference mem_reference, |
| void* base, |
| size_t size) |
| : ThreadActivityTracker(base, size), |
| mem_reference_(mem_reference), |
| mem_base_(base) {} |
| |
| GlobalActivityTracker::ManagedActivityTracker::~ManagedActivityTracker() { |
| // The global |g_tracker_| must point to the owner of this class since all |
| // objects of this type must be destructed before |g_tracker_| can be changed |
| // (something that only occurs in tests). |
| DCHECK(g_tracker_.load(std::memory_order_relaxed)); |
| GlobalActivityTracker::Get()->ReturnTrackerMemory(this); |
| } |
| |
| void GlobalActivityTracker::CreateWithAllocator( |
| std::unique_ptr<PersistentMemoryAllocator> allocator, |
| int stack_depth, |
| int64_t process_id) { |
| // There's no need to do anything with the result. It is self-managing. |
| GlobalActivityTracker* global_tracker = |
| new GlobalActivityTracker(std::move(allocator), stack_depth, process_id); |
| // Create a tracker for this thread since it is known. |
| global_tracker->CreateTrackerForCurrentThread(); |
| } |
| |
| #if !defined(OS_NACL) |
| // static |
| bool GlobalActivityTracker::CreateWithFile(const FilePath& file_path, |
| size_t size, |
| uint64_t id, |
| StringPiece name, |
| int stack_depth) { |
| DCHECK(!file_path.empty()); |
| DCHECK_GE(static_cast<uint64_t>(std::numeric_limits<int64_t>::max()), size); |
| |
| // Create and map the file into memory and make it globally available. |
| std::unique_ptr<MemoryMappedFile> mapped_file(new MemoryMappedFile()); |
| bool success = mapped_file->Initialize( |
| File(file_path, File::FLAG_CREATE_ALWAYS | File::FLAG_READ | |
| File::FLAG_WRITE | File::FLAG_SHARE_DELETE), |
| {0, size}, MemoryMappedFile::READ_WRITE_EXTEND); |
| if (!success) |
| return false; |
| if (!FilePersistentMemoryAllocator::IsFileAcceptable(*mapped_file, false)) |
| return false; |
| CreateWithAllocator(std::make_unique<FilePersistentMemoryAllocator>( |
| std::move(mapped_file), size, id, name, false), |
| stack_depth, 0); |
| return true; |
| } |
| #endif // !defined(OS_NACL) |
| |
| // static |
| bool GlobalActivityTracker::CreateWithLocalMemory(size_t size, |
| uint64_t id, |
| StringPiece name, |
| int stack_depth, |
| int64_t process_id) { |
| CreateWithAllocator( |
| std::make_unique<LocalPersistentMemoryAllocator>(size, id, name), |
| stack_depth, process_id); |
| return true; |
| } |
| |
| // static |
| bool GlobalActivityTracker::CreateWithSharedMemory( |
| base::WritableSharedMemoryMapping mapping, |
| uint64_t id, |
| StringPiece name, |
| int stack_depth) { |
| if (!mapping.IsValid() || |
| !WritableSharedPersistentMemoryAllocator::IsSharedMemoryAcceptable( |
| mapping)) { |
| return false; |
| } |
| CreateWithAllocator(std::make_unique<WritableSharedPersistentMemoryAllocator>( |
| std::move(mapping), id, name), |
| stack_depth, 0); |
| return true; |
| } |
| |
| // static |
| void GlobalActivityTracker::SetForTesting( |
| std::unique_ptr<GlobalActivityTracker> tracker) { |
| CHECK(!g_tracker_.load(std::memory_order_relaxed)); |
| g_tracker_.store(tracker.release(), std::memory_order_release); |
| } |
| |
| // static |
| std::unique_ptr<GlobalActivityTracker> |
| GlobalActivityTracker::ReleaseForTesting() { |
| GlobalActivityTracker* tracker = Get(); |
| if (!tracker) |
| return nullptr; |
| |
| // Thread trackers assume that the global tracker is present for some |
| // operations so ensure that there aren't any. |
| tracker->ReleaseTrackerForCurrentThreadForTesting(); |
| DCHECK_EQ(0, tracker->thread_tracker_count_.load(std::memory_order_relaxed)); |
| |
| g_tracker_.store(nullptr, std::memory_order_release); |
| return WrapUnique(tracker); |
| } |
| |
| ThreadActivityTracker* GlobalActivityTracker::CreateTrackerForCurrentThread() { |
| // It is not safe to use TLS once TLS has been destroyed. |
| if (base::ThreadLocalStorage::HasBeenDestroyed()) |
| return nullptr; |
| |
| DCHECK(!this_thread_tracker_.Get()); |
| |
| PersistentMemoryAllocator::Reference mem_reference; |
| |
| { |
| base::AutoLock autolock(thread_tracker_allocator_lock_); |
| mem_reference = thread_tracker_allocator_.GetObjectReference(); |
| } |
| |
| if (!mem_reference) { |
| // Failure. This shouldn't happen. But be graceful if it does, probably |
| // because the underlying allocator wasn't given enough memory to satisfy |
| // to all possible requests. |
| NOTREACHED(); |
| |
| // Return null, just as if tracking wasn't enabled. |
| return nullptr; |
| } |
| |
| // Convert the memory block found above into an actual memory address. |
| // Doing the conversion as a Header object enacts the 32/64-bit size |
| // consistency checks which would not otherwise be done. |
| DCHECK(mem_reference); |
| void* mem_base; |
| mem_base = |
| allocator_->GetAsObject<ThreadActivityTracker::Header>(mem_reference); |
| |
| DCHECK(mem_base); |
| DCHECK_LE(stack_memory_size_, allocator_->GetAllocSize(mem_reference)); |
| |
| // Create a tracker with the acquired memory and set it as the tracker |
| // for this particular thread in thread-local-storage. |
| auto tracker = std::make_unique<ManagedActivityTracker>( |
| mem_reference, mem_base, stack_memory_size_); |
| DCHECK(tracker->IsValid()); |
| auto* tracker_raw = tracker.get(); |
| this_thread_tracker_.Set(std::move(tracker)); |
| thread_tracker_count_.fetch_add(1, std::memory_order_relaxed); |
| return tracker_raw; |
| } |
| |
| void GlobalActivityTracker::ReleaseTrackerForCurrentThreadForTesting() { |
| if (this_thread_tracker_.Get()) |
| this_thread_tracker_.Set(nullptr); |
| } |
| |
| void GlobalActivityTracker::SetBackgroundTaskRunner( |
| const scoped_refptr<SequencedTaskRunner>& runner) { |
| AutoLock lock(global_tracker_lock_); |
| background_task_runner_ = std::move(runner); |
| } |
| |
| void GlobalActivityTracker::SetProcessExitCallback( |
| ProcessExitCallback callback) { |
| AutoLock lock(global_tracker_lock_); |
| process_exit_callback_ = callback; |
| } |
| |
| void GlobalActivityTracker::RecordProcessLaunch( |
| ProcessId process_id, |
| const FilePath::StringType& cmd) { |
| const int64_t pid = process_id; |
| DCHECK_NE(GetProcessId(), pid); |
| DCHECK_NE(0, pid); |
| |
| base::AutoLock lock(global_tracker_lock_); |
| if (base::Contains(known_processes_, pid)) { |
| // TODO(bcwhite): Measure this in UMA. |
| NOTREACHED() << "Process #" << process_id |
| << " was previously recorded as \"launched\"" |
| << " with no corresponding exit.\n" |
| << known_processes_[pid]; |
| known_processes_.erase(pid); |
| } |
| |
| #if defined(OS_WIN) |
| known_processes_.insert(std::make_pair(pid, WideToUTF8(cmd))); |
| #else |
| known_processes_.insert(std::make_pair(pid, cmd)); |
| #endif |
| } |
| |
| void GlobalActivityTracker::RecordProcessLaunch( |
| ProcessId process_id, |
| const FilePath::StringType& exe, |
| const FilePath::StringType& args) { |
| if (exe.find(FILE_PATH_LITERAL(" "))) { |
| RecordProcessLaunch(process_id, |
| FilePath::StringType(FILE_PATH_LITERAL("\"")) + exe + |
| FILE_PATH_LITERAL("\" ") + args); |
| } else { |
| RecordProcessLaunch(process_id, exe + FILE_PATH_LITERAL(' ') + args); |
| } |
| } |
| |
| void GlobalActivityTracker::RecordProcessExit(ProcessId process_id, |
| int exit_code) { |
| const int64_t pid = process_id; |
| DCHECK_NE(GetProcessId(), pid); |
| DCHECK_NE(0, pid); |
| |
| scoped_refptr<SequencedTaskRunner> task_runner; |
| std::string command_line; |
| { |
| base::AutoLock lock(global_tracker_lock_); |
| task_runner = background_task_runner_; |
| auto found = known_processes_.find(pid); |
| if (found != known_processes_.end()) { |
| command_line = std::move(found->second); |
| known_processes_.erase(found); |
| } else { |
| DLOG(ERROR) << "Recording exit of unknown process #" << process_id; |
| } |
| } |
| |
| // Use the current time to differentiate the process that just exited |
| // from any that might be created in the future with the same ID. |
| int64_t now_stamp = Time::Now().ToInternalValue(); |
| |
| // The persistent allocator is thread-safe so run the iteration and |
| // adjustments on a worker thread if one was provided. |
| if (task_runner && !task_runner->RunsTasksInCurrentSequence()) { |
| task_runner->PostTask( |
| FROM_HERE, |
| BindOnce(&GlobalActivityTracker::CleanupAfterProcess, Unretained(this), |
| pid, now_stamp, exit_code, std::move(command_line))); |
| return; |
| } |
| |
| CleanupAfterProcess(pid, now_stamp, exit_code, std::move(command_line)); |
| } |
| |
| void GlobalActivityTracker::SetProcessPhase(ProcessPhase phase) { |
| process_data().SetInt(kProcessPhaseDataKey, phase); |
| } |
| |
| void GlobalActivityTracker::CleanupAfterProcess(int64_t process_id, |
| int64_t exit_stamp, |
| int exit_code, |
| std::string&& command_line) { |
| // The process may not have exited cleanly so its necessary to go through |
| // all the data structures it may have allocated in the persistent memory |
| // segment and mark them as "released". This will allow them to be reused |
| // later on. |
| |
| PersistentMemoryAllocator::Iterator iter(allocator_.get()); |
| PersistentMemoryAllocator::Reference ref; |
| |
| ProcessExitCallback process_exit_callback; |
| { |
| AutoLock lock(global_tracker_lock_); |
| process_exit_callback = process_exit_callback_; |
| } |
| if (process_exit_callback) { |
| // Find the processes user-data record so the process phase can be passed |
| // to the callback. |
| ActivityUserData::Snapshot process_data_snapshot; |
| while ((ref = iter.GetNextOfType(kTypeIdProcessDataRecord)) != 0) { |
| const void* memory = allocator_->GetAsArray<char>( |
| ref, kTypeIdProcessDataRecord, PersistentMemoryAllocator::kSizeAny); |
| if (!memory) |
| continue; |
| int64_t found_id; |
| int64_t create_stamp; |
| if (ActivityUserData::GetOwningProcessId(memory, &found_id, |
| &create_stamp)) { |
| if (found_id == process_id && create_stamp < exit_stamp) { |
| const ActivityUserData process_data(const_cast<void*>(memory), |
| allocator_->GetAllocSize(ref)); |
| process_data.CreateSnapshot(&process_data_snapshot); |
| break; // No need to look for any others. |
| } |
| } |
| } |
| iter.Reset(); // So it starts anew when used below. |
| |
| // Record the process's phase at exit so callback doesn't need to go |
| // searching based on a private key value. |
| ProcessPhase exit_phase = PROCESS_PHASE_UNKNOWN; |
| auto phase = process_data_snapshot.find(kProcessPhaseDataKey); |
| if (phase != process_data_snapshot.end()) |
| exit_phase = static_cast<ProcessPhase>(phase->second.GetInt()); |
| |
| // Perform the callback. |
| process_exit_callback.Run(process_id, exit_stamp, exit_code, exit_phase, |
| std::move(command_line), |
| std::move(process_data_snapshot)); |
| } |
| |
| // Find all allocations associated with the exited process and free them. |
| uint32_t type; |
| while ((ref = iter.GetNext(&type)) != 0) { |
| switch (type) { |
| case kTypeIdActivityTracker: |
| case kTypeIdUserDataRecord: |
| case kTypeIdProcessDataRecord: |
| case ModuleInfoRecord::kPersistentTypeId: { |
| const void* memory = allocator_->GetAsArray<char>( |
| ref, type, PersistentMemoryAllocator::kSizeAny); |
| if (!memory) |
| continue; |
| int64_t found_id; |
| int64_t create_stamp; |
| |
| // By convention, the OwningProcess structure is always the first |
| // field of the structure so there's no need to handle all the |
| // cases separately. |
| if (OwningProcess::GetOwningProcessId(memory, &found_id, |
| &create_stamp)) { |
| // Only change the type to be "free" if the process ID matches and |
| // the creation time is before the exit time (so PID re-use doesn't |
| // cause the erasure of something that is in-use). Memory is cleared |
| // here, rather than when it's needed, so as to limit the impact at |
| // that critical time. |
| if (found_id == process_id && create_stamp < exit_stamp) |
| allocator_->ChangeType(ref, ~type, type, /*clear=*/true); |
| } |
| } break; |
| } |
| } |
| } |
| |
| void GlobalActivityTracker::RecordLogMessage(StringPiece message) { |
| // Allocate at least one extra byte so the string is NUL terminated. All |
| // memory returned by the allocator is guaranteed to be zeroed. |
| PersistentMemoryAllocator::Reference ref = |
| allocator_->Allocate(message.size() + 1, kTypeIdGlobalLogMessage); |
| char* memory = allocator_->GetAsArray<char>(ref, kTypeIdGlobalLogMessage, |
| message.size() + 1); |
| if (memory) { |
| memcpy(memory, message.data(), message.size()); |
| allocator_->MakeIterable(ref); |
| } |
| } |
| |
| void GlobalActivityTracker::RecordModuleInfo(const ModuleInfo& info) { |
| AutoLock lock(modules_lock_); |
| auto found = modules_.find(info.file); |
| if (found != modules_.end()) { |
| ModuleInfoRecord* record = found->second; |
| DCHECK(record); |
| |
| // Update the basic state of module information that has been already |
| // recorded. It is assumed that the string information (identifier, |
| // version, etc.) remain unchanged which means that there's no need |
| // to create a new record to accommodate a possibly longer length. |
| record->UpdateFrom(info); |
| return; |
| } |
| |
| ModuleInfoRecord* record = |
| ModuleInfoRecord::CreateFrom(info, allocator_.get()); |
| if (!record) |
| return; |
| allocator_->MakeIterable(record); |
| modules_.emplace(info.file, record); |
| } |
| |
| void GlobalActivityTracker::RecordException(const void* pc, |
| const void* origin, |
| uint32_t code) { |
| RecordExceptionImpl(pc, origin, code); |
| } |
| |
| void GlobalActivityTracker::MarkDeleted() { |
| allocator_->SetMemoryState(PersistentMemoryAllocator::MEMORY_DELETED); |
| } |
| |
| GlobalActivityTracker::GlobalActivityTracker( |
| std::unique_ptr<PersistentMemoryAllocator> allocator, |
| int stack_depth, |
| int64_t process_id) |
| : allocator_(std::move(allocator)), |
| stack_memory_size_(ThreadActivityTracker::SizeForStackDepth(stack_depth)), |
| process_id_(process_id == 0 ? GetCurrentProcId() : process_id), |
| thread_tracker_count_(0), |
| thread_tracker_allocator_(allocator_.get(), |
| kTypeIdActivityTracker, |
| kTypeIdActivityTrackerFree, |
| stack_memory_size_, |
| kCachedThreadMemories, |
| /*make_iterable=*/true), |
| user_data_allocator_(allocator_.get(), |
| kTypeIdUserDataRecord, |
| kTypeIdUserDataRecordFree, |
| kUserDataSize, |
| kCachedUserDataMemories, |
| /*make_iterable=*/true), |
| process_data_(allocator_->GetAsArray<char>( |
| AllocateFrom(allocator_.get(), |
| kTypeIdProcessDataRecordFree, |
| kProcessDataSize, |
| kTypeIdProcessDataRecord), |
| kTypeIdProcessDataRecord, |
| kProcessDataSize), |
| kProcessDataSize, |
| process_id_) { |
| DCHECK_NE(0, process_id_); |
| |
| // Ensure that there is no other global object and then make this one such. |
| DCHECK(!g_tracker_.load(std::memory_order_relaxed)); |
| g_tracker_.store(this, std::memory_order_release); |
| |
| // The data records must be iterable in order to be found by an analyzer. |
| allocator_->MakeIterable(allocator_->GetAsReference( |
| process_data_.GetBaseAddress(), kTypeIdProcessDataRecord)); |
| |
| // Note that this process has launched. |
| SetProcessPhase(PROCESS_LAUNCHED); |
| } |
| |
| GlobalActivityTracker::~GlobalActivityTracker() { |
| DCHECK(Get() == nullptr || Get() == this); |
| DCHECK_EQ(0, thread_tracker_count_.load(std::memory_order_relaxed)); |
| g_tracker_.store(nullptr, std::memory_order_release); |
| } |
| |
| void GlobalActivityTracker::ReturnTrackerMemory( |
| ManagedActivityTracker* tracker) { |
| PersistentMemoryAllocator::Reference mem_reference = tracker->mem_reference_; |
| void* mem_base = tracker->mem_base_; |
| DCHECK(mem_reference); |
| DCHECK(mem_base); |
| |
| // Remove the destructed tracker from the set of known ones. |
| DCHECK_LE(1, thread_tracker_count_.load(std::memory_order_relaxed)); |
| thread_tracker_count_.fetch_sub(1, std::memory_order_relaxed); |
| |
| // Release this memory for re-use at a later time. |
| base::AutoLock autolock(thread_tracker_allocator_lock_); |
| thread_tracker_allocator_.ReleaseObjectReference(mem_reference); |
| } |
| |
| void GlobalActivityTracker::RecordExceptionImpl(const void* pc, |
| const void* origin, |
| uint32_t code) { |
| // Get an existing tracker for this thread. It's not possible to create |
| // one at this point because such would involve memory allocations and |
| // other potentially complex operations that can cause failures if done |
| // within an exception handler. In most cases various operations will |
| // have already created the tracker so this shouldn't generally be a |
| // problem. |
| ThreadActivityTracker* tracker = GetTrackerForCurrentThread(); |
| if (!tracker) |
| return; |
| |
| tracker->RecordExceptionActivity(pc, origin, Activity::ACT_EXCEPTION, |
| ActivityData::ForException(code)); |
| } |
| |
| ScopedActivity::ScopedActivity(const void* program_counter, |
| uint8_t action, |
| uint32_t id, |
| int32_t info) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| nullptr, |
| static_cast<Activity::Type>(Activity::ACT_GENERIC | action), |
| ActivityData::ForGeneric(id, info), |
| /*lock_allowed=*/true), |
| id_(id) { |
| // The action must not affect the category bits of the activity type. |
| DCHECK_EQ(0, action & Activity::ACT_CATEGORY_MASK); |
| } |
| |
| void ScopedActivity::ChangeAction(uint8_t action) { |
| DCHECK_EQ(0, action & Activity::ACT_CATEGORY_MASK); |
| ChangeTypeAndData(static_cast<Activity::Type>(Activity::ACT_GENERIC | action), |
| kNullActivityData); |
| } |
| |
| void ScopedActivity::ChangeInfo(int32_t info) { |
| ChangeTypeAndData(Activity::ACT_NULL, ActivityData::ForGeneric(id_, info)); |
| } |
| |
| void ScopedActivity::ChangeActionAndInfo(uint8_t action, int32_t info) { |
| DCHECK_EQ(0, action & Activity::ACT_CATEGORY_MASK); |
| ChangeTypeAndData(static_cast<Activity::Type>(Activity::ACT_GENERIC | action), |
| ActivityData::ForGeneric(id_, info)); |
| } |
| |
| ScopedTaskRunActivity::ScopedTaskRunActivity( |
| const void* program_counter, |
| const base::PendingTask& task) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| task.posted_from.program_counter(), |
| Activity::ACT_TASK_RUN, |
| ActivityData::ForTask(task.sequence_num), |
| /*lock_allowed=*/true) {} |
| |
| ScopedLockAcquireActivity::ScopedLockAcquireActivity( |
| const void* program_counter, |
| const base::internal::LockImpl* lock) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| nullptr, |
| Activity::ACT_LOCK_ACQUIRE, |
| ActivityData::ForLock(lock), |
| /*lock_allowed=*/false) {} |
| |
| ScopedEventWaitActivity::ScopedEventWaitActivity( |
| const void* program_counter, |
| const base::WaitableEvent* event) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| nullptr, |
| Activity::ACT_EVENT_WAIT, |
| ActivityData::ForEvent(event), |
| /*lock_allowed=*/true) {} |
| |
| ScopedThreadJoinActivity::ScopedThreadJoinActivity( |
| const void* program_counter, |
| const base::PlatformThreadHandle* thread) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| nullptr, |
| Activity::ACT_THREAD_JOIN, |
| ActivityData::ForThread(*thread), |
| /*lock_allowed=*/true) {} |
| |
| #if !defined(OS_NACL) && !defined(OS_IOS) |
| ScopedProcessWaitActivity::ScopedProcessWaitActivity( |
| const void* program_counter, |
| const base::Process* process) |
| : GlobalActivityTracker::ScopedThreadActivity( |
| program_counter, |
| nullptr, |
| Activity::ACT_PROCESS_WAIT, |
| ActivityData::ForProcess(process->Pid()), |
| /*lock_allowed=*/true) {} |
| #endif |
| |
| } // namespace debug |
| } // namespace base |