From c75790f8c2f7de2311fb9d963f5fa1f2670dc7c3 Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 23:34:03 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9F=B0=EC=B2=98=20=EC=9C=A0=ED=9C=B4=20CPU?= =?UTF-8?q?=EC=99=80=20AX=20Agent=20=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B0=B1=EC=8B=A0=20=EB=B6=80=EB=8B=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A4=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FuzzyEngine에 인덱스 버전 기준 쿼리 캐시를 추가해 색인 완료 후 반복 검색 반응성을 개선함 - 캐시된 인덱스가 없을 때는 앱 시작 시 watcher를 먼저 켜지 않도록 조정해 런처 유휴 CPU 부담을 완화함 - AX Agent는 최소화/백그라운드 상태에서 task summary, 입력 보조 UI, agent UI flush를 지연했다가 활성화 시 한 번에 반영하도록 정리함 - README와 DEVELOPMENT 문서에 2026-04-06 23:33 (KST) 기준 이력을 반영함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 4 ++ docs/DEVELOPMENT.md | 13 ++++++ src/AxCopilot/App.xaml.cs | 3 +- src/AxCopilot/Core/FuzzyEngine.cs | 63 +++++++++++++++++++++++--- src/AxCopilot/Views/ChatWindow.xaml.cs | 54 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 14357ec..c83bac7 100644 --- a/README.md +++ b/README.md @@ -1383,3 +1383,7 @@ MIT License - 업데이트: 2026-04-06 23:26 (KST) - AX Agent의 중간 진행 메시지를 `claw-code`에 더 가깝게 마무리했습니다. execution history를 접어 둔 상태에서도 `처리 중...`, `컨텍스트 압축 중...`, 중요한 thinking/tool 진행 이벤트는 transcript에 계속 보이도록 필터를 조정했습니다. - 진행 줄 스타일도 카드형 박스보다 더 평평한 요약줄 위주로 정리했습니다. 일반 진행 이벤트는 borderless line처럼 보이고, 실제 장기 대기/압축 상태만 은은한 강조 배경과 펄스 마커를 유지해 “지금 살아 있는 작업”만 더 잘 드러나게 맞췄습니다. +- 업데이트: 2026-04-06 23:33 (KST) + - 런처 검색 반응성을 높이기 위해 [FuzzyEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/FuzzyEngine.cs)에 인덱스 버전 기준 쿼리 캐시를 추가했습니다. 색인이 같은 상태에서 반복 입력되는 쿼리는 결과를 다시 전부 계산하지 않고 즉시 재사용합니다. + - 앱 시작 직후 캐시된 인덱스가 없을 때는 런처 watcher를 먼저 모두 켜지 않도록 [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs)를 조정했습니다. 불필요한 감시기 오버헤드를 줄이고, 실제 첫 색인 완료 뒤에 watcher가 붙도록 정리했습니다. + - AX Agent는 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 최소화/백그라운드 상태일 때 task summary, 입력 보조 UI, 에이전트 상태 반영을 즉시 다시 그리지 않고 대기시켰다가 다시 활성화될 때 한 번에 flush 하도록 바꿨습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a7131c5..3245165 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5110,3 +5110,16 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 진행 줄 메타 표기를 `경과 시간 · 누적 토큰` 형식으로 통일하는 `BuildReadableProgressMetaText(...)`를 추가했다. - 일반 진행 이벤트는 borderless에 가까운 평평한 line 스타일로, 실제 장기 대기/압축 상태만 강조 배경/테두리를 유지하는 `CreateReadableProgressFeedCard(...)`를 추가했다. - 결과적으로 AX Agent의 중간 처리 피드가 `claw-code`처럼 “기다릴 수 있는 라이브 진행 줄”에 더 가까워졌다. + +## 2026-04-06 23:33 (KST) + +- [FuzzyEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/FuzzyEngine.cs) + - 인덱스 버전 기준 쿼리 캐시를 추가했다. + - 같은 인덱스 스냅샷에서 반복되는 검색어는 fuzzy 점수 계산을 다시 전부 돌리지 않고 캐시된 `FuzzyResult` 목록을 즉시 반환한다. + - 인덱스 재빌드가 완료되면 `IndexRebuilt` 이벤트를 받아 캐시를 자동으로 무효화한다. +- [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs) + - 앱 시작 시 캐시된 런처 인덱스가 실제로 로드된 경우에만 watcher를 즉시 시작하도록 조정했다. + - 캐시가 없는 상태에서 거대한 감시기를 먼저 켜는 오버헤드를 피하고, 초기 전체 색인이 끝난 뒤 watcher가 붙도록 정리했다. +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) + - 최소화/비활성/백그라운드 상태를 `IsBackgroundUiThrottleActive()`로 판단해 task summary, 입력 보조 UI, agent UI event flush를 바로 수행하지 않고 pending 상태로 넘긴다. + - 창이 다시 활성화되면 `FlushDeferredUiRefreshIfNeeded()`에서 누적된 갱신을 한 번에 반영해, 백그라운드 상태의 잦은 UI 타이머 churn을 줄였다. diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index 1027d0d..02f818c 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -116,7 +116,8 @@ public partial class App : System.Windows.Application _indexService = new IndexService(settings); var indexService = _indexService; indexService.LoadCachedIndex(); - indexService.StartWatchers(); + if (indexService.HasCachedIndexLoaded) + indexService.StartWatchers(); var fuzzyEngine = new FuzzyEngine(indexService); var commandResolver = new CommandResolver(fuzzyEngine, settings); var contextManager = new ContextManager(settings); diff --git a/src/AxCopilot/Core/FuzzyEngine.cs b/src/AxCopilot/Core/FuzzyEngine.cs index 30f27e7..d3555f0 100644 --- a/src/AxCopilot/Core/FuzzyEngine.cs +++ b/src/AxCopilot/Core/FuzzyEngine.cs @@ -9,10 +9,16 @@ namespace AxCopilot.Core; public class FuzzyEngine { private readonly IndexService _index; + private readonly object _cacheLock = new(); + private readonly Dictionary> _queryCache = new(StringComparer.Ordinal); + private readonly Queue _queryCacheOrder = new(); + private const int QueryCacheLimit = 64; + private int _indexGeneration; public FuzzyEngine(IndexService index) { _index = index; + _index.IndexRebuilt += (_, _) => InvalidateQueryCache(); } /// @@ -25,6 +31,13 @@ public class FuzzyEngine return Enumerable.Empty(); var normalized = query.Trim().ToLowerInvariant(); + var cacheKey = $"{_indexGeneration}:{maxResults}:{normalized}"; + lock (_cacheLock) + { + if (_queryCache.TryGetValue(cacheKey, out var cached)) + return cached; + } + var entries = _index.Entries; // 쿼리 언어 타입 1회 사전 분류 — 항목마다 재계산하지 않음 @@ -36,20 +49,56 @@ public class FuzzyEngine } // 300개 초과 시 PLINQ 병렬 처리 + List results; if (entries.Count > 300) { - return entries.AsParallel() + results = entries.AsParallel() .Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean))) .Where(r => r.Score > 0) .OrderByDescending(r => r.Score) - .Take(maxResults); + .Take(maxResults) + .ToList(); + } + else + { + results = entries + .Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean))) + .Where(r => r.Score > 0) + .OrderByDescending(r => r.Score) + .Take(maxResults) + .ToList(); } - return entries - .Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean))) - .Where(r => r.Score > 0) - .OrderByDescending(r => r.Score) - .Take(maxResults); + StoreCachedResults(cacheKey, results); + return results; + } + + private void InvalidateQueryCache() + { + lock (_cacheLock) + { + _indexGeneration++; + _queryCache.Clear(); + _queryCacheOrder.Clear(); + } + } + + private void StoreCachedResults(string cacheKey, List results) + { + lock (_cacheLock) + { + if (_queryCache.ContainsKey(cacheKey)) + return; + + _queryCache[cacheKey] = results; + _queryCacheOrder.Enqueue(cacheKey); + + while (_queryCacheOrder.Count > QueryCacheLimit) + { + var oldKey = _queryCacheOrder.Dequeue(); + _queryCache.Remove(oldKey); + } + } } /// 미리 계산된 캐시 필드를 활용하는 빠른 점수 계산. diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 848a52c..e4687fa 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -38,6 +38,9 @@ public partial class ChatWindow : Window private bool _isInWindowMoveSizeLoop; private bool _pendingResponsiveLayoutRefresh; private bool _pendingHiddenExecutionHistoryRender; + private bool _pendingBackgroundTaskSummaryRefresh; + private bool _pendingBackgroundInputUiRefresh; + private bool _pendingBackgroundAgentUiEventFlush; private CacheMode? _cachedRootCacheModeBeforeMove; private string _selectedCategory = ""; // "" = 전체 private readonly Dictionary _tabSelectedCategory = new(StringComparer.OrdinalIgnoreCase) @@ -308,6 +311,7 @@ public partial class ChatWindow : Window if (TokenUsagePopup != null) TokenUsagePopup.IsOpen = false; }; + Activated += (_, _) => FlushDeferredUiRefreshIfNeeded(); IsVisibleChanged += (_, _) => FlushDeferredUiRefreshIfNeeded(); StateChanged += (_, _) => FlushDeferredUiRefreshIfNeeded(); UpdateConversationFailureFilterUi(); @@ -2726,6 +2730,12 @@ public partial class ChatWindow : Window if (_inputUiRefreshTimer == null) return; + if (IsBackgroundUiThrottleActive()) + { + _pendingBackgroundInputUiRefresh = true; + return; + } + _inputUiRefreshTimer.Stop(); _inputUiRefreshTimer.Start(); } @@ -5674,10 +5684,37 @@ public partial class ChatWindow : Window _executionHistoryRenderTimer.Stop(); _executionHistoryRenderTimer.Start(); } + + if (_pendingBackgroundTaskSummaryRefresh) + { + _pendingBackgroundTaskSummaryRefresh = false; + _taskSummaryRefreshTimer.Stop(); + _taskSummaryRefreshTimer.Start(); + } + + if (_pendingBackgroundInputUiRefresh) + { + _pendingBackgroundInputUiRefresh = false; + _inputUiRefreshTimer.Stop(); + _inputUiRefreshTimer.Start(); + } + + if (_pendingBackgroundAgentUiEventFlush) + { + _pendingBackgroundAgentUiEventFlush = false; + _agentUiEventTimer.Stop(); + _agentUiEventTimer.Start(); + } } private void ScheduleTaskSummaryRefresh() { + if (IsBackgroundUiThrottleActive()) + { + _pendingBackgroundTaskSummaryRefresh = true; + return; + } + _taskSummaryRefreshTimer.Stop(); _taskSummaryRefreshTimer.Start(); } @@ -5695,10 +5732,27 @@ public partial class ChatWindow : Window private void ScheduleAgentUiEvent(AgentEvent evt) { _pendingAgentUiEvent = evt; + if (IsBackgroundUiThrottleActive()) + { + _pendingBackgroundAgentUiEventFlush = true; + return; + } + _agentUiEventTimer.Stop(); _agentUiEventTimer.Start(); } + private bool IsBackgroundUiThrottleActive() + { + if (!IsLoaded) + return true; + + if (!IsVisible || WindowState == WindowState.Minimized) + return true; + + return !IsActive; + } + private void FlushPendingAgentUiEvent() { var evt = _pendingAgentUiEvent;