DirtySet
A work list for sparse updates. Mark what changed, visit each marked index once, and never scan or clear the entries that did not change.
Large state, small change
A dashboard may hold 10,000 cells while a handful change on a given bar, and an object system may need to refresh only a few of its entries. The usual dirty-flag array has to be scanned in full to find the changed entries, and cleared in full before the next bar.
A DirtySet keeps only the indices that were marked, drops duplicate marks, and starts a new cycle without clearing anything. Work scales with the number of changed entries, not the size of the collection. With 1% of entries changed, it ran 93% faster in our tests than clearing and scanning the full universe.
It is a work list only. It stores no payload and maps no IDs.
Mark a dense index
When the natural address is already an index from 0 upwards, such as a cell number, mark it directly:
// Pseudocode: updatedCells holds the cell numbers that changed on this bar,
// and redrawCell() draws one cell.
var op.DirtySet changedCells = op.dirtySet(400)
// Start this bar's cycle. Last bar's marks are gone, nothing is cleared.
changedCells.begin()
changedCells.markMany(updatedCells)
for cell in changedCells.values()
redrawCell(cell)With a StablePool
Objects identified by an ID need a slot first. Here a StablePool resolves the zone ID, the arrays hold the zone data, and the DirtySet schedules the slots that need rebuilding. The event values are pseudocode:
int MAX_ZONES = 50000
var op.StablePool zones = op.stablePool()
var op.DirtySet dirtySlots = op.dirtySet(MAX_ZONES)
var array<float> tops = array.new<float>()
var array<float> bottoms = array.new<float>()
var array<float> midpoints = array.new<float>()
// Start this bar's sparse-work cycle.
dirtySlots.begin()
if zoneGeometryChanged
// StablePool converts the logical ID into a reusable physical slot.
[zoneSlot, created] = zones.acquire(zoneId)
if created
op.ensureSizeFloat(tops, zoneSlot + 1, na)
op.ensureSizeFloat(bottoms, zoneSlot + 1, na)
op.ensureSizeFloat(midpoints, zoneSlot + 1, na)
tops.set(zoneSlot, newTop)
bottoms.set(zoneSlot, newBottom)
dirtySlots.mark(zoneSlot)
if zoneStyleChanged
int styleSlot = zones.find(zoneId)
if styleSlot >= 0
dirtySlots.mark(styleSlot) // A second mark of the same slot is ignored.
// Process only the distinct physical slots marked during this bar.
for dirtySlot in dirtySlots.values()
float midpoint = (tops.get(dirtySlot) + bottoms.get(dirtySlot)) * 0.5
midpoints.set(dirtySlot, midpoint)
redrawZone(zones.keyAt(dirtySlot), midpoint)
// Output: one zone is rebuilt once even if geometry and style both mark it.Several producers can mark work, and one consumer then updates each affected cell, drawing or record once.
How a cycle works
begin()starts a cycle in O(1). Each index carries the cycle it was last marked in, so starting a new cycle makes every old mark stale at once.mark(index)returns true only for the first mark of that index in the cycle.markMany(indices)returns how many indices it added, andmarkRange(first, last)marks an inclusive range in either order.values()lists the marked indices in the order they were first marked. It is a read-only view, valid until the nextbegin();valuesCopy()returns a copy you own and may sort.contains(index)tests one index, andsize()counts the marks.clear()discards every mark and keeps the universe size. It costs O(universe), so a cycle should usebegin()instead.
valuesCopy() when you need a different order.The universe
The universe is the range of valid indices, from 0 to universeSize - 1, and may not exceed 100,000 entries.
mark(),markMany()andmarkRange()silently ignore an index outside the universe. A DirtySet created withop.dirtySet()and no size has an empty universe, so every mark is ignored.begin(universeSize)resizes it for the new cycle. Growing keeps the existing stamps; shrinking only narrows the accepted range and keeps the allocation.markUnchecked(index)is the Tier 3 version ofmark(), without bounds checks. The caller guarantees the index is inside the universe and thatbegin()has run once; a bad index raises a Pine array error instead of being ignored.
Calls
| Call | What it does |
|---|---|
op.dirtySet(universeSize = 0) | Creates a set over indices 0 to universeSize - 1. |
dirty.begin(universeSize = na) | Starts a cycle, optionally resizing the universe. |
dirty.mark(index) | Marks one index. True only for its first mark in this cycle. |
dirty.markMany(indices), dirty.markRange(first, last) | Mark a batch or an inclusive range. markMany() returns how many it added. |
dirty.markUnchecked(index) | Tier 3. mark() without bounds checks. |
dirty.contains(index), dirty.size() | Test one index and count the marks. |
dirty.values(), dirty.valuesCopy(output = na) | The marked indices in discovery order, as a borrowed view or a copy. |
dirty.universe(), dirty.cycle(), dirty.clear() | Universe size, cycle counter, and discarding every mark. |
Every signature is in the API Reference. Next: Typed Stores.