How can I parse the ID of a parent method that calls from another thread by using Thread.callStackSymbols?

49 views Asked by At

I am trying to implement a small analytics solution to trace calls throughout a code base in an automated manner.

Unfortunately, I have no idea about low-level OS stuff, and I do not even know how to google these things. Therefore, the following points are unclear to me:

  1. What do the columns in a stacktrace refer to?
    1. What is a memory load address?
    2. What is a return address?

I did figure out that the 0x1234 column at index 2 in a stacktrace is a unique identifier for the calling method, and column at index 3 is a unique identifier for its parent method. This way I can track child and parent methods inside the same Thread. (Sometimes this 0x1234 seems to be re-used, but this is alright because at that point I have already tracked the trace.)

  1. How do I track the parent method from within the child method, when it got called from another thread?
  2. How do I see in the stacktrace that a child method completed via a closure?

When debugging in Xcode, I see this information in the Xcode side panel, so it must be somehwere!

enter image description here

Other than that, I am lost. Please help?

I have also attached the code for this example here:

import Foundation

struct Dog {
    func bark() {
        print("CallStackSymbols for '\(#function)':")
        print("\t\(Thread.callStackSymbols.joined(separator: "\n\t"))")
        
        DispatchQueue.global(qos: .userInitiated).async {
            barkInBackground()
        }
    }
    
    func barkInBackground() {
        print("CallStackSymbols for '\(#function)':")
        print("\t\(Thread.callStackSymbols.joined(separator: "\n\t"))")
    }
}

let dog = Dog()
dog.bark()
sleep(1000)
2

There are 2 answers

0
ramnik On BEST ANSWER

After a lot of research, I found the answers to my questions myself.

--

I will omit the answers to the following questions because in light of new information, they have gotten irrelevant, and I do not feel competent enough to answer them satisfyingly:

  1. What do the columns in a stacktrace refer to?
    1. What is a memory load address?
    2. What is a return address?

--

Reframing the Question #1: "How to get a "parent" thread for NSThread in iOS?"

You can't. There is no such thing as parent thread. A thread is an independent entity, even if a thread can communicate with other threads but there is no hierarchy involved. (Source)

--

Reframing the Question #2: "How to track the logical trace of methods across multiple threads?

You have to pass an id into the closure of DispatchQueue, when a new task is given to a background queue (which might create a new thread behind the scenes).

(Clean method swizzling is not possible here because you would have to CHANGE the original implementation instead of ADDING something to it.)

Here is an example code that works (=> customers who want their trace to encompass multiple threads, have to use a custom wrapper (e.g. extension method) because iOS does not expose any interface to do this in an automated manner.)

let traceId = UUID().uuidString
DispatchQueue.global(qos: .userInitiated).async {
    Tracker.trackMethod(self, parentId: traceId)
    barkInBackground()
}
1
Jim Ingham On

libdispatch is the part of Darwin systems that handles queues and running blocks, etc. That is also the agent that the debugger queries for this historical "who dispatched what when" information. However, the installed dispatch runtime library: /usr/lib/system/libdispatch.dylib does not gather any of this information. That's because doing so (or really even adding in affordances TO do so) slows down execution, increases memory usage, and isn't needed by libdispatch to do its real jobs. dispatch is a very performance critical part of the system so the normal version of the library can't afford to do any more work than necessary.

Instead, there's a "introspection" version of libdispatch, which Xcode loads instead of the normal version when you have the "Queue Debugging" feature enabled. That version of libdispatch does gather this information and provides an API to access it, which lldb uses to generate these "History Threads". You can see that you are indeed using the variant library: if while stopped in Xcode you run the lldb command:

(lldb) image list libdispatch.dylib
[  0] 6C6BE4E9-B201-3B02-8E69-33DF61FF3A44 0x0000000102ad8000 /usr/lib/system/introspection/libdispatch.dylib 

That's the introspection version of libdispatch being used.

But an app running normally on Darwin systems shouldn't load this library, and so won't gather history information about queues and dispatches, nor provide the API to query this info. So if you want to track it yourself, you'll have to do that by hand as you were showing.