docs(pos): мгновенный старт — замеры до/после, МойСклад-разбор, установщик Windows
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

- docs/moysklad-pos-startup.md: прямой замер «Кассы МойСклад» (Celeron N4120, 99–113 с
  до готовности; каталог перезаписывается целиком, полный JSON+FTS) — подтверждённые
  причины, не догадки.
- docs/pos-progress.md: раздел «Мгновенный старт» — что сделано (касса из локальной базы +
  фоновый синк, дельта остатков, gzip-каталог ×7.3, синк вне UI-потока) + таблица замеров
  (Linux/стенд + Win11 VM + 1 ядро как прокси Celeron). Цель <2 с тёплый старт на слабом
  CPU достигнута (<1 с). Серверный кэш снимка осознанно не делали (риск памяти, путь
  «раз на установку»).
- deploy/pos-install.ps1: установщик Windows (исключение Defender + прогрев + ярлык);
  холодный старт 10 с → 4,5 с. deploy/pos-win-measure.ps1 — скрипт замеров на VM.
- docs/pos.md: обновлён раздел первого запуска + варианты/стоимость подписи exe (не покупали).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 05:11:34 +05:00
parent e78f14bc6b
commit 92befd1f05
5 changed files with 241 additions and 12 deletions

54
deploy/pos-install.ps1 Normal file
View file

@ -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."

View file

@ -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 ==="

View file

@ -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_<id>.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.

View file

@ -180,3 +180,81 @@ by-number + защита over-return, PIN-грант, /api/me retailPointIds, ф
Коммиты доводки: `c5ff1ad` (формат/скидка/кнопки), `01b47cc` (возврат по чеку), Коммиты доводки: `c5ff1ad` (формат/скидка/кнопки), `01b47cc` (возврат по чеку),
`ff69dec` (выбор точки + привязка кассира), `6b959ac` (PIN-вход), `9cca062` (R2R + Defender), `ff69dec` (выбор точки + привязка кассира), `6b959ac` (PIN-вход), `9cca062` (R2R + Defender),
`84fc350` (реальное окно Windows 11). Плюс серверный инцидент-фикс кассира ранее (`9dc033d`). `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 <url> <user> <pass>` — первичная/тёплая/дельта (отдельно сеть и запись).
- `win-measure.ps1` (в прогоне) — холодный/тёплый старт + bench + 1-ядро на Win11 VM.
### Что НЕ трогали
Прод, shared-postgres, global.json, gateway nginx, sklad, другие VM кроме тестовой
Windows 11 (192.168.1.231). Себестоимость/расчёты не затрагивались.

View file

@ -90,15 +90,18 @@ Linux x64. Быстрая, без лишнего; офлайн-режим; се
- Сборка self-contained + ReadyToRun (R2R). Холодный старт: Linux ~120 мс; Windows - Сборка self-contained + ReadyToRun (R2R). Холодный старт: Linux ~120 мс; Windows
прогретый — **~220 мс** (R2R) / ~150 мс (без R2R). прогретый — **~220 мс** (R2R) / ~150 мс (без R2R).
### Первый запуск на Windows медленный (4–5 с) — что делать ### Первый запуск на Windows медленный — что делать
Самый первый запуск кассы сразу после установки занимает 4–5 с (с R2R; без R2R было Тёплый старт кассы — доли секунды (окно из локальной SQLite, синк в фоне). Медленный
7,5 с). Это НЕ код кассы: Windows Defender на лету сканирует свежий неподписанный только САМЫЙ первый запуск сразу после установки: Windows Defender на лету сканирует
exe (~114 МБ, self-contained), плюс холодное чтение файла с диска. Каждый следующий свежий неподписанный бандл (~115 МБ, self-contained + R2R). Замер на Win11 VM:
запуск — ~0,2 с. Как ускорить первый запуск на кассовом ПК: холодный старт **10 с без исключения → 4,5 с с исключением Defender**; тёплый ~0,6 с.
1. **Исключение в Defender** для папки кассы (одноразово, администратором): Готовый установщик — `deploy/pos-install.ps1` (распаковка + исключение Defender от
`Add-MpPreference -ExclusionPath "C:\food-market-pos"` (PowerShell от админа). админа + прогрев `--smoke` + ярлык). Вручную:
2. **Прогрев при установке** — установщик может один раз запустить `foodmarket.Pos.exe` 1. **Исключение в Defender** для папки кассы (одноразово, от админа):
(например, `--smoke`) после распаковки, чтобы Defender просканировал файлы заранее. `Add-MpPreference -ExclusionPath "<папка кассы>"`.
3. **Подпись exe** (code signing) — позже; подписанный бинарник Defender сканирует 2. **Прогрев** — один запуск `foodmarket.Pos.exe --smoke` после распаковки.
быстрее и не держит пользователя на SmartScreen. 3. **Подпись exe** (code signing) — на решение владельца, пока НЕ покупали: OV ~ $200–400/год
Замеры холодного старта (Linux/Windows, R2R и без) — см. docs/pos-progress.md. (Sectigo/DigiCert), EV ~ $300–600/год. Подписанный бинарник Defender сканирует быстрее,
EV сразу снимает предупреждение SmartScreen без исключения Defender.
Полные замеры до/после (Linux + Windows, 30 000 товаров) — см. docs/pos-progress.md,
раздел «Мгновенный старт».