From 25de53bb13c807f5e59432b2097822c60d8d3b9c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 15 Sep 2016 13:24:19 -0700 Subject: [PATCH] [ASDisplayNode] Add Event Tracing to Help Debugging (#2243) * Add some simple event logging for ASDisplayNode Improve the tracing * Add header to copy files phase * Make event header public --- AsyncDisplayKit.xcodeproj/project.pbxproj | 12 +++ AsyncDisplayKit/ASDisplayNode+Beta.h | 29 +++++++ AsyncDisplayKit/ASDisplayNode.mm | 80 ++++++++++++++++--- AsyncDisplayKit/ASDisplayNodeExtras.h | 2 +- AsyncDisplayKit/Details/ASEnvironment.h | 2 +- AsyncDisplayKit/Details/ASEnvironment.mm | 52 ++++++++++++ AsyncDisplayKit/Details/ASTraceEvent.h | 25 ++++++ AsyncDisplayKit/Details/ASTraceEvent.m | 60 ++++++++++++++ .../Private/ASDisplayNode+FrameworkPrivate.h | 35 +++++++- .../Private/ASDisplayNodeInternal.h | 5 +- .../Private/ASObjectDescriptionHelpers.h | 10 +-- .../Private/ASObjectDescriptionHelpers.m | 30 +++++-- 12 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 AsyncDisplayKit/Details/ASTraceEvent.h create mode 100644 AsyncDisplayKit/Details/ASTraceEvent.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 119f3555d1..f1f46d1606 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -428,6 +428,10 @@ CC446A311D80AAE00071FD03 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CC446A2E1D80AAE00071FD03 /* ASObjectDescriptionHelpers.m */; }; CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; }; CC4981BD1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */; }; + CC4C2A771D88E3BF0039ACAB /* ASTraceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC4C2A781D88E3BF0039ACAB /* ASTraceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.m */; }; + CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.m */; }; + CC4C2A7A1D8902350039ACAB /* ASTraceEvent.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */; }; CC54A81C1D70079800296A24 /* ASDispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = CC54A81B1D70077A00296A24 /* ASDispatch.h */; }; CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC54A81D1D7008B300296A24 /* ASDispatchTests.m */; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; }; @@ -646,6 +650,7 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( + CC4C2A7A1D8902350039ACAB /* ASTraceEvent.h in CopyFiles */, CC88F7AE1D80AF5E000D6D4E /* ASObjectDescriptionHelpers.h in CopyFiles */, F7CE6C981D2CDB5800BE4C15 /* ASInternalHelpers.h in CopyFiles */, F7CE6CB71D2CE2D000BE4C15 /* ASLayoutableExtensibility.h in CopyFiles */, @@ -1111,6 +1116,8 @@ CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = ""; }; CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSIndexSet+ASHelpers.h"; sourceTree = ""; }; CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSIndexSet+ASHelpers.m"; sourceTree = ""; }; + CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTraceEvent.h; sourceTree = ""; }; + CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTraceEvent.m; sourceTree = ""; }; CC54A81B1D70077A00296A24 /* ASDispatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASDispatch.h; sourceTree = ""; }; CC54A81D1D7008B300296A24 /* ASDispatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDispatchTests.m; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; @@ -1428,6 +1435,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC4C2A751D88E3BF0039ACAB /* ASTraceEvent.h */, + CC4C2A761D88E3BF0039ACAB /* ASTraceEvent.m */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1860,6 +1869,7 @@ 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, 764D83D51C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h in Headers */, E5711A2C1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */, + CC4C2A771D88E3BF0039ACAB /* ASTraceEvent.h in Headers */, 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */, CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, DE4843DC1C93EAC100A1F33B /* ASLayoutTransition.h in Headers */, @@ -2183,6 +2193,7 @@ 257754AE1BEE44CD00737CA5 /* ASTextKitRenderer+Positioning.mm in Sources */, ACF6ED2E1B17843500DA7C62 /* ASRatioLayoutSpec.mm in Sources */, AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */, + CC4C2A781D88E3BF0039ACAB /* ASTraceEvent.m in Sources */, 205F0E121B371BD7007741D0 /* ASScrollDirection.m in Sources */, 9C8898BB1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, @@ -2365,6 +2376,7 @@ B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.m in Sources */, 68FC85E61CE29B9400EDD713 /* ASNavigationController.m in Sources */, + CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.m in Sources */, 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, 34EFC7661B701CD200AD841F /* ASRelativeSize.mm in Sources */, diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index 939a003038..9a4051402e 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -10,6 +10,7 @@ #import "ASDisplayNode.h" #import "ASLayoutRangeType.h" +#import "ASTraceEvent.h" NS_ASSUME_NONNULL_BEGIN @@ -18,6 +19,20 @@ void ASPerformBlockOnMainThread(void (^block)()); void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT ASDISPLAYNODE_EXTERN_C_END +#ifndef ASDISPLAYNODE_EVENTLOG_CAPACITY +#define ASDISPLAYNODE_EVENTLOG_CAPACITY 20 +#endif + +#ifndef ASDISPLAYNODE_EVENTLOG_ENABLE +#define ASDISPLAYNODE_EVENTLOG_ENABLE DEBUG +#endif + +#if ASDISPLAYNODE_EVENTLOG_ENABLE +#define ASDisplayNodeLogEvent(node, ...) [node _logEventWithBacktrace:[NSThread callStackSymbols] format:__VA_ARGS__] +#else +#define ASDisplayNodeLogEvent(node, ...) +#endif + /** * Bitmask to indicate what performance measurements the cell should record. */ @@ -110,6 +125,20 @@ extern NSString *const ASDisplayNodeLayoutGenerationNumberOfPassesKey; */ + (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode; +#if ASDISPLAYNODE_EVENTLOG_ENABLE + +/** + * The primitive event tracing method. You shouldn't call this. Use the ASDisplayNodeLogEvent macro instead. + */ +- (void)_logEventWithBacktrace:(NSArray *)backtrace format:(NSString *)format, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * @abstract The most recent trace events for this node. Max count is ASDISPLAYNODE_EVENTLOG_CAPACITY. + */ +@property (readonly, copy) NSArray *eventLog; + +#endif + @end NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 203afd65f2..3f9c111a91 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -308,6 +308,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)_initializeInstance { [self _staticInitialize]; + _eventLogHead = -1; _contentsScaleForDisplay = ASScreenScale(); _displaySentinel = [[ASSentinel alloc] init]; @@ -323,6 +324,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _flags.canClearContentsOfLayer = YES; _flags.canCallSetNeedsDisplayOfLayer = YES; + ASDisplayNodeLogEvent(self, @"init"); } - (instancetype)init @@ -428,7 +430,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDisplayNodeAssertMainThread(); // Synchronous nodes may not be able to call the hierarchy notifications, so only enforce for regular nodes. - ASDisplayNodeAssert(_flags.synchronous || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating; interfaceState: %lu, %@", (unsigned long)_interfaceState, self); + ASDisplayNodeAssert(_flags.synchronous || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating. Node: %@", self); self.asyncLayer.asyncDelegate = nil; _view.asyncdisplaykit_node = nil; @@ -586,6 +588,51 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } +#if ASDISPLAYNODE_EVENTLOG_ENABLE +- (void)_logEventWithBacktrace:(NSArray *)backtrace format:(NSString *)format, ... +{ + va_list args; + va_start(args, format); + ASTraceEvent *event = [[ASTraceEvent alloc] initWithObject:self + backtrace:backtrace + format:format + arguments:args]; + va_end(args); + + ASDN::MutexLocker l(__instanceLock__); + // Create the array if needed. + if (_eventLog == nil) { + _eventLog = [NSMutableArray arrayWithCapacity:ASDISPLAYNODE_EVENTLOG_CAPACITY]; + } + + // Increment the head index. + _eventLogHead = (_eventLogHead + 1) % ASDISPLAYNODE_EVENTLOG_CAPACITY; + if (_eventLogHead < _eventLog.count) { + [_eventLog replaceObjectAtIndex:_eventLogHead withObject:event]; + } else { + [_eventLog insertObject:event atIndex:_eventLogHead]; + } +} + +- (NSArray *)eventLog +{ + ASDN::MutexLocker l(__instanceLock__); + NSUInteger tail = (_eventLogHead + 1); + NSUInteger count = _eventLog.count; + + NSMutableArray *result = [NSMutableArray array]; + + // Start from `tail` and go through array, wrapping around when we exceed end index. + for (NSUInteger actualIndex = 0; actualIndex < ASDISPLAYNODE_EVENTLOG_CAPACITY; actualIndex++) { + NSInteger ringIndex = (tail + actualIndex) % ASDISPLAYNODE_EVENTLOG_CAPACITY; + if (ringIndex < count) { + [result addObject:_eventLog[ringIndex]]; + } + } + return result; +} +#endif + - (UIView *)view { ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); @@ -1646,6 +1693,7 @@ static bool disableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASD _subnodes = [[NSMutableArray alloc] init]; } + ASDisplayNodeLogEvent(self, @"%@ %@", NSStringFromSelector(_cmd), subnode); [_subnodes addObject:subnode]; // This call will apply our .hierarchyState to the new subnode. @@ -1701,6 +1749,7 @@ static bool disableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASD if (!_subnodes) _subnodes = [[NSMutableArray alloc] init]; + ASDisplayNodeLogEvent(self, @"%@: %@", NSStringFromSelector(_cmd), subnode); [_subnodes insertObject:subnode atIndex:subnodeIndex]; [subnode __setSupernode:self]; @@ -1945,6 +1994,7 @@ static NSInteger incrementIfFound(NSInteger i) { return; } + ASDisplayNodeLogEvent(self, @"%@: %@", NSStringFromSelector(_cmd), subnode); [_subnodes removeObjectIdenticalTo:subnode]; [subnode __setSupernode:nil]; @@ -2157,6 +2207,7 @@ static NSInteger incrementIfFound(NSInteger i) { } if (supernodeDidChange) { + ASDisplayNodeLogEvent(self, @"supernodeDidChange: %@, oldValue = %@", ASObjectDescriptionMakeTiny(newSupernode), ASObjectDescriptionMakeTiny(oldSupernode)); // Hierarchy state ASHierarchyState stateToEnterOrExit = (newSupernode ? newSupernode.hierarchyState : oldSupernode.hierarchyState); @@ -2422,11 +2473,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) ASLayoutableValidateLayout(layout); #endif } + ASDisplayNodeLogEvent(self, @"computedLayout: %@", layout); return [layout filteredNodeLayoutTree]; } else { // If neither -layoutSpecThatFits: nor -calculateSizeThatFits: is overridden by subclassses, preferredFrameSize should be used, // assume that the default implementation of -calculateSizeThatFits: returns it. CGSize size = [self calculateSizeThatFits:constrainedSize.max]; + ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size)); return [ASLayout layoutWithLayoutable:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil]; } } @@ -2565,6 +2618,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) { ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeLogEvent(self, @"didLoad"); for (ASDisplayNodeDidLoadBlock block in _onDidLoadBlocks) { block(self); } @@ -2765,8 +2819,10 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) if (nowPreload != wasPreload) { if (nowPreload) { + ASDisplayNodeLogEvent(self, @"didEnterPreloadState: %@", NSStringFromASInterfaceState(newState)); [self didEnterPreloadState]; } else { + ASDisplayNodeLogEvent(self, @"didExitPreloadState: %@", NSStringFromASInterfaceState(newState)); [self didExitPreloadState]; } } @@ -2813,8 +2869,10 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } if (nowDisplay) { + ASDisplayNodeLogEvent(self, @"didEnterDisplayState: %@", NSStringFromASInterfaceState(newState)); [self didEnterDisplayState]; } else { + ASDisplayNodeLogEvent(self, @"didExitDisplayState: %@", NSStringFromASInterfaceState(newState)); [self didExitDisplayState]; } } @@ -2826,8 +2884,10 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) if (nowVisible != wasVisible) { if (nowVisible) { + ASDisplayNodeLogEvent(self, @"didEnterVisibleState: %@", NSStringFromASInterfaceState(newState)); [self didEnterVisibleState]; } else { + ASDisplayNodeLogEvent(self, @"didExitVisibleState: %@", NSStringFromASInterfaceState(newState)); [self didExitVisibleState]; } } @@ -2855,6 +2915,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) if (interfaceState == ASInterfaceStateNone) { return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. } + ASDisplayNodeLogEvent(self, @"%@ %@", NSStringFromSelector(_cmd), NSStringFromASInterfaceState(interfaceState)); ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) { node.interfaceState &= (~interfaceState); }); @@ -2929,10 +2990,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } } - - if (newState != oldState) { - LOG(@"setHierarchyState: oldState = %lu, newState = %lu", (unsigned long)oldState, (unsigned long)newState); - } + + ASDisplayNodeLogEvent(self, @"setHierarchyState: oldState = %@, newState = %@", NSStringFromASHierarchyState(oldState), NSStringFromASHierarchyState(newState)); } - (void)enterHierarchyState:(ASHierarchyState)hierarchyState @@ -2979,6 +3038,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) { ASDisplayNodeAssertMainThread(); + ASDisplayNodeLogEvent(self, @"displayWillStart"); // in case current node takes longer to display than it's subnodes, treat it as a dependent node [self _pendingNodeWillDisplay:self]; @@ -2989,6 +3049,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) { ASDisplayNodeAssertMainThread(); + ASDisplayNodeLogEvent(self, @"displayDidFinish"); [self _pendingNodeDidDisplay:self]; [_supernode subnodeDisplayDidFinish:self]; @@ -3338,15 +3399,15 @@ static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; } if (self.layerBacked) { - CALayer *rootLayer = self.layer; + CALayer *rootLayer = _layer; CALayer *nextLayer = rootLayer; while ((nextLayer = rootLayer.superlayer) != nil) { rootLayer = nextLayer; } - return [self.layer convertRect:self.threadSafeBounds toLayer:rootLayer]; + return [_layer convertRect:self.threadSafeBounds toLayer:rootLayer]; } else { - return [self.view convertRect:self.threadSafeBounds toView:nil]; + return [_view convertRect:self.threadSafeBounds toView:nil]; } } @@ -3396,6 +3457,7 @@ static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; { if (ASEnvironmentTraitCollectionIsEqualToASEnvironmentTraitCollection(environmentTraitCollection, _environmentState.environmentTraitCollection) == NO) { _environmentState.environmentTraitCollection = environmentTraitCollection; + ASDisplayNodeLogEvent(self, @"asyncTraitCollectionDidChange: %@", NSStringFromASEnvironmentTraitCollection(environmentTraitCollection)); [self asyncTraitCollectionDidChange]; } } @@ -3411,7 +3473,7 @@ ASEnvironmentLayoutExtensibilityForwarding - (void)asyncTraitCollectionDidChange { - + // Subclass override } #if TARGET_OS_TV diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 5ee5a53c0c..6888d63bba 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -36,7 +36,7 @@ ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState return ((interfaceState & ASInterfaceStateMeasureLayout) == ASInterfaceStateMeasureLayout); } -ASDISPLAYNODE_INLINE NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfaceState) +__unused static NSString * _Nonnull NSStringFromASInterfaceState(ASInterfaceState interfaceState) { NSMutableArray *states = [NSMutableArray array]; if (interfaceState == ASInterfaceStateNone) { diff --git a/AsyncDisplayKit/Details/ASEnvironment.h b/AsyncDisplayKit/Details/ASEnvironment.h index 42f452ee2a..20439eecd3 100644 --- a/AsyncDisplayKit/Details/ASEnvironment.h +++ b/AsyncDisplayKit/Details/ASEnvironment.h @@ -77,7 +77,7 @@ extern ASEnvironmentTraitCollection ASEnvironmentTraitCollectionMakeDefault(); extern ASEnvironmentTraitCollection ASEnvironmentTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection); extern BOOL ASEnvironmentTraitCollectionIsEqualToASEnvironmentTraitCollection(ASEnvironmentTraitCollection lhs, ASEnvironmentTraitCollection rhs); - +extern NSString *NSStringFromASEnvironmentTraitCollection(ASEnvironmentTraitCollection traits); #pragma mark - ASEnvironmentState typedef struct ASEnvironmentState { diff --git a/AsyncDisplayKit/Details/ASEnvironment.mm b/AsyncDisplayKit/Details/ASEnvironment.mm index d1d07c8fd9..41ff455f3b 100644 --- a/AsyncDisplayKit/Details/ASEnvironment.mm +++ b/AsyncDisplayKit/Details/ASEnvironment.mm @@ -10,6 +10,7 @@ #import "ASEnvironmentInternal.h" #import "ASAvailability.h" +#import "ASObjectDescriptionHelpers.h" ASEnvironmentLayoutOptionsState ASEnvironmentLayoutOptionsStateMakeDefault() { @@ -61,6 +62,57 @@ BOOL ASEnvironmentTraitCollectionIsEqualToASEnvironmentTraitCollection(ASEnviron CGSizeEqualToSize(lhs.containerSize, rhs.containerSize); } +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) { + switch (idiom) { + case UIUserInterfaceIdiomTV: + return @"TV"; + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + case UIUserInterfaceIdiomCarPlay: + return @"CarPlay"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIForceTouchCapability(UIForceTouchCapability capability) { + switch (capability) { + case UIForceTouchCapabilityAvailable: + return @"Available"; + case UIForceTouchCapabilityUnavailable: + return @"Unavailable"; + default: + return @"Unknown"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceSizeClass(UIUserInterfaceSizeClass sizeClass) { + switch (sizeClass) { + case UIUserInterfaceSizeClassCompact: + return @"Compact"; + case UIUserInterfaceSizeClassRegular: + return @"Regular"; + default: + return @"Unspecified"; + } +} + +NSString *NSStringFromASEnvironmentTraitCollection(ASEnvironmentTraitCollection traits) +{ + NSMutableArray *props = [NSMutableArray array]; + [props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }]; + [props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }]; + [props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }]; + [props addObject:@{ @"verticalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.verticalSizeClass) }]; + [props addObject:@{ @"forceTouchCapability": AS_NSStringFromUIForceTouchCapability(traits.forceTouchCapability) }]; + return ASObjectDescriptionMakeWithoutObject(props); +} + ASEnvironmentState ASEnvironmentStateMakeDefault() { return (ASEnvironmentState) { diff --git a/AsyncDisplayKit/Details/ASTraceEvent.h b/AsyncDisplayKit/Details/ASTraceEvent.h new file mode 100644 index 0000000000..3253e3cbe6 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTraceEvent.h @@ -0,0 +1,25 @@ +// +// ASTraceEvent.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/13/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface ASTraceEvent : NSObject + +/** + * This method is dealloc safe. + */ +- (instancetype)initWithObject:(id)object + backtrace:(NSArray *)backtrace + format:(NSString *)format + arguments:(va_list)arguments NS_FORMAT_FUNCTION(3,0); + +@property (nonatomic, readonly) NSArray *backtrace; +@property (nonatomic, strong, readonly) NSString *message; +@property (nonatomic, readonly) NSTimeInterval timestamp; + +@end diff --git a/AsyncDisplayKit/Details/ASTraceEvent.m b/AsyncDisplayKit/Details/ASTraceEvent.m new file mode 100644 index 0000000000..e17cb3144a --- /dev/null +++ b/AsyncDisplayKit/Details/ASTraceEvent.m @@ -0,0 +1,60 @@ +// +// ASTraceEvent.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/13/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASTraceEvent.h" +#import +#import "ASObjectDescriptionHelpers.h" + +@interface ASTraceEvent () +@property (nonatomic, strong, readonly) NSString *objectDescription; +@property (nonatomic, strong, readonly) NSString *threadDescription; +@end + +@implementation ASTraceEvent + +- (instancetype)initWithObject:(id)object backtrace:(NSArray *)backtrace format:(NSString *)format arguments:(va_list)args +{ + self = [super init]; + if (self != nil) { + static NSTimeInterval refTime; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + refTime = CACurrentMediaTime(); + }); + + // Create the format string passed to us. + _message = [[NSString alloc] initWithFormat:format arguments:args]; + + _objectDescription = ASObjectDescriptionMakeTiny(object); + + NSThread *thread = [NSThread currentThread]; + NSString *threadDescription = thread.name; + if (threadDescription.length == 0) { + if ([thread isMainThread]) { + threadDescription = @"Main"; + } else { + // Want these to be 4-chars to line up with "Main". It's possible that a collision could happen + // here but it's so unbelievably likely to impact development, the risk is acceptable. + NSString *ptrString = [NSString stringWithFormat:@"%p", thread]; + threadDescription = [ptrString substringFromIndex:MAX(0, ptrString.length - 4)]; + } + } + _threadDescription = threadDescription; + + _backtrace = backtrace; + _timestamp = CACurrentMediaTime() - refTime; + } + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ (%@) t=%7.3f: %@>", _objectDescription, _threadDescription, _timestamp, _message]; +} + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 22fa229af5..f103dca55f 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -50,16 +50,47 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState) ASHierarchyStateLayoutPending = 1 << 3 }; -inline BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) { return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending); } -inline BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState) +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState) { return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged); } +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRasterized(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized); +} + +ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesTransitioningSupernodes(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateTransitioningSupernodes) == ASHierarchyStateTransitioningSupernodes); +} + +__unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyState hierarchyState) +{ + NSMutableArray *states = [NSMutableArray array]; + if (hierarchyState == ASHierarchyStateNormal) { + [states addObject:@"Normal"]; + } + if (ASHierarchyStateIncludesRangeManaged(hierarchyState)) { + [states addObject:@"RangeManaged"]; + } + if (ASHierarchyStateIncludesLayoutPending(hierarchyState)) { + [states addObject:@"LayoutPending"]; + } + if (ASHierarchyStateIncludesRasterized(hierarchyState)) { + [states addObject:@"Rasterized"]; + } + if (ASHierarchyStateIncludesTransitioningSupernodes(hierarchyState)) { + [states addObject:@"TransitioningSupernodes"]; + } + return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]]; +} + @interface ASDisplayNode () { @protected diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index a3a609e6eb..b5bda3b879 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -121,7 +121,10 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo UIEdgeInsets _hitTestSlop; NSMutableArray *_subnodes; - + NSMutableArray *_eventLog; + // The index of the most recent log entry. -1 until first entry. + NSInteger _eventLogHead; + // Main thread only BOOL _automaticallyManagesSubnodes; _ASTransitionContext *_pendingLayoutTransitionContext; diff --git a/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.h b/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.h index e8e853e7b1..5c3899ef80 100644 --- a/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.h +++ b/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.h @@ -36,12 +36,10 @@ NS_ASSUME_NONNULL_BEGIN ASDISPLAYNODE_EXTERN_C_BEGIN -/** - * Returns e.g. - * - * Note: `object` param is autoreleasing so that this function is dealloc-safe. - * No, unsafe_unretained isn't acceptable here – the optimizer may deallocate object early. - */ +/// Useful for structs etc. Returns e.g. { position = (0 0); frame = (0 0; 50 50) } +NSString *ASObjectDescriptionMakeWithoutObject(NSArray * _Nullable propertyGroups); + +/// Returns e.g. NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray * _Nullable propertyGroups); /** diff --git a/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.m b/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.m index 261a87d175..baaed3137d 100644 --- a/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.m +++ b/AsyncDisplayKit/Private/ASObjectDescriptionHelpers.m @@ -10,7 +10,8 @@ #import #import "NSIndexSet+ASHelpers.h" -NSString *ASGetDescriptionValueString(id object) { +NSString *ASGetDescriptionValueString(id object) +{ if ([object isKindOfClass:[NSValue class]]) { // Use shortened NSValue descriptions NSValue *value = object; @@ -37,18 +38,33 @@ NSString *ASGetDescriptionValueString(id object) { return [object description]; } -NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray *propertyGroups) { - NSMutableString *str = [NSMutableString stringWithFormat:@"<%@: %p", [object class], object]; - +NSString *_ASObjectDescriptionMakePropertyList(NSArray * _Nullable propertyGroups) +{ NSMutableArray *components = [NSMutableArray array]; for (NSDictionary *properties in propertyGroups) { [properties enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [components addObject:[NSString stringWithFormat:@"%@ = %@", key, ASGetDescriptionValueString(obj)]]; }]; } - if (components.count > 0) { - [str appendString:@"; "]; - [str appendString:[components componentsJoinedByString:@"; "]]; + return [components componentsJoinedByString:@"; "]; +} + +NSString *ASObjectDescriptionMakeWithoutObject(NSArray * _Nullable propertyGroups) +{ + return [NSString stringWithFormat:@"{ %@ }", _ASObjectDescriptionMakePropertyList(propertyGroups)]; +} + +NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray *propertyGroups) +{ + if (object == nil) { + return @"(null)"; + } + + NSMutableString *str = [NSMutableString stringWithFormat:@"<%@: %p", [object class], object]; + + NSString *propList = _ASObjectDescriptionMakePropertyList(propertyGroups); + if (propList.length > 0) { + [str appendFormat:@"; %@", propList]; } [str appendString:@">"]; return str;