arizona_diff (arizona v0.4.0)

View Source

Computes 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 a Views accumulator so nested stateful children can be tracked across the recursion.
  • diff/4 -- adds a Changed set 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

op()

-nominal op() :: [integer() | binary() | term()].

Functions

carry_skipped_view(Value, Views)

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

deps_changed(Deps, ChangedKeys)

-spec deps_changed(Deps, ChangedKeys) -> boolean() when Deps :: map(), ChangedKeys :: [term()].

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.

diff(Template, OldSnapshot)

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

diff(Template, OldSnapshot, Views)

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

diff(Template, OldSnapshot, Views, Changed)

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