EP 94 · Adaptive State Management · Mar 16, 2020 ·Members

Video #94: Adaptive State Management: Performance

smart_display

Loading stream…

Video #94: Adaptive State Management: Performance

Episode: Video #94 Date: Mar 16, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep94-adaptive-state-management-performance

Episode thumbnail

Description

It’s time to put the finishing touches to our architecture so that we can use it in production. This week we begin exploring how to make the Composable Architecture adapt to many use cases, and we will use a potential performance problem as inspiration for this exploration.

Video

Cloudflare Stream video ID: a5589484aa02ae84d14ce7c27b55774a Local file: video_94_adaptive-state-management-performance.mp4 *(download with --video 94)*

References

Transcript

0:42

We are now in the final stretches of polishing up the Composable Architecture so that it is production worthy. We’ve used the first few months of this year rounding off some of the rough edges of the architecture. Things like getting rid of code generation by using case paths and making the side effect and testing story more rock solid by baking the environment technique directly into the architecture.

1:13

Today we will begin looking at improving the performance of the Composable Architecture, because there’s one very naive thing we are doing that can be easily addressed. But, while solving the performance problem we are actually going to stumble upon a wonderful way to make the architecture adaptive to many situations. For example, perhaps you are making an app that ships on iOS and macOS, or maybe you are even making an app for all 4 platforms, iOS, macOS, tvOS and watchOS. Wouldn’t it be awesome if you could write the business logic of your application a single time, and have it adapt to each of these platforms very easily? Well, this is exactly what we are going to do. Fixing a couple memory leaks

2:06

Before getting into improving the performance improvements of the architecture let’s fix a small problem that has lurked in our architecture for quite some time now. We have two memory leaks that can be easily fixed. Finding memory leaks in an application can take a lot of skill, and like all skills you get better the more you practice, but luckily for us Xcode comes with an amazing debugging tool specifically for memory leaks. It’s called the Memory Graph Debugger, and it can help point out any retain cycles you have in your code.

2:50

To see the problem all we have to do is start the app, increment the count a single time, and then turn on the memory graph debugger, and we’ll see some purple exclamation marks that indicate we may have a retain cycle somewhere

3:21

We can even click the “!” icon at the bottom of this UI to show only the memory leak rows.

3:28

At the top level we see which frameworks have leaked objects, and so if we expand one we will see what kind of objects were leaked in that framework.

3:42

And then further clicking on the leaked object will show a graph that demonstrates just how the retain cycle occurred:

3:57

Here we see that a Sink value was created, and via its receiveCompletion property it retained a swift closure, which in turn captured a closure, which also captured a closure, which then captured an AnyCancellable . After that we have some kind of link to something called malloc<48> , which I don’t really know what that means, but eventually it looks like that holds onto the Sink object that started this whole cycle.

4:37

This shows us, in very clear terms, that we have a chain of objects holding on to each other that ultimately ends back where it started. It’s now our job to take these little bits of evidence of a retain cycle and figure out where in our code this could be happening. var effectCancellable: AnyCancellable? var didComplete = false effectCancellable = effect.sink( receiveCompletion: { [weak self] _ in didComplete = true guard let effectCancellable = effectCancellable else { return } self?.effectCancellables.remove(effectCancellable) }, receiveValue: self.send ) if !didComplete, let effectCancellable = effectCancellable { self.effectCancellables.insert(effectCancellable) }

4:51

The most incriminating piece of evidence is the Sink object, because the method we invoke to run an effect is called sink . Ostensibly that method is creating some kind of Sink object under the hood that we don’t see publicly. effectCancellable = effect.sink

5:25

Further, the sink method does have an argument named receiveCompletion , and it does indeed take a closure. receiveCompletion: { [weak self] _ in Even more incriminating, we do have a cancellable in this closure, the effectCancellable . So this is starting to seem like the real culprit of this memory leak. guard let effectCancellable = effectCancellable else { return }

5:45

And indeed, there is a pretty obvious retain cycle here. The sink method captures the effectCancellable , yet we are also using the output of sink to define effectCancellable in the first place. So it seems we need to break this cycle by capturing effectCancellable weakly: effectCancellable = effect.sink( receiveCompletion: { [weak self, weak effectCancellable] _ in didComplete = true guard let effectCancellable = effectCancellable else { return } self?.effectCancellables.remove(effectCancellable) }, Correction Adding effectCancellable to the capture block captures it immediately, so it will always be nil and never removed from effectCancellables . Instead we should eagerly create an identifier for the cancellable and use dictionary storage [UUID: AnyCancellable] , instead: public final class Store<Value, Action>: ObservableObject { … private var effectCancellables: [UUID: AnyCancellable] = [:] … func send(_ action: Action) { let effect = self.reducer(&self.state, action) var didComplete = false let uuid = UUID() let effectCancellable = effect.sink( receiveCompletion: { [weak self] _ in didComplete = true self?.effectCancellables[uuid] = nil }, receiveValue: { [weak self] in self?.send($0) } ) if !didComplete { self.effectCancellables[uuid] = effectCancellable } } … }

6:08

And now when we run the app, click around, and check out the memory debugger we will see no purple exclamations.

6:24

So we just saw that we need to be careful with the closures we hand to the sink method because they can cause retain cycles. The sink method takes another closure, called the receiveValue , which is invoked every time the effect produces a value. We want to send those values back into the store, so we just passed it self.send directly: receiveValue: self.send

6:49

This is now technically a retain cycle since the sink holds onto self , but also self holds onto the cancellable that sink returned. The store that runs these effects is the root one from which all other stores are derived. Remember that we have a view method that allows us to focus an existing store into one that only exposes a subset of the state and actions, and we use those smaller stores to pass along to views. All of those derived stores are really only calling out to the root store under the hood, whereas the root store, the one that is actually powering everything, is only created a single time at the topmost level of the application, the SceneDelegate : window.rootViewController = UIHostingController( rootView: ContentView( store: Store( initialValue: AppState(), reducer: with( appReducer, compose( logging, activityFeed ) ) ) ) )

7:38

So this potential memory leak will only rear its ugly head if we were to try to recreate the root store from scratch multiple times, which we aren’t doing in this application. However, there may be some people out there that want to do this in their applications, and so we should fix it. Luckily the fix is easy enough: receiveValue: { [weak self] in self?.send($0) }

8:24

And now we have fixed the two memory leaks that we are aware of in the Composable Architecture. It’s an unfortunate fact of life that the messy runtime part of the architecture, which is everything in this Store class, is always going to be the most difficult part to debug and understand in its entirety. But luckily all of the nice functional parts of it, like the reducers, can remain simple and well understood.

8:50

If you experience any other memory leaks or problems, feel free to reach out to us! View.init/body: tracking

9:11

Now back to the main attraction: performance.

9:18

Right now we have a potential performance problem in our architecture. And we say potential because there is a very good chance you will never see this problem, but it is possible to have a complex enough application that things could start to stutter and lag.

9:31

The root of the problem has to do with the store: public final class Store<Value, Action>: ObservableObject { @Published public private(set) var value: Value … }

9:39

Notice that it is an observable object and it has a @Published field called value . This means that every time value is mutated, interested parties are notified of that change. Technically this notification happens just before the mutation happens, but that is beside the point.

9:56

An example of an “interested party” to these mutations is a SwiftUI view. In fact, each view that holds a store uses an @ObservedObject property wrapper in order to link together the mutations of the store to the re-renders of the view, for example our root ContentView : struct ContentView: View { @ObservedObject var store: Store<AppState, AppAction> … }

10:16

This means that every time the store’s underlying value is changed, the ContentView will be notified of the change and may perform a re-render of its content.

10:23

To see this, let’s add some print statements for every time the ContentView is created and every time the body field is accessed: struct ContentView: View { @ObservedObject var store: Store<AppState, AppAction> init(store: Store<AppState, AppAction>) { self.store = store print("ContentView.init") } var body: some View { print("ContentView.body") return … } }

10:53

When the app first starts up we get some reasonable logs: ContentView.init ContentView.body

11:03

However, if we drill down into the counter demo and increment the count, we get a new log: ContentView.body

11:13

Why would the ContentView ‘s body property need to be re-computed? The ContentView isn’t even using any data from the store to render its view, its just a static list of some navigation links. Further, anything we do in this screen seems to trigger the body property, like if we increment again, ask if its prime, save the prime as a favorite, and then ask for the 2nd prime. Each of those actions triggered the body property: ContentView.body ContentView.body ContentView.body ContentView.body

11:33

Although this seems strange, it’s pretty obvious why it is happening. Our ContentView holds onto a store of the entire application state, Store<AppState, AppAction> , and so any change to any part of the application will cause this view to re-compute itself.

11:46

Now, we don’t know exactly what SwiftUI is doing under the hood with these re-computations of the body. We know that Apple says it uses some advanced diffing techniques so that it only renders what is necessary on the screen, and SwiftUI is probably pretty smart at seeing that nothing changed in our UI and so maybe it doesn’t render anything at all.

12:04

But besides any concerns of rendering, you may also be concerned that the body property is even being called at all. What if the computation happening inside that property is super intensive?

12:14

Well, Apple also gives us guidance on this and says that we should keep the implementations of the body properties as simple and lean as possible. They should be just simple transformations of the view’s state into some view hierarchy. And the construction of these values, like NavigationView , List and NavigationLink are super lightweight, and we shouldn’t sweat creating a bunch of them on the fly.

12:34

Therefore we have no reason to think that this should be a performance problem. Apple tells us they do some powerful diffing under the hood to prevent over rendering our UI, and that the construction of these views is super lightweight and so shouldn’t be too much of a burden.

12:47

But things get a little weirder if we put a few more of these print statements in place. Let’s add a print to the init and body of every view in our application: public struct CounterView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction> public init(store: Store<CounterViewState, CounterViewAction>) { self.store = store print("CounterView.init") } public var body: some View { print("CounterView.body") return … } } public struct IsPrimeModalView: View { @ObservedObject var store: Store<PrimeModalState, PrimeModalAction> public init(store: Store<PrimeModalState, PrimeModalAction>) { self.store = store print("IsPrimeModalView.init") } public var body: some View { print("IsPrimeModalView.body") return … } } public struct FavoritePrimesView: View { @ObservedObject var store: Store<[Int], FavoritePrimesAction> public init(store: Store<[Int], FavoritePrimesAction>) { self.store = store print("FavoritePrimesView.init") } public var body: some View { print("FavoritePrimesView.body") return … } }

13:32

And let’s run the app. The first thing we will see is the following logs: ContentView.init ContentView.body CounterView.init FavoritePrimesView.init

13:38

Notice that the CounterView and FavoritePrimesView are created, but their body properties are not invoked. This is important. We shouldn’t be scared of created these little view structs, because doing so is super lightweight and their bodies are invoked only when needed.

13:52

If we drill down into the CounterView we will get a new log: CounterView.body So we see that only when needing to actually render the counter view do we even access its body field.

14:02

If we increment the count in this view we are going to see the following in the logs: CounterView.body ContentView.body CounterView.init FavoritePrimesView.init CounterView.body

14:06

This has re-computed the CounterView , but also the ContentView was re-computed, which subsequently created a new CounterView and FavoritePrimesView and then re-computed the CounterView . This is strange, but again, we expect this because the root content view holds an observed store of all of app state, which will trigger these things when anything changes in the state.

14:36

Let’s take this further, let’s increment the count to 2 and ask if its prime. This prints more logs, but let’s clear those logs for now, and then save 2 to our favorite primes. All of these logs appear for just that one action: IsPrimeModalView.body ContentView.body CounterView.init FavoritePrimesView.init CounterView.body IsPrimeModalView.init IsPrimeModalView.body CounterView.body IsPrimeModalView.init IsPrimeModalView.body View.init/body: analysis

14:56

What is happening here? The entire view hierarchy is being recreated, from the root all the way to the modal, and sometimes views are even being created multiple times.

15:09

But why is this happening? We are just adding a single number to our array of favorite primes, and the CounterView and ContentView don’t even care about that array. They don’t use it at all in order display their UI. Even worse, the deeper this view hierarchy gets the worse this problem is going to get.

15:25

The reason this is happening is simple. Let’s look at the CounterView : public struct CounterView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction> … }

15:29

Even though this says it needs just a store for CounterViewState , it will notify the view of changes to any part of the application state, not just CounterViewState . This is because the store handed to this view was derived from the root, global store, the one that worked with all of AppState .

15:39

And because of that, it will be notified when any change is made to the application state, not just the counter view state.

15:44

We can see this directly if we look at the implementation of view , which is our transformation method for turning stores of global domains into stores of local domains. In particular, these lines: localStore.viewCancellable = self.$value.sink { [weak localStore] newValue in localStore?.value = toLocalValue(newValue) }

16:09

This says that whenever the global store’s value changes we will immediately replay that change to the local store. This happens regardless of the local value changing at all. Some completely unrelated part of the global value could have changed, but we will still notify the child.

16:23

On thing we may be tempted to do is somehow de-duplicate the stream of values we send to our local store. For instance, if the array of favorite primes didn’t change then there’s no reason to re-compute the IsPrimeModalView view. Thanks to the power of Combine, we can accomplish this quite easily by map ing on the value publisher to compute the local value, and then removing duplicates: localStore.viewCancellable = self.$value .map(toLocalValue) .removeDuplicates() .sink { [weak localStore] newValue in localStore?.value = newValue }

17:04

We of course need the LocalValue generic to be Equatable in order for this to work, but that is easy to accommodate for. However, this is only solving half the problem. This is making sure that unrelated mutations to the global state do not trigger value changes in the local store. But we also have the problem that views hold onto stores which represent more state than they are currently showing. For example, the CounterView re-computed its body when the favorite primes array was mutated, even though it doesn’t display any favorite primes. And even worse, the root ContentView was re-computed when any mutation happened to the application state, even though it doesn’t display any dynamic content whatsoever. Adding removeDuplicates here won’t help with any of that. View.init/body: stress test

17:44

So, this isn’t the solution we want, and we are now seeing how subtle this problem is. One of the main benefits to the Composable Architecture was that we have a unified, composed reducer and store so that we got a single, consistent way to mutate our application state, and so that those mutations could be shared across the entire application. However, that choice has also given us some strange SwiftUI behavior by causing us to recompute our view’s way more than needed.

18:06

Now, again, we don’t know what SwiftUI is doing under the hood and so we don’t know how big of a problem this is. Apple tells us that they do a good job of diffing and that creating views is lightweight and so we shouldn’t sweat creating them. However, it is possible that a large enough application could run into performance problems if we are just naively recomputing views all over the place.

18:26

To see this, let’s stress test one of our views to see if we can reproduce any performance problems. Let’s add a huge number of rows to the root ContentView . We can do this using a ForEach view: ForEach(Array(1...500_000), id: \.self) { value in Text("\(value)") }

18:41

This adds 500,000 rows of integers to our list view. That’s way more than we would typically see in a real life app, but this is a stress test.

18:50

If we run the app, and drill down to the counter demo, we will see that any action we take is delayed by about a second. After tapping a button the UI freezes up, ostensibly because SwiftUI is doing a ton of work to figure out how to render the root ContentView . So, it definitely is possible for our design choices for the store to cause some performance problems in SwiftUI, but it’s not quite clear how big of a problem this is in a real world application. Next time: the solution

19:47

So we’ve now seen that there is a potential problem with the way we have set up the Composable Architecture. Again, we say “potential” because it may not actually become a problem until you have a massive application. But, rather than waiting for that day to come, it turns out there is something pretty simple we can do to fix this. Even better, by fixing this problem we will actually stumble upon a wonderful way to make all of our business logic in our reducers more adaptable to platforms and the constraints we face in real world applications.

20:16

The crux of the problem is that we have a single store inside our views that holds not only all the state it needs to show its UI, but also all the state of its children, which may not be shown until later, if at all. So it seems like perhaps our views need to hold onto a different object that only has access to the state that the view cares about, and nothing more. And then maybe we have a better chance of getting notified of only the changes to the state that we actually care about. And if we succeed in doing that, then maybe the Store doesn’t need to be an observable object at all, only this secondary thing needs to be observable.

20:53

Before figuring out how to do that, let’s do the one thing we know needs to be done: let’s remove the ObservableObject conformance from the Store so that we stop over notifying views of state changes…next time! References Gathering Information About Memory Use Apple documentation around identifying memory-use inefficiencies though various means of measuring and profiling, including the memory graph debugger, which is used in this episode. https://developer.apple.com/documentation/xcode/improving_your_app_s_performance/reducing_your_app_s_memory_use/gathering_information_about_memory_use Downloads Sample code 0094-adaptive-state-management-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .