TODO: Infos zusammentragen und dokumentieren

AUA: Henne oder Ei?

denn:

  • Nexus verwendet CoreService als "upstream", um alle sonstigen Nachriten dorthin zuzustellen
  • CoreService hat Nexus als Upstream, um mit dem restlichen System kommunizieren zu können

gemeint ist: im ctor

Es speichert nur die Referenz

Ganz anders Model::Tangible: dieses registriert sich bei der Konstruktion

oder anders herum,

aber so herum macht es mehr Sinn

CoreService hat keine volle Bus-Connection

...das hab ich mir jetzt explizit so überlegt und es ist sinnvoll.

Nur ein Tangible kann eine volle Bus-Connection haben, und das heißt,

es kann downlink-Nachrichten bekommen. Dagegen hat CoreService lediglich ein "freistehendes"

BusTerm, das damit Nachrichten an den Nexus schicken kann.

UI: GuiNotification

4/18 inzwischen hier alles geklärt.

Wartet nur noch auf proof-of-concept (DemoGuiRoundtrip)

...hängt am UI-Bus,

aber nur via einfacher "uplink"-Verbindung

Sicht "von unten"

...es könnte auch den Feedback des Users meinen

...weil es dadurch passieren könnte,

daß die Konstruktion des GuiRunners schon scheitert, bevor der Rumpf des ctors aufgerufen wird.

In einem solchen Fall wird leider auch der Rumpf des dtors nicht aufgerufen, wodurch das

Term-Signal nicht ausgesendet würde.

gemeint ist: keine volle bidirektionale Connection,

denn CoreService ist kein Tangible. Das macht Sinn so, habe darüber nachgedacht.

Anmerkung: ein "frestehendes" BusTerm ist valide und zugelassen, es hat halt nur eine uplink-Connection.

nur ein Tangible kann downlink-Nachrichten sinnvoll empfangen;

es muß dazu auch jede Menge Methoden implementieren.

GtkLumiera in Konstruktion

NotificationFacade noch nicht offen

müssen eigens aktiviert werden

zwar sind die Fasaden offen, bevor die Main-Loop läuft,

jedoch werden genau durch diese Fasaden alle Aktionen per Dispatch

in die GTK-Loop übertragen. Und das kann nur in der Loop selber  passieren.

Ein zu früher bzw. zu später Aufruf "fällt einfach hinten runter"

verbleibende Probleme sind "marginal" und besser im Subsystem-Runner zu addressieren

...weil unser Thread-Framework

tatsächlich erzwingt, daß der neue Thrad zu laufen beginnt, bevor die

startende Funktion zurückkehrt.

Daher können wir zumindest annehmen, daß die ganze Initialisierung

bereits läuft, wenn die start()-Funktion mit true (Erfolg) zurückkommt.

Allerdings ist definitiv ein Race gegeben, und wenn

direkt beim Starten anderer Subsysteme nach dem GUI etwas schiefläuft,

dann kann der Shutdown-Prozeß den Start des GUI überholen.

wirkt alles mehr oder weniger beliebig...

0000000937: ERR: core-service.hpp:111: worker_3: ~CoreService: Some UI components are still connected to the backbone.

Speicherzugriffsfehler

...muß diejenigen Bus-Verbindungen abziehen, die von Members dieser Klasse stammen

  • CoreService selber
  • der NotificationService

wenn ich per Value capture, dann gibts schon

beim Start des GUI einen SEGFAULT

Und noch schlimmer: im Debugger gibts keinen

die Closure eines Lambdas hängt am Kontext

konkret:

der Kontext ist hier nämlich ein anderes Lambda, das dem Aufruf des GUI-Plugins mitgegeben wurde.

Dagegen die alte Lösung erzeugte an dieser Stelle einen Bind-Ausdruck, und das war offenbar genug,

um nicht mehr von dem direkten Kontext abhängig zu sein, in dem der Thread gestartet wurde.

Denn dieser Kontext (auf dem Stack) ist natürlich lange schon weg, wenn der Thread

terminiert und dann tatsächlich den Fuktor aufrufen möchte

Debug: nur ein Element connected

weil die Abstraktion "UI-Element" eben grade

die UI-internen Framework-Aspekte ausklammert.

Die Elemente stellen eine Abbildung der Strukturen aus der Session dar,

und ihre "Methoden" sind Commands auf der Session!

muß eigens aktiviert werden

Fehlerlog-Anzeige vorläufig irgendwo....

...das ist nützlich zur Diagnose,

  • aber läßt sich das überhaupt auf IterSource übertragen?
  • war es überhaupt je gerechtferigt? zu starke Annahme über den Diff-Erzeuger!

ist schon schlimm genug....

...denn eigentlich geht es nur um ein einfaches Producer-Interface,

das einen Element-Pointer durchreicht. Das einzige Problem, das ich sehe,

ist, daß hier ownership übertragen wird.

nämlich

  • ein sehr theoretisches und anspruchsvolles Konzept
  • der Zwang, das auf jedem Empfänger umzusetzen
  • die hablseidene Trickserei mit der konkreten Puffergröße
  • den double-dispatch im Diff-Framework selber
  • das Variant-basierte GenNode-Framework

das wird sowiso ein Desaster

das heißt, die Diff-Implementierung muß länger leben

faktisch erfolgt somit ein Callback aus einem anderen Thread

um das Diff zu pullen

jede Facade-Funktion brauch einen Dispatcher

Das wird eine ganze Me

bevor die Facade geöffnet wir

und arbeitet asynchron

Argument-Storage

organisieren

brauche dedizierten Dispatcher

kann mich so vague erinnern

daß hier zwar schon ein catch eingebaut war,

aber noch irgend ein Hund begraben liegt

...und insofern ist auch die Behandlung einer Folge-Exception noch offen

Im Moment loggen wir nur ins textuelle Konsole-Log (NoBug)

Habe nochmal alle Aufrufe im Einzelnen durchgeprüft

  • wenn schon das Einfügen des Funktors vor dem Dispatch wirft (out of memory),
    dann bekommt der Aufrufer diese Exception, was typischerweise tödlich ist.
  • aber alles, was innerhalb dieses Funktors passiert, wird abgefangen und geloggt.

mehr erscheint mir nicht sinnvoll; behandeln kann man solche Fehler ohnehin nicht.

...könnte das am Ende nicht sinnvoll sein,

speziell den UI-Shutdown-Trigger über den neuen Mechanismus laufen zu lassen,

obwohl jener doch genau der Anlaß war, diesen neuen Mechanismus zu bauen.

wenn die Queue voll ist

wird erst alles Andere abgearbeitet

wenn UI-Thread blockt/verhungert,

kommt der rettende Shutdown gar nicht durch

das ist schon die endgültige Lösung

Schlußfolgerung: Wizzard wird ein Interface

in proc::asset::meta::theErrorLog_ID

...in den eigentlich kaputten DockManager

es ist schon bekannt, daß dieser Service der ViewLocator sein wird,

und daß man ihn via InteractionDirector erreicht

brauchen wir diese zwischengeschaltete Methode showMsg()?

wäre es nicht sinnvoller, sie direkt auf das API zu heben?

...ob beim Expand/Collapse das umschließende Widget resized werden soll

...wesentliches Struktur-Element:

der Parent-Container ist für das expand/collapse zuständig

als denjenigen Container...

...der das eigentliche Widget expandieren kann

ggfs müssen alle anderen Funktionen nach unten delegieren

wenn nämlich das "Expandieren" in einen weiter reichenden Zusammenhang integriert ist,

wie z.B. Darstellung in verschiedenen Detailierungs-Graden, oder ein generelles

Fokus-/Layout- Management mehrerer Komponenten zusammen

Standard-Beispiel: Property-Panel

...wenn wir eine Mix-in -Implementierung wählen

...um den Umstand zu dokumentieren,

daß hier ## Zeilen gelöscht wurden

erscheint definitiv nicht als CSS-Node

...das liegt vermutlich daran, daß ich die zweite Marke mit "right gravity" einfüge,

aber eben am absoluten Ende des Buffers. Damit wird sie hinter jeder Einfügung bleiben

...dann Einfügestelle davor platzieren

YAGNI

das Widget hat offensichtlich keine eigene graphische Repräsentation.

dieser Fall dürfte häufiger auftreten

ca 2014 finde ich einige Blog-Einträge, daß jetzt keine Theming-Engine mehr notwendig ist,

weil man alles per CSS machen kann. Seit Jessie findet GTK die Engine nicht mehr,

obwohl das Standard-Theme-Paket installiert ist.

Dummerweise funktioniert nun aber auch das Übersteuern mit unseren Farben nicht mehr

...welches ich zusammen mit meinem

normalen 'light' destkop-theme verwenden kann

denn dieses Design gibt die beste Balance zwischen den Belangen

  • kein zusätzlicher Speicherbedarf
  • Callback ist automatisch abgesichert (sigc::mem_fun auf Trackable)
  • kann direkt auf das Styling des Parent zugreifen

Einziger Nachteil ist die etwas verwirrende Schreibweise des dekorierten Typs

weil so sichergestellt ist, daß er stets existiert,

und er trotzdem vom konkreten Widget entkoppelt bleibt

besserer Name: NotificationHub

GTK-Konzepte: "Schließen"

wenn Log-Anzeige verworfen wird

er ist nämlich auch aufgerufen worden, nachdem die GTK-Loop bereits beendet war.

Dieser Code hat früher C-mäßig direkt die GObjects manipuliert (Anfangs hatten wir nur GDL, nicht GDLmm)

Erläuterung: DockItem ist ein 1-elementiger GTK-Container,

der sichtbare Inhalt liegt in diesem Container. Bei uns ist das das Panel.

Removes @widget from @container. @widget must be inside @container.

Note that @container will own a reference to @widget, and that this

may be the last reference held; so removing a widget from its     

container can destroy that widget. If you want to use @widget     

again, you need to add a reference to it before removing it from  

a container, using g_object_ref(). If you don’t want to use @widget

again it’s usually more efficient to simply destroy it directly   

using gtk_widget_destroy() since this will remove it from the     

container and help break any circular reference count cycles.     

das ist das Fazit meiner Untersuchung.

"destroy" ist kein relevantes Konzept für GTK. Es gibt nur show()/hide().

Also müssen wir explizit

  • eine Destroy-Signal bauen
  • dafür sorgen, daß das Infobox-Panel das auch aufruft

entweder sie laufen über den Getter,

oder sie prüfen den Zustand und machen nichts, wenn das Widget nicht da ist

...und nicht bloß inaktiv schalten.

GTK bietet nur Anzeigen von top-down:

  • show_all() auf Containern zeigt rekursiv alle Kinder
  • show() zeigt nur ein bestimmtes Widget

Lösung: delegiere über zwei Stufen...

Neue Revealer-Komponente bindet einen Funktor

  • Set error state.
  • Allocate Widget if necessary
  • expand widget
  • place error message into its buffer"

"If widget exists: expand it, trigger its flash function (paint with timeout).

TODO also doRevealYourself."

...weil der Payload-Typ für diesen Aufruf bool ist,

und nicht string wie für alle anderen state marks

k

Konzept ist nicht, sie gleich in die description zu übernehmen

das System ist ohnehin schon angetötet

keines! Impl verwendet strdup()

...denn wir verwenden das Lumiera-Exception-API gar nicht

der Handler im ProcDispatcher sorgt dafür

...welches keinen separaten Thread aufruft....?

nämlich im shutdownAll() weiter oben im Stack

aber genau der andere Thread, der das gemacht hat...

...dann sind wir sofort tot und das wird billigend in Kauf genommen.

So ist das tatsächlich implementiert im Falle vom OutputDirector.

Der startet nämlich einen Thread erst als Supervisor für den Shutdown.

Wenn das nun wirklich fehlschlägt, bleibt uns sinnvollerweise nur noch die Selbstmord-Option

ein ZombieCheck spricht an

0000001180: INFO: subsystem-runner.hpp:208: worker_3: sigTerm: Subsystem 'Lumiera GTK GUI' terminated.

0000001181: NOTICE: appstate.cpp:170: thread_1: maybeWait: Shutting down Lumiera...

0000001182: ALERT: appstate.cpp:174: thread_1: maybeWait: Triggering emergency exit...

0000001243: WARNING: interfaceregistry.c:199: thread_1: lumiera_interfaceregistry_bulkremove_interfaces: ENTRY NOT FOUND in interfaceregistry at clean-up of interface lumieraorg_Gui, instance lumieraorg_GuiStarterPlugin

(lumiera:9719): Gtk-CRITICAL **: gtk_widget_is_drawable: assertion 'GTK_IS_WIDGET (widget)' failed

(lumiera:9719): Pango-CRITICAL **: pango_layout_get_cursor_pos: assertion 'index >= 0 && index <= layout->length' failed

0000001293: ERR: error-exception.cpp:185: worker_3: lumiera_unexpectedException: ### Lumiera halted due to an unexpected Error ###

### Lumiera halted due to an unexpected Error ###

+++ Caught Exception LUMIERA_ERROR_LIFECYCLE:Lifecycle assumptions violated

0000001298: ERR: error-exception.cpp:196: worker_3: lumiera_unexpectedException: +++ caught error::LumieraError<error::LUMIERA_ERROR_FATAL, error::LumieraError<error::LUMIERA_ERROR_LOGIC, Error> >

0000001298! ERR: error-exception.cpp:196: worker_3: lumiera_unexpectedException: +++ messg: Sorry, Lumiera encountered an internal error.

0000001298! ERR: error-exception.cpp:196: worker_3: lumiera_unexpectedException: +++ descr: LUMIERA_ERROR_LIFECYCLE:Lifecycle assumptions violated (Already deceased object called out of order during Application shutdown. Lumiera Policy violated: Dependencies must not be used from destructors. Offender = ClassLock<gui::interact::LocationQuery, s).

0000001299: ERR: error-exception.cpp:212: worker_3: lumiera_unexpectedException: last registered error was....

0000001299! ERR: error-exception.cpp:212: worker_3: lumiera_unexpectedException: LUMIERA_ERROR_LIFECYCLE:Lifecycle assumptions violated

Abgebrochen

..damit ~GtkLumiera() garantiert vorher aufgerufen wird

0000001028: INFO: proc-dispatcher.cpp:301: worker_2: processCommands: +++ dispatch Command("test_meta_markAction") {exec}

0000001029: INFO: proc-dispatcher.cpp:306: worker_2: processCommands: +++ -------->>> bang!

0000001031: WARNING: handling-pattern.cpp:90: worker_2: invoke: Invocation of Command("test_meta_markAction") {exec} failed: bad lexical cast: source type value could not be interpreted as target

0000001052: INFO: proc-dispatcher.cpp:159: worker_2: ~DispatcherLoop: Proc-Dispatcher stopped.

0000001053: INFO: subsystem-runner.hpp:208: worker_2: sigTerm: Subsystem 'Session' terminated.

0000001054: WARNING: subsystem-runner.hpp:209: worker_2: sigTerm: Irregular shutdown caused by: LUMIERA_ERROR_LOGIC:internal logic broken (Command execution failed: LUMIERA_ERROR_EXTERNAL:failure in external service -- caused by: bad lexical cast: source type value could not be interpreted as target).

0000001090: TODO: output-director.cpp:136: worker_4: bringDown: actually bring down the output generation

0000001091: INFO: subsystem-runner.hpp:208: worker_2: sigTerm: Subsystem 'Dummy-Player' terminated.

0000001092: NOTICE: notification-service.cpp:158: worker_2: triggerGuiShutdown: @GUI: shutdown triggered with explanation 'Application shutdown'....

0000001111: NOTICE: notification-service.cpp:158: worker_2: triggerGuiShutdown: @GUI: shutdown triggered with explanation 'Application shutdown'....

0000001119: INFO: subsystem-runner.hpp:208: worker_4: sigTerm: Subsystem 'PlayOut' terminated.

0000001120: NOTICE: notification-service.cpp:158: worker_4: triggerGuiShutdown: @GUI: shutdown triggered with explanation 'Application shutdown'....

0000001128: INFO: display-service.hpp:147: worker_3: ~DisplayService: Display service dying...

(lumiera:12160): Gtk-CRITICAL **: gtk_widget_is_drawable: assertion 'GTK_IS_WIDGET (widget)' failed

(lumiera:12160): Pango-CRITICAL **: pango_layout_get_cursor_pos: assertion 'index >= 0 && index <= layout->length' failed

0000001160: INFO: workspace-window.cpp:53: worker_3: ~WorkspaceWindow: Closing workspace window...

(lumiera:12160): Gdl-CRITICAL **: gdl_dock_master_get_controller: assertion 'master != NULL' failed

(lumiera:12160): Gdl-CRITICAL **: gdl_dock_master_get_controller: assertion 'master != NULL' failed

(lumiera:12160): Gdl-CRITICAL **: gdl_dock_master_get_controller: assertion 'master != NULL' failed

(lumiera:12160): Gdl-CRITICAL **: gdl_dock_master_get_controller: assertion 'master != NULL' failed

0000001167: INFO: subsystem-runner.hpp:208: worker_3: sigTerm: Subsystem 'Lumiera GTK GUI' terminated.

0000001175: NOTICE: appstate.cpp:170: thread_1: maybeWait: Shutting down Lumiera...

0000001176: ALERT: appstate.cpp:174: thread_1: maybeWait: Triggering emergency exit...

0000001237: WARNING: interfaceregistry.c:199: thread_1: lumiera_interfaceregistry_bulkremove_interfaces: ENTRY NOT FOUND in interfaceregistry at clean-up of interface lumieraorg_Gui, instance lumieraorg_GuiStarterPlugin

(hiv)~/devel/lumi-$

(hiv)~/devel/lumi-$

(hiv)~/devel/lumi-$ echo $?

2

(hiv)~/devel/lumi-$

...und zwar an dem Punkt, an dem ich eine DiffMessage in das GUI schicke.

DiffMessage repräsentiert einen für den User verdeckten Callback

#1174 UI self diagnostics window

bisher können wir das GUI nur aktiv intern schließen,

indem wir ein GTK-Signal erzeugen, das das Hauptfenster schließt

...das war genau der Kern der "Plugin-Debatte".

Eine solche globale, flache, dynamisch gebundene Ebene

klingt nach wahnsinnigen Möglichkeiten, aber nur solange, bis man sich

eine einzige Funktion konkret durchdenkt: es läuft auf Spaghetti-Code hinaus

...indem der NotificatonService nun vom UI-Manager gemanaged wird :)

zieht komplett-Umbau

des Gui-top-Level nach sich

...nur eine heuristische Vermutung von mir

stützt sich auf folgenden Quellcode

Application::Application(const Glib::ustring& application_id, Gio::ApplicationFlags flags)

:

  // Mark this class as non-derived to allow C++ vfuncs to be skipped.

  //Note that GApplication complains about "" but allows NULL (0), so we avoid passing "".

  Glib::ObjectBase(0),

  Gio::Application(Glib::ConstructParams(custom_class_init(), "application_id", (application_id.empty() ? 0 : application_id.c_str()), "flags", GApplicationFlags(flags), static_cast<char*>(0))),

  m_argc(0),

  m_argv(0)

{

  gtk_init(0, 0);

}

Proc: SessionCommand

setzt aktivierten Dispatcher zwingend voraus

es genügt definitiv nicht, nur die Dispatcher-Komponente(Schnittstelle) erreichen zu können.

Jede Operation, die über dieses externe Interface bereitsteht, benötigt zur Implementierung

eine aktiv laufende Dispatcher-Queue.

Daher macht es Sinn, den Interface-Lebenszyklus ganz starr an den Disspatcher zu binden

...und zwar wirklich sehr implizit,

nämlich über die Identität (IDs) der Command-Parameter.

Das heißt, ein eingehendes Command paßt nur zu einer bestimmten Session-Instanz,

was zwar jederzeit (via statisches/internes Session-API) verifizierbar ist, jedoch nicht offensichtlich

das folgt einfach aus den logischen Eigenschaften der beteiligten Komponenten,

welche eben autonom sind.

Das heißt im Klartext, alle Clients müssen darauf vorbereitet sein, daß diese Schnittstelle

jederzeit wegbrechen kann, was dann heißt, daß irgend ein Aufruf eine Exception wirft

wer besitzt die

Implementierung

meint: zwei gekoppelte Statusvariable

muß alle Operationen durchschleifen

oder muß PImpl als Interface exponieren

meint: zwei gekoppelte Statusvariable

Shutdown tricky

Session-Subsystem implementieren (#318)

....das ist schon mehr ein Meta-Ticket,

und es hängt wohl zu viel darunter, um es gleich ganz abschließen zu können.

Aber ich akzeptiere es und verwende es jetzt als Treiber

ist nicht "die Session

das Lock sorgt hier für konsistenten Zustand und Sichtbarkeit (memory barrier)

Lock ist hier das Dispatcher-Lock

...wenn jemand zugreift

grundlegende Design-Enscheidung

  • wir haben Komponenten mit Dependency-Injection
  • da beide Komponenten nur nach ihren eigenen Hinsichten funktionieren,
    wird das System insgesamt einfacher

muß SessionCommandService schließen

bevor die Dispatcher-loop angehalten wird

1/2017 Review durchgeführt und Logik überarbeitet.

Einziger Risikofaktor ist nun, wenn beim Schließen des SessionCommand-Intertfaces

oder beim Signalisieren an den Thread eine Exception fliegt; denn dann loggen wir zwar,

aber die Shutdown-Rückmeldung kommt u.U niemals an, und damit bleiben wir

am Ende von main() einfach hängen.

Ich halte diese Fälle aber für in der Praxis nicht relevant,  und verzichte daher auf eine Spezialbehandlung

Einfachen Aufruf

implementieren

will sagen: in den Visitor-Methoden-Implementierungen

aber ist nicht auf dem Visitor-Interface darstellbar

....über einen GenNode-Visitor nachdenken

aber

  • nicht jetzt
  • das Problem müßte mehrfach auftreten
  • könnte zu Switch-On-Type-programming führen

Idee: Zusammenarbeit

...das wäre eine Protokoll-Erweiterung

letztlich verworfen; siehe Ticket #1058

...das heißt,

die mehrfachen Indirektionen und das Ein-/Auspacken der Argumente

es wäre denkbar, an dieser Stelle

unvollständige Argument-Tupel zu akzeptieren

und die Argumente von links her zu schließen (currying)

denn es erzwingt,

daß die betreffenden Commands schon erzeugt und registriert sein müssen,

wenn in der UI ein InvocationTrail angelegt wird.

Architektur-Entscheidung

kann offen bleiben

reine ID-Wirtschaft wäre möglich

Stock-IDs sind @deprecated

»die Timeline«

Aufgabe: populate timeline

8/2018 there is some overlap with the (not yet fully functional) 

ViewLocator in the InteractionDirector. Right now, PanelLocator  

fills some gaps here and offers preliminary solutions for some   

lookup and allocation tasks ultimately to be performed on a more 

abstract level through the ViewSpec-DSL. This corresponds to the 

observation, that PanelLocator and WindowLocator are somewhat    

tangled currently. The goal is to turn them into access services,

and shift the allocation logic to the aforementioned higher level.

...der aber irgendwann (demnächst in diesem Theater) umgebaut/zurückgebaut werden soll

»der Viewer«

»professionell« bedeutet: unaufdringlich und klar

...damit wir nicht der extremen Variabilität für Desktop-Icons unterliegen

...damit das Icon in verschiedenstem Kontext gleichermaßen funktioniert

letztlich bin ich davon abgekommen, einen gezeichneten Pfeil zu integrieren; auch die Insets habe ich aufgegeben, dadurch wird das Design stringenter. Die Hervorhebung erfolgt nur durch ein Glanzlicht oben links, im Zusammenspiel mit den 2px Gehrungen und einem zusätzlichen Schatten an der Kehle der Gehrung

Inkscape wendet Transformationen normalerweise nur auf Blatt-Elemente direkt an; sonst fügt es ein "transform"-Element ein; dieses ist eine Matrix für homogene Koordinaten.

wenn man den Filter entfernt / auf Null dreht (Blur), dann wendet Inkscape die Transformation an

wenn man die Gruppe auflöst, wendet Inkscape die Transformation auf jedes Einzelelement an, und schiebt eine inverse Transformation in alle referenzierten Gradienten

...und Inkscape wird das erhalten, solange man die betr. Features nicht wieder aktiviert. Im Besonderen kann man

  • alle "Stroke"-Features entfernen, wenn der Stroke deaktiviert ist
  • opacity:1 weglassen
  • diverse Vector-Filter und display-styles weglassen (wenn sie auf dem default-Wert stehen)

das ist Handarbeit und erfordert Erfahrung

  • wichtige Formen auf eine Grid-Linie verschieben
  • Gewicht exakt vertikaler/horizontaler Linien beachten
  • scharfzeichnen mit einer sub-Pixel-Linie mit komplementärem Helligkeitswert

notwendig, damit Hinting, Scharfzeichnen und drop-Shaddows funktionieren

Definition: komplexe Widget-Strukturen,

welche nur einmal in dieser speziellen Konfiguration exisiteren,

und dabei eine zentrale Rolle im UI spielen.

Beispiel:

  • Timeline
  • Property-Grid

Definition: speziell konfigurierte und verdrahtete Teil-Komponenten,

welche wiederholt an verschiedensten Stellen im UI eingesetzt werden,

und sich dort jeweils konsistent und uniform verhalten.

Beispiel:

  • Timecode-Widget
  • Placement-Widget

vorbereitete Grundstrukturen für immer wiederkehrende Setups

...denn das erscheint mir bodenständiger.

GUI-Programmierung muß bodenstäding bleiben,

man gewinnt hier nichts durch blendende Abstraktionen.

Ich hoffe, daß der Hilfscontainer nahezu transparent gemacht werden kann.

Und der Rest sollte so vertraut aussehen, daß es selbsterklärend wird.

Name: WLink

TestControl Dialogbox

...welche ad hoc mit beiläufig geschriebenem Debug/Test-Code belegt werden

Style-Scheme for Lumiera

#1168 : find a way to manage style of

              custom extended UI elements

den Klassennamen ohne das Namespace-Präfix "GTK", und alles lower case.

Im Zweifelsfall den GTK+ Inspector verwenden!

CSS genügt

füge möglichst hoch in der Hierarchie Regeln ein,

die ein property auf den Wert 'inherit' setzten

z.B. in den Block mit den "default-Regeln"

  * {

    color:            inherit; /* by default inherit content colour from parent widget */

    background-color: inherit;

}

beruht auf der sehr sinnigen Einrichtung von Joel Holdsworth

...wartet noch darauf,

daß die alte, obsolete Timeline zurückgebaut ist

siehe guifacade.cpp

Problem ist: wenn das triggerShutdown kommt,

bevor die Notification-Facade geöffnet werden konnte

Aufgabe: docking panels global

wenn die GTK-Loop angehalten wird,

dann koppelt sich GTK anscheinend auch

automatisch vom Window-Manager ab.

Jedenfalls habe ich nun schon mehrfach den Shutdown "von unten" getriggert,

und es wurden alle Fenster geschlossen.

Allerdings habe ich an der Stelle immer noch GTK-Assertions

Begründung:

Das neue System ist anscheinend fest integriert in Gio::Application.

Mir ist nicht klar, wieso ein Fenster/Widget das Interface Gio::Actionable implementieren muß.

Ich werde den Verdacht nicht los, daß hier das Ziel verfolgt wird, eine "Action" von den

Grenzen der Applikation zu befreien und direkt in den Desktop zu integrieren.

Mit Desktop ist natürlich der Gnome-Desktop gemeint. Was diesen Verdacht bestärkt,

ist, daß Gio::Application sofort auch gleich eine dBus-Verbindung hochfährt.

  • soll einmal der »StyleManager« werden
  • baut Stand 2019 auf bestehender Funktionalität von Joel Holdsworth auf...

Style / CSS ⟶ delegiert an UiStyle

das ist ein gaaaanz großes Strategisches Thema; vor 2023 hatte ich schon mehrfach versucht, es zu fassen — das ist mir aber bisher nicht gelungen, und deshalb wartet es.... ⌛

eigentlich wollen wir "das aktuelle"

Lösung: schwebende Bindung

arbeitet dann freischwebend

...meint:

wir müssen zur Aufrufzeit einer Aktion

an den aktuellen Kontext ankoppeln können.

Das heißt, der UiManager muß im Stande sein,

diesen "aktuellen Kontext" irgendwo aufzufischen

dort wird der Kontext aufgegriffen

...und der wird in der Tat an vielen Stellen includiert

und verwendet, und das ist auch gut so

Njet

InvocationTrail ist tot

generisches Öffnen

...um die Entwicklung des Designs zu erzwingen

und den Teufelskreis zu durchbrechen!

...stattdessen einen Fehler-Indikator auslösen

(Beispiel "in-point fehlt")

...das ist eine Reaktion,

die von einem managing Ui-Element ausgeführt wird,

aber von einem externen State-Change getriggert wird

damit UNDO funktionieren kann,

müssen wir schon beim capture wissen,

welches Objekt (ID) hinzugefügt werden wird.

Denn sonst müßten wir uns den gegenwärtigen Inhalt speichern

und das wäre unsinnig.

wir können den größten Teil dieser Einzeiler-Funktionen loswerden,

da es nur darum geht, via globalCtx auf den passenden Controller zuzugreifen

...das können wir erst dann sinnvoll angehen, wenn die Application-Core weiter ausgereift ist

das verschiebt das Problem nur

UI-Koordinaten (UICoord)

...and this anchorage can be covered and backed by the currently existing UI configuration

...by interpolation of some wildcards

...need to be extended to allow anchoring

we may construct the covered part of a given spec, including automatic anchoring.

navigate to the real UI component

designated by the given coordinate spec

...halten wir besser raus aus diesem Design.

Denn es würde stärkere Annahmen über die "Zielelemente" erforderlich machen,

und diese dann doch wieder in ein Korsett zwängen. Im Moment (10/17) habe ich

stark den Verdacht, daß wir das nur in wenigen Spezialfällen brauchen werden,

und dann kann man es auch extern belassen.

...was für verschiedene Arten von Zugriff

sind denkbar und müssen in der Strategy konfigurierbar sein?

mögliche

Komponenten

wie funktioniert

Pfad-Navigation?

habe diese Analyse 2017/2018 ein Stück weit vorangetrieben.

Ergebnis war die Schaffung von UI-Koordinaten und die ViewSpec-DSL

Damit ist das Thema aber bei Weitem noch nicht ausgeschöpft,

jedoch genügend aufgeschlossen, um die konkrete Implementierung fortzusetzen

betrachte ich als ungesund

eine reverse resolution

zentrales Problem

...der beim Erstellen des Elements

mit den zu diesem Zeitpunkt bekannten UI-Korrdinaten bestückt wird

auf welche Eigenschaften

stützen wir uns?

wer bestimmt,

was "Kind" ist?

oder den Spot verschieben

verbleibende

Probleme

VariadicArgumentPicker_test

welche Operationen

sind wirklich notwendig

...denn wir verlangen, daß wir nach dem Interpolieren über eine Lücke

immer noch mindestens ein explizt gegebenes Element im Pfad haben,

welches auch von der UI-Topologie bestätigt wird.

Grund: wir wollen vermeiden, abschließende Wildcards bloß irgendwie zu binden

nämlich wenn der Pfad mit einem explizit gegebenen Präfix anfängt,

dann aber Wildcards enthält, die nicht nach den verschärften Bedingungen gecovert werden können.

beispielsweise

  • wenn schon das Präfix nicht paßt
  • wenn das erste Element nach dem Gap nirgends im realen UI in der tiefe existiert
  • wenn mehr Wildcards da sind, als restliche Tiefe zum Matchen

letzten Endes war es nahezu gleich schwer zu implementieren,

aber von der Aufruf-Logik her einfacher, stets nach partieller zu suchen

und totale Coverage nur nachträglich durch Längenvergleich festzustellen

wo der Spot ist

Innerhalb einer Kinder-Folge gibt es keine duplikaten IDs.

Das heißt, es genügt, den ersten Match zu nehmen.

Warnung: diese Konvention ist besonders tückisch,

denn eine Verletzung kann weithin unbemerkt bleiben

der Resolver macht kein Memory-Management,

sondern speichert einfach Zeiger.

Es wird erwartet, daß diese gültig bleiben,

solange irgend jemand auf den Resolver oder den

daraus resultierenden Pfad zugreift

die Topologie, aber auch der Fokus-Zustand

ändern sich nicht während der aktiven Lebensdauer eines Resolvers

Hierbei ist aktive Lebensdauer wie bei einem Iterator zu verstehen.

aber es gibt Konsistenzchecks + Exceptions

wenn die Auswertung aufgrund einer gebrochenen Konvention entgleist

Tiefensuche über die reale UI-Topologie

Ziel ist, den Pfad bestmöglich zu covern

Es gilt die erste maximal abdeckende Lösung

Tiefe, bis zu der dieser Pfad gedeckt ist.

Sofern der Pfad bereits explizit ist, genügt diese Info allein

heap-allozierter expliziter Pfad.

  • wird notwendig, wenn *this wildcards enthält
  • Lösung wird unter Alternativen ausgewählt (nach maximaler Tiefe)

Ha! das ist eine Monade!!!!!1!11!

...aber im Moment der Lösung brauche ich den Pfad aufwärts.

Das heißt, wir müssen ihn bereits im Aufrufkontext bereit liegen haben

Puffer ab Tiefe

vom Pfad initialisieren

monadische Lösung

möglich?

echte Expand-Funktion notwendig

mathematische Monaden sind viel mehr...

Im Besonderen sind es Typen höherer Ordnung,

also mehr als bloß parametrisierte Typen (Templates)!

alles mit einer Form des IterExplorer machbar ist

...da IterExplorer einen Template-Template-Parameter nimmt,

ist er eigentlich ein Meta-Template, und es gibt diverse

Ausprägungen, die alle subtile Seiteneffekte ausnutzen....

...was nicht grade zur Verständlichkeit des Ganzen beiträgt

das Fortschreiten der Berechnung dargestellt werden kann

Problem: Layer sind verkoppelt

dann aber als State Monad

m >>= f = \r -> let (x, s) = m r

                            in (f x) s

wende die resultierende State-Monade

auf den Zwischenzustand x aus (1) an

sind Monaden

wirklich hilfreich?

alternatives

Ziel

Fazit:

brauche...

das impliziert grundsätzlich einen Stack

expand() ruft eine vorbereitete Parametrisierung

 für diesen Expand-Mechanismus auf

...selbst wenn man ihn für eine triviale Implementierung

eigentlich überhaupt nicht braucht.

Das kann zwar zu einem gewissen Grad abgemildert werden,

indem man einen speziellen Inline-Stack mit Heap-Overflow nutzt

ich brauche ihn nicht

die Builder-Operationen moven den bisherigen Iterator-compound weg.

Ich könnte mir vorstellen, daß das einen naiven User ziemlich schockiert....

Lösung wäre, das Iterator-API erst nach einem expliziten terminalen Aufruf freizuschalten

weil...

  • sich zwar die Logik syntaktisch anschreiben läßt
  • aber beide Zweige u.U nicht auf den gleichen Typ hinauslaufen
  • und erst in der Anwendung dieses Ausdruckes werden die Typen gleichgestellt

_fun<FUN>::Sig scheitert

das ist aber unpraktisch....

....also ist es gradezu natürlich,

einen Expand-Funktor als generisches Lambda zu schreiben!

aber eine falsche Template-Instantiierung

ist ein Compile-Fehler, kein Substitutions-Fehler

oder man fällt auf eine mögliche Substitution zurück

...und wenn die Scheitert, ist das ein compile-Fehler

denn das ist der sinnvollste Fall für ein generisches Lambda

....und das ist alarmierend,

denn Debugging ist mindestens doppelt so schwer...

TreeExplorer per slicing move entfernen

wir haben bisher viel zu naiv angenommen,

daß der parent-Iterator immer auch ein TreeExploer ist.

Dem ist nicht so, ab dem Moment, wo wir mehrere Decorator-Layer haben!!

Aufrufpunkt: invokeTransformation()

In instantiation of 'lib::{anonymous}::_ExpansionTraits<FUN, SRC>::Res lib::{anonymous}::_ExpansionTraits<FUN, SRC>::Functor::operator()(ARG&) [with ARG = long int; FUN = lib::test::IterTreeExplorer_test::verify_transformOperation()::<lambda(auto:2)>&; SRC = lib::iter_explorer::IterableDecorator<long int, lib::iter_explorer::WrappedIteratorCore<lib::TreeExplorer<lib::iter_explorer::StlRange<std::vector<long int>&> > > >; lib::{anonymous}::_ExpansionTraits<FUN, SRC>::Res = std::basic_string<char>]':

src/lib/iter-tree-explorer.hpp:426:50: error: no match for call to '(std::function<std::basic_string<char>(lib::iter_explorer::IterableDecorator<long int, lib::iter_explorer::WrappedIteratorCore<lib::TreeExplorer<lib::iter_explorer::StlRange<std::vector<long int>&> > > >&)>) (long int&)'

also IterableDecorator aufgesetzt auf die Core im Transformer

wir gehen davon aus,

daß der Optimizer solche inline-Accessor-Funktionen

ohnehin restlos wegoptimieren wird

und zwar in dem Moment, wo man die Layer zusammensetzt.

...denn wenn mal ein Layer "nur" Iterator wäre,

dann könnte es eine Kombination geben, die einen solchen Layer übersrpingt

...nur was billig ist,

denn im Moment brauchen wir das überhaupt nicht

...setzt eigentliche Expand-Operation darunter voraus

...also Programmierung analog zum Filter

  • sofort aus dem Konstruktor heraus die Invariante etablieren
  • nach jedem Iterations-Schritt die Invariante erneut wiederherstellen

der Natur der Dinge folgen,

nicht den technischen Möglichkeiten

....und dann könnten diese Transformer in der Kette

nicht mehr die expandChildren aufrufen

...das heißt, es ist nicht möglich,

daß ein Layer irgendwo in der Mitte einen solchen generischen Hook aufruft.

Es sei denn, man speichert einen Funktion-Pointer in der Basis,

oder nimmt eben gleich eine virtuelle Funktion.

Das ist aber hier aus grundsätzlichen Überlegungen heraus keine Option

das IterStateWrapper-API ist optimal

unter der Annahme, daß wir beim Lumiera Forward Iterator - Konzept bleiben

-- das heißt, beliebig oft yield, und Iterations-Ende per bool()-Test

Unter dieser Annahme kommt yield stets vor iterNext (wenn überhaupt).

Und yield muß (a) einen Status liefern, (b) einen Wert liefern.

Einziger Ausweg wäre, wie das IterAdapter macht, einen Pointer rauszugeben.

Das ist eigentlich keine gute Lösung, weil die Implementierung dann sehr tricky wird.

Siehe IterAdapter als abstoßendes Beispiel.

Und als weitere Alternative bleibt nur die Einführung von State, und das bedeutet,

sich im Iterator oder in der Implementierung irgendwo noch eine zusätzliche bool-Flag zu speichern.

implementieren ebenfalls expandChildren()

ganz anders als bei IterAdapter, wo das Sinn macht...

...ohne daß die Funktionen auch virtuell sind,

können wir nicht sicherstellen

  • daß die hereingereichte Implementierung die Funktionen überschreibt

Konzept funktioniert nicht

aus processing-Function

innen heraus

will sagen, das ist ja auch eine durchgeknallte Idee....

wenn eine Funktion in einem Layer expanded

...d.h, entweder man gibt aus dem Functor das zurück,

was vor dem expand anstand (=der Vater), oder man verwirft diesen

und liefert das, was nach dem expand erscheind

  • asIterSource() verwendet ein eigenes Interface, um diesen call an die Implementierung durchzureichen
  • in dieses Interface habe ich nun einen Rückgabewert eingebaut
  • damit kann ich das IterSource-Front-End refreshen
  • trotzdem hässlich...

an welcher Stelle wird diese Mechanik

an einen bestehenden Iterator angeschlossen

Stichwort HierarchyOrientationIndicator

...will sagen, bin nun schon mehrfach in dieses Problem gelaufen,

nachdem ich dachte, alles so schön gelöst zu haben.

Das Problem ist, daß eben auch der Konsument irgendwie

von den verschachtelten Strukturen mit gesteuert wird.

Das Ergebnis ist eben nicht rein linear.

...das ist nämlich der triviale Workaround

wir hatten bisher eine auto-Aufräum-Routine in iterNext(),

welche dazu führt, daß ein erschöpfter Vater sofort weggeräumt wird,

noch bevor wir dazu kommen, die Kinder zu pushen

jeder Zugriff auf ein Sub-Objekt muß durch eine VTable

Stichwort: virtual base offset

...da gibt es eine explizite Ausnahme-Regel.

Die Copy-Konstruktoren werden aus der Kandidaten-Menge entfernt.

Grund ist, daß die default-Initialisierung der Member-Felder noch nicht hinrechend geklärt war.

C++17 holt das nach

...dort wird einfach on-demand in der Basisklasse nachgeschaut.

Wenn dabei ein Basis-Copy-Ctor gezogen wird, dann wird eben default-Init für die Felder im abgeleiteten Objekt gemacht.

Es gibt dann eine neue, explizite Regel, die verhindert, daß zufällig ein aus der Basis geerbter Ctor

die Signatur eines Copy-ctors überdeckt

Allerdings genügt es, dies an einer Stelle in der Kette zu ergänzen

...und zwar genau dort, wo erstmals ein Basis-Objekt akzeptiert wird.

Das ist bei uns im BaseAdapter, also der ersten Ebene über dem zu initialisierenden Basis-Objekt.

Alle anderen Layer darüber reichen dann korrekt mit dem geerbten Ctor diese Initialisierung nach Unten.

übrigens ist es im IterSource<T>::iterator nicht  notwendig

...das war nur ein unnötiger Fix nach dem Gießkannen-Prinzip.

Denn dieser Iterator soll niemals mit einem Basis-Objekt initialisiert werden,

sondern stets von der IterSource-Builder-Funktion konstruiert.

Und wenn man selber keinen Ctor in eine Klasse schreibt, sondern nur ctor-erbt,

dann werden auch die Copy-Konstruktoren korrekt automatisch generiert.

Ticket machen: #1125

...daß man ein Ding komplett in einen Iterator packt,

und dieser es dann auch managed

ist das #190

...ist partiell diese Idee.

Nur auch das auf einem etwas anderem Level,

und immer mit einem Heap-allozierten vector

versehentlich wurde auch der an std::forward gegeben

habs mit FormatUtils_test bewiesen

Dazu in NumIter einen explizit tracenden move-ctor eingebaut

weil der Aufruf von join(&&) selber wasserdicht ist

D.h. er frisst keine Werte.

Deshalb fällt dieses doppelte Problem nicht auf

...weil man den konkreten Typ der Core kennen muß

verschiedendste Pipeline-Konstruktionen

können nun hinter dem gleichen Interface sitzen

rein ein Problem mit der Test-Fixture.

Da die Quelle nun von einem shared-ptr gehalten wird,

erzeugt eine Kopie des Iterator-Front-End

nun nicht mehr eine Kopie des ganzen Zustandes.

das wäre aber bequem für den Test.

Frage: ist das überhaupt eine gute Idee, vom Design her??

siehe std::shuffle

vorgegebene Zahlenfolge
in untendlichem Zufalls-Baum finden

man kann move(iter) verwenden,

wenn man konsumieren möchte

rLet(40878 < 18446744073709551615) → S

|↯| S ... 40878

rLet(40879 < 18446744073709551615) → F

|!| expand 40879

rLet(0 < 4) → A

rLet(40880 < 18446744073709551615) → Q

|.| A -->> 40879

|!| expand 40879

rLet(0 < 4) → F

rLet(1 < 4) → N

|.| F -->> 40879

|↯| F ... 40879

rLet(1 < 4) → W

|↯| W ... 40879-0-1

rLet(2 < 4) → N

|↯| N ... 40879-0-2

rLet(3 < 4) → T

|↯| T ... 40879-0-3

rLet(4 < 4) → F

|↯| N ... 40879-1

rLet(2 < 4) → F

|↯| F ... 40879-2

rLet(3 < 4) → A

|!| expand 40879-3

rLet(0 < 4) → J

rLet(4 < 4) → Y

|.| J -->> 40879-3

|↯| J ... 40879-3

rLet(1 < 4) → H

|↯| H ... 40879-4

rLet(2 < 4) → H

|↯| H ... 40879-5

rLet(3 < 4) → F

|↯| F ... 40879-6

rLet(4 < 4) → V

|↯| Q ... 40880

rLet(40881 < 18446744073709551615) → A

|↯| A ... 40881

rLet(40882 < 18446744073709551615) → X

|↯| X ... 40882

rLet(77943 < 18446744073709551615) → R

|↯| R ... 77943

rLet(77944 < 18446744073709551615) → X

|!| expand 77944

rLet(0 < 4) → U

rLet(77945 < 18446744073709551615) → I

|.| U -->> 77944-0

|↯| U ... 77944-0

rLet(1 < 4) → X

|!| expand 77944-1

rLet(0 < 4) → K

rLet(2 < 4) → Z

|.| K -->> 77944-1-0

|!| expand 77944-1-0

rLet(0 < 4) → V

rLet(1 < 4) → Y

|.| V -->> 77944-1-0-0

|↯| V ... 77944-1-0-0

rLet(1 < 4) → I

|↯| I ... 77944-1-0-1

rLet(2 < 4) → I

|↯| I ... 77944-1-0-2

rLet(3 < 4) → Z

|!| expand 77944-1-0-3

rLet(0 < 4) → Q

rLet(4 < 4) → X

|.| Q -->> 77944-1-0-3-0

|↯| Q ... 77944-1-0-3-0

rLet(1 < 4) → M

Protocol of the search: 77944-1-0-3-1

...weil es mutmaßlich

im realen UI in ähnlicher Form auch auftreten wird:

die Menge der Top-Level-Fenster ist eben etwas anderes,

als die Menge der Tracks in der Timeline.

Erst nach einer Transformation wird daraus eine Menge von Strings

Expand-Funktor hat einen Rückgabe-Typ

wenn es sie gäbe könnte man sie hier nutzen

...das heißt:

diese Struktur muß bereits beim Aufbauen des GUI

nebenbei mit aufgebaut werden, und über alle

mutierenden Aktionen hinweg automatisch konsistent bleiben

möglicherwese aber notwendig

...will sagen

wenn ich mir heute so die Situation vorstelle,

könnte es darauf hinauslaufen, daß man das braucht.

Und zwar, zumindest die Eigenschaft, von gegebenem Element

die Koordinaten zu ermitteln.

Das ist aber dann pratkisch auch schon eine "Up"-Funktion,

selbst wenn man sie nur indirekt implementiert

vorläufige MInimal-Lösung

bis jetzt kommen wir ohne Pos-Abstraktion aus

das Nav-Interface könnte daraus entstehen

es werden jetzt keine weiteren Features für TreeExplorer gebaut....

...mal sehen, ob wir jemals daran anstoßen...

gemeint ist,

zusätzlich zu dem Eintrag im Stack,

der ohnehin selbst Heap-alloziert ist

Was ist Nav und was ist Iteration-control?

Ist es sinnvoll, beide in einem gemeinsamen API zu haben,

oder delegieren wir besser?

Was sind die Kosten dafür?

IterSource muß insgesamt besser erweiterbar werden....

der Expander sitzt nun doch dahinter, in der Implementierung

das Ergebnis ist der konkrete Iterator-Typ

Wire-Tap-Implementierung

das heißt, depth ist aktuelle Tiefe!

ist keine Lösung

ist partielle Lösung

Ergebnis-Ausgabe ist die jeweilige mögliche Coverage

...insofern wir nur eine (partielle) Lösung signalisieren,

wenn wir einen direkten Match erziehlen.

Ein wildcard-Match führt nur dazu, daß wir zu den Kindern absteigen,

aber zählt erst mal für sich nicht als Lösung

Support für elided element

  • nur für internen Gebrauch
  • protected im Builder

Lösungen müssen

am Ende des Patterns liegen

YAGNI

  • die Standard-Implementierung von std::swap macht einen Dreiecks-Move
  • wir haben effiziente Move-Konstruktoren

...und nicht den möglichen Zustand.

Denn für letzteren gibt es die "canXX"-Prädikate

macht es Sinn, dafür einen expliziten Testfall zu konstruieren,

oder verfangen wir uns da sofort zu sehr in der Implementierungs-Technik?

realer Pfad endet mit elided nach Wildcard

kann jedoch demonstrieren,

daß der Algorithmus solche Lösungen verwirft

neue Einsicht 31.12.17

totale Coverage ist das, was man naiverweise erwartet.

Also sollte das auf dem API der default sein

...das heißt: keine Wildcards, keine pseudo-Specs (currentWindow)

Zweck ist vor allem, meta-Specs wie firstWindow, currentWindow aufzulösen

...verwendet einen GenNode-Tree

als Repräsentation des real-existierenden UI

Integration ViewLocator

Resolver / Navigator

aber auch: Resolver

die Regel ist:

Bei zyklischen Abhängigkeiten erfolgt der Ringschluß

an einer Stelle über eine allgemeine Abstraktion

implementiert LocationQuery

...als Namespace-globale Variable mit externer Linkage

Ein Lookup-Vorgang ist schon ehr aufwendig,

jedoch harmlos im Vergleich zu einer einzigen Frame-Berechnung.

  • im ersten Schritt machen wir eine Tiefensuche potentiell über die ganze UI-Topologie
  • im zweiten Schritt wiederholen wir noch mal den Abstiegspfad zur Lösung des ersten Schrittes

Grundsätzlich gilt hier die Einschätzung: Klarheit der Schnittstelle hat Vorrang

ViewLocator ruft die DSL auf

...welcher wiederum von ViewLocator betrieben

sonst bekommen wir eine versteckte

zweite hart-gecodete Fallback-Konfig

existing() sollte default sein und create() explizit anzufordern

weil der größte Teil aller real anzugebenden Regel-Klauseln

von der Bedeutung her "existing" meint

komplett definierter Pfad incl Zielobjekt

ggfs wird höchstes ein abschließendes Element hinzugefügt

dieser Pfad ist stets anchored und partially covered

zwar könnte es (später mal) sein,

daß wir mehrere Perspektiven gleichzeitig in die UI-Topologie abbilden...

...weil die Perspektive eigentlich als etwas Orthogonales empfunden wird,

das nicht dirket zur "harten" Topologie gehört, sondern vielmehr bestehende Elemente umgruppiert.

Andererseits möchte man eben doch manchmal eine View-Spec eigens auf eine bestimmte Perspektive beschränken,

und deshalb habe ich die Perspektive zu den UI-Koordinaten hinzugenommen

...und zwar genau dann, wenn bereits die nächste Komponente unterhalb der Perspektive,

also das Pannel, nicht oder nicht in dieser Form existiert, also erzeugt werden müßte.

Unser Kriterium für Lösungen jedoch verlangt mindestens einen Match jenseits der Wildcards,

um den Match eindeutig zu machen.

...sofern es stets eine Perspektive geben muß

...und zwar zwingend notwendig, weil es (viele) Views geben wird,

welche keine mehrfachen TABs unterstützen. In solchen Fällen brauchen wir

ein Konstrukt, mit dem sich eine Ebene im Baum überspringen läßt

"muß" ist relativ, denn mit den bisherigen Anforderungen

hätte es genügt, den "elided"-Platzhalter nur in den konkreten UI-Koordinaten

zu verwenden, und ihn dann jeweils per Wildcard zu matchen (was automatisch passiert,

einfach wenn die betreffende Komponente in der Angabe fehlt)

Die korrekte Semantik fällt uns hier wirklich in den Schoß,

es ist nur eine weitere Zeile in dem Test, ob ein Match vorliegt

wobei einer, nämlich '*' sehr offensichtlich und bekannt ist,

während der andere (eben dieses '.' == elided) eigens erklärt werden muß

...weil es eigentlich ein Wildcard ist,

aber vom gesamten sonstigen Algorithmus nicht als Wildcard behandelt wird.

Damit kann man alle Einschränkungen unterlaufen

Korrekter Gebrauch setzt eigentlich voraus,

daß es an dieser Stelle auch tatsächlich "gar nichts" oder nur "ein stets festes Element" gibt.

Für die Perspektive ist das (nach jetztiger Planung) stets gegeben.

Wenn man allerdings diese Bedingung verletzt, dann matcht der "elided"-Platzhalter

in mehreren alternativen Zweigen wie ein Wildcard, und es hängt dann von

zufälligen Umständen ab, ob man die erwartete Lösung bekommt

bloß würde sich die Signatur der DSL-Bausteine ändern:

Allocator = std::function<UICoord(UICoordResolver)>;

...denn Reolver ist ein UICoord::Builder und als Solcher non-copyable.

Also würde das ganze Gefrickel mit Referenzen losgehen,

in einem Stück Metaprogramming-Code, das ohnenhin schon ziemlich "dicht" ist....

Der Punkt ist: das ist eine reine Lauzeit/Effzienz-Überlegung.

Nachdem das Pfad-Matching in der DSL für die Location die passende Lösung gewählt hat,

wäre -- im UICoordResolver eben -- auch schon die effektive Coverage bekannt.

Da aber unser API nach (reinen) UI-Coord verlangt, werden diese aus der berechneten Lösung

herausbewegt. Und der Allokator muß sich dann erneut einen UICoordResolver bauen,

oder zumindest das LocationQuery-Interface bemühen, welches dann nochmal den Baum

traversiert um die Coverage festzustellen.

  • Ja, das kostet und ist verschwenderisch...
  • und Ja, vermutlich sind die paar CPU-Zyklen komplett egal

das ist der klassische Fall, wo man wegen einer solchen Optimierung

sich ein Interface versaut und ziemliche zusätzliche Komplexitäten an Bord zieht.

...insofern auch dort

die jeweilige generische Regel parametrisiert / instantiiert wird

gegen den Kontext, mit dem sie matchen soll

das ist eine typische, rein lokale Optimierung (Speicher vs CPU)

Ein solcher View-Index sollte dann ebenfalls via LocationQuery exponiert werden

es gibt nur einen "Locator"

ein LocationSolver

LocationQuery qua Navigator

...die es hinten herum bekommt

sehen ViewLocator-API

Regel-Parametrisierung

Kontextualisierung

Man möchte, daß für spezielle Sub-Elemente,

die aus einem fremden Kontext heraus geöffnet werden,

zunächst versucht wird, einen irgendwo im UI schon bestehenden TAB

für speziell diesen Element-Typ wiederzuverwenden; das erlaubt dem User,

sich einen Platz für sehr spezielle Sachen beiseite zu setzen.

z.B. sehr spezielle Assets oder ein virtueller Clip.

Erst wenn so ein Ort nicht gefunden wird, möchte man auf einen

generischen Ort zurückfallen, und erst als letzte default-Lösung

im aktuellen Fenster einen völlig neuen UI-Elementkontext schaffen.

...statt die gesamte Matching-Engine mit einer Art

halbgaren Unifikation aufzubrezln

nur eingeschränkt auf die TypID?

Preprocessing beim Anlegen der Klausel

bleibt dem Charakter nach imperativ

was ist der Locator?

der Level im UI ist noch offen

fast immer ist das aber UIC_VIEW

im Moment fällt mir überhaupt keine Ausnahme ein

aber man soll niemals nie sagen;

jedenfalls ist der LocationSolver komplett generisch geschrieben,

wäre ja auch dämlich, den auf einen Level festzunageln

kann man den Level erschließen?

es ist nicht klar, ob die pattern bereits das fragliche View-Element mit einschließen,

oder ob das View-Element noch angehängt werden soll. Diese Variation ist essentiell,

um Regeln auszudrücken, die explizit nur eine schon existierende UI-Komponente greifen

auto locate = matchView(

                          panel("blah")

                          or currentWindow().panel("blubb").create() )

LocatorSpec<UIC_VIEW> locate = panel("blah")

                                                     or currentWindow().panel("blubb").create()

ViewSpec locate = panel("blah")

                                or currentWindow().panel("blubb").create()

Anwendung delegiert an einen Serivce

für LocationQuery

für LocationSolver

Schreibweise für create Clauses

technische Lösug diskutierbar

...will sagen,

man kann das erheblich tief und generisch ausbauen

Perspective elided

...denn durch overwrite kann man denormalisierte Pattern erzeugen.

Also muß jeder Anwender dieser Funktion sicherstellen, daß dies

entweder nicht passieren kann, oder explizit normalise() aufrufen.

...das würde bedeuten, daß man sogar ein neues Hauptfenster erzeugt.

Also in diesem Fall würde überhaupt nichts mit dem existierenden UI matchen...

Komponente falls nötig anhängen

...weil wir keinen Zustand sammeln

und daher jede Klausel von Grund auf neu lösen.

könnte man zulassen

not empty? UND

Create ODER totalyCovered?

Komma heißt "and then" in der Logikprogrammierung

nur "hinten herum" über die verwendete LocationQuery

klassischer Fall von »premature optimisation«

wer interpretiert

UI-Koordinaten

um eine Position zu kennzeichnen

nämlich in lib::meta::func::PApply::bindBack

// generic lambda, operator() is a template with one parameter

auto vglambda = [](auto printer) {

    return [=](auto&&... ts) // generic lambda, ts is a parameter pack

    {

        printer(std::forward<decltype(ts)>(ts)...);

        return [=] { printer(ts...); }; // nullary lambda (takes no parameters)

    };

};

Vermutung: muß Lambda instantiieren...

als Subklasse von UICoord

...denn das eigentliche Problem ist,

daß ich noch keinerlei Implementierung schreiben kann.

Mithin schiebe ich mir Platzhalter von der linken in die rechte Tasche

und zwar für die abstrahierte GUI-Location

  • einmal symbolisch als UI-Koordinaten
  • einmal opaque als eingekapselte Lösung

Und sowas ist verwirrend und verlockt gradezu, die Schachtel aufzumachen

und an der Implementierung zu kleben

...was bis jetzt nicht gegeben ist!

Bis jetzt haben wir einen "Durchlauf-Erhitzer": letztlich will man nur die Referenz

auf das GUI-Element haben, und die dazwischenliegende symbolische Schicht

dient nur der Konfiguration und Lösungs-Suche.

Wenn allerdings später mal diese UILocation == bereits decodierte UI-Koordinaten

ein eigenständiges Token wird, welches über mehrere Schnittstellen hinweg geschoben wird,

dann und nur dann würde die zusätzliche API-Komplexität Sinn machen.

die verfickte Performance wird ignoriert

....jaaaaa, das ist verschwenderisch

kann dazu führen,

daß etwas Bestehendes zurückgeliefert wird

...merke

die Spezialbehandlung für const& gilt nur, wenn wir direkt auch diesen Typ nehmen.

Im vorliegenden fall wird aber der conversion-Operator aufgerufen, um den Initializser zu erzeugen.

Daher denkt der Compiler, er kann das Ursprungsobjekt jezt wergwerfen.

Spezialbehandlung

Perspektive

...um zu prüfen, ob das allgemeine Design

mit solchen Asymetrien umgehen kann,

welche ziemlich sicher noch viel mehr

bei der Navigation in einem realen GUI auftreten

...und ich hab mir letzte Woche noch solche Vorwürfe gemacht,

daß ich mich wieder mal "akademisch" verspielt habe.... :-P

...und die IterSource dann nur über WrapIter definieren.

Schichten-Prinzip...

das ist immer schon korrekt erledigt

...und das heißt.

ein Value wird auch sofort konstruiert,

egal, ob man den dann gleich wegwirft.

nur einen Satz Klauseln

...denn irgendwann wird's lächerlich mit der Unit-Testerei.

Oder zumindest Hexagonal.

Bedingt durch die ganzen rausgezogenen Interfaces hat jetzt bereits ViewLocator überhaupt keinen Gehalt mehr.

Wenn ich jetzt auch noch die einzige verbleibende Methode rausziehe, um sie testen zu können,

drehe ich mich komplett im Kreis. Schließlich kann ich diese Methode ja, genau genommen,

im Moment auch noch nicht wirklich testen, aus genau den gleichen Gründen,

warum ViewLocator so nebulös bleibt: es gibt noch kein Lumiera GUI

Das ursprüngliche Ziel für diesen Test

ist in unserem Test-Framework nicht realisierbar

Policy: Unit-Tests dürfen keine GTK-Abhängigkeit haben

  • wird mäßig häufig aufgerufen
  • beim "Öffnen" und zur Navigation
  • im Interaktions-Kontext
  • keinen Speicherdruck erzeugen

...es ist im Rahmen;

denn wir akzeptieren double dispatch sogar in der Diff-Anwendung,

welche viel häufiger läuft, als dieser View-Zugriff hier.

Allerdings, die doppelte Indirektion ist nicht grundsätzlich notwendig hier,

da wir nur einen einzigen Anwendungsfall haben. Die zweite Indirektion in jedem Aufruf

bewirkt nur eine Entkoppelung vom Implementierungs-Kontext

wir brauchen keine Token

....anders als im Diff-Framework

senden wir hier keine beliebigen Nachrichten,

sondern interpretieren jeweils nur eine einzige feste Konfiguration

Allokator pro Typ

...wir brauchen eine Repräsentation,

um auszudrücken, daß gewissen Angaben ausgelassen wurden

Alternative: wrap UI-Coord,

thin augmentation layer

alloc = unlimited

locate = panel(timeline)

alloc = onlyOne

locate = external(beamer)

               or perspective(mediaView).panel(viewer)

               or existingPanel(viewer)

               or firstWindow().panel(viewer)

alloc = limitPerWindow(2)

locate = perspective(edit).existingPanel(viewer)

               or currentWindow().existingPanel(viewer)

               or existingPanel(viewer)

               or panel(viewer)

im Asset-Panel der jeweiligen Gruppe hinzufügen

alloc = unlimited

locate = currentWindow().perspective(edit).existingPanel(asset).existingGroup()

               or perspective(asset)panel(asset)

               or firstWindow().panel(asset)

alloc = limitPerWindow(1)

locate = currentWindow().existingPanel(infobox)

               or firstWindow().panel(infobox)

Voraussetzung: Anwendbarkeit erkennen

was hier vielleicht der Fall sein könnte

wir haben nicht einfach UI-Coordinaten als DSl-Elemente,

sondern einzelne Klauseln, die allerdings jeweils eine UI-Coord wrappen.

Wenn man jedoch, rein syntaktisch zu schreiben beginnt "UICoord::window()"

dann bekommt man einen UICoord::Builder  und das ist noch keine Klausel!

Schicht unter dem ViewLocator

model::Tangible ist schön,

aber ich weiß nicht, ob das nicht zu eingeschränkt ist.

Beispielsweise werden Panel oder WorkspaceWindow ganz sicher keine Tangibles sein,

aber es könnte durchaus sein, daß man auf sie generisch zugreifen möchte

Zwei Fälle sind hier zu unterscheiden:

  1. der gewünschte Wert existiert nicht, und das ist auch das Ergebnis der Anfrage
  2. es liegt eine Fehleinschätzung der Situation vor, insofern fest mit einem Ergebnis gerechnet wurde

In Fall-1 wird man eine bool-Abfrage machen wollen, und man kann auch mit einer false-Antwort umgehen. In Fall-2 dagegen bleibt nur noch der Tod. Und davon ist im Regelfall nicht auszugehen. Im Moment sehe ich Fall-2 als den standard-use-Case

  • im Fall-1 weiß der Client, daß er prüfen muß
  • im Fall-2 marschiert der Client einfach durch

das mag überraschend klingen,

aber in der (zu erwartenden) Nutzsituation interessiert sich keine Sau dafür,

was denn nun konkret gemacht wurde, um den Dienst zu erbringen.

Die einzig interessante Information ist, ob es gelungen ist

aber genau das ist hier jeweils nur in einem Fall gegeben

...welches in eine Kind-Timeline absteigt,

welche gegenwärtig im GUI nicht existiert

und daher auf "inaktiv" geschaltet ist.

...damit es nicht versehentlich über einen anderen Layer gelegt wird,

welchen es dann überdecken und außer Gefecht setzen würde.

Habe verifiziert, daß diese Assertion tatsächlich greift

Frage: wieviel Interaction Control

müssen wir sofort jetzt implementieren

brauche ein aktuelles Modell-Element

Problem: Zusammenarbeit

mit docking panels

Grundlagen für InteractionControl

wird der Link zwischen CoreService und UI-State dangling

...mit der Ausnahme des Automatismus,

der es selbst vom Bus abkoppelt

in generischer UI-Struktur bewegen

...denn das ist das vereinfachte Setup für "einfache" Applikationen

muß kein Manager sein

...weil ich Stand 8/2018 nicht im Stande bin,

den ViewLocator wirklich soweit fertig zu coden, daß er schon einsetzbar ist.

Problem ist die ganze abstrahierte Widget-Access-Schicht, die sich erst sinnvoll bauen läßt,

wenn es schon wirkliche und funktionierende Widgets im System gibt.

...bisher erzeugt die lookup-Operation automatisch

Abstraktion zur Steuerung schaffen

wie bestimmt?

UIStyle: der »StyleManager«

...stammt im Kern noch von Joel Holdsworth < 2012

...d.h. „konventionell“, per ctor-Parameter

das Diff wird auf den Platzhalter angewendet

wenn das Diff ein Element aus einer Kind-Menge wegfallen läßt,

dann muß dieses automatisch deregistriert werden

vermittelt über den ViewLocator (InteractionDirector)

Brücke: gemeinsamer Controller

d.h. das Widget unternimmt selber nichts, und überläßt GTK die Größenbestimmung

d.h. das Text-Label bekommt ggfs. eine Längenbeschränkung.

Und sonst wird der Körper/Hintergrund ausgedehnt

Vorgabe ist ein Fenster(=Intervall) und dessen Position relativ zum Widget-Start.

Das Widget bestimmt daraus selbständig

  • wie das Fenster relativ zu seiner eigenen Ausdehnung liegt (Logik von Intervallen ist komplex!)
  • ob es im Besonderen in seinem Inneren liegt, und proportional wo
  • und wählt dem entsprechend die Platzierung des "Kopf"-Handles (Icon+Label)

Das Resultat ist, daß der Kopf dynamisch und proportional mitrollt,

ähnlich wie das Handle einer Scrollbar. Damit bleibt der Kopf

stets zugreifbar, und gibt einen Hinweis auf die Gesamtlage.

Sehr wichtig für die Anzeige von langen Clips und Effekten.

  • es hat nur genau ein Label
  • für dieses Label kann man nur den Text angeben
  • auch der Frame wird praktisch "schlüsselfertig" bereitgestellt

das heißt: weitgehend custom drawing

...das heißt, der Menü-Button ist dann nur pro forma da, und bietet eine Fläche, auf der man zuverlässig dieses Menü trifft; letzteres ist relevant für eine Anzeige in Listen und Bins, bei der ansonsten die Ausdehnung des Widgets stark reduziert ist. Ein Klick auf das Name-Label hat eine andere Funktion (nämlich Editieren des Namens). Ein Weiterer Aspekt ist die Drag-Geste: es ist denkbar, diese auf dem Menü-Button nicht zu starten (wobei allerdings zu bedenken ist, das Ziehen auch noch an weitere Voraussetzungen zu knüpfen, z.B. einen Modifiere, oder den Umstand, daß das Objekt auch selektiert ist)

Ja, wir wollen das alte Blender-Modell: Selektion mit rechts, Aktion mit links

in diesem Fall ist das dann eine Art Toggle-Button, d.h. er wechselt auch seine Gestalt

das verdrängt lediglich das Kontext-Menü

...denn das Kontext-Menü soll auf dem ganzen Widget liegen (und konkurriert übrigens mit der Drag-Geste)

ScrollWindowPos == wo befindet sich das Scroll-Window (sichtbarer Ausschnitt) innerhalb eines größeren Intervalles, welches vom Widget dargestellt wird

...denn GTK geht von einer fixen Mindestgröße aus, einem Allocation-Request und dann einer Platzzuteilung

ein Marker, ein Pin, Position,....

repräsentiert einen benannten Zeit-Bereich:

  • Range-Mark
  • Effekt

Eintrag in einer Liste oder Asset in einer Sammlung;

hier erfolgt Präsentation nach natürlicher Größe, ggfs mit Längenbeschränkung...

Ein Mini-Container mit Placement, Name und einem Inhalts-Renderer

Terminologie nicht klar...

...was hab ich bei den Kind-Elementen vom Track gemacht?

⟹ die heißen auch »Marker«

Ja, und zwar sogar ziemlich einfach und lesbar... 

auch wenn ich mal wieder einen ganzen Tag gebraucht habe, um diese paar Zeilen Code auszuknobeln

ElementBoxWidget::Config::buildLayoutStrategy(ElementBoxWidget&)

  • festes Schema aus zwei Icons
  • nur die Art der Icons wird ausgewechselt
  • beide Icons sind Buttons und lösen führen eine Steuer-Aktion aus (keine direkte Manipulation)
  • der erste Button führt immer in den Placement-Editor, der so etwas wie eine Property-Box darstellt (ggfs als Pop-Up, ggfs in einem Pannel-Grid)
  • der zweite Button öffnet entweder das normale Pop-up-Menü (das auf Linksklick liegt), oder er betätigt den Expander

...und das ist vielleicht sogar eine gute Idee: Lumiera könnte die Möglichkeit bieten, jedwedes Element eigens für sich darzustellen oder zu inspizieren, transient und ohne Seiteneffekte

veränderte Menü-Steuerung bei derartiger Degradierung

das bedeutet: es ist Aufgabe eines übergeordneten Layout-Managers, dann auch ein reduziertes Display zu schalten; das ElementBoxWidget kann davon ausgehen, den minimal benötigten Platz auch zu bekommen (size request)

im Besonderen keine Border!

Das ist auch genau ein Argument, warum wir auf Gtk::Frame aufsetzen — damit ist der Frame nämlich ins Innere des Widget verlagert, und das Label liegt on-top und grenzt an den Rand an.

das ist eine Design/Architektur-Entscheidung; ein generisches Widget soll noch nicht mit dem speziellen Belang einer Eichung in Zeiteinheiten belastet werden; dies hat dann jeweils ins abgeleiteten Klassen zu erfolgen, so z.B im Clip-Widget

  • die wichtigste Implementierung ist in gtk_widget_size_allocate_with_baseline; sie wird von fast allen Containern verwendet, um ihre Kinder zu allozieren
  • top-level Windows, Dialogboxen und Menu-Items verwenden ihre eigene Implementierung
  • Gtk::Layout hat ebenfalls eine vereinfachte Variante: diese geht von dem minimal_size des Widgets aus!

...insofern dann die Beschränkung der Ausdehnung einzig dadurch aktiviert werden kann, daß man ein geeignetes Verb angibt, welches diese beiden Lambda als Argument nimmt

...und ich muß die Frage, wann genau diese Info bezogen wird, überhaupt nicht klären

wobei letztlich nur ein queue_resize erfolgen muß; es könnte also sein, daß dafür der Aufruf einer bestehenden GTK-Funktion genügt

...unter der Annahme, daß letzlich eine "invalidation" des Widgets genügt, ließe sich das elegant lösen, indem der Canvas-Container insgesamt "invalidated" wird.

In Summe viel eleganter.

  • das Dokumentations-Problem wird durch die DSL gelöst
  • die "Invalidation" sollte sich aus dem Parent-Container ergeben
  • damit bleiben hier praktisch keine Probleme mehr übrig
  • Redundanz im DSL-API ist nicht wirklich ein Problem; Kind=CONTENT impliziert nicht notwendig auch ein geeichtes Display
  • ansonsten sollte dieser Ansatz komplett transparent funktionieren, und dürfte weniger fragil sein

Bedingt durch das Aufrufschema können wir nicht zu Beginn steuernd eingreifen, sondern wir können nur erkennen, wenn das Sub-Widget (aus welchen Gründen auch immer) den extension-constraint verletzt. Und wir bekommen keine direkten Wirkfaktoren in die Hand (weil sich die Ausdehnung aus einem komplexen Zusammenspiel von Font, Pixelgröße und Styles ergibt)

...das heißt...

  • der GTK-Lyout-Callback wird aufgerufen
  • er ruft die btr. Callbacks der Kinder auf, um deren natürliche Größe zu ermitteln
  • wenn die Kinder nicht in den vorhandenen Platz passen, werden sie manipuliert oder verborgen

Die meisten IOS-Stream-Manipulatoren sind »sticky«, d.h. sie verändern per Seiteneffekt den Zustand im jeweiligen ostream. Ich hatte eine Methode zum Ausgeben einer Addresse, eingebaut im to-String-Framework, und diese hat std::output auf Hex-Ausgabe umgeschaltet. Erstaunlich daß ich das jahrelang nicht gemerkt habe!

  • Zufallszahl wurde re-evaluiert im callback
  • darf nicht die Allokation verwenden, sondern stattdessen das Kind nach seiner preferred_size fragen

...was normalerweise ja auch irrelevant ist, denn per Voraussetzung sollte ein Container die Werte von seinen Kindern bereits bei seiner initialen Meldung berücksichtigt haben

Fazit: ja die Lösung funktioniert und erscheint stabil

...daher scheidet die offensichtliche einfache Lösung aus: nämlich einen size-request in der Größe des Icon zu setzen...

Stattdessen müssen wir dieses Minimum explizit in die Verarbeitung des size-Constraint einarbeiten, und dabe auf der natural-size des Icon aufbauen

Nein! minimal und natural size sollten gleich sein

wenn, dann muß man das dynamisch implementieren...

zwar machen wir im Ctor zuletzt ein show_all(), welches dann erst diese Layout-Berechnung anstößt. Aber zum Zeitpunkt der Allokations-Zuteilung für das Container-Widget (ElementBox selber) haben die nested-children noch keine Allokation bekommen; zwar gelten sie schon als "mapped" und "realized", aber offensichtlich führt eine Änderung des visible()-Status dazu, daß das damit ausgeblendete Kind-Widget schon gleich gar nicht gezeichnet wird

ersichtlich aus Trace-Meldungen, welche ich zur Analyse in den get_preferred_*-Funktionen und beim Setzen der Allokation hatte. Auch auf dem Kind-Elementen sehe ich keine erneuten Aufrufe

BEDINGUNG: Constraint < Vollgröße

  • Vollgröße ist im Widget gespeichert und wird nach Textänderung ermittelt (‣damit implizit auch initial)

BEDINUNG: cH > aktuelleHöhe

BEDINUNG: cW > aktuelleWeite

BEDINGUNG: ΔName > goal

  • Hilfsfunktion reduce(Name) : kann im Extremfall den Namen ausblenden

BEDINGUNG: ΔMenu > goal

  • Hilfsfunktion reduce(Menu) : kann das Menü ggfs ausblenden und liefert das dadurch erzielte Delta

Hier kein Test mehr notwendig; mehr als alles ausblenden können wir nicht

Mehrstufige Prüfung mit Hysterese (um Flackern zu vermeiden)...

  • rechnerische Prüfung: Nominalgröße Icon + Hysterese < cW und ebenso < cH
  • danach: Icon einblenden und reale Größe ermitteln und gegen Constraint checken

auch hier mehrstufige Prüfung mit Hysterese...

auch hier mehrstufige Prüfung...

  • zunächst rechnerisch...
    • in der ersten (einfachen) Version wird gegen die nominelle Gesamtgröße geprüft + Hysterese
    • in der (geplanten) Vollversion prüfen wir gegen die Icon-Größe + Hysterese, und unterstellen, daß sich der Name dann hinreichend kürzen kann
  • danach wird der Name eingeblendet und reduce(Name) aufgerufen; sollte dies scheitern, muß eine Warnung und Assertion-Failure erfolgen, da die Logik sonst zwangsläufig in ein Schleife mit permanentem Flackern läuft!

premature optimisation??

Normalwerweise funktionieren alle unsere GUIs trotzdem schnell genug.
Aber hier wollen wir diese Sequenz hunderte Mal für jeden Fokus-Wechsel machen...

Problematisch ist, daß hier über zwei Ebenen hinweg und über zwei mal zwei virtuelle Calls gegangen wird

  • wir springen jeweils in eine non-inline-Funktion auf Gtkmm
  • diese delegiert auf eine non-inline-Funktion in Gtk+
  • und diese nutzt u.U zwei drei virtuelle Callbacks (Layout-Trend, Haupt-Dimension, sekundäre Dimension)
  • und jeder dieser Callbacks wir ein weiteres Mal von Gtkmm durch eine vtable dispatched

...denn sie wird für jeden Fokuswechsel und für jeden erfolglosen Versuch erneut durchlaufen, und zwar in den meisten Fällen (Label) bis zum 3.Schritt, nur um dann zu merken, daß eben doch nichts mehr rauszuholen ist.

wenn das Stylesheet eben doch zusätzliche Paddings definiert, dann geht die Rechnung nicht auf, da sie auf den required-sizes der Kind-Widgets beruht, und daher (mit Paddings) zu optimistisch ist. Daher müssen wir zwingend nach einem versuchten wieder-Einblenden die reale Ausdehnung des ganzen IDLabel ermitteln und gegen die Constraints prüfen. Bei der Verkleinerung ist das nicht der Fall, denn da wirken die wegfallenden Paddings als zusätzlicher "Bonus".

Damit muß ich nach jedem Schritt nur einmal die Größe neu ermitteln; diese Werte schlagen dann aus dem λ per Seiteneffekt auf die Variablen des umschließenden Scope durch. Das λ selber ist "scheinbar" nur eine Prüf-Funktion, und wird als Solche an die Hilfsfunktionen gegeben. Nicht schön, aber auch nicht wirklich gefährlich, da sich alles in einem lokalen Namespace abspielt. Habe eine Warnung im Code hinterlassen

Assignment-Operatoren binden weniger stark als Wertevergleiche, daher ist er immer nach dem ersten Reduktionsschritt ausgestiegen

...weil der Constraint ja gelockert sein könnte

das heißt, wenn das Label schon eingeblendet ist, aber nun zusätzlicher Platz verfügbar wird; deshalb dürfen wir hier bei einem bereits eingeblendeten Label nicht pauschal aussteigen, sondern müssen jedesmal die ganze Prüfung machen

weil in dem Fall jedesmal das Label wieder ausgeblendet wird, und dazu insgesamt drei mal die requested_width ermittelt werden muß

...unter der Annahme, daß das Kürzen ggfs.auch verlängern kann, und damit schon relativ nahe am verfügbaren Platz ist. Dann verhindert die Hysterese, daß nochmal geprüft wird

setName(cuString&) implementieren

  • Alle solchen UI-Aktionen kommen aus dem Event-Thread.
  • Jede Änderung der Visibility zieht eine resize-Kalkulation nach sich
  • dabei werden unsere Hooks aufgerufen, stellen die Änderung fest, und setzten ggfs. visible = false
  • Ergebnis: keinerlei sichtbarer Effekt (es sei denn, der Text wird tatsächlich angezeigt)

Unterscheidung resize ⟺ rerender

Aua!

fällt mir schwer... ich sehe andauernd die späteren Probleme

Analyse: welche beweglichen Teile gibt es überhaupt?

...die name-ID muß eindeutig sein (modulo Objekt-Typ), weil allein daraus eine EntryID zu konstruieren ist, vermöge deren die Kommunikation über den Bus gesteuert wird. Daher ist es denkbar, daß die User [optional] eine mnemonische Form definieren wollen, und diese steht dann im Label-Widget. Und noch etwas: der Label-Text wird ggfs. zur Anzeige gekürzt, also muß irgendwo der Basis-Text stehen (es sei denn, man holt sich den via Request über den Bus)

analog wie ein Player; während das Content-Rendering läuft muß der Clip eine Registrierung in einem zuständigen OutputManager aufrechterhalten, um die berechneten Pixmaps dann entgegenzunehmen

...wir speichern ganz sicher nicht ein Mini-Bild für jeden Frame eines Video, sondern brauchen hier wohl eine Art lokales Caching; das heißt, die Pixmaps für einen aktiven Bereich plus eine gewisse Umgebung sind direkt greifbar im GUI. Dieses Caching ist aber nur ein 1st-Level, denn wir wollen den (als sehr elaboriert konzipierten) globalen Frame-Cache auch für diesen Zweck mitbenutzen; schließlich dürften diese Vorschaubilder die häufigsten laufenden Rendervorgänge sein...

default == keines ⟹ leer

...es würde sich um irgendwelche ausgefallenen Use-Cases handeln, und das direkte Anheften der Kind-Widgets würde dann Overhead sparen, gegenüber einem Canvas; weiterer Vorteil wäre der direkte push-back für die Allokation von Screen-Extension (was allerdings im Falle eines size-constrained Widget dann wieder ein Nachteil wäre)

weil ElementBoxWidget selbst keine "business logic" bereitstellt

  • Aushandeln der benötigten Extension
  • Ändern der Detailtiefe (expand / collapse)
  • Setzen einer Ausdehnungs-Vorgabe

...welches u.U. zwar vom ElementBoxWidget aus eingebunden wird, aber eigentlich auf die Inhalts-Ebene delegiert

...wobei das eigentlich nur für den einfachen Use-Case relevant ist; denn im Falle eines komplexen Content-Containers wird die Steuerung, in welche der Container ohnehin eingebunden sein muß (z.B. der Clip-Controller) wohl auf direktem Wege agieren, ohne durch das ElementBox-Widget zu gehen

Ein Stichwort, eine (ggfs. dynamische) Kurzinformation, ein pivot-Frame als Erkennungszeichen

es gibt anderswo einen dedizierten "Content Controller"

und zwar wegen der Separation of Concerns

und das wäre der eigentliche Fall für einen Content-Renderer

das Kind-Widget muß ElementBox überleben; und das bedeutet, es muß ein Sibling sein... (problematisch für die Struktur vom ClipWidget, es sei denn, man macht ElementBox dort zu einem Member — was allerdings wiederum der Anforderung widerspricht, daß das Clip-Delegate direkt ein Widget ist und sich darstellen kann)

...würde also z.B. als Meyer's Singleton bereitgestellt, und müßte den konkreten Kontext für jeden Aufruf zugereicht bekommen; auch die Render-Vorgänge könnte man als Pool darstellen, denn es macht ohnehin keinen Sinn, alle Clips auf einmal zu rendern; wenn ein Render-Vorgang beendet ist, bleibt nur noch ein Buffer-Handle übrig für das resultierende Pixmap

hierbei ist...

Länge: die horizontale Ausdehnung in der zeitartigen Dimension

Fenster: die Position und Länge eines Anzeige-Fensters im Verhältnis zur Länge

...wannimmersich die Display-Metrik ändert (bei jeder Zoom-Anpassung!)

Naja, stimmt nicht wirklich, denn Pixel-Angaben müssen angepaßt werden, also besteht faktisch eine Verbindung

egal ob nun absolut oder relativ, aber die Lumiera Time-Koordinaten sind gemein. Das ist nicht die »wall clock time«

...wobei der Datentyp dieser Verhältniszahlen noch wählbar ist

  • könnte ein Floating-point sein (float würde eigentlich genügen)
  • könnte ein rational sein mit kleinen Integerzahlen

den Medien-Typ

ASSERTION : widget muß bereits realized sein

0000001052: INFO: steam-dispatcher.cpp:301: worker_2: processCommands: +++ dispatch Command("test_fake_injectSequence_1") {exec}

0000001053: INFO: steam-dispatcher.cpp:306: worker_2: processCommands: +++ -------->>> bang!

0000001055: TODO: dummy-session-connection.cpp:259: worker_2: applyCopy: build internal diagnostic data structure, apply a copy of the message

0000001060: TODO: track-body.cpp:93: worker_3: setTrackName: is the track name of any relevance for the TrackBody widget?

0000001061: INFO: ui-style.cpp:145: worker_3: prepareStyleContext: Body-CSS: path=window.background box.vertical box[2/3].horizontal widget[2/2] widget paned.vertical widget box.vertical notebook[1/1].frame paned.horizontal.timeline__page.timeline box.vertical.timeline__body.timeline fork.timeline__fork

0000001062: INFO: ui-style.cpp:154: worker_3: prepareStyleContext: RulerCSS: path=window.background box.vertical box[2/3].horizontal widget[2/2] widget paned.vertical widget box.vertical notebook[1/1].frame paned.horizontal.timeline__page.timeline box.vertical.timeline__body.timeline fork.timeline__fork frame.timeline__ruler

Clip:: hsize=25

Clip:: hsize=50

Clip:: hsize=25

0000001085: ASSERTION: element-box-widget.cpp:327: worker_3: imposeSizeConstraint: (label_.get_realized()) ElementBoxWidget layout constraint imposed on widget not yet realized by GTK

...denn ich ging von der Annahme aus, daß dieser Code erst im nachfolgenden draw()-Event aufgerufen wird; und diese Annahme hatte sich bisher stets bestätigt, obwohl im ctor von ElementBox direkt ein show_all() aufgerufen wird

...kann nicht recht erkennen, woher der call kommt.

Klar ist nur, die Objekte sind schon konstruiert aber die neue Timeline noch nicht angezeigt; genauer kann man sogar sagen, der Fehler tritt erst auf, wenn das GUI sichtbar werden soll, denn wenn ich sofort in den Konstruktoren Breakpoints setze, und damit dann zum Eclipse-Fenster wechsle, tritt die AssertionFailure nicht auf, solange bis man dem Lumiera-Fenster wieder den Fokus gibt

liegt an der inkrementellen Natur der Layout-Zuteilung

und danach feuert der StructuralChange-Listener

nach dem Setzen eines neuen Label-Texts müssen wir die Länge des IDLabel erneut ausmessen, und dazu müssen alle seine Komponenten vorrübergehend visible() gesetzt werden; hatte bisher darauf gesetzt, daß der size-constraint-Algo dann von selber wieder auf den richtigen Status kommt...

Höhe nur 16px, obwohl doch das Icon mindestens 18px braucht (incl.Border), und das get_required_height() diesen Wert eigentlich liefern sollte

und schaltet den Frame wieder zurück auf ein Text-Label, das natürlich dann in keinster Weise der size-Constraint-Kontrolle unterliegt

...den eingebauten Debug-Meldungen zufolge zu urteilen

get_has_window()  ➔ false

per default greift sich ein Button ja den links-Klick, und das soll auch so sein!

sets für eine feste session::Timeline

Verwaltung autmatisch via ViewLocator -> PanelLocator

es gibt eine EmptyTimeline

Frage ist, wie viel des Verhaltens programmieren wir selber explizit aus,

und welchen Teil des Verhaltens überlassen wir GTK

Standard UI-Mechanik überlassen wir GTK

...und das heißt, wir betreiben ggfs sogar erheblichen Aufwand,

um Standard-Mechanik auch über die Standard-Mechanismen abzubilden.

Aus Gründen der Konsistenz und Zukunftsfähigkeit

unser InteractionControl ist eine Zwischenschicht

InteractionControl

  • ist ein eigenständiges Framework in der Obhut des InteractionDirector
  • lauscht und empfängt die Benutzer-Events zweiten Grades (nicht absolut low-level Pixel und Mausbewegung)
  • erzeugt Trigger, auf die die normale UI-Mechanik reagiert (z.B. setzt den Fokus, scrollt)

Das war zwar schon meine Bauchgefühl,

habe aber sicherheitshalber diese Analyse nochmal gemacht.

Details im  TiddlyWiki....

also bitte nicht mit statischen Globals arbeiten!

braucht feste Speicher-Addresse

..d.h. der Controller muß wieder auf das Widget zugreifen

und sei es auch bloß über ein Interface!

aber: Binding im Diff-System durchaus möglich

...denn:

das Diff-System verlangt nicht, daß Kinder in der Collection auch Tangible sind.

Es verlangt nur

  • daß wir wissen, wie wir Kinder machen
  • daß wir für ein gegebenes Kind ein DiffMutable beschaffen können

wichtigstes Beispiel: wir verwenden einen gemeinsamen Canvas  (Gtk::Layout) zur Darstellung.

Das bedeutet: viele Kind-Widgets werden auf diesem Canvas platziert und müssen daher mit ihm interagieren

verwende eine Beziehungs-Entität

  ViewHook   ⁐   CanvasHook

d.h. das durch dieses ViewHooked eingefügte Element definiert auch ein lokales Koordinatensystem.

Allerdings lebt das erzeugte Kind-ViewHooked danach eigenständig und es gibt keine weitere Beziehung

Erläuterung: man könnte auf die Idee kommen, die vier notwendigen Operationen auf dem Ziel durch Lambdas zu verkapseln. Wenn man dann aber nicht aufpaßt, resultiert das in einer Closure für jedes dieser vier Lamdas, und diese Closure hält zumindest einen Pointer auf das Zielobjekt. Der Vorteil eines solchen Ansatzes wäre natürlich, daß der konkrete Typ von Quelle und Ziel aus der Definition des ViewHook verschwindet (allerdings auch nur, wenn diese Lambdas in std::function-Objekte gewickelt sind)

von Element-Typ A nach Element-Typ B...

Konkret:

  • gegeben ein ViewHook<TrackBody>
  • kann ich daraus einen ViewHook<Gtk::Widget> gewinnen?

da der ViewHook schon zwei Pointer zu zwei Entitäten halten muß, kann man Redundanzen vermeiden, indem man ihn an einem dritten Ort anbringt. Nämlich an einem Ort, an dem ohnehin schon eine Dreiecksbeziehung mit diesen beiden Elementen besteht

Dies wäre dann das Rückgrat in der View-Behandlung in der Timeline

Jeder neue TrackPresenter bekommt zur Erzeugung einen Funktor, mit dem sich der von ihm gehaltene DisplayFrame in einen Vater-Kontext "einhäkeln" kann...

...wohl ehr nicht, aber sie sind nicht allgemeingültig, das ist das Problem

alternativ kann man eine konkrete Template-Funktion deklarieren

denn, ohne daß dies nach Außen sichtbar wäre, ist TrackBody selbst die ViewHookable-Implementierung

man muß nicht eine explizite Spezialisierung des ganzen Interfaces schreiben

man schreibt die konkrete Implementierung direkt bei der Implementierung des Zieltyps mit

wir könnten zwar Widgets aufbauen, diese aber dann später nicht umordnen oder zerstören

durch die Lösung mit dem "Einhäkeln" via temporär durchgereichtem Lambda!

um zu dokumentieren, daß wir in diesem Visitor-Mischfall am Konzept vorbei implementieren

...und nicht hierarchisch! Letzten Endes geht es nur darum, Widgets an einen gemeinsamen Canvas zu heften

nämlich der Vater-TrackBody

will sagen: es gibt keinen Zugriff, der vom Canvas ausgeht, durch die TrackBodies durchsteigt, und dann indirekt in den Cavas zurück greift. Denn -- zumindest im Moment -- handelt es sich um zwei klar geschiedene Belange: einmal das Rendern der Track-Struktur, und andererseits das platzieren von Clips auf dem Canvas

...denn diese wird von der Auswertung der Track-Struktur (vor dem Zeichnen) etabliert, und andererseits beim Platzieren eines Clip genutzt.

und diese Info ist komplett redundant

alle anderen Daten in TrackBody sind nicht redundant

denn der Display/Manager bzw Canvas ist zwar "quasi global", aber eben nicht wirklich, denn er ist für eine Timeline zuständig. Also genau die Art Relation, für die man typischerweise DI macht

weil nach hier etablierter Policy diesen erlaubt wäre, "von oben" in den trackPresenter.displayFrame.trackBody reinzugreifen für die startLine_

...sondern zur Timeline gehört

ViewHooked wird nun zum eigentlichen Front-End

wie ich's auch drehe und wende, daran führt kein Weg vorbei. Weil die Referenz eben nicht wirklich global ist, sondern einen root-Kontext darstellt (nämlich die umschließende Timeline).

...also wenn wir ViewHook für einen anderen Kinder-Typ brauchen

fragt sich von wem wir dieses Hookable für einen andern Typ bekommen

denn wenn alle Methoden auf dem TrackBody liegen, kann man diese auch von außen direkt aufrufen

es sei denn, man packt dann den ViewHook in eine eigene Sub-Komponente

....durch den Versuch, "die Hooks" zu einem allgemeinem Gui-Konstruktionsframework mit double-Dispatch auszubauen, habe ich das bestehende Design erheblich geschärft, und für einige Teilaspekte viel sinnigere Lösungen gefunden. Am Ende hat sich gezeigt, daß meine Vision nicht realisierbar ist, und zwar fehlte eigentlich nur "eine ganz kleine Lücke" ― aber ich bin erfahren genug, um zu wissen, daß man eine solche Situation nicht durch Zaubertricks retten kann. Daher habe ich diese Vision in aller Form begraben, aber die Design-Verbesserungen entsprechend heruntergestuft und so erhalten.

und funktioniert gut für Clip-Widgets auf der Timeline

2/2023

  • diff-Binding UNIMPLEMENTED
  • TrackBody hat eine Collection vector<unique_ptr<RulerTrack>>
  • er verwendet diesen bisher aber nur, um die vertikale Ausdehnung zu berechnen

int hookAdjY (int yPos)  override { return yPos + body_.getContentOffsetY(); };

dieser greift im »Constructor« binding...

  • vom TrackPresenter auf den DisplayFrame zu, um sich dort den CanvasHook zu holen (das ist der DisplayFrame selber, was aber opaque bleibt)
  • und gibt eine Referenz auf diesen CanvasHook dann an den ClipPresenter-ctor weiter....

erleichtert das Testen

aber dieses Design ist schief

...weil der Vater ja auch neue Kinder "hooken" kann

d.h. zugleich wird die alte Verbindung gelöst und die neue konstruiert

es gibt keinen prinzipiellen Grund, warum sie scheitern könnte

denn auch der Canvas ist ein Gtk::Container und hat eine Liste von Widgets

...insofern nämlich auf dem Canvas einzelne Clip-Widgets liegen, und diese können wiederum einen eingebetteten Canvas haben, den man ggfs auch in Form eines nested ViewHook handhaben möchte. Die Details dazu sind aber im Moment noch nebulös, und ich sollte diesem Fall (noch) nicht zu viel Beachtung schenken.

...und letztlich habe ich sie auch im Code komplett getrennt. CanvasHook und ViewHook haben nichts (mehr) miteinander zu tun

"the children of your friends ain't necessarily your friends"

...sonst müßte man das Einfügen als eine weitere (protected)-Operation auf dem ViewHook ausdrücken und könnte dann die Erzeugung des ViewHooked fest in den ViewHook ABC implementieren....

...hab mich davon überzeugt, daß die Namen anders herum verwendet werden sollten.

Das hängt auch damit zusammen, daß in der Praxsis die urspünglich konzipierte »Beziehungs-Entität« niemals eigens und eigenständig auftretend wird; vielmehr bekommen wir es mit einem speziellen Dekorator zu tun. Und dieser wird besser ViewHooked<X> heißen. Damit ist das bisher schon verwirrend benannte "ViewHookable" komplett daneben, und es erscheint viel sinnvoller, diesem den titelgebenenden Namen ViewHook  zu zuzuordnen

weil nun ViewHooked schon als ctor-Parameter einen ViewHook bekommen muß...

Entscheidung: NonCopoyable

denn vernünftigerweise muß man davon ausgehen, daß der Canvas (oder wo auch immer das element platziert wird) sich einen Pointer speichert, und diesen später auch aktiv verwenden wird

weil dieses nicht weiß, daß es sich um eine Subtyp-Beziehung handelt

...ich wollte dadurch ausdrücken, daß das übergebene ViewHooked<Widget>& ursprünglich schon einmal geHooked worden war. Tatsächlich hat ja im originalen Design der ViewHook das Hookable sogar erst konstruiert, und niemand sonst konnte das. Da wir aber nun inzwischen immer mit einem ViewHookable mit eingebettetem Widget arbeiten, muß dieses freistehend konstruiert werden, und des gibt keine direkte Möglichkeit mehr, diese "Verdongelung" auszudrücken. Und außerdem sind auch alle weiteren Ideen aufgegeben, welche auf eine engere Verzahnung der Interfaces aufbauen würden (Stichwort "quer-Beweglichkeit").

...praktisch könnte es zwar sein, daß wir darauf angewiesen sind, das Widget schon zu kennen. Konkret ist das aber im Moment nicht der Fall, und ich sollte mir darüber jetzt auch keine Gedanken machen; das Design muß ohnehin später nochmal überarbeitet werden...

...und ich glaube, das ganze Konstrukt wird nicht wesentlich "tiefer" werden

Ursache ist ein schiefes Design

...und das ist wohl entstanden, weil ich ursprünglich einen generischen Visitor im Blick hatte; es hat sich aber dann gezeigt, daß eine solche universelle "Quer-Beweglichkeit" weder notwendig noch wünschenswert ist

again the problem with the reversed order due to forward_list

die Vertiefungen im Anzeige-Profil

auf der rechten Seite, wo der Content angezeigt wird

Das muß auch so sein, denn sonst wäre das systematische Modell und die Controller zu eng mit dem Display-Code verwoben. Der Nachteil ist aber, daß derart aufgedoppelte Struktur bei jeder Strukturänderung invalide wird

Problem: Slave-Timeline

...für die dritte Lösung, die Repräsentation bereits in der Session

Problem: alle Timelines der Session repräsentieren?

grundsätzliches

Problem

speziell die Umordnungen ergeben sich

...und der Dekorator würde die beobachteten Operationen

an diese Notifikations-Schnittstelle senden.

Implementiert würde sie vom jeweiligen Widget

korrekt wäre, die Diff-Verben mitzulesen.

Das geht aber nicht, weil wir intern (aktiv) iterieren.

Wollten wir das doch, müßten wir das gesamte Diff-Applikator-Design wegwerfen.

Da aber eigentlich eine 1:1-Zuordnung zwischen Diff-Verben und Operations-Primitiven besteht,

könnte man trotzdem (mit etwas Hängen und Würgen) noch hinkommen.

Der Dekorator würde also auf dem TreeMutator sitzen...

Weil wir die "skip"-Operation für zwei Zwecke verwenden,

und man im Skip nicht weiß, ob man das Element überhaupt noch anfassen darf,

denn es könnte ja auch ein von "find" zurückgelassener Müll sein.

Daher gibt es die matchSrc-Operation. Effektiv wird die aber nur bei einem Delete aufgerufen...

  • man sitzt mit dem Detektor unter dem API
  • dadurch entstehen "ungeschriebene Regeln", wie das API auzurufen ist
  • alternativ könnten wir die Operationen komplett 1:1 definieren, also eine explizite delete-Operation einführen
  • dafür würde dann die matchSrc wegfallen, was praktisch alle sinnvollen Unit-Tests stark beschränkt.

nach der Mutation erfolgt Display-Neubewertung

interagiert mit den Presentern

d.h. eine LUID

weil dies ggfs vom Theme her schon gestyled wird

...denn die CSS-Node-Namen von Custom-Widgets kann man via GTKmm nicht ändern.

Das geht nur, wenn man das Custom-Widget direkt per C erzeugt,

weil der betreffende Aufruf gtk_widget_class_set_css_name (GTK_WIDGET_GET_CLASS(gobj()), "my-node")

in der C "class init function" passieren muß

wir lassen es offen, welche Art von ID das ist.

Irgend eine BareEntryID genügt

...da wir unterstellen, daß das Gegenstück im Session-Modell,

von dem der Diff ausgeht, ebenfalls den Root-Track als festes Attribut hält.

Daher sollte eine inkompatible Strukturänderung überhaupt nicht auftreten können

...abstraktes Interface

latürnich

...den muß jeder individuell implementieren,

um die Bindung herzustellen

theoretisch könnte man eine Timeline ohne Sequenz

oder eine Sequenz ohne root-Fork zulassen

Fazit: wir lassen das nicht zu, erzwingen das bereits per ctor

und erwarten abweichend vom Standard ein vollständiges Skelett im INS-Verb

Thema "Darstellung von Objekt-Feldern im Diff"

da hab ich mir ausgiebig Gedanken darüber gemacht (in dieser Mindmap)

  • entweder ein Feld ist wirklich optional belegbar, dann kann es mit dem Diff kommen
  • wenn dagegen ein Feld zwingend befüllt sein soll, muß man das über den Konstruktor erzwingen
    in diesem Fall müssen alle Daten bereits mit dem vorangehenden INS kommen,
    welches den Konstruktor-Aufruf auslöst

die betreffenden Felder sind echt optional.

Der Ctor belegt sie mit einem sinnvollen Leerwert

Das Objekt muß so geschrieben werden, daß es mit den Leerwerten umgehen kann,

was typischerweise heitß, daß es verschiedene Betriebsmodi bekommt.

Das Diff kann dann später die konkreten Werte für die Attribute nachliefern;

typischerweise wird es das in einem Populationsdiff sofort als Nächstes machen.

zwei mögliche

Konsequenzen

funktioniert fast immer

"was kann denn schon passieren??"

und wenn nicht, dann Crash

Betriebsart "partiell initialisiert"

..hier das Widget, das ebenfalls

  • nur partiell aufgebaut existieren können muß
  • später sich dynamisch erweitern können muß
  • in der Behandlung der UI-Signale ebenfalls checks einbauen muß

einen Fall, der praktisch nie auftritt

und zwar interessanterweise über Kreuz gegliedert

  • die Ctor-Lösung (hat aber etwas mehr Umsetzungsaufwand)
  • die "wird schon klappen"-Lösung

wenn alle Objekte wirklich auf partiell initialisierten Zustand vorbereitet sind,

und auch über ihre APIs dem Nutzer diese Unterscheidnung mit aufzwingen

...welche darin besteht,

daß man überall, in der Fläche, sich um Zustandsabhöngigkeit kümmern muß,

und deshalb dazu neigt, das Problem jeweils wegzutricksen.

Es besteht also die große Gefahr, zu "sündigen" und

heimlich in den "wird schon nix passieren" Fall zu geraten.

das heißt, nur diese Lösung gründet in der Natur der behandelten Sachverhalte.

Wenn etwas seinem Wesen nach nicht optional ist, dann wird es auch nicht optional behandelt.

Es ist keine weitere Argumentation notwendig.

...nach allen gängigen Prinzipien der instrumentellen Vernunft.

KISS

YAGNI

"fokussiere Dich"

hey, es ist mein Leben

...hab ich mich je anders entschieden?

wenn ich mich überhaupt entscheiden konnte...

  • die Lösung entspricht der inneren Natur der Dinge
  • sie bedeutet zwar (leider) eine zusätzliche Verpflichtung beim Erzeugen des Diff
  • aber im Fehlerfall scheitern wir halbwegs sauber (Exception, dysfunktionales UI)

...nochmal zusammengefaßt

  • immer wenn ein Feld seinem Wesen nach zwingend gesetzt sein muß (und aus keinem anderen Grund)
  • dann wird dies per Konstruktor so erzwungen
  • daher muß dann im Diff bereits im INS-Verb die notwendige Information transportiert werden
  • das heißt, bei der Diff-Erzeugung muß man aufpassen und an dieser Stelle bereits einen Record mit den Daten liefern

Widgets arbeiten stets in Pixeln

per »einhäkel-λ«

Um eine tatsächliche Indirektion einzusparen, muß die Implementierung schon mit der jeweiligen Interface-Deklaration zusammen sichtbar sein. Flexibilität ist dann nur noch durch Parametrisierung der Implementierung möglich — was im konkreten Fall aber durchaus denkbar wäre, da es sich letztlich nur um eine affin-lineare Transformation handelt (und wir uns darauf dann limitieren würden)

↯ Nein!

Diese Trennung ist praktisch unverzichtbar, allein schon aufgrund der getrennten Allokation: das Widget wird irgendwann erzeugt und bekommt einen Pointer auf einen CanvasHook, welcher zu der Zeit schon besteht, und vermutlich auch länger leben wird. Eine andere Frage ist allerdings, ob dann auf dem CanvasHook ein virtueller Call gemacht wird

CanvasHook könnte ein konkretes mix-In sein; d.h. zumindest die DisplayMetric wäre zwar als weitere mix-in-Komponente in einem anderen Header definiert (um den Code klar zu halten), aber letztlich bereits auf Interface-Ebene komplett in der Implementierung sichtbar. Die zwei Faktoren der affin-linearen-Transformation wären mutable Variable, die per Setter geändert werden können...

  • für den Client erfolgt die Abstraktion beim Zugriff per up-Cast (d.h. er bekommt einen CanvasHook-Pointer)
  • das konkrete TimelineLayout erbt von CanvasHook und inkorporiert damit genau diese Implementierung

⟹ der verbleibende Knackpunkt ist das Einbinden des (relativen) Canvas-Hook

...und das nutzt explizit die bestehenden Indirektionen aus...

  • Client nimmt einen DisplayHook per up-Cast (und weiß nicht, daß es sich um einen DisplayFrame handelt)
  • getDisplayMetric() geht durch die VTable; das Interface legt sich überhaupt nicht fest, von woher die DisplayMetric stammt
  • die Impl in RelativeCanvasHook delegiert auf refHook_.getMetric
  • refHook_ ist selber wieder ein DisplayHook, und wir wissen nicht, wer ihn bereitstellt
  • tatsächlich bestimmt der top-Level DisplayFrame darüber, indem er ihn an das TimelineLayout anbindet

...das ist in der Tat eine überflüssige Indirektion, die jedoch einen Storage-Slot einspart (weil die VTable ohnehin schon da ist). Man könnte sehr wohl schon im Interface CanvasHook einen Pointer auf eine DisplayMetric vorsehen

...das ist die minimale Einsparung...

DisplayMetric wäre demnach schon als Interface auf eine affin-lineare-Transformation festgelegt, und würde nur per Setter parametrisiert...

Da im Clip die Anfrage an die Metrik-Übersetzung x-fach (bei jedem Fokus-Wechsel) erfolgt, dagegen das generelle re-Positionieren auf dem Canvas nur nach einem Strukturwechsel oder Zoom-Wechsel, wäre eine sehr einfache und effektive Optimierung, sich den DisplayMetric-Pointer im individuellen Clip eigens zu speichern. Das wäre allerings ein zusätzlicher »Slot« pro Clip! ... eine Variante mit einer zusätzlichen Pointer-Indirektion würde den DisplayMetric*-Pointer im CanvasHook speichern, und damit einmal pro Track (also wesentlich kleinerer Hebel)

Diskussion ⟹ später!

  • die aktuelle Implementierung ist zwar x-fach indirekt, dafür aber sauber faktorisiert
  • sie ließe sich daher rein auf logischer Basis auch an weitere Use-cases anpassen
  • zumindest die Optimierungen auf einen DisplayMetric-Pointer klingen akzeptabel
  • allerdings sollten solche Optimierungen nicht premature erfolgen, sondern im Rahmen von konkreten Messungen in einer voll besetzten Timeline

Dienst: aktueller Skalenfaktor + Offset

woher soll man das wissen?

muß eigentlich schon von der Session geliefert werden

 �� generische ZoomWindow-Komponente

Name: ZoomMetric

direkten Bezug auf die Timeline vermeiden!

sollte callable sein

...damit würde eine std::function konstruiert,

welche das Target (per Referenz / smart-ptr) aufruft.

Der Change-Listener wird im ZoomWindow zugewiesen,

was lt. C++-Referenz den gleichen Effekt hat wie

  function(std::forward<F>(f)).swap(*this);

Fazit: man sollte also die Layout-Berechnungen machen in dem Zeitraum zwischen dem Aufruf der get_preferred_*-VFunks und dem on_size_allocate()-callback. Denn in dieser Phase sind die Widgets bereits gemapped (=einem GDK-Fenster mit Koordinaten zugeordnet) und der visibility-Status steht bereits fest, aber es wurde noch nichts realized und damit auch noch nichts (erneut) gezeichnet. Sofern kein »fill-Layout« verwendet wird, liefern die get_preferred_width|height()-Funktionen als Standard-Wert genau das, was GTK dann nachfolgend zuteilen wird (die MIN-Werte werden praktisch nicht verwendet). Später könnten Widgets sich noch verkleinern, und GTK könnte eine größere Allokation zuteilen.

�� Es gibt keine praktikable Möglichkeit, die aktuelle reale Größe in Pixeln von Außen und generisch für jedes Widget in Erfahrung zu bringen. Wenn man selber ein custom-Widget schreibt, kann man die reale size-Allocation wegspeichern und zugänglich machen; dann bleibt aber immer noch das Problem mit den Dekorationen und dem Padding per CSS

es ist hier kein by-Name-Zugriff,

weil ein Layout-Manager immer nur im Bereich eines TimelineWidget relevant ist

...und für Signale sind diese Probleme bereits gelöst

  • per direktem Zugriff via Interface DisplayManager
  • per Zuweisung an die einzelnen TrackBody (signal == smart-ptr!)

Es ist sinnvoll, die Feststellung einer Änderung vom Berechnen der neuen Werte zu trennen.

Die Notwendigkeit einer Neuberechnung wird also systematisch festgestellt,

aber die tatsächliche Neuberechnung erfolgt erst spät, und bei Bedarf

muß nach GTK's Behandlung gemacht werden

Grundlagen müssen vor GTK's Behandlung erfolgen

die Teilelemente speichern ihren Wert

Zeichencode verwendet diesen

...deutet darauf hin, daß in diesem Zusammenhang etwas schiefläuft

wenn man die mark "test"-Nachricht an eine Timeline schickt, die vorher per Population "reingeschossen" wurde, ohne sie jemals im UI anzuzeigen. Das heißt, im Moment haben wir da definitiv eine offene Flanke -- allerdings ist das ganze Thema auch bisher ehr ein draft

isnil (profile_)

...bis jetzt genügt es, das TrackProfile selbst als "dirty-flag" zu misbrauchen. Aber ich vermute, längerfristig bekommen wir irgendwo im GUI noch weiteren "dirty-state" ⟹ und dann bleibt die Frage, wo siedeln wir den an...

da links->rechts auf die Zeitachse abgebildet ist

dieses speichern als TrackBody::contentHeight_

wird komplett erledigt durch TrackBody::establishTrackSpace

Auslösen durch signalStructureChange

Das Schöne an diesem Ansatz wäre, daß er für den User komplett natürlich wirkt; solange man gleichartige Clips in einer Timeline liegen hat, würde sich dieses Konzept überaupt nicht auffällig bemerkbar machen; und ein weiterer Vorteil wäre, daß man es als Weiterentwicklung des 1. Lösungsansatzes betrachten kann...

es gibt keinen einfachen und performanten Mechanismus, über den ich mir irgend ein Widget merken und später noch sicher addressieren kann, selbst wenn eine Diff-Nachricht inzwischen die Anzeige umbaut und das bezeichnete Element inzwischen gelöscht ist. Und zwar deshalb, weil wir hier von echten einfachen GTK-Widgets reden, und nicht von unseren "Tangibles", die am UI-Bus hängen. Selbst wenn wir noch gesicherte »Mediatoren« dazwischenschalten, also z.B. diese Nachrichten über jeweils ein zuständiges model::Tangible zustellen, dann haben wiederum diese das identische Problem: sie bekommen nicht garantiert mit, wenn eines der von ihnen verwalteten Widgets inzwischen nicht mehr existiert. Wir bräuchten

  • entweder ein universelles Addressierungs-Schema, das aber dann auch bis auf das einzelne Widget herunter wirksam sein müßte
  • oder jeder Mediator (oder zumindest der DisplayManager selber) müßte Diff-Listener installieren, die auch sicher für Diffs auf ihre Kinder ansprechen (!)

Nein: keine Abhängigkeiten,

sondern Iterationen!

Warum? Weil wir immer die Hilfe von GTK brauchen, um aus unseren veränderten Vorgaben eine Platz-Allokation zu machen. Und diese Hilfe können wir nicht synchron anfordern, sondern triggern sie indirekt durch setzen neuer Mindestgrößen...

Warum "fast"? weil dann der Header-Pass eben doch eine globale Information transportiert, und zwar den Übertrag. Da die Auswertung aber auch hier ausfächert (über die Header in den Clips), sollte man tatsächlich versuchen, globale Datenhaltung (jenseits der Auswertungs-Phase) zu vermeiden!

Rekursion ist immer doppelbödig:

  • angeblich haben wir selbstähnliche Strukturen
  • aber tatsächlich steckt eine Absicht dahinter

weil wir eine Verknüpfung der Body und Header-Informationen herstellen

ausnahmslos von oben nach unten und von links nach rechts!

nicht mehr direkt auf den BodyCanvas als Größe anwenden

Invariante: (nach dem Pass) liegen vorläufig/hinreichend brauchbare Layout-Maße vor

insofern fällt die doppelte Abstraktion und indirektion hier weg

hängen garnicht direkt damit zusammen, sondern wurden getriggert durch den "unorthodoxen" Gebrauch des PtrDerefIter

erst mal nur mit einem Button, und die Layout-Logik ehr "geschätzt" den präzise verstanden und umgesetzt. Immerhinn läuft der DisplayEvaluationPass nun, und auch die Buttons erscheinen an der Stelle, an der ich das erwarten würde...

Ende 2022 habe ich dann doch noch das ElementBoxWidget gebaut. Nun verwenden Clips schon mal dieses, aber die ganze Logik der Clip-Anzeige ist auf später verschoben...

Invariante gilt auf einem Fork

...und damit eine Rekursion triggert

...um die Widgets an die nun korrekte Position gemäß Track-Profil zu schieben

...es sollte an geeigneter Stelle einen Reset geben, da jeder DisplayEvalutaionPass grundsätzlich das ganze Layout sauber ausrechnen kann

noch nicht klar, welche Rolle der spielt; ich sehe ihn erst mal vor, weil er möglich ist. Denkbar wäre, daß er durch User-Interaktion entsteht, oder aber auch systematisch generiert wird, um bestimmte Arten von Clips optisch abzusetzen

Clips dürften die häufigsten Entitäten in der Timeline-Anzeige werden. Es müssen tausende bis zehntausende Clips performant gehandhabt werden

die Kontrollstruktur ist bereits gefährlich komplex; spezielle "Schleifen" durch die Innereien eigentlich nicht involvierter Entitäten allein aus Performance-Gründen sollten vermieden werden

  • denn eigentlich wäre das eine "interne Angelegenheit" von CanvasHook ↔ CanvasHooked
  • da aber die Koordinaten-Daten im Clip liegen, muß der Clip involviert werden
  • obwohl dies überhaupt nicht zu den Belangen des Clip gehört

...ich hab ja nicht umsonst in der theoretischen Analyse herausgefunden, daß dieses Schema auf ein Phasen-Modell hinausläuft; die Hoffnung wäre höchstens gewesen, daß in der Praxis der 3.Pass derart degeneriert,  daß man ihn in den 1.Pass einfalten kann

berücksichtigt also nicht die Dekoration und das Padding... obwohl doch eigentlich der 2.Pass das Track-Profil aufgebaut haben sollte

der Test-Diff ist ungeschickt geschrieben

ich wollte mal ein viel generischeres Design schaffen, das sogar eine Art Grundgerüst für das Zusammenbauen des GUI sein könnte. In der vertieften Analyse wurde dann aber klar, daß dieses Design nicht so ohne Weiteres realisierbar ist. Daraufhin habe ich beschlossen, die Idee aufzugeben und stattdessen auf Einzelfälle zu spezialisieren. Und in einem weiteren Schritt habe ich dann die Themen "Grid" und "Canvas" voneinander getrennt

...and "realizes" the widget

commit 5b33605469352f3403d44cb0d77ef3c224895f5b (HEAD, ichthyo/gui)

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Jan 25 03:06:27 2021 +0100

    Clip: fill in minimal implementation to make the clip appear

Aber der Punkt ist, nach der reinen Lehre sollte eine solche Funktion eine Options-Monade zurückgeben. Aber ich wollte stattdessen den guten alten Fallback-Wert. Wenn man das erst mal akzeptiert, dann muß "man" verdammt noch einmal auch die Wertebereiche ernst nehmen

beim ersten Mal wird "nebenbei" festgestellt, daß das Track-Profil (noch)nicht existiert.

...denn selbst wenn wir das für das erste Mal irgendwie hinbiegen, so kann das doch in jedem späteren draw() wieder passieren

...dafür bräuchte ich aber einen Diff-Listener für alle strukturellen Änderungen incl sub-Scope. Das ist nicht trivial zu implementieren, weil die sub-Scopes ja beliebig tief verschachtelt sein können, und alle rekursiv delegiert werden

also z.B: wenn man das TestControl schließt, oder auch nur den Fokus an das Lumiera-Hauptfenster gibt

...nachdem erstmals ein size-request gesetzt wurde, hat sich die tatsächliche Höhe des Widget noch nicht verändert (das wird erst mit dem nachfolgenden resize-event vollzogen). Aber der size-request spiegelt sich sofort in der desired_height wieder. Wir müssen also die Ermittlung der "aktuellen" Höhe darauf aufbauen, damit die schon vorgenommenen Anpassungen nicht nochmal im Delta landen

damit die Höhen für die Kind-Tracks bereits gesetzt sind, wenn die Gesamthöhe des Parent-Track betrachtet wird

...was bisher nur nicht aufgefallen war, weil die Display-Evaluation nur einmal lief;
Nachdem ich das ZoomWindow integriert hatte, zeigten sich diverse Defekte / run-away-Effekte, die erst beoben waren, nachdem ich die Layout-Berechnung im TrackHeadWidget komplett überarbeitet und systematisch ausgestaltet hatte

Clip-Drag reagiert manchmal nicht »live«

nachweislich...

  • die (x,y)-Koordinaten des Clip werden sinnvoll und korrekt aktualisiert
  • der Aufruf geht durch bis auf das Gtk::Layout
  • nach einer Invalidierung des ganzen Fensters springt der Clip plötzlich an die neue Position

wie in Kopf und Rumpf injizieren

abstrahiert den Zugang zum zugehörigen Widget

sub-Frame wird für unsere Kinder erzeugt

Fazit: DisplayFrame kann sich auf Parent verlassen

  • die Parent/Anker-Widgets werden auf ein Interface reduziert, z.B. Gtk::Container
  • man reicht einen halbfertigen Frame rein, in dem bereits die Anker-Widgets gesetzt sind
  • dann ruft man eine Konstruktor-Funktion auf, die die eigentlichen Widgets erstellt und verdrahtet
  • die Anker-Widgets kommen per Ref auf Interface
  • sie werden explizit an den ctor des Controllers übergeben
  • dieser verwendet die Referenzen, um in einem Streich den DisplayFrame incl. eigentlicher Widgets zu erzeugen
  • der ctor des Controllers bekommt eine Ref. auf den Parent-Frame
  • dieser bietet eine virtuelle Methode, mit der sich die Widgets installieren lassen
  • wenn der Controller den DisplayFrame erzeugt, werden dort unmittelbar die Widgets konstruiert
  • sodann rufen wir die builder-Funktion auf, um sie im Parent-Kontext zu verankern

...aber die gesamte Verankerungs-Aktion läßt sich rein auf Widget-Interface-Ebene machen.

D.h. wir rufen den parent-Frame, aber die Implementierung der Methode greift nicht direkt

auf die Widgets im Frame zu, sondern delegiert an eine Hilfsfunktion....

STOP. Damit ist die virtuelle Methode nur verschoben

der ctor des DisplayFrame nimmt einen generischen Typ (Template),

von dem nur erwartet wird, daß er eine "Verankerungs"-Funktion bietet

der ctor des DisplayFrame bekommt einen Funktor oder Lambda,

welchen er aufruft, um seine neu erstellten Widgets zu verankern

da weit über den Code verstreut

ermöglicht (abstrahierten) Zugang zum Canvas über einen ViewHook

denn er erzwingt entweder....

  • die relevante Implementierung in einm einzigen Objekt zu konzentrieren -- was dann den double-dispatch überflüssig macht, denn dann kann man direkt die bekannte andere Methode aufrufen
  • oder, wenn man die verschiedenen Belange auf getrennte Objekte verteilt, müssen sich diese gegenseitig erreichen können, und die hierfür notwendigen Rückreferenzen untergraben die Trennung

Streng genommen würden wir in der Tat einen generischen Quer-Zugriffsmechanismus brauchen.

Allerdings ist dieser entweder nicht implementierbar, oder, wenn implementierbar, dann auch redundant.

Und zudem kristallisiert sich bereits heraus, daß wir es nicht mit einem "Universum" generischer Typen zu tun bekommen, sondern mit einer kleinen Auswahl, für die wir halt dann die Quer-Verbindungen direkt auscoden und gut is...

beim initialen Aufbau sitzen wir in einem ctor-Aufruf,

und zwar der ctor-TrackPresenter

insofern der ctor für den root-Track (fork-root) vom TimelineController aufgerufen wird,

alle anderen verschachtelten ctors aber von den TrackPresentern

weil nur sie durch ihren Display-Frame die beiden Kind-Widgets kennen

inwiefern gibt es Beschränkungen, wenn man ein Kind-Widget von einem Container entfernt?

"This interface is used for all single item holding containers. Multi-item containers provide their own unique interface as their items are generally more complex. The methods of the derived classes should be prefered over these..."

...und hält damit sein unterliegendes C-Objekt am Leben

(genauer gesagt, die Basisklasse Gtk::Object macht das)

es läuft auf eine Sortier-Operation hinaus

und müßte es danach explizit manuell löschen!!

d.h. es wird dann nicht mehr gemanaged (was in diesem Fall hilfreich ist)

...das ist zunächst ein Versuch, ein mühsam errungenes Design zu verifizieren...

ViewHooks können nach Hause telefonieren

denn, wie sich herausgestellt hat, muß das Interface ViewHook hierarchisch heruntergebrochen werden

also einen speziellen Offset für Clips und einen anderen speziellen Offset für Effekt-Marker?

Oder, anders, wie bekommen wir diese Dinger jeweils in den richtigen unter-Bereich des Track-Layouts??

....sonst wird es zur Sackgasse.

Die zugrundeliegende Idee war gut, aber ich hab mich in einer cleveren Implementierung mit einem Lambda-Parameter festgefahren. Zwar brauche ich nicht die volle Generizität und Quer-Beweglichkeit, aber der verallgemeinerte Enwurf für ViewHook(ed) ist auch ohne das um Längen besser als die bestehende Struktur

aber keine generische Quer-Beweglichkeit

und zwar, genauer gesagt, eine Konsequenz der Entscheidung, nicht nur einen ViewHook, sondern ein ViewHooked zu machen. Ich hab die Beziehung in's Strukturelle hinen genommen. Damit muß auch die Wurzel diese Struktur unterstützen, und damit wird an dieser Stelle die Abstraktion undicht.

nur wenn DisplayFrame selber ein ViewHook wäre

weil der Trigger irgendwo unten passiert, und nicht auf dem top-Level ViewHook

soweit möglich ohne grundlegende Änderungen

Habe mich hier für eine komplett unterirdische Verbindung entschieden, denn ich möchte keinesfalls den Layout-Manager zu einem sichtbaren Akteur ausbauen — ein »Layout-Manager« wäre eine derart offensichtliche zentrale Autorität, daß der Weg zur „Gott-Klasse“ nicht mehr weit ist

weil dieses nämlich keine Koordinaten bekommt, und daher nicht weiß, in welchem Canvas das zu entfernde Widget steckt

wenn das so relevant werden sollte

es ist auf nein hinausgelaufen....

eigentlich brauchen wir den konkreten Typ nur für den ctor-Aufruf

Proof-of-Concept: grundsätzlich lassen sich die Aufrufe so realisieren

d.h. der Versuch, die Probleme geschickt wegzuabstrahieren.

Außerdem ist ja beim Design des ViewHook aufgefallen, daß der Fall mit dem Positionieren per Koordinaten ehr der Spezialfall ist, und nicht der Basisfall, als welches er modlliert wurde...

Der Lösungsansatz ist entschieden...

  • über das ViewHook-Interface ist die Verbindung zwischen Container und Element abstrahiert
  • darauf aufbauend werden zum Umordnen alle Elemente "beiseite geschoben" und neu eingehängt (in geänderter Reihenfolge)

....theoretisch sollte das so ohne Weiteres funktionieren....

(habs gemäß Konzept schon so implementiert für das Track-Head,
aber noch nie geprüft, ob es auch tatsächlich funktioniert)

aber verschachtelte sub-Tracks werden in dieser gehandhabt

...denn das Patchbay-Widget muß diverse Interna des Tracks beachten,

so z.B. sein Placement, welches teilweise als Properties des Track abgebildet wird.

z.B. Folding

wozu haben wir das Gtk::Grid

�� das ist kein Fehler...
Da sie zuerst eingefügt werden, und ohne vexpand() niemals zusätzlichen Platz aufgreifen, wird das Grid insgesamt nur so hoch wie minimal nötig, und jedwedes Alignment-Setting auf der Zelle mit den Placement-Controls bleibt wirkungslos; anschließend wird die zusätzliche Zeile eingefügt und aufgespreitzt, um den Kind-Navigationsbereich auf den Level des ersten Kind-tracks runter zu drücken

In Anspielung auf

  • eine Leiter
  • ein Notensystem (stave)

(*) stave ist zwar selterner und ausgeprägt Brittisch, staff  wäre geläufiger. Aber staff versteht man heutzutage als Personal, wohingegen stave auch die Leitersprosse oder ein Vers in einem Gedicht sein könnte (BE und AE)

...und zwar die konkret wirksame Ausdehnung ⟹ meiner Analyse zufolge ist das get_width|height()

  • das ist die vom Framework gesetzte Allokation
  • aber vor dem Abspeichern bereits um Dekoration verringert
  • zuzüglich aller Anpassungen für das Flow-Layout

nominell: 96dpi

mein Display: 90dpi

Ich habe beim Upgrade auf Debian-Stretch mal nachgemessen: tatsächlich hat mein Display 94dpi. Demnach wäre der andere weithin übliche Wert von 96dpi präziser. Jedoch bin ich nach mehreren Experimenten bei 90dpi geblieben, da für mich so die Schriftarten die „richtige Größe“ haben — das mag auch daran liegen, daß ich leicht kurzsichtig bin und typischeweise etwas näher am Bildschirm sitze.

ich kann zwar einrasten, aber ich kann weder um einen definierten Punkt drehen, noch kann ich Schnittpunkte ermitteln

"Mathe für 6-13 järige" etc

b = 0.6180339887498948482

wenn man nachträglich einzelne Objekte modifiziert, ändern sicn nur diese, aber keine davon abhängigen weiteren Schritte der Konstruktion

Anweisungen in der Statuszeile: wähle ersten Punkt, setze zweiten Punkt....

man zeichnet nicht, sondern man stellt eine Liste von Operationen zusammen

embedded browser control

Das liegt vielleicht auch an der etwas „alten“ Version von ca. 2018.

Könnte aber auch auf grundsätzliche Limitierungen hinweisen; man hat eben vor allem an das Design von 3D-Bauteilen gedacht

Das UI-Design ist vom konkreten Einzelfall-Nutzen ausgegangen, nicht von einem stimmigen Gesamtentwurf. Alles ist auf das Konstruieren von 3D-Bauteilen in typischem handlichen Format ausgelegt: eine kleine Zahl an Einzelobjekten im cm-Bereich....

Ich komme jetzt mit einer Konstruktion im Sub-Millimeter-Bereich, und ich bräuchte sich überlappende Formen ....

  • die Anzeige-Handhabung wird dann "frickelig"
  • die Beschriftungen sind viel zu groß und lassen sich nicht sinnvoll platzieren
  • es gibt nur die starre Unterscheidung in »Hilfslinien« (construction) und »Geometrie«
  • letztere muß überschneidungsfrei sein
  • für mein Design habe ich dann ca 50 nummerierte Constraints, die anhand der Anzeige kaum mehr sinnvoll nachvollziehbar sind...

Die Geometrie-Elemente in den Sketch-Objekten sind eine Spezial-Implementierung, und keine »first class citizens«. Es ist nicht klar, wie man sie aus Expressions referenzieren kann (kein sauberes DSL-Design). Das Dependency-Management ist viel zu naiv implementiert, und es wird empfohlen, mit Tricks und Kniffen zu arbeiten.

Eine Funktion, um eine Linie gemäß Proportion zu teilen, wird zwar oft gewünscht, ist aber derzeit (2022) noch in Entwicklung. Daher kann man im Moment nur eine feste Basislänge als benannter Constraint vorgeben, und dann andere Längen per Expression =Constraint.basis * (1+sqrt(5)/2  daran binden. Außerdem kann man solche Expressions zwar einmal initial eigeben, dann aber nur noch über das XML editieren.

⟹ ich kann das Ergebnis nicht exportieren

a bytes-like object is required, not "str"

die konkrete Aufgabe ist mit FreeCAD elegant
— aber Resultate sind schwer zugänglich —

Mitte der »Oberdiogonalen«, d.h die Diagonale durch das obere Rechteck mit Höhe Φ-major und voller Breite

↶ freeCAD : rechtsdrehend(mathematisch)

↻ SVG : linksdrehend(im Uhrzeigersinn)

die Cairo Zeichen-Operationen sind modelliert als Druckvorgang

  • ein Pfad definiert die Maske
  • durch diese hindurch wird die Source auf die Surface  (Ausgabe) gestempelt

weil ich diese Benennungen von Anfang an auch so in FreeCAD verwendet habe, und jetzt nicht alles umbenennen möchte

...das ist nämlich potentiell verwirrend: der Außenbogen  bildet die Innenkontur der Klammer, weil der Schwung von der Klammer weg nach oben bzw. unten geht

...aber der Code schaut komplett richtig aus; man muß ja die Differenz zwischen oberem und unterem Anker als Höhe ansetzen, und man muß dabei bedenken, daß die Zeichnung des Balkens dann skaliert wird, jedoch nach Skalierung genau bis zum unteren Anker reichen soll (und eben dieses funktioniert nicht)

es war der int vs double

...denn wir müssen sowiso einen globalen Pass machen, und zwar erst spät, wenn das Layout bereits komplett geregelt ist

TrackHeadWidget::syncSubtrackStartHeight (uint directHeight)

kommend von DisplayFrame::sync_and_balance (DisplayEvaluation&)

TrackHeadWidget::accommodateOverallHeight(uint overallHeight)

  • zum Einen habe ich unbemerkt die getLabelHeight() vom Parent zum Child verschleppt. Das ist aber unter dem Strich korrekt, weil jedes Kind wieder mit einem Label beginnt
  • außerdem gibt es da anscheinend im Grid ein zusätzliches Padding, was mir bisher entgangen war

der Expander in der ersten Spalte dient dazu, das TrackHeadWidget auf eine geforderte Nenn-Höhe aufzuspannen. Anscheinend habe ich die Zellen so definiert, daß diese Diskrepanz als spacing zwischen Label, Content und SyncPadding verteilt wird

mache hier die vereinfachtende Annahme, daß alle Brackets die gleiche Metrik haben

geprüft mit Screenshot in Gimp.

Unter der Einschränkung, daß hier viel Aliasing passiert und das Bild schon relativ klein ist

...der default ist 10, und das erzeugt eine Gehrung nur bis ca 20°

  • das Problem tritt auf bei »Leaf-Tracks«
  • bei diesen liegen hier zwei Zellen nebeneinander, und sonst nichts, was eine Verankerung geben könnte
  • zwar habe ich kein homogenous-spacing gesetzt (geht das überhaupt?)
  • aber die Zelle (0,1) enthält ein weiteres Grid-Control
  • das aber seinerseits nur eine einzige Zelle mit einem Platzhalter beinhaltet
  • ...also selbst komplett unterfüllt ist und keine signifikante Allokation erzeugt

⟹ längerfristig wird sich das sowiso regeln

...denn es muß die obere und die untere Kappe gezeichnet werden, und deren Höhe ergibt sich qua Konstruktion aus der Basisbreite

➩ Skala für gesamte Klammer verkleinern

...um festzulegen, an welcher Stelle in der Hierarchie dieses Styling definiert ist; allerdings machen wir bereits genau dies für die Timeline im Allgemeinen (und zwar genau wegen dem custom-drawing)

...anders als bei den Einzel-Widgets im Timeline-Content, haben wir hier im Header nur ein StaveBracket-Widget pro Track

dpi-Wert für aktuellen Screen herausfinden GTK ⟵ GDK

Streng genommen sollte 'ex' die Höhe eines kleinen-x sein. Aber diese Information ist in vielen Fonts überhaupt nicht zuverlässig feststellbar; die resultierende Angabe kann daher unzuverlässig und fehlerhaft, oder gar nicht vorhanden sein, in welchem Fall es statthaft ist, auf ex ≔ em/2 zurückzufallen. Im Gegensatz dazu weiß man bei 'em', was man bekommt, nämlich die nominelle Font-Size (auch hierin unterscheidet sich CSS von der klassischen typographischen Praxis, in der 'em' definiert ist als die Höhe des großen-M)

Die ganze Doku, sowohl in GTK, alsauch die CSS Spec liest sich so, als wollte man sich vor einer Festlegung drücken. Möglicherweise hatte man Sorge, die dummen Leute würden zu einfache Schlußfolgerungen ziehen, und darob die Auflösung des Bildschirms übersehen — 96dpi ist ja nur ein Default, und ob 1pt= 1/72 inch wirklich gilt, darauf möchte sich niemand festnageln lasse, vermute ich (es hängt nämlich davon ab, daß Monitor und Grafikkarte diesen Wert richtig reporten)

Return value

Type: gint

The size field for the font description in points or device units. You must call pango_font_description_get_size_is_absolute()  to find out which is the case. Returns 0 if the size field has not previously been set or it has been set to 0 explicitly. Use pango_font_description_get_set_fields()  to find out if the field was explicitly set or not.

gegeben...

gesucht...

Fall-1: Font-Size ist absolut gegeben ⟹

Fall-1: Font-Size relativ (in Punkten) ⟹

daraus absolute Font-Size und weiter wie [Fall-1]

ich weiß einfach nicht, unter welchen Umständen dann doch mal ein anderer Faktor als 1.0 im gegebenen CairoContext vorliegt; und wenn das dann passiert, wäre das ein ziemlich obskures Fehlverhalten und man wird dann nicht ohne Weiteres die richtige Stelle finden, um diesen Korrekturfaktor einzubringen...

The device-space coordinate system is tied to the surface, and cannot change. The user-space coordinate system

matches that space by default, but can be changed for the above reasons. The helper functions

cairo_user_to_device() and cairo_user_to_device_distance()  tell you what the device-coordinates are for a user-coordinates position or distance.

das ist der Basiswert für weiteres Layout-Entscheidungen

Style-Klasse: .fork__bracket

Mehrere Aspekte sind hier offen (2023: Abschluß der GUI-Neugründung und vor Playback-Vertical-Slice):

  • die Track-Head-Abschnitte beginnen nicht exakt korrekt bündig mit dem zugehörigen Bereich im Body; möglicherweise ein "run-away" um ein paar Pixel
  • die Scopes reagieren noch nicht dynamisch auf den Inhalt (und es fehlt die aktive Abstimmung)
  • die Scopes sollten auch eine dynamische Status-Anzeige unterstützen

weil der Ruler ja in die Präsentation mit einbezogen ist

....ob es was sinnvolles in einem Overview-Ruler anzuzeigen gibt

Unterscheidung verschoben in die Darstellung

anstatt (konventionell) den Time-Ruler separat explizit auszuprogrammieren,

bekommen wir nun eine gemeinsame Darstellungs-Mechanik,

welche dann aber in zwei getrennten Bereichen jeweils anders parametrisiert

zur Anwendung kommt

Problem: muß immer sichtbar sein

aber es bleibt bei diesem Prinzip.

Des Genaueren, es gibt eine jeweils festgesetzte Menge von Elementen

am Anfang des Track-Profils, welche immer sichtbar bleiben soll

weil Verallgemeinern einer einzigen Aktion

stets besser ist, als repetitives aufdoppeln und variieren

  • content renderer
  • vom speziellen Track abhängige Bereichsmarkierungen
  • lokale und spezielle Overlays sind zu zeichnen

nämlich mit minimalem Admin-Overhead,

indem er sich auf die VTable des einzubettenden Typs abstützt.

Die andere Alternative, der OpaqueHolder, verwendet selber nochmal einen zustäzlichen virtuellen Holder,

und Variant paßt auch nicht, da ich ja mit Template-generierten Subklassen arbeite,

und daher nicht a priori die Liste aller einzubettenden Varianten kenne

nämlich hier der Visitor, der den Aufruf letztlich emfpängt

...denn es handelt sich hierbei um einen Konstruktionstrick.

Scott Meyers spricht deshalb auch immer von "unverersal references" und unterscheidet

diese von einer expliziten RValue-Referenz.

Demnach wäre das Standard-Baumuster, daß alle Glieder in  der perfect-forwarding-Kette

per universal-Reference miteinander verbunden sind. Und im Konkreten fall muß man

das so hintricksen, indem man die std::get-Funktion passend bestückt...

Au weia

d.h. std::get<idx> (std::forward<TUP> (tuple))

wenn ich das Tupel als Referenz anliefere

...sichtbar wenn man ein Trackr-Objekt

als value-Parameter auf der Zielfunktion verwendet.

Im extrahierten Beispiel wird das LValue-copy-initialisiert

wohingegen im realen code, wo auch das std::forward(tuple) dabei ist,

der betreffende Wert dann RValue-Initialisiert wird, d.h. dabei das betreffende Tupel-Element konsumiert

d.h er funktioniert nur, wenn man das std::get<idx> (tuple) unmittelbar an den jeweiligen Ziel-Parameter bindet

nämlich einen, der einen LValue entgegennimmt

und einen, der einen RValue entgegennimmt

steht in keinem Verhältnis zum Zweck

Beispiel: Wenn der Typ selber keinen Support anbietet,

dann nehmen wir immer das volle CopySupport-API, und differenzieren nicht

mehr nach Typen mit reinem clone-support. Was dann tatsächlich dazu führt,

daß der Compiler verucht, den Zuweisungsoperator zu verwernden!

nämlich diejenige für Typen ohne Support auf dem API.

...denn diese Duck-Detector-Metafunktion bildet den Typ eines Member-Pointers,

und dieser Typ enthält explizit den statischen Namen der Klasse, welche die gewünschte Methode trägt.

Und wenn man eine Methode bloß erbt, dann existiert diese Methode, statisch, nur auf der Basisklasse.

Also ist das sogar das korrekte Verhalten.

d.h. wenn zufällig das Interface auch eine Methode CloneInto() enthält, aber mit einer unpassenden Signatur

weil ich dann explizit ein bestimmtes Basis-Interface verlangen werde,

nämlich VirtualCopySupport<IFA>

das ist auch gut so, zu viel Flexibilität schadet (besonders, wenn man sie dann gar nicht unterstützt)

weil nämlich der Trait, für den optimalen Fall, ebenfalls die EmptyBase verwendet, um den Mix-In zu deaktivieren.

Leider haben wir dann zweimal die gleiche Basisklasse in beiden Zweigen der multiple inheritance...

nämlich immer dann, wenn man tatsächlich den CopySupport oder CloneSupport als Basis des Interfaces verwendet...

(was ich bisher in der Praxis so noch nie gemacht habe)

...was sich dann in einer Static-Assertion-Failure äußert.

hab das ganz explizit ausgeknobelt, es fehlt hier genau dieser eine zusätzliche "Slot"

...etwas analog zu meinem TreeMutator.

D.h. man muß alle verwendeten Signaturen auf dem Receiver

erst mal in einem Builder-Konstrukt gewissermaßen "registrieren", um dann den passenden

VerbPack-Typ konstruiert zu bekommen.

d.h. man erzeugt in einem einzigen Aufruf den VerbPack für eine Zielfunktion

zwei verschachtelte, delegierende Konstruktoren,

von denen der zweite, innere die eigentliche Typinferenz macht

anstatt, wie es die bisherige (naive) implementierung macht,

die typename ARGS... auch dafür zu verwenden, den Handler-Typ zu konstruieren.

Mit diesem einfachen Kniff passiert die Konvertierung dann in dem Moment,

wo wir die konkreten Argumente in den vorbereiteten Argument-Tupel im Holder schieben

...jeodch nicht eigens im Test dokumentiert

  • weil das sehr aufwendig wäre und den kompletten Test dominieren würde
  • weil die Implementierung (in dieser Hinsicht) letztlich banal ist: Anwenden eines Tupel auf eine Funktion

sollten z.B. die Konstruktor-Funktionen nicht unmittelbar mit definiert werden?

...weil man stets noch einen Layer darübersetzt?

Wie auch im konkreten Fall das TrackProfile, was dann ein vector<VerbPack> werden würde?

weil ich es geschafft habe,

den gesamten Auswahl-Mechanismus in einen einzigen Konstruktur-Aufruf zu packen.

Man kann also nach Belieben VerbPacks in allen Varianten bauen,

und es obliegt der nächst höheren Schicht, dies auch in sinnvollem Rahmen zu tun...

sondern die Argumente sind in einem Tuple eingewickelt,

und befinden sich tief vergraben in der Implementierungsklasse,

innerhalb eines PolymorphicValue.

das ist nur technisch und etwas häslich,

aber durchaus sauber (unter der Annahme, daß wir uns unsere Token

stets selbst erzeugen und daher auf das korrekte Literal Verlaß ist)

Verhältnis zum Inteface Renderer klären

vorher war nämlich das Profil im Canvas selber.

Jetzt ist das Profil in das BodyCanvasWidget hochgewandert,

und soll von beiden sub-Canvas gleichermaßen jeweils passend interpretiert werden

...daher die Factory

  1. getXxxRenderer(CairoC) -> packt Profil ein
  2. Renderer.drawTo(canvas)

Renderer ist bereits der Funktor

  1. der Renderer. Abstrahiert welcher Interpreter wie genau zum Einsatz kommen soll (Hintergrund/Overla sowie Ruler/Content)
  2. der konkrete Interpreter
  3. die einzelnen Verben im Profil

weil man damit eine generische Klammer bauen kann

und bleibt anderweitig ungenutzt

...ist klarer, und erlaubt nebenbei auch noch, zwei Methoden einzusparen

d.h. ich will den Standardfall als Standardfall sichtbar haben,

und dann diese zusätzliche Filterung oben drauf pflanzen

als ob es darauf ankäme...

Hauptsache, keine zusätzliche Speicher-Allokation

  • get_allocation|width|height  ⟼  aktuell gültiger Wert incl Alignment/Modifikationen
  • get_allocated_size  ⟼  was gesetzt wurde, aber bereits ohne Dekoration

Empfänger(»Slot«): TimelineLayout::sizeZoomWindow (Gtk::Allocation&)

commit 09714cfe28739ceff0b5693447be41166c1cc8d6

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Fri Jan 6 03:09:28 2023 +0100

Timeline: draft solution to interconnect ZoomWindow and scrollbar

After quite some tinkering, instead of extending the DisplayManager interface,

I now prefer to treat this connection rather as an intricate implementation detail:

The TimelineLayout implementation now provides two translation functions,

which are directly wired as slots from the Signals emitted by moving the

hand of the scrollbar; the idea is that these functions mutate the ZoomWindow,

which then triggers a DisplayEvaltuation, which in turn causes the

drawing code to pick up and translate back the new metric and position.

Results look promising, insofar the DisplayEvaluation is now triggered

repeatedly, and the actual window width in pixel is propagated;

...es kommt halt nix Spezifisches aus dem Model, aber es gäbe auch bisher gar keine entsprechenden Diff-Bindings und Model-Properties im GUI

|o| cH(line=0) += (14,14)

|o| cH(line=1) += (68,12)

|o| cH(line=2) += (68,82)

...und das würde auch erklären, warum trotzdem die Zelle mit der Glühbirne (=die direkten Controls) überhaupt nicht aufgespreitzt wird

⟹ es muß das strukturell korrekte Maximum explizit berechnet werden; würden wir nur die rechte Spalte summieren, bliebe eine bereits bestehende Spreizung in der linken Spalte unberücksichtigt

⟹ aber auch das umgekehrte Szenario muß berücksichtigt werden: falls die Gesamthöhe von der Summe in der rechten Spalte dominiert wird, genügt es nicht, blindlings nur eine eigentlich kleinere linke Strukturspalte mit dem Δ zu beaufschlagen

...und zwar über den contentOffset, der relativ zur Start-Zeile gemessen wird, sowie der direkten ContentHeight, zuzüglich Padding

das Präludium gehört nicht zum Track, und für alle sonstigen Größenangaben gilt ein unmittelbarer Bezug auf die Canvas-Koordinaten; diese gelten lokal für den Canvas

dann ist nämlich die startLine = 0;

...ja das ist Insider-Wissen, und dann lieber alles da machen, wo die ganzen technischen Details ohnehin stecken...

demnach ändern wir jetzt den Contrakt

nested enthält Näherung für slope-up

hier können wir nicht exakt rechnen, weil aufsteigende Slopes kombiniert werden — und die Info dazu kennen wir nur beim Konstruieren des Profils. Daher überschätzt diese Höhenangabe die Track-Höhe — bleibt zu sehen, ob das relevant wird

Jeder Track kann 0...N Ruler haben (Ruler = horizontale Scala);

  • einer von diesen ist der »scope ruler«
  • er ersetzt den Track und den gesamten Scope der sub-Tracks in der Präsentation

alledings nur, nachdem man das drawing ein/ausgeschaltet hat....

  void

  Canvas::enableDraw (bool yes)

  {

    shallDraw_ = yes;

   

    // force redrawing of the visible area...

    auto win = get_window();

    if (win)

      {

        int w = get_allocation().get_width();

        int h = get_allocation().get_height();

        Gdk::Rectangle rect{0, 0, w, h};

        win->invalidate_rect(rect, false);

      }

  }

...nur stimmen die Zahlenwerte im Moment nicht;

wir malen viel mehr auf den Canvas, als seine Größe erlauben würde.

Macht trotzdem nix

wenn man PACK_SHRINK setzt, dann weist die umschließende Box initial

dem Canvas die Größe 0 zu (weil er zu diesem Zeitpunkt noch nicht realisiert ist).

Problem ist aber, daß diese Zuweisung später, nach einem set_size auf dem Canvas nicht revidiert wird.

  • die Funktionen zum expliziten Setzen und re-Sizing sind deprecated.
  • eigentlich sollte die Box automatisch auf Größenänderungen der Kind-Elemente reagieren gemäß Flow/Fill-Layout
  • für klassische Widgets wie z.B. Button funktioniert das auch
  • aber auf ein set_size() auf dem Canvas reagiert das Layout anscheinend nicht

Anmerkung 1/2023: seinerzeit habe ich die Mechanik der Layout-Zuteilung noch nicht wirklich verstanden

commit fc5eaf857c687d769df22d6f98a25e8e359e7c49

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Thu Aug 22 17:34:32 2019 +0200

    Timeline: find a workaround to cause the Box to reflow the rulerCanvas

   

    seemingly, the Box with PACK_SHRINK allocates a zero height to the rulerCanvas initally,

    which is correct at that point, since the widgets are not yet realised.

    However, when we later set_size() on the rulerCanvas, the enclosing Box should reflow.

    It does indeed if the child widget is a button or something similar, however,

    somehow this reflowing does not work when we set_size on the canvas.

   

    A workaround is to place a new set_size_request().

   

    TODO: do this more precisely, and only on the rulerCanvas. To the contrary,

    the mainCanvas is placed into a scolling-pane and thus does not need a size-Request.

    Moreover, the latter automatically communicates with the hadjustment() / vadjustment() of

    the enclosing scrollbars.

commit 2390385dc50a8504336f5e1fa1a5dc35eca58f2f

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Wed Aug 21 19:13:55 2019 +0200

    Timeline: implement function to set the drawing canvas size

   

    as can be verified with the debugger, it sets the correct sizes now.

    And it is called only once (unless the content size actually changes).

   

    TODO: however, the visible display of the GTK widgets is not adjusted

Eigentlich benötigt wird das »Aufspreizen« nur in der vertikalen Dimension, damit sich die umschließende Box sinngemäß anpaßt; im Grunde würde es sogar genügen, nur das obere (Ruler)-ScrolledWindow zu dimensionieren, aber ich halte es für sicherer, vom eigentlichen innen liegenden Canvas aus aufzuspreizen, schon wegen der ggfs. dynamischen Dekoration für die Scrollbar. Als Kompromiß setze ich jetzt horizontal eine Mindest-Ausdehnung von 100px (das erscheint ohnehin sinnvoll für eine Timeline), aber in vertikaler Richtung setze ich einen size-Request auf die berechnete Canvas-Höhe

per DUMP-Print verifiziert: Maximalwert der Scrollbar (=hadj) ist identisch mit get_allocated_width

Beispiel:

  • initial wurde Canvas-Größe auf 852px gesetzt (Debugging Code zieht 100px ab)
  • die Allocation für das BodyCanvasWidget wird minimal 852
  • wenn man das Fenster (oder den Teiler der Pane) weiter verkleinert, wird trotzdem 852 geliefert ⟹ nach rechts beschnitten
  • zumindest kann man nun das Fenster tatsächlich verkleinern, und dann mit der Scrollbar horizontal den Haupt-Canvas bewegen.
  • aber der Ruler-Canvas scrollt nicht mit
  • und der Anzeige-Zustand der Scrollbar ist bei mouse-over(Scrollbar) nicht richtig synchronisiert; das wird erst korrekt nach einem Focus-loss/gain

...im Session-Modell für eine Timeline jeweils ein Property hierfür geben...

weil dann nämlich someone(TM)

be Struktur-Änderungen von Außen her "aktiv werden" muß.

Irgendjemand muß mal den Müll runtertragen

und macht sich aber hier im Quellcode über mehrere Funktionen verteilt breit,

ohne daß man ihn hier komplett überblicken kann. Dazu kommt, daß die ganze Verdrahterei

über zwei Lambdas hinweg auch nicht besonders geradlinig und verständlich ist

naheliegend: das BodyCanvasWidget selber

grundsätzlich: es zeichnet der Canvas

everyone and my grandma does it this way...

Kein Wunder daß die meisten UIs aus Sicht des Programmierers ein Albtraum sind

Abstraktion: ViewHook (->Canvas)

das ist ein reiner (pesistent) presentation state

das wäre der logisch richtige Ort

weil dann innerhalb des Canvas alles konsistent ist

...in dem der Timeline body-canvas nämlich liegt

nicht alles wird gezeichnet

wir können nicht von 0 bis MAXINT zeichnen

Konvention: Clips mit start==Time::NEVER werden verborgen

manchmal ist die alte Lösung besser

die Idee einer graphischen Benutzeroberfläche hat sich über die 60er / 70er-Jahre herausgebildet.

Nachdem die Idee einmal gefaßt ist, gibt es nur noch Oberflächendifferenzierung.

Und letztere neigt stets zur "Verschlimmbesserung"

gestapelte Strukturen dieser Komplexität lassen sich nicht durch Schattierung vermitteln.

Wohl aber lassen sich lokale Nachbarschafts-Beziehungen (höhe / tiefer) durch Schattierung hervorheben

und zwar im Wesentliche zur Schattierung der Flanken.

Die Inhalts-Flächen selber sind zu groß und zu strukturiert, um sie per Schattierung zu verdeutlichen

...zum Beispiel um einen "Wall" auch expressiv zu schattieren

....und dient damit als Vorlage für Theme-Autoren

...jedenfalls nicht ohne Code-Änderung

...welches aber lokal auf die Anwendung angewendet wird, mit höherer Priorität

widget->get_style_context();

es ruft den Konstruktor WidgetPath(gobject, make_copy=true) auf

  • erzeugt einen neuen leeren GtkWidgetPath
  • kopiert alle Pfad-Elemente

WidgetPath::~WidgetPath() noexcept

{

  if(gobject_)

    gtk_widget_path_free(gobject_);

}

StyleContext::create()

 void set_path(const WidgetPath& path);

As a consequence, the style will be regenerated to match the new given path.

...d.h. dann aus dem InteractionDirector heraus

-- ja dann kann man auch gleich direkt an den UiManager delegieren!

window:backdrop:dir-ltr.background box:backdrop:dir-ltr.vertical box:backdrop:dir-ltr[2/3].horizontal widget:backdrop:dir-ltr[2/2] widget:backdrop:dir-ltr paned:backdrop:dir-ltr.vertical widget:backdrop:dir-ltr box:backdrop:dir-ltr.vertical notebook:backdrop:dir-ltr[1/1].frame paned:backdrop:dir-ltr.horizontal box:backdrop:dir-ltr.vertical fork.timeline

"Widget is in a background toplevel window"

nur der letzte Node wird gegen pseudo-Klassen gematcht

    for (uint i=0; i<pos; ++i)

      gtk_widget_path_iter_set_state(path.gobj(), i, GTK_STATE_FLAG_NORMAL);

window.background box.vertical box[2/3].horizontal widget[2/2] widget paned.vertical widget box.vertical notebook[1/1].frame paned.horizontal.timeline-page box.vertical.timeline.timeline-body fork.timeline

damit kann man zwar einen Klassennamen für ein Custom-Widget setzen, also z.B.  'gtkmm__CustomObject_xyz' (wobei man xyz an den ctor übergeben hat). Aber das hat keinen Einfluß auf den Tag-Namen, wie er für den CSS-Selector relevant ist

spezielle Regel gesetzt auf: .timeline-page > .timeline-body fork.timeline

warum?

weil sonst der TrackBody um die Dekoration wissen müßte,

wenn er mit der HeaderPane den benötigten Platz aushandelt

...anstatt für jeden schließenden nested scope noch ein weiteres Verb hinzuzufügen.

Damit werden effektiv die "schließenden Klammern" in eine einzige zusammengefaßt

und seinerzeit beschlossen, es vorerst im prelude() zu belassen...

nicht nur die Ruler, auch das Prelude selber ist ein solches gePinntes Element (selbst wenn es leer ist)

meine Einschätzung, basiert auf dem gtk-canvas-experiment.cpp

  • zeichnen kann man "irgendwie" auf dem Canvas.
  • außerdem hat das Gtk::Layout ein hadjustment() / vadjustment()
  • diese können (optional) an eine Scrolled-Pane angekoppelt sein
  • die tatsächliche size-Allocation ist ein davon unabhängiges Thema

rein gefühlsmäßig wäre aber vorher wohl geschickter

denn was passiert, wenn sich durch das Setzen einer neuen Größe der sichtbare Bereich ändert? Löst das dann nicht erneut einen draw()-Aufruf aus??

ist ineffizient, aber der Code ist so klarer

man sollte den GtkStyleContext nutzen

speziell die Antwort von ebassi beachten...

Core-Entwickler von GTK

Nein. Es ist ein GDK-Wrapper/Adapter

TiddlyWiki + doc/technical/stage/style/Timeline.txt

Wir erzeugen künstlich einen widget-Path und verankern ihn sinngemäß an der logisch richtigen Stelle (wobei aber die im Pfad aufgeführten Widgets gar nicht existieren, sondern logische Platzhalter sind). Über diesen Widget-Path greifen wir in einen CSS-Scope hinein, der typischerweise alle seine Styles von umschließenden Scopes erbt; indirekt ist damit aber auch die Möglichkeit geschaffen, diesen speziellen Scope gezielt mit CSS-Regeln zu addressieren — wodurch der Designer direkt auf das Erscheinungbild von Lumiera Einfluß nehmen kann

falls inset, wird er nur innerhalb der border und über den Hintergrund gezeichnet

  • nicht über die border selber
  • nicht über den Content

der GTK-Zeichencode achtet sogar explizit darauf, einen CSS-Effekt mit dem korrekten Mischmodus über den bereits auf den CairoCanvas gezeichneten Content zu legen

und zwar für bestimmte Elemente (Konvention)

weil über alles andere darübergezeichnet wird

den Zeichenvorgang für ein normales Frame-Widget analysieren

Kommentar sagt: it is your responsibility that these two coordinates match

/home/hiv/.local/share/themes/PanRosewoodHIV/gtk-3.0/gtk-contained.css

frame > border,

.frame {

  box-shadow: none;

  margin: 0;

  padding: 0;

  border-radius: 0;

  border: 1px solid @borders; }

  box-shadow: inset 0px 0px 0px 1px shade (@theme_bg_color, 1.15), 0px 1px 2px rgba(0, 0, 0, 0.1); }

herausfinden, warum er solid gezeichnet wird

irrtümlich, denn

  • per default ist outline:none
  • outline wird nur gesetzt, wenn ein Element fokus bekommt
  • Outline liegt um die Border, nimmt aber selber keinen Platz ein (sondern ist ein overlay)

g {

  border-bottom-style: outset;

  border-left-style: outset;

  border-right-style: outset;

  border-top-style: outset;

}

weil er dann mehr oder weniger hartverdrahtet ist

und mehrfach gekapselt.

Schaut alles so aus, als sollte man da nicht rumpfuschen

da wird ggfs eine neue CSS-Kaskade erstellt, oder die bestehende Kaskade erweitert.

Hierfür werden Einträge in Arrays verschoben.

Schließlich wird über die ganze Kaskade iteriert, und die Werte werden aufgesammelt

Die Idee mit dem Cascading funktioniert nämlich nur, wenn man die Oberfläche explizit und bewußt

nach einem Struktur-Schema aufbaut. Dies erfordert eine gewisse zusätzliche geistige Spannkraft.

Typisches Beispiel für mich ist eine Barocke Fassade (mit denen ich mich

in einem anderen Projekt grade beschäftige): Auf den ersten Blick sehen die überbordend

und überwuchert aus, aber wenn man genau hinschaut, ist jedes kleinste Detail

aus einem System abgeleitet -- genau wie es dem damaligen Zeitgeist der Aufklärung entspricht.

Ein barocker Baumeister würde niemals an einem einzelnen Fenster herum-tweaken, weil es sich irgendwie besser anfühlt.

Wenn etwas nicht ausgewogen rauskommt, dann wird eben das gesamte System nachjustiert.

Und genau das ist es, was die geistige Spannweite erfordert.

Der Pragmatiger sagt, dann mach halt das verfickte Klofenster hinten links breiter und gut is.

Das würde zwar ein lokales Problem praktisch lösen, wäre aber unorganisch,

weil es nicht mehr aus dem Fluß des Systems im Ganzen folgt.

Und mit CSS-Selektoren und dem Cascading verhält es sich genau entsprechend.

Wenn man einmal anfängt, an irgend einem Detail herumzufummeln, weil es sich irgendewie so anfühlt,

oder der Kunde es so will, dann ist man ganz schnell an dem Punk, wo Selektoren nicht mehr greifen,

und nur noch !important hilft. Und an der Stelle kommt dann der Praktiker, und pflastert eben

jedes Element mit einem eigenen Selektor zu, so daß man ungestraft überall herumfummeln kan.

Und der Oberpragmatiker generiert den ganzen Müllhaufen dann per SASS order SCSS

Wenn man einen neuen Gtk::StyleContext erzeugt,

und (wie in der Doku beschrieben) den path und den screen manuell setzt,

dann führt ein context_save() dazu, daß plötzlich die CSS-Werte auf Defaults zurückspringen

und ja, den Screen sollte man generell setzten.
Steht auch so in der Doku.

Allerdings hat es bisher auch ohne das bei mir funktioniert.

  • Gut ist es insofern, daß man mit einem render_background bereits alles bekommt
  • allerdings muß der Rahmen dann ganz genau passend darüber gezeichnet werden
  • man kann damit weder den Rahmen schattieren, noch den enthaltenen Content

allerdings verhält sich das normale Zeichnen ganz genauso; das sind allgemeine Einschränkungen (von CSS? GTK?)

box-shadow (inset) innerhalb des Rechteckss, und innerhalb der (gedachten) border

weil die CSS-box-shadow-Effekte zum Zeichnen komplexerer Strukturen nur bedingt nützlich sind.

Daher verzichte ich global (für die Slopes) darauf, wende sie aber lokal  an

...wo parktischerweise der Style-Advice in einer lokalen statischen Variablen liegt

...aber das wird sich ganz gewiß ändern ⟶ Stichwort Bereichsmarkierungen

....für den Kreis

Und nicht die sichtbare Größe

d.h. wir müssen...

  • die Clips bündig in einen Track legen können
  • feststellen, ob ein Mausklick in einen bestimmten Track fällt
  • bisher verwende ich den nur im Prolog/Coda.
  • könnte sinnvoll sein, um direkt benachbarte Spuren leicht voneinander abzusetzen
  • muß allerdings wirklich außerhalb der (inset) slopes eingebaut werden

macht ggfs ganz natürlich einen box-shadow sichtbar

sollte theoretisch funktionieren....

ClipPresenter::determineRequiredVerticalExtension()

weil dann der Platz für den "pinned" Ruler redundant im Body-Canvas vorhanden ist!

direkt in der Berechnungs-Schleife erkennen, wenn wir den Präfix-Bereich durchschritten haben

daher Policy ändern: jeder schlägt den totalen absoluten  Offset drauf

...und dieser muß deshalb auch schon eine Funktion getAnchorHook() auf dem API bieten

tatsächlich ist es dann ein Sub-Interface: der CanvasHook. Und dieser

  • kennt (nur) implizit dein Ursprung seines relativen Bezugssystems
  • hat aber auch einen Callback, um formell relativ zu sich selbst einen sub-CanvasHook zu erschaffen

...trotzdem wurde die Idee eines generischen UI-Layout-Frameworks nach gründlicher Untersuchung aufgegeben; dies System der ViewHooks stiftet uns also "nur" ein Baumuster, und wir beschränken uns auf die bekannte kleine Zahl der konkreten Fälle, welche weitgehend im DisplayFrame gebündelt sind. Deshalb gibt es nun hilfsweise das Zugangs-Interface ViewHooks, von dem man alle relevanten, speziell getypten ViewHook, bzw. Canvas-Hook eben im aktuellen Display-Frame (relatives Koordinatensystem) beziehen kann.

...und zwar für den einzigen relevanten Canvas, das ist nämlich der untere, in der ScrolledPane, mit dem Tack-Content. Wir verwenden inzwischen in jedem Canvas nur noch die lokalen Koordinaten, und daher addieren nun die jeweilgen TrackBody auch ihre eigene startLine_ in lokalen Koordinaten auf. Da der DisplayFrame direkten Zugang zu "seinem" zugehörigen TrackBody hat, bekommen wir über diesen Trick stets punktgenaue, lokale Koordinaten, solange wir uns im Geltungsbereich dieses TrackBody aufhalten. Das bedeutet, theoretisch könnte ein Clip auch weit unterhalb des TrackBody angeheftet werden. So etwas muß dann eigens im DisplayEvaluationPass ausgeschlossen werden

die gekoppelten Scrollbars funktionieren dann "von selber"

dann muß ich aber die zwei Bezugssyteme wirklich auseinanderhalten

weil die Canvas-Controls tief eingewickelt in der Struktur liegen

  • Feature-Definition der »Ruler« noch nicht gegeben
  • viele Details zum »Content« in den Tracks sind noch nicht klar
  • vertikale Positionierung von Content innerhalb der Tracks ist noch nicht gelöst

er wird von GTK eigentlich korrekt über die Nachbarbereiche darüber gezeichnet.

Aber an der Nahtstelle entsteht ein helle Linie von 1/2 Pixel Breite,

die sich nur beim Scrollen bemerkbar macht

man soll ohnehin keinen so großen box-shadow verwenden

...darüber bin ich auch beim Zeichnen der Connector im StaveBracket gestolpert

sichtbar an der Höhe der Sub-Scope-Verbindungen auf den StaveBrackets

das war ein größeres Refactoring; dafür fällt dann die Lösung mit den rekursiv "eingehäkelten" Lambdas weg. Ist sicherlich besser so...

...für eine zentrale Schaltstelle und Verteiler.
Denn nur der DisplayFrame ist hinreichend loka und dennoch erreichbar

nämlich eine abgekürzte Übersichts-Darstellung, die wohl ehr auf der Basis ganzer Tracks zu zeichnen wäre (Tracks deshalb, weil wir eine Abkürzungs-Darstellung  der Tracks selber nicht vorsehen)

  • gegen das das Diff-Binding arbeiten kann
  • und welches den Anzeigestil abstrahiert

*) die Referenz steckt nämlich schon im ViewHookable

kann erst erfolgen, wenn's soweit ist

...und muß damit in den State-Change-Mechanismus für den Präsentationsstil verlegt werden

Nebenbei bemerkt: die Aktion muß idempotent sein

oder umgekeht....

  • nur wenn auch Koordinaten da sind
  • und auch ein Anzeigestil gefordert wird, welcher ein dediziertes Widget verlangt
  • kann man ein ViewHooked konstruieren

wenn man das naiv coden würde, dann würden wir für jeden Clip erst mal einen ClipData-Placeholder erzeugen, nur um dann, nach dem Empfangen des vollständigen Diff, diesen wieder zu deallozieren und dafür ein ClipWidget zu erzeugen...

Problem wegen dem Overview-Ruler, d.h. wie berücksichtigt der untere Canvas die Ausdehnung des oberen Canvas?

wir verwenden das ClipDelegate aber auch, um in Clips eingebettete Effekte oder die einzelnen Spuren darzustellen. Die Logik für den Anzeigestil muß das mit berücksichtigen

entweder der ClipPresenter delegiert explizit an einen Service des Display-Managers

oder dieser Übersetzungs-Service wird irgendwo implizit versteckt.

Letzteres gefällt mir definitiv besser

ERR: nexus.hpp:189: worker_3: ~Nexus: Some UI components are still connected to the backbone.

Ein Clip hat verschiedene Erscheinungsformen im UI

Verwende das als Leitgedanke, um das Layout zu entwickeln

UI-Bus gilt nur für globale Belange

es geht nur um Rollen

das lokale Element muß nur als View fungieren

kann sich selbst

transformieren

aktive Elemente im Clip-Widget lösen UI-Signale aus, die eine abstrahierte Funktion aufrufen — und diese gebundene Funktion (zumindest der Einsprungpunkt) liegt im Clip-Presenter

das ist ein übergreifendes Thema zum UI-Verhalten

das Placement-UI wird z.B. durch Klick auf das Placement-Icon aktiviert — aber es kann auch modal gemacht werden, und dann wird es in ein Property-Box/Grid-UI alloziert

...und dieser muß einfach lokal im UI zu realisieren sein, also ein Stock-Icon und ggfs ein Vektorgraphic-Element

...sie verwenden dann ein LabelWidget zur Darstellung

diese Alternative würde dann attraktiv, wenn es häufig vorkommt, daß zwischen einem Clip-Widget und einer anderen Repräsentation des ClipDelegate dynamisch hin- und hergeschaltet werden muß. Weil man dann den relativ schwergewichtigen Datencontainer einfach umhängen könnte

...diese erstreckt sich typischerweise über die gesamte Länge des umschließenden Containers, und paßt sich dieser ohne weiteres dynamisch an

Aufgabe: wie geht man mit einem Anzeigestil um, der den Effkt gar nicht  darstellt

Stand 10/22 : Fragen grundsätzlich geklärt

was ist das?

Wenn ein Clip mehrere Spuren oder Datenströme oder sonst irgend eine Art innerer Struktur trägt, dann braucht man irgendwann einen vom Zeitpunkt unabhängigen »Kopf-Bereich«, um die Struktur zu steuern und zu manipulieren. Für die Tracks gibt es dafür einen eigenen "patchbay"-Bereich links von der Timeline-Anzeige. Aber für die Clips ist nicht klar

  • ob wir das wirklich brauchen
  • wo wir es anordnen
  • und wie wir mit Clips umgehen, die weit über das Anzeigefenster hinsausreichen

Und zwar geht es hier um eine sehr grundsätzliche Layout / Design-Entscheidung, die auch sehr starken Einfluß auf den Workflow und die »Mechanik« der Clips haben wird; im aktuellen Stand (2023) der Entwicklung habe ich nicht den Boden, um diese Enstscheidung treffen zu können ��

Zoomen, Scrollen, Scroll-Window und View-Path sind reine Gui-Bildungen und werden instantan ohne Round-Trip realisiert

Vorsicht Falle: es reicht nicht, nur die auf Pixel quantisierte Position zu speichern, denn diese wird bei geringem Zoom relativ ungenau. Es wäre leicht möglich, durch Heraus- und wieder Hereinscrollen die sichtbare Position eines Clip massiv zu verschieben.

Lösung: Interface DisplayMetric im CanvasHook

die Pixel-Koordinaten sind ein rein interner Belang für die Zeichen-Routinen; insofern genügt es, wenn die Umwandlungsfunktion stets einen konsistenten Ursprung liefert, sich aber auch sinngemäß an verändertetes Timeline-Layout anpaßt

...habe ich aber noch nie getestet...

...das ist eine grundlegende Design-Enscheidung (und zwar eine Gute), aber in der Praxis muß es natürlich trotzdem einen Weg geben, nur ist der dann relativ indirekt und auch nicht wirklich dokumentiert

If the question is more than 365 days old, and ...

  • has a score of 0 or less, or a score of 1 and a deleted owner
  • has no answers
  • is not locked
  • has view count <= the age of the question in days times 1.5
  • has 1 or 0 comments

weil dies die einzige Info ist,

welche direkt vom Widget abgefragt wird,

im Zuge der Layout-Berechnung

Ich will nicht, daß der DisplayManager zu bedeutend wird, weil dann eine direkte Manipulation einzelner Widgets durch den DisplayManager als die "einfachste" und "natürlichste" Lösung erscheinen könnte. Dagegen wehre ich mich, weil es zu einer starken Kopplung führt.

es gibt bereits einen Sündenfall, nämlich im RelativeCanvasHook: der muß delegieren, und daher von außen diese Methode aufrufen

das mag überraschend sein — erst dachte ich, es sein ein Nachteil, aber tatsächlich fügt es sich natürlich in die Layout-Steuerung von GTK ein; denn GTK fragt ja das Widget nach seiner benötigten Ausdehnung, und das ist auch genau der Mechanismus, über den wir eine Beschränkung auf eine vorgegebene zeitliche Ausdehnung realisieren. Zudem hat das Widget die Information über seine eigene Ausdehnung als Zeitangabe vorliegen, und das paßt dann auch gut in dieses Aufrufschema

Das ist eine Konsequenz aus dem Design des Diff-Framework; genauer, aus der konkreten Form, die die Implementierung des Diff-Framework bekommen hat: Und zwar, daß ein generischer Container gebunden wird. Das bedeutet, daß die Elemente im Container wie Werte behandelt werden. Und deshalb müssen sich Widgets selbständig vom Canvas abmelden. Die Alternative dazu wäre, daß der Parent den Diff interpretiert, und seine Kinder direkt manipuliert. Dann wären diese Kinder aber auch Widgets, und nicht nochmal indirekt als Presenter angebunden. Könnte man machen, habe ich aber ein mulmiges Gefühl. Da nehme ich dann doch lieber ein smart-Handle und einen Back-Pointer in Kauf.

Das EventBoxWidget mußte dazu natürlich erst einmal gebaut werden; dieses ist nämlich im Grunde genommen genau die Abstraktion eines abridged clip...

Stand 10/2022: Basis-Implementierung geschaffen #1219 und sieht soweit gut aus...

...da das ElementBoxWidget nicht die ownership für sein Kind-Widget (den ContentRenderer) übernimmt; daher muß das Kind länger leben als das ElementBoxWidget, aber genau das ist nicht gewährleistet, wenn das Clip-Delegate von ElementBoxWidget erbt.

Zum Abschluß der GUI-Überarbeitung 3/23 habe ich diese Zombie-Timeline tot gemacht

...um mal was im UI anzeigen zu können

Docks enthalten Component Views

...nur enabled wenn

mehr als ein top-level Fenster offen

A Gtk::UIManager constructs a user interface (menus and toolbars) from one or more UI definitions,

which reference actions from one or more action groups.

realisiert Vererbung zu fuß

...anstatt eine auf den konkreten Typ getemplatete Subklasse zu verwenden,

wird eine "CreatePanelProc" in einen PanelDescriptor eingewickelt.

Letzten Endes wird dieser dann per Match auf die Typ-ID ausgewählt.

AUA!

wie komme ich da drauf?

Ich wollte untersuchen, ob Gtk::manage( ptr ) korrekt die übergebenen Objekte aufräumt.

Wie sich nun zeigt, passiert das Aufräumen im dtor desjenigen Widget, dem das zu managende Objekt als Kind gegeben wurde.

Im vorliegenden Fall wäre das der dtor des umschließenden ScrolledWindow. Der aber wird offensichtlich nicht aufgerufen,

auch nicht im Application-Shutdown!

....erzeugt wird das hier:

dock_.add_item(timelinePanel->getDockItem(),Gdl::DOCK_BOTTOM);

Helper to build the menu and for registering and handling of user action events

es sieht so aus, als wäre es "das" WorkspaceWindow

aber es kann davon mehrere geben

demnach würde startup sauber beendet,

und unmittelbar danach würder der UI-Thread einen

emergency-Shutdown initiieren

  • InteractionDirector::populateContent_afterStart()
  • meta-cmd.cpp

erinnere mich, diverse Mechanismen gesehen zu haben,

die erlauben, eine Init-Aktion in die Loop zu schedulen

betrifft aber nur Framework-Funktionalität

also kein Initialisieren des Toolkit,

sondern Sachen wie verallgemeinerte "Files", D-Bus-Connection etc etc

  • ein Fenster als Solches braucht noch keine Event-Loop
  • ohne ein Fenster macht die event-Loop keinen Sinn

  //We cannot add and show the window until the GApplication::activate signal

  //has been emitted, or we will crash because the application has not been

  //registered. (At least if window is an ApplicationWindow.)

The signal_activate() signal is emitted on the primary instance

when an activation occurs. See g_application_activate().

g_application_activate()

suche (case insensitive) nach application_activate

  • treffer auf APPLICATION_ACTIVATE in g_application_activate()
  • die Treffer in Gtk::Application
  • diverse false positives mit anderen "Activation"-Signalen, z.B. in Aktionen oder Buttons

Main erbt nicht von Gio::Application

...für Signale, die nicht automatisch detached werden können

@deprecated: 3.4: Key snooping should not be done. Events should  be handled by widgets

wohl einzige verbleibende Verwendung

  • finde keine andere mehr, bei suche nach GtkMainConnectionNode
  • auch der Kommentar zur Klasse sagt dieses (gtk_init_add wohl nicht mehr!)

...und das ist explizit so gewünscht

  • keine Kommandozeilen-Behandlung
  • keine "Registrierung" jedweder Art
  • keine D-Bus-connection
  • kein Application-Lifecycle
  • folglich auch kein Aktivierungs-Signal

das ist ein akzidentelles Problem

denn es ist gradezu der Sinn von Glib::Dispatcher,

schon vor der Loop verfügbar zu sein (?)

nur bei laufender Event-Loop

...das ist für mich eine neue Einsicht.

Die Anzeige eines Fensters und die Event-Verarbeitung sind zunächst einmal unabhängig voneinander.

Sie sind aber aufeinander angewiesen. Die Events machen das Fenster reaktiv, aber ohne

Fenster gibt es gar keine Benutzer-Events.

aber wir brauchen ein laufendes UI

...dann kann sich das Sytem erst mal beruhigen

nach der Lastspitze zum Programmstart, für den viel Code geladen werden mußte.

Außerdem hatten dann die anderen Subsysteme schon Zeit, ihre Grunddatenstrukturen aufzubauen;

im Besonderen spekulieren wir darauf, daß die Session-Daten bereits geladen sind,

und daher der Diff direkt und kompakt in einem Durchgang emittiert werden kann

  • Komponente hat eine interne Struktur-Repräsentation
  • traversiert diese Struktur-Beschreibung
  • und übersetzt sie in UI-Widgets
  • jedes Teilelement wird per Push injiziert
  • es wird in dem Moment sofort und ausschließlich als Widget realisiert

Basis: DiffConstituent

  • ein selbstbezüglicher substanzloser Strukturkern
  • DiffConstituent -> emanation as diff

Aufgabe: inject dummy content

und das zu allem Überfluß auch noch ziemlich hart verdrahtet. Man kann nicht einmal frei über die Modifier-Keys verfügen

Achtung: abstrakter Entwurf war Ende 2017 in eine Sackgasse geraten....

nachdem sich eine Instanz einer Rolle gemeldet hat, kann der Hook sie individuell verknüpfen, typischerweise als Lambda

also nicht generisch, sondern die spezifische Closure für z.B. den Fall drag-Clip

naja... sie besteht schon von Anfang an, insofern das Event-Konzept, wie auch die konkrete Quelle, ohnehin an das UI-Framework gebunden sind

im Beispiel: in dieser Closure wird zunächste eine Verdrahtung auf das button_down-Event angelegt. Wenn diese aktiviert wird, schaltet die die Beobachtung eines ebenfalls vorsorglich verdrahteten motion_notify_event ein, sowie analog das Warten auf ein button_up

die Umrechnung Zeit → Pixel habe ich im CanvasHook versteckt

das bedeutet:

  • wenn im Event explizite Koordinaten vorkommen, dann müssen diese sofort umgerechnet werden in eine Zeit
  • aus der gegenwärtig im Clip gespeicherten Zeit ergibt sich dann ein Delta
  • dieses Delta wird sofort auf den Clip angewendet

Am Ende der Geste steht die neue Zeit-Position fertig im Clip und wird von dort per Command in den Steam-Layer gesendet. Später, nach der Verarbeitung in der Session kommt ein Update über den UI-Bus, welches die Position überschreibt und infolgedessen ggfs auch nochmal das Widget verschiebt

...das heißt, durch die Drag-Geste entstehen vorübergehend lokal inkonsistente Koordinaten; der nächste DisplayEvaluation-Pass würde dies wieder beseitigen. Dieser Ansatz wäre rein logisch der konsistentere Weg, denn erst durch eine Rückmeldung von der Session wird eine neue Position auch offiziell. Allerdings müßte man bei diesem Ansatz vorsichtig vorgehen, und mögliche Interferenzen mit der DisplayEvaluation und dem Layout-Managment bedenken; besonders wenn man eine weite Strecke zurücklegt, könnte es passieren, daß der Clip dann plötzlich aus der Anzeige verschwindet und den Fokus verliert, weil eine DisplayEvaluation ihn wieder an seine gegenwärtig nominelle Position geschoben hat.

gehört diese nun dem Layout-Manager, oder gehört er der Geste?

...wobei das auch noch halbfertig ist; später einmal muß es hier eine Abstimmung mit dem Layout-Manager geben, aber diese Abstimmung sollte eigentlich nicht über den ClipPresenter laufen

...sonst bleibt ein inkonsistender Zustand irgendwo "hängen".
Leider ist das nun das bekanntermaßen unlösbare Problem eines sicheren Verbindungsabbaus, und wir müssen uns deshalb mit einem Timeout, oder re-check-Mechanismus behelfen

insofern wir die Storage nur einmal, im jeweilgen CmdContext der Geste vorsehen müssen

die Konsequenz aus Lösung-1 ist aber nicht wirklich abwegig

Warum möchte man denn überhaupt eine Geste "abbrechen können"?

  • weil sie versehentlich ausgelöst wurde
  • weil man besorgt ist, ein bereits Erreichtes dadurch "kaputt" zu machen
  • weil man ein «UNDO« nicht zuverlässig sieht

Die Konsequenz daraus ist dann, daß das UI von Lumiera stets offen, non-modal und manipulierbar ist. Und die zweite Konsequenz ist, daß wir ein klar und zuverlässig steuerbares «UNDO» brauchen.

..brauchen wir Widget und Subject und Canvas

Registry hat zusätzlichen Overhead (sowohl Speicher, alsauch Management) und ist nur sinnvoll, wenn eine ID in mehreren Objekten vorkommt, oder die ID aus mehreren Thread-Kontexten benötigt wird

...insofern sie genau an die Struktur anbaut, welche ich schon zur Lösung der Querbeziehungen für das Layout genutzt habe

1.Schritt: irgendwie implementieren

Das Ideal wäre, daß man das nicht speziell für Maus, Tastatur, Stift und Hardware einrichten muß, sondern lediglich qualifizieren

...d.h. es wäre zu vermeiden, daß ein irgendwo vorgegebenes Konfigurations-Setup an anderer Stelle im Code noch passend verankert oder korrespondierend eingerichtet werden muß.

Konkret: die drag-Fähigkeit eines Clip wird konfiguriert im Clip-Presenter. Fertig.

Und außerdem: die Wiederverwendung einer Implementierung z.B. für Dragging kann man per Vererbung oder per nested component lokal in der Implementierung regeln

  • es kann passieren, daß zu Beginn keine Timings gegeben sind, und deshalb erst mal nur ein Platzhalter erstellt wird...
  • es kann passieren, daß die Anzeige sofort in den degradierten-Modus schaltet (z.B. Timeline-Übersicht)...

...denn ich will ja grade unnötige Storage-Slots für Rückpointer und sonstige Flags vermeiden; daher besteht eine Gesten-Konfiguration nur darin, irgendwo ganz versteckt ein Signal auf das Widget verdrahtet zu haben...

denn nach dem Konstruktor-Aufruf existiert das Widget bereits ― und das heißt, diese Funktion im Listener muß nichts mehr tun, und deshalb sieht es lokal dort so aus, als hätte sich das Widget gar nicht geändert

da man verschiedene Fälle hier zu beginn gleich ausschließen kann

Maus, Tastenkürzel, Stift, Hardware....

...und dadurch ensteht hier ein "linke-Tasche-rechte-Tasche-Spiel".

Wir müssen annehmen...

  • daß der ClipPresenter das Wiring nur dann einrichtet, wenn tatsächlich ein Clip-Widget erzeugt wurde
  • und daß ein deaktiviertes oder aus dem Display herausgenommenes Widget auch keine Signale mehr sendet

habs in error.hpp untergebracht, direkt unter ERROR_LOG_AND_IGNORE

Das bekommt dieser Controller dann nicht mit, und die Geste wird insofern auch nicht getriggert. Da hat der User eben Pech gehabt.

Wenig problematisch ist dieser Fall, wenn der Button gedrückt bleibt und wir irgendwann zurückkommen; dann setzt sich das Dragging eben an der Stelle fort. Wenn dagegen der Button außerhalb released wurde, handelt es sich tatsächlich um den 3.Fall ― wenn aber eine normale Maus-Bewegung später wieder über das Widget fährt, wird das Dragging fortgesetzt, fälschlicherweise.

wenn das Dragging funktioniert, sollte es eigentlich rein logisch unmöglich sein, das Widget zu verlassen, weil dieses sich ja mitbewegt. Allerdings sind vielerlei undlückliche Umstände denkbar, z.B. verspätete Reaktion der Software, sehr schnelle Mausbewegungen, limitierte Bewegung am Rand eines Containers.

die Lösung auf X-Display / GDK-Ebene für die Maus und das grab-widget von GTK, welches alle Events fangen kann.

 * Generated when a pointer or keyboard grab is broken. On X11, this happens

 * when the grab window becomes unviewable (i.e. it or one of its ancestors

 * is unmapped), or if the same application grabs the pointer or keyboard

 * again. Note that implicit grabs (which are initiated by button presses)

 * can also cause #GdkEventGrabBroken events.

...anstatt bloß die Ausführung entsprechender Commands zurückzuweisen

Demnach wäre die Übersetzung von Pixel-Koordinaten in irgend etwas modell-Releavantes komplet in das Subject eingekapselt; der Gesten-Controller würde dann einen Screen-relativen Offset aggregieren. Aber auf der zweiten Stufe bleiben wir bei dem Ansatz, daß die Geste direkt das Modell manipuliert

...Auch wenn ich ein ungutes Bauchgefühl habe, alle Argumente sprechen im Moment dafür, diesem Hinweis zu folgen und das Design in dieser Richtung auszubauen. Im Besonderen würde nämlich ein konsequentes Umsetzen meines ursprünglichen Konzepts bedeuten, daß das Subject-Interface etwas von der Metrik im Modell, und im Besonderen von eine Zeit-Parameter wissen müßte ― es ist aber absehbar, daß in anderen Situationen gänzlich andere Parameter relevant sein könnten ... man denke bloß an die "Position im Fork"

verwendet hart gedrahtete Konstante TODO_px_per_second

BUTT true flag=«GdkEventType»

MOVE x=18.4 y=12.5

ANCHOR at x=18.4 y=12.5 ('scope_moveRelocateClip')

MOVE x=18.8 y=12.5

MOVE x=19.3 y=12.5

MOVE x=19.8 y=13.0

MOVE x=20.2 y=13.0

MOVE x=20.5 y=13.0

MOVE x=20.9 y=13.0

MOVE x=21.4 y=13.0

MOVE x=21.9 y=13.0

MOVE x=22.3 y=13.0

MOVE x=23.0 y=13.0

MOVE x=23.9 y=13.0

MOVE x=24.8 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (6.4,0.5)

MOVE x=25.8 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (7.4,0.5)

MOVE x=26.7 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (8.3,0.5)

MOVE x=27.3 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (8.9,0.5)

MOVE x=27.9 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (9.5,0.5)

MOVE x=28.6 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (10.2,0.5)

MOVE x=29.0 y=13.0

Gesture(scope_moveRelocateClip) --> Δ := (10.6,0.5)

MOVE x=30.6 y=12.0

Gesture(scope_moveRelocateClip) --> Δ := (12.2,-0.5)

MOVE x=31.5 y=12.0

Gesture(scope_moveRelocateClip) --> Δ := (13.1,-0.5)

...

...

MOVE x=97.8 y=97.9

Gesture(scope_moveRelocateClip) --> Δ := (79.4,85.4)

MOVE x=97.8 y=98.7

Gesture(scope_moveRelocateClip) --> Δ := (79.4,86.2)

MOVE x=97.8 y=100.6

Gesture(scope_moveRelocateClip) --> Δ := (79.4,88.1)

MOVE x=97.8 y=102.6

Gesture(scope_moveRelocateClip) --> Δ := (79.4,90.1)

MOVE x=97.8 y=103.6

Gesture(scope_moveRelocateClip) --> Δ := (79.4,91.1)

MOVE x=98.6 y=104.4

Gesture(scope_moveRelocateClip) --> Δ := (80.2,91.9)

MOVE x=98.6 y=105.2

Gesture(scope_moveRelocateClip) --> Δ := (80.2,92.7)

MOVE x=98.6 y=106.2

Gesture(scope_moveRelocateClip) --> Δ := (80.2,93.7)

!!BANG!! Gesture-Cmd 'scope_moveRelocateClip'

  • der clipHook() delegiert an seinen jeweiligen refHook_
  • dieser wird bei der Konstruktion tatsächlich jweils auf den Anchor-Hook des Parent-Hook gesetzt
  • damit steht im refHook_ effektiv der top-level Canvas, und die jeweilige Blatt-Ebene fügt ihren Offset hinzu
  • allerdings würde nun zur Übersetzung der Koordinaten der top-Level-Canvas nochmal an den Layout-Manager delegieren

das Platzieren auf den Canvas ist keine high-Performance-Operation;
vielmehr ist es sogar vernachlässigbar im Vergleich zum Aufwand der Zeichen-Operationen; und letztere werden eben genau aus Performance-Gründen gebatcht und gebündelt....

im Gegensatz zum CanvasHook ist das hier durchaus relevant

Name: DisplayMetric

das time::Control lebt dann wohl im Observer, und dieser muß eine Schnittstelle haben, über die das time::Control auf das eigentliche Zielfeld gesetzt wird...

...das klingt alles gefährlich indirekt...

Stand: funktioniert ― ist aber halbfertig

(gtypes.h): Provide type definitions for commonly used types.

These are useful because a "gint8" can be adjusted  to be 1 byte (8 bits) on all platforms. Similarly and more importantly, "gint32" can be adjusted to be 4 bytes (32 bits) on all platforms.

sehe nur die MOVE-Meldungen.
eigentlich sollte das Subject ebenfalls Meldungen ausspucken

⟹ mehrere Gründe...

  • bekomme überhaupt kein motion_event, wenn kein Button gedrückt ist. Liegt das daran, daß ich ein Button-Widget zum Testen verwende??
  • unabhängig davon war die Logik auch sonst kaputt. Die Flag isInFormation_ wurde nicht gesetetzt

typischerweise liefern die low-level-Events gerätespezifische Koordinaten ab, und deren Übersetzung in die Modell/Domänenwerte erfordert Hilfsmittel, die man sich mehrstufig beschaffen muß. Da aber die einzelnen Events unverbunden daherkommen, muß die Verarbeitung vereinzelt erfolgen. Und das heißt, man leistet diesen Einrichtungs-Aufwand für jedes einzelne Event; dies geht zu Lasten der »Reaktivität«

...wenn man nun eine fest-vorbereitete Lösung für jeden Fall vorsieht, wird die Schnittstelle bereit, unübersichtlich und könnte im Lauf der Zeit verwuchern. Zudem muß eine komplexe Konvention errichtet werden, wer wann für wen welche Variante aufruft

Daher erscheint ein Adapter sinnvoll, der jeweils für eine einzelne Gesten-Instanz erzeugt wird. Dies erfordert jedoch Storage, welche ohne großen Overhead bereitgestellt und effizient genutzt sein will

...sobald irgendwo eine Abstraktionsbarriere errichtet wird, muß mindestens ein Call indirekt oder virtuell sein....

  • die Abstraktion zwischen Widget und Canvas muß ich erhalten, weil die Timeline-Anzeige noch ziemlich komplex wird
  • sofern der Gesten-Controller mit beliebigen Widgets umgehen soll, gibt es eine weitere Abstraktion
  • im Bezug auf die Performance weiß ich nicht, wo die Meßlatte liegt
  • die tatsächlich benötigte Abstraktion kann sich noch als ganz anders herausstellen

...die konkrete Interpretation der Mausbewegung

...sonst würde entweder das Subject selber ad hoc etwas bereitstellen müssen und dafür zusätzliche Storage brauchen (Hebel, es gibt sehr viele Subjekte!), oder das Interface "Subject" würde löchrig und zu einer Kodifizierung von Einzelfällen. Die Lösung mit dem Adapter stattdessen fällt unter das Prinzip Inversion of Control

wenn wir trotzdem einen Overrun-Check haben wollen,

kommen wir an einem zur Laufzeit verfügbaren Wert nicht vorbei

...jetzt nach einigem Überlegen dämmert mir die Erinnerung...

...auch daran kann ich mich jetzt wieder erinnern...
Dann wäre nämlich das Handle nicht mehr kopierbar

diese verwalten nicht aktiv den Typ und Zustand im Buffer.

Daher muß stets ein Objekt im Buffer (default)-konstruiert sein; ein reines (abstraktes) Interface genügt nicht. An dieser Stelle bietet sich ein "NULL-Ojbekt" an

Name: GestureObserver

...und das ist eleganter, als jede der denkbaren Alternativen

  • stattdessen eine virtuelle Funktion für diesen State verwenden
  • stattdessen einen "magischen wert" in den Anker-Koordinaten speichern

funktioniert... Zeit-Werte werden an der Konsole ausgegeben

nein.... nicht wirklich, weil man ja doch noch die Position korrigieren muß

letztlich kein Problem.

Das ClipWidget-Interface ist nur für den Clip-Presenter gedacht, um von der konkreten GUI-Repräsentation (abdriged, extended) zu abstrahieren

sobald man globale Screen-Koordinaten für die Delta-Berechnung verwendet; der Button klebt jetzt exakt an der Stelle, an der zuerst geklickt wurde

Clip zappelt ― warum?

...bevor die Trigger-Schwelle erreicht ist, wachsen sie schön monoton
...auch ist das Springen exakt alternierend, ein Schritt vor, ein Schritt zurück

aber das von GDK gelieferte Delta hängt eben vom GDK-Window des widgets ab

der Gesten-Controller sollte hier nicht mitmischen

...dieser zeichnet die Kreise mit Ausdehnung der Allocation, und auch genau mit dieser Farbe und Linienbreite

ein cout in genau diesem Testcode gibt stets nur alternierend den Allocation-Wert für den Ruler-Canvas (konstant 5px) und dann den Main-Canvas (variiert mit Fensterhöhe) aus

jede Zeichen-Primitive zeichnet von der aktuellen Position weg

sonst verschleppt man eine zufällig vorher gegebene Position

2.Schritt: rektifizieren

Die Gesten-Controller sollen später einmal Teile eines umfangreicheren Frameworks werden; im Besonderen wollen wir abstrahierte Gesten, die verschiedene Eingabesysteme übergreifen können. Für dieses Ziel muß der konkrete Gesten-Controller soweit schematisiert sein, daß man im Zuge der weiteren Entwicklung sich ergebende Erweiterungspunkte einführen kann, auch in schon bestehende Implementierungen. Als naheliegendes Schema bietet sich die State-Machine an, da die Gesten-Erkennung auf theoretischer Ebene ohnehin ein (ggfs nichdeterministischer) FSA ist.

  • Multi-Touch
  • Zusammensetzen einer komplexen Geste aus bereits implementierten Elementar-Gesten
  • Einbinden eines bestehenden Gesten-Controller-Systems (z.B. in GTK) per Adapter

einfachers Dragging mit der Maus? oder kommt zum Abschluß eine spezielle Taste? oder eine spezielle Mausgeste?

  • Linke Hand: Hardware-Controller; Rechte Hand: Maus
  • Modus per Tastenkombination ausgelöst; Geste dann per Maus oder Stift abgeschlossen

es ist total an das Framework gebunden

Beispiel: das "grab" von Blender ist ein praktisches Konzept. Dort kann man ein Element überhaupt nur bewegen, wenn man vorher die "g"-Taste gedrückt hatte. So etwas will ich in Lumiera auch haben... ist aber nicht so ganz einfach

Hierarchische Gesten-Controller

Stand: Konzeptstudie erfolgreich

Sollte als Nächstes versäubert und verallgemeinert werden!

ich meine rechts-Klick; typischerweise implementiert man den Handler dafür auf einem Canvas; aber dann ist das Problem: wie findet der Handler das konkrete Widget, und von diesem den Kontext für das pop-Up?

genauer, das Action-Binding sendet eine Nachricht in den UI-Bus, aber diese muß an das korrespondierende Modell-Element addressiert werden; zwar gibt es unten im Steam-Layer einen gemeinsamen Command-Handler, und für jeden Command-Typ ist ein Skript hinterlegt — welches jedoch dann eine Element-ID als Parameter bekommen muß (oder besser sogar schon eine direkte Dependency-Injection?)

a fixed absolute number of tick units,

where 1 tick unit depends on the current zoom level

...später möchte ich diesen als eigenen zusätzlichen Freiheitsgrad einführen; dazu muß aber bereits mehr über den Verwendungszusammenhang bekannt sein; insofern habe ich vorerst eine generische Regel implementiert, die den Zoom-Anchor anhand der Position zum Gesamt-Canvas festlegt (ganz am Anfgang liegt er ganz links, am Ende ganz rechts)

  • real, d.h. in Zeit (Mikroticks) und nicht in Bildschirm-Pixeln
  • relativ, d.h. im Bezug auf die nominelle Zeitskala dieser Timeline;
    entsprechend der Angabe auf dem Time-Ruler

Beachte: nicht verwechseln mit absoluten Angaben.

Diese Skala legt überhaupt nicht fest, wo ihr Nullpunkt liegt

im Bezug auf eine globale absolute Zeitskala (denn das macht das betreffende Placement)

Theoretisch könnte eine Skala auf einer Seite oder auf beiden Seiten limitiert sein....?

bedingt durch die interne Repräsesntation (als 64bit µ-Ticks bzw. signed fraction)...

  • die absoluten internen Zeitwerte in µ-Ticks sind beschränkt ±Time::MAX (=numeric_limits<int64_t>::max() / 30)
  • die größtmögliche Zoomstufe wurde (mehr oder weniger willkürlich) festgelegt auf 2px / µ-Tick
  • ein Fenster mit weniger als 560px kann nicht die gesamte Zeit-Domäne darstellen (da sonst die rationale Arithmetik entgleist)
  • Entfernen eines Widget bewirkt lineare Suche im Container auf Pointer-Match ⟹ ineffizient
  • ohne aufwendige Index-Struktur haben wir keine Möglichkeit, alle Widgets in einem bestimmten Bereich zu finden

beispielsweise wenn nach links über den bisherigen Ursprung hinaus gescrollt wird — dann wird (in Maßen) der Canvas vergrößert; dies invalidiert alle Koordinaten und das gesamte Layout, und anschließend bekommen aber alle Widgets und Zeichen-Routinen konsistent eine neue Canvas-Übersetzung

Gesamt-Integration der Applikation ist derzeit (2023) viel wichtiger

CanvasHook /

DisplayMetric

Menu

Actions

SpotLocator

Gestures

(Controller)

...dann müssen wir die Vorgabe speichern, und in jedem Schritt korrigierend eingreifen

das heißt...

  • alten Pixel-Wert berechnet
  • Neue Metrik draus per fractional-Integer-Arithmetik errechnet
  • Assertion daß sich daraus wieder die gleiche Pixel-Zahl ergibt

Liegt in unserem gui::model

analog zu gui::model::Tangible

aber time::Control ist nur auf Zeiten ausgelegt

müßten also dann jeweils selber dies in die Kenn-Parameter übersetzen ⟹ Gefahr von Code-Duplikation und inkonsistentem Verhalten

denn aufgrund der ersten (ggfs sogar falschen Benachrichtigung) würde das ZoomWindow selber die Parameter glattziehen, was erneute Benachrichtigung zur Folge hätte

getter liefern Time-Values

Zoom-Metrik verwendet integer-Brüche

FSecs ≙ boost::rational<int64_t>

Resultat:

  • man hat einen Faktor 1e+6 im Ergebnis
  • ist mir tatsächlich passiert, und zwar ziemlich überraschend
  • Abhilfe: FSecs-ctor explizit anschreiben

und so ist es auch gedacht: TimeVar sollte nicht auf APIs auftauchen!

Wenn man sich daran hält, tritt TimeVar immer nur in einem lokalen Universum auf, wie hier im ZoomWindow, und man weiß genau, wo und warum man dem eine reine Zahl zuweist

  • Canvas vergrößern ⟹ führt zu kontinuierliecher Drift; der Canvas wird fortlaufend größer
  • Window verkleinern ⟹ heißt daß man u.U den ganzen Canvas nicht ohne Rest darstellen kann

alle 20 Schritte ein Sprung, bzw. sogar nur alle 10 Schritte bei 96kHz, denn 1/96000 = 10.41666666 µTick

Die µ-Ticks hatten wir seinerzeit gewählt, weil sie einerseits hinreichend genau sind, andererseits sehr einfach zu implementieren, und dennoch die Darstellung extrem großer Zeitspannen ermöglichen

Tatsächlich kann ein µTick-Grid auch Sound-Samples korrekt addressieren — man darf dann nur nicht diese Zeit-Werte für weitere Berechnungen verwenden (denn sonst sammeln sich Rundungsfehler an). Es könnte also eine Implementierung eben wissen, daß hier Sound-Samples dargestellt/verarbeitet werden, und intern mit der exakten Skala arbeiten. Im Grunde ist das ein Lösungsvorgriff auf die 3.Lösungsvariante (Problem ignorieren und per Metadaten tunneln)... siehe Diskussion in #1258

user-defined Literal: _r

mein Anspruch ist, hier eine absolut fehlerfrei arbeitende Komponente zu schreiben

...und zwar, wenn man wirklich alle Eingangswerte zuläßt, und sich eben nicht nur auf vernünftige Eingaben verläßt.

so ±15 2er-Potenz-Schritte genügen, um von der maximalen Auflösung in den Minuten-Bereich zu kommen

insofern muß es dann aber auch mit maximal großen Integer-Zahlen noch sauber funktionieren

...einmal wirklich sicheren Code schreiben...

...zunächst habe ich hier immer nur das Minimale getan, nämlich 1 µTick aufgeweitet; dies würde zwar funktionieren, aber in der Regel nicht zu einem praktikablen Verhalten führen — wohingegen der DEFAULT_CANVAS  so gewählt ist, daß er klein und handlich ist

sofern wir den Speicher haben...

Will sagen, sie sind nicht gefährlich für den Invarianten-Mechanismus, denn für diesen wird es ehr bei sehr kleinen Zeitspannen kniffelig; wohingegen beliebig starkes zoom-out stets repräsentierbar ist als fraktionaler Integer

...auch das basiert auf einer pragmatischen Überlegung; theoretisch könnten wir beliebig große Pixelanzahl unterstützen, dies würde aber auf allen Ebenen zu unerwartetem Verhalten führen, dessen Konsequenzen ich nicht überblicke ⟹ dann besser eine willkürliche und hinreichend große Grenze

dies ist ein reiner Platzhalter für Tests, denn in der Praxis erwarte ich stets daß sofort zu Beginn eine Breite in Pixeln gesetzt wird — kann ja gar nicht anders sein...

das ist das Kernstück

denn dabei wird gerundet, um die exakte Pixel-Zahl zu erhalten

afterWin_ = startWin_ + Time{dur};

wobei dur einen fraktionalen Anteil < 1 µTick enthalten könnte

Der Grenzfall ist ja changedMetric = MAX_ZOOM

Wenn ich das durch eine irgendwie geartete pxWidth teile, wird es niemals größer, und zudem ist MAX_ZOOM < MAX_TIMESPAN

das bedeutet: genau durch diese Abrundung auf den nächst kleineren µTick könnten wir u.U einen Pixel verlieren

⟹ Abhilfe: originale pxWidth als Paremeter mitgeben

Konkretes Rechenbeispiel:

pxWidth = 99999  (ein Pixel weniger als maximal möglich)

changedMetric = MAX_ZOOM = 2000000

⟹ conformWindowToMetric() errechnet

dur = 49999+1/2 µTick

�� das wird abgerundet auf 49999 µTick

⟹ conformMetricToWindow() errechnet

adjMetric = 99999 / (49999/1000000) = 2000020,0004000080001600032

⟹ das ist definitiv größer als MAX_ZOOM

einmal stark reinzoomen, und dann wieder zurück ⟹ Bereich ist beschnitten und kleiner geworden; das ist lästig, weil die nächst größere Stufe deutlich größer ist; meiner Einschätzung nach wäre es weniger lästig, wenn man ein kleines bischen zu viel sieht, zumal sich das auf der nächsten Zweierpotenz einpendeln dürfte

...vergrößern, weil damit die Metrik kleiner wird und unter der Schwelle MAX_ZOOM gehalten wird

...nach grober Abschätzung sind das weniger Operationen, als die Konvertierung auszuführen und zurück zu konvertieren und dann zu testen; zwar könnte der Optimiser den Test per common subexpression noch vereinfachen — aber letztlich empfinde ich das Addieren einer ULP-Konstante als deklarativer

es kommt eine negative Zahl heraus: IntMax * Scale ⟼ -Scale  (im Nenner)

läßt sich ein »unbeherrschbarer Summand« konstruieren

da wird die Rechnung schwierig, denn ich kann es nicht ohne Weiteres mathematisch greifen....

Ansatz mit Unbekannten

1/S - 1/M = x / I  < 1/S

Ansatz mit Unbekannten führt auf eine nicht-lineare Gleichung, welche die beiden Unbekannten verknüpft....

mit ULP = 1/M  und I (IntMax) und S (µTick Scale) setze ich an:

�� 1/s - 1/M = x / I

denn einen Ausdruck x/I kann man grade noch repräsentieren in fraktionaler Arithmetik.

Das fürhrt dann leider zu einger Gleichung, die nicht besonders sexy daherkommt


�� I·S = I·M - x·M·S

also

  •  x = (I·M - I·S ) / (M·S) = (M - S) / (M·S/I) = I · (M - S)/(M·S)
  •  M = I·S / (I - x·S) = S / (1 - x·S/I)

denn für x ≔ I/S  ergibt sich x/I = 1/s

empirsche Kontrolle

    const Rat UU = Rat(X/I);

    SHOW_EXPR (UU);

    SHOW_EXPR ((UU < MICRO_TICK));

    SHOW_EXPR ((Rat(X+1)/I < MICRO_TICK));

Ausgabe...

Probe rational_cast<int64_t>(LIM) ? = 9223372036854

Probe UU ? = 9223372036854/9223372036854775807

Probe (UU < MICRO_TICK) ? = true

Probe (Rat(X+1)/I < MICRO_TICK) ? = false

  • Wenn der Nenner selbst S ist
  • Wenn der Nenner ein Teiler von S ist
  • wenn der Nenner ein Teiler von S · pxWidth ist

Mathematik halt....

alles Andere ist in diesem Fall eben doch speziell; wir kommen bereits mit einer relativen Angabe, und die aktuelle Fensterposition spielt keine Rolle

warum auch nicht?

Das ist dann so viel konsistenter, insofern nun nämlich genau die Funktionen, die die visible-Window-Position setzen, auch den Canvas erweitern dürfen — aber alle anderen Funktionen stoßen an der Canvas-Grenze an

⟹ dann paßt der Wertebereich im Extremfall grade so rein (1px-Window mit 1µ-Tick, und das per halb-Steps von Time::MIN ⟼ Time::MAX bringen:  lb <= 62 )

gelöst durch geschickte schrittweise Berechnung:

calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration)

Wir nutzen explizit aus, daß das Ergebnis durch truncating quantisiert wird (Integer-Division). Daher können wir erst den ersten Dividenden (die Zeitspanne) aufteilen, und zwar erst mal in den Sekunden-Anteil, dann den Rest, und dann den Rest vom Rest

weil es direkt in das Feld px_per_second_ geht

der Nenner ist limitiert (kleiner Time::SCALE) und nicht toxisch

wenn der Zoom-Faktor einen größeen Nenner bekommt, wird er nach den inzwischen etablierten Richtlinien als »toxisch« betrachtet, und das heißt, die typischen Berechnungen im ZoomWindow könnten entgleisen

hinreichend große Fenster (ab ein paar hunder Pixeln) können die ganze Zeitdomäne abdecken

alle die hier angewendeten Heuristiken werden im normalen Einsatz niemals aktiviert, und daher profitieren wir normalerweise von der extremen Präzision der Integer-Bruchrechnung

...und das betrachte ich als gutmütig und hinreichend abgesichert...

und zwar dann, wenn auch noch die Window-Parameter extrem sind — dann sieht die Lage ziemlich hoffnungslos aus

...denn dadurch würde man die kreuzweise Multiplikation verhindern

...und das betrachte ich als gutmütig und hinreichend abgesichert...

ich habe ja ganz bewußt Time::MAX == INT_MAX/3 genommen

Fazit:

  • erhält Invariante
  • Ergebnis kann aber inhaltlich falsch sein

Vorsicht: Ergebnis-Faktor kann trotzdem giftig sein

Problem: detox() macht den zu Null

"quasi statisch" meint hier: hängt vom pxWith-Parameter ab, ist aber ansonsten unveränderlich (aber eben keine compile-time Konstante)

also davor eine neue Funktion: conformWindowToMetricLimits(px)

aus praktischen Gründen erfolgt die Rechnung in FSecs, und das ist hier nicht gefährlich, weil der Nenner dieses Wertes in der Metrik-Berechnung nur mit den Pixeln in Berührung kommt

Hilfsfunktion maxSaneWinExtension(px) = FSecs{LIM_HAZARD * pxWidth, Time::SCALE};

...zwar ist der Bias in Richtung auf ein größeres Fenster weiterhin im Code gegeben, aber dieser Fall hier zeigt eben, daß er durch andere Rundungs-Effekte sogar übersteuert werden kann; aber wir können sogar eine stärkere Bedinung errichten, nämlich daß wir nicht zu weit von der Vorgabe abweichen

...damit meine ich: ausrechnen wie viele Pixel die jetzt eingestelle Fenstergröße in der ursprünglich geforderten Metrik einnehmen würde.....

sofern wir überhaupt relevant große Zeitspannen zulassen wollen

latente Gefahr: Rest bei giftiger Duration

das unterschlägt den Divisionsrest. Oder anders gesagt, die Integer-Division ist keine lineare Funktion, die man einfach so umkehren kann

anders im Nenner;  md * dd ist garantiert berechenbar, sofern die Duration selber aus einem time::Duration gebildet wurde (und md ist durch das detox() limitiert worden)

Der Rechenweg ist hier eine »Einbahnstraße« : durch einen Kniff ist es gelungen die Quatisierung zu berechnen Metrik ⟼ pxWidth. Aber die Umkehrfunktion können wir nicht berechnen, weil es in der Berechnung zu einem Überlauf kommt. Daher können wir die Fehler-Korrektur nicht einfach ausrechnen, weil wir nicht einfach von einen Pixel-Δ auf ein Metrik-Δ zurückrechnen können

...das mag unsinnig erscheinen, aber der Punkt ist, wir können diese lineare Beziehung nur eingeschränkt numerisch berechnen

eindeutig: wrap-around in der relativen Änderung

...welche ich inzwischen nahezu komplett einmal umgepflügt habe...

weil hier...

  • zum Einen eine sehr große Zahl auf einen beliebigen Bruchfaktor trifft
  • und außerdem als Resultat eine Beliebige Zahl entsteht, die nicht auf Time::SCALE quantisiert ist

und redundant: macht nicht conformWindowToMetric()  inzwischen genau dassselbe??

und die Testsuite ist auf Anhieb GRÜN

ZoomWindow: established size or metric misses expectation by more than 1px. 3px != 0.279620 expected pixel.

da die Metrik ja limitiert wurde und damit per definitionem auch sauber

Grund sind die expliziten Limitierungen in dieser Funktion

bleibt noch der Belang: die Duration soll hier auf µ-Tick aufgerundet  werden

(num * Time::SCALE) % den == 0

Das ist zwar offensichtlich richtig, erlaubt aber als Solches noch keine weiteren Schlüsse, sofern man über num/den nichts weiter weiß

das geht in Floating-Point aber eben grade nicht in Integer-Bruchrechnung; ein ULP ist der »maximal giftige Bruch«

  • man ruft eine Detektor-Funktion auf
  • man inkrementiert die quantisierte Variante

das hier ist die beste Alternative

sonst kann man eine fehlerfreie Kalkulation nicht garantieren

die MAX_TIMESPAN-Limitierung beginnt etwas über 1000px zu greifen; diese Unterschwelle wäre aber px · 1e-14

minimale Pixel-Zahl 1px. Mit 1/10 Band dann 1.1, mit 1px-Band 2

will sagen, wenn man das 0.1px-Band zum Absturz bringen kann, dann klappt das mit der doppelten Zeit auch beim 1px-Band

Die Newton-Approximation kam ja nur ins Spiel, weil ich mit Integer rechnen wollte, in dieser Domain aber die Rechnung nicht umkehren konnte. Im weiteren Kontext betrachtet war das von Anfang an ein Schmuh (und ich habe jetzt schon zwei Tage lang ein dumpfes Gefühl, daß es ein Schmuh ist....)

Im Regelfall möchte ich die präzise Integer-Bruch-Arithmetik, aber ich möchte die Grenzfälle nahtlos mit integrieren, und nehme für diese Grenzfälle absolut betrachtet erehbliche Fehler in Kauf.

kommt dadurch zustande, daß ich nach dem Hochskalieren i.d.R doch noch einen Rest habe, den ich für Integer-Arithmetik abrunde; damit dann trotzdem die gewünschte (i.d.R. um einen Pixel höhere) Pixelzahl rauskommt, inkrementiere ich die letzte Stelle, und das ist zugleich meine maximale Fehlerschranke. In Extremfällen haben wir im Zähler noch eine 4-stellige Zahl (1000/LIM_HAZARD) und damit etwa 1‰ Fehler maximal. Das ist nicht schön, aber akzeptabel — gemessen daran, daß ich dadruch eine »ungiftige« Metrik sicherstellen kann. Sobald man etwas weg ist von der Unterschranke der Metrik 1000/LIM_HAZARD, wirkt das Nach-Skalieren und Mitnehmen des gebrochenen Anteils viel besser, und der Fehler sinkt drastisch

  • und zwar mit einer Zahl zwischen 1001/LIM_HAZARD > gesucht > 1000/LIM_HAZARD
  • die resultierende Metrik sollte idealerweise giftig sein
  • aber auch noch Headroom übrig lassen (andernfalls gehen wir in die falsche Richtung)

1000_r/LIM_HAZARD * 1024_r/1023

geht, weil die Metrik limitiert ist, insofern ist wenigstens dafür im Extremfall noch ein Bit übrig

...und der ganze Ansatz mit Newton-Näherung stellte sich als unsinnig heraus; jetzt rechnen wir die Optimierung mit einem Schuß in float, und reizen dann sogar noch den Headroom aus, wodurch die Werte viel genauer werden

wir rechnen von der Duration auf eine Metrik um, weil wir den Mechanismus zur relativen Positionierung haben wollen. Dieser muß aber detox() verwenden, weil sonst die Division mit der Pixel-Zahl einen numeric-wrap machen würde....

es wäre schon überraschend, wenn der Scrollbalken plötzlich nicht mehr reagiert....

selbst Sound-Samples sind (absolut betrachet) noch 10.41ms auseinander; solange wir das komfortabel und sauber im GUI darstellen und handhaben können, sind wir fein

erfordert Analyse aller relevanten Code-Pfade

...aber ein Zähler > INT_MAX / Time::SCALE kann unmöglich sinnvoll weiterverarbeitet werden, und außerdem sollte er für eine Metrik gar nicht auftreten können, weil die Metrik ja relativ stark in der Größe beschränkt ist

...in vertretbarem Rahmen...

  • es gibt X verschiedene Fallkombinationen die gefährlich werden können
  • die Normalisierung/Invarianten-Behandlung kann selbst grenzwertige Metriken erzeugen
  • Werte können sich über viele Runden aufschaukeln

Dies alles erscheint mir analytisch schwer beherrschbar, bzw. würde exzessiven Analyse-Aufwand erfordern; und letztlich ist das hier doch nur eine „sportliche Herausforderung“

☢ und kein Kernkraftwerk ☢

da reichen meine Algebra-Kenntnisse nicht aus....

Habe dies explizit verifiziert und vergliechen mit komplett ausformulierten inline-Funktionen; wie erwartet: mit -O3 werden Lambdas komplett transparent geinlined.

Extrem wichtig. Und auch wichtig: mit diesem Akkumulator nachher noch etwas machen (z.B. ausgeben). Sonst merkt der Compiler daß das kein beobachtbarer Effekt ist, und entfernt die Addition, und dann in einigen Fällen auch den eigentlichen Funktionsaufruf

Die Vorlage auf Stackoverflow initialisiert auf 0 oder -1, läuft dann aber für Input==0 leer durch die Checks durch. Meine Variante testet auf 0 und returnt sofort.

eine andere Standard-Library, und ilogb() könnte deutlich langsamer sein.

damit kann ich stets so genau wie möglich rechnen

Rat poison = (MAXI-88)/(MAXI/7);

...das ist näherungsweise Sieben...

...und zwar in dem Fall, in dem der Nenner nur knapp über der Schwelle LIM_HAZARD liegt; dann findet nämlich fast keine Reduktion der Größenordnung statt

  • der Nenner wird garantiert gleich dem vorgegebenen Quantisierer sein
  • aber der Zähler ist ggfs um das Verhältnis Zähler / Nenner größer als der Quantisierer

Und zwar, wenn der Nenner viel kleiner ist als der Zähler, und der Zähler extrem groß. Dann würde nämlich die Ganzzahl-Division keine signifikante Verringerung der Dimension bewirken, und die anschließende re-Quantisierung das Ergebnis (bedingt durch die Normierung auf einen gemeinsamen Nenner) sogar noch vergrößern

hier wissen wir, daß wir den Quantiser ggfs hochskalieren können auf 1e6 (er hat sich u.u mit der µ-Tick-Zahl gekürzt). Da es sich um eine valide Duration handelt, ist dieses Hochskalieren stets garantiert. Mithin ist dieser Faktor das garantierte Minimum (die schlechtest mögliche Genauigkeit ≙ 1e-6

aus Gründen der Symmetrie kann man das gleiche Argument jeweils auch auf den Kehrwert anwenden, deshalb haben wir ja 4 Fälle (bei zwei Eingangs-Faktoren). Man muß nur ggfs. dann den Kehrwert vom Ergebnis ausgeben. Beispiel: wir nehmen den Zähler vom ersten Faktor als Quantisierer. Dann ist f1 der Nenner vom ersten Faktor, und muß daher auch im Ergebnis im Nenner landen, nicht im Zähler wie beim regulären Schema

man muß Floating-point runden wenn man glatte Werte will

das ist essentiell wichtig. "Negative" Zeiten dürfen sich keinesfalls  anders verhalten. Eine andere Quantsierungs-Regel kann man dann ggfs. high-level auf ein left-Truncate aufsetzen (z.B. Mitte Frame-Intervall)

⟹ also muß man hier technisch runden

...das sind 4ms

parsing '1/250sec' resulted in 0:00:00.003 instead of 0:00:00.004

#--◆--# _raw(win.overallSpan().duration()) ? = 307445734561825860

#--◆--# _raw(targetPos) ?                    = 206435633551724864

#--◆--# _raw(win.visible().start()) ?        =   2248731193323487

#--◆--# _raw(win.visible().end()) ?          =   2248732049674178

#--◆--# bool(win.visible().start() < targetPos) ? = 1

#--◆--# bool(win.visible().end() > targetPos) ? = 0

...tut sie zwar nicht in dem Beispiel hier, aber mit genügend krimineller Energie ließe sich ein valides Beispiel konstruieren, wobei

  • die Ziel-Position dann außerhalb des legalen Bereichs liegen würde
  • bei korrekter Behandlung daher das Ergebnis-Fenster in den legalen Bereich geschoben werden müßte
  • aber ohne weitere Schutzmaßname hier die Berechnung der Zielposition entgleist

0111 + 0111 = 1110

0011 + 0101 = 1000

aber...

0010 + 0101 = 0111

Gefahr besteht...

  • wenn beide Vorzeichen gleichgerichtet sind
  • wenn mindestens einer der Zähler das 63te Bit gesetzt hat (2^62)

206435709205.57568 - 206435709205575697/1e6  = abs(-0,000017) > 1e-6

...ich hab doch ohnehin Zeiten, die sind µ-Grid quantisiert; also müßte der Fehler einer einfachen Addition auf µ-Grid genau sein

...da wir wissen, daß einer der Multiplikatoren eine bereits quantifizierte Zeit ist, können wir von einem Nenner 1e+6 ausgehen (ggfs noch reduziert um einen gekürzten Faktor). Das andere Argument, der Anteil-Faktor, stammt von außen und ist daher beliebig. Der einzige sichere Weg, den Kürzungs-Trick anzuwenden, ist daher, den Kehrwert des Anteil-Faktors zu re-Quantisieren, so daß sich dieses 1e+6 wegkürzt, und wir dann keine gefährliche Multiplikation mehr haben.

...und damit die rationale Arithmetik als Möglichkeit zu erhalten; sie wäre nicht sinnvoll nutzbar, wenn man ständig die Sorge haben müßte, daß einem die Zahlen »explodieren« können. So ist es nun schon besser, im Regelfall eine hochpräzise Berechnung zu haben (präziser als floating-point), die in Grenzfällen ungenau wird, mit letztlich kontrollierbarem Fehler-Level

Es fühlt sich gut an, das Problem nun doch befriedigend bezwungen zu haben; ich hatte immer das Bauchgefühl, daß da mehr Genauigkeit möglich sein sollte. Allerdings — wie sich zeigte — nur in günstigen Fällen

...aber nur nach Maßgabe der tatsächlichen Dimension der eingehenden Zahlen

Fazit: es ist das gesicherte Minimum

#--◆--# _raw(targetPos) ?             = 206435633551724864

#--◆--# _raw(win.visible().start()) ? = 206435633106265625

#--◆--# _raw(win.visible().end()) ?   = 206435633962616316

also offensichtlich erkennt boost::rational die Möglichkeit, den gemeinsamen Faktor 1e6 aus Zähler und Nenner wegzukürzen. Da wir aber hier einen FSec-Wert als Offset zugeben, haben wir wenig Möglichkeiten, das zu vergiften

rein numerisch würde das gehen, da ich Time::MAX | MIN sinnigerweise auf INT_MAX / 30 gesetzt habe. Das war vorausschauend....

es untergräbt gradezu den Sinn dedizierter Zeit-Entitäten

  • auf den ersten Blick darf es nicht aussehen wie "aha, und hier kann ich alles machen"
  • so wie der Zugang beschrieben ist, ist er völlig logisch und konsistent
  • die erweiterte Möglichkeit erschließt sich erst dem aufmerksamen Leser
  • diese erweiterte Möglichkeit erweist sich aber letztlich als nichts anderes als die Konsequenz der formalen Definition
  • das wäre dann ein ctor mit einem 2. Argument
  • wer so einen ctor aufruft, weiß was er tut
  • das könnte auch später in eine MicroTic-Basisklasse übernommen werden

Das versaut den bisher sehr sauberen Code von TimeValue, und läd gradezu dazu ein, hier beliebige Werte zu konstruieren. Im Hinblick darauf, daß ich umgestalten möchte TimeValue ⟶ MicroTicks, würde damit die Daseinsberechtigung untergraben, denn man kann nun nicht mehr sicher sein, daß MicroTicks ein sicherer Wert ist.

...ganz dunkel kommt mir die Erinnerung an eine „kognitive Dissonanz“ — die ich dann schell unter den Teppich gekehrt hatte, indem ich sie mit einem Assert dokumentierte....

und die unterliegende lumiera_time_of_gridpoint weicht da ebenfalls auffällig ab...

....also ein Feature, das zwar auf theoretischer Basis entwickelt wurde, aber nur im testgetriebenen Kontext; hier wohl entstanden aus der implementierungsmäßigen Symmetrie zu Grid::gridPoint(n)

TimeValue Quantiser::materialise(TimeValue const& raw) { return timeOf (gridPoint (raw)); }

  • weil die Rechnung komplett auf Implementierungs-Ebene passiert, mit int64_t
  • unter der Maßgabe daß die auftretenden Werte dahingehend limititert sind, und daher auch noch die Summe zum Origin ohne wrap-around berechnet werden kann

Ja!!!
Hat lange gedauert, bis ich da drauf gekommen bin; aber das ist so eindeutig die richtige und angemessene Lösung, daß ich jetzt ausgesprochen erleichtert bin.

alle Steuersignale aus dem GUI kommen in Pixel-Einheiten, entweder absolut oder relativ; das ZoomWindow selber aber arbeitet in Zeit-Einheiten (und das ist auch seine Aufgabe).

in Grenzbereichen greift das ZoomWindow ein und begrenzt Ausschläge; selbst wenn der angeschlossene Code direkt die Pixel-Werte verwendet, muß trotzdem auf solche Eingriffe geprüft werden

im nächsten Zyklus wird das aber als Head-content gewertet ⟹ body-content wird erhöht

damals war ich zufrieden, sobald es „halbwegs“ funktioniert hat

  • erster Draft ab718ed6aa9d
    hat nicht funktioniert, vmtl da ich letztlich auf widget.get_allocation().hight() aufgesetzt habe, und das kann initial noch null sein
  • überarbeitet ab718ed6aa9db
    dann habe ich rein auf Basis der einzelnen Zellen einen denfensiven Berechnungsweg verwendet: nimm MAX(preferred_height, reale Allokation); habe dies aber nicht angepaßt auf mehrere Kind-Tracks, oder vielleicht doch? oder die Logik verkehrt herum??

z.B. in (0,1) = Struktur-Graph

Zwischenfazit: das Problem wurde erst dadurch sichtbar, daß in der Logik im Track-Head innere Widersprüche bestanden; nach Durchlaufen eines Abstimmungszyklus bestand sofort wieder eine Diskrepanz, und das Layout konnte sich folglich nicht stabilisieren. Dies habe ich inzwischen behoben, insofern nun das TrackHeadWidget grundsätzlich komplett ausformuliert ist, und alle Zuständigkeiten dort klar verteilt sind. Nach Durchlaufen einer Display-Evaluation ergibt sich nun in Summe genau der Wert, der der mit dem TrackBody abgestimmten Höhe entspricht.

Dennoch besteht das grundsätzliche Problem weiterhin, und wird in dem Moment doch noch zu betrachten sein, wenn das Layout dynamisch auf Inhalte reagieren soll; denn immer noch wachsen die ermittelten Höhen monoton an, und es gibt keinen Reset.

  /** Hooks up an adjustment to focus handling in a container, so when a child

   * of the container is focused, the adjustment is scrolled to show that

   * widget. This function sets the horizontal alignment.

   * See Gtk::ScrolledWindow::get_hadjustment() for a typical way of obtaining

   * the adjustment and set_focus_vadjustment() for setting

   * the vertical adjustment.

   *

   * The adjustments have to be in pixel units and in the same coordinate

   * system as the allocation for immediate children of the container.

   *

   * @param adjustment An adjustment which should be adjusted when the focus is

   * moved among the descendents of @a container.

   */

  void set_focus_hadjustment(const Glib::RefPtr<Adjustment>& adjustment);

0000001086: POSTCONDITION: body-canvas-widget.cpp:512: worker_3: maybeRebuildLayout: (not isnil (profile_)) DisplayEvaluation logic broken

adjust (rulerCanvas_, canvasWidth ≔0, rulerHeight ≔11)

scrollPos ⟿ zoomWindow geändert, nachdem das Profil aufgebaut wurde

...denn den pixSpan sollte sich ja grade eben nicht ändern, sondern nur der sichtbare Fenster-Ausschnitt; die Implementierung mit dem ZoomWindow zielt ja genau darauf, die Metrik konstant zu halten, selbst wenn sich das sichtbare Fenster (wie hier) ändert

und nur das kann man vom Design her einfordern (daß sie sich stabilisiert und nicht in Oszillationen gerät)

Erste Integration: verhält sich korrekt

Beispiel: Fenster vorher sehr schmal machen...

per Trace-Meldung überprüft: calibrateExtension() ist so programmiert, daß es die bestehende Metrik erhält, sondern das ZoomWindow entsprechend verkleinert. Der Code verwendet bisher nur default-Werte für die Timeline ⟹ die Metrik bleibt auf 25px/sec stehen, und damit wird die Gesamtlänge stets mindestens 575px sein; Ausnahme: wenn das Fenster ohnehin größer ist...

innere

Struktur

weil wir einen Mechanismus haben, die <cnt>-Dekoration pro Typ global hochzuzählen (treadsafe)

Problem: key nur innerhalb des Objektes eindeutig

...denn dann müßte das Attribut

von dem Container (GenNode) wissen, der es zwei Ebenen höher enthält und umschließt

...es kommt (vielleicht) überhaupt nicht darauf an,

daß der Hash einer Attribut-ID reproduzierbar ist (auf Basis der symbilschen ID).

Der Hash kann genausogut eine Zufallszahl sein

...nämlich diejenigen, die selber Modell-Elemente sind.

Und genau diese speziellen Objekt-Attribute sind der Anlaß,

sich über dieses Problem Gedanken zu machen

aber: zufällige ID macht Objekt-builder stateful

das heißt.

die Konstruktion des Domain-Modelles ist dafür zuständig

für global eindeutige IDs an den relevanten Stellen zu sorgen

...dem man eine EntryID geben kann

(GlobalCtx)->InteractionDirector (=Model Root)

aus Performance-Gründen.

Weil dann sofort bei ins(clip) == Konstruktion ein ClipDelegate vom Typ ClipWidget erzeugt, und in den GTK-Canvas geheftet wird.  Andernfalls würde erst ein DormantClip erzeugt, der dann erst später, wenn das timing-Attribut gesetzt wird, sich in ein ClipWidget verwandelt. Das wäre dann im Normalfall eine zusätzliche, unnötige Allokation

schon analog zu dem, was wir hier mit dem "timing" machen

(GlobalCtx)->InteractionDirector->Navigator

(GlobalCtx)->WindowLocator->UIComponentAccessor

(GlobalCtx)->InteractionDirector->ViewLocator

(GlobalCtx)->WindowLocator->UIComponentAccessor

man muß die Implementierungs-Details jeder einzelnen Komponente kennen,

um damit überhaupt etwas anfangen zu können. Es gibt hier keine schematische Ordnung.

Selbst die Frage, ob es sich um ein Blatt handelt, oder um einen inneren Knoten,

erfordert bereits Kenntnis der Innereien

(GlobalCtx)->WindowLocator

(GlobalCtx)->WindowLocator->PanelLocator

block[__element][--modifier]

Hier besteht die Gefahr, das Stylesheet mit redundanten Definitionen zu fluten!
Die BEM-Notation ist sinnvoll, insodern sie die intendierte Bedeutung einer Klasse klar macht

für wirklich generische Styles sollte man generische Klassen schaffen

dann immer besser noch ein: frame#element

heißt: Element registriert sich am UI-Bus

heißt: Element deregistriert sich am UI-Bus

...ist immer ein tangible

presentation

state

vom tangible initiiert

dafür genügt der normale Reset

mark "clearMsg"

mark "clearErr"

mark "reset"

Nachricht an irgend ein Wurzel-Element

generisch

sinnvoll?

was haben alle UI-Elemente wirklich gemeinsam?

die Frage ist, wie generisch ist eigentlich ein Command-Aufruf selber?

Macht es daher Sinn, ein generisches API allgemein sichtbar zu machen,

oder handelt es sich nur um ein Implementierungsdetail der UI-Bus-Anbindung?

...wird sinnvoll im Rahmen von InteractionControl

ich wollte explizit kein generisch-introspektives UI,

weil das die Tendenz hat, sich zu einem Framework auszuwachsen.

Für die UI-Programmierung muß man Spaghetticode akzeptieren.

Identität == Bus-ID

gemeint, eine ENUM von verschiedenen Graden der Aufgeklappt-heit

Dann mußte das allerdigns jeweils für alle Elemente sinnvoll sein

und der muß vom konkreten Widget implementiert werden

dann wird eine state mark ausgesendet

need to bubble up

support ist optional

nicht bool-Testbar

ganz bewußt verzichten wir darauf,

festzustellen, ob ein Kind gegenwärtig zugänglich (revealed) ist.

Denn dies auf dem API zu exponieren bringt keinen Mehrwehrt.

Letztlich ist das ein lokales Detail des Layout-Managers.

Es ist nicht Sache des client-Codes, auf einem Widget ein anderes Verhalten auszulösen,

wenn es nicht sichtbar ist. Denn Sichtbarkeit gehört zur UI-Mechanik und geht den Client nix an

...denn dort fehlt noch die konkrete Implementierung,

welche die Monaden-Baum-Repräsentation schließlich

auf konkrete Widgets abbildet. Das ist viel Arbeit, und es ließe sich vereinfachen,

wenn für gewisse Knoten erkannt werden kann, daß sie einen NavScope darstellen;

denn dann gäbe es eine Implementierung "von der Stange"

aber die Operationen sind der Sache nach nicht symmetrisch

nach Broadcast von "reset"

sollte logischerweise der PresentationStateManager leer sein

ist er aber nicht notwendig,

denn er kann Zustand von nicht mehr existierenden Elementen aufgezeichnet haben.

Nur Elemente, die im Moment angeschlossen sind, bekommen die "reset"-Nachricht mit;

sofern sie tatsächlich abweichenden Zustand haben, sollten sie sich resetten

und eine state mark "reset" zurückschicken...

....so harmlos hat alles angefangen

Denn das heißt, ich muß konkret ausarbeiten,

wie man einen Diff gegen eine opaque Implementierungs-Datenstruktur aufspielt.

Und ich muß das in einem Test zumindest emulieren können!

muß DiffApplicationStrategy

noch einmal implementieren

das mag überraschen --

ist aber im Sinne des Erfinders

  • DiffApplicationStrategy war von Anfang an als technisches Binding konzipiert
  • es ist besser, die gleiche Semantik der Sprache X-mal herunterzucoden
  • cleverer Code-re-Use zahlt sich i.d.R. nicht aus

dies setzt volle Implementierung

des Tree-Mutators voraus

der schwierigste Teil, das Mutieren von Attributen,

ist jedoch schon prototypisch implementiert

Mutator verwendet einen Binder

Diff kennt keine Zuweisung

Nein

aber was dann wenn out-of-order

eindeutig überlegen

  • faktorisiert sauber
  • Zustand delegiert auf die jeweilige Kinder-Sammlung
  • diese wird damit auch zum generischen Element

schlechter....

  • sammelt viel technische Komplexität auf top-level
  • wir müssen eine meta-Repräsentation aufbauen
  • wir müssen Adapter zentral generieren, anstatt uns vom Installieren von Closures treiben zu lassen

Primitive

(impl-ops)

of questionable use

with multiple layers

since skipSrc performs both the `del` and the `skip` verb, it can not perform the match itself...

...because it is also used to discard garbage after a findSrc operation.

Thus we need to avoid touching the actual data in the src sequence, because this might lead to SEGFAULT.

For this reason, the implementation of the `del` verb has to invoke matchSrc explicitly beforehand,

and this is the very reason `matchSrc` exists. Moreover, `matchSrc` must be written such

as to ensure to invoke the Selector before performing a local match. And skipSrc has to

proceed in precisely the same way. Thus, if the selector denies responsibility, we'll delegate

to the next lower layer in both cases, and the result and behaviour depends on this next lower layer solely

then move into target

since, on interface level, we're pretending that this mutator is a single collection like thing,

while in fact the implementation might bind to several opaque target structures.

Thus, internally we'll have a selector to determine which onion layer is responsible for

handling an element as designated by the argument. It is then the responsibility

of this specific onion layer to accept forward until meeting this element.

warning: messed-up state in case of failure

this is (probably) the only operation which entirely messes up the mutator state

when the designated target does not exist. The assumption is that a diff application front-end

will check the bool return value and throw an exception in that case

move into target

throw when

insufficent space

...in Fällen, in denen der konkrete onion-layer

überhaupt nicht im Stande ist, das zu beurteilen.

Wichtigster solcher Fall ist die Bindung auf Objekt-Felder

invoke mutateChild

NOTE: mutator need to be written in such a way

to be just discarded when done with the alterations.

That is, the mutator must not incorporate the target data, rather it is expected

to construct the new target data efficiently in place.

Mutator enthält die Bindung auf die konkreten Daten

stellt sich u.U erst während der Verarbeitung heraus:

bei "offenen Datenstrukturen" entscheided jeder Typ selber,

welchen Mutator er erzeugt

aber: Aufrufprinzip

Verb muß den

Diff bekommen

und delegiert iterativ

an die Verben

...denn in dem Moment, wo wir den top-level TreeMutator erzeugen,

können wir rekursiv abfragen, wie groß alle möglichen Kind-Mutatoren werden können

nur Zuweisung einiger Referenzen

....denn der liegt (mind) einmal vor,

eingebettet in ein Selektor-Prädikat,

welches bestimmt, ob dieses Attribut angesprochen wird

was man konventionellerweise auch macht.

Ich verstehe nun, warum. Es ist der vernünftigste Weg.

Leider scheidet das aber für uns hier genau aus,

denn das gesamte Projekt entstand, aufgrund der inhärenten Limitierungen

der "vernünftigen" (=pragmatischen) Lösung.

dieser Ansatz löst tatsächlich das Problem,

aber zu dem Preis, daß er die Strukturen von innen her zersetzt.

Auf lange Sicht wird das System wuchern wie ein Krebsgeschwühr,

und man kann das nur mit Disziplin eindämmen, was realistisch gesprochen meint,

daß es vergeblich ist. Einen Kampf gegen das Menschliche, Allzumenschliche kann man nicht gewinnen.

das ist die schlankeste Lösung, die ästhetisch befriedigt.

Sie hat aber das Problem, daß dadurch die Kollaboration im Kern ausgelöscht wird.

Wir haben eine Seite, die absolute Macht hat, und einen "Partner", der tatsächlich nur ferngesteuert ist.

Wir müssen dafür auf die Subsidiarität verzichten, und damit auf die Möglichkeit zur Entkoppelung.

Dazu kommt, daß die notwendige Fern-Wirkung stets eine zusätzliche Last bedeutet.

Denn wir müssen auf Umstände und Strukturen einwirken, die von dem Ort, an dem die

Steuerung stattfindet, entfernt ist, entfernt in einen anderen Kontext.

und nur letztere sind tangibel

um den Binde- bzw. Anknüpfungs-Punkt in den real-Daten überhaupt zu finden,

müssen IDs aus dem DOM innerhalb der real-Daten nochmal wiederholt, also redundant vorliegen

damit das DOM ein echtes DOM ist, muß es die relevanten real-Daten duplizieren,

um sie in einem abstrahierten Kontext zugänglich zu machen

noch zusätzlich zur genannten Duplikation muß

die Abblidung der Strukturen aufeinander

an irgend einer Stelle repräsentiert werden.

man kann versuchen, die beiden Elemente der Duplikation aufzulösen.

Allerdings gibt es dafür überhaupt nur zwei mögliche Richtungen.

  • man löst die Parallel-Strukturen auf
  • man ersetzt das DOM durch reine Bindungs-Strukturen

Beide Ansätze laufen aber auf eine der schon genanten, anderen Alternativen hinaus.

Wenn man die Parallel-Strukturen beseitigt, enden wir bei irgend einer Form von Fernsteuerung.

Wenn man die Modell-Natur aus dem DOM entfernt, das heißt, dort nur noch reine
Binde-Strukturen speichert, dann endet man bei einer Form von Introspektion. Entweder,

das Rückgrat und die Navigation verbleibt bei dieser Introspektion; dann haben wir eines

der typischen Objekt-Systeme. Oder die Binde-Daten werden zu einem reinen Anhang

an eine selbständig bestehende Datenstruktur; dann enden wir bei klassischer Introspektion.

reflektiert die Zahl der Struktur-Element

...will sagen:

für die habe ich bereits eine effiziente Implementierung,

die darauf beruht, den Content beiseite zu schieben.

Ich brauche also nur ein Container-Frontend (z.B. einen Vector ohne Inhalt) zusätzlich,

um den verschobenen Inhalt erst mal aufzunehmen.

Also zählen Kinder-Collections nur als ein Strukturelement.

rekursiv,

duch Bindung bestimmt

das ist der wesentliche Kniff,

durch den das Problem mit der "absrakten, opaquen" Position entschärft wird

Implementierung: Diff-Bindung -> Diff-System

  • diese wird im Nexus behandelt, in dem die Tangible::mark()-Methode aktiviert wird
  • in dieser wiederum steckt eine Default-Handler-Sequenz, plus ein Strategy-Pattern

Marker-Typ MutationMessage

....denn dann müßte der Benutzer die Mechanik sehr genau verstehen, und stets eine auto-Variable definieren.

Sinnvoll wäre dieser Ansatz nur, wenn das UI-Bus-API eine MutationMessage const& nehmen würde,

denn dann könnte man den Builder-Aufruf inline schreiben.

Da wir aber stets den Diff moven und dann iterieren, scheidet const& aus

Und für eine reine Ref erzeugt C++ niemals eine anonyme Instanz!

...und diesen mit VTable bestücken.

Dafür wird die äußere Hülle non-virtual

und kann noncopyable gemacht werden..

Das erlaubt dem Benutzer, einfach zu schreiben

MutationMessage(blaBlubb())

Abstraktion

nach beiden Setien

...aus gutem Grund!

Der Nexus speichert nämlich eine direkte Referenz in der Routingtabelle

Problem: InteractionControl

...was andernfalles komplett vermeidbar wäre,

da im Übrigen das UI-Modell nur mit LUIDs und generischen Namen arbeitet

Idee: context-bound

Command und Verhaltensmuster

bleiben zusammen

...was ich einen Monat später schon wieder vergessen hatte...

hier geht es darum, eine Regel zu generieren,

die dann den zugrundeliegenden Command-Prototyp automatisch mit konkreten Aufrufparametern binden kann,

sobald bestimmte Umstände im UI einschlägig werden

Das ist ein erweiterter / komplexerer Anwendungsfall.

Der einfache Standard-Anwendungsfall ist, direkt die Command-ID zu senden

das reicht für die erste Integrationsrunde völlig aus

Instanz-Management ist automatisch

»Tangible« : Basis

anfangs hatte ich da eine implizite Konvertierung. Man konnte schreiben ID{xyz}. Das war cool; und verwirrend; und hat jede Menge Ärger gemacht, wie immer

Thema: InteractionControl

Stand 2021:

läuft ehr auf ein Framework hinaus,
denn auf eine zentrale Einrichtung

  • weil es einer generischen Lösung im Weg steht
  • weil die partikuläre Ebene tief in das UI-Toolkit verwoben ist

Spaltung in Pragmatismus und Vision

Auf theoretischer Ebene handel es sich um eine (nicht notwendigerweise deterministische) State-Machine. Allerdings ist diese sehr breit, und daher nicht sinnvoll direkt zu konstruieren. Wohingegen eine Tabellen-Implementierung aus Regeln dynamisch populiert werden kann

GTK macht das auch in der Tat genau so

Also eine generische Implementierung einer Gesten-Erkennung (z.B. dragging), welche dann durch Parametrisierung eingebunden wird

  • also entweder auf den top-Level
  • oder auf die Blätter hinunter
  • oder in Hilfskomponenten

Focus/Spot wird mitbewegt

act, note: Nachricht upstream

mark: Nachricht downstream

Bus-Design is selbstähnlich

Kennzeichen ist die EntryID des zugehörigen Elements

Die Lösung für diese wecheslseitige Abhängigkeit

ist, den Nexus als Member im CoreService zu haben,

weil man dann seine Addresse schon weiß, bevor er erzeugt ist.

Dummerweise rettet mich dieser Trick nicht im Shutdown,

denn hier nun läuft tatsächlicher Code aus dem Destruktor heraus!

bei einem echten Downstream könnte man dafür sorgen,

daß er grundsätzlich vor dem Nexus weggeht. Aber nun kommt, auf dem Umweg

über den Core-Service, der Nexus nach dem Nexus....

ich will nicht damit anfangen, daß man einen Zeiger umsetzen kann....

beendet Deregistrierung,

wenn ein BusTerm sich selbst deregistriert

Mechanismus, der es erlaubt

  • log-Nachrichten aus Mocks zu hinterlassen
  • in der Test-Fixture auf diese zu matchen

Beispiel:

a-b-c-a

match("a").after("b") scheitert, weil sich die Suche am ersten "a" festbeißt.

...ist mir nicht völlig klar, warum das bei einigen Includes auftritt,

und bei anderen nicht. Beispiel

In file included from src/lib/diff/test-mutation-target.hpp:55:0,

                 from tests/library/diff/diff-complex-application-test.cpp:35:

src/lib/test/event-log.hpp:96:9: warning: 'lib::test::EventMatch' has a field 'lib::test::EventMatch::solution_' whose type uses the anonymous namespace [-Wsubobject-linkage]

   class EventMatch

         ^~~~~~~~~~

In file included from src/lib/diff/test-mutation-target.hpp:55:0,

                 from tests/library/diff/tree-mutator-binding-test.cpp:32:

src/lib/test/event-log.hpp:96:9: warning: 'lib::test::EventMatch' has a field 'lib::test::EventMatch::solution_' whose type uses the anonymous namespace [-Wsubobject-linkage]

   class EventMatch

         ^~~~~~~~~~

Beispiel: Aktionen, die im globalen Menü stehen.

"Add Sequence"

  • wer bildet daraus ein Command?
  • auf welchen Kontext bezieht sich das
  • wen kann die Menü-Registrierung konkret ansprechen (Verdrahtung ist statisch)

konzeptionell: fertig

Implementierung der real-world-Variante fehlt!

Dienste im UI, erreichbar über den Bus.

Sie stellen die Verbindung zu zentralen Belangen her

wie Session- und State-Managment, Commands etc.

Compiler-Bug Gcc-#63723

gelöst in GCC-5 -- backport unwahrscheinlich

eine virtuelle Funktion

pro möglichem Umwandlungs-Pfad

wir verwenden die Basis-VTable

und layern nur die tatsächlich möglichen Umwandlungen drüber

empfängt alle state mark notificatons

nach Perspektive

nach work site

einer könnte für

mehrere Commands zuständig sein

mehrere könnten für

ein Command zuständig sein

....denn wir wollen ja grade

den Widget-Code vom Control-System abstrahieren

und ebenso die Gesten abstrahieren

muß Instanzen einsetzen

...und zwar zwingend, sobald

  • das command Argumente hat, die gebunden sein wollen
  • mehrere Invocations des gleichen Grund-Commands "gleichzeitig" unterwegs sein könnten

...vom Linken her nicht, da wir Gui gegen Proc linken

vom Bauen auch nicht, und außerdem...

...coden wir ja nicht gegen die Implementierung,

sondern gegen eine Abstraction (Command), die eigens dafür geschaffen wurde

...wegen

  • Command-Message via UI-Bus
  • Durchreichen durch das Interface-System

...da die DispatcherQueue direkt Command-Objekte (=frontend handle) speichert

...denn das GUI läuft ja synchron.

D.h. wir wissen, wenn wir das Air-Gap überstanden haben.

Ab diesem Punkt hält der Eintrag in der DispatcherQueue das Command am Leben,

und wenn es stirbt, dann stirbt es halt...

aufruf direkt mit Command-ID -> erzeugt automatisch eine Klon-Kopie

GUI: CmdAccessor

Proc: CmdInstanceManager

es könnte z.B. sein, daß man vom InteractionState

direkt einen Record<GenNode> bezieht, und bei diesem Zugriff

automatisch die Kontext-Accessor-Ausdrücke ausgewertet werden

...da dies ein pervasiv genutzter Service ist,

und wir nicht wollen, daß jedes Widget

mit dem InteractionDirector verdrahtet sein muß!

hat überhaupt nichts mit dem Zugang zu Commands zu tun,

und auch nichts mit der Trennung zwischen Layern und Subsystemen

es geht um Service-Dependencies

aka DependencyInjection + Lifecycle Management

  • man hat ein statisches Front-End, d.h. by-name access
  • hinter dem liegt eine Factory
  • die Instanz kann von innen her wieder geschlossen werden
  • wenn geschlossen, dann Fehler werfen

nur eine Art remote procedure call

...und das heißt auch, wo werden die Aussage-Sätze gebildet?
wird das Formen von kontextbezogenen Anweisungen im UI-Bus-Protokoll eigens verankert, oder erfolgt dies komplett intern im Stage-Layer?

...nicht klar, ob das notwendig (und gut) ist

es könnte auch ausreichen, einfach die passende InteractionStateManager-Impl zu verwenden

denn InteractionStateManager ist ein Interface!

...das UI weiß,

wer das konkret immer sein wird.

D.h. beim Start des UI wird eine Verbindung irgendwo hinterlegt

Das könnte ein Advice sein

vom Command her ist der Typ festgelegt

auf das "aktuelle Element" wir eine Art Typ-Match gemacht.

Wenn der paßt, kann das aktuelle Element verwendet werden.

In diesem Fall wird das Command enabled

eine Argumentliste mit mehreren Parametern wir Schritt für Schritt geschlossen

wenn mehrere Objekte als Argumente in Frage kommen,

wird das gemäß Scope "nächstgelegne" genommen

  • Interaktion mit einem UI formuliert Befehls-Aussagen indem sie die »Gesten« des Benutzers beobachtet; dieser bewegt sich dabei in konzeptionell in einem »Interface-Raum«
  • das konkrete Interaktions-System (Keyboard, Mouse, Pen, Hardware-Controller, VR...) ist konsequent zu abstrahieren
  • eine Zwischenschicht trennt die Widgets, die das UI realisieren, und die vom UI abgesetzten Handlungs-Anweisungen (»Commands«)
  • diese Zwischenschicht ― oder Interaktions-Schicht ― zu entwickeln ist ein Kern-Belang; dieser Belang kann niemals von irgend einem Framework abgedeckt werden

2017 habe ich zunächst versucht, die Analyse soweit zu treiben, daß sich daraus Strukturen ablesen lassen; die Intention war, darin die einfache Struktur eines direkt "point and shot" gegebenen Commands eingebettet zu finden. Dieses Bestreben mußte abgebrochen werden, da ich noch nicht genug über das konrete Interface weiß, um sachadäquat beurteilen zu können, was notwendig ist.

...stattdessen habe ich dann die schon bestehenden und definierten Teile zusammengebunden, um das direkte Absetzen von fest im Code vorgegebenen Commands zu ermöglichen. Diese gehen seither als einfache symbolische Nachrichten über den UI-Bus. Das gesamte Thema "Argument Binding" ist bereits abschließend behandelt (Marshalling via GenNode). Ebenso der asynchrone Dispatch, und die ebenso asynchron entkoppelte Rückmeldung ("push up") in das UI.

2018-2019 habe ich eine dieser Vision entsprechende, offene und generische Grundstruktur des UI angelegt, und begonnen, konkret für die Timeline-Repräsentation auszuimplementieren. Auch dies ist grundsätzlich alles geregelt, wir können ein Custom-Stylesheet aufgreifen, wir können eigene Widgets mit custom-drawing realisieren, und trotzdem weitgehend auf das UI-Toolkitset mit allen seinen Zusatzfunktionen zurückgreifen. Nun (2/2021) bin ich wieder an dem Punkt, an dem die erste, einfachste »Geste« zu realisieren wäre: nämlich das Verschieben eines Clip in der Timeline. Und ich halte genau an der Einsicht fest, daß diese Interaktions-Logik nicht fest in ein Widget eingebaut werden darf.
Da stehe ich, und mehr weiß ich noch nicht...

Der Plan ist, einmal Command-Skripts (C++ basierte DSL-Notation) direkt vom Build-System verarbeiten zu lassen; der Build wird daraus passende C++-Translation-Units generieren und übersetzen. Tatsächlich ist all dies nicht besonders anspruchsvoll, denn die eigentliche Arbeit, die DSL-Notation ist bereits geschaffen. Trotzdem ist das Thema vorerst vertagt, weil zur praktischen Ausführung eine Menge zusätzlichem Wissen aus der Praxis notwendig ist, wie z.B. wie teilt man die Commands ein, wer definiert überhaupt Commands, und zu welchem Zweck. Beispielsweise ist es durchaus später einmal denkbar, daß auch eine Lumiera-Extension (Plug-in) zusätzliche Command-Scripts bereitstellt. Dann stellt sich natürlich auf das (ziemlich anspruchsvolle) Problem der Belegung von Command-IDs erneut.

Vorerst und auf längere Sicht genügt es völlig, die Command-Scripts von Hand zu schreiben und in einigen Cpp-Files im Steam-Layer abzulegen. Als allgemein sichtbare Schnittstelle dient der Header cmd.hpp, in dem die Command-IDs fest als globale Konstanten definiert sind. Diesen Ansatz behalten wir solange bei, bis die Pflege dieser fest verdrahteten Definitionen und Command-Skripte tatsächlich zum Problem wird.

YAGNI

die Instanz kommt nicht in der Fixture-Queue an

der Umstand, daß Commands auch ausgeführt werdern können,

gehört nicht zum Thema "Instanz-Management"

...denn ein Command geht dann in die Queue

und kann noch ausgeführt werden, während ein weiteres

schon "in der Mache ist"

...aus gutem Grund

(kann mich erinnern, daß ich mir das überlegt hatte).

Sofern Definitionen wirklich concurrent geändert oder gelöscht werden,

könnte es sein, daß jemand auf einer stale reference arbeitet,

denn das Lock schützt nur den Aufruf innerhalb der CommandRegistry.

Sicher ist der Zugriff nur, wenn im Schutzbereich dieses Locks ein

neues Command-Objekt kopiert wird. Was allerdings den RefCount erhöht.

aber sich mit einem Refcount verrückt machen.....

....künftige Weiterung:

auch in EntryID könnte ein Symbol stecken,

mithin in der GenNode::ID

da dieser Zugriff wirklich für jedes Command passiert,

möchte ich mit dem Minimum an Hashtable-Zugriffen auskommen.

Daher prüfen wir als erstes den CommandInstanceManager,

da dies der Regelfall ist. Wenn dies scheitert, suchen wir noch

in der globalen Registry

d.h. wir müßten dann auch noch das Interface brechen

und die Form der ID-Dokoration zur Konvention machen

das heißt, für das ganze Thema InteractionControl

schwebt mir eine Zwischenschicht unabhängig von den Widgets vor

Wenn nun aber das Anfordern einer neuen Instanz über den Bus laufen soll,

dann würde es wohl ehr direkt von den Tangibles (Widget / Controller) ausgehen.

Das wollte ich genau nicht

Tangible sollte InteractionState verwenden

....und demnach sollte InteractionState eben grade nicht von Tangible wissen

Demnach müßte sich InteractionState irgendwo "hinten rum" an den Bus ranmachen,

z.B. über den InteractionDirector. Das ist aber nun wirklich absurrd,

da es letztlich nur darum geht ein ohnehin öffentliches  Interface aufzurufen

...sonst wird die ganze Sache absurd

und unsinnigerweise aufwendig

Instanz öffnen

CommandID und Argumente gegeben

SessionCommandService::trigger

SessionCommandService::bindArg

SessionCommandService::invoke

managed diese Komponente nicht

aufwendiges Nebenthema

...das war der erste Entwurf

  • overengineered
  • am Bedarf vorbei

Symbol ADD_CLIP = CmdAccess::to (cmd::scope_addClip, INTO_FORK);

prepareCommand (cmdAccess(ADD_CLIP).bind (scope(HERE), element(RECENT)))

issueCommand (cmdAccess(ADD_CLIP).execute());

die DSL muß so konstruiert werden,

daß die Syntax-Elemente nahtlos simplifiziert werden können,

in eine Form, die sich unmittelbar jetzt implementieren läßt

und mit einfachen, direkt gegebenen Objekten

(Anmerkung 2/2021)

Über dieses Thema, und »InteractionControl« im Allgemeinen habe ich intensiv nachgedacht über den Sommer 2017. Die damals aufgestellten Grundzüge erscheinen mir auch heute noch sinnvoll. Allerdings kann dieser Entwurf nicht nur allein aus first principles abgeleitet werden. Daher habe ich im Winter 2017/18 dann entschieden, eine Verkopplung mit dem direkten Aufruf von Commands aufzulösen, wodurch Letztere einfach implementiert werden konnte. Überhaupt muß das UI ein gutes Stück weiter erst konkret gebaut werden, damit überhaupt ein Sachbezug für die Überlegungen gewonnen werden kann....

...stattdessen einen Fehler-Indikator auslösen

(Beispiel "in-point fehlt")

...das ist eine Reaktion,

die von einem managing Ui-Element ausgeführt wird,

aber von einem externen State-Change getriggert wird

invocationTral wurde aufgegeben.

Insofern löst sich dieser Knoten langsam

allerdings, wenn man eine explizite Instanz-ID angibt,

bleibt es bei der stringenten Fehlerbehandlung

das heißt, es geht um die Haupt-Registry für Commands.

Wenn wir eine Instanz machen, um Parameter zu binden und sie dann schließlich auszuführen,

könnte man dieser Instanz einen Namen geben, und sie in die Haupt-Registry eintragen..

Oder man könnte sie anonym verarbeiten, weil Command selber ein smart-Handle ist.

...ist jetzt geklärt.

InteractionState == Kontext

CommandID.KontextID == Instanz

...wenn es doch offenbar für den "fire-and-forget"-Fall

genauso gut möglich ist, über eine zentrale Stelle zu triggern.

Nebenläufigkeit ist kein Argument (da das UI single-threaded läuft)

aber: Parametrisierung könnte partiell sein

sie wird nicht zum Parameter-Sammeln verwendet

...eine Instanz wird dann erzeugt, wenn sie notwendig wird.

Sie kann vom UI-Command-Framework erzeugt werden,

sie wird automatisch erzeugt, sofern Parameter gebunden werden,

oder ein Command an den Dispatcher übergeben...

grundlegender Widerspruch

zwischen Command-Control-Interface

und Messaging

...und kaum erkennbarer Nutzen.

Der einzige Nutzfall wäre ein "this"-Parameter.

Den kann man aber mit geeigneter Syntax auch direkt angeben

hier müßte der InvocationTrail die aufgesammelten Argumente transportieren.

allein dafür genügt eine GenNode

...weil es zu jedem InvocationPath

zu jeder Zeit nur eine "offene" Instanz gibt.

Also genügt es, einen anonymen Klon dieser Instanz zu halten

gemeint ist:

Ein UI-Control wird aktivierbar, weil das zugrundeliegende Command

alle seine Argumente aus dem aktuellen Kontext befriedigen kann

Beispiel: Menü-Eintrag "create duplicate"

die Idee ist hier,

daß diese generischen Rollen bereits in der Einrichtung der Command-Definition verwendet werden.

Das heißt, für einen bestimmten Invocation-Trail legt man fest,

daß ein bestimmtes Argument an eine gewisse Rolle gebunden wird,

oder andernfalls einen bestimmten Namen bekommt

...eben!

Diese Frage hat dann dazu geführt,

daß ich das ganze Konzept "InvocationTrail"

wieder komplett zurückgebaut habe

...wir versuchen gar nicht erst, »etwas Bestehendes zu nutzen« ― sondern wir machen unser eigenes Ding.

vermutlich läuft es immer darauf hinaus

  • daß cmd.hpp die Implementierungs-Einheiten includiert
  • oder daß in einer ausgezeichneten Impl-Einheit das marker-Makro gesetzt wird und dann cmd.hpp includiert wird

...also ist eine Instanz durchaus noch am Leben,

während bereits die nächste Instanz für das GUI ausgegeben wurde.

...damit die Nummer erhalten bleibt

...implementiert "für die Zukunft",

wenn wir context-bound -Commands verwenden

das ist ein grundlegender Beschluß.

InteractionControl ist eine eigene Schicht;

deshalb ist auch der UI-Bus nicht das Universal-Interface schlechthin

Diff-basiertes Δ-Binding

schmerzloses C++ API

Performance: guter Schnitt (etw. besser als boost spirit)

hat ein DOM-API und ein SAX-artiges API

das heißt: nicht einmal abhängig von der STL

wie gson

vjson war Google Code;

nach dem Umzug auf Github heißt es gason

lt. eigenen Benchmakrs deutlich schneller als rapidjson, welches eigentlich immer als der "schnelle" JSON-Parser gilt.

d.h. das Parsen schreibt den Eingabepuffer um, und Strings bleiben einfach liegen

kein Repo auffindbar

ich will nicht noch ein Objekt-System

ohnehin sollte man stets einen Leer-Test mitlaufen lassen

man hätte genausogut std::future und std::async verwenden können.

Vorteil von unseren Framework:

  • wir verwenden es ohnehin durchgehend und es baut auf C++17 auf
  • man baut ein Objekt für einen Thread. Das ist explizit und sauber
  • wir haben eine eingebaute Barriere und können unseren Objekt-Monitor nutzen

habe einen usleep(1000) getimed

daher messen wir die Loop als Ganzes.

Es gibt daher keine Möglichkeit, den Loop-Overhead selber zu messen.

Er sollte sich aber bei einer Wiederholung im Millionenbereich gut amortisieren

Außerdem ist ja auch noch der Aufruf des Funktors mit im Spiel, wenngleich der auch typischerweise geinlined wird

volatile Variable außen, im Aufrufkontext

...was sehr schön beweist,

daß x86_64 tatsächlich cache-kohärent ist

das ist der wichtigste Part: ich grenze mich hier explizit von der STL ab;  ein »Lumiera Forward Iterator« ist nicht eine besondere Art von Pointer — wir wissen sonst wirklich nichts, und wir verzichten auf ein absolut optimale Performance. Und wir bieten keinerlei  weitere Features, wie Rückwärts-Iteration, random-access, Löschen und Einfügen. Einen einmal verbrauchten Iterator kann man nur noch wegwerfen.

offensichtlich habe ich das gleiche Problem hier mehrfach gelöst und das dann wieder vergessen

Ein Pointer wird in diesem Kontext wie ein einfacher primitiver value behandelt, d.h. value_type ≡ der Pointer selber, reference  ≡ eine Referenz auf den Pointer, pointer ≡ ein Pointer auf den Pointer. Und: aus einem Pointer werden niemals nested type bindings abgegriffen.

Das Sandwich-Design tritt durchaus häufig auf, wenn man einen Builder hat, oder eine Pipeline mit einem terminalen Adapter. Aber beim TreeExplorer habe ich das viel klarer gelöst, indem der Kern im Sandwich selbst wieder ein »Lumiera Forward Iterator« ist, nach dem »State Core«-Modell. Im Grunde laufen die »Itertools« auf den gleichen Ansatz hinaus, nur habe ich das damals noch nicht erkannt und stattdessen ein privates Protokoll entwickelt, wodurch die gesamte Implementierung relativ schwer zu verstehen ist, und dennoch weder kompakter, noch performanter wird.

Das Konzept des State-Core-Adapters hat sich im Lauf der Jahre herausgebildet, und wird mehr und mehr zu einer fundamentalen Struktur des Iteratoren-Framworks (in dem Maß, wie aus ad-hoc - Adaptern ein ganzes Framework entstanden ist). Im zweiten Anlauf für den Iter(Tree)Explorer habe ich das bereits erkannt, und die gesamte Pipeline darauf aufgebaut; allerdings zeigte sich hier, daß genau die Entscheidung, die »StateCore« als opaque zu betrachten, im größeren Kontext nicht trägt. Daher habe ich hier bewußt die Core als Basisklasse eingemischt; daraus ist der definierende Wesenszug des IterExplorers entstanden, der aber auch gleichzeitig seine größte Gefahr und Schwäche ist: das komplexe hoch-kohäsive Layering. Und nun stoße ich beim Aufbauen der Allokator-Hierarchie für den Scheduler enteut auf eine ähnlich gelagerte Situation, kann aber (da es sich um high-Performance-Code handelt), definitiv nicht den IterExplorer einsetzen. Dies bewegt mich zu dem Schritt, den IterableDecorator als ein eigenständiges Konzept zu etablieren.

damit wäre dieser Adapter ein »Iterator-Implementierungs-Atom«

seit längerer Zeit ein Thema,

für das ich verschiedene Lösungen ausprobiere

  • IterExplorer : erster Anlauf, etwas schief
  • TreeExplorer : Pipeline-Builder, vielversprechend

Thema: Monaden

sind Monaden

wirklich hilfreich?

»AboutMonads« : das könnte mal eine Seite im Theorieteil werden ("more about...")

...bindet die Betrachtung auf einen technischen Level,

und führt dazu, daß die Abstraktion undicht wird

genau der Umstand,

daß funktionale Sprachen von einer Zustands-Phobie getrieben sind,

macht Monaden nützlich, um inhärenten Zustand wegzuabstrahieren.

Das kann genutzt werden, um den Zustand einer Wechselwirkung

nach einer Seite der Glieder auszukoppeln.

gehört zu dem Themenkomplex "itertools"

Python hat das auch, Java neuerdings auch

...will sagen, es ist klar, wie man sowas machen kann.

Seinerzeit war mir das auch klar, aber ich wollte es nicht gleich ausprogrammieren.

Inzwischen kam dann das Thema UI-Coordinaten, und dort habe ich es ausprogrammiert,

und zwar direkt in die Low-Level-Schicht integriert, was nicht schlecht ist,

da eine Abstraktion hier sehr technisch werden würde

...von der entsprechenden Methode im Transformer

...hatte die Notwendigkeit hierfür seinerzeit während der Tests entdeckt,

und im Unit-Test nur für die Kombination Transformer + Explorer abgedeckt....

sofern längerfristig Itertools durch TreeExplorer abgelöst werden könnte

...und das hängt an einem hauchdünnen Faden,

und ist subtil bis zum geht nicht mehr....

Wenn man das Lambda einfach per [=] schreibt, und das Feld this->predicate_ verwendet,

dann wird this gecaptured (und das ist effektiv per Referenz). Wenn ich dann den

konstruierten Funktor an this->predicate_ zuweise, haben wir eine Endlos-Rekursion.

Lösung: man muß im lokalen Frame eine Referenz auf this->predicate definieren und binden.

Diese wird dann per Value gecaptured, was die gewünschte Kopie bewirkt.

...muß den Chain-Funktor aus dem Template-Argument erzeugen,

und ihn dann in die per-Value-Closure des erzeugten neuen Lambda binden (=Kopie).

Erst ab C++17 kann man Lambda-Captures pre move machen

(und auch dafür ist die Syntax grausam)

...es soll bloß einfach funktionieren!!!!!!!!!!!

keine gute Idee.

Dann verwenden wir im einen Funktor-Framework eine Filter-Komponente

aus dem anderen Framework, obwohl es direkt hier auch eine Filter-Komponente gäbe.

Außerdem habe ich immer noch die Hoffnung, irgendwann mal

die Itertools komplett durch den TreeExplorer ablösen zu können

Name: mutableFilter()

...sieht gut aus.

Folgendes habe ich gesehen

  • zu Beginn zeigt das eingebettete Funktor-Objekt auf eine Position auf dem Stack
  • beim Aufrufen der andFilter()-Funktion werden diverse Funktoren kopiert,
    wobei nacheinander die (zu erwartenden) Argumente als Quelle auftauchen
  • danach hat sich der Funktor geändert: er zeigt nun auf eine Position auf dem Heap
  • der bisherige Funktor wurde mit der Closure des zusammengesetzten Funktors kollabiert (hat gleiche Addresse)
  • der Chain-Funktor hat eine Closure bekommen, die ebenfalls Heap-alloziert ist.
    das deutet darauf hin, daß das capturen per copy funktioniert hat
  • Beim Aufruf steppen wir nacheinander erst in den kombinierten Funktor,
    und von dort wie erwartet in die beiden Lambdas.

...für das dort hineingereichte Funktor-Objekt wird der Argument-Accessor ausgewählt (Metaprogrammierung).

Er ist dann im Typ des Wrappers == _Traits::Functor codiert.

Wir können zwar den im Wrapper enthaltenen Funktor neu zuweisen (in gewissen Grenzen),

aber er wird stets den zu Beginn gewählten Argument-Accessor nehmen.

Typischerweise wird dieser ja sogar beim Aufruf des getemplateteten Funtions-Operators geInlined

Problem: std::function in FilterPredicate

entweder Val -> bool oder Iter -> bool

...ist einfach und offensichtlich;

ohnehin laufen schon ziemlich alle konkreten Layer-Builder nach einem Schema-F

junktor ist asymetrisch: (string, float) ⟼ string

...für das dort hineingereichte Funktor-Objekt wird der Argument-Accessor ausgewählt (Metaprogrammierung).

Er ist dann im Typ des Wrappers == _Traits::Functor codiert.

Wir können zwar den im Wrapper enthaltenen Funktor neu zuweisen (in gewissen Grenzen),

aber er wird stets den zu Beginn gewählten Argument-Accessor nehmen.

Typischerweise wird dieser ja sogar beim Aufruf des getemplateteten Funtions-Operators geInlined

der Aufruf vom äußeren in den inneren Wrapper ist 1:1 und sollte vom Compiler wegoptimiert werden

...ich verbrenne Stunden beim Debuggen von neuem Code

Konsequenz: brauche Template Funktions-Operator

der Funktor für den Expander wird explizit als Sonderfall  aufgefaßt

das eigentliche Problem mit der bestehenden Lösung ist,

daß ich ausgerechnet mit diesem Sonderfall angefangen habe.

Daher ist jetzt das gesamte Design "anders herum"

...wo der volle Typ des Funktors FUN bekannt ist

...und in dem besonders wichtigen Fall,

in dem der Funktor direkt den SRC-Iterator akzeptiert,

wird er ohne Weiteres durchgereicht.

In dem Fall dann keine doppelte Verpackung mehr!

da sich die Iteratoren wirklich unterscheiden können dürfen

...wobei der konkrete Overhead noch nicht wirklich klar ist;

hängt davon ab, wie geschickt der Optimizer ist, und was man konkret als Funktoren angibt.

Vermutlich haben wir hier

  • zwingend eine Indirektion durch einen Funktions-Pointer (weil eine Type Erasure stattfindet)
  • möglicherweise eine zusätzliche Heap-Allocation (es sei denn, der Optimizer ist wirklich clever)

und das ist das Argument, das sticht

...will sagen, wenn schon eine neue Lösung, dann von A bis Z

FAIL___expectation___________

expect:J(11|200ms)-J(22|200ms)-J(11|240ms)-J(22|240ms)-J(11|280ms)-J(22|280ms)

actual:J(11|200ms)-J(22|240ms)-J(11|240ms)-J(22|280ms)-J(11|280ms)-J(22|320ms)

Lösung: Quelle erst weiter iterieren,

wenn wirklich notwendig

Bedeutung der Invariante

technisch:

  • entweder Expansions-Stack ist leer
  • oder der oberste Iterator auf dem Stack ist nichtleer

Und zwar wegen der Objektorientierung: die gesamte Logik ist verkapselt, und nur wenige Operationen sind für abgeleitete Klassen zugänglich; im Besonderen ist incrementCurrent() private und das ist der einzige Weg um an die Iteration des Quell-Iterators zu kommen, aber genau dort wird auch dropExhaustedChildren aufgerufen()

insofern kann das Verwerfen des Quell-Objekts verzögert werden,

solange bis die daraus resultierenden Kinder aufgebraucht sind;

da wir ohnehin einen Stack benötigen, läßt sich dieser Status

auch verschachtelt aufrechterhalten

  • Expansion: Quell wird noch nicht angefaßt, aber Kind-Iterator wird bereitgelegt und daraus ggfs. das erste Element exponiert
  • Wenn eine Kind-Ebene leer wird, wird sie verworfen — und erst an dieser Stelle  wird das nächste Quell-Element wirklich benötigt

bisher stand hier das dropExhaustedChildren(), aber das fällt in disem Fall leer durch, wenn wir uns bereits wieder auf Basis-Level befinden — was bisher kein Problem war, denn es war bereits das Quell-Element konsumiert worden. Da wir aber diesen Fall klar zu fassen bekommen direkt in expandChildren()  (weil eben die Expansion sich leer ergibt), können wir explizit in diesem Fall das Konsumieren der Quelle direkt machen. Ist im Ergebnis sogar logischer.

...ich hatte es seinerzeit wohl bemerkt, aber für nicht relevant bzw sogar „eigentlich logisch“ gehalten...  jedenfalls war es im Testfall  verify_IterSource()  dokumentiert, da dort auch die Source-Pipeline (hinter dem IterSource-Interface) sichtbar ist und mit beobachtet wird. Dieser Fall (Zeile 1229) hat nun angesprochen und wurde entsprechend korrigiert

  • Suche wird geleitet durch die ViewSpecDSL
  • hinter dem (opaquen) TreeExplorer steckt die konkrete UI-Topologie

EventLog ist ein Test-Hilfsmittel,

um Unit-Tests über UI-Bus-Interaktionen schreiben zu können.

Es gibt hierzu Test-Varianten, die jeden Aufruf in ein internes Log notieren.

Im Test verwendet man dann eine Pattern-Such-DSL,

hinter der sich eine verkettete Suche mit Backtracking verbirgt

...so daß es nur noch wenige Zugangs-Punkte zum unterliegenden Iterator gibt

TreeExplorer macht das Wrappen für uns automatisch,

und außerdem haben wir nun ein direktes API für die Laufrichtung

...denn durch das Backtracking

würde man nun ziemlich undurchsichtige Misch-Zustände bekommen.

Und: jeder direkt gesetzte Filter könnte die Invariante im Expander verletzen

(weil er einen Kind-Iterator leer machen könnte, ohne daß dieser gePOPpt wird)

...um "Hängenbleiben" auf dem gleichen Element auszuschließen.

Vorsicht: um sauber genau einen Schritt machen zu können, müssen wir explizit vorübergehend den Filter abschalten

weil wir uns bisher bei allen vorausgegangenen Bedingungen

auf den ersten Match "festgebissen" haben, und nur über den Iterator mit der

zuletzt gesetzen Bedingung weiter iteriert haben.

Künftig gibt es nach jedem Fail ein Backtracking.

...d.h es findet zwar ein Backtracking statt, aber wenn alle konjunktiven Klauseln gesetzt sind,

sollte sich erneut ein FAIL ergeben

est-event-log-test.cpp:228:  verify_callLogging: (log.ensureNot("fun").after("fun").after("fun2"))

Verdacht: Negation

fun after fun matcht auf den immer gleichen Record

und zwar mit abgeschaltetem Filter

für diesen einen Schritt

muß die Filter-Funktion vorübergehend deaktiviert werden,

damit wir exakt das nächste / vorhergehende Element bekommen

die Search-Engine bläht die Debug-Infos gewaltig auf

geht gar nicht anders, denn diese sind templates

Invariante: pullMatch()

...d.h. die einzelnen Steps in der Pipeline direkt wrappen.

Dann ist außen herum keine Anpassung der Argumente mehr notwendig,

und man kann die Expand-Funktion direkt als std::function-Objekt durchgeben

...um den Expand-Funktor zu zwingen, eine Kopie zu machen;

es sollte die Filter-Konfiguration auf der Kopie manipuliert werden,

während das Original auf dem Auswertungs-Stack liegen bleibt,

für späteres Backtracking...

...damit man stets weitere Builder-Funktionen auf der Pipeline aufrufen kann

weil wir diesen manipulieren

...nach der »reinen Lehre«

...insofern wir nämlich zwingend auf den jeweilign Kind-Iterator zugreifen müssen.

Nicht nur auf den aktuellen Wert (=dereferenzierter Iterator, d.h. die Funktion yield())

wir müssen hier eine Festlegung treffen

...weil die Ergebnisse der einzelnen Schritt-Funktoren,

jeweils in den Auswertungs-Stack gepackt werden sollen.

Daher müssen sie untereinander kompatibel sein.

im typischen Fall kopiert man den Basis-Iterator,

und manipuliert dann einige Einstellungen auf diesem.

Wir könnten diese Kopie-Semantik sogar erzwingen,

indem wir als Argument-Typ des Schritt-Funktors eine const& vorgeben

...den ich mache, um den Adapter in jedem Einzelfall zu bekommen

....und fällt dann beim Instantiieren des Template auf die Schnautze

sofern eine Initialisierung ausidem Source-Iterator möglich ist

und sonst ist das nicht wirklich "kniffelig"

...bloß sind die zig-fach geschachtelten Template-Typen,

die dann die Intantiierungs-Call-Hierarchie bevölkern,

nahezu unlesbar....

AAber ... wenn es erst mal duch den Compiler ist,

dann sollte der Optimizer diese gesamten x-fachen Wrapper nahezu restlos entfernen

Konsequenz: jede Argument-Funktion wird nochmal gewrappt

wir sind irgendwo im Baum, nicht auf dem Basis-Layer.

Und der Basis-Layer steht irgendwo, nicht an der aktuellen Position.

...denn

  • es ist praktisch, zunächst "leer" zu konstruieren
  • es ist verständlicher, wenn alle Bedingungen symmetrisch angegeben werden
  • es ist natürlich, daß ein "leerer" Filter alles durchläßt

Filter leer == alles durchlassen

...d.h. direkt das Prädikat, und nicht eine Funktion, die den Filter konfiguriert

...habe ich noch gar nicht gemerkt, daß das geht,

und manchen komischen Workaround implementiert.

weil ich dann auf dem IterChainSearch unmittelbar die builder-Funktionen definieren kann

denn: TreeExplorer == IterableDecorator< Pipeline >

der oberste Layer, also hier IterChainSearch

ist selbst ein StateCore. Also brauche ich noch einen Dekocator

Wenn der fehlt, wird der nächste darunterliegende Decorator gepullt,

und der wickellt direkt den Expander ein

konfiguriert danach direkt den Filter

...weil der Basis-Iterator (also der Template-Parameter SRC)

explizit und absichtlich einen anderen Typ haben könnte, als der expandierte Kind-Iterator.

Das ist ein wesentliches Feature dieses Expander-Designs, würde aber leider eine

komplett generische Accessor-Funktion unmöglich machen (das Template würde in

einem Solchen Fall insgesamt vom Compiler zurückgewiesen)

sausage-bacon-tomato-and-sausage-spam-spam-bacon-spam-tomato-and-spam-spam-bacon-tomato-and-spam-bacon-tomato-and-spam-tomato-and-spam

sausage-bacon-tomato-and-spam-spam-bacon-spam-tomato-and-spam-bacon-tomato-and-bacon-tomato-and-tomato-and

das heißt, diese Funktion darf es nur einmal geben, und das muß dann »die« einschlägige Implementierung sein, ohne Wenn und Aber

also...

  • nimmt einen Lumiera Forward-Iterator
  • verpackt ihn als »state-core«

einfach und minimalistisch implementiert — und zudem sind undendliche Folgen nur etwas für Mathematiker und Freunde der funktionalen Programmierung; in der Praxis braucht man zwingend stets eine Abbruch-Bedingung, allein schon weil sonst die Engine „Amok laufen“ könnte (man bedenke nur, daß die interne Zeit-Repräsentation bereits lange vor der Frame-Nummer wrappt)

optional die Möglichkeit für einen getriggerten Stop vorsehen

  • Hilfsfunktionen fehlen (z.B. Adaptieren des Funktors an StateCore)
  • TreeExplorer sollte itertools nicht includieren (gegenwärtig tut er's aber nur wegen IterSource, und das könnte man refactorn)

das bedeutet: man kann alles, was per std::invoke aufrufbar ist, in eine Ergebnis-Repräsentation materialisieren. Im Besonderen nivelliert das automatisch (zur compile-Zeit) die Unterscheidung void / Rückgabewert. Jedwede Exception beim Aufruf wird gefangen

...wie bekommt man dann den konvertierten Wert 'raus.

Visitor ist entweder void, oder bool

  • kein allgemeines Such-Framework bauen!
  • den Begriff des "Containers" knapp halten: was keine const_reference bietet, ist kein Container

Man würde also neben das Standard-Format ein toleriertes zweites Format stellen, welches dann ein Bürger zweiter Klasse wäre, aber auf allen wichtien APIs als 2.Alternative mit auftaucht. Zudem würde man gewisse Abkürzungs-Pfade schaffen, auf denen die alternative Spec dann verlustfrei durchgereicht werden kann.

  • es ist überhaupt nicht klar, welches Format dann der Standardfall sein sollte
  • das läuft vor allem auf eine Performance-Betrachtung hinaus, und einen trade-off, wo man ggfs Fehler durch andere Programmierer akzeptiert

wenn man zum µ-Grid eine eindeutige Rundungs-Regel hinzufügt,

kann es praktisch auch Sound-Samples korrekt addressieren:

1/96000 ≙ 10,41666666666666666667 µTicks

im Besonderen bei den pragmatischen Lösungen

und ganz im Besonderen: wir stützen uns für die Zeitbehandlung nicht auf libGavl ab

FSecs durch einen neuen Wrapper RSec ersetzen

nicht implizit konstruierbar aus int64_t

...seinerzeit fand ich diese Art »Offenheit« noch gut; auch weil ich mir erhoffte, damit mehr Contributors zu bekommen. Die Erfahrungen sprechen dagegen; klar, die Leute mögen erst einmal gerne „move fast and break things“ — aber wenn dann Aufräumen oder anstrengende Konzeptions-Arbeit notwendig würde, bleibt alles liegen und man verschwindet aus dem Projekt.

Begründung: sie entstehen als Delta aus validen Zeitpunkten

d.h. der usage context entscheidet, ob wir einen Wert,

eine Referenz oder einen konstanten Wert verwenden

Record selber ist immuable

aber hat eine Builder-Mechanik

eigentlich fehlte nur die get()-Operation

erledigt... ähm vertagt

aber nicht wirklich; der workaround könnte schon die Lösung sein #963

ich hatte damals beim Variant und zugehörigen Buffer die Sorge,

daß ich die Implikationen einer generischen Lösung nicht durchdringen kann.

Und ich wollte keine Zeit auf einen exzessiven Unit-Test verwenden

generische Lösung verschoben #963

C++11 erlaubt =default

nicht klar, ob wir das überhaupt brauchen

  • entweder nur die unmittelbaren Kinder -> komplexe Logik fällt auf den Client
  • oder nur die Blätter -> man kann die Baum-Struktur nicht wirklich nutzen

Entscheidung

was wir brauchen

geht nicht:

rekursiver Abstieg in der Mitte eines Iterators

das war die Quintessenz der ganzen Entwicklung zum IterExplorer

Nachdem ich die depth-first / breadth-first -Strategien systematisch aufgebaut hatte,

habe ich das dann reduziert und kompakt nochmal geschrieben.

Sehr schön!

übrigens: genau den verwenden wir auch zur Job-Planung

...denn wir müssen den Weg zurück finden.

Wenn also eine Datenstruktur nur einfach verzeigert ist, oder direkt rekursiv (wie bei uns),

dann ist es absolut unmöglich, eine Traversierung mit konstantem Speicher zu machen.

Das geht nur bei einer Struktur mit Rückreferenzen -- diese enthalten dann nämlich genau den Speicher,

der während dem Einstieg in die einfach verzeigerte Struktur auf dem Stack liegt. Aber letztere

braucht nur eine logarithmische Menge an Speicher, und das auch nur während der Traversierung.

Dies ist die Abwägung, und darunter läßt sich nichts weghandeln.

Der einzige verbleibende Freiheitsgrad ist, bei einer unmittelbaren rekursiven Programmierung

direkt den Prozessor-Stack für die Speicherung des Rückweges mitzuverwenden;

in dem Moment, wo ich mich für einen Iterator entscheide, ist diese Möglichkeit weg.

kann genauso effizient werden

aber nur, wenn man die Initialisierung hinbekommt

oder diese Logik

fest verdrahten

da es sich um einen disjunktiven Typ (entweder-oder-Typ) handelt,

könnte man die Storage mit beiden Bedeutungen überlagern.

Voraussetzung wäre, daß man anhand der konkreten Daten gefahrlos  jeweils herausfinden kann,

welcher Zweig grade gilt. Da wir aber keine Introspektion haben (und auch nicht wollen!),

würde das auf Taschenspielertricks mit der Implementierung hinauslaufen

  • GenNode und Record beginnen beide fraktisch mit einem String. Man müßte diesen interpretieren können
  • oder man nutzt die letzten Bits des Pointers, um sich dort eine Flag zu speichern...

Damit ist schon klar: sowas macht man nicht ohne Grund

Entscheidung: falls eingebetteter Record

Begründung: das Durchlaufen und Rekonstruieren eines Baumes

ist letztlich doch ein sehr spezieller Fall, und rechtfertigt nicht,

den HierarchyOrientationIndicator in jeden Iterator einzubetten.

Zumal -- wenn der level zugänglich ist -- kann man diese Mechanik genauso gut

dort direkt ansiedeln, wo sie gebraucht wird.

also keine Monade

Gleichheit

kombiniert den Wert-Match mit der Iteration

Zweck: kompaktes Anschreiben

von literalen Daten

Object builder

Problem ist, wir definieren den Typ Record generisch,

verwenden dann aber nur die Spezialisierung Record<GenNode>

Und die Builder-Funktionen brauchen eigentlich spezielles Wissen über den zu konstruierenden Zieltyp

Mutator selber is noncopyable

Ergebnis move

pro / contra

Move ist gefährlich

aber auch deutlich effizienter,

denn wir müssen sonst das ganze erzeugte Ergebnis einmal kopieren.

Nicht sicher, ob der Optimiser das hinbekommt

nur auf dem Mutator

dieser ist nicht kopierbar

und muß dediziert erstellt werden

möglicherweise schon gelöst,

denn Record ist insgesamt immutable.

Also können wir einen Find mit einem const_iterator machen

was sinnvoll ist,

hängt vom Payload-Typ ab

bei einer 'key = value' -Syntax mit strings

ist nur ein Value-Rückgabewert sinnvoll

...auch kann man auf diesem Weg die Storage konfigurierbar machten

da wir einen IterAdapter verwenden, können wir nur eine 'pos' (einen Quell-Iterator)

als Zustands-Markierung verwenden; die gleiche 'pos' wird aber auch inkrementiert und dereferenziert.

Daher ist die einzige praktikable Lösung, daß die Typ-ID in einem weiteren Vektor gespeichert wird.

Das könnte dann ein Metadaten-Vektor sein.

Natürlich ist dieser Ansatz nur sinnvoll, wenn wir wirklich Metadaten brauchen.

Denn jeder Record zahlt den Preis für die komplexere (zusätzliche) Datenstruktur!

scheidet aus, wegen Wertsemantik

mit speziellem Ref-Typ

-- im DataCap

heißt: in der Diff-Verarbeitung wird dieser spezielle check verwendet

m.E. die einzig saubere Desgin-Variante!

gemeint ist:

  • man kann alternativ auch eine RecordRef direkt in eine elementare GenNode packen
  • diese verhält sich dann nicht transparent, denn sie hat eine andere Identität als ihr Ziel
  • das kann aber als spezielles Ausdrucksmittel genutzt werden

heißt: wird direkt von standard-equality so behandelt

brauche speziellen Builder,

der das so fabriziert

bekomme einen

"ungenutzten" DataCap

Idee: Ref-GenNode

als Ref erkennbar

(Prädikat)

hash-identische

Ziel-ID ableitbar

Verarbeiten

von Teilbäumen

diese Delegates sind anderswo in eine Collection eingebunden

und deren Reihenfolge ist dort von Bedeutung

brauche Detektor für structural change

...man könnte auch auf die Idee kommen, es nur in das Collection-Binding einzuhängen.

Das wäre aber zu kurz gedacht; auch wenn im Moment dieses die einzige Implementierung ist, die den Listener tatsächlich triggern kann, verbietet uns niemand in der Zukunft, noch eine anderes TreeMutator-Binding zu erfinden. Hinzu kommt, daß ein Listener zusätzliche Storage und zusätzlichen Aufwand bedeutet, und deshalb besser als eigener »onion layer« implementiert wird

und zwar etwas wirklich Einfaches

denn "elementar" bedeutet in diesem Fall, es wird ziemlich technisch und komplex

oh ja....

das war ziemlich halbgar, und wäre früher oder später ganz übel in's Auge gegangen.

Erinnere mich, daß ich damals beim Einführen des MockElm dann eben doch eine »Attribute-Map« gebaut habe, obwohl ich vorher mich eigentlich gegen dieses Konzept entschieden hatte. Es ist in der Praxis dann doch manchmal sinnvolle, wie dieser Fall zeigt. Damit wurde aber mein TreeMutator-Collection-Binding "wackelig", und ich hab mich da mehr oder weniger durchgefrickelt.

Jetzt gibt es also echte ContainerTraits, und damit ist der Aufbau zukunftsfest.

...indem man nur teilweise PICK-Verben sendet, und dann einfach weggeht.

Dann wir der Rest mit dem Mutator zusammen weggeworfen.

um das zu unterbinden, müßten alle Binding-Layer kollaborieren

..weil man diesen Umstand nicht generisch erkennen kann.

Nach gegenwärtigem Stand kann das nur im Collection-Binding auftreten;

dieses aber hätte auch die notwendigen Informationen, um diese Situation zu erkennen

d.h. es prüft, ob keine Elemente im Arbeitspuffer übrig sind

denn einfacher wird der Code dadurch nicht, nur kürzer.

Und mühsam für den Leser ist er so oder so... dann also lieber alles ausschreiben

da muß man auch erst mal drauf kommen, was einem der Compiler da so sagen will.

use of deleted function <some copy-ctor>

note: function <some-copy-ctor> is deleted, because the default definition would be ill formed

error: ...und hier kommt nun die default-definition...

und damit wiederholt sich das Ketten-Argument bis zur Basisklasse runter!!!

selbst wenn er über x Basisklassen von MoveOnly erbt

matchElement([](GenNode const& spec, ELM const& elm)

                         {

                           return spec.matches(elm);

                         })

  • Map hat kein emplace_back
  • Map hat kein back()

Beides ist erst mal sinnvoll. Map hat zwar ein emplace, aber das fügt eben irgendwo ein

Und es gibt nicht sowas wie das "zuletzt behandelte" Element

Reihenfolge

erhalten!

...hat eine "zufällige" Reihenfolge, die von den Hash-Werten der gespeicherten Daten abhängt.

Das bricht mit unserem grundsätzlichen Konzept der kongruenten  Daten-Strukturen

Ein Diff, das von einer ETD gezogen wurde,

läßt sich nicht auf eine Map-Implementierung aufspielen

Entscheidung

...zum Beispiel wie grade hier, beim MockElm

das wird vermutlich niemals wirklich in einem vollen Diff-Zusammenhang gebraucht.

Und dann ist unbestreitbar eine Map eine sehr einfache Implementierung

und auch im Diff-Applikator nicht wirklich schwierig zu unterstützen

Interpreter definiert Sprache

ROOT

INIT

leeres

Objekt

pick(Ref::CHILD)

würde sagen: ja, aber auch nur für das after-Verb!

allgemein halte ich einen wrap-around für keine gute Idee,

weil er zu Zweideutigekeigen führt und daher Struktur oder Konsistenzfehler überspielt

läßt sich stets duch eine inverse Folge von find und pick  emulieren

vorerst verworfen, da zusätzlicher Prüf-Aufwand

...Grund: sie werden durch einen jeweils komplett anderen Ansatz implementiert

  • "Liste" beruht auf dem Attribut-Iterator und dem Aufbauen einer neuen Attribut-Sammlung
  • "Map" beruht darauf, alle Operationen an die Storage zu delegieren

das heißt, man kann Attribute in einer "sinnvoll lesbaren" Ordnung anschreiben

und später angefügte Attribute bleiben so erkennbar.

Vorteilhaft für Version-Management

profitiert also von allen Verbesserungen des allgemeinen Algorithmus

"hoch effizient", unter der Annahme, daß fast immer nur konforme Änderungen kommen.

Weil dann nämlich die in unserer Implementierung ggfs. kostspieligen Umordnungen entfallen,

kommen wir auf lineare Komplexität für die Verarbeitung

+ NlogN für den Index zur Diff-Erzeugung

unsere Impl der Diff-Erzeugung (!)

baut einen Index auf (N*logN), um Einfügungen/Entfernungen zu erkennen und Umordnungs-Suche zu unterstützen.

Wenn wir aber von ausschließlich konformen Operationen ausgehen,

wird dieser Index nicht benötigt. Leider können wir das aber nicht garantieren, denn

es könnte ja zwischenzeitlich ein Attribut gelöscht und dann später (am Ende) wieder

angehängt worden sein, was dann eben doch einen Index erfordert, um einen

korrekten Listen-Diff zu erzeugen

d.h. wenn die Storage hoch-optimiert ist,

dann überträgt sich das auf die Diff-Behandlung

da wir Attribute in einer Liste speichern,

müssen wir für jede Einfügung eine vollständige Suche machen

...gemeint ist: extra, anders als die normale Listenverarbeitung.

Auch wenn diese andere Implementierung nur delegiert

danach noch auftretende Attribute

erfordern Sonder-Behandlung,

indem sie an die Attributs-Liste angehängt werden

wegen Entscheidung für das "Listen"-Modell zur Attribut-Handhabung

das heißt:

  • es wird einfach vom zuständigen Layer (der für die Attribute) aufgegriffen
  • es hat keinen Einfluß auf die nach außen sichtbare Reihenfolge
  • diese Reihenfolge bleibt gruppiert nach Attributen / Kindern

...da das Kind in der Liste der Attribute nämlich garnicht gefunden wird

...wenn wir am Ende der Attribut-Zone stehen,

und die nächste Operation ein fetch eines Kindes ist, müssen wir implizit den

Wechsel in den Scope vollziehen und die Operation dort ausführen.

Aber an allen anderen Stellen in der Attribut-Zone ist ein solcher Fetch ein Fehler!

standardmäßig strikt

List-Diff

als Spezialfall

kann auch nicht

wegen dem Interpreter

leicht auf generischen Container

zu verallgemeinern

Erkennung hat die Sprache als Parameter,

und verwendet sie zur Token-Generierung

man kann auch dem List-Detector

eine Tree-Diff-Language geben

Frage: in-Place?

entscheidende Frage: wie addressieren?

und wird durch die Diff-Anwendung konsumiert

Immutablility erzwingt

  • persistente Datenstrukturen
  • garbage-collector

Lösung: wir arbeiten auf einem Mutator

auf dem Umweg über einen ContentMutator

Innereien des alten Record verbrauchen

Problem: Rekursion

wenn ein MUT kommt

erzeugt man lokal einen DiffApplikator für den geschachtelten Kontext

und gibt ihm rekursiv den Diff hinein. Wenn dieser Aufruf zurückkehrt

ist der gesammte Diff für den eingeschachtelten Kontext konsumiert

wenn ein MUT kommt,

pusht der Applikator seinen privaten Zustand

auf einen explizit im Heap verwalteten std::stack

und legt einen neuen Mutator an für den nested scope

Entscheidung:

interner Stack

....begründet duch die generische Architektur.

Die Trennung von Diff-Iteration und dem Interpreter ermöglicht verschiedene Sprach-Ebenen.

Allerdings werde ich für die Anwendung auf konkrete Datenstrukturen,

also den TreeMutator, vermutlich das andere Modell (rekursiv konsumieren) verwenden.

Problem sind mal wieder die automatisch generierten IDs.

Die sind natürlich anders, wenn wir die ganze Testsuite ausführen...

  • Diff-Anwendung wird massiv und in der Breite stattfinden
  • sie wird als Reaktion auf UI-Events auftreten
  • sie dient dazu, andere UI-Operationen einzusparen
  • also muß speziell das Traversieren bis an den Anwendungsort bedacht werden

...d.h. die bis jetzt geschriebene TreeApplikator-Implementierung

ist erstaunlich leichtgewichtig. Zu den zwei Indirektionien der Sprache

kommt nur entweder ein weiterer aus der GenNode bzw stattedessen ein dynamic cast hinzu.

Alles andere steckt in dem expliziten Mutator-Typ

 -- das gibt einen wichtigen Hinweis --

...da wir eine verb-basierte Sprache implementieren,

also einen double-dispatch haben

weil wir den Anwendungs-Kontext noch überhaupt nicht kennen.

Man könnte also später, wenn das ganze System "steht",

das Diff-System noch einmal reimplementieren, dann mit einem vorgegebenen Diff-Typ

Beschluß: akzeptiert

im Sinn von "polymorpic value" ist das Backend virtuell

....wenngleich auch dieser aus einem Template generiert wird

(will sagen, es ist nicht sofort offensichtlich, daß wir jeweils einen Interpreter generieren)

wir verzichten auf Introspektion der Elemente

denn genau zu diesem Zweck haben wir die "External Tree Description"

...d.h,

kann zusätzlich zu einem anderen Adaptor

in die Mutator-Dekorator-Kette gehängt werden

und protokolliert somit "nebenbei" was an Anforderungen an ihm vorbeigeht

streng genommen ist es nur erlaubt, das ID-Symbol auszuwerten

Visitor bedeutet zwei Indirektionen

...und das ist nicht akzeptabel für ein reines Selektor-Prädikat!

denkbar nur bei Sub-Objekten

gilt für alle praktischen Anwendungen

....auch wenn man zehnmal meinen könnte,

Kinder eines reinen Wert-Typs wären sinnvoll --

sie sind es nicht!

Jede sinnvolle Entität hat mehr als ein Attribut!

denn es macht keinen Sinn, Entitäten und reine Wert-Elemente

auf der gleichen Ebene in der gleichen Sammlung zu mischen.

D.h., entweder man hat ein Objekt, das als Kinder z.B. eine Liste von Strings hat,

oder man hat eine Entität, die z.b. zwei getypte Objekt-Kinder-Sammlungen hat,

wie z.B: eine Spur mit Labels und Clips

"target matches spec"

aber existiert nominell und kontext-abhängig

das sind die konkreten Implementierungen

für spezifische Arten von Bindings

kann niemals geschachtelte sub-Mutatoren modellieren

ja wirklich, das wäre nicht sinnvoll!!!!!

auch wenn man meinen könnte, es geht.

Grund ist nämlich, es kann jeweils nur ein Onion-Layer für ein gegebenes Element "zuständig" sein.

Und aus Gründen der logischen Konsistenz darf dieser Diagnose-Layer niemals für ein Element zuständig sein,

denn sonst würde er es für darunter liegende Layer verschatten.

immer Mitwirkung des Elements

weil beim Assignment die Spec (=GenNode) eben

zwar die ID des Zieles, aber den neu zuzuweisenden Wert enthält.

Also wird sich das Ziel nicht anhand des neuen Wertes finden lassen,

weil es eben grade noch nicht diesen neuen Wert trägt.

generische Repräsentaton ist so gewählt,

daß sich alle relevanten Eigenschaften darstellen lassen

wenn also ein Teil der diff-Funkttionalität nicht verfügbar ist,

dann wird es wohl so sein, daß sie auch nicht gebraucht wird

zwar erscheint es nicht sonderlich sinnvoll,

als target auch eine Menge von primitiven Werten zuzulassen.

Es gibt aber auch keinen wirklichen Grund, dies zu verbieten,

sofern es gelingt, die Funktionalität gutmütig zu degradieren.

...will sagen,

da sind mehrere Layer an praktisch ungebundenem Template-Code dazwischen,

so daß zu befürchten steht, daß ein unpassendes Lambda erst weit entfernt

eine womöglich irreführende Meldung generiert

erfordert wirklich Kooperation

...denn wir verwenden hier als "private" Datenstruktur

eine etwas komische Collection von Strings,

in die wir die String-Repräsentation der Spec-Payload schreiben.

In der Praxis dagegen würde man wirklich einen privaten Datentyp verwenden,

und dann auch voraussetzen, daß man nur Kinder dieses Typs (oder zuweisungskompatibel) bekommt.

Mein Poblem hier ist, daß ich in dieser Demonstrations-Datenstruktur keine nested scopes repräsentieren kann.

Aber hey!, es ist meine private Datenstruktur -- also kann ich einfach eine Map von nested scopes

daneben auf die grüne Wiese stellen. Ist ja nur ein Test :-D

...dankenswerterweise hat der subscript-Operator von std::Map

die nette Eigenschaft, beim ersten Zugriff auf einen neuen Key

dessen payload per default-konstruktor zu erzeugen.

der Builder in der nested DSL generiert einen sonderbar falschen "this"-Typ,

genauer gesagt, eine TYPID die falsch ist.

Und zwar kommt es da zum "Übersprechen" von einem Typ-Parameter in den anderen.

Im Besonderen hab ich beobachtet, daß, wenn man auf den 3.Typparameter ein Lambda gibt,

dann auf dem 4. oder 5. Typparameter der bisherige /alte Typ des 3.Typparameters auftaucht,

u.U auch eingeschachtelt als ein Argument.

Habe mich aber davon überzeugt, daß die eigentlichen Typ-Parameter in Ordnung sind.

Und zwar habe ich das verifiziert

  • durch Ausgeben der Typen im Konstruktor (mithilfe meiner typeStr<TY>()
  • durch Einbauen einer Static-Assertion mit Signatur-Match

gemeint ist:

die native Datenstruktur ist eine Collection von Elementen,

welche ohne Weiteres direkt in eine GenNode gepackt werden könnten. Denn dann läßt

sich eine einfache Default-Implementierung des Matchers angeben

Typisches Beispiel: eine STL-Collection von Strings.

das heißt, wir können Elemente in der gleichen Reihenfolge anfügen, in der sie später dann in der Iteration erscheinen

  • Map hat kein emplace_back
  • Map hat kein back()

Beides ist erst mal sinnvoll. Map hat zwar ein emplace, aber das fügt eben irgendwo ein

Und es gibt nicht sowas wie das "zuletzt behandelte" Element

MockElm, Z 260 : .attach (collection(attrib)

wir integrieren Attribute nicht, weil es so schön symmetrisch ist,

sondern weil sie essentiell zum Wesen von Objekten gehören.

Wenn wir Änderungen an Objekt-Strukturen als Diff erfassen wollen,

dann müssen Attribute irgendwie sinnvoll integriert sein

immer in der Klasse verankert

⟹ es geht eigentlich nur um den Wert des Attributes

manche Felder sind optional

unter der Maßgabe,

wie ETD ein Objekt repäsentiert

"Anwendung" : meint das Anwenden eines Diffs auf ein Ziel-Objekt

"nicht nutzen" : meint ignorieren und verwerfen der Information

meint: ETD -> Objekt und dann später Objekt -> ETD

warum?

Weil sich in der ETD die Reihenfolge ändern kann,

und aber das Aufspielen eines Diffs auf beiden Seiten

zwingend die gleiche Reihenfolge erfordert!

Objekt -> ETD -> Objekt

warum?

weil das Quellobjekt keinen Diff erzeugen wird,

der sich letztlich nicht auf das Zielobjekt aufspielen läßt

abweisen, was das Kriterium sicher verletzt

mandatory : Wert muß per Konstruktor gegeben sein

default : es gibt einen ausgezeichneten Standardwert

das heißt, in dem ins-Verb ist dann ein komplettes Objekt enthalten,

nicht nur eine leere Record-Hülle, die nachfolgend populiert werden kann (aber nicht muß)

Konstruktor befüllt das Feld halt irgendwie.

Ab dem Punkt verhält es sich aber wie ein normales (mandatory) Feld

das Objekt selber kann erkennen, ob das Feld sich im "default-Zustand" befindet

ohne Prüfen ist emptySrc nicht implementierbar

...weil es für emptySrc keine neutrale Antwort gibt.

Denn dieses Prädikat wird von der typischen Implementierung des Diff-Applikators

in beiden Richtungen verwendet, also sowohl Prüfung auf empty ("expect no further elements"),

alsauch der Check, daß überhaupt noch Quellelemente anstehen

d.h., man kann nur global auf Prüfung verzichten 

und da habe ich mich bereits dagegen entschieden

Feld unterstützt default-Wert

Auslegung der

Primitiven

rationale: object fields are hard wired,

thus always available

Einschränkung: accept_until END

...nämlich indem alle Attribute als "berührt" und akzeptiert markiert werden.

Somit könnten sofort Zuweisungen als Nächstes passieren

analog wie assignElm

das heißt, es findet keine Verifikation statt

zu bindende

Operationen

sieht nach Ober-engineering aus,

zumal das erhebliche Statefulness bewirkt

unterstelle Ziel als konstruierbar aus Payload

da effektiv bereits der Setter diese Funktionalität enthalten kann und muß,

denn der Setter nimmt eine GenNode

injectNew tolerieren

....man könnte genausogut auch beim ersten Mal zuweisen

denn die Diff-Anwendung auf GenNode unterstützt Zuweisung

ausschließlich bei schon existierenden Elementen. Demnach muß dort auch jedes Attribut

  • entweder schon mit dem Konstruktor mit gegeben worden sein
  • oder vorher einmal explizit eingefügt

und die Keys der Attribute stecken in der GenNode selber!

...denn wir vermeiden dadurch Komplexität.

Der gesendete Diff muß einfach passen!

Genau deshalb haben wir auch in GenNode verschiedene Varianten des gleichen Grundtyps,

damit wir nicht in die ganzen Ungewissheiten der widening conversions laufen!

d.h. der Attributwert hat Wertsemantik und wird einfach zugewiesen

...d.h. der Attributwert ist ein Objekt und damit ein nested Scope

Problem: immutable values

das alles passiert dann im Lambda

Dilemma: defaultable fields

....mit der ETD,

bzw der Anwendung des selben Diffs auf eine GenNode-Struktur.

Konsequenz: wenn ein feld defaulted war, und nun explizit gesetzt wird,

muß dies als INS geschehen, denn eine Zuweisung an nicht aufgeführtes Element ist verboten

folglich ein Problem,

zu erkennen, wenn wir fertig sind

....weil das defaultable field noch nicht vom Diff berührt wurde.

Aber es ist kein optional field, d.h. wir haben keine Flag, die es als "defaulted" kennzeichnet

Lösung: alles immer explizit

diese Lösung war zunächst mein Favorit.

Sie erscheint sehr elegant, weil man im TreeMutator überhaupt nichts dafür tun muß.

Und die Zusatz-Forderung, daß dann eben das Diff richtig gesendet werden muß,

erscheint "geschenkt", da wir ohnehin zunächst einmal die Diffs explizit im Code erzeugen.

Aber, nach längerer Überlegung wurde mir der Ansatz mehr und mehr zweifelhaft.

Das ist die Art von Verkoppelungen, hier die implizite Annahme einer bestimmten Implementierung,

die ein System unerklärbar und schwer wartbar machen. Das ist die Art von "Features",

für die man sich nach einiger Zeit entschuldigen muß.

Und noch schlimmer: eigentlich läuft dieser Ansatz darauf hinaus, die Konsistenzprüfung

am Ende zu deaktivieren. Nur wir machen das nicht explizit, sondern durch die Hintertür.

Also dann besser klar und ehrlich!

...denn unter dem Strich würden wir hiermit volle Unterstützung für opitonale Attribute einführen,

also eine Attribut-Semantik auf eine Feld-Semantik draufpflanzen.

Aber in der vorausgegangenen Analyse habe ich mich schon davon überzeugt,

daß wir keine Attribut-Semantik brauchen. Und wenn doch, dann bietet das Diff-System

immer noch die Möglichkeit, die Attribute explizit als Sammlung darzustellen.

auf die empty-Prüfung am Ende verzichten

denn in den meisten, wichtigsten Fällen get es um einen non-empty-check,

bevor ein anderes Verifikations-Prädikat angewendet wird.

jedwede "bessere" Implementierung muß zwingend einen Container verwenden,

der dann die Lambdas für die einzelnen Setter auf den Heap legt.

Das ist hier tatsächlich viel schlechter, als das bischen lineare Suche

....durch meinen allerersten Draft,

für den ich damals gezwungen war, die GenNode zu erfinden :)

gleiches Argument...

...damit unterstellen wir, daß später eine Symbol-Tabelle aufgebaut wird.

Dann kann man sich immer noch überlegen, ob man dann an dieser Stelle bereinigt

in einem Fall kann man sie aus der Closure abgreifen

im anderen Fall muß es doch der Client leisten.

Keine klare Linie

...das heißt, es gibt nur minimale, themantische Überlappung.

Also ist die Verwendung von Vererbung hier sogar die beste Lösung

Geschachtelte Typdefs lassen sich vermeiden:

BareEntryID speichern!

...das heißt, wie rum man es auch auflöst, wird die Lösung auf einer Seite schlechter

  • wenn wir für den Payload-Typ einen Typ-Parameter nehmen, blähen wir den Standard-Fall (Setter) auf
  • andererseits ist es unbstreitbar einfach so, daß für den Mutator-Builder die Typisierung komplett implizit ist, das muß die Closure mit sich selbst ausmachen, einfach indem in der Closure ein geschachtelter TreeMutator konstruiert wird, der eben mit diesem impliziten Kind-Typ umgehen kann.
  • wenn wir stattdessen nur einen Key-String speichern, wird zum Einen die Match-Prüfung aufwendiger (Stringvergleich statt Vergleich von Hashes), und außerdem wird ein Typ-Mismatch nicht mehr auf der Ebene der Verb-Anwendung entdeckt und entsprechend gekennzeichnet, sondern wir hoffen, daß es dann innerhalb der Closure zu einem Fehlzugriff auf die Payload der GenNode kommt. Noch schlimmer im Mutator-Fall, da sind wir dann schon im geschachtelten Scope und hoffen, daß dann der eingeschachtelte Mutator irgendwo auf Widerspruch läuft.

...gedacht für verschiedene UseCases.

  • Fall 1: String-Key und der Typ muß irgendwie implizit/explizit gegeben sein
  • Fall2: GenNodeID

...die offensichtlichsten Dinge übersieht man nur zu leicht!!!!!

Da es ein nested scope ist, ist es immer ein Objekt,

also repräsentiert als Rec<GenNode>

zwei Bindings

zwei Collection-Bindings

...diese Abkürzung ist nur auf den Konstruktur aufgepflanzt,

nicht aber in der eigentlichen Implementierung verankert.

Das wollte ich nicht, weil ich längerfristig doch davon ausgehe,

daß es einfach einen Metadaten-Scope gibt

Die Inkonsequenz nun ist, daß im Rec::Mutator keine Magie dafür vorgesehen ist

...mit den Lambdas kann ich nur die Sicht auf die Werte steuern,

nicht aber das eigentliche Verhalten des Bindings.

Denn die Lambdas haben keinen Zugriff auf die Ziel-Datenstruktur!

...wir wollen mehrfach geschichtete TreeMutator-Subklassen,

aber tatsächlich liefert jeder DSL-Aufruf einen Builder<TreeMutator<...>>.

Die normalen DSL-Aufrufe sind eben genau so gestrickt, daß jeweils der oberste Builder entfernt wird,

ein neuer Layer darübergebaut und das Ganze wieder in einen Builder eingewickelt wird.

Dadurch ist es schwer bis unmöglich (wg. den Lambdas), den resultierenden Typ anzuschreiben.

Daher bin ich zwingend auf Wrapper-Funktionen angewiesen, die diesen resultierenden Typ

vom konkreten Aufruf wieder "abgreifen". Ich kann daher nicht die DSL-Notation verwenden,

um den Dekorator für die Behandlung des Typ-Feldes einzubringen.

Mut -> Rekursion

Problem: partielle Ordnung

...das heißt,

das AFTER-Verb wird übersetzt in ein skip_until,

und das läuft dann entweder in jedem Layer

oder nur in dem Layer, der auf die Spec paßt.

In jedem Fall gerät dadurch die relative Verzahnung der Elemente untereinander aus dem Takt

...das heißt also, es wird stets der zuerst gebundene Layer komplett durchgespult,

gefolgt dann von dem nächsten Layer.

Die Konsequenz ist, daß es keine Mischung der Typen geben kann.

Es müssen immer zwingend alle Elemente eines Typs von einem Layer behandelt werden

und diese Elemente müssen geschlossen hintereinander in der Reihenfolge liegen

auf Basis des neu geschaffenen TreeMutators

....man könnte später geeignete Automatismen schaffen,

die sich diesen TreeMutator beschaffen

  • indem erkannt wird, daß das eigentliche Zielobjekt ein bestimmtes API bietet
  • indem andere relevante Eigenschaften des Zielobjekts erkannt werden

...das so häufig in C++ auftretende Problem:

wie baue und verwalte ich eine konkrete Implementierung,

ohne gleich ein ganzes Management-Framework einführen zu müssen.

Letzten Endes lief  das auch in diesem Fall auf inline-Storage hinaus...

...zumindest im GUI, wo parktisch alle Empfänger auch ein Tangible (Widget oder Controller) sind

...und genau diese virtuelle Builder-Methode ist üblicherweise der Ort, wo mit einer DSL und über Lambdas das Mapping auf die internen Strukturen des DiffMutable hergestellt wird; deshalb kann auch DiffMutable selber ein sehr schlankes Interface sein; der resultierende TreeMutator ist dann eine für den jeweiligen konkreten Typ aus Bausteinen generierte Hilfsklasse

...und dort per smart-Ptr.
Für diesen Fall kann die Verdrahtung weitgehend automatisch konfiguiert werden, und man muß eigentlich nur noch den Konstruktor-Aufruf explizit (per Lambda) in das TreeMuator-Binding integrieren. Wenn es mehrere »onion layer« gibt, muß allerdings auch noch ein »Selector« definiert werden, um zu steuern, wo genau dieses Binding angewendet wird, und wo sonst auf einen anderen Layer delegiert wird, z.B. für explizit gebundene Objekt-Attribute.

....daß ein unbedarfter client diesen Trick übershieht

und daher den Rückgabewert wegwirft.

Argument: we soweit einsteigt, die Metaprogramming-Lösung zu nutzen,

sollte auch intelligent genug sein, die API-Doc zu lesen.

Standard == Interface DiffMutable implementieren

Client soll direkt mutatorBinding bieten

nicht generisch: mutatorBinding

Lösungsversuch: extern template

...im Klartext: diesen Zugriff von der generischen Implementierung

auf den eingebauten Stack-Mechanismus benötigen wir nur...

  • einmal zu Beginn, bei der Konstruktion
  • wenn wir in einen geschachtelten Scope eintreten
  • wenn wir einen Solchen verlassen

Zwar sind indirekte Calls aufwendiger, aber letzten Endes auch wieder nicht soooo aufwendig,

daß sie uns im gegebenen Kontext umbringen...

intern: eingebaute initDiffApplication()

...wird automatisch vor Konsumieren eines Diff aufgerufen

Widerspruch: TreeMutator ist Wegwerf-Objekt

Lösungsversuch: doppelte Hülle

kann daher TreeMutator konstruieren

...und zwar per mutatorBinding

implementiert somit initDiffApplication()

TODO: Namensgebung

TreeMutator-Binding muß opaque bleiben

Buffer-Größen-Management vorsehen

das heißt

  • ein sinnvoller Startwert wird heuristisch vorgegeben
  • wenn die Allokation scheitert, die Exception fangen und die tatsächlich benötigte Größe merken

...das heißt:

gegeben ein syntaktisch sinnvoller top-level-Aufruf ("wende das Diff an")

-- wie bzw. von wem bekommen wir dann ein Binding, das einen passenden TreeMutator konstruiert?

erscheint mir die am wenigsten überraschende Lösung.

und zwar per handle.get()

erscheint mir fehleranfällig und irreführend für den Nutzer der Schnittstelle.

Denn er muß zwar das Objekt in das Handle platzieren, dann aber auch noch einen Pointer zurückgeben,

der dann auch noch NULL sein kann, zum Signalisieren von Fehlern.

Ich empfinde das als schlechten Stil

naja, das wäre billig, aber auch wieder beliebig.

Es macht keinen Sinn vom API-Design her, sondern man müßte es halt machen,

weil die Implementierung den Zeiger auf den geschachtelen sub-Mutator umsetzen muß.

die Diff-Sprache verlangt,

daß vor dem Öffnen des geschachtelten Scopes

dieser zumindest einmal per ins "angelegt" wurde.

Das ist verständlich aus der Historie: Zunächst einmal war der TreeMutator gedacht als ein Interface, das der client des Diff-Frameworks zu implementieren hat. Nachdem ich diese Übung aber drei mal gemacht hatte, war mir klar, daß dies zu viel verlangt ist. Denn der TreeMutator ist notwendigerweise stark an den Implementierungs-Ansatz im Diff-Framework gebunden. Das heißt, man kann dieses Interface nur implementieren, wenn man diese interne Funktionsweise verstanden hat. Und das Diff-Framework würde seinen Zweck verfehlen, wenn der Nutzer dieses Wissen haben müßte. Also habe ich über den TreeMutator ein Baukastensystem errichtet, welches über Lambdas in den Anwendungskontext gebunden wird.

In einem zweiten Anlauf habe ich schließlich die schon bestehenden, explizitien Implementierungen des TreeMutator-Interfaces allesamt "eingefangen" und durch ein Meshup aus dem Bauskastensystem ersetzt. Daher gibt es jetzt nur noch eine einzige valide Implementierung, nämlich die im Baukasen. Und das soll auch so bleiben.

Daher ist es zulässig, sich diese Implementierung anzuschauen: in der Tat geht nämlich dort jedem Aufruf des Mutators ein Suchvorgang voraus, und dieser endet immer mit einer erfolgreichen Anwendung des Matchers. Daher ist es grundsätzlich nicht notwendig, den ID-Match nochmal zu prüfen, bevor man in die rekursive Mutation einsteigt.

Wie kommt nun aber dieser explizit ausprogrammierte ID-Match in die Standard-Implementierung? Die Antwort ist, letztlich aus Verlegenheit. Denn zunächst einmal hatte ich für den Unit-Test alles für ein sehr spezielles Setup ausprogrammiert. Das ist auch gut so, denn dieses Setup deckt auch Grenzfälle mit ab. Also im Besonderen auch Bindings, die sich sehr speziell in Implementierungsstrukturen einklinken, und eben grade nicht direkt an ein DiffMutable-Subobjekt delegieren. Und diese Offenheit ist der essentielle Grund, warum ich auf ein Diff-Framework setze, und nicht auf ein Datenmodell mit festen Interfaces. Diese Offenheit bedingt aber auch, daß man dem Client die letztendliche Übersetzung der IDs überlassen muß. Für die rekursive Kind-Mutation muß der Client sich ggfs ein internes Implementierungs-Objekt anhand der gegebenen ID heraussuchen.

Im einfachen Standardfall jedoch ist das nicht notwendig, denn in diesem häufigsten Standardfall haben wir mehr oder weniger direkt das Objekt aus der Collection in der Hand, auf welches dann der rekursive Mutator angewendet werden soll. Also stand ich beim ersten Schreiben einer Implementierung für diesen einfachen Standardfall vor dem Paradoxon, daß die Funktion einen ID-Parameter bekommt, den man anscheinend hier gar nicht braucht. Und, ohne diese Zusammenhänge damals zu verstehen, habe ich dann aus Verlegenheit noch eine Zeile Code eingebaut, die "was Sinnvolles mit dieser ID macht", denn es war ja zunächst auch erst mal ein Proof-of-Concept (und zu diesem Zeitpunkt gab es noch zwei weitere, alternative Implementierungen des TreeMutator-Interfaces). Letztlich hat sich dann dieses "anstandshalber" eingebaute Zeile, eben genau der ID-match, welcher mithin "etwas Sinnvolles" mit der ID macht, per Copy-n-Paste in alle konkreten Implementierungen fortgepflanzt. Wiewohl dieser Schritt im Stand der gegenwärtigen Implementierung stets redundant ist.

...falls die clientseitige Datenstruktur einen Index verwendet, um per ID die eigentliche Zieldatenstruktur zu betreten.

erwarte eine Funktion getID()

...d.h. man muß dann halt doch noch den Matcher explizit in der DSL konfigurieren

...das ist ein Versuch, den Code für den Leser verständlich zu halten.

Die Idee ist, daß es einen high-level Unit-Test gibt, der die gesamte Diff-Anwendung durchspielt

und dazu passend einen low-level Unit-Test, der analog die gleichen Operationen macht,

allerdings direkt auf dem TreeDiff-Interface durch Aufruf der passenden Primitiv-Operaionen.

Letztere müssen für jede Art von "onion-layer" (konkretes Binding) erneut implementiert

und daher auch jeweils eigens per Unit-Test abgedeckt werden.

das ist hier sinnvoll. Das Binding sollte komplexer sein,

als in der Praxis auftretende Bindings. Warum? Weil letztere immer etwas einseitg sind

und damit Abkürzungen im Code-Pfad ausnützen. Die Gefahr schlummert aber im Zusammenspiel

der konkreten Bindings mit mehreren "onion layers"!

...denn es ist sehr verwirrend, welche Signatur denn nun die Lambdas haben müssen

...denn es kann keinen Default-Matcher geben....

...sonst wird niemand Lambdas bereitstellen können, oder gar Diff-Nachrichten erzeugen.

Das ist nun kein spezielles Problem der gewählten Implementierungs-Technik, sondern rührt daher,

daß der Client hier eigentlich ein Protokoll implementieren muß.

wenn überhaupt, dann im Matcher im Binding-Layer implementieren

...denn wir haben nun mehrere Layer,

und der Selector kann einfach anhand von Ref::THIS keine sinnvolle Entscheidung treffen.

Daher versuchen dann alle Layer dieses Element zu behandeln, oder gar keiner

Und da der Selector nur die Spec anschauen darf, läßt sich das auch nachher nicht mehr korrigieren

Daher habe ich mich entschlossen, dieses Sprachkonstrukt zu entfernen

entfernt, da schlechtes Design

entfernt, da schlechtes Design

anscheinend nicht notwendig

es ist wohl nicht ein spezieller Diff. Es tritt sogar auf, wenn man zweimal den gleichen Diff schickt

Diff ist eine abstrakte Quelle,

die nur einmal verbraucht werden kann

Dekorator-Prinzip.

Paßt hier, da IterSource genau dieses Vorgehen nahelegt

MutationMessage::updateDiagnostics()

...diejenige, die zum Zeitpunkt des updateDiagnostics() noch anstand

und zwar typischerweise per Indirektion an einen low-level Allocator, bei dem es sich aber auch um einen Slot in einem Allocation-Cluster handeln kann

  • bevorzugt: die »achitektonische Lösung« : es ist dafür gesorgt daß die Allokator-Impl garantiert länger lebt als der Client
  • in schwierigen Fällen: ref-counting Front-End-Handle denkbar

YAGNI.

Zunächst einmal so wenig Funktionalität wie möglich durchreichen, und das nur auf den festen Bahnen gemäß Design; im Zweifelsfall ist es besser, spezielle Funktionalität im Bedarfsfall in den Wrapper zu packen; genau das ist ja der Vorteil einer Hilfsklasse direkt im Projekt.

Zwar sichert der Standard zu, daß das Ende des ctor-Aufrufs synchronizes_with  dem Start der Thread-Funktion. Streng logisch kann das aber nur für den std::thread-Konstruktor selber gelten (andernfalls hätte man mit einem Sequence-Point argumentieren müssen, und nicht mit dem ctor selber; das würde dann aber auch wieder eine unerwünschte Statefulness einführen, weil dann im gesamten umschließenden Ausdruck der Thread eben noch nicht läuft, was das RAII-Konzept untergraben würde).


Das ist nun zwar ziemlich unwahrscheinlich (weil normalerweise der Scheduler immer eine erhebliche Zeit braucht, bis ein anderer Thread überhaupt zum Zug kommt), aber leider ist es demzufolge theoretisch möglich, daß eine im abgeleiteten Objekt definierte Thread-Funktionalität bereits auf eine noch nicht vollständig initialisierte Objektinstanz zugreift, oder daß die Initialisierung der abgeleiteten Klasse Werte in lokalen Feldern überschreibt, die aus der bereits startenden Thread-Funktion gesetzt wurden. Unerklärliches und reproduzierbares Verhalten wäre die Folge. Und das läßt sich aus dem Wrapper heraus nicht beheben (zumindest nicht ohne verwirrende Konventionen einzuführen).

explizit eine lib::SyncBarrier in der Implementierung einbinden

...damit man auch zusammengesetzte/formatierte Werte bauen kann

std::thread::native_handle() ⟼ liefert hier pthread_t

es wurde von Race-Problemen berichtet, und davon, daß der zuletzt gesetzte Identifier plötzlich auf allen Threads auftaucht

sollte dann den this-Typ extrahieren und den this-Ptr automatisch injizieren

es ergeben sich zwei Schwierigkeiten...

  • eine Varargs-Variante würde die bestehenden Overloads kanibalisieren bzw. würde für 1-2 Argumente einen "ambiguous overload" provozieren
  • mit einer Varargs-Variante gibt es keine Möglichkeit, den Delimiter anzugeben

Er ist aus mehreren Gründen gut

  • er ist bereits konventionell; im Besonderen in der C-Marko-Form (die Lumiera auch verwendet)
  • er teilt sich sprachlich gut mit

Aber es ist ein sehr fester Name; er ermöglicht kaum Verkürzungen oder Varianten

das macht nur den Code komplex, macht aber die Diagnostik nicht besser; std::invoke hat bereits gute Diagnostik (man muß sie nur lesen können)

der Name ist nicht besonders klar

Die hatte ich eingebaut, um für spezialisierte abgeleitete Klassen doch noch erweiterte Zustandsübergänge zu ermöglichen

...da die Implementierung mit einem aktiven Threadpool verbunden war, gab es dort auch eine Managment-Datenstruktur; die Threadpool-Logik hat eine eventuell im Thread noch erkannte Fehlerflag dorthin gespeichert — und join() konnte diese Fehlerflag dann einfach abholen.

...C kennt ja keine Exceptions — und der Author des Threadpool-Frameworks hielt Exceptions für ein bestenfalls überflüssiges Feature, das es ganz bestimmt nicht rechtfertigt, zusätzlichen Aufwand zu treiben

gemeint ist lib::Result

...und zwar im Besonderen, wenn man in einer »reaping-loop« alle Kind-Threads ernten möchte; dann würde nur ein Fehler ausgeworfen, und die weiteren Kinder bleiben ungeerntet (und terminieren jetzt, mit der C++14-Lösung sogar das Programm)

Das ist ein klassischer Fall für ein Feature, das zu Beginn so offensichtlich und irre nützlich aussieht — dann aber in der Realität nicht wirklich „fliegt“

heute bevorzugt man für ähnliche Anforderungen das Future / Promise - Konstrukt

Denn tatsächlich sind aufgetretene Fehler dann ehr schon eine Form von Zustand, den man mit einem speziellen Protokoll im Thread-Objekt erfassen und nach dem join() abfragen sollte; so kann man auch die Ergebnisse mehrerer Threads korrekt kombinieren

Knoten durchhauen!

Warum quäle ich mich damit herum, was ich künftig dürfen darf, und was unangemessen wäre? Meine Einstellung ist doch, daß eine Library strukturieren sollte, aber nicht vorgreifen und bestimmen. Wenn die letzten 10 Jahre eines gezeigt haben, dann den Umstand, daß der Thread-Wrapper vor allem für Tests verwendet wird. Und genau da gelten die ganzen Performance- und Architektur-Überlegungen überhaupt nicht; sondern es kommt auf die den Umständen entsprechende, gradlinige Formulierung im Testcode an. Und da könnte unter Umständen Fork-Join genau die KISS-Lösung sein. Insofern wäre das Design bereits nahezu gelungen: es trennt einen intendierten Standardfall ab, läßt aber sonst alle Türen offen....

und kann zugleich jedwede Exception halten

und ansonsten aber left-biassed sein

incl perfect forwarding

Es wäre zwar so irgendwie hinzubekommen — aber weit entfernt von einem guten Design.

Ich baue hier eine auf Dauer angelegte Lösung, die auch sichtbar ist und in Frage gestellt werden wird!

hab das gestern versucht — so kann das nicht stehen bleiben, das ist ja lächerlich (brauche explizite Template-Instantiierung) — zumindest solange wir keine C++-Module verwenden (C++20)

die Code-Anordnung hat keinen Flow

Ich habe doch selber oben diese Prinzipien formuliert: eine Library-Lösung sollte nicht mutwillig beschränken, sondern eine sinnvolle Gliederung anbieten...

also vielleicht doch Policy-based-Design?

⟹ wenn überhaupt, müßte die Policy einen mittleren Binding-Layer bilden

Uh-Oh ... jetzt bin ich richtig stolz auf mich...

...oder gut gegliedert; das jetzt gefundene Design trennt nämlich das Starten der Funktion, Fangen von Fehlern und Speichern von Ergebnissen als eigenen Belang ab (⟶ lib::Result); dadurch wird die Policy nun wirklich kurz und klar

 Thread  main<FUN,ARGS...>

Hier zeigt sich ein Widerspruch in den Konzepten selber an

  • ein »launch-only«-Thread sollte sich eigentlich komplett abkoppeln
  • aber andererseits soll das Thead-Objekt auch den zugehörigen State kapseln, muß also irgendwo existieren

dieses habe ich als RAII-Objekt angelegt, und der zugehörige Unique-Ptr markiert gleichzeitig den Lifecycle-State; das hat zur Konsequenz, daß der Session-Thread am Ende selber sein eigenes Objekt zerstören muß

...diese erzwingen, daß das Thread-Objekt während der gesamten Ausführung der Thread-Funktion erhalten bleibt; dies Design erscheint auch sinnvoll

der bisherige Lumiera-Threadwrapper hat zwar ein Thread-Handle gehalten, das aber letztlich beim Threadpool registriert war. Daher war das Thread-Objekt selber im Grunde verzichtbar (es sei denn, man hat davon abgeleitet und dort weitere Felder untergebracht)

der Thread selber könnte (müßte aber nicht)

am Ende den optional auf »empty« setzen

...das müßte dann so aufgebaut werden, wie es derzeit beim Session-Thread geschieht (nur daß dort aktuell ein unique_ptr und kein optional diese Möglichkeit schafft); das heißt, die Thread-Funktion muß irgendwie eine Rückreferenz auf den umschließenden Scope haben, sie muß auf ihrem eigenen Handle schließlich ein detach() aufrufen, bevor sie den Manager auf »leer« setzt, und sie sollte unmittelbar danach enden.

reguläre Threads sind auch nicht beeinflußbar, es sei denn, man programmiert das explizit... Und verklemmen können se sich immer

...und wenn ein Thread nicht joinable ist, bringt es oft nichts

Antwort: ja — es kann dafür gute Gründe geben

letzten Endes ist das hier keine general purpose Library — es wird ein interner Nutzer voraussgesetzt, die die betroffenen Belange im Prinzip versteht (und durch die Library lediglich rascher zu klarererm Code kommt)

man müßte das entweder direkt in den Konstruktor einbauen — und das wäre eine Verschlechterung, denn bisher sind die Konstruktoren mehr oder weniger orthogonal zu den Policies

es würde zwar genau diesen einen Fall lösen, aber keinen Zugang zur Lösung ähnlich gelagerter Probleme bieten — welche im Besonderen (siehe Session-Thread) dann mit dem threadID-String zu kämpfen haben, und spezielle Einschränkungen die Storage betreffend zu beachten haben; es wäre besser, wenn man das herausgeführte detach() wieder loswerden könnte!

und er ist semantisch genau für diesen Zweck gedacht

�� ...der Thread-Funktor (innen) managed seine eigene Hülle (Thread-Objekt) — ziemlich irre diese Lösung

das ist ziemlich einfach und natürlich bei der gebenen Code-Struktur — die „schreit“ ja gradezu danach, daß man seine Finger von thread::ThreadLifecycle lassen soll

das wäre der schwierige Part, denn normalerweise kann immer jemand anderen Code daneben stellen, der diese Bausteine geschickt anders verwendet — und aller Erfahrung nach könnte dieser „jemand“ ich selber sein...

und zwar, sofern oben in der konkreten Subklasse irgendwo ein mix-in verwendet wird — in dem Fall müßte nämlich der Pointer nachjustiert werden

ohne Subclassing ist die ganze Übung etwas überzogen

...es ist ja nett, daß der Thread-Wrapper sich selbst verwaltet — und der ganze Aufwand bloß für zwei interne Datenfelder?  wäre da nicht ein »workaround« angemessener?

wozu dann überhaupt noch das Front-End? Um einen etwas sonderbaren new-Aufruf einzupacken?

...und wir wären aus der Nummer raus: das ist nämlich oberhalb der protected inheritance

das ist die viel elegantere Lösung: Variante-1 durch Variante-2  implemenitert

denn: grade hier für den »Autonomous Thread« kommt man von außen nicht an die Addresse des Objektes ran

  • die Addresse ist vor der Allokation nicht bekannt, kann daher nicht in ein Lambda gebunden werden
  • eine statische Funktion würde zusätzlich noch einen Singleton-Mechanismus brauchen
  • ohne Zugang zum Objekt ist es aber auch sinnlos, zusätzliche Daten und Funktionen im abgeleiteten Objekt zu haben

Wenn wir es irgendwie schaffen könnten, das Objekt schon auf den Heap zu allozieren, aber zu verhindern, daß es „fliegt“ (bzw. wirklich konstruiert wird). Dann wüßten wir die Addresse. Danach würden wir ein placement-New in die Allokation machen, und könnten diesem tatsächlichen Konstruktor-Aufruf ganz verträumt den richtigen Instanz-Pointer mitgeben. Vorraussetzung wäre allerdings, daß sich diese manipulierte Allokation später ganz gewöhnlich per operator delete wieder entfernen ließe. Das wäre dann entweder ein fragiler Hack, der sich auf Plattform-Interna abstützt, oder es wäre ein custom-operator-new im Stil der 90er-Jahre

Dafür bräuchte ich eine Stelle, an der die tatsächliche Instanz bereits bekannt ist, aber die Invocation nochin generischer Form vorliegt. In einem solchen Kontext könnte man dann einen Marker-Typ erkennen und an die tatsächliche Instanz binden.

�� Und in der Tat: diesen Kontext gäbe es in der Hilfsfunktion buildLauncher()

  • zuverlässig: sie wird zur Compile-Zeit eingebunden und ist über das Typsystem abgesichert
  • sicher: sie baut auf einen speziellen Marker-Typ auf — wenn er fehlt, passiert nichts an der Stelle
  • effizient: da sie in einer Funktionstemplate-Instantiierung steht, wird sie nur im fraglichen Spezialfall getriggert

Es gibt keinen Grund, die Ersetzung nur an 2.Stelle im Tupel zu machen; auch ist es nicht notwendig, die Typen irgendwie festzunageln. Allein die Gegenwart des Marker-Typs genügt, denn diesen setzt man nur explizit ein. Damit wäre diese Funktion auch auf reine Argument-Tupel anwendbar, nicht nur (wie hier) auf Invocation-Tupel (functor, args...)

da nicht der Thread-Wrapper selber damit überladen wird

user bekommt einen Typ OptionalThread ≡ ein opaquer maßgeschneiderter Wrapper

das wäre nicht zwingend so; viel einfacher wäre die Implementierung, wenn man bloß Variante-2 weiter ausbaut; allerdings impliziert ein Thread-Objekt eben genau die Verwendung von lokaler Storage, und ein mit dem Thread-Ende gekoppelter Destruktor-Aufruf ist nützlich und einfach zu realisieren.

Das bedeutet, man bekommt in diesem Fall sogar eine Barriere geschenkt, die man dann nicht mehr mit SyncBarrier explizit coden muß. Einzige Vorraussetzung: prüfen und verzweigen auf den Lebenszyklus-Zustand

ganz elegant erst mal auf TheadLifecycle downcasten

was „rein zufällig“ hier in diesem nested scope in ThreadLifecycle erlaubt und auch sicher ist, denn hier haben wir noch Zugriff auf die protected geerbten Policy-Klassen...

Im Klartext: der Thread-Wrapper muß immer während der Ausführung der Thread-Funktion am Leben bleiben; die im Spezialfall notwendigen Ausnahmen sind in der Policy selber verborgen.

das heißt: keine Tricks mehr, indem man im laufenden Thread das Desaster nur umschifft. Der threadID-string liegt einfach im ThreadWrapper und basta. Und es gibt keine „besseren“ Statusvariablen mehr, die im Thread-Funktor leben müssen

...sonst wäre das ganze Unterfangen theoretischer Natur; nur dadurch wird es interessant, Storage zu verwalten und einen Zugang freizuschalten

⟹ Initialisierung von Policy-Bausteinen ist nicht möglich

(seufz)

streng genommen könnte die Thread-Funktion bereits mit der Initialisierung des lib::Result beginnen, während ihr dann die default-Initialisierung dazwischenfunkt

da erfahrungsgemäß der OS-Scheduler immer eine gewisse Latenz hat, bis ein Thread tatsächlich ausführbar wird

und trotzdem: das ist kein Gegenbeweis

man könnte das Thema entschärfen,

indem man das Thread-Handle zunächst leer initialisiert

und dann erst ganz oben im ctor-chain den Thread startet

und per move-assign auf das Handle schiebt.

dies hier ist eine Application-Support-Library; die Nutzer kommen intern aus dem Projekt — Verständnis der Belange wird vorausgesetzt; wer die Library ihren Möglichkeiten gemäß nutzt, ist sicher, wer Bausteine aus der Library mit eigenem Code verbindet, sollte wissen, was er tut

Hintertüren wie das detach() gibt es künftig nicht mehr — und die Destruktoren terminieren die Applikation, wenn der Thread noch lebt

es gibt die theoretische Möglichkeit für ein Memory-Leak

...und zwar müßte

  • die Einrichtung des Thread ohne Probleme möglich sein (sonst würde der ctor von std::thread scheitern)
  • das Kopieren des Funktors und der Argumente ebenfalls ohne Probleme erfolgt sein
  • aber danach ein Problem im bereits eingerichteten Thread auftreten, bevor unser in den try-catch-Block eintritt.

Hierzu kommt nur wenig in Frage: einmal die Invocation des Funktors selber mit als Wert vorliegenden Parametern, sowie dann unser eigener Library-Code bis zu der Stelle, an der der control-flow in den try-catch eintritt, sowie das Logging danach. Da sehe ich im Moment wenig Raum für Fehler. Funktoren sind ja entweder Pointer oder Objekte, und durch die erzwungene Kopie sind die tatsächlich dann aktiven Instanzen bereits konstruiert. Ein std::function verwendet zwar einen Invoker, aber das ist ein Trampolin, um die verschiedenen Aufruf-Technologien zu nivellieren; mir ist nicht bekannt, daß das für den Aufruf noch irgend etwas macht, was scheitern könnte. Bleiben also nur noch spezielle esoterische Argument-Typen. Und außerdem müßte der Optimiser so dämlich sein, ein bereits kopiertes Argument noch einmal zu kopieren nur für den Aufruf. Abgesehen davon könnten diese »esoterischen« Typen nur bei den weiteren Funktions-Argumenten auftreten (nicht der Funktor, nicht der this-Pointer). Danach haben wir nur noch nicht-virtuelle Aufrufe in die Policy (ist inline) und das Logging — dieses  stellt vermutlich die größte Gefahr dar; da hier aber kein schützender try-catch mehr darüber liegt, führen Fehler hier sofort zu std::terminate.

Diese Library ist für ein Konzept von »Thread« entworfen, das sicherlich nicht ermöglicht, solche Massen an Threads zu erzeugen, daß ein Leak sich praktisch bemerkbar machen würde....

...man kann es so einrichten, daß zuerst das detach() und dann der Destruktor-Aufruf wirklich als Letztes im Thread passieren, und danach nur noch der Funktor vom Framework de-alloziert wird. Falls hier was schief geht, terminiert die Applikation (und das ist gut so)

allerdings: abgeleitete Klassen leben gefährlich

Ein normaler Thread (und ein Joinable) würden den Aufruf des Destruktors verbieten, und damit die Applikation terminieren

aber der Thread ist schon „unterwegs“

...aber das gibt mein Framework nicht her

Da bin ich einem Kurzschluß aufgesessen: wenn die Konstruktion scheitert, und der Destruktor (so wie hier) sich einfach auskoppelt, dann wird nichts im Wrapper selber gemacht, sondern dieser existiert nicht mehr. Der Thread greift damit wild auf freien Speicher zu, der zudem in keinster Weise synchronisiert ist (d.h. selbst wenn wir das Handle vorher auf 0 setzen, wird der schon startende Thread das wahrscheinlich nicht mehr mitbekommen). Dieses Synchronisations-Problem ist es aber auch, das zu Fehlfunktionen in anderen Konstellationen führt: es kommt sporadisch vor, daß isLive() zu Beginn des Threads noch nicht wahr liefert; die Gründe ließen sich nicht aufklären. In einem solchen Fall würde der Thread spurlos terminieren — und dadurch an anderer Stelle möglichrweise ein Protokoll brechen oder einen Deadlock verursachen.

es muß ein Konstruktor-Aufruf sein

zwar könnte man nun generell an eine Factory denken, aber diese verlagert die gesamten Gefahren mit dem Instance-Management auf den usage-Kontext, und untergräbt damit den Zweck des Unterfangens...

er soll einfach und sprechend sein

der Einstieg muß zuverlässig im Typ verankert sein

also....

unique_ptr<ThreadHookable> thread;

thread.reset(

    new ThreadHookable{

            ThreadHookable::Launch(myFunctor, arg)

                           .atStart(initHook)

                           .atEnd(terminationHook)});

er bietet als internes API eine Funktion configure(this)

da bin ich schon x-mal darüber gestolpert: man kann per Reference oder per Value initialisieren; was nicht funktioniert ist eine move-Initialisierung, d.h. jeder move-only Argument-Typ macht Probleme. Einziger Ausweg war für mich bisher, die captures explizit zu benennen und per std::forward-assign zu initialisieren

wenn halt Argument-Packs einfach als Typ repräsentierbar wären — aber dem ist nicht so; man muß gegen ein getemplatetes Argument matchen, um aus einem Argument-Pack einen anderen Argument-Pack zu konstruieren

Es kann zwar ein rekursives Mutex verwenden, aber jede Ebene von Locking muß dann als eigenes unique_lock-Token repräsentiert werden.

diese setzt nur ein BasicLockable voraus

Mutex selber ist bereits BasicLockable

die C++ - Wrapper sind non-copyable

...gegebenfalls kann es dadurch passieren, daß man ein grade gesperrtes Mutex einfach „fahren läßt“ — mit unabsehbaren Folgen

Genauer gesagt, der Code versucht, die Konventions-basierte Herangehensweise aus POSIX / C in einen Token-orientierten Ansatz für C++ zu übersetzen. Das bedeutet aber, auch das Monitor-API ist noch Konventions-basiert

Ganz am Anfang hatte ich die Makros aus NoBug, deren Gebrauch im C++-Code schwierig ist. Daher dachte ich, ich packe die jeweils in einen Wrapper. Der Monitor ist dann nur ein Zustatz-Feature

...aber nachdem ich den Monitor entwickelt hatte, war mir klar, daß ich an den Basis-Elementen gar kein Interesse mehr habe, weil der Monitor ein Design-Pattern ist und damit ordnend auf den Code wirkt. Im Lauf der Jahre hat sich dann gezeigt, daß ich überhaupt nichts anderes brauche, als nur das Lock-Guard front-End (+Atomics für alle nicht-trivialen Fälle).

Verbesserung, weil sie die Implementierung einfach und klar macht, und mehr dem C++-Stil entspricht. Aber sie ist auch ein stets vorhandener zusätzlicher Storage-Ballast, der eigentlich nicht notwendig wäre

zwar traue ich mir zu, die mittlere Stufe der »strukturellen Verbesserung« direkt zu implementieren, aber in dieser Hinsicht überschätze ich häufig die reale Komplexität und die Projekt-Risiken. Schon allein um aufwendige Regressionen zu vermeiden sollte stets auf gute Test-Coverage geachtet werden, was nurch durch schritweises Vorgehen möglich ist

...zumindest war das wohl die Motivation, wenn ich die Kommentare im Test hinzunehme.

rein konzeptionell sollte ein synchronisierbares Objekt nicht kopierbar sein, schon allein weil sich dadurch der Status der gemeinsamen Nutzbarkeit verwischt

daher erscheint es sinnvoll

  • diese seltenen Fälle als separate Methode herauszuführen
  • die Signatur komplett an C++ anzupassen

Hier gibt es zwar eine Lücke, nämlich die const& auf einen rvalue — aber das hier ist keine general-purpose-Library!

also entweder ganz oben oder ganz unten, und ansonsten nur noch einen Template-Parameter durchgeben

man muß nicht speziell auf den Typ des Trägers abstellen; vielmehr kann man dann einen static-cast nachschalten, und der scheitert dann halt gegebenfalls beim Bauen. Das habe ich grade eben genauso beim neuen Thread-Wrapper gemacht, und es hat sich bisher gut bewährt. Spezielle static-asserts sind dann gar nicht notwendig

man hätte dann also zwei verschiedene APIs für essentiell das Gleiche

λ-Notation ist inzwischen allgemein bekannt, und hat den Vorteil, daß das Binding nahe am verwendeten Code liegt. Das gilt ganz besonders auch für bool-Flags, und zudem müssen wir uns dann nicht mehr mit Konvertierungen, volatile und Atomics herumschlagen

Fazit: Nein — denn λ sind bereits der bessere „Support“

Im Besonderen #994 kann nun wirklich langsam zugemacht werden! Das schiebe ich nun schon so lange ¾ fertig vor mir her ... und es ist klar daß ich den letzten Schritt (TYPES<TY....>) noch länger vor mir her schiebe, wiewohl das eigentliche Problem effetiv bereits seit 2017 gelöst ist

hier hatte ich einen »convenience-shortcut« — und der ist broken

hab dummerweise den this-Ptr nicht mehr an dem Punkt; und das will ich auch nicht ändern — sonst hab ich in jeder dieser delegierenden Funktionen das getMonitor(this), und es ist auch inhaltlich nicht besonders präzise, schließlich arbeitet der Guard auf dem Monitor (siehe ClassLock)

...und zwar wegen der Typedef "Monitor", die nur im Scope von Sync<CONF> definiert ist — das geht gut, SOLANGE niemand das Layout der Klasse Sync ändert, niemand „aus Versehen“ eine VTable einführt, und solange niemand den Typ Monitor abgreift, woanders instantiiert und dann diese Referenz über eine von Lock abgeleitete Klasse in die Referenz einspielt. TJA  ☹ diesen „Jemand“ gibt es bereits: SyncClasslock ��

und zwar, weil eine solche anonyme Instanz den umschließenden Scope nicht schützt; sie sieht aber syntaktisch genauso aus wie ein wirksamer Scope-Guard

...vielmehr wird das Problem tatsächlich gefixt, durch einen try-catch-Block; die spezielle Konstruktor-Variante bleibt damit erhalten, aber wir bieten generell keinen Support mehr für Member-Funktionen, da dies nur für den Konstruktor möglich wäre, aber nich für die frei stehende wait()-Variante. Ohnehin werden nun Lambdas bevorzugt, weil sie meist am Ort der Verwendung definiert werden und damit besser selbsterklärend sind...

...rein konzeptionell ist es nämlich nicht notwendig, das Lock zu erlangen; aber unser bisheriges API hat dazu gezwungen. Nicht schlimm, aber auch nicht schön

muß dazu getMonitor() public machen

  • denke etwa seit einem halben Jahr darüber nach.
  • bisher hatte ich mich nicht getraut...
  • im September mit Benny besprochen (per Mail). Seine Reaktion klang sogar begeistert; das hat mich ermutigt
  • natürlich ist es eine Menge Arbeit, aber jetzt, wo ich allein bin, kann ich sowas einfach durchziehen, ohne daß es zerredet wird

bisher "GUI"

bisher "Proc-Layer"

bisher "Backend"

wenn im Einzelfall bereits mit der INS-Nachricht bestimmte global sichtbare Properties mit gegeben sein müssen

  • dann bedeutet das, auch das betreffende Objekt erzwingt diese als Invariante per ctor
  • und das Struktur-Modell verlangt per Konvention an dieser Stelle das Senden eines entsprechenden Rumpf-Record

...hatte ich damals sehr schnell geschrieben, um zu zeigen daß eine C++ - Lösung auch »einfach« sein kann. Chistian wollte damals unbedingt die Application-main in C implementieren, „damit alles wirklich einfach und verständlich bleibt“. Ich hatte das Gefühl, da stand eine Agenda im Raum, daß alles Wichtige in C sein sollte. Ich vertrat (und vertrete bis heute) den Standpunkt, daß die Erweiterungen in C++ aus gutem Grunde geschaffen wurden, weil C in wesentlichen Aspekten mutwillig zu einfach gehalten ist. Für den Traum von der schönen einfachen Lösung zahlt man dann jeden Tag Zinsen für technische Schulden.

sie erfüllt alle Anforderungen, ist aber zu sehr vereinfacht

Vor allem...

  • ein Subsystem ist nicht einfach gestartet oder nicht-gestartet. Vielmehr ist der Start zunächst initiiert, und nach Erfüllung lokaler Kriterien ist der Start vollständig.
  • entsprechend ist ein Subsystem nicht einfach gestoppt. Vielmehr ist der Shutdown-angekündigt, dann die aktive-Phase-verlassen und schließlich der shutdown-abgeschlossen.

In lib::Sync (genauer: in der Implementierung Monitor-wait) war eine Unterstützung für Timeout nach POSIX eingebaut worden. Dies erfordert, daß die Timeout-Spec in der Storage des Client bereitgehalten wird (weil man POSIX-Funktionen nur einen Pointer übergibt). Das habe ich hierfür ausgenützt, indem nachträglich, im Fall einer Emergency, noch ein Timeout definiert wird. Da Condition-Variablen zur Prüfung der Bedingung immer wieder aufgeweckt werden, kann man so nachträglich eine Art vorzeitigen Abbruch realisieren...

Es fällt auf, daß ich in dem Entwurf von 2011 die Allocation selber nur als Referenz rausgegeben habe, und dann für jeden darauf eröffneten Slot ein smart-Handle. Zunächst einmal sieht das vernünftig  aus, weil die Slots typischerweise sofort noch im gleichen Thread belegt werden. Was ist aber wenn...↯ — kann sich dann die belegte Allocation verklemmen?

GUI und Session schicken Nachrichten.

Während der Builder läuft, kann das GUI schon weitere Nachrichten geschickt haben,

die dann noch in der ProcDispatcher-Queue hängen. Daher kann sich die Antwort

als Resultat auf einen Builder-Lauf noch auf einen vorherigen Zustand beziehen.

Es kann aber auch ein Builder-Lauf die kummulierten Ergebnisse von mehreren Commands behandeln.

In jedem Fall muß der DiffConstituent genau wissen, was der zuletzt geschickte Stand war,

damit er einen Diff erzeugt, der garantiert auf der anderen Seite anwendbar ist.

Denn letzteres ist bei uns eine Grundannahme. Es gibt keine ungefähren Diffs!

Frühjahr 2018 komplett überarbeitet.

Lösung scheint nun "rund" zu sein

war im Einsatz seit Beginn der Lumiera-Projektes.

Wurde aufgegeben da

  • die Policies komplexe Lösungsvarianten implementierten, die nie gebraucht wurden
  • die Implementierung einen tückischen Fehler in CLang aufgedeckt hat

Im Einsatz seit der Behebung des CLang-Problems bis heute (3/2018).

Wird nun aufgegeben, da sich auf dieser Basis keine DI implementieren läßt,

welche auf einem Service mit explizitem Lebenszyklus beruht.

Außerdem stellte sich diese Lösung als ziemlich fragil heraus

und benötigt diverse Laufzeit-Konsistenzchecks, die den Implementierungscode schwer lesbar machen

Bei diesem Wunsch-Profil bleibt nur eine Variante von Lösung-2

...denn nur eine dynamische Laufzeit-Factory ermöglicht, jederzeit  den Konstruktionsmodus zu wechseln

...denn nur ein Instanz-Pointer kann umgebogen oder auf NULL zurückgesetzt werden.

Der einzige Ausweg aus diesem Dilemma wäre eine statische Lösung,

in der bereits durch den #include von lib/depend.hpp endgültig klar wäre,

was für eine Art von Dependency-Factory zum Einsatz kommen soll. Denn nur auf diesem

Weg könnte der Optimiser unmittelbar auf eine Singleton-Instanz im statischen Speicher

zugreifen, nach einem Check auf ein atomic<bool>.

Eine solche statische Lösung allerdings widerspricht nicht nur meinen Wünschen,

sondern wäre auch architektonisch ungünstig, denn dadurch

  • entsteht eine zentrale DI-Konfiguration
  • erfolgt eine Rückverkopplung von lib/depend.hpp auf die Applikations-Struktur

...denn er muß komplett generisch sein, und lib/depend.hpp darf keinerlei Kenntnis

über die konkrete DependencyFactory voraussetzen. Denn sonst würden wir die

Freiheit der dynamischen Laufzeit-Konfiguration verlieren.

...denn ein Lambda kann in dieser Lage grundsätzlich keinen optimierungs-Vorteil bringen,

und die konventionelle Lösung hat demgegenüber den Vorteil, daß sie Struktur und Kontrakt explizit macht.

...denn es läuft darauf hinaus, daß die Nutzung eines Dienstes zwar on demand erfolgt,

jedoch stets erst nach seiner Bereitstellung. So etwas läßt sich niemals über einzelne

technische Einrichtungen lösen, denn es ist eine Frage des Aufbaues der gesamten Applikation.

Es gibt hierfür nur zwei Lösungswege

  • dynamisch: man definiert alle Abhängigkeiten durch Regeln und zieht das System nach Bedarf konsistent hoch, was bedingt, daß alle Abhängigkeiten über diesen Mechanismus laufen müssen
  • statisch: man strukturiert das System so, daß Nutzer erst nach dem Hochfahren der von ihnen benötigten Subsysteme aktiv sein können

scheidet für unser Nutzungsmuster aus

...denn es bedutet effektiv, daß viele Instanz-Zeiger "herumfliegen",

welche man in einer zentralen Registry erfassen müßte, um sie bei Bedarf

wieder auf NULL zurücksetzen zu können.

...d.h. Service-Zugang wird automatisch geschlossen,

wenn die DependInject-Instanz stirbt

sie werden ja sofort ausgewertet, da die Service-Instanz unmittelbar gebaut wird

aber Lebenszyklus ist an die Factory gebunden

Folgendes Szenario ignorieren wir:

  • eine Dependency ist als Service konfiguriert
  • für einen Testmock wird eine Local-Konfiguration darübergelegt
  • der Service wechselt seinen Lebenszyklus-Status (aktiv/inaktiv)

⟹ der Service zerschießt den Local, oder der Local restauriert am Ende den bereits toten Service.

Har Har Har! Selber schuld wer sowas macht.

statische Storage ist irgendwie cool

das gilt im Besonderen für eine default-Storage als Singleton.

Wofern wir dynamisch konfigurieren (wollen), muß dieser Default stets statisch bereitgestellt werden,

selbst wenn die dynamische Konfiguration so angelegt ist, daß die Storage nie benötigt wird

...aber dann eben nicht mehr elegant.

Und das hängt nur von den Umständen ab.

In einem einfachen statisch gelinkten Executable entfernt gcc die gesamte Variable sogar ohne Optimierung.

In Zukunft könnten Compiler/Linker noch "schlauer" werden...

daher muß sein dtor vor dem dtor von DependencyFactory laufen

denn während dem dtor existiert das Lock nicht mehr.

Man kann aber die Logik so umordnen, daß der instance-Ptr nach dem eigentlichen Deleter auf NULL gesetzt wird.

Das ist nicht threadsafe, was aber hier akzeptabel ist (Shutdown läuft überwiegend

single-threaded, sofern man keine thread_locals einsetzt. Aber diese werden vor alle anderen

Destruktoren gesequenced. Sollte passen.

denn nun wird das "singleton" schon ziemlich gehaltlos,

und es ist einigermaßen undurchsichtig, wo nun die Instanz erzeugt wird.

Allerdings gibt es auch kein stichhaltiges Argument, dieses Feature nicht zu implementieren.

Es ist halt einfach nahheliegend, daß man mal eine Subklasse mit abweichenden Parametern

konstruieren wollen könnte, und es ist von der Implementierung her "quasi geschenkt".

Thema: Memory access order constraints

Grundidee: synchronizes-with-Beziehung herstellen auf Guard-Variable

...das meint zweierlei

  • wir brauchen keine volle sequentielle Konsistenz
  • eigentlich würde consume statt acquire genügen,
    aber wir verzichten auf diesen ehr theoretischen Performance-Gewinn,
    welcher nur relevant wäre, wenn wir auf ARM einen modernen Compiler einsetzen

essentiell ist, im Mutex-geschützten Bereich

auf einer temporären lokalen Instanz-Variable zu arbeiten

warum?

weil per Definitionem dieses gesamte Konfigurations-Thema

als nicht performance-kritisch eingestuft wird -- und ich mehr Wert darauf lege,

die verschiedenen Belange im Quelltext nicht zu vermischen

es gilt schlichtweg als Architektur-Fehler, wenn hier eine Kollision geschiet.

Und es gibt keinen sinnvollen Weg, wie die Applikation dann weiterarbeiten kann.

Daher werfen wir ja auch error::Fatal

...denn sonst könnte genau das gleiche Desaster passieren,

das auch in fehlerhaftem Double-Checked-Locking auftritt

...d.h. das ganze Locking und die memory-order schützt uns hier überhaupt nicht!

Es kann sehr wohl passieren, daß ein anderer Thread grade eben noch

sicht den Pointer auf den Service geholt hat, und wir dann den Service zerstören, während

der andere Thread ihn grade nutzt.

unsere Architektur stellt aber sicher,

daß dieser Fall nicht relevant ist

warum?

Weil der "andere Thread" nur von zwei Subsystemen her kommen kann

  • dem Subsystem selber, das auch den Service erzeugt.
    Beispiel ist eine UI-Interaktion aus dem Event-Loop thread
  • aus einem anderen Subsystem, das vom Serivce-Provider abhängt

In beiden Fällen stellen unsere Prinzipien zum Betreiben von Subsystemen sicher,

daß dieser "andere Thread" nicht (mehr) aktiv sein darf, wenn der Shutdown erfolgt.

hier ist ein Segfault möglich

...denn wir sind auf x86_64 -- und diese Plattform ist per default fast überall sequentially coherent

...man könnte ihn aber genausogut auch machen.

Das Argument ist: wenn wir kaskadierend aufrufen, dann ist das Ergebnis in jedem Fall korrekt,

und wird auch durch das Installieren dieses (zweiten) Zuganges in keinster Weise beeinträchtigt.

Sollte Depend<SUB> bereits instantiiert sein, dann auch gut.

Der kaskadierende Aufruf liest dann einfach dessen Instanz-Pointer

hier notwendig, weil wir eine neue Factory-Funktion ablegen.

Das könnte mit dem Factory-Management einer bereits installierten Konfiguration kollidieren

zwar wird beim Löschen des Mock

die ursprüngliche Factory wieder an ihren Platz zurückgeschoben,

aber niemand sagt, daß ein Move auch wirklich ein Move (swap) ist.

Muß daher diese leer gewordene temporäre Factory explizit auf Default-Zustand zurücksetzen,

damit nicht doch noch der Deleter läuft.

...und man stattdessen explizit eine gefährliche Funktion  aufrufen muß

...da es sich ja nur um die Factory handelt,

nicht um das AppState-Singleton selber, welches ja ohnehin nur von main.cpp

verwendet werden sollte. Aber das deutet darauf hin, daß irgend etwas mit der

Initialisierung von Statics "faul" ist, wenn shared objects dynamisch geladen werden.

Konsequenz: das ist keine Library-Implementierung

die von der alten DependencyFactory abhängen

...wenn man nämlicht Lumiera's Lösung nicht genau kennt,

könnte sich das so lesen, als wäre Depend<X> ein Mixin,

welches einer Klasse magisch eine Dependency als protected-Feld zugänglich macht.

Und dann wäre es ziemlich pervasiv, sowas zum Freund zu erklären.

DependencyFactory ist viel besser geeignet

den habe ich nicht mehr über das Interface-System gemappt

weil mir das ganze C-gefrickel zu blöd geworden ist.

Also hat das hier Prototyp-Charakter!

der Interface-Anbieter implementiert einen konkreten Proxy

Modus der Definition

und Instantiierung

...wir müssen immer, für jeden Proxy

explizit eine Template-Instaniierung triggern, und zwar für

brauchen eigenen Zugriffs-Mechanismus auf ein weiteres Handle-Objekt

...oder sogar den ctor, das ist egal --

denn das Problem ist, ctor (oder activate) werden aus demjenigen Kontext heraus aufgerufen,

der die Service-Implementierung startet und damit die Erzeugung des Proxy triggert.

Dieser Kontext darf den konkreten Typ des Proxy aber genau nicht kennen (wegen Entkopplung)…

Genau aus diesem Grunde leiten wir ja ctor/dtor der ServiceInstance(Handle)-Klasse in eine ander TU um.

Problem ist nun, diese umgeleiteten Funktionsdefinitionen können nur einen Typ (Template-Parameter) bekommen, und dieser Typ wiederum muß -- zumindest als abstrakter Platzhalter -- in allen TU sichtbar sein.

obwohl es das sollte.

Aber anscheinend macht der Compiler das nur partiell,

denn es wird ja nur eine const& an den ctor von Binding übergeben.

Allerdings erscheint es mir nicht sinnvoll, hier mit Tricksereien zu arbeiten!

sie ist zwar nicht schön -- aber was ist an einem Interface-Binding schon "schön"?

Sie ist hinreichend wartbar, sofern man sie per copy-n-pate vervielfältigt.

Es wäre sogar denkbar, in diesen Rump eine generierte Proxy-Klasse zu kleben,

da nur wenige Variable erstetzt werden müßten.

das ist ein konzeptionelles Problem.

Eigentlich möchte man durch ein Interface Entkoppelung erreichen.

Nun ist es so, daß

  • das Binding BusinessInterface -> InterfaceSystem
  • InterfaceSystem -> Service-Implementierung

an der gleichen Stelle erfolgen

...und lib::Depend so umarrangiert,

daß re-entrant-Aufrufe während dem dtor erlaubt sind

so wie das Advice-System geschrieben ist,

kann und muß man das "durchwinken"

denn grundsätzlich ist das ganze Advice-System bewußt "billig" implementiert;

Verbesserungen später willkommen....

Der Destructor räumt alle AdviceProvisions weg.

Diese werden in einen statischen Kontext geschoben, damit sie unabhängig vom Advisor weiterleben.

Daher müssen wir aus diesem statischen Kontext heraus wieder zurück in's AdviceSystem kommen.

Alternative wäre, eine komplett spezielle De-Allokations-Routine zu schreiben,

welche die Datenstrukturen direkt traversiert und freigibt, und dann die Provisions

abfischt und ebenfalls alles wegwirft. Machbar, erscheint aber im Mißverhältnis

zum Level der gesamten übrigen Implementierung (welche nämlich um einiges

elaborierter sein könnte, incl. Verwendung von Atomics und einem besseren

Memory-Management.

Aber wie gesagt, das ganze Advice-System ist eine Skizze

...weil Nobug-Init ON_BASIC_INIT braucht,

und lib::Depend wiederum von Nobug-Init abhängig ist.

Also würde DependencyFactory<LifecycleRegistry> aufgerufen,

bevor es statisch initialisiert sein kann...

und heute würde ich den Code so nicht mehr schreiben

genauer:

er ist nicht kaputt, sondern hat sehr gut funktioniert und diesen Unfug festgestellt.

Nämlich daß unser lib::Depend ein ClassLock braucht, um einen Fehler zu melden.

Das ist, für sich betrachtet, eigentlich unvermeidlich, aber verlangt dann auch

nach einer grundlegenden Lösung. D.h. einem echten Schwartz-Counter.

Nicht einem, der in einem Meyer's Singleton steckt...

...es ruft sich selbst rekursiv auf, via Depend<AdviceSystem>

...nämlich eine ganz spezielle, dedzierte Aufräum-Routine schreiben

das «Regel-System»

das «Advice-System»

auch in 2019...

  • nur einige sporadische Use-Cases
  • ich halte es aber weiterhin für wichtig

ein Whiteboard-System

....denn es gibt die default-Lösung

im Sinne des Erfinders...

bloß jetzt etwas abstrakter

...und das kann ziemlich indirekt passieren.

Beispiel ist das ClassLock. Das ist ein Front-End, und verwendet verdeckt wieder einen Static.

Und genau dafür gibt es anscheinend keine Garantieren

C++ hällt die Erzeugungs/Zerstörungs-Reihenfolge exakt ein

d.h. wenn das local static später erzeugt wird, wird es vor  dem Hauptobjekt zerstört

Statische Initialisierung funktioniert präzise, korrekt und zuverlässig

Der Aufruf von Konstrukturen statischer Objekte konstituiert eine (dynamische) Reihenfolge.

Desktuktoren werden exakt rückwärts in dieser Reihenfolge aufgerufen.

Statische Objektfelder werden vor der ersten Verwendung der Klassendefinition  initialisiert

Dagegen Funktions-lokale statische Variablen werden initialisiert, wenn der Kontrollfluß sie zum ersten mal berührt.

Wenn ein Konstruktor ein statisches Feld verwendet, dann wird dieses Feld vor dem Konstruktor erzeugt.

Beachte: in jedem dieser Fälle wird auch die o.g. Reihenfolge konstituiert.

Corollar: wenn man ein Meyer's Singleton erst indirekt aus dem Implementierungs-Code verwendet,

so wird es garantiert zerstört, bevor der Destruktor des aufrufenden Objekts läuft.

Hallo ClassLock...

Aufgabe: produce dummy content

...obwohl die betreffenden Assets in die Kategorie "Medien" fallen,

und wir einen eigenen StreamType definieren wollen, also explizit die Art der Medien offen lassen.

Ganz prominent fehlt hier also z.B: MIDI

...nämlich genau nicht für alle Assets,

die Aufgrund von Klassifikationen automatisch bereits existieren

Meta-Assets sind per Definition "immutable"
Es gibt einen Builder

das ErrorLog

....weil hier ein allgemeines Schema entsteht:

jede Aktion, die in das UI "reflektiert" wird, erfolgt, indem man eine Nachricht

über den UI-Bus schickt, an einen Empfänger mit bekannter ID.

dort gibt es eine ErrorLogView

und den Controller: NotificationHub

das ist diese Idee, daß eine Struktur sich selbst meta-repräsentiert;

dadurch werden Meta-Operationen auf gleiche Ebene gestellt wie normale Struktur-Manipulationen

...und dadruch würden History-Operationen wie

  • undo
  • redo
  • repeat command

...zu ganz normalen Manipulationen der Session, würden ihrerseits geloggt und historisiert

und verlieren ihren magischen Charakter außerhalb der Event-Sourcing-Struktur

1/2023: unklar — wer macht das?

...necessary when closing the session;

we need to wait for the current command or builder run to be completed

...noch nicht implementiert 1/17

Guard beim Zugang über das Interface

nur sie ist atomar

nur ein Thread für Commands und Builder

Ticket #1054

...und "self" == LumieraThrea* == "handle" (im Wrapper).

D.h. solange der Wrapper lebt (!), kann er selber leicht feststellen, ob die aktuelle Ausführung

auch in einem Thread stattfindet, der

  • von unserem Threadpool gestartet wurde
  • ein Thread-Handle hat, das mit dem Handle dieses Wrappers identisch ist.

Das Schöne bei diesem Ansatz ist, daß man dafür weder das Handle exponieren muß,

noch irgendwelche komischen Policies aufmachen. Solange es das Objekt gibt, klappt das.

OO rocks!

Kontrollfluß ist nicht in einer Arbeitsfunktion

...und dann nur noch

  • auf Shutdown reagieren
  • mitbekommen wenn die Sperre aufgehoben wird

billig: unsere Zeit-Lib nutzen

Logik im Looper auf Basis

generischer Überlegungen implementiert

Zeitmaß für diesen Wiedergabevorgang

Und zwar 1:1

  • hierbei bedeutet »Datenstrom« eine Folge von Daten-Frames, die in timed delivery übergeben werden
  • jeder Datenstrom entspricht genau einer Instanz eines Protokolls zur Puffer-Übergabe
  • jeder Datenstrom kann für sich gesondert unterbrochen sein, sich verzögern und hat eine einzige, wohldefinierte Stromgeschwindigkeit

Aber nur, insofern diese logischen Kanäle in den einzigen Datenstrom einfließen, und stets gebündelt auftreten; sie müssen hierfür durch einen Multiplex-Schritt in einen Datenstrom zusammengeführt werden. Die rein logische Ordnung der Kanäle zu einem Medium ist bedeutungslos; was zählt ist was konkret in einem Strom hergestellt und geliefert werden muß

Sofern eine CalcStream-Instanz besteht, bedeutet das, daß Berechnungen laufen und Daten im Datenstrom anfallen. Ein Datenstrom ist insofern nicht weiter zerlegbar und auch nicht weiter parametrisierbar

  • die Aufschlüsselung nach Zeiten enthält überall Puffer, die insgesamt zu einer Zeitverschwendung führen
  • normalerweise wird das Verletzen einer Deadline als Fehler interpretiert

gedacht als ein strukturelles Verzeichnis oder eine Facade, die gleichermaßen für die Planung und die Ausführung nutzbar ist; dadurch sind beide Belange von der Struktur des low-level-Model entkoppelt

...und das liegt genau daran, daß die Monaten auf eine rein-abstrakte strukturelle Eigenschaft hin abstrahieren, welche nicht in der Natur der verhandelten Dinge verwurzelt ist

  void

  CalcPlanContinuation::performJobPlanningChunk(FrameCnt nextStartFrame)

  {

    TimeAnchor refPoint(timings_, nextStartFrame);

    JobPlanningSequence jobs = dispatcher_.onCalcStream(modelPort_, channel_)

                                          .establishNextJobs(refPoint);

   

    Job nextChunkOfPlanning = buildFollowUpJobFrom (refPoint);

   

    UNIMPLEMENTED ("the actual meat: access the scheduler and fed those jobs");

  }

Hier zeigt sich das ganze Elend mit den Monaden: man muß dazu einen Leitfaden schreiben, anstatt daß sie sich selber erklären würden....

expandPrerequisites ist die Planungs-Operation und wird monadisch gebunden

ist eine State-Core ( IterStateWrapper)

SIG_expandPrerequisites

  ⟹ per flatMap wird daraus ein PlanningState ≔ IterStateWrapper<JobPlanning>

Strategy: lib::iter_explorer::RecursiveSelfIntegration

⟹ die Auswertung und Organisation findet in JobTicket::ExecutionState  statt

Monaden allein leisten nicht was hier benötigt wird

wir brauchen nämlich rekursive Entfaltung

Monad-bind transformiert und expandiert — jeweils nur einmal

man kann die Tiefensuche extra noch hinzufügen — als Komplikation

dagegen das TreeExplorer::expand() ist keine monadische Operation

....und zwar in mehrerlei Hinsicht....

  • es wird in der Grundform nur explizit ausgelöst
  • expandierte und nicht expandierte Werte können sich mischen
  • Quell- und Ausgabe-Werte der Expander-Funktion müssen kompatibel sein
  • expandAll() bietet Auswertung-bis-zur-Erschöpfung, exponiert aber auch alle Zwischenwerte

    template<class SRC>

    struct _DecoratorTraits<SRC,   enable_if<is_StateCore<SRC>>>

      {

        using SrcVal  = typename CoreYield<SRC>::value_type;

        using SrcIter = iter_explorer::IterableDecorator<SrcVal, SRC>;

      };

...und die Auswirkungen können kaum sinnvoll vorhergesagt oder bewertet werden

  • da wir die Datenstruktur nicht beliebig austauschen können
  • da sie mit dem Allokations-Schema verwoben ist
  • da die erziehlbare Cache-Locality von den Anforderungen der konkreten Render-Pipeline abhängt

initial gibt es eine Allokation im AllocationCluster — ab diesem Teil soll die Planung weitgehend ohne „bewegliche Teile“ ablaufen

Einige Aspekte wachsen noch graduell, möglicherweise jedoch bis zum Ende vom Playback

  • besagter Stack für die child-Prerequisites braucht Speicher gemäß maximaler Rekursionstiefe (normalerweise wenig)
  • in jedem neu berührtten Segment muß einmal das JobTicket aufgebaut werden (wird dann aber von weiteren Play-Vorgängen wiederverwendet)

�� abgesehen davon ist der Speicher stabil

wie erkennt man dies von „oben“ ?

...denn es heißt, daß die top-level-Jobs bis <= der nächsten Deadline komplett entfaltet wurden

Es sieht für mich dennoch nach dem erstrebenswerten Ansatz aus, da der Scheduler ja dynamisch nachjustieren soll, und daher ohnehin so eine Aktuaisierung der Zeitfenster machen muß; zudem würde diese Berechnung und Steuerung nur intern im Scheduler stattfinden, und auch der Dispatch-Schritt wäre noch im Wesentlichen deklarativ — er würde nur eine heuristische Grob-Bestimmung der Zeitfenster vornehmen

...denn im JobTicket sind sie zwar explizit vorhanden, dieses ist aber nicht konkret instantiiert und gilt für ein ganze Segment; mithin fehlt die Verbindung von einer Dependency zu dem konkreten Job, welcher diese Dependency realisiert. Im Gegensatz dazu ist während dem Dispatch-Schritt diese Information implizit vorhanden, denn für den Dispatch wird genau das JobTicket interpretiert, und die konkreten Jobs werden von diesem abgeleitet

Entweder, man verzichtet auf ein dynamisches Nachjustieren und läßt die einmal eingefädelten Berechnungen einfach laufen — oder man muß für jede einzelne Berechnungkette die gesamte Dependency-Struktur duplizieren und im Speicher halten.

und man kann Nachfolger benachrichtigen, statt Vorgänger zu suchen

...da man einen Lumiera-Forward-Iterator aktiv weiterschalten muß, aber stets auf das aktuelle Element zugreifen kann...

...solange ein Planungs-»Chunk« hinreichend weit vor dem Ende seiner abgedeckten Zeitspanne aufgebaut wird; das Planungsintervall könnte sogar dynamisch reguliert werden, wichtig ist nur, einen Mindest-Vorlauf vor der tatsächlich abgedeckten Zeitspanne einzuhalten, so daß idealerweise der nächste Planungs-»Chunk« bereits stattfindet, noch bevor die Deadlines für den Vorgänger ganz abgelaufen sind. Denn unter diesen Umständen müßte nicht einmal ein vollständiger Dependency-Tree in einem Chunk abgearbeitet werden, solange nur die geplanten Jobs rechtzeitig in der Scheduler-Queue sind. Tatsächlich aber tritt in unserem Iterator-Mechanismus der top-level Planungsschritt als erstes auf, und damit springt jeweils die Deadline um eine ganze Frame-Dauer in die Zukunft — dies wäre dann in jedem Fall ein ganz klares Signal, um das Überschreiten des zu planenden Zeitspanne zu erkennen

...auch das erscheint mir heute als eine Folge der problematischen Monaden-Struktur. In dem neueren Pipeline-Design liefert die Expand-Funktion eben ganz bewußt zunächst die Parent-Node, und dann erst die Kinder, bzw. man kann die Navigation sogar steuern (da expandChildren() explizit aufgerufen wird). Damit ist ein solcher Struktur-Marker gar nicht mehr notwendig. Diese These wird dadruch bestätitgt, daß ich seither diverse rekursive Auswertungen implementiert habe, und nie mehr auf den HierarchyOrientationIndicator zurückgekommen bin

Scheduler-Interface ist ein Gefahrenübergang

verwendete noch den pre-C++11-Stil mit explizit ausgewalzten Template-Spezialisierungen für 1...5 Argumente

geändert mit


commit 856d8a3b519e4fc99e7b6749fbed67df33128741
Author: Ichthyostega <prg@ichthyostega.de>
Date:   Thu Apr 20 18:53:17 2023 +0200

    Library: allow to reverse intrusive single linked list

...denn dazu benötigen wir detailierte Timing-Beobachtungen, welche erst im speziellen Job-Funktor vorliegen können, nicht auf der Ebene eines ganzen Planning-Chunk

dann reduziert sich der TimeAnchor komplett auf einen Proxy zu den Timings (die obendrein noch als Kopie mitgeschleppt werden)

eigentlich werden nur zwei Felder genutzt, und davon der ModelPort-IDX nur an einer einzigen Stelle (beim Zugriff auf das JobTicket). Die absolute Frame-Nr geht bisher nirgends ein — sie ist redundant, wenn man die Timings kennt, und jenseits der Job-Erstellung komplett irrelevant (soll auch irrelevant sein)

für die Deadline-Berechnung...

...damit könnten die FrameCoord ihre Bestimmung gefunden haben, denn in diesem Bezug sind auch FrameNr und nominalTime nicht redundant (Quantisierung!), sondern machen die Grid-Anwendung explizit

Fazit: haben keine Rolle mehr und werden aufgegeben

  • Prerequisites und Channel-Tabelle als (single)linked-List RLY?
  • SubTicketStack wirklich schon gut durchdacht? Funktionsweise?

die werden nämlich nur einfach traversiert, und dahinter verbergen sich verschachtelte JobTicket, die allesamt im gleichen Speicherblock (im AllocationCluster) liegen

Impl: JobBuilder::relativeFrameLocation (TimeAnchor&, FrameCnt)

      /** core dispatcher operation: based on the coordinates of a reference point,

       *  establish binding frame number, nominal time and real (wall clock) deadline.

       * @return new FrameCoord record (copy), with the nominal time, frame number

       *         and deadline adjusted in accordance to the given frame offset.

       */

      virtual FrameCoord locateRelative (FrameCoord const&, FrameCnt frameOffset)  =0;

...erschlossen aus den bestehenden Strukturen + der neuen Intention

  • repräsentieren eine parameter-basierte Instanz-Identität
  • sind jeweils voll kopierbare Wertobjekte
  • benötigen zudem eine Dependency-Injection
  • gespeichert im Play-Process und damit in der »Prozess-Tafel«
  • Der CalcStream ist eben das, also ein Organisationsmerkmal bzw. die Identität eines Teilprozesses
  • Der RenderDrive soll ein zyklischer Mechanismus sein, und als JobFunctor genutzt werden

ein Play/Renderprozeß wird mit einer definiten Quality-of-Service-Strategie aufgebaut; daraus ergibt sich implizit, was benötigt wird — und das ist ein sehr erweiterungsfähiges Konzept: beispielsweise könnte man das auf die Verfügbarkeit gewisser Klassen von Mediendaten erweitern, und es müßten somit nicht alle Daten immer direkt greifbar sein

mit der Erstellung eines CalcStream geht die Zusage einher, alle benötigten Resourcen tatsächlich im geforderten Maß verfügbar zu haben; diese Zusage mündet in die Übersetzung in eine abstrahierte RenderEnvironmentClosure; dahinter können sehr weitreichende Dispositionen verborgen sein, z.B. verteilte Resourcen in einem Render-Cluster/Netwerk-Setup, oder spezielle Hardware

die Zusage ist verbindlich und ohne zeitliche Parametrisierung; sollte eine Resource wegbrechen, so läßt man sofort den btr. Renderprozeß zusammenbrechen und markiert ihn als schadhaft

dieser wird nun mehr und mehr entkernt....

...und das kommt nicht von ungefähr; schon im Entwurf von 2012 sollte ja Dispatcher  ein Interface sein, und die aktuelle Implementierung wäre eine  DispatchTable, die direkt in der Fixture angesiedelt und gemanaged würde

JobTicket& accessJobTicket (TimeValue nominalTime, ModelPort, channel)

size_t resolveModelPort (ModelPort)

Aufbau Dispatcher::PipelineBuilder

JobPlanning wird als Aggregator angelegt

liegt also im ItemWrapper eines Transformers, und wird ab hier von den oberen Layern der Pipeline referenziert

erzeugt verkettetes JobPlanning

Es gibt also eine verkettete Version vom JobPlanning, die per Pointer auf einen parent-Plan zurückverweist, und dazu eine Prerequisite bereitstellt; tatsächlich liegt dieses verkettete Job-Planning in einem ItemWrapper in einem TransformIterator, welcher wiederum auf dem Stack vom Explorer angesiedelt ist; damit entsteht jeweils für ein Kind eine stabile Folge von Parent-Plannings bis zum Frame-Plan(root)

sehr altes Ticket...

Effektiv ist das der Implementierungs-Kern; seinerzeit hatte ich unter diesem Ticket die Idee der CalcPlanContinuation entwickelt, welche nun zum RenderDrive wird...

...bevor ich jetzt anfange, eine symbolische Notation und einen Parser zu erfinden, wäre ein einfache Konvetion für structured data  wohl sinnvoller; dann könnte man die Builder-Notation für GenNode nutzen

den skizzierten, wie üblich weitreichenden Plänen gemäß kann mit erheblicher Komplexität gerechnet werden; es erscheint daher angemessen, die eigentlichen Mock-Bausteine separat nutzbar zu halten und die DummyPlayConnection zur Orchestrierung des jeweiligen Test-Setup zu verwenden

ich steck mit dem Entwurf fest,

  • weil ein deduktives Interface-Design gefährlich wäre
  • aber ich keine ausgearbeiteten Details für einen induktiven Zugang habe

Daher gehe ich von der bis jetzt entwickelten Architekturskizze aus, und baue zunächst ein System von Mock-Implementierungen bottom-up; auf dieser Basis kann ich dann (hoffentlich) in die Konstruktion der Engine eindringen, um so erst die Basis zu schaffen für eine realistische Beurteilung der Möglichkeiten und Anforderungen

...das wird dann später die Basis für die Implementierung des Change-Builders

  • diese Operation untersucht die bestehende Segmentation
  • und spaltet bestehende Segmente auf
  • sie kann unterscheiden zwischen Umbau(=replacement) und Kürzen bzw. Klonen eines Segments
  • Schlußfolgerung: ein Segment selbst darf nichts über seine Zeitspanne wissen

Das mache ich ganz bewußt, um die Notation intuitiver lesbar zu machen. Tatsächlich ist der Unterschied zwischen Segment und zugehörigem JobTicket nur formaler oder systematischer Natur; es sind eben zwei verschiedene Entitäten, aber sie korrelieren 1:1 und repräsentieren lediglich eine unterschiedliche Sicht auf die gleiche Sache — denn ein Segmeint ist genau dadurch definiert, daß die Verarbeitungs-Pipeline konstant ist

...die Sache so einfach wie möglich halten, und keine spezielle ID-Logik nur für die Tests erfinden!

Ich bin weiterhin noch nicht davon überzeugt, daß die Differenzierung in Channel überhaupt notwendig und hilfreich ist; aber ich kann das derzeit nicht belegen ⟹ daher spiele ich das Thema erst mal herunter

derzeit erzeugt die Mock-Lösung nur ein NodeGraphAttachment für ModelPort-Index ≔ 0

und der MockDispatcher doppelt die definierte Exit-Node struktur identisch auf

dieses würde auf top-Level dann jeweils die generierte ExitNode-Struktur dieser ModelPort-ID zuordnen, und auf subtree-Ebene auf diesen ModelPort einschränken / filtern; allerdings kann ein Attribut in Rec<GenNode> nur einmal gesetzt werden. Alternativ könnte man in die Spec die Möglichkeit einbauen, ein Segment mit gleichen Zeiten mehrfach zu definieren, und dann mit jeweils anderem modelPort.

alle offensichtlichen einfachen Erweiterungen der Spec sind nicht schön...

auch wenn es irre scheint, sehr wahrscheinlich werde ich den ganzen Scheduler entwickeln können, ohne jemals ein reales low-level-Model anzufassen; daher ist die Port-Nummer wahrscheinlich völlig egal, und es gnügt, lediglich die APIs entsprechend zu erweitern...

definiert als: weniger als 1ms in der Vergangenheit

FUN = std::function<lib::TransformIter<

                        lib::TransformIter<

                            lib::IterStateWrapper<

                                steam::engine::JobTicket::Prerequisite,

                                LinkedElements<JobTicket::Prerequisite>::IterationState

                            >,

                            const steam::engine::JobTicket&

                        >,

                        steam::engine::JobTicket*

                                      >(steam::engine::JobTicket*)

                   >&

src/lib/iter-tree-explorer.hpp:632

src/lib/iter-tree-explorer.hpp:513

Er geht über die Instantiierung des Expander-Typs -> _DecoratorTraits<RES>::SrcIter

  • wobei RES der sichtbare Ergebnis-Typ des Expand-Funktors ist, also ein Iterator.
  • mir fällt auf, daß die Auswertung von shall_wrap_STL_Iter<RES> geht
  • etwas sonderbar ist, daß dieser Auswertungspfad als Call-Stack für die Assertion-Failure auftaucht (könnte aber einfach der Arbeitsweise des Compilers geschuldet sein)

aber ResIter::value_type war JobTicket

Das Design an der Stelle ist schon in Ordnung: als Base-Case nehmen wir die Situation, in der der Funktor eben gar nicht mehr adaptiert werden muß — auch wenn das rein logisch ein Sonderfall ist; andererseits erfordert der rein logisch „einfachste“ Fall eben eine zusätzliche Operation, nämlich eine Dereferenzierung des Quell-Iterators — und es ist sinnlos, diesen Spezialfall zu verdrahten, wenn die Typen nicht passen. Hinzu kommt, daß wir mit dieser Anordnung auch gezielter die Parameter-Typen prüfen können

...insofern wurde es erst sichtbar, als ich angefangen habe, den Aufruf zu de/re-konstruieren

...der dafür sorgen soll, daß aus dem Expand-Funktor ein JobTicket* rauskommt

FUN = std::function<

    TransformIter<TransformIter<IterStateWrapper<JobTicket::Prerequisite, LinkedElements<JobTicket::Prerequisite>::IterationState>

                               ,engine::JobTicket const&>

                 ,engine::JobTicket*>(steam::engine::JobTicket*)

                   >&;

SRC = iter_explorer::BaseAdapter<SingleValIter<engine::JobTicket*> >

per TypeDebugger verifiziert: die explizit angeschriebene Variable 'start' hat den richtigen Typ

TransformIter<TransformIter<IterStateWrapper<engine::JobTicket::Prerequisite

                                            ,LinkedElements<engine::JobTicket::Prerequisite>::IterationState>

                           ,engine::JobTicket const&

                           >

             ,steam::engine::JobTicket*

             >

lib::iter_explorer::Expander<lib::iter_explorer::BaseAdapter<lib::SingleValIter<steam::engine::JobTicket*> >, lib::TransformIter<lib::TransformIter<lib::IterStateWrapper<steam::engine::JobTicket::Prerequisite, lib::LinkedElements<steam::engine::JobTicket::Prerequisite>::IterationState>, const steam::engine::JobTicket&>, steam::engine::JobTicket*> >

Expander<SrC, ExpandedChildren>

ResIter::value_type ≡ JobTIcket

was wäre an der Stelle logisch korrekt ?

die Frage lautet:

 „können die Expanded-Results verwendet werden,

  an Stellen, wo SRC-Results erwartet werden“ 

für die Expanded-Results klar: brauche exakt den Ergebnis-Typ  des Iterators

...denn das ergibt sich erst im Aufrufkontext; man denke z.B. an rvalue/lvalue-Probleme und temporaries, und das alles noch mit constness gemischt. Auf weia

Fazit: Konversion in SRC::value_type ist per Abschwächung die korrekte Forderung

Und zwar erscheint mir die STL zugleich als zu komplex und zu offen; sie verleitet zu einem Low-level-Geknobel. Genau deshalb habe ich die gesamte Iteratoren- und Konzept-Hierarchie der STL beiseite geschoben, und ein »Lumiera Forward Iterator«-Concept neu definiert.

Denn ganz bewußt habe ich es abgelehnt, mit Lumiera ein universelles Framework zu schaffen (wiewohl eine Tendenz dorthin nicht zu verleugnen ist). Vielmehr geht es mit allen Konventionen und Definitionen um eine Gründung, also darum, einen Raum zu schaffen, in dem sich etwas Neues von einem wohl bestimmten Charakter entwickeln kann. Ich richte mich also nicht nach dem Vorhandenen und ich muß nicht dem Vorhandenen in allen seinen Weiterungen und Verzweigungen entsprechen.  Diese Haltung entspricht jedoch auch meinem Charakter: meine Fähigkeiten zum Erfassen und Sortieren vorgegebener Normen und Gepflogenheiten waren (und sind) gering, wenn ich mich auf einen solchen Anspruch einlasse, stehe ich schnell kräftemäßig mit dem Rücken zur Wand. Anders die meisten meiner Kollegen, die blühen da gradezu auf.

der Implementator möchte mit den Typedefs die Signaturen aufbauen

der Nutzer möchte wissen, wie er mit Resultaten umgehen kann

...für die STL schon, aber das ist genau meine Kritik: die STL ist hier zu konkret und zu sehr low-level — ein const_iterator ist demnach etwas Anderes als ein iterator von einem Container über const-Werte, und das widerspricht meinem Konzept, einen Iterator als eine opaque Quelle zu betrachten

auch das zum Glück

denn diese ist am aller fragwürdigsten

...was wohl genau daran liegt, daß das TypeBinding bisher konzeptionell "daneben" war

hier ist ein Hinweis auf TICKET #1125 : get rid of Val

analog zu meta::Strip, nur weniger aggressiv

  • Referenzen werden nivelliert
  • Pointer bleiben ein separater Typ

      using              _PID = PlacementMO::ID;

      using         ScopeIter = std::unordered_multimap<_PID,_PID>::const_iterator;

      using    ScopeRangeIter = lib::RangeIter<ScopeIter>;

      using _ID_TableIterator = lib::TransformIter<ScopeRangeIter, PlacementMO&>;

TransformIter<RangeIter<__normal_iterator<const GenNode*, vector<GenNode> > >

             ,JobTicket&

             >

Hatte zunächst gedacht, generell sollte value_type den Typparameter unverändert durchreichen (sofern kein nested Binding erfolgt). Tatsächlich bestand das Problem aber nur bei Pointern, und das lösen wir besser direkt in den RefTraits

IterAdapter selber sieht sauber aus....

und im Besonderen kein automatisches Entfernen von Indirektionen, und kein Rückgriff auf interne Typdefinitionen (also grade nicht  ValueTypeBinding verwenden!)

   * @note

   *  - when IT is just a pointer, we use the pointee as value type

   *  - but when IT is a class, we expect the usual STL style nested typedefs

   *    `value_type`, `reference` and `pointer`

der Double wird nämlich (wohlweislich) für diese Diagnostic-Anzeige auf 8 Stellen gerundet ... genau wegen Probemen wie hier

1 * 1.1 = 1.100000000000000000000000000001

konkret: double verhält sich anders als const double oder volatile double

  • lib/meta/util.hpp wird pervasiv verwendet und darf daher nahezu keine weiteren Includes vorraussetzen
  • demgegenüber wiegt lib/meta/trait.hpp schon mehr
  • und die Format-Funktionen können auch Itertools etc. mit includieren (denn sie werden i.d.r. aus der Implementierung heraus verwendet)

das heißt, wir erkennen gar nicht mehr, daß der Typ möglicherweise nested Bindings hat, weil viele Konstrukte für Klassen gar nicht valide sind für Referenzen, und daher die Erkennungs-Mechanismen ins Leer laufen

...welches sich hier konkret so manifestiert hat, daß der ResultIterator::value_type falsch deduziert wurde als JobTicket  (anstatt JobTicket* )

möglicherweise hat damals (2017) das perfect forwarding noch nicht so gut funktioniert (wir verwenden ja debian/stable, das war dann von 2015 und hatte noch etwas ältere Compiler)

HA!  std::is_convertible<const JobTicket, const JobTicket>()  ⟶ false

denn es ist letztlich egal, welcher Typ für welchen eintritt

nein! das Muster ist nicht symmetrisch

was bedeutet, das Ergebnis muß ein gemeinsamer Schnitt-Typ von beiden sein

und das bedeutet, daß beide Ergebnistypen einen gemeinsamen Schnitt-Typ haben müssen, der auf den Argumenttyp des Expand-Funktors konvertierbar ist

ist vielleicht sogar ein Compiler-Bug?

  • zwar hat std::common_type definitiv eine nested Typedef
  • aber diese wird relativ indirekt erzeugt, über den decltype einer Check-Funktion im Basis-Helper
  • und eine einfache Template-Spezialisierung wird matcht darauf nicht
  • obwohl die gleiche Technik bei einer einfachen Struct funktioniert (selbst bei Vererbung)

...und zwar weil eben auch der IterStack mit involviert ist, und die State-Core-Implementierungs-Funktionen allesamt verzweigen, je nachdem ob schon expandierte Kinder da sind; es bringt nichts, diese Verzweigungs-Struktur irgendwo wegzupacken, entweder man zerreißt sie in zwei Teile (wodurch sie unverständlich wird), oder der Expander selber wird eine leere Hülle, und alle Logik wandert in ein Delegate (wozu das?)

...das hat sich nun schon mehrfach als sehr hilfreich erwiesen: man instantiiert das Trait-Template „auf der grünnen Wiese“ und kann es dann direkt mit lib::test::TypeDebugger analysieren...

und korrumpiert daher die laufende Berechnung auf top-Level

Prerequisites sind privat in JobTicket

deshalb können sie erst im Konstruktor gebaut werden

außerdem gibt es kein Mutatons-API

also müssen sie auch bereits im Konstruktor vollständig gebaut werden

und obendrein ist JobTicket non-Copyable

also muß es per emplace erzeugt werden

Entscheidung im Hinblick auf den AllocationCluster

das kommt nicht von Ungefähr: dieses ganze (relativ fragile) Setup mit den Referenzen in LinkedElements mache ich ganz bewußt, weil am Ende ein Allocation-Schema beabsichtigt ist, bei dem viele Elemente in kurzer Zeit in einen gemeinsam allozierten großen Block gelegt werden; dort bleiben sie bestehen, selbst nachdem ihr Destruktor aufgerufen wurde. Die De-Allokation erfolgt auf einmal, zusammen mit dem gesamten Segment

das ist eine »bastel-Lösung«

list::emplace_back  funktioniert

Verwende eine halb-Rotation über size_t ⟹

  • die beiden Hälften des Bitstring der µ-ticks werden vertauscht
  • damit erheblicher Abstand zwischen konsekutiven Werten
  • hash² ≡ id
  • impl sollte i.d.R. nach inlining eine einzige Assembler-Instruktion sein ⟶ https://stackoverflow.com/a/31488147

...und das ist gut so

ärgerliche Konsequenz: bekomme viele Instanzen vom JobFunktor

...ich darf nicht daran hängenbleiben, daß der Marker literal in der InvocationInstanceID steckt; das mit der Uniton ist ohnehin nur ein temporärer Trick und kann nicht dauerhaft so bleiben — vielmehr ist die Lösung, den chained-Hash-Mechanismus für den Test so umzufunktionieren, daß man mit ihm beweisen kann, daß ein ganz bestimmter Job auch aufgerufen wurde.

...mit dem man zufällige pseudo-Invocations erzeugen kann und diese auch später mithilfe eines statischen Invocation-Log verifizieren.

...und die setzt auf die für später tatsächlich vorgesehene Hash-Verknüpfung auf, welche in diesem Fall auch die nominelle Zeit in die InvocationInstanceID mit einrechnet — nicht jedoch die reale Deadline (die in der Job-Instanz explizit vermerkt ist)

Bisher wird noch die Fiktion aufrecht erhalten, daß die Basis-Schnittstelle zum Scheduler in reinem C geschrieben ist; tatsächlich ist dadurch so mancher Teil der implementierung grenzwertig bzw. würde tatsächlich mit reinem C nicht (mehr) funktionieren; außerdem bekommen wir mehfrach geschichtete Vererbungen und müssen regelmäßig casten und implizite ungeprüfte Annahmen machen.


Und das gemischte Setup ist tückisch: habe gestern Nacht und heute ein paar Stunden einen Link-Fehler gesucht, der auf ein fehlendes extern "C" { } zurückging, aber nicht aufgefallen war, solange C++ die Definitionen inlinen konnte...

hier Problem mit der Model-Port-Differenzierung geeignet »unter den Teppich kehren«

tatsächlich: erst mal nur stupide aufdoppeln

...eine billige und manipulative Implementierung in MockSegmentation

das erlaubt flexibles Hinzufügen, ohne daß Addressen invalidiert werden. Die Tickets selber müssen NonCopyable sein

sonst ⟹ REJECT

Nach logischer Analyse der spezifiziereten Fälle lassen sich einige Verzweigungen verkürzen

  • die Suche nach dem Split-Punkt und das Festsetzen der Start/Endpunkte fasse ich zusammen in den Konstruktor; danach sind Start/Endzeit immutable
  • sichere die Verkürzungen möglichst durch Assertions ab
  • der SPLIT und SWAP-Fall wird dargestellt, indem ich den Predecessor auch als Successor verwende, und lediglich für beide verschiedene Operationen codiere
  • sofern für beide bereits eine Operation definiert wurde, kann man die gesamte Untersuchung des Successors überspringen
  • in dem Fall, in dem ggfs. mehrere überdeckte Successoren übersprungen werden, schiebe ich lediglich die Variablen
  • den Umstand, daß Predecessor/Successor verworfen werden, oder eben nicht, setze ich direkt durch Justieren des Iterator-Bereichs um
  • anschließend werden zuerst die neuen Elemente davorgehängt und dann der zu verwerfende Bereich gelöscht
  • als Ergebnis gebe ich die bezeichneten Iterator-Positionen zurück
  • alle Mutator-Operationen arbeiten auf Iteratoren (d.h. setzen eine nicht-invalidierende random-access-Liste vorraus)

lib/split-splice.hpp

tja...

das hätte ich schon seit JAHREN machen können....

(aber auf den Kniff mit dem Marker-Typ bin ich erst gekommen, seitdem ich mich neulich nochmal mit user-defined-Literals beschäftigt habe)

[-100~100[┤

[-100~5[[5_23[[23~100[┤

expect:├[-100~2[[4_5[[5_10[[10~100[┤

actual:├[-100~4[[4_5[[5_10[[5_10[[10~100[┤!gap_10<>5_!

bei der Verarbeitung des Successors hat einer der Fälle auf die Operation opPred_ für den Predecessor geprüft; im konkreten Fall war das TRUNC, wohingegen für den Successor SEAMLESS vorgesehen war. Daher wurde dann an dieser Stelle eine Trucated-copy dies Successors eingefügt, wobei in diesem Fall das Trucate gar nicht verkürzt hat, da der Anfangspunkt des Successors bündig liegt; so kommt es, daß der Successor komplett aufgedoppelt wurde

...habe im Debugger verifiziert, daß das mit den Interatoren-Positionen wirklich klappt: sie bleiben stabil. Trotzdem wird der Code lesbarer, wenn man nur in diesem Stück pred_ explizit bezeichnet als "insPos" (und pred_ nicht weiter verwendet).

Ich kann an dieser Stelle noch nicht auf die tatsächiche Implementierung vorgreifen, sonst wird das alles ein undurchdringbares Knäuel. Die eigentliche Implementierung muß stark auf Performance optimiert sein, und daher ist die Datenstruktur vermutlich schwierig zu navigieren. Also baue ich hier ganz bewußt erst mal eine Fake-Variante auf, mit einer anderen Implementierung und einer bequemen Datenstruktur

... vor allem eine zusätzliche Indirektion, deren Wirkung nicht einfach abzuschätzen ist. Die tatsächliche Listenlänge muß letztlich immer irgendwo explizit repräsentiert werden, und wenn man dies in einem Subtyp verbirgt, muß jeder Datenzugriff zwingend einmal durch eine Indirektion laufen

geht nicht: ist privat (und das ist sinnvoll so!)

und zwar ist die JobTicket-Struktur explizit darauf angelegt, anderweitig erstellte Deskriptoren zu verlinken; das soll so sein aus Performance-Gründen (Cache Locality ⟹ AllocationCluster)

Spezifikation: JobTicket erstellen

die muß definitiv von „wo anders“ kommen

Ziel sollte tatsächlich sein, die Komplexitäten mit der Allokation aus dem funktionalen Code heraus zu verbergen; denn dies dient nur dem separaten Belang der Performance

...ist hier relevant, denn dies scheidet eine einfache Funktions/Konstruktor-Schnittstelle aus; wir müssen einen strukturierten Datensatz bereitstellen

Zunächst wurden LinkedElements lediglich aus Gründen der Konsistenz auch hierfür verwendet. Eine intrusive-single-linked-List mag für die Vernetzung der Prerequisites sinnvoll sein, aber für eine Sprungtafel nach Channel-Nr ließe sich genauso gut eine Array-backed-Implementation konstruieren (vielleicht dann ein neuer Anlauf anstelle der alten Idee des »RefArray« ?)

...denn es wird noch deutlich über dieses PlaybackVerticalSlice hinaus dauern, bis die erste rudimentäre Implementierung des Builders am Start ist — und erst dann gibt es eine praktischen Bezugspunkt für das Design

...möglicherweise könnte zumindest so der wichtigste Standardfall komplett ohne einen eigens allozierten JobFunktor dargestellt werden — einfach indem alle notwendigen Parameter direkt aus der referenzierten ProcNode gezogen werden. Um diese Möglichkeit abzuschätzen, müßte aber zuerst definiert werden, wie der Übergang zu den Prerequisites konkret im Proc-Node-Graph dargestellt werden: durch spezielle Metadaten? oder durch eine besondere Marker-Node, die wie eine Quelle fungiert?

...nach zwei Tagen Gewürge....

über die interne Provision-Datastruktur (LinkedElements)

...was auf eine Verwirrung in den Konzepten und Begriffen hindeutet (die ich schon seit Tagen vermute, aber noch nicht recht fassen kann)

also relativ zum Ursprung des Time-Grid dieser Timeline

...deshalb kapiere ich ihn immer nicht

...dann lese ich ihn einzeln durch, und alles sieht „richtig“ aus...

...weil ich erst mal einen Test schreiben wollte, und sonst noch keinerlei tragende Strukturen hatte

⟹ alles mit Latency und Deadline muß weg

also besser dann als separaten Test dokumentieren

allein schon, weil wir eine limitierte Domäne haben bezüglich lib::time::Time

...oder genauer gesagt, ein Design-Mismatch — das ganze ID-System in Lumiera ist magisch und „hintenrum“ mit globalen Tabellen verbunden; damals erschien mir das gradezu natürlich, heute sehe ich leider keine bessere Lösung, die man in C++ realisieren kann (Scala löst dieses Problem mit den Implicits bzw. Givens)

...und nicht das ultimative Medien-Framework!!

....also ist es durchaus im Rahmen, wenn es eine fest vereinbarte Hintertür gibt für die Tests

es lebe die DummyPlayConnection

DataSink ist ein lib::Handle<play::OutputSlot::Connection>

Also muß es im einfachsten Fall auf eine Subklasse von Connection zeigen; diese füge ich hinzu mit lauter UNIMPLEMENTED stubs

...denn eine »state core« wird automatisch von TreeExplorer erkannt und adaptiert...

siehe iter-chain-search.hpp

...man packt den eigentlichen TreeExplorer-Builder-Aufruf in eine Hilfsunktion und greift den decltype vom Rückgabewert ab; von diesem Typ kann man dann erben (und verwendet die erwähnte Hilfsfunktion im Konstruktor, um den Parent-Typ zu initialisieren)i

So macht es der TreeExplorer selber, und nach etlichen Versuchen bin ich auch hier bei dieser Lösung gelandet (und zufrieden damit)

In diesem Zusammenhang ist type inference praktisch unvermeidbar, denn die komplexen Typen vom TreeExplorer können anders nicht explizit gemacht werden — aber das Problem ist, die Typen sind rekursiv, denn den neuen, erweiterten Builder-Typ muß ich schon kennen, um ihn in dem TreeEplorer-Builder als Core zu übergeben, aber erst durch diesen Aufruf wird dieser neue erweiterte Builder überhaupt definiert

...also zunächst einen deaktivierten NumIter als Basisklasse, und an diesen später einen aktivierten NumIter zuweisen...  ziemlich häßlich, aber damit bekomme ich es überhaupt erst mal durch den Compiler...

...weil sie erst nach den Builder-Typen definiert werden kann, aber bereits vor ihnen gebraucht wird

...weil man dadruch von der Definitionsreihenfolge entkoppeln kann

geht, ist aber alles andere als klar, da man zunächst den Member-Fun-Pointer abgreifen muß, und dann von diesem den decltype ziehen (und erst damit kann man lib::meta::_Fun anwenden)


  using Pointer = decltype(&Dispatcher::PipeFrameTick::timeRange);
  using ResType = lib::meta::_Fun<Pointer>::Ret;
 
  struct Dispatcher::PipeSelector
    : ResType
    {
      ...
    };

das löst ganz elegant beide Probleme

  • da es sich um ein Template handelt, findet der Definitions-Check erst später statt, wenn die tatsächliche Pipeline gebaut wird
  • der jeweilige Quell-Iterator-Typ ist als Template-Parameter gegeben, und kann damit direkt abgegriffen werden und in die nächste Instanz einfließen, welche einfach von ihm erbt

Zwischen-Fazit: Anwendung von TreeExplorer passiert nur in einem Funktions-Scope

Der expand()-Mechanismus im TreeExplorer ist kein monadisches flatMap  — sondern nur ähnlich (aber an den intendierten Nutzen angepaßt): das Vater-Element erscheint zunächst selbst im Resultat-Iterator, und dann erst folgen expandierte Kind-Elemente; monadisches flatMap würde den Vater sofort konsumieren und rekursives flatMap würde sofort bis auf unterste Blatt-Ebene entfalten. Aber die Konsequenz ist: da wir den Vater selber einmal durchreichen, müssen Ergebnistyp und Quelltyp kompatibel sein

Fazit: in diesem dritten Anlauf konnte ich das Problem befriedigend lösen

...denn TreeExplorer hat die Eigenschaft, von der »state-core« zu erben, und damit ihr public-API nach außen durchzureichen — im Besonderen auch für obere Layer in der Pipeline

...und zwar durch den gleichen Template-Slicing-Trick, der auch schon bei TreeExplorer selber so erfolgreich funktioniert: der Builder wird durch ein std::move auf die Basisklasse beschnitten und fällt dann als Temporary am Ende der Builder-Expressison einfach weg, aber aller relevanter Content ist in das Ergebnis geschoben worden, welches wegen RVO direkt am Zielort konstruiert wird

ich gehe davon aus, daß praktisch alle Tests, die dieses Mock-Framework verwenden, lediglich prüfen daß eine gewisse Connection durchgereicht wurde

ich will für den ganzen Mock-Support eine Lösung, die „einfach funktioniert“ — sofern man mit einer Instanz von MockSegmentation bzw. MockDispatcher arbeitet (und sonst nichts beeinflußt)

...denn das entspricht noch am Meisten dem später mal erwarteten Ablauf

FAIL___expectation___________

expect:11-22-33-44-55

actual:11-22-44

...ursprünglich hatte ich nämlich per emplace_back() alloziert....

an der Stelle, wo die Referenz auf Objekt-33 zurückgeliefert werden sollte, kommt tatsächlich eine Referenz auf das zuletzt rekursiv erzeugte Objekt-44  zurück, und wird daher in die Liste der Prerequisites eingefügt.

um den Lifecycle komplett kontrollieren zu können

...auf irgend ein Objekt...

Allerdings — da die Segmente vermutlich nicht gemockt werden (sondern nur die Segmentation), muß ich mich da schon um eine Allokation für ein Dummy-Objekt kümmern

...da aktuell noch keine Verbindung zum Render-Nodes-Network besteht...

(das heißt, das Problem mit den Mocks verschiebe ich in die Zukunft)

und das heißt im Klartext: std::deque — später kombiniert mit einem custom-Allocator, der auf den AllocationCluster delegiert

aktuell wäre spezifisch besser —

aber AllocationCluster wäre generisch

Beschluß: Allo ≡ Funktor mit variadischen Argumenten

exakt dieses Problem hatte ich schon vor einigen Wochen, bei der ersten (damals Mock)-Implementierung: da die Prerequisites private sind, gibt es keine andere Lösung als sie aus dem übergeordneten ctor heraus zu konstruieren, und damit re-entrant aus dem JobTicket-ctor ein weiteres JobTicket zu allozieren. Vector und Deque können das nicht hanhaben, und es kommt zu einem gefährlichen Aliasing, bei dem das geschachtelte Ticket an der gleichen Stelle im Speicher steht wie das Haupt-Ticket, und damit dessen ctor-Aufruf korrumpiert

damit sitz ich in der Falle...

  • da ich rekursiv die Vorläufer-Tickets allozieren muß, ist ein λ unabdingbar
  • aber damit kann ich den Typ des Transform-Iterators nicht mehr explizit anschreiben
  • anderseits kann ich aus dem gleichen Grund den Typ des Spec-Tuples nicht explizit anschreiben

bisherige Mock-Lösung
nun darauf aufsetzen

nach dem ersten Schreck (weil ich es doch grade erst neu programmiert hatte...) zeigt sich, daß die bisherige Mock-Implementierung bereits sehr gut ist, und strukturell direkt in die endgültige Implementierung übersetzt werden kann; wir machen dann lediglich zweimal eine strukturgleiche rekursive Verarbeitung (GenNode ⟼ ExitNode-Struktur sowie dann ExitNode-Struktur ⟼ JobTicket)

ursprünglich dachte ich, im Ticket würde intern noch nach einer Channel-ID (für Mehrkanal-Medien) differenziert; die Analyse im Detail ergab jedoch, daß dies stets unter dem Konstrukt »ModelPort« subsummiert werden kann — viele interne Komplexitäten fallen damit weg

Testfall: retrieve_JobTicket

head_ steam::engine::JobTicket::Provision * 0x2525252525252525

provision.requirements.emplace(preNode)

und die nimmt im ctor ein JobTicket const&

...denn das ist nicht wirklich ein Belang der Segmentation (diese arbeitet stets mit den Model-Port-Indices), sondern hat ehr mit dem globalen Mapping auf eine Timeline zu tun — also etwas, was  irgendwo in der RenderEnvironmentClosure verborgen liegt, denn wir wollen definitiv nicht ein globales »Timelines«-Array durch die Hintertür einführen.  Das High-Level-Model ist auf oberster Ebene ein Wald, und nicht eine einzige kohärente Struktur

neue Dispatcher-Operation: size_t resolveModelPort(ModelPort)

...denn dieser hat natürlicherweise beide Elemente in der Hand...

Eigentlich könnte das JobTicket für ein ganzes Segment konstant sein, ggfs noch aufdifferenziert nach ModelPort — der eigentliche JobFunctor (im Ticket) wäre vom Prinzip her in jedem Fall konstant und damit nur einmal alloziert. Aber jeder separate CalcStream braucht die Daten in einem anderen Ausgabe-Puffer, gegeben durch das DataSink-Handle.

  • »just in time«
  • wir sparen uns eine weitere Buffer-Allokation
  • und einen weiteren Kopiervorgang

Wenn wir das erhalten wollen, dann müßten Jobs untereinander kollaborieren und einen gemeinsamen Buffer verwenden, oder ein Buffer-Handle weitergeben

also stets das 2.Element im Tupel. Bin nämlich zu faul, hier auch noch das Tupel irgendwie zu rendern. Das mach ich dann im nächsten Testfall...

FAIL___expectation___________

expect:J(11|200ms)-J(22|200ms)-J(11|240ms)-J(22|240ms)-J(11|280ms)-J(22|280ms)

actual:J(11|200ms)-J(22|240ms)-J(11|240ms)-J(22|280ms)-J(11|280ms)-J(22|320ms)

  • non-Copyable-Objekte in einem Vector sind nicht möglich...
  • die Zahl der Feeds und CalcStreams muß offen bleiben ⟹ Heap
  • aber spätestens vom RenderDrive darf es nur noch eine Instanz geben (pro realem CalcStream)

bisher: FrameCnt getNextAnchorPoint()

lib::Handle<OutputSlot::Connection>

denn die Inovcation muß in diese Sink rendern

Verhältnis von JobPlanning

und JobTicket::Provision klären

und zwar wegen Separation of Concerns   —  der JobFunctor ist kein Informations-Service (genau dafür haben wir ja das JobTicket geschaffen)

Problem : habe nur JobTicket des unmittelbaren  Vorgängers

weil das die einzige Stelle ist in der zugleich Dependent und Dependency zugänglich sind

da im JobPlanning nochmal eine JobTicket& liegt, sind die zwei bisher im darunter liegenden Tupel gespeicherten JobTicket* eigentlich redundant und werden nur für die Explorer-Mechanik gebraucht

weil nun auf jeder Ebene im Explorer-Stack eine JobPlanning-Instanz liegt, und zwar dort im ItemWrapper für den jeweiligen TransformIterator (der die Explorer-Funktion implementiert)

....und das gilt aber auch für die einzelnen Felder darin, nachdem nun FrameCoord als Konzept aufgegeben wurden: es sind nun zwei Referenzen statt einer, aber ich erwarte, daß alle diese Referenzen vom Optimiser erkannt und ausgelassen werden...

Wenn man die Daten entsprechend reorganisiert und dadurch die Redundanzen in der Pipeline minimiert kann der Optimiser viel machen, da (private) Referenzen überhaupt nicht repräsentiert werden müssen, sofern die referenzierte Quelle (wie hier) stabil im gleichen Objekt liegt. Wenn ich richtig schätze, habe ich mit dem Umbau nur einen »slot« mehr Speicher verbraucht (weil drei redundante »slots« wegfallen können)

AUA: dieser parent-Pointer war bisher sogar eine dangling reference — er zeigte nämlich auf einen Pointer im Stack-Frame des λ-Aufrufs, der aber gar nicht mehr existiert, wenn man den Transform-Iterator auswertet. Soschnellkannsgehen

...denn sonst müßte ich sie in eine λ-Closure binden, was aber für zwei der drei Felder redundate Storage wäre; es macht auch sonst Sinn, wenn der Tick-Generator eben auf den FrameCoord arbeitet

dann müssen die FrameCoord jetzt sterben

verwendet nur absoluteNominalTime

das ist die einzige Verwendung von zwei Feldern

Das ist genau der Grund warum sie jetzt zum Problem werden: diese Funktion erzeugt FrameCoord, die irgendwo leben müssen, und das bloß, um den einen Aufruf zu machen. Das bestätigt die Enscheidung, sie zurückzubauen

leider ist das ziemlich abschüssiges Gelände

....per SFINAE feststellen, daß ein Asignment-Operator unterdrückt wurde; lt. Standard sollte das gehen (das war eine späte Änderung zu C++11, da es in der Stdlib so viel Probleme gemacht hat) — aber in der Praxis weiß ich, daß das fragil ist, manchmal Fehler auslösen kann (statt SFINAE), und daß es Diskrepanzen zwischen den Compilern gibt. Ich hatte auch schon Fälle, wo std::is_assignable rundweg versagt hat....

also käme erst mal nur in Frage, dort stets die alte Payload

zu zerstören und eine neue aus dem zugewiesenen Argument

zu konstruieren, was aber meist auch das ist, was man dort möchte

Der typische use-case, für den dieser ItemWrapper geschaffen wurde, ist, ein beliebiges Lambda auszuwerten, und das Resultat dann im Puffer vorzuhalten; speziell wenn das Lambda ein neues Objekt konstruiert und per Value zurückgibt, sorgt die RVO dafür, daß dieses Objekt dann (per copy elision) sofort im Puffer im ItemWrapper konstruiert wird — in der Praxis ist das der häufigste use-case und tritt im Besonderen in einem Transform-Iterator auf. Wenn andererseits die Payload ein POD oder einfacher Wert ist, dann sind Destruktor und Konstruktor ohnehin trivial...

...denn sonst würde es genau zu rekursiven kaskadierenden (quadratisch aufwendigen) Aufrufen der Deadline-Berechnungs-Logik kommen; um das Caching zu steuern, kann ich einen Marker-Wert Time::NEVER verwenden

Ich kann mir nicht vorstellen, daß die große Mehrheit der Jobs mehr als eine Prerequisite bekommt...  der Aufwand ist n/2 * (n+1), das bringt uns erst mal solange nicht um (bis es uns nachweislich umbringt...)

...weil es der Sache nach wirklich nur eine Konversion der aufgesammelten Infos ist, und nicht eine komplexe Berechnung; außerdem paßt es so halbwegs auch syntaktisch zum beabsichtigen Nutzen — aber genau dieser Nutz-Kontext ist auch ein Gegen-Argument: der Sinn der Sache erschließt sich nämlich nicht intuitiv, wenn man da sieht: *pipeline

Definition: der letzte Zeitpunkt an dem der Job starten darf

Definition: Pufferzeit, um die der Job bereits früher starten kann

das ist adäquat, da in diesem Bereich grundsätzlich nichts mehr gecheckt wird (das hat alles schon der Builder getan)

sonst wird der Test später instabil

...aktuell denke ich, die Details der Timings kommen aus der RenderEnvironmentClosure (aber was das heißt sei dahingestellt....)

InvocationInstanceID invoKey{timeHash (nominalTime, provision.invocationSeed)};

brauche dann aber einen Weg,

um die Frame-Nummer zu transportieren

im Job wäre die nominelle Zeit gegeben

die aktuelle Frame-Nummer ist nun nur noch ein lokales Detail in der Planning-Pipeline; für den re-Trigger-Job genügt eine Zeitangabe, aus der dann eine realTime-Deadline abgeleitet wird

Das könnte potentiell tückisch werden; vermutlich ist es aber harmlos in der Praxis, sofern durch sinnvolle Parametrisierung für ausreichenden Zeitpuffer gesorgt wird. Das Restrisiko besteht darin, daß die Aktivierung bereits geplanter Jobs den Planungsvorgang überholt, welcher dann nachträglich noch Vorraussetzungen aufschaltet auf einen bereits gestarteten Job; schlimmstenfalls gibt sogar der BlockFlow bereits die ganze Epoche frei, während der aktuell laufende Planning-Chunk noch auf den damit verbundenen Activities arbeitet ⟹ data corruption, Segfault

bekommen wir das Problem durch Checks in den Griff —

oder brauchen wir eine transaktionelle Schnittstelle?

...denn auch das Entnehmen und Starten von Jobs muß unter Grooming-Token erfolgen, kann also erst erfolgen, nachdem der Planning-Chunk komplett abgeschlossen wurde

unklar: JobTicket::startExploration() — wie wird die eigentliche Planung eingefädelt?

es wird das Vorhandensein abstrakt definierter Zustands-Deskriptoren impliziert, deren Sinn der Implementierung vorbehalten bleibt

weil Segment inhärent ein mutabler Typ ist (ich denke an das Umbauen und Modifizieren), gibt der reine Access nur ein Segment const& raus

...uns anders wäre es sogar erst mal viel plausibler; ich selber habe lange Zeit anders gedacht, nämlich das die Channel hier den Medienkanälen entsprechen. Dann müßte man aber jedem CalcStream noch ein Channel-Mapping mitgeben, und auch für Prerequisites müßte eine Transformation für dieses Mapping mit angegeben werden. Daraus wird plausibel, warum ich diesen naiven Ansatz verworfen habe....

...denn der Builder-Ansatz bedeutet, daß das semantische / domänen-bezogene high-level-Modell explizit übersetzt wird in ein reines Ausführungs-Modell. Letzteres soll keine Intelligenz mehr zur Interpretation enthalten, sondern nur noch zwangsläufig ausgeführt werden. Dem entsprechend muß in diesem Ausführungs-Modell (low-level-Modell) jedweder dynamische Auswertungszustand soweit möglich vermieden werden. Zur konkreten Ausführung dennoch erforderlicher Zustand wird symbolisch repräsentiert ("representational state")

...zur Übersetzung der internen Strukturen im JobTicket in eine Folge verschachtelter Prerequisite-JobTicket

denn diese wären rekursiv wieder JobTickets; würde man die Aufteilung nach ModelPort in das JobTicket selber hineinnehmen, dann wären auch die Prerequisites wieder nach ModelPort untergliedert; das wiederspricht den Freiheitsgraden der Struktur (Prerequisites sind an die einzelne ExitNode gebunden)

Grundsätzlich ist es erst mal egal, man braucht eben einen Descriptor-Record pro Segment pro ModelPort. Da aber JobTickets on-demand erzeugt werden, wird der Aufwand hierfür verschoben in die Job-Planung (und findet gar nicht statt, solange eine bestimmte ExitNode konkret noch gar nicht bespielt wurde)

Und zwar, weil es so am Einfachsten ist für die Implementierung der OutputConnection = slot.allocate() ....

Denn es kann durchaus so sein, daß sich hinter einer Connection gleich mehrere Sinks auf Hardware / Driver-Ebene befinden. In diesem Fall wäre es andernfalls notwendig, wieder innerhalb der OutputConnection ein de-Multiplexing zu machen. Und das will ich nicht, sowas gehört in die Engine. Der OutputSink ist wirklich nur noch ein Buffer, der mit gewissen Timing-Constraints bespielt werden muß

...es ist keineswegs sicher, daß wir mit einer bloßen Addressierung per Channel-Nummer auskommen; es könnte durchaus passieren, daß Erweiterung auf eine generische Selektions-Sprache notwendig wird

man denke nur an higher-order Ambisonics...

den Channel-Parameter kann man leichter wegfallen lassen, als ihn nachträglich durchzufädeln; die Entscheidung selber wird erst relevant, wenn wir das low-Level-Model konkretisieren

...Kernargument ist: das Multiplexing von Medienchannels ist Teil der Berechnungen selber, tritt also nicht auf Interface-Ebene auf. Selbst die Funktionalität des Switch-Board im GUI-Player wird in der Renderpipeline selber implementiert...

Klärung: „Channel“ ist hier eine Port-Nummer

Dies folgt aus den Überlegungen zur Identität eines CalcStream, sowie aus dem Builder-Ansatz: es handelt sich demnach nicht um den Medien-Channel, sondern nur um die Auswahl aus N an dieser Stelle theoretisch möglichen CalcStrams / Daten-Strömen. Im klassischen Standard-Beispiel gäbe es also einen Port für Video und einen Port für Sound, und ein Play-Prozeß könnte optional nur einen oder alle beide abspielen, aber jeder von diesen geht an ein anderes Device und läuft deshalb selbständig. Die Zuordnung eines solchen möglichen Datenstroms zu einer ganz bestimmten Processing-Pipeline ist schon im Build-Vorgang vorentschieden worden, und ist im Job-Ticket jeweils für ein Segment komplett voreingestellt; soll dann konkret ein bestimmter Datenstrom erzeugt werden, muß man nur noch diese Port-Nummer anzugeben, um dafür die passenden Jobs zu generieren

Dies ist erst mal nicht offensichtlich, denn grundsätzlich wird ja immer auf einheitliche Topologie hin segmentiert; es wäre sehr wohl denkbar, daß z.B. der Sound fast durchgehend nur eine einzige Topologie der Processing-Pipeline hat, während für Video mehrfach die Topologie gewechselt wird (Fades, Overlays...). Aber eine praktische Abschätzung ergibt, daß im Regelfall für jeden Clip sowohl die Sound-Quelle, alsauch die Video-Quelle jeweils spezifish ist, auch bezüglich des Offset im Quellmedium; wenn ein Clip wechselt, dann wechseln sowohl Bild und Ton. Daher ist es wahrscheinlich, daß eine feingranularer angesetzte Segmentation in der Regel redundant wäre. Und diese Redundanz wäre kostspielig, denn die Zahl der Segmente ist erwartungsgemäß hoch.

und alle müssen an dieser Stelle auf einen Schlag angebunden werden

Begründung: wir wollen definitiv nicht für jedes Segment wieder einen Map-Lookup machen

Nach aktuellem Stand verweist das JobTicket per Pointer oder Referenz auf ein ExitNode-Objekt, und diese Referenz wird später an den JobFunktor durchgereicht. Irgendwo dahinter verbirgt sich ein Element mit fester Identität (Referenz-Semantik), aber ich weiß noch nicht wo genau. Meine Vorstellung ist, daß das Segment an einer festen Position im Speicher fixiert bleibt, solange noch CalcStreams bzw. Jobs aktiv sind, die die dahinter hängenden Render-Nodes referenzieren — das ist die Grundidee hinter dem »AllocationCluster«

...und zwar, um den Umgang mit der Datenstrutur und das Testing nicht unnötig zu verkomplizieren; ich hoffe, der Umstand, daß ExitNode als MoveOnly markiert ist, sorgt für ausreichend Sicherheit (deshalb dürfte sich die Collection der ExitNodes nicht ohne Weiteres kopieren lassen)

Also konkret die Frage: kann ein-und-diesselbe ProcNode mit allen darunter hängenden Strukturen gleichzeitig von mehreren Segmenten verwendet werden? Das klingt zunächst einmal durchaus nach einer plausiblen Möglichkeit, da ja die Render-Nodes selber nichts über ihre nominelle Zeit wissen sollen; demnach wären die Render-Nodes eine persistente Datenstruktur

⟹ Render-Nodes als persistente Datenstruktur behandeln?

↯ ABER dann brauchen wir einen Ref-count...

im low-level-Model selber gibt es keine Entscheidungslogik mehr; sofern es mehrere Ausprägungen oder Verzweigungen gibt, werden diese als Zweige im Modell explizit gemacht

die konkrete Ausprägung jedweder Eigenschaft findet im Node-Graph statt, nicht sonstwo in der Fixture. Beispielsweise ist im Node-Graph festgelegt, wo Caching stattfinden kann, oder welche Sub-Zweige als Prerequisites separat gescheduled werden. Die ExitNode aggregiert diese Informationen nur, und im JobTicket werden sie aufgehängt

sofern irgend eine Eigenschaft renderbar ist, gibt es eine Anknüpfung aus einem Segment per ModelPort-Nr in das Modell

...damit, daß das JobPlanning bereits in der Mitte der Pipeline erzeugt wird...

hier spielt auch mit, daß man das expandPrerequisites() stets weglassen kann (das folgt schon aus den Eigenschaften einer solchen Expander-Funktion — sie muß auch ohne Expansion funktionieren)

Vorläufig: einen Transformer vorsehen, der das JobPlanning noch manipulieren kann

Wichtig: dieser Transformer muß explizit JobPlanning& als Ergebnistyp deklarieren: damit bekommt man die »Referenz-Variante« vom ItemWrapper — leider aber auch eine zusätzliche Indirektion, die nicht wegoptimiert werden kann (weil sie je nach darunter liegendem Expander woanders hin zeigt)

es gibt einen commit

Einträge sind woanders alloziert

nur eine einzige, die Aktive

geplant: generisches front-End für Custom Allocator

das bedeutet: die alten Interfaces müssen von den neuen Interfaces erben, dann kann schon stückweise Code geschrieben werden, der die neuen Interfaces vorraussetzt...

werden vom jeweiligen Funktor flexibel interpretiert (ggfs auch als void*); der wichtigste Funktor ist der für das eigentliche Rendern �� dieser speichert hier die ExitNode und das DataSink

wir können nicht die Daten für jeden Job einzeln nachverfolgen (dann würden wir etwa den Level von Arbeit leisten, wie ein Garbage-Collector) — vielmehr muß es sowas wie »epochs« geben

In extremen Situationen kann es vorkommen, daß das Checkpoint-System eine Rest-Menge identifiziert, die aus anderen Gründen  zuverlässig beobachtet werden muß. Als Beispiel denke ich über obsolete Reste einer aufwendigen Berechnungskette, die unterwegs geändert wurde: hier müssen diejenigen (wenigen) Activities gefunden werden, die bereits „unterwegs“ sind; erst wenn diese alle wieder zurückgekommen sind, kann ein Clean-up-Trigger feuern

ganz im Gegenteil: hier braucht man eine trivial kopierbare und destruierbare Datenstruktur mit Standard-Layout und keinerlei Magic

dispatches a JobFunctor into an appropriate worker thread, based on the job definition's execution spec

no further dependency checks; Activities attached to the job are re-dispatched after the job function's completion

signal start of some processing — for the purpose of timing measurement, but also to detect crashed tasks; beyond that, also transition from grooming mode ⟼ work mode

correspondingly signal end of some processing

push a message to another Activity or process record

probe a launch window from start to deadline, and additionally check a count-down latch; on success activate the next Activity, else re-schedule @self into the future

supply additional payload data for a preceding Activity

post a message providing a chain of further time-bound Activities

internal engine »heart beat« -- invoke internal maintenance hook(s)

 

Activity-Terme sind sowohl semantsich, als auch operational polymorph; das heißt es gibt einen gemeinsamen Kontrakt und eine flexible Ausdifferenzierung im Verhalten. ABER aus Performance-Gründen können wir uns im Scheduler keine unnötigen Indirektionen und variablen Allokationen leisten; stattdessen wird die gesamte Storage per Mehrfachbelegung auf einen einzigen Datenblock abgebildet, und das Verhalten wird in ein Switch-on-Selector-Field übersetzt

startTime

deadline

...wir arbeiten komplett unter dem Schutz des Typsystemts, und deshalb werden auf 32bit-Maschinen trotzdem 64bit für die Zeitangaben bereitgestellt, zuzüglich Alignment; theoretisch könnte man ein spezielles Encoding für den »Instant« definieren, das nur int32_t benötigt; das betrachte ich allerdings als Optimierung

Richtwert: 2 * 64bit + next-Ptr

dieses würde auch auf das GATE aufgeschaltet, welches dadurch mit einem höheren Latch startet; erst nachdem das NOTIFY eigens aktiviert wurde, könnte das GATE grundsätzlich freigeben

dazu allerdings müßte dieses unterscheiden können, ob es vom Scheduler explizit aktiviert wird, oder ob es durch eine Notification freigeschaltet wurde

Das ergibt sich unmittelbar aus den Prinzipien dieses Designs: Job-Planung läuft auf das Erstellen von Activities hinaus, und dies bedingt Speicherverwaltung — welche der grundlegenden Entscheidung gemäß nur im Management-Modus (single-threaded, unter GroomingToken) stattfinden darf

zusätzlicher Ankerpunkt callback_

gib mir alle Ressourcen jetzt

gib mir anderweitig nicht benötigte freie Resourcen

zu knapp ⟹ Render korrumpiert

Der implizite Kontrakt sowohl für »freewheeling render«, alsauch für »background render« ist, daß die Berechnungen vollständig sind — man hat ja genau dafür im Gegenzug auf die Deadline verzichtet...

Nach aktueller Einschätzung stellt dies kein echtes Problem dar, sondern ist nur unschön: exzessiv in die Zukunft gesetzte Deadlines bewirken das Belegen vieler dazwischen liegender Slots und belegen einen Teil des »Allokations-Stroms« mit längst schon obsoleten Activity-Daten; beide Effekte verlängern die Liste der aktiven Slots und erhöhen damit den wichtigsten Aufwands-Faktor für die Leistung des Allokators. Allerdings ist mein aktuelles Vor-Urteil, daß der Aufwand des Allokator (und des Schedulers) insgesamt vernachlässigbar bleibt im Verhältnis zu den aktuellen Medien-Berechnungen

Falls es eine zeitliche Grenze gibt, vor der die Berechnungen grundsätzlich nicht starten sollen — beispielsweise weil die Ergebnis-Daten in einen Puffer geschrieben werden, der erst ab einem bestimmten Zeitpunkt verfügbar ist; dies wird dann indirekt codiert durch den Start-Zeitpunkt der Haupt-Berechnung, und das Einschleifen einer Notification auf das direkt nachfolgende Gate, was dazu führt, daß eine extern empfangene Notification von abgeschlossenen Vorgänger-Berechnungen erst nach diesem Zeitpunkt das Gate öffnen können. Diese interne Freischaltungs-Notification wird direkt hinter dem Ankerpunkt (dem POST) eingehängt, verwendet aber ansonsten die vorhandenen Mechanismen (indirekte Aktivierung über den Dispatch-hook)

aus dem Term können die

Instruct-Daten gewonnen werden

nur Activities, die tatsächlich gescheduled werden müssen

Builder-Operationen können auch nachher

(in Grenzen) noch stattfinden

das Job-Planning „muß wissen was es tut“ — typischerweise müssen alle Zeitfenster hinreichend weit in der Zukunft liegen

nach vorläufiger Analyse: harmlos

wenig Raum für Diskussionen — und zwar wegen der Memory-Allokation

Zunächst hatte ich auf abstrakter Ebene festgestellt, daß „zur Aktivierung stets ein Zeitfenster gegeben sein muß“ — und beschlossen, dies „kontextuell“ bereitzustellen. Inzwischen hat sich aber herausgebildet, daß nur die allerwenigsten Activities tatsächlich vom Scheduler aktiviert werden; es könnte sein, daß der Scheduler ausschließlich POST-Activities handhabt, welche ohnehin das Zeitfenster explizit als Parameter transportieren. Allerdings wird das λ-post  in verschiedenen Fällen aufgerufen, und zwar mit POST-, GATE- und NOTIFY-Activities.

Die GATE-Activity definiert auch noch einmal eine Deadline; welche Angabe gilt dann, und unter welchen Umständen? Kann es hier Widersprüche geben, und damit eine Präzedenz?

wenn es zur Ausführung kommt: when ≡ now

Ein λ in den Scheduler binden?

Verwaltung des GroomingToken

pass on the activation down the chain

skip rest of the Activity chain for good

nothing to do; wait and re-check for work later

obliterate the complete Activity-Term and all its dependencies

abandon this play / render process

....denn die Implementierung des Notification-Dispatch erkennt, wenn eine Notification das Gate öffnet, und dispatched in diesem Fall den Chain sofort zur Ausführung und sperrt dann auch das Gate endgültig; das bedeutet: wenn die Aktivierung über diesen Mechanismus erfolgt, wird ein geplanter re-Trigger niemals zum Zug kommen (sondern dann stets ein bereits gesperrtes Gate vorfinden

entscheidendes Kriterium: was ist der Regelfall?

Hier keine zusätzliche Steuer-Logik einführen!

die Zeitplanung muß eben gut genug sein!

Während einer Wartezeit auf re-Test kann nicht erkannt werden, ob der Nachfolgejob vielleicht inzwischen schon arbeiten könnte; somit besteht einerseits die Gefahr, Ressourcen zu verschwenden (re-Test Activities müssen von Workern im Management-Modus (single-threaded) abgearbeitet werden), andererseits Ressourcen ungenützt zu lassen (Worker könnte sich schlafen legen, obwohl der wartende Nachfolge-Job bereits ausführbar wäre und genau von diesem Worker behandelt werden könnte. Das ist ein klassischer Zielkonflikt, und läuft auf die Forderung hinaus, nur so häufig zu prüfen, wie die Engine im Schnitt ohnehin freie Kapazität hätte. Ansatz: ∅ Job-Zeit geteilt durch Zahl der Worker. Und das dann noch mal zwei oder drei, um etwas Puffer zu schaffen...

nur wenn dadruch das Latch ⟼ 0 geht

indem die deadline ≔ Time::MIN

bewirkt anderswo einen Trigger

wenn wir nun unmittelbar re-dispatchen,

könnte der chain zweimal aktiviert werden

Das wäre dann eine sehr spezielle Design-Entscheidung: nicht eine passiver Threadpool, dem von einem aktiven Manager aus die Aufgaben zugewiesen werden, sondern aktive Worker, die sich selber regulieren und Arbeit holen

stabile Benennung: this = ActivityDetector(ID)

wieder mal das lästige Problem mit den variadischen Templates: der Argument-Pack ist selber kein Typ, sondern man kann darauf nur matchen

...auch wenn's bisher bloß der Test-Unterstützung dient, es ist in jedem Fall gerechtfertigt, einen generischen Erweiterungspunkt zu haben

geht leider nicht anders, weil ich mich für eine generische (concept-artige) Form des Execution-context entschieden habe; eine virtuelle Methode kann selber kein Template sein, und andererseits möchte ich den Activity-Record selber template-frei halten

einfache Lösungen wie State-Flags oder Checksummen genügen nicht, denn es muß stets geprüft werden, daß mehrere Vorgänge insgesamt stattgefunden haben, und das in der richtigen Reihenfolge...

analog zu dem Muster für das allgemeine EventLog....

...das war vom Design her nicht vorgesehen, und wäre auch nicht ganz einfach zu realisieren; man müßte den EventMatch per CRTP definieren (und damit müßte er Header-only sein, mit den bekannten Folgen für die Größe des Debug-Build)

...denn dies hier ist ein geschlossenes Ökosystem; daher ist sichergestellt, daß *this stets ein ActivityMatch ist

CHECK (not ... ) führt zu irrelevanten Fehlermeldungen im Output

....weil ich seinerzeit Diagnose-Ausgaben per Seiteneffekt eingeführt habe — sowas ist unabdingbar zur Fehlersuche, aber lästig, wenn es aus einem negierten CHECK resultiert

...allerdings liegt das an der Natur der logischen Negation selber, nicht an der unterstützung im Verifkations-Framework; ein Check ist eine Existenz-Aussage, und daher müßte zur Negation eine All-Aussage geprüft werden, durch eine erschöpfende Suche aller möglichen alternativen Prüf-Ketten. Anfangs habe ich das nicht gemacht, aber 9/2018 habe ich richtiges Backtracking eingebaut; damit sollte das nun korrekt funktionieren (was ich aber nie abschließend verifiziert habe, nur durch einzelne Testfälle geprüft)

das Schema dafür wurde im EventLog bereits geschaffen

ein Funktionsaufruf (typischwerweise von einem rigged-Functor)

Der Ansatz, alles ein einen Prüf-Aufruf zu packen, ist insgeseamt ungeschickt; genau deshalb wurde doch für das EventLog dieser komplizierte Builder / Verfeinerungs-Ansatz gemacht: darüber kann man ganz natürlich ausdrücken, wenn man etwas über die Argumente speziell geprüft haben möchte, oder eben nicht...

detector.verifyInvocation ("funny").arg(-rnd)

Log-Ausgabe: FAILED to match-arguments(4294965953)

(man kann per verifyInvocation prüfen)

kann (optional) die Aktivierung an ein dekoriertes Subjekt weitergeben

das heißt, die ActivityProbe wird dem zu beobachteten Activity-Record vorgeschaltet; man muß dann auch die Verdrahtung enstprechend anpassen, so daß die Aktivierung durch diesen Dekorator hindurch erfolgt

es ist noch gar nicht klar, was für Verifikationen wirklich benötigt werden

möglicherweise geht es hier vornehmlich um die Aktiviertung  (den Fakt des Aufrufs)

kann ja normalerweise leer bleiben (es ist ja nur ein Test)

...weil es eine eigens aufgerufene Funktion ist, die ich auch für alle drei Spezialisierungen sinngemäß anders implementieren kann

Ein Problemfall wäre, wenn eine Funktion einen Rückgabetyp hat, der nicht default-konstruierbar ist. Ja, sowas kommt vor — aber ich betrachte es hier nun wirklich als die Ausnahme, und es gibt immer noch den Workaround, in einem solchen Fall eben den gewünschten Rückgabewert explizit zu setzen (oder gleich die λ-Impl zu verwenden).

bisher gab es nur einen Testfall, und der betraf den ActivityDetector selber

passende ID hierfür vergeben: afterGATE

...für den internen Memory-Pool. Der kann zwar wachsen (was dann aber ggfs. blockt). Und: der Pool schrumpft nie!

...man könnte ja auf die Idee kommen, daß sich der Scheduler diese Zeit aus dem Activity-Record holt; das wäre aber eine extrem schlechte Idee, denn es würde die ganze Einteilung in Schichten hintertreiben.

  • der Layer-1 soll nur seine low-Level Funktionalität machen
  • der Layer-2 soll nur seine Entscheidungen treffen
  • die Activity-Language ist allein für die Bedeutung der Activity-Parameter zuständig

wird das hier auf Layer-1 realisiert —

oder bietet Layer-1 einen Direkt-Eingang?

single level of abstraction / do only one thing and do it well

...es handelt sich offensichtlich bei Layer-1 um eine low-Level-Einrichtung und um ein internes API, das nicht gegen Mißbrauch gewappnet sein muß

Beispiel: __throw_if_empty()

und sollte nur in Frage gestellt werden, sofern dafür explizit  ein unverhältnismäßiger Overhead nachweisbar ist...

↯ eine Standard-Lösung impliziert auch ein Ausführungs-Framework

die Limitierungen und der Threadpool waren gut gemeint

...wenn es sich ohnehin nur um eine Einrichtung innerhalb einer bestimmten Applikation handelt, dann bleibt lediglich die Frage übrig, wohin der Quell/Objektcode gepackt wird — wenn man ein bestimmtes Feature braucht, kann man es implementieren (und die bestehende Lib-Implementierung kann das nicht verhindern)

Da Library-Funktionen ohne Weiteres genutzt werden, verleiten solche eingebaute Hürden ehr dazu, schmutzige Workarounds zu machen

Sprachmittel sind stets aus gutem Grund da. Entweder es gibt aktuell adäquaten Nutzen, oder es gab früher mal einen relevanten Nutzen. Daher stellt es eine Kompetenzüberschreitung dar, wenn jemand rundheraus behauptet, es sei falsch dieses Sprachmittel zu verwenden. Wenn man einen Anfänger ohne ausreichende Kompetenzen entlasten möchte, oder wenn man unter Einschränkung ein bestimmtes Nutmuster vorgeben möchte, dann baut man ein Framework, und keine Library.

Um das klar zu sagen: zwar hat Christian ein Verbot von »join« angedacht, aber grade in dieser Frage war und bin ich absolut einer Meinung mit ihm. Tatsächlich war Christian sogar zurückhaltender („man sollte es deprecaten“) — und das hat mich  dann auf die Idee gebracht, einen separaten »Joinable«-Wrapper zu schreiben. Und jetzt, viele Jahre später stehe ich an dem Punkt, daß es kein absehbares Usage-Pattern gibt, und mein Design von damals ungerchtfertigt erscheint.

in allen sonstigen Eigenschaften hatte unsere Library-Lösung genau das Design gewählt,

das sich nun auch im Standard als angemessen herausgebildet hat —

aber die Standard-Lösung ist ausgereifter und verwendet moderne Sprachmittel (Atomics)

sonst könnte der Fall nicht zuverlässig getestet werden, daß der Test-Runner selber kurz nach dem Start stirbt

ein Spinlock sollte man nur verwenden, wenn man sicher sein kann, daß man (a) fast nicht warten muß und (b) die CPU für sich hat. Andernfalls kann das schlimmstenfalls zum Verhungern des ganzen Systems führen...

Mutex + ConditionVar ist auch gar nicht so schlecht (aber noch schwergewichtiger)

Der Sprachstandard garantiert bereits, daß der Thread startklar ist, wenn der std::thread-Konstruktor verlassen wird. Allerdings kann es immer passieren, daß dummerweise der OS-Scheduler grade Anderes mit dieser CPU vor hat — und genau dann laufen wir in die Situation, vor der diese Sync-Barriere schützen soll; d.h. spätestens nach einer Schedule-Runde sollten alle Threads einmal vorbeigekommen sein.

Wiederverwendung nach automatischem Reset nach Erfüllung ist in manchen Situationen verdammt tückisch; ohnehin stellt ein Latch stets eine Deadlock-Gefahr dar.

...darauf läuft es nämlich hinaus; wenn wir tatsächlich auf C++20 sind, sollte die genaue API-Semantik und das Laufzeitverhalten bewertet werden, um zu entscheiden, ob beide Lösungen äquivalent sind...

Letztlich eine Entscheidung nach Bauchgefühl und ohne konkreten Anhaltspunkt; die Entscheidung kann erst überprüft werden, wenn es belastbare Einsatz-Szenarien gibt

...trotzdem war ich überrascht, um wie viel langsamer sie ist; das kann ich mir eigentlich nur dadurch erklären, daß die Threads in einen Schlafzustand versetzt werden, ggfs auch bereits schon beim Versuch, die exclusive Zone zu betreten. Möglicherweise dauert es auch grundsätzlich länger, bis ein schlafender Thread überhaupt wieder aufgeweckt wird. Die Progression scheint allerdings linear in der Zahl der Threads zu sein, während die Atomic-yield-Implementierung etwas überproportional langsamer wird. Das ist jetzt aber mehr Intuition, denn jenseits von 8 Threads gibt es ja zunehmend Stau im OS-Scheduler

Zum einen handelt es sich um bekannte Erfahrungswerte, zumindest für die Fälle mit wenigen Threads: daß zwei zusammen gestartete Threads um bis zu 0.5µs auseinanderlaufen, ist erwartbar, wesentlich mehr aber nicht ohne Weiteres. Und dann läßt es sich bestätigen, indem die Implementierung versuchsweise auf busy-wait umgestellt wird ⟹ für kleine Anzahl Threads bleiben die Meßwerte nahezu unverändert (sie sind minimal schlechter, aber das System geht auch in Vollast). Das bedeutet: die beobachteten Werte stellen bereits nahezu optimales Verhalten dar, für kleine Anzahl Threads.

solange sie noch auf den alten Code gehen, der den Threadpool voraussetzt (welcher aber für Library-Tests nicht zugelinkt wird)

Denn wir betreiben hier nun keine System-Einrichtungen mehr, keinen Resource-Collector und keinen Threadpool. Es handelt sich lediglich um Adapter über der Std-Lib

 * thread_loop: ensure no error remains pending.

 * trigger application shutdown in case of unrecoverable errors

 * lumiera_threadpool_acquire_thread: error handling (in collaboration with the resourcecollector)

 * lumiera_thread_sync: error handing, maybe timed mutex (using the threads heartbeat timeout, shortly before timeout)

Dieser Test ist zwar pfiffig geschrieben, aber er testet eigentlich den Sync/Monitor, nicht den Thread-Wrapper. In der aktuellen Form ist er sogar nahe an der Thema-Verfehlung, weil überhaupt nicht getestet wird, daß mehrere Threads gestartet wurden und gelaufen sind

...indem man testet, daß eine bestimmte Berechnung stattgefunden hat

Da ich jetzt einen einfachsten Fall habe, kann der eigentliche Test doch wieder etwas komplexer sein...

  • in jeden Thread ist eine zufällige Verzögerung eingebaut (1µs < 1ms)
  • mache ein Benchmark über das Starten-warten-Prüfen
  • die durchschnittliche Zeit muß kleiner sein, als die sequentielle Ausführung aller durchschinittlichen Wartezeiten

### Lumiera halted due to an unexpected Error ###

+++ Caught Exception LUMIERA_ERROR_INVALID:invalid input or parameters

...das ist interessant, aber nicht kritisch; und zwar weil ich den Test jetzt umgeschrieben habe auf ein Lambda, und gar keine eigenständige Klasse mehr verwende — viel spannender ist, daß der C++ - Compiler überhaupt schafft, solchen Code zu „knacken“

nicht mutmaßen — messen!

...was mich nun schon mehrfach verwundert hat; aber letzten Endes habe ich bisher noch nicht viele Performance-Tests gemacht, weil sie sozusagen mühsam sind: Sie kosten Laufzeit in der Suite, sind aufwendig einzurichten, und es ist schwer, eine Testbedingung zu finden, die auch in Debug-Builds zuverlässig geprüft werden kann. Die einzigen Tests, die bisher massiv multi-thraded testen, sind noch aus der Anfangszeit, und direkt gecodet. Insgesamt hat sich dieser Header aus anlaßbezogenen Testaufbauten entwickelt, und es gäbe noch einige weitere Stellen, wo man eine direkt gecodete Test-Loop dadurch ersetzen könnte. Bisher war nämlich auch ein Hindernis, daß Thread-bezogene Hilfsmittel erst in »Core« verfügbar waren, nicht in »Lib«

Denn der neue Thread-Wrapper ist noch nicht da — dafür brauche ich ja grade die SyncBarrier, die hier zu testen wäre. Und die bestehende Implementierung verwendet noch das alte POSIX-basierte Framework, was direkt an den Threadpool geknüpft war, und deshalb eigens als ein Subsystem gestartet werden muß; daher konnte dieser Header bisher auch nicht in Lib-Tests zum Einsatz kommen

  • 1 Thread 100ns
  • 2 Threads = 400ns
  • 100 Threads ⟶ 20µs
  • 1000 Threads ⟶ 250µs
  • 2000 Threads ⟶ 500µs

Nanos wären die natürliche Skala für moderne PCs

sollte das mal wirklich zum Problem werden: man könnte den SCALE-Parameter als letztes default-Argument durchgeben

für den BlockFlow-Test habe ich das definitiv gebraucht, um damit eine »Zeitachse« zu konstruieren; und auch für multithreaded-Tests ist das innerhalb des einzelnen Thread durchaus sinnvoll (⟹ siehe SyncBarrierPerformance_test)

es ist ja ein einziger Zufallszahlengenerator, und es wäre eine schlechte Idee, wenn die Stdlib das nicht gegen concurrency schützen würde

Messungen(Release-Build)

  • die Werte sind zwar verdächtig klein, aber stabil.
  • habe zum Vergleich einmal den testSubject(i)-Aufruf in der Schleife auskommentiert ⟹ Werte um > Faktor 10 kleiner, und fluktuieren stark
  • es ist wichtig, keine Konstante aus der Schleife zurückzugeben (sondern die Index-Variable). Mit Konstante verhält sich die Schleife wie leer!

...die führen dann nochmal zu um den Faktor 10 größeren Werten (was mit meiner Erfahrung konsistent ist).
Daher erscheint die aktuelle Lösung als optimal: wir zwingen den Optimiser, die Schleife auszuführen, weil ein Wert berechnet wird; dieser greift aber nur auf eine Variable in der Klasse zu, und muß nicht atomar, volatil oder synchronisiert sein. Mit diesem Setup kann man also auch den Einfluß von Atomic-Zugriffen noch gut messen

wir messen, wie lange ein Thread im Durchschnitt baucht, bis er sich via SyncBarrier mit den anderen Partner-Threads synchronisiert hat. Dieser Wert ist nicht deterministisch, da die zeitliche Lage der Threads zueinander nicht deterministisch ist. Wir können aber auch nicht anders messen, da der Thread typischerweise in der sync()-Funktion blockt.

⟹ wir beobachten die Barriere bei ihrer bestimmungsgemäßen Arbeit

⟹ wir bekommen so nicht den Implementierungs-Overhead  zu fassen

...denn die Fehler sind nicht Teil des Tests, sondern »könnten« nur auftregen (wenn irgendwo in der Applikation was faul ist)

Theoretisch sollte sie sehr wohl nötig sein, da wir hier einen TypedCounter initialisieren, und das bedingt globales Locking; d.h. die weitere Initialisierung im ctor eines Threads kann durch einen anderen Thread aufgehalten werden.

...denn das sollte sich nur während der Konstruktoren auswirken, und die sind ja alle "durch", gemäß Barriere

dafür braucht man heutzutage nun wirklich kein Lock mehr

aber hier schläft der Dispatcher-Thrad gar nicht — sondern wartet auf das Lock

⟹ der Test-Controller-Thread kommt gar nicht dazu,

auf das Ende der Worker zu warten

ein verschleppter Error-State

...und zwar an der einzigen Stelle, an der das zuverlässig möglich ist: im Konstruktur von Exceptions

Debugger ⟹ Lock-Guard-Destruktor wird nicht aufgerufen

die Exception fliegt ja aus dem Konstruktor vom Guard

(ja dann KANNs ja gar nicht funktionieren)

das Problem ist entstanden weil der Monitor „vorsorglich“ den Error-State auswertet,

dann aber nicht klar ist, wie mit einer solchen Situation umzugehen ist.

Des weiteren ist nicht klar, was die Kopplung zwischen Exception und Error-State soll

die Gleichwertigkeit von Error-Flag und Exception wird nun in Frage gestellt

Plan: künftig sollen Error-Flags nur noch aus C-Code stammen

solange ich nur Detail-Komponenten gebaut habe, konnte ich von komplett definiertem Kontext (Unit-Test) ausgehen; damit waren Exceptions vor allem etwas, was man pro forma noch mit einbaut, aber letztlich nur „über die Mauer wirft“

die verschleppte Exception ist aber nur der Anlaß

der Grund ist ein Bug im Objekt-Monitor

CHECK: session-command-function-test.cpp:425: thread_1: perform_massivelyParallel: (testCommandState - prevState == Time(expectedOffset))

...denn der zielt auf eine echte Laufzeit, die benötigt wird um 500 Loop-Durchgänge zu machen

würde es nicht genügen,

einen einzigen Kontext concurrent zu testen?

...das bedeutet, es wird erst zur Laufzeit bestimmt, in welcher Reihenfolge die Counter für die Typen alloziert werden; damit kommt es zu Beginn zu einer aggressiven contention auf slot<X>. Einschränkung: in dieser Form wirkt dieser Test nur beim ersten Lauf innerhalb einer Programm-Instanz, weil danach die Slots belegt sind. In der Praxis stellt das keine Einschränkung dar

...denn es muß eine hohe Wahrscheinlichkeit geben, daß gleichzeitig zwei Threads auf den gleichen Counter zugreifen ⟹ es muß deutlich mehr Threads geben als counter. Das ist aber schwierig, weil die Zahl der Cores beschränkt ist. Hier hilft nur (a) sehr viel zu viele Threads verwenden und (b) diese lang laufen lassen.

das heißt ich muß keinen abstrakten Typ mehr konstruieren und daraus abgeleitete Dummy<i>, weil letzten Endes nur zwei Operationen notwendig sind: den Zähler inkrementieren und am Ende den Zählerstand auslesen

das gilt aber nur, wenn nicht bereits summAllCounters() aufgerufen wurde!

das ist jetzt alles keine Verbesserung

sie sieht bloß »umständlich« aus

waitGracePeriod: Thread 'Lumiera_Session' failed to terminate after grace period.

warum ist das so?

ist das sinnvoll?

in beiden Fällen wird die Barriere als erstes aufgerufen

  • im Haupt-Thread wird das Thread-Handle als Letztes konstruiert — und da gibt es die synchronizes-with - Garantie
  • im neuen Thread passiert außerdem nur die Konstruktion eines noch leeren Fehler-Strings — alles danach ist per try-catch gesichert

SteamDispatcher ist ein Service und wird in lib::Depend gemanaged. Derartige Singletons werden zwar irgendwann in der Shutdown-Phase bereinigt, aber normalerweise treten wir erst in die Shutdown-Phase ein, nachdem allen Subsystemen zumindest ein Shutdown signalisiert wurde.

...man bekommt das nun automatisch, für die andere Lösung müßte diffiziler Code geschrieben werden, der die Gefahr bringt, den Shutdown-Vorgang insgesamt zu blocken. Oder man müßte ein »try-lock« machen mit Timeout; das ist noch gar nicht auf das API herausgeführt, wäre also noch mehr komplizierter Code. Und würde letzten Endes doch nicht verhindern können, daß genau in den problematischen Fällen der Thread nicht aufwacht/nicht reagiert und dann doch noch die Applikation terminiert wird

außerdem ist das thread-Objekt (nach dem Refactoring) nur noch Member

zudem muß zwingend detach() aufgerufen werden — hier  und nicht im Destruktor

Und zwar addressiert das hier das subtile Problem, daß der umschließende Kontext auch auf außergewöhnlichem Weg und vorzeitig zerstört werden kann, obwohl der Thread noch läuft. Daher ist es wünschenswert, daß der Destruktor bei noch laufendem Thread die Applikation hart terminiert

es ist hier sinnvoll, direkt in der Thread-Funktion eine dedizierte Fehlerbehandlung vorzusehen — auch schon im Vorgriff auf weitere Entwicklung

das heißt, er ist nicht zu verwechseln mit dem atExit-λ aus dem Thread-wrapper Framework

es muß in keinster Weise noch extra per Lifecycle-Hook implementiert werden, sondern entspricht genau dem Verhalten, das die PolicyLaunchOnly ohnehin bietet

ziemlich dämliche Idee, einen Worker nicht »Worker« zu nennen

nicht mehr verwendeter Code

  • aus namespace vault
  • Support-Library

da ich jetzt den Scheduler entwickle

die Zukunft liegt aber bei IO_URING

Das Konzept escheint mit mehr wie ein »das könnte vielleicht funktionieren«-Traum. Es wurde nie auch nur ansatzweise geklärt, wie man einerseits Performance durch Ressourcen-Verschwendung gewinnen kann, dann aber im Notfall im Stande sein sollte, diese verschwendeten Resourcen wieder einzusammeln. Und nicht nur das; es wurde nie gezeigt, daß ein solches Konzept zu einem stabilen Betriebszustand führen kann — tatsächlich wäre zu befürchten, daß das regelmäßige Einsammeln von Resourcen die Applikation sogar ausbremst. Als Beispiel führe ich die allseits bekannten Probleme mit Garbage-Collection an; man bedenke nur, was inzwischen alles an Komplexität in den Java-GC eingebaut wurde, um ihn auf ein gutes Performance-Niveau zu heben

Tatsächlich laden wir nichts aus dem geplanten Config-System (sondern aus der setup.ini).

Habe grade geprüft: selbst der Plugin-Loader hatte zwar eine Abhängigkeit, verwendet dann aber hart gecodete Werte.

ALLERDINGS wird lumiera_config_setdefault() verwendet, um den Wert aus der setup.ini zu injizieren

lumiera_config_setdefault() wird weiterhin im Plugin-Loader gebraucht

Es handelt sich um ein eigenständiges Feature, nämlich daß ein wait abgebrochen wird

  • wir haben dafür wenige eche use-Cases in der Codebasis (Emergency-Shutdown und Builder-Timeout im Steam-Dispatcher)
  • für feste Verzögerungen gibt es inzwischen std::this_thread::sleep_for
  • Angleichen an das C++ API ⟹ eigenständige Funktion wait_for()
  • Rückbau der dynamischen Änderbarkeit per setTimeout()

...weil es im Konstruktor steht, würde ein Fehler hier den Konstruktor als gescheitert klassifizieren; dadurch würden zwar die Teil-Objekte abgebaut, aber der Objekt-Destruktor nicht aufgerufen. Daher

  • das lock() steht ungeschützt ⟹ wenn es scheitert muß auch kein unlock() folgen
  • dagegen das wait() wird geschützt ⟹ bei Exception explizit unlock() und re-throw

Der Test setzt das Locking zwar erheblich unter Druck, aber er ist so komplex, daß man sich einarbeiten muß, um ihn überhaupt zu verstehen

das bedeutet, daß die Work-Function bereits auf Typ-Ebene definiert sein muß; eine Möglichkeit ist, sie über eine Config-Template-Basis einzumischen

klinkt sich vor Beenden aus: thread::detach()

Entsprechend dem Design-Prinzip für die Scheduler-Steuerung möchte ich einen aktivenen Management-Thread und eine Zuweisung von Aktivitäten vermeiden

Wir vermeiden Kontrollverlust durch einen Fehler im zentralen Steuer-Thread, Verklemmungen oder Split-brain

das bedeutet auch: sein dtor muß blocken (oder laufenden Threads einfach abwerfen)

...man könnte auch sagen, das ist eine Frage der Logik: die WorkForce ist allein, was den Scheduler aktiv macht. Deshalb sollte der dieser Status nicht woanders sein....

Damit ist gemeint,die Behandlung eines Belanges an einer Stelle im Code zusammenzufassen. Da die Ausführungs-Logik für Activities ohnehin im Layer-2 liegt, und dort auch die Rückgabewerte generiert werden, welche letztlich die Worker steuern, würde man dort auch ohne Weiteres eine Shutdown-Flag mit unterbringen können; dafür spricht auch, daß der Zugriff auf diesen Steuer-Code über eine (verdeckte) Rück-Referenz über die Lambdas in den einzelenn Threads erfolgen muß — und diese Rückreferenz zeigt ohnehin schon auf Scheduler-Layer-2, jeder andere Ort wäre extra zu realisieren...

...denn ohne eine robuste Schutzmaßnahme bleibt nun jeder fehlerhafte Test einfach hängen, sobald die Test-Funktion verlassen werden soll, denn der Destruktor von WorkForce muß auf die laufenden Threads joinen. Das muß er auch schon deshalb, weil die laufenden Threads noch die in den Lambdas gebundenen Resourcen brauchen.

...und zwar der dtor von WorkForce selber, weil jede andere Lösung fragil und nicht-offensichtlich wäre....

...es wäre denkbar, daß es sich nicht um einen KILL-Switch handelt, sondern um einen Heartbeat, der z.B. aus den tick()-Aufrufen erneuert werden muß, so daß im Fall einer Verklemmung sich der Scheduler selbst terminiert

  • der Exception-Handler in einem Worker-Thread muß diese Flag setzen können; dieser Handler kann jedoch jederzeit greifen (nicht nur im Managment-Mode)
  • die Implementierung der Work-Fun im Scheduler-Commutator muß darauf zugreifen, um dann den enstprechenden Rückgabewert zu liefern, der die Worker terminiert
  • entweder ein terminierende Thread löscht sich selbst aus der Storage (gefährlich)
  • oder es hängen bereits terminierte Threads in der Storage, und jede Kapazitäts-Ermittlung muß O(n) Überprüfungen machen

lambda in local class 'vault::gear::test::WorkForce_test::simpleUsage()::Setup' cannot capture variables from the enclosing context

...das könnte zwar ein statischer Service sein, der dann sogar im Lebenszyklus der Applikation irgendwo konfiguriert wird

und in jeden zu startenden Thread kopiert

C++ zwingt uns dazu, explizit das zu tun was ohnehin getan werden muß; da jedoch der Typ der Config per Template-Parameter gewählt wird, ist komplettes Inlining möglich; letztlich wird daher nur ein Pointer auf das Scheduler-Objekt in alle Threads kopiert — exakt das  was wir brauchen

  • Exception wird gefangen im WorkForce-dtor
  • mit Debugger nicht reproduzierbar

Vermutung: zwischen joinable() ≡ true und dem join()-Aufruf hat sich der Thread selber detached()

  /* Is the thread joinable?.  */

  if (IS_DETACHED (pd))

    /* We cannot wait for the thread.  */

    return EINVAL;

entweder ein Thread soll gereaped werden, oder ein Thread verschwindet einfach von der Bildfläche

ja — und zwar über den Scheduler selbst

per Seiteneffekt erlangt

per Prozeß freigegeben

die Verzweigung ist essentiell

das bedeutet, der Check muß darauf hinauslaufen; es genügt nicht, bloß irgend einen Wert vom Guard gelesen zu haben, sondern man muß sicherstellen, daß der gelesene Wert tatsächlich aus derjenigen Operation stammen muß, welche auf der anderen Seite (beim Producer) als letztes nach der Manipulation der Speicherinhalte stattfand...

  • andere Operationen-A sequenced-before release
  • acquire sequenced-before andere Operationen-B
  • ⟹ Operationen A  happens-before  Operationen-B

...sie ist entstanden aus den ersten Analysen, als die Bedeutung der »Activity-Language« noch nicht geklärt war

wir sind hier in einer performance-kritischen Zone; insofern kein std::optional<Activity&>

man könnte es hier auch freigeben; das würde dann aber im Regelfall ein weiteres mal geprüft und freigegeben; zudem haben wir hier auf dieser Ebene auch kein try-catch

Das bedeutet: der Rückgabewert ist ein Kompromiß, und so gestaltet, daß er in beiden Nutz-Szenarien kein Fehlverhalten verursacht. Er ist damit keine Ausführungs-Steuerung, aber signalisiert Fehlersituationen

activity::PASS ≡ alles gut … weitermachen

activity::WAIT ≡  … aktuell nichts mehr zu tun

activity::HALT ≡ oh weh … bitte anhalten

versuche GroomingToken zu erlangen (Seiteneffekt)

...weil im normalen Control-flow direkt danach die Activity in die Queue gestellt wird — und da können wir eine signifikannte Beschleunigung erziehlen wenn wir das GroomingToken halten und deshalb nicht durch die Instruct-Queue gehen müssen

...und zwar, weil sie infolge der hier getroffenen Entscheidung unmittelbar in den Dispatch gehen können; wäre das nicht so, dann würde u.U aus der Priority-Queue zunächst eine noch dringendere/frühere Aufgabe entnommen werden.

die Ausführungs-Struktur liegt in den Termen (nicht in der Queue)

Bisher wurde asynchrones IO nur als theoretische Möglichkeit betrachtet, aber noch nicht im Detail untersucht — im Besonderen die Möglichkeiten von IO_URING werden relevant sein

  • wie werden dort Callbacks eingerichtet und von wem aufgerufen?
  • welche custom-Payload kann mitgegeben werden?

wichtig für die IO-Jobs

derzeit genügt der Deadline-Check von Activity::GATE

konkretes operationales Verhalten des Schedulers, der Concurrency- und Worker-Steuerung

Unterscheidung in Aktivierung und Dispatch

d.h. wird stehts von einem Worker ausgeführt, der aktuell das GroomingToken hält ⟹ Race zwischen Inkrementieren und Gate-Check ausgeschlossen

wenn die Nachricht kommt, daß alle Prerequisites erfüllt sind, könnte die Berechnung sofort starten (und den aktuellen Worker nutzen); wenn wir stattdessen nur dekrementieren, verzögert sich die Weiterverarbeitung, bis durch Scheduling oder re-Scheduling das Gate erneut geprüft wird (spinning)

Es ist noch nicht klar, wie die Callbacks von async-IO aufgeschaltet werden

setze aber einen höheren Durchsatz an

(damit das System unter Druck kommt)

»Activity Language« Functionality test

...weil ich damit auch die erweiterten Scheduler-Tests mit abdecken könnte (concurrency)...

...paßt perfekt hier; muß lediglich die elementaren Verifikations-Primitive hier verpacken, um semantisch relevante Elemente direkt zu prüfen

hier ist es nicht sinnvoll, die MockJobs aus dem Dispatcher zu verwenden, und zwar aus zwei Gründen

  • Linking-Dependencies: wir sind in der Vault
  • Verifikation per EventLog ⟷ Verifikation per internem Invocation-Record

POST⟶TIMESTART⟶INVOKE⟶FEED⟶FEED⟶TIMESTOP

gesamte Kette ausführen per ActivityLang::dispatchChain()

Zugriff auf die aktuelle Scheduler-Zeit verifizieren

zwar wird jetzt über ein λ im ExecutionContext zugegriffen, aber da ich keinen Callback im eigentlichen Funktor habe, fehlt die Möglichkeit, diesen Zeitwert auch tatsächlich während der Ausführung der Kette zu variieren — um die dynamische Auswertung im Test zu zeigen, kann aber der Zugriffs-Funktor so manipuliert werden, daß er jedesmal die „aktuelle Zeit“ inkrementiert

Im ActivityDetector sind normalerweise die ctx-λ als reine Logging-Funktionen implementiert; aber die nun konstruierte Ausführungs-Logik setzt fest darauf, daß ein Aufruf von ctx.post() sofort in einen ActivityLang::dispatchChain() umgesetzt wird (allerdings nicht wirklich immer, sondern nur in dem Fall, daß wir das GroomingToken halten und die im post() gegebene Zeit die aktuelle Zeit ist)

gemeint ist: den Aufruf von λ-post könnte man weitergeben an die ActivityLang-Instanz, die im Test-Kontext konstruiert wurde; damit würde also die Prüfung/Steuerung mit dem GroomingToken ausgelassen, aber ansonsten würde dann die Aktivierung direkt an ActivityLang::dispatchChain() weitergegeben.

Wichtige Einschränkung: dieses »Durchreichen« darf man nur machen, wenn der post für „jetzt gleich“ ist, d.h. die im post() gegebene Zeit muß man checken (sonst würde z.B. ein geblocktes Gate, das sich in die Zukunft schedulen möchte, sofort in eine Endlosschleife laufen)

FakeExecutionCtx bzw. die dort verwendete DiagnosticFun, auf die man ja tatsächlich zugreifen können sollte (um den Rückgabewert zu manipulieren) — und das wäre bei einer std::function  nicht mehr möglich...

Testfall-1:

notification ⟼ decr(Gate) ⟼ ✗

Testfall-2:

primary-chain ⟼ notify an Gate ⟼ λ-post ⟼ dispatch ⟼ decr(gate) ⟼ PASS ⟼ Rest der Kette

offen: wie erfolgt hier der »Seed« ?

typischerweise ist das eine Planungs-Activity, also ein activity::Term::META_JOB

...und würde dadurch dann nach dem Meta-Job auch gleich noch den ersten Frame-Job ausführen, was aber genau so auch wünschenswert ist

...was schlimmstenfalls dazu führt, daß er direkt den ersten Planungs-Chunk ausführt

  • ohne Schutz braucht die Operation ∅ 600µs (bei 500µs Schlafpause) und die Checksumme ist viel zu klein (~6000 statt wie erwartet 99000)
  • mit Schutz braucht die Operation im Schnitt 10ms und die Summe 0...N-1 stimmt exakt

aber das ganze Geschehen einmal durchspielen

Activity-Term präparieren wie im SchedulerActivity_test::scenario_RenderJob()

SchedulerActivity_test::scenario_Notification()

Dieses Schema hat grundsätzlich die gleichen Charakteristiken wie ein flacher Kachel-Pool, aber die Cluster-Größe wirkt als Hebel:  zusammengehörige Elemente liegen im Cluster und sind damit cache-freundlicher. Fragementierung findet zwar statt, aber auf Cluster-Ebene; deshalb braucht man auch weiterhin eine Free-List, aber auch diese wird nur einmal pro Cluster aktualisiert.

Am Ende einer Kette von Activities muß eine Spezialbehandlung liegen, die die Länge der Kette weiß; zusätzlich kompliziert wird es, wenn diese Kette nicht in einen Cluster paßt (dann braucht es mehrere Notifications). Im Commutator ist spezielle Logik notwendig, die die Ausführung dieser Freigabe-Benachrichtigungen sicherstellt, damit diese dann ein gemeinsames Cluster-Gate dekrementieren, das in der ersten Allokation des Clusters liegt (und insofern Platz verschwendet)

Komplexe zeitliche Struktur, die auch zwingend zu einem gewissen Leerlauf führt, man braucht also reichlich Speicher. Außerdem ist nicht von Vornherein klar, wie viel Speicher bis zu einer Deadline noch gebraucht wird; man steht dadurch vor der Wahl, entweder Fragmentierungin Kauf zu nehmen, oder eben überschüssige Allokationen der nächsten Deadline zuzuschlagen, wodurch sie dann unnötig lange geblockt gehalten werden müssen

Das führt zu einer Komplikation, da es solche Activities geben wird (Render-to-File, background activities); es muß dann eine pseudo-Deadline eingeführt werde, bei deren Überschreitung ein re-Scheduling in einen neuen Pool erfolgt. Ebenso stellen Deadlines weit in der Zukunft ein Problem dar

man muß für jeden Worker mitverfolgen, ob er die Epoche verlassen hat; das ist so nicht ohne Weiteres möglich, da es Activities geben könnte, die später gestartet werden aber doch eine knappere Deadline haben (und damit noch in eine frühere Epoche fallen würden) — man müßte also warten, bis die Startzeitpunkte hinter der Deadline der Epoche liegen

Allokationen erfolgen single-threaded, unter GroomingToken

ein externer Trigger kann einen »post« auslösen; da ein solcher Trigger auch noch verspätet auftreten kann (nach Überschreiten der Deadline), muß nochmal explizit geprüft werden, ob die Epoche „noch lebt“, bevor man in ihr eine Activity auslöst. Das bedingt aber zumindest noch einen beweglichen Parameter für einen generischen Funktor, der irgendwo gespeichert werden muß (und nicht in der Storage der Epoche selber liegen kann).

...vielmehr muß man den Callback selber „erwischen“ und unschädlich machen, bevor er bereits wiederverwendete Storage anspricht

da Jobs nach Zeit geordnet aktiviert werden, kann man für jeden Worker eine Markierung der aktuellen Epoche erzeugen; sie wäre jeweils zu aktualisieren, wenn ein Worker das GroomingToken abgibt... (und damit in einen Render-Job einsteigt) — mithin ließe sich feststellen, wenn alle Worker eine Epoche verlassen haben.

Es handelt sich hierbei um ein grundsätzliches Problem. Es liegt in der Natur von IO, daß eine solche Operation eine unbestimmte Zeit dauern kann; und diese Zeit kann ganz erheblich sein, wenn das IO-Subsystem überlastet wird. Es gibt keine Möglichkeit, eine IO-Operation abzubrechen; vielmehr kommen die Daten irgendwann an, und landen dann in dem dafür vorgesehenen Buffer. Und solange das nicht passiert ist, muß der Buffer und der Callback im Speicher bereitliegen. Ich sehe keine andere Möglichkeit, als für jede Epoche einen Zähler aller schwebenden IO-Operationen mitzuführen. Mithilfe der »post«, »notify« und »gate«-Activities ließe sich das jedoch single-threaded verwirklichen — Synchronisations-Effekte treten daher nur für Threads, die grade eine IO-Operation abgeschlossen haben, sowie den Thread, der das GroomingToken hält

ein Extent ist ein uninitialisierter Speicherblock

Begründung:

  • Performance...
  • mehr Dynamik wird gar nicht benötigt, da das BlockFlow-Schema noch die Dauer einer Epoche justieren kann

nested Typedef Payload

das heißt: es findet zwar eine default-Initialisierung statt, aber für einen Objekt-Typ mit implizitem default-ctor bedeutet das default-Initialisierung der Member. Nach meinem Verständnis hat std::array einen impliziten default-ctor und als einziges Member ein Array, und dafür wiederum erfolgt dann default-Initialisierung jedes einzelnen Elements. Und da das Element ein base-value (char) ist, erfolgt überhaupt keine Initialisierung.

genau für sowas war der gedacht ☻

‖ StateCore ‖

(Backlink, Index)

Begründung:

  • der direkte Zugang zum Index ist natürlich und leicht verständlich
  • Performance ist gleich (nicht besonders toll aber voll OK) wie eine Pointer-Lösung
  • und mit dem Marker-Wert muß ich dann halt leben; Konstante definieren

Damit vermeide ich, einen speziellen Marker-Wert zu verwenden;

In der Tat darf der Index für einen aktiven und gültigen Extent niemals after_ sein, er muß entweder davor stehen, oder start_ == after_ und der Container ist leer

...da wir den Platz im EpochGuard bereits anderweitig komplett ausschöpfen, muß die ExtentFamily auch beim Navigieren über die Sequenz der Epochen hinweg helfen. Und im Besonderen eine Navigation über das Ende hinaus muß nahtlos eine Erweiterung der Allokation  ermöglichen. Habe mich entschlossen, die Limitierung als Ausnahme/Exception zu definieren, so daß im Regelfall die ExtentFamily einfach immer weiter wachsen soll

...das heißt, es könnte kein »for each« mehr geben, weil die Iteration bis zum Out-of-Memory weiterliefe. Könnte man machen, sollte man aber nicht so machen

stattdessen Allokations-Erweiterung auf dem Iterator anbieten: expandAlloc()

...dieser tauscht per std::swap aus; und zwar rekursiv inkrementell: erst das Segment vom Ende an den Anfang und dann rekursiv wieder std::rotate für den Rest. Komplexität: linear

...selbst wenn es Zusammenhänge gibt — notfalls wird die Aktion in die nächst nachfolgende Epoche geschoben (welche stets länger lebt)

Entscheidung: Allokation entgleist nur ausnahmsweise

jede Deadline vor dieser Zeit ist damit grundsätzlich obsolet

Grundproplem: downcast  Extent   ⟼  Epoch

es ist alles Inline und in einer einzigen Klassenhierarchie, und die Iteratoren sind hier komplett PODs

...das war eine Design-Entscheidung, die im allgemeinen richtig ist (es lohnt sich nicht, auf bound-checking zu verzichten). Aber hier sind wir in einer high-Performance-Zone, und es wäre gefährlich, sich nur darauf zu verlassen, daß der Compiler das per globaler Optimierung noch repariert....

Das ist eigentlich auch logisch-architektonisch sauber so: man bezieht sich sowohl auf den Allokator, als auch auf das Epochen-Management.


Durch einen Kniff ließe sich dieser Doppelbezug jedoch reduzieren, aber nur auf einen Bezug auf den BlockFlow (der dann kaskadierend auf die ExtentFamily rekurieren müßte). Dazu wäre es notwendig, in BlockFlow einen komplett eigenständigen Iterator zu implementieren; zudem müßte man darauf hoffen (⟹ überprüfen), daß der Optimiser diese kaskadierenden Aufrufe für die reinie Iteration komplett auflösen kann — andernfalls hätten wir einen Performance-Nachteil beim wichtigen/häufigen durchsuchen der Epochen nach dem passenden Slot. Daher nehme ich von dieser Lösung Abstand, und bleibe bei den klaren Bezügen.

...denn speziell das Count-down-Feld ist gradezu der springende Punkt beim EpochGate (damit lösen wir das Problem, daß IO-Operationen hängenbleiben können)

...weil ich sonst auch die abgeleiteten Parameter jedesmal wieder neu definieren muß, anstatt für den konkreten Fall nur das zu ändern, was wirklich abweicht; benötigt wird hier also so etwas wie »statische Vererbung«

das wäre das Einfachste, auch zur Nutzung — aber es bläht das Objekt auf (nicht gefährlich, aber unschön) und der Ansatz steht auf Dauer einer zentralen Engine-Konfiguration mit möglicher dynamischer Steuerung im Weg

...und das ist schwieriger, wegen der Initialisierung, die dann constexpr sein müßte — und das verschärft das Problem im Hinblick auf die zukünftige Entwicklung

Das widerspricht aber der Idee einer mehr oder weniger statischen Grundkonfiguration — zudem stellt sich dann die Frage, wie der Zugriff passiert; es besteht die Gefahr, eine tatsächliche Indirektion zur Laufzeit einzuführen (der Overhead wäre vermutlich Gering im Vergleich zur Suche durch die Epochen, aber genau deshalb könnte eine solche Ineffizienz leicht verborgen bleiben)

Die Verwendung wird etwas komplizierter, aber für Tests kann man direkt die Instanz von der Config verwenden und in lokale Variable ziehen. Abgesehen davon gilt Meyer's Singleton als die performanteste Lösung, ein Singleton threadsafe zu implementieren (ca 50% schneller als mit Atomics und double-checked locking). Falls der Konstruktor trivial ist, entfernt der Optimizer die Initialisierung komplett. Es bleibt nur das Risiko, daß diese Optimierung aus irgend einem Grund nicht stattfindet, und dann bleibt der Schaden gering (viel geringer als wenn man versehentlich eine Indirektion reinbekommt)

  • gewichtetes Mittel
  • eigentlich sind es N alte Punkte, aber wir ziehen ja jedesmal einen Punkt ab ⟹ N-1 gemittelte Beiträge

denn sonst könnte es relativ lang dauern, bis sich das System einreguliert, und während dieser Zeit könnte es bereits in Sättigung gehen, und Regelschwingungen produzieren

Blöcke unterhalb dieser Schwelle werden nicht mehr hochgerechnet, sondern mit ihrer aktuellen Länge eingebucht, so als wäreen sie optimal; damit sollte die Regelung bei Leerfallen der Engine auf dem zuletzt eingependelten Wert stehenbleiben...

aber: BlockFlow macht schon verdammt viel für jede Allokation

die erste belegte Epoche ist exterm viel zu lang ⟹ sehr viele Overflows zu Beginn

sie wird für jedes Element aktiviert!

vermutlich findet deshalb auch überhaupt kein clean-up statt

bisher werden stark unterfüllte Epochen als neutral eingebucht

Grundsätzlich ist der Ansatz schon richtig: wenn wir uns weit vom Regelfokus entfernen, dann die Rückstellkraft stärker dämpfen

muß die Dämpfung für extreme Zustände so einstellen,

daß sie etwa eine Größenordnung langsamer greift,

als die charakteristische Regelzeit (≙ Verzögerung)

Feinabstimmung: den Testfall rund laufen lasssen

Die Korrektur bei Overflows darf nur so stark sein, daß die Blöcke während der Vorlaufzeit etwa halbiert werden, aber nicht so stark, daß wir gegen das Limit laufen

  • wenn die Blöcke noch leer sind, kommt es zu Regelschwingungen und es dauert doppelt so lange bis zum stationären Zustand
  • weitgehend volle Blöcke dagegen verlängern die Überlastungsphase unnötig

man müßte ja den shared_ptr direkt in die Activity-Datenstruktur einbetten

das heißt, bei jedem Overflow müssen wir doch einen Ticken stärker die Epochenlänge reduzieren... fragt sich nur, wie stark...?

Auch ein Tiling-Pool-Allocator mit Freelist erziehlt die beobachtbare Leistuntssteigerung vor allem durch die Amortisierung über einen Skalenfaktor. Ich muß hier auf etwas ähnliches ziehlen: möglichst viele Allokationen aus einem bereits etablierten Block, und möglichst wenig Blöcke gleichzeitig

mit loakelm Caching und Optimierungen: 37ns

...allerdings fällt er nicht so stark aus, wie zuerst gedacht — der Effekt wurde vom Effekt der auskommentieren Testfälle überlagert

auch mit weiterer Variation der Parameter kommt man kaum unter 30ns

Begründung: dann ist das Block-spacing initial zu kurz und die Blöcke unterfüllt — von dort kommt man leichter zu einem stabilen Arbeitspunkt

vor allem die 2-Schichtige Regelung, die Übersteuerungen wieder einfängt...

bereits nach 5 sec im lock-step

unter extremer Überlast gibt es einen ungünstigen Mitkopplungs-Effekt

... dann werden extrem viele neue Blöcke hinzugefügt

In diesem extremen Overload gilt: jeder neue Block wandert die Kette entlang und wird „hinten abgeworfen“. Dort hinten ist dann zwar das Spacing bereits klein genug, um die Last aufzufangen — aber vorne besteht noch ein viel größeres Spacing. Konsequenz: während die Einfügeposition vorne immer noch die gleichen alten zu langen Epochen überfüllt, werden hinten permanent neue Mini-Epochen angehängt. Erst wenn auch die Einfüge-Position im Bereich der kleinen Blöcke angekommen ist, baut sich der Rückstau (ziemlich schnell) ab.

nach weiteren 6 Sekunden ist die Regelung locked to target

blockFlow: 32ns

ein POST könnte geplant werden,

aber die angehängten Activities werden

dafür später kopiert (Addresse ändert sich)

Ich gehe davon aus, daß „dieser ganze Scheduler“ so dimensioniert ist und sich selber derart einregelt, daß er mit dem Flow der Berechnungen klarkommt. Aufgrund der dynamischen und offenen Natur dieser Konstruktion kann ich jedoch keine Garantien geben, daß diese Steuerung stets funktioniert. Die Activity-Language schafft ja grade die Möglichkeit, gegenwärtig noch gar nicht vorstellbare Steuerungs-Mechanismen später noch realisieren zu können, lediglich durch eine veränderte Parametrisierung. Aber zumindest ein Szenario des Scheiterns kann aus aktueller Sicht bereits konstruiert werden: und zwar wenn durch eine Überlastung des IO-Subsystems ältere Epochen noch nicht freigegeben werden können, da an ihnen noch Buffer für schwebende IO-Vorgänge hängen. Ein laufend fortschreitender BlockFlow würde dann sukzessive den verbleibenden Arbeitsspeicher belegen und blockieren.

Ein im BlockFlow aktiv verwaltetes Limit für den Allokations-Pool stellt daher eine Sicherheitsbarriere dar, damit das System nicht insgesamt in einen degenerierten und unkontrollierbaren Betriebszustand gleitet (sog. „trashing“). Dem Zufolge sollte ein Auslösen dieser Sicherung nicht in Reparatur-Versuche münden, sondern schnellstmöglich zu einem Scheitern des Playback/Rendervorganges führen

Es ist klar, wenn eine Epoche überläuft, aber in dem Moment ist noch nicht klar, welcher Prozentsatz des Platztes nun noch unterzubringen ist. Umgekehrt ist nicht klar, wann eine Epoche fertig besetzt ist; Hilfsweise könnte man beim Aufräumen prüfen, was aber erst deutlich später passiert — in beide Richtungen ist also die Rückkopplung verzögert und ungenau und kann daher zu Regelschwingungen führen.

damit kann die Mittelung in beide Richtungen um den Optimalwert arbeiten

  • die Überläufe erzeugen einen sofort wirksamen Druck Richtung mehr Kapazität
  • aber eine exponentielle Mittelung wirkt verzögert, jedoch mit größerer Kraft
  • die Mittelung zielt auf 90% Füllung, hat also eine kleine Regelzone, in der sie beidseitig wirkt
  • im Extremfall jedoch wirken Overflow und Mittelwert gegensinning, wobei der Overflow direkter und schneller wirksam wird, um dem System Luft zu verschaffen
  • die längerfristige Regelung jedoch bremst den Overflow mit einiger Verzögerung auch wieder aus; zwar führt das dazu, daß die Überlastung länger besteht, dafür aber auch weicher ausgeregelt wird, wodurch das System anschließend ohne weitere Schwingungen direkt in den lock-step übergeht

Zunächst dachte ich, das ist nur ein Provisorium...

Aber derzeit ist eine volltändige und elaborierte Regelung im BlockFlow selber implementiert, die bereits robust aussieht und auch kurzzeitige Lastspitzen gut abfedert; es erscheint zweifelhaft, ob hier noch eine externe Regelung benötigt wird. Möglicherweise könnte das TimingObservable einen 2. Layer bilden, der die Grundparameter des Algorithmus längerfristig optimiert

Beispiel 128 Cores: dann pollen ununterbrochen andere Threads das GroomingToken; eigentlich sollte das aber kein Problem darstellen (wenn ich recht verstehe, belastet ein »acquire« nur denjenigen Thread, der auf den Wert aus dem allgemeinen Speicher zugreifen möchte; dieser Thread aber wartet hier sowiso)

...ein eigenartiger Widerspruch, den ich nicht auflösen kann...

Absolut ⟹ nicht relativ ⟹ nicht explizit auf etwas bezogen ⟹ also implizit auf „den Nullpunkt“ bezogen ⟹ also relativ zur jeweiligen Timeline

»Deadline« ist immer in realTime

(Deadlines sind danach und wirken daher exclusiv)

symbolische Repräsentation eines dynamisch laufenden Berechnungsprozesses

es enthält keine relevanten Informationen und dient auch nicht dem Tracking; vielmehr ist es ein Schlüssel, mit dessen Hilfe der PlayProcess später mit der Render-Engine reden kann

Es ist im Entwurf von 2011 noch nicht festgelegt, ob diese Implementierung konkret ist (d.h. direkt Elemente aus dem Play-Service verbindet), oder nur abstrakt, durch Aufruf der RenderEnvironmentClosure. Letzteres wäre adäquat im sinne von Inversion of Control

soll genau dann reproduzierbar sein,

wenn das Resultat ebenfalls quasi-reproduzierbar(*) ist

(*) bedeutet

  • effektiv gleiche Wirkung
  • bis auf Rechenfehler identisch

Änderung auch nur eines Teils des Render-Graphen: Hash ändert sich ⟹ Cache invalidiert

hier ist mindestens eine Indirektion notwendig

aktuell passieren sogar zwei Indirektionen

  • Job-Deskriptor ⟼ JobFunctor-Objekt
  • JobFunctor vtable ⟼ Nutzfunktion

wie generisch, ist eine offene Frage, die eing damit zusammenhängt, was tatsächlich durch die PriorityQueue läuft: Wenn nämlich auch schon der Job-Parameter in separater Storage liegt, dann könnte bereits dieser eine Vorstufe zum Funktor sein, und bsp. auch den Einsprung für die passende ProcNode transportieren; in diesem Fall gäbe es dann nur noch ganz wenige, prototypische Basis-Funktoren (Render aufrufen, Daten laden, Netzkommunikation, Verwaltungsaufgaben), und der konkrete Aufrufkontext würde im Parameter stecken — wir hätten dann wieder zwei Indirektionen (Job⟼Parameter und Parameter⟼Nutzfunktion)

welcher Teil muß wirklich flexibel sein?

...denn von den grundsätzlichen Abhängigkeiten her weiß nicht einmal ein JobTicket, in welchem Segment es hängt; auch ProcNodes könnten theoretisch sogar in mehreren Segmenten hängen ⟹ es schreint mehr als fragwürdig, ob ein JobFunktor überhaupt im Stande ist, eine inhaltlich sinnvolle verifikation zu machen (über das Prüfen auf null-Pointer hinaus)

und zwar »point and shot« — es muß genau die Implementierungs-Framenummer sein, die dann für die Verarbeitung wirklich gebraucht wird (und welche ist das?? ... wer weiß das???)

da hinter der Invocation stets ein JobFunctor steht, der über eine Closure Zugriff auf konkrete Kontext-Informationen hat — das einzige Prinzip in der für Lumiera entwickelten offenen Daten-Architektur ist, daß alles zu Berechnende zeitlich eingeordnet werden kann. Damit sollte jedwede Aktivität anhand der Zeitangabe im Stande sein, ihre konkrete Parametrisierung abzuleiten. Das Wichtige dabei: diese Ableitung bleibt ein lokales Detail. Der Planungs-Prozeß muß es überhaupt nicht wissen

da Jobs massiv über mehrere Threads hinweg gereicht werden, könnte der bloße Umstand einer Datenaktualisierung bereits zu Cache-Coherency-Overheads führen

Zeitsteuerung funktioniert am Besten bei eigentlich entkoppelten Aktivitäten, welche ohne weitere Prüfungen und Synchronisationen starten und enden können

Event ⟶ push

Zeit ⟵ pull

weil außer dem zeitgetriggerten Start...

  • noch zusätzlich etwas Externes geprüft werden muß, was Varianz einführt
  • deshalb zwischen Prüfung und eigentlicher Aktion ein künstlicher Sicherheitspuffer liegen muß
  • außerdem noch eine Kommunikation stattfinden muß, schlimmstenfalls über Thread-Grenzen hinweg

...nämlich beim finalen Rendern! Lumiera ist darauf ausgelegt, und grade nicht als Aufführungs-System konzipiert. Und auf der anderen Seite, wenn es um zeitgebundene Wiedergabe geht, fordern wir ohnehin, den Aufwand so sehr zu reduzieren (Proxyies), daß das System weit von Vollauslastung entfernt läuft

Denn stabiler Playback erfordert zwingend zusätzliche Leistungspuffer (sofern auch nur ein kleiner Teil der Pipeline zufällig streut). Daher steht und fällt die Möglichkeit für flüssigen Playback damit, den Aufwand garantiert erheblich unter das Limit zu drücken. Unter diesen Umständen würde meist auch Batching plus Vorlauf mit Datenpuffer genügen

...mehr noch, da letzten Endes die amortisierte Lade-Zeit pro Frame deutlich kleiner sein muß als die Framerate, brauchen wir ehr einen ausreichenden Vorlauf und einfache Datenpuffer — zumal flüssiger Playback ohnehin nur mit kleinen Frames funktioniert, denn die I/O-Bandbreite ist bei Weitem nicht so angewachsen, wie die Rechenleistung

In diesem Fall verschlechtert Zeitsteuerung eigentlich die benötigte Gesamtzeit; die optimale Lösung wäre hier, jeweils parallel mehrere I/O-Pipelines, und dann einige Worker nach best serve-Verteilung laufen zu lassen

praktisch alle Video-Verarbeitung involviert I/O in irgendeiner Form, und muß deshalb auf Events reagieren, dann aber einen nachfolgenden festen Arbeitsaufwand gleichmäßig disponieren

die wirklich recht komplexe Steuerung ist einzig und allein dadurch gerechtfertigt, daß so lange laufende und schwer vorhersehbare Sequenzen überlappend verschachtelt werden können, was bei einem strikten Pipelining oder rein Event-getriebener Verarbeitung nicht möglich wäre; letztere Ansätze funktionieren nur gut bei sehr kleinen Arbeitspaketen oder bei extremer horizontaler Skalierung

das heißt, die Priority-Queue ist das Entscheidende; das Ordnungsmaß darf gar nicht primär als ein Zeitplan verstanden werden, sondern als ein Maß an aktueller Dringlichkeit

Bei einem reinen Pipelining kann es zur Stau und Lastspitzen kommen, und genau dann auch zu Lock-contention. Wir sollten also die zur Integration der widersprüchlichen Anforderungen notwendigen Pufferzeiten als eine zufällige Streuung deuten, die Lastspitzen durch Verteilung entzerrt; sofern nicht in kurzer Zeit Aktionen an mehrere Threads zugleich  weitergegeben werden müssen, treten auch weniger Behinderungen durch Synchronisation auf

Die Implementierung sollte sich von den exakten Zeiten lösen — besser sollte stets mit reichlich Puffer gearbeitet werden, dafür aber auch eine Activity in einer gewissen Toleranzzone um den definierten Zeitpunkt herum gestartet werden können. Dafür sollte im Gegenzug die Menge der jeweils aus der Queue entommenen Activities dynamisch geregelt werden, und stets nur so viel wie gut möglich getan werden. Die concurrency-Struktur sollte zum Leitmaß erhoben werden; idealerweise sollte stets nur ein Thread abnehmen, und auch das nur so lange,  bis er wieder auf eine größere Aufgabe geschickt wird.

Keinesfalls sollte a priori eine bestimmte Thread-Struktur festgelegt werden! Im Besonderen auch nicht die Annahme, daß es einen »Scheduler-Thread« gibt, welcher Aufgaben verteilt. Denn letztlich hängen alle Leistungs-Eigenschaften an der Cache-Locality, und diese läßt sich nur empirisch optimieren, denn es ist klar, daß unser Daten-Durchsatz die Cache-Kapazität insgesamt übersteigt — inwiefern sich eine einzelne CPU für bestimmte Aufgaben überhaupt „warm laufen“ kann, muß sich in der Praxis zeigen; gut möglich, daß I/O-Limitierungen jedweden Cache-Efekt überdecken. Grundsätzlich sollte die Möglichkeit vorgesehen werden, daß jeder Thread die Queue bedienen kann — sowohl eingangsseitig alsauch ausgangsseitig. Natürlich darf es stets nur ein Thread sein, der die Queue bedient; andere untätige Threads legen sich schlafen.

Earliest deadline first (EDF) or least time to go is a dynamic priority scheduling algorithm used in real-time operating systems to place processes in a priority queue. Whenever a scheduling event occurs (task finishes, new task released, etc.) the queue will be searched for the process closest to its deadline. This process is the next to be scheduled for execution.

...und würde dann strikt nach zeit-Ordnung zuerst bedient, und damit dem Realtime-Task die Ressourcen wegnehmen

zwei Queues

reguliert über ein konventionelles Lock, oder (besser) über ein Atomic-Token mit CaS

zu den Steuerungs-Aufgaben gehört gleichermaßen

Activities werden in/aus der Notification-Queue verschoben

is ja eine linked List

Die Koordination von Memory-Management ist ein gefährlicher Verzögerungs-Faktor (weil sie zu einem globalen flush der Cache-Hierarchie führen kann).

Ein Render-Vorgang wurde in die Wege geleitet, aber beim Bereitstellen der Mittel zeigt sich, daß das beabsichtigte Ziel grundsätzlich oder akzidenteller Weise gar nicht erreichbar ist

Ein grunsätzlich bereits komplett vorgeplanter Render-Vorgang stellt wider erwarten fest, daß eine Prüfung oder Annahme falsch war und eine Vorbedingung nicht erfüllt werden kann bzw. ein Arbeitsmittel doch nicht verfügbar ist

Obwohl die Render-Aktivitäten komplett wie geplant ablaufen, ist zum aktuellen Zeitpunkt der vorgeplante Zeitrahmen verlassen, so daß Termine absehbar nicht mehr gehalten werden können

Ein unerwartetes Ereignis mitten in der Berechnung führt zum Kontrollverlust oder korumpiert die bereitgestellten Daten

Im Rahmen normaler Verarbeitung wird ein nebenbei beobachteter Schwellwert verletzt, so daß das Ergebnis — wiewohl formal korrekt — nicht mehr den ursprünglichen Erwartungen genügt; es handelt sich hierbei um ein Ereignis, welches bekanntermaßen mit einer gewissen Wahrscheinlichkeit eintreten kann, beispielsweise um die Verzögerung oder Übertragungsstörung eines Remote-Feed, oder um eine limitierte Resource, die in geringerem Maß oder erst später als erwartet zur Verfügung steht

tatsächlich ... wenn die Datenfelder base values sind

Die Methode steht und fällt damit, daß die Tangente näher zur Lösung zeigt, als der aktuelle (Start)Wert. Sonst kann die Iteration in weite Oszillationen münden und sich ggfs sogar in einem neben-Minimum festbeißen. Die Newton-Methode spielt daher ihr Potential einer senationell schnellen Konvergenz nur aus, wenn man anderweitig schon eine sehr gute Näherung als Startwert hat.

saugeil

das Konzept gibts nicht

GTK-Widget: hide()

GObject: unref()

...in der Praxis aber entsteht indirekt eine Auswirkung,

insofern Container auf das DESTROY-Signal ihrer Kinder reagieren

Widget berechnet adjusted allocation

...also abzüglich Dekoration und Margin

...was der Fall sein kann in einer Box oder einem Grid,

sofern das Widget mit entsprechendem Modus eingefügt wurde

"Widget is in a background toplevel window"

Adwaita ist leider direkt in GTK integriert und ist ein aufgeblähter Wust an SCSS-generierten Regeln, die dann aus »performance-Gründen« (d.h. aus Dummheit und Arroganz) binär compiliert und eingebettet werden müssen.

geläufiges
Farbschema

Es war und ist immer noch üblich, daß jedes Theme einen Satz von Basis-Farben definiert. Auch Adwaita macht das aktuell so (wenngleich es den Gnome-Leuten lästig ist, und sie immer wieder darauf hinweisen, daß das alles "difficult" und "challenging" ist. Sprich, man will raus aus der Nummer, aber der Widerstand der Nutzer ist zu stark)

widget text/foreground color

text color for entries, views and content in general

widget base background color

text widgets and the like base background color

base background color of selections

text/foreground color of selections

base background color of insensitive widgets

text foreground color of insensitive widgets

insensitive text widgets and the like base background color

widget text/foreground color on backdrop windows

text color for entries, views and content in general on backdrop windows

widget base background color on backdrop windows

text widgets and the like base background color on backdrop windows

base background color of selections on backdrop windows

text/foreground color of selections on backdrop windows

insensitive color on backdrop windows

widgets main borders color

widgets main borders color on backdrop windows

these colors are exported for the window manager and shouldn't be used in applications,

read if you used those and something break with a version upgrade you're on your own...

Warum?

Weil man normalerweise das bestehende Styling vom System-Theme abgreifen möchte,

und das baut auf den bekannten Node-Namen auf, wie z.B "fame", "box", "notebook"

Und das erklärt wohl auch, warum man diesen CSS-Node-Namen nur direkt in C ändern kann,

indem man eine eigene XXX_class_init() - Funktion schreibt

in den Fällen, die ich mir angeschaut habe, steht dieser String hart codiert in der XXXX_class_init()-Funktion

d.h. die angegebenen Abmessungen entsprechen der bounding box

es geht also nicht, daß man sagt

border: solid yellow;

border-left: 2px

in diesem Falle würde die linke Border mit der geerben Farbe gezeichnet, und nicht gelb

<offX> <offY> [<blur> [<spread>]] <colour>

d.h. wir haben Referenz-Semantik

...die sich aus den CSS-Selektoren ergibt, gemäß dem StylePath für diesen StyleContext,

plus zuzüglich den lokal, explizit hinzugefügten Classes und Modifiers, welche auch

automatisch eine höhere Priorität haben, als das, was sich aus den Path-match ergibt

Bounding-Box meint ja, den effektiven äußeren Umfang.

GTK zeichnet also von diesem ausgehend nach Innen.

Wenn die Bounding-Box kleiner ist als die effektiven border-width, und zwar jeweils zweimal pro Richtung

(oben+unten == Höhe..), dann zeichnet GTK ggfs über die BoundingBox hinaus,

typischerweise nach Oben. Und natürlich sieht das Ganze dann auch nicht mehr wie ein frame aus.

Man muß also genügend Platz allozieren für border-top-width + border-bottom-width + gewünschter Content!

@deprecated früher gab es die Stock-IDs

Glib::RefPtr<StyleContext> context = widget->get_style_context();

context->add_class("ohMy");

this->set_name("my-widget")

(2019 / Debian-Stretch)

Das schließe ich aus einem Kommentar im GTKmm-Tutorial

  // Set the widget name to use in the CSS file.

  set_name("my-widget");

  // If you make a custom widget in C code, based on gtk+'s GtkWidget, there is

  // an alternative to gtk_widget_set_name(): Set a CSS name for your custom

  // class (instead of the widget instance) with gtk_widget_class_set_css_name()

  // (new in gtk+ 3.19.1). That's not possible for custom widgets defined in gtkmm.

  // gtk_widget_class_set_css_name() must be called in the class init function,

  // which can't be customized, when the widget is based on gtkmm's Gtk::Widget.

  //

  // Another alternative: The custom widget inherits the CSS name "widget" from

  // GtkWidget. That name can be used in the CSS file. This is not a very good

  // alternative. GtkWidget's CSS name is not documented. It can probably be

  // changed or removed in the future.

gtk_widget_class_set_css_name (GTK_WIDGET_GET_CLASS(gobj()), "body");

aber der Tag-Name für den CSS-Selector wird dadurch nicht  geändert

8/2018:

ist das eine Sequenz von konkreten Objekten, die ineinander verschachtelt sind?

oder ist es eine Vererbungs-Hierarchie, wie sie für das CSS-Styling benötigt wird?

gtk_widget_path ()

void

gtk_widget_path (GtkWidget *widget,

                 guint *path_length,

                 gchar **path,

                 gchar **path_reversed);

gtk_widget_path has been deprecated since version 3.0 and should not be used in newly-written code.

Use gtk_widget_get_path() instead

Obtains the full path to widget.

The path is simply the name of a widget and all its parents in the container hierarchy,

separated by periods. The name of a widget comes from gtk_widget_get_name().

Paths are used to apply styles to a widget in gtkrc configuration files.

Widget names are the type of the widget by default (e.g. “GtkButton”)

or can be set to an application-specific value with gtk_widget_set_name().

By setting the name of a widget, you allow users or theme authors to

apply styles to that specific widget their gtkrc file.

d.h. nur ein Pointer auf ein GObj

mit automatischem Refcounting

As a consequence, the style will be regenerated to match the new given path.

ein Widget kann einem GDK-Window zugeordnet sein

...nicht mehr das klassische gtk::Main

Wozu das?

  • Design: Main war ein Singleton; aber sein dtor hat auch Plattform-Aufräum-Arbeiten gemacht
  • Framework: anscheinend ist hier eine Tendenz in Richtung auf ein integriertes Framework im Gange; im Besonderen will man "Aktionen" direkt aus dem Desktop aufrufen können

...und erzeugt diesen on demand auch neu

warum?

nur wegen ApplictationWindow!

Denn dieses setzte eine "Registrierung" voraus.

Alles in ein Framework zwingen. Alternativlos, capisce?

...allerdings eingepackt in eine vfunc,

welche ggfs C++ - Exceptions fängt

...nur wegen dem ganzen Registrierungs-Glump.

Ein "GTK-Application-Window" ist auch irgendwie registriert und hängt am Bus.

Frag mich nicht wie. Jedenfalls kann man das nicht im Konstruktor von Gtk::Application machen.

Nur das ist der Grund. Es geht gar nicht um die Event-Loop

suche (case insensitive) nach application_activate

  • treffer auf APPLICATION_ACTIVATE in g_application_activate()
  • die Treffer in Gtk::Application
  • diverse false positives mit anderen "Activation"-Signalen, z.B. in Aktionen oder Buttons

nicht durch gtk_main

wichtige Einsicht:

  • gtk_main ist Toolit ohne Framework
  • bietet keine solchen Lebenszyklus-Signale

beachte: ruft nicht gtk_main sondern macht das Äquivalent

initialisiert das

Framework

gboolean

gtk_init_check (int    *argc,

                char ***argv)

{

  gboolean ret;

  if (!gtk_parse_args (argc, argv))

    return FALSE;

  ret = GDK_PRIVATE_CALL (gdk_display_open_default) () != NULL;

  if (gtk_get_debug_flags () & GTK_DEBUG_INTERACTIVE)

    gtk_window_set_interactive_debugging (TRUE);

  return ret;

}

void Main::init_gtkmm_internals()

{

  static bool init_done = false;

  if(!init_done)

  {

    Glib::init();

    Gio::init();

    // Populate the map of GTypes to C++ wrap_new() functions.

    Pango::wrap_init();

#ifdef GTKMM_ATKMM_ENABLED

    Atk::wrap_init();

#endif //GTKMM_ATKMM_ENABLED

    Gdk::wrap_init();

    Gtk::wrap_init();

    init_done = true;

  }

}

...und das ist nicht gtk_main,

aber macht in etwa die gleichen Operationen

...das heißt, es wurde "retrofitted".

die Lib Gio bietet ein generisches "Main-Loop-Framework",

in dem ein Main-Context gepollt wird, solange, bis ein use-count auf Null geht.

Gtk-Main verwendet inzwischen den gleichen Mechanismus

der springende Punkt mit sigc::trackable ist,

daß Desktuktoren automatisch die Signale abkoppeln.

Dieser Vorgang ist nicht threadsafe. Folglich müssen

auch die Destruktoen im GUI-Thread laufen.

Das ist eine subtile Falle.

alles was von sigc::trackable erbt

für alles aus GTKmm zu verwenden

...gemeint ist:

alles das nicht aus dem GUI-Thread heraus geschieht

struct bar : public sigc::trackable {};
sigc::signal<void()> some_signal;
void foo(bar&);
{
bar some_bar;
some_signal.connect([&some_bar](){ foo(some_bar); });
// NOT disconnected automatically when some_bar goes out of scope
some_signal.connect(sigc::track_obj([&some_bar](){ foo(some_bar); }, some_bar);
// disconnected automatically when some_bar goes out of scope
}
The event mask determines which events a widget will receive. Keep in mind that different widgets have different default event masks, and by changing the event mask you may disrupt a widget’s functionality, so be careful. This function must be called while a widget is unrealized. Consider

add_events() for widgets that are already realized, or if you want to preserve the existing event mask. This function can’t be used with widgets that have no window. (See get_has_window()). To get events on those widgets, place them inside a Gtk::EventBox and receive events on the event box.

Beispiel:

  • Button hat ein Window, denn er möchte button_pressed-Events empfangen
  • Frame dagegen hat kein Window, Icon auch nicht

wenn ein Kind-Widget sein eigenes Fenster aufmacht, dann ist ein unsichtbares EventBox-GdkWindow nicht ein Parent-Window von dem Kind-Widget-Window. Das kann das Propagieren von Events verhindern

jedoch indirekt dann auch für darauf aufbauende Events, z.B. GTK-Events, weil eben nur noch das zum grabbed window gehörige Widgets diese Events überhaupt sieht

....bin damals mit size_request eingestiegen — aber von dort bald auf gtk_widget_size_allocate_with_baseline gestoßen; letzteres ist die eigentliche zentrale Funktion

setzt bei Bedarf ein queue_resize ab

danach: priv->resize_needed = TRUE

  • Container-Widgets haben so etwas wie ein generisches Layout-Muster ⟹ gtk_widget_size_allocate_with_baseline
  • ABER es gibt anscheinend viele Widgets, die das direkt machen (und vom Container-Konzept bewegt sich GTK weg, zu Gunsten des Konzepts "CSS Gadget"); konkret habe ich bei einem GtkFrame durchaus beobachtet, daß dem Widget nur seine geforderte min-size zugeteilt wurde

Einstiegspunkt aus vielen Widgets

gtk_widget_size_allocate_with_baseline

aber: spezielle Widgets/Container

behandeln hier die Child-Widgets

gtk_widget_size_allocate_with_baseline

Beachte: _GtkCssGadgetClass->allocate

typischerweise(via GadgetClass): gtk_css_gadget_real_allocate

und damit wieder von gtk_css_gadget_allocate

allerdings dann vom umschließenden Widget

Implementierung: gtk_frame_allocate

dieses baut auf auf: gtk_css_gadget_allocate

(konkret im Beispiel): gtk_frame_size_allocate

ruft damit gtk_css_gadget_allocate

alle Fäden laufen zusammen bei....

gtk_widget_size_allocate_with_baseline

...da Funktionen irgendwo dann an Funktionspointer zugewiesen werden für "virtuelle Funktionen"

wird im class-init mit der jeweiligen konkreten Implementierung belegt, z.b. gtk_button_size_allocate

das ist eine Flag in der privaten Widget-Struktur. Keine Ahnung was das bedeutet.
Und übrigens: sichtbar muß das Widget auch noch sein...

 * Gets whether the widget prefers a height-for-width layout

 * or a width-for-height layout.

 *

 * #GtkBin widgets generally propagate the preference of

 * their child, container widgets need to request something either in

 * context of their children or in context of their allocation

 * capabilities.

also stellt eigentlich grade nicht die versprochene Logik bereit

danach noch die adjust_baseline_allocation

also entweder in horizontaler, oder in vertikaler Richtung

/*

 * Gadgets are 'next-generation widgets' - they combine a CSS node

 * for style matching with geometry management and drawing. Each gadget

 * corresponds to 'CSS box'. Compared to traditional widgets, they are more

 * like building blocks - a typical GTK+ widget will have multiple gadgets,

 * for example a check button has its main gadget, and sub-gadgets for

 * the checkmark and the text.

 *

 * Gadgets are not themselves hierarchically organized, but it is common

 * to have a 'main' gadget, which gets used by the widgets size_allocate,

 * get_preferred_width, etc. and draw callbacks, and which in turn calls out

 * to the sub-gadgets. This call tree might extend further if there are

 * sub-sub-gadgets that a allocated relative to sub-gadgets. In typical

 * situations, the callback chain will reflect the tree structure of the

 * gadgets CSS nodes.

 *

 * Geometry management - Gadgets implement much of the CSS box model for you:

 * margins, border, padding, shadows, min-width/height are all applied automatically.

 *

 * Drawing - Gadgets implement standardized CSS drawing for you: background,

 * shadows and border are drawn before any custom drawing, and the focus outline

 * is (optionally) drawn afterwards.

 *

 * Invalidation - Gadgets sit 'between' widgets and CSS nodes, and connect

 * to the nodes ::style-changed signal and trigger appropriate invalidations

 * on the widget side.

 */

Gtk::Expander

...ob beim Expand/Collapse das umschließende Widget resized werden soll

ob eingeklappt oder ausgeklappt

ein Frame setzt beim Kind property_expand() = true,

was dazu führt, daß das Kind stets allen verfügbaren Platz nimmt

Gtk::Grid

stets zwischen den Zeichen

Danke! endlich bekommt das mal jemand korrekt hin

This function is only used by Gtk::Container subclasses, to assign a size, position and (optionally) baseline to their child widgets.

"Destroys the widget.

The widget will be automatically removed from the parent container."

  //This has probably been called already from Gtk::Object::destroy_(), which is called from derived destructors.

  _release_c_instance();

d.h. zerstört auch die Heap-Allokation,

wenn das managende Widget zerstört wird

ein normaler Widget/Container tut das nicht

...führen automatisch dazu, daß das Widget

ggfs. neu gemapped und invalidiert wird, woraufhin es neu gezeichnet wird

...können vom CSS-Stylesheet aus gesetzt werden.

Siehe Beschreibung im Beispiel/Tutorial

ist of einfacher und der bevorzugete Weg.

Im Besonderen kann man sich an Signale anderer Widgets anhängen

Hier steckt die Implementierung, und zwar die Kernimplementierung der Platzzuteilungs-Logik von GTK

min_size = get_number (style, GTK_CSS_PROPERTY_MIN_WIDTH);

min_for_size = get_number (style, GTK_CSS_PROPERTY_MIN_HEIGHT); 

basierend auf der Widget-Struktur, welche in Widget-Paths übersetzt wurde

Keine gute Idee: diese Funtktion wird verwendet, um die Dekoration zu entfernen, und Stil-Anpassungen zu machen; sie sollte daher besser als pure function betrachtet werden

in dem Widget, das diesen Aufruf empfängt, wird der Margin abgezogen, und nur dieser reduzierte Wert wird im Widget selber als Allocation gespeichert

...per Design von GTK sind nur wenig Eingriffsmöglichkeiten vorgesehen; stattdessen soll man die Layout-Manager nutzen, die das Layout-reflow automatisch erledigen. Was man definitiv tun kann ist, aus den get_preferred_*()-VFunks dynamisch angepaßte Werte zu liefern

man kann sich nur dazwischen schalten und auf Stabilisierung hoffen

...wenn man in der Phase der Layout-Steuerung eingreift, und einzelne Elemente verändert, muß man durch "invalidation" dafür sorgen, daß GTK die Layout-Berechnung später nochmal macht, und dann hoffen, daß sich in diesem zweiten (oder N-ten) Durchgang keine Änderung mehr ergibt.

sofern in dieser Phase ein Widget visible ist, stimmen auch seine Layout-Antworten

....how does the event dispatching deal with partially covered widgets

...for embedded widgets

...meaning, "this event is not yet fully processed",

i.e. the enclosing parent widget also gets a chance to redraw itself

Warning: allocation is the visible area

asked on stackoverflow...

...as can be observed

by printing values from the on_draw() callback

...otherwise adjustment values will cummulate,

causing us to adjust too much

...die anderen, die noch in Frage kommen würden,

sind nur für den Fall, daß ein Widget neu instantiiert wird

oder neu in das Window-System gemappt wird.

on_check_resize() wird nicht aufgerufen

...keine Ahnung, was ich beim ersten Mal falsch gemacht habe.

jedenfalls hab ich da sofort beim ersten Aufruf der Closure einen SEGFAULT bekommen.

Auch im zweiten Anlauf habe ich ein Lambda verwendet.

Möglicherweise ist der einzige Unterschied, daß ich es nun aus dem draw-callback

aufrufe, und daß demgegenüber bei der ersten Verwendung die Allocation des jeweiligen

Kind-Widgets noch gar nicht festgelegt war (denn das passiert erst beim draw).

in der Implementierung, mywidget.cc

ist eine komplette Sequenz, wie man einen CSS-StyleProvider setzt

und auch ein Signal für Parse-Fehler anschließt

Beispiel im Guide

  • left gravity: Marker bleibt beim Einfügen an dieser Stelle links von der Einfügung stehen
  • right gravity: Marker wird durch Einfügen an dieser Stelle nach rechts geschoben

Beachte: der Text-Cursor (Marker "insert") hat right gravity

Multithreded-Beispiel

im Guide demonstriert das

A dock item is a container widget that can be docked at different place.

It accepts a single child and adds a grip allowing the user to click on it

to drag and drop the widget.

The grip is implemented as a GdlDockItemGrip

der Druck-Metapher gemäß ist das die „Farbwalze“ oder das „Stempelkissen“

wird mit jeder stroke() oder fill()-Operation geleert. Ist Teil des Context, und weitere Zeichen-Operationen fügen zu dem Pfad hinzu

die Source wird durch die Mask hindurch „gestempelt“

  • Kann angezeigt werden
  • Kann in ein Bitmap gerendered werden
  • Kann als »source« für weitere Zeichenvorgänge dienen

current transformation matrix (CTM) : User-Space ⟼ Device-Space

Transformations-Definition wird eingangsseitig vor die aktuelle CTM vorgeschaltet

Beispiel:

   cairo_scale (cr, 100, 100);

   cairo_translate (cr, 0.1, 0.1);

...die aktuellen Transformations-Einstellungen (current transformation matrix CTM)

...und nicht nur das, auch die Source-Einstellungen wie Linienstärke und Zeichenfarben

anscheinend gibt es eine Konvention, welche die im UI sichtbaren Koordinaten in vertikaler Richtung spiegelt; auch ist der auf dem Lineal angezeigte Ursprung unten links

Koordinaten »versäubern«

...und Inkscape wird das erhalten, solange man die betr. Features nicht wieder aktiviert. Im Besonderen kann man

  • alle "Stroke"-Features entfernen, wenn der Stroke deaktiviert ist
  • opacity:1 weglassen
  • die Defaults "color:#000000;display:inline;overflow:visible;visibility:visible;"  kann man meist weglassen
  • diverse Vector-Filter und display-styles weglassen (wenn sie auf dem default-Wert stehen)

style="fill:black;fill-opacity:0.5;stroke:#5a8fb2;stroke-opacity:1;stroke-width:0.1"

style="fill:#ffffff;fill-opacity:0.75;stroke:none;stroke-width:0.05"

aber leider nur als globales Verhalten der Inkscape-Installation

Behaviour > Transforms > store optimised

wenn ein Member eine nichttriviale Funktion hat,

dann muß die gleiche Funktion für die unition explizit geschrieben werden

  • ctor (auch default)
  • copy-ctor
  • assignment
  • dtor
  • vtable

im Zweifelsfall: std::is_pod<TY>() fragen

Achtung: der Begriff »POD« is @deprecated

„trivial“ ⟹ der Compiler muß nix Spezielles  machen

Ticket #886

09.05.19 17:10

Library: further narrowing down the tuple-forwarding problem

    
    ...yet still not successful.
    
    The mechanism used for std::apply(tuple&) works fine when applied directly to the target function,
    but fails to select the proper overload when passed to a std::forward-call for
    "perfect forwarding". I tried again to re-build the situation of std::forward
    with an explicitly coded function, but failed in the end to supply a type parameter
    to std::forward suitably for all possible cases

OpaqueUncheckedBuffer_test für InPlaceBuffer

Wieder das Gleiche: die create<ARGS....> - Funktion delegiert an den copy-ctor, selbst wenn dieser deleted ist

vielleicht doch

etwas Anderes

das Problem sind implizit definierte Konstruktoren in der Vererbuns-Hierarchie

sobald ich alle Move-Konstruktoren explizit definiere, funktionieren alle Fälle

Wenn man eine Klasse C<T> hat, und einen Konstruktur definiert, der ein T&& - Argument nimmt, dann ist das T bereits durch den Klassen-Templateparameter festgelegt; es kommt in diesem Fall nicht zu einem reference collapsing, sondern es handelt sich einfach um eine rvalue-Referenz T&&. Symptom "can not bind rvalue reference to lvalue"

...denn in diesem Fall würde es sich zwar um eine »universelle Referenz« handeln, aber der Zusammenhang zum Klassen-Template-Parameter ist nicht klar

  • steht im enclosing scope nach der Template-Definition
  • syntaktisch eine frei stehende Funktions-Deklaration mit einem trailing return type, welcher die resultierende Template-Spezialisierung konstruieren muß (kann beliebig komplexe Typ-Ausdrücke enthalten)

Beispiel

  /** deduction guard: allow _perfect forwarding_ of a any result into the ctor call. */

  template<typename VAL>

  Result (VAL&&) -> Result<VAL>;

Wenn ich die Beschreibung in CPPReference.com richtig verstehe, dann würde ohne expliziten deduction guide folgender guide automatisch generiert:

template<class X>

class C {

  C(X&& val);

};

Auto-Guides:

template<class X>

C<X> g1(X&& val)

template<class X>

C<X> g2(C<X>)

Hilfs-Deduction+Resolution...

struct S {

  template<class X>

  S(X&&);

  template<class X>

  S(C<X>);

};

S = <initializer>

Was ich nicht verstehe: der gemäß Regeln auto-generierte Guide sieht exakt genauso aus, wie der Guide, den man manuell definieren muß, damit es funktioniert. Warum wird dann hier ein anderer Typ deduziert? Ich habe Experimente gemacht: ohne den manuellen deduction Guide ergibt sich für einen <initializer> ≔ int&  ⟼ ein deduzierter Parameter X = int  (was dann natürlich scheitert)

Coroutinen sind nicht per se asynchron

welche dann jedoch stets aus einer Coroutine heraus per co_await aktiviert werden. Beispiel: ein lock-free mutex

Kontrollübergänge erfolgen ausschließlich an den suspend points in der Coroutine selber

Das untermauert nochmal, daß Coroutinen inhärent synchron und deterministisch sind. Man kann allerdings das low-level-Framework nutzen, um an einem bestimmten suspend-point den "eingefrorenen" Zustand der Coroutine asynchron an einen anderen Thread zu übertragen

...also für template templtate parameter;

für diese muß man allerdings stets die umständlichere requires-Syntax verwenden

template <template <typename...> class ALO>
concept Allocator = true; // actual constraint here

template <template <typename...> class ALO>
  requires Allocator<ALO>
class Something
{ };

...man kann aber auch ein Concept als Kombination weiterer definieren

verblüffenderweise ist das im Schnitt die effizienteste Lösung, aufgrund der guten Cache-Lokalität des Vektors

  • da die Heap-Algorithmen stets die Elemente paarweise vertauschen, werden niemals Einfüge/Löschoperationen des unterliegenden Containers benötigt
  • da der Heap eine raum-organisierende Datenstruktur ist, werden niemals direkte Zeiger auf andere Elemente benötigt, sonder der Zugriff erfolgt stets per (logarithmisch) errechnetem Index

Eine Störstelle in der Heap-Struktur wird durch „lokales bubble“ korrigiert. Ein am Ende hinzugefügtes Element bubbled hoch, bis es größer ist als beide Kinder. Für ein entferntes größtes Element rutscht das größere Kind hoch. Für ein entferntes inneres Element wird zunächst das letzte Element an diese Stelle gesetzt  (weil dadurch dann die letzte Position frei wird); die dadurch entstehende Störstelle wird dann nach oben/unten korrigiert: ist das Element größer als der Vater, tauscht es mit diesem, ist es kleiner als das größere seiner Kinder, tauscht es mit diesem.

...hatte ich vor einigen Jahren schon durchgeführt. Die Boost-Impl wird weithin empfohlen, sie kommt einer Standard-Lib am nächsten. Für Ringbuffer gibt es diverse Alternativen, aber für Multi-Producer / Multi-Consumer-Queue habe ich ansonsten nur Einzelfall-Implementierungen gefunden

"load effective address"
Es entspricht also dem &-Operator in C

Lessons

learned

auf incomplete type achten

Vorsicht bei

mutually dependent templates

kann eines der Templates im Zyklus vorrübergehend als "incomplete" gelten.

...wenn man dummerweise auf verschlungenen Pfaden

genau in dieser Phase die Metafunktion anfragt,

kann der betreffende Check stillschweigend scheitern.

Konsequenz: man wählt dann z.B. eine subtil falsche Spezialisierung.

werden u.U sillschweigend nicht generiert

Wenn bestimmte Vorraussetzungen nicht erfüllt sind, dann unterbleibt das automatische Injizieren eines generierten Konstruktors. Die Klasse verhält sich dann so, als wäre die btr. Definition nicht gegeben...

d.h. Member-Init kann unterbleiben (bei elementaren Typen, wenn die Klasse damit ein POD wird).

Und noch häufiger: wir schwenken von Move-Konstruktion auf Copy-Konstruktion um

error: use of deleted function Derived::Derived(const Derived &)

note: Derived::Derived(const Derived &) is implicitly deleted because the default definition would be ill-formed:

...und dann kommt der Fehler, der passieren würde....

error: use of deleted function Base::Base(const Base&)

note: Base::Base(const Base&)  is implicitly deleted because the default definition would be ill-formed:

error: use of deleted function SomeMember::SomeMember(const SomeMember &)

...

...

...

...

...

die uniform initialisation ist leider unrund geraten

die Werte in der initializer_list sind Values und werden per copy-Konstruktion erstellt

wenn ein Template ein statisches member-Feld hat,

dann ist zusätzlich eine getemplatete Definition dieses Feldes notwendig.

Diese wird erst generiert, wenn der erste odr-use des statischen Member-Feldes passiert.

Dieser odr-use kann nun z.B. aus einer Funktion des Template heraus erfolgen

Allerdings beobachte ich, daß dann der ctor-Aufruf zur Initialisierung erst nach dem Zugriff auf

das member-Feld passiert, sofern der Aufruf und damit die Instanz des umschließenden Template

selber aus einem statischen Initialisierungs-Kontext heraus erfolgt.

#include <iostream>

using std::cout;

using std::endl;

template<typename T>

class Factory

  {

  public:

    T val;

   

    Factory()

      : val{}

      {

        cout << "Factory-ctor  val="<<val<<endl;

      }

  };

template<typename T>

class Front

  {

  public:

    static Factory<T> fac;

   

    Front()

      {

        cout << "Front-ctor    val="<<fac.val<<endl;

        fac.val += 100;

      }

   

    T&

    operate ()

      {

        cout << "Front-operate val="<<fac.val<<endl;

        ++ fac.val;

        return fac.val;

      }

  };

template<typename T>

Factory<T> Front<T>::fac;

namespace {

  Front<int> front;

  int global_int = front.operate();

}

int

main (int, char**)

  {

    Front<int> fint;

   

    int& i = fint.operate();

    cout << "main:         val="<<i<<endl;

    cout << "global_int.......="<<global_int<<endl;

   

    return 0;

  }

das sind verschiedene Blickwinkel auf das gleiche Thema

Bedeutung dieser Schreibweise:

  • der erste Zugriff liegt vor der Barriere, der zweite danach
  • die Barriere garantiert jeweils nur, daß der zweitgenannte Zugriff nicht vor den erstgenannten verschoben werden kann

zusätzlich gib es eine daran gebundene Payload aus nicht-atomaren Werten

  • diese nicht-atomaren Werte sind ganz normale Speicherinhalte, die potentiell von verschiedenen Threads aus zugreifbar wären
  • sie müssen aber an den Guard gebunden sein, und zwar durch einen conditional-Check auf den vom Guard gelesenen Wert

Dieser Zusammenhang fußt auf den exakten Definitionen der release  und acquire-semantics und einer damit verbundenen synchronizes-with  Zusicherung. Das hat nichts mit data-dependency zu tun! Es ist egal, wie auf die "anderen" Werte vor/nach der Synchronisierung zugegriffen wird (pointer, argument, globale Variable). Sondern entscheidend ist, daß dazwischen liegend auf eine bestimmte Eigenschaft/Wert im Guard geprüft wird; diese Eigenschaft muß sicherstellen, daß das synchronizes-with stattgefunden hat. Zugriffe ohne diese Prüfung sind undefiniert (d.h. sie können oder können nicht einen Mix von veränderten Werten sehen).

eine Solche konstituiert die synchronizes-with-Beziehuung

Grundbeziehung: synchronizes-with

das gilt nur im Rahmen der synchronizes-with-Beziehung

das heißt, nur für einen vom gleichen Mutex geschützen  Bereich!

In der naiven Implementierung greift der prüfende Thread auf die instance-Variable

ohne jedwede Beziehung zum anderen Thread zu; er verwendet keinen Mutex und keinen Atomic.

Und genau deshalb kann er das Setzen des Instanz-Pointers sehen, ohne daß eine

Ordnungsbeziehung zur der restlichen Initialisierung oder lazy computation besteht.

Fix: die Beziehung herstellen. Das ist verursacht stets zusätzliche Kosten.

Allerdings nicht auf einer Plattform, die ohnehin sequentiell-konsistent ist. Wie "zum Beispiel" x86/64

...haben wir es hier mit einem Pattern zu tun

ich nenne es "synchronised visibility cones"

Dieses errichtet die Fiktion,

als würden wir nur auf einer gemeinsamen (shared) Instanz arbeiten.

In Realität arbeiten mehrere Threads/Cores mit mehreren Entitäten

...und andernfalls überhaupt vermeiden,

die shared zone anzufassen!

man ruft explizit eine Service-Loop-Funktion auf — io_context::run()

Teil der Vision / wir wollen das

/** @file mmap.h

 ** MMap objects cover a memory mapped range in a file.

 ** They are managed through a global mmap registry/cache.

 */

wenn eine getemplatete Klasse zum Qualifizieren eines Feldes verwendet wird,

dann müssen die formalen Template-Parameter in spitzen Klammern mit angegeben werden.

D.h. Doxygen ist hier genauso penibel wie C++ selber

Beispiel

Query<RES>::resolveBy

@param hat stets einen Parameternamen als Argument

...der ist nicht optional

vielmehr wird blindlings immer das erste Wort genommen.

Wenn der Parameter selber nicht benannt ist (z.B. pure virtual function),

kann man ersatzweise einfach einen Typnamen angeben.

Sofern alle Parameter dokumentiert sind, klappt das.

sonst kommt Doxygen durcheinander

also keine "Banner" aus Sternen.
Abhilfe: /*********************//**

....haben in ihrem @file-Kommentar

einen Verweis \ref DieserUnit_test

Und obwohl das der exakte Klassennahme ist,

und obwohl genau diese Klasse im Klassenindex zu finden ist

wird hier kein Link erzeugt

...wird zwar vom Skript ausgelesen,

aber nicht weiterverwendet.

Die Icon-Größen ergeben sich aus den Boxes auf 'plate'

...im Besonderen die guten Diagramme für Pulse, ALSA und Jack

auf diese Stellen konzentrierte sich für längere Zeit die Aktivität, und keines dieser Themen konnte ich wirklich abschließen, denn es sind noch zu viele strategische Fragen ungeklärt — mit 2022 ist immerhin jedes dieser Themen auf einen längerfristig tragfähigen Grund gestellt

kein Vorwärts/Rückwärts spulen, kein Springen, kein Looping, keine variable Geschwindigkeit

involviert Problem des Output Network

nur eben dahinter geschaltet, und flexibler

  • zumindest die Clip-ID muß irgendwie auf high-level-Ebene repräsentiert werden
  • sofern die Timeline involviert ist: es könnte Änderungen der Start/Stopzeit geben

Entscheidung: im Builder

Baum-Monaden-Framework für Job-Planung
fragwürdiges Design

habe das wohl schon damals gemerkt... der Quelltext ist voller "entschuldigender Kommentare"

das primäre Problem ist hier nicht C++, oder eine ungeschickte Implementierung meinerseits, sondern das brennende Problem ist, daß die Monaden-Struktur nichts zum Verständnis der Verwendung beiträgt, sondern diese durch eine rein-mathematische Symmetrie verstellt und verschleiert

und bietet monadische-Expansion als Spezialfall

Es ist zwar schon klar, daß ein separates Output-Network gebaut werden muß, um die generierten Frames für den konkreten Viewer in der GUI aufzubereiten. Aber es ist noch nicht wirklich geklärt, wie dieses in das generelle Design mit High-Level-Model, Builder und Low-Level-Model zu integrieren ist. Bisher hatte ich immer nur den Begriff "ViewConnection" verwendet (Siehe TiddlyWiki) und dazu festgehalten, daß es sich um ein »Binding« handeln soll. Unklar ist aber die genaue Verhältnisbestimmung zu dem Binding, das für Timeline und VirtualClip zum Tragen kommen wird.

...dieser Monaden-Code ist undurchdringlich (selbst für micht, und ich kann mich noch gut daran erinnern, was er leisten soll)

Leitlinie: so viel tun, daß uns das Thema nicht über den Kopf wächst

hier stammt die Darstellung im Wesentlichen aus den ersten Jahren, ist aber mit dem teilweisen Re-Design verflochten ⟹ verwirrende Mix aus überholten Ansichten und Konzepten für die ferne Zukunft

Aber es gibt auch stellenweise Überarbeitungen (von Benny) auf der offizielen Linie

Im Zuge der laufenden Entwicklung wurden...

  • Seiten umbenannt
  • neue Bereiche begonnen

alles dies ist noch nicht auf dem offiziellen "dok"-Branch und deshalb noch nicht publiziert!

⟹ lokal/gui auschecken und Dok-Site auf localhost verwenden

...ging völlig problemlos, und das Resultat/Changeset sieht plausibel aus...

  • das ist das größte Impediment, weshalb Informationen nicht aus dem TidlyWiki migrieren können
  • wir hatten das vor Jahren bereits besprochen, und Christian hat eine »Link-Farm« vorgeschlagen
  • Benny wollte sich darum kümmern, hat das aber  vergessen/verdrängt
  • ich selber könnte das in ca. 4 Tagen auf die Beine stellen

Beispiele zur Verwendung

der Styles dokumentieren

Diese Info hatte ich vor längerer Zeit mal "aufgegabelt" und sie wer der erste Content auf dieser Styling-Doku-Seite. Inzwischen hat sich der Scope verschoben, und sowas gehört ehr in die CodeBase-Sektion

TODO: dokumentieren, wie das Lumiera-Interface ausgelegt und angeordnet wird, bis hinab zu den Details der Widget-Anordnung, damit ein Außenstehender im Stande ist, am Styling mitzuarbeiten...

Darstellung: about Monads

Dezember 2017 ausgearbeitet und im TiddlyWiki vergraben.

bin baff... das hatte ich völlig vergessen

das ViewHook / CanvasHook-Konzept hat sich hervorragend bewährt, und durch das zoom-metric-Mix-in gibt es auch bereits eine sehr elaborierte und robuste Implementierung für alle Belange der temporalen Ausdehnung (horizontal). Allerdings funktionieren alle diese Basis-Implementierungen bisher nur für den eigentlichen Content in der Timeline — ganz einfach weil ich noch nicht so recht weiß, was es mit den »Rulern« so auf sich hat ....  ⌛

Das Thema »Layout-Steuerung in der Timeline« lief aus dem Ruder (wie erwartet — warum soll's mir besser gehen, als Joel Holdsworth?). Ich habe dahinter aber ein Architektur-Problem identifiziert, nämlich daß hier eine selbs-ähnliche rekursive Struktur notwendig ist, welche aber mehrere separate Kontexte überspannt. Dafür habe ich die Entität DisplayFrame  erfunden, und die Idee war, diesen als Kristallisationskern zu verwenden, um die gesamte Struktur um das Timeline-Layout zu reinigen. Leider konnte ich das aber (Stand 9/2022) nicht weiterführen, weil ich nun das GUI wieder verlassen muß, um an der RenderEngine weiterzuarbeiten...

....dieses Thema sollte generisch gelöst werden, und dann weiter entwickelt in Richtung »Interaction Design« — also mit einem aktuellen Fokus verbunden, und mit jeweils eigener Historie pro Fokus, so daß man in die verschiedenen Fokus-Zonen über gut etablierte "Leitern" wieder einsteigen könnte. Leider muß ich (Stand 9/2022) das GUI nun wieder verlassen, und es ist ganz und gar ungeklärt, unter welchen Umständen dieses zentrale Thema wieder aufgenommen werden kann...

Es gibt immer noch Zweifel ob gewisser Race-Conditions....

Diese hängen jedoch eng mit dem Design des Subsystem-Runners zusammen, welches zwar grundsätzlich in Ordnung ist, aber alles in allem „etwas zu knapp gehalten“: man sollte hier explizit und fein differenziert die Lebenszyklus-Phasen ausformulieren, denn dann ist man nicht mehr von Subtilitäten der Implementierungs-Reihenfolge abhängig

...beim Herauslösen aus dem Gnome-Application / GIO-Framework habe ich detailiert die Strukturen um die Main-Loop untersucht und verstanden. Leider stehen diese Infos bisher (Stand 9/2022) nur in meiner Mindmap; ich sollte eine eigene Kategorie in der Webseite/Dokumentation schaffen, in der solche Einsichten aufgezeichnet werden können...

  • was wollen wir mit den Placements und wie weit soll das reichen?
  • wie wird mit den »Dimensionen« im Placement umgegangen? (⟶ siehe Replantor-Konzept)
  • können wir den Weg zu CQRS / Event-Sourcing gehen?

Habe mir lange die vielen Tickets von damals angeschaut:

  • in der Tat, dieses Ticket steht an einer Schlüsselstelle, und ist korrekt mit anderen Themen verdrahtet
  • es besteht eine Wechselbeziehung zu der Frage, wie das Modell überhaupt manipuliert wird

Bezug: Changeset bf35ae0

als Patch (Tag) dump.blockFlow

bekannter Bug binutils #16936

Lumiera-Ticket #965

gelöst in 4e8e63ebe

...man "hilft" dem Linker mit

"-Wl,-rpath-link=target/modules"

laufen wieder alle

test.sh Zeile 138

Debian-Bug #724461

nebenbei ohweh:

ulimit -t 1 ist wirkungslos

Christian:  bash -c "ulimit -t 1; while :; do :; done"

und wir verbringen unsere Zeit mit contention

ist klar, hab ich gebrochen

siehe Ticket #587

Kollisionen jetzt bereits nach 4000 lfd. Nummern

Vorher hatte ich erste Kollisionen nach 25000 Nummern

erinnere mich an den

guten alten "Knuth-Trick"

wow: es genügt,

die letzten beiden Zeichen mit der Knuth-Konstante zu spreizen,

und ich komme locker auf 100000 Nummern ohne Kollision

Aug 10 04:51:39 flaucher kernel: gdb[8234]: segfault at 7ffe3fa79f50 ip 0000000000718b95 sp 00007ffe3fa79f40 error 6 in gdb[400000+574000]

Aug 10 04:51:39 flaucher kernel: traps: test-suite[8249] trap int3 ip:7ffff7deb241 sp:7fffffffe5c8 error:0

function gebunden an ein lambda

wobei ein Argument-Typ als vom Template-Argument

der umschließenden Funktion aufgegriffen wird

Bugreport für Debian/Jessie #795445

Git: debBild/Gdb_DEB.git

bison dejagnu flex gobjc libncurses5-dev libreadline-dev liblzma-dev libbabeltrace-dev libbabeltrace-ctf-dev python3-dev

dutzende Tests scheitern

verräterrischer Code im debian/rules

check-stamp:

ifeq ($(run_tests),yes)

        $(MAKE) $(NJOBS) -C $(DEB_BUILDDIR)/gdb check \

          || echo "**Tests failed, of course.**"

endif

        touch $@

au weia LEUTE!

speziell: unused-function bei dem Trick mit dem std::hash macht mir Sorgen.

und tatsächlich: das ist daneben, GCC hat Recht!

aktualisieren und neu bauen

früher war das so eine typische "nörgel"-Warnung, die man unter den Teppich kehren konnte: 'ey, der Compiler bekommt es ja trotzdem richtig hin.

  • sinnvollen Zustand erreichen: zumindest das neue GUI sollte wieder starten
  • dann Paket aktualisieren und neu bauen

standard hardening-flags setzen #971

wähle Kompatibiltät genau so, daß Ubuntu-Trusty noch unterstützt wird.

env.GuiResource(f) for f in env.Glob('stage/*.css')

wenn ich doch mal noch komplexere Bäume transportieren muß

ich mag code-nahe Resourcen lieber beim Code selber

aber bei der Implementierung hab' ich dann Pragmatismus walten lassen

  •  Stichwort: getDirname()
  • effektiv ist das nur eine Ebene tief gestaffelt
  • alles darunter wäre in ein Verzeichnis gekippt worden

weil dann die Builder-Funktion die Quelle nicht mehr findet :-P

Dann

nämlich im src/SConscript, wenn es um das GUI geht

und sie war ohnehin schon so geschrieben worden, daß das Endresultat irgendwie paßt

...damit man auch im Paketbau-Build-Output wenigstens einmal alle  generischen Platform-Schalter sieht

Ich meine also: zu Beginn vom Build sollte das Buildsystem einmal eine Infozeile ausgeben

...denn die stören jeweils beim erzeugen eines Hotfix/Patch im Paketbau per dpkg --commit

deprecated: auto_ptr

Tests mit TypeIDs scheitern

Grund ist die Umstellung auf inline-Storage

wäre theoretisch jetzt möglich,

da wir nun eine vollwertige String-Tabelle haben

waren nur minimale Anpassungen

das Problem wurde vom GCC beim Bauen des alten Lumiera-Paketes angemahnt; tatsächlich aber hatte ich das Problem inzwischen längst schon anderweitig bemerkt und an der Wurzel gelöst, anstatt nur Symptome zu behandeln

als "workaround" hatte ich boost::rational<long> genommen

und wir brauchen die definitiv zum sinnvollen Rechenen mit Zeiten auf micro-Scala

Der kritsche Fall ist nämlich, wenn wir mit FSecs anfangen, und dann irgendwo in der Rechnung mal Time::SCALE multiplizieren, um auf die µ-Skala zu wechseln. Am Ende der Rechnung würde dann typischerweise ein rational_cast stehen. Damit das funktioniert, muß vor allem der Zähler des Bruches die volle Zeitskala unterstützen. Daher sind 64bit zwingend

weil 64bit -> 32bit eine narrowing conversion ist, die zumindest eine Warnung erzeugt

src/lib/itertools.hpp:805

error: no type named 'Ret' in 'struct lib::meta::_Fun<std::__cxx11::basic_string<char> (*)(const std::__cxx11::basic_string<char>&) noexcept, void>'

die überladene const-Variante ist der Grund

Frage: wollen wir auf noexcept einschränken?

  • als schneller Fix implementiert
  • und tatsächlich nur einmal, für einen Test verwendet
  • eigentlich wird damit das Problem "unter den Teppich gekehrt"

"sinnvoll" heißt

  • stabil
  • lesbar

warum...?

..vermutlich, weil ich ab einem gewissen Punkt damit angefangen habe, auch die Lumiera-Iteratoren als "foreach-iterierbar" zu dekorieren (indem sie freie begin(iter) und end(iter)-Funktionen bieten).

es sollte genau die Eigenschaften abdecken, die wir tatsächlich brauchen

mit const und volatile...

Doku durchkämmen nach Müll

hier nach offensichtlich obsoleter Info checken

WICHTIG: keine vorgreifende Infos publizieren!!!!!

die explizit angegebenen Paketnamen schon mal vorchecken

die Abschnitte zu den LIbraries prüfen / umschreiben

insgesamt sorgfältig durchlesen

knappe Kennzeichnung des Releases in den Kommentar

hier geht es darum, Konsistenz im Git herzustellen.

Wenn alles korrekt gemacht wurde, dürfte es hier keinen Rückfluß von Änderungen geben.

Bitte auch daran denken, zuerst den DEB-Zweig zu prüfen. Diesen aber nicht zurückmergen,

denn wir wollen keine DEB-Info im Master haben!

einzeilige Kennzeichnung wiederholen

die unmittelbaren Release-Dokumente durchgehen

Merge-commit auf den Release-Zweig.

Sollte konfliktfrei sein

...das heißt bauen und hochladen

Referenz: Debian/Jessie (stable) : i386 and x86_64

Probleme mit der Compile-Reihenfolge  #973

...führt sowohl eine README, alsauch ein Verzeichnis /usr/share/doc/lumiera/html auf, das (noch) nicht existiert

unter Debian/Jessie wird das ignoriert

stelle fest: Fehler auf Trusty,

nur Warnung auf Mint

das heißt, daß ich versuchen kann, das Problem erst mal "unter den Teppich zu kehren"

Die Wahrscheinlichkeit, daß irgend jemand Lumiera unter Ubuntu/Trusty installieren möchte, erscheint mir akademisch

bauen mit gcc-5 scheitert

in lib/hash-standard.hpp

mit gcc-5 gebaute Tests scheitern

bauen mit gcc-4.9 nicht möglich

es gibt Probleme beim Linken mit den Boost-Libraries, die auf Ubuntu/wily mit gcc-5 gebaut sind.

Wichtig: hier nur was wirklich gebaut ist und funktioniert!

eigentlich war die nur notwendig für das Video-Viewer Widget,

was nun leider tot ist. Wir haben noch keinen Ersatz. Deshalb lasse ich die Abhängigkeit

bestehen, aber irgendwann müssen wir das schon glattziehen

hardening-flags! #971

Ticket #722

seit gcc-4.8 ist kein static_assert mehr in der STDlib

Aber Vorsicht: es wird noch gar nicht verwendet

typed-allocation-manager.hpp 217

dumme Heap-Allokation eines char[]

!!!!!11!!

Einmal-Event beim Ausführen der Testsuite nach Compile.

TEST Render job planning calculation: JobPlanning_test .. FAILED

unexpected data on stdout

'total latency    : ≺11ms≻':1

does not match

literal: total latency    : ≺30ms≻:1

Bemerkenswert:

  • der Test selber scheitert nicht, weil die Berechnung wie erwartet abläuft
  • aber die berichtete Latency ist 11ms statt 30ms wie erwartet
  • das bedeutet: das JobTicket -> isEmpty() [weil dann fallback auf JOB_MINIMUM_RUNTIME in JobTicket::getExpectedRuntime ]
  • ohne das nun im Detail zu analysieren: ich bin beunruhigt, wie KANN das JobTicket empty sein -- es wird doch hier im Test explizit aus der MockFixture erstellt

Kontext: bin grade am Umbauen des Thread-Frameworks...

Zufällig aber durchaus regelmäßig wieder auftretendes Fehlverhalten

  • der Test erzeugt 75 ThreadJoinable, welche jeweils eine rekursive Berechnung mit thread_lokal storage und sleep machen
  • danach explizit alle Threads join()
  • ⟹ aus den aufgesammelten lib::Result wird manchmal die Platzhalter-Exception ausgeworfen, mit der in der Policy zu Beginn der std::exception_ptr vorbelegt wurde ("No result yet, thread still running; need to join() first.").

Theoretische Analyse: in der Doku (Cppreference) heißt es, join() synchornizes-with dem Ende des Thread. Eigentlich würde ich daraus schlußfolgern, daß wir auch eine release-acquire-Barriere haben. Das könnte aber eine zu starke Schlußfolgerung sein. Wir erzeugen ja im Thread ein neues lib::Result und weisen das per move-assign zu; wenn der Effekt der Zuweisung auf den eingebetteten std::exception_ptr im Haupt-Thread noch nicht sichtbar ist, würde das beobachtete Verhalten resultieren.

Aber ich sehe auch noch eine zweite Theorie: gestern hatte ich an den Anfang der ThreadLifecycle::invokeThreadFunction() einen Guard eingefügt, zum Schutz gegen eine Exception aus dem Konstruktor einer abgeleiteten Klasse; d.h. wenn es den Wrapper gar nicht(mehr) gibt, soll die Thread-Funktion ebenfalls ganz geräuschlos verschwinden. Könnte es sein, daß die Bedingung Policy::isLive() zu Beginn des Thread nicht zuverlässig anspricht? Falls das passiert, würde dann auch das eingebettete lib::Result nicht zugewiesen, der Thread wäre aber im Mater-Thread später als Joinable markiert, und das beobachtete Verhalten wäre die Folge.

Probleme mit der Compile-Reihenfolge  #973

TEST Dispatch functors into other threads: CallQueue_test .. FAILED

unexpected return value 134, expected 0

stderr was:

0000000459: INFO: suite.cpp:180: thread_1: invokeTestCase: ++------------------- invoking TEST: CallQueue_test

0000003117: CHECK: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: (globalProducerSum == globalConsumerSum)

0000003127: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: /Werk/devel/lumi/target/modules/libtest-basics.so(_ZN3lib4test14CallQueue_test19verify_ThreadSafetyEv+0x24a) [0x7fdfce328a20]

0000003128: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: /Werk/devel/lumi/target/modules/libtest-basics.so(_ZN3lib4test14CallQueue_test3runERSt6vectorISsSaISsEE+0x34) [0x7fdfce32764e]

0000003129: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: /Werk/devel/lumi/target/modules/liblumierasupport.so(+0x1d7cb0) [0x7fdfcb7dfcb0]

0000003130: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: /Werk/devel/lumi/target/modules/liblumierasupport.so(_ZN4test5Suite3runERSt6vectorISsSaISsEE+0x38f) [0x7fdfcb7e0249]

0000003131: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: ./test-suite() [0x40625e]

0000003132: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fdfc9029b45]

0000003133: BACKTRACE: call-queue-test.cpp:251: thread_1: verify_ThreadSafety: ./test-suite() [0x4060a9]

END

for I in `seq 1 50`; do target/test-suite CallQueue_test; done

habe gleichzeitig erst die Testsuite gebaut mit -j 36 und dann laufen lassen.

Gleichzeitig aber auch noch das ./build-website-Skript

und eine Doxygen-Seite im Browser geladen

weil sich die Threads gegenseitig ihre Counter inkrementieren.

alle anderen (mit Ausnahme von BusTerm_test)

verwenden globale Variable oder überhaupt keine Objektfelder

angeregt durch Gabriel;

er wollte "versuchen, Lumiera zu bauen"

wann sind Funktoren äquivalent ??

mathematisch gilt:

sie sind gleich, wenn sie für alle gleichen Argumente das gleiche Resultat liefern.

sei die Dose offen...

in tr1::functional war ein equality-Operator spezifiziert

boost::function hat sich geweigert diesen zu implementieren,

weil es keine vernünftige und konsistente Implementierung gibt.

Für den C++11 - Standard hat man dann einen Kompromiß geschlossen,

demnach der Vergleich mit einem NULLPTR sinnvoll (und implementierbar) ist,

aber ansonsten alle validen definierten Funktionen untereinander verschieden sind.

sonst auf Äquivalenz getestet

und genau das Letztere ist nicht garantiert korrekt implementierbar

Selbst verschiedene Closures haben selbst die noch eine eindeutige Identität

d.h. wir brauchen keine Äquivalenz?

sporadich, nicht bei jedem Lauf, aber reproduzierbar.

Bleibt hängen an der Stelle, wo der Test den Dispatcher vorübergehend deaktiviert,

und dann auf den Deaktiviert-Zustand wartet

...weil der Objekt-Monitor nicht mehr bedingungslos gewartet hat,

nachdem einmal ein wait mit Timeout verwendet worden war

aber inChange bleibt true

...es könnte dann nämlich die Session geschlossen und freigegeben werden,

obwohl noch der Builder läuft