Skip to content

Commit 4913aaa

Browse files
committed
[perf] box RenderItem behavior
Move RenderItem's lifecycle closures and reuse/transition/animation config into a single boxed reference (Storage); id/frame stay inline. Copying an item up the container tree is now one retain instead of ~13, with the public API preserved via computed forwards. Flat-layer scroll profile: RenderItem copy/teardown self-time dropped from ~839 to ~124 samples; scroll.flatSmall.5000 median ~0.113 -> ~0.097 ms.
1 parent 602962f commit 4913aaa

1 file changed

Lines changed: 95 additions & 36 deletions

File tree

ComposeUI/Sources/ComposeUI/ComposeNode/RenderItem/RenderItem.swift

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,16 @@ public struct RenderItem<T> {
5656
/// The frame of the renderable.
5757
public var frame: CGRect
5858

59+
/// The renderable's behavior: the lifecycle callbacks plus the reuse / transition / animation configuration.
60+
///
61+
/// This is boxed in a reference type so that copying a `RenderItem` — which the render walk does at every container
62+
/// level — is a single retain instead of copying ~10 escaping closures, and so the value type stays small (cheaper to
63+
/// store in, and tear down, the per-pass render dictionaries). `id` and `frame` stay inline because they are mutated
64+
/// per render pass.
65+
private let storage: Storage
66+
5967
/// The block to create a renderable.
60-
public let make: (RenderableMakeContext) -> T
68+
public var make: (RenderableMakeContext) -> T { storage.make }
6169

6270
/// The block to be called when the renderable is just made and is about to be inserted into the renderable hierarchy.
6371
///
@@ -66,7 +74,7 @@ public struct RenderItem<T> {
6674
/// after the insertion.
6775
///
6876
/// This is guaranteed to be the first call for the renderable's lifecycle in the renderable hierarchy.
69-
public let willInsert: ((T, RenderableInsertContext) -> Void)?
77+
public var willInsert: ((T, RenderableInsertContext) -> Void)? { storage.willInsert }
7078

7179
/// The block to be called when the renderable is just inserted into the renderable hierarchy.
7280
///
@@ -76,14 +84,14 @@ public struct RenderItem<T> {
7684
/// same as the new frame, because during the transition animation, the renderable maybe updated for additional changes.
7785
///
7886
/// This is not guaranteed to be called before the `update` block is called.
79-
public let didInsert: ((T, RenderableInsertContext) -> Void)?
87+
public var didInsert: ((T, RenderableInsertContext) -> Void)? { storage.didInsert }
8088

8189
/// The block to be called when the renderable is about to be updated.
8290
///
8391
/// At this point, the renderable might be not inserted into the renderable hierarchy yet. The renderable's properties, including its
8492
/// frame, are not updated yet. You can use this block to get the properties of the renderable before the update and use the
8593
/// information to help renderable content update. For example, to get the old properties for animating the renderable's changes.
86-
public let willUpdate: ((T, RenderableUpdateContext) -> Void)?
94+
public var willUpdate: ((T, RenderableUpdateContext) -> Void)? { storage.willUpdate }
8795

8896
/// The block to be called when the renderable's frame is just updated and is ready to be updated for additional changes.
8997
///
@@ -96,21 +104,21 @@ public struct RenderItem<T> {
96104
/// Note that it is possible that when a renderable is being inserted into the renderable hierarchy with a transition animation, a
97105
/// new update, which is triggered by a `refresh()`, is called on the renderable. In this case, an update call with
98106
/// `RenderableUpdateType.refresh` type will be called before the update call with `RenderableUpdateType.insert` type.
99-
public let update: (T, RenderableUpdateContext) -> Void
107+
public var update: (T, RenderableUpdateContext) -> Void { storage.update }
100108

101109
/// The block to be called when the renderable is about to be removed from the renderable hierarchy.
102110
///
103111
/// At this point, the renderable is still in the renderable hierarchy, but it is about to be removed, before the transition
104112
/// animation if has one.
105-
public let willRemove: ((T, RenderableRemoveContext) -> Void)?
113+
public var willRemove: ((T, RenderableRemoveContext) -> Void)? { storage.willRemove }
106114

107115
/// The block to be called when the renderable is just removed from the renderable hierarchy.
108116
///
109117
/// At this point, the renderable is already removed from the renderable hierarchy with its transition animation completed.
110118
///
111119
/// Note that it is possible that a removing renderable, during its removal transition animation, is re-inserted into the
112120
/// renderable hierarchy. In this case, the `didRemove` block won't be called.
113-
public let didRemove: ((T, RenderableRemoveContext) -> Void)?
121+
public var didRemove: ((T, RenderableRemoveContext) -> Void)? { storage.didRemove }
114122

115123
/// An optional reuse identifier that opts the renderable into the recycle pool.
116124
///
@@ -120,20 +128,67 @@ public struct RenderItem<T> {
120128
///
121129
/// To ensure a reused renderable is in the same state a freshly made one would have, use the `resetForReuse` block to
122130
/// reset the renderable's modified properties.
123-
let reuseId: ReuseId?
131+
var reuseId: ReuseId? { storage.reuseId }
124132

125133
/// The concrete renderable type generated by `ObjectIdentifier(T.self)`.
126-
let renderableType: ObjectIdentifier?
134+
var renderableType: ObjectIdentifier? { storage.renderableType }
127135

128136
/// The block to be called when the renderable is about to be added to the reuse pool, to reset any modified state so
129137
/// the reused renderable is clean (freshly-made-equivalent) for its next use.
130-
public let resetForReuse: ((T) -> Void)?
138+
public var resetForReuse: ((T) -> Void)? { storage.resetForReuse }
131139

132140
/// The transition of the renderable. The transition is used to animate the renderable's insertion and removal.
133-
public let transition: RenderableTransition?
141+
public var transition: RenderableTransition? { storage.transition }
134142

135143
/// The animation timing of the renderable. The animation timing is used to animate the renderable's changes.
136-
public let animationTiming: AnimationTiming?
144+
public var animationTiming: AnimationTiming? { storage.animationTiming }
145+
146+
/// The boxed behavior backing a `RenderItem`. See `storage`.
147+
///
148+
/// Holds everything except `id` and `frame`. Because all fields are immutable, the box can be shared freely across
149+
/// the copies a render pass makes (containers copy child items to re-position them), so each copy is one retain.
150+
private final class Storage {
151+
152+
let make: (RenderableMakeContext) -> T
153+
let willInsert: ((T, RenderableInsertContext) -> Void)?
154+
let didInsert: ((T, RenderableInsertContext) -> Void)?
155+
let willUpdate: ((T, RenderableUpdateContext) -> Void)?
156+
let update: (T, RenderableUpdateContext) -> Void
157+
let willRemove: ((T, RenderableRemoveContext) -> Void)?
158+
let didRemove: ((T, RenderableRemoveContext) -> Void)?
159+
let reuseId: ReuseId?
160+
let renderableType: ObjectIdentifier?
161+
let resetForReuse: ((T) -> Void)?
162+
let transition: RenderableTransition?
163+
let animationTiming: AnimationTiming?
164+
165+
init(make: @escaping (RenderableMakeContext) -> T,
166+
willInsert: ((T, RenderableInsertContext) -> Void)?,
167+
didInsert: ((T, RenderableInsertContext) -> Void)?,
168+
willUpdate: ((T, RenderableUpdateContext) -> Void)?,
169+
update: @escaping (T, RenderableUpdateContext) -> Void,
170+
willRemove: ((T, RenderableRemoveContext) -> Void)?,
171+
didRemove: ((T, RenderableRemoveContext) -> Void)?,
172+
reuseId: ReuseId?,
173+
renderableType: ObjectIdentifier?,
174+
resetForReuse: ((T) -> Void)?,
175+
transition: RenderableTransition?,
176+
animationTiming: AnimationTiming?)
177+
{
178+
self.make = make
179+
self.willInsert = willInsert
180+
self.didInsert = didInsert
181+
self.willUpdate = willUpdate
182+
self.update = update
183+
self.willRemove = willRemove
184+
self.didRemove = didRemove
185+
self.reuseId = reuseId
186+
self.renderableType = renderableType
187+
self.resetForReuse = resetForReuse
188+
self.transition = transition
189+
self.animationTiming = animationTiming
190+
}
191+
}
137192

138193
public init(id: ComposeNodeId,
139194
frame: CGRect,
@@ -151,18 +206,20 @@ public struct RenderItem<T> {
151206
{
152207
self.id = id
153208
self.frame = frame
154-
self.make = make
155-
self.willInsert = willInsert
156-
self.didInsert = didInsert
157-
self.willUpdate = willUpdate
158-
self.update = update
159-
self.willRemove = willRemove
160-
self.didRemove = didRemove
161-
self.reuseId = reuseId.map { ReuseId(namespace: .user, id: $0) } // external callers can only set a user-provided identifier
162-
self.renderableType = ObjectIdentifier(T.self)
163-
self.resetForReuse = resetForReuse
164-
self.transition = transition
165-
self.animationTiming = animationTiming
209+
self.storage = Storage(
210+
make: make,
211+
willInsert: willInsert,
212+
didInsert: didInsert,
213+
willUpdate: willUpdate,
214+
update: update,
215+
willRemove: willRemove,
216+
didRemove: didRemove,
217+
reuseId: reuseId.map { ReuseId(namespace: .user, id: $0) }, // external callers can only set a user-provided identifier
218+
renderableType: ObjectIdentifier(T.self),
219+
resetForReuse: resetForReuse,
220+
transition: transition,
221+
animationTiming: animationTiming
222+
)
166223
}
167224

168225
/// Internal initializer for creating a renderable item with a specified renderable type and reuse identifier.
@@ -184,18 +241,20 @@ public struct RenderItem<T> {
184241
{
185242
self.id = id
186243
self.frame = frame
187-
self.make = make
188-
self.willInsert = willInsert
189-
self.didInsert = didInsert
190-
self.willUpdate = willUpdate
191-
self.update = update
192-
self.willRemove = willRemove
193-
self.didRemove = didRemove
194-
self.reuseId = reuseId
195-
self.renderableType = renderableType
196-
self.resetForReuse = resetForReuse
197-
self.transition = transition
198-
self.animationTiming = animationTiming
244+
self.storage = Storage(
245+
make: make,
246+
willInsert: willInsert,
247+
didInsert: didInsert,
248+
willUpdate: willUpdate,
249+
update: update,
250+
willRemove: willRemove,
251+
didRemove: didRemove,
252+
reuseId: reuseId,
253+
renderableType: renderableType,
254+
resetForReuse: resetForReuse,
255+
transition: transition,
256+
animationTiming: animationTiming
257+
)
199258
}
200259

201260
/// Add an additional will insert block to the renderable item.

0 commit comments

Comments
 (0)