File: RenderTree\RenderTreeDiffBuilder.cs
Web Access
Project: src\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable warnings
 
using System.Diagnostics;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
 
namespace Microsoft.AspNetCore.Components.RenderTree;
 
internal static class RenderTreeDiffBuilder
{
    enum DiffAction { Match, Insert, Delete }
 
    // We use int.MinValue to signal this special case because (1) it would never be used by
    // the Razor compiler or by accident in developer code, and (2) we know it will always
    // hit the "old < new" code path during diffing so we only have to check for it in one place.
    public const int SystemAddedAttributeSequenceNumber = int.MinValue;
 
    public static RenderTreeDiff ComputeDiff(
        Renderer renderer,
        RenderBatchBuilder batchBuilder,
        int componentId,
        ArrayRange<RenderTreeFrame> oldTree,
        ArrayRange<RenderTreeFrame> newTree)
    {
        var editsBuffer = batchBuilder.EditsBuffer;
        var editsBufferStartLength = editsBuffer.Count;
 
        var diffContext = new DiffContext(renderer, batchBuilder, componentId, oldTree.Array, newTree.Array);
        AppendDiffEntriesForRange(ref diffContext, 0, oldTree.Count, 0, newTree.Count);
 
        var editsSegment = editsBuffer.ToSegment(editsBufferStartLength, editsBuffer.Count);
        var result = new RenderTreeDiff(componentId, editsSegment);
 
        return result;
    }
 
    public static void DisposeFrames(RenderBatchBuilder batchBuilder, int componentId, ArrayRange<RenderTreeFrame> frames)
        => DisposeFramesInRange(batchBuilder, componentId, frames.Array, 0, frames.Count);
 
    private static void AppendDiffEntriesForRange(
        ref DiffContext diffContext,
        int oldStartIndex, int oldEndIndexExcl,
        int newStartIndex, int newEndIndexExcl)
    {
        // This is deliberately a very large method. Parts of it could be factored out
        // into other private methods, but doing so comes at a consequential perf cost,
        // because it involves so much parameter passing. You can think of the code here
        // as being several private methods (delimited by #region) pre-inlined.
        //
        // A naive "extract methods"-type refactoring will worsen perf by about 10%. So,
        // if you plan to refactor this, be sure to benchmark the old and new versions
        // on Mono WebAssembly.
 
        var origOldStartIndex = oldStartIndex;
        var origNewStartIndex = newStartIndex;
        var hasMoreOld = oldEndIndexExcl > oldStartIndex;
        var hasMoreNew = newEndIndexExcl > newStartIndex;
        var prevOldSeq = -1;
        var prevNewSeq = -1;
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
        var matchWithNewTreeIndex = -1; // Only used when action == DiffAction.Match
        Dictionary<object, KeyedItemInfo> keyedItemInfos = null;
 
        try
        {
            while (hasMoreOld || hasMoreNew)
            {
                DiffAction action;
 
                #region "Read keys and sequence numbers"
                int oldSeq, newSeq;
                object oldKey, newKey;
                if (hasMoreOld)
                {
                    ref var oldFrame = ref oldTree[oldStartIndex];
                    oldSeq = oldFrame.SequenceField;
                    oldKey = KeyValue(ref oldFrame);
                }
                else
                {
                    oldSeq = int.MaxValue;
                    oldKey = null;
                }
 
                if (hasMoreNew)
                {
                    ref var newFrame = ref newTree[newStartIndex];
                    newSeq = newFrame.SequenceField;
                    newKey = KeyValue(ref newFrame);
                }
                else
                {
                    newSeq = int.MaxValue;
                    newKey = null;
                }
                #endregion
 
                // If there's a key on either side, prefer matching by key not sequence
                if (oldKey != null || newKey != null)
                {
                    #region "Get diff action by matching on key"
                    // Regardless of whether these two keys match, since you are using keys, we want to validate at this point that there are no clashes
                    // so ensure we've built the dictionary that will be used for lookups if any don't match
                    keyedItemInfos ??= BuildKeyToInfoLookup(diffContext, origOldStartIndex, oldEndIndexExcl, origNewStartIndex, newEndIndexExcl);
 
                    if (Equals(oldKey, newKey))
                    {
                        // Keys match
                        action = DiffAction.Match;
                        matchWithNewTreeIndex = newStartIndex;
                    }
                    else
                    {
                        // Keys don't match
                        var oldKeyItemInfo = oldKey != null ? keyedItemInfos[oldKey] : new KeyedItemInfo(-1, -1);
                        var newKeyItemInfo = newKey != null ? keyedItemInfos[newKey] : new KeyedItemInfo(-1, -1);
                        var oldKeyIsInNewTree = oldKeyItemInfo.NewIndex >= 0;
                        var newKeyIsInOldTree = newKeyItemInfo.OldIndex >= 0;
 
                        // If either key is not in the other tree, we can handle it as an insert or a delete
                        // on this iteration. We're only forced to use the move logic that's not the case
                        // (i.e., both keys are in both trees)
                        if (oldKeyIsInNewTree && newKeyIsInOldTree)
                        {
                            // It's a move
                            // Since the recipient of the diff script already has the old frame (the one with oldKey)
                            // at the current siblingIndex, recurse into oldKey and update its descendants in place.
                            // We re-order the frames afterwards.
                            action = DiffAction.Match;
                            matchWithNewTreeIndex = oldKeyItemInfo.NewIndex;
 
                            // Track the post-edit sibling indices of the moved items
                            // Since diffContext.SiblingIndex only increases, we can be sure the values we
                            // write at this point will remain correct, because there won't be any further
                            // insertions/deletions at smaller sibling indices.
                            keyedItemInfos[oldKey] = oldKeyItemInfo.WithOldSiblingIndex(diffContext.SiblingIndex);
                            keyedItemInfos[newKey] = newKeyItemInfo.WithNewSiblingIndex(diffContext.SiblingIndex);
                        }
                        else if (newKey == null)
                        {
                            // Only the old side is keyed.
                            // - If it was retained, we're inserting the new (unkeyed) side.
                            // - If it wasn't retained, we're deleting the old (keyed) side.
                            action = oldKeyIsInNewTree ? DiffAction.Insert : DiffAction.Delete;
                        }
                        else
                        {
                            // The rationale is complex here because there are a lot of different cases that
                            // merge together into the same rule.
                            // | old is keyed | newKeyIsInOldTree | oldKeyIsInNewTree  | Outcome   |
                            // | ------------ | ----------------- | ------------------ | --------- |
                            // | false        | true              | n/a - it's unkeyed | Delete    |
                            // | false        | false             | n/a - it's unkeyed | Insert    |
                            // | true         | true              | must be false[1]   | Delete    |
                            // | true         | false             | true               | Insert    |
                            // | true         | false             | false              | Insert[2] |
                            // [1] because we already know they were not both retained (checked above)
                            // [2] because neither was retained, so it's both an insert and a delete, and thus
                            //     we can pick either one to handle on this iteration, and the other will be
                            //     found and handled on the next iteration
                            // So all cases can be handled by the following simple criterion, which is pleasingly
                            // symetrically opposite the case for newKey==null.
                            action = newKeyIsInOldTree ? DiffAction.Delete : DiffAction.Insert;
                        }
 
                        // The above logic doesn't explicitly talk about whether or not we've run out of items on either
                        // side of the comparison. If we do run out of items on either side, the logic should result in
                        // us picking the remaining items from the other side to insert/delete. The following assertion is
                        // just to simplify debugging if future logic changes violate this.
                        Debug.Assert(action switch
                        {
                            DiffAction.Insert => hasMoreNew,
                            DiffAction.Delete => hasMoreOld,
                            _ => true,
                        }, "The chosen diff action is illegal because we've run out of items on the side being inserted/deleted");
                    }
                    #endregion
                }
                else
                {
                    #region "Get diff action by matching on sequence number"
                    // Neither side is keyed, so match by sequence number
                    if (oldSeq == newSeq)
                    {
                        // Sequences match
                        action = DiffAction.Match;
                        matchWithNewTreeIndex = newStartIndex;
                    }
                    else
                    {
                        // Sequences don't match
                        var oldLoopedBack = oldSeq <= prevOldSeq;
                        var newLoopedBack = newSeq <= prevNewSeq;
                        if (oldLoopedBack == newLoopedBack)
                        {
                            // Both sequences are proceeding through the same loop block, so do a simple
                            // preordered merge join (picking from whichever side brings us closer to being
                            // back in sync)
                            action = newSeq < oldSeq ? DiffAction.Insert : DiffAction.Delete;
 
                            if (oldLoopedBack)
                            {
                                // If both old and new have now looped back, we must reset their 'looped back'
                                // tracker so we can treat them as proceeding through the same loop block
                                prevOldSeq = -1;
                                prevNewSeq = -1;
                            }
                        }
                        else if (oldLoopedBack)
                        {
                            // Old sequence looped back but new one didn't
                            // The new sequence either has some extra trailing elements in the current loop block
                            // which we should insert, or omits some old trailing loop blocks which we should delete
                            // TODO: Find a way of not recomputing this next flag on every iteration
                            var newLoopsBackLater = false;
                            for (var testIndex = newStartIndex + 1; testIndex < newEndIndexExcl; testIndex++)
                            {
                                if (newTree[testIndex].SequenceField < newSeq)
                                {
                                    newLoopsBackLater = true;
                                    break;
                                }
                            }
 
                            // If the new sequence loops back later to an earlier point than this,
                            // then we know it's part of the existing loop block (so should be inserted).
                            // If not, then it's unrelated to the previous loop block (so we should treat
                            // the old items as trailing loop blocks to be removed).
                            action = newLoopsBackLater ? DiffAction.Insert : DiffAction.Delete;
                        }
                        else
                        {
                            // New sequence looped back but old one didn't
                            // The old sequence either has some extra trailing elements in the current loop block
                            // which we should delete, or the new sequence has extra trailing loop blocks which we
                            // should insert
                            // TODO: Find a way of not recomputing this next flag on every iteration
                            var oldLoopsBackLater = false;
                            for (var testIndex = oldStartIndex + 1; testIndex < oldEndIndexExcl; testIndex++)
                            {
                                if (oldTree[testIndex].SequenceField < oldSeq)
                                {
                                    oldLoopsBackLater = true;
                                    break;
                                }
                            }
 
                            // If the old sequence loops back later to an earlier point than this,
                            // then we know it's part of the existing loop block (so should be removed).
                            // If not, then it's unrelated to the previous loop block (so we should treat
                            // the new items as trailing loop blocks to be inserted).
                            action = oldLoopsBackLater ? DiffAction.Delete : DiffAction.Insert;
                        }
                    }
                    #endregion
                }
 
                #region "Apply diff action"
                switch (action)
                {
                    case DiffAction.Match:
                        AppendDiffEntriesForFramesWithSameSequence(ref diffContext, oldStartIndex, matchWithNewTreeIndex);
                        oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
                        newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
                        hasMoreOld = oldEndIndexExcl > oldStartIndex;
                        hasMoreNew = newEndIndexExcl > newStartIndex;
                        prevOldSeq = oldSeq;
                        prevNewSeq = newSeq;
                        break;
                    case DiffAction.Insert:
                        InsertNewFrame(ref diffContext, newStartIndex);
                        newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
                        hasMoreNew = newEndIndexExcl > newStartIndex;
                        prevNewSeq = newSeq;
                        break;
                    case DiffAction.Delete:
                        RemoveOldFrame(ref diffContext, oldStartIndex);
                        oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
                        hasMoreOld = oldEndIndexExcl > oldStartIndex;
                        prevOldSeq = oldSeq;
                        break;
                }
                #endregion
            }
 
            #region "Write permutations list"
            if (keyedItemInfos != null)
            {
                var hasPermutations = false;
                foreach (var keyValuePair in keyedItemInfos)
                {
                    var value = keyValuePair.Value;
                    if (value.OldSiblingIndex >= 0 && value.NewSiblingIndex >= 0)
                    {
                        // This item moved
                        hasPermutations = true;
                        diffContext.Edits.Append(
                            RenderTreeEdit.PermutationListEntry(value.OldSiblingIndex, value.NewSiblingIndex));
                    }
                }
 
                if (hasPermutations)
                {
                    // It's much easier for the recipient to handle if we're explicit about
                    // when the list is finished
                    diffContext.Edits.Append(RenderTreeEdit.PermutationListEnd());
                }
            }
            #endregion
        }
        finally
        {
            if (keyedItemInfos != null)
            {
                keyedItemInfos.Clear();
                diffContext.KeyedItemInfoDictionaryPool.Return(keyedItemInfos);
            }
        }
    }
 
    private static Dictionary<object, KeyedItemInfo> BuildKeyToInfoLookup(DiffContext diffContext, int oldStartIndex, int oldEndIndexExcl, int newStartIndex, int newEndIndexExcl)
    {
        var result = diffContext.KeyedItemInfoDictionaryPool.Get();
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
 
        while (oldStartIndex < oldEndIndexExcl)
        {
            ref var frame = ref oldTree[oldStartIndex];
            var key = KeyValue(ref frame);
            if (key != null)
            {
                if (result.ContainsKey(key))
                {
                    ThrowExceptionForDuplicateKey(key, frame);
                }
 
                result[key] = new KeyedItemInfo(oldStartIndex, -1);
            }
 
            oldStartIndex = NextSiblingIndex(frame, oldStartIndex);
        }
 
        while (newStartIndex < newEndIndexExcl)
        {
            ref var frame = ref newTree[newStartIndex];
            var key = KeyValue(ref frame);
            if (key != null)
            {
                if (!result.TryGetValue(key, out var existingEntry))
                {
                    result[key] = new KeyedItemInfo(-1, newStartIndex);
                }
                else
                {
                    if (existingEntry.NewIndex >= 0)
                    {
                        ThrowExceptionForDuplicateKey(key, frame);
                    }
 
                    result[key] = new KeyedItemInfo(existingEntry.OldIndex, newStartIndex);
                }
            }
 
            newStartIndex = NextSiblingIndex(frame, newStartIndex);
        }
 
        return result;
    }
 
    private static void ThrowExceptionForDuplicateKey(object key, in RenderTreeFrame frame)
    {
        switch (frame.FrameTypeField)
        {
            case RenderTreeFrameType.Component:
                throw new InvalidOperationException($"More than one sibling of component '{frame.ComponentTypeField}' has the same key value, '{key}'. Key values must be unique.");
 
            case RenderTreeFrameType.Element:
                throw new InvalidOperationException($"More than one sibling of element '{frame.ElementNameField}' has the same key value, '{key}'. Key values must be unique.");
 
            default:
                throw new InvalidOperationException($"More than one sibling has the same key value, '{key}'. Key values must be unique.");
        }
    }
 
    private static object KeyValue(ref RenderTreeFrame frame)
    {
        switch (frame.FrameTypeField)
        {
            case RenderTreeFrameType.Element:
                return frame.ElementKeyField;
            case RenderTreeFrameType.Component:
                return frame.ComponentKeyField;
            default:
                return null;
        }
    }
 
    // Handles the diff for attribute nodes only - this invariant is enforced by the caller.
    //
    // The diff for attributes is different because we allow attributes to appear in any order.
    // Put another way, the attributes list of an element or  component is *conceptually*
    // unordered. This is a case where we can produce a more minimal diff by avoiding
    // non-meaningful reorderings of attributes.
    private static void AppendAttributeDiffEntriesForRange(
        ref DiffContext diffContext,
        int oldStartIndex, int oldEndIndexExcl,
        int newStartIndex, int newEndIndexExcl)
    {
        // The overhead of the dictionary used by AppendAttributeDiffEntriesForRangeSlow is
        // significant, so we want to try and do a merge-join if possible, but fall back to
        // a hash-join if not. We'll do a merge join until we hit a case we can't handle and
        // then fall back to the slow path.
        //
        // Also since duplicate attributes are not legal, we don't need to care about loops or
        // the more complicated scenarios handled by AppendDiffEntriesForRange.
        //
        // We also assume that we won't see an attribute occur with different sequence numbers
        // in the old and new sequences. It will be handled correct, but will generate a suboptimal
        // diff.
        var hasMoreOld = oldEndIndexExcl > oldStartIndex;
        var hasMoreNew = newEndIndexExcl > newStartIndex;
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
 
        while (hasMoreOld || hasMoreNew)
        {
            var (oldSeq, oldAttributeName) = hasMoreOld
                ? (oldTree[oldStartIndex].SequenceField, oldTree[oldStartIndex].AttributeNameField)
                : (int.MaxValue, null);
            var (newSeq, newAttributeName) = hasMoreNew
                ? (newTree[newStartIndex].SequenceField, newTree[newStartIndex].AttributeNameField)
                : (int.MaxValue, null);
 
            if (oldSeq == newSeq &&
                string.Equals(oldAttributeName, newAttributeName, StringComparison.Ordinal))
            {
                // These two attributes have the same sequence and name. Keep merging.
                AppendDiffEntriesForAttributeFrame(ref diffContext, oldStartIndex, newStartIndex);
 
                oldStartIndex++;
                newStartIndex++;
                hasMoreOld = oldEndIndexExcl > oldStartIndex;
                hasMoreNew = newEndIndexExcl > newStartIndex;
            }
            else if (oldSeq < newSeq)
            {
                if (oldSeq == SystemAddedAttributeSequenceNumber)
                {
                    // This special sequence number means that we can't rely on the sequence numbers
                    // for matching and are forced to fall back on the dictionary-based join in order
                    // to produce an optimal diff. If we didn't we'd likely produce a diff that removes
                    // and then re-adds the same attribute.
                    // We use the special sequence number to signal it because it adds almost no cost
                    // to check for it only in this one case.
                    AppendAttributeDiffEntriesForRangeSlow(
                        ref diffContext,
                        oldStartIndex, oldEndIndexExcl,
                        newStartIndex, newEndIndexExcl);
                    return;
                }
 
                // An attribute was removed compared to the old sequence.
                RemoveOldFrame(ref diffContext, oldStartIndex);
 
                oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
                hasMoreOld = oldEndIndexExcl > oldStartIndex;
            }
            else if (oldSeq > newSeq)
            {
                // An attribute was added compared to the new sequence.
                InsertNewFrame(ref diffContext, newStartIndex);
 
                newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
                hasMoreNew = newEndIndexExcl > newStartIndex;
            }
            else
            {
                // These two attributes have the same sequence and different names. This is
                // a failure case for merge-join, fall back to the slow path.
                AppendAttributeDiffEntriesForRangeSlow(
                    ref diffContext,
                    oldStartIndex, oldEndIndexExcl,
                    newStartIndex, newEndIndexExcl);
                return;
            }
        }
    }
 
    private static void AppendAttributeDiffEntriesForRangeSlow(
        ref DiffContext diffContext,
        int oldStartIndex, int oldEndIndexExcl,
        int newStartIndex, int newEndIndexExcl)
    {
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
 
        // Slow version of AppendAttributeDiffEntriesForRange that uses a dictionary.
        // Algorithm:
        //
        // 1. iterate through the 'new' tree and add all attributes to the attributes set
        // 2. iterate through the 'old' tree, removing matching attributes from set, and diffing
        // 3. iterate through the remaining attributes in the set and add them
        for (var i = newStartIndex; i < newEndIndexExcl; i++)
        {
            diffContext.AttributeDiffSet[newTree[i].AttributeNameField] = i;
        }
 
        for (var i = oldStartIndex; i < oldEndIndexExcl; i++)
        {
            var oldName = oldTree[i].AttributeNameField;
            if (diffContext.AttributeDiffSet.TryGetValue(oldName, out var matchIndex))
            {
                // Has a match in the new tree, look for a diff
                AppendDiffEntriesForAttributeFrame(ref diffContext, i, matchIndex);
                diffContext.AttributeDiffSet.Remove(oldName);
            }
            else
            {
                // No match in the new tree, remove old attribute
                RemoveOldFrame(ref diffContext, i);
            }
        }
 
        foreach (var kvp in diffContext.AttributeDiffSet)
        {
            // No match in the old tree
            InsertNewFrame(ref diffContext, kvp.Value);
        }
 
        // We should have processed any additions at this point. Reset for the next batch.
        diffContext.AttributeDiffSet.Clear();
    }
 
    private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
    {
        switch (frame.FrameTypeField)
        {
            case RenderTreeFrameType.Component:
                return frameIndex + frame.ComponentSubtreeLengthField;
            case RenderTreeFrameType.Element:
                return frameIndex + frame.ElementSubtreeLengthField;
            case RenderTreeFrameType.Region:
                return frameIndex + frame.RegionSubtreeLengthField;
            default:
                return frameIndex + 1;
        }
    }
 
    private static void AppendDiffEntriesForFramesWithSameSequence(
        ref DiffContext diffContext,
        int oldFrameIndex,
        int newFrameIndex)
    {
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
        ref var oldFrame = ref oldTree[oldFrameIndex];
        ref var newFrame = ref newTree[newFrameIndex];
 
        // This can't happen for sequence-matched frames from .razor components, but it can happen if you write your
        // builder logic manually or if two dissimilar frames matched by key. Treat as completely unrelated.
        var newFrameType = newFrame.FrameTypeField;
        if (oldFrame.FrameTypeField != newFrameType)
        {
            InsertNewFrame(ref diffContext, newFrameIndex);
            RemoveOldFrame(ref diffContext, oldFrameIndex);
            return;
        }
 
        // We can assume that the old and new frames are of the same type, because they correspond
        // to the same sequence number (and if not, the behaviour is undefined).
        // TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations.
        //       It should only be a matter of calling RemoveOldFrame+InsertNewFrame
        switch (newFrameType)
        {
            case RenderTreeFrameType.Text:
                {
                    var oldText = oldFrame.TextContentField;
                    var newText = newFrame.TextContentField;
                    if (!string.Equals(oldText, newText, StringComparison.Ordinal))
                    {
                        var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
                        diffContext.Edits.Append(RenderTreeEdit.UpdateText(diffContext.SiblingIndex, referenceFrameIndex));
                    }
                    diffContext.SiblingIndex++;
                    break;
                }
 
            case RenderTreeFrameType.Markup:
                {
                    var oldMarkup = oldFrame.MarkupContentField;
                    var newMarkup = newFrame.MarkupContentField;
                    if (!string.Equals(oldMarkup, newMarkup, StringComparison.Ordinal))
                    {
                        var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
                        diffContext.Edits.Append(RenderTreeEdit.UpdateMarkup(diffContext.SiblingIndex, referenceFrameIndex));
                    }
                    diffContext.SiblingIndex++;
                    break;
                }
 
            case RenderTreeFrameType.Element:
                {
                    var oldElementName = oldFrame.ElementNameField;
                    var newElementName = newFrame.ElementNameField;
                    if (string.Equals(oldElementName, newElementName, StringComparison.Ordinal))
                    {
                        var oldFrameAttributesEndIndexExcl = GetAttributesEndIndexExclusive(oldTree, oldFrameIndex);
                        var newFrameAttributesEndIndexExcl = GetAttributesEndIndexExclusive(newTree, newFrameIndex);
 
                        // Diff the attributes
                        AppendAttributeDiffEntriesForRange(
                            ref diffContext,
                            oldFrameIndex + 1, oldFrameAttributesEndIndexExcl,
                            newFrameIndex + 1, newFrameAttributesEndIndexExcl);
 
                        // Diff the children
                        var oldFrameChildrenEndIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLengthField;
                        var newFrameChildrenEndIndexExcl = newFrameIndex + newFrame.ElementSubtreeLengthField;
                        var hasChildrenToProcess =
                            oldFrameChildrenEndIndexExcl > oldFrameAttributesEndIndexExcl ||
                            newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl;
                        if (hasChildrenToProcess)
                        {
                            diffContext.Edits.Append(RenderTreeEdit.StepIn(diffContext.SiblingIndex));
                            var prevSiblingIndex = diffContext.SiblingIndex;
                            diffContext.SiblingIndex = 0;
                            AppendDiffEntriesForRange(
                                ref diffContext,
                                oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl,
                                newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl);
                            AppendStepOut(ref diffContext);
                            diffContext.SiblingIndex = prevSiblingIndex + 1;
                        }
                        else
                        {
                            diffContext.SiblingIndex++;
                        }
                    }
                    else
                    {
                        // Elements with different names are treated as completely unrelated
                        RemoveOldFrame(ref diffContext, oldFrameIndex);
                        InsertNewFrame(ref diffContext, newFrameIndex);
                    }
                    break;
                }
 
            case RenderTreeFrameType.Region:
                {
                    AppendDiffEntriesForRange(
                        ref diffContext,
                        oldFrameIndex + 1, oldFrameIndex + oldFrame.RegionSubtreeLengthField,
                        newFrameIndex + 1, newFrameIndex + newFrame.RegionSubtreeLengthField);
                    break;
                }
 
            case RenderTreeFrameType.Component:
                {
                    if (oldFrame.ComponentTypeField == newFrame.ComponentTypeField)
                    {
                        // As an important rendering optimization, we want to skip parameter update
                        // notifications if we know for sure they haven't changed/mutated. The
                        // "MayHaveChangedSince" logic is conservative, in that it returns true if
                        // any parameter is of a type we don't know is immutable. In this case
                        // we call SetParameters and it's up to the recipient to implement
                        // whatever change-detection logic they want. Currently we only supply the new
                        // set of parameters and assume the recipient has enough info to do whatever
                        // comparisons it wants with the old values. Later we could choose to pass the
                        // old parameter values if we wanted. By default, components always rerender
                        // after any SetParameters call, which is safe but now always optimal for perf.
 
                        // When performing hot reload, we want to force all components to re-render.
                        // We do this using two mechanisms - we call SetParametersAsync even if the parameters
                        // are unchanged and we ignore ComponentBase.ShouldRender.
                        // Furthermore, when a hot reload edit removes component parameters, the component should be
                        // disposed and reinstantiated. This allows the component's construction logic to correctly
                        // re-initialize the removed parameter properties.
 
                        var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldFrameIndex);
                        var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
                        var newParameters = new ParameterView(newParametersLifetime, newTree, newFrameIndex);
                        var isHotReload = HotReloadManager.Default.MetadataUpdateSupported && diffContext.Renderer.IsRenderingOnMetadataUpdate;
 
                        if (isHotReload && newParameters.HasRemovedDirectParameters(oldParameters))
                        {
                            // Components with parameters removed during a hot reload edit should be disposed and reinstantiated
                            RemoveOldFrame(ref diffContext, oldFrameIndex);
                            InsertNewFrame(ref diffContext, newFrameIndex);
                        }
                        else
                        {
                            var componentState = oldFrame.ComponentStateField;
 
                            // Preserve the actual componentInstance
                            newFrame.ComponentStateField = componentState;
                            newFrame.ComponentIdField = componentState.ComponentId;
 
                            if (!newParameters.DefinitelyEquals(oldParameters) || isHotReload)
                            {
                                componentState.SetDirectParameters(newParameters);
                            }
 
                            diffContext.SiblingIndex++;
                        }
                    }
                    else
                    {
                        // Child components of different types are treated as completely unrelated
                        RemoveOldFrame(ref diffContext, oldFrameIndex);
                        InsertNewFrame(ref diffContext, newFrameIndex);
                    }
                    break;
                }
 
            case RenderTreeFrameType.ElementReferenceCapture:
                {
                    // We could preserve the ElementReferenceCaptureId from the old frame to the new frame,
                    // and even call newFrame.ElementReferenceCaptureAction(id) each time in case it wants
                    // to do something different with the ID. However there's no known use case for
                    // that, so presently the rule is that for any given element, the reference
                    // capture action is only invoked once.
                    break;
                }
 
            case RenderTreeFrameType.NamedEvent:
                {
                    // We don't have a use case for the event types changing, so we don't even check that. We assume for a given sequence number
                    // the event type is always a constant. What can change is the frame index and the assigned name.
                    if (oldFrameIndex != newFrameIndex
                        || !string.Equals(oldFrame.NamedEventAssignedName, newFrame.NamedEventAssignedName, StringComparison.Ordinal))
                    {
                        // We could track the updates as a concept in its own right, but this situation will be uncommon,
                        // so it's enough to treat it as a delete+add
                        diffContext.BatchBuilder.RemoveNamedEvent(diffContext.ComponentId, oldFrameIndex, ref oldFrame);
                        diffContext.BatchBuilder.AddNamedEvent(diffContext.ComponentId, newFrameIndex, ref newFrame);
                    }
 
                    break;
                }
 
            // We don't handle attributes here, they have their own diff logic.
            // See AppendDiffEntriesForAttributeFrame
            default:
                throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameTypeField}");
        }
    }
 
    // This should only be called for attributes that have the same name. This is an
    // invariant maintained by the callers.
    private static void AppendDiffEntriesForAttributeFrame(
        ref DiffContext diffContext,
        int oldFrameIndex,
        int newFrameIndex)
    {
        var oldTree = diffContext.OldTree;
        var newTree = diffContext.NewTree;
        ref var oldFrame = ref oldTree[oldFrameIndex];
        ref var newFrame = ref newTree[newFrameIndex];
 
        // Using Equals to account for string comparisons, nulls, etc.
        var valueChanged = !Equals(oldFrame.AttributeValueField, newFrame.AttributeValueField);
        if (valueChanged)
        {
            InitializeNewAttributeFrame(ref diffContext, ref newFrame);
            var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
            diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
 
            // If we're replacing an old event handler ID with a new one, register the old one for disposal,
            // plus keep track of the old->new chain until the old one is fully disposed
            if (oldFrame.AttributeEventHandlerIdField > 0)
            {
                diffContext.Renderer.TrackReplacedEventHandlerId(oldFrame.AttributeEventHandlerIdField, newFrame.AttributeEventHandlerIdField);
                diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerIdField);
            }
        }
        else if (oldFrame.AttributeEventHandlerIdField > 0)
        {
            // Retain the event handler ID by copying the old frame over the new frame.
            // this will prevent us from needing to dispose the old event handler
            // since it was unchanged.
            newFrame = oldFrame;
        }
    }
 
    private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex)
    {
        var newTree = diffContext.NewTree;
        ref var newFrame = ref newTree[newFrameIndex];
        switch (newFrame.FrameTypeField)
        {
            case RenderTreeFrameType.Attribute:
                {
                    InitializeNewAttributeFrame(ref diffContext, ref newFrame);
                    var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
                    diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
                    break;
                }
            case RenderTreeFrameType.Component:
            case RenderTreeFrameType.Element:
                {
                    InitializeNewSubtree(ref diffContext, newFrameIndex);
                    var referenceFrameIndex = diffContext.ReferenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLengthField);
                    diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
                    diffContext.SiblingIndex++;
                    break;
                }
            case RenderTreeFrameType.Region:
                {
                    var regionChildFrameIndex = newFrameIndex + 1;
                    var regionChildFrameEndIndexExcl = newFrameIndex + newFrame.RegionSubtreeLengthField;
                    while (regionChildFrameIndex < regionChildFrameEndIndexExcl)
                    {
                        InsertNewFrame(ref diffContext, regionChildFrameIndex);
                        regionChildFrameIndex = NextSiblingIndex(newTree[regionChildFrameIndex], regionChildFrameIndex);
                    }
                    break;
                }
            case RenderTreeFrameType.Text:
            case RenderTreeFrameType.Markup:
                {
                    var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
                    diffContext.Edits.Append(RenderTreeEdit.PrependFrame(diffContext.SiblingIndex, referenceFrameIndex));
                    diffContext.SiblingIndex++;
                    break;
                }
            case RenderTreeFrameType.ElementReferenceCapture:
                {
                    InitializeNewElementReferenceCaptureFrame(ref diffContext, ref newFrame);
                    break;
                }
            case RenderTreeFrameType.ComponentReferenceCapture:
                {
                    InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame);
                    break;
                }
            case RenderTreeFrameType.NamedEvent:
                {
                    InitializeNewNamedEvent(ref diffContext, newFrameIndex);
                    break;
                }
            default:
                throw new NotImplementedException($"Unexpected frame type during {nameof(InsertNewFrame)}: {newFrame.FrameTypeField}");
        }
    }
 
    private static void RemoveOldFrame(ref DiffContext diffContext, int oldFrameIndex)
    {
        var oldTree = diffContext.OldTree;
        ref var oldFrame = ref oldTree[oldFrameIndex];
        switch (oldFrame.FrameTypeField)
        {
            case RenderTreeFrameType.Attribute:
                {
                    diffContext.Edits.Append(RenderTreeEdit.RemoveAttribute(diffContext.SiblingIndex, oldFrame.AttributeNameField));
                    if (oldFrame.AttributeEventHandlerIdField > 0)
                    {
                        diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerIdField);
                    }
                    break;
                }
            case RenderTreeFrameType.Component:
            case RenderTreeFrameType.Element:
                {
                    var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLengthField;
                    DisposeFramesInRange(diffContext.BatchBuilder, diffContext.ComponentId, oldTree, oldFrameIndex, endIndexExcl);
                    diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
                    break;
                }
            case RenderTreeFrameType.Region:
                {
                    var regionChildFrameIndex = oldFrameIndex + 1;
                    var regionChildFrameEndIndexExcl = oldFrameIndex + oldFrame.RegionSubtreeLengthField;
                    while (regionChildFrameIndex < regionChildFrameEndIndexExcl)
                    {
                        RemoveOldFrame(ref diffContext, regionChildFrameIndex);
                        regionChildFrameIndex = NextSiblingIndex(oldTree[regionChildFrameIndex], regionChildFrameIndex);
                    }
                    break;
                }
            case RenderTreeFrameType.Text:
            case RenderTreeFrameType.Markup:
                {
                    diffContext.Edits.Append(RenderTreeEdit.RemoveFrame(diffContext.SiblingIndex));
                    break;
                }
            case RenderTreeFrameType.NamedEvent:
                {
                    diffContext.BatchBuilder.RemoveNamedEvent(diffContext.ComponentId, oldFrameIndex, ref diffContext.OldTree[oldFrameIndex]);
                    break;
                }
            default:
                throw new NotImplementedException($"Unexpected frame type during {nameof(RemoveOldFrame)}: {oldFrame.FrameTypeField}");
        }
    }
 
    private static int GetAttributesEndIndexExclusive(RenderTreeFrame[] tree, int rootIndex)
    {
        var descendantsEndIndexExcl = rootIndex + tree[rootIndex].ElementSubtreeLengthField;
        var index = rootIndex + 1;
        for (; index < descendantsEndIndexExcl; index++)
        {
            if (tree[index].FrameTypeField != RenderTreeFrameType.Attribute)
            {
                break;
            }
        }
 
        return index;
    }
 
    private static void AppendStepOut(ref DiffContext diffContext)
    {
        // If the preceding frame is a StepIn, then the StepOut cancels it out
        var previousIndex = diffContext.Edits.Count - 1;
        if (previousIndex >= 0 && diffContext.Edits.Buffer[previousIndex].Type == RenderTreeEditType.StepIn)
        {
            diffContext.Edits.RemoveLast();
        }
        else
        {
            diffContext.Edits.Append(RenderTreeEdit.StepOut());
        }
    }
 
    private static void InitializeNewSubtree(ref DiffContext diffContext, int frameIndex)
    {
        var frames = diffContext.NewTree;
        var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLengthField;
        for (var i = frameIndex; i < endIndexExcl; i++)
        {
            ref var frame = ref frames[i];
            switch (frame.FrameTypeField)
            {
                case RenderTreeFrameType.Component:
                    InitializeNewComponentFrame(ref diffContext, i);
                    break;
                case RenderTreeFrameType.Attribute:
                    InitializeNewAttributeFrame(ref diffContext, ref frame);
                    break;
                case RenderTreeFrameType.ElementReferenceCapture:
                    InitializeNewElementReferenceCaptureFrame(ref diffContext, ref frame);
                    break;
                case RenderTreeFrameType.ComponentReferenceCapture:
                    InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref frame);
                    break;
                case RenderTreeFrameType.NamedEvent:
                    InitializeNewNamedEvent(ref diffContext, i);
                    break;
            }
        }
    }
 
    private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
    {
        var frames = diffContext.NewTree;
        ref var frame = ref frames[frameIndex];
        var parentComponentId = diffContext.ComponentId;
        var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(frames, frameIndex, parentComponentId);
 
        // Set initial parameters
        var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
        var initialParameters = new ParameterView(initialParametersLifetime, frames, frameIndex);
        childComponentState.SetDirectParameters(initialParameters);
    }
 
    private static void InitializeNewAttributeFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
    {
        // Any attribute with an event handler id will be callable via DOM events
        //
        // We're following a simple heuristic here that's reflected in the ts runtime
        // based on the common usage of attributes for DOM events.
        if ((newFrame.AttributeValueField is MulticastDelegate || newFrame.AttributeValueField is EventCallback) &&
            newFrame.AttributeNameField.Length >= 3 &&
            newFrame.AttributeNameField.StartsWith("on", StringComparison.Ordinal))
        {
            diffContext.Renderer.AssignEventHandlerId(diffContext.ComponentId, ref newFrame);
        }
    }
 
    private static void InitializeNewElementReferenceCaptureFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
    {
        var newElementReference = ElementReference.CreateWithUniqueId(diffContext.Renderer.ElementReferenceContext);
        newFrame.ElementReferenceCaptureIdField = newElementReference.Id;
        newFrame.ElementReferenceCaptureActionField(newElementReference);
    }
 
    private static void InitializeNewComponentReferenceCaptureFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
    {
        ref var parentFrame = ref diffContext.NewTree[newFrame.ComponentReferenceCaptureParentFrameIndexField];
        if (parentFrame.FrameTypeField != RenderTreeFrameType.Component)
        {
            // Should never happen, but will help with diagnosis if it does
            throw new InvalidOperationException($"{nameof(RenderTreeFrameType.ComponentReferenceCapture)} frame references invalid parent index.");
        }
 
        var componentInstance = parentFrame.Component;
        if (componentInstance == null)
        {
            // Should never happen, but will help with diagnosis if it does
            throw new InvalidOperationException($"Trying to initialize {nameof(RenderTreeFrameType.ComponentReferenceCapture)} frame before parent component was assigned.");
        }
 
        newFrame.ComponentReferenceCaptureActionField(componentInstance);
    }
 
    private static void InitializeNewNamedEvent(ref DiffContext diffContext, int newTreeFrameIndex)
    {
        diffContext.BatchBuilder.AddNamedEvent(diffContext.ComponentId, newTreeFrameIndex, ref diffContext.NewTree[newTreeFrameIndex]);
    }
 
    private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, int componentId, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
    {
        for (var i = startIndex; i < endIndexExcl; i++)
        {
            ref var frame = ref frames[i];
            if (frame.FrameTypeField == RenderTreeFrameType.Component && frame.ComponentStateField != null)
            {
                batchBuilder.ComponentDisposalQueue.Enqueue(frame.ComponentIdField);
            }
            else if (frame.FrameTypeField == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerIdField > 0)
            {
                batchBuilder.DisposedEventHandlerIds.Append(frame.AttributeEventHandlerIdField);
            }
            else if (frame.FrameTypeField == RenderTreeFrameType.NamedEvent)
            {
                batchBuilder.RemoveNamedEvent(componentId, i, ref frames[i]);
            }
        }
    }
 
    /// <summary>
    /// Exists only so that the various methods in this class can call each other without
    /// constantly building up long lists of parameters. Is private to this class, so the
    /// fact that it's a mutable struct is manageable.
    ///
    /// Always pass by ref to avoid copying, and because the 'SiblingIndex' is mutable.
    /// </summary>
    private struct DiffContext
    {
        public readonly Renderer Renderer;
        public readonly RenderBatchBuilder BatchBuilder;
        public readonly RenderTreeFrame[] OldTree;
        public readonly RenderTreeFrame[] NewTree;
        public readonly ArrayBuilder<RenderTreeEdit> Edits;
        public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
        public readonly Dictionary<string, int> AttributeDiffSet;
        public readonly StackObjectPool<Dictionary<object, KeyedItemInfo>> KeyedItemInfoDictionaryPool;
        public readonly int ComponentId;
        public int SiblingIndex;
 
        public DiffContext(
            Renderer renderer,
            RenderBatchBuilder batchBuilder,
            int componentId,
            RenderTreeFrame[] oldTree,
            RenderTreeFrame[] newTree)
        {
            Renderer = renderer;
            BatchBuilder = batchBuilder;
            ComponentId = componentId;
            OldTree = oldTree;
            NewTree = newTree;
            Edits = batchBuilder.EditsBuffer;
            ReferenceFrames = batchBuilder.ReferenceFramesBuffer;
            AttributeDiffSet = batchBuilder.AttributeDiffSet;
            KeyedItemInfoDictionaryPool = batchBuilder.KeyedItemInfoDictionaryPool;
            SiblingIndex = 0;
        }
    }
}