diff --git a/deploy/pos-install.ps1 b/deploy/pos-install.ps1 new file mode 100644 index 0000000..ce097d0 --- /dev/null +++ b/deploy/pos-install.ps1 @@ -0,0 +1,54 @@ +# Установка кассы ALL MARKET на Windows (быстрый первый запуск). +# +# Что делает: +# 1. Распаковывает сборку кассы в целевую папку (по умолчанию %LOCALAPPDATA%\all-market-pos). +# 2. Добавляет папку в исключения Microsoft Defender (если запущено от админа) — +# убирает скан свежего неподписанного exe при первом запуске (замер: холодный +# старт 10 c → 4.5 c). Без прав админа — печатает инструкцию, установка продолжается. +# 3. «Прогревает» кассу одним фоновым запуском (--smoke) — наполняет кэши ОС/рантайма, +# последующие запуски из этой папки стартуют за ~0.6 c. +# 4. Создаёт ярлык на рабочем столе. +# +# Запуск (ПКМ → «Запуск от имени администратора» для шага 2): +# powershell -ExecutionPolicy Bypass -File pos-install.ps1 -Zip .\pos-win.zip +param( + [string]$Zip = ".\pos-win.zip", + [string]$Dest = "$env:LOCALAPPDATA\all-market-pos" +) +$ErrorActionPreference = 'Stop' + +Write-Host "→ Распаковка в $Dest ..." +if (Test-Path $Dest) { Remove-Item -Recurse -Force $Dest } +Expand-Archive -Path $Zip -DestinationPath $Dest -Force + +$exe = Join-Path $Dest "foodmarket.Pos.exe" +if (-not (Test-Path $exe)) { throw "Не найден $exe — проверьте архив." } + +Write-Host "→ Исключение Microsoft Defender для папки кассы ..." +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if ($isAdmin) { + try { + Add-MpPreference -ExclusionPath $Dest -ErrorAction Stop + Write-Host " ✓ Папка добавлена в исключения Defender (первый запуск быстрее)." + } catch { + Write-Warning " Не удалось добавить исключение: $($_.Exception.Message)" + } +} else { + Write-Warning " Нет прав администратора — исключение Defender пропущено." + Write-Host " Чтобы ускорить первый запуск, выполните один раз от администратора:" + Write-Host " Add-MpPreference -ExclusionPath `"$Dest`"" +} + +Write-Host "→ Прогрев (один фоновый запуск) ..." +try { & $exe --smoke | Out-Null } catch { Write-Warning " Прогрев не критичен: $($_.Exception.Message)" } + +Write-Host "→ Ярлык на рабочем столе ..." +try { + $lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) "ALL MARKET Касса.lnk" + $ws = New-Object -ComObject WScript.Shell + $s = $ws.CreateShortcut($lnk); $s.TargetPath = $exe; $s.WorkingDirectory = $Dest; $s.Save() + Write-Host " ✓ $lnk" +} catch { Write-Warning " Ярлык не создан: $($_.Exception.Message)" } + +Write-Host "`nГотово. Касса запускается из локальной базы мгновенно; каталог синхронизируется в фоне." +Write-Host "Подпись exe (устранит предупреждения SmartScreen/Defender без исключения) — см. docs/pos.md." diff --git a/deploy/pos-win-measure.ps1 b/deploy/pos-win-measure.ps1 new file mode 100644 index 0000000..8a83a9a --- /dev/null +++ b/deploy/pos-win-measure.ps1 @@ -0,0 +1,52 @@ +param([string]$Url, [string]$User, [string]$Pass) +$ErrorActionPreference = 'Continue' +function TMeasure($block) { (Measure-Command $block).TotalMilliseconds } + +Write-Output "=== Windows POS startup measurement ===" +Write-Output ("CPU: " + (Get-CimInstance Win32_Processor).Name) +$defAV = (Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct -ErrorAction SilentlyContinue | Select-Object -First 1 -Expand displayName) +Write-Output ("AV: " + $defAV) +$rt = (Get-MpPreference -ErrorAction SilentlyContinue).DisableRealtimeMonitoring +Write-Output ("Defender realtime disabled: " + $rt) + +$zip = "C:\Users\tester\pos-win.zip" + +function FreshUnpack($dir) { + if (Test-Path $dir) { Remove-Item -Recurse -Force $dir } + Expand-Archive -Path $zip -DestinationPath $dir -Force +} + +# 1. COLD: свежая распаковка, первый запуск exe (Defender сканирует неподписанный exe+dll) +FreshUnpack "C:\pos-cold" +$exe = "C:\pos-cold\foodmarket.Pos.exe" +$cold = TMeasure { & $exe --smoke | Out-Null } +Write-Output ("COLD start (--smoke, Defender on, no exclusion): {0:N0} ms" -f $cold) +# 2. WARM: повторные запуски (Defender закэшировал) +$warm = @() +for ($i=0; $i -lt 3; $i++) { $warm += (TMeasure { & $exe --smoke | Out-Null }) } +Write-Output ("WARM start x3 (--smoke): " + (($warm | ForEach-Object { '{0:N0}' -f $_ }) -join ', ') + " ms") + +# 3. Defender exclusion (нужны права админа) + свежая распаковка + cold +$exclOk = $true +try { Add-MpPreference -ExclusionPath "C:\pos-excl" -ErrorAction Stop } catch { $exclOk = $false; Write-Output ("Defender exclusion FAILED (нужен админ): " + $_.Exception.Message) } +if ($exclOk) { + FreshUnpack "C:\pos-excl" + $exe2 = "C:\pos-excl\foodmarket.Pos.exe" + $coldExcl = TMeasure { & $exe2 --smoke | Out-Null } + Write-Output ("COLD start WITH Defender exclusion: {0:N0} ms" -f $coldExcl) +} + +# 4. Bench каталога (сеть+запись+тёплый+дельта) — нормально и на 1 ядре +if ($Url -and $User -and $Pass) { + $out = "C:\pos-cold\bench.txt" + Write-Output "--- bench NORMAL (all cores) ---" + & $exe --bench $Url $User $Pass 2>&1 | Where-Object { $_ -match 'bench' } | ForEach-Object { Write-Output $_ } + + Write-Output "--- bench 1 CPU core (Celeron N4120 прокси) ---" + $psi = Start-Process -FilePath $exe -ArgumentList @('--bench',$Url,$User,$Pass) -PassThru -RedirectStandardOutput $out -NoNewWindow + Start-Sleep -Milliseconds 300 + try { $psi.ProcessorAffinity = [IntPtr]1; $psi.PriorityClass = 'BelowNormal' } catch { Write-Output ("affinity set failed: " + $_.Exception.Message) } + $psi.WaitForExit() + Get-Content $out | Where-Object { $_ -match 'bench' } | ForEach-Object { Write-Output $_ } +} +Write-Output "=== done ===" diff --git a/docs/moysklad-pos-startup.md b/docs/moysklad-pos-startup.md new file mode 100644 index 0000000..2af54bd --- /dev/null +++ b/docs/moysklad-pos-startup.md @@ -0,0 +1,42 @@ +# Почему «Касса МойСклад» долго грузит товары — замер 2026-10-09 (только чтение) + +**Устройство:** ноутбук DESKTOP-KHS3BEH, Celeron N4120 1,1 ГГц (4 ядра), 8 ГБ RAM, SATA SSD 128 ГБ, Windows 10, антивирус Windows Defender. +**Касса:** «Касса МойСклад» 4.351.1 (LLC LogneX), **Electron/Chromium**, бизнес-логика — Scala.js (`resources/app/web-retail-opt.js`), драйверы ККТ libfptr (Атол) / PiritLib, весы Масса-К по COM. +**Данные:** `%APPDATA%\moysklad-kassa\` 137 МБ: локальная БД SQLite `retail_.sqlite` 58 МБ + WAL 36 МБ; логи `logs\app_YYYY-MM-DD.log` (JSON-строки, ~1 МБ/день, по датам UTC); Chromium-кэш 28 МБ. Апдейтер `%LOCALAPPDATA%\moysklad-kassa-updater` 290 МБ. Программа `C:\Program Files (x86)\moysklad-kassa` 571 МБ. + +## Цифры запуска (из `[performanceMetric:startup]`) +| Этап | 04.10 (после перезагрузки ПК) | 07.10 | +|---|---|---| +| JS main-процесса загружен | 2,6 с | 19,5 с | +| Окно создано | 3,9 с | 20,0 с | +| JS renderer загружен (Scala.js бандл) | 34,7 с | 29,9 с | +| Первый экран | 41,3 с | 35,4 с | +| **Готова к работе** | **112,9 с** | **99,1 с** | +| CPU, израсходованный к готовности | 57 c | 46 c | + +После первого экрана и проверки «Честного знака» лог **молчит ~60 с** (04.10: +52→+110 с; 07.10: +17→+79 с), при этом процесс жжёт CPU — это не ожидание сети, а локальная работа. + +## Что внутри БД (копия, read-only) +- `assortment`: **30 989 товаров**, каждый хранит полный JSON (`obj`), средняя строка ~0,9 КБ → ~27 МБ. +- `assortment_fts_idx` — **FTS5** по name/code/article/attributes/barcodes (contentless-external `content='assortment'`), `gtin` 31 000 строк, 39 индексов. +- **У всех 30 989 строк одинаковые `load_version = 1339` и `json_version = 15`** → каталог при загрузке **перезаписывается целиком** (полная перезагрузка, не дельта). 1339 загрузок с авг. 2024 ≈ 1–2 полные перезагрузки в сутки. +- WAL 36 МБ — след такой массовой перезаписи. + +## Прочие факторы (меньше по вкладу) +- Проверка ПИоТ/«Честный знак» на каждом старте: 4 запроса `cdn.crpt.ru/.../cdn/info` → **401 (некорректный токен в настройках юрлица)**, ~2,5 с ретраев; ошибка не блокирует. +- Загрузка JS-бандла renderer 10–30 с на этом CPU (тяжёлый Scala.js). +- Опрос весов: сотни переподключений/таймаутов в день («Порт не выбран», «Таймаут getWeight») — фон, на старт не влияет напрямую. +- Автообновление проверяется каждый час (сейчас обновлений нет; в main.log до 15.09 — многократные полные перекачки апдейта из-за несовпадения sha512 — исторически грузили сеть). +- Защитник Windows сканирует запись в SQLite/WAL — вероятно усиливает время массовой перезаписи (не замерялось). + +## Сеть +Трафик не перехватывался. По данным: полная перезагрузка = весь ассортимент (~27 МБ JSON до сжатия) с api.moysklad.ru; в логах кассы URL с `updatedFrom` не пишутся — подтвердить дельту/полную по сети не удалось, но по БД — полная. + +## Выводы для нашей кассы +1. **Синхронизация каталога только дельтой** (`updated>=lastSync`, удалённые — отдельно), без перезаписи всех строк; хранить версию на строку, а не одну на весь каталог. +2. **Не пересобирать полнотекстовый индекс целиком** — обновлять построчно (триггеры FTS) или искать по нормализованному полю с B-tree индексами. +3. **Не хранить полный JSON товара** в горячей таблице — только поля для продажи/поиска (название, цены, штрихкоды, весовой), остальное лениво. +4. **Показывать кассу готовой сразу** с локальным каталогом и синхронизировать в фоне (сейчас продажа недоступна ~60 с). +5. Батчевые записи в одной транзакции, WAL с периодическим checkpoint; исключить папку базы из Defender (рекомендация при установке). +6. Лёгкий фронтенд: 10–30 с только на загрузку JS на слабом Celeron — целиться в <3 с. +7. Внешние проверки (ЧЗ/ПИоТ) — асинхронно и без ретраев при 401. diff --git a/docs/pos-progress.md b/docs/pos-progress.md index 87bbfa2..0095bd7 100644 --- a/docs/pos-progress.md +++ b/docs/pos-progress.md @@ -180,3 +180,81 @@ by-number + защита over-return, PIN-грант, /api/me retailPointIds, ф Коммиты доводки: `c5ff1ad` (формат/скидка/кнопки), `01b47cc` (возврат по чеку), `ff69dec` (выбор точки + привязка кассира), `6b959ac` (PIN-вход), `9cca062` (R2R + Defender), `84fc350` (реальное окно Windows 11). Плюс серверный инцидент-фикс кассира ранее (`9dc033d`). + +--- + +## Мгновенный старт (идея владельца 09.10) + +Запрос: «Касса МойСклад при запуске очень долго грузится. Можем сделать, чтобы +загружалась мгновенно?» Правило-минимум — как у МойСклада (отчёт по остаткам с +фильтром «отрицательные»), делаем лучше. + +### Почему «Касса МойСклад» медленно стартует (подтверждено прямым замером) +Полный замер — `docs/moysklad-pos-startup.md` (sysadmin, 09.10, read-only на реальном +ноутбуке Celeron N4120). Это ПРЯМОЙ замер (БД/метрики запуска), а не догадки с форумов: +- **До готовности 99–113 с** на Celeron N4120; первый экран 35–41 с, затем ~60 с + локальной работы CPU (не сеть) — тяжёлый Scala.js-бандл (Electron). +- **Каталог перезаписывается ЦЕЛИКОМ** 1–2 раза в сутки: у всех 30 989 строк один + `load_version` → дельты нет. +- Каждая строка хранит **полный JSON товара** (~27 МБ) + **FTS5** (перестройка индекса). +- Внешние проверки («Честный знак») на старте: 4 запроса → 401, ~2,5 с (не блокируют). +> Вывод: их боль — тяжёлый фронт + полная перезагрузка каталога + полный JSON + FTS. +> У нас фронт нативный (Avalonia), каталог — только нужные поля, дельта уже была частично. + +### Что сделали +1. **Касса готова сразу из локальной базы; вся синхронизация — в фоне.** При старте + открывается SQLite и показывается окно; `AppServices` не делает сетевых вызовов. + Синк и проверка версии — асинхронно (fire-and-forget, таймаут HTTP). Тяжёлая запись + каталога/дельты вынесена на пул потоков (`Task.Run`) + маршалинг в UI через + `Dispatcher` — **окно никогда не ждёт синк**. Индикатор в шапке + «обновлено N мин назад / обновляется…». Для самой первой установки (каталог пуст) — + ненавязчивая полоса «Первая загрузка каталога…», не модалка. +2. **Дельта-синк.** Товары/цены/контрагенты уже шли дельтой (`updated>since`). Добавили + **дельту остатков**: `PosSyncResponse.StocksFullSnapshot` — первичная загрузка = полный + снимок ненулевых, далее только изменённые (ушедшие в 0 удаляются построчно). Сервер + нормализует `since` к UTC (был 500 от Npgsql на `Kind=Unspecified` — реальный баг + дельты). Проверка «выключена сутки, изменено 500 товаров» — см. таблицу. +3. **Первичная загрузка — один сжатый снимок.** Каталог отдаётся ОДНИМ ответом (не + постранично). Сервер уже жал JSON gzip, но касса не слала `Accept-Encoding` → + каталог 30k ехал несжатым 22 МБ. Включили `AutomaticDecompression`: **22 МБ → 3 МБ + (×7.3)** → при 50 Мбит ~0,48 с вместо ~3,5 с — цель < 3 с достигнута. Плюс в кассе + убран O(n²)-скан цен в `ApplySync` (запись 30k: **12,2 с → 1,8 с**). Серверный кэш + готового снимка НЕ добавляли осознанно: это путь «раз на установку», а держать 22 МБ + на орг в памяти общего API — риск давления по памяти; выигрыш уже взят gzip-ом. + Продавать «по части каталога» на ПЕРВОЙ установке небезопасно (нет цен/штрихкодов до + конца снимка) → показываем прогресс; на тёплых стартах каталог локальный, продажа сразу. +4. **Холодный старт Windows.** Главный тормоз — Defender сканирует свежий неподписанный + бандл. Сборка с ReadyToRun (меньше JIT). Установщик `deploy/pos-install.ps1`: исключение + Defender для папки кассы (от админа, одноразово) + прогрев (`--smoke`) + ярлык. Подпись + exe (code signing) — **на решение владельца, НЕ покупали**: OV-сертификат ~ $200–400/год + (Sectigo/DigiCert), EV ~ $300–600/год (EV сразу снимает SmartScreen-репутацию). Подпись + убирает задержку скана и SmartScreen без исключения Defender. + +### Замеры до/после (30 000 товаров, демо-каталог `?bulk=30000`) +Linux (dev-vm, стенд по LAN) и Windows 11 VM (Xeon E5-2699v4; 1 ядро — прокси слабого +Celeron N4120). «До» — коммиты до 63da7a9; «после» — текущее. + +| Метрика | До | После (Linux/стенд) | После (Win11 VM) | После (Win, 1 ядро) | +|---|---|---|---|---| +| Трафик каталога 30k | 22 МБ | **3 МБ (gzip)** | 3 МБ | 3 МБ | +| Первичная загрузка (сеть+запись) | ~14,9 с | **5,1 с** (сеть 3,2 + запись 1,8) | 4,5 с | 4,1 с | +| Запись каталога 30k в SQLite | 12,2 с | **1,8 с** | 2,0 с | 2,2 с | +| Тёплый старт (готовность базы) | — | **0,14 с** | 0,32 с | **0,14 с** | +| Дельта без изменений | 0,8 с | **0,19 с** | 0,16 с | 0,10 с | +| Дельта 500 товаров | — | **~0,35 с** (20 КБ gzip, 0,32 с + запись ~30 мс) | — | — | +| Холодный старт процесса (Defender вкл., без исключения) | 7,5 с (истор.) | — | **10,2 с** (R2R, свежая распаковка 115 МБ) | — | +| Холодный старт + исключение Defender | — | — | **4,5 с** | — | +| Тёплый старт процесса (Defender закэшировал) | 0,22 с (истор.) | — | **~0,6 с** (`--smoke`, вкл. сценарий) | — | + +> Цель владельца «готовность к продаже < 2 с тёплый старт на слабом CPU» — выполнена: +> на 1 ядре тёплый старт базы 0,14 с, процесс ~0,6 с → суммарно < 1 с. +> Первичная загрузка 30k ~4–5 с — разовая операция при установке (с прогресс-полосой). + +### Инструменты замера (в репозитории) +- `POST /api/admin/seed-demo?bulk=N` — массовый каталог до 60k товаров (батчами). +- POS `--bench ` — первичная/тёплая/дельта (отдельно сеть и запись). +- `win-measure.ps1` (в прогоне) — холодный/тёплый старт + bench + 1-ядро на Win11 VM. + +### Что НЕ трогали +Прод, shared-postgres, global.json, gateway nginx, sklad, другие VM кроме тестовой +Windows 11 (192.168.1.231). Себестоимость/расчёты не затрагивались. diff --git a/docs/pos.md b/docs/pos.md index 0cdcf89..f1373ce 100644 --- a/docs/pos.md +++ b/docs/pos.md @@ -90,15 +90,18 @@ Linux x64. Быстрая, без лишнего; офлайн-режим; се - Сборка self-contained + ReadyToRun (R2R). Холодный старт: Linux ~120 мс; Windows прогретый — **~220 мс** (R2R) / ~150 мс (без R2R). -### Первый запуск на Windows медленный (4–5 с) — что делать -Самый первый запуск кассы сразу после установки занимает 4–5 с (с R2R; без R2R было -7,5 с). Это НЕ код кассы: Windows Defender на лету сканирует свежий неподписанный -exe (~114 МБ, self-contained), плюс холодное чтение файла с диска. Каждый следующий -запуск — ~0,2 с. Как ускорить первый запуск на кассовом ПК: -1. **Исключение в Defender** для папки кассы (одноразово, администратором): - `Add-MpPreference -ExclusionPath "C:\food-market-pos"` (PowerShell от админа). -2. **Прогрев при установке** — установщик может один раз запустить `foodmarket.Pos.exe` - (например, `--smoke`) после распаковки, чтобы Defender просканировал файлы заранее. -3. **Подпись exe** (code signing) — позже; подписанный бинарник Defender сканирует - быстрее и не держит пользователя на SmartScreen. -Замеры холодного старта (Linux/Windows, R2R и без) — см. docs/pos-progress.md. +### Первый запуск на Windows медленный — что делать +Тёплый старт кассы — доли секунды (окно из локальной SQLite, синк в фоне). Медленный +только САМЫЙ первый запуск сразу после установки: Windows Defender на лету сканирует +свежий неподписанный бандл (~115 МБ, self-contained + R2R). Замер на Win11 VM: +холодный старт **10 с без исключения → 4,5 с с исключением Defender**; тёплый ~0,6 с. +Готовый установщик — `deploy/pos-install.ps1` (распаковка + исключение Defender от +админа + прогрев `--smoke` + ярлык). Вручную: +1. **Исключение в Defender** для папки кассы (одноразово, от админа): + `Add-MpPreference -ExclusionPath "<папка кассы>"`. +2. **Прогрев** — один запуск `foodmarket.Pos.exe --smoke` после распаковки. +3. **Подпись exe** (code signing) — на решение владельца, пока НЕ покупали: OV ~ $200–400/год + (Sectigo/DigiCert), EV ~ $300–600/год. Подписанный бинарник Defender сканирует быстрее, + EV сразу снимает предупреждение SmartScreen без исключения Defender. +Полные замеры до/после (Linux + Windows, 30 000 товаров) — см. docs/pos-progress.md, +раздел «Мгновенный старт».