diff --git a/ETTrace/ETTrace/EMGChannelListener.m b/ETTrace/ETTrace/EMGChannelListener.m index 610a4c7..ef38c7c 100644 --- a/ETTrace/ETTrace/EMGChannelListener.m +++ b/ETTrace/ETTrace/EMGChannelListener.m @@ -65,10 +65,10 @@ - (void)ioFrameChannel:(PTChannel*)channel didReceiveFrameOfType:(uint32_t)type if (runAtStartup) { [EMGPerfAnalysis setupRunAtStartup:recordAllThreads]; } else { - [EMGPerfAnalysis setupStackRecording:recordAllThreads]; + [EMGPerfAnalysis startRecording:recordAllThreads]; } } else if (type == PTFrameTypeStop) { - [EMGPerfAnalysis stopRecordingThread]; + [EMGPerfAnalysis stopRecording]; } else if (type == PTFrameTypeRequestResults) { [self sendReportData]; } diff --git a/ETTrace/ETTrace/EMGPerfAnalysis.mm b/ETTrace/ETTrace/EMGPerfAnalysis.mm index 1fb6b34..df00765 100644 --- a/ETTrace/ETTrace/EMGPerfAnalysis.mm +++ b/ETTrace/ETTrace/EMGPerfAnalysis.mm @@ -7,7 +7,7 @@ #import #import -#import "EMGWriteLibraries.h" +#import #import #import #import @@ -24,158 +24,14 @@ @implementation EMGPerfAnalysis -static thread_t sMainMachThread = {0}; -static thread_t sETTraceThread = {0}; - -static const int kMaxFramesPerStack = 512; -static NSThread *sStackRecordingThread = nil; -typedef struct { - CFTimeInterval time; - uint64_t frameCount; - uintptr_t frames[kMaxFramesPerStack]; -} Stack; - -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); -} - -+ (Thread *) createThread:(thread_t) threadId -{ - 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); - } - } - - // Create stacks vector - thread->stacks = new std::vector; - thread->stacks->reserve(400); - - return thread; -} - -+ (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; - } - - sSpanTimes = [NSMutableArray array]; - - // Make sure that +recordStack is always called on the same (non-main) thread. - // This is because a Process keeps its own "current thread" variable which we need - // to keep separate - // from the main thread. This is because unwinding itself from the main thread - // requires Crashlyics to use a hack, and because the stack recording would show up - // in the trace. The current strategy is to sleep for 4.5 ms because - // 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 - sRecordAllThreads = recordAllThreads; - - sThreadsMap = new std::map; - - sStackRecordingThread = [[NSThread alloc] initWithBlock:^{ - if (!sETTraceThread) { - sETTraceThread = mach_thread_self(); - } - - NSThread *thread = [NSThread currentThread]; - while (!thread.cancelled) { - [self recordStackForAllThreads]; - usleep(4500); - } - }]; - sStackRecordingThread.qualityOfService = NSQualityOfServiceUserInteractive; - [sStackRecordingThread start]; -} - -+ (void)stopRecordingThread { - [sStackRecordingThread cancel]; - sStackRecordingThread = nil; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [EMGPerfAnalysis stopRecording]; - }); ++ (void)startRecording:(BOOL)recordAllThreads { + sSpanTimes = [NSMutableArray array]; + [EMGTracer setupStackRecording:recordAllThreads]; } + (void)setupRunAtStartup:(BOOL) recordAllThreads { @@ -199,7 +55,7 @@ + (void)startObserving { object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { - if (sStackRecordingThread == nil) { + if (![EMGTracer isRecording]) { return; } @@ -215,7 +71,7 @@ + (void)startObserving { object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { - if (sStackRecordingThread == nil) { + if (![EMGTracer isRecording]) { return; } @@ -228,90 +84,11 @@ + (void)startObserving { }]; } -+ (BOOL)isRunningOnSimulator -{ -#if TARGET_OS_SIMULATOR - return YES; -#else - return NO; -#endif -} - -+ (NSString *)osBuild { - int mib[2] = {CTL_KERN, KERN_OSVERSION}; - u_int namelen = sizeof(mib) / sizeof(mib[0]); - size_t bufferSize = 0; - - NSString *osBuildVersion = nil; - - // Get the size for the buffer - sysctl(mib, namelen, NULL, &bufferSize, NULL, 0); - - u_char buildBuffer[bufferSize]; - int result = sysctl(mib, namelen, buildBuffer, &bufferSize, NULL, 0); - - if (result >= 0) { - osBuildVersion = [[NSString alloc] initWithBytes:buildBuffer length:bufferSize encoding:NSUTF8StringEncoding]; - } - - NSCharacterSet *nonAlphanumericStrings = [[NSCharacterSet alphanumericCharacterSet] invertedSet]; - - // Remove final NULL character - return [osBuildVersion stringByTrimmingCharactersInSet:nonAlphanumericStrings]; -} - -+ (NSString *)deviceName { - struct utsname systemInfo; - uname(&systemInfo); - - return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; -} - -+ (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--) { - [stack addObject:@((NSUInteger)cStack.frames[j])]; - } - NSDictionary *stackDictionary = @{ - @"stack": [stack copy], - @"time": @(cStack.time) - }; - [threadStacks addObject:stackDictionary]; - } - return threadStacks; -} - + (void)stopRecording { - sThreadsLock.lock(); - NSMutableDictionary *> *threads = [NSMutableDictionary dictionary]; + [EMGTracer stopRecording:^(NSDictionary *results) { + NSMutableDictionary *info = [results mutableCopy]; + info[@"events"] = sSpanTimes; - 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 = [@{ - @"libraryInfo": EMGLibrariesData(), - @"isSimulator": @([self isRunningOnSimulator]), - @"osBuild": [self osBuild], - @"cpuType": cpuType, - @"device": [self deviceName], - @"events": sSpanTimes, - @"threads": threads, - } mutableCopy]; - NSError *error = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:info options:0 error:&error]; if (error) { @@ -330,17 +107,17 @@ + (void)stopRecording { NSLog(@"ETTrace result written"); } [channelListener sendReportCreatedMessage]; + }]; } + (void)load { NSLog(@"Starting ETTrace"); - sMainMachThread = mach_thread_self(); + [EMGTracer setup]; fileEventsQueue = dispatch_queue_create("com.emerge.file_queue", DISPATCH_QUEUE_SERIAL); - EMGBeginCollectingLibraries(); BOOL infoPlistRunAtStartup = ((NSNumber *) NSBundle.mainBundle.infoDictionary[@"ETTraceRunAtStartup"]).boolValue; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"runAtStartup"] || infoPlistRunAtStartup) { - sRecordAllThreads = [[NSUserDefaults standardUserDefaults] boolForKey:@"recordAllThreads"]; - [EMGPerfAnalysis setupStackRecording:sRecordAllThreads]; + BOOL recordAllThreads = [[NSUserDefaults standardUserDefaults] boolForKey:@"recordAllThreads"]; + [EMGPerfAnalysis startRecording:recordAllThreads]; [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"runAtStartup"]; [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"recordAllThreads"]; } diff --git a/ETTrace/ETTrace/EMGPerfAnalysis_Private.h b/ETTrace/ETTrace/EMGPerfAnalysis_Private.h index f64b69b..05b5016 100644 --- a/ETTrace/ETTrace/EMGPerfAnalysis_Private.h +++ b/ETTrace/ETTrace/EMGPerfAnalysis_Private.h @@ -10,9 +10,9 @@ #import "PerfAnalysis.h" @interface EMGPerfAnalysis (Private) -+ (void)setupStackRecording:(BOOL) recordAllThreads; ++ (void)startRecording:(BOOL) recordAllThreads; ++(void)stopRecording; + (void)setupRunAtStartup:(BOOL) recordAllThreads; -+ (void)stopRecordingThread; + (NSURL *)outputPath; @end diff --git a/ETTrace/Tracer/EMGTracer.mm b/ETTrace/Tracer/EMGTracer.mm new file mode 100644 index 0000000..2791bea --- /dev/null +++ b/ETTrace/Tracer/EMGTracer.mm @@ -0,0 +1,261 @@ +// +// Tracer.m +// +// +// Created by Noah Martin on 10/27/23. +// + +#import "Tracer.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import + +static const int kMaxFramesPerStack = 512; +static NSThread *sStackRecordingThread = nil; +typedef struct { + CFTimeInterval time; + uint64_t frameCount; + uintptr_t frames[kMaxFramesPerStack]; +} Stack; + +typedef struct { + std::vector *stacks; + char name[256]; +} Thread; +static std::map *sThreadsMap; +static std::mutex sThreadsLock; + +static BOOL sRecordAllThreads = false; + +static thread_t sMainMachThread = {0}; +static thread_t sETTraceThread = {0}; + +extern "C" { +void FIRCLSWriteThreadStack(thread_t thread, uintptr_t *frames, uint64_t framesCapacity, uint64_t *framesWritten); +} + +@implementation EMGTracer + ++ (BOOL)isRecording { + return sStackRecordingThread != nil; +} + ++ (void)stopRecording:(void (^)(NSDictionary *))stopped { + [sStackRecordingThread cancel]; + sStackRecordingThread = nil; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + stopped([EMGTracer getResults]); + }); +} + ++ (NSDictionary *)getResults { + 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] + }; + } + sThreadsLock.unlock(); + + const NXArchInfo *archInfo = NXGetLocalArchInfo(); + NSString *cpuType = [NSString stringWithUTF8String:archInfo->description]; + return @{ + @"libraryInfo": EMGLibrariesData(), + @"isSimulator": @([self isRunningOnSimulator]), + @"osBuild": [self osBuild], + @"cpuType": cpuType, + @"device": [self deviceName], + @"threads": threads, + }; +} + ++ (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--) { + [stack addObject:@((NSUInteger)cStack.frames[j])]; + } + NSDictionary *stackDictionary = @{ + @"stack": [stack copy], + @"time": @(cStack.time) + }; + [threadStacks addObject:stackDictionary]; + } + return threadStacks; +} + ++ (BOOL)isRunningOnSimulator +{ +#if TARGET_OS_SIMULATOR + return YES; +#else + return NO; +#endif +} + ++ (NSString *)osBuild { + int mib[2] = {CTL_KERN, KERN_OSVERSION}; + u_int namelen = sizeof(mib) / sizeof(mib[0]); + size_t bufferSize = 0; + + NSString *osBuildVersion = nil; + + // Get the size for the buffer + sysctl(mib, namelen, NULL, &bufferSize, NULL, 0); + + u_char buildBuffer[bufferSize]; + int result = sysctl(mib, namelen, buildBuffer, &bufferSize, NULL, 0); + + if (result >= 0) { + osBuildVersion = [[NSString alloc] initWithBytes:buildBuffer length:bufferSize encoding:NSUTF8StringEncoding]; + } + + NSCharacterSet *nonAlphanumericStrings = [[NSCharacterSet alphanumericCharacterSet] invertedSet]; + + // Remove final NULL character + return [osBuildVersion stringByTrimmingCharactersInSet:nonAlphanumericStrings]; +} + ++ (NSString *)deviceName { + struct utsname systemInfo; + uname(&systemInfo); + + return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; +} + ++ (Thread *) createThread:(thread_t) threadId +{ + 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); + } + } + + // Create stacks vector + thread->stacks = new std::vector; + thread->stacks->reserve(400); + + return thread; +} + ++ (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)setup { + sMainMachThread = mach_thread_self(); + EMGBeginCollectingLibraries(); +} + ++ (void)setupStackRecording:(BOOL) recordAllThreads +{ + if (sStackRecordingThread != nil) { + return; + } + + // Make sure that +recordStack is always called on the same (non-main) thread. + // This is because a Process keeps its own "current thread" variable which we need + // to keep separate + // from the main thread. This is because unwinding itself from the main thread + // requires Crashlyics to use a hack, and because the stack recording would show up + // in the trace. The current strategy is to sleep for 4.5 ms because + // 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 + sRecordAllThreads = recordAllThreads; + + sThreadsMap = new std::map; + + sStackRecordingThread = [[NSThread alloc] initWithBlock:^{ + if (!sETTraceThread) { + sETTraceThread = mach_thread_self(); + } + + NSThread *thread = [NSThread currentThread]; + while (!thread.cancelled) { + [self recordStackForAllThreads]; + usleep(4500); + } + }]; + sStackRecordingThread.qualityOfService = NSQualityOfServiceUserInteractive; + [sStackRecordingThread start]; +} + +@end diff --git a/ETTrace/ETTrace/EMGWriteLibraries.m b/ETTrace/Tracer/EMGWriteLibraries.m similarity index 99% rename from ETTrace/ETTrace/EMGWriteLibraries.m rename to ETTrace/Tracer/EMGWriteLibraries.m index 257d2f7..e64438d 100644 --- a/ETTrace/ETTrace/EMGWriteLibraries.m +++ b/ETTrace/Tracer/EMGWriteLibraries.m @@ -14,7 +14,7 @@ #import #import -#import "EMGWriteLibraries.h" +#import "Tracer.h" static NSRecursiveLock *sLock; static NSMutableArray *sLoadedLibraries; diff --git a/ETTrace/ETTrace/EMGWriteLibraries.h b/ETTrace/Tracer/Public/Tracer.h similarity index 55% rename from ETTrace/ETTrace/EMGWriteLibraries.h rename to ETTrace/Tracer/Public/Tracer.h index 3f300ce..be3e850 100644 --- a/ETTrace/ETTrace/EMGWriteLibraries.h +++ b/ETTrace/Tracer/Public/Tracer.h @@ -21,5 +21,16 @@ void EMGBeginCollectingLibraries(void); } #endif +@interface EMGTracer : NSObject + ++ (void)setupStackRecording:(BOOL)recordAllThreads; ++ (void)stopRecording:(void (^)(NSDictionary *))stopped; +// Must be called on the main thread, before setupStackRecording is called ++ (void)setup; ++ (NSDictionary *)getResults; ++ (BOOL)isRecording; + +@end + #endif /* EMGWriteLibraries_h */ diff --git a/Package.swift b/Package.swift index 0b43d2b..decce09 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let package = Package( type: .dynamic, targets: ["ETTrace"] ), + .library(name: "Tracer", targets: ["Tracer"]), .executable( name: "ETTraceRunner", targets: ["ETTraceRunner"] @@ -26,13 +27,21 @@ let package = Package( .target( name: "ETTrace", dependencies: [ - "Unwinding", + "Tracer", "CommunicationFrame", .product(name: "Peertalk", package: "peertalk") ], path: "ETTrace/ETTrace", publicHeadersPath: "Public" ), + .target( + name: "Tracer", + dependencies: [ + "Unwinding", + ], + path: "ETTrace/Tracer", + publicHeadersPath: "Public" + ), .target( name: "CommunicationFrame", path: "ETTrace/CommunicationFrame",