Repository: aome510/spotify-player (language: rust)

Issue:
Leak of memory on iTerm2 after long period of usage
**Describe the bug**
For some reason after open spotify player the iterm2 memory is increasing little by little in the meanwhile the TUI is opened and playing music.

**To Reproduce**
Just play music for a more than 30 minutes and check the memory usage every so often.

**Expected behavior**
Do not see anomalies in the memory usage while using the TUI for long amount of time.

**Log and backtrace**
Not important, no error occurred.

**Screenshots**
<img width="270" alt="Screenshot 4" src="https://github.com/aome510/spotify-player/assets/45423661/02bca099-dbca-4660-91dc-438d390b6671">

**Environment**

- OS: macOs Sonoma 14.1.2
- Application version: latest
- Application features: image, media control

**Additional context**
Running on Iterm2 Build 3.4.22

Only 2 tabs/processes opened:
1. `spotify_player`
2. nvim

This behavior only occurs when spotify player is open.

## important
After a couple of hours of usage, the memory reached even `38GB`!!
The capture above is after 15 minutes of usage, it reached `3GB`.



Discussion / hints:
By the way I'm in love with this project, I was using it all the past days, now, the only thing that's stopping me of using it is the memory usage leak it causes.

Hope I can find some kind of solution on this issue.
Thank you!
@diegoulloao what is the value of `Scrollback lines` in the **Terminal** tab of your ITerm2 settings? Using a large or unbounded scrollback buffer may cause a memory issue as rendered images are kept inside the scrollback buffer.
FYI, I use `1000` for scrollback lines and have never seen ITerm2 memory usage exceed `2GB`. Memory usage can be estimated with each image's size is about `1MB`, so `1000` means memory usage (only for images) is about `1GB`.

**Edit**: it does exceed 2GB for me after a while, so this issue may not be related to the scrollback buffer
<img width="594" alt="Screenshot 6" src="https://github.com/aome510/spotify-player/assets/45423661/8ffe465f-baf1-4f1b-908e-d47107e22566">


Using `1000` too. Is very weird, after 3-4 hours of usage my memory reached `38GB` using +swap

Fix patch:
diff --git a/docs/config.md b/docs/config.md
index e466b058..c9828aaa 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -21,38 +21,37 @@ All configuration files should be placed inside the application's configuration
 
 `spotify_player` uses `app.toml` to configure general application configurations:
 
-| Option                               | Description                                                                              | Default                                                    |
-| ------------------------------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
-| `client_id`                          | the Spotify client's ID                                                                  | `65b708073fc0480ea92a077233ca87bd`                         |
-| `client_port`                        | the port that the application's client is running on to handle CLI commands              | `8080`                                                     |
-| `tracks_playback_limit`              | the limit for the number of tracks played in a **tracks** playback                       | `50`                                                       |
-| `playback_format`                    | the format of the text in the playback's window                                          | `{track} • {artists}\n{album}\n{metadata}`                 |
-| `notify_format`                      | the format of a notification (`notify` feature only)                                     | `{ summary = "{track} • {artists}", body = "{album}" }`    |
-| `copy_command`                       | the command used to execute a copy-to-clipboard action                                   | `xclip -sel c` (Linux), `pbcopy` (MacOS), `clip` (Windows) |
-| `player_event_hook_command`          | the hook command executed when there is a new player event                               | `None`                                                     |
-| `ap_port`                            | the application's Spotify session connection port                                        | `None`                                                     |
-| `proxy`                              | the application's Spotify session connection proxy                                       | `None`                                                     |
-| `theme`                              | the application's theme                                                                  | `default`                                                  |
-| `app_refresh_duration_in_ms`         | the duration (in ms) between two consecutive application refreshes                       | `32`                                                       |
-| `playback_refresh_duration_in_ms`    | the duration (in ms) between two consecutive playback refreshes                          | `0`                                                        |
-| `cover_image_refresh_duration_in_ms` | the duration (in ms) between two cover image refreshes (`image` feature only)            | `2000`                                                     |
-| `page_size_in_rows`                  | a page's size expressed as a number of rows (for page-navigation commands)               | `20`                                                       |
-| `enable_media_control`               | enable application media control support (`media-control` feature only)                  | `true` (Linux), `false` (Windows and MacOS)                |
-| `enable_streaming`                   | enable streaming (`streaming` feature only)                                              | `Always`                                                   |
-| `enable_notify`                      | enable notification (`notify` feature only)                                              | `true`                                                     |
-| `enable_cover_image_cache`           | store album's cover images in the cache folder                                           | `true`                                                     |
-| `notify_streaming_only`              | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false`                                                    |
-| `default_device`                     | the default device to connect to on startup if no playing device found                   | `spotify-player`                                           |
-| `play_icon`                          | the icon to indicate playing state of a Spotify item                                     | `▶`                                                       |
-| `pause_icon`                         | the icon to indicate pause state of a Spotify item                                       | `▌▌`                                                       |
-| `liked_icon`                         | the icon to indicate the liked state of a song                                           | `♥`                                                       |
-| `border_type`                        | the type of the application's borders                                                    | `Plain`                                                    |
-| `progress_bar_type`                  | the type of the playback progress bar                                                    | `Rectangle`                                                |
-| `playback_window_position`           | the position of the playback window                                                      | `Top`                                                      |
-| `playback_window_width`              | the width of the playback window                                                         | `6`                                                        |
-| `cover_img_width`                    | the width of the cover image (`image` feature only)                                      | `5`                                                        |
-| `cover_img_length`                   | the length of the cover image (`image` feature only)                                     | `9`                                                        |
-| `cover_img_scale`                    | the scale of the cover image (`image` feature only)                                      | `1.0`                                                      |
+| Option                            | Description                                                                              | Default                                                    |
+| --------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
+| `client_id`                       | the Spotify client's ID                                                                  | `65b708073fc0480ea92a077233ca87bd`                         |
+| `client_port`                     | the port that the application's client is running on to handle CLI commands              | `8080`                                                     |
+| `tracks_playback_limit`           | the limit for the number of tracks played in a **tracks** playback                       | `50`                                                       |
+| `playback_format`                 | the format of the text in the playback's window                                          | `{track} • {artists}\n{album}\n{metadata}`                 |
+| `notify_format`                   | the format of a notification (`notify` feature only)                                     | `{ summary = "{track} • {artists}", body = "{album}" }`    |
+| `copy_command`                    | the command used to execute a copy-to-clipboard action                                   | `xclip -sel c` (Linux), `pbcopy` (MacOS), `clip` (Windows) |
+| `player_event_hook_command`       | the hook command executed when there is a new player event                               | `None`                                                     |
+| `ap_port`                         | the application's Spotify session connection port                                        | `None`                                                     |
+| `proxy`                           | the application's Spotify session connection proxy                                       | `None`                                                     |
+| `theme`                           | the application's theme                                                                  | `default`                                                  |
+| `app_refresh_duration_in_ms`      | the duration (in ms) between two consecutive application refreshes                       | `32`                                                       |
+| `playback_refresh_duration_in_ms` | the duration (in ms) between two consecutive playback refreshes                          | `0`                                                        |
+| `page_size_in_rows`               | a page's size expressed as a number of rows (for page-navigation commands)               | `20`                                                       |
+| `enable_media_control`            | enable application media control support (`media-control` feature only)                  | `true` (Linux), `false` (Windows and MacOS)                |
+| `enable_streaming`                | enable streaming (`streaming` feature only)                                              | `Always`                                                   |
+| `enable_notify`                   | enable notification (`notify` feature only)                                              | `true`                                                     |
+| `enable_cover_image_cache`        | store album's cover images in the cache folder                                           | `true`                                                     |
+| `notify_streaming_only`           | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false`                                                    |
+| `default_device`                  | the default device to connect to on startup if no playing device found                   | `spotify-player`                                           |
+| `play_icon`                       | the icon to indicate playing state of a Spotify item                                     | `▶`                                                       |
+| `pause_icon`                      | the icon to indicate pause state of a Spotify item                                       | `▌▌`                                                       |
+| `liked_icon`                      | the icon to indicate the liked state of a song                                           | `♥`                                                       |
+| `border_type`                     | the type of the application's borders                                                    | `Plain`                                                    |
+| `progress_bar_type`               | the type of the playback progress bar                                                    | `Rectangle`                                                |
+| `playback_window_position`        | the position of the playback window                                                      | `Top`                                                      |
+| `playback_window_width`           | the width of the playback window                                                         | `6`                                                        |
+| `cover_img_width`                 | the width of the cover image (`image` feature only)                                      | `5`                                                        |
+| `cover_img_length`                | the length of the cover image (`image` feature only)                                     | `9`                                                        |
+| `cover_img_scale`                 | the scale of the cover image (`image` feature only)                                      | `1.0`                                                      |
 
 ### Notes
 
diff --git a/examples/app.toml b/examples/app.toml
index de46f1d0..c20163d6 100644
--- a/examples/app.toml
+++ b/examples/app.toml
@@ -10,7 +10,6 @@ notify_format = { summary = "{track} • {artists}", body = "{album}" }
 # copy_command = { command = "clip", args = [] } # windows
 app_refresh_duration_in_ms = 32
 playback_refresh_duration_in_ms = 0
-cover_image_refresh_duration_in_ms = 2000
 page_size_in_rows = 20
 enable_media_control = false
 enable_streaming = "Always"
diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs
index 188e0ff3..b63794b1 100644
--- a/spotify_player/src/config/mod.rs
+++ b/spotify_player/src/config/mod.rs
@@ -41,8 +41,6 @@ pub struct AppConfig {
     // duration configs
     pub app_refresh_duration_in_ms: u64,
     pub playback_refresh_duration_in_ms: u64,
-    #[cfg(feature = "image")]
-    pub cover_image_refresh_duration_in_ms: u64,
 
     pub page_size_in_rows: usize,
 
@@ -210,9 +208,6 @@ impl Default for AppConfig {
             app_refresh_duration_in_ms: 32,
             playback_refresh_duration_in_ms: 0,
 
-            #[cfg(feature = "image")]
-            cover_image_refresh_duration_in_ms: 2000,
-
             page_size_in_rows: 20,
 
             pause_icon: "▌▌".to_string(),
diff --git a/spotify_player/src/state/ui/mod.rs b/spotify_player/src/state/ui/mod.rs
index bb6408a7..0fc0f28d 100644
--- a/spotify_player/src/state/ui/mod.rs
+++ b/spotify_player/src/state/ui/mod.rs
@@ -25,7 +25,7 @@ pub struct UIState {
     pub playback_progress_bar_rect: tui::layout::Rect,
 
     #[cfg(feature = "image")]
-    pub last_cover_image_render_info: Option<(String, std::time::Instant)>,
+    pub last_cover_image_render_info: Option<(String, tui::layout::Rect)>,
 }
 
 impl UIState {
diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs
index 8b12f08e..624fb62c 100644
--- a/spotify_player/src/ui/mod.rs
+++ b/spotify_player/src/ui/mod.rs
@@ -15,6 +15,8 @@ pub fn run(state: SharedState) -> Result<()> {
 
     let ui_refresh_duration =
         std::time::Duration::from_millis(state.configs.app_config.app_refresh_duration_in_ms);
+    let mut last_terminal_size = None;
+
     loop {
         {
             let mut ui = state.ui.lock();
@@ -23,12 +25,35 @@ pub fn run(state: SharedState) -> Result<()> {
                 std::process::exit(0);
             }
 
+            let terminal_size = terminal.size()?;
+            if Some(terminal_size) != last_terminal_size {
+                last_terminal_size = Some(terminal_size);
+                #[cfg(feature = "image")]
+                {
+                    // redraw the cover image when the terminal's size changes
+                    ui.last_cover_image_render_info = None;
+                }
+            }
+
             if let Err(err) = terminal.draw(|frame| {
+                #[cfg(feature = "image")]
+                {
+                    for x in 1..state.configs.app_config.cover_img_length + 1 {
+                        for y in 1..state.configs.app_config.cover_img_width + 1 {
+                            frame
+                                .buffer_mut()
+                                .get_mut(x as u16, y as u16)
+                                .set_skip(true);
+                        }
+                    }
+                }
+
                 // set the background and foreground colors for the application
+                let rect = frame.size();
                 let block = Block::default().style(ui.theme.app_style());
-                frame.render_widget(block, frame.size());
+                frame.render_widget(block, rect);
 
-                if let Err(err) = render_application(frame, &state, &mut ui, frame.size()) {
+                if let Err(err) = render_application(frame, &state, &mut ui, rect) {
                     tracing::error!("Failed to render the application: {err:#}");
                 }
             }) {
diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs
index 8bb8c489..795c971d 100644
--- a/spotify_player/src/ui/playback.rs
+++ b/spotify_player/src/ui/playback.rs
@@ -65,21 +65,12 @@ pub fn render_playback_window(
                         let url = crate::utils::get_track_album_image_url(track).map(String::from);
                         if let Some(url) = url {
                             let needs_render = match &ui.last_cover_image_render_info {
-                                Some((last_url, last_time)) => {
-                                    url != *last_url
-                                        || last_time.elapsed()
-                                            > std::time::Duration::from_millis(
-                                                state
-                                                    .configs
-                                                    .app_config
-                                                    .cover_image_refresh_duration_in_ms,
-                                            )
-                                }
+                                Some((last_url, _)) => *last_url != url,
                                 None => true,
                             };
-
                             if needs_render {
-                                render_playback_cover_image(state, ui, cover_img_rect, url);
+                                render_playback_cover_image(state, ui, cover_img_rect, url)
+                                    .context("render playback's cover image")?;
                             }
                         }
 
@@ -110,12 +101,25 @@ pub fn render_playback_window(
             tracing::warn!("Got a non-track playable item: {:?}", playback.item);
         }
     } else {
-        // Previously rendered image can result in weird rendering text,
-        // clear the widget area before rendering the text.
+        // Previously rendered image can result in a weird rendering text,
+        // clear the previous widget's area before rendering the text.
         #[cfg(feature = "image")]
-        if ui.last_cover_image_render_info.is_some() {
-            frame.render_widget(Clear, rect);
-            ui.last_cover_image_render_info = None;
+        {
+            if ui.last_cover_image_render_info.is_some() {
+                frame.render_widget(Clear, rect);
+                ui.last_cover_image_render_info = None;
+            }
+
+            // reset the `skip` state of cells in cover image area
+            // to render the "No playback found" message
+            for x in 1..state.configs.app_config.cover_img_length + 1 {
+                for y in 1..state.configs.app_config.cover_img_width + 1 {
+                    frame
+                        .buffer_mut()
+                        .get_mut(x as u16, y as u16)
+                        .set_skip(false);
+                }
+            }
         }
 
         frame.render_widget(
@@ -275,7 +279,7 @@ fn render_playback_cover_image(
     ui: &mut UIStateGuard,
     rect: Rect,
     url: String,
-) {
+) -> Result<()> {
     fn remove_temp_files() -> Result<()> {
         // Clean up temp files created by `viuer`'s kitty printer to avoid
         // possible freeze because of too many temp files in the temp folder.
@@ -291,14 +295,10 @@ fn render_playback_cover_image(
         Ok(())
     }
 
-    if let Err(err) = remove_temp_files() {
-        tracing::error!("Failed to remove temp files: {err:#}");
-    }
+    remove_temp_files().context("remove temp files")?;
 
     let data = state.data.read();
     if let Some(image) = data.caches.images.get(&url) {
-        ui.last_cover_image_render_info = Some((url, std::time::Instant::now()));
-
         // `viuer` renders image using `sixel` in a different scale compared to other methods.
         // Scale the image to make the rendered image more fit if needed.
         // This scaling factor is user configurable as the scale works differently
@@ -308,19 +308,23 @@ fn render_playback_cover_image(
         let width = (rect.width as f32 * scale).round() as u32;
         let height = (rect.height as f32 * scale).round() as u32;
 
-        if let Err(err) = viuer::print(
+        viuer::print(
             image,
             &viuer::Config {
                 x: rect.x,
                 y: rect.y as i16,
                 width: Some(width),
                 height: Some(height),
+                restore_cursor: true,
                 ..Default::default()
             },
-        ) {
-            tracing::error!("Failed to render the image: {err:#}",);
-        }
+        )
+        .context("print image to the terminal")?;
+
+        ui.last_cover_image_render_info = Some((url, rect));
     }
+
+    Ok(())
 }
 
 /// Splits the application rectangle into two rectangles, one for the playback window


Test patch: