diff --git a/src/lib/incidence-count.hpp b/src/lib/incidence-count.hpp index 1cc1ba6df..d2b7a94cd 100644 --- a/src/lib/incidence-count.hpp +++ b/src/lib/incidence-count.hpp @@ -175,11 +175,13 @@ namespace lib { size_t activationCnt{0}; double cumulatedTime{0}; double coveredTime{0}; + double avgConcurrency{0}; vector caseCntr{}; vector thrdCntr{}; vector caseTime{}; vector thrdTime{}; + vector concTime{}; template static VAL @@ -192,6 +194,7 @@ namespace lib { size_t cntThread(size_t id) { return access (thrdCntr, id); } double timeCase (size_t id) { return access (caseTime, id); } double timeThread(size_t id) { return access (thrdTime, id); } + double timeAtConc(size_t id) { return access (concTime, id); } }; Statistic evaluate(); @@ -237,6 +240,7 @@ namespace lib { vector active_thrd(numThreads); stat.thrdCntr.resize (numThreads); stat.thrdTime.resize (numThreads); + stat.concTime.resize (numThreads); // Integrate over the timeline... // - book the preceding interval length into each affected partial sum @@ -255,6 +259,10 @@ namespace lib { stat.caseTime[i] += active_case[i] * timeSlice.count(); for (uint i=0; i < numThreads; ++i) stat.thrdTime[i] += active_thrd[i] * timeSlice.count(); + size_t concurr = explore(active_thrd).filter([](int a){ return 0 < a; }).count(); + ENSURE (concurr <= numThreads); + stat.avgConcurrency += concurr * timeSlice.count(); // contribution for weighted average + stat.concTime[concurr] += timeSlice.count(); if (event.isLeave) { ASSERT (0 < active); @@ -278,6 +286,9 @@ namespace lib { Dur covered = timeline.back().when - timeline.front().when; stat.coveredTime = covered.count(); stat.eventCnt = timeline.size(); + ENSURE (0 < stat.activationCnt); + ENSURE (stat.eventCnt % 2 == 0); + stat.avgConcurrency /= stat.coveredTime; // time used as weight sum return stat; } diff --git a/tests/library/incidence-count-test.cpp b/tests/library/incidence-count-test.cpp index fc44d0a50..6cb20d254 100644 --- a/tests/library/incidence-count-test.cpp +++ b/tests/library/incidence-count-test.cpp @@ -27,7 +27,9 @@ #include "lib/test/run.hpp" #include "lib/test/diagnostic-output.hpp"//////////////TODO RLY? +#include "lib/test/microbenchmark.hpp" #include "lib/incidence-count.hpp" +#include "lib/thread.hpp" #include "lib/util.hpp" //#include @@ -38,6 +40,7 @@ using util::isLimited; using std::this_thread::sleep_for; using std::chrono_literals::operator ""ms; +using std::chrono::microseconds; namespace lib { @@ -58,7 +61,8 @@ namespace test{ { demonstrate_usage(); verify_incidentCount(); - verify_multithreadCount(); + verify_concurrencyStatistic(); + perform_multithreadStressTest(); } @@ -86,8 +90,8 @@ namespace test{ } - /** @test TODO verify proper counting of possibly overlapping incidences - * @todo WIP 2/24 ✔ define ⟶ 🔁 implement + /** @test verify proper counting of possibly overlapping incidences + * @todo WIP 2/24 ✔ define ⟶ ✔ implement */ void verify_incidentCount() @@ -154,11 +158,100 @@ SHOW_EXPR(stat.timeThread(1)); } + /** @test TODO verify observation of concurrency degree + * @todo WIP 2/24 ✔ define ⟶ ✔ implement + */ + void + verify_concurrencyStatistic() + { + MARK_TEST_FUN + const size_t CONCURR = std::thread::hardware_concurrency(); + + IncidenceCount watch; + watch.expectThreads(CONCURR) + .expectIncidents(5000); + + auto act = [&]{ // two nested activities with random delay + uint delay = 100 + rand() % 800; + watch.markEnter(); + sleep_for (microseconds(delay)); + watch.markEnter(2); + sleep_for (microseconds(delay)); + watch.markLeave(2); + watch.markLeave(); + }; + + auto run_parallel = [&] + { + ThreadJoinable t1("test-1", act); + ThreadJoinable t2("test-2", act); + t1.join(); + t2.join(); + }; + + double runTime = test::benchmarkTime (run_parallel); + + // join ensures visibility of all data changes from within threads, + // which is a prerequisite for performing the data evaluation safely. + auto stat = watch.evaluate(); +SHOW_EXPR(runTime) +SHOW_EXPR(stat.cumulatedTime); +SHOW_EXPR(stat.coveredTime); +SHOW_EXPR(stat.eventCnt); +SHOW_EXPR(stat.activationCnt); +SHOW_EXPR(stat.cntCase(0)); +SHOW_EXPR(stat.cntCase(1)); +SHOW_EXPR(stat.cntCase(2)); +SHOW_EXPR(stat.cntCase(3)); +SHOW_EXPR(stat.timeCase(0)); +SHOW_EXPR(stat.timeCase(1)); +SHOW_EXPR(stat.timeCase(2)); +SHOW_EXPR(stat.timeCase(3)); +SHOW_EXPR(stat.cntThread(0)); +SHOW_EXPR(stat.cntThread(1)); +SHOW_EXPR(stat.cntThread(2)); +SHOW_EXPR(stat.timeThread(0)); +SHOW_EXPR(stat.timeThread(1)); +SHOW_EXPR(stat.timeThread(2)); +SHOW_EXPR(stat.avgConcurrency); +SHOW_EXPR(stat.timeAtConc(0)); +SHOW_EXPR(stat.timeAtConc(1)); +SHOW_EXPR(stat.timeAtConc(2)); + CHECK (runTime > stat.coveredTime); + CHECK (stat.coveredTime < stat.cumulatedTime); + CHECK (8 == stat.eventCnt); + CHECK (4 == stat.activationCnt); + CHECK (2 == stat.cntCase(0)); + CHECK (0 == stat.cntCase(1)); + CHECK (2 == stat.cntCase(2)); + CHECK (0 == stat.cntCase(3)); + CHECK (2 == stat.cntThread(0)); + CHECK (2 == stat.cntThread(1)); + CHECK (0 == stat.cntThread(3)); + CHECK (isLimited(0, stat.avgConcurrency, 2)); + CHECK (stat.timeAtConc(0) == 0.0); + CHECK (stat.timeAtConc(1) < stat.coveredTime); + CHECK (stat.timeAtConc(2) < stat.coveredTime); + + auto isNumEq = [](double d1, double d2){ return 0,001 > abs(d1-d2); }; + + CHECK (isNumEq (stat.avgConcurrency, (1*stat.timeAtConc(1) + 2*stat.timeAtConc(2)) // average concurrency is a weighted mean + / stat.coveredTime)); // of the times spent at each concurrency level + + CHECK (isNumEq (stat.cumulatedTime , stat.timeThread(0) + stat.timeThread(1))); // cumulated time is spent in both threads + CHECK (isNumEq (stat.cumulatedTime , stat.timeCase(0) + stat.timeCase(2))); // and likewise in all cases together + CHECK (isNumEq (stat.coveredTime , stat.timeAtConc(1) + stat.timeAtConc(2))); // the covered time happens at any non-zero concurrency level + + CHECK (stat.timeCase(2) < stat.timeCase(0)); // Note: case-2 is nested into case-0 + CHECK (isNumEq (stat.coveredTime , stat.timeCase(0) - stat.timeAtConc(2))); // Thus, case-0 brackets all time, minus the overlapping segment + } + + /** @test TODO verify thread-safe operation under pressure * @todo WIP 2/24 🔁 define ⟶ implement */ void - verify_multithreadCount() + perform_multithreadStressTest() { UNIMPLEMENTED("verify thread-safe operation under pressure"); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 292a4f38d..d8bb068f6 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -110889,9 +110889,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Fokus ⟹ Ziel ⟹ es geht um Halten ⟷ Brechen @@ -110904,9 +110902,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

es geht nicht um die Ermittlung einer empirischen »Performance« @@ -110917,9 +110913,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

sie muß gut genug im Sinn von tauglich sein @@ -110937,42 +110931,33 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Zwar ist die ComputationalLoad auf die jeweilige Hardware dynamisch geeicht, aber in den Einzelbeobachtungen habe ich immer ganz erhebliche Abweichungen für Node-Aufrufe mit Load gesehen. Daher ist die Frage, wie verhält sich die insgesamt (kumuliert, d.h. integriert) verbrachte Zeit zu der nominell geeichten Zeit? Daraus ergäbe sich ein Korrektur-Faktor, den man aus dem StressFac herausrechnen könnte

- -
+
- - - +

Untersuchung per Godbolt.org : SystemClock-Aufrufe werden nicht wegoptimiert

- -
+ - - - +

...das ist überraschend, denn sonst wird wirklich alles wie erwartet eingedampft und es bleibt nur die eigentliche Nutzfunktion übrig; der Grund ist aber leicht einzusehen: es handelt sich um einen externen Call, der Seiteneffekte haben könnte.

- -
+
@@ -110992,16 +110977,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

das bedeutet: jeder Thread hat seinen eigenen »Slot«, den er ohne jedwede Synchronisation zugreifen kann

- -
+
@@ -111010,16 +110992,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

Aus Performance-Gründen möchte ich für jeden separaten Thread eine initial belegte slotID haben, so daß der Thread direkt ohne weiteres Locking mit seinen separaten Daten arbeiten kann. Dafür muß ich für neu auftauchende Threads immer die nächste ID vergeben, brauche also einen atomaren counter. Da aber jeder Thread dann seine ID kennen muß, brauche ich zudem eine thread_local-Variable, in der er sich seinen Slot merken kann. Und damit tut sich ein Dilemma auf: ein übergreifendes Management von Instanzen wird richtig komplex, besonders dann, wenn es auch noch performant sein soll. Daher der KISS-Beschluß ⟹ das gesamte Thema wird auf den User abgewältzt

- -
+
@@ -111062,8 +111041,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -111097,26 +111076,29 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - - + + + + - - + + - - + + - + + + +