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
- 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>
93 lines
4.9 KiB
C#
93 lines
4.9 KiB
C#
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 <serverUrl> <user> <password></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);
|
||
}
|
||
}
|
||
}
|