RenderCore/RCComputeRootLayout.mm (110 lines of code) (raw):

// (c) Facebook, Inc. and its affiliates. Confidential and proprietary. #import "RCComputeRootLayout.h" #import <unordered_map> #import <RenderCore/CKInternalHelpers.h> #import <RenderCore/CKMountable.h> #import <RenderCore/CKSizeRange.h> #import <RenderCore/RCLayout.h> // Considers NaNs equal to each other (unlike CGSizeEqualToSize). This is important for the layout cache // keys as identical keys that contain NaNs will be otherwise treated as different. static bool sizesAreEqual(const CGSize &lhs, const CGSize &rhs) { return CKFloatsEqual(lhs.width, rhs.width) && CKFloatsEqual(lhs.height, rhs.height); } struct RCLayoutCacheKey { CKSizeRange constrainingSize; CGSize parentSize; bool operator==(const RCLayoutCacheKey &other) const { return constrainingSize == other.constrainingSize && sizesAreEqual(parentSize, other.parentSize); } }; namespace std { template <> struct hash<RCLayoutCacheKey> { size_t operator()(const RCLayoutCacheKey &key) const noexcept { return key.constrainingSize.hash(); } }; } /** A layout cache is really just an unordered_map, under the covers. */ struct RCLayoutCache { std::unordered_map<id<CKMountable>, std::unordered_map<RCLayoutCacheKey, RCLayout>, RC::hash<id>> map; }; thread_local const RCLayoutCache *currentLayoutReadCache; thread_local RCLayoutCache *currentLayoutWriteCache; /** Recursively copies layout cache entries for the layout and all of its children. This ensures that the layout cache has comprehensive coverage of all component layouts, even when we get a cache hit. */ static void copyFromReadCacheToWriteCache(const RCLayout &layout, const RCLayoutCache *readCache, RCLayoutCache *writeCache) { const auto &matches = readCache->map.find(layout.component); if (matches != readCache->map.end()) { // Copy all entries in the readCache's map to the writeCache's map, // skipping any that are already present. writeCache->map[layout.component].insert( matches->second.begin(), matches->second.end() ); } if (layout.children) { for (const auto &child : *layout.children) { copyFromReadCacheToWriteCache(child.layout, readCache, writeCache); } } } RCLayout RCFetchOrComputeLayout(id<CKMountable> mountable, const CKSizeRange &sizeRange, CGSize parentSize, RCLayout (*layoutFunction)(id<CKMountable> mountable, const CKSizeRange &sizeRange, CGSize parentSize)) { const RCLayoutCacheKey key {sizeRange, parentSize}; if (currentLayoutWriteCache) { const auto it = currentLayoutWriteCache->map.find(mountable); if (it != currentLayoutWriteCache->map.end()) { const auto match = it->second.find(key); if (match != it->second.end()) { return match->second; } } } if (currentLayoutReadCache) { const auto it = currentLayoutReadCache->map.find(mountable); if (it != currentLayoutReadCache->map.end()) { const auto match = it->second.find(key); if (match != it->second.end()) { // Found a hit! Copy the cached layout for this node *and descendants* // from the read cache to the write cache, if there is one. if (currentLayoutWriteCache) { copyFromReadCacheToWriteCache( match->second, currentLayoutReadCache, currentLayoutWriteCache ); } return match->second; } } } const RCLayout layout = layoutFunction(mountable, sizeRange, parentSize); if (currentLayoutWriteCache) { currentLayoutWriteCache->map[mountable].emplace(std::make_pair(key, layout)); } return layout; } RCLayoutResult RCComputeRootLayout(id<CKMountable> model, const CKSizeRange &constrainingSize, std::shared_ptr<RCLayoutCache> cache) { const auto writeCache = std::make_shared<RCLayoutCache>(); if (cache) { // We expect the writeCache to have about as many elements as the readCache. // Reserve the appropriate number of buckets now to avoid rehashing later. writeCache->map.reserve(cache->map.size()); } // We don't expect nested root layouts, so the thread-local caches should generally be null. // But if a nested root layout *does* happen, we restore the previous caches before returning. const RCLayoutCache *const previousReadCache = currentLayoutReadCache; RCLayoutCache *const previousWriteCache = currentLayoutWriteCache; currentLayoutReadCache = cache.get(); currentLayoutWriteCache = writeCache.get(); RCLayout layout = [model layoutThatFits:constrainingSize parentSize:constrainingSize.max]; currentLayoutReadCache = previousReadCache; currentLayoutWriteCache = previousWriteCache; return { .layout = layout, .cache = writeCache, }; } BOOL RCLayoutCacheContainsEntryForMountable(const RCLayoutCache &cache, id<CKMountable> mountable) { return cache.map.find(mountable) != cache.map.end(); }