Skip to content

Fast, lazy font opening across source formats #154

Description

@kostyafarber

Goal

Open production font sources quickly enough that Shift feels immediate, while preserving complete authoring semantics. The target is under 300 ms from the file chooser returning to the first useful glyph grid content on the representative dogfood fonts.

The whole font does not need to be materialized before the document appears. After the directory and first visible glyph projections are ready, complete authored glyphs can continue loading into the workspace store in the background.

Representative repro:

/Users/kostyafarber/repos/shift-dogfood/sources/fraunces/Fraunces.designspace

This applies across Designspace, UFO, Glyphs/Glyphspackage, binary fonts, and .shift packages. It is not a Designspace-only optimization.

Agreed API boundary

The native caller remains:

FontWorkspace::open(source_path, store_path)

FontLoader::read_font(path) -> Font remains the eager, full-fidelity reader and correctness backstop. It does not have to be the fast document-open path.

Do not add public OpenedFont, Definition, DocumentIndex, openIndex, hydration-session, readiness, request-generation, or location-keyed preview APIs. Do not make shift_font::Font an ambiguously partly loaded public value. Staged native state belongs inside FontWorkspace and its internal source reader.

The renderer continues to use its existing Font model and demand APIs:

  • requestGlyphViews(glyphIds) for compact glyph projections;
  • loadGlyph(glyphId) and loadGlyphs(glyphIds) for complete authored glyphs;
  • the grid normally consumes projections through useGlyphViews;
  • FontStore owns resident glyph records, projections, authored layers, variation models, and their signals;
  • WorkspaceEditCoordinator orders batched reads and edits.

The implementation behind these operations may become lazy. Callers should not manage hydration sessions or global readiness flags.

Opening sequence

  1. Capture workspace-owned immutable backing for the complete imported source closure.
  2. Read and persist the font directory:
    • metadata and metrics;
    • axes, mappings, and labels;
    • sources, source metrics, and named instances;
    • glyph order, IDs, names, unicodes, and source/layer addressing metadata.
  3. Load one batched set of compact projections for the visible and overscanned grid glyphs, including required component dependencies.
  4. Reveal a useful document and glyph grid.
  5. Continue materializing in the background, off the renderer and Electron main threads:
    • finish globally useful lightweight structure such as the component-reference graph;
    • load complete authored glyph and layer bodies into SQLite in bounded batches;
    • reach the same complete workspace state as the eager reader.

Axis scrubbing evaluates resident compact projection models synchronously in TypeScript. It must not issue location-keyed native requests.

Background loading and foreground commands

Background loading is opportunistic, not a separate competing ownership model.

  • Background work and user commands share one coordinator and single-flight work per glyph/resource.
  • Visible projections and foreground commands have priority over speculative background batches.
  • If a command needs unloaded information, it promotes and awaits only its prerequisites before mutating. It must never guess from partial data.
  • For example, deleting a selection may require the complete component-reference index, but should not require every unrelated outline to be decoded first.
  • Materialization batches and edit transactions are ordered so a late import result cannot overwrite an edit or resurrect a deleted glyph.
  • Pending work for deleted or replaced entities is cancelled or rejected before commit.

These are internal residency semantics. The normal renderer API remains Font, projections, authored glyph loading, and signals.

Workspace-owned lazy backing

Once open succeeds, later lazy reads must not depend on the user's original source still existing or remaining unchanged. The original path is provenance and an external-change reference only.

The workspace must own an immutable raw-byte snapshot of the complete source closure:

  • the single input file for TTF/OTF and monolithic Glyphs;
  • the whole UFO or Glyphspackage directory;
  • the Designspace file and every referenced UFO directory;
  • preservation material such as layers, features, kerning, libs, data, images, and unknown files.

Capturing this snapshot must not require parsing or normalizing every glyph. Prefer copy-on-write clones or reflinks where available, with a correctness-preserving fallback. Do not use hardlinks: an in-place change to the original would also change the workspace backing. The exact cross-platform capture mechanism remains an implementation decision for this ticket.

SQLite is the canonical mutable workspace. The source snapshot is immutable backing for records that have not yet been materialized.

Reader-level work

The lazy architecture does not replace reader optimization. Each source format should avoid work not needed for the current phase.

  • Glyphspackage: read top-level metadata and ordering first; decode glyph files on demand or in background batches.
  • UFO: read root metadata and contents.plist first; decode .glif bodies on demand. Reuse each unique UFO parse/index.
  • Designspace: read axes and source descriptors first; index referenced UFOs without eagerly decoding all geometry.
  • Binary fonts: retain table-backed/random-access data where possible and decode glyph bodies on demand.
  • .shift: open its directory/store representation without rebuilding an intermediate complete font unnecessarily.

The previous whole-FontIndex rebuild per inserted layer has already been removed and is not the remaining architectural direction. Continue profiling parser conversion, component resolution, snapshot capture, initial SQLite writes, and background materialization.

Correctness and full fidelity

  • Lazy open and eager FontLoader::read_font must produce equivalent authored font semantics after full materialization.
  • Save and export must include unloaded glyphs and preservation data. They may force completion initially, but may never silently skip data.
  • Global operations may await a larger prerequisite barrier when correctness requires it.
  • Glyph/source/layer order and identities, default source, axes and mappings, components, metrics, interpolation, features, kerning, libs, and binaries must remain intact.
  • Imported source bytes must remain available to the workspace after the original is moved, edited, or deleted.

Measurement

Measure these milestones separately from the native file dialog:

  1. immutable source backing captured;
  2. font directory ready;
  3. first visible compact projection batch ready;
  4. first useful grid painted;
  5. global structural index complete;
  6. full authored workspace materialized in SQLite.

Preliminary eager-path profiling found roughly 1.0–1.2 s for Fraunces after bulk-construction improvements, with smaller representative Glyphs, TTF, and UFO inputs around 126 ms, 149 ms, and 307 ms respectively. These are investigation numbers, not stable benchmarks or CI thresholds.

Acceptance criteria

  • The representative large fonts target under 300 ms from chooser return to useful grid content, with before/after phase measurements recorded in the PR.
  • Initial open does not decode or insert every full glyph body before revealing the document.
  • Visible and overscanned glyph projections load in batches with component dependencies.
  • Background materialization proceeds in bounded work without making scrolling, scrubbing, or editing choppy.
  • A foreground edit issued before background completion promotes its prerequisites and commits correctly; a late background batch cannot overwrite it.
  • Full background materialization produces the same semantic font as the eager reader.
  • Save/export while materialization is incomplete produces a complete result.
  • Lazy reads continue to work after the original source is changed, moved, or deleted.
  • Performance coverage uses profiling fixtures or benchmarks rather than flaky wall-clock assertions in normal CI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions