From c124ffb2fd67e4880113dd0d5fdc87ec8945fe6d Mon Sep 17 00:00:00 2001 From: mx57 <38256814+mx57@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:03:39 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Perf=20&=20BugFix:=20AI=20Orchestra?= =?UTF-8?q?tor=20optimizations=20and=20history=20persistence=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed critical history cache corruption in AiHistoryStore. - Improved AI loop responsiveness using SemaphoreSlim signaling. - Implemented thread-safe RunCycleAsync execution with _cycleLock. - Added BuildTargets result caching based on file timestamps and state equality. - Stabilized built-in strategy IDs using relative engine paths for better portability. - Optimized strategy exploration in BanditSelector from O(N log N) to O(N). - Implemented IEquatable for efficient equality checks. --- .Jules/bolt.md | 4 + FluxRoute.AI/Services/AiHistoryStore.cs | 12 +-- .../Services/AiOrchestratorService.cs | 77 +++++++++++++++++-- FluxRoute.AI/Services/BanditSelector.cs | 5 +- FluxRoute.Core/Models/TargetEntry.cs | 13 +++- 5 files changed, 92 insertions(+), 19 deletions(-) diff --git a/.Jules/bolt.md b/.Jules/bolt.md index 2d455a9..6290776 100644 --- a/.Jules/bolt.md +++ b/.Jules/bolt.md @@ -7,3 +7,7 @@ ## 2025-05-15 - Оптимизация AiStrategyRegistry (O(N) -> O(1)) **Инсайт:** Метод выбора стратегии BanditSelector.Pick вызывает агрегацию статистики (Alpha/Beta) для каждой активной стратегии. В AiStrategyRegistry эти данные хранились в плоском списке, что приводило к O(N) поиску при каждом вызове. При 100+ стратегиях и накопленной истории это создавало значительную паразитную нагрузку на CPU. **Действие:** Внедрил композитные индексы на основе Dictionary и группировку по GenomeId/NetworkHash. Это снижает сложность поиска до O(1), устраняя задержки при выборе стратегии ИИ. + +## 2026-06-18 - Исправление кэша истории и оптимизация цикла ИИ +**Инсайт:** Обнаружен критический баг в `AiHistoryStore`: при добавлении новой записи в пустой кэш он инициализировался списком из одного элемента, что блокировало загрузку всей предыдущей истории из файла. Также выявлена задержка реакции ИИ на смену сети из-за использования фиксированного `Task.Delay`. +**Действие:** Исправлена логика инициализации кэша (теперь `Append` не инициализирует кэш, оставляя это методу `LoadAll`). Внедрён `SemaphoreSlim` для мгновенного пробуждения фонового цикла ИИ при смене сети или ручном запросе. Добавлено кэширование списка целей проверки (BuildTargets) на основе меток времени файлов. diff --git a/FluxRoute.AI/Services/AiHistoryStore.cs b/FluxRoute.AI/Services/AiHistoryStore.cs index 61fd6b6..bf51441 100644 --- a/FluxRoute.AI/Services/AiHistoryStore.cs +++ b/FluxRoute.AI/Services/AiHistoryStore.cs @@ -25,14 +25,10 @@ public void Append(ProbeOutcome outcome) Directory.CreateDirectory(dir); File.AppendAllText(_path, line); - if (_cache != null) - { - _cache.Add(outcome); - } - else - { - _cache = new List { outcome }; - } + // BOLT ⚡: If _cache is null, we do NOT initialize it here with a single item. + // Doing so would cause LoadAll to return only that item and ignore previous history. + // We leave it null so LoadAll can perform a full file read later. + _cache?.Add(outcome); } } diff --git a/FluxRoute.AI/Services/AiOrchestratorService.cs b/FluxRoute.AI/Services/AiOrchestratorService.cs index d59356e..34f4ac1 100644 --- a/FluxRoute.AI/Services/AiOrchestratorService.cs +++ b/FluxRoute.AI/Services/AiOrchestratorService.cs @@ -38,6 +38,8 @@ public sealed class AiOrchestratorService : IDisposable private readonly AiHistoryStore _history; private readonly BanditSelector _bandit; private readonly StrategyEvolver _evolver; + private readonly SemaphoreSlim _loopSignal = new(0, 1); + private readonly SemaphoreSlim _cycleLock = new(1, 1); private CancellationTokenSource? _cts; private int _consecutiveFailures; @@ -49,6 +51,11 @@ public sealed class AiOrchestratorService : IDisposable private readonly ConcurrentDictionary _networkProbeCache = new(); private bool _fastStartDone; + private List _cachedTargets = []; + private DateTime _lastTargetsFileTime = DateTime.MinValue; + private HashSet _lastEnabledSites = []; + private List _lastUserSiteTargets = []; + public event EventHandler? StatusChanged; public AiOrchestratorService( @@ -158,8 +165,11 @@ public async Task ProbeAllEnabledStrategiesAsync(CancellationToken ct = default) } } - private void OnNetworkChanged(object? sender, (NetworkFingerprint OldFp, NetworkFingerprint NewFp) e) => + private void OnNetworkChanged(object? sender, (NetworkFingerprint OldFp, NetworkFingerprint NewFp) e) + { _networkDirty = true; + SignalLoop(); + } private async Task LoopAsync(CancellationToken ct) { @@ -191,12 +201,16 @@ private async Task LoopAsync(CancellationToken ct) try { - await Task.Delay(interval, ct).ConfigureAwait(false); + await _loopSignal.WaitAsync(interval, ct).ConfigureAwait(false); } catch (OperationCanceledException) { break; } + catch (ObjectDisposedException) + { + break; + } await RunCycleAsync(ct).ConfigureAwait(false); } @@ -240,6 +254,21 @@ private async Task PickAndApplyInitialAsync(CancellationToken ct) } private async Task RunCycleAsync(CancellationToken ct) + { + if (!await _cycleLock.WaitAsync(0, ct).ConfigureAwait(false)) + return; + + try + { + await RunCycleInternalAsync(ct).ConfigureAwait(false); + } + finally + { + _cycleLock.Release(); + } + } + + private async Task RunCycleInternalAsync(CancellationToken ct) { var ai = _aiSettings(); _history.RotateOldEntries(ai.KeepHistoryDays); @@ -775,7 +804,11 @@ private void SyncBuiltins() var name = Path.GetFileNameWithoutExtension(bat); var genome = GenomeParser.FromLaunchPlan(plan, name, StrategyOrigin.Builtin); - genome.Id = StableGuid.FromString("builtin:" + Path.GetFullPath(bat)); + + // Use relative path for ID stability if the app is moved + var relPath = Path.GetRelativePath(engineDir, bat).Replace('\\', '/'); + genome.Id = StableGuid.FromString("builtin:" + relPath); + genome.SourceBatPath = bat; genome.BatFileName = fn; genome.DisplayName = name; @@ -829,7 +862,17 @@ private static void TryDeleteGenomeBatFile(StrategyGenome g) private List BuildTargets() { - var targets = TargetEntry.ParseFile(_getTargetsPath()); + var path = _getTargetsPath(); + var fileInfo = new FileInfo(path); + var lastWrite = fileInfo.Exists ? fileInfo.LastWriteTime : DateTime.MinValue; + + bool sitesChanged = !EnabledSites.SetEquals(_lastEnabledSites); + bool userTargetsChanged = !UserSiteTargets.SequenceEqual(_lastUserSiteTargets); + + if (_cachedTargets.Count > 0 && lastWrite == _lastTargetsFileTime && !sitesChanged && !userTargetsChanged) + return _cachedTargets; + + var targets = TargetEntry.ParseFile(path); foreach (var site in EnabledSites) { @@ -839,11 +882,28 @@ private List BuildTargets() targets.AddRange(UserSiteTargets); - return targets + var final = targets .Where(x => !string.IsNullOrWhiteSpace(x.Value)) .GroupBy(x => $"{x.Kind}|{x.Key}|{x.Value}", StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToList(); + + _cachedTargets = final; + _lastTargetsFileTime = lastWrite; + _lastEnabledSites = [.. EnabledSites]; + _lastUserSiteTargets = [.. UserSiteTargets]; + + return final; + } + + private void SignalLoop() + { + try + { + if (_loopSignal.CurrentCount == 0) + _loopSignal.Release(); + } + catch (ObjectDisposedException) { } } private void Notify(string msg, bool switched = false, string? newProfile = null, @@ -858,5 +918,10 @@ private void Notify(string msg, bool switched = false, string? newProfile = null }); } - public void Dispose() => Stop(); + public void Dispose() + { + Stop(); + _loopSignal.Dispose(); + _cycleLock.Dispose(); + } } diff --git a/FluxRoute.AI/Services/BanditSelector.cs b/FluxRoute.AI/Services/BanditSelector.cs index 79d8259..93252c6 100644 --- a/FluxRoute.AI/Services/BanditSelector.cs +++ b/FluxRoute.AI/Services/BanditSelector.cs @@ -34,10 +34,7 @@ public BanditSelector(AiStrategyRegistry registry, Func? aiSettings if (_rng.NextDouble() * 1000 < explorationPermil) { - usable.Sort((a, b) => - _registry.SumPullsForGenomeOnNetwork(a.Id, networkHash) - .CompareTo(_registry.SumPullsForGenomeOnNetwork(b.Id, networkHash))); - return usable[0]; + return usable.MinBy(g => _registry.SumPullsForGenomeOnNetwork(g.Id, networkHash)); } var effective = _aiSettings().ParetoEnabled diff --git a/FluxRoute.Core/Models/TargetEntry.cs b/FluxRoute.Core/Models/TargetEntry.cs index d8ae4fb..d79a7ed 100644 --- a/FluxRoute.Core/Models/TargetEntry.cs +++ b/FluxRoute.Core/Models/TargetEntry.cs @@ -4,12 +4,23 @@ namespace FluxRoute.Core.Models; public enum TargetKind { Http, Ping } -public sealed class TargetEntry +public sealed class TargetEntry : IEquatable { public string Key { get; init; } = ""; public TargetKind Kind { get; init; } public string Value { get; init; } = ""; + public bool Equals(TargetEntry? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Key == other.Key && Kind == other.Kind && Value == other.Value; + } + + public override bool Equals(object? obj) => Equals(obj as TargetEntry); + + public override int GetHashCode() => HashCode.Combine(Key, Kind, Value); + public static List ParseFile(string path) { var result = new List();