Meridian Office

Сквозные алгоритмы ядра Meridian Kernel

Алгоритмы, общие для всех приложений: структуры текста, стили, идентификаторы, журнал операций, CRDT, рендер, файлы, поиск, буфер обмена, печать, планировщик, память, детерминизм. Для каждого — псевдокод или Rust, сложность, тесты. Привязка к крейтам — 03-architecture.md §3. Алгоритмы отдельных приложений (раскладка страниц Write, пересчёт Sheets, анимации Slides) — в docs/apps/<app>.md. Версия 0.1 · 2026-10-03.

Обозначения: n — длина текста в байтах, p — число кусков piece table, k — число спанов атрибутов, m — число узлов документа, s — число стилей, c — число команд списка отображения.


1. Структура текста

1.1 Выбор: piece table с индексом-B-деревом

Вариант Вставка/удаление Чтение диапазона Память Undo/CRDT Вердикт
String + копирование O(n) O(1) 1× дорого нет
Rope (дерево строк, как ropey) O(log n) O(log n + len) 1×–1,5×, фрагментация хорошо хорошо, но буфер оригинала переписывается кусками при загрузке
Piece table, список кусков O(p) поиск + O(1) правка O(p + len) 1× + 24 Б/кусок отлично: оригинал неизменяем, куски = история p растёт при длинном редактировании
Piece table + B-дерево кусков (выбрано) O(log p) O(log p + len) 1× + ~40 Б/кусок отлично лучшее сочетание

Доводы: буфер оригинала (original) — это ровно текст из файла (часто просто срез mmap-буфера, без копии), он неизменяем — безопасен для фоновых снимков; add-буфер только растёт — тоже безопасен для чтения из других потоков; все правки — правки списка кусков, малого и дешёвого для клонирования в снимок (§5 архитектуры). Piece table естественно ложится на CRDT: Yrs тоже хранит текст как цепочку неизменяемых блоков. Реализация в kernel/crates/meridian-text/src/piece_table.rs — линейный список кусков (достаточно для абзацев: p редко > 50); индекс-B-дерево включается, когда p > 64, прозрачно для API.

1.2 Индекс кусков

/// B-дерево порядка 16 над кусками; внутренние узлы хранят суммарные длины поддеревьев
/// (байты и символы), что даёт поиск по байтовому и символьному смещению за O(log p).
/// B-tree of order 16 over pieces; inner nodes keep subtree byte and char totals.
struct Inner { keys: ArrayVec<Summary, 16>, children: ArrayVec<NodeRef, 16> }
struct Leaf  { pieces: ArrayVec<Piece, 32>, summary: Summary }
#[derive(Clone, Copy, Default)]
struct Summary { bytes: u32, chars: u32, newlines: u32 }   // newlines — для быстрых строк/абзацев

fn locate(root: &Node, mut byte: u32) -> (LeafRef, usize /*piece idx*/, u32 /*offset*/) {
    let mut node = root;
    loop {
        match node {
            Node::Inner(inner) => {
                // спуск по накопленным суммам / descend by cumulative sums
                let mut i = 0;
                while i + 1 < inner.keys.len() && byte >= inner.keys[i].bytes { byte -= inner.keys[i].bytes; i += 1; }
                node = &inner.children[i];
            }
            Node::Leaf(leaf) => {
                let mut i = 0;
                while i + 1 < leaf.pieces.len() && byte >= leaf.pieces[i].len { byte -= leaf.pieces[i].len; i += 1; }
                return (leaf.as_ref(), i, byte);
            }
        }
    }
}

Вставка: locate → разрез куска (0–2 новых куска) → вставка в лист → при переполнении листа (32) — split и подъём ключа; суммы обновляются вверх по пути (O(log p)). Удаление диапазона: два locate, обрезка крайних кусков, удаление целого поддиапазона листов/узлов за O(log p + удалённые куски), слияние узлов ниже 1/4 заполнения. Последовательный набор сливается с последним куском add-буфера (как в линейной версии).

Сложность: insert/delete — O(log p); byte_to_char/char_to_byte — O(log p) по Summary.chars; итерация диапазона — O(log p + число кусков в диапазоне); снимок (клон дерева) — O(1) с персистентными узлами (Arc, copy-on-write при правке по пути: O(log p) аллокаций на правку).

Компактация: при p > 4·(n / 4096) (слишком много мелких кусков) фоновая задача переписывает текст в новый original (единый кусок) — делается только в точке покоя (нет активного набора 2 с) и фиксируется как Op::Compact, невидимый для undo.

1.3 Атрибутные спаны и их слияние

Инварианты (meridian-text::AttrSpans): спаны покрывают [0, len) без дыр, отсортированы, непустые, соседние с равными Attrs слиты. Операции:

set(range, key, value):
  i = split_at(range.start)      # гарантирует границу; O(log k) поиск + O(k) сдвиг в Vec
  j = split_at(range.end)
  for span in spans[i..j]: span.attrs[key] = value
  normalize(i-1 ..= j)           # слить только затронутую окрестность / merge the touched neighbourhood only
on_insert(at, len):              # новые символы наследуют атрибуты слева (в начале — справа)
  span = spans[partition_point(end < at)] ; span.end += len ; сдвинуть последующие на len
on_delete(range):
  map(p) = p если p <= start; start если start < p < end; p - removed иначе
  для каждого спана: (start, end) = (map(start), map(end)) ; удалить пустые ; слить равных соседей

Представление спанов — Vec<AttrSpan>: для абзацев k мало (десятки), а линейная память и кэш-локальность важнее O(log k) обновлений. Для абзацев с k > 256 (сгенерированные документы с посимвольным форматированием) включается то же B-дерево с суммами, что у кусков. Attrs — BTreeMap<AttrKey, AttrValue>: детерминированный порядок (нужен для хэша резолвера §2 и для CBOR-сериализации), сравнение за O(|attrs|).

Слияние при вставке форматированного фрагмента (paste): спаны фрагмента сдвигаются на at, вставляются, затем normalize на окрестности; для копирования форматирования между абзацами с разными стилями применяется «приведение к явному»: атрибуты, которые во фрагменте наследовались от стиля-источника, материализуются прямыми (resolve(source) − resolve(target)), чтобы вид сохранился — O(k · |attrs|).

1.4 Тесты


2. Резолвер стилей с кэшем

2.1 Цепочка наследования

Для узла N вычисленное форматирование R(N):

R(N) = merge(
  defaults(kind),                                   # встроенные значения по типу узла
  chain(style(N)) ,                                 # basedOn-цепочка от корня к стилю узла, глубина ≤ 32
  conditional(table_style, N.position),             # условное форматирование таблиц (первая строка, полосы)
  inherited(parent(N)),                             # для символов: абзацные свойства «по умолчанию для runs»
  N.attrs,                                          # прямое форматирование
)

merge — поатрибутное наложение: позже — сильнее; toggle-свойства (bold/italic в OOXML) — XOR по цепочке стилей символов (как в Word), что реализовано отдельным проходом apply_toggle. Тема (Theme) резолвится последней: ссылки на цвета/шрифты темы (theme:accent1, +mn-lt) заменяются значениями.

2.2 Кэш

#[derive(Hash, Eq, PartialEq)]
struct ResolveKey { style_chain: u64, attrs: u64, context: u32, sheet_version: u64 }

fn resolve(&mut self, node: &Node, ctx: &ResolveContext) -> Arc<ResolvedStyle> {
    let key = ResolveKey {
        style_chain: self.chain_hash(node.style),        // кэшируется по StyleId: O(1) после первого вызова
        attrs: hash_attrs(&node.attrs),                   // BTreeMap → детерминированный порядок → стабильный хэш
        context: ctx.bits(),                              // позиция в таблице, уровень списка, секция
        sheet_version: self.sheet.version,
    };
    if let Some(r) = self.cache.get(&key) { return r.clone(); }
    let r = Arc::new(self.compute(node, ctx));
    self.cache.insert(key, r.clone());                    // LRU 10 000 записей на документ
    r
}

chain_hash(style) = хэш последовательности (StyleId, style.version) по цепочке basedOn — пересчитывается при изменении любого стиля в цепочке (style.version++ и инвалидация потомков через обратный индекс based_on_children: HashMap<StyleId, Vec<StyleId>>). Так как большинство абзацев документа имеют пустые attrs и один из ~10 стилей, попадание в кэш > 99 % — резолв одного абзаца амортизированно O(1), холодный — O(d · a), d — глубина цепочки, a — число атрибутов.

Потокобезопасность: кэш — на поток раскладки (без блокировок); результаты — Arc<ResolvedStyle>, разделяются между потоками. Инвалидация по sheet.version делает старые ключи недостижимыми, LRU их вытесняет.

2.3 Тесты


3. Адресация узлов и стабильные ID

3.1 UUIDv7 и монотонность

Генератор (meridian-core::id): 48 бит мс + 12 бит счётчика + 62 бита xorshift64*; при одной миллисекунде счётчик растёт, при переполнении (4096) время сдвигается на 1 мс вперёд; при уходе часов назад используется последнее время. Гарантии: строгая монотонность в процессе, уникальность между процессами с вероятностью коллизии < 2⁻⁶² на пару при одинаковой мс. Сложность O(1), блокировка — Mutex на ~30 нс.

3.2 Детерминированные ID при импорте

import_id(path, content) = v7_from_parts(
    unix_ms = IMPORT_EPOCH_MS,                        # константа 2026-01-01: все импортные ID в прошлом → сортируются раньше новых
    seq     = blake3(path)[0..12 бит],
    rand    = blake3(path ‖ content_hash)[0..62 бит])

path — структурный путь в исходном формате (word/document.xml#/w:body/w:p[17]), content_hash — хэш содержимого узла без детей. Повторный импорт того же файла даёт те же ID (сравнение версий, повторное открытие после сбоя без истории); два одинаковых абзаца различаются путём. Коллизии в пределах документа проверяются при импорте (HashSet), при коллизии — к rand прибавляется индекс.

3.3 Пути и разрешение

NodePath = Vec<(NodeId, u32)>; resolve(path): спуск от корня по индексам с проверкой children[i] == id (O(глубина)); при несовпадении (документ изменился) — поиск id по хэш-карте (O(1)) и перестроение пути. Текстовые позиции TextPos { node, byte } переживают правки через StickyIndex Yrs (§5.4) либо, вне CRDT, через подписку на TextInsert/TextDelete того же абзаца (O(1) обновление на операцию, список подписчиков — курсоры, закладки, комментарии, результаты поиска).

3.4 Тесты

Монотонность 5 000 ID (есть), переполнение счётчика (есть), детерминизм импорта (два прогона → равные ID), разрешение пути после перемещения узла, миграция TextPos через 10⁴ случайных правок (позиция указывает на тот же символ).


4. Журнал операций и группировка undo

4.1 Применение транзакции

apply(doc, tx):
  inverse = []
  for op in tx.ops:                      # по порядку / in order
    inv = doc.apply_op(op)               # возвращает обратную операцию с «старыми» значениями
    inverse.push(inv)
  tx.inverse = reverse(inverse)          # обратный порядок для отката
  doc.version += 1
  emit(ChangeSet::from(&tx))             # для раскладки/UI: множество NodeId + диапазоны
  undo.push(tx) ; oplog.append(tx) ; crdt.apply(tx)

Каждая Op обратима локально: InsertNode ↔︎ RemoveNode(с сохранённым поддеревом), SetAttr(old), TextInsert ↔︎ TextDelete(с текстом), CellSet(old), MoveNode(old parent, old index). Сложность применения — сумма сложностей операций; построение обратной — O(размер удаляемых данных) (копия текста/поддерева).

4.2 Группировка undo

Группа = последовательность транзакций, отменяемая одним действием. Правила объединения новой tx с верхней группой:

Условие Объединять
tx.origin = Remote/System никогда; закрыть текущую группу
та же команда с флагом coalesce (ввод символа, Backspace, Delete, перетаскивание) да
ввод символа: позиция = конец предыдущей вставки, прошло < 500 мс, символ не пробел после буквы да (слово — группа; пробел начинает новую после слова)
удаление: позиция смежна с предыдущим удалением в ту же сторону, < 500 мс да
явная граница (UndoManager::barrier() от команды) нет
размер группы > 10 000 операций нет

Отмена: pop группу → применить inverse всех её транзакций в обратном порядке как одну tx с Origin::Undo → группа уходит в redo-стек; любая новая локальная tx очищает redo. Курсор восстанавливается из cursor_before первой транзакции группы. Сложность отмены — O(число операций группы).

4.3 Коалесценция в журнале

OpLog хранит транзакции как есть, но для автосохранения (§10) последовательные TextInsert в один абзац подряд сливаются в один кадр (TextInsert{at, "abc"}), что сокращает журнал в ~10 раз при наборе.

4.4 Тесты

Свойство «undo ∘ do = id» и «redo ∘ undo ∘ do = do» на случайных последовательностях операций (proptest, сравнение сериализованной модели); группировка набора слов ("Hello world" → 2 группы); удалённая транзакция закрывает группу; redo очищается новой правкой; восстановление курсора.


5. CRDT на Yrs

5.1 Представление

Yrs (YATA): каждый вставленный элемент — Item { id: (client, clock), left, right, origin, right_origin, content, deleted }; текст — цепочка Item с содержимым-строками (сливаются при последовательном наборе одним клиентом), карты — Item на ключ (последний по (clock, client) виден), массивы — цепочки. Состояние документа описывает state vector SV = {client → clock} — максимальный виденный clock каждого клиента; удаления — delete set DS = {client → интервалы clock}.

5.2 Слияние

Обновление U (lib0 v2) содержит элементы и DS. Интеграция элемента: по origin/right_origin найти позицию среди уже существующих соседей; конфликт одновременных вставок в одну позицию решается правилом YATA — порядок по client среди элементов с одинаковым origin (детерминированно у всех участников). Элементы, зависимости которых ещё не получены (clock предшественника > SV[client]), откладываются в pending до прихода недостающих обновлений. Сложность интеграции элемента — O(число конкурирующих соседей), обычно O(1); обмен состояниями — SyncStep1(SV) → SyncStep2(diff(U, SV)) за O(|diff|).

Применение к модели: события Yrs (observe_deep) дают дельты (insert/delete/retain для текста, set/remove для карт, вставки/удаления элементов для массивов) → транслятор строит Op с Origin::Remote → apply (§4) без записи в undo (кроме закрытия группы).

5.3 Сжатие истории

5.4 Снимки и версии

Snapshot = (SV, DS) — O(число клиентов + число интервалов удалений). Восстановление состояния на момент снимка: Doc::from_snapshot проходит элементы и считает видимыми те, у которых clock < SV[client] и которые не в DS снимка (требует несобранных GC элементов до этой точки, поэтому GC ограничен самым старым сохранённым снимком). Дифф двух версий: прогон текста обеих и Myers-дифф по абзацам (O((N+M)·D), D — размер изменений), внутри изменённых абзацев — посимвольный Myers с ограничением 10 000 символов (дальше — «абзац заменён»); дерево узлов — сравнение по NodeId (O(m)) с классификацией «добавлен/удалён/перемещён/изменён».

5.5 Тесты


6. Список отображения и инвалидация

6.1 Построение

build_page_dl(page):
  dl = DisplayList::new()
  for block in page.blocks:                         # абзацы, таблицы, фигуры в порядке z-index
    if let Some(cached) = block_dl_cache.get(block.id, block.layout_version):
      dl.append_translated(cached, block.origin)     # копия команд со сдвигом, O(c_block)
    else:
      sub = render_block(block)                      # глифы, линии, заливки; O(размер блока)
      block_dl_cache.insert(block.id, block.layout_version, sub.clone())
      dl.append_translated(sub, block.origin)
    dl.index.insert(block.bbox, cmd_range)           # R-tree по блокам, не по командам
  return dl

append_translated не трансформирует координаты по одной: добавляет Transform(translate) + команды + Restore; глифы хранятся в одном плотном Vec<Glyph> (SoA: glyph_ids: Vec<u16>, positions: Vec<[f32;2]>). R-tree (rstar) — bulk-load за O(b log b) по b блокам страницы, запрос видимой области — O(log b + ответ).

6.2 Распространение «грязи»

on_changeset(cs):                                   # из apply() §4
  for node in cs.nodes: mark_dirty(block_of(node))  # абзац/таблица/фигура; O(1) через индекс node → block
  schedule(layout_pass, priority = Visible if any dirty block visible else Background)

layout_pass():
  first = min_dirty_block_by_flow_order()
  pos = first.flow_position_before
  for block in flow.from(first):
    new_layout = layout(block, pos)                  # пересчёт только изменённых; остальные — сдвиг
    dirty_rect |= old_bbox(block) ∪ new_bbox(block)
    pos = new_layout.end
    if !block.dirty && new_layout.start == old_start(block): break   # ранний выход: дальше ничего не сдвинулось
  invalidate_tiles(dirty_rect)

Ранний выход — ключевое свойство: вставка символа в абзац, не меняющая число строк, стоит O(абзац); изменение числа строк — O(до конца страницы или до первого блока, чья позиция совпала); вставка страницы — O(остаток документа), но в фоне и с приоритетом видимого (§17).

6.3 Сложность и тесты

Построение DL страницы при горячем кэше блоков — O(c) копирования; хит-тест точки — O(log b) по R-tree + O(строк абзаца) внутри блока. Тесты: инкрементальная раскладка даёт тот же DL, что полная (.dl.txt равны) на корпусе после случайных правок; dirty_rect покрывает все изменившиеся пиксели (сравнение растров до/после по маске); ранний выход срабатывает на вставке символа в середине 100-страничного документа (< 2 мс).


7. Тайловый кэш

struct TileKey { page: u32, tx: u16, ty: u16, scale_q: u16 /* масштаб·64 */, theme: u8 }
struct TileCache { map: HashMap<TileKey, TileSlot>, lru: LinkedList<TileKey>, bytes: usize, limit: usize }

fn get_or_schedule(&mut self, key: TileKey, dl: &DisplayList) -> TileState {
    if let Some(slot) = self.map.get_mut(&key) { self.lru.touch(key); return slot.state(); }   // O(1)
    self.scheduler.submit(RasterJob { key, cmds: dl.index.query(key.rect()), priority: Visible });
    TileState::Pending(fallback: self.nearest_scale(key))   // ближайший масштаб для немедленного показа
}
fn invalidate(&mut self, page: u32, rect: RectPx) {       // из §6.2
    for key in self.keys_intersecting(page, rect) { self.map.remove(&key); }   // через per-page сетку, O(затронутых тайлов)
}
fn insert(&mut self, key: TileKey, tex: Texture) {
    while self.bytes + tex.bytes() > self.limit { let k = self.lru.pop_oldest(); self.bytes -= self.map.remove(&k).bytes(); }
    self.bytes += tex.bytes(); self.map.insert(key, TileSlot::Ready(tex)); self.lru.push(key);
}

Предвыборка: после кадра планируются тайлы на 1 экран по направлению прокрутки с приоритетом Background; при смене направления очередь очищается (cancel_pending). Квантование масштаба: scale_q = round(scale·64); во время жеста зума рисуются тайлы ближайшего готового масштаба с билинейной интерполяцией, точные тайлы запрашиваются после 80 мс покоя. Память тайла — 256·256·4 = 256 КБ; лимит 512 МБ = 2 048 тайлов ≈ 16 экранов 4K.

Тесты: LRU вытесняет самые старые; инвалидация прямоугольника удаляет ровно пересекающие тайлы; после invalidate + get тайл перерисован (хэш пикселей изменился); бенч: прокрутка 100 страниц без кэш-промахов на видимых тайлах при включённой предвыборке.


8. Детект формата файла

Расширение — только подсказка; решает содержимое. Порядок: магические байты → контейнер → содержимое → текстовые эвристики → расширение как tie-breaker.

detect(bytes: &[u8; ≤ 64 KiB], ext: Option<&str>) -> FormatId:
  match bytes:
    "PK\x03\x04" | "PK\x05\x06" → zip_detect(bytes)
    "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" → cfb_detect()         # doc/xls/ppt/msg/vsd: по именам потоков
    "%PDF-" (в первых 1024 байтах) → Pdf
    "{\\rtf" → Rtf
    "\x89PNG\r\n\x1A\n" → Png ; "\xFF\xD8\xFF" → Jpeg ; "RIFF....WEBP" → WebP ; "GIF8" → Gif
    "BM" → Bmp ; "II*\0" | "MM\0*" → Tiff ; "\0\0\x01\0" → Ico ; "8BPS" → Psd ; "....ftypheic|avif" → Heic/Avif
    "<?xml" | "<" с корнем:
        "office:document" → ODF flat ; "urn:meridian:office:<app>" → MOP flat ; "pkg:package" → OOXML flat (чтение)
        "svg" → Svg ; "math" → MathML ; "FictionBook" → Fb2 ; "html"|"HTML"|"!DOCTYPE html" → Html
    "From " | "Return-Path:" | "Received:" → Mbox/Eml (Eml, если нет "From " в начале)
    "BEGIN:VCALENDAR" → Ics ; "BEGIN:VCARD" → Vcf
    "\x00\x01\x00\x00Standard Jet DB" | "Standard ACE DB" → Mdb/Accdb
    "SQLite format 3\0" → Sqlite (→ Mbx, если есть таблица meridian_manifest)
    "!<arch>" | "\x1F\x8B" → не документ
    иначе → text_detect(bytes, ext)

zip_detect(bytes):                                  # читаем центральный каталог первых 64 КБ или весь файл по требованию
  if первая запись == "mimetype" (stored): → по значению ("application/vnd.meridian.write" → Mwx; "application/vnd.oasis.opendocument.text" → Odt; …)
  if есть "[Content_Types].xml": по частям "word/document.xml" → Docx (macro → Docm по "vbaProject.bin"; шаблон по ContentType),
     "xl/workbook.xml" → Xlsx/Xlsm/Xltx ; "ppt/presentation.xml" → Pptx ; "visio/document.xml" → Vsdx
  if есть "META-INF/manifest.xml" без mimetype → ODF по корню content.xml
  if есть "Index/Document.iwa" | "Index/" + "Metadata/" → iWork (Pages/Numbers/Keynote по BuildVersionHistory.plist)
  if есть "mimetype" == "image/openraster" → Ora ; "META-INF/container.xml" → Epub ; "Document.xml"+"Index" → Key
  иначе → Zip (не документ)

text_detect(bytes, ext):
  decode: BOM → UTF-8/16; иначе UTF-8 valid? → UTF-8; иначе по статистике байтов → cp1251/koi8-r/cp866 (для ru), latin1
  if начинается с "---\n" или много "# " / "**" / "](": → Markdown (score ≥ 3)
  delimiter sniff (первые 20 строк): кандидаты [",", ";", "\t", "|"]; выбрать тот, у которого число вхождений
     одинаково в ≥ 80 % строк и > 0 → Csv/Tsv (Csv с разделителем в метаданных)
  if ext ∈ {tex} и есть "\documentclass" | "\begin{document}" → Tex
  иначе → PlainText

Сложность O(64 КБ) на файл; для ZIP — чтение EOCD с конца (§9) и имён каталога (O(число записей)). Тесты: таблица «байты → формат» (100 кейсов, включая обманчивые расширения .docx с RTF внутри), все файлы корпуса детектируются как их format в .toml, CSV-сниффер на 50 образцах с разными разделителями и кавычками.


9. Потоковый ZIP

open(file):
  size = file.len(); tail = read(file, max(0, size-66_000)..size)      # EOCD 22 Б + комментарий ≤ 65 535
  eocd = rfind(tail, "PK\x05\x06") else Err(Corrupt)
  if eocd.entries == 0xFFFF || eocd.cd_offset == 0xFFFF_FFFF:         # ZIP64
      locator = find(tail, "PK\x06\x07") ; eocd64 = read_at(locator.eocd64_offset)
  cd = read(file, cd_offset..cd_offset+cd_size)                        # центральный каталог целиком (≤ 100 000 записей ⇒ ≤ ~10 МБ)
  entries = parse_cd(cd) → Vec<Entry{name, method, crc, csize, usize, local_offset, flags}>
  проверить: имена без "..", без абсолютных путей, без дубликатов; сумма usize ≤ 4 ГБ; usize/csize ≤ 200 (zip-бомба)
  index = HashMap<name, idx>

read_entry(e) -> impl Read:
  local = read_at(e.local_offset, 30) ; data_start = local_offset + 30 + local.name_len + local.extra_len
  match e.method: 0 (stored) → CrcReader(Slice(mmap[data_start..+csize]))  # без копии
                  8 (deflate) → CrcReader(Inflate(Slice(…)))               # потоково, буфер 64 КБ
                  иначе → Err(Unsupported)
  CrcReader на EOF сравнивает crc32 с e.crc → Err(Corrupt) при расхождении

Для флага 3 (data descriptor: размеры после данных) используем значения из центрального каталога — они всегда есть. Шифрованные записи (ZipCrypto/AES) → Unsupported с подсказкой (документные шифрования — на уровне формата, §8.6 архитектуры). Запись: ZipWriter::start_entry(name, method) → Write → finish_entry() считает CRC и размеры, пишет local header заранее с флагом 3 и data descriptor после данных (потоково, без seek) или, если цель seekable, патчит заголовок (без descriptor — лучше для старых читателей). mimetype — первой, stored, без extra. Сложность: открытие O(E) по числу записей, чтение записи O(размер), память — каталог + буфер 64 КБ + mmap.

Тесты: round-trip 1 000 случайных пакетов (proptest: имена, размеры 0..10 МБ, методы); ZIP64 с записью 5 ГБ (ночной); обрезанный файл → Corrupt без паники; zip-бомба 42.zip → отказ по лимиту; совместимость — пакеты читаются unzip, 7z, Word (для docx).


10. Журнал автосохранения

Формат journal.bin: заголовок MRDJ + u16 version + u128 doc_id + u64 base_seq (seq снимка), далее кадры:

frame := u32 len | u32 crc32c(payload) | payload
payload := CBOR { seq: u64, ts_ms: u64, tx_id: uuid, origin: u8, ops: [Op…] }   # ops — коалесцированные (§4.3)
writer (поток автосохранения):
  on_transaction(tx): queue.push(tx); timer.arm(2 s, coalesce)       # debounce, но не дольше 10 с от первой правки
  on_timer(): batch = queue.drain(); frame = encode(coalesce(batch)); file.write_all(frame); file.sync_data()
  if frames_since_snapshot ≥ 1000 || bytes ≥ 16 MiB || elapsed ≥ 5 min: schedule(snapshot)
snapshot():                                                           # по снимку модели (§4.5 архитектуры), фон
  write MOP → snapshot.mop.tmp ; fsync ; rename → snapshot.mop ; journal = new(base_seq = last_seq) ; remove old journal

recover(dir):
  doc = open(snapshot.mop) or empty
  for frame in journal: if crc_ok(frame) && frame.seq == expected: apply(frame.ops); expected += 1 else break
  report(recovered_through = expected-1, lost = tail present && !crc_ok)

Ошибки диска (ENOSPC, EIO) → автосохранение отключается с баннером «не удаётся сохранить резервную копию»; журнал на другой FS не делается. Шифрование кадров (ChaCha20-Poly1305 с nonce = seq) — если документ защищён паролем. Стоимость кадра — O(размер операций), fsync раз в 2 с; при наборе — ~200 Б/с.

Тесты: обрезание журнала в каждой байтовой позиции последнего кадра → восстановление до предыдущего кадра без паники; порча байта в середине → восстановление до кадра перед ним, остальные игнорируются (не применяются «через дыру»); снимок + журнал = полное состояние (хэш модели); потеря питания имитируется O_DIRECT-тестом на CI Linux.


11. Хэш-отпечаток устройства

Только алгоритм клиента; сервер — в 10-licensing-accounts.md.

Компоненты (каждый нормализуется: trim, lowercase, пустое → отсутствует):

# Компонент Windows macOS Linux
1 идентификатор установки ОС HKLM\…\Cryptography\MachineGuid IOPlatformUUID /etc/machine-id
2 серийный номер платы/системы WMI Win32_BaseBoard.SerialNumber IOPlatformSerialNumber /sys/class/dmi/id/board_serial (часто недоступен без root → пропуск)
3 серийный номер системного диска IOCTL_STORAGE_QUERY_PROPERTY diskutil/IOKit Serial Number /sys/block/<root>/device/serial или /dev/disk/by-id
4 модель CPU + число ядер cpuid/WMI sysctl machdep.cpu.brand_string /proc/cpuinfo
5 MAC первого проводного/встроенного адаптера (исключая виртуальные/USB) GetAdaptersAddresses IOKit en0 /sys/class/net/*/address, type == 1, не virtual

Персональных данных нет (имена пользователей/хостов не используются). Отпечаток:

component_hash_i = sha256(salt_install ‖ "meridian-fp-v1" ‖ i ‖ normalized_value_i)   # 32 Б каждый; salt_install — 16 случайных байт установки (P6 в 10-licensing-accounts.md)
fingerprint = { v: 1, parts: [Option<[u8;32]>; 5] }                                  # хранится в лицензии и запросе активации
device_id  = sha256(concat(parts_present_sorted_by_index))[0..16]                     # короткий id для UI/портала (`fingerprint_hash` в .mlic)
match(stored, current) = count(i : stored[i] == current[i], оба присутствуют) ≥ 3 && stored[1] == current[1] или stored[2] == current[2]

Правило совпадения терпит замену диска или сетевой карты (пользователь меняет железо), но требует совпадения ОС-идентификатора или платы; смена ≥ 3 компонентов → повторная активация (онлайн или через портал). Виртуальные машины (детект по Win32_ComputerSystem.Model/DMI «VMware/VirtualBox/QEMU») — помечаются vm = true; политика лицензий для ВМ — серверная. Сложность: 5 системных вызовов при старте (< 20 мс), кэш на сессию.

Тесты: нормализация (регистр, пробелы); совпадение при замене одного и двух компонентов; несовпадение при трёх; стабильность между запусками (мок-источники); отсутствующие компоненты не ломают сравнение; device_id детерминирован и не раскрывает компоненты (нет обратимости — только хэши).


12. Полнотекстовый индекс (tantivy) и подсветка

Индекс Hub (недавние и папки пользователя), Notes (книжки), Mail (письма) — один движок, разные индексы (<data>/index/<scope>/).

schema: doc_id (STRING, stored, fast) | path (STRING, stored) | title (TEXT, stored) | body (TEXT) |
        app (STRING, fast) | modified (DATE, fast) | tags (TEXT) | lang (STRING)
tokenizer "meridian": SimpleTokenizer → RemoveLong(40) → LowerCaser → AsciiFolding/Unicode NFKC-fold →
        Stemmer(по lang: ru, en, de, fr, es, …; snowball) ; для CJK — ngram(2..3)
indexing: наблюдатель FS (notify) → очередь (debounce 1 с) → meridian-convert --text (в песочнице, лимит 2 МБ текста) →
        IndexWriter.add_document ; удаления — по doc_id ; commit раз в 5 с или 100 документов ; merge policy LogMerge
query: QueryParser по полям [title^3, body, tags^2] ; фразы "…" ; фильтры app:, modified:[..] ; опечатки — FuzzyTermQuery(1) при 0 результатов
snippet: SnippetGenerator(body, max 160 символов) → фрагмент с <b>…</b> по позициям токенов

Подсветка в открытом документе использует не tantivy, а поиск §13 по модели (точные байтовые позиции → TextPos → раскладка → прямоугольники строк → слой оверлеев). Сложность: индексация O(размер текста), запрос O(log N + результаты); 10 000 документов (~2 ГБ текста) → индекс ~400 МБ, запрос < 50 мс.

Тесты: русская морфология («документы» находит «документ»), фразовый поиск, фильтр по приложению, удаление файла убирает его из результатов, переиндексация после изменения, лимит текста не ломает индекс.


13. Поиск с нормализацией Unicode

Задача: найти вхождения запроса в тексте абзаца с опциями «учитывать регистр», «учитывать диакритику», «слово целиком», «регулярное выражение», «похожие формы» (ё/е, кавычки, дефисы) и вернуть байтовые позиции в исходном тексте.

normalize(text) -> (norm: String, map: Vec<u32>):          # map[i] = байтовое смещение в исходнике для norm[i]
  for (orig_off, ch) in text.char_indices():
    for nch in nfd(ch).filter(not combining if ignore_diacritics).map(casefold if ignore_case).map(unify):
        # unify: ё→е, "«»„“”"→'"', "‐‑–—"→'-', NBSP→' ', fi→fi (NFKC для совместимых)
        push(nch) ; map.push(orig_off)     # все байты nch указывают на начало исходного символа
  map.push(text.len())

find_all(text, query, opts):
  (hay, map) = normalize(text) ; needle = normalize(query).0
  matcher = if opts.regex { Regex::new(with_flags(needle)) } else { memchr::memmem::Finder(needle) }   # Two-Way, O(n+m)
  for (s, e) in matcher.find_iter(hay):
    if opts.whole_word && !(is_boundary(hay, s) && is_boundary(hay, e)): continue   # UAX#29 word boundaries
    yield (map[s], map_end(e))             # map_end: смещение конца исходного символа, покрывающего hay[e-1]

Совпадения, начинающиеся или заканчивающиеся внутри одного исходного символа (например, fi → fi, запрос «f»), расширяются до границ исходного символа. Замена: replace(range, text) через транзакции §4 с корректировкой последующих позиций (StickyIndex/подписка §3.3) — замена всех N вхождений в абзаце за O(n + N). Поиск по документу — обход абзацев в порядке потока, лениво (итератор), с кэшем нормализации на абзац (инвалидируется по layout_version). Для регулярных выражений — крейт regex (линейное время, без катастрофического отката), размер скомпилированного автомата ограничен 10 МБ.

Тесты: «Résumé» находится по «resume» без диакритики; «ёлка» по «елка»; fi по «fi» с корректными границами; регистр: «Σ» и «ς/σ» при casefold; слово целиком не находит «кот» в «кота»; позиции указывают на исходные байты (слайс исходника == исходный текст совпадения); 10 МБ текста — поиск < 50 мс.


14. Буфер обмена

14.1 Форматы

Формат Идентификатор (Windows / macOS / Linux) Копирование Вставка
Фрагмент MOP (свой) Meridian.Fragment / ru.meridian-office.fragment / application/vnd.meridian.fragment CBOR-поддерево узлов + стили + медиа (≤ 50 МБ, иначе ссылка на временный файл) приоритет 1 между своими приложениями
Фрагмент OOXML HTML Format? нет — Office Clipboard недоступен; используем Rich Text Format и свой Meridian.Ooxml (пакет .docx/.xlsx в памяти) docx/xlsx-фрагмент (как делает Word: «Chunk» пакет) приоритет 2; Word/Excel вставляют через RTF/HTML
RTF Rich Text Format / public.rtf / text/rtf генерируется лениво (delayed rendering) приоритет 3
HTML HTML Format (с заголовком StartHTML…) / public.html / text/html лениво; инлайн-стили, изображения как data: ≤ 1 МБ приоритет 4
Текст CF_UNICODETEXT / public.utf8-plain-text / text/plain;charset=utf-8 сразу приоритет 5
Изображение PNG, CF_DIBV5 / public.png, public.tiff / image/png PNG + DIB для растров: PNG > TIFF > DIB > JPEG
Файлы CF_HDROP / public.file-url / text/uri-list — вставка как вложение/картинка/открытие по типу
Ячейки (Sheets) свой фрагмент + Csv/text/csv + TSV в text/plain TSV как текст TSV с эвристикой типов
Формулы (Math) MathML / public.mathml / application/mathml+xml + LaTeX в тексте

14.2 Алгоритмы

copy(selection):
  frag = model.extract(selection)                       # поддерево с материализованными стилями, O(размер)
  offer(Meridian.Fragment → encode_cbor(frag))          # сразу
  offer(text/plain → frag.to_plain_text())              # сразу (дёшево)
  offer_delayed(RTF, HTML, OOXML, PNG(для фигур))        # по запросу системы: render через кодеки в фоне (§17)
  hub.clipboard_history.push(summary(frag))             # Hub хранит до 25 последних (Pro+), с превью

paste(target):
  avail = system.formats()
  for fmt in priority_list(target.kind):                # Write: MOP > OOXML > RTF > HTML > text ; Sheets: MOP > OOXML > TSV/CSV > HTML > text
    if fmt in avail: data = system.get(fmt); frag = decode(fmt, data) (в песочнице для чужих форматов); break
  frag = sanitize(frag)                                 # §14.3
  frag = adapt_styles(frag, target.doc, mode)           # mode: keep-source | merge | text-only (как в Office «параметры вставки»)
  doc.transaction("Вставка", |tx| tx.insert(target.pos, frag))

adapt_styles: стили фрагмента, одноимённые со стилями целевого документа, заменяются целевыми (merge), отсутствующие — добавляются с суффиксом (Heading 1 (2)) только при keep-source; прямое форматирование — resolve(source) − resolve(target) (§1.3). «Специальная вставка» показывает все доступные форматы.

14.3 Санитизация

HTML/RTF из внешних источников: удаление скриптов, событий, iframe/object/embed, внешних изображений (заменяются заглушкой с кнопкой «загрузить» — §10.1 архитектуры), ограничений нет только для data:-картинок известных типов. Фрагменты чужих форматов декодируются в codec-host (песочница). Текст из буфера проходит проверку на Unicode-трюки: двунаправленные переопределения (U+202E) показываются видимыми маркерами.

Тесты: матрица «источник × цель» (Write→Sheets, Sheets→Write, Slides→Draw, внешний Word/Excel/браузер через записанные дампы буфера), сохранение форматирования в keep-source, корректность text/plain для таблиц (TSV), delayed rendering не блокирует UI (вызывается в фоновом потоке, таймаут 5 с → только текст).


15. Drag-and-drop

drag_start(selection):                                  # внутренний источник
  payload = { origin_doc, selection_ref (NodeId/диапазоны), formats: same as copy }
  system.start_drag(payload, preview = render_thumbnail(selection, ≤ 256 px))   # Qt QDrag с теми же MIME

drag_over(point):                                        # цель — холст
  hit = dl.index.query_point(point) → block → точная позиция (строка/символ/ячейка/слот фигуры)  O(log b + строки)
  indicator = caret_or_outline(hit) ; autoscroll if point within 24 px of edge (скорость ∝ расстоянию)
  effect = if same_doc && !ctrl { Move } else { Copy }

drop(point, payload):
  if payload.is_internal && effect == Move:
      tx: Op::MoveNode / TextDelete+TextInsert (один Composite — одна группа undo), позиции корректируются,
      если цель после источника (вычесть удалённую длину)
  else: paste(target = hit, data = payload)              # §14.2 с тем же приоритетом форматов
  external files: по формату — документ → открыть в новом окне (или вставить как объект при Alt), картинка →
      вставить, шрифт → предложить установить (Hub), .mxt → установить расширение (с подтверждением)

Внешние файлы приходят как text/uri-list/CF_HDROP; содержимое читается кодеками в песочнице. На Wayland перетаскивание между окнами разных процессов работает через портал — Qt делает это прозрачно. Тесты: перемещение абзаца вниз/вверх в том же документе (позиции корректны), перетаскивание ячеек с формулами (ссылки корректируются как в Excel: относительные сдвигаются), внешняя картинка вставляется с привязкой к абзацу, автопрокрутка останавливается при уходе курсора.


16. Печать

16.1 Разбиение на страницы

Экранная раскладка уже постраничная (Write) или задаёт область (Sheets: PrintArea, разрывы, масштаб «вписать в N страниц»; Slides: раздаточные N-up). Печать берёт PageSetup документа и переопределения задания:

print_pages(doc, job):
  setup = doc.page_setup.override(job: paper, orientation, margins ≥ printer.hardware_margins, scale)
  if setup != doc.page_setup: layout = relayout(doc.snapshot(), setup)    # фон, снимок; экран не трогаем
  pages = select(layout.pages, job.range)                                  # 1-5,8 ; чётные/нечётные ; обратный порядок
  for sheet in n_up(pages, job.pages_per_sheet, job.duplex):             # N-up: трансформации страниц на листе
      dl = compose(sheet.pages.map(build_page_dl))                         # §6.1, со сдвигом/масштабом
      yield dl

Sheets: разбиение на страницы — по накопленным ширинам столбцов/высотам строк (prefix sums, O(log n) на границу), заголовки печати повторяются, масштаб «вписать» — бинарный поиск по масштабу 10..100 % (≤ 7 раскладок).

16.2 Вывод

Принтер с PDF/PostScript/XPS-путём (CUPS, macOS, Windows XPS): DisplayList → SkDocument постранично, потоком в спулер (не держим весь документ в памяти, ≤ 1 страница + шрифты). Принтер без векторного пути (Windows GDI-only, некоторые сетевые): растеризация полосами:

rasterize_banded(dl, dpi = min(printer.dpi, 600), band_px = floor(64 MiB / (width_px * 4))):
  for y in (0..height_px).step(band_px):
      surface = cpu_surface(width_px, min(band_px, height_px - y))
      surface.translate(0, -y) ; backend.render(dl, surface)             # Skia отсекает команды вне полосы по R-tree §6.1
      spooler.write_band(surface.pixels(), y)                             # StretchDIBits / CUPS raster

Текст при растеризации — без хинтинга и субпикселей, grayscale AA; цвет — sRGB → профиль принтера только при наличии ICC (иначе как есть). Сложность — O(c · число полос) в худшем случае, фактически O(c) благодаря отсечению. Предпросмотр — те же dl в тайловый кэш с эмуляцией непечатаемых полей.

Тесты: .dl.txt печатной страницы == экранной при одинаковом PageSetup; N-up 2/4/6 даёт верные трансформации (снимки); полосный растр == цельный растр (сравнение пикселей); печать 1 000 страниц держит память < 300 МБ.


17. Планировщик фоновых задач

pub enum Priority { Interactive = 0, Visible = 1, Background = 2, Idle = 3 }
pub struct Task { id: TaskId, priority: Priority, cancel: CancellationToken, deadline: Option<Instant>,
                  run: Box<dyn FnOnce(&Ctx) -> Outcome + Send> }
pub struct Scheduler { queues: [SegQueue<Task>; 4], pool: rayon::ThreadPool /* n = cores-1, ≥ 2 */,
                       io: Thread /* отдельный поток для fs/ipc */, running: DashMap<TaskId, Handle> }

impl Scheduler {
    pub fn submit(&self, t: Task) -> Handle { self.queues[t.priority as usize].push(t); self.notify(); handle }
    fn worker_loop(&self) {
        loop {
            // строгий приоритет с антиголоданием: 1 из 8 выборов берёт следующий уровень, если он непуст
            let task = self.pick_with_aging();
            let ctx = Ctx { cancel: task.cancel.clone(), budget: Budget::new(4ms) };
            match catch_unwind(|| (task.run)(&ctx)) { … }  // паника задачи не роняет пул
        }
    }
}
// Кооперативность: длинные задачи проверяют ctx.should_yield() каждые ~4 мс (счётчик итераций + Instant)
// и возвращают Outcome::Yield(continuation), чтобы Interactive-задачи не ждали.

Правила:

Сложность: submit/pick — O(1) амортизированно (lock-free очереди), дедупликация — O(1) по хэшу ключа. Тесты: приоритеты соблюдаются (Interactive всегда стартует < 1 мс при загруженном пуле); антиголодание (Idle выполняется при постоянном потоке Visible); отмена прерывает длинную задачу ≤ 8 мс; паника в задаче не ломает пул; детерминизм результата не зависит от числа потоков (раскладка на 1 и 8 потоках даёт равный .dl.txt).


18. Память

18.1 Арены с генерационными индексами

pub struct Arena<T> { slots: Vec<Slot<T>>, free: Vec<u32>, generation: Vec<u32>, live: usize }
#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct Idx { index: u32, generation: u32 }
impl<T> Arena<T> {
    pub fn insert(&mut self, v: T) -> Idx {                    // O(1): переиспользуем свободный слот
        if let Some(i) = self.free.pop() { self.slots[i as usize] = Slot::Full(v); self.generation[i as usize] += 1; … }
        else { self.slots.push(Slot::Full(v)); self.generation.push(0); … }
    }
    pub fn get(&self, idx: Idx) -> Option<&T> {                 // O(1); устаревший индекс → None, а не чужой объект
        (self.generation[idx.index as usize] == idx.generation).then(|| self.slots[idx.index as usize].as_ref())
    }
    pub fn remove(&mut self, idx: Idx) -> Option<T> { … self.free.push(idx.index) … }
}

Узлы, абзацы, фигуры, стили — в аренах по типу; NodeId → Idx через HashMap (FxHash). Арены дают локальность (обход детей — последовательные слоты при импорте), отсутствие Rc<RefCell>-циклов, дешёвые снимки (персистентный вариант PArena<T> с Arc-чанками по 1 024 слота: copy-on-write чанка при записи, клон за O(число чанков)).

18.2 Интернирование строк

Interner: HashMap<Arc<str>, Symbol(u32)> + Vec<Arc<str>>; интернируются имена стилей, ключи Custom-атрибутов, имена шрифтов, имена листов, ключи CBOR. Сравнение — по u32, хэш — u32; память на символ — 24 Б + строка. Интернер на документ (не глобальный) — детерминированные номера при повторном открытии (порядок первого появления), что важно для воспроизводимых сериализаций.

18.3 Лимиты и давление памяти

MemoryPressure: Normal → High (RSS > 70 % лимита процесса или сигнал ОС) → Critical (> 90 %)
on High:     tile cache → 25 %, shape cache → 25 %, block DL cache → только видимые ± 2 страницы,
             декодированные изображения → только видимые, миниатюры → сброс
on Critical: + отменить Background/Idle задачи, отказ открывать новые документы в этом процессе (Hub откроет в новом),
             + предупреждение пользователю, если документ > 1 ГБ в памяти

Сигналы ОС: Windows CreateMemoryResourceNotification, macOS DISPATCHSOURCE_MEMORYPRESSURE, Linux PSI (/proc/pressure/memory) + cgroup.events. Лимит процесса — настраиваемый ([performance] memory_limit_mb), по умолчанию 50 % физической памяти. Большие растры (Paint, >100 МП) — тайловые с вытеснением на диск (<cache>/tiles/), mmap-обратно.

Тесты: арена — get по устаревшему Idx возвращает None; 10⁶ вставок/удалений без роста slots сверх максимума живых; интернер даёт равные Symbol для равных строк и стабильные номера при повторной загрузке; имитация High сбрасывает кэши и RSS падает ≥ 30 % на тестовом документе с 200 картинками.


19. Детерминизм раскладки

Цель: одинаковый .dl.txt для одного документа на всех ОС/CPU/числе потоков. Правила и механизмы:

Источник недетерминизма Правило Механизм
Плавающая арифметика (порядок суммирования, FMA, x87/SSE) в раскладке — только целые EMU (i64), в растре — f32 только для финальных координат Length, Length::mul_div, запрет f32/f64 в meridian-layout (lint clippy::float_arithmetic deny в этом крейте)
Метрики шрифтов advance = round_half_away(units × size_emu / upem); кернинг и GPOS-сдвиги — то же округление по глифу, не накопительно ShapedRun.advances: Vec<Length> из rustybuzz positions (font units)
Разные шрифты на машинах документ раскладывается шрифтами из комплекта, если нужного нет — подмена по таблице с метрической совместимостью; имя фактического шрифта фиксируется в .dl.txt meridian-fonts::Fallback, снимочные тесты с MERIDIAN_FONTS=bundled-only
Выравнивание по ширине (justify) остаток R EMU распределяется по g промежуткам целочисленно: первые R mod g получают ⌊R/g⌋+1, остальные ⌊R/g⌋ (детерминированный «Брезенхэм»); Word-совместимый порядок — слева направо justify_distribute(R, g)
Высота строки max(ascent)+max(descent)+leading в EMU, округление к twips только при экспорте целые
Порядок обхода дети — по children: Vec, карты — BTreeMap, параллельные секции сшиваются в детерминированном порядке независимо от числа потоков (§17) запрет итерации HashMap при построении DL (lint disallowed_methods в meridian-layout/render)
Переносы словари hyphenation закреплённой версии в комплекте; язык — из атрибутов текста, резерв — язык документа версия словаря — в .dl.txt заголовке
Bidi и шейпинг unicode-bidi/rustybuzz закреплённых версий; Unicode-версия фиксируется на релиз обновление Unicode — мажорное событие с перегенерацией эталонов
Время, случайность в раскладке нет now()/rand; поля с датой вычисляются в meridian-fields с явной датой из контекста (для тестов — фиксированной) FieldContext { now }
Хэши в ключах кэшей только для кэширования, не для порядка; ключи — детерминированные (BTreeMap-хэши, FxHash без случайного seed)

Фиксированная арифметика там, где нужны дроби: масштабные коэффициенты — рациональные (num, den) в i64 (mul_div), проценты — Permille/Percent100k, тригонометрия для поворотов фигур — табличная i32-фиксированная (sin/cos в 1/65536 с линейной интерполяцией по 1/60000°-углу) — чтобы повёрнутый прямоугольник имел одни и те же EMU-координаты у всех. Растровый вывод (f32) недетерминизма на уровне раскладки не создаёт: расхождения в последних битах f32 не меняют .dl.txt, а снимки сравниваются с порогом.

Проверка: CI сравнивает .dl.txt всех файлов smoke-корпуса между Linux, Windows и macOS (job determinism, ночной) — любое расхождение блокирует релиз; proptest-тест layout_is_pure — раскладка одного документа дважды в разных потоках даёт равный результат; бенч justify_distribute — O(g) и сумма равна R.


Приложение. Сводка сложностей

Операция Сложность Раздел
Вставка/удаление в тексте O(log p) §1
Установка атрибута на диапазоне O(log k + сдвиг) §1.3
Резолв стиля (горячий/холодный) O(1) / O(d·a) §2
Генерация ID O(1) §3
Применение транзакции Σ операций §4
Интеграция CRDT-элемента O(1) обычно §5
Построение DL страницы (горячий кэш) O(c) §6
Инкрементальная раскладка после ввода O(абзац) … O(страница) §6.2
Тайл: get/invalidate O(1) / O(затронутых) §7
Детект формата O(64 КБ) §8
Открытие ZIP O(E) §9
Восстановление журнала O(кадров) §10
Отпечаток устройства 5 syscalls §11
Полнотекстовый запрос O(log N + результаты) §12
Поиск в тексте O(n + m) §13
Вставка из буфера O(размер фрагмента) §14
Печать страницы O(c) §16
Планировщик submit/pick O(1) §17
Арена get/insert/remove O(1) §18