Skip to content

Commit 435f66c

Browse files
committed
[perf] avoid unnecessary work in disableActions
1 parent c5a06f2 commit 435f66c

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

ComposeUI/Sources/ComposeUI/Animations/CALayer+DisableActions.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ public extension CALayer {
7979
/// - keys: The keys to disable actions for.
8080
/// - work: The block to execute.
8181
func disableActions(for keys: [String], _ work: () throws -> Void) rethrows {
82+
if CATransaction.disableActions() {
83+
// The current transaction already suppresses implicit actions for every key, so installing a per-layer `NSNull`
84+
// actions dictionary would change nothing.
85+
// skip it and run the work directly to avoid snapshotting/assigning/restoring `actions`, which triggers a
86+
// `-[CALayer setActions:]` KVO + dictionary teardown per call.
87+
try work()
88+
return
89+
}
90+
8291
let originalActions = actions
8392

8493
var disabledActions = [String: CAAction]()

ComposeUI/Tests/ComposeUITests/Animations/CALayer+DisableActionsTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,45 @@ class CALayer_DisableActionsTests: XCTestCase {
6868
}
6969
}
7070

71+
func test_disableActions_insideDisablingTransaction_skipsActionsInstall() throws {
72+
let frame = CGRect(x: 0, y: 0, width: 50, height: 50)
73+
let layer = CALayer()
74+
layer.frame = frame
75+
76+
let window = TestWindow()
77+
window.layer.addSublayer(layer)
78+
79+
// wait for the layer to have a presentation layer
80+
expect(layer.presentation()).toEventuallyNot(beNil())
81+
82+
// install a single-entry sentinel actions dictionary so we can tell whether the call swaps `actions` (slow path,
83+
// which would install a 2-key disabling dictionary) or leaves them untouched (fast path).
84+
let sentinel: [String: CAAction] = ["sentinel": NSNull()]
85+
layer.actions = sentinel
86+
87+
var workRan = false
88+
var actionsCountDuringWork: Int?
89+
90+
// mimic the render pass: run inside a transaction that already disables actions for all keys.
91+
CATransaction.begin()
92+
CATransaction.setDisableActions(true)
93+
layer.disableActions(for: "position", "bounds") {
94+
workRan = true
95+
actionsCountDuringWork = layer.actions?.count
96+
layer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
97+
}
98+
CATransaction.commit()
99+
100+
// the work runs
101+
expect(workRan) == true
102+
// fast path: `actions` stays as the sentinel (count 1), not swapped to the 2-key disabling dictionary.
103+
expect(actionsCountDuringWork) == 1
104+
// `actions` is left intact after the call.
105+
expect(layer.actions?.count) == 1
106+
// no implicit animation is added because the transaction already suppressed it.
107+
expect(layer.animationKeys()) == nil
108+
}
109+
71110
func test_disableAllActions_mainThread() throws {
72111
let frame = CGRect(x: 0, y: 0, width: 50, height: 50)
73112
let layer = CALayer()

0 commit comments

Comments
 (0)