Yuki Takei 6 месяцев назад
Родитель
Сommit
1066103a42
1 измененных файлов с 210 добавлено и 0 удалено
  1. 210 0
      .serena/memories/socketio-update-desc-count-optimization-plan.md

+ 210 - 0
.serena/memories/socketio-update-desc-count-optimization-plan.md

@@ -0,0 +1,210 @@
+# UpdateDescCount イベント最適化計画
+
+## 📋 分析サマリー
+
+### 現状の問題
+- **UpdateDescCount** イベントが **全クライアントへブロードキャスト** されている
+- 他のSocket.IOイベントはすべて適切にルーム機能を使用しているが、このイベントだけが取り残されている
+- 一括マイグレーション時に数千回のイベントが短時間で発生し、パフォーマンス問題を引き起こす
+
+### 他イベントとの比較
+
+| イベント名 | 配信範囲 | 実装方法 | 適切性 |
+|-----------|---------|---------|--------|
+| **UpdateDescCount** | ❌ 全ユーザー | `socket.emit()` | ❌ 不適切 |
+| **S2cMessagePageUpdated** | ✅ ページ閲覧者のみ | `.in(page:${pageId})` | ✅ 適切 |
+| **notificationUpdated** | ✅ 特定ユーザーのみ | `.in(user:${userId})` | ✅ 適切 |
+| **YjsAwarenessStateSizeUpdated** | ✅ ページ閲覧者のみ | `.in(page:${pageId})` | ✅ 適切 |
+| **YjsHasYdocsNewerThanLatestRevisionUpdated** | ✅ ページ閲覧者のみ | `.in(page:${pageId})` | ✅ 適切 |
+
+---
+
+## 🎯 最適化方針
+
+### 推奨アプローチ: ルーム機能の導入
+
+影響を受ける祖先ページを閲覧しているユーザーにのみイベントを配信する。
+
+---
+
+## 🔧 実装計画
+
+### Phase 1: ルーム機能の導入
+
+#### 1. `emitUpdateDescCount` メソッドの修正
+
+**ファイル**: `apps/app/src/server/service/page/index.ts`
+
+**現在の実装** (L3482-3486):
+```typescript
+private emitUpdateDescCount(data: UpdateDescCountRawData): void {
+  const socket = this.crowi.socketIoService.getDefaultSocket();
+
+  socket.emit(SocketEventName.UpdateDescCount, data);
+}
+```
+
+**修正後**:
+```typescript
+private emitUpdateDescCount(data: UpdateDescCountRawData): void {
+  const socket = this.crowi.socketIoService.getDefaultSocket();
+  
+  // 各祖先ページを閲覧しているユーザーにのみ配信
+  Object.entries(data).forEach(([pageId, count]) => {
+    socket
+      .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
+      .emit(SocketEventName.UpdateDescCount, { [pageId]: count });
+  });
+}
+```
+
+**必要なインポート追加**:
+```typescript
+import { RoomPrefix, getRoomNameWithId } from '~/server/service/socket-io/helper';
+```
+
+#### 2. クライアント側の修正は不要
+
+クライアント側 (`apps/app/src/client/components/ItemsTree/ItemsTree.tsx`) は、データ構造が変わらないため修正不要。
+
+---
+
+### Phase 2 (オプション): 一括処理時の最適化
+
+一括マイグレーションなど、大量のページを処理する場合にイベント送信を抑制するオプションを追加。
+
+#### `updateDescendantCountOfAncestors` メソッドの修正
+
+**ファイル**: `apps/app/src/server/service/page/index.ts`
+
+**修正後**:
+```typescript
+async updateDescendantCountOfAncestors(
+  pageId: ObjectIdLike, 
+  inc: number, 
+  shouldIncludeTarget: boolean,
+  suppressEmit: boolean = false  // 新規パラメータ
+): Promise<void> {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const ancestors = await Page.findAncestorsUsingParentRecursively(pageId, shouldIncludeTarget);
+  const ancestorPageIds = ancestors.map(p => p._id);
+
+  await Page.incrementDescendantCountOfPageIds(ancestorPageIds, inc);
+
+  if (!suppressEmit) {
+    const updateDescCountData: UpdateDescCountRawData = Object.fromEntries(ancestors.map(p => [p._id.toString(), p.descendantCount + inc]));
+    this.emitUpdateDescCount(updateDescCountData);
+  }
+}
+```
+
+#### 一括処理での使用例
+
+`normalizeParentRecursivelySubOperation` メソッド内で:
+```typescript
+await this.updateDescendantCountOfAncestors(page._id, inc, false, true); // suppressEmit: true
+```
+
+---
+
+## 📊 期待される効果
+
+### Phase 1 実装後
+
+#### 通常操作の場合
+- **Before**: 全クライアント(例: 100人)に配信 → 100回のクライアント処理
+- **After**: 関連ページ閲覧者のみ(例: 3人)に配信 → 3回のクライアント処理
+- **削減率**: 97%
+
+#### 一括マイグレーション(5,000ページ)の場合
+- **Before**: 5,000回 × 100クライアント = 500,000回のクライアント処理
+- **After**: 5,000回 × 平均3クライアント = 15,000回のクライアント処理
+- **削減率**: 97%
+
+### Phase 2 実装後(一括処理時)
+
+- **Before**: 5,000回のSocket.IO emit
+- **After**: 0回のSocket.IO emit(suppressEmit: true)
+- **削減率**: 100%
+
+---
+
+## ✅ テスト項目
+
+### 機能テスト
+
+1. **ページ作成**
+   - [ ] 作成したページの祖先ページを閲覧しているユーザーにのみイベントが届く
+   - [ ] 無関係なページを閲覧しているユーザーにはイベントが届かない
+   - [ ] CountBadgeが正しく更新される
+
+2. **ページ削除**
+   - [ ] 削除したページの祖先ページを閲覧しているユーザーにのみイベントが届く
+   - [ ] CountBadgeが正しく減少する
+
+3. **ページ移動**
+   - [ ] 移動元と移動先の祖先ページを閲覧しているユーザーにイベントが届く
+   - [ ] 両方のCountBadgeが正しく更新される
+
+4. **複数タブでの動作**
+   - [ ] 同じページを複数タブで開いている場合、すべてのタブで更新される
+   - [ ] 異なるページを開いているタブでは、関連するページのみ更新される
+
+---
+
+## 🎯 実装優先度
+
+### 高優先度: Phase 1(ルーム機能の導入)
+
+- **実装難易度**: 低
+- **影響範囲**: 限定的
+- **効果**: 大(97%削減)
+- **リスク**: 低
+- **推奨**: すぐに実装すべき
+
+### 中優先度: Phase 2(一括処理時の最適化)
+
+- **実装難易度**: 中
+- **影響範囲**: 中(一括処理のみ)
+- **効果**: 大(一括処理時のみ)
+- **リスク**: 低(suppressEmitフラグで制御)
+- **推奨**: Phase 1完了後、必要に応じて実装
+
+---
+
+## 📚 参考実装
+
+### 正しいルーム使用の例
+
+**S2cMessagePageUpdated** (`apps/app/src/server/service/system-events/sync-page-status.ts`):
+```typescript
+socketIoService.getDefaultSocket()
+  .in(getRoomNameWithId(RoomPrefix.PAGE, page._id))
+  .except(getRoomNameWithId(RoomPrefix.USER, user._id))
+  .emit('page:update', { s2cMessagePageUpdated });
+```
+
+**notificationUpdated** (`apps/app/src/server/service/in-app-notification.ts`):
+```typescript
+this.socketIoService.getDefaultSocket()
+  .in(getRoomNameWithId(RoomPrefix.USER, userId))
+  .emit('notificationUpdated');
+```
+
+---
+
+## 💡 まとめ
+
+### 推奨アクション
+
+1. **Phase 1を優先実装**
+   - ルーム機能の導入により、97%のパフォーマンス改善が期待できる
+   - リアルタイム性を維持
+   - リスクが低い
+
+2. **Phase 2は状況に応じて**
+   - 一括マイグレーションが頻繁に発生する場合のみ実装
+   - Phase 1で十分な効果が得られる可能性が高い
+
+3. **他のイベントとの整合性**
+   - UpdateDescCountを修正することで、すべてのSocket.IOイベントが適切にルーム機能を使用する統一された実装になる