evgene-kopylov_fsrs_plugin/docs/optimization-attempts.md
2026-05-13 21:46:03 +03:00

7.9 KiB
Raw Permalink Blame History

Попытки оптимизации

В этом файле фиксируются попытки оптимизации, которые не дали результата, чтобы не повторять их в будущем. Либо дали, но спор не достаточный.

1. Time-based yield при сканировании

В performCacheScanAsync добавлен yield по времени: каждые N итераций проверять performance.now() - lastYieldTime и если > 15ms — отдавать управление через setTimeout.

Результат: Не устранило violation 'setTimeout' handler took Nms. Накладные расходы на performance.now() и setTimeout замедлили сканирование с ~3.3с до ~4.2с.

2. Yield по итерациям (50, 100, 200)

В performCacheScanAsync добавлен yield каждые N итераций (пробовали 50, 100, 200, 500).

Результат: Violation оставался (первые итерации до первого yield выполняются синхронно). Overhead от частых yield замедлял сканирование.

3. Кеширование parametersToJson в computeCardState

prepareCommonArgs вызывается для каждой карточки. parametersToJson(settings.parameters) сериализует одни и те же параметры 5000 раз. Добавлены опциональные parametersJson и nowStr в TS-функции, кеширование в performCacheScanAsync.

Результат: Выигрыш < 10ms на 5000 вызовов. parametersToJson и now.toISOString() настолько быстрые, что оверхед от условной логики перевешивает профит. Откачено.

4. Батчевый computeCardsState (первая версия)

Добавлена Rust-функция compute_cards_state, которая принимает JSON-массив карточек и возвращает JSON-массив состояний. На TS — computeCardsState, в performCacheScanAsync — один вызов на пачку.

Первая версия Rust: парсила массив в Vec<serde_json::Value>, для каждого элемента вызывала compute_current_state (которая парсит JSON заново). Двойная сериализация.

Результат: Сканирование не ускорилось (3.39 → 3.63 с) из-за двойной сериализации в Rust. Загрузка таблицы ускорилась (2.16 → 0.87 с), но это случайная вариативность — рендеринг таблицы не использует computeCardsState.

5. Батчевый computeCardsState (вторая версия, без двойной сериализации)

Та же идея, но в Rust выделена внутренняя функция compute_card_state_inner, работающая с уже распарсенными CardData и FsrsParameters. compute_cards_state парсит массив один раз и вызывает inner для каждой карточки без промежуточной сериализации.

Результат: Сканирование 3.30-3.32 с — без изменений относительно baseline (3.3 с). Bottleneck не в FFI/сериализации, а в 105k итерациях цикла по файлам (проверка metadataCache).

6. Чтение metadataCache из внутреннего Map вместо getFileCache

Вместо 105k вызовов app.metadataCache.getFileCache(file) — читать из (app.metadataCache as any).metadataCache (внутренний Map<string, CachedMetadata>).

Результат: Первая попытка — каст в Map (сломало). Вторая — каст в Record, доступ по [file.path] (105k без frontmatter). Третья — Object.entries, фильтр .md, итерация по entries (0 карточек). Внутренняя структура metadataCache не соответствует публичному API getFileCache. Откачено.

Вывод: Оптимизация через обход внутренней структуры metadataCache ненадёжна — формат ключей/значений может отличаться в разных версиях Obsidian.

7. Сохранение кэша карточек между запусками (cache.json)

Идея: после первого полного сканирования сохранять кэш (cache.json, ~8.5 МБ) и при следующих запусках загружать из него, избегая повторного обхода 105k файлов.

Реализация:

  • При первом запросе таблицы — полное сканирование + сохранение cache.json
  • При повторном запуске — загрузка из кэша (0.83 с вместо 3.30 с)
  • params_hash — при изменении параметров FSRS кэш сбрасывается
  • mtime — для отслеживания устаревших записей
  • Ленивая загрузка: кэш читается только при открытии fsrs-table
  • Debounce-сохранение (2 с) при изменении файлов

Результат: Таблица доступна мгновенно при повторных запусках. Минификация JSON могла бы снизить размер до ~1.52 МБ.

Почему отклонено:

  • Создаёт сложность и неопределённость (устаревание, инвалидация, гонки)
  • Вероятны сбои при частых обновлениях плагина (структура кэша может меняться)
  • При умеренном количестве карточек (< 7000) накладные расходы на запись/чтение ощутимы, а выигрыш незначителен
  • Эффект проявляется только на очень больших хранилищах

Вывод: Метод не однозначный. Может иметь смысл только при значительном (> 7000) количестве карточек. А может и не иметь. ОТКЛОНЕНО.

8. Unix-таймстемп вместо ISO 8601

Замена строковых дат ("2026-01-15T10:30:00Z") на целые числа (1736935800) в reviews[].date и due. Убирает parse_datetime_flexible (парсинг 6 форматов), заменяет на DateTime::from_timestamp. Сокращает длину JSON-строк.

Результат: Сканирование 3.30 → 3.37 с (в пределах погрешности). Парсинг дат не был узким местом — основные затраты в обходе metadataCache и setTimeout между пачками. НЕ ПРИНЯТО.

9. opt-level = 3 (скорость) против opt-level = "s" (размер)

Сравнение после замены regex на regex-lite, с wasm-opt -Oz, на 100 карточках с WHERE ~, ORDER BY, LIMIT.

opt-level WASM query (ms) queryCount (ms)
"s" (размер) 612 KB 0.696 0.632
3 (скорость) 784 KB 0.688 0.631
Δ +28% -1.2% -0.2%

Результат: Разница в скорости в пределах погрешности (~1%), а размер +172 KB. Оставлен opt-level = "s".