arizona_diff (arizona v0.4.0)
View SourceComputes the minimal set of patch operations between an old snapshot and a freshly evaluated template.
Three entry points, each more powerful than the last:
diff/2-- bare diff: walks new dynamics against old, emits patch ops.diff/3-- adds aViewsaccumulator so nested stateful children can be tracked across the recursion.diff/4-- adds aChangedset of dirty bindings, so dynamics whose dependencies didn't change are skipped entirely (the fast path).
Each op is a list whose first element is one of the ?OP_* codes defined
in arizona.hrl; the rest are operands consumed by the JS client. The
op codes:
?OP_TEXT, ?OP_SET_ATTR, ?OP_REM_ATTR, ?OP_REMOVE_NODE, ?OP_INSERT,
?OP_REMOVE, ?OP_ITEM_PATCH, ?OP_REPLACE, ?OP_MOVE, ?OP_LIST_PATCH?each list vs. stream diffing
A ?each over a plain list is unkeyed: its items render as bare HTML
with no az-key, so there is no per-item DOM address to patch. Any change
(item content or list length) therefore re-renders the whole list with a
single ?OP_TEXT patching the slot's <!--az:X-->...<!--/az--> marker
content; an unchanged list emits nothing. (The marker-aware ?OP_TEXT is
what every container full render uses: a plain-list each is anchored in a
content slot, so an innerHTML write on the fallback enclosing element would
clobber the slot's static siblings.)
Use an arizona_stream when you need keyed, incremental updates --
per-item ?OP_ITEM_PATCH/?OP_INSERT/?OP_REMOVE/?OP_MOVE and stable
item identity (e.g. to preserve a stateful child's state across updates). A
plain list's full re-render re-mounts any such children.
Stream diffing
Streams ship with a queue of pending mutations (insert, delete,
update, move, reorder, reset). The diff drains that queue into
the corresponding ops, enforces visibility limits, and uses Longest
Increasing Subsequence (LIS) to emit the minimum number of ?OP_MOVEs
for a reorder.
Az-nodiff safety
Templates compiled with az-nodiff carry diff => false and dynamics
with Az = undefined. A ROOT snapshot with diff => false short-circuits
diff/2,3,4 before any dynamic is inspected. A SLOT holding such a template
is still diffed as a slot: when it re-runs and lands on the same az-nodiff element
(arizona_template:same_nodiff/3) the old render stands and the template's
own dynamics are never evaluated; when it switches to anything else it is
replaced through its markers. Either way no op targets an undefined Az.
A diffable template can also carry an individual Az = undefined dynamic:
a content slot inside a raw-text element (script/style/textarea/title),
where HTML comment markers would become literal content. Such a slot is
render-once -- diff_dynamics/3 and diff_dynamics_v/5 skip any undefined
Az dynamic, so it is never re-evaluated and never produces an op. The
per-item and child-view walkers (diff_item_dynamics_v/3,
diff_child_dynamics/3) skip it the same way; the one place such a change
IS delivered is a plain-list ?each, whose wholesale ?OP_TEXT re-render
ships raw-text content as plain HTML (so a markerless item slot routes the
list to that fallback instead of a per-item patch).
Summary
Functions
Carries a skipped value's child views (and their live subtrees) from the old
views map into the new one, so a value that is kept rather than re-evaluated
does not have its views pruned. Also used by arizona_eval for a child view it
keeps without calling handle_update/3.
Returns true when any key in ChangedKeys appears in Deps. Used by
diff/4 and the per-item skipping renderer to decide whether a dynamic
needs re-evaluation. The changed set is fixed for a whole dynamics walk
while every slot brings its own deps map, so the caller materializes the
changed KEYS once (maps:keys/1, one pass in C) and each slot then costs
only is_map_key/2 probes -- no per-slot iterator stepping or key-list
allocation, both of which measured 20-30% slower at every small-map shape
tried.
Bare diff: returns {Ops, NewSnapshot} for a new template against an old
snapshot.
Diff with view tracking: threads a Views map through the recursion so
nested stateful children are accumulated alongside the patch ops.
Dependency-aware diff: takes a Changed map of dirty binding keys and
skips dynamics whose stored deps don't intersect with it.
Types
Functions
-spec carry_skipped_view(Value, Views) -> Views1 when Value :: term(), Views :: {map(), map()}, Views1 :: {map(), map()}.
Carries a skipped value's child views (and their live subtrees) from the old
views map into the new one, so a value that is kept rather than re-evaluated
does not have its views pruned. Also used by arizona_eval for a child view it
keeps without calling handle_update/3.
Returns true when any key in ChangedKeys appears in Deps. Used by
diff/4 and the per-item skipping renderer to decide whether a dynamic
needs re-evaluation. The changed set is fixed for a whole dynamics walk
while every slot brings its own deps map, so the caller materializes the
changed KEYS once (maps:keys/1, one pass in C) and each slot then costs
only is_map_key/2 probes -- no per-slot iterator stepping or key-list
allocation, both of which measured 20-30% slower at every small-map shape
tried.
-spec diff(Template, OldSnapshot) -> {Ops, NewSnapshot} when Template :: arizona_template:template(), OldSnapshot :: arizona_template:snapshot(), Ops :: [op()], NewSnapshot :: arizona_template:snapshot().
Bare diff: returns {Ops, NewSnapshot} for a new template against an old
snapshot.
Short-circuits to {[], OldSnap} when the old snapshot carries
diff => false (set by az-nodiff).
-spec diff(Template, OldSnapshot, Views) -> {Ops, NewSnapshot, Views1} when Template :: arizona_template:template(), OldSnapshot :: arizona_template:snapshot(), Views :: map(), Ops :: [op()], NewSnapshot :: arizona_template:snapshot(), Views1 :: map().
Diff with view tracking: threads a Views map through the recursion so
nested stateful children are accumulated alongside the patch ops.
-spec diff(Template, OldSnapshot, Views, Changed) -> {Ops, NewSnapshot, Views1} when Template :: arizona_template:template(), OldSnapshot :: arizona_template:snapshot(), Views :: map(), Changed :: map(), Ops :: [op()], NewSnapshot :: arizona_template:snapshot(), Views1 :: map().
Dependency-aware diff: takes a Changed map of dirty binding keys and
skips dynamics whose stored deps don't intersect with it.
This is the production fast path -- only the dynamics actually affected by the bindings that changed are re-evaluated.