lumiera_documentation/src/lib/incidence-count.hpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
 * there is no entity "Lumiera.org" which holds any copyrights
 * Lumiera source code is provided under the GPL Version 2+

== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''

The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!

The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00

288 lines
9.8 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
INCIDENCE-COUNT.hpp - instrumentation helper to watch possibly multithreaded task activations
Copyright (C)
2024, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file incidence-count.hpp
** Record and evaluate concurrent activations.
** This observation and instrumentation helper is typically used to capture
** density of incidence and effective concurrency of performance critical tasks.
** The overhead per measurement call amounts to one SystemClock invocation plus
** some heap memory access, assuming that sufficient memory was pre-allocated
** prior to the actual observation phase. Moreover, on first invocation per Thread,
** a thread local ID is constructed, thereby incrementing an global atomic counter.
** Statistics evaluation is comprised of integrating and sorting the captured
** event log, followed by a summation pass.
**
** # Usage and limitations
** This helper is intended for tests and one-time usage. Create an instance,
** launch a test, retrieve the observed statistics, destroy the object. Each
** separate Threads encountered gets the next consecutive ID. Thus it is *not possible*
** to have long-living instances or even multiple instances of IncidenceCount; doing so
** would require a much more elaborate ID management, which is beyond requirement's scope.
**
** @see IncidenceCount_test
** @see vault::gear::TestChainLoad::ScheduleCtx
** @see SchedulerStress_test
*/
#ifndef LIB_INCIDENCE_COUNT_H
#define LIB_INCIDENCE_COUNT_H
#include "lib/nocopy.hpp"
#include "lib/iter-explorer.hpp"
#include <cstdint>
#include <atomic>
#include <vector>
#include <chrono>
#include <limits>
#include <algorithm>
namespace lib {
using std::vector;
/**
* A recorder for concurrent incidences.
* Start and end of individual activations are recorded by direct calls,
* automatically detecting the thread identity; for further differentiation,
* an additional `caseID` can be given. Accumulated observations can be
* integrated into a statistics evaluation.
* @warning never operate multiple instances of this helper at the same time
*/
class IncidenceCount
: util::NonCopyable
{
using TIMING_SCALE = std::micro; // Results are in µ-sec
using Clock = std::chrono::steady_clock;
using Instance = decltype(Clock::now());
using Dur = std::chrono::duration<double, TIMING_SCALE>;
struct Inc
{
Instance when;
uint8_t thread :8;
uint8_t caseID :8;
bool isLeave :1;
};
using Sequence = vector<Inc>;
using Recording = vector<Sequence>;
Recording rec_;
std::atomic_uint8_t slotID_{0};
/** threadsafe allocation of thread/slotID */
uint8_t
allocateNextSlot()
{ // Note : returning previous value before increment
return slotID_.fetch_add(+1, std::memory_order_relaxed);
}
uint8_t
getMySlot()
{
thread_local uint8_t threadID{allocateNextSlot()};
ASSERT (threadID < std::numeric_limits<uint8_t>::max(), "WOW -- so many threads?");
return threadID;
}
Sequence&
getMySequence(uint8_t threadID)
{
if (threadID >= rec_.size())
{
rec_.reserve (threadID+1);
for (size_t i = rec_.size(); i < threadID+1u; ++i)
rec_.emplace_back();
}
return rec_[threadID];
}
void
addEntry(uint8_t caseID, bool isLeave)
{
uint8_t threadID{getMySlot()};
Sequence& seq = getMySequence(threadID);
Inc& incidence = seq.emplace_back();
incidence.when = Clock::now();
incidence.thread = threadID;
incidence.caseID = caseID;
incidence.isLeave = isLeave;
}
public:
IncidenceCount() = default;
IncidenceCount&
expectThreads(uint8_t cnt)
{
REQUIRE (cnt);
rec_.resize (cnt);
return *this;
}
IncidenceCount&
expectIncidents(size_t cnt)
{
REQUIRE (cnt);
for (Sequence& s : rec_)
s.reserve (2*cnt);
return *this;
}
/* ===== Measurement API ===== */
void markEnter(uint8_t caseID =0) { addEntry(caseID, false); }
void markLeave(uint8_t caseID =0) { addEntry(caseID, true); }
/* ===== Evaluations ===== */
struct Statistic
{
size_t eventCnt{0};
size_t activationCnt{0};
double cumulatedTime{0}; ///< aggregated time over all cases
double activeTime{0}; ///< compounded time of thread activity
double coveredTime{0}; ///< overall timespan of observation
double avgConcurrency{0}; ///< amortised concurrency in timespan
vector<size_t> caseCntr{}; ///< counting activations per case
vector<size_t> thrdCntr{}; ///< counting activations per thread
vector<double> caseTime{}; ///< aggregated time per case
vector<double> thrdTime{}; ///< time of activity per thread
vector<double> concTime{};
template<typename VAL>
static VAL
access (vector<VAL> const& data, size_t idx)
{
return idx < data.size()? data[idx]
: VAL{};
}
size_t cntCase (size_t id) const { return access (caseCntr, id); }
size_t cntThread(size_t id) const { return access (thrdCntr, id); }
double timeCase (size_t id) const { return access (caseTime, id); }
double timeThread(size_t id) const { return access (thrdTime, id); }
double timeAtConc(size_t id) const { return access (concTime, id); }
};
Statistic evaluate();
double
calcCumulatedTime()
{
return evaluate().cumulatedTime;
}
};
/**
* Visit all data captured thus far, construct an unified timeline
* and then compute statistics evaluations to characterise observations.
* @warning caller must ensure there was a barrier or visibility sync before invocation.
*/
inline IncidenceCount::Statistic
IncidenceCount::evaluate()
{
Statistic stat;
size_t numThreads = rec_.size();
if (numThreads == 0) return stat;
size_t numEvents = explore(rec_)
.transform([](Sequence& seq){ return seq.size(); })
.resultSum();
if (numEvents == 0) return stat;
Sequence timeline;
timeline.reserve(numEvents);
for (Sequence& seq : rec_)
for (Inc& event : seq)
timeline.emplace_back(event);
std::stable_sort (timeline.begin(), timeline.end()
,[](Inc const& l, Inc const& r) { return l.when < r.when; }
);
int active{0};
vector<int> active_case;
vector<int> active_thrd(numThreads);
stat.thrdCntr.resize (numThreads);
stat.thrdTime.resize (numThreads);
stat.concTime.resize (numThreads+1); // also accounting for idle times in range
// Integrate over the timeline...
// - book the preceding interval length into each affected partial sum
// - adjust current active count in accordance to the current event
Instance prev = timeline.front().when;
for (Inc& event : timeline)
{
if (event.caseID >= stat.caseCntr.size())
{
active_case .resize (event.caseID+1);
stat.caseCntr.resize (event.caseID+1);
stat.caseTime.resize (event.caseID+1);
}
Dur timeSlice = event.when - prev;
stat.cumulatedTime += active * timeSlice.count();
for (uint i=0; i < stat.caseCntr.size(); ++i)
stat.caseTime[i] += active_case[i] * timeSlice.count();
for (uint i=0; i < numThreads; ++i)
if (active_thrd[i]) // note: counting activity per thread, without overlapping cases
stat.thrdTime[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);
ASSERT (0 < active_case[event.caseID]);
ASSERT (0 < active_thrd[event.thread]);
--active;
--active_case[event.caseID];
--active_thrd[event.thread];
}
else
{
++active;
++active_case[event.caseID];
++active_thrd[event.thread];
++stat.caseCntr[event.caseID];
++stat.thrdCntr[event.thread];
++stat.activationCnt;
}
prev = event.when;
}
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
stat.activeTime = explore(stat.thrdTime).resultSum();
return stat;
}
} // namespace lib
#endif /*LIB_INCIDENCE_COUNT_H*/