Skip to content

Potential memory leak in child store caching #3634

Closed
@Kolos65

Description

@Kolos65

Description

Upon investigating a performance issue in my app, I found a potential leak related to child store caching. 

I am developing a transcription app with a very basic navigation setup:

  • there is a root screen with a list of projects
  • you can tap a project that pushes a project edit screen onto the stack
  • the project edit screen is quite complex with many child features (it operates on an array of subtitles, each having its own reducer)
  • I use NavigationStack(path:destination:) and a Path enum reducer with StackState and StackAction in the main feature

When navigating back to the root screen after opening a project, I would expect all child states to be removed from memory. Instead I can see:

  • multiple instances of ObservableStateID.Storage
  • multiple instances of ObservationRegistrar.Extent

Both Storage and Extent are held by what seems to be the child feature’s state (which I would expect having been deallocated at this point). The number of instances in both cases correlates with the number of child features I create when opening a project. So if I open and close a project X times, you would see X times more instances of these in the memory debugger.



I also noticed that setting Store.canCacheChildren to have a default false starting value will resolve the above leak. This indicates that the issue is related to caching child states in the scope implementation of the store:

public final class Store<State, Action> {
  // Setting to false resolves the leak
  var canCacheChildren = true
  // Keeps child states in memory after lifecycle ends?
  private var children: [ScopeID<State, Action>: AnyObject] = [:]
...

According to LLMs, storing strong references to child stores in the children array is a memory leak. I know things are often more nuanced (thus the “potential” in the title), but when and who removes child stores from that children array?

I am happy to provide more context if needed.

Memory after popping back to the root with Store.canCacheChildren = true:
Image
Memory after popping back to the root with Store.canCacheChildren = false:
Image
Storage memory graph:
Image
Extent memory graph:
Image

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

No response

Actual behavior

No response

Reproducing project

No response

The Composable Architecture version information

1.18.0

Destination operating system

iOS 18.0

Xcode version information

Xcode 16.2

Swift Compiler version information

swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working due to a bug in the library.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions