diff --git a/src/lib/typed-counter.hpp b/src/lib/typed-counter.hpp index 12a9337e4..da79383e0 100644 --- a/src/lib/typed-counter.hpp +++ b/src/lib/typed-counter.hpp @@ -230,7 +230,7 @@ namespace lib { friend string operator+ (string const& prefix, FamilyMember id) { - return prefix+id; + return prefix+string(id); } friend string diff --git a/tests/core/proc/control/session-command-function-test.cpp b/tests/core/proc/control/session-command-function-test.cpp index 70d2b6a76..195c66b6a 100644 --- a/tests/core/proc/control/session-command-function-test.cpp +++ b/tests/core/proc/control/session-command-function-test.cpp @@ -21,6 +21,58 @@ * *****************************************************/ +/** @file session-command-function-test.cpp + ** Function(integration) test of command dispatch into session thread. + ** This is a test combining several components to operate similar as in the real application, + ** while still relying upon an unit-test like setup. The goal is to cover how _session commands_ + ** are issued from an access point (CoreService) in the UI backbone, passed on through an + ** abstraction interface (the SessionCommand facade), handed over to the ProcDispatcher, + ** which, running within a dedicated thread (the »session loop thread«), enqueues all + ** these commands and dispatches them one by one. + ** + ** # the test operation + ** This test setup defines a specifically rigged _test command,_ which does not actually + ** operate on the session. Instead, it performs some time calculations and adds the resulting + ** time offset to a global variable, which can be observed from the test methods. The generated + ** values are controlled by the command arguments and thus predictable, which allows to verify + ** the expected number of invocations happened, using the right arguments. + ** + ** # massively multithreaded stress test + ** The last test case performs a massively multithreaded _torture test_ to scrutinise the sanity + ** of locking and state management. It creates several threads, each of which produces several + ** instances of the common _test command_ used in this test, and binds each instance with different + ** execution arguments. All these operations are sent as command messages, interspersed with short + ** random pauses, which causes them to arrive in arbitrary order within the dispatcher queue. + ** Moreover, while the "command producer threads" are running, the main thread temporarily + ** disables command dispatch, which causes the command queue to build up. After re-enabling + ** dispatch, the main thread spins to wait for the queue to become empty. The important + ** point to note is that the test command function itself _contains no locking._ But since + ** all command operations are triggered in a single dedicated thread, albeit in arbitrary + ** order, at the end the checksum must add up to the expected value. + ** + ** ## parametrisation + ** It is possible to change the actual setup with the following positional commandline arguments + ** - the number of threads to start + ** - the number of consecutive command instances produced in each thread + ** - the maximum delay (in µs) between each step in each thread + ** Astute readers might have noticed, that the test fixture is sloppy with respect to proper + ** locking and synchronisation. Rather, some explicit sleep commands are interspersed in a way + ** tuned to work satisfactory in practice. This whole approach can only work, because each + ** Posix locking call actually requires the runtime system to issue a read/write barrier, + ** which are known to have global effects on the relevant platforms (x86 and x86_64). + ** And because the production relevant code in ProcDispatcher uses sufficient (in fact + ** even excessive) locking, the state variables of the test fixture are properly synced + ** by sideeffect. + ** + ** This test case can fail when, by bad coincidence, the command queue is temporarily emptied, + ** while some producer threads are still alive -- because in this case the main thread might + ** verify the checksum before all command instances have been triggered. To avoid this + ** situation, make sure the delay between actions in the threads is not too long and + ** start a sufficiently high nubmer of producer threads. + ** + */ + + #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" extern "C" { @@ -33,7 +85,7 @@ extern "C" { #include "gui/interact/invocation-trail.hpp" #include "backend/thread-wrapper.hpp" #include "lib/typed-counter.hpp" -//#include "lib/format-cout.hpp" //////////TODO +#include "lib/format-string.hpp" #include "lib/symbol.hpp" #include "lib/util.hpp" @@ -47,8 +99,6 @@ namespace control { namespace test { -// using std::function; -// using std::rand; using boost::lexical_cast; using lib::test::randTime; using gui::interact::InvocationTrail; @@ -62,18 +112,20 @@ namespace test { using lib::time::FSecs; using lib::FamilyMember; using lib::Symbol; + using util::_Fmt; using util::isnil; using std::string; using std::vector; + using std::rand; namespace { // test fixture... /* === parameters for multi-threaded stress test === */ - uint NUM_THREADS_DEFAULT = 20; ///< @note _not_ const, can be overridden by command line argument + uint NUM_THREADS_DEFAULT = 50; ///< @note _not_ const, can be overridden by command line argument uint NUM_INVOC_PER_THRED = 10; - uint MAX_RAND_DELAY_ms = 10; + uint MAX_RAND_DELAY_us = 50; ///< @warning be sure to keep this way shorter than the delay in the main thread void maybeOverride (uint& configSetting, Arg cmdline, uint paramNr) @@ -113,7 +165,7 @@ namespace test { }//(End) test fixture -#define __DELAY__ usleep(10000); +#define __DELAY__ usleep(20000); @@ -196,6 +248,7 @@ namespace test { bool thread_has_ended{false}; + /** @test verify the »session loop thread« has finished properly */ void stopDispatcher() { @@ -208,6 +261,7 @@ namespace test { } + /** @test demonstrate a simple direct invocation */ void perform_simpleInvocation() { @@ -227,10 +281,11 @@ namespace test { } + /** @test invoke a command in the same way as CoreService does * when handling command messages from the UI-Bus * - use the help of an InvocationTrail, similar to what the - * [generic UI element](\ref gui::model::Tangible) does + * [generic UI element](\ref gui::model::Tangible) does * - generate a argument binding message * - generate a "bang!" message */ @@ -261,12 +316,13 @@ namespace test { __DELAY__ CHECK (Command::canUndo(COMMAND_I2)); - CHECK (testCommandState - prevState == Time(500, 1)); // execution added 2500ms -2*500ms == 1.5sec + CHECK (testCommandState - prevState == Time(FSecs(3,2))); // execution added 2500ms -2*500ms == 1.5sec } - /** @test verify that commands are properly enqueued - * and executed one by one + + /** @test massively multithreaded _torture test_ to verify + * that commands are properly enqueued and executed one by one * - create several threads to send random command messages * - verify that, after executing all commands, the internal * state variable reflects the result of a proper @@ -277,25 +333,51 @@ namespace test { { maybeOverride(NUM_THREADS_DEFAULT, args_for_stresstest, 1); maybeOverride(NUM_INVOC_PER_THRED, args_for_stresstest, 2); - maybeOverride(MAX_RAND_DELAY_ms, args_for_stresstest, 3); + maybeOverride(MAX_RAND_DELAY_us, args_for_stresstest, 3); + + // we'll run several instances of the following thread.... class InvocationProducer : backend::ThreadJoinable { FamilyMember id_; - string id_buffer_{COMMAND_ID + ".thread-"+id_}; - Symbol cmdID_{cStr(id_buffer_)}; + vector cmdIDs_; + Symbol + cmdID(uint j) + { + cmdIDs_.push_back (_Fmt("%s.thread-%02d.%d") % COMMAND_ID % id_ % j); + return cStr(cmdIDs_.back()); + } + + + public: + InvocationProducer() + : ThreadJoinable("test command producer", [&](){ fabricateCommands(); }) + { + this->sync(); + } + + ~InvocationProducer() + { + this->join().maybeThrow(); + for (auto& id : cmdIDs_) + Command::remove (cStr(id)); + } + + private: void fabricateCommands() { - syncPoint(); - InvocationTrail invoTrail{Command(cmdID_)}; + syncPoint(); // barrier to ensure initialisation of the object - for (uint i=0; isync(); - } - - ~InvocationProducer() - { - this->join().maybeThrow(); - Command::remove (cmdID_); + if (not MAX_RAND_DELAY_us) return; + usleep (1 + rand() % MAX_RAND_DELAY_us); // random delay varying in steps of 1µs } }; + + /* == controlling code in main thread == */ Time prevState = testCommandState; // fire up several threads to issue commands in parallel... vector producerThreads{NUM_THREADS_DEFAULT}; - FSecs expectedOffset{0}; for (uint i=0; i -
+
All communication between Proc-Layer and GUI has to be routed through the respective LayerSeparationInterfaces. Following a fundamental design decision within Lumiera, these interface are //intended to be language agnostic// &mdash; forcing them to stick to the least common denominator. Which creates the additional problem of how to create a smooth integration without forcing the architecture into functional decomposition style. To solve this problem, we rely on ''messaging'' rather than on a //business facade// -- our facade interfaces are rather narrow and limited to lifecycle management. In addition, the UI exposes a [[notification facade|GuiNotificationFacade]] for pushing back status information created as result of the edit operations, the build process and the render tasks.
 
 !anatomy of the Proc/GUI interface
-* the GuiFacade is used as a general lifecycle facade to start up the GUI and to set up the LayerSeparationInterfaces.
-* GuiFacade is implemented by a class //in core// and exposes a notification proxy implementing GuiNotificationFacade, which actually is wired through the InterfaceSystem to forward to the corresponding implementation facade object within the GUI
-* similarly, starting the fundamental subsystems within proc installs Interfaces into the InterfaceSystem and exposes them through proxy objects.<br/>{{red{TODO 12/2016}}} there shall be an interface to [[invoke commands|CommandHandling]]...
+* the GuiFacade is used as a general lifecycle facade to start up the GUI and to set up the LayerSeparationInterfaces.<br/>It is implemented by a class //in core// and loads the Lumiera ~GTK-UI as a plug-in.
+* once the UI is running, it exposes the GuiNotificationFacade, to allow pushing state and structure updates up into the user interface.
+* in the opposite direction, for initiating actions from the UI, the SessionSubsystem opens the SessionCommandFacade, which can be considered //"the" public session interface.//
 
 !principles of UI / Proc interaction
 By all means, we want to avoid a common shared data structure as foundation for any interaction. For a prominent example, have a look at [[Blender|https://developer.blender.org]] to see where this leads; //such is not bad,// but it limits to a very specific kind of evolution. //We are aiming for less and for more.// Fuelled by our command and UNDO system, and our rules based [[Builder]] with its asynchronous responses, we came to rely on a messaging system, known as the [[UI-Bus]].
@@ -5230,11 +5230,11 @@ Besides, they provide an __inward interface__ for the [[ProcNode]]s, enabling th
 [img[Asset Classess|uml/fig131077.png]]
 {{red{Note 3/2010}}} it is very unlikely we'll organise the processing nodes as a class hierarchy. Rather it looks like we'll get several submodules/special capabilities configured in within the Builder
-
+
//The guard and coordinator of any operation within the session subsystem.//
 The session and related components work effectively single threaded. Any tangible operation on the session data structure has to be enqueued as [[command|CommandHandling]] into the dispatcher. Moreover, the [[Builder]] is triggered from the ProcDispatcher; and while the Builder is running, any command processing is halted. The Builder in turn creates or reshapes the processing nodes network, and the changed network is brought into operation with a //transactional switch// -- while render processes on this processing network operate unaffected and essentially multi-threaded.
 
-Enqueueing commands through the {{{SessionCommandFacade}}} into the ProcDispatcher is the official way to cause changes to the session. And the running state of the ProcDispatcher is equivalent with the running state of the //session subsystem as a whole.//
+Enqueueing commands through the SessionCommandFacade into the ProcDispatcher is the official way to cause changes to the session. And the running state of the ProcDispatcher is equivalent with the running state of the //session subsystem as a whole.//
 
 !Requirements
 To function properly as action coordinator of the session subsystem, the dispatcher has to fulfil multiple demands
@@ -6138,6 +6138,11 @@ The Session object is a singleton &mdash; actually it is a »~PImpl«-Facade
 The session lifecycle need to be distinguished from the state of the [[session subsystem|SessionSubsystem]]. The latter is one of the major components of Lumiera, and when it is brought up, the {{{SessionCommandFacade}}} is opened and the ProcDispatcher started. On the other hand, the session as such is a data structure and pulled up on demand, by the {{{SessionManager}}}. Whenever the session is fully populated and configured, the ProcDispatcher is instructed to //actually allow dispatching of commands towards the session.// This command dispatching mechanism is the actual access point to the session for clients outside Proc-Layer; when dispatching is halted, commands can be enqueued non the less, which allows for a reactive UI.
 
+
+
LayerSeparationInterface, provided by the Proc-Layer.
+The {{{SessionCommand}}} façade and the corresponding {{{proc::control::SessionCommandService}}} can be considered //the public interface to the session://
+They allow to send [[commands|CommandHandling]] to work on the session data structure. All these commands, as well as the [[Builder]], are performed in a dedicated thread, the »session loop thread«, which is operated by the ProcDispatcher. As a direct consequence, all mutations of the session data, as well as all logical consequences determined by the builder, are performed single-threaded, without the need to care for synchronisation issues. Another consequence of this design is the fact that running the builder disables session command processing, causing further commands to be queued up in the ProcDispatcher. Any structural changes resulting from builder runs will finally be pushed back up into the UI, asynchronously.
+
While the core of the persistent session state corresponds just to the HighLevelModel, there is additionaly attached state, annotations and specific bindings, which allow to connect the session model to the local application configuration on each system. A typical example would be the actual output channels, connections and drivers to use on a specific system. In a Studio setup, these setup and wiring might be quite complex, it may be specific to just a single project, and the user might want to work on the same project on different systems. This explains why we can't just embody these configuration information right into the actual model.
@@ -6179,19 +6184,24 @@ The session and the models rely on dependent objects beeing kept updated and con &rarr; see [[details here...|ModelDependencies]]
-
-
"Session Interface", when used in a more general sense, denotes a compound of several interfaces and facilities, together forming the primary access point to the user visible contents and state of the editing project.
-* the API of the session class
-* the accompanying management interface (SessionManager API)
-* the primary public ~APIs exposed on the objects to be [[queried and retrieved|SessionStructureQuery]] via the session class API
-** [[Timeline]]
-** [[Sequence]]
-** [[Placement]]
-** [[Clip]]
-** [[Fork]]
-** Effect
-** Automation
-* the [[command|CommandHandling]] interface, including the [[UNDO|UndoManager]] facility
+
+
"Session Interface" has several meanings, depending on the context.
+;application global
+:the session is a data structure, which can be saved and loaded, and manipulated by [[sending commands|CommandHandling]]
+;within ~Proc-Layer
+:here »the session« can be seen as a compound of several interfaces and facilities,
+:together forming the primary access point to the user visible contents and state of the editing project.
+:* the API of the session class
+:* the accompanying management interface (SessionManager API)
+:* the primary public ~APIs exposed on the objects to be [[queried and retrieved|SessionStructureQuery]] via the session class API
+:** [[Timeline]]
+:** [[Sequence]]
+:** [[Placement]]
+:** [[Clip]]
+:** [[Fork]]
+:** Effect
+:** Automation
+:* the [[command handling framework|CommandHandling]], including the [[UNDO|UndoManager]] facility
 
 __Note__: the SessionInterface as such is //not a [[external public interface|LayerSeparationInterfaces]].// Clients from outside Proc-Layer can talk to the session by issuing commands through the {{{SessionCommandFacade}}}. Processing of commands is coordinated by the ProcDispatcher, which also is responsible for starting the [[Builder]].
 
@@ -6417,12 +6427,12 @@ And last but not least: the difficult part of this whole concept is encapsulated
 
 {{red{WIP ... draft}}}
-
+
//A subsystem within Proc-Layer, responsible for lifecycle and access to the editing [[Session]].//
 [img[Structure of the Session Subsystem|uml/Session-subsystem.png]]
 
 !Structure
-The ProcDispatcher is at the heart of the //Session Subsystem.// Because the official interface for working on the session, the {{{SessionCommand}}} façade, is expressed in terms of sending command messages to invoke predefined [[commands|CommandHandling]] to operate on the SessionInterface, the actual implementation of such a {{{SessionCommandService}}} needs a component actually to enqueue and dispatch those commands -- which is the {{{DispatcherLoop}}} within the ProcDispatcher. As usual, the ''lifecycle'' is controlled by a subsystem descriptor, which starts and stops the whole subsystem; and this starting and stopping in turn translates into starting/stopping of the dispatcher loop. On the other hand, //activation of the dispatcher,// which means actively to dispatch commands, is controlled by the lifecycle of the session proper. The latter is just a data structure, and can be loaded / saved and rebuilt through the ''session manager''.
+The ProcDispatcher is at the heart of the //Session Subsystem.// Because the official interface for working on the session, the [[SessionCommand façade|SessionCommandFacade]], is expressed in terms of sending command messages to invoke predefined [[commands|CommandHandling]] to operate on the SessionInterface, the actual implementation of such a {{{SessionCommandService}}} needs a component actually to enqueue and dispatch those commands -- which is the {{{DispatcherLoop}}} within the ProcDispatcher. As usual, the ''lifecycle'' is controlled by a subsystem descriptor, which starts and stops the whole subsystem; and this starting and stopping in turn translates into starting/stopping of the dispatcher loop. On the other hand, //activation of the dispatcher,// which means actively to dispatch commands, is controlled by the lifecycle of the session proper. The latter is just a data structure, and can be loaded / saved and rebuilt through the ''session manager''.
 
 !Lifecycle
 As far as lifecycle is concerned, the »session subsystem« has to be distinguished from the //session proper,// which is just a data structure with its own, separate lifecycle considerations. Accessing the session data only makes sense when this data structure is fully loaded, while the //session subsystem,// deals with performing commands on the session and with triggering the builder runs.
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm
index 3848fc47e..38d792372 100644
--- a/wiki/thinkPad.ichthyo.mm
+++ b/wiki/thinkPad.ichthyo.mm
@@ -199,7 +199,7 @@
 
 
 
-
+
 
   
     
@@ -210,6 +210,7 @@
     

+ @@ -407,7 +408,7 @@
- + @@ -684,7 +685,7 @@ - + @@ -699,14 +700,15 @@ - - - + + + - + + @@ -908,7 +910,7 @@ - + @@ -931,22 +933,38 @@ - - + + - - + + + + + + + + + + + + + - - + + + + + + + @@ -958,6 +976,13 @@ + + + + + + + @@ -11520,7 +11545,8 @@ - + + @@ -11530,6 +11556,9 @@ + + + @@ -11989,7 +12018,9 @@ - + + +