23 KiB
Why Quark Can Be Faster Than Solid Store
Status: experimental performance model
Conclusion
Quark can be faster than the current Solid Store timeline because it accepts a stronger contract and uses that contract to do less runtime work.
The advantage does not come from TypeScript types by themselves. TypeScript types disappear at runtime. The useful information comes from explicit runtime inputs and API laws:
- A row key is unique and cannot change during its ownership lifetime.
- Row equivalence is supplied before updates begin.
- A keyed list has one stable slot per key.
- Value changes and structural changes publish through separate channels.
- A transaction publishes only settled graph state.
Those guarantees remove choices from the hot path. Quark does not need to rediscover identity, shape, equivalence, index membership, or ownership on every update. Less discovery means fewer comparisons, proxy traps, allocations, writes, and downstream invalidations.
This is a conditional claim, not a claim that Quark is universally faster than Solid. Solid can be as fast or faster when an application performs a precise direct store write, when lists are tiny, or when Quark receives no useful identity information. The relevant comparison is the current OpenCode path: whole projected records and ordered rows are reconciled into a Solid Store.
The Physical Argument Is “Do Less Work”
Software cannot escape the physical costs of instructions, memory reads, allocations, pointer writes, cache misses, and notification fan-out. An abstraction can only become faster by reducing those costs or arranging them more favorably.
Quark's proposed advantage has four concrete sources:
- Declare repeated decisions once. Identity and equivalence become preconfigured functions rather than per-event discovery.
- Replace searches with addresses. Stable keys resolve to persistent
slots through a
Map. - Separate unrelated change dimensions. An item value can change without publishing a new list structure.
- Stop propagation early. Schema equivalence and slot equivalence suppress writes before they reach Solid or the terminal renderer.
These are ordinary computer costs, not framework mythology. If profiling shows that Quark executes the same work as Solid plus an adapter, the hypothesis is false. If counters show fewer structural publications, fewer component-owner reconciliations, and fewer proxy/store operations, the speedup has a mechanical explanation.
Solid Store Must Preserve Generality
The current timeline asks Solid Store to accept ordinary JavaScript objects and reconcile new projections into a proxy-backed graph.
flowchart LR
A[Projected rows] --> B[Solid reconcile]
B --> C[Inspect keys and shape]
C --> D[Proxy-backed Store writes]
D --> E[Dependency invalidation]
E --> F[For reconciliation]
F --> G[SessionRowView]
Solid's generality is valuable. It can react to arbitrary nested property access, partial path writes, and plain objects without requiring a schema-owned model. That generality also means the runtime must maintain proxy metadata and interpret each reconciliation against the current store graph.
Solid is not inherently inefficient. A precise call such as
setStore(index, "value", next) can avoid whole-list reconciliation. OpenCode's
timeline frequently receives whole projected messages or rebuilt row arrays,
however, so its current implementation uses produce and reconcile to
recover identity and preserve nested owners.
Quark Makes Identity an Input
Layout.collection compiles identity, equivalence, and indexes once when the
timeline is created. Every row carries a collision-safe primitive id. Groups
also retain their immutable origin for message-boundary projection:
const PartRefLayout = Layout.struct({
messageID: Layout.string,
partID: Layout.string,
})
const SessionRowLayout = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
"compaction-queued": Layout.struct({ inputID: Layout.string }),
part: Layout.struct({ ref: PartRefLayout }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
pending: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
},
}),
"assistant-footer": Layout.struct({ messageID: Layout.string }),
},
})
type PartRef = Layout.Type<typeof PartRefLayout>
type SessionRow = Layout.Type<typeof SessionRowLayout>
Factories build the ID once with length-prefixed segments. Length prefixes avoid delimiter collisions without invoking a serializer:
function segment(value: string) {
return `${value.length}:${value}`
}
function groupRow(kind: "reasoning" | "exploration", origin: PartRef): SessionRow {
const id = `g${kind === "reasoning" ? "r" : "e"}${segment(origin.messageID)}${segment(origin.partID)}`
if (kind === "reasoning") return { id, type: "group", kind, origin, refs: [origin], completed: false }
return { id, type: "group", kind, origin, refs: [origin], pending: [], completed: false }
}
Generated equivalence answers a different question: can consumers distinguish
these two values for the same identity? The layout compares rendered fields,
dispatches through the row and group tags, and ignores immutable origin.
const SessionRows = Layout.collection(
SessionRowLayout,
({ members }) => ({
parts: members(partIDs),
}),
{ backend: "generated" },
)
const rows = SessionRows.make()
The parts index contains standalone and grouped part IDs. Duplicate stream
deltas return through hasMember before aggregate projection. Arbitrary
replacements derive membership automatically; the hot group-append path applies
the exact one-member delta already known from the event.
It exposes two reactive surfaces:
rows.slots // changes only when keys are inserted, removed, or reordered
rows.values // changes when any current value changes
Each slot is itself a readable value. Solid <For> receives slots, so a
group can grow or complete without replacing its component owner. Generic
aggregate consumers can subscribe to values when they need every value
change.
The TUI's message-boundary projection deliberately does not subscribe to
values. Boundary identity uses only immutable row fields, so it tracks the
structural slots array and reads each slot value untracked. Value-only deltas
therefore perform no boundary work; inserts, removals, reorders, or message
changes recompute boundaries.
Here is a complete value-only transition. The group changes, but its key and position do not:
const origin = { messageID: "assistant-1", partID: "call-read" }
const initial = groupRow("exploration", origin)
rows.set([initial])
const structure = rows.slots()
const groupSlot = structure[0]
rows.modify(initial.id, (group) => ({ ...group, completed: true }))
rows.slots() === structure // true: <For> receives no structural change
rows.slots()[0] === groupSlot // true: component owner survives
groupSlot().completed === true // true: row value updated
A structural insertion changes the outer list but preserves every retained slot:
const footer: SessionRow = {
id: "f11:assistant-1",
type: "assistant-footer",
messageID: "assistant-1",
}
const footerSlot = rows.insert(footer)
rows.slots().length === 2 // true
rows.slots()[0] === groupSlot // true
rows.slots()[1] === footerSlot // true
Removing and later reinserting a key begins a new ownership lifetime:
rows.remove(footer.id)
const nextFooterSlot = rows.insert(footer)
nextFooterSlot === footerSlot // false
flowchart LR
A[Next rows] --> B[Keyed.set]
B --> C{Same keys and order?}
C -->|yes| D[Keep slots array]
C -->|no| E[Publish slots array]
B --> F{Equivalent value?}
F -->|yes| G[No slot write]
F -->|no| H[Publish one slot]
D --> I[Solid For unchanged]
E --> J[Solid For reconciles structure]
H --> K[Existing SessionRowView updates]
The key distinction is that row identity is not inferred from row value.
The timeline gives every reasoning or exploration group an immutable creation
key. Permission partitioning may move refs between refs and pending, but it
cannot change the group slot or reset local expanded state.
The Keyed.set Algorithm
The current algorithm is deliberately small:
set(next):
1. Compute every next key and reject duplicates.
2. Look up retained slots in the persistent key map.
3. For each next value:
a. Reuse the previous slot for its key, or create one.
b. Run generated equivalence over previous and next values.
c. Write only a changed slot.
4. Publish the outer slot array only if membership or order changed.
5. Synchronize declared collection indexes.
6. Flush slot, index, and structural writes in one transaction.
The aggregate values readable maps the current slots to their values. It is a
computed node, so it remains lazy without subscribers and publishes one settled
array when subscribed.
The algorithm preflights duplicate keys before any slot mutation. A rejected
set therefore cannot partially update the graph. Map and Set give keys
SameValueZero semantics, including consistent treatment of 0/-0 and
NaN.
The Laws That Unlock the Optimizations
[
{
"term": "Unique key law",
"definition": "No two current values have the same key. This permits one Map entry and one slot per identity."
},
{
"term": "Stable key law",
"definition": "An entity or logical row keeps its key for its lifetime. This permits component ownership and subscriptions to survive value changes."
},
{
"term": "Equivalence law",
"definition": "If equivalent(left, right) is true, publishing right cannot change any consumer-visible meaning. This permits early cutoff."
},
{
"term": "Structural cutoff law",
"definition": "Value-only updates preserve the exact outer slots-array identity. This prevents keyed-list reconciliation for non-structural changes."
},
{
"term": "Settled publication law",
"definition": "Subscribers observe the state after every slot and structural write in the operation, never a partial combination."
},
{
"term": "Ownership law",
"definition": "Removing a key removes its slot from the collection. Reinserting that key creates a fresh slot; old external references cannot attach to a new lifetime."
}
]
These laws are stronger than a plain Array<object> contract. They are also
testable. Quark should reject a violated unique-key law and should have direct
tests for every other law.
Layout Compiles Trusted Collection Metadata
The timeline now uses Quark's trusted in-memory Layout. It supports the scope
required by this experiment:
- primitive, array, struct, discriminated-union, and keyed-union fields;
- immutable fields excluded from rendered equivalence;
- compiled key extraction and closure or generated equivalence;
- imperative multi-value membership and ordered first-match indexes;
- automatic index derivation for arbitrary replacements;
- typed member deltas for event-native mutations that know exact changes.
Generated nested union comparison measured 1.06x-1.10x handwritten cost,
versus 1.19x-1.22x for closure Layout across three runs. Generation occurs
once when SessionRows is declared. The older 1.322x closure result motivated
this backend; it no longer describes the production comparator.
Automatic membership derivation is linear in an affected row's members. That
is the safe default, but it made repeated append to one growing group roughly
27x-30x the handwritten index path. The event already identifies the new
part, so SessionTimeline.appendPart supplies a typed one-member delta. Generic
completion and permission repartition retain automatic derivation.
Numeric field positions, change masks, per-field reactive slots, models, and columnar storage remain future research. They are not required by the TUI.
Asymptotics and Constants
For a whole next array of N keyed values, both Quark Keyed.set and a general
keyed reconciliation are O(N). Quark does not break a lower bound: it must
read the next keys to validate and order them.
The observed win is currently a constant-factor win at the same asymptotic complexity:
- one key extraction per next item; previous slots are already indexed;
- one
Maplookup per next item; - one explicit equivalence check;
- one slot write per changed value;
- one outer write only for changed structure;
- no general nested proxy reconciliation inside Quark.
For direct collection operations, stronger bounds are possible. An immutable
key lets has, get, hasMember, and modify find their target in expected
O(1) time. Work is then proportional to the changed row, affected declared
indexes, and any ordered structural array copy. SessionTimeline caches the
active group and queued boundary IDs; inserts and removals remain O(N) because
the ordered slots array must be copied. Full synchronization and revert rebuilds
remain linear set operations.
Controlled Benchmark Evidence
The benchmarks are checked into the fork and run directly:
cd packages/quark
bun run bench:keyed
bun run bench:row-key
cd ../tui
bun run bench:timeline
Each invocation interleaves and rotates variants over nine measured samples after a discarded warmup. It reports median nanoseconds per operation, median absolute deviation, machine-readable metrics, and a checksum.
The original whole-array benchmark established the initial direction:
| Workload | Run | Quark | Solid Store | Quark / Solid |
|---|---|---|---|---|
| One value changes in a 1,000-item next array | 1 | 177.2 us | 863.7 us | 0.207x |
| One value changes in a 1,000-item next array | 2 | 158.8 us | 755.6 us | 0.213x |
| Reorder a 1,000-item list | 1 | 155.3 us | 1,020.8 us | 0.157x |
| Reorder a 1,000-item list | 2 | 147.1 us | 975.6 us | 0.152x |
Across these two invocations:
- keyed value publication was approximately 4.7x-4.8x faster;
- keyed reorder was approximately 6.4x-6.6x faster.
The checksum was stable, and both variants performed the same next-array construction inside their timed workloads.
The refreshed lower-level Keyed benchmark measures event-native updates with
the aggregate values channel subscribed, plus adverse workloads. It does not
include SessionTimeline, generated row equivalence, or compiled index
maintenance. The July 17, 2026 evidence run produced:
| Workload | Quark | Solid | Quark / Solid |
|---|---|---|---|
| Direct update, no subscribers, 1,000 | 46.8 ns | 761.6 ns | 0.068x |
| Precise Solid path write, 1,000 | 54.3 ns | 327.5 ns | 0.170x |
| Subscribed aggregate, 10 rows | 608.0 ns | 7.4 us | 0.085x |
| Subscribed aggregate, 100 rows | 4.4 us | 80.0 us | 0.055x |
| Subscribed aggregate, 1,000 rows | 37.4 us | 703.1 us | 0.052x |
| Subscribed aggregate, 10,000 rows | 388.5 us | 8.1 ms | 0.044x |
| Dense 1,000-row update | 270.2 us | 2.3 ms | 0.132x |
| Unstable keys, 100 rows | 68.0 us | 245.2 us | 0.282x |
A second independent invocation produced paired ratios of 0.084x,
0.170x, 0.077x, 0.059x, 0.061x, 0.042x, 0.130x, and 0.266x
in the same row order. The direct-path result reproduced exactly to three
decimal places; the other workloads retained the same direction and broad
magnitude despite normal timing variance.
The aggregate path is still O(N), but it remained substantially cheaper than
the equivalent Solid Store projection. The precise direct-path case was added
specifically to favor Solid: it calls setValues(index, "value", next) while
Quark still constructs and compares a replacement object. The predicted Solid
win did not occur in this environment; Quark measured 0.170x. This is a
falsified prediction for this setup, not evidence that Solid can never win a
direct-write comparison. The production TUI has no aggregate subscriber; it
tracks structural slots and immutable boundary metadata instead.
The checked-in SessionTimeline benchmark compares the final domain module to
the previous handwritten Keyed + Set reducer mechanics and the original Solid
Store produce path. Across three runs:
| Workload | SessionTimeline result |
|---|---|
| Growing reasoning-group append / handwritten Quark | about 1.3x-1.5x |
Growing reasoning-group append / Solid produce |
about 0.013x |
Duplicate part in a 1,000-ref group / Solid produce |
about 0.0004x-0.0005x |
The final module pays roughly 30%-50% over the minimal handwritten Quark path
for compiled layout dispatch, domain policy, and maintained index ownership.
It remains roughly 74x-79x faster than the baseline Solid append in this
growing-group stress workload. Duplicate lookup is expected O(1) through the
compiled members index; the Solid baseline scans 1,000 refs, producing an
approximately 1,960x-2,500x stress-case difference. These are state-operation
results, not claims about total OpenCode response latency.
Key extraction was measured independently over 10,000 representative rows:
| Key strategy | Median | Paired ratio to JSON |
|---|---|---|
JSON.stringify tuple |
109.6 ns/op | 1.000x |
| Collision-safe length-prefixed string | 25.6 ns/op | 0.205x |
| Precomputed primitive row ID field read | 8.7 ns/op | 0.075x |
The TUI now uses the precomputed primitive ID strategy.
What the Benchmark Does Not Prove
The suite compares whole-array reconciliation, direct point updates, subscribed aggregate projection, dense changes, unstable keys, key extraction, growing timeline groups, and duplicate deltas. It does not prove that Quark beats:
- every Solid keyed-list primitive;
- rendering, terminal layout, or paint;
- all list sizes and mutation distributions;
- a complete OpenCode session under real provider and tool traffic.
The end-to-end opencode-drive runs establish behavioral parity, including
visible terminal completion and projected reasoning/text content. Their timing
varied too widely to support a speed claim because it includes provider
simulation, process scheduling, event transport, terminal rendering, and UI
polling.
When Quark Should Lose
The design predicts smaller or negative gains when:
- lists are so small that setup and adapter costs dominate;
- keys are unstable or expensive to compute;
- equivalence is more expensive than simply publishing;
- almost every item and the structure change on every operation;
- there are no subscribers, so reactive precision has no downstream value;
- Solid receives a cheaper primitive update while Quark must perform a more expensive projection or whole-array reconciliation;
- copying the next application-level projection dominates both runtimes;
- the Quark-to-Solid adapter duplicates work rather than cutting it off.
These are falsifiable predictions. The current adverse suite did not find a Solid win, including the newly added precise path write, but it preserves these cases so future changes cannot optimize only the favorable sparse path.
Deterministic Tests Measure Work, Not Just Time
The single-binary deterministic event trace counts:
| Deterministic transition | Slot delta | Structure delta | Observed ownership behavior |
|---|---|---|---|
| First exploration part | +0 | +1 | New group slot |
| Extend exploration group | +1 | +0 | Existing group slot retained |
| Permission repartition | +1 | +0 | Existing group slot retained |
| Insert queued user row | +0 | +1 | Group remains mounted and incomplete |
| Complete promoted-input group | +1 | +0 | Existing group slot retained |
| Duplicate text delta | +0 | +0 | Returns through compiled membership index |
| Full unchanged two-row rebuild | +0 | +0 | Two equivalence suppressions, no publication |
Keyed exposes optional counters for slot publications, structural
publications, and equivalence suppressions. The focused TUI trace
completes exploration when a queued prompt is promoted records the first five
transitions and asserts the same slot identity across extension, repartition,
and completion. does not publish timeline rows for duplicate streaming deltas records the duplicate path. The direct Keyed law test records the full
unchanged rebuild. Boundary tests independently assert that value-only changes
do not invalidate structural boundary projection.
Pure timeline tests additionally cover cached queue advancement, out-of-order
promotion, duplicate message/footer idempotence, stable group ownership, and
parity between event-native operations and snapshot reduction. Unique append
uses cached IDs and never reads state.values().
Decision Rule
Continue the Quark timeline experiment if deterministic replay confirms all of the following:
- Behavior and component ownership remain equivalent to the Solid baseline.
- Value-only events publish no structural changes.
- CPU or allocation cost improves on the real event distribution.
- The Solid adapter remains a thin boundary rather than a second reactive system doing duplicate work.
- The reusable
Keyedand layout laws contain the complexity; TUI code does not grow its own reconciliation engine.
If those conditions fail, the standalone microbenchmarks are not sufficient reason to replace Solid Store. If they hold, the speedup is not mysterious: Quark is faster because OpenCode supplied stronger information and Quark used that information to execute less work.
The current evidence satisfies ownership, structural cutoff, adapter
thinness, and containment in Keyed. Controlled workloads and the
deterministic TUI trace show lower CPU work for the exercised event shapes.
That is enough to continue and land the timeline experiment, not to claim a
universal framework win. Real-session soak remains the check on whether the
measured event distribution matches production usage.