Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) на основе меток времени файлов.
12 changes: 4 additions & 8 deletions FluxRoute.AI/Services/AiHistoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProbeOutcome> { 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);
}
}

Expand Down
77 changes: 71 additions & 6 deletions FluxRoute.AI/Services/AiOrchestratorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,6 +51,11 @@ public sealed class AiOrchestratorService : IDisposable
private readonly ConcurrentDictionary<string, (int score, DateTimeOffset at)> _networkProbeCache = new();
private bool _fastStartDone;

private List<TargetEntry> _cachedTargets = [];
private DateTime _lastTargetsFileTime = DateTime.MinValue;
private HashSet<string> _lastEnabledSites = [];
private List<TargetEntry> _lastUserSiteTargets = [];

public event EventHandler<OrchestratorEventArgs>? StatusChanged;

public AiOrchestratorService(
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -829,7 +862,17 @@ private static void TryDeleteGenomeBatFile(StrategyGenome g)

private List<TargetEntry> 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)
{
Expand All @@ -839,11 +882,28 @@ private List<TargetEntry> 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,
Expand All @@ -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();
}
}
5 changes: 1 addition & 4 deletions FluxRoute.AI/Services/BanditSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ public BanditSelector(AiStrategyRegistry registry, Func<AiSettings>? 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
Expand Down
13 changes: 12 additions & 1 deletion FluxRoute.Core/Models/TargetEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ namespace FluxRoute.Core.Models;

public enum TargetKind { Http, Ping }

public sealed class TargetEntry
public sealed class TargetEntry : IEquatable<TargetEntry>
{
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<TargetEntry> ParseFile(string path)
{
var result = new List<TargetEntry>();
Expand Down