Independent unwinding library in Perfetto without external dependency on android/libunwindstack

Authors: @safayat-google

Status: Draft

Problem

Currently, unwinding in Perfetto is based on the android-unwinding/libunwindstack library. This is a dependency that we want to remove for several reasons:

  • Changes to the libunwindstack API break our builds.
  • Extra memory overhead in libunwindstack's implementation does not benefit us.

Decision

Pending

Design

  • Start with the same architecture and memory layout as libunwindstack.
  • Use the same names for the types/classes; however, only keep the APIs that we are currently using.
  • Use the perfetto::profiling namespace for the new types/classes.
    • Note: Before the final migration, use the perfetto::profiling::unwinding namespace to avoid build breaks.
  • Optimize the memory usage of the unwinding implementation based on heapprofd and traced_perf usage patterns.

Folder Structure

repo-root/
└── src/
    └── profiling/
        └── common/
            └── unwinding/
                ├── Regs.h
                ├── Memory.h
                ├── Maps.h
                ├── Elf.h
                └── Unwinder.h

Splitting the changes into multiple PRs

Phase #1: Remove libunwindstack headers while keeping the logic as is

1.1 Implement Regs.h API
1.2 Implement Memory & Maps API
1.3 Implement Elf.h API
1.4 Implement the Unwinder API

Phase #2: Migrate usage to the new API

2.1 Migrate heapprofd unwinding usage
2.2 Migrate traced_perf unwinding usage

Phase #3: Refactor / optimize implementation based on our needs while optimizing memory usage (TBD)

3.1 Memory usage optimization
    - Remove any ELF-related memory overhead that we do not need.
3.2 Other feature or CPU/memory optimizations

Core APIs we currently use from libunwindstack

  • unwindstack/Unwinder.h
  struct FrameData { num, rel_pc, pc, sp, function_name, offset, map_info }
  ...

  class Unwinder {
    Unwinder(size_t max_frames, Maps* maps, Regs* regs, std::shared_ptr<Memory> process_memory);
    ...
    virtual void Unwind(initial_map_names_to_skip, map_suffixes_to_ignore);
    ...
    void SetJitDebug(JitDebug* jit_debug);
    void SetDexFiles(DexFiles* dex_files);
    ...
  }
  • unwindstack/Memory.h
// abstraction to read a block of memory from file/cache etc
class Memory {
  ...
  virtual size_t Read(addr, dst, size);

  // local unwinding
  CreateProcessMemoryCached(pid);
  // remote unwinding
  CreateProcessMemoryThreadCached(pid);
}
  • unwindstack/Maps.h
// Read /proc/[pid]/maps from an open file descriptor
unwindstack::Maps {
  std::vector<std::shared_ptr<MapInfo>> maps_;
}
  • unwindstack/Regs.h
// Core API to capture register snapshots inline
AsmGetRegs(void* regs);

// Different register mappings on different architectures
enum X86Reg : uint16_t {
  X86_REG_EAX = 0,
  ...
}

enum X86_64Reg, ArmReg, Arm64Reg
  • unwindstack/Elf.h
class Elf {
 public:
  Elf(std::shared_ptr<Memory>& memory);
  // we call it to reset the elf cache
  static void SetCachingEnabled(bool enable);
}
  • unwindstack/JitDebug.h and unwindstack/DexFiles.h
CreateJitDebug(arch, memory, search_libs = {});
CreateDexFiles(arch, memory, search_libs = {});

Alternatives considered

A wrapper around libunwindstack

  • An abstraction over libunwindstack that internally depends on libunwindstack.
  • The rest of our codebase calls this abstraction instead of libunwindstack directly.

Pro:

  • We still benefit from libunwindstack's active development.
  • If an API changes in libunwindstack, we only need to update our abstraction layer.

Con:

  • It won't fix the memory overhead of libunwindstack.
  • We still need to deal with build breaks on upstream API changes from libunwindstack.

Write everything from scratch

Pro:

  • We can optimize and apply best practices from the start.

Con:

  • It will require more design, review, and implementation effort.
  • Higher risk of introducing regressions in production or requiring longer timelines with complex feature flag maintenance.