Skip to main content

A boolean flag beats a Set for the dirty batch

· 3 min read

A keyed emitter dedups its per-flush batch. The default tool is a Set. A boolean flag on the carrier plus a plain array does the same job ~4× cheaper, paid for with a hand-maintained invariant.

Context

OIMCarrierKeyedEmitter is the per-key pub/sub behind collections and indexes. Handlers live on the carrier — the collection's entity slot, or the index's bucket (see handlers on the carrier). On a write the changed carrier is marked dirty; on queue.flush() the batch of dirty carriers is delivered once.

Problem

Marking a carrier dirty runs on every mutation — the hottest write-path op, and far more frequent than flushes. The batch has to dedup: a carrier written ten times in one tick must be delivered once.

Options

  • Set<carrier>set.add(carrier) computes the carrier's identity hash, probes the set's table, inserts. Membership and dedup come for free.
  • flag + arrayif (carrier.dirty) return; carrier.dirty = true; arr.push(carrier). Dedup is a boolean read, membership lives on the carrier itself, append is an array push.

What OIMDB does

Flag + array:

if (carrier.dirty) return; // already in the batch
carrier.dirty = true;
this.dirtyCarriers.push(carrier);

The flag is reset as each carrier is delivered, leaving it re-markable next tick.

Microbench — 1000 carriers, marked + iterated over 20,000 flushes (20M mark cycles), reused containers:

pending batchall dirty5% dirty per flush
Set<carrier>778 ms43 ms
flag + array181 ms9 ms

~4.3× (all dirty), ~4.8× (sparse). The gap is the identity-hash that Set.add runs on every mark and the array push skips.

This measures the batching primitive in isolation — mark + iterate, nothing else. A real write does more around it (apply the patch, schedule the flush), so the dirty-batch op is a fraction of one write, not 4× of end-to-end write time. The point is that it's a fraction paid on every mutation while flushes are rare, so shaving the per-mark constant compounds.

Cost

The flag is shared mutable state with an invariant: carrier.dirty === true iff the carrier is in dirtyCarriers. Break it and you either drop updates (flag stuck true → future marks skipped) or double-deliver. A Set needs none of this — removal from the batch is implicit.

Holding the invariant when a handler throws mid-flush takes explicit cleanup: delivery resets the flags of any carriers it never reached.

} finally {
for (; i < flushing.length; i++) flushing[i].dirty = false;
}

Where it lives

packages/core/src/core/OIMCarrierKeyedEmitter.ts