From b136e84b4e9f413b501a2cf91377b672b84aedf7 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 4 Aug 2018 07:33:53 -0700 Subject: [PATCH] Add an experimental framesetter cache in ASTextNode2 (#1063) * Add an experimental framesetter cache in ASTextNode2, and stop keeping framesetters around * Update configuration schema * Fix imports * Fix import again and remove set statement --- CHANGELOG.md | 1 + Schemas/configuration.json | 1 + Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.m | 3 +- .../TextExperiment/Component/ASTextLayout.h | 2 - .../TextExperiment/Component/ASTextLayout.m | 82 +++++++++++++++---- 6 files changed, 73 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ec5fff1d..b62de53597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Optimize ASDisplayNode -> ASNodeController reference by removing weak proxy and objc associated objects. [Adlai Holler](https://github.com/Adlai-Holler) - Remove CA transaction signpost injection because it causes more transactions and is too chatty. [Adlai Holler](https://github.com/Adlai-Holler) - Optimize display node accessibility by not creating attributed & non-attributed copies of hint, label, and value. [Adlai Holler](https://github.com/Adlai-Holler) +- Add an experimental feature that reuses CTFramesetter objects in ASTextNode2 to improve performance. [Adlai Holler](https://github.com/Adlai-Holler) ## 2.7 diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 71b5917294..29c4375a41 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -21,6 +21,7 @@ "exp_network_image_queue", "exp_dealloc_queue_v2", "exp_collection_teardown", + "exp_framesetter_cache" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index cc02d8a683..448deb87a3 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -27,6 +27,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalNetworkImageQueue = 1 << 5, // exp_network_image_queue ASExperimentalDeallocQueue = 1 << 6, // exp_dealloc_queue_v2 ASExperimentalCollectionTeardown = 1 << 7, // exp_collection_teardown + ASExperimentalFramesetterCache = 1 << 8, // exp_framesetter_cache ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.m b/Source/ASExperimentalFeatures.m index dea872b365..5b33fe50be 100644 --- a/Source/ASExperimentalFeatures.m +++ b/Source/ASExperimentalFeatures.m @@ -23,7 +23,8 @@ NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags @"exp_infer_layer_defaults", @"exp_network_image_queue", @"exp_dealloc_queue_v2", - @"exp_collection_teardown"])); + @"exp_collection_teardown", + @"exp_framesetter_cache"])); if (flags == ASExperimentalFeatureAll) { return allNames; diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.h b/Source/Private/TextExperiment/Component/ASTextLayout.h index 544d9f5f33..1a6625a3af 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.h +++ b/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -225,8 +225,6 @@ AS_EXTERN const CGSize ASTextContainerMaxSize; @property (nonatomic, readonly) NSAttributedString *text; ///< The text range in full text @property (nonatomic, readonly) NSRange range; -///< CTFrameSetter -@property (nonatomic, readonly) CTFramesetterRef frameSetter; ///< CTFrame @property (nonatomic, readonly) CTFrameRef frame; ///< Array of `ASTextLine`, no truncated diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.m b/Source/Private/TextExperiment/Component/ASTextLayout.m index d0aceee9ac..d3d316d14d 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.m +++ b/Source/Private/TextExperiment/Component/ASTextLayout.m @@ -16,11 +16,15 @@ // #import + +#import #import #import #import #import +#import + const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; typedef struct { @@ -320,7 +324,6 @@ dispatch_semaphore_signal(_lock); @property (nonatomic) NSAttributedString *text; @property (nonatomic) NSRange range; -@property (nonatomic) CTFramesetterRef frameSetter; @property (nonatomic) CTFrameRef frame; @property (nonatomic) NSArray *lines; @property (nonatomic) ASTextLine *truncatedLine; @@ -484,10 +487,71 @@ dispatch_semaphore_signal(_lock); frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft); } - // create CoreText objects - ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); + /* + * Framesetter cache. + * Framesetters can only be used by one thread at a time. + * Create a CFSet with no callbacks (raw pointers) to keep track of which + * framesetters are in use on other threads. If the one for our string is already in use, + * just create a new one. This should be pretty rare. + */ + static pthread_mutex_t busyFramesettersLock = PTHREAD_MUTEX_INITIALIZER; + static NSCache *framesetterCache; + static CFMutableSetRef busyFramesetters; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (ASActivateExperimentalFeature(ASExperimentalFramesetterCache)) { + framesetterCache = [[NSCache alloc] init]; + framesetterCache.name = @"org.TextureGroup.Texture.framesetterCache"; + busyFramesetters = CFSetCreateMutable(NULL, 0, NULL); + } + }); + + BOOL haveCached = NO, useCached = NO; + if (framesetterCache) { + // Check if there's one in the cache. + ctSetter = (__bridge_retained CTFramesetterRef)[framesetterCache objectForKey:text]; + + if (ctSetter) { + haveCached = YES; + + // Check-and-set busy on the cached one. + pthread_mutex_lock(&busyFramesettersLock); + BOOL busy = CFSetContainsValue(busyFramesetters, ctSetter); + if (!busy) { + CFSetAddValue(busyFramesetters, ctSetter); + useCached = YES; + } + pthread_mutex_unlock(&busyFramesettersLock); + + // Release if it was busy. + if (busy) { + CFRelease(ctSetter); + ctSetter = NULL; + } + } + } + + // Create a framesetter if needed. + if (!ctSetter) { + ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); + } + if (!ctSetter) FAIL_AND_RETURN ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFDictionaryRef)frameAttrs); + + // Return to cache. + if (framesetterCache) { + if (useCached) { + // If reused: mark available. + pthread_mutex_lock(&busyFramesettersLock); + CFSetRemoveValue(busyFramesetters, ctSetter); + pthread_mutex_unlock(&busyFramesettersLock); + } else if (!haveCached) { + // If first framesetter, add to cache. + [framesetterCache setObject:(__bridge id)ctSetter forKey:text]; + } + } + if (!ctFrame) FAIL_AND_RETURN lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); @@ -857,8 +921,7 @@ dispatch_semaphore_signal(_lock); if (attachments.count == 0) { attachments = attachmentRanges = attachmentRects = nil; } - - layout.frameSetter = ctSetter; + layout.frame = ctFrame; layout.lines = lines; layout.truncatedLine = truncatedLine; @@ -903,14 +966,6 @@ dispatch_semaphore_signal(_lock); return layouts; } -- (void)setFrameSetter:(CTFramesetterRef)frameSetter { - if (_frameSetter != frameSetter) { - if (frameSetter) CFRetain(frameSetter); - if (_frameSetter) CFRelease(_frameSetter); - _frameSetter = frameSetter; - } -} - - (void)setFrame:(CTFrameRef)frame { if (_frame != frame) { if (frame) CFRetain(frame); @@ -920,7 +975,6 @@ dispatch_semaphore_signal(_lock); } - (void)dealloc { - if (_frameSetter) CFRelease(_frameSetter); if (_frame) CFRelease(_frame); if (_lineRowsIndex) free(_lineRowsIndex); if (_lineRowsEdge) free(_lineRowsEdge);