Skip to content

WASM-оптимизации

WASM-режимы TimelineChart v2 выключены по умолчанию. Они нужны для плотных производственных сцен, где bottleneck находится в повторяемой числовой работе: поиск видимых кандидатов, grouping узких задач, сборка простых rect-batches или batch hit-test.

Главный флаг - rendering.wasm.enabled. Если он выключен, timeline всегда использует обычный JS path, даже если дочерние флаги выставлены в true.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
    },
  },
}

Visible extraction

visibleExtraction разрешает numeric WASM kernel для извлечения видимых range-кандидатов из плотных buffers. JS остается владельцем объектов, групп, selection, tooltip и публичной модели данных. Если текущий кадр нельзя безопасно перевести в numeric path, timeline пишет fallback diagnostics и продолжает JS path.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      visibleExtraction: true,
    },
  },
}

Включайте этот флаг для сцен с большим числом задач и частым zoom/scroll. Не ждите выигрыша на малых наборах или если основная стоимость находится в task template, text rendering или custom callbacks.

Task Density LOD

taskDensityLod - render-only LOD. Он не меняет данные, visibleBlocks, hit-test, tooltip и selection. Если в одной строке несколько узких range-задач становятся визуально неразличимыми, planner заменяет их одним серым rect-cluster и не вызывает дорогой task profile для этих задач.

Для работы режима нужно включить сам LOD и, отдельно, WASM implementation:

ts
const options = {
  rendering: {
    taskDensityLod: {
      enabled: true,
      maxTaskWidthPx: 5,
      maxGapPx: 2,
      minClusterSize: 2,
      color: '#b8c0c7',
    },
    wasm: {
      enabled: true,
      taskDensityLod: true,
    },
  },
}

Параметры:

ts
const taskDensityLod = {
  maxTaskWidthPx: 5, // задача считается узкой
  maxGapPx: 2, // соседние узкие задачи попадают в один run
  minClusterSize: 2, // минимальный размер run для замены
  color: '#b8c0c7',
}

Включайте для zoom-out, где отдельные балки уже не читаются. Не включайте, если важно видеть цвет/иконки/текст каждой узкой задачи даже при сильном zoom-out.

Hit-test batch-only

hitTest в WASM работает только в batch-only режиме. Он не заменяет обычный pointer/mousemove path, потому что один JS-to-WASM вызов на каждое движение мыши обычно дороже, чем JS hit-test. Режим нужен для массовых запросов, например для внешнего overlay, аналитики или prefetch-проверок.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      hitTest: {
        enabled: true,
        mode: 'batch-only',
      },
    },
  },
}

Пример batch-запроса:

ts
const hits = timelineRef.value?.hitTestBatch([
  { x: 120, y: 80 },
  { x: 240, y: 80 },
  { x: 360, y: 120 },
])

Standard-mode WASM kernels

Standard-mode kernels не являются LOD. Они не заменяют task template на серый rect, не меняют visibleBlocks, selection, tooltip и hit-test model. Их задача - перенести повторяемую числовую работу в WASM, сохранив обычную картинку.

Все standard-mode kernels зависят от общего флага:

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
    },
  },
}

Visible geometry

visibleGeometry считает экранную геометрию плотного task buffer: visible indices, x/y/width/height. JS остается владельцем объектов, групп, фильтров и callbacks.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      visibleGeometry: true,
    },
  },
}

Включайте для больших сцен, где profiling показывает стоимость подготовки видимой геометрии. Не включайте, если bottleneck находится в templates, text или GPU upload.

Overlap layout

overlapLayout ускоряет расчет collapsed overlap ordering: какие задачи входят в overlap-компонент, какой у них zRank и какой component id использовать для отрисовки.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      overlapLayout: true,
    },
  },
}

Включайте для сцен с большим числом пересекающихся задач в allowOverlap группах. Этот режим не упрощает визуал: он только ускоряет числовую часть расчета overlap.

Sort/filter

sortFilter переносит numeric sort/filter для плотных task buffers. Пользовательские callbacks, object mapping и доменные фильтры остаются в JS.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      sortFilter: true,
    },
  },
}

Включайте после замера, если в diagnostics видно, что сортировка или числовая фильтрация task buffers занимает заметное время.

Incremental diff

incrementalDiff считает affected tasks/groups/rows/time range для batch updates. Он полезен для SSE или частых update/batch, где за кадр приходит много изменений.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      incrementalDiff: true,
    },
  },
}

Включайте для потоковых обновлений. Для редких одиночных updates overhead копирования в WASM может быть больше выигрыша.

Batch hit-test standard kernel

hitTest остается batch-only, но использует standard-mode spatial kernel. Он за один вызов извлекает видимую геометрию и ищет ближайшие задачи для массива точек.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      hitTest: {
        enabled: true,
        mode: 'batch-only',
      },
    },
  },
}

Обычный pointer/mousemove path остается JS. Для одиночных запросов boundary overhead обычно съедает пользу.

Fused render pipeline

fusedRenderPipeline - самый агрессивный режим. Он объединяет несколько шагов в один WASM-коридор: numeric visible pass, density grouping и сборку rect batch для серых density-clusters. Цель - избежать нескольких больших JS/WASM boundary calls и повторного копирования buffers.

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      fusedRenderPipeline: {
        enabled: true,
        maxTaskWidthPx: 5,
        maxGapPx: 2,
        minClusterSize: 2,
        color: '#b8c0c7',
      },
    },
  },
}

Приоритеты:

ts
const options = {
  rendering: {
    wasm: {
      enabled: true,
      visibleExtraction: true,
      taskDensityLod: true,
      fusedRenderPipeline: {
        enabled: true,
        maxTaskWidthPx: 5,
        maxGapPx: 2,
        minClusterSize: 2,
      },
    },
  },
}

Если fusedRenderPipeline.enabled === true, timeline сначала пробует fused path. Если WASM недоступен или кадр не подходит, он откатывается к отдельным включенным kernels, а затем к JS. Широкие, selected, invalid, background и collapsed-overlap задачи остаются в обычном JS planner, чтобы не ломать слойность и интерактивность.

Включайте fused pipeline для сильного zoom-out и десятков тысяч видимых узких задач. Не включайте его, если нужна pixel-perfect отрисовка каждого task template при любой ширине.

Отличие от clusterThresholdPx

clusterThresholdPx меняет видимый список блоков и идет через cluster template. Это полезно, когда продукт хочет показать пользователю настоящие агрегированные clusters.

taskDensityLod и fusedRenderPipeline не меняют модель данных и interaction. Они только заменяют render output для задач, которые уже визуально слились в плотную серую массу.

ts
const dataClusterOptions = {
  clusterThresholdPx: 4,
}

const renderOnlyOptions = {
  clusterThresholdPx: 0,
  rendering: {
    taskDensityLod: {
      enabled: true,
      maxTaskWidthPx: 5,
      maxGapPx: 2,
      minClusterSize: 2,
    },
    wasm: {
      enabled: true,
      fusedRenderPipeline: {
        enabled: true,
      },
    },
  },
}

Diagnostics

Timeline пишет статус каждого kernel в wasmDiagnostics. Это нужно читать при ручном profiling: активный флаг не гарантирует, что конкретный кадр ушел в WASM.

ts
import type { NovaApp } from '@endge/nova'
import type { NovaCanvasReadyPayload } from '@endge/nova-vue'
import type { TimelineWasmDiagnostics } from '@engine2d/timeline-chart'

let timelineApp: NovaApp | null = null

function handleReady(payload: NovaCanvasReadyPayload): void {
  timelineApp = payload.app
}

function logWasmDiagnostics(): void {
  const diagnostics = findTimelineStore(timelineApp)?.wasmDiagnostics

  console.table({
    enabled: diagnostics?.enabled,
    visibleExtraction: diagnostics?.kernels.visibleExtraction,
    visibleGeometry: diagnostics?.kernels.visibleGeometry,
    overlapLayout: diagnostics?.kernels.overlapLayout,
    sortFilter: diagnostics?.kernels.sortFilter,
    incrementalDiff: diagnostics?.kernels.incrementalDiff,
    taskDensityLod: diagnostics?.kernels.taskDensityLod,
    hitTest: diagnostics?.kernels.hitTest,
    fusedRenderPipeline: diagnostics?.kernels.fusedRenderPipeline,
    fallbackReason: diagnostics?.fallbackReason,
    jsToWasmCalls: diagnostics?.jsToWasmCalls,
    copiedBytesToWasm: diagnostics?.copiedBytesToWasm,
    copiedBytesFromWasm: diagnostics?.copiedBytesFromWasm,
  })
}

function findTimelineStore(app: NovaApp | null): TimelineDiagnosticsStore | null {
  if (!app) return null

  for (const surface of app.surfaces) {
    const store = findTimelineStoreInNode(surface as TimelineDiagnosticsNode)
    if (store) return store
  }

  return null
}

function findTimelineStoreInNode(node: TimelineDiagnosticsNode): TimelineDiagnosticsStore | null {
  if (node.store?.wasmDiagnostics) return node.store

  for (const child of node.children ?? []) {
    const store = findTimelineStoreInNode(child)
    if (store) return store
  }

  return null
}

interface TimelineDiagnosticsStore {
  wasmDiagnostics?: TimelineWasmDiagnostics
}

interface TimelineDiagnosticsNode {
  store?: TimelineDiagnosticsStore
  children?: TimelineDiagnosticsNode[]
}

Практическое правило: включайте WASM только после сравнения с JS baseline на своей сцене. Если FPS не меняется, значит bottleneck находится не в numeric kernels, а в templates, text, GPU upload, draw calls или внешних updates.