blob: 1940ddb89af3436e0aca44abe4a6055e5475c75a [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/allocator/etw_allocation_observer_win.h"
#include <windows.h>
#include <evntrace.h>
#include <stddef.h>
#include "base/allocator/dispatcher/subsystem.h"
#include "base/check_is_test.h"
#include "base/functional/bind.h"
namespace base::allocator {
namespace {
// TraceLogging writes the event name and the field names into every event
// record, and a busy renderer produces millions of allocations per second, so
// both are deliberately terse. "PA" marks the events that come from direct
// PartitionAlloc use (Blink partitions and friends) rather than malloc/new.
constexpr EVENT_DESCRIPTOR kAllocEvent =
TlmEventDescriptor(TRACE_LEVEL_VERBOSE, kEtwAllocationKeyword);
constexpr EVENT_DESCRIPTOR kFreeEvent =
TlmEventDescriptor(TRACE_LEVEL_VERBOSE, kEtwFreeKeyword);
static_assert((kEtwProcessTypeKeywords &
(kEtwAllocationKeyword | kEtwFreeKeyword)) == 0,
"process type keywords must not overlap the event keywords");
// These are defined in content/public/common/content_switches.h, which is not
// accessible in ::base. They must be kept in sync.
constexpr std::string_view kRendererProcess = "renderer";
constexpr std::string_view kGpuProcess = "gpu-process";
constexpr std::string_view kUtilityProcess = "utility";
// EventWrite() is not documented to allocate, but re-entering the allocation
// path from within an allocation hook would recurse without bound, so pay for
// a thread-local flag on the (already slow) writing path.
constinit thread_local bool g_writing_event = false;
class ScopedWriteGuard {
public:
ScopedWriteGuard() : allowed_(!g_writing_event) { g_writing_event = true; }
ScopedWriteGuard(const ScopedWriteGuard&) = delete;
ScopedWriteGuard& operator=(const ScopedWriteGuard&) = delete;
~ScopedWriteGuard() {
if (allowed_) {
g_writing_event = false;
}
}
explicit operator bool() const { return allowed_; }
private:
const bool allowed_;
};
} // namespace
EtwProcessTypeKeyword GetEtwProcessTypeKeyword(std::string_view process_type) {
if (process_type.empty()) {
return EtwProcessTypeKeyword::kBrowser;
}
if (process_type == kRendererProcess) {
return EtwProcessTypeKeyword::kRenderer;
}
if (process_type == kGpuProcess) {
return EtwProcessTypeKeyword::kGpu;
}
if (process_type == kUtilityProcess) {
return EtwProcessTypeKeyword::kUtility;
}
return EtwProcessTypeKeyword::kOther;
}
EtwAllocationObserver::EtwAllocationObserver() = default;
EtwAllocationObserver::~EtwAllocationObserver() = default;
// static
EtwAllocationObserver* EtwAllocationObserver::Get() {
static base::NoDestructor<EtwAllocationObserver> instance;
return instance.get();
}
void EtwAllocationObserver::Register(std::string_view process_type) {
// A session that is already listening can enable the provider before
// provider_.Register() returns, so this has to be known first.
process_type_ = GetEtwProcessTypeKeyword(process_type);
provider_.Register(
kEtwProviderName, kEtwProviderGuid,
BindRepeating(&EtwAllocationObserver::OnProviderStateChanged,
Unretained(this)));
// A session may already have been listening before this process started.
if (provider_.IsEnabled()) {
OnProviderStateChanged(TlmProvider::EventControlCode::kEnableProvider);
}
}
void EtwAllocationObserver::SetEnabledKeywordsForTesting(uint64_t keywords) {
// Not a trivial accessor: this opens the event writing path without a
// tracing session having asked for it.
CHECK_IS_TEST();
enabled_keywords_.store(keywords, std::memory_order_relaxed);
}
void EtwAllocationObserver::OnProviderStateChanged(
TlmProvider::EventControlCode event) {
const uint64_t keywords =
event == TlmProvider::EventControlCode::kDisableProvider
? 0u
: FilterKeywordsForProcessType(provider_.keyword_any(),
process_type_);
enabled_keywords_.store(keywords, std::memory_order_relaxed);
}
// static
uint64_t EtwAllocationObserver::FilterKeywordsForProcessType(
uint64_t keywords,
EtwProcessTypeKeyword process_type) {
// Sessions that name no process type at all trace every process.
if ((keywords & kEtwProcessTypeKeywords) == 0 ||
(keywords & static_cast<uint64_t>(process_type)) != 0) {
return keywords;
}
return 0;
}
NOINLINE void EtwAllocationObserver::WriteAllocationEvent(
const dispatcher::AllocationNotificationData& notification_data) {
ScopedWriteGuard guard;
if (!guard) {
return;
}
const uint64_t address =
reinterpret_cast<uintptr_t>(notification_data.address());
const uint64_t size = notification_data.size();
if (notification_data.allocation_subsystem() ==
dispatcher::AllocationSubsystem::kPartitionAllocator) {
// PartitionAlloc knows the partition a typed allocation came from, which is
// attribution the call stack alone does not always provide.
const char* const type_name = notification_data.type_name();
provider_.WriteEvent(
kEtwPartitionAllocationEventName, kAllocEvent,
TlmUInt64Field("Addr", address), TlmUInt64Field("Size", size),
TlmUtf8StringField("Type", type_name ? type_name : ""));
return;
}
provider_.WriteEvent(kEtwAllocationEventName, kAllocEvent,
TlmUInt64Field("Addr", address),
TlmUInt64Field("Size", size));
}
NOINLINE void EtwAllocationObserver::WriteFreeEvent(
const dispatcher::FreeNotificationData& notification_data) {
ScopedWriteGuard guard;
if (!guard) {
return;
}
const uint64_t address =
reinterpret_cast<uintptr_t>(notification_data.address());
// Frees carry no size: consumers have to remember it from the matching
// allocation, exactly as they already do for the Windows heap provider.
if (notification_data.allocation_subsystem() ==
dispatcher::AllocationSubsystem::kPartitionAllocator) {
provider_.WriteEvent(kEtwPartitionFreeEventName, kFreeEvent,
TlmUInt64Field("Addr", address));
return;
}
provider_.WriteEvent(kEtwFreeEventName, kFreeEvent,
TlmUInt64Field("Addr", address));
}
} // namespace base::allocator