Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit shm frame count #12363

Merged
merged 15 commits into from
Sep 3, 2024
16 changes: 8 additions & 8 deletions docs/docs/frigate/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,23 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your

Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**.

The default shm size of **64MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose).
The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose).

The Frigate container also stores logs in shm, which can take up to **30MB**, so make sure to take this into account in your math as well.
The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well.

You can calculate the necessary shm size for each camera with the following formula using the resolution specified for detect:
You can calculate the **minimum** shm size for each camera with the following formula using the resolution specified for detect:

```console
# Replace <width> and <height>
$ python -c 'print("{:.2f}MB".format((<width> * <height> * 1.5 * 9 + 270480) / 1048576))'
$ python -c 'print("{:.2f}MB".format((<width> * <height> * 1.5 * 10 + 270480) / 1048576))'

# Example for 1280x720
$ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 9 + 270480) / 1048576))'
12.12MB
$ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 10 + 270480) / 1048576))'
13.44MB

# Example for eight cameras detecting at 1280x720, including logs
$ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 9 + 270480) / 1048576) * 8 + 30))'
126.99MB
$ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 10 + 270480) / 1048576) * 8 + 40))'
136.99MB
```

The shm size cannot be set per container for Home Assistant add-ons. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration.
Expand Down
41 changes: 28 additions & 13 deletions frigate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ def start_camera_capture_processes(self) -> None:
capture_process = mp.Process(
target=capture_camera,
name=f"camera_capture:{name}",
args=(name, config, self.camera_metrics[name]),
args=(name, config, self.shm_frame_count, self.camera_metrics[name]),
)
capture_process.daemon = True
self.camera_metrics[name]["capture_process"] = capture_process
Expand Down Expand Up @@ -601,19 +601,34 @@ def start_watchdog(self) -> None:
self.frigate_watchdog.start()

def check_shm(self) -> None:
available_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1)
min_req_shm = 30

for _, camera in self.config.cameras.items():
min_req_shm += round(
(camera.detect.width * camera.detect.height * 1.5 * 9 + 270480)
/ 1048576,
1,
)
total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1)

# required for log files + nginx cache
min_req_shm = 40 + 10

if self.config.birdseye.restream:
min_req_shm += 8

available_shm = total_shm - min_req_shm
cam_total_frame_size = 0

for camera in self.config.cameras.values():
if camera.enabled:
cam_total_frame_size += round(
(camera.detect.width * camera.detect.height * 1.5 + 270480)
/ 1048576,
1,
)

self.shm_frame_count = min(50, int(available_shm / (cam_total_frame_size)))

logger.debug(
f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {self.shm_frame_count} frames for each camera in SHM"
)

if available_shm < min_req_shm:
if self.shm_frame_count < 10:
logger.warning(
f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {min_req_shm}MB."
f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size)}MB."
)

def init_auth(self) -> None:
Expand Down Expand Up @@ -718,6 +733,7 @@ def start(self) -> None:
self.init_historical_regions()
self.start_detected_frames_processor()
self.start_camera_processors()
self.check_shm()
self.start_camera_capture_processes()
self.start_audio_processors()
self.start_storage_maintainer()
Expand All @@ -729,7 +745,6 @@ def start(self) -> None:
self.start_event_cleanup()
self.start_record_cleanup()
self.start_watchdog()
self.check_shm()
self.init_auth()

# Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT
Expand Down
8 changes: 5 additions & 3 deletions frigate/embeddings/maintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ def _process_updates(self) -> None:
try:
frame_id = f"{camera}{data['frame_time']}"
yuv_frame = self.frame_manager.get(frame_id, camera_config.frame_shape_yuv)
data["thumbnail"] = self._create_thumbnail(yuv_frame, data["box"])
self.tracked_events[data["id"]].append(data)
self.frame_manager.close(frame_id)

if yuv_frame is not None:
data["thumbnail"] = self._create_thumbnail(yuv_frame, data["box"])
self.tracked_events[data["id"]].append(data)
self.frame_manager.close(frame_id)
except FileNotFoundError:
pass

Expand Down
1 change: 1 addition & 0 deletions frigate/object_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def receiveSignal(signalNumber, frame):
)

if input_frame is None:
logger.warning(f"Failed to get frame {connection_id} from SHM")
continue

# detect and send the output
Expand Down
24 changes: 16 additions & 8 deletions frigate/object_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def compute_score(self):
"""get median of scores for object."""
return median(self.score_history)

def update(self, current_frame_time, obj_data):
def update(self, current_frame_time: float, obj_data, has_valid_frame: bool):
thumb_update = False
significant_change = False
autotracker_update = False
Expand All @@ -168,7 +168,7 @@ def update(self, current_frame_time, obj_data):
self.false_positive = self._is_false_positive()
self.active = self.is_active()

if not self.false_positive:
if not self.false_positive and has_valid_frame:
# determine if this frame is a better thumbnail
if self.thumbnail_data is None or is_better_thumbnail(
self.obj_data["label"],
Expand Down Expand Up @@ -668,10 +668,14 @@ def on(self, event_type: str, callback: Callable[[dict], None]):
def update(self, frame_time, current_detections, motion_boxes, regions):
# get the new frame
frame_id = f"{self.name}{frame_time}"

current_frame = self.frame_manager.get(
frame_id, self.camera_config.frame_shape_yuv
)

if current_frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")

tracked_objects = self.tracked_objects.copy()
current_ids = set(current_detections.keys())
previous_ids = set(tracked_objects.keys())
Expand All @@ -695,14 +699,14 @@ def update(self, frame_time, current_detections, motion_boxes, regions):
for id in updated_ids:
updated_obj = tracked_objects[id]
thumb_update, significant_update, autotracker_update = updated_obj.update(
frame_time, current_detections[id]
frame_time, current_detections[id], current_frame is not None
)

if autotracker_update or significant_update:
for c in self.callbacks["autotrack"]:
c(self.name, updated_obj, frame_time)

if thumb_update:
if thumb_update and current_frame is not None:
# ensure this frame is stored in the cache
if (
updated_obj.thumbnail_data["frame_time"] == frame_time
Expand Down Expand Up @@ -886,12 +890,16 @@ def update(self, frame_time, current_detections, motion_boxes, regions):

with self.current_frame_lock:
self.tracked_objects = tracked_objects
self.current_frame_time = frame_time
self.motion_boxes = motion_boxes
self.regions = regions
self._current_frame = current_frame
if self.previous_frame_id is not None:
self.frame_manager.close(self.previous_frame_id)

if current_frame is not None:
self.current_frame_time = frame_time
self._current_frame = current_frame

if self.previous_frame_id is not None:
self.frame_manager.close(self.previous_frame_id)

self.previous_frame_id = frame_id


Expand Down
16 changes: 7 additions & 9 deletions frigate/output/birdseye.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,16 +357,14 @@ def copy_to_position(self, position, camera=None, frame_time=None):
frame = None
channel_dims = None
else:
try:
frame = self.frame_manager.get(
f"{camera}{frame_time}", self.config.cameras[camera].frame_shape_yuv
)
except FileNotFoundError:
# TODO: better frame management would prevent this edge case
logger.warning(
f"Unable to copy frame {camera}{frame_time} to birdseye."
)
frame = self.frame_manager.get(
f"{camera}{frame_time}", self.config.cameras[camera].frame_shape_yuv
)

if frame is None:
logger.debug(f"Unable to copy frame {camera}{frame_time} to birdseye.")
return

channel_dims = self.cameras[camera]["channel_dims"]

copy_yuv_to_position(
Expand Down
13 changes: 6 additions & 7 deletions frigate/output/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def receiveSignal(signalNumber, frame):
signal.signal(signal.SIGINT, receiveSignal)

frame_manager = SharedMemoryFrameManager()
previous_frames = {}

# start a websocket server on 8082
WebSocketWSGIHandler.http_version = "1.1"
Expand Down Expand Up @@ -99,6 +98,10 @@ def receiveSignal(signalNumber, frame):

frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)

if frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")
continue

# send camera frame to ffmpeg process if websockets are connected
if any(
ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager
Expand Down Expand Up @@ -128,10 +131,6 @@ def receiveSignal(signalNumber, frame):
)
preview_write_times[camera] = frame_time

# delete frames after they have been used for output
if camera in previous_frames:
frame_manager.delete(f"{camera}{previous_frames[camera]}")

# if another camera generated a preview,
# check for any cameras that are currently offline
# and need to generate a preview
Expand All @@ -141,7 +140,7 @@ def receiveSignal(signalNumber, frame):
preview_recorders[camera].flag_offline(frame_time)
preview_write_times[camera] = frame_time

previous_frames[camera] = frame_time
frame_manager.close(frame_id)

move_preview_frames("clips")

Expand All @@ -161,7 +160,7 @@ def receiveSignal(signalNumber, frame):

frame_id = f"{camera}{frame_time}"
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)
frame_manager.delete(frame_id)
frame_manager.close(frame_id)

detection_subscriber.stop()

Expand Down
4 changes: 4 additions & 0 deletions frigate/ptz/autotrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def motion_estimator(self, detections, frame_time, camera):
frame_id, self.camera_config.frame_shape_yuv
)

if yuv_frame is None:
self.coord_transformations = None
return None

frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2GRAY_I420)

# mask out detections for better motion estimation
Expand Down
15 changes: 15 additions & 0 deletions frigate/review/maintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ def update_existing_segment(
yuv_frame = self.frame_manager.get(
frame_id, camera_config.frame_shape_yuv
)

if yuv_frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")
return

self.update_segment(
segment, camera_config, yuv_frame, active_objects, prev_data
)
Expand All @@ -305,6 +310,11 @@ def update_existing_segment(
yuv_frame = self.frame_manager.get(
frame_id, camera_config.frame_shape_yuv
)

if yuv_frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")
return

segment.save_full_frame(camera_config, yuv_frame)
self.frame_manager.close(frame_id)
self.update_segment(segment, camera_config, None, [], prev_data)
Expand Down Expand Up @@ -401,6 +411,11 @@ def check_if_new_segment(
yuv_frame = self.frame_manager.get(
frame_id, camera_config.frame_shape_yuv
)

if yuv_frame is None:
logger.debug(f"Failed to get frame {frame_id} from SHM")
return

self.active_review_segments[camera].update_frame(
camera_config, yuv_frame, active_objects
)
Expand Down
39 changes: 27 additions & 12 deletions frigate/util/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,31 +687,46 @@ def delete(self, name):

class SharedMemoryFrameManager(FrameManager):
def __init__(self):
self.shm_store = {}
self.shm_store: dict[str, shared_memory.SharedMemory] = {}

def create(self, name, size) -> AnyStr:
def create(self, name: str, size) -> AnyStr:
shm = shared_memory.SharedMemory(name=name, create=True, size=size)
self.shm_store[name] = shm
return shm.buf

def get(self, name, shape):
if name in self.shm_store:
shm = self.shm_store[name]
else:
shm = shared_memory.SharedMemory(name=name)
self.shm_store[name] = shm
return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
def get(self, name: str, shape) -> Optional[np.ndarray]:
try:
if name in self.shm_store:
shm = self.shm_store[name]
else:
shm = shared_memory.SharedMemory(name=name)
self.shm_store[name] = shm
return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
except FileNotFoundError:
return None

def close(self, name):
def close(self, name: str):
if name in self.shm_store:
self.shm_store[name].close()
del self.shm_store[name]

def delete(self, name):
def delete(self, name: str):
if name in self.shm_store:
self.shm_store[name].close()
self.shm_store[name].unlink()

try:
self.shm_store[name].unlink()
except FileNotFoundError:
pass

del self.shm_store[name]
else:
try:
shm = shared_memory.SharedMemory(name=name)
shm.close()
shm.unlink()
except FileNotFoundError:
pass


def create_mask(frame_shape, mask):
Expand Down
Loading