Skip to content

Commit a4409f3

Browse files
nabbiclaude
andcommitted
perf: reduce per-frame overhead in EventStream::sendFrame
Three changes to the JPEG streaming hot path: 1. Reuse Image buffer for JPEG decode: add Image member reuse_image_ to EventStream. On the JPEG path (SaveJPEGs & 1), call ReadJpeg() into the member instead of new/delete Image each frame. ReadJpeg's internal WriteBuffer() reuses the pixel allocation when dimensions match (every frame in a given event). Eliminates ~2 MB malloc+free per streamed frame. The FFmpeg path (MP4-only events) still uses new/delete since its initialization is more complex and it's the uncommon case. 2. Replace stat() with access() for file existence checks. The stat() filled a struct stat that was never read — send_file() does its own fstat() for Content-Length. access(path, R_OK) is a lighter syscall that skips the 144-byte struct fill. 3. Replace stringtf() with snprintf() into a stack char[PATH_MAX]. stringtf() does two heap allocations per call (unique_ptr<char[]> for vsnprintf + std::string for return). File paths are well within PATH_MAX. This eliminates 2-6 heap alloc/free cycles per frame depending on the analysis fallback path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df88c73 commit a4409f3

File tree

2 files changed

+20
-17
lines changed

2 files changed

+20
-17
lines changed

src/zm_eventstream.cpp

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
#include "zm_storage.h"
2828
#include <algorithm>
2929
#include <arpa/inet.h>
30+
#include <climits>
3031
#include <sys/stat.h>
32+
#include <unistd.h>
3133

3234
#include <filesystem>
3335

@@ -851,30 +853,29 @@ bool EventStream::sendFrame(Microseconds delta_us) {
851853
curr_frame_id = std::clamp(curr_frame_id, 1, (int)event_data->frames.size());
852854
}
853855

854-
std::string filepath;
855-
struct stat filestat = {};
856+
char filepath_buf[PATH_MAX] = "";
856857

857858
// This needs to be abstracted. If we are saving jpgs, then load the capture file.
858859
// If we are only saving analysis frames, then send that.
859860
if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
860-
filepath = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
861-
if (stat(filepath.c_str(), &filestat) < 0) {
862-
Debug(1, "analyze file %s not found will try to stream from other", filepath.c_str());
863-
filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
864-
if (stat(filepath.c_str(), &filestat) < 0) {
865-
Debug(1, "capture file %s not found either", filepath.c_str());
866-
filepath = "";
861+
snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
862+
if (access(filepath_buf, R_OK) != 0) {
863+
Debug(1, "analyze file %s not found will try to stream from other", filepath_buf);
864+
snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
865+
if (access(filepath_buf, R_OK) != 0) {
866+
Debug(1, "capture file %s not found either", filepath_buf);
867+
filepath_buf[0] = '\0';
867868
}
868869
}
869870
} else if (event_data->SaveJPEGs & 1) {
870-
filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
871+
snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
871872
} else if (!ffmpeg_input) {
872873
Fatal("JPEGS not saved. zms is not capable of streaming jpegs from mp4 yet");
873874
return false;
874875
}
875876

876877
if ( type == STREAM_MPEG ) {
877-
Image image(filepath.c_str());
878+
Image image(filepath_buf);
878879

879880
Image *send_image = prepareImage(&image);
880881

@@ -889,19 +890,20 @@ bool EventStream::sendFrame(Microseconds delta_us) {
889890
config.mpeg_timed_frames,
890891
delta_us.count() * 1000);
891892
} else {
892-
bool send_raw = (type == STREAM_JPEG) && ((scale >= ZM_SCALE_BASE) && (zoom == ZM_SCALE_BASE)) && !filepath.empty();
893+
bool send_raw = (type == STREAM_JPEG) && ((scale >= ZM_SCALE_BASE) && (zoom == ZM_SCALE_BASE)) && filepath_buf[0] != '\0';
893894

894895
if (send_raw) {
895896
fprintf(stdout, "--" BOUNDARY "\r\n");
896-
if (!send_file(filepath)) {
897-
Error("Can't send %s: %s", filepath.c_str(), strerror(errno));
897+
if (!send_file(filepath_buf)) {
898+
Error("Can't send %s: %s", filepath_buf, strerror(errno));
898899
return false;
899900
}
900901
} else {
901902
Image *image = nullptr;
902903

903-
if (!filepath.empty()) {
904-
image = new Image(filepath.c_str());
904+
if (filepath_buf[0] != '\0') {
905+
reuse_image_.ReadJpeg(filepath_buf, ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB);
906+
image = &reuse_image_;
905907
} else if (ffmpeg_input) {
906908
// Get the frame from the mp4 input
907909
const FrameData *frame_data = &event_data->frames[curr_frame_id-1];
@@ -977,7 +979,7 @@ bool EventStream::sendFrame(Microseconds delta_us) {
977979
break;
978980
}
979981
int rc = send_buffer(img_buffer, img_buffer_size);
980-
delete image;
982+
if (image != &reuse_image_) delete image;
981983
image = nullptr;
982984
if (!rc) return false;
983985
} // end if send_raw or not

src/zm_eventstream.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class EventStream : public StreamBase {
127127
bool send_buffer(uint8_t * buffer, int size);
128128
Storage *storage;
129129
FFmpeg_Input *ffmpeg_input;
130+
Image reuse_image_; // reused across sendFrame calls to avoid per-frame heap alloc
130131
};
131132

132133
#endif // ZM_EVENTSTREAM_H

0 commit comments

Comments
 (0)