Caching in validation
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:
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.