PhoenixUndeadView - let's discuss optimization possibilities for something like Phoenix LiveView

Yup, I do follow everything up. But since you are in experiment land, I would like to suggest for you to finally implement this all directly in the engine instead of doing traversals. Then you only need to look at expressions in the format of {:safe, {:__block__, _, _}} for further inlining.

1 Like

Oh, regarding the optimizations, if any composite expression has a non-literal in it, then it is not a literal. So the most important thing you can do is to merge the static parts together as much as possible unless they have a dynamic bit between them.

EDIT: It is also likely better if you emit: {:__block__, [], [..., ..., ..., {:safe, [static, dynamic, ...]}]}. At runtime it should be the same but it may make compilation slightly faster.

1 Like

I’m already merging the static parts together.

I was talking about having a specific engine which doesn’t even reference the static parts to save memory (and maybe some CPU cycles).

At runtime, evaluating the dynamic parts directly seems more efficient than creating a list with both static and dynamic parts and then filtering out the static parts, especially in terms of memory consumption.

Ok, I can compare both possibilities. This sounds like a great use case for my schism library :slight_smile: (I’ve specifically written it to compare two implementations with just a small change deep into the calling hierarchy, which is what we have here).

But you do understand what the problem is with expanding a macro outside of its quoted expression, right? Suppose you have something like this:

<% a = 1 %>
<%= bad_macro(a) %>

Where bad_macro/1 uses something like Code.eval_quoted(a), which needs a to be available. Macros like this are not very common, of course. If you expand the expression in isolation, you will get an error, which I think is inconsistent with normal EEx semantics.

EDIT: in “experiment land” I can expand the macros like you suggest, of course.

I agree the separation should be done at compile time. What I am saying though is that the separation could be done outside of the engine. Basically, the engine focuses on emitting the AST in a given format and a next step can go ahead and separate the bits, without having to introduce a whole new engine.

1 Like

So you’re suggesting something like:

defmodule PhoenixUndeadView.TemplateCompiler do
  def compile_full(text, env) do
    EEx.compile_string(text, engine: UndeadEngineFull, opts: [env: env])
  end

  def compile_static(text, env) do
    text
    |> EEx.compile_string(engine: UndeadEngineFull, opts: [env: env])
    |> extract_static()
  end

  def compile_dynamic(text, env) do
    text
    |> EEx.compile_string(engine: UndeadEngineFull, opts: [env: env])
    |> extract_dynamic()
  end
end

Or something even more optimized such as:

defmodule PhoenixUndeadView.OptimizedTemplateCompiler do
  def compile(text, env) do
    full = EEx.compile_string(text, engine: UndeadEngineFull, opts: [env: env])
    static = extract_static(full)
    dynamic = extract_dynamic(full)

    %{full: full, static: static, dynamic: dynamic}
  end
end

In that case I agree :slight_smile:

1 Like

Yes, exactly!

1 Like

The UndeadEngine is now an implementation detail inaccessible to the user. The user should never have to deal with the engine directly.

The main entry point is the UndeadEEx module, which exports the UndeadEEx.compile_string/2 function. This function compiles the text into a PhoenixUndeadView.%UndeadTemplate{}, which contains quoted expressions for the full template, the static parts and the dynamic parts.

iex> alias PhoenixUndeadView.UndeadEEx
PhoenixUndeadView.UndeadEEx
iex> UndeadEEx.compile_string(text, env: __ENV__)
%PhoenixUndeadView.UndeadTemplate{
  # Quoted expression for the full template
  full: ...,
  # Quoted expression for the static parts
  static: ...,
  # Quoted expression for the dynamic parts
  dynamic: ...
}

The implementation is what you suggest. I generate the full template and then remove the static or dynamic parts accordingly. To increase the efficiency, the literal binaries now appear in the final list. The template now returns this:

{:safe, ["", dynamic__1, "\nBlah blah blah\n", dynamic__2, "\nBlah blah\n", dynamic__3, ""]}

instead of this:

{:safe, [static__1, dynamic__1, static__2, dynamic__2, static__3, dynamic__3, static__4]}

Besides being cleaner, it makes the return values easier to analyze statically. It’s now obvious what is static and what is dynamic: everything that’s a literal string is static; everything else is dynamic.

I still haven’timplemented macro expansion… I’ll do so when I have a bigger stretch of free time.

EDIT: you can look at the implementation in the repo I’ve linked.

Just a note regarding some easy optimizations:

The following return value:

{:safe, ["", dynamic__1, "\nBlah blah blah\n", dynamic__2, "\nBlah blah\n", dynamic__3, ""]}

Could be optimized further into:

{:safe, [dynamic__1, "\nBlah blah blah\n", dynamic__2, "\nBlah blah\n" | dynamic__3]}

by removing the empty binaries and turning it into something that’s no longer a proper list (iolists don’t need to be proper lists). I’m not doing this because it’s easier to work on a list if the list is proper starts and ends with a binary (for other reasons I don’t want to explain right now). After all other optimizations have been performed, we can optimize it even further by performing the transformation above.

Thankfully the BEAM doesn’t copy them into the list, it only references them from the global storage. :slight_smile:

1 Like

Cool. But I think it’s still worth it to filter them out at compile time inestead of doing it at runtime.

1 Like

Before dealing with the problem of macros, I think there is something else that deserves some attention. Some parts of a template are evaluated at runtime but they never change. For example, the CSRF token of a form (<%= Plug.get_csrf_token() %>), translations done with gettext (<%= gettext "Hello world" %>), among others.

Having to send these over the network seems very wasteful. These should only be rendered on the initial page load and treated as static afterwards.

I should have a new marker (for example: <%| ... %>) for text that never changes like in the examples above.

So this means there will be static terms (raw binaries), fixed terms (static binaries + expressions that never change) and dynamic/reactive terms (terms that are re-rendered when something changes).

EDIT: I’m aware this is moving me away from the focus on macro expansion, and it also complicates the architecture, but it’s a very high yield optimization, so I’ll solve it first.

1 Like

I have some free time now, but I’m away from the computer, so I’ll sketch my thoughts about actually sending the dynamic data into the client.

Dynamic data is a list of iolists. The length of the list is fixed. This list must be encoded in the client somehow. JSON would not be a bad choice, because encoding a list as a JSON array has a small overhead.

Could we do it using a stream protocol to avoid allocating memory for the binary that encodes the list? Can we reuse the default encoder for Phoenix channels? If so, then we only need to pass the list of binaries and Phoenix will take care of the rest. But that requires us to render the iolists inside the list into binaries, which allocates memory.

I should take a look at how JSON encoders actually handle iolists. My guess is that they don’t, because an iolist is indistinguishable from a deeply nested json array.

1 Like

To me that’s a complete separate problem. We will need to figure out a light-weight protocol to send data to the client but that is a separate problem than the engine one. :slight_smile:

3 Likes

Yes, I agree completely, it’s just something I thought about a while ago and had some free time to write. My main focus is the engine itself. You or some other network whiz can probably think of something much more efficient (for example, sending strings as a flat binary separated by null bytes or something like that; as long as the Javascript on the other side can make sense of it you can use whatever you want).

My priorities are still the two I’ve outlined above:

  1. Add support for fixed template segments (i.e. <%| this_will_be_rendered_only_once %>) and
  2. Expand and optimize macros (is possible in a semantically correct way)
2 Likes

Going back to the “fixed segments”, one might ask why I’m using <%| ... %> as the equivalent to <%= ... %> in “normal” phoenix templates. Maybe it would make sense to use <%= ... %> for the fixed parts (the same semantics as the normal templates) and <%| ... %> for the dynamic/reactive parts (which is a concept we don’t have in “normal” templates).

I’m doing this for two reasons:

  1. It’s more obvious for the user: if the user wants Reactive (= Undead) templates, most of the content will be reactive, so it makes sense to use the “common” marker instead of the weird one.
  2. (and most important) The fixed segments will be rendered only once, and won’t be available when rendering the dyamic parts. This means that fixed segments must have no side effects, including but not limited to assigning variables…)

That is, you can’t do this:

<%| f = slow_computation_i_dont _want_to_repeat_each_time() %>
<%= do_stuff(f) %>

Because f won’t be available on the dynamic template! So using the fixed segments require some care on behalf of the user.

1 Like

Drab also has a special indicator (I think <%/?) for such things that should only ever be sent on initial load. :slight_smile:

Hmm, I wonder how drab handles this case…

1 Like

Personally I’m not planning on supporting this at all. I’ll look at what Drab does.

1 Like

It’s normal that we’ll converge into similar solutions. The problems are very similar.

1 Like

I already know how I want to expand macros (I’ll publish the code when it’s ready), but I’m having some troubles in thinking on how to turn form_for into a macro that outputs something useful… Ideally, one would have a compatible of form_for (which is a function) but which does something useful at compile time and expands into a mostly static template.

Until I have something concrete on how to optimized form_for, I’ll put most of the rest on hold. The most dynamic arts of websites are often forms, and it’s important that dynamic features in forms are as efficient as possible.

1 Like