【TCA】Storeの数が多くなるほどアクション送信のパフォーマンスが劣化していく

# はじめに
:::note warn
本記事はTCA 1.9.2で検証を行っています。
:::

TCA公式サンプルの[Todos](https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/Todos/README.md)では、リストの各アイテムに対してStoreを持たせる構造になっています。

TODOリストのようなアプリでは、ルートReducerで全てのアクションを受けるよりも、各アイテムがReducerを持ち、自身に関わるアクションを自ら処理できたほうが、責務も明確で実装がクリアになるでしょう。

しかし、リストの全てのアイテムがStoreを持つような実装をすると、アイテムの数が多くなるほど、アクション送信時のパフォーマンスが劣化していくという事象を経験しました。

ライブラリのコードを読んでボトルネックになっていそうな箇所を見つけたので、参考までに解説してみようと思います。

# 事象を再現してみる
まずは「Storeが多いとアクション送信のパフォーマンスが悪い」を再現します。

TCAのリポジトリには、ライブラリの性能を評価するためのベンチマークのコードがいくつか用意されています。
[swift-benchmark](https://github.com/google/swift-benchmark)というツールを使って、Store等のパフォーマンスを計測しているようです。

https://github.com/pointfreeco/swift-composable-architecture/tree/main/Sources/swift-composable-architecture-benchmark

このフォルダに以下のコードスニペットを追加し、ベンチマークを実行してみます。

```swift
import Benchmark
import ComposableArchitecture

@Reducer
private struct Parent {
    @ObservableState
    struct State: Equatable {
        var children: IdentifiedArrayOf<Child.State> = []
    }

    enum Action {
        case children(IdentifiedActionOf<Child>)
        case tapped
    }

    var body: some ReducerOf<Self> {
        EmptyReducer()
            .forEach(\.children, action: \.children) { Child() }
    }
}

@Reducer
private struct Child {
    @ObservableState
    struct State: Equatable, Identifiable {
        let id: Int
    }

    enum Action {
        case tapped
    }
}

let manyStoresSuite = BenchmarkSuite(name: "Many stores") { suite in
    func makeStore(num: Int) -> StoreOf<Parent> {
        let children = IdentifiedArrayOf<Child.State>(
            uniqueElements: (0...num).map { Child.State(id: $0) }
        )
        let store = Store(initialState: Parent.State(children: children)) { Parent() }
        for i in 0...num {
            _ = store.scope(state: \.children[i], action: \.children[id: i])
        }
        return store
    }

    let store10 = makeStore(num: 10)
    let store100 = makeStore(num: 100)
    let store1000 = makeStore(num: 1000)

    suite.benchmark("10 stores") {
        store10.send(.tapped)
    }
    suite.benchmark("100 stores") {
        store100.send(.tapped)
    }
    suite.benchmark("1000 stores") {
        store1000.send(.tapped)
    }
}
```

コードの内容について簡単に説明します。

まず、ParentとChildという2つのFetureがあり、これらは親子の関係になっています。
Parentは複数のChildを保持することができます。

そして、Parentが保持するChildの数を`10`、`100`、`1000`の3パターン用意し、その数だけStoreを生成します。
Storeを生成したら、ルートのStoreに対してアクションを送信し、その実行時間を計測します。

ベンチマークの実行方法ですが、まず`main.swift`ファイルを開いて以下の内容に編集します。

```swift
import Benchmark
import ComposableArchitecture

Benchmark.main([
  manyStoresSuite
])
```

そして、ターミナルで以下のコマンドを実行すればOKです。

```
$ swift run -c release
...

name                    time           std        iterations
------------------------------------------------------------
Many stores.10 stores     24958.000 ns ±   8.12 %      55393
Many stores.100 stores   139667.000 ns ±   6.97 %       9632
Many stores.1000 stores 1283250.000 ns ±   1.57 %       1069
```

Storeの数が増えるほど、1回のアクション送信にかかる時間が増えていくことがわかります。

# アクションを送信したときにTCA内部で起こっていること
`store.send(.tapped)`をしたときに、TCA内部ではどのような処理が行われているのでしょうか？
ざっくりかいつまんで説明してみます。

まず、`Store.send(_:)`を呼び出すと、`Store.send(_:originatingFrom:)`に処理を委譲します。

`Store.send(_:originatingFrom:)`は`RootStore.send(_:originatingFrom:)`を呼び出します。

```swift
@_spi(Internals)
public func send(
  _ action: Action,
  originatingFrom originatingAction: Action?
) -> Task<Void, Never>? {
  return self.rootStore.send(self.fromAction(action))
}
```

RootStoreは、アプリ全体のStateとReducerを保持する、Storeの親玉？です。
アクションが送信され、Stateが更新されたときにどういう処理が行われるかを理解するために、RootStoreのコードを抜粋しました（Effect周りのコードは無視しています）。

```swift
public final class RootStore {
  let didSet = CurrentValueRelay(())
  private let reducer: any Reducer
  private(set) var state: Any {
    didSet {
      didSet.send(())
    }
  }

  func send(_ action: Any, originatingFrom originatingAction: Any? = nil) -> Task<Void, Never>? {
    // ...
    var currentState = self.state as! State
    // ...

    defer {
      // ...
      self.state = currentState
      // ...
    }

    // ...
    let effect = reducer.reduce(into: &currentState, action: action)
    // ...
  }
}
```

まず、現在のStateを`currentState`という変数に代入しておきます。

次に、Actionと`currentState`をReducerに渡し、`currentState`を更新します。

そして、最後に自身の`state`プロパティを`currentState`の値に更新します。

このとき、`state`プロパティの`didSet`が呼ばれ、CurrentValueRelayというPublisherに準拠したクラスの`send()`メソッドが呼ばれます。

CurrentValueRelayは、アクションがReducerによって処理されたことを各Storeに通知する役割を持っています。

各Storeは通知を受け取り、自身が関知すべき範囲のStateに更新があったかどうかを確認し、更新があった場合はビューにそれを通知するという流れになっています。

`Store.init(rootStore:toState:fromAction:)`を見ると、StoreがCurrentValueRelayをサブスクライブしていることがわかります。

このイニシャライザは`Store.scope(state:action:)`を呼び出したときに内部で実行されているものです。

```swift
private init(
  rootStore: RootStore,
  toState: PartialToState<State>,
  fromAction: @escaping (Action) -> Any
) {
  defer { Logger.shared.log("\(storeTypeName(of: self)).init") }
  self.rootStore = rootStore
  self.toState = toState
  self.fromAction = fromAction

  #if canImport(Perception)
    func subscribeToDidSet<T: ObservableState>(_ type: T.Type) -> AnyCancellable {
      let toState = toState as! PartialToState<T>
      return rootStore.didSet
        .compactMap { [weak rootStore] in
          rootStore.map { toState($0.state) }?._$id
        }
        .removeDuplicates()
        .dropFirst()
        .sink { [weak self] _ in
          guard let self else { return }
          self._$observationRegistrar.withMutation(of: self, keyPath: \.currentState) {}
        }
    }
    if let stateType = State.self as? ObservableState.Type {
      self.parentCancellable = subscribeToDidSet(stateType)
    }
  #endif
}
```

`subscribeToDidSet(_:)`の内部で、`rootStore.didSet`に対してsinkしています。
`rootStore.didSet`はCurrentValueRelayです。

このように、Storeが生成されるたびにCurrentValueRelayのサブスクライバーは増えていきます。

最後に、`CurrentValueRelay.send()`の処理を見てみましょう。

```swift
func send(_ value: Output) {
  self.currentValue = value
  for subscription in subscriptions {
    subscription.forwardValueToBuffer(value)
  }
}
```

Storeが生成されるたびに、`CurrentValueRelay.subscriptions`が増えていきます。
その結果、ここのfor文の実行時間が長くなり、アクションの実行完了までにかかる時間が長くなっていくのではないかと思います。

# CurrentValueRelay.didSetをサブスクライブしないようにしてみる
`subscribeToDidSet(_:)`を呼び出しているところをコメントアウトしてベンチマークを実行してみると、Storeが増えてもアクション送信にかかる時間は変わらないことがわかります。

このことから、上述した考察は合っているんじゃないかなあと思います。

```swift
private init(
  rootStore: RootStore,
  toState: PartialToState<State>,
  fromAction: @escaping (Action) -> Any
) {
  // ...

  // if let stateType = State.self as? ObservableState.Type {
  //  self.parentCancellable = subscribeToDidSet(stateType)
  //}
}
```

```
$ swift run -c release
...

name                    time        std        iterations
---------------------------------------------------------
Many stores.10 stores   9833.000 ns ±  10.24 %     132118
Many stores.100 stores  9833.000 ns ±  10.55 %     141192
Many stores.1000 stores 9833.000 ns ±   9.20 %     140850
```

# まとめ
[TCAのパフォーマンスに関するドキュメント](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance)にも言及がありますが、TCAにおいてアクションを送信するという処理は、メソッド呼び出しのような軽い処理ではありません。

そして、本記事で示したように、Storeを不用意に生成してしまうと、アプリのパフォーマンスが劣化する原因となってしまいます。

[TCAのDiscussionsでもこのあたりについて触れられている投稿](https://github.com/pointfreeco/swift-composable-architecture/discussions/1290#discussioncomment-3467282)がありました。

TCAの仕組み的に仕方ない部分なのかもしれませんが、アプリの要件によっては大量のStoreを生成したいケースもあったりするので、今後何か良いAPIが提供されないかと期待しています。