Skip to content

Caching in validation

Technique

Caching in validation

ArcGIS runs updateParameters and updateMessages on every dialog change, including every keystroke. Cache the expensive read so a typing user is not waiting on a cursor.

The problem

The Quick Start tool populates a dropdown from the distinct values in a field:

def updateParameters(self, parameters):
    fc_in      = parameters[0]   # p_00
    field      = parameters[1]   # p_01
    value_pick = parameters[2]   # p_02

    if fc_in.value:
        fc_in_path = arcs.param.to_path(fc_in)
        arcs.param.cascade_clear(field, [value_pick])
        if arcs.param.state(field) in ("pending", "settled") and field.value:
            values = arcs.flds.unique_values(fc_in_path, field.valueAsText)
            arcs.param.drop_populate(value_pick, values)

    return

unique_values opens a SearchCursor and reads every row. ArcGIS calls updateParameters again on each dialog change, so on a table of any size the dialog goes sluggish while the user types in an unrelated box.

The fix

Move the read into a module-level helper and let it remember its last answer. functools is part of the Python standard library, so there is nothing to install and nothing to vendor alongside the toolbox:

import functools


@functools.lru_cache(maxsize=1)
def _unique_values(fc_path, field_name):
    """The distinct values in one field, remembered between validation passes."""
    return tuple(arcs.flds.unique_values(fc_path, field_name))

Then call the helper instead:

values = _unique_values(fc_in_path, field.valueAsText)
arcs.param.drop_populate(value_pick, values)

The cursor now runs once per field the user picks, rather than once per keystroke.

What the decorator does

It stores the return value under the arguments it was called with. Call it again with the same arguments and it hands back the stored value without running the body.

LRU stands for least recently used, which is the rule the cache follows once it is full: the entry that has gone longest without a hit is the one thrown out to make room. maxsize sets how many answers it will hold before that rule starts applying, and at maxsize=1 there is only ever one, so the rule reduces to keeping the latest answer and discarding the previous one:

_unique_values(trails, "TRAIL_TYPE")   # runs the cursor, stores the result
_unique_values(trails, "TRAIL_TYPE")   # returns the stored result, no cursor
_unique_values(trails, "DISTRICT")     # different arguments: runs, evicts TRAIL_TYPE
_unique_values(trails, "TRAIL_TYPE")   # runs again, its entry is gone

One slot is the right size for a dialog. Only the current parameter values are ever wanted, and a user who switches fields and switches back has almost certainly changed something worth re-reading anyway.

Two rules

Pass hashable arguments. The decorator builds its key from the arguments, so every one of them must be hashable. Strings and numbers are; lists, dicts, arcpy.Parameter, and arcpy.mp.Map are not, and passing one raises TypeError: unhashable type. Resolve parameters to plain text at the call site and hand the helper those strings.

Return something immutable. The decorator returns the same object on every hit, so a caller who mutates it corrupts what the next caller receives. That is why the helper above wraps the result in tuple(...): unique_values returns a fresh list each call, and storing that list in a cache would make it shared.

What the cache cannot know

The key is the arguments, not the data. If someone edits the feature class or adds a field while the dialog is open, the arguments have not changed, so the stale answer is returned.

The cache also lives as long as the Python process, which in ArcGIS Pro is the whole application session rather than one dialog. It survives closing the tool, reopening it, and running it. A helper that takes no arguments at all holds a single answer for the entire session.

functools adds a cache_clear() method to the decorated function, so _unique_values.cache_clear() forces the next call to read again.

When not to cache

Anything the tool itself changes during the session. A cached arcs.flds.list_cols(fc) is wrong the moment arcs.flds.add_fld runs, and nothing about the call site will show it. Cache reads that describe inputs the tool only looks at, not outputs it builds.

ArcSmith does not cache for you

Every ArcSmith function reads live state on every call, deliberately. The library cannot know how much staleness a given dialog can tolerate, and a hidden process-wide cache inside a library is difficult to reason about from the call site. The decision belongs in the toolbox, where it is visible.