OQ-024 — тотальная оптимизация приложения, runtime и плагинов
Источник:
dev/fera/project-design/context/OPTIM-IDEAS-4-oq24.md(read-only; сгенерировано scripts/sync-docs.mjs)
OQ-024 — тотальная оптимизация приложения, runtime и плагинов
Section titled “OQ-024 — тотальная оптимизация приложения, runtime и плагинов”Главный вывод
Section titled “Главный вывод”Цель «потреблять как можно меньше» нужно превратить не в набор микрооптимизаций, а в систему бюджетов и режимов: CPU-время, число wake-up, RSS/PSS, виртуальная память, очередь сообщений, число одновременно активных плагинов, p95/p99 latency и энергия на фиксированную работу. Без такой системы легко получить обратный результат: например, включить work-stealing, spawn_blocking, pooling allocator или aggressive JIT и увеличить расход памяти и энергии.
Наиболее безопасная стартовая архитектура для проекта:
- Обычный режим: Tokio с явно заданным числом worker-потоков, а не с дефолтом «по числу CPU». Сравнивать
current_threadиmulti_thread().worker_threads(2). - Экономичный режим: один управляющий runtime/поток, событийное ожидание без polling, приостановка best-effort-активности и ограничение фоновых плагинов. Не «замораживать» произвольные Tokio worker-потоки изнутри приложения.
- Акторы: один владелец состояния, bounded mailbox, короткие
poll, backpressure и контролируемое число акторов. Work-stealing применять как средство исполнения коротких задач, а не как замену правильной гранулярности. - Блокирующий код: отдельный ограниченный контур.
spawn_blocking— только для коротких конечных операций; долгоживущие native/декодирующие workers — именованные потоки с протоколом остановки или отдельный процесс. - Плагины: WASI Component с узким версионированным WIT-контрактом для недоверенных, часто обновляемых или многочисленных расширений. Нативный
cdylib— только для доверенного и измеренно критичного к latency кода. - Reclamation: не использовать EBR/QSBR/HP в обычной акторной модели. Для действительно lock-free атомарных структур начать с короткого EBR/epoch scope без
.await; HP оставить для случаев, где нужна защита конкретного объекта и EBR не даёт приемлемого поведения. QSBR — только для явно контролируемых OS-thread/event-loop с проверяемыми quiescent points. - Hot-swap: сначала логически остановить поколение плагина, закрыть вход, дренировать или отменить операции, дождаться нулевого числа активных вызовов и только после этого освобождать состояние. Для native DLL нельзя считать фактический
dlcloseнадёжным способом вернуть память на всех платформах.
Конкретный выигрыш от каждого решения должен быть принят только после сравнения на целевом железе и на одинаковой нагрузке.
1. Runtime, Tokio и акторная модель
Section titled “1. Runtime, Tokio и акторная модель”Tokio multi-thread по умолчанию создаёт worker на каждое доступное CPU. Для продукта с целью «1–2 потока» это необходимо переопределить. Однопоточный current_thread не создаёт worker-пул и исполняет задачи только пока его ведёт Runtime::block_on; это естественная база для I/O-ориентированного режима на одном потоке. Двухпоточный runtime нужно рассматривать как отдельный измеряемый профиль, а не как безусловно более быстрый вариант. 1 2
Work-stealing полезен, когда есть несколько workers и достаточно мелких готовых задач. При одном worker красть работу не у кого. При двух workers появляется потенциальный выигрыш в параллельном исполнении коротких задач, но добавляются межпоточные очереди, пробуждения и синхронизация. Теоретическая оценка work-stealing для подходящих структурированных вычислений не доказывает выигрыш на произвольном async workload, где существенны I/O, wake-ups, FFI и длина poll. 3
Поэтому акторную модель лучше не переписывать ради ручного управления OS threads, а дополнить:
- один actor владеет изменяемым состоянием;
- команды идут через
tokio::sync::mpsc::channel(capacity), а ответы — черезoneshot; - mailbox bounded и имеет политику переполнения: ожидание, timeout, отказ, coalescing или контролируемый drop;
- размер пула ChatActor конфигурируется: 1 для экономичного профиля, 2 или больше для desktop-профиля после замеров;
!Sendили thread-affine части получают отдельныйLocalSet/current-thread домен и общаются сообщениями;- долгие операции не выполняются внутри actor handler и не удерживают mutex через
.await.
Unbounded mailbox противоречит цели по памяти: при отставании producer может бесконечно накапливать сообщения, а при исчерпании памяти Tokio предупреждает о возможном аварийном завершении. Для проектных команд нужно ограничивать не только число сообщений, но и размер payload. 4 5
Что делать с блокирующими задачами
Section titled “Что делать с блокирующими задачами”Блокирующий I/O, синхронный FFI и CPU-heavy код нельзя выполнять прямо в Tokio worker. spawn_blocking подходит для коротких конечных операций, но имеет высокий default limit, может создавать дополнительные потоки, складывать ожидающие задачи в очередь без встроенного backpressure и не умеет отменять уже начатую closure. Даже при current_thread этот механизм создаёт дополнительные OS threads. 6
Для проекта нужны два разных механизма:
- короткая blocking-операция:
spawn_blockingплюс semaphore и явный лимит concurrency; - долгоживущий worker: выделенный именованный OS thread или отдельный процесс, который ждёт события блокирующим примитивом и имеет команду остановки.
Не отправляйте в spawn_blocking бесконечный actor loop, polling или неконтролируемую очередь CPU-задач. Это лишь переносит проблему и может увеличить RSS, число потоков и время shutdown.
Настройка scheduler
Section titled “Настройка scheduler”Начинайте с дефолтных event_interval и global_queue_interval. Менять их стоит только после профилирования: более частые проверки могут улучшить fairness и реакцию, но увеличивают синхронизацию и системные вызовы. Длинные poll нужно исправлять декомпозицией, cooperative yield и вынесением блокирующего кода, а не только настройкой scheduler. 1 2
2. Экономичный режим и фоновые состояния
Section titled “2. Экономичный режим и фоновые состояния”Требование «в фоне оставить только управляющий и network» разумно как логическая политика, но не как грубое принудительное вмешательство в потоки. Tokio сам паркует worker, когда нет готовой работы. Правильная цель — убрать лишние wake-ups и позволить runtime/ОС входить в глубокие idle states, а не вручную гонять park/unpark. 7 8
Предлагаемая state machine:
Active
Section titled “Active”Все пользовательские функции доступны. Worker count — 1 или 2 в зависимости от профиля. Плагины работают в пределах admission-control. Housekeeping выполняется с обычными дедлайнами.
Idle-candidate
Section titled “Idle-candidate”В течение заданного окна нет пользовательских команд, активных запросов и срочных deadline. Supervisor прекращает запуск best-effort задач, объединяет необязательные таймеры и запрещает новые тяжёлые plugin jobs. Уже начатые операции не обрываются посередине небезопасным способом.
Background-saving
Section titled “Background-saving”Остаются network/control actor и минимальный runtime. Cache sweep, telemetry flush, prefetch, индексация, медиа-декодирование и второстепенные плагины либо паркуются на событии, либо переводятся в очередь с низким приоритетом. Все actor loops ждут recv().await, readiness или допустимый deadline. Запрещены try_recv в цикле, sleep(0), spin-wait и постоянный polling.
Suspended
Section titled “Suspended”Для полностью внешней паузы Linux можно заморозить выделенный cgroup v2 и дождаться подтверждения cgroup.events:frozen=1, но это deployment/orchestration-механизм, а не обычная внутренняя функция runtime. После thaw нужно заново сверить соединения, дедлайны, отмену и накопившуюся работу. 9
При возобновлении Supervisor должен:
- пересчитать просроченные deadlines по монотонным часам;
- проверить отменённые и протухшие запросы;
- обработать backlog с ограниченным burst budget;
- восстановить plugin actors по одному или небольшими партиями;
- записать причину и длительность паузы в telemetry.
На Linux можно отдельно исследовать PR_SET_TIMERSLACK для второстепенных таймеров: ядро получает право сгруппировать их, но это не должно применяться к network deadline, heartbeat, shutdown и пользовательской latency без явного budget. 10
Для macOS и Windows нужны platform adapters, а не безусловные вызовы из общего слоя. App Nap, Timer Coalescing, Windows execution-speed throttling и CPUQuota меняют latency и поведение I/O; они могут быть полезны как внешние политики, но не заменяют устранение wake-ups и не являются доказанным энергосбережением для данного приложения. 11 12
3. Минимальная память и модель плагинов
Section titled “3. Минимальная память и модель плагинов”Нативные плагины
Section titled “Нативные плагины”Нельзя строить долгоживущий ABI на extern "Rust", Rust trait objects, String, Vec, references, closures, Rust vtables или Rust-owned контейнерах. Rust ABI не имеет стабильных гарантий между версиями compiler/crate/build profile. Для native plugin нужен узкий версионированный C ABI: extern "C", фиксированные integer types, #[repr(C)] POD-структуры, opaque handles и явные create/destroy в одном владельце памяти. 13 14
libloading::Symbol<'lib, T> специально связывает symbol с lifetime Library. Raw pointer может обойти эту защиту, но не делает выгрузку безопасной. Library::close может быть no-op в зависимости от платформы, а init/fini-код библиотеки выполняет произвольный код. Поэтому безопасная политика для native hot-swap:
- новый plugin загружается и валидируется side-by-side;
- registry переключается атомарно;
- новые вызовы старого поколения запрещаются;
- старые actor mailboxes закрываются и дренируются;
- все in-flight calls и handles дожидаются завершения с дедлайном;
- symbols, callbacks, destructors и allocator-owned objects очищаются;
- только после этого библиотека может быть закрыта;
- если отсутствие живого plugin-кода доказать нельзя, DLL держится до завершения процесса либо plugin запускается в отдельном процессе.
Это означает: EBR или HP не делают dlclose безопасным. Они защищают данные, а не код, который может быть выгружен.
WASI Component
Section titled “WASI Component”Для сотен, часто обновляемых или потенциально недоверенных плагинов предпочтительнее WASI Component с узким WIT world. Component Model даёт межъязыковой Canonical ABI, capability-based imports и отдельную linear memory. Плагин получает только те capabilities, которые host явно выдал. 15 16
Практический контракт:
- один общий
EngineиLinker, чтобы не дублировать тяжёлое состояние; - отдельный
Storeи actor-владелец на plugin instance; - минимальный WIT world вместо универсального host API;
- versioned interfaces для breaking changes;
- только нужные preopened directories, network permissions и env/stdio;
- отсутствие
inherit_network, широких ReadWrite-доступов и secrets по умолчанию; - явные
StoreLimitsдо инстанцирования: memory, tables, instances, stack; - semaphore и bounded admission-control перед созданием instance.
WASI — не бесплатная замена DLL. Есть цена преобразования данных и вызовов через границу, а resource limits нужно задавать отдельно. Capability sandbox также не остановит CPU DoS без fuel/epoch deadline и backpressure.
JIT, AOT и Winch
Section titled “JIT, AOT и Winch”Module/Component нужно компилировать и кэшировать вне request/hot-swap critical path. Для известных релизных плагинов оптимальный кандидат — AOT в доверенном build pipeline. В артефакте нужно хранить Wasmtime version, target, CPU features, Config и hash исходного wasm. Непроверенный precompiled bytecode нельзя принимать: десериализация Wasmtime небезопасна и требует доверия к producer. 17
Для неизвестного wasm сравнивайте:
- Winch — быстрое baseline-компилирование, но обычно хуже steady-state throughput;
- Cranelift — более тяжёлая компиляция, но потенциально лучше длительная работа;
- AOT — отсутствие compile cost на устройстве, но зависимость от target/config.
Не включайте pooling allocator «для экономии памяти» автоматически. Он может ускорить instantiation, но резервирует значительный virtual address space, удерживает resident pages и имеет фиксированную capacity. Его нужно включать только при известном максимуме concurrent instances и после сравнения RSS, PSS, VA и latency. 18
4. EBR, QSBR и Hazard Pointers
Section titled “4. EBR, QSBR и Hazard Pointers”Главное решение: для обычного actor-owned state reclamation-механизмы не нужны. Если actor владеет объектом, его можно освобождать обычным Rust ownership при завершении поколения. SMR нужен только там, где lock или передача владения действительно стали доказанным bottleneck.
EBR/epoch
Section titled “EBR/epoch”EBR почти бесплатен на read-side, когда scope короткий: pin → load/deref/CAS → retire → drop guard. Но освобождение задерживается, пока pinned participant не выйдет. Stalled или упавший participant может удерживать retired memory без определённого срока. В Tokio есть дополнительный риск: задача может перейти на другой worker на .await, поэтому guard или raw pointer нельзя держать через .await, blocking I/O, plugin call или долгий callback. 19 20
Стартовая рекомендация — использовать проверенную реализацию вроде crossbeam-epoch только в синхронной атомарной структуре и lexical scope без .await. Collector и deferred destruction должны жить в host/core, а не в выгружаемой библиотеке.
QSBR может быть самым дешёвым read-side вариантом, когда участники известны и явно сообщают quiescent states. Но это не общий примитив для произвольно перемещаемых Tokio tasks. Его можно рассматривать для отдельного контролируемого OS thread/event loop, где определены online/offline и гарантированные точки покоя. Для async actor system это требует отдельного протокола и тестов, а не простой обёртки.
Hazard Pointers
Section titled “Hazard Pointers”HP защищает конкретный объект и лучше переносит остановившегося читателя, чем глобальный epoch. Цена — публикация hazard slot, повторная проверка указателя, memory ordering/fences и сканирование slots. Поэтому HP оправдан для небольшого числа крупных, критичных, отдельно адресуемых объектов или когда grace period EBR непредсказуем. Это не повод переводить каждый вызов таблицы плагина на HP.
Итоговый сплит лучше сформулировать так:
- actor-owned state: обычное ownership;
- короткие lock-free host tables: EBR/epoch;
- контролируемый dedicated reader loop: возможен QSBR;
- крупные объекты с требованиями к точному освобождению: HP;
- самодельный универсальный EBR+HP hybrid — только после доказанного bottleneck и аудита.
Не передавайте через plugin ABI guards, epoch records, Crossbeam shared pointers или native raw pointers. Передавайте opaque IDs/handles, а reclamation и allocator оставляйте host-owned. 21
5. Hot-swap lifecycle
Section titled “5. Hot-swap lifecycle”Для WASI:
- проверить подпись/hash и WIT compatibility;
- заранее подготовить
Module,ComponentиInstancePre; - создать новый
Storeс лимитами; - прогреть новый instance и проверить health;
- атомарно переключить routing;
- запретить новые вызовы старого поколения;
- дождаться или отменить in-flight calls на допустимых границах;
- экспортировать/мигрировать состояние явно;
- закрыть старый Store.
Для native DLL шаги те же, но добавляется строгая проверка, что ни один function pointer, callback, destructor, deferred reclamation callback или Drop не исполняется после unload. Если это нельзя доказать, не делать жёсткий unload. Отдельный process/plugin supervisor часто даёт лучшую надёжность и реальное освобождение памяти, чем сложный in-process dlclose.
6. План реализации по фазам
Section titled “6. План реализации по фазам”Фаза 0 — измерительный baseline
Section titled “Фаза 0 — измерительный baseline”Зафиксировать железо, ОС, allocator, compiler flags, число плагинов, размеры payload и сценарии. Снять current-thread, 2-worker и текущую production-конфигурацию.
Сценарии минимум такие:
- idle foreground;
- idle background;
- burst сообщений;
- постоянный I/O;
- CPU-heavy plugin;
- blocking disk/media operation;
- cold и warm plugin start;
- repeated hot-swap;
- stalled reader и отмена задачи;
- 1, 10, 100 и 500 plugin instances.
Метрики: CPU time, wall time, RSS/PSS, virtual memory, allocation rate, peak memory, mailbox high-watermark, blocking queue depth, число OS threads, Tokio busy duration, park/unpark counters, p50/p95/p99 latency, wake-ups и joules/CPU package energy на фиксированную работу. Runtime metrics — диагностические, не замена измерению энергии. Для Linux использовать доступные perf/RAPL или платформенный power counter; если счётчика нет, фиксировать CPU time и wake-ups как proxy и явно помечать это ограничение.
Правило принятия: изменение принимается только если оно улучшает нужный KPI при сохранении SLO, не ухудшает остальные бюджеты сверх заранее заданного порога и воспроизводится минимум на нескольких прогонах на целевом железе.
Фаза 1 — безрисковые ограничения
Section titled “Фаза 1 — безрисковые ограничения”- убрать unbounded channels;
- задать worker count явно;
- добавить semaphore вокруг
spawn_blocking; - убрать polling и zero-timeout loops;
- ограничить payload и mailbox;
- сократить
pollи запретить blocking FFI на worker; - добавить supervisor, который хранит JoinHandle;
- включить runtime metrics и plugin lifecycle telemetry.
Фаза 2 — экономичный профиль
Section titled “Фаза 2 — экономичный профиль”current_threadbaseline;- логическая background state machine;
- остановка best-effort actors;
- coalesced/jittered housekeeping;
- восстановление с reconciliation;
- platform-specific timer slack только для некритичных событий;
- отдельное сравнение одного и двух workers.
Фаза 3 — plugin backend
Section titled “Фаза 3 — plugin backend”- зафиксировать WIT world и policy capabilities;
- общий Engine/Linker и отдельный Store на plugin actor;
- per-Store limits и admission-control;
- AOT pipeline для доверенных плагинов;
- cold/warm cache и подпись артефактов;
- сравнение native
cdylib, WASI AOT и WASI JIT на representative payloads.
Фаза 4 — reclamation и оптимизация таблиц
Section titled “Фаза 4 — reclamation и оптимизация таблиц”Только после профилирования определить, есть ли реальный bottleneck в shared atomic registry. Если нет — оставить actor ownership. Если есть — сравнить обычную блокировку/ownership, EBR, HP и, в узком dedicated loop, QSBR. Обязательно тестировать retired queue age, grace period, stalled participant, cancellation и repeated swap.
Фаза 5 — агрессивные оптимизации
Section titled “Фаза 5 — агрессивные оптимизации”LTO, codegen-units, strip/symbol policy, allocator choice, cache layout, batching, intrusive structures, pooling и platform power policies. Каждое изменение — отдельный эксперимент с rollback. Не форкайте библиотеку runtime заранее: сначала нужен baseline, профили bottleneck и минимальный upstream-compatible patch. Форк увеличивает стоимость обновлений, аудита unsafe-кода и security fixes.
7. Что не делать
Section titled “7. Что не делать”- Не оставлять Tokio worker count равным числу CPU при энергобюджете 1–2 потока.
- Не считать work-stealing доказанной оптимизацией для actor/I/O workload.
- Не использовать unbounded mailbox или массовый
spawn_blockingбез admission-control. - Не выполнять CPU-heavy, blocking FFI или долгий Wasm вызов в одном async
poll. - Не удерживать EBR guard или HP protection через
.await. - Не использовать QSBR как общий механизм для Tokio tasks.
- Не выгружать DLL сразу после
abort,flushили переключения registry. - Не передавать Rust ABI-типы и raw pointers между independently built plugins.
- Не включать Wasmtime pooling allocator с дефолтами для сотен плагинов.
- Не принимать непроверенный AOT/.cwasm.
- Не выдавать WASI широкие capabilities и считать sandbox полной защитой от resource DoS.
- Не форкать runtime library до появления измеренного bottleneck и тестового baseline.
- Не объявлять любую оптимизацию успешной без энергопрофиля и регрессионного теста.
Обновлённая формулировка OQ-024
Section titled “Обновлённая формулировка OQ-024”OQ-024: Total resource optimization and energy-saving runtime.
Минимизировать CPU, wake-ups, RSS/PSS, виртуальную память, число потоков, allocation rate и энергию как first-class цели, сохраняя заданные latency/throughput SLO. Поддержать отдельный экономичный режим с current-thread или явно ограниченным runtime, parked event-driven workers, остановкой best-effort фоновых акторов, ограниченным blocking pool, reconciliation после resume и platform adapters. Не делать внешнее «замораживание» произвольных Tokio tasks частью базового протокола.
Плагинный backend разделить по назначению: WASI Component + узкий versioned WIT для недоверенных, многочисленных и часто обновляемых расширений; native
cdylibс C ABI — только для доверенного кода, если benchmark докажет необходимость. Для WASI использовать общий Engine/Linker, отдельный Store и явные capability/resource limits. AOT — для доверенного release pipeline; JIT/Winch/Cranelift выбирать экспериментом.Hot-swap реализовать как двухфазный lifecycle: prepare, validate, route switch, drain/cancel, zero active references, cleanup, затем destroy. Reclamation не использовать в обычном actor-owned state. Для действительно lock-free структур: EBR/epoch как основной кандидат для коротких sync scopes, HP — точечно при необходимости per-object protection, QSBR — только в контролируемом dedicated loop. Ни один механизм reclamation сам по себе не делает native code unload безопасным.
Форкнуть библиотеку модульного runtime только после baseline, профилирования и фиксации upstream-compatible изменений. Любая оптимизация принимается через reproducible benchmark: CPU/RAM/energy/latency, idle/burst/load/hot-swap и 1/10/100/500 plugin scenarios.