diff --git a/ETTrace/CommunicationFrame/Public/CommunicationFrame.h b/ETTrace/CommunicationFrame/Public/CommunicationFrame.h index 17b438f..ffce851 100644 --- a/ETTrace/CommunicationFrame/Public/CommunicationFrame.h +++ b/ETTrace/CommunicationFrame/Public/CommunicationFrame.h @@ -32,6 +32,7 @@ enum { PTFrameTypeResultsMetadata = 105, PTFrameTypeResultsData = 106, PTFrameTypeResultsTransferComplete = 107, + PTFrameTypeStartMultiThread = 108, }; typedef struct _PTStartFrame { diff --git a/ETTrace/ETModels/Flamegraph.swift b/ETTrace/ETModels/Flamegraph.swift index eb0d901..26697f4 100644 --- a/ETTrace/ETModels/Flamegraph.swift +++ b/ETTrace/ETModels/Flamegraph.swift @@ -13,31 +13,31 @@ public class Flamegraph: NSObject { public let osBuild: String @objc - public let device: String? + public let device: String @objc public let isSimulator: Bool - @objc - public var nodes: FlameNode - @objc public var events: [FlamegraphEvent] @objc public var libraries: [String:UInt64] + @objc + public var threadNodes: [ThreadNode] + public init(osBuild: String, - device: String?, + device: String, isSimulator: Bool, - nodes: FlameNode, libraries: [String:UInt64], - events: [FlamegraphEvent]) { + events: [FlamegraphEvent], + threadNodes: [ThreadNode]) { self.osBuild = osBuild self.device = device self.isSimulator = isSimulator - self.nodes = nodes self.events = events self.libraries = libraries + self.threadNodes = threadNodes } } diff --git a/ETTrace/ETModels/ThreadNode.swift b/ETTrace/ETModels/ThreadNode.swift new file mode 100644 index 0000000..7ccef9e --- /dev/null +++ b/ETTrace/ETModels/ThreadNode.swift @@ -0,0 +1,23 @@ +// +// ThreadNode.swift +// +// +// Created by Itay Brenner on 18/8/23. +// + +import Foundation + +@objc +public class ThreadNode: NSObject { + @objc + public let threadName: String? + + @objc + public var nodes: FlameNode + + public init(nodes: FlameNode, + threadName: String? = nil) { + self.nodes = nodes + self.threadName = threadName + } +} diff --git a/ETTrace/ETTrace/EMGChannelListener.m b/ETTrace/ETTrace/EMGChannelListener.m index a6b2f13..4c904b3 100644 --- a/ETTrace/ETTrace/EMGChannelListener.m +++ b/ETTrace/ETTrace/EMGChannelListener.m @@ -43,7 +43,8 @@ - (BOOL)ioFrameChannel:(PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)typ if (channel != self.peerChannel) { // A previous channel that has been canceled but not yet ended. Ignore. return NO; - } else if (type == PTFrameTypeStart || type == PTFrameTypeStop || type == PTFrameTypeRequestResults){ + } else if (type == PTFrameTypeStart || type == PTFrameTypeStop || + type == PTFrameTypeRequestResults || type == PTFrameTypeStartMultiThread){ return YES; } else { NSLog(@"Unexpected frame of type %u", type); @@ -53,14 +54,15 @@ - (BOOL)ioFrameChannel:(PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)typ } - (void)ioFrameChannel:(PTChannel*)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(NSData *)payload { - if (type == PTFrameTypeStart) { + if (type == PTFrameTypeStart || type == PTFrameTypeStartMultiThread) { PTStartFrame *startFrame = (PTStartFrame *)payload.bytes; NSLog(@"Start received, with: %i", startFrame->runAtStartup); BOOL runAtStartup = startFrame->runAtStartup; + BOOL recordAllThreads = type == PTFrameTypeStartMultiThread; if (runAtStartup) { - [EMGPerfAnalysis setupRunAtStartup]; + [EMGPerfAnalysis setupRunAtStartup:recordAllThreads]; } else { - [EMGPerfAnalysis setupStackRecording]; + [EMGPerfAnalysis setupStackRecording:recordAllThreads]; } } else if (type == PTFrameTypeStop) { [EMGPerfAnalysis stopRecordingThread]; diff --git a/ETTrace/ETTrace/EMGPerfAnalysis.mm b/ETTrace/ETTrace/EMGPerfAnalysis.mm index 042351c..1fb6b34 100644 --- a/ETTrace/ETTrace/EMGPerfAnalysis.mm +++ b/ETTrace/ETTrace/EMGPerfAnalysis.mm @@ -17,6 +17,7 @@ #import "EMGChannelListener.h" #import #import "PerfAnalysis.h" +#include NSString *const kEMGSpanStarted = @"EmergeMetricStarted"; NSString *const kEMGSpanEnded = @"EmergeMetricEnded"; @@ -24,6 +25,8 @@ @implementation EMGPerfAnalysis static thread_t sMainMachThread = {0}; +static thread_t sETTraceThread = {0}; + static const int kMaxFramesPerStack = 512; static NSThread *sStackRecordingThread = nil; typedef struct { @@ -31,37 +34,107 @@ @implementation EMGPerfAnalysis uint64_t frameCount; uintptr_t frames[kMaxFramesPerStack]; } Stack; -static std::vector *sStacks; -static std::mutex sStacksLock; + +typedef struct { + std::vector *stacks; + char name[256]; +} Thread; +static std::map *sThreadsMap; +static std::mutex sThreadsLock; static dispatch_queue_t fileEventsQueue; static EMGChannelListener *channelListener; static NSMutableArray *sSpanTimes; +static BOOL sRecordAllThreads = false; extern "C" { void FIRCLSWriteThreadStack(thread_t thread, uintptr_t *frames, uint64_t framesCapacity, uint64_t *framesWritten); } -+ (void)recordStack ++ (Thread *) createThread:(thread_t) threadId { - Stack stack; - thread_suspend(sMainMachThread); - stack.time = CACurrentMediaTime(); - FIRCLSWriteThreadStack(sMainMachThread, stack.frames, kMaxFramesPerStack, &(stack.frameCount)); - thread_resume(sMainMachThread); - sStacksLock.lock(); - try { - sStacks->emplace_back(stack); - } catch (const std::length_error& le) { - fflush(stdout); - fflush(stderr); - throw le; + Thread *thread = new Thread; + + if(threadId == sMainMachThread) { + strcpy(thread->name,"Main Thread"); + } else { + // Get thread Name + char name[256]; + pthread_t pt = pthread_from_mach_thread_np(threadId); + if (pt) { + name[0] = '\0'; + int rc = pthread_getname_np(pt, name, sizeof name); + strcpy(thread->name, name); + } } - sStacksLock.unlock(); + + // Create stacks vector + thread->stacks = new std::vector; + thread->stacks->reserve(400); + + return thread; } -+ (void)setupStackRecording ++ (void)recordStackForAllThreads +{ + thread_act_array_t threads; + mach_msg_type_number_t thread_count; + if (sRecordAllThreads) { + if (task_threads(mach_task_self(), &threads, &thread_count) != KERN_SUCCESS) { + thread_count = 0; + } + } else { + threads = &sMainMachThread; + thread_count = 1; + } + + // Suspend all threads but ETTrace's + for (mach_msg_type_number_t i = 0; i < thread_count; i++) { + if (threads[i] != sETTraceThread) { + thread_suspend(threads[i]); + } + } + + CFTimeInterval time = CACurrentMediaTime(); + for (mach_msg_type_number_t i = 0; i < thread_count; i++) { + if (threads[i] == sETTraceThread) { + continue; + } + + Stack stack; + stack.time = time; + FIRCLSWriteThreadStack(threads[i], stack.frames, kMaxFramesPerStack, &(stack.frameCount)); + + std::vector *threadStack; + sThreadsLock.lock(); + if (sThreadsMap->find(threads[i]) == sThreadsMap->end()) { + Thread *thread = [self createThread:threads[i]]; + // Add to hash map + sThreadsMap->insert(std::pair(threads[i], thread)); + + threadStack = thread->stacks; + } else { + threadStack = sThreadsMap->at(threads[i])->stacks; + } + + try { + threadStack->emplace_back(stack); + } catch (const std::length_error& le) { + fflush(stdout); + fflush(stderr); + throw le; + } + sThreadsLock.unlock(); + } + + for (mach_msg_type_number_t i = 0; i < thread_count; i++) { + if (threads[i] != sETTraceThread) + thread_resume(threads[i]); + } +} + ++ (void)setupStackRecording:(BOOL) recordAllThreads { if (sStackRecordingThread != nil) { return; @@ -78,12 +151,18 @@ + (void)setupStackRecording // usleep is guaranteed to sleep more than that, in practice ~5ms. We could use a // dispatch_timer, which at least tries to compensate for drift etc., but the // timer's queue could theoretically end up run on the main thread - sStacks = new std::vector; - sStacks->reserve(400); + sRecordAllThreads = recordAllThreads; + + sThreadsMap = new std::map; + sStackRecordingThread = [[NSThread alloc] initWithBlock:^{ + if (!sETTraceThread) { + sETTraceThread = mach_thread_self(); + } + NSThread *thread = [NSThread currentThread]; while (!thread.cancelled) { - [self recordStack]; + [self recordStackForAllThreads]; usleep(4500); } }]; @@ -99,8 +178,9 @@ + (void)stopRecordingThread { }); } -+ (void)setupRunAtStartup { ++ (void)setupRunAtStartup:(BOOL) recordAllThreads { [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"runAtStartup"]; + [[NSUserDefaults standardUserDefaults] setBool:recordAllThreads forKey:@"recordAllThreads"]; exit(0); } @@ -187,10 +267,9 @@ + (NSString *)deviceName { return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; } -+ (void)stopRecording { - sStacksLock.lock(); - NSMutableArray *> *stacks = [NSMutableArray array]; - for (const auto &cStack : *sStacks) { ++ (NSArray *> *) arrayFromStacks: (std::vector)stacks { + NSMutableArray *> *threadStacks = [NSMutableArray array]; + for (const auto &cStack : stacks) { NSMutableArray *stack = [NSMutableArray array]; // Add the addrs in reverse order so that they start with the lowest frame, e.g. `start` for (int j = (int)cStack.frameCount - 1; j >= 0; j--) { @@ -200,20 +279,37 @@ + (void)stopRecording { @"stack": [stack copy], @"time": @(cStack.time) }; - [stacks addObject:stackDictionary]; + [threadStacks addObject:stackDictionary]; } - sStacks->clear(); - sStacksLock.unlock(); + return threadStacks; +} + ++ (void)stopRecording { + sThreadsLock.lock(); + NSMutableDictionary *> *threads = [NSMutableDictionary dictionary]; + + std::map::iterator it; + for (it = sThreadsMap->begin(); it != sThreadsMap->end(); it++) { + Thread thread = *it->second; + NSString *threadId = [[NSNumber numberWithUnsignedInt:it->first] stringValue]; + threads[threadId] = @{ + @"name": [NSString stringWithFormat:@"%s", thread.name], + @"stacks": [self arrayFromStacks: *thread.stacks] + }; + } + sThreadsMap->empty(); + sThreadsLock.unlock(); + const NXArchInfo *archInfo = NXGetLocalArchInfo(); NSString *cpuType = [NSString stringWithUTF8String:archInfo->description]; NSMutableDictionary *info = [@{ - @"stacks": stacks, @"libraryInfo": EMGLibrariesData(), @"isSimulator": @([self isRunningOnSimulator]), @"osBuild": [self osBuild], @"cpuType": cpuType, @"device": [self deviceName], @"events": sSpanTimes, + @"threads": threads, } mutableCopy]; NSError *error = nil; @@ -243,8 +339,10 @@ + (void)load { EMGBeginCollectingLibraries(); BOOL infoPlistRunAtStartup = ((NSNumber *) NSBundle.mainBundle.infoDictionary[@"ETTraceRunAtStartup"]).boolValue; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"runAtStartup"] || infoPlistRunAtStartup) { - [EMGPerfAnalysis setupStackRecording]; + sRecordAllThreads = [[NSUserDefaults standardUserDefaults] boolForKey:@"recordAllThreads"]; + [EMGPerfAnalysis setupStackRecording:sRecordAllThreads]; [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"runAtStartup"]; + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"recordAllThreads"]; } [EMGPerfAnalysis startObserving]; } diff --git a/ETTrace/ETTrace/EMGPerfAnalysis_Private.h b/ETTrace/ETTrace/EMGPerfAnalysis_Private.h index f643ee7..f64b69b 100644 --- a/ETTrace/ETTrace/EMGPerfAnalysis_Private.h +++ b/ETTrace/ETTrace/EMGPerfAnalysis_Private.h @@ -10,8 +10,8 @@ #import "PerfAnalysis.h" @interface EMGPerfAnalysis (Private) -+ (void)setupStackRecording; -+ (void)setupRunAtStartup; ++ (void)setupStackRecording:(BOOL) recordAllThreads; ++ (void)setupRunAtStartup:(BOOL) recordAllThreads; + (void)stopRecordingThread; + (NSURL *)outputPath; @end diff --git a/ETTrace/ETTraceRunner/Devices/DeviceManager.swift b/ETTrace/ETTraceRunner/Devices/DeviceManager.swift index d0aad50..0f79aef 100644 --- a/ETTrace/ETTraceRunner/Devices/DeviceManager.swift +++ b/ETTrace/ETTraceRunner/Devices/DeviceManager.swift @@ -17,12 +17,13 @@ protocol DeviceManager { } extension DeviceManager { - func sendStartRecording(_ runAtStartup: Bool) async throws -> Void { + func sendStartRecording(_ runAtStartup: Bool, _ multiThread: Bool) async throws -> Void { return try await withCheckedThrowingContinuation { continuation in var boolValue = runAtStartup ? 1 : 0 let data = Data(bytes: &boolValue, count: 2) - communicationChannel.channel.sendFrame(type: UInt32(PTFrameTypeStart), tag: UInt32(PTNoFrameTag), payload: data) { error in + let type = multiThread ? PTFrameTypeStartMultiThread : PTFrameTypeStart + communicationChannel.channel.sendFrame(type: UInt32(type), tag: UInt32(PTNoFrameTag), payload: data) { error in if let error = error { continuation.resume(throwing: error) } else { diff --git a/ETTrace/ETTraceRunner/PerfAnalysisRunner.swift b/ETTrace/ETTraceRunner/PerfAnalysisRunner.swift index d32f46a..e25b167 100644 --- a/ETTrace/ETTraceRunner/PerfAnalysisRunner.swift +++ b/ETTrace/ETTraceRunner/PerfAnalysisRunner.swift @@ -20,12 +20,15 @@ struct PerfAnalysisRunner: ParsableCommand { @Flag(name: .shortAndLong, help: "Verbose logging") var verbose: Bool = false + + @Flag(name: .shortAndLong, help: "Record all threads") + var multiThread: Bool = false mutating func run() throws { if let dsym = dsyms, dsym.hasSuffix(".dSYM") { PerfAnalysisRunner.exit(withError: ValidationError("The dsym argument should be set to a folder containing your dSYM files, not the dSYM itself")) } - let helper = RunnerHelper(dsyms, launch, simulator, verbose) + let helper = RunnerHelper(dsyms, launch, simulator, verbose, multiThread) Task { do { try await helper.start() diff --git a/ETTrace/ETTraceRunner/ResponseModels/ResponseModel.swift b/ETTrace/ETTraceRunner/ResponseModels/ResponseModel.swift index c7ef32b..0f1fbe6 100644 --- a/ETTrace/ETTraceRunner/ResponseModels/ResponseModel.swift +++ b/ETTrace/ETTraceRunner/ResponseModels/ResponseModel.swift @@ -9,12 +9,12 @@ import Foundation struct ResponseModel: Decodable { let osBuild: String - let stacks: [Stack] let isSimulator: Bool let libraryInfo: LibraryInfo let cpuType: String - let device: String? - let events: [Event]? + let device: String + let events: [Event] + let threads: [String: Thread] } struct LibraryInfo: Decodable { @@ -23,11 +23,6 @@ struct LibraryInfo: Decodable { let loadedLibraries: [LoadedLibrary] } -struct Stack: Decodable { - let stack: [UInt64] - let time: Double -} - struct LoadedLibrary: Decodable, Equatable, Hashable { let path: String let loadAddress: UInt64 @@ -44,3 +39,13 @@ enum EventType: String, Decodable { case start case stop } + +struct Thread: Decodable { + let name: String + let stacks: [Stack] +} + +struct Stack: Decodable { + let stack: [UInt64] + let time: Double +} diff --git a/ETTrace/ETTraceRunner/RunnerHelper.swift b/ETTrace/ETTraceRunner/RunnerHelper.swift index 3f69456..8233c40 100644 --- a/ETTrace/ETTraceRunner/RunnerHelper.swift +++ b/ETTrace/ETTraceRunner/RunnerHelper.swift @@ -18,14 +18,17 @@ class RunnerHelper { let launch: Bool let useSimulator: Bool let verbose: Bool + let multiThread: Bool var server: HttpServer? = nil + var symbolicator: Symbolicator! - init(_ dsyms: String?, _ launch: Bool, _ simulator: Bool, _ verbose: Bool) { + init(_ dsyms: String?, _ launch: Bool, _ simulator: Bool, _ verbose: Bool, _ multiThread: Bool) { self.dsyms = dsyms self.launch = launch self.useSimulator = simulator self.verbose = verbose + self.multiThread = multiThread } func start() async throws { @@ -42,7 +45,7 @@ class RunnerHelper { try await deviceManager.connect() - try await deviceManager.sendStartRecording(launch) + try await deviceManager.sendStartRecording(launch, multiThread) if launch { print("Re-launch the app to start recording, then press return to exit") @@ -83,45 +86,67 @@ class RunnerHelper { } var osVersion = responseData.osBuild osVersion.removeAll(where: { !$0.isLetter && !$0.isNumber }) + + symbolicator = Symbolicator(isSimulator: isSimulator, dSymsDir: dsyms, osVersion: osVersion, arch: arch, verbose: verbose) + let outputUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + var allThreads:[Flamegraph] = [] + var mainThreadFlamegraph: Flamegraph! + var mainThreadData: Data! + for (threadId, thread) in responseData.threads { + let flamegraph = createFlamegraphForThread(thread, responseData) + allThreads.append(flamegraph) + + let outJsonData = JSONWrapper.toData(flamegraph)! + + if thread.name == "Main Thread" { + mainThreadFlamegraph = flamegraph + mainThreadData = outJsonData + } + try saveFlamegraph(outJsonData, outputUrl, threadId) + } + + guard mainThreadFlamegraph != nil else { + fatalError("No main thread flamegraphs generated") + } + + // Serve Main Thread + try startLocalServer(mainThreadData) + + let url = URL(string: "https://emergetools.com/flamegraph")! + NSWorkspace.shared.open(url) - let symbolicator = Symbolicator(isSimulator: isSimulator, dSymsDir: dsyms, osVersion: osVersion, arch: arch, verbose: verbose) - let syms = symbolicator.symbolicate(responseData.stacks, responseData.libraryInfo.loadedLibraries) - let flamegraphNodes = FlamegraphGenerator.generateFlamegraphs(stacks: responseData.stacks, syms: syms, writeFolded: verbose) + // Wait 4 seconds for results to be accessed from server, then exit + sleep(4) + print("Results saved to \(outputUrl)") + } + + private func createFlamegraphForThread(_ thread: Thread, _ responseData: ResponseModel) -> Flamegraph { + let stacks = thread.stacks + let syms = symbolicator.symbolicate(stacks, responseData.libraryInfo.loadedLibraries) + let flamegraphNodes = FlamegraphGenerator.generateFlamegraphs(stacks: stacks, syms: syms, writeFolded: verbose) + let threadNode = ThreadNode(nodes: flamegraphNodes, threadName: thread.name) - let startTime = responseData.stacks.sorted { s1, s2 in + let startTime = stacks.sorted { s1, s2 in s1.time < s2.time }.first!.time - let events = responseData.events?.map { event in + let events = responseData.events.map { event in return FlamegraphEvent(name: event.span, type: event.type.rawValue, time: event.time-startTime) - } ?? [] + } let libraries = responseData.libraryInfo.loadedLibraries.reduce(into: [String:UInt64]()) { partialResult, library in partialResult[library.path] = library.loadAddress } - let flamegraph = Flamegraph(osBuild: responseData.osBuild, - device: responseData.device, - isSimulator: responseData.isSimulator, - nodes: flamegraphNodes, - libraries: libraries, - events: events) - - let outJsonData: Data = JSONWrapper.toData(flamegraph) - - let jsonString = String(data: outJsonData, encoding: .utf8)! - try jsonString.write(toFile: "output.json", atomically: true, encoding: .utf8) - let outputUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("output.json") - - try startLocalServer(outJsonData) - let url = URL(string: "https://emergetools.com/flamegraph")! - NSWorkspace.shared.open(url) - - // Wait 4 seconds for results to be accessed from server, then exit - sleep(4) - print("Results saved to \(outputUrl)") + return Flamegraph(osBuild: responseData.osBuild, + device: responseData.device, + isSimulator: responseData.isSimulator, + libraries: libraries, + events: events, + threadNodes: [threadNode]) } func startLocalServer(_ data: Data) throws { @@ -150,4 +175,14 @@ class RunnerHelper { } try server?.start(37577) } + + private func saveFlamegraph(_ outJsonData: Data, _ outputUrl: URL, _ threadId: String? = nil) throws { + var saveUrl = outputUrl.appendingPathComponent("output.json") + if let threadId = threadId { + saveUrl = outputUrl.appendingPathComponent("output_\(threadId).json") + } + + let jsonString = String(data: outJsonData, encoding: .utf8)! + try jsonString.write(to: saveUrl, atomically: true, encoding: .utf8) + } } diff --git a/ETTrace/JSONWrapper/JSONWrapper.m b/ETTrace/JSONWrapper/JSONWrapper.m index 88c64ce..4f7cd94 100644 --- a/ETTrace/JSONWrapper/JSONWrapper.m +++ b/ETTrace/JSONWrapper/JSONWrapper.m @@ -41,13 +41,14 @@ + (NSDictionary *)flamegraphToDictionary:(Flamegraph *)flamegraph { NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithDictionary:@{ @"osBuild": flamegraph.osBuild, @"isSimulator": @(flamegraph.isSimulator), - @"nodes": [self flameNodeToDictionary:flamegraph.nodes], @"libraries": flamegraph.libraries, - @"events": [self eventsToArray:flamegraph.events] + @"events": [self eventsToArray:flamegraph.events], + @"device": flamegraph.device, }]; - if (flamegraph.device != nil) { - [result setObject:flamegraph.device forKey:@"device"]; - } + + ThreadNode *thread = flamegraph.threadNodes.firstObject; + [result setObject:[self flameNodeToDictionary:thread.nodes] forKey:@"nodes"]; + return result; }