Change StoreProtocol to implement transaction#47
Conversation
...rather than send. `send` becomes an asynchronous main actor isolated protocol extension that guarantees your changes will always happen on the main thread, but offers only a fire-and-forget interface.
|
@bfollington here's my take on #46. Not sure if the method rename will cause too much carnage in Subconscious. If so, we could instead do:
The tradeoff would be that we have to manually go change The constraints we're working with:
As a result:
|
|
Nice! This is very similar to (one of) the abstractions I tried, it looks good on paper:
BUT it seems the major performance penalty comes from the synchronous mutation of a binding. Comparing the attached code snippets, the first one has a noticeable hang (visible in profiling tools) on each tab transition and the second does not. A - has hangs TabView(
selection: Binding(
get: { store.state.selectedAppTab },
transact: store.transact,
tag: AppAction.setSelectedAppTab
)
)B - no hangs TabView(
selection: Binding(
get: { store.state.selectedAppTab },
transact: { action in Task { store.transact(action) } },
tag: AppAction.setSelectedAppTab
)
)Now, this violates a bunch of runtime guarantees so it's not acceptable. I think, if we want safe bindings + good performance, we might have to pull a similar trick to C - proxy binding @State var selectedTab: AppTab = .notebook
var body: some View {
TabView(
selection: $selectedTab
) { ... }
.onAppear {
selectedTab = store.state.selectedAppTab
}
.onChange(of: selectedTab) { v in
store.send(.setSelectedAppTab(selectedTab))
}
}This has the best performance I've seen yet in the profiler and suggests we should avoid ever directly calling I agree with your assessment that bindings seem to expect synchronous updates but we seem to be doing WAY too much work on each update to support that (because we touch so much of the tree on every change to every store). |
|
You can somewhat bundle this into a ViewModifier: @State var selectedTab: AppTab = .notebook
var body: some View {
TabView(selection: $selectedTab) { ... }
.modifier(
StoreProxyViewModifier(
state: $selectedTab,
get: { store.state.selectedAppTab },
send: store.send,
tag: AppAction.setSelectedAppTab
)
)
}I also had a go at writing a custom |
bfollington
left a comment
There was a problem hiding this comment.
I think we should go ahead and land this, along with subconsciousnetwork/subconscious#1124. They make a considerable improvement to performance, even if we know we need to go deeper on this problem in the long run.
...rather than send.
transact(:)is a non-isolated method that performs a synchronous state transaction in response to an action.send(:)becomes an asynchronous main actor isolated protocol extension that guarantees your changes will always happen on the main thread, but offers only a fire-and-forget interface.