FBControlCore/Utility/FBInstrumentsOperation.m (173 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBInstrumentsOperation.h" #import "FBCollectionInformation.h" #import "FBControlCoreError.h" #import "FBControlCoreLogger.h" #import "FBDataConsumer.h" #import "FBFuture.h" #import "FBInstrumentsConfiguration.h" #import "FBiOSTarget.h" #import "FBProcessBuilder.h" const NSTimeInterval DefaultInstrumentsOperationDuration = 60 * 60 * 4; const NSTimeInterval DefaultInstrumentsTerminateTimeout = 600.0; const NSTimeInterval DefaultInstrumentsLaunchErrorTimeout = 15.0; const NSTimeInterval DefaultInstrumentsLaunchRetryTimeout = 360.0; @interface FBInstrumentsConsumer : NSObject <FBDataConsumer> @property (nonatomic, strong, readonly) FBMutableFuture<NSNull *> *hasStoppedRecording; @property (nonatomic, strong, readonly) FBMutableFuture<NSNull *> *hasStartedLoadingTemplate; @property (nonatomic, strong, readonly) NSMutableArray<NSString *> *logs; @property (nonatomic, strong, readonly) id<FBDataConsumer> consumer; @end @implementation FBInstrumentsConsumer #pragma mark Initializers - (instancetype)init { self = [super init]; if (!self) { return nil; } _hasStoppedRecording = FBMutableFuture.future; _hasStartedLoadingTemplate = FBMutableFuture.future; _logs = [NSMutableArray array]; _consumer = [FBBlockDataConsumer asynchronousLineConsumerWithBlock:^(NSString *logLine) { if (![logLine isEqualToString:@""]) { [self.logs addObject:logLine]; } if ([logLine containsString:@"Loading template"]) { [self.hasStartedLoadingTemplate resolveWithResult:NSNull.null]; } if ([logLine containsString:@"Instruments Trace Complete"]) { if (![self.hasStoppedRecording hasCompleted]) { FBFuture *failFuture = [[FBControlCoreError describeFormat:@"Instruments did not start properly. Instruments logs:\n%@", [self.logs componentsJoinedByString:@"\n"]] failFuture]; [self.hasStoppedRecording resolveFromFuture:failFuture]; } } }]; return self; } #pragma mark FBDataConsumer - (void)consumeData:(NSData *)data { [self.consumer consumeData:data]; } - (void)consumeEndOfFile { [self.consumer consumeEndOfFile]; } @end @implementation FBInstrumentsOperation #pragma mark Initializers + (FBFuture<FBInstrumentsOperation *> *)operationWithTarget:(id<FBiOSTarget>)target configuration:(FBInstrumentsConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger { // The instruments cli is unreliable and sometimes stops recording right after starting // To make it reliable, we retry it until it either succeeds or we timeout return [[FBFuture onQueue:target.asyncQueue resolveUntil:^ FBFuture * { return [self operationWithTargetInternal:target configuration:configuration logger:logger]; }] timeout:configuration.timings.launchRetryTimeout waitingFor:@"successful instruments startup"]; } + (FBFuture<FBInstrumentsOperation *> *)operationWithTargetInternal:(id<FBiOSTarget>)target configuration:(FBInstrumentsConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger { dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.instruments", DISPATCH_QUEUE_SERIAL); NSString *traceDir = [target.auxillaryDirectory stringByAppendingPathComponent:[@"instruments-" stringByAppendingString:[[NSUUID UUID] UUIDString]]]; NSError *innerError = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:traceDir withIntermediateDirectories:NO attributes:nil error:&innerError]) { return [[FBControlCoreError describeFormat:@"Failed to create instruments trace output directory: %@", innerError] failFuture]; } NSString *traceFile = [traceDir stringByAppendingPathComponent:@"trace.trace"]; NSString *durationMilliseconds = [@(configuration.timings.operationDuration * 1000) stringValue]; NSMutableArray<NSString *> *arguments = [NSMutableArray new]; if ([[configuration toolArguments] count] > 0) { [arguments addObjectsFromArray:[configuration toolArguments]]; } [arguments addObjectsFromArray:@[@"-w", target.udid, @"-D", traceFile, @"-t", configuration.templateName, @"-l", durationMilliseconds, @"-v"]]; if (configuration.targetApplication && [configuration.targetApplication length] > 0) { [arguments addObject:configuration.targetApplication]; for (NSString *key in configuration.appEnvironment) { [arguments addObjectsFromArray:@[@"-e", key, configuration.appEnvironment[key]]]; } [arguments addObjectsFromArray:configuration.appArguments]; } [logger logFormat:@"Starting instruments with arguments: %@", [FBCollectionInformation oneLineDescriptionFromArray:arguments]]; FBInstrumentsConsumer *instrumentsConsumer = [[FBInstrumentsConsumer alloc] init]; id<FBControlCoreLogger> instrumentsLogger = [FBControlCoreLoggerFactory loggerToConsumer:instrumentsConsumer]; id<FBControlCoreLogger> compositeLogger = [FBControlCoreLoggerFactory compositeLoggerWithLoggers:@[logger, instrumentsLogger]]; return [[[[[[[[FBProcessBuilder withLaunchPath:@"/usr/bin/instruments"] withArguments:arguments] withStdOutToLogger:compositeLogger] withStdErrToLogger:compositeLogger] withTaskLifecycleLoggingTo:logger] start] onQueue:target.asyncQueue fmap:^ FBFuture * (FBProcess *task) { return [instrumentsConsumer.hasStartedLoadingTemplate onQueue:target.asyncQueue fmap:^ FBFuture * (id _) { [logger logFormat:@"Waiting for %f seconds for instruments to start properly", configuration.timings.launchErrorTimeout]; // Instruments profiling started correctly if timer expires before 'hasStoppedRecording' resolves. // This is necessary because instruments doesn't print anything when profiling has begun. // We detect a failure by checking for 'Instruments Trace Completed' output before launchErrorTimeout. FBFuture *timerFuture = [FBFuture.empty delay:configuration.timings.launchErrorTimeout]; return [[[FBFuture race:@[instrumentsConsumer.hasStoppedRecording, timerFuture]] onQueue:target.asyncQueue handleError:^ FBFuture * (NSError *error) { return [[task sendSignal:SIGTERM] chainReplace:[FBFuture futureWithError:error]]; }] mapReplace:task]; }]; }] // Yay instruments started properly onQueue:target.asyncQueue map:^ FBInstrumentsOperation * (FBProcess *task) { [logger logFormat:@"Started instruments %@", task]; return [[FBInstrumentsOperation alloc] initWithTask:task traceDir:[NSURL fileURLWithPath:traceFile] configuration:configuration queue:queue logger:logger]; }]; } - (instancetype)initWithTask:(FBProcess *)task traceDir:(NSURL *)traceDir configuration:(FBInstrumentsConfiguration *)configuration queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _task = task; _traceDir = traceDir; _configuration = configuration; _queue = queue; _logger = logger; return self; } #pragma mark Public Methods - (FBFuture<NSURL *> *)stop { return [[FBFuture onQueue:self.queue resolve:^{ [self.logger logFormat:@"Terminating instruments %@. Backoff Timeout %f", self.task, self.configuration.timings.terminateTimeout]; return [self.task sendSignal:SIGINT backingOffToKillWithTimeout:self.configuration.timings.terminateTimeout logger:self.logger]; }] chainReplace:[[self.task exitCode] onQueue:self.queue fmap:^FBFuture<NSURL *> *(NSNumber *exitCode) { if ([exitCode isEqualToNumber:@0]) { return [FBFuture futureWithResult:self.traceDir]; } else { return [[FBControlCoreError describeFormat:@"Instruments exited with failure - status: %@", exitCode] failFuture]; } }] ]; } + (FBFuture<NSURL *> *)postProcess:(NSArray<NSString *> *)arguments traceDir:(NSURL *)traceDir queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger { if (!arguments || arguments.count == 0) { return [FBFuture futureWithResult:traceDir]; } NSURL *outputTraceFile = [[traceDir URLByDeletingLastPathComponent] URLByAppendingPathComponent:arguments[2]]; NSMutableArray<NSString *> *launchArguments = [@[arguments[1], traceDir.path, @"-o", outputTraceFile.path] mutableCopy]; if (arguments.count > 3) { [launchArguments addObjectsFromArray:[arguments subarrayWithRange:(NSRange){3, [arguments count] - 3}]]; } [logger logFormat:@"Starting post processing | Launch path: %@ | Arguments: %@", arguments[0], [FBCollectionInformation oneLineDescriptionFromArray:launchArguments]]; return [[[[[[[[FBProcessBuilder withLaunchPath:arguments[0]] withArguments:launchArguments] withStdInConnected] withStdOutToLogger:logger] withStdErrToLogger:logger] withTaskLifecycleLoggingTo:logger] runUntilCompletionWithAcceptableExitCodes:[NSSet setWithObject:@0]] onQueue:queue map:^(id _) { return outputTraceFile; }]; } @end