Repository: dermotduffy/frigate-hass-card
Pull request #1507 (2024-08-27 02:59:02)

Add `max_simultaneous_engine_requests` parameter
* Related: #1304


Patch:
diff --git a/docs/configuration/performance.md b/docs/configuration/performance.md
index 9590a07c..f0597905 100644
--- a/docs/configuration/performance.md
+++ b/docs/configuration/performance.md
@@ -22,10 +22,11 @@ performance:
     # [...]
 ```
 
-| Option                        | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                         |
-| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `animated_progress_indicator` | `true`  | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.                                                                                                                                                                                                                                                                                                              |
-| `media_chunk_size`            | `50`    | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, it just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to). |
+| Option                             | Default    | Description                                                                                                                                                                                                                                                                                                                                                                                                         |
+| ---------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `animated_progress_indicator`      | `true`     | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.                                                                                                                                                                                                                                                                                                              |
+| `media_chunk_size`                 | `50`       | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, it just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to). |
+| `max_simultaneous_engine_requests` | _Infinity_ | How many camera engine requests to allow occur in parallel. Setting lower values will slow the card down since more requests will run in sequence, but it will increase the chances of positive cache hit rates and reduce the chances of overwhelming the backend.                                                                                                                                                 |
 
 ### `style`
 
@@ -59,6 +60,7 @@ performance:
   features:
     animated_progress_indicator: true
     media_chunk_size: 50
+    max_simultaneous_engine_requests: 100
   style:
     border_radius: true
     box_shadow: true
diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts
index 71ba9d4f..93dd9a3b 100644
--- a/src/camera-manager/manager.ts
+++ b/src/camera-manager/manager.ts
@@ -112,6 +112,7 @@ export class CameraManager {
   protected _engineFactory: CameraManagerEngineFactory;
   protected _store: CameraManagerStore;
   protected _initializationLimit = new PQueue({ concurrency: 1 });
+  protected _requestLimit = new PQueue();
 
   constructor(
     api: CardCameraAPI,
@@ -135,6 +136,9 @@ export class CameraManager {
       return false;
     }
 
+    this._requestLimit.concurrency =
+      config.performance.features.max_simultaneous_engine_requests ?? Infinity;
+
     // For each camera merge the config (which has no defaults) into the camera
     // global config (which does have defaults). The merging must happen in this
     // order, to ensure that the defaults in the cameras global config do not
@@ -535,7 +539,10 @@ export class CameraManager {
     }
 
     const queryStartTime = new Date();
-    await engine.favoriteMedia(hass, cameraConfig, media, favorite);
+
+    await this._requestLimit.add(() =>
+      engine.favoriteMedia(hass, cameraConfig, media, favorite),
+    );
 
     log(
       this._api.getConfigManager().getCardWideConfig(),
@@ -590,7 +597,11 @@ export class CameraManager {
       return null;
     }
 
-    return await engine.getMediaSeekTime(hass, this._store, media, target);
+    return (
+      (await this._requestLimit.add(() =>
+        engine.getMediaSeekTime(hass, this._store, media, target),
+      )) ?? null
+    );
   }
 
   protected async _handleQuery<QT extends DataQuery>(
@@ -653,7 +664,9 @@ export class CameraManager {
       }
       await Promise.all(
         Array.from(engines.keys()).map((engine) =>
-          processEngineQuery(engine, { ...query, cameraIDs: engines.get(engine) }),
+          this._requestLimit.add(() =>
+            processEngineQuery(engine, { ...query, cameraIDs: engines.get(engine) }),
+          ),
         ),
       );
     };
@@ -789,6 +802,8 @@ export class CameraManager {
     if (!engine || !hass) {
       return;
     }
-    return await engine.executePTZAction(hass, cameraConfig, action, options);
+    return await this._requestLimit.add(() =>
+      engine.executePTZAction(hass, cameraConfig, action, options),
+    );
   }
 }
diff --git a/src/config/profiles/low-performance.ts b/src/config/profiles/low-performance.ts
index 2ee9d8cf..6bd8a49e 100644
--- a/src/config/profiles/low-performance.ts
+++ b/src/config/profiles/low-performance.ts
@@ -35,6 +35,7 @@ import {
   CONF_MENU_BUTTONS_TIMELINE,
   CONF_MENU_STYLE,
   CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
+  CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
   CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
   CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
   CONF_PERFORMANCE_STYLE_BOX_SHADOW,
@@ -137,4 +138,7 @@ export const LOW_PERFORMANCE_PROFILE = {
 
   // No trigger actions.
   [CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER]: 'none',
+
+  // One engine request at a time.
+  [CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS]: 1,
 };
diff --git a/src/config/types.ts b/src/config/types.ts
index b5875991..2989e1ec 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -1913,6 +1913,7 @@ export const performanceConfigSchema = z
           .min(0)
           .max(MEDIA_CHUNK_SIZE_MAX)
           .default(performanceConfigDefault.features.media_chunk_size),
+        max_simultaneous_engine_requests: z.number().min(1).optional(),
       })
       .default(performanceConfigDefault.features),
     style: z
diff --git a/src/const.ts b/src/const.ts
index 92d4045f..08783be4 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -348,6 +348,7 @@ export const CONF_OVERRIDES = 'overrides' as const;
 const CONF_PERFORMANCE = 'performance' as const;
 export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`;
 export const CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE = `${CONF_PERFORMANCE}.features.media_chunk_size`;
+export const CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS = `${CONF_PERFORMANCE}.features.max_simultaneous_engine_requests`;
 export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`;
 export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`;
 export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
diff --git a/src/editor.ts b/src/editor.ts
index 6e0e1059..cc2e8318 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -171,6 +171,7 @@ import {
   CONF_MENU_POSITION,
   CONF_MENU_STYLE,
   CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
+  CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
   CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
   CONF_PERFORMANCE_PROFILE,
   CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
@@ -2790,6 +2791,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
                   ${this._renderNumberInput(CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, {
                     max: MEDIA_CHUNK_SIZE_MAX,
                   })}
+                  ${this._renderNumberInput(
+                    CONF_PERFORMANCE_FEATURES_MAX_SIMULTANEOUS_ENGINE_REQUESTS,
+                    {
+                      min: 1,
+                    },
+                  )}
                 `,
               )}
               ${this._putInSubmenu(
diff --git a/src/localize/languages/ca.json b/src/localize/languages/ca.json
index efb70f05..27a008ef 100644
--- a/src/localize/languages/ca.json
+++ b/src/localize/languages/ca.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Indicador animat del progrés",
         "editor_label": "Opcions de característiques",
+        "max_simultaneous_engine_requests": "",
         "media_chunk_size": "Mida del fragment multimèdia"
       },
       "style": {
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index dbb6703c..f2279649 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Animated Progress Indicator",
         "editor_label": "Feature Options",
+        "max_simultaneous_engine_requests": "Max simultaneous camera engine requests",
         "media_chunk_size": "Media chunk size"
       },
       "style": {
diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json
index 567e431b..b41ebeaa 100644
--- a/src/localize/languages/fr.json
+++ b/src/localize/languages/fr.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Indicateur de progression animé",
         "editor_label": "Options de fonctionnalités",
+        "max_simultaneous_engine_requests": "",
         "media_chunk_size": "Taille du morceau de média"
       },
       "style": {
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index 7876a92f..a87f9b75 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Indicatore di avanzamento animato",
         "editor_label": "Opzioni funzionalità",
+        "max_simultaneous_engine_requests": "",
         "media_chunk_size": "Dimensione del blocco multimediale"
       },
       "style": {
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index bd16f26b..2fac32db 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Indicador de Carregamento Animado",
         "editor_label": "Opções de recursos",
+        "max_simultaneous_engine_requests": "",
         "media_chunk_size": "Tamanho do bloco de mídia"
       },
       "style": {
diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json
index ad342dea..7fdb3de5 100644
--- a/src/localize/languages/pt-PT.json
+++ b/src/localize/languages/pt-PT.json
@@ -401,6 +401,7 @@
       "features": {
         "animated_progress_indicator": "Animação na barra de progresso",
         "editor_label": "Editor de etiquetas",
+        "max_simultaneous_engine_requests": "",
         "media_chunk_size": "Tamanho do ficheiro"
       },
       "style": {


Test patch:
diff --git a/tests/camera-manager/manager.test.ts b/tests/camera-manager/manager.test.ts
index 69a609c2..545235b3 100644
--- a/tests/camera-manager/manager.test.ts
+++ b/tests/camera-manager/manager.test.ts
@@ -1271,5 +1271,22 @@ describe('CameraManager', async () => {
         middleTime,
       );
     });
+
+    it('handles null return value', async () => {
+      const api = createCardAPI();
+      const engine = mock<CameraManagerEngine>();
+      vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
+      const manager = createCameraManager(api, engine);
+
+      expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
+      engine.getMediaSeekTime.mockResolvedValue(null);
+
+      const media = new TestViewMedia({
+        cameraID: 'id',
+        startTime: startTime,
+        endTime: endTime,
+      });
+      expect(await manager.getMediaSeekTime(media, middleTime)).toBeNull();
+    });
   });
 });
diff --git a/tests/config/profiles/low-performance.test.ts b/tests/config/profiles/low-performance.test.ts
index 61c7f709..b491e186 100644
--- a/tests/config/profiles/low-performance.test.ts
+++ b/tests/config/profiles/low-performance.test.ts
@@ -39,6 +39,7 @@ it('low performance profile', () => {
     'menu.buttons.timeline.enabled': false,
     'menu.style': 'outside',
     'performance.features.animated_progress_indicator': false,
+    'performance.features.max_simultaneous_engine_requests': 1,
     'performance.features.media_chunk_size': 10,
     'performance.style.border_radius': false,
     'performance.style.box_shadow': false,