Togli dal percorso caldo la rilettura dell'intero storico

Sei modifiche chieste in questa sessione, di cui una pesa piu' di tutte le altre.

CompletedAuctionsStore rileggeva e rideserializzava l'intero file a ogni chiamata:
misurati 72 ms su 5876 record, 3,9 MB. Append pagava quel prezzo piu' la
riserializzazione e la riscrittura completa per cambiare un record solo, 120 ms
in tutto, e la chiamata arriva da un gestore di eventi di MainWindow, quindi dal
thread dell'interfaccia, che restava fermo a ogni asta conclusa. Il recupero
delle puntate dei vincitori lo faceva in un ciclo: 4675 aste erano 7,2 minuti di
solo rimescolamento JSON e 17,8 GB scritti sul disco.

Adesso c'e' una copia in memoria invalidata sulla data di modifica del file, cosi'
resta giusta anche se il file viene sostituito da fuori (ripristino di un backup),
e un AppendMany che applica molti record con un salvataggio solo, con un indice
per id perche' il caso per cui esiste non torni quadratico. Il recupero salva a
blocchi di cinquanta e deposita comunque quello che ha in mano quando esce, anche
per annullamento: le richieste gia' fatte sono state pagate al server.

Misurato sullo stesso file: 95 volte piu' veloce, 7,2 minuti diventano 4,5
secondi, 17,8 GB diventano 0,36 GB.

Le altre cinque:

- Il setup si chiama AutoBidder_<versione>.exe.
- I pulsanti di stop sono rossi, sia quello globale sia quelli per asta nella
  griglia: erano gli unici tre senza colore mentre avvia e osserva ce l'avevano.
- La pulizia dello storico sa togliere le aste seguite solo in parte. Non serve
  nessun campo nuovo: il prezzo finale in centesimi e' il totale delle puntate
  dell'asta, quindi il rapporto con i reset osservati dice quanta asta si e'
  vista, e la regola vale anche sullo storico gia' raccolto. Verificata sui dati
  veri: su 5876 aste nessuna supera copertura 1,02, che e' quanto ci si aspetta
  se l'identita' e' giusta.
- La griglia ricicla i contenitori di riga invece di ricrearli.
- Il costo puntata gia' esisteva col predefinito giusto: al suggerimento e' stato
  aggiunto perche' 0,20 e' il numero da tenere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 14:48:54 +02:00
co-authored by Claude Opus 5
parent 5adc4a6526
commit 1d578debe2
14 changed files with 448 additions and 50 deletions
+69 -37
View File
@@ -56,57 +56,89 @@ namespace AutoBidder.Services
var failed = 0;
var skipped = 0;
for (var i = 0; i < missing.Count; i++)
// ── Perché si salva a blocchi ────────────────────────────────
// Un Append per asta rilegge e riscrive l'intero storico ogni volta:
// su un recupero completo (4675 aste) erano minuti di solo rimescolamento
// JSON e svariati gigabyte scritti sul disco. Un blocco ogni cinquanta
// taglia i salvataggi di cinquanta volte senza mettere a rischio più di
// cinquanta richieste già pagate: quello che non è ancora stato salvato
// viene comunque scritto all'uscita, annullamento compreso.
const int DimensioneBlocco = 50;
var daSalvare = new List<CompletedAuctionRecord>(DimensioneBlocco);
void Deposita()
{
if (ct.IsCancellationRequested) break;
if (daSalvare.Count == 0) return;
var record = missing[i];
CompletedAuctionsStore.AppendMany(daSalvare);
daSalvare.Clear();
}
try
try
{
for (var i = 0; i < missing.Count; i++)
{
var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
if (ct.IsCancellationRequested) break;
if (state?.WinnerBidsTotal is null)
{
skipped++;
}
else
{
record.WinnerBidsPaid = state.WinnerBidsPaid;
record.WinnerBidsFree = state.WinnerBidsFree;
var record = missing[i];
var check = AuctionIntegrity.CheckWinnerBids(record);
if (check.IsTrustworthy)
try
{
var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
if (state?.WinnerBidsTotal is null)
{
// Si riscrive solo il record toccato: Append sostituisce
// quello con lo stesso id.
CompletedAuctionsStore.Append(record);
updated++;
skipped++;
}
else
{
// Il valore c'è ma non regge il confronto col prezzo: non si
// salva, e si dice perché. Meglio un buco che una media storta.
record.WinnerBidsPaid = null;
record.WinnerBidsFree = null;
skipped++;
OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
record.WinnerBidsPaid = state.WinnerBidsPaid;
record.WinnerBidsFree = state.WinnerBidsFree;
var check = AuctionIntegrity.CheckWinnerBids(record);
if (check.IsTrustworthy)
{
// Va nel blocco: AppendMany sostituisce i record con lo
// stesso id, quindi il risultato è identico a un Append
// per ciascuno, con un salvataggio invece di cinquanta.
daSalvare.Add(record);
if (daSalvare.Count >= DimensioneBlocco) Deposita();
updated++;
}
else
{
// Il valore c'è ma non regge il confronto col prezzo: non
// si salva, e si dice perché. Meglio un buco che una media
// storta.
record.WinnerBidsPaid = null;
record.WinnerBidsFree = null;
skipped++;
OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
}
}
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
failed++;
OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
failed++;
OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
}
OnProgress?.Invoke(i + 1, missing.Count, updated);
OnProgress?.Invoke(i + 1, missing.Count, updated);
if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
}
}
finally
{
// Anche se si esce per annullamento o per un errore: le richieste già
// fatte sono state pagate al server, buttarne i risultati significa
// rifarle al giro dopo.
Deposita();
}
return new Result(missing.Count, updated, failed, skipped);