diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index e16b7821a..47980505d 100755 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -577,6 +577,7 @@ SET(UT_FILES test/unit_tests.cpp test/cv_memory_leaks_tests.cpp test/module_tests.cpp + test/modulefpsthrottle_tests.cpp test/calchistogramcv_tests.cpp test/filenamestrategy_tests.cpp test/test_utils.cpp diff --git a/base/include/Module.h b/base/include/Module.h index 53955d780..b30bd1693 100644 --- a/base/include/Module.h +++ b/base/include/Module.h @@ -111,7 +111,9 @@ class ModuleProps size_t qlen; // run time changing doesn't effect this bool logHealth; // can be updated during runtime with setProps int logHealthFrequency; // 1000 by default - logs the health stats frequency - + int targetFrameRate = 0; + bool enableFpsThrottle = false; + int fpsCheckIterations = 0; // used for VimbaSource where we want to create the max frames and keep // recycling it for the VimbaDrive we announce frames after init - 100/200 see // VimbaSource.cpp on how it is used @@ -126,6 +128,8 @@ class ModuleProps // skipD >= skipN int skipN = 0; int skipD = 1; + int intervalLength = 0; + int framesToSkip = 0; // have one more enum and then in module.cpp dont call run if the enum is pull // type. FrameFetchStrategy frameFetchStrategy; @@ -212,7 +216,7 @@ class Module void registerHealthCallback(APHealthCallback callback); void executeErrorCallback(const APErrorObject &error); void registerErrorCallback(APErrorCallback callback); - + bool tryThrottlingFPS(int targetFrameRate, int fpsCheckInterval = 100, int retryUntilThrottle = 2); ModuleProps getProps(); protected: @@ -226,7 +230,7 @@ class Module bool preProcessNonSource(frame_container &frames); bool preProcessControl(frame_container &frames); bool isRunning() { return mRunning; } - + bool setTargetModuleFPS(); void setProps(ModuleProps &props); void fillProps(ModuleProps &props); template diff --git a/base/src/Module.cpp b/base/src/Module.cpp index 0e6a72f06..391a538ed 100644 --- a/base/src/Module.cpp +++ b/base/src/Module.cpp @@ -15,6 +15,7 @@ #include "BufferMaker.h" #include "PaceMaker.h" #include "PausePlayMetadata.h" +#include // makes frames from this module's frame factory Module::FFBufferMaker::FFBufferMaker(Module &module) : myModule(module) {} @@ -122,13 +123,12 @@ class Module::Profiler { healthCallback = _healthCallback; } - + int printFrequency; private: string moduleId; sys_clock::time_point processingStart; sys_clock::time_point pipelineStart; sys_clock::time_point lastHealthCallbackTime; - int printFrequency; uint64_t counter = 0; double totalProcessingDuration = 0; double totalPipelineDuration = 0; @@ -1028,6 +1028,95 @@ void Module::setMetadata(std::string &pinId, framemetadata_sp &metadata) return; } +/** + * Set target FPS for the transform module. + * + * This function configures the module to throttle its FPS (frames per second) to the specified + * `targetFrameRate`. It also configures how often to check the FPS (`fpsCheckInterval`) and + * how many iterations to perform to accurately calculate current module fps until throttling is applied. + * + * @param targetFrameRate The desired frames per second for the module. + * @param fpsCheckInterval The interval (in frames) to process to calculate the current module FPS. + * @param fpsCheckIterations Number of iterations to calculate current fps before throttling. + * @return `true` if the configuration is successfully applied. + * + * Comments : Change of source module connected to the transform module by means of relays can change + * incoming framerate while skipN and skipD carry on values according to the previous source fps. + */ + +bool Module::tryThrottlingFPS(int targetFrameRate,int fpsCheckInterval, int fpsCheckIterations) +{ + if (myNature == SOURCE) + { + LOG_ERROR << "Throttling FPS for source is not supported"; + return false; + } + + mProfiler->printFrequency = fpsCheckInterval; + mProps->enableFpsThrottle = true; + mProps->targetFrameRate = targetFrameRate; + mProps->fpsCheckIterations = fpsCheckIterations; + return true; +} + +/** + * In setTargetModuleFPS + * - `skipN` is set to the difference between the current FPS and the target FPS, defining how many frames to skip. + * - `skipD` is set to the current module FPS to manage frame throttling. + * - `intervalLength` is set to the simplified value of skipD + * - `framesToSkip` is set to the simplified value of skipN + */ + +bool Module::setTargetModuleFPS() +{ + float skipRatio = 0; + int gcdValue = 0; + int targetIntervalRange = 10; + int moduleFPS = getPipelineFps(); + moduleFPS = (moduleFPS % 2 == 0 || moduleFPS % 5 == 0) ? moduleFPS : moduleFPS + 1; //Adjust moduleFPS to avoid burst flow of frames + if (mProps->targetFrameRate == moduleFPS) + { + LOG_INFO<<"The module is running at "<targetFrameRate > moduleFPS) + { + LOG_INFO << "The module is running at " << moduleFPS<<" cannot throttle to higher fps"; + return true; + } + if(mProps->targetFrameRate) + { + mProps->skipN = moduleFPS - mProps->targetFrameRate; + mProps->skipD = moduleFPS; + + //Simplifying skipN and skipD + while ((gcdValue = boost::math::gcd(mProps->skipN, mProps->skipD)) > 1) + { + mProps->skipN /= gcdValue; + mProps->skipD /= gcdValue; + } + + if (mProps->skipD <= targetIntervalRange) //If the fraction can be simplified within interval range + { + + mProps->intervalLength = mProps->skipD; + mProps->framesToSkip = mProps->skipN; + } + else + { + //If the simplified ratio does not fall within the specified interval range(e.g., when either number is prime), the skipRatio in decimal form + //is multiplied by the intervalLength to estimate the framesToSkip. This ensures the resulting FPS is close to the expected target FPS. + skipRatio = static_cast(mProps->skipN) / mProps->skipD; + mProps->intervalLength = targetIntervalRange; + mProps->framesToSkip = std::round(mProps->intervalLength * skipRatio); + } + + return true; + } + LOG_ERROR<<"Cannot set module FPS to zero"; + return false; +} + frame_sp Module::getEOSFrame() { return mpCommandFactory->getEOSFrame(); } frame_sp Module::getEmptyFrame() { return mpCommandFactory->getEmptyFrame(); } @@ -1312,26 +1401,31 @@ bool Module::step() else { mProfiler->startPipelineLap(); - - // LOG_ERROR << "Module Id is " << Module::getId() << "Module FPS is " << Module::getPipelineFps() << mProps->fps; auto frames = mQue->pop(); preProcessNonSource(frames); - - if (frames.size() == 0 || shouldSkip()) + // shouldSkip is moved inside stepNonSource to factor in the fps + if (frames.size() == 0) { - // it can come here only if frames.erase from processEOS or processSOS or processEoP or isPropsChange() or isCommand() - return true; + // it can come here only if frames.erase from processEOS or processSOS or processEoP or isPropsChange() or isCommand() + return true; } - if (mPlay) { mProfiler->startProcessingLap(); ret = stepNonSource(frames); mProfiler->endLap(mQue->size()); - } - else - { - ret = true; + if (mProps->enableFpsThrottle && (getPipelineFps() != 0)) + { + if (!mProps->fpsCheckIterations) + { + setTargetModuleFPS(); + mProps->enableFpsThrottle = false; + } + else + { + mProps->fpsCheckIterations--; + } + } } } @@ -1544,7 +1638,12 @@ bool Module::preProcessNonSource(frame_container &frames) bool Module::stepNonSource(frame_container &frames) { - bool ret = true; + bool ret = true; + // skip frames to be factored into the fps calculation + if (shouldSkip()) + { + return ret; + } try { ret = process(frames); @@ -1757,29 +1856,17 @@ bool Module::shouldForceStep() bool Module::shouldSkip() { - if (mProps->skipN == 0) - { - return false; - } - - if (mProps->skipN == mProps->skipD) - { - return true; - } - - auto skip = true; - - if (mSkipIndex <= 0 || mSkipIndex > mProps->skipD) - { - mSkipIndex = mProps->skipD; - } + if (mProps->skipN == 0) + { + return false; + } - if (mSkipIndex > mProps->skipN) - { - skip = false; - } + if (mProps->skipN == mProps->skipD) + { + return true; + } - mSkipIndex--; + mSkipIndex = (mSkipIndex + 1) % mProps->intervalLength; - return skip; + return mSkipIndex < mProps->framesToSkip; } \ No newline at end of file diff --git a/base/test/modulefpsthrottle_tests.cpp b/base/test/modulefpsthrottle_tests.cpp new file mode 100644 index 000000000..663b7259b --- /dev/null +++ b/base/test/modulefpsthrottle_tests.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include "Module.h" +#include "FileReaderModule.h" +#include "ValveModule.h" +#include "StatSink.h" +#include "PipeLine.h" +#include "EncodedImageMetadata.h" + +BOOST_AUTO_TEST_SUITE(modulefpsthrottle_tests) + +BOOST_AUTO_TEST_CASE(throttle_transform_fps) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + auto readerProps = FileReaderModuleProps("./data/mono_1920x960.jpg"); + auto fileReader = boost::shared_ptr(new FileReaderModule(readerProps)); + auto metadata = framemetadata_sp(new FrameMetadata(FrameMetadata::ENCODED_IMAGE)); + readerProps.readLoop = true; + readerProps.logHealth = true; + readerProps.logHealthFrequency = 50; + fileReader->addOutputPin(metadata); + + auto valveProps = ValveModuleProps(-1); + auto valve = boost::shared_ptr(new ValveModule(valveProps)); + valveProps.logHealthFrequency = 100; + fileReader->setNext(valve); + auto valveMetadata = framemetadata_sp(new RawImageMetadata()); + auto rawImagePin = valve->addOutputPin(valveMetadata); + + StatSinkProps sinkProps; + sinkProps.logHealthFrequency = 100; + auto m3 = boost::shared_ptr(new StatSink(sinkProps)); + valve->setNext(m3); + + //Pipeline + PipeLine p("throttle_fps_test"); + p.appendModule(fileReader); + p.init(); + p.run_all_threaded(); + Logger::setLogLevel(boost::log::trivial::severity_level::info); + boost::this_thread::sleep_for(boost::chrono::seconds(20)); + + auto sinkFPS = m3->getPipelineFps(); + LOG_INFO << " Transform fps initial value is : "<< sinkFPS; + //Throttling Transform fps to 25 and verifying fps change in sinkFPS + LOG_INFO << "Throttling Transform fps to 25."; + valve->tryThrottlingFPS(25); + boost::this_thread::sleep_for(boost::chrono::seconds(30)); + sinkFPS = m3->getPipelineFps(); + bool testFPS = sinkFPS > 23 && sinkFPS < 27; + BOOST_TEST(testFPS); + LOG_INFO << "Transform FPS is " << sinkFPS << "."; + + //Throttling Transform fps to 10 + LOG_INFO << "Throttling Transform fps to 10."; + valve->tryThrottlingFPS(10); + boost::this_thread::sleep_for(boost::chrono::seconds(30)); + sinkFPS = m3->getPipelineFps(); + testFPS = sinkFPS > 8 && sinkFPS < 12; + BOOST_TEST(testFPS); + LOG_INFO << "Transform FPS is " << sinkFPS << "."; + + //Throttling Transform fps to 40 + LOG_INFO << "Throttling Transform fps to 40."; + valve->tryThrottlingFPS(40); + boost::this_thread::sleep_for(boost::chrono::seconds(30)); + sinkFPS = m3->getPipelineFps(); + testFPS = sinkFPS > 38 && sinkFPS < 42; + BOOST_TEST(testFPS); + LOG_INFO << "Transform FPS is " << sinkFPS << "."; + p.stop(); + p.term(); + p.wait_for_all(); +} + +BOOST_AUTO_TEST_CASE(throttle_fps_branch) +{ + LoggerProps loggerProps; + loggerProps.logLevel = boost::log::trivial::severity_level::info; + Logger::setLogLevel(boost::log::trivial::severity_level::info); + Logger::initLogger(loggerProps); + + auto readerProps = FileReaderModuleProps("./data/mono_1920x960.jpg"); + auto fileReader = boost::shared_ptr(new FileReaderModule(readerProps)); + auto metadata = framemetadata_sp(new FrameMetadata(FrameMetadata::ENCODED_IMAGE)); + readerProps.readLoop = true; + readerProps.logHealth = true; + readerProps.logHealthFrequency = 50; + fileReader->addOutputPin(metadata); + + auto valve = boost::shared_ptr(new ValveModule(ValveModuleProps(-1))); + fileReader->setNext(valve); + StatSinkProps sink1Props; + sink1Props.logHealthFrequency = 250; + auto sink1 = boost::shared_ptr(new StatSink(sink1Props)); + fileReader->setNext(sink1); + StatSinkProps sink2Props; + sink2Props.logHealthFrequency = 250; + auto sink2 = boost::shared_ptr(new StatSink(sink2Props)); + valve->setNext(sink2); + StatSinkProps sink3Props; + sink3Props.logHealthFrequency = 250; + auto sink3 = boost::shared_ptr(new StatSink(sink3Props)); + valve->setNext(sink3); + + PipeLine p("throttle_fps_test"); + p.appendModule(fileReader); + p.init(); + p.run_all_threaded(); + Logger::setLogLevel(boost::log::trivial::severity_level::info); + boost::this_thread::sleep_for(boost::chrono::seconds(20)); + + //Checking if Transform FPS is 50 for both sinks + auto sink2FPS = sink2->getPipelineFps(); + LOG_INFO << "Initial Transform FPS according to Sink2 is " << sink2FPS << "."; + auto sink3FPS = sink3->getPipelineFps(); + LOG_INFO << "Initial Transform FPS according to Sink3 is " << sink3FPS << "."; + + //Throttling Transform fps to 40 and verifying fps change in sinkFPS + LOG_INFO << "Throttling Transform fps to 40."; + valve->tryThrottlingFPS(40); + boost::this_thread::sleep_for(boost::chrono::seconds(60)); + sink2FPS = sink2->getPipelineFps(); + auto testFPS = sink2FPS > 39 && sink2FPS < 41; + BOOST_TEST(testFPS); + LOG_INFO << "Transform FPS according to Sink2 is " << sink2FPS << "."; + sink3FPS = sink3->getPipelineFps(); + testFPS = sink3FPS > 39 && sink3FPS < 41; + BOOST_TEST(testFPS); + LOG_INFO << "Transform FPS according to Sink3 is " << sink3FPS << "."; +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file