diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index bc8f2fe7a8..e03f91d94e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -29,6 +29,15 @@ #import "CGRect+ASConvenience.h" +/** + * If set, we will record all values set to attributedText into an array + * and once we get 2000, we'll write them all out into a plist file. + * + * This is useful for gathering realistic text data sets from apps for performance + * testing. + */ +#define AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS 0 + static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; @@ -439,7 +448,9 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } _attributedText = ASCleanseAttributedStringOfCoreTextAttributes(attributedText); - +#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS + [ASTextNode _registerAttributedText:_attributedText]; +#endif // Sync the truncation string with attributes from the updated _attributedString // Without this, the size calculation of the text with truncation applied will // not take into account the attributes of attributedText in the last line @@ -1364,6 +1375,30 @@ static NSAttributedString *DefaultTruncationAttributedString() return truncationMutableString; } +#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS ++ (void)_registerAttributedText:(NSAttributedString *)str +{ + static NSMutableArray *array; + static NSLock *lock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lock = [NSLock new]; + array = [NSMutableArray new]; + }); + [lock lock]; + [array addObject:str]; + if (array.count % 20 == 0) { + NSLog(@"Got %d strings", (int)array.count); + } + if (array.count == 2000) { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AttributedStrings.plist"]; + NSAssert([NSKeyedArchiver archiveRootObject:array toFile:path], nil); + NSLog(@"Saved to %@", path); + } + [lock unlock]; +} +#endif + @end @implementation ASTextNode (Deprecated) diff --git a/AsyncDisplayKitTests/ASPerformanceTestContext.h b/AsyncDisplayKitTests/ASPerformanceTestContext.h index 730445a024..2fe0523afa 100644 --- a/AsyncDisplayKitTests/ASPerformanceTestContext.h +++ b/AsyncDisplayKitTests/ASPerformanceTestContext.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN -typedef void (^ASTestPerformanceCaseBlock)(dispatch_block_t startMeasuring, dispatch_block_t stopMeasuring); +typedef void (^ASTestPerformanceCaseBlock)(NSUInteger i, dispatch_block_t startMeasuring, dispatch_block_t stopMeasuring); @interface ASPerformanceTestResult : NSObject @property (nonatomic, readonly) NSTimeInterval timePer1000; diff --git a/AsyncDisplayKitTests/ASPerformanceTestContext.m b/AsyncDisplayKitTests/ASPerformanceTestContext.m index d9e86efde6..628999922d 100644 --- a/AsyncDisplayKitTests/ASPerformanceTestContext.m +++ b/AsyncDisplayKitTests/ASPerformanceTestContext.m @@ -95,10 +95,10 @@ { __block CFTimeInterval time = 0; for (NSInteger i = 0; i < _iterationCount; i++) { - __block CFAbsoluteTime start = 0; + __block CFTimeInterval start = 0; __block BOOL calledStop = NO; @autoreleasepool { - block(^{ + block(i, ^{ ASDisplayNodeAssert(start == 0, @"Called startMeasuring block twice."); start = CACurrentMediaTime(); }, ^{ diff --git a/AsyncDisplayKitTests/ASTextNodePerformanceTests.m b/AsyncDisplayKitTests/ASTextNodePerformanceTests.m index 8d85e8cb56..6f0f1c1730 100644 --- a/AsyncDisplayKitTests/ASTextNodePerformanceTests.m +++ b/AsyncDisplayKitTests/ASTextNodePerformanceTests.m @@ -32,6 +32,54 @@ static NSString *const kTestCaseUIKitWithNoContext = @"UIKitNoContext"; static NSString *const kTestCaseUIKitWithFreshContext = @"UIKitFreshContext"; static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; ++ (NSArray *)realisticDataSet +{ + static NSArray *array; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *file = [[NSBundle bundleForClass:self] pathForResource:@"AttributedStringsFixture0" ofType:@"plist" inDirectory:@"TestResources"]; + if (file != nil) { + array = [NSKeyedUnarchiver unarchiveObjectWithFile:file]; + } + NSAssert([array isKindOfClass:[NSArray class]], nil); + NSSet *unique = [NSSet setWithArray:array]; + NSLog(@"Loaded realistic text data set with %d attributed strings, %d unique.", (int)array.count, (int)unique.count); + }); + return array; +} + +- (void)testPerformance_RealisticData +{ + NSArray *data = [self.class realisticDataSet]; + + CGSize maxSize = CGSizeMake(355, CGFLOAT_MAX); + CGSize __block uiKitSize, __block asdkSize; + + ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + NSAttributedString *text = data[i % data.count]; + startMeasuring(); + uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; + stopMeasuring(); + }]; + uiKitSize.width = ASCeilPixelValue(uiKitSize.width); + uiKitSize.height = ASCeilPixelValue(uiKitSize.height); + ctx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); + + [ctx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + ASTextNode *node = [[ASTextNode alloc] init]; + NSAttributedString *text = data[i % data.count]; + startMeasuring(); + node.attributedText = text; + asdkSize = [node measure:maxSize]; + stopMeasuring(); + }]; + ctx.results[kTestCaseASDK].userInfo[@"size"] = NSStringFromCGSize(asdkSize); + + ASXCTAssertEqualSizes(uiKitSize, asdkSize); + ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.2, 0.5); +} + - (void)testPerformance_TwoParagraphLatinNoTruncation { NSAttributedString *text = [ASTextNodePerformanceTests twoParagraphLatinText]; @@ -40,7 +88,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; CGSize __block uiKitSize, __block asdkSize; ASPerformanceTestContext *ctx = [[ASPerformanceTestContext alloc] init]; - [ctx addCaseWithName:kTestCaseUIKit block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; stopMeasuring(); @@ -49,7 +97,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; uiKitSize.height = ASCeilPixelValue(uiKitSize.height); ctx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); - [ctx addCaseWithName:kTestCaseASDK block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { ASTextNode *node = [[ASTextNode alloc] init]; startMeasuring(); node.attributedText = text; @@ -70,7 +118,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; CGSize __block uiKitSize, __block asdkSize; ASPerformanceTestContext *testCtx = [[ASPerformanceTestContext alloc] init]; - [testCtx addCaseWithName:kTestCaseUIKit block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [testCtx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); uiKitSize = [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil].size; stopMeasuring(); @@ -79,7 +127,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; uiKitSize.height = ASCeilPixelValue(uiKitSize.height); testCtx.results[kTestCaseUIKit].userInfo[@"size"] = NSStringFromCGSize(uiKitSize); - [testCtx addCaseWithName:kTestCaseASDK block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [testCtx addCaseWithName:kTestCaseASDK block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { ASTextNode *node = [[ASTextNode alloc] init]; startMeasuring(); node.attributedText = text; @@ -101,7 +149,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine; __block CGSize size; // nil context - [ctx addCaseWithName:kTestCaseUIKitWithNoContext block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKitWithNoContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); size = [text boundingRectWithSize:maxSize options:options context:nil].size; stopMeasuring(); @@ -109,7 +157,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; ctx.results[kTestCaseUIKitWithNoContext].userInfo[@"size"] = NSStringFromCGSize(size); // Fresh context - [ctx addCaseWithName:kTestCaseUIKitWithFreshContext block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKitWithFreshContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { NSStringDrawingContext *stringDrawingCtx = [[NSStringDrawingContext alloc] init]; startMeasuring(); size = [text boundingRectWithSize:maxSize options:options context:stringDrawingCtx].size; @@ -119,7 +167,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; // Reused context NSStringDrawingContext *stringDrawingCtx = [[NSStringDrawingContext alloc] init]; - [ctx addCaseWithName:kTestCaseUIKitWithReusedContext block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKitWithReusedContext block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); size = [text boundingRectWithSize:maxSize options:options context:stringDrawingCtx].size; stopMeasuring(); @@ -142,7 +190,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; // No caching, reused ctx NSStringDrawingContext *defaultCtx = [[NSStringDrawingContext alloc] init]; XCTAssertFalse([[defaultCtx valueForKey:@"cachesLayout"] boolValue]); - [ctx addCaseWithName:kTestCaseUIKit block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKit block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); uncachedSize = [text boundingRectWithSize:maxSize options:options context:defaultCtx].size; stopMeasuring(); @@ -153,7 +201,7 @@ static NSString *const kTestCaseUIKitWithReusedContext = @"UIKitReusedContext"; // Caching NSStringDrawingContext *cachingCtx = [[NSStringDrawingContext alloc] init]; [cachingCtx setValue:@YES forKey:@"cachesLayout"]; - [ctx addCaseWithName:kTestCaseUIKitPrivateCaching block:^(dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { + [ctx addCaseWithName:kTestCaseUIKitPrivateCaching block:^(NSUInteger i, dispatch_block_t _Nonnull startMeasuring, dispatch_block_t _Nonnull stopMeasuring) { startMeasuring(); cachedSize = [text boundingRectWithSize:maxSize options:options context:cachingCtx].size; stopMeasuring(); diff --git a/AsyncDisplayKitTests/TestResources/AttributedStringsFixture0.plist b/AsyncDisplayKitTests/TestResources/AttributedStringsFixture0.plist new file mode 100644 index 0000000000..1d5554e472 Binary files /dev/null and b/AsyncDisplayKitTests/TestResources/AttributedStringsFixture0.plist differ