Storage

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.

Updated Publisher Alien_AlgorithmsPine Script v6

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:

Redraw only the cells that received datapine
// 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:

Two producers, one rebuild per zonepine
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, and markRange(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 next begin(); valuesCopy() returns a copy you own and may sort.
  • contains(index) tests one index, and size() counts the marks.
  • clear() discards every mark and keeps the universe size. It costs O(universe), so a cycle should use begin() instead.
Never modify values()
The list and the per-index stamps describe each other. Appending to, sorting or clearing the view corrupts both. Take 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() and markRange() silently ignore an index outside the universe. A DirtySet created with op.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 of mark(), without bounds checks. The caller guarantees the index is inside the universe and that begin() has run once; a bad index raises a Pine array error instead of being ignored.

Calls

CallWhat 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.

OptiPine is a performance library for Pine Script. TradingView's memory, execution and collection limits still apply. © Alien_Algorithms.