Livecoding EEx template improvements (twitch livestream)

@josevalim has published this video, where he livecodes some improvemens to EEx templates based on ideas from PhoenixUndeadView/Vampyre: https://www.twitch.tv/videos/338743347

It was very interesting to see @josevalim going more or less down the same paths while solving a problem similar to my own with my Vampyre/PhoenixUndeadView project.

It’s amazing the way he managed to live code all of that without any preparation. My own solution to more or less the same problem is clearly over-engineered when compared to @josevalim’s solution, but it solves a different problem with different requirements, so it can’t be directly compared. My implementation also took much longer and is not as elegant (again, different requirements, so they can’t be directly compared).

I guess the main take-away from @josevalim’s video is that one can be very naïve when naming the variables, because variables nested deeper into the template will not overwrite the ones outside. I believe I had proved that to myself, but I still wanted unique names, so I went ahead anyway… I still think there is some value in having unique variable names (it makes it easier to inspect the compiled output), but seeing how much simpler it is to implement it the way @josevalim did it, I guess I think it’s not worth it to do it like I did.

The main difference between the new EEx improvements and Vampyre (and the reason why you can’t compare both probjects) is that EEx doesn’t attempt to expand macros and optimize the result (it wouldn’t even make sense in the case of such a generic project as EEx). It might make sense in a more specialized engine, like Vampyre, or the engine in Phoenix.HTML, where one expects a library of widgets to be available. That way, optimizing those widgets as much as possible makes sense.

From now on, I and @josevalim will probably pursue further optimizations in different directions, as explained on this github issue.

I’m still betting on using macros to do as much work as possible at compile-time and generate templates which are as optimized as possible, and regenerate all the dynamic parts each time the template is rendered. That is not as bad as it sounds, as I’ve managed to make the dynamic parts as minimal as possible and to make my templates as flat as possible.

On the other hand, @josevalim is now thinking about optimizing the templates by building a dependency graph on the template assigns, so that it’s possible to optimize only those segment that depend on the data that has changed.

Anyway, this was an amazing video

20 Likes

Thanks @tmbb for the compliments and all of the ideas so far. The changes I did in EEx are indeed a small subset of what we would find in Vampyre or LiveView.

Today I live streamed the implementation of LiveEEx: https://github.com/josevalim/live_eex

The engine does two main things:

  • It keeps static and dynamic parts apart
  • It tracks when each assign is used and it does not compute such parts if the assigns did not change

The analyze code is relatively straight-forward and all it does is to track whenever an assign is used. However, in order to not change the semantics of the code, we give up the analyze if it sees any variable or lexical command.

This is a different set of optimization compared to Vampyre. Vampyre is about increasing the amount of static parts. Currently I am working on reducing when the dynamic parts are sent. Both are very useful.

Tomorrow we should work on template fingerprinting so we can handle nesting and we will likely work on the Phoenix integration. The whole thing should be less than 400LOC, which is really neat, and this would not be possible if it was not for your initial suggestions on how to structure compiled code! :slight_smile:

13 Likes

Yes, it’s possible to have both groups of optimizations at the same time. I believe that whatever optimizations you’re applying (I haven’t been able to see the video yet) can be made even more efficient on Vampyre’s fully macroexpanded code.

4 Likes

I just went ahead and implemented fingerprinting now. Here are the docs on why we need it.

I have also improved the analysis code to be more optimal and it is around 110LOC. There is a big comment section explaining how it all works: https://github.com/josevalim/live_eex/blob/3b4a8b727d577d8885f8816ef9adb3d244cb6acf/lib/engine.ex#L399-L511

With the analysis improvements, everything is on 440 LOC, slightly more than I expected but also doing more than I expected, which is quite good for everything it does. :slight_smile: The test suite is also really good, so I recommend checking that for any questions.

8 Likes

Very interesting. It’s amazing how you can do all of this with so little code (as I said, my version does some extra things, but still, the conciseness in your code is impressive). There is something I don’t understand. You define the fingerprint like this:

fingerprint =
  binaries
  |> :erlang.term_to_binary()
  |> :erlang.md5()

Why not something like

fingerprint = state |> :erlang.term_to_binary() |> :erlang.md5()

?

I don’t understand why you single out the binaries instead of evaluating the md5 of the whole struct, which is “more unique” and also independent of the dynamic parts. After all, the state depends on the quoted expressions that represent the dynamic parts but not on the runtime values of the dynamic parts.

I think you’re leaving out some “uniqueness” for reasons I don’t understand, and I’d like to understand why.

EDIT: a quoted expression can always be converted into a binary using :erlang.term_to_binary/1, right?

2 Likes

That’s because if two templates have the same static parts but different dynamic ones, they are the same for client caching purposes. Take this template:

<p><%= @foo %></p>

and

<p><%= some_fun(@foobar) %></p>

The static parts are precisely the same and if the client already has the static parts cached, we don’t need to send them again, it just needs to send the dynamic bits to be inserted.

EDIT: although I have to confess the odds of this happening are quite low. Although I would say it is the correct semantic choice to do.

3 Likes

@josevalim, it looks like you’re planning on sending a map of the form %{index => new_string}. I was thinking of using the fact that dynamic parts alternate with static parts to only send an array, which is more compact than a map…

But I guess sending a map is more flexible because it allows you to only send the prts that have changed, right? I thought it sending null for an array element that hadn’t changed, but your possibility is conceptually simpler.

It’s possible that both of our approaches demand different transport protocols.

In LiveEEx you try to minimize the number of segments you transmit, which means the list of segments will be sparse. A JSON map seems like a better choice in that case.

In Vampyre, I make no attempt to minimize the number of segments, but the I try to minimize the total amount of data, which results in many short segments. In this case, the overhead of encoding the map indices might be significant. However, I think the map idea is probably the best one, even for Vampyre.

If you have a lot of segments, then it is even more reason to use a map, because it is unlikely that all of them are changing at the same time and you don’t have want to have large lists with nulls. Also, when we adopt a custom serializer, we can probably reduce the representation of the map considerably.

Also keep in mind that we don’t try to reduce the number of segments, we will have as many segments as user’s <%= ... %>. But since Vampyre is able to inline stuff, then you naturally have more segments. So we are agreeing, just saying there isn’t an active attempt to reduce them. :slight_smile:

I have also added support to for-comprehensions to live_eex, which drastically reduces the amount of data sent in for comprehensions, even on regular rendering. Take for example Phoenix’s scaffolded HTML page for index:

<%= for user <- @users do %>
  <tr>
    <td><%= field1 %></td>
    <td><%= field2 %></td>
    <td><%= field3 %></td>
    <td><%= field4 %></td>
    <td><%= link "Show", to: ... %></td>
    <td><%= link "Edit", to: ... %></td>
    <td><%= link "Delete", to: ... %></td>
  </tr>
<% end %>

If you have 10 users, the example above would send the static parts (the tr and td) for each user. Now we just send the static parts once regardless of the number of users. And if you are adding or removing users, the static parts are never sent again. This reduces the data of each initial render and of each update alike.

After all of those optimizations, the rainbow example that Chris showed at ElixirConf is now sending only 1% of the data that it did at the time. Back then it already rendered at 60 fps and reducing the amount of data sent is definitely going to make it more resilient to latency.

8 Likes

Currently I make no efforts to send only what’s changed. I just send everything, and that’s why I think an array might be better for me.

Exactly

Oh, now I see what you mean by “an attempt to reduce the amount of data sent”. :+1:

1 Like

Yeah, sending ,"... segment ..." is slightly less data than index:"... segment ...". That’s what I meant. And for a flat array I can have even more effucient encodings.

This is amazing. I can’t believe I’ve missed this optimization! This is probably bigger than my inline stuff. It requires a slightly smarter Javascript client, of course, but it’s a great idea.

And one I can implement in Vampyre, too.

Are you sure you eant to apply this optimization on regular rendering? This makes the initial render dependent on Javascript, which I’m not sure is a good idea.

I wanted to say the first rendering for the JS client. Good catch.

@josevalim, do you already have a strategy to optimize form_for/4 without turning it into a macro? I’m extremely curious about what you’ll come up with

We will go with the simplest approach for now which is to convert this:

<%= form_for @changeset, ..., fn -> %>
  <%= text_input f, :name %>
<% end %>

into this:

<%= f = form_for @changeset, ... %>
  <%= text_input f, :name %>
</form>

Effectively removing the nesting. Not the prettiest thing but definitely the simplest that works.

Hm… I don’t see how this can work. How is it possible that f is simultaneously a %Phoenix.HTML.Form{} struct and a binary containing the opening <form> tag, the CSRF token and the other hidden input tags that forms in Phoenix have?

You’d have to define something like:

<% f = Phoenix.HTML.FormData.to_form(@changeset) %>
<%= form_tag f %>
  <%= text_input f, :name %>
</form>

which I actually prefer instead of your version above.

On further thought, I wonder if optimizing forms is such a big deal. With the lack on inlining, it’s possible that you don’t gain much if you’re not inlining segments (which Vampyre can do but LiveEEx can’t - at least not yet). After all, forms in phoenix depend a lot on HTML generating functions. For example, let’s look at an example of a form generated by a Phoenix generator:

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :field1 %>
  <%= text_input f, :field1 %>
  <%= error_tag f, :field1 %>

  <%= label f, :field2 %>
  <%= text_input f, :field2 %>
  <%= error_tag f, :field2 %>

  <%= label f, :field3 %>
  <%= text_input f, :field3 %>
  <%= error_tag f, :field3 %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

The only static segments inside the form are sequences of whitespaces, <div> and </div> at the end. Also, almost all of the dynamic segments depend indirectly on the changeset. And on f, of course. LiveEEx doesn’t have a way of determining which fields to update when f (or @changeset) changes, so it will have to regenerate all widgets which depend on f, which are all of them.

You will also have to regenerate the CSRF token, unless you mark that as static somehow, as well as the other hidden input tags.

If you’re going to have to generate almost the whole form, maybe it’s not worth it to optimize that part.

On the other hand, in Vampyre I can optimize most almost all input fields to mostly static data with just 3 dynamic segments, with most of it being static.

<input name="<%= form.name %>[field1]" id="<%= form.name %>_field1" value="<%= form_value(form, field) %>">

And the form.name segments can be optimized into static segments if we assume that the form shape won’t change (which is pretty much true for almost all forms).

PS: I’ve just noticed on reviewing my code that my form_for macro doesn’t support setting the value attribute yet, but supporting it is a very minor change.

We just need to implement Phoenix.HTML.Safe for it.

It is most likely that actual user forms are more complex than this. It also serves as a good example on how to handle nesting and it opens up the possibility to explore other approaches later.

I think if there is a lesson in this whole discussion is that starting simple allows us to incrementally improve based on new ideas, feedback, etc. If I had started thinking about how to optimize comprehensions, we may have ended-up with a more complex solution and very different than the one today. I am happy to go with baby steps.