1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/trace_event/process_memory_dump.h"
6
7 #include <errno.h>
8
9 #include <vector>
10
11 #include "base/memory/ptr_util.h"
12 #include "base/memory/shared_memory_tracker.h"
13 #include "base/process/process_metrics.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/trace_event/memory_infra_background_whitelist.h"
16 #include "base/trace_event/trace_event_argument.h"
17 #include "base/unguessable_token.h"
18 #include "build/build_config.h"
19
20 #if defined(OS_IOS)
21 #include <mach/vm_page_size.h>
22 #endif
23
24 #if defined(OS_POSIX) || defined(OS_FUCHSIA)
25 #include <sys/mman.h>
26 #endif
27
28 #if defined(OS_WIN)
29 #include <windows.h> // Must be in front of other Windows header files
30
31 #include <Psapi.h>
32 #endif
33
34 namespace base {
35 namespace trace_event {
36
37 namespace {
38
39 const char kEdgeTypeOwnership[] = "ownership";
40
GetSharedGlobalAllocatorDumpName(const MemoryAllocatorDumpGuid & guid)41 std::string GetSharedGlobalAllocatorDumpName(
42 const MemoryAllocatorDumpGuid& guid) {
43 return "global/" + guid.ToString();
44 }
45
46 #if defined(COUNT_RESIDENT_BYTES_SUPPORTED)
GetSystemPageCount(size_t mapped_size,size_t page_size)47 size_t GetSystemPageCount(size_t mapped_size, size_t page_size) {
48 return (mapped_size + page_size - 1) / page_size;
49 }
50 #endif
51
GetTokenForCurrentProcess()52 UnguessableToken GetTokenForCurrentProcess() {
53 static UnguessableToken instance = UnguessableToken::Create();
54 return instance;
55 }
56
57 } // namespace
58
59 // static
60 bool ProcessMemoryDump::is_black_hole_non_fatal_for_testing_ = false;
61
62 #if defined(COUNT_RESIDENT_BYTES_SUPPORTED)
63 // static
GetSystemPageSize()64 size_t ProcessMemoryDump::GetSystemPageSize() {
65 #if defined(OS_IOS)
66 // On iOS, getpagesize() returns the user page sizes, but for allocating
67 // arrays for mincore(), kernel page sizes is needed. Use vm_kernel_page_size
68 // as recommended by Apple, https://forums.developer.apple.com/thread/47532/.
69 // Refer to http://crbug.com/542671 and Apple rdar://23651782
70 return vm_kernel_page_size;
71 #else
72 return base::GetPageSize();
73 #endif // defined(OS_IOS)
74 }
75
76 // static
CountResidentBytes(void * start_address,size_t mapped_size)77 size_t ProcessMemoryDump::CountResidentBytes(void* start_address,
78 size_t mapped_size) {
79 const size_t page_size = GetSystemPageSize();
80 const uintptr_t start_pointer = reinterpret_cast<uintptr_t>(start_address);
81 DCHECK_EQ(0u, start_pointer % page_size);
82
83 size_t offset = 0;
84 size_t total_resident_pages = 0;
85 bool failure = false;
86
87 // An array as large as number of pages in memory segment needs to be passed
88 // to the query function. To avoid allocating a large array, the given block
89 // of memory is split into chunks of size |kMaxChunkSize|.
90 const size_t kMaxChunkSize = 8 * 1024 * 1024;
91 size_t max_vec_size =
92 GetSystemPageCount(std::min(mapped_size, kMaxChunkSize), page_size);
93 #if defined(OS_WIN)
94 std::unique_ptr<PSAPI_WORKING_SET_EX_INFORMATION[]> vec(
95 new PSAPI_WORKING_SET_EX_INFORMATION[max_vec_size]);
96 #elif defined(OS_MACOSX)
97 std::unique_ptr<char[]> vec(new char[max_vec_size]);
98 #elif defined(OS_POSIX) || defined(OS_FUCHSIA)
99 std::unique_ptr<unsigned char[]> vec(new unsigned char[max_vec_size]);
100 #endif
101
102 while (offset < mapped_size) {
103 uintptr_t chunk_start = (start_pointer + offset);
104 const size_t chunk_size = std::min(mapped_size - offset, kMaxChunkSize);
105 const size_t page_count = GetSystemPageCount(chunk_size, page_size);
106 size_t resident_page_count = 0;
107 #if defined(OS_WIN)
108 for (size_t i = 0; i < page_count; i++) {
109 vec[i].VirtualAddress =
110 reinterpret_cast<void*>(chunk_start + i * page_size);
111 }
112 DWORD vec_size = static_cast<DWORD>(
113 page_count * sizeof(PSAPI_WORKING_SET_EX_INFORMATION));
114 failure = !QueryWorkingSetEx(GetCurrentProcess(), vec.get(), vec_size);
115
116 for (size_t i = 0; i < page_count; i++)
117 resident_page_count += vec[i].VirtualAttributes.Valid;
118 #elif defined(OS_FUCHSIA)
119 // TODO(fuchsia): Port, see https://crbug.com/706592.
120 ALLOW_UNUSED_LOCAL(chunk_start);
121 ALLOW_UNUSED_LOCAL(page_count);
122 #elif defined(OS_MACOSX)
123 // mincore in MAC does not fail with EAGAIN.
124 failure =
125 !!mincore(reinterpret_cast<void*>(chunk_start), chunk_size, vec.get());
126 for (size_t i = 0; i < page_count; i++)
127 resident_page_count += vec[i] & MINCORE_INCORE ? 1 : 0;
128 #elif defined(OS_POSIX)
129 int error_counter = 0;
130 int result = 0;
131 // HANDLE_EINTR tries for 100 times. So following the same pattern.
132 do {
133 result =
134 #if defined(OS_AIX)
135 mincore(reinterpret_cast<char*>(chunk_start), chunk_size,
136 reinterpret_cast<char*>(vec.get()));
137 #else
138 mincore(reinterpret_cast<void*>(chunk_start), chunk_size, vec.get());
139 #endif
140 } while (result == -1 && errno == EAGAIN && error_counter++ < 100);
141 failure = !!result;
142
143 for (size_t i = 0; i < page_count; i++)
144 resident_page_count += vec[i] & 1;
145 #endif
146
147 if (failure)
148 break;
149
150 total_resident_pages += resident_page_count * page_size;
151 offset += kMaxChunkSize;
152 }
153
154 DCHECK(!failure);
155 if (failure) {
156 total_resident_pages = 0;
157 LOG(ERROR) << "CountResidentBytes failed. The resident size is invalid";
158 }
159 return total_resident_pages;
160 }
161
162 // static
CountResidentBytesInSharedMemory(void * start_address,size_t mapped_size)163 base::Optional<size_t> ProcessMemoryDump::CountResidentBytesInSharedMemory(
164 void* start_address,
165 size_t mapped_size) {
166 #if defined(OS_MACOSX) && !defined(OS_IOS)
167 // On macOS, use mach_vm_region instead of mincore for performance
168 // (crbug.com/742042).
169 mach_vm_size_t dummy_size = 0;
170 mach_vm_address_t address =
171 reinterpret_cast<mach_vm_address_t>(start_address);
172 vm_region_top_info_data_t info;
173 MachVMRegionResult result =
174 GetTopInfo(mach_task_self(), &dummy_size, &address, &info);
175 if (result == MachVMRegionResult::Error) {
176 LOG(ERROR) << "CountResidentBytesInSharedMemory failed. The resident size "
177 "is invalid";
178 return base::Optional<size_t>();
179 }
180
181 size_t resident_pages =
182 info.private_pages_resident + info.shared_pages_resident;
183
184 // On macOS, measurements for private memory footprint overcount by
185 // faulted pages in anonymous shared memory. To discount for this, we touch
186 // all the resident pages in anonymous shared memory here, thus making them
187 // faulted as well. This relies on two assumptions:
188 //
189 // 1) Consumers use shared memory from front to back. Thus, if there are
190 // (N) resident pages, those pages represent the first N * PAGE_SIZE bytes in
191 // the shared memory region.
192 //
193 // 2) This logic is run shortly before the logic that calculates
194 // phys_footprint, thus ensuring that the discrepancy between faulted and
195 // resident pages is minimal.
196 //
197 // The performance penalty is expected to be small.
198 //
199 // * Most of the time, we expect the pages to already be resident and faulted,
200 // thus incurring a cache penalty read hit [since we read from each resident
201 // page].
202 //
203 // * Rarely, we expect the pages to be resident but not faulted, resulting in
204 // soft faults + cache penalty.
205 //
206 // * If assumption (1) is invalid, this will potentially fault some
207 // previously non-resident pages, thus increasing memory usage, without fixing
208 // the accounting.
209 //
210 // Sanity check in case the mapped size is less than the total size of the
211 // region.
212 size_t pages_to_fault =
213 std::min(resident_pages, (mapped_size + PAGE_SIZE - 1) / PAGE_SIZE);
214
215 volatile char* base_address = static_cast<char*>(start_address);
216 for (size_t i = 0; i < pages_to_fault; ++i) {
217 // Reading from a volatile is a visible side-effect for the purposes of
218 // optimization. This guarantees that the optimizer will not kill this line.
219 base_address[i * PAGE_SIZE];
220 }
221
222 return resident_pages * PAGE_SIZE;
223 #else
224 return CountResidentBytes(start_address, mapped_size);
225 #endif // defined(OS_MACOSX) && !defined(OS_IOS)
226 }
227
228 #endif // defined(COUNT_RESIDENT_BYTES_SUPPORTED)
229
ProcessMemoryDump(const MemoryDumpArgs & dump_args)230 ProcessMemoryDump::ProcessMemoryDump(
231 const MemoryDumpArgs& dump_args)
232 : process_token_(GetTokenForCurrentProcess()),
233 dump_args_(dump_args) {}
234
235 ProcessMemoryDump::~ProcessMemoryDump() = default;
236 ProcessMemoryDump::ProcessMemoryDump(ProcessMemoryDump&& other) = default;
237 ProcessMemoryDump& ProcessMemoryDump::operator=(ProcessMemoryDump&& other) =
238 default;
239
CreateAllocatorDump(const std::string & absolute_name)240 MemoryAllocatorDump* ProcessMemoryDump::CreateAllocatorDump(
241 const std::string& absolute_name) {
242 return AddAllocatorDumpInternal(std::make_unique<MemoryAllocatorDump>(
243 absolute_name, dump_args_.level_of_detail, GetDumpId(absolute_name)));
244 }
245
CreateAllocatorDump(const std::string & absolute_name,const MemoryAllocatorDumpGuid & guid)246 MemoryAllocatorDump* ProcessMemoryDump::CreateAllocatorDump(
247 const std::string& absolute_name,
248 const MemoryAllocatorDumpGuid& guid) {
249 return AddAllocatorDumpInternal(std::make_unique<MemoryAllocatorDump>(
250 absolute_name, dump_args_.level_of_detail, guid));
251 }
252
AddAllocatorDumpInternal(std::unique_ptr<MemoryAllocatorDump> mad)253 MemoryAllocatorDump* ProcessMemoryDump::AddAllocatorDumpInternal(
254 std::unique_ptr<MemoryAllocatorDump> mad) {
255 // In background mode return the black hole dump, if invalid dump name is
256 // given.
257 if (dump_args_.level_of_detail == MemoryDumpLevelOfDetail::BACKGROUND &&
258 !IsMemoryAllocatorDumpNameWhitelisted(mad->absolute_name())) {
259 return GetBlackHoleMad();
260 }
261
262 auto insertion_result = allocator_dumps_.insert(
263 std::make_pair(mad->absolute_name(), std::move(mad)));
264 MemoryAllocatorDump* inserted_mad = insertion_result.first->second.get();
265 DCHECK(insertion_result.second) << "Duplicate name: "
266 << inserted_mad->absolute_name();
267 return inserted_mad;
268 }
269
GetAllocatorDump(const std::string & absolute_name) const270 MemoryAllocatorDump* ProcessMemoryDump::GetAllocatorDump(
271 const std::string& absolute_name) const {
272 auto it = allocator_dumps_.find(absolute_name);
273 if (it != allocator_dumps_.end())
274 return it->second.get();
275 return nullptr;
276 }
277
GetOrCreateAllocatorDump(const std::string & absolute_name)278 MemoryAllocatorDump* ProcessMemoryDump::GetOrCreateAllocatorDump(
279 const std::string& absolute_name) {
280 MemoryAllocatorDump* mad = GetAllocatorDump(absolute_name);
281 return mad ? mad : CreateAllocatorDump(absolute_name);
282 }
283
CreateSharedGlobalAllocatorDump(const MemoryAllocatorDumpGuid & guid)284 MemoryAllocatorDump* ProcessMemoryDump::CreateSharedGlobalAllocatorDump(
285 const MemoryAllocatorDumpGuid& guid) {
286 // A shared allocator dump can be shared within a process and the guid could
287 // have been created already.
288 MemoryAllocatorDump* mad = GetSharedGlobalAllocatorDump(guid);
289 if (mad && mad != black_hole_mad_.get()) {
290 // The weak flag is cleared because this method should create a non-weak
291 // dump.
292 mad->clear_flags(MemoryAllocatorDump::Flags::WEAK);
293 return mad;
294 }
295 return CreateAllocatorDump(GetSharedGlobalAllocatorDumpName(guid), guid);
296 }
297
CreateWeakSharedGlobalAllocatorDump(const MemoryAllocatorDumpGuid & guid)298 MemoryAllocatorDump* ProcessMemoryDump::CreateWeakSharedGlobalAllocatorDump(
299 const MemoryAllocatorDumpGuid& guid) {
300 MemoryAllocatorDump* mad = GetSharedGlobalAllocatorDump(guid);
301 if (mad && mad != black_hole_mad_.get())
302 return mad;
303 mad = CreateAllocatorDump(GetSharedGlobalAllocatorDumpName(guid), guid);
304 mad->set_flags(MemoryAllocatorDump::Flags::WEAK);
305 return mad;
306 }
307
GetSharedGlobalAllocatorDump(const MemoryAllocatorDumpGuid & guid) const308 MemoryAllocatorDump* ProcessMemoryDump::GetSharedGlobalAllocatorDump(
309 const MemoryAllocatorDumpGuid& guid) const {
310 return GetAllocatorDump(GetSharedGlobalAllocatorDumpName(guid));
311 }
312
DumpHeapUsage(const std::unordered_map<base::trace_event::AllocationContext,base::trace_event::AllocationMetrics> & metrics_by_context,base::trace_event::TraceEventMemoryOverhead & overhead,const char * allocator_name)313 void ProcessMemoryDump::DumpHeapUsage(
314 const std::unordered_map<base::trace_event::AllocationContext,
315 base::trace_event::AllocationMetrics>&
316 metrics_by_context,
317 base::trace_event::TraceEventMemoryOverhead& overhead,
318 const char* allocator_name) {
319 std::string base_name = base::StringPrintf("tracing/heap_profiler_%s",
320 allocator_name);
321 overhead.DumpInto(base_name.c_str(), this);
322 }
323
SetAllocatorDumpsForSerialization(std::vector<std::unique_ptr<MemoryAllocatorDump>> dumps)324 void ProcessMemoryDump::SetAllocatorDumpsForSerialization(
325 std::vector<std::unique_ptr<MemoryAllocatorDump>> dumps) {
326 DCHECK(allocator_dumps_.empty());
327 for (std::unique_ptr<MemoryAllocatorDump>& dump : dumps)
328 AddAllocatorDumpInternal(std::move(dump));
329 }
330
331 std::vector<ProcessMemoryDump::MemoryAllocatorDumpEdge>
GetAllEdgesForSerialization() const332 ProcessMemoryDump::GetAllEdgesForSerialization() const {
333 std::vector<MemoryAllocatorDumpEdge> edges;
334 edges.reserve(allocator_dumps_edges_.size());
335 for (const auto& it : allocator_dumps_edges_)
336 edges.push_back(it.second);
337 return edges;
338 }
339
SetAllEdgesForSerialization(const std::vector<ProcessMemoryDump::MemoryAllocatorDumpEdge> & edges)340 void ProcessMemoryDump::SetAllEdgesForSerialization(
341 const std::vector<ProcessMemoryDump::MemoryAllocatorDumpEdge>& edges) {
342 DCHECK(allocator_dumps_edges_.empty());
343 for (const MemoryAllocatorDumpEdge& edge : edges) {
344 auto it_and_inserted = allocator_dumps_edges_.emplace(edge.source, edge);
345 DCHECK(it_and_inserted.second);
346 }
347 }
348
Clear()349 void ProcessMemoryDump::Clear() {
350 allocator_dumps_.clear();
351 allocator_dumps_edges_.clear();
352 }
353
TakeAllDumpsFrom(ProcessMemoryDump * other)354 void ProcessMemoryDump::TakeAllDumpsFrom(ProcessMemoryDump* other) {
355 // Moves the ownership of all MemoryAllocatorDump(s) contained in |other|
356 // into this ProcessMemoryDump, checking for duplicates.
357 for (auto& it : other->allocator_dumps_)
358 AddAllocatorDumpInternal(std::move(it.second));
359 other->allocator_dumps_.clear();
360
361 // Move all the edges.
362 allocator_dumps_edges_.insert(other->allocator_dumps_edges_.begin(),
363 other->allocator_dumps_edges_.end());
364 other->allocator_dumps_edges_.clear();
365 }
366
SerializeAllocatorDumpsInto(TracedValue * value) const367 void ProcessMemoryDump::SerializeAllocatorDumpsInto(TracedValue* value) const {
368 if (allocator_dumps_.size() > 0) {
369 value->BeginDictionary("allocators");
370 for (const auto& allocator_dump_it : allocator_dumps_)
371 allocator_dump_it.second->AsValueInto(value);
372 value->EndDictionary();
373 }
374
375 value->BeginArray("allocators_graph");
376 for (const auto& it : allocator_dumps_edges_) {
377 const MemoryAllocatorDumpEdge& edge = it.second;
378 value->BeginDictionary();
379 value->SetString("source", edge.source.ToString());
380 value->SetString("target", edge.target.ToString());
381 value->SetInteger("importance", edge.importance);
382 value->SetString("type", kEdgeTypeOwnership);
383 value->EndDictionary();
384 }
385 value->EndArray();
386 }
387
AddOwnershipEdge(const MemoryAllocatorDumpGuid & source,const MemoryAllocatorDumpGuid & target,int importance)388 void ProcessMemoryDump::AddOwnershipEdge(const MemoryAllocatorDumpGuid& source,
389 const MemoryAllocatorDumpGuid& target,
390 int importance) {
391 // This will either override an existing edge or create a new one.
392 auto it = allocator_dumps_edges_.find(source);
393 int max_importance = importance;
394 if (it != allocator_dumps_edges_.end()) {
395 DCHECK_EQ(target.ToUint64(), it->second.target.ToUint64());
396 max_importance = std::max(importance, it->second.importance);
397 }
398 allocator_dumps_edges_[source] = {source, target, max_importance,
399 false /* overridable */};
400 }
401
AddOwnershipEdge(const MemoryAllocatorDumpGuid & source,const MemoryAllocatorDumpGuid & target)402 void ProcessMemoryDump::AddOwnershipEdge(
403 const MemoryAllocatorDumpGuid& source,
404 const MemoryAllocatorDumpGuid& target) {
405 AddOwnershipEdge(source, target, 0 /* importance */);
406 }
407
AddOverridableOwnershipEdge(const MemoryAllocatorDumpGuid & source,const MemoryAllocatorDumpGuid & target,int importance)408 void ProcessMemoryDump::AddOverridableOwnershipEdge(
409 const MemoryAllocatorDumpGuid& source,
410 const MemoryAllocatorDumpGuid& target,
411 int importance) {
412 if (allocator_dumps_edges_.count(source) == 0) {
413 allocator_dumps_edges_[source] = {source, target, importance,
414 true /* overridable */};
415 } else {
416 // An edge between the source and target already exits. So, do nothing here
417 // since the new overridable edge is implicitly overridden by a strong edge
418 // which was created earlier.
419 DCHECK(!allocator_dumps_edges_[source].overridable);
420 }
421 }
422
CreateSharedMemoryOwnershipEdge(const MemoryAllocatorDumpGuid & client_local_dump_guid,const UnguessableToken & shared_memory_guid,int importance)423 void ProcessMemoryDump::CreateSharedMemoryOwnershipEdge(
424 const MemoryAllocatorDumpGuid& client_local_dump_guid,
425 const UnguessableToken& shared_memory_guid,
426 int importance) {
427 CreateSharedMemoryOwnershipEdgeInternal(client_local_dump_guid,
428 shared_memory_guid, importance,
429 false /*is_weak*/);
430 }
431
CreateWeakSharedMemoryOwnershipEdge(const MemoryAllocatorDumpGuid & client_local_dump_guid,const UnguessableToken & shared_memory_guid,int importance)432 void ProcessMemoryDump::CreateWeakSharedMemoryOwnershipEdge(
433 const MemoryAllocatorDumpGuid& client_local_dump_guid,
434 const UnguessableToken& shared_memory_guid,
435 int importance) {
436 CreateSharedMemoryOwnershipEdgeInternal(
437 client_local_dump_guid, shared_memory_guid, importance, true /*is_weak*/);
438 }
439
CreateSharedMemoryOwnershipEdgeInternal(const MemoryAllocatorDumpGuid & client_local_dump_guid,const UnguessableToken & shared_memory_guid,int importance,bool is_weak)440 void ProcessMemoryDump::CreateSharedMemoryOwnershipEdgeInternal(
441 const MemoryAllocatorDumpGuid& client_local_dump_guid,
442 const UnguessableToken& shared_memory_guid,
443 int importance,
444 bool is_weak) {
445 DCHECK(!shared_memory_guid.is_empty());
446 // New model where the global dumps created by SharedMemoryTracker are used
447 // for the clients.
448
449 // The guid of the local dump created by SharedMemoryTracker for the memory
450 // segment.
451 auto local_shm_guid =
452 GetDumpId(SharedMemoryTracker::GetDumpNameForTracing(shared_memory_guid));
453
454 // The dump guid of the global dump created by the tracker for the memory
455 // segment.
456 auto global_shm_guid =
457 SharedMemoryTracker::GetGlobalDumpIdForTracing(shared_memory_guid);
458
459 // Create an edge between local dump of the client and the local dump of the
460 // SharedMemoryTracker. Do not need to create the dumps here since the tracker
461 // would create them. The importance is also required here for the case of
462 // single process mode.
463 AddOwnershipEdge(client_local_dump_guid, local_shm_guid, importance);
464
465 // TODO(ssid): Handle the case of weak dumps here. This needs a new function
466 // GetOrCreaetGlobalDump() in PMD since we need to change the behavior of the
467 // created global dump.
468 // Create an edge that overrides the edge created by SharedMemoryTracker.
469 AddOwnershipEdge(local_shm_guid, global_shm_guid, importance);
470 }
471
AddSuballocation(const MemoryAllocatorDumpGuid & source,const std::string & target_node_name)472 void ProcessMemoryDump::AddSuballocation(const MemoryAllocatorDumpGuid& source,
473 const std::string& target_node_name) {
474 // Do not create new dumps for suballocations in background mode.
475 if (dump_args_.level_of_detail == MemoryDumpLevelOfDetail::BACKGROUND)
476 return;
477
478 std::string child_mad_name = target_node_name + "/__" + source.ToString();
479 MemoryAllocatorDump* target_child_mad = CreateAllocatorDump(child_mad_name);
480 AddOwnershipEdge(source, target_child_mad->guid());
481 }
482
GetBlackHoleMad()483 MemoryAllocatorDump* ProcessMemoryDump::GetBlackHoleMad() {
484 DCHECK(is_black_hole_non_fatal_for_testing_);
485 if (!black_hole_mad_) {
486 std::string name = "discarded";
487 black_hole_mad_.reset(new MemoryAllocatorDump(
488 name, dump_args_.level_of_detail, GetDumpId(name)));
489 }
490 return black_hole_mad_.get();
491 }
492
GetDumpId(const std::string & absolute_name)493 MemoryAllocatorDumpGuid ProcessMemoryDump::GetDumpId(
494 const std::string& absolute_name) {
495 return MemoryAllocatorDumpGuid(StringPrintf(
496 "%s:%s", process_token().ToString().c_str(), absolute_name.c_str()));
497 }
498
operator ==(const MemoryAllocatorDumpEdge & other) const499 bool ProcessMemoryDump::MemoryAllocatorDumpEdge::operator==(
500 const MemoryAllocatorDumpEdge& other) const {
501 return source == other.source && target == other.target &&
502 importance == other.importance && overridable == other.overridable;
503 }
504
operator !=(const MemoryAllocatorDumpEdge & other) const505 bool ProcessMemoryDump::MemoryAllocatorDumpEdge::operator!=(
506 const MemoryAllocatorDumpEdge& other) const {
507 return !(*this == other);
508 }
509
510 } // namespace trace_event
511 } // namespace base
512