Lumiera  0.pre.03
»edit your freedom«
stress-test-rig.hpp
Go to the documentation of this file.
1 /*
2  STRESS-TEST-RIG.hpp - setup for stress and performance investigation
3 
4  Copyright (C) Lumiera.org
5  2024, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 */
22 
140 #ifndef VAULT_GEAR_TEST_STRESS_TEST_RIG_H
141 #define VAULT_GEAR_TEST_STRESS_TEST_RIG_H
142 
143 
144 #include "test-chain-load.hpp"
145 #include "lib/binary-search.hpp"
146 #include "lib/test/transiently.hpp"
147 
148 #include "vault/gear/scheduler.hpp"
149 #include "lib/time/timevalue.hpp"
150 #include "lib/meta/function.hpp"
151 #include "lib/format-string.hpp"
152 #include "lib/format-cout.hpp"
153 #include "lib/gnuplot-gen.hpp"
154 #include "lib/stat/statistic.hpp"
155 #include "lib/stat/data.hpp"
156 #include "lib/util.hpp"
157 
158 #include <algorithm>
159 #include <utility>
160 #include <vector>
161 #include <tuple>
162 #include <array>
163 
164 
165 namespace vault{
166 namespace gear {
167 namespace test {
168 
169  using std::make_tuple;
170  using std::forward;
171 
172 
181  template<size_t maxFan =DEFAULT_FAN>
184  {
185  public:
187  using TestSetup = typename TestLoad::ScheduleCtx;
188 
189 
190  /***********************************************************************/
197  template<class CONF>
198  static auto
200  {
201  return Launcher<CONF>{};
202  }
203 
204 
205  /* ======= default configuration (inherited) ======= */
206 
207  uint CONCURRENCY = work::Config::getDefaultComputationCapacity();
208  bool INSTRUMENTATION = true;
209  double EPSILON = 0.01;
210  double UPPER_STRESS = 1.7;
211  double FAIL_LIMIT = 2.0;
212  double TRIGGER_FAIL = 0.55;
215  bool showRuns = false;
216  bool showStep = true;
217  bool showRes = true;
218  bool showRef = true;
219 
220  static uint constexpr REPETITIONS{20};
221 
222  BlockFlowAlloc bFlow{};
223  EngineObserver watch{};
224  Scheduler scheduler{bFlow, watch};
225 
226 
227 
228  protected:
230  auto
231  testLoad(size_t nodes =64)
232  {
233  return TestLoad{nodes};
234  }
235 
238  auto
240  {
241  return testLoad.setupSchedule(scheduler)
242  .withLevelDuration(200us)
243  .withJobDeadline(500ms)
244  .withUpfrontPlanning();
245  }
246 
247  template<class CONF>
248  struct Launcher : CONF
249  {
250  template<template<class> class TOOL, typename...ARGS>
251  auto
252  perform (ARGS&& ...args)
253  {
254  return TOOL<CONF>{}.perform (std::forward<ARGS> (args)...);
255  }
256  };
257  };
258 
259 
260 
261 
262  namespace bench {
263 
264  using util::_Fmt;
265  using util::min;
266  using util::max;
267  using std::vector;
268  using std::declval;
269 
270 
271  /**************************************************/
275  template<class CONF>
277  : public CONF
278  {
279  using TestLoad = typename CONF::TestLoad;
280  using TestSetup = typename TestLoad::ScheduleCtx;
281 
282  struct Res
283  {
284  double stressFac{0};
285  double percentOff{0};
286  double stdDev{0};
287  double avgDelta{0};
288  double avgTime{0};
289  double expTime{0};
290  };
291 
293  void
294  configureTest (TestSetup& testSetup, double stressFac)
295  {
296  testSetup.withInstrumentation(CONF::INSTRUMENTATION) // side-effect: clear existing statistics
297  .withAdaptedSchedule(stressFac, CONF::CONCURRENCY, adjustmentFac);
298  }
299 
301  Res
302  runProbes (TestSetup& testSetup, double stressFac)
303  {
304  auto sqr = [](auto n){ return n*n; };
305  Res res;
306  auto& [sf,pf,sdev,avgD,avgT,expT] = res;
307  sf = stressFac;
308  std::array<double, CONF::REPETITIONS> runTime;
309  for (uint i=0; i<CONF::REPETITIONS; ++i)
310  {
311  runTime[i] = testSetup.launch_and_wait() / 1000;
312  avgT += runTime[i];
313  maybeAdaptScaleEmpirically (testSetup, stressFac);
314  }
315  expT = testSetup.getExpectedEndTime() / 1000;
316  avgT /= CONF::REPETITIONS;
317  avgD = (avgT-expT); // can be < 0
318  for (uint i=0; i<CONF::REPETITIONS; ++i)
319  {
320  sdev += sqr (runTime[i] - avgT);
321  double delta = (runTime[i] - expT);
322  bool fail = (delta > CONF::FAIL_LIMIT);
323  if (fail)
324  ++ pf;
325  showRun(i, delta, runTime[i], runTime[i] > avgT, fail);
326  }
327  pf /= CONF::REPETITIONS;
328  sdev = sqrt (sdev/CONF::REPETITIONS);
329  showStep(res);
330  return res;
331  }
332 
334  bool
336  {
337  return res.percentOff > 0.99
338  or( res.percentOff > CONF::TRIGGER_FAIL
339  and res.stdDev > CONF::TRIGGER_SDEV
340  and res.avgDelta > CONF::TRIGGER_DELTA);
341  }
342 
347  template<class FUN>
348  Res
349  conductBinarySearch (FUN&& runTestCase, vector<Res> const& results)
350  {
351  double breakPoint = lib::binarySearch_upper (forward<FUN> (runTestCase)
352  , 0.0, CONF::UPPER_STRESS
353  , CONF::EPSILON);
354  uint s = results.size();
355  ENSURE (s >= 2);
356  Res res;
357  auto& [sf,pf,sdev,avgD,avgT,expT] = res;
358  // average data over the last three steps investigated for smoothing
359  uint points = min (results.size(), 3u);
360  for (uint i=results.size()-points; i<results.size(); ++i)
361  {
362  Res const& resx = results[i];
363  pf += resx.percentOff;
364  sdev += resx.stdDev;
365  avgD += resx.avgDelta;
366  avgT += resx.avgTime;
367  expT += resx.expTime;
368  }
369  pf /= points;
370  sdev /= points;
371  avgD /= points;
372  avgT /= points;
373  expT /= points;
374  sf = breakPoint;
375  return res;
376  }
377 
379  double adjustmentFac{1.0};
380  size_t gaugeProbes = 3 * CONF::REPETITIONS;
381 
392  void
393  maybeAdaptScaleEmpirically (TestSetup& testSetup, double stressFac)
394  {
395  if (not gaugeProbes) return;
396  double gain = util::limited (0, pow(stressFac, 9), 1);
397  if (gain < 0.2) return;
398  //
399  double formFac = testSetup.determineEmpiricFormFactor (CONF::CONCURRENCY);
400  adjustmentFac = gain*formFac + (1-gain)*adjustmentFac;
401  testSetup.withAdaptedSchedule(stressFac, CONF::CONCURRENCY, adjustmentFac);
402  --gaugeProbes;
403  }
404 
405 
406  _Fmt fmtRun_ {"....·%-2d: Δ=%4.1f t=%4.1f %s %s"}; // i % Δ % t % t>avg? % fail?
407  _Fmt fmtStep_{ "%4.2f| : ∅Δ=%4.1f±%-4.2f ∅t=%4.1f %s %%%-3.0f -- expect:%4.1fms"};// stress % ∅Δ % σ % ∅t % fail % pecentOff % t-expect
408  _Fmt fmtResSDv_{"%9s= %5.2f ±%4.2f%s"};
409  _Fmt fmtResVal_{"%9s: %5.2f%s"};
410 
411  void
412  showRun(uint i, double delta, double t, bool over, bool fail)
413  {
414  if (CONF::showRuns)
415  cout << fmtRun_ % i % delta % t % (over? "+":"-") % (fail? "●":"○")
416  << endl;
417  }
418 
419  void
420  showStep(Res& res)
421  {
422  if (CONF::showStep)
423  cout << fmtStep_ % res.stressFac % res.avgDelta % res.stdDev % res.avgTime
424  % (decideBreakPoint(res)? "—◆—":"—◇—")
425  % (100*res.percentOff) % res.expTime
426  << endl;
427  }
428 
429  void
430  showRes(Res& res)
431  {
432  if (CONF::showRes)
433  {
434  cout << fmtResVal_ % "stresFac" % res.stressFac % "" <<endl;
435  cout << fmtResVal_ % "fail" %(res.percentOff * 100) % '%' <<endl;
436  cout << fmtResSDv_ % "delta" % res.avgDelta % res.stdDev % "ms"<<endl;
437  cout << fmtResVal_ % "runTime" % res.avgTime % "ms"<<endl;
438  cout << fmtResVal_ % "expected" % res.expTime % "ms"<<endl;
439  }
440  }
441 
442  void
443  showRef(TestSetup& testSetup)
444  {
445  if (CONF::showRef)
446  cout << fmtResVal_ % "refTime"
447  % (testSetup.calcRuntimeReference() /1000)
448  % "ms" << endl;
449  }
450 
451 
452  public:
458  auto
460  {
461  TRANSIENTLY(work::Config::COMPUTATION_CAPACITY) = CONF::CONCURRENCY;
462 
463  TestLoad testLoad = CONF::testLoad().buildTopology();
464  TestSetup testSetup = CONF::testSetup (testLoad);
465 
466  vector<Res> observations;
467  auto performEvaluation = [&](double stressFac)
468  {
469  configureTest (testSetup, stressFac);
470  auto res = runProbes (testSetup, stressFac);
471  observations.push_back (res);
472  return decideBreakPoint(res);
473  };
474 
475  Res res = conductBinarySearch (move(performEvaluation), observations);
476  showRes (res);
477  showRef (testSetup);
478  return make_tuple (res.stressFac, res.avgDelta, res.avgTime);
479  }
480  };
481 
482 
483 
484 
485 
486  /**************************************************/
491  template<class CONF>
493  : public CONF
494  {
495  using TestLoad = typename CONF::TestLoad;
496  using TestSetup = typename TestLoad::ScheduleCtx;
497 
498  // Type binding for data evaluation
499  using Param = typename CONF::Param;
500  using Table = typename CONF::Table;
501 
502 
503  void
504  runTest (Param param, Table& data)
505  {
506  TestLoad testLoad = CONF::testLoad(param).buildTopology();
507  TestSetup testSetup = CONF::testSetup (testLoad)
508  .withInstrumentation(); // Note: by default Schedule with CONF::LEVEL_STEP
509  double millis = testSetup.launch_and_wait() / 1000;
510  auto stat = testSetup.getInvocationStatistic();
511  CONF::collectResult (data, param, millis, stat);
512  }
513 
514  public:
520  Table
521  perform (Param lower, Param upper)
522  {
523  TRANSIENTLY(work::Config::COMPUTATION_CAPACITY) = CONF::CONCURRENCY;
524 
525  Param dist = upper - lower;
526  uint cnt = CONF::REPETITIONS;
527  vector<Param> points;
528  points.reserve (cnt);
529  Param minP{upper}, maxP{lower};
530  for (uint i=0; i<cnt; ++i)
531  {
532  auto random = double(rand())/RAND_MAX;
533  Param pos = lower + Param(floor (random*dist + 0.5));
534  points.push_back(pos);
535  minP = min (pos, minP);
536  maxP = max (pos, maxP);
537  }
538  // ensure the bounds participate in test
539  if (maxP < upper) points[cnt-2] = upper;
540  if (minP > lower) points[cnt-1] = lower;
541 
542  Table results;
543  for (Param point : points)
544  runTest (point, results);
545  return results;
546  }
547  };
548 
549 
550 
551  /* ====== Preconfigured ParamRange-Evaluations ====== */
552 
553  using lib::stat::Column;
554  using lib::stat::DataTable;
555  using lib::stat::DataSpan;
556  using lib::stat::CSVData;
558 
563  template<typename F, typename G>
564  inline auto
565  linearRegression (Column<F> const& x, Column<G> const& y)
566  {
567  lib::stat::RegressionData points;
568  size_t cnt = min (x.data.size(), y.data.size());
569  points.reserve (cnt);
570  for (size_t i=0; i < cnt; ++i)
571  points.emplace_back (x.data[i], y.data[i]);
572  return lib::stat::computeLinearRegression (points);
573  }
574 
585  {
586  using Param = size_t;
587 
588  struct DataRow
589  {
590  Column<Param> param {"load size"}; // independent variable / control parameter
591  Column<double> time {"result time"};
592  Column<double> conc {"concurrency"};
593  Column<double> jobtime {"avg jobtime"};
594  Column<double> impeded {"avg impeded"};
595 
596  auto allColumns()
597  { return std::tie(param
598  ,time
599  ,conc
600  ,jobtime
601  ,impeded
602  );
603  }
604  };
605 
606  using Table = DataTable<DataRow>;
607 
608  void
609  collectResult(Table& data, Param param, double millis, bench::IncidenceStat const& stat)
610  {
611  (void)millis;
612  data.newRow();
613  data.param = param;
614  data.time = stat.coveredTime / 1000;
615  data.conc = stat.avgConcurrency;
616  data.jobtime = stat.activeTime / stat.activationCnt;
617  data.impeded = (stat.timeAtConc(1) + stat.timeAtConc(0))/stat.activationCnt;
618  }
619 
620 
621  static double
622  avgConcurrency (Table const& results)
623  {
624  return lib::stat::average (DataSpan<double> (results.conc.data));
625  }
626 
627  static string
628  renderGnuplot (Table const& results)
629  {
630  using namespace lib::gnuplot_gen;
631  string csv = results.renderCSV();
632  Param maxParam = * std::max_element (results.param.data.begin(), results.param.data.end());
633  Param xtics = maxParam > 500? 50
634  : maxParam > 200? 20
635  : maxParam > 100? 10
636  : 5;
637  return scatterRegression(
638  ParamRecord().set (KEY_CSVData, csv)
639  .set (KEY_TermSize, "600,600")
640  .set (KEY_Xtics, int64_t(xtics))
641  .set (KEY_Xlabel, "load size ⟶ number of jobs")
642  .set (KEY_Ylabel, "active time ⟶ ms")
643  .set (KEY_Y2label, "concurrent threads ⟶")
644  .set (KEY_Y3label, "avg job time ⟶ µs")
645  );
646  }
647  };
648  //
649  }// namespace bench
650 }}}// namespace vault::gear::test
651 #endif /*VAULT_GEAR_TEST_STRESS_TEST_RIG_H*/
Res runProbes(TestSetup &testSetup, double stressFac)
perform a repetition of test runs and compute statistics
bool showRuns
print a line for each individual run
double TRIGGER_SDEV
in ms : criterion-2 standard derivation
Automatically use custom string conversion in C++ stream output.
double TRIGGER_DELTA
in ms : criterion-3 average delta above this limit
#define TRANSIENTLY(_OO_)
Macro to simplify capturing assignments.
Wrapper to simplify notation in tests.
Definition: csv.hpp:149
auto testSetup(TestLoad &testLoad)
(optional) extension point: base configuration of the test ScheduleCtx
bool showRef
calculate single threaded reference time
Definition: run.hpp:49
Any copy and copy construction prohibited.
Definition: nocopy.hpp:46
Preconfigured setup for data visualisation with Gnuplot.
Front-end for printf-style string template interpolation.
auto linearRegression(Column< F > const &x, Column< G > const &y)
Calculate a linear regression model for two table columns.
auto testLoad(size_t nodes=64)
Extension point: build the computation topology for this test.
Configurable template framework for running Scheduler Stress tests Use to build a custom setup class...
Generate synthetic computation load for Scheduler performance tests.
string scatterRegression(ParamRecord params)
Generate a (X,Y)-scatter plot with regression line.
Res conductBinarySearch(FUN &&runTestCase, vector< Res > const &results)
invoke a binary search to produce a sequence of test series with the goal to narrow down the stressFa...
double FAIL_LIMIT
delta-limit when to count a run as failure
A Generator for synthetic Render Jobs for Scheduler load testing.
A front-end for using printf-style formatting.
bool showStep
print a line for each binary search step
double UPPER_STRESS
starting point for the upper limit, likely to fail
void maybeAdaptScaleEmpirically(TestSetup &testSetup, double stressFac)
Attempt to factor out some observable properties, which are considered circumstantial and not a direc...
Read-only view into a segment within a sequence of data.
Definition: statistic.hpp:100
ScheduleCtx setupSchedule(Scheduler &scheduler)
establish and configure the context used for scheduling computations.
double EPSILON
error bound to abort binary search
double avgConcurrency
amortised concurrency in timespan
»Scheduler-Service« : coordinate render activities.
Definition: scheduler.hpp:222
Service for coordination and dispatch of render activities.
Metaprogramming tools for transforming functor types.
Manage a table with data records, stored persistently as CSV.
Specific test scheme to perform a Scheduler setup over a given control parameter range to determine c...
auto binarySearch_upper(FUN &&fun, PAR lower, PAR upper, PAR epsilon)
entrance point to binary search to ensure the upper point indeed fulfils the test.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
double coveredTime
overall timespan of observation
Table with data values, stored persistently as CSV file.
Definition: data.hpp:194
Test helper to perform temporary manipulations within a test scope.
Descriptor and Accessor for a data column within a DataTable table.
Definition: data.hpp:130
Mix-in for setup of a #ParameterRange evaluation to watch the processing of a single load peak...
double activeTime
compounded time of thread activity
Table perform(Param lower, Param upper)
Launch a measurement sequence running the Scheduler with a varying parameter value to investigate (x...
Textbook implementation of the classical binary search over continuous domain.
Setup and wiring for a test run to schedule a computation structure as defined by this TestChainLoad ...
static size_t COMPUTATION_CAPACITY
Nominal »full size« of a pool of concurrent workers.
Definition: work-force.hpp:115
double TRIGGER_FAIL
%-fact: criterion-1 failures above this rate
static auto with()
Entrance Point: build a stress test measurement setup using a dedicated TOOL class, takes the configuration CONF as template parameter and which is assumed to inherit (indirectly) from StressRig.
void configureTest(TestSetup &testSetup, double stressFac)
prepare the ScheduleCtx for a specifically parametrised test series
a family of time value like entities and their relationships.
Vault-Layer implementation namespace root.
Collector and aggregator for performance data.
Specific stress test scheme to determine the »breaking point« where the Scheduler starts to slip...
static size_t getDefaultComputationCapacity()
default value for full computing capacity is to use all (virtual) cores.
Definition: work-force.cpp:60
bool decideBreakPoint(Res &res)
criterion to decide if this test series constitutes a slipped schedule
auto perform()
Launch a measurement sequence to determine the »breaking point« for the configured test load and para...