blob: 95cf90208b5040677160ae8b07b0768de2947314 [file]
// Copyright 2015 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/trace_event/memory_dump_manager.h"
#include <inttypes.h>
#include <stdio.h>
#include <algorithm>
#include <array>
#include <memory>
#include <utility>
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/debug/alias.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/span_printf.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/heap_profiler.h"
#include "base/trace_event/heap_profiler_allocation_context_tracker.h"
#include "base/trace_event/malloc_dump_provider.h"
#include "base/trace_event/memory_dump_provider.h"
#include "base/trace_event/memory_dump_request_args.h"
#include "base/trace_event/memory_dump_scheduler.h"
#include "base/trace_event/memory_infra_background_allowlist.h"
#include "base/trace_event/process_memory_dump.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "base/tracing/protos/chrome_track_event.pbzero.h"
#include "build/build_config.h"
#include "partition_alloc/buildflags.h"
#include "third_party/abseil-cpp/absl/base/dynamic_annotations.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/jni_android.h"
#include "base/trace_event/java_heap_dump_provider_android.h"
#endif // BUILDFLAG(IS_ANDROID)
#if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
#include "base/trace_event/address_space_dump_provider.h"
#endif
namespace base::trace_event {
namespace {
MemoryDumpManager* g_memory_dump_manager_for_testing = nullptr;
// When enabled, MemoryDumpManager::ContinueAsyncProcessDump() runs all
// MemoryDumpProvider bound to a TaskRunner which RunsTasksInCurrentSequence(),
// no matter their position in the list of providers. This minimizes the number
// of PostTasks involved in a Memory Dump by grouping providers that run on the
// same sequence, even if they are bound to different TaskRunner instances
// (which previously resulted in a task post).
BASE_FEATURE(kMemoryDumpProviderGroupBySequence,
base::FEATURE_ENABLED_BY_DEFAULT);
// Temporary (until scheduler is moved outside of here)
// trampoline function to match the |request_dump_function| passed to Initialize
// to the callback expected by MemoryDumpScheduler.
// TODO(primiano): remove this.
void DoGlobalDumpWithoutCallback(
MemoryDumpManager::RequestGlobalDumpFunction global_dump_fn,
MemoryDumpType dump_type,
MemoryDumpLevelOfDetail level_of_detail) {
global_dump_fn.Run(dump_type, level_of_detail);
}
} // namespace
// static
constexpr const char* MemoryDumpManager::kTraceCategory;
// static
const int MemoryDumpManager::kMaxConsecutiveFailuresCount = 3;
// static
const uint64_t MemoryDumpManager::kInvalidTracingProcessId = 0;
// static
const char* const MemoryDumpManager::kSystemAllocatorPoolName =
#if defined(MALLOC_MEMORY_TRACING_SUPPORTED)
MallocDumpProvider::kAllocatedObjects;
#else
nullptr;
#endif
// static
MemoryDumpManager* MemoryDumpManager::GetInstance() {
if (g_memory_dump_manager_for_testing) {
return g_memory_dump_manager_for_testing;
}
return Singleton<MemoryDumpManager,
LeakySingletonTraits<MemoryDumpManager>>::get();
}
// static
std::unique_ptr<MemoryDumpManager>
MemoryDumpManager::CreateInstanceForTesting() {
DCHECK(!g_memory_dump_manager_for_testing);
auto instance = base::WrapUnique(new MemoryDumpManager());
g_memory_dump_manager_for_testing = instance.get();
return instance;
}
MemoryDumpManager::MemoryDumpManager() = default;
MemoryDumpManager::~MemoryDumpManager() {
Thread* dump_thread = nullptr;
{
AutoLock lock(lock_);
if (dump_thread_) {
dump_thread = dump_thread_.get();
}
}
if (dump_thread) {
dump_thread->Stop();
}
AutoLock lock(lock_);
dump_thread_.reset();
g_memory_dump_manager_for_testing = nullptr;
}
void MemoryDumpManager::Initialize(
RequestGlobalDumpFunction request_dump_function,
bool is_coordinator) {
{
AutoLock lock(lock_);
DCHECK(!request_dump_function.is_null());
DCHECK(!can_request_global_dumps());
request_dump_function_ = request_dump_function;
is_coordinator_ = is_coordinator;
}
// Enable the core dump providers.
#if defined(MALLOC_MEMORY_TRACING_SUPPORTED)
RegisterDumpProvider(MallocDumpProvider::GetInstance(), "Malloc", nullptr);
#endif
#if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
RegisterDumpProvider(AddressSpaceDumpProvider::GetInstance(),
"PartitionAlloc.AddressSpace", nullptr);
#endif
#if BUILDFLAG(IS_ANDROID)
if (base::android::IsJavaAvailable()) {
RegisterDumpProvider(JavaHeapDumpProvider::GetInstance(), "JavaHeap",
nullptr);
}
#endif
}
void MemoryDumpManager::RegisterDumpProvider(
MemoryDumpProvider* mdp,
MemoryDumpProvider::Name name,
scoped_refptr<SingleThreadTaskRunner> task_runner,
MemoryDumpProvider::Options options) {
options.dumps_on_single_thread_task_runner = true;
RegisterDumpProviderInternal(mdp, std::move(name), std::move(task_runner),
options);
}
void MemoryDumpManager::RegisterDumpProvider(
MemoryDumpProvider* mdp,
MemoryDumpProvider::Name name,
scoped_refptr<SingleThreadTaskRunner> task_runner) {
// Set |dumps_on_single_thread_task_runner| to true because all providers
// without task runner are run on dump thread.
MemoryDumpProvider::Options options;
options.dumps_on_single_thread_task_runner = true;
RegisterDumpProviderInternal(mdp, std::move(name), std::move(task_runner),
options);
}
void MemoryDumpManager::RegisterDumpProviderWithSequencedTaskRunner(
MemoryDumpProvider* mdp,
MemoryDumpProvider::Name name,
scoped_refptr<SequencedTaskRunner> task_runner,
MemoryDumpProvider::Options options) {
DCHECK(task_runner);
options.dumps_on_single_thread_task_runner = false;
RegisterDumpProviderInternal(mdp, std::move(name), std::move(task_runner),
options);
}
void MemoryDumpManager::RegisterDumpProviderInternal(
MemoryDumpProvider* mdp,
MemoryDumpProvider::Name name,
scoped_refptr<SequencedTaskRunner> task_runner,
const MemoryDumpProvider::Options& options) {
if (dumper_registrations_ignored_for_testing_) {
return;
}
// Only a handful of MDPs are required to compute the memory metrics. These
// have small enough performance overhead that it is reasonable to run them
// in the background while the user is doing other things. Those MDPs are
// 'allowed in background mode'.
bool allowed_in_background_mode =
IsMemoryDumpProviderInAllowlist(name.static_name().data());
auto mdpinfo = base::MakeRefCounted<MemoryDumpProviderInfo>(
mdp, std::move(name), std::move(task_runner), options,
allowed_in_background_mode);
{
AutoLock lock(lock_);
bool already_registered = !dump_providers_.insert(mdpinfo).second;
// This actually happens in some tests which don't have a clean tear-down
// path for RenderThreadImpl::Init().
if (already_registered) {
return;
}
}
}
void MemoryDumpManager::UnregisterDumpProvider(MemoryDumpProvider* mdp) {
UnregisterDumpProviderInternal(mdp, false /* delete_async */);
}
void MemoryDumpManager::UnregisterAndDeleteDumpProviderSoon(
std::unique_ptr<MemoryDumpProvider> mdp) {
UnregisterDumpProviderInternal(mdp.release(), true /* delete_async */);
}
void MemoryDumpManager::UnregisterDumpProviderInternal(
MemoryDumpProvider* mdp,
bool take_mdp_ownership_and_delete_async) {
std::unique_ptr<MemoryDumpProvider> owned_mdp;
if (take_mdp_ownership_and_delete_async) {
owned_mdp.reset(mdp);
}
AutoLock lock(lock_);
auto mdp_iter = dump_providers_.begin();
for (; mdp_iter != dump_providers_.end(); ++mdp_iter) {
if ((*mdp_iter)->dump_provider == mdp) {
break;
}
}
if (mdp_iter == dump_providers_.end()) {
return; // Not registered / already unregistered.
}
if (take_mdp_ownership_and_delete_async) {
// The MDP will be deleted whenever the MDPInfo struct will, that is either:
// - At the end of this function, if no dump is in progress.
// - In ContinueAsyncProcessDump() when MDPInfo is removed from
// |pending_dump_providers|.
DCHECK(!(*mdp_iter)->owned_dump_provider);
(*mdp_iter)->owned_dump_provider = std::move(owned_mdp);
} else {
// If you hit this DCHECK, your dump provider has a bug.
// Unregistration of a MemoryDumpProvider is safe only if:
// - The MDP has specified a sequenced task runner affinity AND the
// unregistration happens on the same task runner. So that the MDP cannot
// unregister and be in the middle of a OnMemoryDump() at the same time.
// - The MDP has NOT specified a task runner affinity and its ownership is
// transferred via UnregisterAndDeleteDumpProviderSoon().
// In all the other cases, it is not possible to guarantee that the
// unregistration will not race with OnMemoryDump() calls.
DCHECK((*mdp_iter)->task_runner &&
(*mdp_iter)->task_runner->RunsTasksInCurrentSequence())
<< "MemoryDumpProvider \"" << (*mdp_iter)->name.static_name()
<< "\" attempted to unregister itself in a racy way. Please file a "
"crbug.";
}
// The MDPInfo instance can still be referenced by the
// |ProcessMemoryDumpAsyncState.pending_dump_providers|. For this reason
// the MDPInfo is flagged as disabled. It will cause InvokeOnMemoryDump()
// to just skip it, without actually invoking the |mdp|, which might be
// destroyed by the caller soon after this method returns.
(*mdp_iter)->disabled = true;
dump_providers_.erase(mdp_iter);
}
bool MemoryDumpManager::IsDumpProviderRegisteredForTesting(
MemoryDumpProvider* provider) {
AutoLock lock(lock_);
for (const auto& info : dump_providers_) {
if (info->dump_provider == provider) {
return true;
}
}
return false;
}
void MemoryDumpManager::ResetForTesting() {
AutoLock lock(lock_);
request_dump_function_.Reset();
dump_providers_.clear();
}
scoped_refptr<SequencedTaskRunner>
MemoryDumpManager::GetDumpThreadTaskRunner() {
base::AutoLock lock(lock_);
return GetOrCreateBgTaskRunnerLocked();
}
scoped_refptr<base::SequencedTaskRunner>
MemoryDumpManager::GetOrCreateBgTaskRunnerLocked() {
if (dump_thread_) {
return dump_thread_->task_runner();
}
dump_thread_ = std::make_unique<Thread>("MemoryInfra");
bool started = dump_thread_->Start();
CHECK(started);
return dump_thread_->task_runner();
}
void MemoryDumpManager::CreateProcessDump(const MemoryDumpRequestArgs& args,
ProcessMemoryDumpCallback callback) {
char guid_str[20];
base::SpanPrintf(guid_str, "0x%" PRIx64, args.dump_guid);
TRACE_EVENT_BEGIN(kTraceCategory, "ProcessMemoryDump",
perfetto::Track(args.dump_guid), "dump_guid",
TRACE_STR_COPY(guid_str));
scoped_refptr<ProcessMemoryDumpAsyncState> pmd_async_state;
{
AutoLock lock(lock_);
pmd_async_state = base::MakeRefCounted<ProcessMemoryDumpAsyncState>(
args, dump_providers_, std::move(callback),
GetOrCreateBgTaskRunnerLocked());
}
// Start the process dump. This involves task runner hops as specified by the
// MemoryDumpProvider(s) in RegisterDumpProvider()).
ContinueAsyncProcessDump(std::move(pmd_async_state));
}
// Invokes OnMemoryDump() on all MDPs that are next in the pending list and run
// on the current sequenced task runner. If the next MDP does not run in current
// sequenced task runner, then switches to that task runner and continues. All
// OnMemoryDump() invocations are linearized. |lock_| is used in these functions
// purely to ensure consistency w.r.t. (un)registrations of |dump_providers_|.
void MemoryDumpManager::ContinueAsyncProcessDump(
scoped_refptr<ProcessMemoryDumpAsyncState> pmd_async_state) {
HEAP_PROFILER_SCOPED_IGNORE;
// In theory |pmd_async_state| should be a unique_ptr. The only reason why it
// isn't is because of the corner case logic of |did_post_task| below, which
// needs to take back the ownership of the |pmd_async_state| when the
// PostTask() fails.
// Unfortunately, PostTask() destroys its arguments upon failure to prevent
// accidental leaks, which would destroy a unique_ptr but only drops one
// reference to a scoped_refptr. Using a scoped_refptr instead of a unique_ptr
// allows us to retain a reference until we know the PostTask succeeded. If
// not we can skip the hop and move on.
auto get_effective_task_runner = [&](const MeasuredMemoryDumpProviderInfo&
mdpinfo) {
if (mdpinfo.provider_info()->task_runner) {
return mdpinfo.provider_info()->task_runner;
}
DCHECK(mdpinfo.provider_info()->options.dumps_on_single_thread_task_runner);
// If no TaskRunner affinity is specified, dump on `dump_thread_`.
return pmd_async_state->dump_thread_task_runner;
};
auto& pending_providers = pmd_async_state->pending_dump_providers;
if (base::FeatureList::IsEnabled(kMemoryDumpProviderGroupBySequence)) {
// Move providers which can run on this sequence to the back of the list.
std::ranges::stable_partition(
pending_providers, [&](const MeasuredMemoryDumpProviderInfo& mdpinfo) {
// Return true for providers which can't run on this
// sequence, to keep them at the front.
return !get_effective_task_runner(mdpinfo)
->RunsTasksInCurrentSequence();
});
}
// Refresh the number of following providers, following the above sort.
for (size_t i = 0; i < pending_providers.size(); ++i) {
pending_providers[i].set_num_following_providers(pending_providers.size() -
i - 1);
}
while (!pending_providers.empty()) {
// Read MemoryDumpProviderInfo thread safety considerations in
// memory_dump_manager.h when accessing `mdpinfo.provider_info()` fields.
MeasuredMemoryDumpProviderInfo& mdpinfo = pending_providers.back();
// If we are in background mode, we should invoke only the allowed
// providers. Ignore other providers and continue.
if (pmd_async_state->req_args.level_of_detail ==
MemoryDumpLevelOfDetail::kBackground &&
!mdpinfo.provider_info()->allowed_in_background_mode) {
mdpinfo.SetStatus(
MeasuredMemoryDumpProviderInfo::Status::kIgnoredInBackground);
// This deletes `mdpinfo` and logs final histograms.
pending_providers.pop_back();
continue;
}
const scoped_refptr<SequencedTaskRunner> task_runner =
get_effective_task_runner(mdpinfo);
// If |RunsTasksInCurrentSequence()| is true then no PostTask is
// required since we are on the right SequencedTaskRunner.
if (task_runner->RunsTasksInCurrentSequence()) {
// Transfers ownership of `mdpinfo`.
InvokeOnMemoryDump(std::move(mdpinfo),
pmd_async_state->process_memory_dump.get());
pending_providers.pop_back();
continue;
}
mdpinfo.SetStatus(MeasuredMemoryDumpProviderInfo::Status::kPosted);
bool did_post_task = task_runner->PostTask(
FROM_HERE, BindOnce(&MemoryDumpManager::ContinueAsyncProcessDump,
Unretained(this), pmd_async_state));
if (did_post_task) {
// Ownership is transferred to the posted task. Drop our reference to
// `pmd_async_state` when leaving scope.
return;
}
// PostTask usually fails only if the process or thread is shut down. So,
// the dump provider is disabled here. But, don't disable unbound dump
// providers, since the |dump_thread_| is controlled by MDM.
if (mdpinfo.provider_info()->task_runner) {
// A locked access is required to R/W |disabled| (for the
// UnregisterAndDeleteDumpProviderSoon() case).
AutoLock lock(lock_);
mdpinfo.provider_info()->disabled = true;
}
// PostTask failed. Ignore the dump provider and continue.
// This deletes `mdpinfo` and logs final timing histograms.
mdpinfo.SetStatus(MeasuredMemoryDumpProviderInfo::Status::kFailedToPost);
pending_providers.pop_back();
}
FinishAsyncProcessDump(std::move(pmd_async_state));
}
// This function is called on the right task runner for current MDP. It is
// either the task runner specified by MDP or |dump_thread_task_runner| if the
// MDP did not specify task runner. Invokes the dump provider's OnMemoryDump()
// (unless disabled).
void MemoryDumpManager::InvokeOnMemoryDump(
MeasuredMemoryDumpProviderInfo measured_mdpinfo,
ProcessMemoryDump* pmd) {
HEAP_PROFILER_SCOPED_IGNORE;
MemoryDumpProviderInfo* mdpinfo = measured_mdpinfo.provider_info();
DCHECK(!mdpinfo->task_runner ||
mdpinfo->task_runner->RunsTasksInCurrentSequence());
TRACE_EVENT(kTraceCategory, "MemoryDumpManager::InvokeOnMemoryDump",
[&](perfetto::EventContext ctx) {
ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>()
->set_memory_dump_provider()
->set_name(mdpinfo->name.histogram_name());
});
// Do not add any other TRACE_EVENT macro (or function that might have them)
// below this point. Under some rare circunstances, they can re-initialize
// and invalide the current ThreadLocalEventBuffer MDP, making the
// |should_dump| check below susceptible to TOCTTOU bugs
// (https://crbug.com/763365).
bool is_thread_bound;
{
// A locked access is required to R/W |disabled| (for the
// UnregisterAndDeleteDumpProviderSoon() case).
AutoLock lock(lock_);
// Unregister the dump provider if it failed too many times consecutively.
if (!mdpinfo->disabled &&
mdpinfo->consecutive_failures >= kMaxConsecutiveFailuresCount) {
mdpinfo->disabled = true;
DLOG(ERROR) << "Disabling MemoryDumpProvider \""
<< mdpinfo->name.static_name()
<< "\". Dump failed multiple times consecutively.";
}
if (mdpinfo->disabled) {
measured_mdpinfo.SetStatus(
MeasuredMemoryDumpProviderInfo::Status::kIgnoredDisabled);
return;
}
is_thread_bound = mdpinfo->task_runner != nullptr;
} // AutoLock lock(lock_);
// Invoke the dump provider.
// A stack allocated string with dump provider name is useful to debug
// crashes while invoking dump after a |dump_provider| is not unregistered
// in safe way.
DEBUG_ALIAS_FOR_CSTR(provider_name_for_debugging,
mdpinfo->name.static_name().data(), 16);
ABSL_ANNOTATE_BENIGN_RACE(&mdpinfo->disabled, "best-effort race detection");
CHECK(!is_thread_bound ||
!*(static_cast<volatile bool*>(&mdpinfo->disabled)));
base::ElapsedLiveTimer memory_dump_timer;
bool dump_successful =
mdpinfo->dump_provider->OnMemoryDump(pmd->dump_args(), pmd);
measured_mdpinfo.LogMemoryDumpTimeHistograms(memory_dump_timer.Elapsed());
mdpinfo->consecutive_failures =
dump_successful ? 0 : mdpinfo->consecutive_failures + 1;
measured_mdpinfo.SetStatus(
dump_successful ? MeasuredMemoryDumpProviderInfo::Status::kDumpSucceeded
: MeasuredMemoryDumpProviderInfo::Status::kDumpFailed);
// Log final histograms as `measured_mdpinfo` goes out of scope.
}
void MemoryDumpManager::FinishAsyncProcessDump(
scoped_refptr<ProcessMemoryDumpAsyncState> pmd_async_state) {
HEAP_PROFILER_SCOPED_IGNORE;
DCHECK(pmd_async_state->pending_dump_providers.empty());
const uint64_t dump_guid = pmd_async_state->req_args.dump_guid;
if (!pmd_async_state->callback_task_runner->BelongsToCurrentThread()) {
scoped_refptr<SingleThreadTaskRunner> callback_task_runner =
pmd_async_state->callback_task_runner;
callback_task_runner->PostTask(
FROM_HERE, BindOnce(&MemoryDumpManager::FinishAsyncProcessDump,
Unretained(this), std::move(pmd_async_state)));
return;
}
TRACE_EVENT0(kTraceCategory, "MemoryDumpManager::FinishAsyncProcessDump");
if (!pmd_async_state->callback.is_null()) {
std::move(pmd_async_state->callback)
.Run(ProcessMemoryDumpOutcome::kSuccess, dump_guid,
std::move(pmd_async_state->process_memory_dump));
}
TRACE_EVENT_END(kTraceCategory, /* ProcessMemoryDump */
perfetto::Track(dump_guid));
}
void MemoryDumpManager::SetupForTracing(
const TraceConfig::MemoryDumpConfig& memory_dump_config) {
AutoLock lock(lock_);
// At this point we must have the ability to request global dumps.
DCHECK(can_request_global_dumps());
MemoryDumpScheduler::Config periodic_config;
for (const auto& trigger : memory_dump_config.triggers) {
if (trigger.trigger_type == MemoryDumpType::kPeriodicInterval) {
if (periodic_config.triggers.empty()) {
periodic_config.callback =
BindRepeating(&DoGlobalDumpWithoutCallback, request_dump_function_,
MemoryDumpType::kPeriodicInterval);
}
periodic_config.triggers.push_back(
{trigger.level_of_detail, trigger.min_time_between_dumps_ms});
}
}
// Only coordinator process triggers periodic memory dumps.
if (is_coordinator_ && !periodic_config.triggers.empty()) {
MemoryDumpScheduler::GetInstance()->Start(periodic_config,
GetOrCreateBgTaskRunnerLocked());
}
}
void MemoryDumpManager::TeardownForTracing() {
// There might be a memory dump in progress while this happens. Therefore,
// ensure that the MDM state which depends on the tracing enabled / disabled
// state is always accessed by the dumping methods holding the |lock_|.
AutoLock lock(lock_);
MemoryDumpScheduler::GetInstance()->Stop();
}
MemoryDumpManager::ProcessMemoryDumpAsyncState::ProcessMemoryDumpAsyncState(
MemoryDumpRequestArgs req_args,
const MemoryDumpProviderInfo::OrderedSet& dump_providers,
ProcessMemoryDumpCallback callback,
scoped_refptr<SequencedTaskRunner> dump_thread_task_runner)
: req_args(req_args),
callback(std::move(callback)),
callback_task_runner(SingleThreadTaskRunner::GetCurrentDefault()),
dump_thread_task_runner(std::move(dump_thread_task_runner)) {
// `pending_dump_providers_` is a LIFO list, and `dump_providers` is sorted by
// priority, so add the highest-priority providers last.
pending_dump_providers.reserve(dump_providers.size());
absl::flat_hash_map<std::string, size_t> provider_counts;
for (scoped_refptr<MemoryDumpProviderInfo> provider :
base::Reversed(dump_providers)) {
++provider_counts[provider->name.histogram_name()];
pending_dump_providers.emplace_back(std::move(provider), req_args);
}
MemoryDumpArgs args = {req_args.level_of_detail, req_args.determinism,
req_args.dump_guid};
process_memory_dump = std::make_unique<ProcessMemoryDump>(args);
// Log the count of objects for each provider type.
for (const auto& [provider_name, count] : provider_counts) {
MeasuredMemoryDumpProviderInfo::LogProviderCountHistograms(
provider_name, req_args.level_of_detail, count);
}
base::UmaHistogramEnumeration(
base::StrCat({"Memory.Dump.Type.",
MeasuredMemoryDumpProviderInfo::LevelOfDetailString(
req_args.level_of_detail)}),
req_args.dump_type);
base::UmaHistogramEnumeration("Memory.Dump.Type.AllDetailLevels",
req_args.dump_type);
}
MemoryDumpManager::ProcessMemoryDumpAsyncState::~ProcessMemoryDumpAsyncState() =
default;
} // namespace base::trace_event