btrace

repository·master·Indexed 25 days ago

https://github.com/bytedance/btrace

A command-line tool and SDK for recording trace data and generating flame graphs for performance analysis. It provides platform-specific implementations for HarmonyOS (via harmony-trace-cli and @bytedance/btrace SDK), Android (via rhea-trace-shell.jar and RheaTrace3 SDK), and iOS.

Tokens
23K
Snippets
48
Records
126
Agent score
81%

What's inside btrace

  1. How btrace-harmony implements tracing on HarmonyOS

    master

    btrace-harmony provides a high-performance tracing tool for HarmonyOS that supports both ArkTS and Native mixed-language stacks. It uses a dual-approach engine:

    1. Synchronous Tracing: Uses the official HiDebug_Backtrace_Object API to unwind mixed ArkTS and Native frames in a single pass using the frame pointer (fp).
    2. Asynchronous Tracing: Acts as a fallback for when threads are stuck in long-running functions or slow system calls. It uses a timer to periodically send real-time signals (tgkill) to target threads to trigger stack sampling.

    To prevent signal interference with slow system calls (like epoll_wait, recv, or nanosleep), btrace-harmony uses a SlowSysCallProxy to transparently hook these calls, masking sampling signals before entry and restoring them upon exit.

  2. Avoid deadlocks during asynchronous trace collection

    master

    Asynchronous trace collection involves pausing a thread to capture its state. This can lead to deadlocks if the sampling thread attempts to acquire a lock already held by the paused thread (e.g., during malloc calls).

    To prevent deadlocks, avoid calling the following 'dangerous' APIs within the sampling thread while a target thread is suspended:

    • Objective-C methods: Due to potential internal locks during dynamic dispatch.
    • Text printing: printf family, NSLog, etc.
    • Heap memory allocation: malloc, etc.
    • pthreads APIs
  3. How btrace 3.0 tracing works

    master

    btrace 3.0 uses a hybrid approach combining Dynamic Instrumentation and Synchronous Backtracing to overcome the limitations of traditional code instrumentation (high overhead, increased app size) and pure sampling backtracing (low precision, scheduling uncertainty).

    The Hybrid Model

    1. Dynamic Instrumentation: High-frequency "leaf node" methods (e.g., function endpoints, memory allocations, or blocking operations) are instrumented with trace points. These points act as triggers.
    2. Synchronous Backtracing: When an instrumentation trigger is hit, the system performs backtracing directly on the target thread. This eliminates the overhead of thread suspension and resume operations required by asynchronous sampling.

    Platform Differences

    • Android: Uses dynamic instrumentation via ShadowHook to hook high-frequency methods (Memory Allocation, MonitorEnter, Object.wait, Unsafe.park, GC) which then trigger synchronous backtracing using ART's StackVisitor.
    • iOS: Combines Synchronous Backtracing (hooking high-frequency methods identified via dtruss) with Asynchronous Backtracing (periodic sampling of all threads) to ensure data continuity and richness.
  4. Analyze Thread Blocking and Lock Contention

    master

    btrace can identify why the main thread is blocked by hooking monitor functions. When a thread releases a lock that the main thread is currently waiting for, btrace forcefully captures the stack at the moment of release and correlates the two threads.

    This allows you to see messages like Woken up by thread ID <ID> in your trace, making it easy to locate the specific code in the releasing thread that caused the wakeup.

    static void *currentMainMonitor = nullptr;
    static uint64_t currentMainNano = 0;
    
    void *Monitor_MonitorEnter(void *self, void *obj, bool trylock) {
        SHADOWHOOK_STACK_SCOPE();
        if (rheatrace::isMainThread()) {
            rheatrace::ScopeSampling a(rheatrace::SamplingType::kMonitor, self);
            currentMainMonitor = obj; // 记录当前阻塞的锁
            currentMainNano = a.beginNano_;
            void *result = SHADOWHOOK_CALL_PREV(Monitor_MonitorEnter, self, obj, trylock);
            currentMainMonitor = nullptr; // 锁已经拿到,这里重置
            return result;
        }
        ...
    }
    
    bool Monitor_MonitorExit(void *self, void *obj) {
        SHADOWHOOK_STACK_SCOPE();
        if (!rheatrace::isMainThread()) {
            if (currentMainMonitor == obj) { // 当前释放的锁正式主线程等待的锁
               rheatrace::SamplingCollector::request(rheatrace::SamplingType::kUnlock, self, true, true, currentMainNano); // 强制抓栈,并通过 currentMainNano 和主线程建立联系
                ALOGX("Monitor_MonitorExit wakeup main lock %ld", currentMainNano);
            }
        }
        return SHADOWHOOK_CALL_PREV(Monitor_MonitorExit, self, obj);
    }
  5. How trace data is visualized using Perfetto logic

    master

    btrace uses a logic similar to Android's Debug.startMethodTracingSampling to visualize data via Perfetto. The core algorithm compares consecutive captured stacks to determine function execution time:

    1. Compare the current stack with the previous stack from top to bottom.
    2. Find the first differing function.
    3. Pop all functions from the previous stack after the differing point.
    4. Push all functions from the current stack from the differing point onwards.
    5. The time interval between the entry and exit of these functions represents their execution duration.

    Note on Sampling Artifacts: Because this is a sampling-based approach, identical stacks might appear to belong to different executions. A mitigation strategy for message-based scenarios is to include a unique message ID (e.g., incrementing after nativePollOnce) in the stack trace to distinguish between different execution cycles.

    // Generate a virtual Root node
    CallNode root = CallNode.makeRoot();
    Stack<CallNode> stack = new Stack<>();
    stack.push(root);
    
    for (int i = 0; i < stackList.size(); i++) {
        StackItem curStackItem = stackList.get(i);
        nanoTime = curStackItem.nanoTime;
        if (i == 0) {
            for (String name : curStackItem.stackTrace) {
                stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek()));
            }
        } else {
            StackItem preStackItem = stackList.get(i - 1);
            int preIndex = 0;
            int curIndex = 0;
            while (preIndex < preStackItem.size() && curIndex < curStackItem.size()) {
                if (preStackItem.getPtr(preIndex) != curStackItem.getPtr(curIndex)) {
                    break;
                }
                preIndex++;
                curIndex++;
            }
            for (; preIndex < preStackItem.size(); preIndex++) {
                stack.pop().end(nanoTime);
            }
            for (; curIndex < curStackItem.size(); curIndex++) {
                String name = curStackItem.get(curIndex);
                stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek()));
            }
        }
    }
    while (!stack.isEmpty()) {
        stack.pop().end(nanoTime);
    }
  6. Optimize iOS trace storage using CallstackTable

    master

    BTrace optimizes trace data storage by leveraging the spatial and temporal similarity of call stacks:

    1. Spatial Similarity (CallstackTable): Instead of storing full stacks, it uses a CallstackTable to store unique nodes. Each node consists of a parent pointer (address of the caller) and the current method's address. This allows multiple stacks to share common upper-level methods.
    2. Temporal Similarity: If a call stack remains identical across multiple sampling intervals, BTrace merges these records, storing only the start and end timestamps rather than every individual sample.
    class CallstackTable
    {
    public:
        struct Node
        {
            uint64_t parent;
            uint64_t address;
        };
        
        struct NodeHash {
            size_t operator()(const Node* node) const {
                size_t h = std::hash<uint64_t>{}(node->parent);
                h ^= std::hash<uint64_t>{}(node->address);
                return h;
            }
        };
        
        struct NodeEqual
        {
            bool operator()(const Node* node1, const Node* node2) const noexcept
            {
                bool result = (node1->parent == node2->parent) && (node1->address == node2->address);
                return result;
            }
        };
    
        using CallStackSet = hash_set<Node *, NodeHash, NodeEqual>;
    private:
        CallStackSet stack_set_;
    };
  7. Android Implementation: Efficient Backtracing

    master

    To avoid the performance penalty of Android's native Thread.getStackTrace() (which parses method symbols during the trace), btrace 3.0 optimizes the process by:

    1. Storing only method pointers during the actual backtracing phase.
    2. Batch-symbolizing those pointers later offline.
    3. Using ART's StackVisitor for backtracing, utilizing a mSpaceHolder buffer to ensure version compatibility and avoid hardcoding memory layouts.
    class StackVisitor {
    ...
        [[maybe_unused]] virtual bool VisitFrame();
        
        // preserve for real StackVisitor's fields space
        [[maybe_unused]] char mSpaceHolder[2048]; 
    ...
    };
  8. Android Trace Collection: Dynamic Instrumentation

    master

    BTrace uses dynamic instrumentation to decide when to capture stacks. It utilizes ShadowHook to hook system methods and insert tracing logic.

    Tracing is triggered at two main types of 'leaf node' events:

    1. High-Frequency Execution (Active Sampling)

    Captures the stack immediately when a high-frequency event occurs.

    • Java Object Allocation: BTrace implements a real-time AllocationListener to monitor memory allocation without the ANR risks associated with the standard JVM Heap listener.
    • Sampling Control: To prevent performance degradation from excessive allocation-triggered traces, BTrace uses a frequency control mechanism (sampling interval) to ensure traces only occur if a certain time threshold (threadCaptureInterval) has passed since the last trace.
    • Other triggers: JNI method calls, etc.

    2. Blocking Execution (Duration Sampling)

    Captures both the stack and the duration of a blocking event. This is useful for identifying lock contention or waiting states.

    • Mechanism: A ScopeSampling object is used to record the start time in the constructor and trigger the stack capture and duration calculation in the destructor.
    • Examples: MonitorEnter (lock acquisition), Object.wait, Unsafe.park, and GC events.
  9. How the HarmonyOS backtracing engine works

    master

    The HarmonyOS implementation of btrace uses a dual-strategy backtracing engine to ensure high performance and coverage across the ArkTS/Native dual-language stack:

    1. Synchronous Backtracing: Uses the official HiDebug_Backtrace_Object to perform unified stack unwinding. It can unwind both ArkTS and Native frames simultaneously using a single frame pointer (fp), providing end-to-end visibility from application to system layers.
    2. Asynchronous Backtracing: Acts as a fallback for "sampling gaps" (e.g., when a thread is blocked on a slow system call or stays in one function too long). It uses a timer to send real-time signals (tgkill) to target threads, performing a backtrace inside the signal handler. It employs "sampling interval throttling + thread state filtering" to skip exited or yielding threads.
    3. Transparent Slow Syscall Proxy: To prevent asynchronous signals from interrupting signal-unsafe system calls (like epoll_wait, recv, or nanosleep) and causing EINTR errors, btrace-harmony uses a SlowSysCallProxy. This layer proactively blocks sampling signals before entering the call and restores the signal mask upon return, while performing synchronous backtraces at the entry and exit boundaries.
  10. iOS Implementation: Asynchronous Backtracing Safety

    master

    To ensure stability during periodic asynchronous sampling on iOS, btrace 3.0 implements several safety measures:

    • Deadlock Prevention: The sampler restricts calls to dangerous APIs such as Objective-C methods, malloc, and NSLog during the sampling process.
    • Active Thread Filtering: To reduce performance overhead, the system only samples threads that are not in an idle state.
    • Safe Backtracing: The engine uses vm_read_overwrite to handle potentially invalid pointers safely, while prioritizing direct memory reads for maximum performance.
  11. Android Trace Collection: Fast Stack Tracing

    master

    To achieve high-frequency trace collection on Android without the performance overhead of symbol resolution, BTrace uses a 'Fast Stack Tracing' approach. Instead of using Thread.getStackTrace() (which is slow due to symbol parsing), BTrace leverages the ART (Android Runtime) StackVisitor class to capture only method pointers.

    Key Workflow:

    1. Pointer Capture: During stack walking, only the raw method pointers are saved to a buffer.
    2. Deferred Symbolization: Method pointers are de-duplicated and converted into human-readable symbols offline during the data reporting phase. This minimizes the CPU cost during the actual tracing process.
    3. Implementation Detail: BTrace uses a custom StackVisitor implementation that uses a large mSpaceHolder buffer to ensure compatibility across different Android versions by avoiding assumptions about the internal memory layout of the StackVisitor object.
    class StackVisitor {
    ...
        [[maybe_unused]] virtual bool VisitFrame();
        
        // preserve for real StackVisitor's fields space
        [[maybe_unused]] char mSpaceHolder[2048]; 
    ...
    };
  12. iOS Implementation: Storage Optimization

    master

    To minimize the footprint of trace data on iOS, btrace 3.0 employs two optimization strategies:

    1. Spatial Locality (Unique Stack Nodes)

    Instead of storing full callstacks repeatedly, the system stores unique stack nodes in a CallstackTable. Each node consists of a parent pointer (the address of the previous node) and the current method's address. This eliminates duplicate entries for shared callstack segments.

    2. Temporal Locality (Record Merging)

    Adjacent records that share identical callstacks are merged. Instead of multiple individual records, the system stores only the start and end records for that specific stack sequence, significantly reducing storage requirements.

    class CallstackTable
    {
    public:
        struct Node
        {
            uint64_t parent;
            uint64_t address;
        };
        
        struct NodeHash {
            size_t operator()(const Node* node) const {
                size_t h = std::hash<uint64_t>{}(node->parent);
                h ^= std::hash<uint64_t>{}(node->address);
                return h;
            }
        };
        
        struct NodeEqual
        {
            bool operator()(const Node* node1, const Node* node2) const noexcept
            {
                bool result = (node1->parent == node2->parent) && (node1->address == node2->address);
                return result;
            }
        };
    
        using CallStackSet = hash_set<Node *, NodeHash, NodeEqual>;
    private:
        CallStackSet stack_set_;
    };