TwitterImagePipeline/TIPImageFetchMetrics.m (408 lines of code) (raw):

// // TIPImageFetchMetrics.m // TwitterImagePipeline // // Created on 6/19/15. // Copyright (c) 2015 Twitter. All rights reserved. // #import <mach/mach_time.h> #import "TIP_Project.h" #import "TIPImageFetchMetrics+Project.h" #import "TIPTiming.h" NS_ASSUME_NONNULL_BEGIN @interface TIPImageFetchMetricInfo () // Concrete properties for `NetworkSourceInfo` category @property (nonatomic, readonly, nullable) id networkMetrics; @property (nonatomic, readonly, nullable) NSURLRequest *networkRequest; @property (nonatomic, readonly) NSTimeInterval totalNetworkLoadDuration; @property (nonatomic, readonly) NSTimeInterval firstProgressiveFrameNetworkLoadDuration; @property (nonatomic, readonly) NSUInteger networkImageSizeInBytes; @property (nonatomic, copy, readonly, nullable) NSString *networkImageType; @property (nonatomic, readonly) CGSize networkImageDimensions; @property (nonatomic, readonly) float networkImagePixelsPerByte; @end @implementation TIPImageFetchMetrics { struct { TIPImageLoadSource currentSource:4; BOOL isTrackingCurrentSource:1; BOOL wasCancelled:1; } _flags; uint64_t _machStartTime; uint64_t _machFirstImageLoadTime; uint64_t _machEndTime; /* max value + 1 == number of values number of values - 1 == number of non-unknown values number of non-unknown values == max value */ TIPImageFetchMetricInfo *_infos[TIPImageLoadSourceMaxValue]; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initProject { return [super init]; } #pragma mark Public - (nullable TIPImageFetchMetricInfo *)metricInfoForSource:(TIPImageLoadSource)source { if (source == TIPImageLoadSourceUnknown || source > TIPImageLoadSourceMaxValue) { return nil; } return _infos[source - 1]; } - (BOOL)wasCancelled { return _flags.wasCancelled; } - (NSTimeInterval)totalDuration { if (_machStartTime && _machEndTime) { return TIPComputeDuration(_machStartTime, _machEndTime); } return 0.0; } - (NSTimeInterval)firstImageLoadDuration { if (_machStartTime && _machFirstImageLoadTime) { return TIPComputeDuration(_machStartTime, _machFirstImageLoadTime); } return 0.0; } #pragma mark Project - (void)startWithSource:(TIPImageLoadSource)source { if (_flags.wasCancelled) { return; } if (_flags.isTrackingCurrentSource) { @throw [NSException exceptionWithName:NSObjectNotAvailableException reason:[NSString stringWithFormat:@"%@ cannot %@ while in the middle of capturing the metrics of another TIPImageLoadSource!", NSStringFromClass([self class]), NSStringFromSelector(_cmd)] userInfo:nil]; } if (_flags.currentSource >= source) { // also prevents accessing Unknown which is 0 @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@ cannot %@ with a TIPImageLoadSource (%ti) that has already been captured!", NSStringFromClass([self class]), NSStringFromSelector(_cmd), (TIPImageLoadSource)_flags.currentSource] userInfo:nil]; } _flags.isTrackingCurrentSource = 1; _flags.currentSource = source; const uint64_t machTime = mach_absolute_time(); if (!_machStartTime) { _machStartTime = machTime; } _infos[source-1] = [[TIPImageFetchMetricInfo alloc] initWithSource:source startTime:machTime]; } - (void)endSource { if (_flags.wasCancelled) { return; } if (!_flags.isTrackingCurrentSource) { NSString *reason = [NSString stringWithFormat:@"%@ cannot %@ when %@ has not been called yet!", NSStringFromClass([self class]), NSStringFromSelector(_cmd), @"startWithSource:"]; @throw [NSException exceptionWithName:NSObjectNotAvailableException reason:reason userInfo:nil]; } _machEndTime = mach_absolute_time(); [_infos[_flags.currentSource - 1] end]; _flags.isTrackingCurrentSource = 0; } - (void)cancelSource { if (_flags.wasCancelled) { return; } _flags.wasCancelled = 1; _machEndTime = mach_absolute_time(); if (_flags.isTrackingCurrentSource) { [_infos[_flags.currentSource - 1] cancel]; _flags.isTrackingCurrentSource = 0; } } - (void)convertNetworkMetricsToResumedNetworkMetrics { if (_flags.wasCancelled) { return; } if (_flags.currentSource != TIPImageLoadSourceNetwork) { return; } if (!_flags.isTrackingCurrentSource) { return; } TIPImageFetchMetricInfo *currentInfo = _infos[_flags.currentSource - 1]; [currentInfo flipLoadSourceFromNetworkToNetworkResumed]; _flags.currentSource = TIPImageLoadSourceNetworkResumed; _infos[TIPImageLoadSourceNetwork - 1] = nil; _infos[TIPImageLoadSourceNetworkResumed - 1] = currentInfo; } - (void)addNetworkMetrics:(nullable id)metrics forRequest:(NSURLRequest *)request imageType:(nullable NSString *)imageType imageSizeInBytes:(NSUInteger)sizeInBytes imageDimensions:(CGSize)dimensions { if (_flags.wasCancelled) { return; } const BOOL isNetworkSource = _flags.currentSource == TIPImageLoadSourceNetwork || _flags.currentSource == TIPImageLoadSourceNetworkResumed; if (!_flags.isTrackingCurrentSource || !isNetworkSource) { NSString *reason = [NSString stringWithFormat:@"%@ cannot %@ when not tracking network source!", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSObjectNotAvailableException reason:reason userInfo:nil]; } [_infos[_flags.currentSource - 1] addNetworkMetrics:metrics forRequest:request imageType:imageType imageSizeInBytes:sizeInBytes imageDimensions:dimensions]; } - (void)previewWasHit:(NSTimeInterval)renderLatency { [self _hit:TIPImageFetchLoadResultHitPreview renderLatency:renderLatency synchronously:NO]; } - (void)progressiveFrameWasHit:(NSTimeInterval)renderLatency { [self _hit:TIPImageFetchLoadResultHitProgressFrame renderLatency:renderLatency synchronously:NO]; } - (void)finalWasHit:(NSTimeInterval)renderLatency synchronously:(BOOL)sync { [self _hit:TIPImageFetchLoadResultHitFinal renderLatency:renderLatency synchronously:sync]; } - (void)_hit:(TIPImageFetchLoadResult)result renderLatency:(NSTimeInterval)latency synchronously:(BOOL)synchronously TIP_OBJC_DIRECT { if (_flags.isTrackingCurrentSource) { if (!_machFirstImageLoadTime) { _machFirstImageLoadTime = mach_absolute_time(); } [_infos[_flags.currentSource - 1] hit:result renderLatency:latency synchronously:synchronously]; } } - (NSString *)description { const size_t count = sizeof(_infos) / sizeof(_infos[0]); NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:count]; for (TIPImageLoadSource i = 1; (size_t)i <= count; i++) { TIPImageFetchMetricInfo *info = _infos[i - 1]; if (info) { [array addObject:info]; } } NSString *state = nil; if (self.wasCancelled) { state = @"cancelled"; } else if (_machFirstImageLoadTime) { state = @"hit"; } else { state = @"miss"; } [array addObject:[NSString stringWithFormat:@"total : %@=%.3fs", state, self.totalDuration]]; return array.description; } @end @implementation TIPImageFetchMetricInfo { struct { BOOL wasCancelled:1; BOOL didEnd:1; } _flags; uint64_t _machStartTime; uint64_t _machEndTime; NSTimeInterval _renderLatency; uint64_t _machFirstTime; TIPImageFetchLoadResult _firstResult; NSTimeInterval _firstResultRenderLatency; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initWithSource:(TIPImageLoadSource)source startTime:(uint64_t)startMachTime { if (TIPImageLoadSourceUnknown == source) { NSString *reason = [NSString stringWithFormat:@"%@ cannot init with an Unknown TIPImageLoadSource!", NSStringFromClass([self class])]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; } if (self = [super init]) { _result = _firstResult = TIPImageFetchLoadResultNeverCompleted; _machStartTime = startMachTime; _source = source; } return self; } - (NSTimeInterval)loadDuration { if (_machEndTime) { return TIPComputeDuration(_machStartTime, _machEndTime); } return 0.0; } - (BOOL)wasCancelled { return _flags.wasCancelled; } - (void)end { if (_flags.wasCancelled) { return; } if (_flags.didEnd) { NSString *reason = [NSString stringWithFormat:@"%@ cannot %@ after it has already ended!", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSObjectNotAvailableException reason:reason userInfo:nil]; } _flags.didEnd = 1; _machEndTime = mach_absolute_time(); if (TIPImageFetchLoadResultNeverCompleted == _result) { _result = TIPImageFetchLoadResultMiss; } } - (void)cancel { if (_flags.wasCancelled || _flags.didEnd) { return; } _flags.wasCancelled = 1; _machEndTime = mach_absolute_time(); } - (void)hit:(TIPImageFetchLoadResult)result renderLatency:(NSTimeInterval)renderLatency synchronously:(BOOL)sync { if (_flags.didEnd || _flags.wasCancelled) { return; } if (_result < result) { if (_result != TIPImageFetchLoadResultNeverCompleted) { _firstResult = _result; _firstResultRenderLatency = _renderLatency; } if (!_machFirstTime) { _machFirstTime = mach_absolute_time(); } if (TIPImageFetchLoadResultHitProgressFrame == result) { _firstProgressiveFrameNetworkLoadDuration = TIPComputeDuration(_machStartTime, mach_absolute_time()); } _wasLoadedSynchronously = sync; if (sync) { TIPAssert(TIPImageFetchLoadResultHitFinal == result && TIPImageLoadSourceMemoryCache == _source); } _result = result; _renderLatency = renderLatency; } } - (void)flipLoadSourceFromNetworkToNetworkResumed { if (_source == TIPImageLoadSourceNetwork) { _source = TIPImageLoadSourceNetworkResumed; } } - (void)addNetworkMetrics:(nullable id)metrics forRequest:(NSURLRequest *)request imageType:(nullable NSString *)imageType imageSizeInBytes:(NSUInteger)sizeInBytes imageDimensions:(CGSize)dimensions { if (_flags.wasCancelled) { return; } if (_source != TIPImageLoadSourceNetwork && _source != TIPImageLoadSourceNetworkResumed) { return; } _networkMetrics = metrics; _networkRequest = request; if (imageType) { _networkImageDimensions = dimensions; _networkImageSizeInBytes = sizeInBytes; _networkImageType = [imageType copy]; uint64_t machTime = mach_absolute_time(); _totalNetworkLoadDuration = TIPComputeDuration(_machStartTime, machTime); } } - (float)networkImagePixelsPerByte { if (_networkImageSizeInBytes > 0 && _networkImageDimensions.height > 0 && _networkImageDimensions.width > 0) { return ((float)_networkImageDimensions.height * (float)_networkImageDimensions.width) / (float)_networkImageSizeInBytes; } return 0; } - (NSString *)description { const NSTimeInterval duration = self.loadDuration; NSString *source = nil; NSString *result = nil; switch (_source) { case TIPImageLoadSourceMemoryCache: { source = @"memory"; break; } case TIPImageLoadSourceDiskCache: { source = @"disk"; break; } case TIPImageLoadSourceAdditionalCache: { source = @"alt"; break; } case TIPImageLoadSourceNetwork: { source = @"network"; break; } case TIPImageLoadSourceNetworkResumed: { source = @"network_resumed"; break; } default: { break; } } switch (_result) { case TIPImageFetchLoadResultNeverCompleted: { result = (_flags.wasCancelled) ? @"cancelled" : @"DNF"; break; } case TIPImageFetchLoadResultMiss: { result = @"miss"; break; } case TIPImageFetchLoadResultHitPreview: { result = @"preview"; break; } case TIPImageFetchLoadResultHitProgressFrame: { result = @"progressFrame"; break; } case TIPImageFetchLoadResultHitFinal: { result = @"final"; break; } } NSString *firstResult = @""; if (_firstResult == TIPImageFetchLoadResultHitPreview || _firstResult == TIPImageFetchLoadResultHitProgressFrame) { NSString *firstLatency = (_firstResultRenderLatency > 0) ? [NSString stringWithFormat:@" 1_dRen=%.3fs", _firstResultRenderLatency] : @""; firstResult = [NSString stringWithFormat:@", 1_%@=%.3fs%@", (_firstResult == TIPImageFetchLoadResultHitPreview) ? @"preview" : @"progress", TIPComputeDuration(_machStartTime, _machFirstTime), firstLatency]; } NSString *renderLatency = @""; if (_renderLatency > 0) { renderLatency = [NSString stringWithFormat:@" dRen=%.3fs", _renderLatency]; } NSString *pixelsPerByte = @""; NSString *imageType = @""; if (_source == TIPImageLoadSourceNetwork || _source == TIPImageLoadSourceNetworkResumed) { if (_networkImageType) { imageType = [NSString stringWithFormat:@" %@", _networkImageType]; } float pixelsPerByteFloat = self.networkImagePixelsPerByte; if (pixelsPerByteFloat > FLT_EPSILON) { pixelsPerByte = [NSString stringWithFormat:@" pixelsPerByte=%.3f", pixelsPerByteFloat]; } } return [NSString stringWithFormat:@"%@ : %@=%.3fs%@%@%@%@", source, result, duration, renderLatency, firstResult, pixelsPerByte, imageType]; } @end NS_ASSUME_NONNULL_END