HowTo: Adding Memory Infra Tracing to a Component

Motivation

If you have a component that manages memory allocations, you should be registering and tracking those allocations with Chrome's memory-infra system. This lets you:

  • See an overview of your allocations, giving insight into total size and breakdown.
  • Understand how your allocations change over time and how they are impacted by other parts of Chrome.
  • Catch regressions in your component's allocations size by setting up telemetry tests which monitor your allocation sizes under certain circumstances.
Some existing components which make use of memory tracing infra:
  • Discardable Memory - Tracks usage of discardable memory throughout chrome.
  • GPU - tracks GL and other GPU object allocations.
  • V8 - tracks heap size for JS.
  • many more....

Overview

In order to hook into Chrome's memory infra system, your component needs to do two things:
  1. Create a base::trace_event::MemoryDumpProvider for your component.
  2. Register/Unregister your base::trace_event::MemoryDumpProvider with the base::trace_event::MemoryDumpManager.

Creating a MemoryDumpProvider

In order to tie into the memory infra system, you must write a base::trace_event::MemoryDumpProvider for your component. You can implement this as a stand-alone class, or as an additional interface on an existing class. For example, this interface is frequently implemented on classes which manage a pool of allocations (see cc::ResourcePool for an example).

A MemoryDumpProvider has one basic job, to implement MemoryDumpProvider::OnMemoryDump. This function is responsible for iterating over the resources allocated/tracked by your component, and creating a base::trace_event::MemoryAllocatorDump for each using ProcessMemoryDump::CreateAllocatorDump. Here is a simple example:

bool MyComponent::OnMemoryDump(
    const base::trace_event::MemoryDumpArgs& args,
    base::trace_event::ProcessMemoryDump* process_memory_dump) {
  for (const auto& allocation : my_allocations_) {
    auto* dump = process_memory_dump->CreateAllocatorDump(
        "path/to/my/component/allocation_" + allocation.id().ToString());
    dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
                    base::trace_event::MemoryAllocatorDump::kUnitsBytes,
                    allocation.size_bytes());
    // While you will typically have a kNameSize entry, you can add additional
    // entries to your dump with free-form names. In this example we also dump
    // an object's "free_size", assuming the object may not be entirely in use.
    dump->AddScalar("free_size",
                    base::trace_event::MemoryAllocatorDump::kUnitsBytes,
                    allocation.free_size_bytes());
  }
}

For many components, this may be all that is needed. See Handling Shared Memory Allocations and Sub Allocations for information on more complex use cases.

Registering/Unregistering a MemoryDumpProvider

Registration

Once you have created a base::trace_event::MemoryDumpProvider, you need to register it with the base::trace_event::MemoryDumpManager before the system can start polling it for memory information. Registration is generally straightforward, and involves calling MemoryDumpManager::RegisterDumpProvider:

...

  // Each process uses a singleton MemoryDumpManager
  base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
      my_memory_dump_provider_, my_single_thread_task_runner_);

...

In the above code, my_memory_dump_provider_ is the base::trace_event::MemoryDumpProvider outlined in the previous section. my_single_thread_task_runner_ is more complex and may be a number of things:
In all cases, if your base::trace_event::MemoryDumpProvider accesses data that may be used from multiple threads, you should make sure the proper locking is in place in your implementation of MemoryDumpProvider::OnMemoryDump.

Unregistration

Unregistering a base::trace_event::MemoryDumpProvider is very similar to registering one:

...

  // Each process uses a singleton MemoryDumpManager
  base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
      my_memory_dump_provider_);

...

The main complexity here is that unregistration must happen on the thread belonging to the base::SingleThreadTaskRunner provided at registration time. Unregistering on another thread can lead to race conditions if tracing is active when the provider is unregistered.

Handling Shared Memory Allocations

When an allocation is shared between two components, it may be useful to dump the allocation in both components, but you also want to avoid double-counting the allocation. This can be achieved using memory infra's concept of "ownership edges". An ownership edge represents that the "source" memory allocator dump owns a "target" memory allocator dump. If multiple "source" dumps own a single "target", then the cost of that "target" allocation will be split between the "source"s. Additionally, importance can be added to a specific ownership edge, allowing the highest importance "source" of that edge to claim the entire cost of the "target".

In the typical case, you will use base::trace_event::ProcessMemoryDump to create a shared global allocator dump. This dump will act as the target of all component-specific dumps of a specific resource:

...

  // Component 1 is going to create a dump, source_mad, for an allocation,
  // alloc_, which may be shared with other components / processes.
  MyAllocationType* alloc_;
  base::trace_event::MemoryAllocatorDump* source_mad;

  // Component 1 creates and populates 
source_mad;
  ...

  // In addition to creating a source dump, we must create a global shared
  // target dump. This dump should be created with a unique GUID which can be
  // generated any place the allocation is used. I recommend adding a GUID
  // generation function to the allocation type.
  base::trace_event::MemoryAllocatorDumpGUID guid(alloc_->GetGUIDString());
  
  // From this GUID we can generate the parent allocator dump.
  base::trace_event::MemoryAllocatorDump* target_mad =
      process_memory_dump->CreateSharedGlobalAllocatorDump(guid);

  // We now create an ownership edge from the source dump to the target dump.
  // When creating an edge, you can assign an importance to this edge. If all
  // edges have the same importance, the size of the allocation will be split
  // between all sources which create a dump for the allocation. If one
  // edge has higher importance than the others, its soruce will be assigned the
  // full size of the allocation.
  const int kImportance = 1;
  process_memory_dump->AddOwnershipEdge(
      source_mad->guid(), target_mad->guid(), kImportance);

...


If an allocation is being shared across process boundaries, it may be useful to generate a GUID which incorporates the ID of the local process, preventing two processes from generating colliding GUIDs. As it is not recommended to pass a Process ID between processes for security reasons, a function MemoryDumpManager::GetTracingProcessId is provided which generates a unique ID per process that can be passed with the resource without security concerns. Frequently this ID is used to generate a GUID that is based on the allocated resource's ID combined with the allocating process' tracing ID.

Sub Allocations

Another advanced use case involves tracking sub-allocations of a larger allocation. For instance, this is used in gpu::gles2::TextureManager to dump both the sub-allocations which make up a texture. Creating a sub allocation is easy - instead of calling ProcessMemoryDump::CreateAllocatorDump to create a base::trace_event::MemoryAllocatorDump, you simply call ProcessMemoryDump::AddSubAllocation, providing the GUID of the parent allocation as the first parameter.

Comments