Skip to content

Refactor EventStream::sendFrame to use reusable filepath member#4616

Merged
connortechnology merged 4 commits intomasterfrom
copilot/refactor-filepath-handling
Mar 4, 2026
Merged

Refactor EventStream::sendFrame to use reusable filepath member#4616
connortechnology merged 4 commits intomasterfrom
copilot/refactor-filepath-handling

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 11, 2026

Eliminates per-frame heap allocations in EventStream::sendFrame by replacing the local std::string filepath variable with a reusable member reuse_filepath_.

Changes

  • src/zm_eventstream.h: Added std::string reuse_filepath_ member to EventStream class
  • src/zm_eventstream.cpp:
    • Replaced local filepath variable with reuse_filepath_.clear() at method entry
    • Updated all references (assignments, .c_str(), .empty() calls) to use member variable

Performance Impact

After the first frame, the string's internal buffer is reused across subsequent sendFrame() calls, avoiding repeated allocations from stringtf() on hot path.

Before:

std::string filepath;  // new allocation each sendFrame call
filepath = stringtf(...);

After:

reuse_filepath_.clear();  // reuses existing buffer capacity
reuse_filepath_ = stringtf(...);
Original prompt

Problem Statement

Refactor the filepath handling in EventStream::sendFrame to use a pre-allocated reusable std::string member instead of the C-style char[PATH_MAX] buffer with snprintf() introduced in PR #4609.

Context

PR #4609 (commit c8d5ea3) optimized EventStream::sendFrame by replacing stringtf() calls with snprintf() into a stack-allocated char[PATH_MAX] buffer wrapped in std::string_view. While this eliminated heap allocations, it uses C-style string handling.

This PR proposes a more idiomatic C++ approach that achieves the same performance benefits while maintaining type safety.

Proposed Changes

1. Add reusable string member to EventStream class

File: src/zm_eventstream.h

In the private section of the EventStream class (around line 127-130), modify:

private:
  bool send_file(const std::string &filepath);
  bool send_buffer(uint8_t * buffer, int size);
  Storage *storage;
  FFmpeg_Input  *ffmpeg_input;
  Image reuse_image_;  // reused across sendFrame calls to avoid per-frame heap alloc
  std::string reuse_filepath_;  // reused across sendFrame calls to avoid per-frame heap alloc
};

2. Refactor sendFrame method to use reuse_filepath_

File: src/zm_eventstream.cpp

In the EventStream::sendFrame method (starting around line 844), replace the current implementation that uses char filepath_buf[PATH_MAX] and std::string_view filepath with the reusable string approach:

Current code (from PR #4609):

  // Stack buffer + string_view avoids the per-frame heap allocations that
  // stringtf() would incur (2 allocs per call).  All consumers need const char*
  // anyway (access, ReadJpeg, Image ctor, Debug/Error format args).
  char filepath_buf[PATH_MAX] = "";
  std::string_view filepath;  // non-owning view into filepath_buf

  // This needs to be abstracted.  If we are saving jpgs, then load the capture file.
  // If we are only saving analysis frames, then send that.
  if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
    snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
    if (access(filepath_buf, R_OK) != 0) {
      Debug(1, "analyze file %s not found will try to stream from other", filepath_buf);
      snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
      if (access(filepath_buf, R_OK) != 0) {
        Debug(1, "capture file %s not found either", filepath_buf);
        filepath_buf[0] = '\0';
      }
    }
    filepath = filepath_buf;
  } else if (event_data->SaveJPEGs & 1) {
    snprintf(filepath_buf, sizeof(filepath_buf), staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
    filepath = filepath_buf;
  } else if (!ffmpeg_input) {
    Fatal("JPEGS not saved. zms is not capable of streaming jpegs from mp4 yet");
    return false;
  }

Replace with:

  // Reusable string member avoids per-frame heap allocations.
  // After the first frame, the string's buffer is reused (unless path exceeds capacity).
  reuse_filepath_.clear();

  // This needs to be abstracted.  If we are saving jpgs, then load the capture file.
  // If we are only saving analysis frames, then send that.
  if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
    reuse_filepath_ = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
    if (access(reuse_filepath_.c_str(), R_OK) != 0) {
      Debug(1, "analyze file %s not found will try to stream from other", reuse_filepath_.c_str());
      reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
      if (access(reuse_filepath_.c_str(), R_OK) != 0) {
        Debug(1, "capture file %s not found either", reuse_filepath_.c_str());
        reuse_filepath_.clear();
      }
    }
  } else if (event_data->SaveJPEGs & 1) {
    reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
  } else if (!ffmpeg_input) {
    Fatal("JPEGS not saved. zms is not capable of streaming jpegs from mp4 yet");
    return false;
  }

3. Update all uses of filepath in sendFrame

Throughout the rest of the sendFrame method, replace references to filepath_buf and filepath with reuse_filepath_:

Around line 877-882 (STREAM_MPEG path):

  if ( type == STREAM_MPEG ) {
    Image image(reuse_filepath_.c_str());

    Image *send_image = prepareImage(&image);

Around line 901-908 (send_raw path):

    if (send_raw) {
      fprintf(stdout, "--" BOUNDARY "\r\n");
      if (!send_file(reuse_filepath_)) {
        Error("Can't send %s: %s", reuse_filepath_.c_str(), strerror(errno));
        return false;
      }

Around line 910-913 (image loading path):

      Image *image = null...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 2 commits February 11, 2026 04:19
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor filepath handling in EventStream::sendFrame Refactor EventStream::sendFrame to use reusable filepath member Feb 11, 2026
@connortechnology connortechnology marked this pull request as ready for review March 4, 2026 22:44
Copilot AI review requested due to automatic review settings March 4, 2026 22:44
@connortechnology connortechnology merged commit b1067e0 into master Mar 4, 2026
6 of 10 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors EventStream::sendFrame() filepath construction to reuse a class member string (reuse_filepath_) across frames, aiming to reduce per-frame overhead on the event streaming hot path.

Changes:

  • Added reuse_filepath_ member to EventStream.
  • Updated sendFrame() to clear/assign and reference reuse_filepath_ instead of a local filepath variable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/zm_eventstream.h Adds reusable filepath member on EventStream.
src/zm_eventstream.cpp Switches filepath construction/usage in sendFrame() to the reusable member.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

bool send_buffer(uint8_t * buffer, int size);
Storage *storage;
FFmpeg_Input *ffmpeg_input;
std::string reuse_filepath_; // reused across sendFrame calls to avoid per-frame heap alloc
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The member comment says it avoids per-frame heap allocations, but with the current stringtf() usage there are still heap allocations per call. Consider rewording to reflect that this primarily reuses the destination string’s capacity (and/or changing the formatting approach so the comment becomes true).

Suggested change
std::string reuse_filepath_; // reused across sendFrame calls to avoid per-frame heap alloc
std::string reuse_filepath_; // reused across sendFrame calls to reuse capacity and reduce per-frame heap allocations

Copilot uses AI. Check for mistakes.
Comment on lines +866 to 871
reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath.c_str(), &filestat) < 0) {
Debug(1, "Capture file %s not found (bulk/interpolated frame %d), trying ffmpeg_input",
filepath.c_str(), curr_frame_id);
filepath = "";
reuse_filepath.c_str(), curr_frame_id);
reuse_filepathi.clear();
// Fall through — ffmpeg_input will be tried below if available
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo/identifier mismatches here will not compile: reuse_filepath should be reuse_filepath_, and reuse_filepathi is an undefined identifier. Also the .c_str() call in the Debug uses the wrong variable name. Rename these consistently to reuse_filepath_ and clear via reuse_filepath_.clear().

Copilot uses AI. Check for mistakes.
Comment on lines +848 to +857
// Reusable string member avoids per-frame heap allocations.
// After the first frame, the string's buffer is reused (unless path exceeds capacity).
reuse_filepath_.clear();
struct stat filestat = {};

// This needs to be abstracted. If we are saving jpgs, then load the capture file.
// If we are only saving analysis frames, then send that.
if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
filepath = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(filepath.c_str(), &filestat) < 0) {
Debug(1, "analyze file %s not found will try to stream from other", filepath.c_str());
filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(filepath.c_str(), &filestat) < 0) {
Debug(1, "capture file %s not found either", filepath.c_str());
filepath = "";
reuse_filepath_ = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comments claim this avoids per-frame heap allocations, but stringtf() allocates a heap buffer (unique_ptr<char[]>) on every call. Reusing the destination std::string capacity only avoids reallocation of reuse_filepath_ itself, not the allocations inside stringtf(). To actually eliminate per-frame heap allocations, keep the prior snprintf/stack-buffer approach or introduce a formatter that writes directly into reuse_filepath_ (e.g., vsnprintf into a resized string buffer).

Copilot uses AI. Check for mistakes.
Comment on lines 851 to +867
struct stat filestat = {};

// This needs to be abstracted. If we are saving jpgs, then load the capture file.
// If we are only saving analysis frames, then send that.
if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) {
filepath = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(filepath.c_str(), &filestat) < 0) {
Debug(1, "analyze file %s not found will try to stream from other", filepath.c_str());
filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(filepath.c_str(), &filestat) < 0) {
Debug(1, "capture file %s not found either", filepath.c_str());
filepath = "";
reuse_filepath_ = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
Debug(1, "analyze file %s not found will try to stream from other", reuse_filepath_.c_str());
reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath_.c_str(), &filestat) < 0) {
Debug(1, "capture file %s not found either", reuse_filepath_.c_str());
reuse_filepath_.clear();
}
}
} else if (event_data->SaveJPEGs & 1) {
filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(filepath.c_str(), &filestat) < 0) {
reuse_filepath_ = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id);
if (stat(reuse_filepath.c_str(), &filestat) < 0) {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filestat is only used as an out-param to stat() for existence checks, and none of its fields are read afterwards. This adds unnecessary work in the hot path. Consider using access(path, R_OK) (as in PR #4609) and drop filestat entirely, or otherwise avoid filling a struct stat when you only need to know if the file is readable.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants