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)
5  2024, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 */
13 
131 #ifndef VAULT_GEAR_TEST_STRESS_TEST_RIG_H
132 #define VAULT_GEAR_TEST_STRESS_TEST_RIG_H
133 
134 
135 #include "test-chain-load.hpp"
136 #include "lib/binary-search.hpp"
137 #include "lib/test/transiently.hpp"
138 
139 #include "vault/gear/scheduler.hpp"
140 #include "lib/time/timevalue.hpp"
141 #include "lib/meta/function.hpp"
142 #include "lib/format-string.hpp"
143 #include "lib/format-cout.hpp"
144 #include "lib/gnuplot-gen.hpp"
145 #include "lib/stat/statistic.hpp"
146 #include "lib/stat/data.hpp"
147 #include "lib/random.hpp"
148 #include "lib/util.hpp"
149 
150 #include <algorithm>
151 #include <utility>
152 #include <vector>
153 #include <tuple>
154 #include <array>
155 
156 
157 namespace vault{
158 namespace gear {
159 namespace test {
160 
161  using std::make_tuple;
162  using std::forward;
163 
164 
173  template<size_t maxFan =DEFAULT_FAN>
176  {
177  public:
179  using TestSetup = typename TestLoad::ScheduleCtx;
180 
181 
182  /***********************************************************************/
189  template<class CONF>
190  static auto
192  {
193  return Launcher<CONF>{};
194  }
195 
196 
197  /* ======= default configuration (inherited) ======= */
198 
199  uint CONCURRENCY = work::Config::getDefaultComputationCapacity();
200  bool INSTRUMENTATION = true;
201  double EPSILON = 0.01;
202  double UPPER_STRESS = 1.7;
203  double FAIL_LIMIT = 2.0;
204  double TRIGGER_FAIL = 0.55;
207  bool showRuns = false;
208  bool showStep = true;
209  bool showRes = true;
210  bool showRef = true;
211 
212  static uint constexpr REPETITIONS{20};
213 
214  BlockFlowAlloc bFlow{};
215  EngineObserver watch{};
216  Scheduler scheduler{bFlow, watch};
217 
218 
219 
220  protected:
222  auto
223  testLoad(size_t nodes =64)
224  {
225  return TestLoad{nodes};
226  }
227 
230  auto
232  {
233  return testLoad.setupSchedule(scheduler)
234  .withLevelDuration(200us)
235  .withJobDeadline(500ms)
236  .withUpfrontPlanning();
237  }
238 
239  template<class CONF>
240  struct Launcher : CONF
241  {
242  template<template<class> class TOOL, typename...ARGS>
243  auto
244  perform (ARGS&& ...args)
245  {
246  return TOOL<CONF>{}.perform (std::forward<ARGS> (args)...);
247  }
248  };
249  };
250 
251 
252 
253 
254  namespace bench {
255 
256  using util::_Fmt;
257  using util::min;
258  using util::max;
259  using std::vector;
260  using std::declval;
261 
262 
263  /**************************************************/
267  template<class CONF>
269  : public CONF
270  {
271  using TestLoad = typename CONF::TestLoad;
272  using TestSetup = typename TestLoad::ScheduleCtx;
273 
274  struct Res
275  {
276  double stressFac{0};
277  double percentOff{0};
278  double stdDev{0};
279  double avgDelta{0};
280  double avgTime{0};
281  double expTime{0};
282  };
283 
285  void
286  configureTest (TestSetup& testSetup, double stressFac)
287  {
288  testSetup.withInstrumentation(CONF::INSTRUMENTATION) // side-effect: clear existing statistics
289  .withAdaptedSchedule(stressFac, CONF::CONCURRENCY, adjustmentFac);
290  }
291 
293  Res
294  runProbes (TestSetup& testSetup, double stressFac)
295  {
296  auto sqr = [](auto n){ return n*n; };
297  Res res;
298  auto& [sf,pf,sdev,avgD,avgT,expT] = res;
299  sf = stressFac;
300  std::array<double, CONF::REPETITIONS> runTime;
301  for (uint i=0; i<CONF::REPETITIONS; ++i)
302  {
303  runTime[i] = testSetup.launch_and_wait() / 1000;
304  avgT += runTime[i];
305  maybeAdaptScaleEmpirically (testSetup, stressFac);
306  }
307  expT = testSetup.getExpectedEndTime() / 1000;
308  avgT /= CONF::REPETITIONS;
309  avgD = (avgT-expT); // can be < 0
310  for (uint i=0; i<CONF::REPETITIONS; ++i)
311  {
312  sdev += sqr (runTime[i] - avgT);
313  double delta = (runTime[i] - expT);
314  bool fail = (delta > CONF::FAIL_LIMIT);
315  if (fail)
316  ++ pf;
317  showRun(i, delta, runTime[i], runTime[i] > avgT, fail);
318  }
319  pf /= CONF::REPETITIONS;
320  sdev = sqrt (sdev/CONF::REPETITIONS);
321  showStep(res);
322  return res;
323  }
324 
326  bool
328  {
329  return res.percentOff > 0.99
330  or( res.percentOff > CONF::TRIGGER_FAIL
331  and res.stdDev > CONF::TRIGGER_SDEV
332  and res.avgDelta > CONF::TRIGGER_DELTA);
333  }
334 
339  template<class FUN>
340  Res
341  conductBinarySearch (FUN&& runTestCase, vector<Res> const& results)
342  {
343  double breakPoint = lib::binarySearch_upper (forward<FUN> (runTestCase)
344  , 0.0, CONF::UPPER_STRESS
345  , CONF::EPSILON);
346  uint s = results.size();
347  ENSURE (s >= 2);
348  Res res;
349  auto& [sf,pf,sdev,avgD,avgT,expT] = res;
350  // average data over the last three steps investigated for smoothing
351  uint points = min (results.size(), 3u);
352  for (uint i=results.size()-points; i<results.size(); ++i)
353  {
354  Res const& resx = results[i];
355  pf += resx.percentOff;
356  sdev += resx.stdDev;
357  avgD += resx.avgDelta;
358  avgT += resx.avgTime;
359  expT += resx.expTime;
360  }
361  pf /= points;
362  sdev /= points;
363  avgD /= points;
364  avgT /= points;
365  expT /= points;
366  sf = breakPoint;
367  return res;
368  }
369 
371  double adjustmentFac{1.0};
372  size_t gaugeProbes = 3 * CONF::REPETITIONS;
373 
384  void
385  maybeAdaptScaleEmpirically (TestSetup& testSetup, double stressFac)
386  {
387  if (not gaugeProbes) return;
388  double gain = util::limited (0, pow(stressFac, 9), 1);
389  if (gain < 0.2) return;
390  //
391  double formFac = testSetup.determineEmpiricFormFactor (CONF::CONCURRENCY);
392  adjustmentFac = gain*formFac + (1-gain)*adjustmentFac;
393  testSetup.withAdaptedSchedule(stressFac, CONF::CONCURRENCY, adjustmentFac);
394  --gaugeProbes;
395  }
396 
397 
398  _Fmt fmtRun_ {"....·%-2d: Δ=%4.1f t=%4.1f %s %s"}; // i % Δ % t % t>avg? % fail?
399  _Fmt fmtStep_{ "%4.2f| : ∅Δ=%4.1f±%-4.2f ∅t=%4.1f %s %%%-3.0f -- expect:%4.1fms"};// stress % ∅Δ % σ % ∅t % fail % pecentOff % t-expect
400  _Fmt fmtResSDv_{"%9s= %5.2f ±%4.2f%s"};
401  _Fmt fmtResVal_{"%9s: %5.2f%s"};
402 
403  void
404  showRun(uint i, double delta, double t, bool over, bool fail)
405  {
406  if (CONF::showRuns)
407  cout << fmtRun_ % i % delta % t % (over? "+":"-") % (fail? "●":"○")
408  << endl;
409  }
410 
411  void
412  showStep(Res& res)
413  {
414  if (CONF::showStep)
415  cout << fmtStep_ % res.stressFac % res.avgDelta % res.stdDev % res.avgTime
416  % (decideBreakPoint(res)? "—◆—":"—◇—")
417  % (100*res.percentOff) % res.expTime
418  << endl;
419  }
420 
421  void
422  showRes(Res& res)
423  {
424  if (CONF::showRes)
425  {
426  cout << fmtResVal_ % "stresFac" % res.stressFac % "" <<endl;
427  cout << fmtResVal_ % "fail" %(res.percentOff * 100) % '%' <<endl;
428  cout << fmtResSDv_ % "delta" % res.avgDelta % res.stdDev % "ms"<<endl;
429  cout << fmtResVal_ % "runTime" % res.avgTime % "ms"<<endl;
430  cout << fmtResVal_ % "expected" % res.expTime % "ms"<<endl;
431  }
432  }
433 
434  void
435  showRef(TestSetup& testSetup)
436  {
437  if (CONF::showRef)
438  cout << fmtResVal_ % "refTime"
439  % (testSetup.calcRuntimeReference() /1000)
440  % "ms" << endl;
441  }
442 
443 
444  public:
450  auto
452  {
453  TRANSIENTLY(work::Config::COMPUTATION_CAPACITY) = CONF::CONCURRENCY;
454 
455  TestLoad testLoad = CONF::testLoad().buildTopology();
456  TestSetup testSetup = CONF::testSetup (testLoad);
457 
458  vector<Res> observations;
459  auto performEvaluation = [&](double stressFac)
460  {
461  configureTest (testSetup, stressFac);
462  auto res = runProbes (testSetup, stressFac);
463  observations.push_back (res);
464  return decideBreakPoint(res);
465  };
466 
467  Res res = conductBinarySearch (move(performEvaluation), observations);
468  showRes (res);
469  showRef (testSetup);
470  return make_tuple (res.stressFac, res.avgDelta, res.avgTime);
471  }
472  };
473 
474 
475 
476 
477 
478  /**************************************************/
483  template<class CONF>
485  : public CONF
486  {
487  using TestLoad = typename CONF::TestLoad;
488  using TestSetup = typename TestLoad::ScheduleCtx;
489 
490  // Type binding for data evaluation
491  using Param = typename CONF::Param;
492  using Table = typename CONF::Table;
493 
494 
495  void
496  runTest (Param param, Table& data)
497  {
498  TestLoad testLoad = CONF::testLoad(param).buildTopology();
499  TestSetup testSetup = CONF::testSetup (testLoad)
500  .withInstrumentation(); // Note: by default Schedule with CONF::LEVEL_STEP
501  double millis = testSetup.launch_and_wait() / 1000;
502  auto stat = testSetup.getInvocationStatistic();
503  CONF::collectResult (data, param, millis, stat);
504  }
505 
506  public:
512  Table
513  perform (Param lower, Param upper)
514  {
515  TRANSIENTLY(work::Config::COMPUTATION_CAPACITY) = CONF::CONCURRENCY;
516 
517  Param dist = upper - lower;
518  uint cnt = CONF::REPETITIONS;
519  vector<Param> points;
520  points.reserve (cnt);
521  Param minP{upper}, maxP{lower};
522  for (uint i=0; i<cnt; ++i)
523  {
524  auto random = lib::defaultGen.uni(); // [0 .. 1.0[
525  Param pos = lower + Param(floor (random*dist + 0.5));
526  points.push_back(pos);
527  minP = min (pos, minP);
528  maxP = max (pos, maxP);
529  }
530  // ensure the bounds participate in test
531  if (maxP < upper) points[cnt-2] = upper;
532  if (minP > lower) points[cnt-1] = lower;
533 
534  Table results;
535  for (Param point : points)
536  runTest (point, results);
537  return results;
538  }
539  };
540 
541 
542 
543  /* ====== Preconfigured ParamRange-Evaluations ====== */
544 
545  using lib::stat::Column;
546  using lib::stat::DataTable;
547  using lib::stat::DataSpan;
548  using lib::stat::CSVData;
550 
555  template<typename F, typename G>
556  inline auto
557  linearRegression (Column<F> const& x, Column<G> const& y)
558  {
559  lib::stat::RegressionData points;
560  size_t cnt = min (x.data.size(), y.data.size());
561  points.reserve (cnt);
562  for (size_t i=0; i < cnt; ++i)
563  points.emplace_back (x.data[i], y.data[i]);
564  return lib::stat::computeLinearRegression (points);
565  }
566 
577  {
578  using Param = size_t;
579 
580  struct DataRow
581  {
582  Column<Param> param {"load size"}; // independent variable / control parameter
583  Column<double> time {"result time"};
584  Column<double> conc {"concurrency"};
585  Column<double> jobtime {"avg jobtime"};
586  Column<double> impeded {"avg impeded"};
587 
588  auto allColumns()
589  { return std::tie(param
590  ,time
591  ,conc
592  ,jobtime
593  ,impeded
594  );
595  }
596  };
597 
598  using Table = DataTable<DataRow>;
599 
600  void
601  collectResult(Table& data, Param param, double millis, bench::IncidenceStat const& stat)
602  {
603  (void)millis;
604  data.newRow();
605  data.param = param;
606  data.time = stat.coveredTime / 1000;
607  data.conc = stat.avgConcurrency;
608  data.jobtime = stat.activeTime / stat.activationCnt;
609  data.impeded = (stat.timeAtConc(1) + stat.timeAtConc(0))/stat.activationCnt;
610  }
611 
612 
613  static double
614  avgConcurrency (Table const& results)
615  {
616  return lib::stat::average (DataSpan<double> (results.conc.data));
617  }
618 
619  static string
620  renderGnuplot (Table const& results)
621  {
622  using namespace lib::gnuplot_gen;
623  string csv = results.renderCSV();
624  Param maxParam = * std::max_element (results.param.data.begin(), results.param.data.end());
625  Param xtics = maxParam > 500? 50
626  : maxParam > 200? 20
627  : maxParam > 100? 10
628  : 5;
629  return scatterRegression(
630  ParamRecord().set (KEY_CSVData, csv)
631  .set (KEY_TermSize, "600,600")
632  .set (KEY_Xtics, int64_t(xtics))
633  .set (KEY_Xlabel, "load size ⟶ number of jobs")
634  .set (KEY_Ylabel, "active time ⟶ ms")
635  .set (KEY_Y2label, "concurrent threads ⟶")
636  .set (KEY_Y3label, "avg job time ⟶ µs")
637  );
638  }
639  };
640  //
641  }// namespace bench
642 }}}// namespace vault::gear::test
643 #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:140
auto testSetup(TestLoad &testLoad)
(optional) extension point: base configuration of the test ScheduleCtx
bool showRef
calculate single threaded reference time
Definition: run.hpp:40
Any copy and copy construction prohibited.
Definition: nocopy.hpp:37
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:91
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:213
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:185
Test helper to perform temporary manipulations within a test scope.
Descriptor and Accessor for a data column within a DataTable table.
Definition: data.hpp:121
Mix-in for setup of a #ParameterRange evaluation to watch the processing of a single load peak...
double activeTime
compounded time of thread activity
Generating (pseudo) random numbers with controlled seed.
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:106
double TRIGGER_FAIL
%-fact: criterion-1 failures above this rate
double uni()
random double drawn from interval [0.0 ... 1.0[
Definition: random.hpp:232
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.
Random defaultGen
a global default RandomSequencer for mundane purposes
Definition: random.cpp:70
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:51
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...