food-market/src/food-market.pos/Tooling/BenchRunner.cs
nns e78f14bc6b
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
test(pos): инструменты замера старта — bulk-каталог + bench
- POST /api/admin/seed-demo?bulk=N — массовое наполнение каталога (до 60k товаров)
  батчами для нагрузочных замеров (идемпотентно по маркеру BULK-).
- POS --bench <url> <user> <pass>: мерит первичную загрузку (сеть/запись отдельно),
  тёплый старт (открытие базы + поиск), дельта-синк. С gzip-декомпрессией.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 04:59:11 +05:00

93 lines
4.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>Замеры старта/синхронизации кассы против живого сервера.
/// Запуск: <c>dotnet run -- --bench &lt;serverUrl&gt; &lt;user&gt; &lt;password&gt;</c>.
///
/// Мерит: (1) первичную полную загрузку каталога (HTTP+десериализация отдельно
/// от записи в SQLite), (2) тёплый старт — открытие локальной базы + поиск,
/// (3) дельта-синк (изменений ~нет). Всё — с включённой gzip-декомпрессией.</summary>
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 <serverUrl> <user> <password>"); return 2; }
return RunAsync(rest[0].TrimEnd('/'), rest[1], rest[2]).GetAwaiter().GetResult();
}
private static async Task<int> 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<IServerApi>(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<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken ct)
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
return base.SendAsync(req, ct);
}
}
}