RC: W4 D5 — Two techniques to remove flickering when printing on the terminal
March 8, 2024Today, I came back on the little game of life that Ludwig and I had implemented a few weeks ago because there was one thing was not very satisfying about it and I thought it was time to solve it.
Conway’s game of life is a cellular automation that evolves based on some simple rules which state whether a given cell should be live or dead at the next iteration, depending on the current state of its neighbors. After implementing the rules that allow to generate a new grid based on the previous one, we wrote the following to display each new grid on the terminal:
import os, sys
from time import sleep
Grid = list[list[str]]
def initialize_grid() -> Grid:
...
def apply_rules(grid: Grid) -> Grid:
...
def display(grid: Grid):
for x in range(len(grid)):
for y in range(len(grid[x])):
print("█" if grid[x][y] else " ", end="")
print("")
if __name__ == '__main__':
grid = initialize_grid()
while True:
# Compute new grid
new_grid = apply_rules(grid)
# Display new grid
os.system("clear")
display(new_grid)
sys.stdout.flush()
# Wait before running next iteration
sleep(1 / 30.0)
grid = new_grid
So at each iteration, we compute the new grid, call os.system("clear")
to clear the terminal, display the new grid and
wait one 30th of a second to execute the next iteration (so that the result is visible on the screen).
This worked well. BUT: the terminal was sometimes flickering, which was kind of annoying. In the recording below, if you focus on the two crosses that are in the center-right part of the screen, you can see the stutter at least 3 times (they fully disappear and reappear when they have no reason to).
I wanted to understand why and figured that there were two problems with the way we had implemented it.
The first one is the fact that we displayed the grid line by line (display
function).
Instead, we can prepare the entire content in a single string and then print it all at once.
This technique is called double buffering: it holds the block of
data that is being created in a buffer (here, the string containing the full grid content) and only display it once it
is complete.
This allows to read the complete block of data (the full grid) instead of a partially written one (the first lines of
the grid), and thus helps to prevent stutter.
Using this technique, the display
function becomes:
def display(grid):
output = '\n'.join(''.join("█" if grid[x][y] else " "
for y in range(len(grid[x])))
for x in range(len(grid)))
print(output)
The second problem was the way we cleared the screen: we used os.system("clear")
which is a heavy operation.
Under the hood, here is what this instruction does:
- It creates a new process
- The operating system allocates resources to this process (memory and CPU time)
- The operating system switches context from the Python script to this process
- The “clear” command is executed in the shell
- The operating system switches back to the Python script
So, this introduces some overhead to start the process, execute the command and clean up once the process is finished, and thus partly explains why the screen stutters.
Instead, to be more efficient, we can do the following:
def clear_screen():
print("\033[H\033[J", end='')
This cryptic command tells the terminal to move the cursor to the top-left part of the screen (\033[H
) and to clear
the screen from that position until the end of the screen (\033[J
).
Combining these two changes, here is the result in action:
No more flickering!! 🍾