I am writing a snake game as my first LV non DB app.
I have a 10 by 10 css grid (will obviously be bigger in the final game) and am filling the cells with either ‘.’ or ‘x’ (I am just getting started). My problem is that the whole grid is sent every repaint,
Could you please share also draw_cell/4 function? What we know is that you reduce over some acc, but it’s not really clear what current value is. From the initial data it looks like integer, but in practice it could be anything. The draw_cell naming is unclear as you only return an atom and the current integer.
For example if the current is index (i.e. 0, 1, 2 etc.) then you may consider using Enum.with_index/1 and then write a cell function component with a :for HEEx attribute instead of Enum.reduce/3 call. This way while you still would generate all cells, LiveView would only send only the changed ones.
if Enum.at(assigns.snake.cells, current) == {row, col} do
{:snake, current + 1}
else
{:empty, current}
end
end
I know I have tied myself in knots a bit trying to step through the snake.cells list, I realize that this is unneccassry becuase Enum.at doesnt directly index. I Could simplify this code but I dont think that would change anything regarding the efficent diffing
def grid_size(width, height) do
(height - 1) * width - 1
end
def draw_cell(ids, width, height, snake_cells) do
if {div(idx, width), rem(idx, width)} in snake_cells do
"x"
else
"."
end
end
LiveView’s change tracking doesn’t work when iterating over lists. You can use LiveView streams (not to be confused with Stream module) or LiveComponents to compensate.
You might want to look at this reply to another thread.
Try using phx-update="replace" on grid cells to minimize re-rendering. LiveComponents for each cell could also help update only the changed ones efficiently.
OK, i now use live view but although i get the diff sent now (thank you for the pointer) a large amount of meta data is still sent along side the diff - there is an entry per cell.
code
def render(assigns) do
require Logger
# Logger.debug(IO.inspect(assigns))
# cell_idx = 0
# cur_cell = Enum.at(assigns.snake.cells, cell_idx)
~H"""
<div>
<h1>Snake Game</h1>
</div>
<div>{IO.inspect(@snake.direction)}</div>
<div class="snake_grid" id="game_surface" phx-window-keydown="key_pressed">
<.live_component
:for={idx <- 0..(grid_size(10, 10) - 1)}
module={SnakeWeb.Cell}
id={"cell_#{idx}"}
content={draw_cell(idx, 10, 10, @snake.cells)}
/>
</div>
"""
end
def grid_size(width, height) do
height * width
end
def draw_cell(idx, width, height, snake_cells) do
if {div(idx, height), rem(idx, width)} in snake_cells do
"x"
else
"."
end
end
I can see the actual UI patch at the end but there are also 100 entries in that ‘d:’ array. My final UI will have maybe 10k cells, that adds up in data being transmitted,
I’m pretty sure assigning a function call (in this case draw_cell) will prevent the change tracking from functioning as you expect. See Assigns and HEEx templates — Phoenix LiveView v1.0.4. You could try pre-building a map from the results of draw_cell, keyed by index, and use that in the assign. I haven’t tried it.
Late to this conversation, but a quick general note about performance: functions like in (on a list) and Enum.at should be viewed with extreme suspicion if they’re used in a loop (ie the for here).
This is because they have to traverse the list to do their work, so they add an extra O(list length) cost to each call. Inside a loop that means turning an O(N) calculation into an O(N^2) and so on.
Using better data structures can help:
instead of using in on a large list, consider a MapSet which instead has O(log length) performance
for a sparsely-populated 2D grid, consider using a Map with keys shaped like {row, col} which has the same O(log length) behavior
Map can get unwieldy at large sizes with lots of writes, but it’s reasonably simple to swap in ETS if you get to that point.
I used this co-ordinate map approach in a LV project that rendered a flexbox grid a while back, and I seem to remember it working well—though I don’t recall if I had to do anything special in the HEEX to keep the diffs minimal.
fully aware of the inefficiency of looking for things in a list, at the moment I have 2 entries in it so its not really an issue, thatnks you for all the suggestions for fixing this when it gets bigger. For now I am trying to reduce the size of the transmitted diff packet
My point was not about list iteration vs map performance, but rather that by using the coordinate-map strategy I remember getting a smaller diff, though not if the map alone was fully sufficient to get the small diff or if I had to mix in other approaches.
I thought it /did/ work when using the :for heex syntax? or am I confused?
EDIT: I was indeed mistaken, and that linked post really clears it up. Also form the docs: “However, keep in mind LiveView does not track changes within the collection given to the comprehension. In other words, if one entry in @posts changes, all posts are sent again.”
I’m kind of surprised that I got this wrong. Maybe it’s worth a special callout somehow, or just italics/bold?