Ottimizzazione RAM, UI e sistema di timing aste

- Ridotto consumo RAM: limiti log, pulizia e compattazione dati aste, timer periodico di cleanup
- UI più fluida: cache locale aste, throttling aggiornamenti, refresh log solo se necessario
- Nuovo sistema Ticker Loop: timing configurabile, strategie solo vicino alla scadenza, feedback puntate tardive
- Migliorato layout e splitter, log visivo, gestione cache HTML
- Aggiornata UI impostazioni e fix vari per performance e thread-safety
This commit is contained in:
2026-02-07 19:28:30 +01:00
parent 5b95f18889
commit 690f7e636a
10 changed files with 1068 additions and 396 deletions

View File

@@ -13,8 +13,9 @@ namespace AutoBidder.Models
{ {
/// <summary> /// <summary>
/// Numero massimo di righe di log da mantenere per ogni asta /// Numero massimo di righe di log da mantenere per ogni asta
/// Ridotto per ottimizzare consumo RAM
/// </summary> /// </summary>
private const int MAX_LOG_LINES = 500; private const int MAX_LOG_LINES = 200;
public string AuctionId { get; set; } = ""; public string AuctionId { get; set; } = "";
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
@@ -176,6 +177,11 @@ namespace AutoBidder.Models
{ {
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
// DEBUG: Print per verificare che i log vengano aggiunti
#if DEBUG
System.Diagnostics.Debug.WriteLine($"[AddLog] {AuctionId}: {message}");
#endif
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore // ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
if (AuctionLog.Count > 0) if (AuctionLog.Count > 0)
{ {
@@ -233,9 +239,9 @@ namespace AutoBidder.Models
public List<int> LatencyHistory { get; set; } = new(); public List<int> LatencyHistory { get; set; } = new();
/// <summary> /// <summary>
/// Numero massimo di latenze da memorizzare /// Numero massimo di latenze da memorizzare (ridotto per RAM)
/// </summary> /// </summary>
private const int MAX_LATENCY_HISTORY = 20; private const int MAX_LATENCY_HISTORY = 10;
/// <summary> /// <summary>
/// Aggiunge una misurazione di latenza allo storico /// Aggiunge una misurazione di latenza allo storico
@@ -390,6 +396,103 @@ namespace AutoBidder.Models
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public double DuelAdvantage { get; set; } = 0; public double DuelAdvantage { get; set; } = 0;
// ???????????????????????????????????????????????????????????????????
// GESTIONE MEMORIA
// ???????????????????????????????????????????????????????????????????
/// <summary>
/// Pulisce tutti i dati in memoria dell'asta per liberare RAM.
/// Chiamare prima di rimuovere l'asta dalla lista.
/// </summary>
public void ClearData()
{
// Pulisci liste storiche
BidHistory?.Clear();
BidHistory = null!;
RecentBids?.Clear();
RecentBids = null!;
AuctionLog?.Clear();
AuctionLog = null!;
BidderStats?.Clear();
BidderStats = null!;
LatencyHistory?.Clear();
LatencyHistory = null!;
AggressiveBidders?.Clear();
AggressiveBidders = null!;
// Pulisci oggetti complessi
LastState = null;
CalculatedValue = null;
DuelOpponent = null;
WinLimitDescription = null;
// Reset flag
IsTrackedFromStart = false;
TrackingStartedAt = null;
DeadlineUtc = null;
LastDeadlineUpdateUtc = null;
}
/// <summary>
/// Compatta i dati mantenendo solo le informazioni recenti.
/// Utile per ridurre la memoria senza eliminare completamente i dati.
/// </summary>
public void CompactData(int maxBidHistory = 50, int maxRecentBids = 30, int maxLogLines = 100)
{
// Compatta BidHistory
if (BidHistory != null && BidHistory.Count > maxBidHistory)
{
var recent = BidHistory.TakeLast(maxBidHistory).ToList();
BidHistory.Clear();
BidHistory.AddRange(recent);
BidHistory.TrimExcess();
}
// Compatta RecentBids
if (RecentBids != null && RecentBids.Count > maxRecentBids)
{
var recent = RecentBids.TakeLast(maxRecentBids).ToList();
RecentBids.Clear();
RecentBids.AddRange(recent);
RecentBids.TrimExcess();
}
// Compatta AuctionLog
if (AuctionLog != null && AuctionLog.Count > maxLogLines)
{
var recent = AuctionLog.TakeLast(maxLogLines).ToList();
AuctionLog.Clear();
AuctionLog.AddRange(recent);
AuctionLog.TrimExcess();
}
// Compatta LatencyHistory
if (LatencyHistory != null && LatencyHistory.Count > 10)
{
var recent = LatencyHistory.TakeLast(10).ToList();
LatencyHistory.Clear();
LatencyHistory.AddRange(recent);
LatencyHistory.TrimExcess();
}
// Compatta BidderStats - mantieni solo i top bidders
if (BidderStats != null && BidderStats.Count > 20)
{
var topBidders = BidderStats
.OrderByDescending(kv => kv.Value.BidCount)
.Take(20)
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
BidderStats.Clear();
foreach (var kv in topBidders)
BidderStats[kv.Key] = kv.Value;
}
}
} }
/// <summary> /// <summary>

View File

@@ -27,7 +27,7 @@
<div class="status-indicators"> <div class="status-indicators">
<div class="status-pill total" title="Totale aste"> <div class="status-pill total" title="Totale aste">
<i class="bi bi-collection"></i> <i class="bi bi-collection"></i>
<span>@auctions.Count</span> <span>@(auctions?.Count ?? 0)</span>
</div> </div>
<div class="status-pill active" title="Aste attive"> <div class="status-pill active" title="Aste attive">
<i class="bi bi-play-circle-fill"></i> <i class="bi bi-play-circle-fill"></i>
@@ -76,7 +76,7 @@
<i class="bi bi-x-circle"></i> <i class="bi bi-x-circle"></i>
</button> </button>
<div class="manage-separator"></div> <div class="manage-separator"></div>
<button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi TUTTE"> <button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@((auctions?.Count ?? 0) == 0)" title="Rimuovi TUTTE">
<i class="bi bi-trash-fill"></i> <i class="bi bi-trash-fill"></i>
</button> </button>
</div> </div>
@@ -91,7 +91,7 @@
<div class="panel-header"> <div class="panel-header">
<span><i class="bi bi-list-check"></i> Aste Monitorate</span> <span><i class="bi bi-list-check"></i> Aste Monitorate</span>
</div> </div>
@if (auctions.Count == 0) @if ((auctions?.Count ?? 0) == 0)
{ {
<div class="alert alert-info animate-fade-in-up m-2"> <div class="alert alert-info animate-fade-in-up m-2">
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su <i class="bi bi-plus-lg"></i> per iniziare. <i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su <i class="bi bi-plus-lg"></i> per iniziare.
@@ -521,7 +521,7 @@
</div> </div>
} }
@* Script Splitter *@ @* Script Splitter - Versione Fixed senza sovrapposizioni *@
<script suppress-error="BL9992"> <script suppress-error="BL9992">
(function() { (function() {
function initSplitters() { function initSplitters() {
@@ -541,12 +541,23 @@
let startPos = 0; let startPos = 0;
let startSizeA = 0; let startSizeA = 0;
let startSizeB = 0; let startSizeB = 0;
let containerSize = 0;
function onMouseDown(e, type, elA, elB) { function onMouseDown(e, type, elA, elB) {
active = { type, elA, elB }; active = { type, elA, elB };
startPos = type === 'v' ? e.clientX : e.clientY; startPos = type === 'v' ? e.clientX : e.clientY;
startSizeA = type === 'v' ? elA.offsetWidth : elA.offsetHeight;
startSizeB = type === 'v' ? elB.offsetWidth : elB.offsetHeight; // Calcola dimensioni attuali
if (type === 'v') {
startSizeA = elA.offsetWidth;
startSizeB = elB.offsetWidth;
containerSize = elA.parentElement.offsetWidth - gutterV.offsetWidth;
} else {
startSizeA = elA.offsetHeight;
startSizeB = elB.offsetHeight;
containerSize = elA.parentElement.offsetHeight - gutterH.offsetHeight;
}
document.body.style.cursor = type === 'v' ? 'col-resize' : 'row-resize'; document.body.style.cursor = type === 'v' ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
e.preventDefault(); e.preventDefault();
@@ -560,16 +571,34 @@
const { type, elA, elB } = active; const { type, elA, elB } = active;
const pos = type === 'v' ? e.clientX : e.clientY; const pos = type === 'v' ? e.clientX : e.clientY;
const diff = pos - startPos; const diff = pos - startPos;
const total = startSizeA + startSizeB;
let newA = startSizeA + diff; let newA = startSizeA + diff;
let newB = startSizeB - diff; let newB = startSizeB - diff;
const minA = type === 'v' ? 200 : 150; // Limiti minimi
const minB = type === 'v' ? 150 : 80; const minA = type === 'v' ? 300 : 200;
const minB = type === 'v' ? 200 : 150;
if (newA < minA) { newA = minA; newB = total - newA; } // Applica limiti
if (newB < minB) { newB = minB; newA = total - newB; } if (newA < minA) {
newA = minA;
newB = containerSize - newA;
}
if (newB < minB) {
newB = minB;
newA = containerSize - newB;
}
// Assicura che la somma sia corretta (no sovrapposizioni/gap)
const totalCheck = newA + newB;
if (Math.abs(totalCheck - containerSize) > 1) {
// Normalizza le dimensioni
const ratio = containerSize / totalCheck;
newA = Math.round(newA * ratio);
newB = containerSize - newA;
}
// Applica le nuove dimensioni con flex none per dimensioni fisse
if (type === 'v') { if (type === 'v') {
elA.style.width = newA + 'px'; elA.style.width = newA + 'px';
elA.style.flex = 'none'; elA.style.flex = 'none';

View File

@@ -15,7 +15,44 @@ namespace AutoBidder.Pages
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!; [Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
[Inject] private StatsService StatsService { get; set; } = default!; [Inject] private StatsService StatsService { get; set; } = default!;
private List<AuctionInfo> auctions => AppState.Auctions.ToList(); // Cache locale per evitare ricreare liste ad ogni render
private List<AuctionInfo>? _cachedAuctions = new List<AuctionInfo>(); // Inizializzata per evitare NullRef
private int _lastAuctionsHash;
private List<AuctionInfo> auctions
{
get
{
try
{
// Protezione null-safety
if (AppState == null) return _cachedAuctions ?? new List<AuctionInfo>();
// Usa cache per evitare copie continue
var current = AppState.GetAuctionsDirectRef();
if (current == null) return _cachedAuctions ?? new List<AuctionInfo>();
var hash = current.Count;
if (_cachedAuctions == null || hash != _lastAuctionsHash)
{
_cachedAuctions = current.ToList();
_lastAuctionsHash = hash;
}
return _cachedAuctions;
}
catch
{
// Fallback sicuro
return _cachedAuctions ?? new List<AuctionInfo>();
}
}
}
// Invalida cache quando necessario
private void InvalidateAuctionCache()
{
_cachedAuctions = null;
}
private AuctionInfo? selectedAuction private AuctionInfo? selectedAuction
{ {
@@ -37,7 +74,7 @@ namespace AutoBidder.Pages
} }
} }
private List<LogEntry> globalLog => AppState.GlobalLog.ToList(); private List<LogEntry> globalLog => AppState.GetLogDirectRef();
private bool isMonitoringActive private bool isMonitoringActive
{ {
get => AppState.IsMonitoringActive; get => AppState.IsMonitoringActive;
@@ -45,8 +82,16 @@ namespace AutoBidder.Pages
} }
private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer; private System.Threading.Timer? sessionTimer;
private System.Threading.Timer? logRefreshTimer;
private DateTime _lastUiUpdate = DateTime.MinValue;
private DateTime _lastLogRefresh = DateTime.MinValue;
private const int UI_UPDATE_THROTTLE_MS = 250; // Max 4 aggiornamenti al secondo
private const int LOG_REFRESH_MS = 500; // Aggiorna log ogni 500ms se ci sono cambiamenti
// Tracking log asta selezionata
private int _lastSelectedAuctionLogCount = 0;
private string? _lastSelectedAuctionId = null;
// Dialog Aggiungi Asta // Dialog Aggiungi Asta
private bool showAddDialog = false; private bool showAddDialog = false;
@@ -92,20 +137,68 @@ namespace AutoBidder.Pages
AuctionMonitor.OnLog += OnGlobalLog; AuctionMonitor.OnLog += OnGlobalLog;
AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated; AuctionMonitor.OnAuctionUpdated += OnAuctionUpdated;
refreshTimer = new System.Threading.Timer(async _ => // RIMOSSO: Timer refresh ogni 1s causava lag estremo
// Gli aggiornamenti UI ora avvengono SOLO quando necessario via eventi
// Timer dedicato per aggiornare i log (leggero, solo se ci sono cambiamenti)
logRefreshTimer = new System.Threading.Timer(async _ =>
{
try
{
bool needsRefresh = false;
// Controlla se il log globale è cambiato
var currentGlobalLogCount = globalLog.Count;
if (currentGlobalLogCount != lastLogCount)
{
lastLogCount = currentGlobalLogCount;
needsRefresh = true;
}
// Controlla se l'asta selezionata ha nuovi log
var selected = selectedAuction;
if (selected != null)
{
var currentAuctionLogCount = selected.AuctionLog.Count;
// Se è cambiata l'asta selezionata, forza refresh
if (_lastSelectedAuctionId != selected.AuctionId)
{
_lastSelectedAuctionId = selected.AuctionId;
_lastSelectedAuctionLogCount = currentAuctionLogCount;
needsRefresh = true;
}
// Se il count dei log è cambiato, refresh
else if (currentAuctionLogCount != _lastSelectedAuctionLogCount)
{
_lastSelectedAuctionLogCount = currentAuctionLogCount;
needsRefresh = true;
}
}
else
{
// Nessuna asta selezionata, reset tracking
_lastSelectedAuctionId = null;
_lastSelectedAuctionLogCount = 0;
}
if (needsRefresh)
{ {
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); }
}
catch { /* Ignora errori del timer */ }
}, null, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(300));
// Carica sessione all'avvio // Carica sessione all'avvio
LoadSession(); LoadSession();
// Timer per aggiornamento sessione ogni 30 secondi // Timer per aggiornamento sessione ogni 60 secondi (era 30)
sessionTimer = new System.Threading.Timer(async _ => sessionTimer = new System.Threading.Timer(async _ =>
{ {
await RefreshSessionAsync(); await RefreshSessionAsync();
await InvokeAsync(StateHasChanged); await ThrottledStateHasChanged();
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); }, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -132,6 +225,20 @@ namespace AutoBidder.Pages
// Handler async per eventi da background thread // Handler async per eventi da background thread
private async Task OnAppStateChangedAsync() private async Task OnAppStateChangedAsync()
{ {
await ThrottledStateHasChanged();
}
/// <summary>
/// Aggiorna UI con throttling per evitare troppi re-render
/// </summary>
private async Task ThrottledStateHasChanged()
{
var now = DateTime.UtcNow;
if ((now - _lastUiUpdate).TotalMilliseconds < UI_UPDATE_THROTTLE_MS)
{
return; // Skip, aggiornamento recente già fatto
}
_lastUiUpdate = now;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -171,12 +278,12 @@ namespace AutoBidder.Pages
private void OnGlobalLog(string message) private void OnGlobalLog(string message)
{ {
AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}"); AppState.AddLog($"[{DateTime.Now:HH:mm:ss}] {message}");
InvokeAsync(StateHasChanged); // Non forziamo StateHasChanged qui - verrà aggiornato dal throttle
} }
private void OnAuctionUpdated(AuctionState state) private void OnAuctionUpdated(AuctionState state)
{ {
var auction = auctions.FirstOrDefault(a => a.AuctionId == state.AuctionId); var auction = AppState.GetAuctionById(state.AuctionId);
if (auction != null) if (auction != null)
{ {
// Salva l'ultimo stato ricevuto // Salva l'ultimo stato ricevuto
@@ -188,18 +295,21 @@ namespace AutoBidder.Pages
auction.BidsUsedOnThisAuction = state.MyBidsCount.Value; auction.BidsUsedOnThisAuction = state.MyBidsCount.Value;
} }
// Notifica il cambiamento usando InvokeAsync per thread-safety // Invalida cache
_ = InvokeAsync(() => InvalidateAuctionCache();
{
AppState.ForceUpdate(); // Notifica con throttling
StateHasChanged(); _ = ThrottledStateHasChanged();
});
} }
} }
private void SelectAuction(AuctionInfo auction) private void SelectAuction(AuctionInfo auction)
{ {
selectedAuction = auction; // Imposta direttamente senza notifiche async per risposta immediata
AppState.SetSelectedAuctionDirect(auction);
// Forza aggiornamento immediato per visualizzare i log dell'asta selezionata
StateHasChanged();
} }
// Gestione controlli globali // Gestione controlli globali
@@ -1384,8 +1494,8 @@ namespace AutoBidder.Pages
public void Dispose() public void Dispose()
{ {
refreshTimer?.Dispose();
sessionTimer?.Dispose(); sessionTimer?.Dispose();
logRefreshTimer?.Dispose();
// Rimuovi sottoscrizioni (ASYNC) // Rimuovi sottoscrizioni (ASYNC)
if (AppState != null) if (AppState != null)
@@ -1475,34 +1585,69 @@ namespace AutoBidder.Pages
private int GetActiveAuctionsCount() private int GetActiveAuctionsCount()
{ {
return auctions.Count(a => a.IsActive && !a.IsPaused && try
(a.LastState == null || a.LastState.Status == AuctionStatus.Running)); {
return auctions?.Count(a => a.IsActive && !a.IsPaused &&
(a.LastState == null || a.LastState.Status == AuctionStatus.Running)) ?? 0;
}
catch
{
return 0;
}
} }
private int GetPausedAuctionsCount() private int GetPausedAuctionsCount()
{ {
return auctions.Count(a => a.IsPaused || try
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused)); {
return auctions?.Count(a => a.IsPaused ||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused)) ?? 0;
}
catch
{
return 0;
}
} }
private int GetWonAuctionsCount() private int GetWonAuctionsCount()
{ {
return auctions.Count(a => a.LastState != null && try
a.LastState.Status == AuctionStatus.EndedWon); {
return auctions?.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedWon) ?? 0;
}
catch
{
return 0;
}
} }
private int GetLostAuctionsCount() private int GetLostAuctionsCount()
{ {
return auctions.Count(a => a.LastState != null && try
a.LastState.Status == AuctionStatus.EndedLost); {
return auctions?.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedLost) ?? 0;
}
catch
{
return 0;
}
} }
private int GetStoppedAuctionsCount() private int GetStoppedAuctionsCount()
{ {
return auctions.Count(a => !a.IsActive && try
{
return auctions?.Count(a => !a.IsActive &&
(a.LastState == null || (a.LastState == null ||
(a.LastState.Status != AuctionStatus.EndedWon && (a.LastState.Status != AuctionStatus.EndedWon &&
a.LastState.Status != AuctionStatus.EndedLost))); a.LastState.Status != AuctionStatus.EndedLost))) ?? 0;
}
catch
{
return 0;
}
} }
} }
} }

View File

@@ -138,7 +138,15 @@
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
</div> </div>
<div class="row g-3"> <!-- TICKER LOOP - TIMING -->
<h6 class="fw-bold mb-3 text-primary"><i class="bi bi-clock-history"></i> Sistema di Timing (Ticker Loop)</h6>
<div class="alert alert-warning border-0 shadow-sm mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Importante:</strong> Il sistema rispetta ESATTAMENTE i valori inseriti.
Se la puntata arriva tardi, aumenta l'"Anticipo puntata". Non vengono applicate compensazioni automatiche.
</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label> <label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
<div class="input-group"> <div class="input-group">
@@ -149,7 +157,33 @@
<i class="bi bi-arrow-repeat"></i> <i class="bi bi-arrow-repeat"></i>
</button> </button>
</div> </div>
<div class="form-text">Millisecondi prima della scadenza per tentare la puntata</div>
</div> </div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hourglass-split"></i> Intervallo Ticker (ms)</label>
<input type="number" class="form-control" @bind="settings.TickerIntervalMs" min="10" max="500" />
<div class="form-text">Più basso = più preciso ma più CPU. Consigliato: 50-100ms</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-funnel"></i> Soglia controllo strategie (ms)</label>
<input type="number" class="form-control" @bind="settings.StrategyCheckThresholdMs" />
<div class="form-text">Inizia a verificare le strategie solo sotto questa soglia (ottimizza CPU)</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check form-switch mt-4">
<input type="checkbox" class="form-check-input" id="lateBidWarning" @bind="settings.ShowLateBidWarning" />
<label class="form-check-label" for="lateBidWarning">
<strong>Mostra avviso puntata tardiva</strong>
<div class="form-text">Suggerisce di aumentare l'anticipo se la puntata arriva tardi</div>
</label>
</div>
</div>
</div>
<hr class="my-4" />
<h6 class="fw-bold mb-3"><i class="bi bi-sliders"></i> Limiti Default</h6>
<div class="row g-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label> <label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
<div class="input-group"> <div class="input-group">

View File

@@ -551,4 +551,48 @@ app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout)
app.MapBlazorHub(); app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); app.MapFallbackToPage("/_Host");
// ?????????????????????????????????????????????????????????????????
// TIMER PULIZIA MEMORIA PERIODICA
// ?????????????????????????????????????????????????????????????????
// Timer per pulizia periodica della memoria (ogni 5 minuti)
var memoryCleanupTimer = new System.Threading.Timer(async _ =>
{
try
{
using var scope = app.Services.CreateScope();
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
var htmlCache = scope.ServiceProvider.GetRequiredService<HtmlCacheService>();
// Pulisci cache HTML scaduta
htmlCache.CleanExpiredCache();
// Compatta dati aste completate
appState.CleanupCompletedAuctions();
// Forza garbage collection leggera
GC.Collect(1, GCCollectionMode.Optimized, false);
// Log statistiche memoria
var stats = appState.GetMemoryStats();
var memoryMB = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
Console.WriteLine($"[MEMORY] Cleanup: {stats.AuctionsCount} aste, " +
$"{stats.TotalBidHistoryEntries} bid history, " +
$"{stats.TotalRecentBidsEntries} recent bids, " +
$"{stats.GlobalLogEntries} global log, " +
$"RAM: {memoryMB:F1}MB");
}
catch (Exception ex)
{
Console.WriteLine($"[MEMORY ERROR] Cleanup failed: {ex.Message}");
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
// Assicura che il timer venga disposto quando l'app si chiude
app.Lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("[SHUTDOWN] Disposing memory cleanup timer...");
memoryCleanupTimer.Dispose();
});
app.Run(); app.Run();

View File

@@ -53,6 +53,32 @@ namespace AutoBidder.Services
} }
} }
/// <summary>
/// Ottiene riferimento diretto alla lista per lettura veloce (NO COPY).
/// ATTENZIONE: Non modificare la lista, usare solo per lettura!
/// </summary>
public List<AuctionInfo> GetAuctionsDirectRef()
{
return _auctions; // Accesso diretto senza lock per velocità
}
/// <summary>
/// Ottiene riferimento diretto al log per lettura veloce (NO COPY).
/// </summary>
public List<LogEntry> GetLogDirectRef()
{
return _globalLog;
}
/// <summary>
/// Imposta l'asta selezionata SENZA notificare eventi async.
/// Usare per risposta UI immediata.
/// </summary>
public void SetSelectedAuctionDirect(AuctionInfo? auction)
{
_selectedAuction = auction;
}
/// <summary> /// <summary>
/// Ottiene la lista originale delle aste per il salvataggio. /// Ottiene la lista originale delle aste per il salvataggio.
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche! /// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
@@ -277,15 +303,16 @@ namespace AutoBidder.Services
{ {
_globalLog.Add(entry); _globalLog.Add(entry);
// Mantieni solo gli ultimi 1000 log // Mantieni solo gli ultimi 500 log (ridotto da 1000 per RAM)
if (_globalLog.Count > 1000) if (_globalLog.Count > 500)
{ {
_globalLog.RemoveRange(0, _globalLog.Count - 1000); _globalLog.RemoveRange(0, _globalLog.Count - 500);
_globalLog.TrimExcess();
} }
} }
_ = NotifyLogAddedAsync(message); // RIMOSSO: NotifyStateChangedAsync qui causava troppi re-render
_ = NotifyStateChangedAsync(); // I log vengono visualizzati al prossimo refresh naturale
} }
public void ClearLog() public void ClearLog()
@@ -391,6 +418,80 @@ namespace AutoBidder.Services
{ {
_ = NotifyStateChangedAsync(); _ = NotifyStateChangedAsync();
} }
// ???????????????????????????????????????????????????????????????????
// GESTIONE MEMORIA
// ???????????????????????????????????????????????????????????????????
/// <summary>
/// Compatta i dati di tutte le aste per ridurre il consumo RAM
/// </summary>
public void CompactAllAuctions()
{
lock (_lock)
{
foreach (var auction in _auctions)
{
try
{
auction.CompactData();
}
catch { /* Ignora errori */ }
}
}
Console.WriteLine($"[AppState] Compattati dati di {_auctions.Count} aste");
}
/// <summary>
/// Pulisce i dati delle aste terminate dalla memoria
/// </summary>
public void CleanupCompletedAuctions()
{
lock (_lock)
{
foreach (var auction in _auctions.Where(a => !a.IsActive))
{
try
{
// Per le aste terminate, mantieni solo dati essenziali
auction.CompactData(maxBidHistory: 20, maxRecentBids: 10, maxLogLines: 50);
}
catch { }
}
}
}
/// <summary>
/// Ritorna statistiche sull'uso della memoria
/// </summary>
public MemoryStats GetMemoryStats()
{
lock (_lock)
{
return new MemoryStats
{
AuctionsCount = _auctions.Count,
ActiveAuctionsCount = _auctions.Count(a => a.IsActive),
TotalBidHistoryEntries = _auctions.Sum(a => a.BidHistory?.Count ?? 0),
TotalRecentBidsEntries = _auctions.Sum(a => a.RecentBids?.Count ?? 0),
TotalLogEntries = _auctions.Sum(a => a.AuctionLog?.Count ?? 0),
GlobalLogEntries = _globalLog.Count
};
}
}
}
/// <summary>
/// Statistiche memoria per debug
/// </summary>
public class MemoryStats
{
public int AuctionsCount { get; set; }
public int ActiveAuctionsCount { get; set; }
public int TotalBidHistoryEntries { get; set; }
public int TotalRecentBidsEntries { get; set; }
public int TotalLogEntries { get; set; }
public int GlobalLogEntries { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -137,6 +137,13 @@ namespace AutoBidder.Services
OnLog?.Invoke($"[REMOVE] Rimozione asta non terminata: {auction.Name} (non salvata nelle statistiche)"); OnLog?.Invoke($"[REMOVE] Rimozione asta non terminata: {auction.Name} (non salvata nelle statistiche)");
} }
// ?? IMPORTANTE: Pulisci tutti i dati in memoria per liberare RAM
try
{
auction.ClearData();
}
catch { /* Ignora errori durante pulizia */ }
_auctions.Remove(auction); _auctions.Remove(auction);
} }
} }
@@ -244,10 +251,18 @@ namespace AutoBidder.Services
private async Task MonitoringLoop(CancellationToken token) private async Task MonitoringLoop(CancellationToken token)
{ {
var settings = SettingsManager.Load();
int tickerIntervalMs = Math.Max(100, settings.TickerIntervalMs); // Minimo 100ms
int pollingIntervalMs = 500; // Poll API ogni 500ms max
DateTime lastPoll = DateTime.MinValue;
OnLog?.Invoke($"[TICKER] Avviato con intervallo={tickerIntervalMs}ms, polling={pollingIntervalMs}ms");
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
try try
{ {
// === FASE 1: Raccogli aste attive ===
List<AuctionInfo> activeAuctions; List<AuctionInfo> activeAuctions;
lock (_auctions) lock (_auctions)
{ {
@@ -259,70 +274,48 @@ namespace AutoBidder.Services
if (activeAuctions.Count == 0) if (activeAuctions.Count == 0)
{ {
await Task.Delay(1000, token); // Nessuna asta attiva - polling molto lento
await Task.Delay(2000, token);
continue; continue;
} }
// Poll tutte le aste in parallelo // === FASE 2: Poll API solo ogni pollingIntervalMs ===
var pollTasks = activeAuctions.Select(a => PollAndProcessAuction(a, token)); var now = DateTime.UtcNow;
bool shouldPoll = (now - lastPoll).TotalMilliseconds >= pollingIntervalMs;
// Poll più frequente se vicino alla scadenza
bool anyNearDeadline = activeAuctions.Any(a =>
GetEstimatedTimerMs(a) < settings.StrategyCheckThresholdMs);
if (shouldPoll || anyNearDeadline)
{
var pollTasks = activeAuctions.Select(a => PollAuctionState(a, token));
await Task.WhenAll(pollTasks); await Task.WhenAll(pollTasks);
lastPoll = now;
// Ottimizzazione polling per aste in pausa
bool anyPaused = false;
DateTime now = DateTime.Now;
int pauseDelayMs = 1000;
foreach (var a in activeAuctions)
{
if (a.IsPaused)
{
anyPaused = true;
if (now.Hour < 9 || (now.Hour == 9 && now.Minute < 55))
pauseDelayMs = 60000;
else if (now.Hour == 9 && now.Minute >= 55)
pauseDelayMs = 5000;
}
}
if (anyPaused)
{
await Task.Delay(pauseDelayMs, token);
continue;
} }
// ?? Delay adattivo ULTRA-OTTIMIZZATO // === FASE 3: TICKER CHECK - Verifica timing per ogni asta ===
// Considera l'offset target più basso tra tutte le aste attive foreach (var auction in activeAuctions)
var settings = Utilities.SettingsManager.Load();
// ?? Polling VELOCE quando vicino alla scadenza
int minDelayMs = 500;
foreach (var a in activeAuctions)
{ {
if (a.IsPaused || !a.LastDeadlineUpdateUtc.HasValue) continue; if (auction.IsPaused || auction.LastState == null) continue;
// Calcola tempo stimato rimanente // Calcola timer stimato LOCALMENTE (più preciso del polling)
var elapsed = (DateTime.UtcNow - a.LastDeadlineUpdateUtc.Value).TotalMilliseconds; double estimatedTimerMs = GetEstimatedTimerMs(auction);
double estimatedRemaining = a.LastRawTimer - elapsed;
int offsetMs = a.BidBeforeDeadlineMs > 0 // Offset configurato dall'utente (SENZA compensazioni)
? a.BidBeforeDeadlineMs int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs; : settings.DefaultBidBeforeDeadlineMs;
double msToTarget = estimatedRemaining - offsetMs; // TRIGGER: Timer <= Offset configurato dall'utente
if (estimatedTimerMs <= offsetMs && estimatedTimerMs > 0)
// Polling più veloce quando vicino al target {
int delay; await TryPlaceBidTicker(auction, estimatedTimerMs, offsetMs, token);
if (msToTarget < 100) delay = 5; }
else if (msToTarget < 300) delay = 10;
else if (msToTarget < 500) delay = 20;
else if (msToTarget < 1000) delay = 50;
else if (msToTarget < 2000) delay = 100;
else if (msToTarget < 5000) delay = 200;
else delay = 400;
minDelayMs = Math.Min(minDelayMs, delay);
} }
await Task.Delay(minDelayMs, token); // === FASE 4: Delay fisso del ticker ===
await Task.Delay(tickerIntervalMs, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -330,10 +323,202 @@ namespace AutoBidder.Services
} }
catch (Exception ex) catch (Exception ex)
{ {
OnLog?.Invoke($"[ERRORE] Loop monitoraggio: {ex.Message}"); OnLog?.Invoke($"[ERRORE] Ticker loop: {ex.Message}");
await Task.Delay(1000, token); await Task.Delay(500, token);
} }
} }
OnLog?.Invoke("[TICKER] Fermato");
}
/// <summary>
/// Calcola il timer stimato localmente (interpolazione tra poll)
/// </summary>
private double GetEstimatedTimerMs(AuctionInfo auction)
{
if (!auction.LastDeadlineUpdateUtc.HasValue || auction.LastRawTimer <= 0)
{
return auction.LastState?.Timer * 1000 ?? 0;
}
// Tempo trascorso dall'ultimo poll
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
// Timer stimato = timer raw - tempo trascorso
double estimated = auction.LastRawTimer - elapsed;
return Math.Max(0, estimated);
}
/// <summary>
/// Poll dello stato dell'asta (SOLO aggiornamento dati, nessuna logica di puntata)
/// </summary>
private async Task PollAuctionState(AuctionInfo auction, CancellationToken token)
{
try
{
var state = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (state == null) return;
// Log primo poll per debug
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
{
auction.AddLog($"[START] Monitoraggio avviato - Timer: {state.Timer:F1}s, Prezzo: €{state.Price:F2}");
}
// Aggiorna latenza
auction.AddLatencyMeasurement(state.PollingLatencyMs);
// Tracking inizio
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
{
auction.IsTrackedFromStart = true;
auction.TrackingStartedAt = DateTime.UtcNow;
}
// Aggiorna storia puntate
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
{
MergeBidHistory(auction, state.RecentBidsHistory);
}
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
EnsureCurrentBidInHistory(auction, state);
}
// Gestione fine asta
if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed)
{
HandleAuctionEnded(auction, state);
return;
}
// Notifica SOLO se ci sono cambiamenti significativi
bool shouldNotify = auction.LastState == null ||
Math.Abs(auction.LastState.Price - state.Price) > 0.001 ||
auction.LastState.LastBidder != state.LastBidder ||
Math.Abs(auction.LastState.Timer - state.Timer) > 1;
auction.LastState = state;
if (shouldNotify)
{
OnAuctionUpdated?.Invoke(state);
}
UpdateAuctionHistory(auction, state);
// Calcola valore prodotto
UpdateProductValue(auction, state);
}
catch (Exception ex)
{
auction.AddLog($"[POLL ERROR] {ex.Message}");
}
}
/// <summary>
/// Gestisce la fine dell'asta
/// </summary>
private void HandleAuctionEnded(AuctionInfo auction, AuctionState state)
{
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
bool won = state.Status == AuctionStatus.EndedWon;
// Fix: Aggiungi ultima puntata se mancante
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
var statePrice = (decimal)state.Price;
var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
if (!alreadyExists)
{
auction.RecentBids.Insert(0, new BidHistoryEntry
{
Username = state.LastBidder,
Price = statePrice,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
BidType = "Auto"
});
}
}
auction.IsActive = false;
auction.LastState = state;
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg}");
// Feedback per aste perse
var settings = SettingsManager.Load();
if (!won && settings.ShowLateBidWarning)
{
// Se abbiamo provato a puntare ma fallito con errore timer
var lastBidAttempt = auction.BidHistory
.Where(b => b.EventType == BidEventType.MyBid && !b.Success)
.OrderByDescending(b => b.Timestamp)
.FirstOrDefault();
if (lastBidAttempt != null &&
(lastBidAttempt.Notes?.Contains("timer") == true ||
lastBidAttempt.Notes?.Contains("scaduto") == true))
{
int currentOffset = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
auction.AddLog($"[⚠️ SUGGERIMENTO] Puntata arrivata troppo tardi! " +
$"Tempo attuale: {currentOffset}ms. " +
$"Prova ad aumentarlo a {currentOffset + 500}ms o più.");
OnLog?.Invoke($"[LATE] {auction.Name}: aumenta il tempo di puntata (attuale: {currentOffset}ms)");
}
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset,
Bidder = state.LastBidder,
Price = state.Price,
Timer = 0,
Notes = $"Asta {statusMsg}"
});
OnAuctionUpdated?.Invoke(state);
try
{
OnAuctionCompleted?.Invoke(auction, state, won);
}
catch (Exception ex)
{
OnLog?.Invoke($"[STATS ERROR] {ex.Message}");
}
}
/// <summary>
/// Aggiorna il valore calcolato del prodotto
/// </summary>
private void UpdateProductValue(AuctionInfo auction, AuctionState state)
{
if (!auction.BuyNowPrice.HasValue && !auction.ShippingCost.HasValue) return;
try
{
var productValue = Utilities.ProductValueCalculator.Calculate(
auction,
state.Price,
auction.RecentBids?.Count ?? 0
);
auction.CalculatedValue = productValue;
}
catch { /* Silenzioso */ }
} }
private bool IsAuctionTerminated(AuctionInfo auction) private bool IsAuctionTerminated(AuctionInfo auction)
@@ -353,189 +538,91 @@ namespace AutoBidder.Services
return false; return false;
} }
/// <summary>
/// TICKER: Tenta la puntata quando il timer locale raggiunge l'offset configurato.
/// NESSUNA compensazione automatica della latenza - l'utente decide il timing esatto.
/// </summary>
private async Task TryPlaceBidTicker(AuctionInfo auction, double estimatedTimerMs, int offsetMs, CancellationToken token)
{
var settings = SettingsManager.Load();
var state = auction.LastState;
if (state == null || state.IsMyBid || estimatedTimerMs <= 0) return;
// Log timing se abilitato
if (settings.LogTiming)
{
auction.AddLog($"[TICKER] Timer stimato={estimatedTimerMs:F0}ms <= Offset={offsetMs}ms");
}
// === PROTEZIONE DOPPIA PUNTATA ===
// Reset se timer è aumentato (qualcuno ha puntato = nuovo ciclo)
if (estimatedTimerMs > auction.LastScheduledTimerMs + 500)
{
auction.BidScheduled = false;
}
// Reset se passato troppo tempo dall'ultima puntata
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{
auction.BidScheduled = false;
}
// Skip se già schedulata per questo ciclo
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - estimatedTimerMs) < 200)
{
return;
}
// Cooldown 1 secondo tra puntate
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
{
return;
}
// === OTTIMIZZAZIONE: Controlla strategie SOLO se vicino al target ===
// Evita calcoli inutili quando siamo lontani
if (estimatedTimerMs > settings.StrategyCheckThresholdMs)
{
return;
}
// === CONTROLLI FONDAMENTALI ===
if (!ShouldBid(auction, state))
{
return;
}
// === STRATEGIE AVANZATE ===
var session = _apiClient.GetSession();
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? "");
if (!decision.ShouldBid)
{
auction.AddLog($"[STRATEGY] {decision.Reason}");
return;
}
// === ESEGUI PUNTATA ===
auction.BidScheduled = true;
auction.LastScheduledTimerMs = estimatedTimerMs;
auction.AddLog($"[BID] Timer locale={estimatedTimerMs:F0}ms, Offset utente={offsetMs}ms");
await ExecuteBid(auction, state, token);
}
// ═══════════════════════════════════════════════════════════════════
// METODO LEGACY - Mantenuto per compatibilità ma non più usato
// ═══════════════════════════════════════════════════════════════════
[Obsolete("Usare TryPlaceBidTicker con il nuovo sistema Ticker Loop")]
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token) private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
{ {
try // Ora gestito da PollAuctionState + TryPlaceBidTicker
{ await PollAuctionState(auction, token);
var state = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (state == null)
{
auction.AddLog("ERRORE: Nessun dato ricevuto da API");
OnLog?.Invoke($"[ERRORE] [{auction.AuctionId}] API non ha risposto");
return;
}
// ?? Aggiorna latenza con storico
auction.AddLatencyMeasurement(state.PollingLatencyMs);
// ?? Segna tracking dall'inizio se è la prima volta
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
{
auction.IsTrackedFromStart = true;
auction.TrackingStartedAt = DateTime.UtcNow;
}
// Aggiorna storia puntate mantenendo quelle vecchie
if (state.RecentBidsHistory != null && state.RecentBidsHistory.Count > 0)
{
MergeBidHistory(auction, state.RecentBidsHistory);
}
// ?? FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
// Questo assicura che la puntata vincente sia sempre inclusa
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
EnsureCurrentBidInHistory(auction, state);
}
if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed)
{
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
bool won = state.Status == AuctionStatus.EndedWon;
// ?? FIX: Aggiungi ultima puntata mancante a RecentBids
// L'API spesso non include l'ultima puntata nella storia
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var statePrice = (decimal)state.Price;
// Verifica se questa puntata non è già presente
var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
if (!alreadyExists)
{
auction.RecentBids.Insert(0, new BidHistoryEntry
{
Username = state.LastBidder,
Price = statePrice,
Timestamp = lastBidTimestamp,
BidType = "Auto"
});
}
}
auction.IsActive = false;
auction.LastState = state; // Salva stato finale per statistiche
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
// 💡 SUGGERIMENTO: Se persa e non abbiamo mai provato a puntare, potrebbe essere un problema di timing
if (!won && auction.SessionBidCount == 0)
{
var settings = Utilities.SettingsManager.Load();
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// Se l'offset è <= 1000ms, il polling (~1s) potrebbe non catturare il momento giusto
if (offsetMs <= 1000)
{
auction.AddLog($"[💡 SUGGERIMENTO] Asta persa senza mai puntare. Con offset={offsetMs}ms e polling~1s, " +
$"potresti non vedere mai il timer scendere sotto {offsetMs}ms. Considera di aumentare l'offset a 1500-2000ms nelle impostazioni.");
}
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset,
Bidder = state.LastBidder,
Price = state.Price,
Timer = 0,
Notes = $"Asta {statusMsg}"
});
OnAuctionUpdated?.Invoke(state);
// ?? Emetti evento per salvare statistiche
try
{
OnAuctionCompleted?.Invoke(auction, state, won);
OnLog?.Invoke($"[STATS] Evento OnAuctionCompleted emesso per {auction.Name}");
}
catch (Exception ex)
{
OnLog?.Invoke($"[STATS ERROR] Errore in OnAuctionCompleted: {ex.Message}");
}
return;
}
if (state.Status == AuctionStatus.Running)
{
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
// Solo eventi importanti (bid, reset, errori) vengono loggati
}
else if (state.Status == AuctionStatus.Paused)
{
// Log solo primo cambio stato, non ad ogni polling
var lastLog = auction.AuctionLog.LastOrDefault();
if (lastLog == null || !lastLog.Contains("[PAUSA]"))
{
auction.AddLog($"[PAUSA] Asta in pausa - Timer: {state.Timer:F3}s, EUR{state.Price:F2}");
}
}
OnAuctionUpdated?.Invoke(state);
UpdateAuctionHistory(auction, state);
// ?? NUOVO: Calcola e aggiorna valore prodotto se disponibili dati
if (auction.BuyNowPrice.HasValue || auction.ShippingCost.HasValue)
{
try
{
var settings = Utilities.SettingsManager.Load();
var productValue = Utilities.ProductValueCalculator.Calculate(
auction,
state.Price,
auction.RecentBids?.Count ?? 0
);
auction.CalculatedValue = productValue;
// Log valore solo se abilitato nelle impostazioni
bool shouldLogValue = false;
if (settings.LogValueCalculations && auction.PollingLatencyMs % 10 == 0) // Ogni ~10 poll
{
shouldLogValue = true;
}
if (shouldLogValue && productValue.Savings.HasValue)
{
var valueMsg = Utilities.ProductValueCalculator.FormatValueMessage(productValue);
auction.AddLog($"[VALUE] {valueMsg}");
}
}
catch (Exception ex)
{
// Silenzioso - non vogliamo bloccare il polling per errori di calcolo
auction.AddLog($"[WARN] Errore calcolo valore: {ex.Message}");
}
}
// ?? NUOVA LOGICA: Controlla PRIMA il timing, POI le strategie
if (state.Status == AuctionStatus.Running && !auction.IsPaused)
{
await TryPlaceBid(auction, state, token);
}
}
catch (Exception ex)
{
auction.AddLog($"[EXCEPTION] {ex.Message}");
OnLog?.Invoke($"[EXCEPTION] [{auction.AuctionId}] {ex.Message}");
}
} }
/// <summary> /// <summary>
@@ -543,99 +630,16 @@ namespace AutoBidder.Services
/// Punta esattamente a `offset` ms dalla scadenza. /// Punta esattamente a `offset` ms dalla scadenza.
/// Le strategie decidono SE puntare, non QUANDO. /// Le strategie decidono SE puntare, non QUANDO.
/// </summary> /// </summary>
[Obsolete("Usare TryPlaceBidTicker")]
private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token) private async Task TryPlaceBid(AuctionInfo auction, AuctionState state, CancellationToken token)
{ {
// Reindirizza al nuovo sistema
var settings = SettingsManager.Load(); var settings = SettingsManager.Load();
// Offset: millisecondi prima della scadenza (configurato dall'utente)
int offsetMs = auction.BidBeforeDeadlineMs > 0 int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs ? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs; : settings.DefaultBidBeforeDeadlineMs;
// Timer dall'API (in secondi, convertito in ms)
double timerMs = state.Timer * 1000; double timerMs = state.Timer * 1000;
await TryPlaceBidTicker(auction, timerMs, offsetMs, token);
// Skip se già vincitore o timer scaduto
if (state.IsMyBid || timerMs <= 0) return;
// Log timing solo se abilitato
if (settings.LogTiming)
{
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Offset={offsetMs}ms");
}
// Punta quando il timer API è <= offset configurato dall'utente
// NESSUNA modifica automatica - l'utente decide il timing
if (timerMs > offsetMs)
{
return;
}
// Timer <= offset = È IL MOMENTO DI PUNTARE!
auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
// Resetta BidScheduled se il timer è AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
if (timerMs > auction.LastScheduledTimerMs + 500)
{
auction.BidScheduled = false;
}
// Resetta anche se è passato troppo tempo dall'ultima puntata (nuovo ciclo)
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{
auction.BidScheduled = false;
}
// Protezione doppia puntata SOLO per lo stesso ciclo di timer
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - timerMs) < 100)
{
auction.AddLog($"[SKIP] Puntata già schedulata per timer~={timerMs:F0}ms in questo ciclo");
return;
}
// Cooldown 1 secondo tra puntate
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
{
auction.AddLog($"[COOLDOWN] Attesa cooldown puntata precedente");
return;
}
// 🔴 CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
if (!ShouldBid(auction, state))
{
// I motivi vengono ora loggati sempre dentro ShouldBid
return;
}
// ✅ MOMENTO GIUSTO! Verifica strategie avanzate
var session = _apiClient.GetSession();
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, session?.Username ?? "");
if (!decision.ShouldBid)
{
// 🔥 FIX: Logga SEMPRE il motivo del blocco strategia, non solo se LogStrategyDecisions è attivo
// Questo aiuta a capire perché si perdono le aste
auction.AddLog($"[STRATEGY] {decision.Reason}");
// Log aggiuntivo solo se debug strategie attivo
if (settings.LogStrategyDecisions)
{
OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
}
return;
}
// ?? PUNTA!
auction.BidScheduled = true;
auction.LastScheduledTimerMs = timerMs;
if (settings.LogBids)
{
auction.AddLog($"[BID] Puntata con timer API={timerMs:F0}ms");
}
await ExecuteBid(auction, state, token);
} }
/// <summary> /// <summary>
@@ -690,8 +694,31 @@ namespace AutoBidder.Services
else else
{ {
var pollingPing = auction.PollingLatencyMs; var pollingPing = auction.PollingLatencyMs;
var settings = SettingsManager.Load();
// Rileva errore "timer scaduto" per feedback utente
bool isLateBid = result.Error?.Contains("timer") == true ||
result.Error?.Contains("scaduto") == true ||
result.Error?.Contains("closed") == true ||
result.Error?.Contains("ended") == true;
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms"); auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}"); OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
// Feedback per puntata tardiva
if (isLateBid && settings.ShowLateBidWarning)
{
int currentOffset = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
int suggestedOffset = currentOffset + 300 + pollingPing;
auction.AddLog($"[⚠️ TIMING] Puntata arrivata troppo tardi! " +
$"Offset attuale: {currentOffset}ms. Latenza totale: ~{pollingPing + result.LatencyMs}ms. " +
$"Suggerimento: aumenta a {suggestedOffset}ms");
OnLog?.Invoke($"[LATE] {auction.Name}: puntata tardiva, aumenta offset a {suggestedOffset}ms");
}
} }
auction.BidHistory.Add(new BidHistory auction.BidHistory.Add(new BidHistory
@@ -926,6 +953,17 @@ namespace AutoBidder.Services
if (isNewBid) if (isNewBid)
{ {
auction.ResetCount++; auction.ResetCount++;
// Log quando c'è una nuova puntata (se logging attivo)
var settings = SettingsManager.Load();
if (settings.LogAuctionStatus)
{
var session = _apiClient.GetSession();
var isMyBid = state.LastBidder?.Equals(session?.Username, StringComparison.OrdinalIgnoreCase) == true;
auction.AddLog($"[RESET #{auction.ResetCount}] {state.LastBidder} → €{state.Price:F2} | Timer: {state.Timer:F1}s{(isMyBid ? " (TU)" : "")}");
}
auction.BidHistory.Add(new BidHistory auction.BidHistory.Add(new BidHistory
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
@@ -936,6 +974,12 @@ namespace AutoBidder.Services
Notes = $"Puntata: EUR{state.Price:F2}" Notes = $"Puntata: EUR{state.Price:F2}"
}); });
// ?? Limita dimensione BidHistory per evitare memory leak
if (auction.BidHistory.Count > 100)
{
auction.BidHistory.RemoveRange(0, auction.BidHistory.Count - 100);
}
// ? RIMOSSO: Non incrementare qui - è già gestito da UpdateBidderStatsFromRecentBids // ? RIMOSSO: Non incrementare qui - è già gestito da UpdateBidderStatsFromRecentBids
// L'incremento doppio causava conteggi gonfiati // L'incremento doppio causava conteggi gonfiati
@@ -1011,10 +1055,12 @@ namespace AutoBidder.Services
.ToList(); .ToList();
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista) // Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries) // ?? MEMORY: Limite ridotto per ottimizzazione RAM
int limit = maxEntries > 0 ? Math.Min(maxEntries, 50) : 50;
if (auction.RecentBids.Count > limit)
{ {
auction.RecentBids = auction.RecentBids auction.RecentBids = auction.RecentBids
.Take(maxEntries) .Take(limit)
.ToList(); .ToList();
} }

View File

@@ -28,6 +28,7 @@ namespace AutoBidder.Services
private readonly int _maxConcurrentRequests; private readonly int _maxConcurrentRequests;
private readonly TimeSpan _cacheExpiration; private readonly TimeSpan _cacheExpiration;
private readonly int _maxRetries; private readonly int _maxRetries;
private readonly int _maxCacheEntries;
// Logging callback // Logging callback
public Action<string>? OnLog { get; set; } public Action<string>? OnLog { get; set; }
@@ -36,12 +37,14 @@ namespace AutoBidder.Services
int maxConcurrentRequests = 3, int maxConcurrentRequests = 3,
int requestsPerSecond = 5, int requestsPerSecond = 5,
TimeSpan? cacheExpiration = null, TimeSpan? cacheExpiration = null,
int maxRetries = 2) int maxRetries = 2,
int maxCacheEntries = 50)
{ {
_maxConcurrentRequests = maxConcurrentRequests; _maxConcurrentRequests = maxConcurrentRequests;
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond); _minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5); _cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(3); // Ridotto da 5 a 3 minuti
_maxRetries = maxRetries; _maxRetries = maxRetries;
_maxCacheEntries = maxCacheEntries;
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests); _rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
_httpClient.Timeout = TimeSpan.FromSeconds(15); _httpClient.Timeout = TimeSpan.FromSeconds(15);
@@ -191,10 +194,26 @@ namespace AutoBidder.Services
} }
/// <summary> /// <summary>
/// Salva HTML in cache /// Salva HTML in cache con limite dimensione
/// </summary> /// </summary>
private void SaveToCache(string url, string html) private void SaveToCache(string url, string html)
{ {
// Limita dimensione cache per evitare memory leak
if (_cache.Count >= _maxCacheEntries)
{
// Rimuovi le entry più vecchie
var oldestEntries = _cache
.OrderBy(kvp => kvp.Value.Timestamp)
.Take(_cache.Count - _maxCacheEntries + 10) // Rimuovi 10 extra per evitare chiamate frequenti
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in oldestEntries)
{
_cache.TryRemove(key, out _);
}
}
_cache[url] = new CachedHtml _cache[url] = new CachedHtml
{ {
Html = html, Html = html,

View File

@@ -15,6 +15,33 @@ namespace AutoBidder.Utilities
public int DefaultMinResets { get; set; } = 0; public int DefaultMinResets { get; set; } = 0;
public int DefaultMaxResets { get; set; } = 0; public int DefaultMaxResets { get; set; } = 0;
// ═══════════════════════════════════════════════════════════════════
// TICKER LOOP - SISTEMA DI TIMING SEMPLIFICATO
// ═══════════════════════════════════════════════════════════════════
/// <summary>
/// Intervallo del ticker in millisecondi.
/// Più basso = più preciso ma più CPU.
/// Valori consigliati: 50-100ms
/// Default: 50ms
/// </summary>
public int TickerIntervalMs { get; set; } = 50;
/// <summary>
/// Soglia in millisecondi per iniziare i controlli delle strategie.
/// Se il timer è superiore a questo valore, non vengono eseguiti i controlli.
/// Questo ottimizza le risorse evitando controlli inutili quando siamo lontani dal momento di puntare.
/// Default: 5000ms (5 secondi)
/// </summary>
public int StrategyCheckThresholdMs { get; set; } = 5000;
/// <summary>
/// Mostra avviso quando una puntata arriva troppo tardi (timer scaduto).
/// Suggerisce all'utente di aumentare il tempo di puntata.
/// Default: true
/// </summary>
public bool ShowLateBidWarning { get; set; } = true;
// LIMITI LOG // LIMITI LOG
/// <summary> /// <summary>
/// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500) /// Numero massimo di righe di log da mantenere per ogni singola asta (default: 500)

View File

@@ -208,6 +208,68 @@
background: rgba(239, 68, 68, 0.15); background: rgba(239, 68, 68, 0.15);
} }
/* ═══════════════════════════════════════════════════════════════════
LOG BOX - SCROLL FISSO
═══════════════════════════════════════════════════════════════════ */
.log-box {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.75rem;
line-height: 1.4;
padding: 0.5rem;
height: 100%; /* Usa altezza dal pannello */
overflow-y: auto; /* Scroll verticale */
overflow-x: hidden;
}
.log-box-compact {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.75rem;
line-height: 1.4;
padding: 0.5rem;
max-height: 100%; /* Non superare il contenitore */
overflow-y: auto; /* Scroll verticale */
overflow-x: hidden;
}
.log-entry {
padding: 0.25rem 0.5rem;
margin-bottom: 0.15rem;
border-radius: 4px;
word-wrap: break-word;
transition: background 0.1s ease;
}
.log-entry:hover {
background: rgba(255, 255, 255, 0.05);
}
.log-entry-error {
color: #f87171;
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid #ef4444;
padding-left: 0.5rem;
}
.log-entry-warning {
color: #fbbf24;
background: rgba(245, 158, 11, 0.1);
border-left: 3px solid #f59e0b;
padding-left: 0.5rem;
}
.log-entry-success {
color: #4ade80;
background: rgba(34, 197, 94, 0.1);
border-left: 3px solid #22c55e;
padding-left: 0.5rem;
}
.log-entry-debug {
color: #60a5fa;
opacity: 0.7;
}
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
LAYOUT CON SPLITTER TRASCINABILI LAYOUT CON SPLITTER TRASCINABILI
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
@@ -220,34 +282,38 @@
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
gap: 0; /* IMPORTANTE: nessun gap tra toolbar e content */
} }
/* Area contenuto principale */ /* Area contenuto principale */
.main-content-area { .main-content-area {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1 1 auto; /* Cresce e si riduce */
min-height: 0; min-height: 0; /* IMPORTANTE per flex */
overflow: hidden; overflow: hidden;
gap: 0; gap: 0; /* IMPORTANTE: nessun gap, gli splitter gestiscono lo spazio */
} }
/* Riga superiore (Aste + Log) */ /* Riga superiore (Aste + Log) */
.top-row { .top-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 1; flex: 1 1 auto; /* Cresce e si riduce - NON percentuale fissa */
min-height: 150px; min-height: 200px; /* Altezza minima */
overflow: hidden; overflow: hidden;
gap: 0; gap: 0; /* IMPORTANTE: nessun gap, gutter gestisce lo spazio */
} }
/* Riga inferiore (Dettagli) */ /* Riga inferiore (Dettagli) */
.bottom-row { .bottom-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 80px; flex: 0 0 auto; /* NON cresce automaticamente */
height: 300px; /* Altezza iniziale fissa */
min-height: 150px; /* Altezza minima */
overflow: hidden; overflow: hidden;
gap: 0;
} }
/* Pannello generico */ /* Pannello generico */
@@ -259,33 +325,55 @@
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
height: 100%; /* IMPORTANTE: altezza fissa dal contenitore */
position: relative; /* Per z-index */
}
/* IMPORTANTE: Previeni collasso dei bordi */
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
} }
/* Pannello Aste */ /* Pannello Aste */
.panel-auctions { .panel-auctions {
flex: 1; flex: 1 1 auto; /* Cresce per riempire lo spazio */
min-width: 200px; min-width: 300px;
height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden;
} }
/* Pannello Log */ /* Pannello Log */
.panel-log { .panel-log {
flex: 0 0 280px; flex: 0 0 auto; /* NON cresce/riduce automaticamente */
min-width: 150px; width: 320px; /* Larghezza fissa iniziale */
max-width: 450px; min-width: 200px;
max-width: 500px;
height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden;
} }
/* Pannello Dettagli */ /* Pannello Dettagli */
.panel-details { .panel-details {
flex: 1; flex: 1 1 auto; /* Cresce per riempire */
min-height: 80px; min-height: 150px;
overflow: auto; height: 100%; /* Usa tutta l'altezza del contenitore */
overflow: hidden; /* Il contenitore non scrolla */
} }
/* Gutter/Splitter */ /* Gutter/Splitter */
.gutter { .gutter {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
flex-shrink: 0; flex-shrink: 0; /* NON si riduce mai */
flex-grow: 0; /* NON cresce mai */
transition: background 0.15s ease; transition: background 0.15s ease;
z-index: 10; /* Sopra i pannelli */
} }
.gutter:hover { .gutter:hover {
@@ -299,11 +387,15 @@
.gutter-vertical { .gutter-vertical {
width: 6px; width: 6px;
cursor: col-resize; cursor: col-resize;
min-width: 6px; /* Larghezza fissa */
max-width: 6px;
} }
.gutter-horizontal { .gutter-horizontal {
height: 6px; height: 6px;
cursor: row-resize; cursor: row-resize;
min-height: 6px; /* Altezza fissa */
max-height: 6px;
} }
/* Header pannello */ /* Header pannello */
@@ -327,15 +419,47 @@
.panel-content { .panel-content {
flex: 1; flex: 1;
overflow: auto; overflow-y: auto; /* Scroll verticale */
min-height: 0; overflow-x: hidden;
min-height: 0; /* Importante per flex */
max-height: 100%; /* Non superare il pannello */
} }
/* Contenuto dettagli */ /* Contenuto dettagli */
.auction-details-content { .auction-details-content {
padding: 0.5rem; padding: 0.5rem;
height: 100%; height: 100%;
overflow: auto; display: flex;
flex-direction: column;
overflow: hidden; /* Il contenitore non scrolla */
}
/* Tab content deve scrollare */
.tab-content {
flex: 1;
overflow: hidden;
min-height: 0;
display: flex; /* IMPORTANTE per i tab-pane */
flex-direction: column;
}
.tab-pane {
height: 100%;
overflow: hidden;
display: none; /* Bootstrap lo gestisce con show active */
}
.tab-pane.show.active {
display: flex; /* Quando attivo diventa flex */
flex-direction: column;
}
.tab-panel-content {
flex: 1; /* Riempie il tab-pane */
overflow-y: auto; /* Scroll per il contenuto dei tab */
overflow-x: hidden;
padding: 0.5rem;
min-height: 0; /* IMPORTANTE per flex */
} }
.details-header { .details-header {