Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Why sequence numbers should relate to code line numbers, not execution order

Why sequence numbers should relate to code line numbers, not execution order

Or in other words, why you should hard-code sequence numbers, and not generate them programmatically.

Unlike .jsx files, .razor/.cshtml files are always compiled. This is potentially a great advantage for .razor, because we can use the compile step to inject information that makes things better or faster at runtime.

A key example of this are sequence numbers. These indicate to the runtime which outputs came from which distinct and ordered lines of code. The runtime uses this information to generate efficient tree diffs in linear time, which is far faster than is normally possible for a general tree diff algorithm.

Example

Consider the following simple .razor file:

@if (someFlag)
{
    <text>First</text>
}
Second

This compiles to something like the following:

if (someFlag)
{
    builder.AddContent(0, "First");
}
builder.AddContent(1, "Second");

When this executes for the first time, if someFlag == true, the builder receives:

Sequence Type Data
0 Text node First
1 Text node Second

Now imagine that someFlag becomes false, and we render again. This time the builder receives:

Sequence Type Data
1 Text node Second

When it performs a diff, it sees that the item at sequence 0 was removed, so it generates the following trivial edit script:

  • Remove the first text node

What goes wrong if you generate sequence numbers programmatically

Imagine instead that you wrote the following rendertree builder logic:

var seq = 0;

if (someFlag)
{
    builder.AddContent(seq++, "First");
}
builder.AddContent(seq++, "Second");

Now the first output would be:

Sequence Type Data
0 Text node First
1 Text node Second

... in other words, identical to before, so no issues yet. But then on the second render when someFlag==false, the output would be:

Sequence Type Data
0 Text node Second

This time, the diff algorithm will see that two changes have occurred, and will generate the following edit script:

  • Change the value of the first text node to Second
  • Remove the second text node

Generating the sequence numbers has lost all the useful information about where the if/else branches and loops were present in the original code, and has resulted in a diff that is now twice as long.

This is a very trivial example. In more realistic cases with complex and deeply nested structures, and especially with loops, the performance cost is more severe still. Instead of immediately identifying which loop blocks or branches have been inserted/removed, the diff algorithm would have to recurse deeply into your trees and would build far longer edit scripts, because you're misleading it about how the old and new structures relate to each other.

Questions

  • Q: Despite this, I still want to generate the sequence numbers dynamically. Can I?
    • A: You can, but it will make your app performance worse.
  • Q: Couldn't the framework make up its own sequence numbers automatically at runtime?
    • A: No. The necessary information doesn't exist unless it is captured at compile time. Please see the example above.
  • Q: I find it impractical to hard-code sequence numbers in really long blocks of manually-implemented RenderTreeBuilder logic. What should I do?
    • A: Don't write really long blocks of manually-implemented RenderTreeBuilder logic. Preferably use .razor files and let the compiler deal with this for you, or if you can't, split up your code into smaller pieces wrapped in OpenRegion/CloseRegion calls. Each region has its own separate space of sequence numbers, so you can restart from zero (or any other arbitrary number) inside each region if you want.
  • Q: OK, so I'm going to hardcode the sequence numbers. What actual numeric values should I use?
    • A: The diffing algorithm only cares that they should be an increasing sequence. It doesn't matter what initial value they start from, or whether there are gaps. One legitimate option would be to use the code line number as the sequence number. Or start from zero and increase by ones or hundreds or whatever interval you like.
  • Q: Why does Razor Components use sequence numbers, when other tree-diffing UI frameworks don't?
    • A: Because it makes diffing far faster, and Razor Components has the advantage of a compile step that deals with this automatically for people authoring .razor/.cshtml files.
@Andrzej-W

This comment has been minimized.

Copy link

Andrzej-W commented Mar 27, 2019

@SteveSandersonMS it was great explanation. Now everything is crystal clear. So far I was thinking that sequence numbers have to be consecutive. Thank you very much for your time and effort.

@Lupusa87

This comment has been minimized.

Copy link

Lupusa87 commented Mar 28, 2019

Thank you, it is much clear now.

I can't stop using buildrendertree because I found it very convenient and powerful, so all I will do now is hard coding current line numbers in buildrendertree methods after finish to write component code.

It is sad to know that this procedure should be done again after any changes in code because line numbers will be changed, but not big tragedy before better times :)

OpenRegion/CloseRegion is good advise, it can be used in injected methods which itself have not any loops or conditionals, so numbering once will be enough and not requires line numbers anymore, thank you.

This was helpful but there are some component related bugs and missing features I am going to raise issues for.

Hope this gist will be used in docs and my main concern not to cover buildrendertree with razor will be considered in future.

@honkmother

This comment has been minimized.

Copy link

honkmother commented Mar 29, 2019

An almost irrelevant thought: Can C#8.x (in the future) provide us some kind of keyword that gives us access to the scope's local line number? This could be useful for a number of other use case scenarios. Something similar has to already exist for exceptions, right?

@EdCharbeneau

This comment has been minimized.

Copy link

EdCharbeneau commented Mar 29, 2019

@SteveSandersonMS I think some from of this post, a less detailed version, needs to make its way into the Blazing Pizza workshop docs. You covered this in person at NDC London but it's not written anywhere, so it's a bit of tribal knowledge at this point.

@SQL-MisterMagoo

This comment has been minimized.

Copy link

SQL-MisterMagoo commented Mar 30, 2019

It seems like OpenRegion and CloseRegion were made internal in February last year

@shawty

This comment has been minimized.

Copy link

shawty commented Apr 4, 2019

Forgive me for being stupid here, but let's suppose I want to for example, generate a menu component from a database list, or an XML or something, I might very well do something like:

foreach(var item in allItems)
{
  builder.OpenElement(xx, "a");
  builder.AddAttribute(xx, "href");
  builder.AddContent(xx, item.url");
  builder.CloseElement(xx, "a");
}

( Yes I know the syntax is wrong... I'm just trying to illustrate a point :-D )

Anyway, back to the above, in this case I have little choice but to auto generate the id's, because I don't know ahead of time how many tags I might be adding?

I bring this up, because an experiment I'm working with, actually does this. Depending on the logged in user and the "roles" they have available, the menu is drawn differently. Currently I do have it implemented in a razor page, and to be fair that actually works well, but if I'd wanted to do the menu in the manner above what would be the advice?

My thoughts would be hard code the static stuff, and autogen only the loop portions, would that be correct?

@Lupusa87

This comment has been minimized.

Copy link

Lupusa87 commented May 7, 2019

You can just enter line number as sequence number parameter. Or numerate by yourself. If/loop does not matter, just numbers should increase in following LOCs from up to down.
Here is important that particular builder line of code always got same sequence number not depended on state changes.

@arivoir

This comment has been minimized.

Copy link

arivoir commented May 24, 2019

I have some doubt about the level of detail Blazor is able to reach to minimize the impact of the diff algorithm.

aspnet/AspNetCore.Docs#12504 (comment)

Specially in virtualizing components it would be desired to be able to preserve the elements that didn't change. I firstly thought it was related to the sequence number, but then I understood I was wrong. All the elements in a cycle must have the same number. So I'm wondering if there is any way to achieve more granularity in the elements so Blazor can identify every element thoroughly and avoid unwanted dom changes.

@guardrex

This comment has been minimized.

Copy link

guardrex commented May 26, 2019

@majedur-rahaman

This comment has been minimized.

Copy link

majedur-rahaman commented Aug 25, 2019

Best explanation! I agree with @SteveSandersonMS.
But yet, when the component RenderTreeBuilder is big then there might be problem after updating or removing any tag.
So i think there should be intellisense that auto put line number when i select code block just like code formatting.

@legistek

This comment has been minimized.

Copy link

legistek commented Sep 12, 2019

I'm dropping the OpenRegion/CloseRegion remark in the topic on aspnet/AspNetCore.Docs#12546. They aren't public ...

Why not? Seems like they would be useful.

@guardrex

This comment has been minimized.

Copy link

guardrex commented Sep 12, 2019

They were removed because they weren't public. Now, they are ... https://github.com/aspnet/AspNetCore/blob/057ec9cfae1831f91f1a0e01397059b20431db7b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs#L578-L597 ... so I'll open an issue to add them back.

[EDIT] ... just opened at aspnet/AspNetCore.Docs#14277.

@legistek

This comment has been minimized.

Copy link

legistek commented Sep 12, 2019

So I'm having trouble with iterative loops (e.g. for and foreach) and unnecessary renders, and I wonder if the sequence number has something to do with it.

Suppose I have a loop like this:

@foreach (var person in People)
{
    <PersonComponent Person=@person />    
}

I want to make sure that if the People collection changes, the list re-renders efficiently, i.e., I don't want every PersonComponent to have to re-render every time the collection changes. (Assume PersonComponent executes StateHasChanged when its Person parameter changes).

If I append to the end of People everything is fine. But if, for example, I insert a Person in front of the list, then all of the remaining PersonComponents re-render.

The problem, I think I've determined, lies in the way Blazor is re-using the PersonComponents. Rather than just insert a new PersonComponent for the new person, and leave the rest of them alone, it's iterating through the PersonComponents created in the last render and assigning the Persons to them in list order.

What I'm wondering is if there's a way around this through better use of the sequence number? When the list changes Is there a way to trick Blazor into reusing components so that they remain paired with their original list items whenever possible? Do we have any control at all over how Blazor reuses components?

I wonder if a robust solution to this really requires an ItemsControl paradigm a la WPF, wherein there a mechanism for associating stored visual elements with data and data - rather than sequence number - governs re-use of the visual. I'm not sure to what extent if any something like this can be implemented without going into the internals of Blazor. But without it I fear that very large collections with remotely complex visuals will have insurmountable performance issues.

@legistek

This comment has been minimized.

Copy link

legistek commented Sep 12, 2019

By the way, here's a project file that demonstrates the issue.

https://www.dropbox.com/s/kficzqanv92td99/ItemsCollectionExample.zip?dl=1

The Counter page is where things are happening.

As you can see what I do in the PersonComponent class is try to be efficient with rendering by overriding ShouldRender to only re-render when Person has changed.

When you press the various buttons you can see how many renders and calls to ShouldRender have occurred. When appending the components correctly get re-used, but when inserting or deleting at the beginning, not surprisingly every component gets a new Person assigned based on their position in the list.

UPDATE -
Well I answered my own question. It looks like the secret is to use the @key attribute. You guys really thought of everything didn't you? :)

@fenati

This comment has been minimized.

Copy link

fenati commented Sep 13, 2019

While I was reading this, I got an idea.

Couldn't we just use the CallerLineNumberAttribute to generate the sequence numbers automatically?
For example, if I'd create an extension method like this, i think we could avoid hardcoding the sequence numbers:

public static void AddContent(this RenderTreeBuilder builder, string textContent, [CallerLineNumber] int sequence = 0)
{
  builder.AddContent(sequence, textContent);
}

With such extension method, I could write this:

builder.AddContent("anything");

In this case, the sequence number would be the caller's line number automatically.

Some things to keep in mind with this technique:

  • Do not invoke multiple extension methods in the same line - though I would avoid that anyway to keep good readability.
  • If the RenderTreeBuilder instance is used in several methods, then the order of method definitions is important. The "root" method should be defined first and the called methods afterwards.
@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

SteveSandersonMS commented Sep 13, 2019

@legistek There's a feature for your exact scenario: @key. This tells the diff algorithm how to map the old items to the new ones. https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.0#use-key-to-control-the-preservation-of-elements-and-components

@fenati It's a good idea - one we considered before but ultimately decided against: aspnet/AspNetCore#10890

@legistek

This comment has been minimized.

Copy link

legistek commented Sep 13, 2019

Thanks @SteveSandersonMS! Works like a charm. One word to the wise though, is that I was experimenting with OpenRegion and CloseRegion and those seemed to mess it up to the point of NO components in the region getting re-used at all. But using SetKey with correct numbering works perfectly.

@Joebeazelman

This comment has been minimized.

Copy link

Joebeazelman commented Oct 25, 2019

Hmmm. From what I gather here, the sequence number isn't sequential, but rather a unique ID. If that's the case, shouldn't be named LineID or DiffLineID? Assuming it is so, is there a reason why RenderTreeBuilder can't be passed in a LineID from the source file? There must be a compiler preprocessor directive to capture the current source line such as #line?

@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

SteveSandersonMS commented Oct 28, 2019

@Joebeazelman That was considered and decided against - see aspnet/AspNetCore#10890 (comment) for reasoning

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.