Skip to content

Bound the tail for long open lists/blockquotes (frozen/tail v2) #29

Description

@jonathanKingston

Type: performance · Area: src/streaming-frozen-tail.ts · Follow-up to #21 / #23

Summary

The frozen/tail split (#23) makes committed-prefix rendering O(tail) per commit — except when the trailing top-level group never settles. settledTailStart keeps an entire open list (or blockquote) in the tail because a later item can still change it, so streaming a single long top-level list re-renders/re-sanitizes/re-morphs the whole list on every item commit. That is the old O(n²) behaviour for arguably the most common long LLM output shape (a bulleted list). Correct output, but no speedup for list-dominated content.

Documented as a known bound in the streaming-frozen-tail.ts header today.

Why it's hard

The current frozen region is a whole number of top-level group nodes (a <ul> is one node). Bounding a list's tail means freezing individual <li> nodes inside a shared <ul>, which the top-level-boundary mechanism can't express — rendering the tail <li>s separately would emit a second <ul>.

Two correctness hazards:

  1. Tight → loose flip. Loose/tight is a whole-list property (collectListGroup, render-blocks.ts). A frozen tight prefix becomes wrong the moment a blank-separated continuation makes the list loose (items gain <p> wrappers). So a tight list's items can never be frozen while the list is still open.
  2. Same-list continuation across blanks (#306) — a blank run followed by a same-marker item continues the list, so "the list ended" is only knowable after a non-continuing block.

Sketch

  • Introduce a sub-group freezing mode: the frozen region may end inside a trailing <ul>/<ol>, and the tail morph appends <li>s into that existing list element instead of creating a new one.
  • Only engage it once the list is provably loose (a blank-separated continuation has been observed → loose is monotonic for that list), so a frozen item renders identically (loose) whether frozen or in-tail. Tight lists stay wholly in the tail.
  • Add a list-looseness analogue of the tokenStraddles guard: if the observed looseness or marker type flips against a frozen assumption, fall back to full morph.
  • Same parity-first method as perf(streaming): incremental committed-prefix rendering (fixes #21) #23: assert byte-identity across the baseline corpus and exhaustive convergence fuzz before wiring in, behind the full-morph fallback.

Acceptance

  • Streaming a 1,000-item top-level list grows sub-quadratically (add a scaling case to bench-streaming.mts).
  • Byte-identical to the at-rest render at every committed frame; exhaustive convergence fuzz passes.
  • Tight-list, loose-flip, marker-change, and blank-run-continuation cases covered by targeted tests.

References

  • src/streaming-frozen-tail.ts — settledTailStart, isMultiTokenGroupKind, FrozenTailRenderer.update
  • src/render-blocks.ts — collectListGroup (loose/tight, #306), collectBlockquoteGroup

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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