Scheduler-test: simplify binary search implementation

While the idea with capturing observation values is nice,
it definitively does not belong into a library impl of the
search algorithm, because this is usage specific and grossly
complicates the invocation.

Rather, observation data can be captured by side-effect
from the probe-λ holding the actual measurement run.
This commit is contained in:
Fischlurch 2024-01-04 01:32:11 +01:00
parent bdc1b089d7
commit e52aed0b3c
3 changed files with 450 additions and 62 deletions

387
src/lib/binary-search.hpp Normal file
View file

@ -0,0 +1,387 @@
/*
BINARY-SEARCH.hpp - generic search over continuous domain with a probe predicate
Copyright (C) Lumiera.org
2024, Hermann Vosseler <Ichthyostega@web.de>
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file binary-search.hpp
** Textbook implementation of the classical binary search over continuous domain.
**
** @see TestChainLoad_test
** @see SchedulerStress_test
*/
#ifndef LIB_BINARY_SEARCH_H
#define LIB_BINARY_SEARCH_H
#include "vault/common.hpp"
//#include "test-chain-load.hpp"
//#include "lib/test/transiently.hpp"
#include "vault/gear/scheduler.hpp"
#include "lib/time/timevalue.hpp"
//#include "lib/iter-explorer.hpp"
#include "lib/meta/function.hpp"
#include "lib/format-string.hpp"
#include "lib/format-cout.hpp"//////////////////////////TODO RLY?
//#include "lib/util.hpp"
//#include <functional>
#include <utility>
//#include <memory>
//#include <string>
#include <vector>
#include <tuple>
#include <array>
namespace lib {
using util::_Fmt;
using util::min;
using util::max;
// using util::isnil;
// using util::limited;
// using util::unConst;
// using util::toString;
// using util::isLimited;
// using lib::time::Time;
// using lib::time::TimeValue;
// using lib::time::FrameRate;
// using lib::time::Duration;
// using lib::test::Transiently;
// using lib::meta::_FunRet;
// using std::string;
// using std::function;
using std::make_pair;
using std::make_tuple;
// using std::forward;
// using std::string;
// using std::swap;
using std::vector;
using std::move;
namespace err = lumiera::error; //////////////////////////TODO RLY?
namespace { // Default definitions ....
}
namespace stress_test_rig {
template<class FUN, typename PAR>
inline auto
binarySearch_inner (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
ASSERT_VALID_SIGNATURE (FUN, bool(PAR) );
REQUIRE (lower <= upper);
while ((upper-lower) >= epsilon)
{
PAR div = (lower+upper) / 2;
bool hit = fun(div);
if (hit)
upper = div;
else
lower = div;
}
return (lower+upper)/2;
}
template<class FUN, typename PAR>
inline auto
binarySearch_upper (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
REQUIRE (lower <= upper);
bool hit = fun(upper);
if (not hit)
{// the upper end breaks contract => search above
PAR len = (upper-lower);
lower = upper - len/10;
upper = lower + 14*len/10;
}
return binarySearch_inner (forward<FUN> (fun), lower,upper,epsilon);
}
template<class FUN, typename PAR>
inline auto
binarySearch (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
REQUIRE (lower <= upper);
bool hit = fun(lower);
if (hit)
{// the lower end breaks contract => search below
PAR len = (upper-lower);
upper = lower + len/10;
lower = upper - 14*len/10;
}
return binarySearch_upper (forward<FUN> (fun), lower,upper,epsilon);
}
/**
* Specific stress test scheme to determine the
* »breaking point« where the Scheduler starts to slip
*/
template<class CONF>
class BreakingPointBench
: CONF
{
using TestLoad = decltype(std::declval<CONF>().testLoad());
using TestSetup = decltype(std::declval<CONF>().testSetup (std::declval<TestLoad&>()));
struct Res
{
double stressFac{0};
double percentOff{0};
double stdDev{0};
double avgDelta{0};
double avgTime{0};
double expTime{0};
};
/** prepare the ScheduleCtx for a specifically parametrised test series */
void
configureTest (TestSetup& testSetup, double stressFac)
{
testSetup.withLoadTimeBase (CONF::LOAD_BASE)
.withAdaptedSchedule(stressFac, CONF::CONCURRENCY);
}
/** perform a repetition of test runs and compute statistics */
Res
runProbes (TestSetup& testSetup, double stressFac)
{
auto sqr = [](auto n){ return n*n; };
Res res;
auto& [sf,pf,sdev,avgD,avgT,expT] = res;
sf = stressFac;
expT = testSetup.getExpectedEndTime() / 1000;
std::array<double, CONF::REPETITIONS> runTime;
for (uint i=0; i<CONF::REPETITIONS; ++i)
{
runTime[i] = testSetup.launch_and_wait() / 1000;
avgT += runTime[i];
}
avgT /= CONF::REPETITIONS;
avgD = fabs (avgT-expT);
for (uint i=0; i<CONF::REPETITIONS; ++i)
{
sdev += sqr (runTime[i] - avgT);
double delta = fabs (runTime[i] - expT);
bool fail = (delta > CONF::FAIL_LIMIT);
if (fail)
++ pf;
showRun(i, delta, runTime[i], runTime[i] > avgT, fail);
}
pf /= CONF::REPETITIONS;
sdev = sqrt (sdev/CONF::REPETITIONS);
showStep(res);
return res;
}
/** criterion to decide if this test series constitutes a slipped schedule */
bool
decideBreakPoint (Res& res)
{
return res.percentOff > CONF::TRIGGER_FAIL
and res.stdDev > CONF::TRIGGER_SDEV
and res.avgDelta > CONF::TRIGGER_DELTA;
}
/**
* invoke a binary search to produce a sequence of test series
* with the goal to narrow down the stressFact where the Schedule slips away.
*/
template<class FUN>
Res
conductBinarySearch (FUN&& runTestCase, vector<Res> const& results)
{
double breakPoint = binarySearch_upper (forward<FUN> (runTestCase), 0.0, CONF::UPPER_STRESS, CONF::EPSILON);
uint s = results.size();
ENSURE (s >= 2);
Res res;
auto& [sf,pf,sdev,avgD,avgT,expT] = res;
// average data over the last three steps investigated for smoothing
uint points = min (results.size(), 3u);
for (uint i=results.size()-points; i<results.size(); ++i)
{
Res const& resx = results[i];
pf += resx.percentOff;
sdev += resx.stdDev;
avgD += resx.avgDelta;
avgT += resx.avgTime;
expT += resx.expTime;
}
pf /= points;
sdev /= points;
avgD /= points;
avgT /= points;
expT /= points;
sf = breakPoint;
return res;
}
_Fmt fmtRun_ {"....·%-2d: Δ=%4.1f t=%4.1f %s %s"}; // i % Δ % t % t>avg? % fail?
_Fmt fmtStep_{ "%4.2f| : ∅Δ=%4.1f±%-4.2f ∅t=%4.1f %s %%%3.1f -- expect:%4.1fms"}; // stress % ∅Δ % σ % ∅t % fail % pecentOff % t-expect
_Fmt fmtResSDv_{"%9s= %5.2f ±%4.2f%s"};
_Fmt fmtResVal_{"%9s: %5.2f%s"};
void
showRun(uint i, double delta, double t, bool over, bool fail)
{
if (CONF::showRuns)
cout << fmtRun_ % i % delta % t % (over? "+":"-") % (fail? "":"")
<< endl;
}
void
showStep(Res& res)
{
if (CONF::showStep)
cout << fmtStep_ % res.stressFac % res.avgDelta % res.stdDev % res.avgTime
% (decideBreakPoint(res)? "—◆—":"—◇—")
% res.percentOff % res.expTime
<< endl;
}
void
showRes(Res& res)
{
if (CONF::showRes)
{
cout << fmtResVal_ % "stresFac" % res.stressFac % "" <<endl;
cout << fmtResVal_ % "fail" %(res.percentOff * 100) % '%' <<endl;
cout << fmtResSDv_ % "delta" % res.avgDelta % res.stdDev % "ms"<<endl;
cout << fmtResVal_ % "runTime" % res.avgTime % "ms"<<endl;
cout << fmtResVal_ % "expected" % res.expTime % "ms"<<endl;
}
}
void
showRef(TestLoad& testLoad)
{
if (CONF::showRef)
cout << fmtResVal_ % "refTime"
% (testLoad.calcRuntimeReference(CONF::LOAD_BASE) /1000)
% "ms" << endl;
}
public:
/**
* Launch a measurement sequence to determine the »breaking point«
* for the configured test load and parametrisation of the Scheduler.
* @return a tuple `[stress-factor, delta, run-time]`
*/
auto
searchBreakingPoint()
{
TRANSIENTLY(work::Config::COMPUTATION_CAPACITY) = CONF::CONCURRENCY;
TestLoad testLoad = CONF::testLoad().buildTopology();
TestSetup testSetup = CONF::testSetup (testLoad);
vector<Res> observations;
auto performEvaluation = [&](double stressFac)
{
configureTest (testSetup, stressFac);
auto res = runProbes (testSetup, stressFac);
observations.push_back (res);
return decideBreakPoint(res);
};
Res res = conductBinarySearch (move(performEvaluation), observations);
showRes (res);
showRef (testLoad);
return make_tuple (res.stressFac, res.avgDelta, res.avgTime);
}
};
}//namespace stress_test_rig
/** configurable template for running Scheduler Stress tests */
class StressRig
: util::NonCopyable
{
public:
using usec = std::chrono::microseconds;
usec LOAD_BASE = 500us;
uint CONCURRENCY = work::Config::getDefaultComputationCapacity();
double EPSILON = 0.01; ///< error bound to abort binary search
double UPPER_STRESS = 0.6; ///< starting point for the upper limit, likely to fail
double FAIL_LIMIT = 2.0; ///< delta-limit when to count a run as failure
double TRIGGER_FAIL = 0.55; ///< %-fact: criterion-1 failures above this rate
double TRIGGER_SDEV = FAIL_LIMIT; ///< in ms : criterion-2 standard derivation
double TRIGGER_DELTA = 2*FAIL_LIMIT; ///< in ms : criterion-3 delta above this limit
bool showRuns = false; ///< print a line for each individual run
bool showStep = true; ///< print a line for each binary search step
bool showRes = true; ///< print result data
bool showRef = true; ///< calculate single threaded reference time
static uint constexpr REPETITIONS{20};
BlockFlowAlloc bFlow{};
EngineObserver watch{};
Scheduler scheduler{bFlow, watch};
/** Extension point: build the computation topology for this test */
auto
testLoad()
{
return TestChainLoad<>{64};
}
/** (optional) extension point: base configuration of the test ScheduleCtx */
template<class TL>
auto
testSetup (TL& testLoad)
{
return testLoad.setupSchedule(scheduler)
.withJobDeadline(100ms)
.withUpfrontPlanning();
}
/**
* Entrance Point: build a stress test measurement setup
* to determine the »breaking point« where the Scheduler is unable
* to keep up with the defined schedule.
* @tparam CONF specialised subclass of StressRig with customisation
* @return a builder to configure and then launch the actual test
*/
template<class CONF>
static auto
with()
{
return stress_test_rig::BreakingPointBench<CONF>{};
}
};
} // namespace lib
#endif /*LIB_BINARY_SEARCH_H*/

View file

@ -90,7 +90,7 @@
#include <utility>
//#include <memory>
//#include <string>
//#include <vector>
#include <vector>
#include <tuple>
#include <array>
@ -121,6 +121,7 @@ namespace test {
// using std::forward;
// using std::string;
// using std::swap;
using std::vector;
using std::move;
namespace err = lumiera::error; //////////////////////////TODO RLY?
@ -131,69 +132,55 @@ namespace test {
namespace stress_test_rig {
template<class X, class P>
struct _ValidateBinarySearchFun
{
static_assert (not sizeof(P), "Functor unsuitable for binary search. "
"Expected signature pair<bool,X>(PAR)" );
};
template<class RES, class PAR>
struct _ValidateBinarySearchFun<std::pair<bool,RES>(PAR), PAR>
{
using Result = RES;
};
template<class FUN, typename PAR>
inline auto
make_binarySearchResults()
{
using Sig = typename lib::meta::_Fun<FUN>::Sig;
using Res = typename _ValidateBinarySearchFun<Sig,PAR>::Result;
using Data = std::vector<std::pair<bool, Res>>;
return Data{};
}
template<class FUN, class CON, typename PAR>
inline auto
binarySearch_impl (FUN&& fun, CON results, PAR lower, PAR upper, PAR epsilon)
{
REQUIRE (lower <= upper);
while ((upper-lower) >= epsilon)
{
PAR div = lower + (upper-lower) / 2;
results.emplace_back (fun(div));
bool hit = results.back().first;
if (hit)
upper = div;
else
lower = div;
cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<" hit="<<hit<<endl;
}
return results;
}
template<class FUN, typename PAR>
inline auto
binarySearch_inner (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
auto results = make_binarySearchResults<FUN,PAR>();
return binarySearch_impl(forward<FUN> (fun), results, lower,upper,epsilon);
ASSERT_VALID_SIGNATURE (FUN, bool(PAR) );
REQUIRE (lower <= upper);
while ((upper-lower) >= epsilon)
{
PAR div = (lower+upper) / 2;
bool hit = fun(div);
if (hit)
upper = div;
else
lower = div;
}
return (lower+upper)/2;
}
template<class FUN, typename PAR>
inline auto
binarySearch_upper (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
REQUIRE (lower <= upper);
auto results = make_binarySearchResults<FUN,PAR>();
results.emplace_back (fun(upper));
bool hit = results.back().first;
bool hit = fun(upper);
if (not hit)
{// the upper end breaks contract => search above
PAR len = (upper-lower);
lower = upper - len/10;
upper = lower + 14*len/10;
}
return binarySearch_impl(forward<FUN> (fun), results, lower,upper,epsilon);
return binarySearch_inner (forward<FUN> (fun), lower,upper,epsilon);
}
template<class FUN, typename PAR>
inline auto
binarySearch (FUN&& fun, PAR lower, PAR upper, PAR epsilon)
{
REQUIRE (lower <= upper);
bool hit = fun(lower);
if (hit)
{// the lower end breaks contract => search below
PAR len = (upper-lower);
upper = lower + len/10;
lower = upper - 14*len/10;
}
return binarySearch_upper (forward<FUN> (fun), lower,upper,epsilon);
}
@ -273,9 +260,9 @@ cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<"
*/
template<class FUN>
Res
conductBinarySearch (FUN&& runTestCase)
conductBinarySearch (FUN&& runTestCase, vector<Res> const& results)
{
auto results = binarySearch_upper (forward<FUN> (runTestCase), 0.0, CONF::UPPER_STRESS, CONF::EPSILON);
double breakPoint = binarySearch_upper (forward<FUN> (runTestCase), 0.0, CONF::UPPER_STRESS, CONF::EPSILON);
uint s = results.size();
ENSURE (s >= 2);
Res res;
@ -284,7 +271,7 @@ cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<"
uint points = min (results.size(), 3u);
for (uint i=results.size()-points; i<results.size(); ++i)
{
Res resx = results[i].second;
Res const& resx = results[i];
pf += resx.percentOff;
sdev += resx.stdDev;
avgD += resx.avgDelta;
@ -296,14 +283,13 @@ cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<"
avgD /= points;
avgT /= points;
expT /= points;
// »breaking point« stress in the middle of the last interval
sf = (results[s-1].second.stressFac + results[s-2].second.stressFac) / 2;
sf = breakPoint;
return res;
}
_Fmt fmtRun_ {"....·%-2d: Δ=%4.1f t=%4.1f %s %s"}; // i % Δ % t % t>avg? % fail?
_Fmt fmtStep_{ "%4.2f| : ∅Δ=%4.1f±%-4.2f ∅t=%4.1f %%%3.1f -- expect:%4.1fms"}; // stress % ∅Δ % σ % ∅t % fail % t-expect
_Fmt fmtRun_ {"....·%-2d: Δ=%4.1f t=%4.1f %s %s"}; // i % Δ % t % t>avg? % fail?
_Fmt fmtStep_{ "%4.2f| : ∅Δ=%4.1f±%-4.2f ∅t=%4.1f %s %%%3.1f -- expect:%4.1fms"}; // stress % ∅Δ % σ % ∅t % fail % pecentOff % t-expect
_Fmt fmtResSDv_{"%9s= %5.2f ±%4.2f%s"};
_Fmt fmtResVal_{"%9s: %5.2f%s"};
@ -319,7 +305,9 @@ cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<"
showStep(Res& res)
{
if (CONF::showStep)
cout << fmtStep_ % res.stressFac % res.avgDelta % res.stdDev % res.avgTime % res.percentOff % res.expTime
cout << fmtStep_ % res.stressFac % res.avgDelta % res.stdDev % res.avgTime
% (decideBreakPoint(res)? "—◆—":"—◇—")
% res.percentOff % res.expTime
<< endl;
}
@ -360,14 +348,16 @@ cout<<"##################LOOP lower="<<lower<<" div="<<div<<" upper="<<upper<<"
TestLoad testLoad = CONF::testLoad().buildTopology();
TestSetup testSetup = CONF::testSetup (testLoad);
vector<Res> observations;
auto performEvaluation = [&](double stressFac)
{
configureTest (testSetup, stressFac);
auto res = runProbes (testSetup, stressFac);
return make_pair (decideBreakPoint(res), res);
observations.push_back (res);
return decideBreakPoint(res);
};
Res res = conductBinarySearch (move (performEvaluation));
Res res = conductBinarySearch (move(performEvaluation), observations);
showRes (res);
showRef (testLoad);
return make_tuple (res.stressFac, res.avgDelta, res.avgTime);

View file

@ -107823,8 +107823,8 @@ Date:&#160;&#160;&#160;Thu Apr 20 18:53:17 2023 +0200<br/>
<node CREATED="1704232960613" ID="ID_689181948" MODIFIED="1704232994486" STYLE="bubble" TEXT="&#xbb;StressTestRig&#xab;">
<font BOLD="true" NAME="SansSerif" SIZE="14"/>
</node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1704228416349" ID="ID_1774406683" MODIFIED="1704237584351" TEXT="Entwurf">
<icon BUILTIN="pencil"/>
<node COLOR="#338800" CREATED="1704228416349" ID="ID_1774406683" MODIFIED="1704330551725" TEXT="Entwurf">
<icon BUILTIN="button_ok"/>
<node CREATED="1704228425177" ID="ID_758715159" MODIFIED="1704228439379" TEXT="nicht zu viel Fachlichkeit explizit in das Test-Setup einbauen"/>
<node CREATED="1704228440158" ID="ID_18825123" MODIFIED="1704228452703" TEXT="ein Baukasten-System anstreben (f&#xfc;r weitere Tests)"/>
<node CREATED="1704237555914" ID="ID_1779084715" MODIFIED="1704237568213" TEXT="funktionale Konfigurations-Hooks"/>
@ -107870,19 +107870,30 @@ Date:&#160;&#160;&#160;Thu Apr 20 18:53:17 2023 +0200<br/>
</node>
</node>
</node>
<node CREATED="1704317215639" ID="ID_250813072" MODIFIED="1704317220333" TEXT="Einstiegs-Varianten">
<node CREATED="1704317372561" ID="ID_902270424" MODIFIED="1704317376906" TEXT="lower pr&#xfc;fen">
<node COLOR="#338800" CREATED="1704317215639" ID="ID_250813072" MODIFIED="1704330484708" TEXT="Einstiegs-Varianten">
<icon BUILTIN="button_ok"/>
<node COLOR="#435e98" CREATED="1704317372561" ID="ID_902270424" MODIFIED="1704330492807" TEXT="lower pr&#xfc;fen">
<icon BUILTIN="idea"/>
<node CREATED="1704317242076" ID="ID_540462019" MODIFIED="1704317358168" TEXT="true &#x27fc; Intervallverschiebung+Vergr&#xf6;&#xdf;erung &#x25bd;"/>
<node CREATED="1704317389152" ID="ID_1595570689" MODIFIED="1704317398327" TEXT="sonst: upper pr&#xfc;fen"/>
</node>
<node CREATED="1704317230133" ID="ID_821454359" MODIFIED="1704317232648" TEXT="upper pr&#xfc;fen">
<node COLOR="#435e98" CREATED="1704317230133" ID="ID_821454359" MODIFIED="1704330492807" TEXT="upper pr&#xfc;fen">
<icon BUILTIN="idea"/>
<node CREATED="1704317242076" ID="ID_1523564757" MODIFIED="1704317363990" TEXT="false &#x27fc; Intervallverschiebung+Vergr&#xf6;&#xdf;erung &#x25b3;"/>
<node CREATED="1704317412253" ID="ID_884069153" MODIFIED="1704317421832" TEXT="sonst Basis-Algo [lower, upper]"/>
</node>
</node>
<node CREATED="1704317427155" ID="ID_366800456" MODIFIED="1704317431566" TEXT="Datensammlung">
<node COLOR="#5b280f" CREATED="1704317427155" ID="ID_366800456" MODIFIED="1704330498684" TEXT="Datensammlung">
<icon BUILTIN="button_cancel"/>
<node CREATED="1704317465350" ID="ID_1437456177" MODIFIED="1704317479448" TEXT="Ergebnistupel aller Auswertungen werden in einem Vector gesammelt"/>
<node CREATED="1704317540235" ID="ID_696343908" MODIFIED="1704317608812" TEXT="man kann dann das letzte Ergebnis nehmen, oder mehrere mitteln"/>
<node CREATED="1704330500251" ID="ID_1349038155" MODIFIED="1704330510852" TEXT="das geh&#xf6;rt nicht in die Lib-Implementierung">
<icon BUILTIN="yes"/>
<node CREATED="1704330512194" ID="ID_151533884" MODIFIED="1704330523380" TEXT="macht die Implementierung irre komplex"/>
<node CREATED="1704330524016" ID="ID_501099585" MODIFIED="1704330539605" TEXT="geht viel einfacher per Seiteneffekt aus dem pr&#xfc;f-Funktor">
<icon BUILTIN="ksmiletris"/>
</node>
</node>
</node>
</node>
</node>