using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using foodmarket.Pos.Core.Auth;
using foodmarket.Pos.Core.Data;
using foodmarket.Pos.Core.Sync;
using Refit;
namespace foodmarket.Pos.Tooling;
/// Замеры старта/синхронизации кассы против живого сервера.
/// Запуск: dotnet run -- --bench <serverUrl> <user> <password>.
///
/// Мерит: (1) первичную полную загрузку каталога (HTTP+десериализация отдельно
/// от записи в SQLite), (2) тёплый старт — открытие локальной базы + поиск,
/// (3) дельта-синк (изменений ~нет). Всё — с включённой gzip-декомпрессией.
public static class BenchRunner
{
public static int Run(string[] args)
{
var rest = args.SkipWhile(a => a != "--bench").Skip(1).ToArray();
if (rest.Length < 3) { Console.Error.WriteLine("usage: --bench "); return 2; }
return RunAsync(rest[0].TrimEnd('/'), rest[1], rest[2]).GetAwaiter().GetResult();
}
private static async Task RunAsync(string url, string user, string pass)
{
var sw = Stopwatch.StartNew();
var auth = new AuthService(new HttpClient());
var tok = await auth.LoginAsync(url, user, pass);
Console.WriteLine($"[bench] login: {sw.ElapsedMilliseconds} ms");
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli };
var http = new HttpClient(new BearerHandler(tok.AccessToken, handler)) { BaseAddress = new Uri(url), Timeout = TimeSpan.FromSeconds(120) };
var api = RestService.For(http);
var dbFile = Path.Combine(Path.GetTempPath(), $"pos-bench-{Guid.NewGuid():N}.db");
try
{
// 1. Первичная полная загрузка: сеть+десериализация, затем запись.
var store = new PosStore(dbFile);
sw.Restart();
var full = await api.SyncAsync(null);
var netMs = sw.ElapsedMilliseconds;
sw.Restart();
store.ApplySync(full);
var applyMs = sw.ElapsedMilliseconds;
Console.WriteLine($"[bench] ПЕРВИЧНАЯ загрузка: товаров={full.Products.Count} цен={full.Prices.Count} остатков={full.Stocks.Count}");
Console.WriteLine($"[bench] сеть+десериализация: {netMs} ms; запись в SQLite: {applyMs} ms; ИТОГО до продажи: {netMs + applyMs} ms");
Console.WriteLine($"[bench] в базе товаров: {store.ProductCount()}");
store.Dispose();
// 2. Тёплый старт: открыть существующую базу + поиск по штрихкоду и имени.
sw.Restart();
var warm = new PosStore(dbFile);
var openMs = sw.ElapsedMilliseconds;
sw.Restart();
var cnt = warm.ProductCount();
var firstBc = full.Products.FirstOrDefault()?.Barcodes.FirstOrDefault();
if (firstBc is not null) _ = warm.FindByBarcode(firstBc);
_ = warm.SearchByName("товар", 50);
var queryMs = sw.ElapsedMilliseconds;
Console.WriteLine($"[bench] ТЁПЛЫЙ старт: открытие базы ({cnt} товаров) {openMs} ms; поиск+COUNT {queryMs} ms; готовность {openMs + queryMs} ms");
// 3. Дельта-синк (с момента full — изменений почти нет).
var sync = new SyncService(warm, api);
warm.SetKv("last_sync", full.ServerTime.ToString("O"));
sw.Restart();
var deltaResp = await api.SyncAsync(full.ServerTime);
var dNet = sw.ElapsedMilliseconds;
sw.Restart();
warm.ApplySync(deltaResp);
var dApply = sw.ElapsedMilliseconds;
Console.WriteLine($"[bench] ДЕЛЬТА (без изменений): товаров={deltaResp.Products.Count} остатков={deltaResp.Stocks.Count}; сеть {dNet} ms; запись {dApply} ms");
warm.Dispose();
return 0;
}
finally { try { File.Delete(dbFile); } catch { } }
}
private sealed class BearerHandler : DelegatingHandler
{
private readonly string _token;
public BearerHandler(string token, HttpMessageHandler inner) : base(inner) { _token = token; }
protected override Task SendAsync(HttpRequestMessage req, CancellationToken ct)
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
return base.SendAsync(req, ct);
}
}
}