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); } } }