diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 4e8a7a19a..d1c6c6a50 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -332,6 +332,9 @@ CC224E962066CA6D00BBA57F /* configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = CC224E952066CA6D00BBA57F /* configuration.json */; }; CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */; }; CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */; }; + CC35CEC320DD7F600006448D /* ASCollections.h in Headers */ = {isa = PBXBuildFile; fileRef = CC35CEC120DD7F600006448D /* ASCollections.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC35CEC420DD7F600006448D /* ASCollections.m in Sources */ = {isa = PBXBuildFile; fileRef = CC35CEC220DD7F600006448D /* ASCollections.m */; }; + CC35CEC620DD87280006448D /* ASCollectionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC35CEC520DD87280006448D /* ASCollectionsTests.m */; }; CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20811C3F76D600798563 /* ASPendingStateController.h */; settings = {ATTRIBUTES = (Private, ); }; }; CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20821C3F76D600798563 /* ASPendingStateController.mm */; }; CC3B208A1C3F7A5400798563 /* ASWeakSet.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20871C3F7A5400798563 /* ASWeakSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -839,6 +842,9 @@ CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = ""; }; CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = ""; }; CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMutableElementMap.m; sourceTree = ""; }; + CC35CEC120DD7F600006448D /* ASCollections.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASCollections.h; sourceTree = ""; }; + CC35CEC220DD7F600006448D /* ASCollections.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASCollections.m; sourceTree = ""; }; + CC35CEC520DD87280006448D /* ASCollectionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASCollectionsTests.m; sourceTree = ""; }; CC3B20811C3F76D600798563 /* ASPendingStateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPendingStateController.h; sourceTree = ""; }; CC3B20821C3F76D600798563 /* ASPendingStateController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPendingStateController.mm; sourceTree = ""; }; CC3B20871C3F7A5400798563 /* ASWeakSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakSet.h; sourceTree = ""; }; @@ -1115,6 +1121,8 @@ 058D09B1195D04C000B7D73C /* Source */ = { isa = PBXGroup; children = ( + CC35CEC120DD7F600006448D /* ASCollections.h */, + CC35CEC220DD7F600006448D /* ASCollections.m */, 058D0A42195D058D00B7D73C /* Base */, CCE04B1D1E313E99006AEBBB /* Collection Data Adapter */, DE89C1691DCEB9CC00D49D74 /* Debug */, @@ -1238,6 +1246,7 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( + CC35CEC520DD87280006448D /* ASCollectionsTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m */, 696FCB301D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm */, @@ -2019,6 +2028,7 @@ CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */, + CC35CEC320DD7F600006448D /* ASCollections.h in Headers */, CC7AF196200D9BD500A21BDE /* ASExperimentalFeatures.h in Headers */, CCCCCCDF1EC3EF060087FE10 /* ASTextRunDelegate.h in Headers */, 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */, @@ -2300,6 +2310,7 @@ 1A6C00111FAB4EDD00D05926 /* ASCornerLayoutSpecSnapshotTests.mm in Sources */, 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, + CC35CEC620DD87280006448D /* ASCollectionsTests.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, E52AC9C01FEA916C00AA4040 /* ASRectMapTests.m in Sources */, CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */, @@ -2444,6 +2455,7 @@ CCA282B51E9EA7310037E8B7 /* ASTipsController.m in Sources */, B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.m in Sources */, + CC35CEC420DD7F600006448D /* ASCollections.m in Sources */, 68FC85E61CE29B9400EDD713 /* ASNavigationController.m in Sources */, CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.m in Sources */, 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index e74bac89d..ebb50c45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Clean up C-function `extern` decorators. [Adlai Holler](https://github.com/Adlai-Holler) - Add an experiment to reduce work involved in collection teardown. [Adlai Holler](https://github.com/Adlai-Holler) - Optimize layout flattening, particularly reducing retain/release operations. [Adlai Holler](https://github.com/Adlai-Holler) +- Create a method to transfer strong C-arrays into immutable NSArrays, reducing retain/release traffic. [Adlai Holler](https://github.com/Adlai-Holler) ## 2.7 - Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index d19e59898..555707f3d 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -23,6 +23,7 @@ #import #import #import +#import #import #import #import @@ -751,19 +752,7 @@ - (NSIndexPath *)convertIndexPathToCollectionNode:(NSIndexPath *)indexPath - (NSArray *)convertIndexPathsToCollectionNode:(NSArray *)indexPaths { - if (indexPaths == nil) { - return nil; - } - - NSMutableArray *indexPathsArray = [NSMutableArray arrayWithCapacity:indexPaths.count]; - - for (NSIndexPath *indexPathInView in indexPaths) { - NSIndexPath *indexPath = [self convertIndexPathToCollectionNode:indexPathInView]; - if (indexPath != nil) { - [indexPathsArray addObject:indexPath]; - } - } - return indexPathsArray; + return ASArrayByFlatMapping(indexPaths, NSIndexPath *viewIndexPath, [self convertIndexPathToCollectionNode:viewIndexPath]); } - (ASCellNode *)supplementaryNodeForElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath @@ -2225,13 +2214,7 @@ - (void)nodesDidRelayout:(NSArray *)nodes return; } - NSMutableArray *uikitIndexPaths = [NSMutableArray arrayWithCapacity:nodes.count]; - for (ASCellNode *node in nodes) { - NSIndexPath *uikitIndexPath = [self indexPathForNode:node]; - if (uikitIndexPath != nil) { - [uikitIndexPaths addObject:uikitIndexPath]; - } - } + auto uikitIndexPaths = ASArrayByFlatMapping(nodes, ASCellNode *node, [self indexPathForNode:node]); [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:uikitIndexPaths batched:NO]; diff --git a/Source/ASCollections.h b/Source/ASCollections.h new file mode 100644 index 000000000..bc36864f4 --- /dev/null +++ b/Source/ASCollections.h @@ -0,0 +1,42 @@ +// +// ASCollections.h +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray<__covariant ObjectType> (ASCollections) + +/** + * Create an immutable NSArray from a C-array of strong pointers. + * + * Note: The memory for the array you pass in will be zero'd (to prevent ARC from releasing + * the references when the array goes out of scope.) + * + * Can be combined with vector like: + * vector vec; + * vec.push_back(@"foo"); + * vec.push_back(@"bar"); + * NSArray *arr = [NSArray arrayTransferring:vec.data() count:vec.size()] + * ** vec is now { nil, nil } ** + * + * Unfortunately making a convenience method to do this is currently impossible because + * vector can't be converted to vector by the compiler (silly). + * + * See the private __CFArrayCreateTransfer function. + */ ++ (NSArray *)arrayByTransferring:(ObjectType _Nonnull __strong * _Nonnull)pointers + count:(NSUInteger)count NS_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASCollections.m b/Source/ASCollections.m new file mode 100644 index 000000000..3d9793d04 --- /dev/null +++ b/Source/ASCollections.m @@ -0,0 +1,65 @@ +// +// ASCollections.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASCollections.h" + +/** + * A private allocator that signals to our retain callback to skip the retain. + * It behaves the same as the default allocator, but acts as a signal that we + * are creating a transfer array so we should skip the retain. + */ +static CFAllocatorRef gTransferAllocator; + +static const void *ASTransferRetain(CFAllocatorRef allocator, const void *val) { + if (allocator == gTransferAllocator) { + // Transfer allocator. Ignore retain and pass through. + return val; + } else { + // Other allocator. Retain like normal. + // This happens when they make a mutable copy. + return (&kCFTypeArrayCallBacks)->retain(allocator, val); + } +} + +@implementation NSArray (ASCollections) + ++ (NSArray *)arrayByTransferring:(__strong id *)pointers count:(NSUInteger)count NS_RETURNS_RETAINED +{ + // Custom callbacks that point to our ASTransferRetain callback. + static CFArrayCallBacks callbacks; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + callbacks = kCFTypeArrayCallBacks; + callbacks.retain = ASTransferRetain; + CFAllocatorContext ctx; + CFAllocatorGetContext(NULL, &ctx); + gTransferAllocator = CFAllocatorCreate(NULL, &ctx); + }); + + // NSZeroArray fast path. + if (count == 0) { + return @[]; // Does not actually call +array when optimized. + } + + // NSSingleObjectArray fast path. Retain/release here is worth it. + if (count == 1) { + NSArray *result = [[NSArray alloc] initWithObjects:pointers count:1]; + pointers[0] = nil; + return result; + } + + NSArray *result = (__bridge_transfer NSArray *)CFArrayCreate(gTransferAllocator, (void *)pointers, count, &callbacks); + memset(pointers, 0, count * sizeof(id)); + return result; +} + +@end diff --git a/Source/ASExperimentalFeatures.m b/Source/ASExperimentalFeatures.m index e42421905..dea872b36 100644 --- a/Source/ASExperimentalFeatures.m +++ b/Source/ASExperimentalFeatures.m @@ -12,6 +12,8 @@ #import +#import + NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags) { NSArray *allNames = ASCreateOnce((@[@"exp_graphics_contexts", diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 241816d0f..003b81936 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -24,6 +24,7 @@ #import #import #import +#import #import #import #import diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 8a1c73bf4..227b13f21 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -96,6 +96,7 @@ #import #import #import +#import #import #import #import diff --git a/Source/Base/ASBaseDefines.h b/Source/Base/ASBaseDefines.h index 3adcf3aad..54a7b8594 100755 --- a/Source/Base/ASBaseDefines.h +++ b/Source/Base/ASBaseDefines.h @@ -214,13 +214,18 @@ /** * Create a new array by mapping `collection` over `work`, ignoring nil. */ -#define ASArrayByFlatMapping(collection, decl, work) ({ \ - NSMutableArray *a = [[NSMutableArray alloc] init]; \ - for (decl in collection) {\ - id result = work; \ - if (result != nil) { \ - [a addObject:result]; \ +#define ASArrayByFlatMapping(collectionArg, decl, work) ({ \ + id __collection = collectionArg; \ + NSArray *__result; \ + if (__collection) { \ + id __buf[[__collection count]]; \ + NSUInteger __i = 0; \ + for (decl in __collection) {\ + if ((__buf[__i] = work)) { \ + __i++; \ + } \ } \ + __result = [NSArray arrayByTransferring:__buf count:__i]; \ } \ - a; \ + __result; \ }) diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 548fa0181..706ee9c87 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -22,6 +22,7 @@ #import #import #import +#import #import #import #import diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index 2734e9649..c6e818ab0 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -17,6 +17,7 @@ #import #import #import +#import #import #import #import @@ -102,9 +103,9 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte return [[ASCollectionLayoutState alloc] initWithContext:context]; } - NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, - ASCollectionElement *element, - [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + NSArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); if (children.count == 0) { return [[ASCollectionLayoutState alloc] initWithContext:context]; } diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index de06f8567..2007c28da 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -19,6 +19,7 @@ #import +#import #import #import #import @@ -236,7 +237,7 @@ - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED queue.push_back({sublayout, sublayout.position}); } - auto flattenedSublayouts = [[NSMutableArray alloc] init]; + std::vector flattenedSublayouts; while (!queue.empty()) { const Context context = std::move(queue.front()); @@ -254,9 +255,9 @@ - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED size:layout.size position:absolutePosition sublayouts:@[]]; - [flattenedSublayouts addObject:newLayout]; + flattenedSublayouts.push_back(newLayout); } else { - [flattenedSublayouts addObject:layout]; + flattenedSublayouts.push_back(layout); } } else if (sublayoutsCount > 0) { // Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue. @@ -268,7 +269,10 @@ - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED } } - ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts]; + NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()]; + // flattenedSublayouts is now all nils. + + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array]; // All flattened layouts must have this flag enabled // to ensure sublayout elements are retained until the layouts are applied. layout.retainSublayoutLayoutElements = YES; diff --git a/Source/Layout/ASLayoutSpec.mm b/Source/Layout/ASLayoutSpec.mm index 7b70df4e3..d52702fba 100644 --- a/Source/Layout/ASLayoutSpec.mm +++ b/Source/Layout/ASLayoutSpec.mm @@ -20,6 +20,7 @@ #import +#import #import #import #import diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index 35d925fb0..f35b656e3 100644 --- a/Source/Private/_ASHierarchyChangeSet.mm +++ b/Source/Private/_ASHierarchyChangeSet.mm @@ -17,6 +17,7 @@ #import #import +#import #import #import #import diff --git a/Tests/ASCollectionsTests.m b/Tests/ASCollectionsTests.m new file mode 100644 index 000000000..17857a6e4 --- /dev/null +++ b/Tests/ASCollectionsTests.m @@ -0,0 +1,59 @@ +// +// ASCollectionsTests.m +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@interface ASCollectionsTests : XCTestCase + +@end + +@implementation ASCollectionsTests + +- (void)testTransferArray { + id objs[2]; + objs[0] = [NSObject new]; + id o0 = objs[0]; + objs[1] = [NSObject new]; + __weak id w0 = objs[0]; + __weak id w1 = objs[1]; + CFTypeRef cf0 = (__bridge CFTypeRef)objs[0]; + CFTypeRef cf1 = (__bridge CFTypeRef)objs[1]; + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSArray *arr = [NSArray arrayByTransferring:objs count:2]; + XCTAssertNil(objs[0]); + XCTAssertNil(objs[1]); + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSArray *immutableCopy = [arr copy]; + XCTAssertEqual(immutableCopy, arr); + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + NSMutableArray *mc = [arr mutableCopy]; + XCTAssertEqual(CFGetRetainCount(cf0), 3); + XCTAssertEqual(CFGetRetainCount(cf1), 2); + arr = nil; + immutableCopy = nil; + XCTAssertEqual(CFGetRetainCount(cf0), 2); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + [mc removeObjectAtIndex:0]; + XCTAssertEqual(CFGetRetainCount(cf0), 1); + XCTAssertEqual(CFGetRetainCount(cf1), 1); + [mc removeObjectAtIndex:0]; + XCTAssertEqual(CFGetRetainCount(cf0), 1); + XCTAssertNil(w1); + o0 = nil; + XCTAssertNil(w0); +} + +@end