Commit graph

2035 commits

Author SHA1 Message Date
e0766f2262 Chain-Load: draft usage for Scheduler testing
- use a dedicated context "dropped off" the TestChainLoad instance
- encode the node-idx into the InvocationInstanceID
- build an invocation- and a planning-job-functor
- let planning progress over an lib::UninitialisedStorage array
- plant the ActivityTerm instances into that array as Scheduling progresses
2023-12-04 00:34:06 +01:00
c5679b0fd0 Library: Uninitialised-Storage array (see #1204)
Introduced as remedy for a long standing sloppiness:
Using a `char[]` together with `reinterpret_cast` in storage management helpers
bears danger of placing objects with wrong alignment; moreover, there are increasing
risks that modern code optimisers miss the ''backdoor access'' and might apply too
aggressive rewritings.

With C++17, there is a standard conformant way to express such a usage scheme.
 * `lib::UninitialisedStorage` can now be used in a situation (e.g. as in `ExtentFamily`)
   where a complete block of storage is allocated once and then subsequently used
   to plant objects one by one
 * moreover, I went over the code base and adapted the most relevant usages of
   ''placement-new into buffer'' to also include the `std::launder()` marker
2023-12-02 23:56:46 +01:00
229541859d Chain-Load: demonstrate use of reduction rule
... special rule to generate a fixed expansion on each seed
... consecutive reductions join everything back into one chain
... can counterbalance expansions and reductions
2023-11-30 03:20:23 +01:00
aafd277ebe Chain-Load: rework the pattern for dynamic rules
...as it turns out, the solution embraced first was the cleanest way
to handle dynamic configuration of parameters; just it did not work
at that time, due to the reference binding problem in the Lambdas.
Meanwhile, the latter has been resolved by relying on the LazyInit
mechanism. Thus it is now possible to abandon the manipulation by
side effect and rather require the dynamic rule to return a
''pristine instance''.

With these adjustments, it is now possible to install a rule
which expands only for some kinds of nodes; this is used here
to crate a starting point for a **reduction rule** to kick in.
2023-11-30 02:13:39 +01:00
3d5fdce1c7 Chain-Load: demonstrate use of the expansion rule
...played a lot with the parameters
...behaviour and DOT graphs look plausible
...document three typical examples with statistics
2023-11-29 02:58:55 +01:00
a780d696e5 Chain-Load: verify connectivity and recalculation
It seams indicated to verify the generated connectivity
and the hash calculation and recalculation explicitly
at least for one example topology; choosing a topology
comprised of several sub-graphs, to also verify the
propagation of seed values to further start-nodes.

In order to avoid addressing nodes directly by index number,
those sub-graphs can be processed by ''grouping of nodes'';
all parts are congruent because topology is determined by
the node hashes and thus a regular pattern can be exploited.

To allow for easy processing of groups, I have developed a
simplistic grouping device within the IterExplorer framework.
2023-11-27 21:58:37 +01:00
619a5173b0 Chain-Load: handle node seed and recalculation
- with the new pruning option, start-Nodes can now be anywhere
- introduce predicates to detect start-Nodes and exit-Nodes
- ensure each new seed node gets the global seed on graph construction
- provide functionality to re-propagate a seed and clear hashes
- provide functionality to recalculate the hashes over the graph
2023-11-26 22:28:12 +01:00
5af2279271 Chain-Load: ability to inject further shuffling
up to now, random values were completely determined by the
Node's hash, leading to completely symmetrical topology.
This is fine, but sometimes additional randomness is desirable,
while still keeping everything deterministic; the obvious solution
is to make the results optionally dependent on the invocation order,
which is simply to achieve with an additional state field. After some
tinkering, I decided to use the most simplistic solution, which is
just a multiplication with the state.
2023-11-26 19:46:48 +01:00
dbe71029b7 Chain-Load: now able to define RandomDraw rules
...all existing tests reproduced
...yet notation is hopefully more readable

Old:
  graph.expansionRule([](size_t h,double){ return Cap{8, h%16, 63}; })

New:
  graph.expansionRule(graph.rule().probability(0.5).maxVal(4))
2023-11-26 03:04:59 +01:00
f1c156b4cd Chain-Load: lazy init of functional configuration now complete
...so this was yet another digression, caused by the desire
somehow to salvage this problematic component design. Using a
DSL token fluently, while internally maintaining a complex and
totally open function based configuration is a bit of a stretch.
2023-11-25 23:47:20 +01:00
659441fa88 Chain-Load: verify (and bugfix) 2023-12-03 04:59:18 +01:00
ed8d9939bd Chain-Load: provide a scheme for repeated init
For context: I've engaged into writing a `LazyInit` helper component,
to resolve the inner contradiction between DSL use of `RandomDraw`
(implying value semantics) and the design of a processing pipeline,
which quite naturally leads to binding by reference into the enclosing
implementation.

In most cases, this change (to lazy on-demand initialisation) should be
transparent for the complete implementation code in `RandomDraw` -- with
one notable exception: when configuring an elaborate pipeline, especially
with dynamic changes of the probability profile during the simulation run,
then then obviously there is the desire to use the existing processing
pipeline from the reconfiguration function (in fact it would be quite
hard to explain why and where this should be avoided). `LazyInit` breaks
this usage scenario, since -- at the time the reconfiguration runs --
now the object is not initialised at all, but holds a »Trojan« functor,
which will trigger initialisation eventually.

After some headaches and grievances (why am I engaging into such an
elaborate solution for such an accidental and marginal topic...),
unfortunately it occurred to me that even this problem can be fixed,
with yet some further "minimal" adjustments to the scheme: the LazyInit
mechanism ''just needs to ensure'' that the init-functor ''sees the
same environment as in eager init'' -- that is, it must clear out the
»Trojan« first, and it ''could apply any previous pending init function''
fist. That is, with just a minimal change, we possibly build a chain
of init functors now, and apply them in given order, so each one
sees the state the previous one created -- as if this was just
direct eager object manipulation...
2023-12-03 04:59:18 +01:00
04ca79fd65 Chain-Load: verify re-initialisation and copy
...this is a more realistic demo example, which mimics
some of the patterns present in RandomDraw. The test also
uses lambdas linking to the actual storage location, so that
the invocation would crash on a copy; LazyInit was invented
to safeguard against this, while still allowing leeway
during the initialisation phase in a DSL.
2023-12-03 04:59:18 +01:00
e95f729ad0 Chain-Load: verify simple usage of LazyInit
...turns out I'd used the wrong Opaque buffer component;
...but other than that, the freaky mechanism seems to work
2023-12-03 04:59:18 +01:00
c658512d7b Chain-Load: verify building blocks of lazy-init 2023-12-03 04:59:18 +01:00
b00f4501a3 Chain-Load: draft the lazy-init mechanism
...oh my.
This is getting messy. I am way into danger territory now....
I've made a nifty cool design with automatically adapted functors;
yet at the end of the day, this does not bode well with a DSL usage,
where objects appear to be simple values from a users point of view.
2023-12-03 04:59:18 +01:00
8de3fe21bb Chain-Load: detect small-object optimisation
- Helper function to find out of two objects are located
  "close to each other" -- which can be used as heuristics
  to distinguish heap vs. stack storage

- further investigation shows that libstdc++ applies the
  small-object optimisation for functor up to »two slots«
  in size -- but only if the copy-ctor is trivial. Thus
  a lambda capturing a shared_ptr by value will *always*
  be maintained in heap storage (and LazyInit must be
  redesigned accordingly)...

- the verify_inlineStorage() unit test will now trigger
  if some implementation does not apply small-object optimisation
  under these minimal assumptions
2023-12-03 04:59:18 +01:00
3c713a4739 Chain-Load: invent the heart of the trap-mechanism
...the intention is to plant a »trojan lambda« into the target functor,
to set off initialisation (and possibly relocation) on demand.
2023-12-03 04:59:18 +01:00
1892d1beb5 Chain-Load: safety problems with rule initialisation
the RandomDraw rules developed last days are meant to be used
with user-provided λ-adapters; employing these in a context
of a DSL runs danger of producing dangling references.

Attempting to resolve this fundamental problem through
late-initialisation, and then locking the component into
a fixed memory location prior to actual usage. Driven by
the goal of a self-contained component, some advanced
trickery is required -- which again indicates better
to write a library component with adequate test coverage.
2023-12-03 04:59:18 +01:00
8b1326129a Library: RandomDraw - implementation complete and tested. 2023-12-03 04:59:17 +01:00
3808166494 Library: RandomDraw - invent new scheme for dynamic configuration
...now using the reworked partial-application helper...
...bind to *this and then recursively re-invoke the adaptation process
...need also to copy-capture the previously existing mapping-function

first test seems to work now
2023-12-03 04:59:17 +01:00
32b740cd40 Library: RandomDraw - dynamic configuration requires partial application
Investigation in test setup reveals that the intended solution
for dynamic configuration of the RandomDraw can not possibly work.
The reason is: the processing function binds back into the object instance.
This implies that RandomDraw must be *non-copyable*.

So we have to go full circle.
We need a way to pass the current instance to the configuration function.
And the most obvious and clear way would be to pass it as function argument.
Which however requires to *partially apply* this function.

So -- again -- we have to resort to one of the functor utilities
written several years ago; and while doing so, we must modernise
these tools further, to support perfect forwarding and binding
of reference arguments.
2023-12-03 04:59:17 +01:00
75cbfa8991 Library: RandomDraw - adaptor and mapping functions
...the beautiful thing with functions and Metaprogramming is:
it mostly works as designed out of the box, once you make it
past the Compiler.
2023-11-22 04:26:22 +01:00
2578df7c1d Library: RandomDraw - verify numerics (II)
- strive at complete branch coverage for the mapping function
- decide that the neutral value can deliberately lie outside
  the value range, in which case the probability setting
  controls the number of _value_ result incidents vs
  neutral value result incidents.
- introduce a third path to define this case clearly
- implement the range setting Builder-API functions
- absorb boundrary and illegal cases
2023-11-22 02:36:34 +01:00
4f28e8ad6c Library: RandomDraw - verify numerics (I)
- use a Draw with only a few values
- but with an origin within the value range
- verify stepping and distributions for various probabilities
2023-11-21 22:07:51 +01:00
bdb2f12b80 Library: RandomDraw - use dynamic quantiser
For sake of simplicity, since this whole exercise is a byproduct,
the mapping calculations are done in doubles. To get even distribution
of values and a good randomisation, it is thus necessary to break
down the size_t hash value in a first step (size_t can be 64bit
and random numbers would be subject to rounding errors otherwise)

The choice of this quantiser is tricky; it must be a power of two
to guarantee even distribution, and if chosen to close to the grid
of the result values, with lower probabilities we'd fail to cover
some of the possible result values.  If chosen to large, then
of course we'd run danger of producing correlated numbers on
consecutive picks.

Attempting to use 4 bits of headroom above the log-2 of the
required value range. For example, 10-step values would use
a quantiser of 128, which looks like a good compromise.
The following tests will show how good this choice holds up.
2023-11-21 19:50:22 +01:00
418a5691ea Library: relocate integer-log2 and make it constexpr
This highly optimised function was introduced about one year ago
for handling of denomals with rational values (fractions), as
an interim solution until we'll switch to C++20.

Since this function uses an unrolled loop and basically
just does a logarithmic search for the highest set bit,
it can just be declared constexpr. Moreover, it is now
relocated into one of the basic utility headers

Remark: the primary "competitor" is the ilogb(double),
which can exploit hardware acceleration. For 64bit integers,
the ilog2() is only marginally faster according to my own
repeated invocation benchmarks.
2023-11-21 19:39:18 +01:00
5b9a463b38 Library: RandomDraw - rework mapping rule to support origin
The first step was to allow setting a minimum value,
which in theory could also be negative (at no point is the
code actually limited to unsigned values; this is rather
the default in practice).

But reconsidering this extensions, then you'd also want
the "neutral value" to be handled properly. Within context,
this means that the *probability* controls when values other
than the neutral value are produced; especially with p = 1.0
the neutral value shall not be produced at all
2023-11-21 17:49:50 +01:00
75dd4210f2 Library: RandomDraw - must accept generic arguments
...since the Policy class now defines the function signature,
we can no longer assume that "input" is size_t. Rather, all
invocations must rely on the generic adaptaion scheme.

Getting this correct turns out rather tricky again;
best to rely on a generic function-composition.

Indeed I programmed such a helper several years ago,
with the caveat that at that time we used C++03 and
could not perfect-forward arguments. Today this problem
can be solved much more succinct using generic Lambdas.
2023-11-21 04:07:30 +01:00
651e28bac9 Library: RandomDraw - introduce policy template
to define this as a generic library component,
any reference to the actual data source moust be extracted
from the body of the implementation and supplied later
at usage site. In the actual case at hand the source
for randomness would be the node hash, and that is
absolutely an internal implementation detail.
2023-11-20 21:05:18 +01:00
605c1b4a17 Library: RandomDraw - consolidate prototype
...still same functionality as established yesterday in experimentation (try.cpp)
2023-11-20 18:49:00 +01:00
e5f5953b15 Library: RandomDraw - extract as generic component
The idea is to use some source of randomness to pick a
limited parameter value with controllable probability.
While the core of the implementation is nothing more
than some simple numeric adjustments, these turn out
to be rather intricate and obscure; the desire to
package these technicalities into a component
however necessitates to make invocations
at usage site self explanatory.
2023-11-20 16:38:55 +01:00
cc56117574 Chain-Load: integrate topology visualisation (DOT)
- provide as ''operator'' on the TestChainLink instance
- show shortened Node-Hash as label on each Node
2023-11-16 18:42:36 +01:00
76f250a5cf Library: extract Graphviz-DOT generation helpers
...these were developed driven by the immediate need
to visualise ''random generated computation patterns''
for ''Scheduler load testing.''

The abstraction level of this DSL is low
and structures closely match some clauses of the DOT language;
this approach may not yet be adequate to generate more complex
graph structures and was extracted as a starting point
for further refinements....
2023-11-16 17:20:36 +01:00
1c4b1a2973 Chain-Load: draft - generate DOT diagram from calculation topology
With all the preceding DSL work, this turns out to be surprisingly easy;
the only minor twist is the grouping of nodes into (time)levels,
which can be achieved with a "lagging" update from the loop body

Note: next step will be to extract the DSL helpers into a Library header
2023-11-16 17:19:29 +01:00
3135887914 Scheduler: connect BlockFlow capacity announcement
...refine the handling of FrameRates close to the definition bounds
...implement the actual rule to scale allocator capacity on announcement
...hook up into the seedCalcStream() with a default of +25FPS

+ test coverage
2023-11-10 23:52:20 +01:00
a2a960f544 Scheduler: look for ways to propagate a capacity-hint
...whenever a new CalcStream is seeded, it would be prudent
not only to step up the WorkForce (which is already implemented),
but also to provide a hint to the BlockFlow allocator regarding
the expected calculation density.

Such a hint would allow to set a more ample »epoch« spacing,
thereby avoiding to drive the allocator into overload first.
The allocator will cope anyway and re-balance in a matter of
about 2 seconds, but avoiding this kind of control oscillations
altogether will lead to better performance at calculation start.
2023-11-10 05:14:55 +01:00
7a22e7f987 Test: helper for transitory manipulations
Use a simple destructor-trick to set up a concise notation
for temporarily manipulating a value for testing.
The manipulation will automatically be undone
when leaving scope
2023-11-08 19:27:08 +01:00
3c3d31dd40 Library: ensure thread-ID is initialised at thread start
While testing, I repeatedly had SEGFAULT in the new thread-wrapper,
but only when running under debugger. While the language spec guarantees
that exit from the thread handle initialisation synchronizes-with
the start of the new thread, there is no guarantee in the reverse
direction. Here this means that the new thread may not see the
newly initialised thread handle ID at start. Thus I've added
a yield-wait at the very beginning of the new thread function.

Under normal conditions, the startup of a thread takes at least
100 - 500µs and thus I've never seen the problematic behaviour
without debugger. However, adding a yield-wait loop at that point
seems harmless (it typically checks back every 400ns or so).

All real usages of the thread wrapper in the application use
some kind of additional coordination or even a sync barrier
to ensure the thread can pick up all further data before
going into active work.

WARNING: if someone would detach() the thread immediately after
creating it, then this added condition would cause the starting
thread function to hang forever. In our current setup for the
thread wrapper, this is not possible, since the thread handle
is embedded into protected code. The earliest point you could
do that would be in the handle_begin_thread(), which is called
from the thread itself *after* the new check. And moreover,
this would require to write a new variation of the Policy.
2023-11-07 16:22:29 +01:00
8056bebf9c Scheduler: allow to manipulate nominal full capacity
While building increasingly complex integration tests for the Scheduler,
it turns out helpful to be able to manipulate the "full concurreency"
as used by Scheduler, WorkForce and LoadController.

In the current test, I am facing a problem that new entries from the
threadsafe entrance queue are not propagated to the priority queue
soon enough; partly this is due to functionality still to be added
(scaling up when new tasks are passed in) -- but this will further
complicate the test setup.
2023-11-07 16:12:56 +01:00
b49de0738d Scheduler: implement automatic clean-up of outdated entries
Hooked into the existing processing logic at Layer-2,
and relying on the information functions of Layer-1
2023-11-03 01:17:10 +01:00
6a7a2832bf Scheduler: simplify usage of microbenchmark helper
as an aside, the header lib/test/microbenchmark.hpp
turns out to be prolific for this kind of investigation.

However, it is somewhat obnoxious that the »test subject«
must expose the signature <size_t(size_t)>.

Thus, with some metaprogramming magic, an generic adaptor
can be built to accept a range of typical alternatives,
and even the quite obvious signature void(void).
Since all these will be wrapped directly into a lambda,
the optimiser will remove these adaptations altogether.
2023-10-30 20:17:16 +01:00
b5e9d67a79 Scheduler: wrap-up and comment test cases thus far
...up to now, Behaviour is as expected
- with some minor discrepancies still to be fixed
- and an effect due to the test-scaffolding
2023-10-27 03:37:24 +02:00
097001d16f Scheduler: investigate timings of dispatch()
...there seemed to be an anomaly of 50...100µs

==> conclusion: this is due to the instrumentation code
    - it largely caused by the EventLog, which was never meant
      to be used in performance-critical code, and does hefty
      heap allocations and string processing.
    - moreover, there clearly is a cache-effect, adding a Factor 2
      whenever some time passed since the last EventLog call

==> can be considered just an artifact of the test setup and
    will have no impact on the scheduler


remark: this commit adds a lot of instrumentation code
2023-10-27 02:53:34 +02:00
a71bcaae43 Scheduler: shorthand notation for work-Function test
To cover the visible behaviour of the work-Function,
we have to check an amalgam of timing delays and time differences.

This kind of test tends to be problematic, since timings are always
random and also machine dependent, and thus we need to produce pronounced effects
2023-10-26 01:14:13 +02:00
5164ead929 Scheduler: access invocation time for test
...find a way to sneak out the "now" parameter passed on Invocation
...this is prerequisite to demonstrate expected behaviour of the work-Function
2023-10-25 23:40:47 +02:00
b61ca94ee5 Scheduler: rectify λ-post API
...to bring it more in line with all the other calls dealing with Activity*
...allows also to harmonise the ActivityLang::dispatchChain()
...and to compose the calls in Scheduler directly

NOTE: there is a twist: our string-formatting helper did not render
custom string conversions for objects passed as pointer. This was a
long standing problem, caused by ambiguous templates overloads;
now I've attempted to solve it one level more down, in util::StringConv.
This solution may turn out brittle, since we need to exclude any direct
string conversion, most notably the ones for C-Strings (const char*)

In case this solution turns out unsustainable, please feel free
to revert this API change, and return to passing Activity& in λ-post,
because in the end this is cosmetics.
2023-10-23 01:48:46 +02:00
d67c62b02f Scheduler: solve difficulties with member function signature
The approach to provide the ExecutionCtx seems to work out well;
after some investigation I found a solution how to code a generic
signature-check for "any kind of function-like member"...

(the trick is to pass a pointer or member-pointer, which happens
to be syntactically the same and can be handled with our existing
function signature helper after some minor tweaks)
2023-10-22 00:42:57 +02:00
3af6a54219 Library/Application: complete technology switch (closes #1279)
As follow-up to the rework of thread-handling, likewise also
the implementation base for locking was switched over from direct
usage of POSIX primitives to the portable wrappers available in
the C++ standard library. All usages have been reviewed and
modernised to prefer λ-functions where possible.

With this series of changes, the old threadpool implementation
and a lot of further low-level support facilities are not used
any more and can be dismantled. Due to the integration efforts
spurred by the »Playback Vertical Slice«, several questions of
architecture could be decided over the last months. The design
of the Scheduler and Engine turned out different than previously
anticipated; notably the Scheduler now covers a wider array of
functionality, including some asynchronous messaging. This has
ramifications for the organisation of work tasks and threads,
and leads to a more deterministic memory management. Resource
management will be done on a higher level, partially superseding
some of the concepts from the early phase of the Lumiera project.
2023-10-16 01:44:04 +02:00
685be1b039 Library/Application: consolidate Monitor API and usage
This is Step-2 : change the API towards application

Notably all invocation variants to support member functions
or a reference to bool flags are retracted, since today a
λ-binding directly at usage site tends to be more readable.

The function names are harmonised with the C++ standard and
emergency shutdown in the Subsystem-Runner is rationalised.

The old thread-wrapper test is repurposed to demonstrate
the effectiveness of monitor based locking.
2023-10-15 20:42:55 +02:00