런처 유휴 CPU와 AX Agent 백그라운드 갱신 부담을 줄임
Some checks failed
Release Gate / gate (push) Has been cancelled

- 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)
This commit is contained in:
2026-04-06 23:34:03 +09:00
parent dc2574bb17
commit c75790f8c2
5 changed files with 129 additions and 8 deletions

View File

@@ -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 하도록 바꿨습니다.

View File

@@ -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을 줄였다.

View File

@@ -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);

View File

@@ -9,10 +9,16 @@ namespace AxCopilot.Core;
public class FuzzyEngine
{
private readonly IndexService _index;
private readonly object _cacheLock = new();
private readonly Dictionary<string, List<FuzzyResult>> _queryCache = new(StringComparer.Ordinal);
private readonly Queue<string> _queryCacheOrder = new();
private const int QueryCacheLimit = 64;
private int _indexGeneration;
public FuzzyEngine(IndexService index)
{
_index = index;
_index.IndexRebuilt += (_, _) => InvalidateQueryCache();
}
/// <summary>
@@ -25,6 +31,13 @@ public class FuzzyEngine
return Enumerable.Empty<FuzzyResult>();
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<FuzzyResult> 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<FuzzyResult> 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);
}
}
}
/// <summary>미리 계산된 캐시 필드를 활용하는 빠른 점수 계산.</summary>

View File

@@ -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<string, string> _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;