Lumiera  0.pre.03
»edit your freedom«
test-chain-load.hpp
Go to the documentation of this file.
1 /*
2  TEST-CHAIN-LOAD.hpp - produce a configurable synthetic computation load
3 
4  Copyright (C)
5  2023, 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 
109 #ifndef VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H
110 #define VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H
111 
112 
113 #include "vault/common.hpp"
114 #include "lib/test/transiently.hpp"
115 
116 #include "vault/gear/job.h"
117 #include "vault/gear/scheduler.hpp"
121 #include "lib/incidence-count.hpp"
122 #include "lib/time/timevalue.hpp"
123 #include "lib/time/quantiser.hpp"
124 #include "lib/iter-explorer.hpp"
125 #include "lib/format-string.hpp"
126 #include "lib/format-cout.hpp"
127 #include "lib/random-draw.hpp"
128 #include "lib/dot-gen.hpp"
129 #include "lib/util.hpp"
130 
131 #include <boost/functional/hash.hpp>
132 #include <functional>
133 #include <utility>
134 #include <future>
135 #include <memory>
136 #include <string>
137 #include <vector>
138 #include <tuple>
139 #include <array>
140 
141 
142 namespace vault{
143 namespace gear {
144 namespace test {
145 
146  using util::_Fmt;
147  using util::min;
148  using util::max;
149  using util::isnil;
150  using util::limited;
151  using util::unConst;
152  using util::toString;
153  using util::isLimited;
154  using util::showHashLSB;
155  using lib::time::Time;
156  using lib::time::TimeValue;
157  using lib::time::FrameRate;
158  using lib::time::Duration;
162  using lib::meta::_FunRet;
163 
164  using std::string;
165  using std::function;
166  using std::make_pair;
167  using std::forward;
168  using std::string;
169  using std::swap;
170  using std::move;
171  using std::chrono_literals::operator ""s;
172 
173  namespace err = lumiera::error;
174  namespace dot = lib::dot_gen;
175 
176  namespace { // Default definitions for structured load testing
177 
178  const size_t DEFAULT_FAN = 16;
179  const size_t DEFAULT_SIZ = 256;
180 
181  const auto SAFETY_TIMEOUT = 5s;
182  const auto STANDARD_DEADLINE = 30ms;
183  const size_t DEFAULT_CHUNKSIZE = 64;
184  const double UPFRONT_PLANNING_BOOST = 2.6;
185  const size_t GRAPH_BENCHMARK_RUNS = 5;
186  const size_t LOAD_BENCHMARK_RUNS = 500;
187  const double LOAD_SPEED_BASELINE = 100;
188  const microseconds LOAD_DEFAULT_TIME = 100us;
189  const size_t LOAD_DEFAULT_MEM_SIZE = 1000;
190  const Duration SCHEDULE_LEVEL_STEP{_uTicks(1ms)};
192  const Duration SCHEDULE_PLAN_STEP{_uTicks(100us)};
193  const Offset SCHEDULE_WAKE_UP{_uTicks(10us)};
194  const bool SCHED_DEPENDS = false;
195  const bool SCHED_NOTIFY = true;
196 
197  inline uint
198  defaultConcurrency()
199  {
201  }
202 
203  inline double
204  _uSec (microseconds ticks)
205  {
206  return std::chrono::duration<double, std::micro>{ticks}.count();
207  }
208  }
209 
210  struct LevelWeight
211  {
212  size_t level{0};
213  size_t nodes{0};
214  size_t endidx{0};
215  size_t weight{0};
216  };
217 
227  inline double
228  computeWeightFactor (LevelWeight const& lw, uint concurrency)
229  {
230  REQUIRE (0 < concurrency);
231  double speedUp = lw.nodes? lw.nodes / std::ceil (double(lw.nodes)/concurrency)
232  : 1.0;
233  ENSURE (1.0 <= speedUp);
234  return lw.weight / speedUp;
235  }
236 
237  struct Statistic;
238 
239 
240 
241 
242  /***********************************************************************/
248  template<size_t maxFan =DEFAULT_FAN>
251  {
252 
253  public:
255  struct Node
257  {
258  using _Arr = std::array<Node*, maxFan>;
259  using Iter = typename _Arr::iterator;
260  using CIter = typename _Arr::const_iterator;
261 
263  struct Tab : _Arr
264  {
265  Iter after = _Arr::begin();
266 
267  Iter end() { return after; }
268  CIter end() const{ return after; }
269  friend Iter end (Tab & tab){ return tab.end(); }
270  friend CIter end (Tab const& tab){ return tab.end(); }
271 
272  Node* front() { return empty()? nullptr : _Arr::front(); }
273  Node* back() { return empty()? nullptr : *(after-1); }
274 
275  void clear() { after = _Arr::begin(); }
276 
277  size_t size() const { return unConst(this)->end()-_Arr::begin(); }
278  bool empty() const { return 0 == size(); }
279 
280  Iter
281  add(Node* n)
282  {
283  if (after != _Arr::end())
284  {
285  *after = n;
286  return after++;
287  }
288  NOTREACHED ("excess node linkage");
289  }
290 
291  };
292 
293  size_t hash;
294  size_t level{0}, weight{0};
295  Tab pred{0}, succ{0};
296 
297  Node(size_t seed =0)
298  : hash{seed}
299  { }
300 
301  void
302  clear()
303  {
304  hash = 0;
305  level = weight = 0;
306  pred.clear();
307  succ.clear();
308  }
309 
310  Node&
311  addPred (Node* other)
312  {
313  REQUIRE (other);
314  pred.add (other);
315  other->succ.add (this);
316  return *this;
317  }
318 
319  Node&
320  addSucc (Node* other)
321  {
322  REQUIRE (other);
323  succ.add (other);
324  other->pred.add (this);
325  return *this;
326  }
327  Node& addPred(Node& other) { return addPred(&other); }
328  Node& addSucc(Node& other) { return addSucc(&other); }
329 
330  size_t
331  calculate()
332  {
333  for (Node* entry: pred)
334  boost::hash_combine (hash, entry->hash);
335  return hash;
336  }
337 
338  friend bool isStart (Node const& n) { return isnil (n.pred); };
339  friend bool isExit (Node const& n) { return isnil (n.succ); };
340  friend bool isInner (Node const& n) { return not (isStart(n) or isExit(n)); }
341  friend bool isFork (Node const& n) { return 1 < n.succ.size(); }
342  friend bool isJoin (Node const& n) { return 1 < n.pred.size(); }
343  friend bool isLink (Node const& n) { return 1 == n.pred.size() and 1 == n.succ.size(); }
344  friend bool isKnot (Node const& n) { return isFork(n) and isJoin(n); }
345 
346 
347  friend bool isStart (Node const* n) { return n and isStart(*n); };
348  friend bool isExit (Node const* n) { return n and isExit (*n); };
349  friend bool isInner (Node const* n) { return n and isInner(*n); };
350  friend bool isFork (Node const* n) { return n and isFork (*n); };
351  friend bool isJoin (Node const* n) { return n and isJoin (*n); };
352  friend bool isLink (Node const* n) { return n and isLink (*n); };
353  friend bool isKnot (Node const* n) { return n and isKnot (*n); };
354  };
355 
356 
358  class NodeControlBinding;
359 
362 
365 
366  private:
367  using NodeTab = typename Node::Tab;
369 
370  std::unique_ptr<Node[]> nodes_;
371  size_t numNodes_;
372 
373  Rule seedingRule_ {};
374  Rule expansionRule_{};
375  Rule reductionRule_{};
376  Rule pruningRule_ {};
377  Rule weightRule_ {};
378 
379  Node* frontNode() { return &nodes_[0]; }
380  Node* afterNode() { return &nodes_[numNodes_]; }
381  Node* backNode() { return &nodes_[numNodes_-1];}
382 
383  public:
384  explicit
385  TestChainLoad(size_t nodeCnt =DEFAULT_SIZ)
386  : nodes_{new Node[nodeCnt]}
387  , numNodes_{nodeCnt}
388  {
389  REQUIRE (1 < nodeCnt);
390  }
391 
392 
393  size_t size() const { return numNodes_; }
394  size_t topLevel() const { return unConst(this)->backNode()->level; }
395  size_t getSeed() const { return unConst(this)->frontNode()->hash; }
396 
397 
398  auto
399  allNodes()
400  {
401  return lib::explore (NodeIT{frontNode(),afterNode()});
402  }
403  auto
404  allNodePtr()
405  {
406  return allNodes().asPtr();
407  }
408 
409  auto
410  allExitNodes()
411  {
412  return allNodes().filter([](Node& n){ return isExit(n); });
413  }
414  auto
415  allExitHashes() const
416  {
417  return unConst(this)->allExitNodes().transform([](Node& n){ return n.hash; });
418  }
419 
421  size_t
422  getHash() const
423  {
424  auto combineBoostHashes = [](size_t h, size_t hx){ boost::hash_combine(h,hx); return h;};
425  return allExitHashes()
426  .filter([](size_t h){ return h != 0; })
427  .reduce(lib::iter_explorer::IDENTITY
428  ,combineBoostHashes
429  );
430  }
431 
432 
434  size_t nodeID(Node const* n){ return n - frontNode(); };
435  size_t nodeID(Node const& n){ return nodeID (&n); };
436 
437 
438 
439  /* ===== topology control ===== */
440 
441  TestChainLoad&&
442  seedingRule (Rule r)
443  {
444  seedingRule_ = move(r);
445  return move(*this);
446  }
447 
448  TestChainLoad&&
449  expansionRule (Rule r)
450  {
451  expansionRule_ = move(r);
452  return move(*this);
453  }
454 
455  TestChainLoad&&
456  reductionRule (Rule r)
457  {
458  reductionRule_ = move(r);
459  return move(*this);
460  }
461 
462  TestChainLoad&&
463  pruningRule (Rule r)
464  {
465  pruningRule_ = move(r);
466  return move(*this);
467  }
468 
469  TestChainLoad&&
470  weightRule (Rule r)
471  {
472  weightRule_ = move(r);
473  return move(*this);
474  }
475 
476 
478  static Rule rule() { return Rule(); }
479  static Rule value(size_t v) { return Rule().fixedVal(v); }
480 
481  static Rule
482  rule_atStart (uint v)
483  {
484  return Rule().mapping([v](Node* n)
485  {
486  return isStart(n)? Rule().fixedVal(v)
487  : Rule();
488  });
489  }
490 
491  static Rule
492  rule_atJoin (uint v)
493  {
494  return Rule().mapping([v](Node* n)
495  {
496  return isJoin(n) ? Rule().fixedVal(v)
497  : Rule();
498  });
499  }
500 
501  static Rule
502  rule_atLink (uint v)
503  {
504  return Rule().mapping([v](Node* n)
505  { // NOTE: when applying these rules,
506  // successors are not yet wired...
507  return not (isJoin(n) or isStart(n))
508  ? Rule().fixedVal(v)
509  : Rule();
510  });
511  }
512 
513  static Rule
514  rule_atJoin_else (double p1, double p2, uint v=1)
515  {
516  return Rule().mapping([p1,p2,v](Node* n)
517  {
518  return isJoin(n) ? Rule().probability(p1).maxVal(v)
519  : Rule().probability(p2).maxVal(v);
520  });
521  }
522 
523 
525  TestChainLoad&&
527  {
528  pruningRule(value(1));
529  weightRule(value(1));
530  return move(*this);
531  }
532 
534  TestChainLoad&&
536  {
537  pruningRule(rule().probability(0.8));
538  weightRule(value(1));
539  return move(*this);
540  }
541 
543  TestChainLoad&&
545  {
546  pruningRule(rule().probability(0.6));
547  seedingRule(rule_atStart(1));
548  weightRule(value(1));
549  return move(*this);
550  }
551 
553  TestChainLoad&&
555  {
556  seedingRule(rule().probability(0.8).maxVal(1));
557  reductionRule(rule().probability(0.75).maxVal(3));
558  pruningRule(rule_atJoin(1));
559  weightRule(value(1));
560  return move(*this);
561  }
562 
565  TestChainLoad&&
567  {
568  expansionRule(rule().probability(0.27).maxVal(4));
569  reductionRule(rule().probability(0.44).maxVal(6).minVal(2));
570  weightRule (rule().probability(0.66).maxVal(3));
571  setSeed(55); // ◁─────── produces a prelude with parallel chains,
572  return move(*this);// then fork at level 17 followed by bursts of load.
573  }
574 
575 
576 
583  TestChainLoad&&
585  {
586  NodeTab a,b, // working data for generation
587  *curr{&a}, // the current set of nodes to carry on
588  *next{&b}; // the next set of nodes connected to current
589  Node* node = frontNode();
590  size_t level{0};
591 
592  // transient snapshot of rules (non-copyable, once engaged)
593  Transiently originalExpansionRule{expansionRule_};
594  Transiently originalReductionRule{reductionRule_};
595  Transiently originalseedingRule {seedingRule_};
596  Transiently originalPruningRule {pruningRule_};
597  Transiently originalWeightRule {weightRule_};
598 
599  // prepare building blocks for the topology generation...
600  auto moreNext = [&]{ return next->size() < maxFan; };
601  auto moreNodes = [&]{ return node <= backNode(); };
602  auto spaceLeft = [&]{ return moreNext() and moreNodes(); };
603  auto addNode = [&](size_t seed =0)
604  {
605  Node* n = *next->add (node++);
606  n->clear();
607  n->level = level;
608  n->hash = seed;
609  return n;
610  };
611  auto apply = [&](Rule& rule, Node* n)
612  {
613  return rule(n);
614  };
615  auto calcNode = [&](Node* n)
616  {
617  n->calculate();
618  n->weight = apply(weightRule_,n);
619  };
620 
621  // visit all further nodes and establish links
622  while (moreNodes())
623  {
624  curr->clear();
625  swap (next, curr);
626  size_t toReduce{0};
627  Node* r = nullptr;
628  REQUIRE (spaceLeft());
629  for (Node* o : *curr)
630  { // follow-up on all Nodes in current level...
631  calcNode(o);
632  if (apply (pruningRule_,o))
633  continue; // discontinue
634  size_t toSeed = apply (seedingRule_, o);
635  size_t toExpand = apply (expansionRule_,o);
636  while (0 < toSeed and spaceLeft())
637  { // start a new chain from seed
638  addNode(this->getSeed());
639  --toSeed;
640  }
641  while (0 < toExpand and spaceLeft())
642  { // fork out secondary chain from o
643  Node* n = addNode();
644  o->addSucc(n);
645  --toExpand;
646  }
647  if (not toReduce)
648  { // carry-on chain from o
649  r = spaceLeft()? addNode():nullptr;
650  toReduce = apply (reductionRule_, o);
651  }
652  else
653  --toReduce;
654  if (r) // connect chain from o...
655  r->addPred(o);
656  else // space for successors is already exhausted
657  { // can not carry-on, but must ensure no chain is broken
658  ENSURE (not next->empty());
659  if (o->succ.empty())
660  o->addSucc (next->back());
661  }
662  }
663  ENSURE (not isnil(next) or spaceLeft());
664  if (isnil(next)) // ensure graph continues
665  addNode(this->getSeed());
666  ENSURE (not next->empty());
667  ++level;
668  }
669  ENSURE (node > backNode());
670  // all open nodes on last level become exit nodes
671  for (Node* o : *next)
672  calcNode(o);
673  //
674  return move(*this);
675  }
676 
677 
682  TestChainLoad&&
683  setSeed (size_t seed = rani())
684  {
685  frontNode()->hash = seed;
686  return move(*this);
687  }
688 
689 
694  TestChainLoad&&
695  setWeight (size_t fixedNodeWeight =1)
696  {
697  for (Node& n : allNodes())
698  n.weight = fixedNodeWeight;
699  return move(*this);
700  }
701 
702 
706  TestChainLoad&&
708  {
709  size_t seed = this->getSeed();
710  for (Node& n : allNodes())
711  {
712  n.hash = isStart(n)? seed : 0;
713  n.calculate();
714  }
715  return move(*this);
716  }
717 
718 
722  TestChainLoad&&
724  {
725  size_t seed = this->getSeed();
726  for (Node& n : allNodes())
727  n.hash = isStart(n)? seed : 0;
728  return move(*this);
729  }
730 
731 
732 
733  /* ===== Operators ===== */
734 
735  std::string
736  generateTopologyDOT()
737  {
738  using namespace dot;
739 
740  Section nodes("Nodes");
741  Section layers("Layers");
742  Section topology("Topology");
743 
744  // Styles to distinguish the computation nodes
745  Code BOTTOM{"shape=doublecircle"};
746  Code SEED {"shape=circle"};
747  Code TOP {"shape=box, style=rounded"};
748  Code DEFAULT{};
749 
750  // prepare time-level zero
751  size_t level(0);
752  auto timeLevel = scope(level).rank("min ");
753 
754  for (Node& n : allNodes())
755  {
756  size_t i = nodeID(n);
757  string tag{toString(i)+": "+showHashLSB(n.hash)};
758  if (n.weight) tag +="."+toString(n.weight);
759  nodes += node(i).label(tag)
760  .style(i==0 ? BOTTOM
761  :isnil(n.pred)? SEED
762  :isnil(n.succ)? TOP
763  : DEFAULT);
764  for (Node* suc : n.succ)
765  topology += connect (i, nodeID(*suc));
766 
767  if (level != n.level)
768  {// switch to next time-level
769  layers += timeLevel;
770  ++level;
771  ENSURE (level == n.level);
772  timeLevel = scope(level).rank("same");
773  }
774  timeLevel.add (node(i));
775  }
776  layers += timeLevel; // close last layer
777 
778  // combine and render collected definitions as DOT-code
779  return digraph (nodes, layers, topology);
780  }
781 
782  TestChainLoad&&
783  printTopologyDOT()
784  {
785  cout << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───\n"
786  << generateTopologyDOT()
787  << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
788  << endl;
789  return move(*this);
790  }
791 
792 
800  double
802  ,size_t sizeBase =0
803  ,size_t repeatCnt=GRAPH_BENCHMARK_RUNS
804  )
805  {
806  return microBenchmark ([&]{ performGraphSynchronously(timeBase,sizeBase); }
807  ,repeatCnt)
808  .first; // ∅ runtime in µs
809  }
810 
812  TestChainLoad&& performGraphSynchronously(microseconds timeBase =LOAD_DEFAULT_TIME
813  ,size_t sizeBase =0);
814 
815  TestChainLoad&&
816  printRuntimeReference(microseconds timeBase =LOAD_DEFAULT_TIME
817  ,size_t sizeBase =0
818  ,size_t repeatCnt=GRAPH_BENCHMARK_RUNS
819  )
820  {
821  cout << _Fmt{"runtime ∅(%d) = %6.2fms (single-threaded)\n"}
822  % repeatCnt
823  % (1e-3 * calcRuntimeReference(timeBase,sizeBase,repeatCnt))
824  << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
825  << endl;
826  return move(*this);
827  }
828 
829 
831  size_t
833  {
834  return allNodes()
835  .transform([](Node& n){ return n.weight; })
836  .resultSum();
837  }
838 
840  auto
842  {
843  return allNodes()
844  .groupedBy([](Node& n){ return n.level; }
845  ,[this](LevelWeight& lw, Node const& n)
846  {
847  lw.level = n.level;
848  lw.weight += n.weight;
849  lw.endidx = nodeID(n);
850  ++lw.nodes;
851  }
852  );
853  }
854 
856  auto
857  levelScheduleSequence (uint concurrency =1)
858  {
859  return allLevelWeights()
860  .transform([schedule=0.0, concurrency]
861  (LevelWeight const& lw) mutable
862  {
863  schedule += computeWeightFactor (lw, concurrency);
864  return schedule;
865  });
866  }
867 
869  double
870  calcExpectedCompoundedWeight (uint concurrency =1)
871  {
872  return allLevelWeights()
873  .transform([concurrency](LevelWeight const& lw){ return computeWeightFactor (lw, concurrency); })
874  .resultSum();
875  }
876 
877 
878 
879  Statistic computeGraphStatistics();
880  TestChainLoad&& printTopologyStatistics();
881 
882  class ScheduleCtx;
883  friend class ScheduleCtx; // accesses raw storage array
884 
885  ScheduleCtx setupSchedule (Scheduler& scheduler);
886  };
887 
888 
889 
898  template<size_t maxFan>
900  : public std::function<Param(Node*)>
901  {
902  protected:
904  static size_t
905  defaultSrc (Node* node)
906  {
907  return node? node->hash:0;
908  }
909 
910  static size_t
911  level (Node* node)
912  {
913  return node? node->level:0;
914  }
915 
916  static double
917  guessHeight (size_t level)
918  { // heuristic guess for a »fully stable state«
919  double expectedHeight = 2*maxFan;
920  return level / expectedHeight;
921  }
922 
923 
924 
926  template<class SIG>
927  struct Adaptor
928  {
929  static_assert (not sizeof(SIG), "Unable to adapt given functor.");
930  };
931 
933  template<typename RES>
934  struct Adaptor<RES(size_t)>
935  {
936  template<typename FUN>
937  static auto
938  build (FUN&& fun)
939  {
940  return [functor=std::forward<FUN>(fun)]
941  (Node* node) -> _FunRet<FUN>
942  {
943  return functor (defaultSrc (node));
944  };
945  }
946  };
947 
951  template<typename RES>
952  struct Adaptor<RES(size_t,double)>
953  {
954 
955  template<typename FUN>
956  static auto
957  build (FUN&& fun)
958  {
959  return [functor=std::forward<FUN>(fun)]
960  (Node* node) -> _FunRet<FUN>
961  {
962  return functor (defaultSrc (node)
963  ,guessHeight(level(node)));
964  };
965  }
966  };
967 
969  template<typename RES>
970  struct Adaptor<RES(double)>
971  {
972 
973  template<typename FUN>
974  static auto
975  build (FUN&& fun)
976  {
977  return [functor=std::forward<FUN>(fun)]
978  (Node* node) -> _FunRet<FUN>
979  {
980  return functor (guessHeight(level(node)));
981  };
982  }
983  };
984  };
985 
986 
987 
988 
989  /* ========= Graph Statistics Evaluation ========= */
990 
991  struct StatKey
992  : std::pair<size_t,string>
993  {
994  using std::pair<size_t,string>::pair;
995  operator size_t const&() const { return this->first; }
996  operator string const&() const { return this->second;}
997  };
998  const StatKey STAT_NODE{0,"node"};
999  const StatKey STAT_SEED{1,"seed"};
1000  const StatKey STAT_EXIT{2,"exit"};
1001  const StatKey STAT_INNR{3,"innr"};
1002  const StatKey STAT_FORK{4,"fork"};
1003  const StatKey STAT_JOIN{5,"join"};
1004  const StatKey STAT_LINK{6,"link"};
1005  const StatKey STAT_KNOT{7,"knot"};
1006  const StatKey STAT_WGHT{8,"wght"};
1007 
1009  const uint CAT = KEYS.size();
1010  const uint IDX_SEED = 1; // index of STAT_SEED
1011 
1012  namespace {
1013  template<class NOD>
1014  inline auto
1015  prepareEvaluations()
1016  {
1017  return std::array<std::function<uint(NOD&)>, CAT>
1018  { [](NOD& ){ return 1; }
1019  , [](NOD& n){ return isStart(n);}
1020  , [](NOD& n){ return isExit(n); }
1021  , [](NOD& n){ return isInner(n);}
1022  , [](NOD& n){ return isFork(n); }
1023  , [](NOD& n){ return isJoin(n); }
1024  , [](NOD& n){ return isLink(n); }
1025  , [](NOD& n){ return isKnot(n); }
1026  , [](NOD& n){ return n.weight; }
1027  };
1028  }
1029  }
1030 
1031  using VecU = std::vector<uint>;
1032  using LevelSums = std::array<uint, CAT>;
1033 
1039  struct Indicator
1040  {
1041  VecU data{};
1042  uint cnt{0};
1043  double frac{0};
1044  double pS {0};
1045  double pL {0};
1046  double pLW{0};
1047  double cL {0};
1048  double cLW{0};
1049  double sL {0};
1050  double sLW{0};
1051 
1052  void
1053  addPoint (uint levelID, uint sublevelID, uint width, uint items)
1054  {
1055  REQUIRE (levelID == data.size()); // ID is zero based
1056  REQUIRE (width > 0);
1057  data.push_back (items);
1058  cnt += items;
1059  pS += items;
1060  pL += items;
1061  pLW += items / double(width);
1062  cL += levelID * items;
1063  cLW += levelID * items/double(width);
1064  sL += sublevelID * items;
1065  sLW += sublevelID * items/double(width);
1066  }
1067 
1068  void
1069  closeAverages (uint nodes, uint levels, uint segments, double avgheight)
1070  {
1071  REQUIRE (levels == data.size());
1072  REQUIRE (levels > 0);
1073  frac = cnt / double(nodes);
1074  cL = pL? cL/pL :0; // weighted averages: normalise to weight sum
1075  cLW = pLW? cLW/pLW :0;
1076  sL = pL? sL/pL :0;
1077  sLW = pLW? sLW/pLW :0;
1078  pS /= segments; // simple averages : normalise to number of levels
1079  pL /= levels; // simple averages : normalise to number of levels
1080  pLW /= levels;
1081  cL /= levels-1; // weight centres : as fraction of maximum level-ID
1082  cLW /= levels-1;
1083  ASSERT (avgheight >= 1.0);
1084  if (avgheight > 1.0)
1085  { // likewise for weight centres relative to subgraph
1086  sL /= avgheight-1; // height is 1-based, while the contribution was 0-based
1087  sLW /= avgheight-1;
1088  }
1089  else
1090  sL = sLW = 0.5;
1091  }
1092  };
1093 
1097  struct Statistic
1098  {
1099  uint nodes{0};
1100  uint levels{0};
1101  uint segments{1};
1102  uint maxheight{0};
1103  double avgheight{0};
1104  VecU width{};
1105  VecU sublevel{};
1106 
1107  std::array<Indicator, CAT> indicators;
1108 
1109  explicit
1110  Statistic (uint lvls)
1111  : nodes{0}
1112  , levels{0}
1113  {
1114  reserve (lvls);
1115  }
1116 
1117  void
1118  addPoint (uint levelWidth, uint sublevelID, LevelSums& particulars)
1119  {
1120  levels += 1;
1121  nodes += levelWidth;
1122  width.push_back (levelWidth);
1123  sublevel.push_back (sublevelID);
1124  ASSERT (levels == width.size());
1125  ASSERT (0 < levels);
1126  ASSERT (0 < levelWidth);
1127  for (uint i=0; i< CAT; ++i)
1128  indicators[i].addPoint (levels-1, sublevelID, levelWidth, particulars[i]);
1129  }
1130 
1131  void
1132  closeAverages (uint segs, uint maxSublevelID)
1133  {
1134  segments = segs;
1135  maxheight = maxSublevelID + 1;
1136  avgheight = levels / double(segments);
1137  for (uint i=0; i< CAT; ++i)
1138  indicators[i].closeAverages (nodes,levels,segments,avgheight);
1139  }
1140 
1141  private:
1142  void
1143  reserve (uint lvls)
1144  {
1145  width.reserve (lvls);
1146  sublevel.reserve(lvls);
1147  for (uint i=0; i< CAT; ++i)
1148  {
1149  indicators[i] = Indicator{};
1150  indicators[i].data.reserve(lvls);
1151  }
1152  }
1153  };
1154 
1155 
1156 
1169  template<size_t maxFan>
1170  inline Statistic
1172  {
1173  auto totalLevels = uint(topLevel());
1174  auto classify = prepareEvaluations<Node>();
1175  Statistic stat(totalLevels);
1176  LevelSums particulars{0};
1177  size_t level{0},
1178  sublevel{0},
1179  maxsublevel{0};
1180  size_t segs{0};
1181  uint width{0};
1182  auto detectSubgraphs = [&]{ // to be called when a level is complete
1183  if (width==1 and particulars[IDX_SEED]==1)
1184  { // previous level actually started new subgraph
1185  sublevel = 0;
1186  ++segs;
1187  }
1188  else
1189  maxsublevel = max (sublevel,maxsublevel);
1190  };
1191 
1192  for (Node& node : allNodes())
1193  {
1194  if (level != node.level)
1195  {// Level completed...
1196  detectSubgraphs();
1197  // record statistics for previous level
1198  stat.addPoint (width, sublevel, particulars);
1199  // switch to next time-level
1200  ++level;
1201  ++sublevel;
1202  ENSURE (level == node.level);
1203  particulars = LevelSums{0};
1204  width = 0;
1205  }
1206  // classify and account..
1207  ++width;
1208  for (uint i=0; i<CAT; ++i)
1209  particulars[i] += classify[i](node);
1210  }
1211  ENSURE (level == topLevel());
1212  detectSubgraphs();
1213  stat.addPoint (width, sublevel, particulars);
1214  stat.closeAverages (segs, maxsublevel);
1215  return stat;
1216  }
1217 
1218 
1219 
1252  template<size_t maxFan>
1253  inline TestChainLoad<maxFan>&&
1255  {
1256  cout << "INDI: cnt frac ∅pS ∅pL ∅pLW γL◆ γLW◆ γL⬙ γLW⬙\n";
1257  _Fmt line{"%4s: %3d %3.0f%% %5.1f %5.2f %4.2f %4.2f %4.2f %4.2f %4.2f\n"};
1258  Statistic stat = computeGraphStatistics();
1259  for (uint i=0; i< CAT; ++i)
1260  {
1261  Indicator& indi = stat.indicators[i];
1262  cout << line % KEYS[i]
1263  % indi.cnt
1264  % (indi.frac*100)
1265  % indi.pS
1266  % indi.pL
1267  % indi.pLW
1268  % indi.cL
1269  % indi.cLW
1270  % indi.sL
1271  % indi.sLW
1272  ;
1273  }
1274  cout << _Fmt{"LEVL: %3d\n"} % stat.levels;
1275  cout << _Fmt{"SEGS: %3d h = ∅%3.1f / max.%2d\n"}
1276  % stat.segments
1277  % stat.avgheight
1278  % stat.maxheight;
1279  cout << "───═══───═══───═══───═══───═══───═══───═══───═══───═══───═══───"
1280  << endl;
1281  return move(*this);
1282  }
1283 
1284 
1285 
1286 
1287 
1288  /* ========= Configurable Computational Load ========= */
1289 
1312  {
1313  using Sink = volatile size_t;
1314 
1315  static double&
1316  computationSpeed (bool mem)
1317  {
1318  static double cpuSpeed{LOAD_SPEED_BASELINE};
1319  static double memSpeed{LOAD_SPEED_BASELINE};
1320  return mem? memSpeed : cpuSpeed;
1321  }
1322 
1323  public:
1324  microseconds timeBase = LOAD_DEFAULT_TIME;
1325  size_t sizeBase = LOAD_DEFAULT_MEM_SIZE;
1326  bool useAllocation = false;
1327 
1329  double
1330  invoke (uint scaleStep =1)
1331  {
1332  if (scaleStep == 0 or timeBase < 1us)
1333  return 0; // disabled
1334  return useAllocation? benchmarkTime ([this,scaleStep]{ causeMemProcessLoad (scaleStep); })
1335  : benchmarkTime ([this,scaleStep]{ causeComputationLoad(scaleStep); });
1336  }
1337 
1339  double
1340  benchmark (uint scaleStep =1)
1341  {
1342  return microBenchmark ([&]{ invoke(scaleStep);}
1344  .first; // ∅ runtime in µs
1345  }
1346 
1347  void
1348  calibrate()
1349  {
1350  TRANSIENTLY(useAllocation) = false;
1351  performIncrementalCalibration();
1352  useAllocation = true;
1353  performIncrementalCalibration();
1354  }
1355 
1356  void
1357  maybeCalibrate()
1358  {
1359  if (not isCalibrated())
1360  calibrate();
1361  }
1362 
1363  bool
1364  isCalibrated() const
1365  {
1366  return computationSpeed(false) != LOAD_SPEED_BASELINE;
1367  }
1368 
1369  private:
1370  uint64_t
1371  roundsNeeded (uint scaleStep)
1372  {
1373  auto desiredMicros = scaleStep*timeBase.count();
1374  return uint64_t(desiredMicros*computationSpeed(useAllocation));
1375  }
1376 
1377  auto
1378  allocNeeded (uint scaleStep)
1379  {
1380  auto cnt = roundsNeeded(scaleStep);
1381  auto siz = max (scaleStep * sizeBase, 1u);
1382  auto rep = max (cnt/siz, 1u);
1383  // increase size to fit
1384  siz = cnt / rep;
1385  return make_pair (siz,rep);
1386  }
1387 
1388  void
1389  causeComputationLoad (uint scaleStep)
1390  {
1391  auto round = roundsNeeded (scaleStep);
1392  Sink sink;
1393  size_t scree{sink};
1394  for ( ; 0 < round; --round)
1395  boost::hash_combine (scree,scree);
1396  sink = scree;
1397  sink++;
1398  }
1399 
1400  void
1401  causeMemProcessLoad (uint scaleStep)
1402  {
1403  auto [siz,round] = allocNeeded (scaleStep);
1404  lib::UninitialisedDynBlock<size_t> memBlock{siz};
1405  Sink sink;
1406  *memBlock.front() = sink+1;
1407  for ( ; 0 < round; --round)
1408  for (size_t i=0; i<memBlock.size()-1; ++i)
1409  memBlock[i+1] += memBlock[i];
1410  sink = *memBlock.back();
1411  sink++;
1412  }
1413 
1414  double
1415  determineSpeed()
1416  {
1417  uint step4gauge = 1;
1418  double micros = benchmark (step4gauge);
1419  auto stepsDone = roundsNeeded (step4gauge);
1420  return stepsDone / micros;
1421  }
1422 
1423  void
1424  performIncrementalCalibration()
1425  {
1426  double& speed = computationSpeed(useAllocation);
1427  double prev{speed},delta;
1428  do {
1429  speed = determineSpeed();
1430  delta = abs(1.0 - speed/prev);
1431  prev = speed;
1432  }
1433  while (delta > 0.05);
1434  }
1435  };
1436 
1437 
1439  inline SpecialJobFun
1440  onetimeCrunch (milliseconds runTime)
1441  { // ensure calibration prior to use
1442  ComputationalLoad().maybeCalibrate();
1443  //
1444  return SpecialJobFun{
1445  [runTime](JobParameter) -> void
1446  {
1447  ComputationalLoad crunch;
1448  crunch.timeBase = runTime;
1449  crunch.invoke();
1450  }};
1451  }
1452 
1453 
1454 
1455 
1462  template<size_t maxFan>
1464  TestChainLoad<maxFan>::performGraphSynchronously (microseconds timeBase, size_t sizeBase)
1465  {
1466  ComputationalLoad compuLoad;
1467  compuLoad.timeBase = timeBase;
1468  if (not sizeBase)
1469  {
1470  compuLoad.sizeBase = LOAD_DEFAULT_MEM_SIZE;
1471  compuLoad.useAllocation =false;
1472  }
1473  else
1474  {
1475  compuLoad.sizeBase = sizeBase;
1476  compuLoad.useAllocation =true;
1477  }
1478  compuLoad.maybeCalibrate();
1479 
1480  size_t seed = this->getSeed();
1481  for (Node& n : allNodes())
1482  {
1483  n.hash = isStart(n)? seed : 0;
1484  if (n.weight)
1485  compuLoad.invoke (n.weight);
1486  n.calculate();
1487  }
1488  return move(*this);
1489  }
1490 
1491 
1492 
1493 
1494 
1495  /* ========= Render Job generation and Scheduling ========= */
1496 
1501  : public JobClosure
1502  {
1503 
1504  static lib::time::Grid&
1505  testGrid()
1506  {
1508  return gridOne;
1509  }
1510 
1511  /* === JobFunctor Interface === */
1512 
1513  string diagnostic() const =0;
1514  void invokeJobOperation (JobParameter) =0;
1515 
1516  JobKind
1517  getJobKind() const
1518  {
1519  return TEST_JOB;
1520  }
1521 
1523  buildInstanceID (HashVal) const override
1524  {
1525  return InvocationInstanceID();
1526  }
1527 
1528  size_t
1529  hashOfInstance (InvocationInstanceID invoKey) const override
1530  {
1531  std::hash<size_t> hashr;
1532  HashVal res = hashr (invoKey.frameNumber);
1533  return res;
1534  }
1535 
1536  public:
1537 
1542  static InvocationInstanceID
1543  encodeNodeID (size_t idx)
1544  {
1545  InvocationInstanceID invoKey;
1546  invoKey.code.w1 = idx;
1547  return invoKey;
1548  };
1549 
1550  static size_t
1551  decodeNodeID (InvocationInstanceID invoKey)
1552  {
1553  return size_t(invoKey.code.w1);
1554  };
1555 
1556  static Time
1557  encodeLevel (size_t level)
1558  {
1559  return Time{testGrid().timeOf (FrameCnt(level))};
1560  }
1561 
1562  static size_t
1563  decodeLevel (TimeValue nominalTime)
1564  {
1565  return testGrid().gridPoint (nominalTime);
1566  }
1567  };
1568 
1569 
1570 
1578  template<size_t maxFan>
1580  : public ChainFunctor
1581  {
1582  using Node = typename TestChainLoad<maxFan>::Node;
1583  using Watch = lib::IncidenceCount;
1584 
1585  Node* startNode_;
1586  ComputationalLoad* compuLoad_;
1587  Watch* watch_;
1588 
1589  public:
1590  RandomChainCalcFunctor(Node& startNode, ComputationalLoad* load =nullptr, Watch* watch =nullptr)
1591  : startNode_{&startNode}
1592  , compuLoad_{load}
1593  , watch_{watch}
1594  { }
1595 
1596 
1598  void
1599  invokeJobOperation (JobParameter param) override
1600  {
1601  if (watch_) watch_->markEnter();
1602  size_t nodeIdx = decodeNodeID (param.invoKey);
1603  size_t level = decodeLevel (TimeValue{param.nominalTime});
1604  Node& target = startNode_[nodeIdx];
1605  ASSERT (target.level == level);
1606  // invoke the »media calculation«
1607  if (compuLoad_ and target.weight)
1608  compuLoad_->invoke (target.weight);
1609  target.calculate();
1610  if (watch_) watch_->markLeave();
1611  }
1612 
1613  string diagnostic() const override
1614  {
1615  return _Fmt{"ChainCalc(w:%d)◀%s"}
1616  % maxFan
1617  % util::showAdr(startNode_);
1618  }
1619  };
1620 
1621 
1626  template<size_t maxFan>
1628  : public ChainFunctor
1629  {
1630  using Node = typename TestChainLoad<maxFan>::Node;
1631 
1632  function<void(size_t,size_t)> scheduleCalcJob_;
1633  function<void(Node*,Node*)> markDependency_;
1634  function<void(size_t,size_t,size_t,bool)> continuation_;
1635 
1636  size_t maxCnt_;
1637  Node* nodes_;
1638 
1639  size_t currIdx_{0}; // Note: this test-JobFunctor is statefull
1640 
1641  public:
1642  template<class CAL, class DEP, class CON>
1643  RandomChainPlanFunctor(Node& nodeArray, size_t nodeCnt,
1644  CAL&& schedule, DEP&& markDepend,
1645  CON&& continuation)
1646  : scheduleCalcJob_{forward<CAL> (schedule)}
1647  , markDependency_{forward<DEP> (markDepend)}
1648  , continuation_{forward<CON> (continuation)}
1649  , maxCnt_{nodeCnt}
1650  , nodes_{&nodeArray}
1651  { }
1652 
1653 
1658  void
1659  invokeJobOperation (JobParameter param) override
1660  {
1661  size_t start{currIdx_};
1662  size_t reachedLevel{0};
1663  size_t targetNodeIDX = decodeNodeID (param.invoKey);
1664  for ( ; currIdx_<maxCnt_; ++currIdx_)
1665  {
1666  Node* n = &nodes_[currIdx_];
1667  if (currIdx_ <= targetNodeIDX)
1668  reachedLevel = n->level;
1669  else // continue until end of current level
1670  if (n->level > reachedLevel)
1671  break;
1672  scheduleCalcJob_(currIdx_, n->level);
1673  for (Node* pred: n->pred)
1674  markDependency_(pred,n);
1675  }
1676  ENSURE (currIdx_ > 0);
1677  continuation_(start, currIdx_-1, reachedLevel, currIdx_ < maxCnt_);
1678  }
1679 
1680 
1681  string diagnostic() const override
1682  {
1683  return "ChainPlan";
1684  }
1685  };
1686 
1687 
1688 
1689 
1702  template<size_t maxFan>
1704  : util::MoveOnly
1705  {
1706  TestChainLoad& chainLoad_;
1707  Scheduler& scheduler_;
1708 
1710 
1711  FrameRate levelSpeed_{1, SCHEDULE_LEVEL_STEP};
1712  FrameRate planSpeed_{1, SCHEDULE_PLAN_STEP};
1713  TimeVar nodeExpense_{SCHEDULE_NODE_STEP};
1714  double stressFac_{1.0};
1715  bool schedNotify_{SCHED_NOTIFY};
1716  bool schedDepends_{SCHED_DEPENDS};
1717  uint blockLoadFac_{2};
1718  size_t chunkSize_{DEFAULT_CHUNKSIZE};
1719  TimeVar startTime_{Time::ANYTIME};
1720  microseconds deadline_{STANDARD_DEADLINE};
1721  microseconds preRoll_{guessPlanningPreroll()};
1722  ManifestationID manID_{};
1723 
1724  std::vector<TimeVar> startTimes_{};
1725  std::promise<void> signalDone_{};
1726 
1727  std::unique_ptr<ComputationalLoad> compuLoad_;
1728  std::unique_ptr<RandomChainCalcFunctor<maxFan>> calcFunctor_;
1729  std::unique_ptr<RandomChainPlanFunctor<maxFan>> planFunctor_;
1730 
1731  std::unique_ptr<lib::IncidenceCount> watchInvocations_;
1732 
1733 
1734  /* ==== Callbacks from job planning ==== */
1735 
1737  void
1738  disposeStep (size_t idx, size_t level)
1739  {
1740  schedule_[idx] = scheduler_.defineSchedule(calcJob (idx,level))
1741  .manifestation(manID_)
1742  .startTime (jobStartTime(level, idx))
1743  .lifeWindow (deadline_);
1744  Node& n = chainLoad_.nodes_[idx];
1745  if (isnil (n.pred)
1746  or schedDepends_)
1747  schedule_[idx].post();
1748  }// Node with dependencies will be triggered by NOTIFY
1749  // and thus must not necessarily be scheduled explicitly.
1750 
1752  void
1753  setDependency (Node* pred, Node* succ)
1754  {
1755  size_t predIdx = chainLoad_.nodeID (pred);
1756  size_t succIdx = chainLoad_.nodeID (succ);
1757  bool unlimitedTime = not schedNotify_;
1758  schedule_[predIdx].linkToSuccessor (schedule_[succIdx], unlimitedTime);
1759  }
1760 
1762  void
1763  continuation (size_t chunkStart, size_t lastNodeIDX, size_t levelDone, bool work_left)
1764  {
1765  if (work_left)
1766  {
1767  size_t nextChunkEndNode = calcNextChunkEnd (lastNodeIDX);
1768  scheduler_.continueMetaJob (calcPlanScheduleTime (lastNodeIDX+1)
1769  ,planningJob (nextChunkEndNode)
1770  ,manID_);
1771  }
1772  else
1773  {
1774  auto wakeUp = scheduler_.defineSchedule(wakeUpJob())
1775  .manifestation (manID_)
1776  .startTime(jobStartTime (levelDone+1, lastNodeIDX+1) + SCHEDULE_WAKE_UP)
1777  .lifeWindow(SAFETY_TIMEOUT)
1778  .post();
1779  for (size_t exitIDX : lastExitNodes (chunkStart))
1780  wakeUp.linkToPredecessor (schedule_[exitIDX]);
1781  } // Setup wait-dependency on last computations
1782  }
1783 
1784 
1785  std::future<void>
1786  performRun()
1787  {
1788  auto finished = attachNewCompletionSignal();
1789  size_t numNodes = chainLoad_.size();
1790  size_t firstChunkEndNode = calcNextChunkEnd(0);
1791  schedule_.allocate (numNodes);
1792  compuLoad_->maybeCalibrate();
1793  calcFunctor_.reset (new RandomChainCalcFunctor<maxFan>{chainLoad_.nodes_[0], compuLoad_.get(), watchInvocations_.get()});
1794  planFunctor_.reset (new RandomChainPlanFunctor<maxFan>{chainLoad_.nodes_[0], chainLoad_.numNodes_
1795  ,[this](size_t i, size_t l){ disposeStep(i,l); }
1796  ,[this](auto* p, auto* s) { setDependency(p,s);}
1797  ,[this](size_t s,size_t n,size_t l, bool w)
1798  { continuation(s,n,l,w); }
1799  });
1800  startTime_ = anchorSchedule();
1801  scheduler_.seedCalcStream (planningJob(firstChunkEndNode)
1802  ,manID_
1803  ,calcLoadHint());
1804  return finished;
1805  }
1806 
1807 
1808  public:
1809  ScheduleCtx (TestChainLoad& mother, Scheduler& scheduler)
1810  : chainLoad_{mother}
1811  , scheduler_{scheduler}
1812  , compuLoad_{new ComputationalLoad}
1813  , calcFunctor_{}
1814  , planFunctor_{}
1815  { }
1816 
1820  double
1822  try {
1823  return benchmarkTime ([this]
1824  {
1825  awaitBlocking(
1826  performRun());
1827  })
1828  -_uSec(preRoll_); // timing accounted without pre-roll
1829  }
1830  ERROR_LOG_AND_RETHROW(test,"Scheduler testing")
1831 
1832  auto
1833  getScheduleSeq()
1834  {
1835  if (isnil (startTimes_))
1836  fillDefaultSchedule();
1837 
1838  return lib::explore(startTimes_)
1839  .transform([&](Time jobTime) -> TimeVar
1840  {
1841  return jobTime - startTimes_.front();
1842  });
1843  }
1844 
1845  double
1846  getExpectedEndTime()
1847  {
1848  return _raw(startTimes_.back() - startTimes_.front()
1849  + Duration{nodeExpense_}*(chainLoad_.size()/stressFac_));
1850  }
1851 
1852  auto
1853  getInvocationStatistic()
1854  {
1855  return watchInvocations_? watchInvocations_->evaluate()
1857  }
1858 
1859  double
1860  calcRuntimeReference()
1861  {
1862  microseconds timeBase = compuLoad_->timeBase;
1863  size_t sizeBase = compuLoad_->useAllocation? compuLoad_->sizeBase : 0;
1864  return chainLoad_.calcRuntimeReference (timeBase, sizeBase);
1865  }
1866 
1867  double getStressFac() { return stressFac_; }
1868 
1869 
1870 
1871  /* ===== Setter / builders for custom configuration ===== */
1872 
1873  ScheduleCtx&&
1874  withInstrumentation (bool doWatch =true)
1875  {
1876  if (doWatch)
1877  {
1878  watchInvocations_.reset (new lib::IncidenceCount);
1879  watchInvocations_->expectThreads (work::Config::COMPUTATION_CAPACITY)
1880  .expectIncidents(chainLoad_.size());
1881  }
1882  else
1883  watchInvocations_.reset();
1884  return move(*this);
1885  }
1886 
1887  ScheduleCtx&&
1888  withPlanningStep (microseconds planningTime_per_node)
1889  {
1890  planSpeed_ = FrameRate{1, Duration{_uTicks(planningTime_per_node)}};
1891  preRoll_ = guessPlanningPreroll();
1892  return move(*this);
1893  }
1894 
1895  ScheduleCtx&&
1896  withChunkSize (size_t nodes_per_chunk)
1897  {
1898  chunkSize_ = nodes_per_chunk;
1899  preRoll_ = guessPlanningPreroll();
1900  return move(*this);
1901  }
1902 
1903  ScheduleCtx&&
1904  withPreRoll (microseconds planning_headstart)
1905  {
1906  preRoll_ = planning_headstart;
1907  return move(*this);
1908  }
1909 
1910  ScheduleCtx&&
1911  withUpfrontPlanning()
1912  {
1913  withChunkSize (chainLoad_.size());
1914  preRoll_ *= UPFRONT_PLANNING_BOOST;
1915  return move(*this);
1916  }
1917 
1918  ScheduleCtx&&
1919  withLevelDuration (microseconds fixedTime_per_level)
1920  {
1921  levelSpeed_ = FrameRate{1, Duration{_uTicks(fixedTime_per_level)}};
1922  return move(*this);
1923  }
1924 
1925  ScheduleCtx&&
1926  withBaseExpense (microseconds fixedTime_per_node)
1927  {
1928  nodeExpense_ = _uTicks(fixedTime_per_node);
1929  return move(*this);
1930  }
1931 
1932  ScheduleCtx&&
1933  withSchedDepends (bool explicitly)
1934  {
1935  schedDepends_ = explicitly;
1936  return move(*this);
1937  }
1938 
1939  ScheduleCtx&&
1940  withSchedNotify (bool doSetTime =true)
1941  {
1942  schedNotify_ = doSetTime;
1943  return move(*this);
1944  }
1945 
1952  ScheduleCtx&&
1953  withAdaptedSchedule (double stressFac =1.0, uint concurrency=0, double formFac =1.0)
1954  {
1955  if (not concurrency) // use hardware concurrency (#cores) by default
1956  concurrency = defaultConcurrency();
1957  ENSURE (isLimited (1u, concurrency, 3*defaultConcurrency()));
1958  REQUIRE (formFac > 0.0);
1959  stressFac /= formFac;
1960  withLevelDuration (compuLoad_->timeBase);
1961  fillAdaptedSchedule (stressFac, concurrency);
1962  return move(*this);
1963  }
1964 
1965  double
1966  determineEmpiricFormFactor (uint concurrency=0)
1967  {
1968  if (not watchInvocations_) return 1.0;
1969  auto stat = watchInvocations_->evaluate();
1970  if (0 == stat.activationCnt) return 1.0;
1971  // looks like we have actual measurement data...
1972  ENSURE (0.0 < stat.avgConcurrency);
1973  if (not concurrency)
1974  concurrency = defaultConcurrency();
1975  double worktimeRatio = 1 - stat.timeAtConc(0) / stat.coveredTime;
1976  double workConcurrency = stat.avgConcurrency / worktimeRatio;
1977  double weightSum = chainLoad_.calcWeightSum();
1978  double expectedCompoundedWeight = chainLoad_.calcExpectedCompoundedWeight(concurrency);
1979  double expectedConcurrency = weightSum / expectedCompoundedWeight;
1980  double formFac = 1 / (workConcurrency / expectedConcurrency);
1981  double expectedNodeTime = _uSec(compuLoad_->timeBase) * weightSum / chainLoad_.size();
1982  double realAvgNodeTime = stat.activeTime / stat.activationCnt;
1983  formFac *= realAvgNodeTime / expectedNodeTime;
1984  return formFac;
1985  }
1986 
1987  ScheduleCtx&&
1988  withJobDeadline (microseconds deadline_after_start)
1989  {
1990  deadline_ = deadline_after_start;
1991  return move(*this);
1992  }
1993 
1994  ScheduleCtx&&
1995  withAnnouncedLoadFactor (uint factor_on_levelSpeed)
1996  {
1997  blockLoadFac_ = factor_on_levelSpeed;
1998  return move(*this);
1999  }
2000 
2001  ScheduleCtx&&
2002  withManifestation (ManifestationID manID)
2003  {
2004  manID_ = manID;
2005  return move(*this);
2006  }
2007 
2008  ScheduleCtx&&
2009  withLoadTimeBase (microseconds timeBase =LOAD_DEFAULT_TIME)
2010  {
2011  compuLoad_->timeBase = timeBase;
2012  return move(*this);
2013  }
2014 
2015  ScheduleCtx&&
2016  deactivateLoad()
2017  {
2018  compuLoad_->timeBase = 0us;
2019  return move(*this);
2020  }
2021 
2022  ScheduleCtx&&
2023  withLoadMem (size_t sizeBase =LOAD_DEFAULT_MEM_SIZE)
2024  {
2025  if (not sizeBase)
2026  {
2027  compuLoad_->sizeBase = LOAD_DEFAULT_MEM_SIZE;
2028  compuLoad_->useAllocation =false;
2029  }
2030  else
2031  {
2032  compuLoad_->sizeBase = sizeBase;
2033  compuLoad_->useAllocation =true;
2034  }
2035  return move(*this);
2036  }
2037 
2038  private:
2040  std::future<void>
2042  {
2043  std::promise<void> notYetTriggered;
2044  signalDone_.swap (notYetTriggered);
2045  return signalDone_.get_future();
2046  }
2047 
2048  void
2049  awaitBlocking(std::future<void> signal)
2050  {
2051  if (std::future_status::timeout == signal.wait_for (SAFETY_TIMEOUT))
2052  throw err::Fatal("Timeout on Scheduler test exceeded.");
2053  }
2054 
2055  Job
2056  calcJob (size_t idx, size_t level)
2057  {
2058  return Job{*calcFunctor_
2059  , calcFunctor_->encodeNodeID(idx)
2060  , calcFunctor_->encodeLevel(level)
2061  };
2062  }
2063 
2064  Job
2065  planningJob (size_t endNodeIDX)
2066  {
2067  return Job{*planFunctor_
2068  , planFunctor_->encodeNodeID(endNodeIDX)
2069  , Time::ANYTIME
2070  };
2071  }
2072 
2073  Job
2074  wakeUpJob ()
2075  {
2076  SpecialJobFun wakeUpFun{[this](JobParameter)
2077  {
2078  signalDone_.set_value();
2079  }};
2080  return Job{ wakeUpFun
2082  , Time::ANYTIME
2083  };
2084  }
2085 
2086  microseconds
2087  guessPlanningPreroll()
2088  {
2089  return microseconds(_raw(Time{chunkSize_ / planSpeed_}));
2090  }
2091 
2092  FrameRate
2093  calcLoadHint()
2094  {
2095  return FrameRate{levelSpeed_ * blockLoadFac_};
2096  }
2097 
2098  size_t
2099  calcNextChunkEnd (size_t lastNodeIDX)
2100  {
2101  lastNodeIDX += chunkSize_;
2102  return min (lastNodeIDX, chainLoad_.size()-1);
2103  } // prevent out-of-bound access
2104 
2105  Time
2106  anchorSchedule()
2107  {
2108  Time anchor = RealClock::now() + _uTicks(preRoll_);
2109  if (isnil (startTimes_))
2110  fillDefaultSchedule();
2111  size_t numPoints = chainLoad_.topLevel()+2;
2112  ENSURE (startTimes_.size() == numPoints);
2113  Offset base{startTimes_.front(), anchor};
2114  for (size_t level=0; level<numPoints; ++level)
2115  startTimes_[level] += base;
2116  return anchor;
2117  }
2118 
2119  void
2120  fillDefaultSchedule()
2121  {
2122  size_t numPoints = chainLoad_.topLevel()+2;
2123  stressFac_ = 1.0;
2124  startTimes_.clear();
2125  startTimes_.reserve (numPoints);
2126  for (size_t level=0; level<numPoints; ++level)
2127  startTimes_.push_back (level / levelSpeed_);
2128  }
2129 
2130  void
2131  fillAdaptedSchedule (double stressFact, uint concurrency)
2132  {
2133  REQUIRE (stressFact > 0);
2134  stressFac_ = stressFact;
2135  size_t numPoints = chainLoad_.topLevel()+2;
2136  startTimes_.clear();
2137  startTimes_.reserve (numPoints);
2138  startTimes_.push_back (Time::ZERO);
2139  chainLoad_.levelScheduleSequence (concurrency)
2140  .transform([&](double scheduleFact){ return (scheduleFact/stressFac_) * Offset{1,levelSpeed_};})
2141  .effuse(startTimes_);
2142  }
2143 
2144  Time
2145  jobStartTime (size_t level, size_t nodeIDX =0)
2146  {
2147  ENSURE (level < startTimes_.size());
2148  return startTimes_[level]
2149  + nodeExpense_ * (nodeIDX/stressFac_);
2150  }
2151 
2152  auto
2153  lastExitNodes (size_t lastChunkStartIDX)
2154  {
2155  return chainLoad_.allExitNodes()
2156  .transform([&](Node& n){ return chainLoad_.nodeID(n); })
2157  .filter([=](size_t idx){ return idx >= lastChunkStartIDX; });
2158  } // index of all Exit-Nodes within last planning-chunk...
2159 
2160  Time
2161  calcPlanScheduleTime (size_t lastNodeIDX)
2162  {/* must be at least 1 level ahead,
2163  because dependencies are defined backwards;
2164  the chain-load graph only defines dependencies over one level
2165  thus the first level in the next chunk must still be able to attach
2166  dependencies to the last row of the preceding chunk, implying that
2167  those still need to be ahead of schedule, and not yet dispatched.
2168  */
2169  lastNodeIDX = min (lastNodeIDX, chainLoad_.size()-1); // prevent out-of-bound access
2170  size_t nextChunkLevel = chainLoad_.nodes_[lastNodeIDX].level;
2171  nextChunkLevel = nextChunkLevel>2? nextChunkLevel-2 : 0;
2172  return jobStartTime(nextChunkLevel) - _uTicks(preRoll_);
2173  }
2174  };
2175 
2176 
2181  template<size_t maxFan>
2184  {
2185  clearNodeHashes();
2186  return ScheduleCtx{*this, scheduler};
2187  }
2188 
2189 
2190 
2191 }}} // namespace vault::gear::test
2192 #endif /*VAULT_GEAR_TEST_TEST_CHAIN_LOAD_H*/
static const Time ANYTIME
border condition marker value. ANYTIME <= any time value
Definition: timevalue.hpp:313
Distribution indicators for one kind of evaluation.
a mutable time value, behaving like a plain number, allowing copy and re-accessing ...
Definition: timevalue.hpp:232
const double LOAD_SPEED_BASELINE
initial assumption for calculation speed (without calibration)
const StatKey STAT_NODE
all nodes
void setDependency(Node *pred, Node *succ)
Callback: define a dependency between scheduled jobs.
Table with connections to other Node records.
double calcExpectedCompoundedWeight(uint concurrency=1)
calculate the simplified/theoretic reduction of compounded weight through concurrency ...
double cLW
weight centre level width-reduced
const StatKey STAT_LINK
1:1 linking node
const StatKey STAT_SEED
seed node
TestChainLoad && configureShape_short_chains3_interleaved()
preconfigured topology: simple 3-step chains, starting interleaved
const Duration SCHEDULE_PLAN_STEP
time budget to reserve for each node to be planned and scheduled
const size_t LOAD_DEFAULT_MEM_SIZE
default allocation base size used if ComputationalLoad.useAllocation
const size_t LOAD_BENCHMARK_RUNS
repetition count for calibration benchmark for ComputationalLoad
Types marked with this mix-in may be moved and move-assigned.
Definition: nocopy.hpp:63
void seedCalcStream(Job planningJob, ManifestationID manID=ManifestationID(), FrameRate expectedAdditionalLoad=FrameRate(25))
Set the Scheduler to work on a new CalcStream.
Definition: scheduler.hpp:318
const StatKey STAT_KNOT
knot (joins and forks)
Automatically use custom string conversion in C++ stream output.
Support for generation of Graphviz-DOT code for structure visualisation.
const bool SCHED_NOTIFY
explicitly set notify dispatch time to the dependency&#39;s start time.
#define TRANSIENTLY(_OO_)
Macro to simplify capturing assignments.
static InvocationInstanceID encodeNodeID(size_t idx)
package the node-index to invoke.
Adaptor to handle further mapping functions.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
const StatKey STAT_WGHT
node weight
bool filter(Placement< DummyMO > const &candidate)
a filter predicate to pick some objects from a resultset.
AnyPair entry(Query< TY > const &query, typename WrapReturn< TY >::Wrapper &obj)
helper to simplify creating mock table entries, wrapped correctly
Definition: run.hpp:40
Framerate specified as frames per second.
Definition: timevalue.hpp:655
const bool SCHED_DEPENDS
explicitly schedule a dependent job (or rely on NOTIFY)
auto levelScheduleSequence(uint concurrency=1)
sequence of the summed compounded weight factors after each level
static Rule rule()
Abbreviation for starting rules.
const StatKey STAT_EXIT
exit node
const StatKey STAT_INNR
inner node
Types marked with this mix-in may be moved but not copied.
Definition: nocopy.hpp:49
const Offset SCHEDULE_WAKE_UP
tiny offset to place the final wake-up job behind any systematic schedule
Front-end for printf-style string template interpolation.
TestChainLoad && configure_isolated_nodes()
preconfigured topology: only unconnected seed/exit nodes
A configurable one-time job to invoke some special function.
ScheduleCtx && withAdaptedSchedule(double stressFac=1.0, uint concurrency=0, double formFac=1.0)
Establish a differentiated schedule per level, taking node weights into account.
int rani(uint bound=_iBOUND())
Definition: random.hpp:135
Primary class template for std::hash.
ScheduleSpec post()
build Activity chain and hand-over to the Scheduler.
Definition: scheduler.hpp:547
size_t getHash() const
global hash is the combination of all exit node hashes != 0
double invoke(uint scaleStep=1)
cause a delay by computational load
TestChainLoad && buildTopology()
Use current configuration and seed to (re)build Node connectivity.
Functions to perform (multithreaded) timing measurement on a given functor.
void continuation(size_t chunkStart, size_t lastNodeIDX, size_t levelDone, bool work_left)
continue planning: schedule follow-up planning job
Front-end to configure a special job functor for one-time use.
A Result Value confined into fixed bounds.
A Generator for synthetic Render Jobs for Scheduler load testing.
A front-end for using printf-style formatting.
Record and evaluate concurrent activations.
static const Duration NIL
constant to indicate "no duration"
Definition: timevalue.hpp:506
const size_t DEFAULT_CHUNKSIZE
number of computation jobs to prepare in each planning round
void invokeJobOperation(JobParameter param) override
render job invocation to trigger one batch of scheduling; the installed callback-λ should actually pl...
const Duration SCHEDULE_NODE_STEP
additional time step to include in the plan for each job (node).
ScheduleSpec defineSchedule(Job job)
Render Job builder: start definition of a schedule to invoke the given Job.
Definition: scheduler.hpp:357
double pS
average per segment
const auto SAFETY_TIMEOUT
maximum time limit for test run, abort if exceeded
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:299
auto microBenchmark(FUN const &testSubject, const size_t repeatCnt=DEFAULT_RUNS)
perform a simple looped microbenchmark.
std::future< void > attachNewCompletionSignal()
push away any existing wait state and attach new clean state
double benchmarkTime(FUN const &invokeTestCode, const size_t repeatCnt=1)
Helper to invoke a functor or λ to observe its running time.
ScheduleCtx setupSchedule(Scheduler &scheduler)
establish and configure the context used for scheduling computations.
double sL
weight centre on subgraph
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:190
JobKind
Definition: job.h:62
»Scheduler-Service« : coordinate render activities.
Definition: scheduler.hpp:213
static const FrameRate STEP
1 frame per second
Definition: timevalue.hpp:673
Service for coordination and dispatch of render activities.
Marker for current (and obsolete) manifestations of a CalcStream processed by the Render-Engine...
Definition: activity.hpp:84
TestChainLoad && recalculate()
Recalculate all node hashes and propagate seed value.
TestChainLoad && configureShape_short_chains2()
preconfigured topology: isolated simple 2-step chains
const size_t DEFAULT_SIZ
default node count for the complete load graph
void disposeStep(size_t idx, size_t level)
Callback: place a single job into the scheduler.
int reduce(Gtk::Button &icon)
attempt to reduce space consumption
size_t calcWeightSum()
overall sum of configured node weights
Render JobFunctor to invoke the calculation of a single Node.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
double computeWeightFactor(LevelWeight const &lw, uint concurrency)
simplified model for expense of a level of nodes, computed concurrently.
double calcRuntimeReference(microseconds timeBase=LOAD_DEFAULT_TIME, size_t sizeBase=0, size_t repeatCnt=GRAPH_BENCHMARK_RUNS)
Conduct a number of benchmark runs over processing the Graph synchronously.
std::string toString(TY const &val) noexcept
get some string representation of any object, reliably.
Definition: format-obj.hpp:191
double pL
average per level
Managed uninitialised Heap-allocated storage with array like access.
static size_t defaultSrc(Node *node)
by default use Node-hash directly as source of randomness
Baseclass: JobFunctor to invoke TestChainLoad.
TestChainLoad && configureShape_chain_loadBursts()
preconfigured topology: single graph with massive »load bursts«
test and diagnostic and research
Definition: job.h:67
const size_t DEFAULT_FAN
default maximum connectivity per Node
const StatKey STAT_JOIN
joining node
TestChainLoad && setSeed(size_t seed=rani())
Set the overall seed value.
Statistic computeGraphStatistics()
Operator on TestChainLoad to evaluate current graph connectivity.
void continueMetaJob(Time nextStart, Job planningJob, ManifestationID manID=ManifestationID())
Place a follow-up job-planning job into the timeline.
Definition: scheduler.hpp:333
A raw memory block with proper alignment and array access.
Policy/Binding for generation of random parameters by »drawing« based on the node-hash.
Definition of a render job.
Test helper to perform temporary manipulations within a test scope.
A recorder for concurrent incidences.
opaque ID attached to each individual job invocation.
Definition: job.h:103
const StatKey STAT_FORK
forking node
Library functions to support the formation of grid-aligned time values.
Basic set of definitions and includes commonly used together (Vault).
size_t HashVal
a STL compatible hash value
Definition: hash-value.h:52
Interface of the closure for frame rendering jobs.
Definition: job.h:235
Offset measures a distance in time.
Definition: timevalue.hpp:358
Statistic data calculated for a given chain-load topology.
const auto STANDARD_DEADLINE
deadline to use for each individual computation job
double sLW
weight centre on subgraph width-reduced
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:468
NUM constexpr limited(NB lowerBound, NUM val, NB upperBound)
force a numeric to be within bounds, inclusively
Definition: util.hpp:91
const microseconds LOAD_DEFAULT_TIME
default time delay produced by ComputationalLoad at Node.weight==1
static double & computationSpeed(bool mem)
const double UPFRONT_PLANNING_BOOST
factor to increase the computed pre-roll to ensure up-front planning
Setup and wiring for a test run to schedule a computation structure as defined by this TestChainLoad ...
Accessing a STL element range through a Lumiera forward iterator, An instance of this iterator adapte...
SpecialJobFun onetimeCrunch(milliseconds runTime)
a »throw-away« render-job
Build a component to select limited values randomly.
static size_t COMPUTATION_CAPACITY
Nominal »full size« of a pool of concurrent workers.
Definition: work-force.hpp:106
TestChainLoad && clearNodeHashes()
Clear node hashes and propagate seed value.
TestChainLoad && configureShape_short_segments3_interleaved()
preconfigured topology: simple interwoven 3-step graph segments
double launch_and_wait()
dispose one complete run of the graph into the scheduler
double cL
weight centre level for this indicator
Statistic evaluate()
Visit all data captured thus far, construct an unified timeline and then compute statistics evaluatio...
Abstraction of a value alignment grid.
Definition: grid.hpp:58
Building tree expanding and backtracking evaluations within hierarchical scopes.
Render JobFunctor to perform chunk wise planning of Node jobs to calculate a complete Chain-Load grap...
Token to capture a value and restore original when leaving scope.
Definition: transiently.hpp:45
void invokeJobOperation(JobParameter param) override
render job invocation to trigger one Node recalculation
double frac
fraction of all nodes
Individual frame rendering task, forwarding to a closure.
Definition: job.h:268
a family of time value like entities and their relationships.
double pLW
average per level and level-width
const size_t GRAPH_BENCHMARK_RUNS
repetition count for reference calculation of a complete node graph
basic constant internal time value.
Definition: timevalue.hpp:133
const Duration SCHEDULE_LEVEL_STEP
time budget to plan for the calculation of each »time level« of jobs
typename _Fun< FUN >::Ret _FunRet
abbreviation for referring to a function&#39;s return type
Definition: function.hpp:187
Vault-Layer implementation namespace root.
auto allLevelWeights()
calculate node weights aggregated per level
TestChainLoad && performGraphSynchronously(microseconds timeBase=LOAD_DEFAULT_TIME, size_t sizeBase=0)
Emulate complete graph processing in a single threaded loop.
static size_t getDefaultComputationCapacity()
default value for full computing capacity is to use all (virtual) cores.
Definition: work-force.cpp:51
A calibratable CPU load to be invoked from a node job functor.
TestChainLoad && printTopologyStatistics()
Print a tabular summary of graph characteristics.
Simple stand-alone Quantiser implementation based on a constant sized gird.
Definition: quantiser.hpp:135
TestChainLoad && setWeight(size_t fixedNodeWeight=1)
Set a fixed weight for all nodes.
uint cnt
global sum over all levels