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
This commit is contained in:
Adlai Holler 2018-08-04 07:33:53 -07:00 committed by GitHub
parent c5b1d09b49
commit b136e84b4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 73 additions and 17 deletions

View File

@ -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

View File

@ -21,6 +21,7 @@
"exp_network_image_queue",
"exp_dealloc_queue_v2",
"exp_collection_teardown",
"exp_framesetter_cache"
]
}
}

View File

@ -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
};

View File

@ -23,7 +23,8 @@ NSArray<NSString *> *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;

View File

@ -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

View File

@ -16,11 +16,15 @@
//
#import <AsyncDisplayKit/ASTextLayout.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASTextUtilities.h>
#import <AsyncDisplayKit/ASTextAttribute.h>
#import <AsyncDisplayKit/NSAttributedString+ASText.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <pthread.h>
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<NSAttributedString *, id> *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);