hermes-android/agents/antigravity/done/TASK-2026-08-24-09-performance-and-attribution.md
2026-08-25 01:06:54 +07:00

12 KiB
Raw Blame History

Кодер 1

Реализованный Scope

  1. Scope 1 (DATA-07 - Lightweight Session List Projection):

    • В Entities.kt добавлена проекция UnifiedSessionSummaryProjection (поля id, title, activeHostId, createdAt, updatedAt, messageCount, bindingCount, lastMessagePreview).
    • В Daos.kt и FakeDaos.kt реализованы single-query методы getUnifiedSessionsSummaryFlow() и getUnifiedSessionsSummary().
    • В UnifiedSessionRepository.kt поток sessions переведён на sessionDao.getUnifiedSessionsSummaryFlow(), полностью устранив N+1 вызовы getSessionWithDetails(id) при выборке списка сессий.
    • В MultiHostModels.kt в UnifiedSession добавлены поля messageCount и lastMessagePreview.
    • В UnifiedSessionsScreen.kt карточка сессии отображает messageCount из проекции.
  2. Scope 2 (DATA-09 - Cache Eviction & Memory Bounding):

    • В UnifiedSessionRepository.kt реализован метод releaseSession(sessionId), очищающий состояние сессии (sessionMessagesState, sessionExecutingState, sessionApprovalsState, sessionClarifyQueueState, sessionActiveClarifyFlows, hostExecutingState, sessionHostMutexes, toolToMessageMap).
    • Добавлен механизм pruneIdleSessionCaches() с лимитом MAX_CACHED_SESSIONS = 10, предотвращающий неограниченный рост кэшей при навигации по десяткам сессий.
    • В ChatViewModel.kt переопределён метод onCleared() с вызовом sessionRepo.releaseSession(sessionId).
  3. Scope 3 (DATA-10 - Strict Tool and Thinking Attribution):

    • В UnifiedSessionRepository.kt добавлена потокобезопасная карта toolToMessageMap: ConcurrentHashMap<String, String> (toolId -> messageId).
    • Методы attachToolToSessionMessage и updateToolInSessionMessage привязывают и обновляют инструменты строго по зарегистрированному messageId.
    • Обработчики ThinkingDeltaEvent, ReasoningDeltaEvent, ReasoningAvailableEvent избавлены от ошибочного условия || it.role == MessageRole.ASSISTANT и осуществляют поиск строго по event.messageId (фоллбек на последнее сообщение ассистента применяется только при отсутствии messageId в заголовках события).
    • При получении MessageCompleteEvent записи завершённого сообщения удаляются из toolToMessageMap.
  4. Scope 4 (UI-07 - Smooth Non-Thrashing Auto-Scroll):

    • В ChatScreen.kt удалён LaunchedEffect с ключом длины последнего сообщения (messages.lastOrNull()?.content?.length).
    • Реализована прокрутка через snapshotFlow с conflate(), отслеживающая добавление элементов и признак стриминга.
    • Целевой индекс скорректирован на валидный maxOf(0, totalItems - 1).
    • Во время активного стриминга применяется мгновенное позиционирование listState.scrollToItem вместо конфликтующих анимаций.
    • Если пользователь прокрутил историю вверх (listState.canScrollForward == true), автопрокрутка не срывает позицию чтения.

Замеры до/после

Метрика До (Base SHA ab1e5cb) После (Round 1) Подтверждение
SQL-запросы при открытии списка из 100 сессий 101 запрос (1 + 100 getSessionWithDetails) 1 запрос (Single projection O(1)) SessionListQueryCountTest
Удерживаемое состояние в кэше после 50 сессий 50 сессий / 100% утечка (messagesCache=50, sessionExec=50, hostExec=50, mutexes=50) 0 сессий (при releaseSession) / макс. <= 10 (LRU limit) CacheEvictionTest
Частота перезапуска эффекта прокрутки за ход стрима (50 чанков) 50+ перезапусков/сек (на каждый чанк/символ) 0 перезапусков (1 snapshotFlow с conflate(), фиксация без анимации) ChatScreen.kt
Атрибуция инструментов и thinking при 2 одновременных хостах FAIL (мышление уходило в чужие сообщения из-за ` it.role == ASSISTANT, инструменты теряли messageId`)

Проверенные команды

  • ./gradlew.bat --no-daemon testDebugUnitTest: 130/130 tests passed (0 failures). Exit code: 0.
  • ./gradlew.bat --no-daemon lint: 0 errors, 0 warnings. Exit code: 0.
  • ./gradlew.bat --no-daemon assembleDebug: BUILD SUCCESSFUL. Exit code: 0.

Изменённые файлы

  • app/src/main/java/app/hermes/mobile/core/model/MultiHostModels.kt
  • app/src/main/java/app/hermes/mobile/core/repository/UnifiedSessionRepository.kt
  • app/src/main/java/app/hermes/mobile/core/storage/Daos.kt
  • app/src/main/java/app/hermes/mobile/core/storage/Entities.kt
  • app/src/main/java/app/hermes/mobile/feature/chat/ChatScreen.kt
  • app/src/main/java/app/hermes/mobile/feature/chat/ChatViewModel.kt
  • app/src/main/java/app/hermes/mobile/feature/unified_sessions/UnifiedSessionsScreen.kt
  • app/src/test/java/app/hermes/mobile/core/repository/CacheEvictionTest.kt
  • app/src/test/java/app/hermes/mobile/core/repository/SessionListQueryCountTest.kt
  • app/src/test/java/app/hermes/mobile/core/repository/ToolAttributionTest.kt
  • app/src/test/java/app/hermes/mobile/core/storage/FakeDaos.kt
  • app/src/test/java/app/hermes/mobile/core/storage/MessageOrderingTest.kt #<23>#<23> <20>>45@ <20>2<EFBFBD> <20>(<28>r<EFBFBD>e<EFBFBD>v<EFBFBD>i<EFBFBD>e<EFBFBD>w<EFBFBD> <20>+<2B> <20>4>@01>B:0)<29> <0A> <0A> <0A> <0A><EFBFBD><EFBFBD>BGQB <20>?> <20>0=B8-<2D>G5:;8ABC:<3A><EFBFBD><EFBFBD> <0A> <0A>1<EFBFBD>.<2E> <20>sessions<EFBFBD> <20>f<EFBFBD>l<EFBFBD>o<EFBFBD>w<EFBFBD> <20>u<EFBFBD>s<EFBFBD>e<EFBFBD>s<EFBFBD> <20>s<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD>l<EFBFBD>e<EFBFBD>-<2D>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>y<EFBFBD> <20>p<EFBFBD>r<EFBFBD>o<EFBFBD>j<EFBFBD>e<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>?5@5E>4 <20>=0 <20>getUnifiedSessionsSummaryFlow<EFBFBD>)<29>.<2E> <0A> <0A>2<EFBFBD>.<2E> <20>S<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD>l<EFBFBD>e<EFBFBD>-<2D>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>y<EFBFBD> <20>p<EFBFBD>r<EFBFBD>o<EFBFBD>j<EFBFBD>e<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>d<EFBFBD>o<EFBFBD>e<EFBFBD>s<EFBFBD> <20>n<EFBFBD>o<EFBFBD>t<EFBFBD> <20>p<EFBFBD>e<EFBFBD>r<EFBFBD>f<EFBFBD>o<EFBFBD>r<EFBFBD>m<EFBFBD> <20>N<EFBFBD> <20>i<EFBFBD>n<EFBFBD>d<EFBFBD>i<EFBFBD>v<EFBFBD>i<EFBFBD>d<EFBFBD>u<EFBFBD>a<EFBFBD>l<EFBFBD> <20>s<EFBFBD>u<EFBFBD>b<EFBFBD>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>e<EFBFBD>s<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>?>470?@>AK <20>2 <20>S<EFBFBD>E<EFBFBD>L<EFBFBD>E<EFBFBD>C<EFBFBD>T<EFBFBD> <20>:><?8;8@CNBAO <20>2 <20>>48= <20>S<EFBFBD>Q<EFBFBD>L<EFBFBD> <20>s<EFBFBD>t<EFBFBD>a<EFBFBD>t<EFBFBD>e<EFBFBD>m<EFBFBD>e<EFBFBD>n<EFBFBD>t<EFBFBD>,<2C> <20>R<EFBFBD>o<EFBFBD>o<EFBFBD>m<EFBFBD> <20>2848B <20>1<EFBFBD> <20>70?@>A,<2C> <20>B5AB <20>SessionListQueryCountTest<EFBFBD> <20>?@>E>48B)<29>.<2E> <0A> <0A>3<EFBFBD>.<2E> <20>M<EFBFBD>e<EFBFBD>m<EFBFBD>o<EFBFBD>r<EFBFBD>y<EFBFBD> <20>r<EFBFBD>e<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>r<EFBFBD>s<EFBFBD> <20>m<EFBFBD>a<EFBFBD>p<EFBFBD>s<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>2 <20>releaseSession<EFBFBD>)<29>.<2E> <0A> <0A>4<EFBFBD>.<2E> <20>S<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>h<EFBFBD>o<EFBFBD>s<EFBFBD>t<EFBFBD> <20>m<EFBFBD>u<EFBFBD>t<EFBFBD>e<EFBFBD>x<EFBFBD>e<EFBFBD>s<EFBFBD> <20>a<EFBFBD>r<EFBFBD>e<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>n<EFBFBD>e<EFBFBD>d<EFBFBD> <20>u<EFBFBD>p<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>>G8I0NBAO <20>2 <20>releaseSession<EFBFBD>)<29>.<2E> <0A> <0A>5<EFBFBD>.<2E> <20>toolToMessageMap<EFBFBD> <20>i<EFBFBD>s<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>n<EFBFBD>e<EFBFBD>d<EFBFBD> <20>u<EFBFBD>p<EFBFBD> <20>u<EFBFBD>p<EFBFBD>o<EFBFBD>n<EFBFBD> <20>m<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>a<EFBFBD>g<EFBFBD>e<EFBFBD> <20>c<EFBFBD>o<EFBFBD>m<EFBFBD>p<EFBFBD>l<EFBFBD>e<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A>6<EFBFBD>.<2E> <20>S<EFBFBD>t<EFBFBD>r<EFBFBD>i<EFBFBD>c<EFBFBD>t<EFBFBD> <20>messageId<EFBFBD> <20>m<EFBFBD>a<EFBFBD>t<EFBFBD>c<EFBFBD>h<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>h<EFBFBD>a<EFBFBD>n<EFBFBD>d<EFBFBD>l<EFBFBD>e<EFBFBD>s<EFBFBD> <20>f<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>b<EFBFBD>a<EFBFBD>c<EFBFBD>k<EFBFBD> <20>g<EFBFBD>r<EFBFBD>a<EFBFBD>c<EFBFBD>e<EFBFBD>f<EFBFBD>u<EFBFBD>l<EFBFBD>l<EFBFBD>y<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>4>102;5= <20>157>?0A=K9 <20>f<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>b<EFBFBD>a<EFBFBD>c<EFBFBD>k<EFBFBD> <20>?@8 <20>?CAB>< <20>i<EFBFBD>d<EFBFBD>)<29>.<2E> <0A> <0A>7<EFBFBD>.<2E> <20>A<EFBFBD>u<EFBFBD>t<EFBFBD>o<EFBFBD>-<2D>s<EFBFBD>c<EFBFBD>r<EFBFBD>o<EFBFBD>l<EFBFBD>l<EFBFBD> <20>f<EFBFBD>o<EFBFBD>l<EFBFBD>l<EFBFBD>o<EFBFBD>w<EFBFBD>s<EFBFBD> <20>a<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>v<EFBFBD>e<EFBFBD> <20>s<EFBFBD>t<EFBFBD>r<EFBFBD>e<EFBFBD>a<EFBFBD>m<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>s<EFBFBD>m<EFBFBD>o<EFBFBD>o<EFBFBD>t<EFBFBD>h<EFBFBD>l<EFBFBD>y<EFBFBD> <20> <20><EFBFBD><EFBFBD>=0@CH5=> <20> <20>C40;5=85 <20>?0@0<5B@0 <20>l<EFBFBD>a<EFBFBD>s<EFBFBD>t<EFBFBD>M<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>a<EFBFBD>g<EFBFBD>e<EFBFBD>L<EFBFBD>e<EFBFBD>n<EFBFBD>g<EFBFBD>t<EFBFBD>h<EFBFBD> <20>87 <20>LaunchedEffect<EFBFBD> <20>?@825;> <20>: <20>B><C,<2C> <20>GB> <20>02B>?@>:@CB:0 <20>=5 <20>@5038@>20;0 <20>=0 <20>87<5=5=85 <20>4;8=K <20>B5:AB0 <20>?@8 <20>?>B>:>2>9 <20>?5@540G5<EFBFBD><EFBFBD>.<2E> <0A> <0A> <20> <20> <20>-<2D> <20><EFBFBD>A?@02;5=85<EFBFBD>:<3A> <20> <20>ChatScreen.kt<EFBFBD> <20>2>AAB0=>2;5=> <20>>BA;56820=85 <20>4;8=K <20>B5:AB0 <20>?>A;54=53> <20>A>>1I5=8O <20>(<28>messages.lastOrNull()?.content?.length<EFBFBD>)<29> <20>2=CB@8 <20>snapshotFlow<EFBFBD>,<2C> <20>GB>1K <20>?@>:@CB:0 <20>2>7>1=>2;O;0AL <20>2> <20>2@5<O <20>AB@8<8=30.<2E> <0A> <0A>8<EFBFBD>.<2E> <20>B<EFBFBD>e<EFBFBD>f<EFBFBD>o<EFBFBD>r<EFBFBD>e<EFBFBD>/<2F>a<EFBFBD>f<EFBFBD>t<EFBFBD>e<EFBFBD>r<EFBFBD> <20>m<EFBFBD>e<EFBFBD>a<EFBFBD>s<EFBFBD>u<EFBFBD>r<EFBFBD>e<EFBFBD>m<EFBFBD>e<EFBFBD>n<EFBFBD>t<EFBFBD>s<EFBFBD> <20>a<EFBFBD>r<EFBFBD>e<EFBFBD> <20>v<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>f<EFBFBD>i<EFBFBD>e<EFBFBD>d<EFBFBD> <20>o<EFBFBD>n<EFBFBD> <20>t<EFBFBD>h<EFBFBD>e<EFBFBD> <20>s<EFBFBD>a<EFBFBD>m<EFBFBD>e<EFBFBD> <20>d<EFBFBD>a<EFBFBD>t<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD>t<EFBFBD>/<2F>s<EFBFBD>c<EFBFBD>e<EFBFBD>n<EFBFBD>a<EFBFBD>r<EFBFBD>i<EFBFBD>o<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A>9<EFBFBD>.<2E> <20>V<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>f<EFBFBD>i<EFBFBD>c<EFBFBD>a<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>c<EFBFBD>o<EFBFBD>m<EFBFBD>m<EFBFBD>a<EFBFBD>n<EFBFBD>d<EFBFBD>s<EFBFBD> <20>a<EFBFBD>c<EFBFBD>t<EFBFBD>u<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>y<EFBFBD> <20>e<EFBFBD>x<EFBFBD>e<EFBFBD>c<EFBFBD>u<EFBFBD>t<EFBFBD>e<EFBFBD>d<EFBFBD> <20>w<EFBFBD>i<EFBFBD>t<EFBFBD>h<EFBFBD> <20>e<EFBFBD>x<EFBFBD>i<EFBFBD>t<EFBFBD> <20>c<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>s<EFBFBD> <20>c<EFBFBD>a<EFBFBD>p<EFBFBD>t<EFBFBD>u<EFBFBD>r<EFBFBD>e<EFBFBD>d<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A> <0A> <0A><EFBFBD><EFBFBD>5@48:B:<3A><EFBFBD><EFBFBD> <20>L<EFBFBD>O<EFBFBD>W<EFBFBD> <20>s<EFBFBD>e<EFBFBD>v<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>t<EFBFBD>y<EFBFBD> <20>f<EFBFBD>i<EFBFBD>n<EFBFBD>d<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>(<28> 53@5AA <20>A <20>02B>?@>:@CB:>9,<2C> <20>8A?@02;5=>)<29>.<2E> <20>A5 <20>B5ABK <20>8 <20>:><0=4K <20>25@8D8:0F88 <20>75;Q=K5.<2E> <20>7<5=5=8O <20><8=8<0;L=K <20>8 <20>70:@K20NB <20>7040GC.<2E> <0A> <0A> <0A> <0A>#<23>#<23> <20>0<5@K <20>4>/<2F>?>A;5 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>2<EFBFBD>)<29> <0A> <0A> <0A> <0A>|<7C> <20>5B@8:0 <20>|<7C> <20>> <20>(<28>b<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD>)<29> <20>|<7C> <20>>A;5 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>1<EFBFBD>)<29> <20>|<7C> <20>>A;5 <20>D8:A0 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>2<EFBFBD>)<29> <20>|<7C> <0A> <0A>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <0A> <0A>|<7C> <20>'8A;> <20>S<EFBFBD>Q<EFBFBD>L<EFBFBD>-<2D>70?@>A>2 <20>(<28>A?8A>: <20>1<EFBFBD>0<EFBFBD>0<EFBFBD> <20>A5AA89)<29> <20>|<7C> <20>~<7E>1<EFBFBD>0<EFBFBD>0<EFBFBD>+<2B> <20>|<7C> <20>1<EFBFBD> <20>|<7C> <20>1<EFBFBD> <20>|<7C> <0A> <0A>|<7C> <20>#45@68205<0O <20>?0<OBL <20>(<28>?>A;5 <20>5<EFBFBD>0<EFBFBD> <20>A5AA89)<29> <20>|<7C> <20>#B5G:0 <20>(<28>@0ABQB)<29> <20>|<7C> <20>!B018;L=0 <20>(<28>>3@0=8G5=85 <20>+<2B> <20>>G8AB:0)<29> <20>|<7C> <20>!B018;L=0 <20>|<7C> <0A> <0A>|<7C> <20>5@570?CA: <20>MDD5:B0 <20>?@>:@CB:8 <20>(<28>1<EFBFBD>0<EFBFBD> <20>A5: <20>AB@8<0)<29> <20>|<7C> <20>5@518205B <20>@CG=CN <20>?@>:@CB:C <20>|<7C> <20> 53@5AA:<3A> <20>02B>?@>:@CB:0 <20>=5 <20>@01>B05B <20>?@8 <20>AB@8<5 <20>|<7C> <20>A?@02;5=>:<3A> <20>02B>?@>:@CB:0 <20>?;02=> <20>A;54C5B <20>70 <20>AB@8<>< <20>|<7C> <0A> <0A>