Liveview process memory usage seems high, am I doing something wrong?

For me the most compelling use case of using liveview is to update nested form data without requiring page reloads.

I think I’ve managed to cobble together a working example here:

This example is related to invoicing. An invoice has total hours and total dollars and has_many workdays. A workday has a date and two work periods, each have a start and end time.

The form will dynamically redraw when the underlying changeset is updated (It’s taking a bit of effort to figure out how to update a changeset, and maybe I’m still not doing it right.) So far so good… as you change the dates, add workdays, or change periods the total hours will change as well as the validation error messages (periods cannot overlap, start time must be earlier than end time).

I noticed however that the liveview process seems rather high

That’s 1.2Mb just for a form that has two rows in it.

Let’s try adding a few more workdays to this invoice:

And check observer again:

3.5Mb on a single process, sorting by memory makes this process jump to the top of the list.

The code is on github, (Clone, run migrations and click on the invoice example). Am I doing something tremendously inefficient somewhere that’s causing this?

1 Like

Where exactly? There is a lot of code on GitHub…

But due to the nature of LiveView, I think old state just remains on the heap until collected, but due to the nature of how the BEAM works, collection won’t happen until it is necessary…

I have no experience with LiveView, but you could try forcing a GC by hibernating the process. Be aware that this could cause some lag in the page, but for some experiment it might be OK.

1 Like

can you please share the url or, at least, the liveview code?

There is a link in the opening post:

I think I’ve managed to cobble together a working example here:

1 Like

Oh, haven’t seen that…

Sorry, link was at the top, but maybe wasn’t obvious. Here is an additional link to the github “compare” page, which is probably the easiest way to quickly view all the changes at once:

look what @josevalim added earlier today… can you try master and see if it helps?

`:hibernate_after` (optional) - the amount of time in miliseconds
of inactivity inside the LiveView so it hibernates (i.e. it
compresses its own memory and state). Defaults to 15000ms (15 seconds)

Yes! It totally helped. Observer showed me that the process went from 3Mb down to 13kb after it hibernated. Awesome!


Do you notice any hitches or lag after a hibernation happens and then gets un-hibernated??

I haven’t gotten deep into the woods yet with LV so I don’t have a working example to test. But from the commit linked above, it sounds like after 15 seconds of inactivity a hibernation happens. If after 15 seconds, the user becomes active again, how does it come out of hibernation and what are the implications?

I’m not sure what this means in the end but if you had 100 people looking at that form concurrently, your memory usage would spike to 350mb right? And it wouldn’t get cleared until that hibernate happens?

That sounds like it wouldn’t take much traffic to saturate a server even with the new patch.

I haven’t looked at the linked repo, but one thing to keep in mind is you can leverage temporary_assigns and only hold the state in memory that is required to be there. So for lists of resources on the page, large bodies of text, etc, the server would discard the assigns after render. If on a subsequent event you needed access to the data, you could refetch. This can also be combined with phx-update=append to avoid storing lists of resources while still appending/prepending/updating items in a collection on the page.


What about cases like a form submission like the original poster’s example? It sounds like you would need to use assigns here to keep the state server side, but the memory usage is going to blow up very quickly.

Should we use temporary_assigns for everything then?

I don’t notice any observable lag when “waking up” a hibernated liveview process. The only way I even know that hibernation is happening is by watching the observer and seeing memory usage go down. When I continue to make changes to the form, the liveview updates feel as snappy as the unhibernated process. The memory usage for that process does jump back up, but not as high as before the first hibernation.

Regarding the total memory usage when having lots of concurrent liveview processes, that is a concern still, but at least it can be mitigated somewhat by setting shorter duration for when hibernation kicks in.

I also haven’t looked into optimizing my form, I suspect my HTML select dropdowns are lengthly and taking up a lot of that memory.

1 Like

I think phx-update=append would be appropriate for things like infinite scroll, if I don’t need to hold the state of previous results in memory, however my use case here is a form submit. I’m building up an ecto changeset with nested associations for the purposes of immediate feedback for validation, so I think it’s appropriate to have the entire changeset in memory.

Building this example was mostly straight forward because I can directly apply the knowledge (and implementation) from traditional static phoenix forms, except with liveview this form can tell me immediately when:

  1. there is a missing start or end time for period1
  2. when there is an overlap between period1 and period2
  3. when period2 starts before period1
  4. when there is any duplicate date

It’s pretty great.

I was just surprised that the memory usage was over 400k for even one workday.

Could it be that my form dropdowns that are timepickers are holding onto lots of memory? time_options() is a helper function that generates a list of tuples of strings (lots of concatenation to build them) and it is used in multiple places (4 times per workday) in the form. (I know Phoenix already comes with a time picker builder but it separates each component for HH, MM, etc, so it seemed to make the form too wide for me.)

If I remove the time pickers from the liveview form the initial process drops from 427k to 144k.

I remember from the talk that liveview separates the parts that change from the parts that don’t, but is there some special treatment that’s needed for looping over the nested associations in inputs_for such that in the example below time_options is cached for re-use?

  <%= inputs_for f, :workdays, fn wd -> %>
      <%= date_select wd, :date, builder: fn b -> %>
      <%= b.(:month, []) %> <%= b.(:day, []) %> <%= b.(:year, [options: 2018..2020]) %>
    <% end %>
    <%= select wd, :period1_start, time_options(), value: form_to_time(wd, :period1_start) %>
    <%= error_tag wd, :period1_start %> -
    <%= select wd, :period1_end, time_options(), value: form_to_time(wd, :period1_end) %>
    <%= error_tag wd, :period1_end %>
    <%= select wd, :period2_start, optional_time_options(), value: form_to_time(wd, :period2_start) %>
    <%= error_tag wd, :period2_start %> -
    <%= select wd, :period2_end, optional_time_options(), value: form_to_time(wd, :period2_end) %>
    <%= error_tag wd, :period2_end %>
    <%= Ecto.Changeset.get_field(wd.source, :hours) %>
  <%= if do %>
    <td><%= checkbox wd, :overnight %>
      <%= error_tag wd, :overnight %></td>
  <% end %>
  <td><button type="button" phx-click="delete_workday" phx-value-workday_id="<%= %>" class="alert-danger">🗑</button></td>
<% end %>

Looking at the websocket Messages, it looks like all the HTML generated inside the inputs_for/4 block is “hardcoded” and sent down the wire. Ideally, since the dropdowns for all the workdays are the same (except for which option is selected), they could be sent down as part of the template and only the workday dates and times sent as the diff.

I came across Liveview Comprehensions which seems capable of doing this kind of optimization, but I have yet to work out how to use it inside the context of a form’s nested records.

The Liveview documentation regarding forms also mentions:

Because Phoenix.LiveView is unable to compute diffs inside anonymous functions, Phoenix.HTML provides form_for/3 that works without passing an anonymous function

Wouldn’t we need the same thing for inputs_for/4 ?

1 Like