diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f37ff0ab..80e09e9c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -107,6 +107,7 @@ endif() add_executable(zmc zmc.cpp) add_executable(zms zms.cpp) add_executable(zmu zmu.cpp) +add_executable(zmbenchmark zmbenchmark.cpp) target_link_libraries(zmc PRIVATE @@ -129,6 +130,13 @@ target_link_libraries(zmu ${ZM_EXTRA_LIBS} ${CMAKE_DL_LIBS}) +target_link_libraries(zmbenchmark + PRIVATE + zm-core-interface + zm + ${ZM_EXTRA_LIBS} + ${CMAKE_DL_LIBS}) + # Generate man files for the binaries destined for the bin folder if(BUILD_MAN) foreach(CBINARY zmc zmu) diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index ad9bb3ef1..8a393cf9c 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -91,46 +91,8 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { Warning("You have set the max video packets in the queue to %u." " The queue is full. Either Analysis is not keeping up or" " your camera's keyframe interval is larger than this setting." - " We are dropping packets.", max_video_packet_count); - if (add_packet->keyframe) { - // Have a new keyframe, so delete everything - while ((*pktQueue.begin() != add_packet) and (packet_counts[video_stream_id] > max_video_packet_count)) { - std::shared_ptr zm_packet = *pktQueue.begin(); - ZMLockedPacket *lp = new ZMLockedPacket(zm_packet); - if (!lp->trylock()) { - Debug(1, "Found locked packet when trying to free up video packets. Can't continue"); - delete lp; - break; - } - delete lp; + , max_video_packet_count); - for ( - std::list::iterator iterators_it = iterators.begin(); - iterators_it != iterators.end(); - ++iterators_it - ) { - packetqueue_iterator *iterator_it = *iterators_it; - // Have to check each iterator and make sure it doesn't point to the packet we are about to delete - if ( *(*iterator_it) == zm_packet ) { - Debug(1, "Bumping IT because it is at the front that we are deleting"); - ++(*iterators_it); - } - } // end foreach iterator - - pktQueue.pop_front(); - packet_counts[zm_packet->packet.stream_index] -= 1; - Debug(1, - "Deleting a packet with stream index:%d image_index:%d with keyframe:%d, video frames in queue:%d max: %d, queuesize:%zu", - zm_packet->packet.stream_index, - zm_packet->image_index, - zm_packet->keyframe, - packet_counts[video_stream_id], - max_video_packet_count, - pktQueue.size()); - } // end while - } - } // end if too many video packets - if (max_video_packet_count > 0) { while (packet_counts[video_stream_id] > max_video_packet_count) { Error("Unable to free up older packets. Waiting."); condition.notify_all(); @@ -148,16 +110,13 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { packet_counts[add_packet->packet.stream_index]); for ( - std::list::iterator iterators_it = iterators.begin(); + auto iterators_it = iterators.begin(); iterators_it != iterators.end(); ++iterators_it ) { packetqueue_iterator *iterator_it = *iterators_it; if (*iterator_it == pktQueue.end()) { - Debug(4, "pointing it %p to back", iterator_it); --(*iterator_it); - } else { - Debug(4, "it %p not at end", iterator_it); } } // end foreach iterator } // end lock scope @@ -166,7 +125,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { condition.notify_all(); return true; -} // end bool PacketQueue::queuePacket(ZMPacket* zm_packet) +} // end bool PacketQueue::queuePacket(ZMPacket* zm_packet) void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { // Only do queueCleaning if we are adding a video keyframe, so that we guarantee that there is one. @@ -195,7 +154,6 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { add_packet->packet.stream_index, video_stream_id, add_packet->keyframe, keep_keyframes, packet_counts[video_stream_id], pre_event_video_packet_count, ( *(pktQueue.begin()) != add_packet ) ); - Warning("Keyframe interval may be larger than MaxImageBufferCount and PreEventCount. Please increase MaxImageBufferCount"); return; } std::unique_lock lck(mutex); @@ -241,8 +199,8 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { return; } - packetqueue_iterator it = pktQueue.begin(); - packetqueue_iterator next_front = pktQueue.begin(); + auto it = pktQueue.begin(); + auto next_front = pktQueue.begin(); // First packet is special because we know it is a video keyframe and only need to check for lock std::shared_ptr zm_packet = *it; @@ -250,18 +208,13 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { return; } - Debug(1, "trying lock on first packet"); ZMLockedPacket *lp = new ZMLockedPacket(zm_packet); if (lp->trylock()) { int video_packets_to_delete = 0; // This is a count of how many packets we will delete so we know when to stop looking - Debug(1, "Have lock on first packet"); + Debug(4, "Have lock on first packet"); ++it; delete lp; - if (it == pktQueue.end()) { - Debug(1, "Hit end already"); - it = pktQueue.begin(); - } else { // Since we have many packets in the queue, we should NOT be pointing at end so don't need to test for that while (*it != add_packet) { zm_packet = *it; @@ -272,10 +225,14 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { } delete lp; - if (is_there_an_iterator_pointing_to_packet(zm_packet) and (pktQueue.begin() == next_front)) { - Warning("Found iterator at beginning of queue. Some thread isn't keeping up"); +#if 0 + // There are no threads that follow analysis thread. So there cannot be an it pointing here + if (is_there_an_iterator_pointing_to_packet(zm_packet)) { + if (pktQueue.begin() == next_front) + Warning("Found iterator at beginning of queue. Some thread isn't keeping up"); break; } +#endif if (zm_packet->packet.stream_index == video_stream_id) { if (zm_packet->keyframe) { @@ -283,15 +240,14 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { next_front = it; } ++video_packets_to_delete; - Debug(4, "Counted %d video packets. Which would leave %d in packetqueue tail count is %d", - video_packets_to_delete, packet_counts[video_stream_id]-video_packets_to_delete, tail_count); + Debug(4, "Counted %d video packets. Which would leave %d in packetqueue tail count is %d", + video_packets_to_delete, packet_counts[video_stream_id]-video_packets_to_delete, tail_count); if (packet_counts[video_stream_id] - video_packets_to_delete <= pre_event_video_packet_count + tail_count) { break; } } ++it; } // end while - } } // end if first packet not locked Debug(1, "Resulting pointing at latest packet? %d, next front points to begin? %d", ( *it == add_packet ), diff --git a/src/zm_time.h b/src/zm_time.h index f7574bbf7..d3d5b95c5 100644 --- a/src/zm_time.h +++ b/src/zm_time.h @@ -83,4 +83,41 @@ Duration duration_cast(timeval const &tv) { } } +// +// This can be used for benchmarking. It will measure the time in between +// its constructor and destructor (or when you call Finish()) and add that +// duration to a microseconds clock. +// +class TimeSegmentAdder { + public: + TimeSegmentAdder(Microseconds &in_target) : + target_(in_target), + start_time_(std::chrono::steady_clock::now()), + finished_(false) { + } + + ~TimeSegmentAdder() { + Finish(); + } + + // Call this to stop the timer and add the timed duration to `target`. + void Finish() { + if (!finished_) { + const TimePoint end_time = std::chrono::steady_clock::now(); + target_ += (std::chrono::duration_cast(end_time - start_time_)); + } + finished_ = true; + } + + private: + // This is where we will add our duration to. + Microseconds &target_; + + // The time we started. + const TimePoint start_time_; + + // True when it has finished timing. + bool finished_; +}; + #endif // ZM_TIME_H diff --git a/src/zmbenchmark.cpp b/src/zmbenchmark.cpp new file mode 100644 index 000000000..244094a54 --- /dev/null +++ b/src/zmbenchmark.cpp @@ -0,0 +1,326 @@ +// +// ZoneMinder Benchmark, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#include +#include +#include +#include +#include +#include + +#include "zm_config.h" +#include "zm_image.h" +#include "zm_monitor.h" +#include "zm_time.h" +#include "zm_utils.h" +#include "zm_zone.h" + +static std::mt19937 mt_rand(111); + +// +// This allows you to feed in a set of columns and timing rows, and print it +// out in a nice-looking table. +// +class TimingsTable { + public: + explicit TimingsTable(std::vector in_columns) : columns_(std::move(in_columns)) {} + + // + // Add a row to the end of the table. + // + // Args: + // label: The name of the row (printed in the first column). + // timings: The values for all the other columns in this row. + void AddRow(const std::string &label, const std::vector &timings) { + assert(timings.size() == columns_.size()); + Row row; + row.label = label; + row.timings = timings; + rows_.push_back(row); + } + + // + // Print out the table. + // + // Args: + // columnPad: # characters between table columns + // + void Print(const int column_pad = 5) { + // Figure out column widths. + std::vector widths(columns_.size() + 1); + + // The first width is the max of the row labels. + auto result = std::max_element(rows_.begin(), + rows_.end(), + [](const Row &a, const Row &b) -> bool { + return a.label.length() < b.label.length(); + }); + widths[0] = result->label.length() + column_pad; + + // Calculate the rest of the column widths. + for (size_t i = 0 ; i < columns_.size() ; i++) + widths[i + 1] = columns_[i].length() + column_pad; + + auto PrintColStr = [&](size_t icol, const std::string &str) { + printf("%s", str.c_str()); + PrintPadding(widths[icol] - str.length()); + }; + + // Print the header. + PrintColStr(0, ""); + for (size_t i = 0 ; i < columns_.size() ; i++) { + PrintColStr(i + 1, columns_[i]); + } + printf("\n"); + + // Print the timings rows. + for (const Row &row : rows_) { + PrintColStr(0, row.label); + + for (size_t i = 0 ; i < row.timings.size() ; i++) { + std::string num = stringtf("%.2f", std::chrono::duration_cast(row.timings[i]).count()); + PrintColStr(i + 1, num); + } + + printf("\n"); + } + } + + private: + static void PrintPadding(size_t count) { + std::string str(count, ' '); + printf("%s", str.c_str()); + } + + struct Row { + std::string label; + std::vector timings; + }; + + std::vector columns_; + std::vector rows_; +}; + +// +// Generate a greyscale image that simulates a delta that can be fed to +// Zone::CheckAlarms. This first creates a black image, and then it fills +// a box of a certain size inside the image with random data. This is to simulate +// a typical scene where most of the scene doesn't change except a specific region. +// +// Args: +// changeBoxPercent: 0-100 value telling how large the box with random data should be. +// Set to 0 to leave the whole thing black. +// width: The width of the new image. +// height: The height of the new image. +// +// Return: +// An image with all pixels initialized to values in the [minVal,maxVal] range. +// +std::shared_ptr GenerateRandomImage( + const int change_box_percent, + const int width = 3840, + const int height = 2160) { + // Create the image. + Image *image = new Image(width, height, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE); + + // Set it to black initially. + memset((void *) image->Buffer(0, 0), 0, (size_t) image->LineSize() * (size_t) image->Height()); + + // Now randomize the pixels inside a box. + const int box_width = (width * change_box_percent) / 100; + const int box_height = (height * change_box_percent) / 100; + const int box_x = (int) ((uint64_t) mt_rand() * (width - box_width) / RAND_MAX); + const int box_y = (int) ((uint64_t) mt_rand() * (height - box_height) / RAND_MAX); + + for (int y = 0 ; y < box_height ; y++) { + uint8_t *row = (uint8_t *) image->Buffer(box_x, box_y + y); + for (int x = 0 ; x < box_width ; x++) { + row[x] = (uint8_t) mt_rand(); + } + } + + return std::shared_ptr(image); +} + +// +// This is used to help rig up Monitor benchmarks. +// +class TestMonitor : public Monitor { + public: + TestMonitor(int width, int height) : cur_zone_id(111) { + this->width = width; + this->height = height; + + // Create a dummy ref_image. + std::shared_ptr tempImage = GenerateRandomImage(0, width, height); + ref_image = *tempImage; + + shared_data = &temp_shared_data; + } + + // + // Add a new zone to this monitor. + // + // Args: + // checkMethod: This controls how this zone will actually do motion detection. + // + // p_filter_box: The size of the filter to use. + // + void AddZone(Zone::CheckMethod checkMethod, const Vector2 &p_filter_box = Vector2(5, 5)) { + const int p_min_pixel_threshold = 50; + const int p_max_pixel_threshold = 255; + const int p_min_alarm_pixels = 1000; + const int p_max_alarm_pixels = 10000000; + + const int zone_id = cur_zone_id++; + const std::string zone_label = std::string("zone_") + std::to_string(zone_id); + const Zone::ZoneType zone_type = Zone::ZoneType::ACTIVE; + const Polygon poly({Vector2(0, 0), + Vector2(width - 1, 0), + Vector2(width - 1, height - 1), + Vector2(0, height - 1)}); + + Zone zone(this, + zone_id, + zone_label.c_str(), + zone_type, + poly, + kRGBGreen, + Zone::CheckMethod::FILTERED_PIXELS, + p_min_pixel_threshold, + p_max_pixel_threshold, + p_min_alarm_pixels, + p_max_alarm_pixels, + p_filter_box); + zones.push_back(zone); + } + + void SetRefImage(const Image *image) { + ref_image = *image; + } + + private: + SharedData temp_shared_data; + int cur_zone_id; +}; + +// +// Run zone benchmarks on the given image. +// +// Args: +// label: A label to be printed before the output. +// +// image: The image to run the tests on. +// +// p_filter_box: The size of the filter to use for alarm detection. +// +// Return: +// The average time taken for each DetectMotion call. +// +Microseconds RunDetectMotionBenchmark(const std::string &label, + const std::shared_ptr& image, + const Vector2 &p_filter_box) { + // Create a monitor to use for the benchmark. Give it 1 zone that uses + // a 5x5 filter. + TestMonitor testMonitor(image->Width(), image->Height()); + testMonitor.AddZone(Zone::CheckMethod::FILTERED_PIXELS, p_filter_box); + + // Generate a black image to use as the reference image. + std::shared_ptr blackImage = GenerateRandomImage( + 0, image->Width(), image->Height()); + testMonitor.SetRefImage(blackImage.get()); + + Microseconds totalTimeTaken(0); + + // Run a series of passes over DetectMotion. + const int numPasses = 10; + for (int i = 0 ; i < numPasses ; i++) { + printf("\r%s - pass %2d / %2d ", label.c_str(), i + 1, numPasses); + fflush(stdout); + + TimeSegmentAdder adder(totalTimeTaken); + + Event::StringSet zoneSet; + testMonitor.DetectMotion(*image, zoneSet); + } + printf("\n"); + + return totalTimeTaken / numPasses; +} + +// +// This runs a set of Monitor::DetectMotion benchmarks, one for each of the +// "delta box percents" that are passed in. This adds one row to the +// TimingsTable specified. +// +// Args: +// table: The table to add timings into. +// +// deltaBoxPercents: Each of these defines a box size in the delta images +// passed to DetectMotion (larger boxes make it slower, sometimes significantly so). +// +// p_filter_box: Defines the filter size used in DetectMotion. +// +void RunDetectMotionBenchmarks( + TimingsTable &table, + const std::vector &delta_box_percents, + const Vector2 &p_filter_box) { + std::vector timings; + + for (int percent : delta_box_percents) { + Microseconds timing = RunDetectMotionBenchmark( + std::string("DetectMotion: ") + std::to_string(p_filter_box.x_) + "x" + std::to_string(p_filter_box.y_) + + " box, " + std::to_string(percent) + "% delta", + GenerateRandomImage(percent), + p_filter_box); + timings.push_back(timing); + } + + table.AddRow( + std::to_string(p_filter_box.x_) + "x" + std::to_string(p_filter_box.y_) + " filter", + timings); +} + +int main(int argc, char *argv[]) { + // Init global stuff that we need. + config.font_file_location = "../fonts/default.zmfnt"; + config.event_close_mode = "time"; + config.cpu_extensions = true; + + // Detect SSE version. + HwCapsDetect(); + + // Setup the column titles for the TimingsTable we'll generate. + // Each column represents how large the box in the image is with delta pixels. + // Each row represents a different filter size. + const std::vector percents = {0, 10, 50, 100}; + std::vector columns(percents.size()); + std::transform(percents.begin(), percents.end(), columns.begin(), + [](const int percent) { return std::to_string(percent) + "% delta (ms)"; }); + TimingsTable table(columns); + + std::vector filterSizes = {Vector2(3, 3), Vector2(5, 5), Vector2(13, 13)}; + for (const auto filterSize : filterSizes) { + RunDetectMotionBenchmarks(table, percents, filterSize); + } + + table.Print(); + return 0; +} + diff --git a/web/skins/classic/views/filter.php b/web/skins/classic/views/filter.php index 60cc585d4..48673f17b 100644 --- a/web/skins/classic/views/filter.php +++ b/web/skins/classic/views/filter.php @@ -506,16 +506,16 @@ if ( ZM_OPT_EMAIL ) { - + - - + + Id() ) { ?> - +