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.

...mit dem Ergebnis

  • die XV-Videowidergabe funktioniert immer noch
  • eine einfache Bitmap-basierte Anzeige kann leicht mit Gtk::Image + Gdk::Pixbuf realisiert werden (aber Vorsicht, Pixpuf ist @deprecated)

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.

STL-Iteratoren sind »abstrahierte Pointer« und setzen eigentlich die Idee eines Datencontainers vorraus. Das gilt nicht für Lumiera-Iteratoren; diese sind nicht dafür gedacht, Container-Inhalte  zu extrahieren oder zu manipulieren, sondern sie verkörpern eine Folge von Berechnungen.

...und zwar vor allem im Zusammenspiel mit dem zentralen Konzept einer »State Core« — der Nutzer sollte nicht dazu verleitet werden, zu viele Annahmen über diesen State zu machen, ganz einfach, weil die Konsequenzen komplex und gedanklich schwer handhabbar sind. Das habe ich aus eigener Erfahrung gelernt (Stichwort der monadische Iter-Explorer)

...wäre eigentlich ganz billig zu haben, analog zu ConstIter

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«

das heißt, der erste erschöpfte Iterator terminiert die gesamte Sequenz

damit man auch die Fälle erschlagen kann, in denen irgendwo eine Index-Variable gebraucht wird; oft ist das nämlich der einzige Grund, dann doch eine klassische For-Iteration zu machen

stelle fest: ich darf kein Quell-Tupel konstruieren

Es ist eigentlich nur eine Verpackung für std::apply, welches man mit dem gleichen Aufruf-Aufwand stets auch direkt aufrufen könnte. Hinzu kommt, daß nun die Argument-Ordnung auf dem API entweder links-rum oder rechts-rum nicht paßt und verwirrend ist

der spezielle Trick mit dem inneren Lambda ist nur für ein Tupel notwendig, um die Tupel-Elemente flexibel aber korrekt wieder in die Hand zu bekommen (alternativ mußte man vor C++17 mit einem Index-Seq-Iterator arbeiten, was sehr undurchsichtig ist)

...und damit sind nämlich beide Ansätze letzlich wieder versöhnt. Das war die wichtige Einsicht beim zweiten Design-Anlauf. Beim ersten Anlauf wollte ich »brav« sein und Werte liefern, und das ganze Design wurde dadurch extrem undurchschaubar und wackelig, denn natürlich wurde beim ersten Kontakt mit der Realität klar, daß man dann smart-Pointer durch die Gegend schiebt.

....aber auch der Punkt, an dem es immer wieder gefährlich wird.

Warum ist das so wichtig?

Antwort: weil nur dadurch ein Iterator durch eine Abstraktions-Barriere hindurch arbeiten kann. Der Aufbau eines komplexen Such-Algorithmus erfolgt meist in mehreren Layern, und man möchte eben nicht, daß irgendwo von unten Werte geliefert werden müssen, die dann oben »passen«. Sondern man möchte mehrere (oft virtuelle) Funktionsaufrufe aufeinander mappen. Und nicht zuletzt dadurch kann man geziehlt einzelne Werte in einem anderen Implementierungs-Realm zur Manipulation exponieren. Denn tatsächlich ist ein solcher IterExplorer eine View in eine State-Core

Im Normalfall liefert schon der Quell-Iterator eine Referenz (oder const-Referenz). Und die wird dann durch das Value-Tuple durchgemappt und zeigt eigentlich in die State-Core oder einen unterliegenden Container. Wenn aber an irgend einer Stelle tatsächlich ein Wert geliefert werden muß, so wird das auf diesem Level stets abgefedert, und die stabile Storage für den Wert liegt dann im Iterator selber (im ItemWrapper)

(ob dieserer test portabel ist....)

andere Tricks mit der _rawCore()-Zugriffsfunktion scheinen nicht zu gehen

...da alle Processing-Layer auf State-Cores beruhen, präsentieren wir dem darüberzulegenden IterExplorer die neue Core, und der findet dann die unter diesem Layer liegende CheckedCore des Vorgängers und strippt den neuen Layer weg. YESS es funktioniert!!!!!!

vorher:

IterExplorer<

  BaseAdapter<

    IterableDecorator<uint,

      CheckedCore<

        IterExplorer<

          IterableDecorator<uint,

            CheckedCore<

              Transformer<

                BaseAdapter<NumIter<int> >,

                uint

nachher:

IterExplorer<

  IterableDecorator<uint,

    CheckedCore<

      Transformer<

        BaseAdapter<NumIter<int> >,

        uint

NumIter hatte ich bei Gelegenheit aus Test-Code extrahiert, ohne weiter viel Gedanken darauf zu ver(sch)wenden....

Bestenfalls eliminiert der Optimiser ohnehin alle Indirektionen, aber auch alle Kopien. Wenn das aber nicht geht, weil es von irgendwo noch einen Zugriff geben könnte, dann sitzt man auf einem Speicherzugriff, wo andernfalls direkt ein Register verwendet werden könnte. Und wenn dann auch noch die Referenz non-const ist, versaut man sich leicht internen State und wundert sich dann... Nee, nee! Wenn man wirklich Seiteneffekte über Referenzen möchte, dann soll man sich das bitte explizit auscoden, aber nicht eine Library-Funktion kreativ nutzen

ich kann mir keinen Anwendungsfall vorstellen, wo so etwas nicht zumindest überraschend wäre.... Ja man könnte INT = custom magic type setzen, aber wie war das nochmal mit Magie und so?

Das heißt: in dem Moment wo wir die transformer-Funktion tatsächlich installieren, packen wir sie in ein std::function-Objekt mit einer neu synthetisierten Signatur. Diese ist (der Intention nach) paßgenau, d.h. sie nimmt den yield des vorgelagerten Iterators als Input

typedef typename IT::reference InType;

function<VAL(InType)> trafo_;

damals wollte ich erst mal schnell durch, wie üblich;

viele Jahre später habe ich dann den NumIter definiert, und das ist vielleicht das erste Beispiel, bei dem dieses neue Konzept wirklich ausgereitzt wurde. Ich habe so dunkel in Erinnerung, daß es dann nicht funktioniert hat, und ich deshalb „einfach“ das const in die eingebetteten Ergebnis-Typdefinitionen gepackt habe. Was ja nicht falsch war, aber der Weg des geringsten Wiederstandes

Und das ist mir zunehmend wichtiger geworden, vor allem nach meinen Erfahrungen mit dem monadischen IterExplorer v1. Grade weil man manchmal wirklich nur Values bieten kann, muß diese Hintertür offen bleiben. Andererseits wäre es ein gefährlicher Fehlschluß, daß eine Type-def »reference« hier „in Wirkklichkeit“ meint, das was der Iterator liefert. Der Schluß liegt nahe, und deshalb bin ich ihm hier auch verfallen. Aber wenn man mit derartigem Konzept-Mapping erst mal anfängt, dann verliert man schnell die Kontrolle

in iter-adapter.hpp

also exakt die zentrale Stelle, an der entschieden wird, ob eine nachgeschaltete Funktion auf den Resultat-Wert losgeht, oder direkt auf den Iterator-Typ zugreift

jetzt wirds aber brandgefährlich

...das würde bedeuten: man reicht eine weitere Typ-Def komplett durch die ganze Kette durch, und jeweils nur am Ende der Kette würde diese für den operator* gelten. Da wir aber Ketten im Builder auch noch nachträglich weiter verlängern, indem eine neue Core darüber gesetzt wird, ist das jedoch nicht ohne Weiteres realisierbar

...auch wenn es normalerweise kein Performance-Problem darstellt, weil der Optimiser redundante Aufrufe problemlos eliminiert, macht es die Typen für den Außenstehend noch viel undurchtringbarer, und belastet auch die Debug-Builds durch den Umfang unnötiger Typ-Information

das zwingt mich, einen inneren Quell-Layer in den äußeren Wrapper zu schieben; selbst wenn ich das Argument by-value nehme, ist es damit noch nicht in dem Objekt-Feld, wo es hingehört. Das bedeutet dann, entweder eine Kopie (und man ist vom Optimiser abhängig), oder eben ein move(SRC).

tatsächlich ist der Filter besonders gefährdet

weil der Filter selber ja nur den Feed pullt, aber selber keine Werte speichert; damit erzeugt er möglicherweise upstream obsolete Referenzen.

ich brauche einen engaged-State

...er läßt nämlich den ItemWrapper zunächst leer, und verwendet das für eine Lazy-invocation der Transformation

ungutes Gefühl...

das Design hat hier eine Schwachstelle

...dann würden wir eine ganze Pipeline »flippen« können, wenn der erste Result-Typ auf Value schwenkt

habe die mehrfachen Adapter nun allesamt weg; daher wäre es nun doch denkbar, da die Call-Pfade nun von einem Punkt aus aufspalten (nämlich Core::yield ->  entweder operator* oder operator-> )

Booo!  is_const_v<T&> ist immer false

ruck-zuck ein paar Stunden weg ... hätte man doch gleich einen Test geschrieben

  • dediziertes Trait schaffen
  • Propagations-Logik explizit auscoden
  • den operator-> auf Values weglassen bzw mit compile-Fehler unterdrücken

...da bisher alles auf Rückgabe by-ref ausgelegt ist, und dieser Fall deshalb durch die Änderung ohne Modifikation durchgereicht wird

es wird vermutlich auf wenige Fälle beschränkt bleiben, in denen es aber durchaus hilfreich ist, da sowohl Generatoren als auch Transformatoren einfacher zu schreiben sind

der auto-Expander kann und darf ja nicht wissen, wo genau unterhalb die Expansion stattfindet; wenn eben gar keine stattfindet, dann bleiben alle durchgereichten expandChildren()-Aufrufe ohne Wirkung, aber ein iterNext() wird dann auch nicht mehr gesendet

ich kann mir keinen Fall vorstellen, in dem der Default gelten könnte; vermutlich habe ich den Default damals angeschrieben, als ich mir das Konzept neu ausgedacht habe, und mir noch nicht klar war, wohin das führt (und wie bedeutend das mal wird....)

...und zwar in LinkedElements.

Das ist nicht irgend ein Randfall, sondern eine der zentralen Nutzungsmöglichkeiten: um nämlich einen iterator und einen const-iterator aus der gleichen Core zu erzeugen

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

man muß dafür nun i.d.R. etwas coden und die (stets vorhandenen) Value-Type-Bindings extrahieren, oder meta::Yield<IT> verwenden

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

Nebenbei bei der der Entwicklung eines tuple-zipping-Iterators; da ist mir aufgefallen, daß über weite Strecken der Ergebnistyp durchgereicht wird; einziger Knackpunkt wären Filter und Transformer, die ihre Argumente by-Ref nehmen; ich gehe aber davon aus, daß solche Funktoren ehr für den Einzelfall geschrieben sind — und in den allermeisten Fällen liefern wir ja weiterhin by-ref

in value-type-binding.hpp

Diese Gefahr resultiert exakt aus seiner Stärke, daß er nämlich jede beliebige Funktion adaptieren kann, also auch eine Funktion, die Referenzen liefert. Wenn diese Referenzen in internen State der Iterator-Pipeline zeigen, dann werden sie dangling, sobald man die Pipeline verschiebt. Und das passiert leider bereits bei der Konstruktion. Wenn dann hinter dem Transformer noch ein Filter hängt, nimmt das Verhängnis seinen Lauf

  • 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

matchAtStart(str, regex) : partieller Match am Anfang verankert

wichtig für Text-Parsing, wo man bestimmte Tokens akzeptieren  möchte, aber durchaus ein Rest »dahinter« übrig bleibten darf.

top-Level-Einstiege bieten ⟹ Präfix accept_

not from first principbles...

Es geht um eine konzise Abkürzungs-Notation für den gelegentlichen Gebrauch zum Parsen einer Spezifikation. Also um die paar wenigen Dinge, für die eine Regular-Expression nicht genügt; anders gesagt, ich möchte eine bequeme Erweiterung von Regular-Expressions um LL1-Ausdrücke. Und zwar in einer Form, die man möglichst in ein paar Definitionszeilen „mal eben hinschreiben“ kann.

  • keine pfiffige Operator-Syntax, die man sich nicht merken kann
  • sondern eine Hülle als Builder, die die definition direkt leitet
  • Aufbau anhand einiger praktischer Beispiele
  • abschrägen bis es paßt

Bekanntermaßen ist die Fehlerbehandlung die Stelle, an der jedes schöne Design eines Regelsystems zusammenbricht. Deshalb kümmern wir uns erst mal um die schönen Dinge.

anders als in einer funktionalen Sprache stellt sich aus Sicht der imperativen Verarbeitung sofort die Frage, wie die Beweglichkeit bzw die Vielgestaltigkeit verschiedener Elementar-Parser unter ein gemeinsames Verarbeitungsschema gebracht werden kann (das dann auch noch akzeptable Kosten hat). Da ich hier vor allem auf Gelegenheits-Gebrauch ziehle, also auf eine Situation, wo beiläufig gewisse syntaktische Strukturen bezeichnet werden sollen, betone ich vor allem die Optimierbarkeit der Syntax-Definition. Das führt zu der Forderung, Indirektionen so lange wie möglich zu vermeiden oder verzögern.

Also: keine std::function

Es erscheint mir als Pedanterie, wenn ich nun einen extra terminal-Builder-Schritt einführen würde, nur um das "bin fertig mit der Definition" zu markieren. Dazu gibt es (cost) Variablen-Definitionen. Außerdem sehe ich schon das Thema mit den explizit konfigurierten Fehlermeldungen, und dafür würde ich noch eine separate Fehlerbehandlungs-Spec brauchen. Eben diese Art von weitverzweigtem, feingranularem Framework wollte ich vermeiden. Daher neige ich zu einem Design, in dem der Parser stateless ist, die Syntax-Spec aber stateful, weil sie dann auch gleich noch das Model enthalten kann, auf das sie ohnehin stringent getypt ist. Dafür nehme ich in Kauf, die Syntax-Definition (also die eigentlichen Parser-Combinators) mit dem Auswertungs-Zustand zu vermischen, grade auch, weil man auf diesem Weg wieder in die Definition von Sub-Klauseln einsteigen kann

Wenn man schon ein zustandsbehaftetes Objekt akzeptiert, könnte auch Zuweisbarkeit bequem sein (man muß dann ja ohnehin aufpassen). Allerdings steht das im Konflikt mit dem Ansatz einer fein-granularen Typisierung, welche die Modell-Struktur abbildet. Und die Entscheidung, die recursive-descent-Struktur als parse-λ einzubetten, verhindert in den meisten Fällen die Zuweisbarkeit, denn man kann (und darf) nicht wissen, was in der Closure steckt

....in der weiteren Entwicklung zeigte sich, daß der Ansatz mit den direkt aufgegriffenen λ-Typen seine Grenzen findet, sobald die Syntax-Klauseln rekursiv werden; in diesem Fall müßen wir explizit die Typisierung abschneiden. Das ist ein Kompromiß, den ich für angemessen halte

...das ist eine Grundentscheidung, vor allem motiviert durch die einfachere Implementierbarkeit (aber auch unterstützt dadurch, daß ich mir in vielen relevanten Fällen eine einfachere, fluidere  Verwendung erhoffe). Das hat die wichtige Konsequenz, daß wir nicht mit Variadics arbeiten, sondern die Summen- und Produkttypen schrittweise aufbauen

Das heißt, es gibt keinen explizit gespeicherten »Binding-Funktor«, sondern stattdessen eine spezielle Kompositions-Mechanik, mit der man eine Transformation an die Parse-Funktion innerhalb des Optional  realisieren kann. Also ein monadisches Muster hier

Die Parser-Kombinatoren sollen also nicht direkt vom Benutzer angefaßt werden, wodurch sie auch keiner besonderen Absicherung bedürftig sind

separater Namespace sinnvoll (util::parse)

...da für den intendierten Nutzen typischerweise Syntax-Spezifikationen bei den Basis-Konstanten und Definitionen einer anderen Einrichtung mit abgelegt werden, und in den meisten Fällen der Einstieg erfolgen kann per util::accept

Das ist eine grundsätzliche Entscheidung, die direkt aus den explizit aufgegriffenen und eingebundenen λ-Typen folgt:

  • dieses Framework baut selber keinen Syntaxbaum auf
  • dafür aber verwenden wir Metaprogrammierung, um aus den Builder-DSL-Aufrufen einen strukturierten Model-Term aufzubauen, der dem Aufbau der Syntax folgt

...man ist also nicht in die komplexen, verschachtelten Strukturen hineingezwungen, sondern kann an strategisch günstiger Stelle in einen eigenen Model-Typ übersetzen; für die unterstützung rekursiver Syntax-Klauseln erweist sich das sogar als essentiell

Aus Sicht der technischen Konstruktion erschien es mir erst sehr naheliegend, die DSL-Syntax weitgehend auf Postfix-Operatoren aufzubauen. Das erwies sich als Trugschluß, denn solche Konstrukte sind in der praktischen Anwendung schwer zu durchschauen. Daher habe ich die Präferenzen unterwegs geändert und die DSL so umstrukturiert, daß sie eine Reihe freier Funktionen als Einstiegspunkt bietet, und diese freien Funktionen auch dazu dienen sollen, verschachtelte Sub-Syntax-Klauseln einzuleiten

Rekursion ist ein essentielles Element jeden echten Parsers; sie ist aber auch komplex (und potentiell nicht-terminierend). Daher sollen die meisten Anwendungsfälle durch vorgefertigte Syntax-Elemente (Repetition und Klammerung) abgefangen werden. Für die sonstigen Fälle verlangen wir vom Benutzer etwas Vorarbeit: der Model-Result-Type muß in diesem Fall durch ein Binding reduziert werden, so daß das Ergebnis einer rekursiven Referenz bereits feststeht. Damit konnte an der Stelle (mithilfe von std::function) eine elegante Lösung gefunden werden, die nach außen nahezu unsichtbar bleibt:

  • der Benutzer muß rekursiv zu referenzierende Klauseln vordefinieren, mit angegebenem Ergebnistyp
  • später muß in der Definition der Klausel am Ende ein Model-Binding stehen, das genau diesen Result-Typ liefert
  • schließlich wird diese Definition dem pre-deklarierten Syntax-Objekt zugewiesen

das ist mehr als eine Policy: Struktur-Bindeglied ⟹ Connex

das ist essentiell für den Vorgang des Parsens: es wird jeweils ein Präfix-Match gesucht, dann »akzeptiert« und mit dem Rest dahinter weitergemacht — wobei allerdings sich dieser »Rest« erst durch den Match überhaupt definiert. Leider bieten die Regex-Operationen nur ein find (mit beliebigem Match irgendwo) oder match (auf die ganze Sequenz).

das löst das Problem, ist aber keine gute Lösung; denn das ist ein subtiler Punkt, den man dem User überlassen muß. Man könnte noch versuchen, diese Verankerung am Anfang automatisch mit dazuzubauen, was aber nur geht, wenn die Regular-Expression als String-Definition geliefert wird. Also ungeschickt wie man's dreht

...woduch der Code ziemlich verwirrend wird  ⟹  idealerweise wäre die gesamte Combinator-Logik in den buildConnex()-Overloads

ist viel näher an der Grundidee aus der Funktionalen Programmierung, und dennoch in einer low-Level-Funktion, mit der man direkt nix anfangen kann. Denn darauf läuft das Design ja immer mehr hinaus: ein eingekappseltes Kombinator-Framework.

wenn das Vorgänger-Element eine passende Model-Variante ist (tuple, AltTypes, array), dann wird an dieses angebaut

Angenommen, die Syntax sieht wie folgt aus:

'(' num '+' num ')' '*' num

Wenn der geklammerte Ausdruck als sub-Syntax formuliert ist, würden alle Model-Elemente in ein einziges 7-Tupel nivelliert, obwohl man eingentlich ein 3-Tupel mit verschachtelung wollte

...da wir keinen expliziten Binding-Funktor speichern, sondern ihn in die Parse-Funktion einarbeiten, kann auch der Ergebnis-Typ durch eine weitere solche Komposition geLIFTet werden.

Das heißt, die Operation zur inkrementellen Erweierung erkennt den gruppierenden Summen/Produkttyp im Parse-Ergebnis und wird demenstprechend, »Klammer schließen« oder »anhängen«. In der Implementierung der Verknüpfungs-Operation liegt also eine Fallunterscheidung oder ein double-Dispatch vor, analog zum Pattern-Match in der funktionalen Programmierung. Soweit so gut.

Beispiel

  accept(

     accept("x").seq("y")

  ).seq("z")

⟹ wird zu...

  Syntax( 'x' 'y' | tuple<Model(x), Model(y)> )

      .seq("z")

⟹ wird zu...

  Syntax( 'x' 'y' 'z' | tuple<Model(x), Model(y), Model(z)> )

⟺ äquivalent zu...

  accept("x")

     .seq("y")

     .seq("z")

...denn das wäre nahe an der Stelle, wo's zur Erkennung gebraucht wird, und ist damit auch ziemlich tief in der Implementierung verborgen

will sagen, wer ein solches zweimal verschachteltes accept(accept()) anfängt, und dann nicht in einen Produkt / Summenterm einsteigt, ist selber schuld

man könnte es sogar nur im Result-Typ verstecken

...was zwar auch etwas grenzwertig wäre, aber noch vertretbar, wenn es als eine Tagging-Subklasse realisiert ist. Dann ist zwar die Bedingung gebrochen, daß Connex::Result ≡ parse-Funktion-Result, aber ersteres wäre eine Subklasse und somit noch kompatibel

Ansatz: ModelJoin<R1,R2>::Result

nebenbei: namespace parse einführen

...weil wir die Typen ja permanent umbauen;

  •  man müßte hier massiv mit Typsequenzen programmieren, um eine präzise, erwartete Signatur zu konstruieren;
  • alternativ könnte man in die _Join - Rebinder-Templates noch eine explizite cons-Funktion mit aufnehmen (dann hätte man aber das gleiche Problem hier)
  • oder man könnte alle Argumente durch eine lift()-Hilfsfunktion schieben, die dann aber mit den möglichen Models verkoppelt wäre

Das wäre schon denkbar, da wir alle Aufrufe hier kontrollieren — und diese Model-Klassen nicht außerhalb des Parsers verwendet werden sollten, sondern nur in den Combinatoren selber und (lesend) in Binding-λ.

  • Im Fall des SeqModel gibt es noch ein zusätzliches Sicherheitsnetz, da wir von einem explizit getypten Tupel erben.
  • Im Fall des AltModel gibt es kein direktes Sicherheitsnetz, weil wir »blind« in eine Variant-Storage schreiben werden. Man sollte dann noch einen Check auf den Selektor realisieren

Das ist eine mehrstufige Kette

  • aus der Aufrufstruktur ergibt sich die ID des Branches
  • gleichzeitig ergibt sich damit die Position des Typ-Arguments
  • dieses greifen wir uns per Typ-Sequenz-Manipulation
  • und verwenden es im generierten Konstruktor

⟹ Resultat: man kann in den jeweiligen «Slot» nur mit einem kompatiblen Typ rein, und Typsicherheit ist gewährleistet (im Parser; wenn ein Client falsch zugreift, ist er selber schuld)

oder mit \\b

bei der Sequenz wurde die gleiche Annahme gemacht, und daß der Optimiser das schon wuppen wird

warum? weil eine Folge zunehmend variantenreicherer Summen-Typen entsteht, und die Puffergröße ist das Maximum des benötigten Platzes

das ist ähnlich zu einer persistenten Datenstruktur: man fragt den jeweiligen Vorgänger-Datentyp mit dem dekrementierten Selektor an; ist der Selektor 0, wird der dort fest gecodete Typ-Zugriff verwendet

das heißt, die Operationen führt stets derjenige partielle Summen-Typ aus, bei dem der aktive Zweig auf der Top-Position (am Anfang) steht; der aktuelle Selektor-Wert legt also das »Stockwerk« fest, auf dem gearbeitet wird

...weil der Compiler ja nichts über den Selektor-Wert weiß, muß er in jedem Fall die gesamte rekursive Kette instantiieren; das Ende der Typ-Sequenz stellt also eine zweite Abbruchbedinung der Rekursion bereit, und diese muß so in die Kontrollstruktur eingebunden sein, daß die rekursiven Instantiierungen wirklich aufhören

Fazit: mit Einschränkumg umsetzbar...

Und zwar im Bezug auf allgemeine Handwerksregeln...

  • man könnte ohne Basisklasse auskommen
  • der opaque-Bufer wäre als Implementierungsdetail direkt im private-Scope
  • es wird nur eine einzige Klasse (pro Typ-Signatur) tatsächlich instantiiert

Das fängt schon damit an, daß man doch noch getemplatete Funktionen für die Elementar-Operationen schreiben muß (zumindest in C++17). Dazu kommt dann diese doch einigermaßen trickreiche generische select-Operation

...weil praktisch alle Kern-Methoden nun in zweifacher Ausfertigung gecodet werden müssen: einmal rekursiv, und einmal für den Abschluß

...denn mein Compiler weigert sich, eine Argumentliste für die Spezialisierung mit dem Parameter-Pack zu beginnen (ich versteh den Grund nicht wirklich, denn ich dachte, in anderen Fällen wäre das durchaus möglich gewesen, denn die Spezialisierungen wären eindeutig

konkret stellt das zufälligerweise kein Problem dar

das erscheint zunächst wie »glückliche Umstände«;

bei genauerer Überlegung wird aber klar, daß ich diese Einsicht bereits intuitiv angewendet hatte, als ich von Vornherein auch ein Selektor-Feld mit disponierte; es ist nämlich so, daß wir uns vermöge des Selektor-Feldes genau diese explizite Info zur Compile-Zeit beschaffen können, indem der aufrufende Code ein switch-case auf den Selector macht.

das im Prototyp entworfene select(λ) ist ein Variant-Visitor

template<typename TX, typename...TS, class FUN>

void

select (size_t slot, FUN&& fun)

  {

    REQUIRE (slot <= sizeof...(TS));

    if constexpr (sizeof...(TS))

      if (0 < slot)

        select<TS..> (slot-1, forward<FUN>(fun));

    fun (access<TX>());

  }

Das funktioniert nur wenn man einige subtile Details richtig hinbekommt....

  • die Abbruchbedingung muß für den Compiler sichtbar sein, daher als constexpr-if zu formulieren
  • der Funktor oder das Lambda muß wirklich per offenem Template-Argument übergeben werden, weil es tatsächlich selber ein Template sein muß (generische Lambda bieten hierfür eine abgekürzte Schreibweise)
  • der Zugriff ist trotzdem komplett ungeschützt
  • man muß also anderweitig dafür sorgen, daß die slot-# tatsächlich korrekt belegt ist

wenn man ohnehin eine bestimmte Syntax parsen möchte, ist es naheliegend und natürlich, auf den vorliegenden konkreten Zweig zu prüfen und zu verzweigen; in dieser Form erscheint das eine vertretbare und nicht weiter gefährliche Herangehensweise. Dieser Ansatz kann allerdings durchaus in einen schwer wartbaren Zustand abgleiten, wenn die Syntax komplex und heterogen strukturiert ist; dann muß man die Varianten mehr oder weniger im Kopf haben. Für dieses Problem sehe ich keine allgemeingültige Lösung...

Das ist eine schöne neue Möglichkeit, die sich durch die generischen Lambdas auftut; durchaus möglich, daß in manchen Fällen eine solche Formulierung eine drastische Vereinfachung darstellt, beispielsweise wenn alle Zweige jeweils einen RegExp-Match mit äquivalent angeordneten Capture-Groups beinhalten; die Einzelfälle würden damit sozusagen transparent verschmolzen.

Fazit: implementierbar mit generischem λ-Visitor

weil ein Objekt ohne VTable mit seinem ersten Member beginnen muß, size_t der »slot«-Größe entspricht und der Storage-Puffer stets direkt dahinter ist

...hier ist die Logik umgedreht

  • für den Produkt-Fall müssen alle Zweige erfüllt sein; erst danach wird das Model gebaut bzw. um die neuen Zweige ergänzt. Sonst fallen wir leer raus
  • hier kann jeder der Zweige greifen, nur wenn kein einziger Zweig greift, fallen wir leer raus. Das heißt, hier müssen wir bereits in den einzelnen Zweigen je nach Fall ein unterschiedliches Modell aufbauen

linker Zweig: ein sub-Model, in dem irgend ein Zweig gematched hat

Der Argument-Pack muß stets am Ende stehen

Grundidee: man baut die neue, umgebaute Typ-Sequenz in den variadischen Argumenten eines beliebigen Templates, das selbst als Template-Template-Parameter gegeben wird. Damit kann man unmittelbar in einem einzigen Zug das redefinierte Ziel-Template konstruieren, ohne erst in eine andere Verarbeitungs-Domäne (tuple, Typsequenz, Typliste) mappen zu müssen. Zudem kann das gleiche Verarbeitungs-Template auch Spezial-Belegungen für Hilfs-Operationen mit anbieten, und man kann gleich die häufigsten verwandten Tools in einer einzigen Definition zur Verfügung stellen.

da zeichnet sich ein Schema ab, das die bekannten Sequenz-Umordnungen sehr direkt ausführt, ohne erst in eine andere Repräsentation (wie Typelist) zu mappen. Trotzdem ist der Aufwand O(n), für das Umkehren der Sequenz sogar O(n²)

...Hinweis darauf ist der Umstand, daß ich gar nicht mehr viel auswerten / prüfen muß, sondern direkt der Match auf die Konstruktor-Argumente den Rest der Logik erledigt.

da geht nämlich eine Branch-ID ein, und die muß <= TOP sein

wenn am Ende keine weitere Iteration akzeptiert werden kann, ist das kein Fehler, sondern wir stehen hinter der zuletzt akzeptierten Iteration

ein Trenner muß nicht gegeben sein (dann wird lediglich der Rumpf iteriert); wenn aber ein Trenner gegeben ist, dann wird er beim 1.Mal explizit übersprungen (darf also nicht da sein), bei allen anderen Iterationen wird er zu Beginn der Iteration erwartet

man kann sich Situationen denken.... aber dann hätte man auch stets dieses Ergebnis-Tupel zu handhaben.

weil wir bisher keinen generischen Rekursions-Mechanismus vorsehen und ansonsten die Zahl der Iterationen erst in einem Post-Proecssing-Schritt geprüft werden könnte.

Es würde sich um einige Randfälle handeln, denn im Regelfall ist eine Iteration offen / abzählbar. Und wir müßten in eine derart performance-kritische Situation vorstoßen, in der eine Heap-Allokation prohibitiv wäre

dem inzwischen etablierten Schema der Syntax-Klauseln zufolge muß jeder Junktor etwas mit der aktuellen Syntax-Spec anknüpfen. Wenn nun ein iter()-Prädikat essentiell eine neue Syntax anfängt, wäre eine implizite Konvention zu treffen, was mit der bestehenden Klausel passiert. Naheliegend wäre eine Sequenz; dann besteht aber die Gefahr, daß in der Praxis oft eine leere Sequenz spezifiziert wird (wäre noch akzeptabel) — und man zur Model-Anknüpfung stets dieses Tupel aufmachen müßte (schröcklich)

dann muß man aber die Postfix-takes-all-Logik akzeptieren

stets mit Präfix accept_

der Implementierung nach ist es ein Dekorator

....muß dann aber durch die Entscheidungs-Logik sicherstellen, daß dann auch die zugehörige schließende Klammer entweder erwartet, oder übersprungen wird.

nebenbei abgefallen

das muß tatsächlich ein Postfix-Operator sein

Vorschlag: bind(FUN)

damit klar gesagt wird, wenn die Funktion nicht den bisherigen Result-Typ nimmt

Standard-Variante: bindMatch(n)

...denn es steuert die Art der Dekoration

analog wie die anderen Combinatoren und buildConnex()

nachdem ich die Model-Fälle wegdiskutiert habe ☺

es bräuchte für alle erdenklichen Fälle einen Pfad, um auf einen String zu kommen; also bräuchte es sowas wie einen operator string(), oder man müßte rekursiv in alle Teilkomponenten hinein mappen; und was dann mit Komponenten, die bereits explizit transformiert wurden, wie erkennt man die, und was macht man mit denen??

wozu will man das? doch nur für Tests.

Für eine reale Anwendung sollte man möglichst tief unten mappen, und bäuchte auch ein Konzept, um auf einen gemeinsamen Ergebnis-Typ zu kommen, möglicherweise dann doch so etwas wie einen AST. Und wenn man es dann doch wirklich bräuchte, kann man's immer noch nachrüsten

Die C++ »structured bindings« funktionieren für Arrays, für tuple-like  und aber auch für einfache PODs. Wenn std::tuple_size ein incomplete-type  ist, dann versucht der Compiler ein Binding auf Struct-Felder, scheitert aber daran, daß es eine nicht-triviale Basis-Klasse gibt (und damit die Feld-Nummer nicht mehr offensichtlich klar ist)

...rein nach Bauchgefühl dürfte das in der Praxis dann doch nicht so schlimm werden, sofern man konsistent jede Klausel auch mit einem Binding ausstattet, und die Ergebnistypen systematisch aufbaut. Schließlich ist das ja auch die Aufgabe schlechthin beim Parsen

ganz banal: habe eval.consumed nicht weitergegeben

d.h. jede sub-expression setzt wieder am Anfang auf

....weil eine rekursive Definition im Prinzip offen ist und im Extremfall auch tatsächlich nicht terminiert; in Haskell könnte man einen solchen Typ anschreiben, in C++ nicht (weil Typ-Ausdrücke eager aufgelöst werden)

...denn was wir abschneiden ist nur die komplett ausformulierte Struktur des Typs; an der Stelle, an der eine andere Klausel rekursiv eingebunden wird, greifen wir nur deren Ergebnis-Model auf.

der Lambdas wegen

das heißt, es würde eine Art late-Binding notwendig

und später an diese zuweisen

das impliziert Heap-Storage für das Parse-λ

...denn sonst dürfte es kaum möglich sein, einen explizit angebbaren Result-Typ zu konstituieren; in diesem Result-Binding steckt der eigentliche Ansatz, mit dem eine offen-rekursive Grammatik dennoch handhabbar wird, denn es muß eine Art Reduktion der Komplexität erfolgen, beispielsweise indem sofort ein Ergebnis ausgerechnet wird

Konsequenz ⟹ das Einbinden in andere Syntax ist speziell zu behandeln

....weil ja das Einbinden technisch nichts anderes ist, als eine Dekoration (und damit unterbunden würde)

diese andere Syntax hätte dann aber auch einen anderen Typ und müßte in einer anderen Syntax-Variablen gespeichert werden;

Variante-1 ist »filosofisch« und praktsich attraktiv

...weil sie der „dann mach's halt nicht falsch“-Haltung entspricht, die diesem ganzen Parser-Framework zugrunde gelegt wurde; und ganz praktisch: man bekommt diese Variante geschenkt, alles funktioniert von selber so wie es soll — und wenn irgendjemand unbedingt dekorieren möchte, dann soll er halt

Weil das, was man nun zusätzlich machen könnte, nur auf Basis der Implementierung verständlich ist, aber für jeden Benutzer ziemlich verwirrend

da sie ohnehin das erlaubt was man machen sollte, aber Fehl-Verwendungen unterbindet

da man ja dennoch irgendwie auf diese Funktion Bezug nehmen kann, indem man sie in andere Sytnax einbaut, ist die Abgrenzung zum »Dekorieren«  nicht klar

...sie bestünde darin, die Referenzen nach der Zuweisung zu materialisieren;  aber die Schwierigkeit besteht darin dieses Linken auszulösen, da die ganze DSL darauf abstellt, Funktionen beliebig ineinander zu verschachteln, und damit sehr viel zu kopieren; man müßte dann entweder eine komplette Link-Infrastruktur hochziehen (Parser-Funktionen wären speziell als noch ungelinkt markiert und es gäbe einen separaten Call-Chain), oder man müßt das Binden/Materialisieren beim ersten Aufruf machen, was in der Praxis nicht sonderlich hilfreich ist

FUN ≡ std::function &

Connex-Definition

erfüllt das bereits

da viele Builder-Funktionen in ein neues Syntax-Objekt schieben

es sei denn, man würde Connex komplett aufdoppeln für den Referenz-Fall (wobei ärgerlicherweise wirklich aller Code identisch wäre, bis auf eine Variante im Konstruktor). Natürlich habe ich auch versucht, nur den Konstruktor allein zu spezialisieren, das ist mir aber nicht gelungen

ausdefinierte Syntax per Value (!) nehmen

...es fällt mir schwer, keine Fehler-Checks zu machen, aber ein kurzer Versuch zeigt, daß diese so einiges Metaprogramming erfordern würden — und ich habe generell beschlossen, hier keine Parser-Library zu entwickeln, sondern nur Abkürzungen für einfache Parse-Tasks,  die jemand wie ich auch von Hand (per Rekursive-descent) schreiben könnte. In dem Fall bin ich also mal arrogant und warte, was passiert, denn ich kann mir nicht vorstellen, daß man dieses Framework ohne gewisse Erfahrungen mit Parsern verwenden kann...

E ::= T [ + E ]

T ::= F [ / F ]

F ::= ( E ) | V

V ::= num   |  num

Spezialfall hier: homogenes Model

....denn nur so bekommt man den Aufwand in den Griff;

ich demonstriere auch die (beabsichtigte) Anordnung im Quelltext, indem die bindings in eine Spalte rechts geschrieben werden; jede, wirklich jede Syntax-Klausel sollte den beabsichtigten Ergebnistyp haben (hier double), sonst läuft dieses Schema aus dem Ruder

...war ja, daß ich eine Signatur einer Render-Node definieren und später zerlegen möchte, wobei in den Argument-Listen möglicherweise Typ-Ausdrücke der Sprache C++ stehen könnten (wenn man später mal diese Node-Spec halb-automatisch generiert)

das heißt, das akzeptiert beliebige Zeichen, nur nicht die speziellen Zeichen, die eine Quotation oder Klammerung auslösen oder beenden könnten

Beobachtung: beide haben zwei führende Leerzeichen

das liegt daran, daß wir diese Info nicht aufzeichnen, weil generell das Akzeptieren (und Backtracking) durch rekursiven Funktionsaufruf auf Substrings realisiert ist...

...der diese Entwicklung eines Parser-Frameworks angestoßen hat

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)

Verstehe die Doku so: wenn directory existiert ⟹ kein Fehler

if -1 is returned, no directory shall be created.

ja man könnte.... aber welches Risiko soll damit adressiert werden (da es ja nun doch zusätzliche Kosten in der Form von Nonportabilität beinhaltet)? Immerhin ist die ganze Sache mit den Zufallszahlen sowiso mehr oder weniger professionelle Paranoia, denn 2^64 ~ 10^19 ist schon eine Menge

JAGNI

die gestern implementierte Zufalls-Sequenz entropyGen.u64()

Rückgabewert von create_directory

+ weitere Eigenschaften prüfen

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

Das hat sich über längere Zeit herauskristalisiert.

  • zunächst war das die einzige Möglichkeit, überhaupt in meta-Generierung von Typen einzusteigen
  • dann dachte ich, es wird durch die Variadics und Lambdas obsolet werden, und hab schon teilweise mit dem Rückbau begonnen
  • doch dann kam die Ernüchterung: für Variadics bin ich einfach zu blöd — dafür muß man andauernd rückwärts denken.
  • viele Jahre habe ich nun Erfahrungen gesammelt, was mit welcher Technik am besten geht; habe Helper für Tuples gebaut, die dann doch nicht gebraucht wurden
  • 2025 für die Umstellung der Render-Engine auf Tuples und strikte Typisierung war ich erneut mit den »tuple-likes« konfrontiert. Ich hab mir wieder durch Einschieben einer Abstraktion geholfen, welche allerdings nur mit Typ-Sequenzen einigermaßen sinnvoll zu implementieren ist. Damit gibt es nun drei Systeme zusammenhängender Metaprogramming-Helper, und alle werden wohl erhalten bleiben.
  • habe daraufhin begonnen, Header umzuordnen, um die Include-Linien möglichst sauber zu bekommen.

Es ist klar, daß die Loki-Typlisten und Sequenzen erhalten bleiben, und in Zukunft sogar mehr zum Einsatz kommen, da ich nun im Bereich der Render-Engine viel mit Parameter-Tupeln arbeite.

...wie bekommt man dann den konvertierten Wert 'raus.

Visitor ist entweder void, oder bool

soll sowohl einfache Tyen, alsauch »strukturierte Typen« (tuple-like) akzeptieren

Konkret: man soll ein Binding auf eine Render-Node herstellen können von einer Funktion, die einfach einen Buffer-Pointer braucht. Aber aman soll auch Funktionen einbinden können, die zig verschiedene Eingabe- und Ausgabepuffer wollen, und dazu noch eine Menge an Parametern, die dann vielleicht auch noch per Automation zu versorgen sind

...denn die weitere Node-Invocation beruht auf rekursiven Aufrufen in Vorgänger-Nodes, die dann jeweils ein BuffHandle abliefern. Sowas möchte man schmerzfrei in eine Art Array abstellen können (wenngleich es auch in der Praxis als UninitialisedStorage implementiert ist)

deshalb ist es sinnvoll, einen λ-closed Code-Block für jeden Index zu instantiieren

lib::meta::enable_if_TupleProtocol<TY>

damit kann man von jedem »Invocable« eine Signatur abgreifen

...denn man verwendet diese Metafunktion ja stets explizit mit einem gegebenen Typ, und im Falle eines Ausdrucks muß man noch einen decltype() darum wickeln. Mögliche Probleme:

  • der Ausdruck in decltype() ist syntaktisch gar nicht valide (z.B. Scope::member  bei einer Member-Funktion)
  • der sich ergebende Typ ist keine Funktion, und deshalb sind die nested-typedefs (Sig, Args...) nicht vorhanden ⟹ compile Fehler bzw. SFINAE

Beispiel: explore(elements).transform(....irgendwas....)

has_Sig sollte keine Compile-Fehler auslösen, wenn der gegebene Typ überhaupt nicht Funktions-artig ist (⟹ denn dann ist die Aussage trivialer Weise wiederlegt; was gar keine Signatur hat, kann auch nicht eine bestimmte Signatur haben). Realisieren kann man das über den bool-Check, den ich vor einiger Zeit bereits in lib::meta::_Fun eingebaut habe (im Zusammenhang mit IterExplorer)

Warum Macro? damit man per STRINGIFY() einen lesbareren Hinweis in die Static-Assertion bekommt

der Rückgabetyp muß explizit deklariert werden — ähnlich wie decltype(auto) — denn es kann sein, daß die zusammengesetzte Funktion eine Referenz liefert

...also eine Funktion, der man andere Funktoren gibt, und diese baut einen manipulierten Funktor. Sowas ist extrem nützlich...

also die Möglichkeit, Argumente durch Funktions-artige »Binder« zu schließen, die dann aber erst zum Aufruf-Zeitpunkt aktiviert werden. In verallgemeinterter Form könnten das beliebige passende Funktoren sein, die auch weiterhin Argumente benötigen — und es müßte ein neuer Funktionstyp synthetisiert werden (klingt schlimmer als es tatsächlich umzusetzen ist, da wir so viele Tuple-Manipulationsfunktionen haben)

warum? weil das leichtgewichtiger und inline-freundlicher ist. Ein λ ist ja erst mal eine anonyme Klasse mit jedweder Storage direkt inline; es läßt sich per Kopie und Verschiebung handhaben. Wer damit Probleme hat, kann sich das resultierende Lambda immer in eine Funktion packen, und den dafür passenden Funktor-Typ sollten wir auch als Metafunktion anbieten. Der Nachteil (oder auch Vorteil) von std::function ist, daß das ein Objekt fester (relativ begrenzter) Größe ist, und notfalls automatisch ein Erweiterungsspeicher auf dem Heap mitgeführt wird.

ist aber im Rückblick nicht mehr so ganz klar, was das Problem war. Die Zielsetzung erscheint etwas widersprüchlich... ging es um perfect-forwrading, um move-only-Funktoren, wollte ich Referenzen binden können?

definiert in <functional>, ca. Zeile 280

alle Initialiser werden per perfect-forwarding genommen

im Placeholder-Typ wird die Index-Position gespeichert

und zwar als Signatur Fun(Arg....);

dabei sind die Arg entweder exakt die Typen, wie sie gebunden und damit im Binder gespeichert sind, oder die Placeholder, die in ihrem Typ die Position aus dem verbleibenden Arg-Tupel codiert halten.

commit 03b17c78da67b8cdba014773fa99736f2f9ed8b5

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Dec 16 23:01:57 2024 +0100

    Buffer-Provider: investigate Problem with embedded type-constructor-arguments

   

    This is a possible extension which frequently comes up again during the design of the Engine.

    Basically, the `TypeHandler` in the metadata-descriptor used by the `BufferProvder` could capture

    additional context-arguments, which are then later passed to an object instance embedded into the buffer.

   

    Yesterday I attempted to use this feature for a simple demonstration in `NodeBasic_test`,

    just to find out that passing additional constructor arguments to the capture fails with

    a confusing compilation error message. This failure could be traced down to the function binder;

    and what at first sight seemed to be a compiler error, turned out to be a quite logical limitation:

    When we »close« some objects of the constructor, but delay the construction itself, we'll have to

    store a copy in the constructor-λ. And this implies, that we'll have to change the types

    used for instantiation of the compiler, so that the construction-function can be invoked

    by passing references from the captured copy of the additional arguments.

   

    When naively passing those forwarded arguments into the std::bind()-call,

    the resulting functor will fail at instantiation, when the compiler attempts

    to generate the function-call `operator()`

   

    see: https://stackoverflow.com/q/30968573/444796

....durch std::apply und std::invoke

Achtung: perfect-forwarding in das Tuple-Remapping hinein wäre gefährlich

Habe das im Detail analysiert.

  • vor dem Binding wird ein remappetes Tupel konstruiert, bei dem u.U einzelne Werte durch Placeholder ersetzt wurden (und andere Werte an andere Positionen gingen)
  • dies baut auf TupleConstructor / ElmMapper auf. TupleConstructor nimmt das Tupel per-Value, ElmMapper reicht darauf eine Referenz
  • das ist essentiell, weil dadurch ElmMapper völlig frei ist und beliebige Mappings, auch mehrfach-Mappings realisieren kann
  • würde in dieses Vorstufen-Tupel eine RValue-Referenz gelangen, und würde man im ElmMapper eine RValue-Referenz durchreichen, dann würde jeder Wert sofort konsumiert.

Diese Anmerkung mache ich Juni 2025, als ich versucher herauszufinden, warum ich in den letzten zwei Jahren umgebaut habe, und warum ich den TupleConstructor auf const& gelassen habe, und einen eigenes Binder-λ implementiert habe.


Es könnte nämlich sein, daß diese Bedenken rein theoretisch sind, weil std::bind explizit auf eine Kopie abstellt, also mit unique-ownership ohnehin nicht arbeiten könnte. Habe da wohl vor vier Monaten (Feb.25) explizit in die Implementierung der Stdlib geschaut....

Denn: wenn man std::get das Quell-Tupel als RValue-Ref gibt, dann wird diese Funktion auch die Inhalte als RValue-Ref exponieren; im Zusammenspiel mit std::forward_as_tuple wäre so »perfect-forwarding« möglich, und für diesen zweiten Schritt auch sinnvoll, denn das remapped-Zwischen-Tupel bauen wir nur einmal auf, um es dann elementweise an den Binder weiterzugeben. Der Binder wiederum nimmt ausschließlich die zu bindenden Elemente (nicht die Placeholder) per RValue-Referenz und schiebt sie in ein Tuple in inline-Storage im Binder selber, wo sie dauerhaft liegen müssen, damit der Binder-Funktor beliebig oft aufgerufen werden kann

std::bind und std::apply schiebein alle Argumente per perfect-forwarding durch. Damit könnte man auch move-only-Funktoren verarbeiten, was die bestehende Impl nicht kann.

und zwar

  • Referenz irgendwo in den sonstigen Argumenten
  • Referenz als das Argument, das gebunden wird (⟶ bleibt eine Referenz)
  • den Funktor selber als Referenz übergeben (⟶ bleibt eine Referenz, man kann die konkrete Funktion austauschen)

...das bekannte leidige Problem: argument packs sind keine Typen; man kann sie daher nicht verarbeiten und weitergeben; stattdessen müsen wir einen Typ mit varidadic parameters verwenden, um am Zielort in der Argumentliste dagegen zu matchen. Da lib::meta::_Fun mehr und mehr zum Analyse-Tool für Funktions-artige Entitäten wird, bietet es sich an, diese Metafunktion selber auch als Transporter für Funktionssignaturen zu verwenden

wenn man ein Objekt wie einen unique_ptr oder einen vector per value an den partial-application-Functor übergibt, dann wandert die Ownership zu der Kopie, die in der Closure gespeichert ist. Mit dem ersten Aufruf der Funktion wander die Ownership weiter zum jeweils gebunden Funktions-Argument, und hinterläßt in der Closure ein »verbrauchtes Objekt«. Weitere Aufrufe der gleichen Funktion bekommen dann dieses potentiell invalidierte Objekt.

std::bind funktioniert nur mit copy-konstruierbaren  Objekten

nur die frei-bleibenden Argumente können move-Semantik haben. Und wenn man definitiv eine Referenz im Binder speichern möchte, so muß man einen std::reference_wrapper abliefern.

std::bind wendet intern ganz ausdrücklich std::decay_t  an, und speichert das sich ergebende Value-artige Objekt

..zum einen der Namespace: das führt dann zu using-Klauseln an vielen Stellen

...außerdem die Sichtbarkeit: solche Definitionen erzeugen einen Sog

...und zwar, weil for all practical purposes entweder igendwo iteriert wird (iter-adapter sind includiert) oder eine Format-Operation stattfindet oder zumindest eines der elementaren Metaprogrammnig-Hilfsmittel indirekt zum Einsatz kommt. D.h. die Wahrscheinlichkeit, daß lib/meta/util.hpp »zufällig schon« includiert wird, ist hoch, und damit kann dieser Sündenfall unter dem Radar fliegen

solange es nur limitiert genutzt wird, ist mir das lieber, als einen zentralen Header zu schaffen, der dann überall includiert wird und Begehrlichkeiten wecken könnte

lb(1GiB) ≡ 30

wrap-around droht

  • bei Zuweisung an einen signed-Type passiert automatische Konversion (potentiell gefährlich).
  • in einem Ausdruck setzt sich der Unsigned durch (es sei denn, das andere Argument hat einen größeren Wertebereich)
  • Vergleiche erzeugen signed-unsigned-Warnings — aus praktischen Gründen ist das für mich ausschlaggebend
  • kein allgemeines Such-Framework bauen!
  • den Begriff des "Containers" knapp halten: was keine const_reference bietet, ist kein Container

RandomDraw: »ziehen« von zufälligen Parameterwerten

...ich brauche ein Ausführungs-Framework, damit man die einzelnen Fälle zu Greifen bekommt.

Das impliziert leider auch eine Überarbeitung des Test-Frameworks

es soll möglich seine, eine opaque Quelle zu übergeben und erst zur Laufzeit / dynamisch bestimmen, woher der Seed stammt; Entscheidung per late-binding, ob echte Entropie zum Einsatz kommt, oder eine reproduzierbare Sequenz oder gar ein fixierter Wert

das ist ein Feature; man kann damit einen State speichern

ein Grund ist: STDLIB ist umständlich und unvollständig...

  • während Boost-functional-hash direkt per free-function (hash_value(x)) erweitert werden kann, muß man für die STDLIB-Variante immer den Namespace std aufmachen
  • Boost bietet automatisch bereits Hash-Spezialisierungen für alle erdenklichen Container, Tupel und sonstiges

de-facto wird nur noch 64-bit entwickelt. Inzwischen habe ich einige Tests, bei denen ich Hash-Values direkt prüfe. Die würden auf 32bit alle brechen

12/2023  per Debugger verifiziert ⟶ TestChainLoad

...ich hatte damals die gängige Implementierung genommen (wohl aus Boost, aber nicht genau genug geschaut); tatsächlich hat Boost (mindestens) zwei optimierte Varianten, und es ist undurchsichtig, welche wann genommen wird. Die Boost-Doku warnt aber eigens, daß Hash-Werte weder stabil noch portabel sind.

ich versuche schon seit vielen Jahren, Umwege abzukürzen, ohne zu viel Schaden anzuwenden. Portabilität, Plattform-Testing, Releases, Aufbereitung der Dokumentation, das Buildsystem, Vollständigere Tests ...

Lieber opfere ich Dinge, die als »Wert« gelten, und beschränke mich auf den Kern meiner Vision

jetzt zeigt sich bereits, daß wir locked-in sind:

der ganze aufwendige Test für die Scheduler Test-Chain-Load verwendet Hash-Chaining zur Verifikation; zwar sind das erst mal nur die Prüfsummen, weil die Topologie zum Glück aus einer zu Fuß programmierten, deterministischen Auswahl-Mechanik gebaut wird; als könnte man theoretisch dieses Mal noch eine neue Hash-Funktion nehmen, und müßte dann halt alle Hashes neu berechnen und im Test aktualisieren. Aber dieser Fall zeigt bereits, welche Bedeutung Hash-Werte bald spielen könnten.

Und daß wir nun nicht mehr einfach so die Verbesserungen von Upstream mitnehmen können.

Und daß wir jetzt in einer einzigen Plattform festsitzen

Und zwar sowas von froh!

  • Daß ich das noch rechtzeitig „geschnallt“ habe, und ohne viel Nachdenken die bestehende Boost-Implementierung in-Tree genommen habe.
  • Daß ich derart vollständige und pedantische Tests geschrieben habe, die auch die Hash-Berechnung im Einzelnen nachzeichnen
  • Und daß ich die Zeit in eine selbst programmierte Graph-Generierung gesteckt habe, anstatt schnell irgend etwas "mit Hashes" zu zaubern.

Dadurch kann ich jetzt auf die alte Implementierung umstellen (die in unserer Library in-Tree liegt), und weiß sofort, daß die Graph-Topologie unverändert ist.

EventLog : zeitliche Abläufe verifizieren

commit bb627fc1f8180009958c72a03696bacf87c65434

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Thu Nov 26 21:10:38 2015 +0100

    draft of the UI-Bus communication structure

   

    what you see here now is just the tip of the icebearg...

    If we follow this route, the Lumiera UI will become way more

    elaborate and responsive than average desktop applications

commit 40b69e1fd2b335b303e60497906d754a8ec599af

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Fri Feb 19 01:13:44 2016 +0100

    planning: consider implications of tree-diff application to arbitrary data structures

commit 2520ee82d13cb78248efb8d798f9ac9b05ca99ed

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Sat Sep 1 17:30:20 2018 +0200

    EventLog: investigate failed match in EventLog

   

    seemingly my quick-n-dirty implementation was to naiive.

    We need real backtracking, if we want to support switches

    in the search direction (match("y").after("x").before("z")

   

    Up to now, I have cheated myself around this obvious problem :-/

commit db1adb63a71f470970013f6cbbcc6e48bb0fe471

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Jul 31 21:53:16 2023 +0200

    Activity-Lang: draft a diagnostic helper

   

    ...for coverage of the Activity-Language,

    various invocations of unspecific functions must be verified,

    with the additional twist that the implementation avoids indirections

    and is thus hard to rig for tests.

   

    Solution-Idea: provide a λ-mock to log any invocation into the

    Event-Log helper, which was created some years ago to trace GUI communication...

commit 4df4ff27922c2596ee1b78c84d8dc9d18ebf526f

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Sun Oct 13 03:49:01 2024 +0200

    Invocation: consider minimal test setup and verification

   

    Analysis: what kind of verifications are sensible to employ

    to cover building, wiring and invocation of render nodes?

    Notably, a test should cover requirements and observable functionality,

    while avoiding direct hard coupling to implementation internals...

   

    Draft: the most simple node builder invocation conceivable...

TestFrame : verifizierbare Dummy-Berechnungen

belegen daß eine Berechnung stattgefunden hat

commit cafe271830c136d613de19e91f6ce8a804c0e4f5

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Sep 19 01:42:37 2011 +0200

    stubbing of basic buffer provider functionality

TestFrame wurde zwar schon 2011 angelegt, soll nun (2024) aber auch die Basis für eine Test-Ontology werden; die Analyse ergab, daß die vorhandene Funktionalität hierfür lediglich maßvoll erweitert werden muß:

  • das Datenlayout im Speicher sollte konsistenter sein
  • wir brauchen einen dedizierten Metadaten-Block, der aber optional ist
  • die Zufallsberechnung bleibt inhaltlich komplett gleich, aber wird umgestellt auf std::random
  • zusätzlich wird noch eine Hashberechnung über die konkreten Datenwerte hinzugefügt

Diskriminator-ID ≔ (seq+1) * (dataSeed+family)

Erläuterung: jede »family« hat ein eigenes Stepping

...weil es dann passieren könnte, daß für bestimmte Familien die Frames sich nicht mehr unterscheiden

also per reinterpret-cast mit std::launder, storage als std::byte mit sauberer Alignment-Angabe

Entscheidung: verwende ab jetzt die 64bit-Implementierung aus Boost

neue Bedeutung: es ist ein TestFrame mit erkennbarem Metadatenblock (auch wenn ansonsten keine der Prüfsummen mit den Daten zusammenpaßt); es sollte recht unwahrscheinlich sein, daß ein zufälliger Datenblock diesen Test besteht.

Es ist ein TestFrame mit gültigem Metadaten-Block, und die dort gespeicherte Prüfsumme wird durch die Daten bestätigt

entspricht dem bisherigen isSane() — d.h. isValid() aber zusätzlich passen die Daten auf die gespeicherte distinction, und das heißt, die Daten wurden vom Standard-Schema erzeugt und nicht weiter bearbeitet

frameNr und seqNr - Parameter waren vertauscht bei testData()

Das habe ich erst bemerkt, als ich tatsächlich dazu noch einen frei stehenden TestFrame mit gleichen Koordinaten erzeugt und verglichen habe...

GnaGnaGna

...essentielle Vorraussetzung für reproduzierbare Tests: Es gibt einen statischen Basis-Hash, auf dem dann die einzelnen Frame-Serien aufbauen (indem sie ihn jeweils in den Schritt-Spread einarbeiten; dadurch haben benachbarte Frames ähnliche, aber sicher unterschiedliche Diskriminator-Seeds, aus denen ein Linear-Congruential-PRNG gebaut wird, um jeweils die Daten für einen Frame zu befüllen. Der Basis-Seed ist zwar initial komplett zufällig (Entropie), wird aber durch TestFrame::reseed() aus dem aktuellen defaultGen neu gezogen. Und der defaultGen ist für Tests seit #1378 deterministisch pseudo-zufällig und reproduzierbar

es gibt einen fest eingebauten Seed-Mechanismus, der auf der Kanal- und Sequenz-Nummer beruht

kann Lifecycle markieren als CREATED, EMITTED, DISCARDED

neue Bedeutung: es ist ein TestFrame mit erkennbarem Metadatenblock (auch wenn ansonsten keine der Prüfsummen mit den Daten zusammenpaßt); es sollte recht unwahrscheinlich sein, daß ein zufälliger Datenblock diesen Test besteht.

Es ist ein TestFrame mit gültigem Metadaten-Block, und die dort gespeicherte Prüfsumme wird durch die Daten bestätigt

entspricht dem bisherigen isSane() — d.h. isValid() aber zusätzlich passen die Daten auf die gespeicherte distinction, und das heißt, die Daten wurden vom Standard-Schema erzeugt und nicht weiter bearbeitet

TestFrame_test

...für den Zweck hier; wir brauchen durchaus mengenweise Zufallszahlen mit guten statistischen Eigenschaften, aber wir brauchen weder unglaublich lange Zyklenzahlen, noch bauchen wir einen extrem großen Konfigurationsraum

...und mein Interface SeedNucleus ist nur darauf ausgelegt (auf den kleinsten gemeinsamen Nenner)

...und zu allem Überfluß gibt es auch noch eine recht fragwürdige standard-Implementierung, std::seed_seq, welche die gegebenen Zahlen in einen Vector auf den Heap speichert.

meist wird Seeden aus einer einzigen Zahl unterstützt, aber viele Algorithmen brauchen eigentlich mehr initial state und hierfür ist mehr Seed-Input nützlich; wenn man also eine größere Entropiequelle hätte, könnte man eine bessere Diffusion erreichen

...sofern man tatsächlich ein abstrahiertes Interface für das Seeding haben möchte (d.h. im usage-Kontext ist nicht explizit klar, was für eine Quelle tatsächlich verwendet wird)

std::seed_seq

  • soll vor allem die Schwächen des Mersenne-Twister überbrücken, welcher einen sehr großen initial state benötigt, und für einige Eingabe-Sequenzen gefährlich degenerieren kann; diese Idee wurde dann verallgemeinert...
  • unabhängig davon wurde aber die zweite Variante erlaubt, nämlich den Random-Generator mit einer einzigen Zahl zu seeden...
  • Melissa O'Neil (sie spätere Autorin von PCG-Random) hat den Algorithmus für seed_seq dahingehend kritisiert, daß dieser eiene große Menge Entropie nicht optimal nutzt. Dem wurde von verschiedener Seite widersprochen; desweiteren ist fraglich, für welche Art Anwendungen diese Frage überhaupt relevant ist; denn auch nur mit einem einzigen Seed-Value erzeugt der Mersenne-Twiseter 2^32 verschiedene, extrem hochqualitative Sequenzen — es geht nur um die Frage, ob er auch 2^(624·32) verschiedene Sequenzen erzeugen könnte.
  • das Interface für <random> ist generisch: es gibt ein Konzept für Seed-seq, und ein Konzept Generator, und die beiden werden dann jeweils für den Einzelfall konkret kombiniert

SeedNucleus

  • ist auf die besondere Situation ausgerichtet, daß ein nicht näher bekannter Generator irgendwann zur Laufzeit eine nicht näher bekannte Seed-Quelle bekommen soll
  • es soll also zur Laufzeit aber damit auch dynamisch umgeschaltet werden zwischen einem festen Seed und deterministischem Verhalten und pseudo- oder echter Randomisierung
  • hier ist also die potentielle reproduzierbarkeit eine zentrale Hinsicht, wohingegen davon ausgegangen wird, daß durch interaktiven Gebrauch ohnehin eine unerschöpfliche Entropie-Quelle gegeben ist

...und zwar würde es genügen, statt eines Seed-Value einen Iterator zurückzuliefern; dieses Proxy-Objekt müßte dann allerdings im Typ entsprechend fixiert sein (also z.B. ein uint32_t*). Dann bliebe allerdings auch noch die Frage offen, ob auch der im C++-Standard festgelegte Algorithmus implementiert werden muß, d.h. ob intern aus dem Aufruf des SeedNucleus wieder eine konforme Seed-seq erzeugt werden soll. Das wäre durchaus machbar (er ist genau spezifiziert und leicht zu implementieren) — würde aber wieder die gleichen Probleme aufwerfen, die auch für std::seed_seq gelten:

  • der Algorithmus muß die Länge der gelieferten Seed-Seq und die Länge der Quell-Sequenz kennen ⟹ beide Infos könnte man aus Pointer-Arrithmetik gewinnen (und das bereitzustellende C++ Interface für die Seed-seq läßt auch gar keinen anderen Ausweg zu)
  • alle Unsicherheiten und Einwände bezüglich der Qualität der Seed-seq. bleiben bestehen (das wäre aber immer noch besser als nur ein einziger Seed-Wert)
  • es bleibt offen, ob dieses Schema auch für andere (modernere) PRNG-Algorithmen vorteilhaft ist

damit wird klar: das aktuelle SeedNucleus-Interface is zunächst gut genug (KISS)

der SeedNucleus soll ja eben grade opaque sein, also möchte ich da keine zusätzliche Operation zum State-Speichern hinzunehmen; und für die std::seed_seq ergibt sich das Problem, daß die Länge nicht bekannt ist; man bekäme also u.U eine unbeschränkt lange Sequenz von Zahlen als Seed-Zustand

  • ein SeedNucleus, der einen quell-Nucleus einpackt und den/die ausgelesenen Wert(e) dokumentiert
  • und natürlich dann als Gegenstück ein Seed-Nucleus der einen festen Wert ausliefert (trivial), oder ggfs. sogar eine Weiche, die entweder die Quelle verwendet, oder einen gegeben Override

eigentlich hatte ich das nur als Vorsichtsmaßnahme eingefürt, da wir noch kein Framework für Zufallszahlen in Tests hatten; die Tests liefen seither stets mit einem festen Seed los, aber es sollte eben eine »Hintertür« geben, mit der man in bestimmte Tests echte Zufälligkeit injizieren klnnte

...das wäre nur notwendig, wenn mal eine ganze Familie von Generatoren (z.B. in parallelen Tests) zum Einsatz kommt, und zudem der unterliegende PRNG eine schlechte Diffusion hat. Normalerweise aber erzeugen wir solche Generatoren (gemäß diesem Plan) nun stets aus dem Seed eines Vater-Generators, und wir verwenden qualitativ hochwertige PRNGs, und das sollte eigentlich zur Dekorrelation genügen

...und zwar weil das einfach umständlich ist, die Aufrufe sind länger, man muß es durchreichen und ggfs ist dann doch noch irgendwo bei den sonstigen Utilities ein versteckter Aufruf des default-Generators. Es gibt auch nur selten einen Mehrwert, verschiedene Generatoren zu verwenden; das wäre nur notwendig wenn ein komplexer Prozeß durchgeführt wird, der in Teilen nicht völlig ablaufstabil ist, z.B. weil in mehreren Threads unabhängig voneinander Zufallszahlen gezogen werden.

der Test-Autor ist selber dafür verantwortlich,
die Zufallszahlen-Generierung zu verstehen...

etwa analog zu dem Framework, welches mein Kollege Oleg Galimov bei der Baaderbank mitgebracht hat (es ist OpenSource, in Java geschrieben)

das ist der klassische Fall, in dem eine »saubere« Repräsentation in der einzelnen Testklasse zu massiver Speicherverschwendung führt, weil jede Testklasse einen redundanten Pointer auf den gleichen globalen Kontext bekommt

Die klasse enthält diverse Checks, die aber (weitgehend?) constexpr sind. Der eigentliche operative Aufruf ist eine reine Funktion, die den Generator hereingereicht bekommt. Diese Funktion gibt es in zwei Ausprägungen, einmal für einen einzigen Wert, und einmal (wohl optimiert) für Ausgabe in einen Iterator-Range. Letztere heißt __generate und ist wohl nur für die STL intern gedacht

an einigen wenigen Stellen wird eigens dafür gesorgt, daß die grenz inclusiv ist

rani(max)

0000000515: CHECK: rational-test.cpp:90: thread_1: demonstrate_basics: (util::toString(23_r/55) == "23/55sec")

...hatte vor einiger Zeit aufgeräumt und einen eigenen #include "lib/integral.hpp" geschaffen. Dadurch ist downstream der #include für time-value.hpp rausgefallen, und damit fehlte die String-conversion für Rationals

...dazu nochmal über die ganze Problematik nachgedacht und entsprechende Kommentare in #1258 und #1261 hinterlassen

0000000514: INFO: suite.cpp:202: thread_1: invokeTestCase: ++------------------- invoking TEST: vault::gear::test::SchedulerCommutator_test

0000000515: NOTICE: suite.cpp:118: thread_1: getSeed:      ++>>> SEED(rand) <<<: 12483615036814281831

0000000598: UNIMPLEMENTED: scheduler-commutator.hpp:204: thread_1: findWork: how to trigger a Scheduler-Emergency from here

ommit 5b62438eb404e20762a9f2ff6109c7cf54022064

Author: Ichthyostega <prg@ichthyostega.de> 2024-04-10 20:04:53

Scheduler-test: investigate logic problem related to the »Tick« deadline

In the end, I decided that it ''is to early to decide anything'' in this respect...

The actual situation encountered is a **Catch-22**:

 * in its current form, the »Tick« handler detects compulsory jobs beyond deadline

 * since such a Job ''must not be touched anymore,'' there is no way scheduling can proceed

 * so this would constitute a ''Scheduler Emergency''

All fine — just the »Tick« handler ''itself is a compulsory job'' — and being a job, it can well be driven beyond its deadline. In fact this situation was encountered as part of stress testing.

Several mitigations or real solutions are conceivable, but in the end,

too little is known yet regarding the integration of the scheduler within the Engine

Thus I'll marked the problematic location and opened #1362

...die definitiv auftreten kann, und die auf einem höheren (derzeit nicht existenten) Level der Architektur behandelt werden muß, als eine Scheduler-Emergency.

hab damals einfach »aufgegeben«,

da der Trigger-Punkt unpassenderweise in Layer-2 liegt

...bzw dorthin gezogen ist durch den Umbau der Scheduler-Struktur, welche zwar spät erfolgte, als Resultat der Stress-tests, aber insgesamt eine signifikante Verbessung des Codes darstellt. Leider hat dieser Umbau nun dazu geführt, daß Layer-2 diverse »Hooks« auf Service-Level ansprechen muß, und das ist wiederum ein HInweis, daß die Code-Anordnung nicht optimal ist

  • der Trigger würde in Scheduler::doWork() erkannt werden, was allerdings aus den Workern aufgerufen wird.
  • der »Tick« dutyCycle() würde aber logischerweise die gleiche Situation auch erkennen
  • nur das Problem ist: dieser »Tick« könnte unerreichbar sein, denn er liegt selber in einem Compulsory-Job, und die Queue könnte schon vorher geblockt sein durch einen anderen Compulsory-Job

...und genau diese Entscheidung konnte/wollte ich vor einem halben Jahr nicht treffen (und bin im Moment nicht sicher, ob ich sie jetzt treffen kann)

siehe auch Kommentar im Ticket #1362 Scheduler Emergency

TEST Scheduler Performance: SchedulerStress_test .. FAILED

unexpected return value 152, expected 0

stderr was:

0000000514: INFO: suite.cpp:202: thread_1: invokeTestCase: ++------------------- invoking TEST: vault::gear::test::SchedulerStress_test

0000000515: NOTICE: suite.cpp:118: thread_1: getSeed:      ++>>> SEED(rand) <<<: 401457506165208591

END

scheduler-stress-test.cpp:415: watch_expenseFunction: (isLimited (3, socket, 9 ))

Hier beobachten wir ein lineares Model der Beladung; die lineare Regression hat einen konstanten Sockel(9ms) knapp oberhalb der bisher tolerierten Overheads für start-up und spin-down (+Test-setup); erwartet werden ~6ms

scheduler-stress-test.cpp:270: setup_systematicSchedule: (fabs (runTime-expected) < 5000)

Hier wird zunächst »auf dem Trockenen« demonstriert, wie aus der Kapazitätsrechnung eine zu erwartende effektive concurrent-run-time abgeleitet wird. Am Ende wird ein tatsächlicher Scheduler-Lauf mit diesem (relativ locker dimensionierten) Schedule gestartet  — und hier überschreiten wir den Timeout von 5 Sekunden, was bedeutet, daß das Schedule komplett aus dem Ruder gelaufen sein muß (möglicherweise wurden sogar Job-Deadlines überfahren, in welchem Fall das Scheduler unvollständig hängen bleibt)

wenn der Test durläuft sind die beobachteten Kenndaten des Schedulers wie erwartet

also die Concurrency ist wirklich gut, die back-to-back-Zeit weicht nur 10ms vom theoretischen Wert ab

CHECK: work-force-test.cpp:425: thread_1: verify_scalePool: (2*fullCnt == uniqueCnt)

Dieser Test skaliert den Worker-Pool, wartet dann jeweils eine (fest konfigurierte) Zeitspanne, um anschließend den Status zu prüfen — ein bekanntermaßen fragiles Schema, obwohl ich inzwischen Timings ausgeknobelt habe, die auf meiner Maschine hinreichend sicher sind. Für zuverlässiges Testen müßte man auf den Status der WorkForce eigens warten, und dafür möchte ich jedoch kein Core-API bereitstellen, denn ich möchte niemanden ermutigen, in Richtung einer »pinball-machine« zu gehen....

0000000516: PRECONDITION: tracking-heap-block-provider.cpp:281: thread_1: detachBuffer: (util::isSameObject(storage, block4buffer->accessMemory()))

Damals im Juli habe ich versucht, die Info zum Ausgabe-Buffer durch das allgemeine Buffer-Handling-Framework durchzufädeln; dafür habe ich das LocalTag (vorher LocalKey genannt) eingesetzt; in der weiteren Entwicklung habe ich jedoch dann diesen architektonisch unpassenden Ansatz verworfen und die Identifikation des Ausgabe-Buffers direkt in den Aufruf von oben hereingegeben.

In diesem Zusammenhang habe ich das BufferProvider-Framework nochmal genauer angeschaut, einige potentielle Gefahren abgedichtet und — TADAA — zusätzliche Assertions eingebaut, die jetzt ansprechen. Gut gebrüllt, Löwe

ich hab damals auf theoretischem Weg die Gefahr duplikater Einträge entdeckt...

Und zwar durch den Umstand, daß die Einträge im Buffer-Provider durch Key-Records markiert sind, welche untereinander hierarchisch zusammenhängen können. Denn zunächst stellt ein solcher Eintrag einen Buffer-Typ dar, von dem dann viele einzelne konkrete Buffer registiert und verwarbeitet werden. Das Problem entsteht nun aber, weil auch ein zusätzliches LocalTag mit in den Key einfließt zur zusätzlichen Differenzierung. Das ist zunächst sinnvoll, um einen Typ zusätzlich zu qualifizieren. Gefährlich ist aber, daß zu einem bestehenden Buffer mit Adresse noch ein Local-Tag hinzugefügt werden könnte, oder daß man versucht, die gleiche Adresse einmal mit und einmal ohne LocalTag anzusprechen. Denn dadurch würden zwei Metadatensätze erzeugt, die dann jeweils auch einen eigenständigen Lifecycle durchlaufen, obwohl sie zur gleichen Buffer-Adresse gehören.

...und aber eine dieser Infos einfach ignoriert wird, nämlich die konkrete Info, die der im BuffHandle vom Client eingebettet ist. Daher habe ich für die einzige Implementierung, den TrackingHeapBlockProvider beide Infos in einer Assertion verglichen. Und genau dieser Check scheitert jetzt (noch nicht klar warum)

und zwar für die Key-Erstellung; die nimmt wohl das Argument by-value

bisher hab ich immer den »Fehler« gemacht, solche trickreichen Experimente im Blindflug in util.hpp machen zu wollen. Das ging dann schief, und nach drei vergeblichen Versuchen hab ich das Thema aufgegeben, schließlich dauert jeder Build 5 Minuten.....

es braucht hier kein perfect-forwarding

CHECK ( isSameObject (*ptrWrap.get(), x));

CHECK (!isSameObject ( ptrWrap.get(), x));

CHECK (not isSameObject (y1.c(), y2.c())); 

CHECK (    isSameObject (*y1.c(), *y2.c()));

ein einer einzigen „harmlosen“ Zeile...

  • wenn man die Policy für »unrelated objects« verwendet, wäre BaseP ≡ void* und es gäbe einen Compile-Fehler. Zeigt daß die Tests oberflächlich sind
  • wenn der Payload-Typ SUB ein Pointer ist (auch das wäre theoretisch möglich im Falle der »unrelated objects«) dann würde der Object-Vergleich funktionieren (aber am void* scheitern), wohingegen der Address-Vergleich den Payload-Pointer auspacken, und somit zu einem falschen Urteil kommen würde

wir brauchen zwei Semantiken ⟹ also zwei Utilities

in den allemeisen Fällen entspricht das, was geprüft wird, tatsächlich dem Begriff "object" aus dem C++ - Standard, also keine Referenz, keine Funktion und nicht void.

erzeugt die zufälligen Summanden vorher im Haupt-Thread

diese verwendet uint_fast32_t, das aber auf meinem System ebenfalls auf unit64_t gemapped ist

schon kleine Änderungen in der Payload-Funktion können die Inzidenz der Probleme drastisch ändern; beispielsweise hat das Hinzufügen einer Begrenzung per Modulo für einen Generator die Inzidenz drastisch erhöht, für einen anderen die Probleme nahezu zum Verschwinden gebracht

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

nach Alexandrescu: sollte composable sein

kann Objekte beliebigen Typs erzeugen und verwerfen

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

Namespace: lib::allo

die Grundbegriffe sind für Concepts reserviert

wenngleich auch der Vortrag relativ viel Geplänkel enthält, so ergibt sich doch insgesamt eine Sicht auf die Design-Erfahrungen der letzten 15 Jahre, und die daraus ableitbaren Strukturen erscheinen mir ausgereifter als die Strukturen aus dem C++ - Standard

Hier wird wahrscheinlich ein komplexes Subsystem aus Metaprogrammierung entstehen. Die verschiedensten praktischen Varianten sind denkbar und auch valide

  • die Factory kann sich selbst erzeugen, indem sie auf einen Monostate zurückgreift. Das ist in der Praxis der wichtigste Fall, der im Besonderen für die normale Heap-Allokation gilt, aber auch Thread-Local und damit Kontext-Bezogen auslegbar ist.
  • es könnte nur Copy-Construction oder Delegation erlaubt sein, und die Factory ist in diesem Fall nur ein Front-End-Handle
  • man könnte einen Konstruktor bieten, der einen C++-Standard-Allokator akzeptiert und adaptiert

Factory wird das zentrale Schnittstellen-Concept zur restlichen Applikation

Präfix Std

man möchte ein reines Funktor-Objekt, das irgendwie hintenrum verdrahtet ist. Auf diesem ruft man nur noch den Funktionsoperator auf mit den konkreten Argumenten, denn der Zieltyp ist bereits festgelegt

...auch hier soll die Verdrahtung verborgen bleiben; jedoch ist man nicht auf einen Objekttyp festgelegt, und außerdem soll es die Möglichkeit geben, esplizit eine destroy()-Funktion aufzurufen, oder alternativ schon aus der Erzeugung ein managing-handle zu bekommen

Speziell bei Containern treten verschiedene Nutzungsmuster und Variationen auf...

  • man möchte das Erzeugen neuer Objekte komplett unterbinden
  • man möchte zum Erzeugen einen eingebetteten Custom-Allocator verwenden
  • man möchte Destuktoren aufrufen oder nicht aufrufen
  • man möchte beim Verwerfen eines Objekts die Allokation freigeben oder auch nicht und ggfs. auch bloß mitzählen

Das ist in etwa die Funktionalität, die man von der Standard-Library bekommt, und die damit auch von STL-Containern genutzt werden kann; eigentlich braucht man hier nur die Bereitstellung (und Freigabe) von uninitialisiertem Speicher; das eigentliche Konstruieren und Zerstören erledigen die std::allocator_traits

⟹ LinkedElements ist nicht mehr kongruent

      mit einem einfachen Pointer

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...

...man muß also abstrakte Einstiegspunkte für einige vordefinierte Aktions- und Themenkomplexe schaffen; innerhalb des Callbacks kann sich dann eine Library in ihrer eigenen Domain-Ontology bewegen

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

«Domain-Ontology-Mapping»

Einfach gesagt: jede Media-handling-Library definiert, was für Medien-Dinge es geben kann, wie diese zusammenhängen und klassifiziert werden und wie deren Eigenschaften festgestellt werden können...

Feststellung: Domain-Ontologies werden über Aufgaben-Callbacks  eingebunden

Dies ist ein Beschluß auf Basis einer induktiven Grundhaltung: Wir haben den bestehenden Umgang mit dem Thema »Media-Processing« betrachtet und auf diesem Hintergrund eine Architektur-Lösung gefunden, die nicht auf einer mutwillig deduktiv gesetzten Ordnung beruht. Es wird ein Rahmen abgesteckt, was man typischerweise „mit Medien machen“ möchte, und aus diesem wird ein Baukasten-System destilliert, auf dessen Basis sich diese üblichen Ziele und Zwecke erreichen lassen. Dieser Rahmen bleibt jedoch offen, insofern er nicht als eine innere Systematik ausgearbeitet wird. Stattdessen gibt es — aus diesem Baukasktensystem heraus — bestimmte Aufgaben, die im Rahmen der jeweiligen Domain-Ontology zu lösen sind. Lumiera stellt dafür den Raum für ein Modell bereit, und einen Ordnungsrahmen, wie mit den Modellbestandteilen umzugehen ist. Es obliegt dann aber dem jeweiligen Domain-Adapter (Façade), diese von Lumiera vorgegebenen Erwartungen in der jeweiligen Domäne zu realisieren.

Zur Wirkung der aufgerufenen Aufgaben und zur Semantik müssen gewisse Annahmen gemacht werden, wie z.B. das ein Medium gerendert werden kann. Es wird aber nicht versucht, dies weiter klassifikatorisch zu fassen; die Wirkung dieser Aktionen wird durchgereicht, und der Sinn liegt bei demjenigen, der Lumiera verwendet, um damit etwas zu bauen.

Beispiele:

  • Feststellen, ob eine Verbindung für einen bestimmten Medienstrom realisierbar ist
  • Auswahl eines Codecs für einen »Strom-Prototypen«, dessen Bedeutung anderweitig festgelegt wurde
  • Entscheiden, in welcher Reihenfolge und in welcher Form Voraussetzungen bereitzustellen sind
  • Übersetzen eines Medienstroms in die Repräsentation einer anderen Media-handling-Library
  • Bereitstelleung einer Verschaltung im Detail, damit ein Verarbeitungsschritt im Rahmen der Lumiera Render-Engine als pull-Job ausgeführt werden kann

....gehe aber nicht davon aus, daß dies möglich ist, weil es den Zugriff auf Inhalte aus Plug-ins überall deutlich komplexer macht und auch zu Contention führen könnte

...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...

die konkrete Antwort schlägt sich

(direkt oder mittelbar)

im Event-State nieder

wenn sich die Entscheidungsbasis ändert,

würde das Ergebnis u.U. anders ausfallen.

sofern sich eine Entscheidungsgrundlage ändert, müssen alle davon abhängigen Entscheidungen re-evaluiert werden und könnten ander ausfallen; ist dies der Fall, so müssen rekursiv alle darauf aufbauenden Schritte überprüft und neu aufgespielt werden. Am Ende kann ein drastisch anderer Zustand resultieren, und dies wäre selbst wieder als Event zu dokumentieren.

die seinerzeit getroffene Entscheidung ist als Event festgehalten und wird von selbst nicht nochmal überprüft. Es können sich somit weitreichende Diskrepanzen im Event-State festsetzen, welche dem aktuell gültigen Zustand der Konfiguration widersprechen; dies kann zu Entgleisungen, katastrophaler Fehlfunktion und nicht-deterministischem Verhalten führen.

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

Es könnte in der Tat so funktionieren;

....mir fehlte aber komplett der Widerhalt und Anker in dem, was denn vom Builder gebaut werden soll. Für einen klassischen Compiler dagegen ist genau dieser Teil ganz fest vorgegeben, in der Form des Maschinencodes, der vom Compiler erzeugt werden muß. Ein Compiler ist eine Maschinerie, insofern er eine eigene Innen-Logik hat, welche nur lose mit dem Zweck verbunden ist, dem sie dient. So etwas baut man, indem man von Bekanntem ausgeht, und es daraufhin prüft, ob es passend gemacht werden kann. Fest steht dabei eine Grundstruktur, hier das Schema aus Integrations-Schritten, die jewelis als Durchgang über die gesamte Datenstruktur zu bewerkstelligen sind. Auch bezüglich der Methode, eine Abstraktion einzuziehen zwischen der Mechanik der Daten-Traversierung und der eigentlichen Daten-Transformation — diese Methode steht außer Zweifel....

Was bisher fehlte, war einfach die Möglichkeit, mit der echten Arbeit an diesem Thema beginnen zu können; daher bleibt der bestehende Code auf dem Rang einer Programmier-Übung stehen (und es war angemessen, ihn einfach liegenzulassen)

Schwierigkeit: die Verbindungen sollten per Stream-Type markiert sein

Callback: configureNode(builder)

weiteres Argument: ExpectationContext

essentiell wichtig diese offen zu halten, denn die direkten Aufrufer können diese nicht interpretieren, sondern reichen sie aus einem offen zu gestaltenden Model heraus durch; dort können Regeln gewirkt haben, die Attributierungen anbringen, welche dann wieder innerhalb der konkreten Domain-Ontology interpretierbar sind

...da beide Seiten wohl einen konkreten Typ-Kontext haben, ist eine Art double-Dispatch notwendig....

  • der aufrufende Builder muß eine Policy mitgeben, in der der Allokator und weitere Dependency-Injection steckt
  • der empfangende Library-Kontext muß dann aber auf dieser Basis selbst eine Template-Instanz machen, um seine konkreten Implementierungs-Buffer-Typen und Funktoren einzubringen

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

um fliegende Änderungen durchführen zu können

  • 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

...dient als Bezugspunkt um im Scheduler einen »fliegenden Wechsel« der unterliegenden Definition zu ermöglichen

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()

Das war ein theoretisches Konstrukt, das auf den ersten Blick (auf einer abstrakten Ebene) unglaublich plausibel erscheint; tatsächlich gibt es aber nirgends eine Domäne, in der eine solche Entität oder Daten-Gruppierung eine Rolle spielt. Aufgrund dieser Einsicht wurde das Konzept im Sommer 2023 aufgegeben.

bestimmende Freiheitsgrade

für die Job-Invocation

lib::Handle<OutputSlot::Connection>

denn die Inovcation muß in diese Sink rendern

was darüberhinaus benötigt wird, ist noch nicht wirklich klar

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

...die Eingangs-Schnittstelle zum Scheduler ist nebenbei beim Bauen entstanden — und erscheint doch als ein rechtes Stückwerk, zumal auch erhebliche Probleme unter Druck auftreten können; hab daher beschlossen, dieses Thema nochmal insgesamt zu durchdenken

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

...damit sie das Grooming-Token hat und damit Zugriff auf die Scheduler-Ressourcen. Außerdem ist damit ein konsitenter Rahmen sichergestellt: geplante Startzeiten sollten frühestens 20ms danach beginnen und können (einschließlich Deadline) höchstens ein Fenster von ~20 Sekunden überspannen

...für den »Playback Vertical Slice« wird eine Fixture fabriziert, wie sie vermutlich später mal vom Builder produziert werden wird (wenn es ihn dann endlich mal gibt) — tatsächlich sehe ich das als ersten prototypischen Entwurf, den ich hier nebenbei aus einer bottom-up-Bewegung gewinnen kann

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

...also sowas wie Lösung-4, mit einem strukturierten Term; leider kostet ein solcher Term zwangsläufig viel Platz (weil er offen sein muß) und läuft damit stets auf eine Heap-Allokation hinaus; da müssen wir sehr vorsichtig sein, bedingt durch den Hebel aufgrund der großen Zahl an Segmenten

...allerdings nur unter der Annahme, daß die DataSink jeweils komplett unique ist (was tatsächlich nicht zu erwarten ist); wenn man allerdings im API von DataSink noch etwas unterbringt, um auf eine Stream-Type-ID zuzugreifen, dann wäre CalcStram bzw. InvocationInstanceID tatsächlich redundant und könnte aus den anderen Angaben errechnet werden.

...will sagen, eine »ExitNode« muß nicht eine besondere Art von Node sein, denn der Zwek von Nodes und der/den ExitNodes überschneidet sich überhaupt nicht

CalcStream ≡ eine aktiv laufende Folge von Berechnungen für einen  Ausgabekanal

beachte: dieser »eine Frame« kann durchaus noch inhärent strukturiert sein, beispielsweise mehrere Planes für ein planares Format enthalten oder ein Interleaved-Zyklus von Einzelframes für zusammengehörige Kanäle sein — entscheidend ist, daß dieses Datenelement jeweils als EInheit verwendet und zusammen produziert oder konsumiert wird.

für jeden Feed gibt es einen ModelPort (ggfs. eine Ausprägung)

und eine top-Level-Node in jedem Segment

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

habe mit e1248d195af07febbdc die bestehende Impl beiseite geschoben

commit 1f13931640fe09f84b798722a6edd2aa95ebe7ea

Date:   Sat Sep 17 01:50:11 2011 +0200

erste Rohentwürfe

+Basis-Implementation

...mit der bin ich nämlich heute überhaupt nicht mehr d'accord...

insofern wichtig für einen 2.Anlauf

Das erscheint mir als eine wichtige neue Einsicht: wir haben keine Zweiteilung (in Planung vs. Ausführung), sondern eine dreistufige Organisation:

  • Builder ⟼ Render-Node network
  • Job-Planning ⟼ JobTicket + Closure
  • Invocation ⟼ pull-linkage

beinhaltet all die Verbindungen, die bis zuletzt flexibel bleiben müssen und für jeden einzelnen Aufruf durchgegangen werden, um die korrekten Verbindungen herzustellen; beispielsweise Verbindungen, die noch von einem Schalter oder von Automation abhängen, oder Verzweigung infolge des Cache-Zustands

...denn das wäre im direkten Widerspruch zu dem Builder-Ansatz; denn klassischerweise versteht man unter »pull-processing« eine lazy evaluation, eine late discovery — diese zielt im Kern darauf, eben genau keine erschöpfende Planung vorneweg zu machen. Das »pull principle« ist ein eleganter Implementierungs-Ansatz, der erstaunlich komplexe Funktionalität abbilden kann, ohne eigentlich vorher eine strukturierte Analyse und Vorbereitung zu machen; jedes Teil definiert nur seine Vorraussetzungen, und auf geheimnisvolle Weise findet die Berechnung ihren Weg, wenn man nur lange genung „saugt“. Tatsächlich ist es mir sogar im Gegenteil ein Anliegen, überhaupt nichts mehr zur Laufzeit „herauszufinden“. Ich ziele vielmehr auf eine komplette Durchdringung in einem vorgelagerten Compilations-Prozeß. Die genauere Analyse hat mich dorthin gebracht: ein klassisches »pull-processing« führt nämlich zu einem chaotischen Suchverhalten und erfordert zumindest dynamische Allokation, besser noch sogar einen Garbage-Collector. Und — was für mich noch viel wichtiger geworden ist — es verteilt die Entscheidungs-Logik über den Ausführungs-Pfad, und zwar in Form von cleveren Tricks. Kaum etwas verachte ich mehr, als einen solchen Programmierstil.

Allerdings — zwei Aspekte bleiben von diesem Ansatz dennoch erhalten:

  • der rekursive depth-first-Aufruf, und damit der Verzicht auf ein vorausgeplantes Daten-Management
  • die Analyse-Richtung rückwärts vom Ergebnis zu den Vorraussetzungen (wenngleich auch dies bereits im Builder passiert)

...so im Sinn eines »Node-Editors« — eine Node die eigentlich ein plug-in ist, und „alles machen“ kann; das würde nämlich voraussetzen, daß alle Nodes irgendwie miteinander kompatibel sind oder automatisch aneinander angepaßt werden — das wäre etwas, was wir hier ganz explizit nicht wollen, sondern bereits im Builder abhandeln

...der erste Entwurf war viel zu sehr von der Idee einer „geschickten“ Implementierung geprägt; daher war vorgesehen, direkt auf eine »high-level«-Logik durchzugreifen — im Besonderen auch im Hinblick auf die Automation. Von diesem Ansatz habe ich mich inzwischen komplett gelöst; vielmehr setze ich auf symbolische Repräsentation, die dann die nächsten Übersetzungs-Schritte ansteuert, und so schrittweise in eine vollständige Parametrisierung und Konfiguration des Rechenvorgangs mündet

...im ersten Entwurf dachte ich, daß der Kern der Berechnung aus dem direkten Aufruf von C-Funktionen bestehen muß, weil dadurch die Berechnung maximal performant wird; daraus ergab sich auch, daß der »pull-call« direkt ein Array von Buffer(pointern) weitergibt. Das hat sich auf mehreren Ebenen als naiv herausgestellt. Zunächst einmal, die Berechnung soll stets von einem externen plug-in oder einer Library übernommen werden; daher ist klar, daß jeder Aufruf zwangsläufig durch einen Adapter-Layer geht. Demzufolge wird es im »pull-call« stets darum gehen, gewisse Handles oder andere symbolische Repräsentationen weiterzuleiten, und die vorgeblich besonders performante low-Leveligkeit ist nichts als Augenwischerei, denn die Laufzeit wird ohnehin in der Berechnungsfunktion verbracht. Viel wichtiger ist es, explizite Auswertungen zur Laufzeit zu vermeiden, sofern diese bereits vorneweg für ein gesamtes Segment gemacht werden können.

an der WiringSituation bin ich gescheitert

es besteht (weiterhin) ein bedenkliches Henne-oder-Ei-Problem

...weil es so stringent ist und man direkt bei der Compilation eine Rückmeldung bekommt; aufgrund der fehlenden Außen-Verankerung habe ich lange gar nicht gemerkt, daß die compiletime vs runtime division  eine Hürde darstellt, die die verlockende Idee praktisch unmöglich macht. Andererseits habe ich hier erstmals die sehr fruchbare Idee des compiletime-Layering angewendet, konnte damals aber nicht richtig damit umgehen. Inzwischen habe ich die Möglichkeiten und Grenzen dieser Idee verstanden und erfolgreich angewandt (für die GenNode, das Diff-System, die Event-Log-Suche und den IterExplorer)

favour composition over inheritance

  • DataSink ist verwoben mit dem Buffer-Protokoll; dieses muß von der Node-Invocation im Job selber durchgespielt werden
  • der pull()-Call passiert als ein Teilschritt in demselben; hier werden OutputBuffer vorrausgesetzt

Rolle der chanNo im bestehenden Invocation-Code

...welche ihrerseits ein komplexes, widersprüchliches und auf lange Sicht unbestimmtes Thema ist

Medien-Erzeugung (Sound, Bild) kann durchaus mehrere, gleichberechtigte Aspekte oder Dimensionen erzeugen, und es kann sehr wohl sein, daß man einen Teil davon oder alle auch weiterverarbeitet. Beispielsweise kann eine Sound-Synthese verschiedene und miteinander verwobene Abstrahlungen nachbilden, beispielsweise in Anlehnung an ein Blasinstrument, das eine Abstrahlung am Mund hat, eine Abstrahlung des Körpers und eine Abstrahlung vom Trichter. Und man möchte diese jeweils eigens in einen virtuellen Raum einbinden können. Oder eine Bild-Analyse, die ein interpoliertes Bildelement, sowie einen Bewegungsvektor desselben ausgibt

In diese Kategorie fallen alle Verarbeitungsschritte, die sich nicht auf komplett isolierte Primitiv-Operationen auf Einzelkanälen faktorisieren lassen. Beispiel wäre ein Resonanz- oder Reflexionsvorgang in einem Schallfeld, oder eine Überblende- oder Overlay-Operation mit mehreren Masken-Ebenen, die neben dem Basis-Farbwert sowohl Transparenz alsauch Luminanz umfaßt (also das Problem, das pre-multiplied Alpha nicht wirklich lösen kann).

Alle »de-Coder« fallen tatsächlich in diese Kategorie, im Besonderen beim Extrahieren von Einzelkanälen zur getrennten Weiterverarbeitung, aber auch beim decodieren von komprimierten Strömen in Roh-Datenframes über eine Zeitspanne hinweg

die eigentliche Frage ist: wie übersetzt sich das in Topologie?

diese Frage muß wohl eigens gestellt werden — und das wird leider meistens versäumt

An der Stelle habe ich nicht weiter analysiert, sondern einfach Heap-Allokationen gemacht; der Grund seinerzeit war, daß Christian den »Mempool« überall einführen wollte — ein Ansatz, den ich grundsätzlich unterstützte, wenngleich auch seine Implementierung zu einfach war, und ich damit diesen use-Case nicht sauber realisieren konnte. Damit unterblieben aber weitere Überlegungen zum Allocation-Trend

damals hatte ich als Vorbild den small-objects pool allocator von Alexandrescu im Kopf; deshalb habe ich auch »Familien« von Objekten vorgesehen — ohne jedoch zu klären, ob und wie sich daraus ein Amortisierungs-Effekt ergibt. Nach gründlicherer Überlegung erscheint mir das als ein Widerspruch im Konzept, denn diese small-objects-Pools laufen ja auf ein Tiling mit fortlaufend stattfindedenden Allokationen hinaus; das ist exakt das Gegenteil von dem, was mir hier vorschwebt. Damit würden die Einzelpools nur Administrations-Overhead verursachen, der seine Vorteile überhaupt nicht ausspielen kann; stattdessen sollte besser in Betracht gezogen werden, alles heterogen, so wie es kommt, in größere Blöcke zu packen. Das Tiling würde damit auf einem größeren Level stattfinden, und wäre in den Basis-Allocator verlagert...

das waren allesamt Datenstrukturen aus der ersten Zeit; wir waren auf C++03 und ich hatte noch wenig Erfahrung mit Library-Design in C++ — leider wurde dieser teilweise recht dilettantische Code immerfort festgehalten  von den ersten Entwürfen zum Memory-Management und zur Node-Invocation. Insofern habe ich mich fast schon daran gewöhnt, das immerfort zu übersehen. Aber zum Glück ist jetzt die Ausrede weg, und die letzten Reste sollten keinen großen Aufwand mehr machen

  • Daten können aus dem Cache kommen, oder frisch aus einer Quelle
  • per Parametrisierung können manche Verbindungen deaktiviert sein

jede ProcNode kann gleichermaßen gePULLt werden.

Insofern braucht es im NodeGraph selber hier keinerlei spezielle Funktionalität, sondern jeweils nur eine top-level-Node, deren Output zur DataSink für den pull-Vorgang paßt. All die Services, die wir von einer ExitNode erwarten, finden auf einem Level höher statt, nämlich in der Job-Planung; dort brauchen wir diverse ID-Auflösungen und Informationsdienste

...was natürlich KISS ist; in einem zweiten Schritt repariert man diese Verschwendung durch ein mehr- oder weniger trickreiches Caching

bei dieser Variante ist der entscheidende Aspekt, daß diese Adaptierung automatisch in der Verschaltung realisiert wird; ebenso sorgt die Verschaltung dafür, ggfs den Cache mit anzusteuern

...sie werden en bloc in seine Liste der Inputs übernommen; erst in einem internen Adaptions-Schritt (FeedManifold) wird ggfs daraus ausgewählt

in dieser Variante wird die spezielle Adaptierung als separate Node explizit gemacht; somit werden Mehrfach-Verbindungen explizit zugelassen, und für ein solches Verbindungs-Bündel ist nur ein einziger Pull-Aufruf notwendig, wobei die Routing-Node dann sofort alle Outputs auf einmal bekommt — allerdings hat die Routing-Node stets nur einen Vorläufer

nicht nur welche Vorgänger, sondern auch wie diese angebunden sind

das „schreit“ gradezu nach einem Builder

...das Buch kannte ich damals noch nicht, und das merkt man

denn das high-Level-Model kann man genau deshalb im Kern (noch) nicht definieren, weil der Builder und das low-Level-Model fehlt — der eigentliche Grund dahinter aber war, daß ich den Sachverhalt noch nicht so weit verstanden hatte, daß ich für einen eignen Entwurf bereit war...

aber erwartungsgemäß hat man das eben grade nicht

...sondern nur einen lokalen Kontext

...und ich kann mich erinnern, daß ich sie auch damals nicht vestanden habe, sondern mich immer nur von Definition zu Definition hangelte...

das ist die gradezu klassische »Top-down«-Falle:

  • man definiert ausgehend von einem abstrakten Verständnis der Anforderungen (keinem echten Verständnis)
  • jedes ungelöste Problem und jedes fehlende Verständnis wird jeweils als Anforderung in die nächste Detail-Ebene verschoben
  • und beim Zusammenschalten der gebauten Teile steht man dann vor einem Wirrwarr, der nicht recht funktionieren will, aber immerhin alle präzise definierten (aber nicht verstandenen) Anforderungen erfüllt.

dieser speichert einen HashVal subClassification_

siehe type-handler.hpp

dieser zielt darauf ab, Strukturen in den Buffer zu pflanzen

und dieser gehört zu einer DomainOntology

Konsequenz ⟹ die Spec legt fest was in den Buffer gelegt wird

leider setzt das bereits die Domain-Ontology vorraus

...und zur Laufzeit wird nur noch ein Stück von diesem »was« angefordert

...diese Verbindet alle definierten Elemente in der einfachst möglichen / am wenigsten überraschenden Weise

  • es wird überall ein default-Buffertyp eingefüllt
  • die Reihenfolge der Eingangs-Buffer entspricht der Reihenfolge der Leads
  • es wird alles stets auf die durchgehend gleiche Port-# gelegt (D.h. wenn wir grade Port-2 bauen, dann werden auch alle Vorgänger auf Port-2 gepullt)

ausgeführt wird die Verdrahtung erst in der Terminal-Operation

man hat bereits die Vorgänger ProcNode(s) irgendwo sitzen

build() generiert einen Connectivity-Record by-value

weil sie ja tatsächlich als Brücke fungiert und Daten einspeist, die jenseits der normalen Aufruf-Hierarchie über das Turnout-System vermittelt werden

...also vor allem die Frage: wirft das jetzt alles über den Haufen, kann man es außen anbauen, oder gar in bestehende Strukturen lediglich hinein-codieren?

...da ich (wie so oft) mich bereits viel zu tief ins »wie« verstrickt habe...

...wobei es darum geht, wie die (bisher rein intuitive) Vorstellung vom Turnout-System in tatsächliche Strukturen übersetzt werden kann, und auf welcher Ebene dabei die Parameter-Verarbeitung sinnvollerweise anzusiedeln ist

...denn bereits für die Umsetzung der verknüpften, kaskadierenden Storage für das Turnout-System mußte ich massiv in die Meta-Programmierung einsteigen und mir das Instrumentarium für den Umgang mit beliebigen Datentupeln bereitlegen ... und so fühlte ich mich gerüstet, diese Art Datenhaltung direkt in die Kernverarbeitung hineinzunehmen, die dadurch (abzüglich der Metaprogrammierung) „eigentlich einfach“ wird...

Das bedeutet zweierlei

  • die Daten in der Invocation werden breiter und weisen Parameter-Werte direkt sichtbar aus
  • die Parameter-Verarbeitung aber wandert von der Oberfläche tief in die Weaving-Patterns hinein und wird beim Aufruf implizit mit angetriggert.

Der Ankerpunkt der normalen Verarbeitung sitzt nun im FeedPrototype, wird also sofort im ersten mount()-Schritt der Webe-Sequenz aufgerufen

denn nun wird in der Regel erst mal aus dem PortBuilderRoot das Binding für die Processing-Function angelegt ⟹ PortBuilder — und erst von dort gibt man dann Parameter an ⟹ modifizierter PortBuilder

Beachte: es handelt sich um einen Erweiterungspunkt von theoretischer Relevanz...

Ich kann derzeit lediglich vorhersagen, daß die strikte Beschränkung allein auf die absolute-nominal-Time im Allgemeinen nicht durchzuhalten ist. Es sind Fälle denkbar, in denen im Besondern aktzeller kontextueller Zustand der Applikation mit einfließt — die Automations-Auswertung mithin keine »pure Function« mehr ist.

Ich halte es für ehrlicher, diese Möglichkeit offen aufzuzeigen, und dafür sogar einen Ansatzpunkt im Modell vorzusehen; denn es ist allemal besser, wenn eine solche externe Verbindung sich als eine »AgentNode« im Modell niederschlägt, als wenn sie irgendwo durch eine Hintertür in einem Seiten-Effekt einzelner Parameter-Funktoren versteckt wird...

Die ParamAgentNode wird bezüglich des Aufrufs eine darunter liegende Node durchreichen wollen. Aber die Definition dieser darunter liegenden Node und deren Leads wird, typischerweise, auf die »Slots« im ParamAgent bezug nehmen...

⟹ folglich braucht es eine vorgelagerte ParamBuildSpec

Das heißt, hier haben wir immer einen festen Satz an Parametern, die stets im TurnoutSystem eingebettet vorliegen; diese können aber als HeteroData-Chain erweitert werden um die Blöcke, die wir über eine (oder eine Kette von) ParamBuildSpec erzeugen

mutmaßlich Nein

er wird tatsächlich auf Level-3 gebildet, und besteht zunächst auf einer inhaltlichen Identifikation des Proc-Asset, dann ggfs. noch einer technischen Differenzierung, welche die Domain-Ontology anhängt, sofern die errechneten Ergebnis-Mediendaten dadurch abweichen, und schließlich aus der Liste aller Input/Output Stream-Implementierungstypen

»dropper-Funktion« verwenden, die einen fest hinterlegten Parameter-Wert für jede Invocation in die FeedManifold setzt

Parameter aus TurnoutSystem per Accessor abholen; ein solcher typ-sicherer Accessor kann über das HeteroData-Framework generiert werden, welches als Storage mit verketteten, getypten Datenblöcken direkt in das TurnoutSystem eingebaut ist. In der Standard-Konfiguration enthält so ein TurnoutSystem nur einen ersten Block mit den Invocation-Parameter (absoluteNominalTime und ein processKey). Daher ist diese API-Variante nur interessant, falls vorher schon per Parameter-Node ein erweiterter Datenblock mit zusätzlichen Parametern irgendwo auf den Stack gelegt wurde

zeitbasierte Funktion adaptieren, für klassische Parameter-Automation. Der Zeit-Parameter ist dabei die absolute-nominal-Time,  welche aus dem Render-Job stammt und im TurnoutSystem abgelegt ist

beliebiger Parameter-Funktor, der auf dem TurnoutSystem arbeitet und einen passenden Parameter-Wert produziert; Typisierung wird zur compile-Zeit geprüft.

  • orientiert sich an der Reihenfolge der Outputs aus der Proc-Function
  • lediglich der Result-Slot muß spezifiziert werden

konkret: der InvocationAdapter muß Code enthalten, der sich aus den jeweiligen BuffHandles die expliziten Buffer-Pointer holt. Dazu muß er wissen, wo und in welcher Reihenfolge diese BuffHandles liegen — Beachte: diese Reihenfolge ist nicht zwingend die Parameter-Reihenfolge der aufzurufenden Library-Funktion.

  • in einem früheren Build-Schritt wird festgestellt,
    welche Eingabeparameter eine Lib-Funktion braucht
  • für alle diese Eingabe-Parameter wird eine Quelle vorgemerkt
  • ein weiterer Build-Schritt organisiert diese Datenquellen in Vorläufer-Nodes
  • ein weiterer Build-Schritt legt die pull()-Reihenfolge dieser Vorläufer fest
  • hierbei können weitere Vorläufer-Nodes hinzugefügt werden, um externe Belange zu befriedigen, wie z.B. Automation bereitzustellen

⟹ Nun, auf diesem Level muß der eigentliche LIbrary-Aufruf konfiguriert werden, und dabei muß jeder Input der Library-Funktion mit den richtigen Daten aus dem richtigen Vorläufer BuffHandle versorgt werden. Diese BuffHandles liegen nun aber in einer möglicherweise geänderten Reihenfolge vor, und es sind weitere BuffHandles dabei, die für den eigentlichen Aufruf nicht gebraucht werden (sondern für die weiteren, externen Belange)

diese wird später irgendwo instantiiert

Erwartetes Resultat: Aufruf der Library Funktion

Terminal: completePort()

das heißt, muß ohnehin einen Back-link halten, muß aber sich selber in den Parent-Builder zurückschieben (tricky und potentiell gefährlich....)

...nach dem Level-3-Build-walk sind sie allesamt nicht mehr erforderlich — und könnten per AllocationCluster auf einen Schlag weggeworfen werden

...d.h. die Planung steigt für einen Port ein, findet für dieses ein Proc-Asset in einem bereits vorliegenden Prototypen der Connectivity, und kann für dieses Proc-Asset alle benötigten Inputs identifizieren und jeden von diesen einem anderen, ebenfalls bereits bekannten Vorläufer Proc-Asset zuordnen; diese Zuordnung wäre dann die Belegung eines Port, und das Proc-Asset würde zu einer geplanten Node. Im Besonderen ist durch diese Vorraussetzung festgelegt, daß alle diese Belegungen bereits eindeutig und entscheidbar sind; Zweideutigeiten und Unmöglichkeiten sind in den vorausgehenden Verarbeitungsschritten bereits aussortiert worden...

Leads (≙Vorgänger-Nodes) werden nach Bedarf angelegt als Level-3-Builder

...anders als die Konfigurations-Walks auf Level-3 vor dem Triggern des Build-Walk; diese traversieren über Ports und Stream-Verbindungen, folgen also einem bestimmten Berechnungspfad. Dagegen für die Level-2-Builder sind Lead-Nodes vorrausgesetzt, und das bedeutet, ein Build erfolgt für die ganze Nodes, für alle Ports zusammen

tatsächlich wird jeder Build-Schritt hier delegiert per Proc-Asset ⟶ Media-Lib (Ontology)

....denn dadurch können wir eine Meta-Ontology vermeiden: der entscheidende Übersetzungs-Schritt, das Ontology-Mapping aus dem Kontext der Edit-Session in das konkrete Processing erfolgt direkt im Kontext einer konkreten Domain-Ontology, beispielsweise FFMpeg. Vorgegeben ist eine semantische Attributierung der Syntax-Struktur im Session-Model und erwartet wird eine korrekte, konkrete Implementierung derselben.

ein Schritt: ExpectationContext ⟼ ProcNode

sodann wird für den Vorgänger ein Level-2-Builder erstellt

tatsächlich ist jede Port-Impl ≙ Turnout ein Level-1-Builder

nichts davon hängt von Strukturen aus der Domain-Ontology ab

...und hinter dem liegen ebenfalls Strukturen aus der Ontologie; das ist ja sogar gradezu der Kern der Sache: jede Library bestimmt was es für Arten von Medien und Strömen geben kann

...das klingt vielleicht nach einer steilen These, beruht aber auf der Beobachtung, daß eine solche Schematisierung (wiewohl denkbar), ihrerseits wieder Teil einer Domain-Ontology wäre — jeder Versuch, dies zu generalisieren liefe auf eine Universal-Ontologie des ganzen Fachbereichs hinaus, und ist daher grundsätzlich zum Scheitern verurteilt, denn so etwas gehört nicht auf die Ebene praktischer Organisation, auf der wir uns hier bewegen, bei der Konstruktion von Systemen der Informationstechnik.

Konsequenz ⟹ der Aufruf der Level-2-Parametrisierung muß in einem Callback passieren

Das bedeutet: wenn man auf dem default-konstruierten Builder die build()-Metode ausführt, dann entsteht eine Node, die zwar alle erwarteten Ports hat, aber alle diese Ports liefern bei Aufruf ein NULL-Handle des jeweils eingesetzten BufferProviders. (Selbstverständlich wäre es viel schöner, an dieser Stelle einen leeren Frame zu liefern, aber das ist nicht möglich, ohne das Format zu kennen, welches jedoch von der Domain-Façade geleistet werden muß)

der Callback lautet: configureNode(builder)

Hiermit sei nur angezeigt, daß es weitere solche Festlegungen zur Betriebsart geben kann, die typischerweise auf einem höheren Level bereits entschieden wurden, vermutlich ebernfalls unter Zuhilfename der Domain-Façade

Alle diese Callbacks werden auf dem Interface DomainFacade gesammelt

...diese muß die Grundzüge des Asset-Systems, die Schnittstellen-Entitägen des Builders, das Stream-Type-Framework (und vmtl.) noch Weiteres enthalten. Außerdem müssen alle die genannten Entitäten von der Implementierungs-Ebene entkoppelt sein

...da der Typ des Builders vom Allocator abhängt

Aufbau erfolgt aus dem Scope der DomainOntology

⟹ Konsequenz: zusätzlicher Template-Parameter für das Spacing

oder man bekommt eine implizite Runtime

wenn die Daten „woanders“ liegen

⟹ das Allocator-Problem überträgt sich komplett  auf den Container selber

warum? weil man dann zwingend im Container selber einen »Slot« mit einem Functor oder Allocator-Pointer rumschleppt — oder doch wieder einen zusätzlichen Instanz-Typ-Tag

Sorge:

  • ich erwarte ~10k Nodes im Modell
  • die meisten Nodes haben nur einen Vorläufer / Nachfolger

⟹ bläht die Storage um 30% auf

Da der Heap-Allokator inzwischen ziemlich performant ist, könnte man damit durchkommen...

Sorgen:

  • wenn es dann doch Probleme gibt, ist der Zug bereits abgefahren (weil Allokationen überall im Code passieren)
  • auf anderen Plattformen kann die Performance vom Heap-Allocator ganz anders sein, und wir haben darauf keinerlei Einfluß

...man verwendet nur speziell im produktiven Einsatz im Node-Graph einen besonderen Allocator, der zwar den Destruktor aufruft, aber den Speicher nicht freigibt; alloziert wird immer in einen kompakten Block hinein, der dann auf der Basis der Prozeß-Kenntnis als Ganzes verworfen und neu verwendet wird.

...weil std::vector zwar bereits alles bietet, aber eingebettet in sehr komplexen Code — im Besonderen dürfte es schwierig werden, das Thema on-demand-growth vs non-copyable zu umschiffen

...das heißt, ich gehe mal davon aus, daß ich mit einer einzigen, dedizierten Implementierung erst mal den aktuellen Bedarf decken kann; daraus könnte allerdings später immer noch ein Concept gemacht werden, welches dann alternativ auch durch ScopedCollection oder durch eine embedded-storage-Lösung erfüllt werden kann.

heterogene Allokation in eine Sequenz größerer Blöcke; keinerlei de-Allokation und kein Locking

...sie ist ja fertig und getestet, und wartet seit Jahren auf ihren Einsatz; allerdings wäre ein solches Vorgehen erklärungsbedürftig

  • man tut so, als wäre etwas in Ordnung, das nicht in Ordnung ist
  • die bestehende Implementierung ist sogar maximal-dämlich
  • und bietet zudem keinen guten Pfad zur Weiterentwicklung, sondern müßte irgendwann ersetzt werden
  • allerdings spielt das Thema vermutlich lange Zeit gar keine Rolle (solange wir nicht sehr große Modelle bauen)

...wenn man schon die bestehenden Implementierung nutzt (wohl wissend, daß ihre inhärenten Probleme erst mal nicht relevant sind), dann kann man genausogut ganz auf blöd sich auf den KISS-Standpunkt stellen und einfach Heap-Allokationen machen, denn die sind heutzutage verdammt effizient geworden

...und alles das läuft auf weitere technische Schulden hinaus

...die allesamt mit dem Model + Player zu tun haben; einzige externe Verkoppelung ist der LinkedElements_test, und auch dieser stellt explizit einen Vorgriff auf die Verwendung im low-level-Model dar.

...da der davon abhängende Code effektiv nur compilierbar ist, aber nicht lauffähig

let it crash — wenn tatsächlich eine Exception fliegt, ist es ziemlich wahrscheinlich, daß der ganze Cluster sowiso weggeworfen wird; wenn nicht, dann akzeptieren wir einfach toten Speicher.

die Bedeutung ist geringer geworden

seinerzeit habe ich im AllocationCluster etwas gesehen, daß pervasiv überall im Code verwendet wird, analog zum Mempool. Inzwischen stehe ich auf dem Standpunkt, daß für die meisten Allokationen der Standard-Heap-Allokator sowiso gut genug ist (oder man nutzt ohnehin den Stack oder eine statische Variable); spezielle Allokatoren sind nach meinem heutigen Verständnis nur noch sinnvoll, wenn sie extrem spezifisch sind

...oder zumindest könnte man ein limitiertes Teil-Konzept umsetzen; mir fällt auf, daß diverse Methoden im Standard-Allocator inzwischen durch Traits ersetzt wurden.

Konsequenz: man kann einen einzigen Typ Several<X> anschreiben

unser Storage-Puffer ist alignas(I) — das kann zu locker sein für das tatsächliche Objekt

Beispiel: I ist eine leere Klasse, hat also sizeof(I) ≡ alignof(I) ≡ 1

Wenn's dumm läuft, kann der Puffer mit jedem beliebigen Byte-Offset beginnen z.B. 3

Nun platzieren wir aber eine Subklasse mit alignof(E) ≡ 8 und sizeof(E) ≡ 12

⟹ die naive Lösung beginnt bei offset ≔ 3 und fügt für jeden Index +12 Bytes hinzu; damit sind alle Objekte grob falsch ausgerichtet

man könnte das nun ungenau oder punktgenau oder grob korrekt handhaben 

auf Basis der buffSiz, d.h. der Größe des reinen Nutzdatenpuffers; auf diese konnte man noch den statisch ermittelbaren Overhead aufschlagen

Der Allocator macht nur eine raw-storage-Allocation, da is nix mit Alignment. Insofern kann der Beginn des gelieferten Speicherbereichs auch „daneben liegen“ — so daß wir einen Korrektur-Offset brauchen. Und der läßt sich nicht systematisch erschließen

...to allocate storage required for a single object whose alignment requirement exceeds __STDCPP_DEFAULT_NEW_ALIGNMENT__

...weshalb ich ja keinen wirklich passenden statischen Typ für ArrayBucket angeben kann, weil die Puffergröße erst zur Laufzeit bekannt wird.

das ist eine gewisse Vereinfachung, die aber aller Wahrscheinlichkeit von allen Plattformen erfüllt wird, allein schon weil man sonst an jedem Objekt mit VTable herumfummeln müßte

Der Aufruf des Allokators erfolgt aus dem Builder, und an der Stelle ist ohne Weiteres der volle Typ konstelliert

Für den reinen Zugriff genügt ein vereinfachter Typ — insofern dadurch der Zugang zur dynamischen Layout-Information möglich wird

  • verwende sizeof(ArrayBucket<I>) als Offset
  • dimensioniere die eigentliche Allokation so, daß vorne das ArrayBucket reinpasst, und dahinter alle gewünschten Daten

...und zwar vor allem durch den AllocationCluster mit einer festen Extent-Size. Vorhersehbar werden die Extents meist zu groß sein...

...und was noch besser ist: die Storage liegt kompakt

...weil wir durch den Connectivity-Descriptor bereits eine gefährlich komplexes Stück Metaprogramming haben, mit dem Risiko kombinatorischer Explosion

Kann derzeit keine befriedigende Lösung für die diversen Zielkonflikte finden. Daher wähle ich eine Lösung, die Raum für zukünftige Lösungen schafft, und aktuell im Basisfall einfach und gradlinig zu implementieren ist. Speicher-Mehrverbrauch für die Metadaten wird in Kauf genommen

ein einfacher Pointer geht nicht, wegen dem anzuwendenden Spread

einfach ein lib::IterIndex<const Several<X>>

diese Funktion würde eine Exception werfen, wen das ArrayBucket eben doch ownership-managed ist. Ansonsten erzeugt sie eine neue Instanz mit Verweis auf das gemeinsam nutzbare Bucket

...weil ich den Zieltyp zum Cast nicht dynamisch konstruieren kann

günstigenfalls bleibt der Backlink dann inaktiv

heißt: er kann zwar nicht wegfallen (weil sich das dann im Typ ausdrücken würde) — aber der Pointer bleibt dann NULL und der Container fällt auf Standard-Verhalten zurück

....das ist ein besonders relevanter use-case, nämlich wenn wir einen Allocation-Cluster verwenden; in diesem Fall wird anfangs vom Builder der Speicher zugeteilt, und dann später einfach „fallen gelassen“. Aufgrund einer allgemeinen Lifecycle-Argumentation kann zu einem späteren Zeitpunkt der gesamte Allocation-Cluster ohne weitere Aufräum-Aktivitäten verworfen werden. Das ist wie ein Garbage-Collector ohne Garbage-Collection ☺

Das ist die Quintessenz. Hat er eine eigene Identität, so muß er eigens konstruiert werden und damit eigens verdrahtet sein — und wir brauchen genau diese Allokator-Instanz zur De-Allokation.

Zunächst einmal ist das eine logische Abschwächung, und keine Verstärkung. Daher ist diese Bedingung logisch nicht äquivalent zur vorgenannten Voraussetzung (eigene Identität). Jedoch läßt sich dieses Kriterium nachschärfen: man fordert, daß der Allokator default-Konstruierbar ist, und daß eine default-konstruierte Instanz mit dem gegebenen Allokator äquivalent ist (gemäß C++ Konventionen prüfbar durch den Vergleichsoperator).

Wenn dieser Test scheitert, dann muß die Allokator-Instanz erhalten bleiben.

z.B. durch einen Meta-Allokator, wie z.B. ein statisch vorgehaltener Puffer....

In der Tat: siehe special-job-fun.hpp

Same as std::move unless the type's move constructor could throw and the type is copyable, in which case an lvalue-reference is returned instead.

__and_<__not_<is_nothrow_move_constructible<_Tp>>

      , is_copy_constructible<_Tp>>

sonst: _M_realloc_insert

die C-Funktion realloc() kann zwar scheinbar „zaubern“, ist aber tatsächlich auf Hilfe vom Allokator angewiesen, insofern dieser sich intern gewisse zusätzliche Reserven sichert. Und wenn seine Resevern nicht reichen, dann wird sofort woanders alloziert und alles per memmove()  umkopiert....

Tatsächlich kann std::vector() dasselbe besser machen, da er ggfs move-Konstruktoren aufrufen und außerdem als zusätzliche Heuristik die aktuelle Größe des Vektors heranziehen kann, um eine angemessene Reserve bereitzustellen; außerdem ist die Größe der Reserve direkt auf das API herausgeführt.

Fazit:

 entweder sie ist statisch —

 oder es ist eine selbstzerstörende Heap-Allokation

jetzt habe ich mich sowiso schon mal für die vorläufige Verschwendung entschieden

weil der Several-Container selber nur den Spread kennt, aber keine konkrete Typ-Info mehr hat

...das mach die Verwendung einfacher (kein "template"-Präfix vor jeder Methode) und speziell das Deleter-λ kann nun direkt auf die geerbte Factory mit dem eingebetteten Allokator durchgreifen; ja der Code ist inzwischen riesengroß, aber alles hängt irgendwie mit allem zusammen, und ich sehe nicht, wie ich hier eine Teilkomponente so extrahieren könnte, daß der Code wirklich einfacher wird. Jedenfalls die separate Strategy-Klasse ist es nicht...

oder es wird ausschließlich der Element-Typ E konstruiert

Vorsicht: hab es aus der emplaceElm()-Funktion entfernt

...und das kann vom Allokator abhängen — sollte also in den meisten für Lumiera wirklich relevanten Fällen gar nicht vorkommen

...nicht nur eine Schwäche, sondern ein ausgewachsenes Ärgerniss. Allerdings betrifft das strenggenommen nur die libStc++

Und zwar ist es so: man könnte einen Vector per resize() vordimensionieren, so daß die Storage nicht per alloc-and-copy wachsen muß; danach könnte man eigentlich ohne Weiteres auch non-copyable-Objekte im Vector haben — solange man den Vector selber ebenfalls nicht kopiert (Verschieben wäre möglich).

Soweit die Theorie... in der Praxis aber scheitert das, weil der Compiler versucht, den realloc-Code zu instantiieren und dafür keinen Copy/Move-Konstruktor findet

Und dazu sind wir im Stande...

  • wir haben entsprechende Prüf-Logik, die zur Laufzeit eine Exception wirft, wenn die Kapazität nicht reicht. Da Objekte nur im Builder hinzugefügt werden, ist das hier viel akzeptabler als für std::vector, der durch ein solches Verhalten ja doch massiv unzuverlässig würde.
  • wir müssen dann nur sicherstellen, daß der typisierte copy-Code in unserer realloc()-Funktion ebenfalls einen statischen Guard hat, und damit auch überhaupt nicht emittiert wird, wenn wir ihn ohnehin nicht nutzen können

gefunden in: new_allocator.h, Line 130

zieht noch einen internen Admin-Overhead ab von der Extent-Size

sonst würde eine Spread-Vergrößerung

tatsächlich die Reserve verkleinern...

  • Beispiel:
  • Buffer hat Kapazität für 10 Elemente bei Spread ≡ 1 und 3 Elemente sind belegt
  • es wird ein 4.Element der Größe 8 bytes verlangt
  • 4*8 = 32 > 10 ⟹ realloc()
  • naiver weise würde man jetzt auf 32 Bytes vergrößern, aber danach wäre der Buffer bei Spread ≡ 8 sofort wieder ganz voll
  • daher ist es sinnvoll, die bisherige Reserve von 7 freien Slots zu beachten; d.h. man vergrößert auf 10*8 = 80 bytes
  • danach paßt das 4. Element rein und es ist nach-wie-vor Platz für 6 weitere Elemente

Begründung: das ganze Thema »spread« ist extrem technisch und für den Nutzer normalerweise nicht nachvollziehbar, aber die Kapazität in Anzahl der freien Slots ist sehr wohl verständlich für den User; es wäre also ziemlich überraschend wenn — scheinbar ohne ersichtlichen Grund — plötzlich die Reserve-Kapazität verschwunden wäre.

...man könnte geneigt sein, das zu überspringen; aber ich entscheide mich hier explizit dagegen, weil es gegen den Stil von C++ verstößt. Wenn man den Deleter-Aufruf vermeiden will, dann soll man eben triviale Objekte verwenden. Ende der Diskussion.

Trick: capture der factory per value ⟹ Kopie des Allokators

Grundsätzlich ist der Allokator einfach durch die Belegung des 3.Template-Parameters festgelegt, das heißt, er steckt in der Policy — diese ist aber rein operational bestimmt, indem sie die richtigen Primitiv-Operationen mit der richtigen operationalen Semantik bereitstellt. Das ist keine gute Schnittstelle für den praktischen Gebrauch, und deshalb wird ein Hilfs-Schema über die Builder-Schnittstelle bereitgestellt; das erscheint eine sinnvolle Trennung zu sein, um diese zusätzliche Komplexität aus der Kernkomponente herauszuhalten

das ist jetzt noch ein weiteres extra Datenfeld

denn der Allocation-Cluster weiß selber die Zahl seiner belegten Roh-Blöcke; zur de-Allokation muß ansonsten gar nichts gemacht werden

Der vermutlich häufigste Fall ist ein Several<ProcNode*> — mit typischerweise genau einem Element mit einem »Slot«Größe. Dafür haben wir jetzt schon 4 »Slot« Overhead, und jetzt sollen das 5 »Slot« werden....

...das heißt, wir verwenden im ersten »Slot« ein Bit-Feld, da die Zahl der Elemente ohnehin praktisch begrenzt ist (auf ein paar Hundert); in den freien oberen Bits kann daher das konkrete weitere Layout encodiert werden. Das kostet nur einen minimalen Runtime-Overhead (ein paar Bit-Manipulationen vor der Indirektion zum Datenelement, welches ebenfalls im Cache liegt). Der intendierte use-case nutzt diese Collection als Basis einer verzeigerten Datenstruktur, und nicht in einer innersten Loop.

besonderes geschickt: man kann das „später mal“ machen

Denn Lumiera hat im Moment wirklich andere Sorgen, als die Optimierung einer nocht-gar-nicht-wirklich-genutzten Datenstruktur

die grundsätzliche Möglichkeit der Anpassung ist bereits vorher sichergestellt, denn der veranlassende API-call wäre sonst schon per Exception abgebrochen worden

Das ist eine Festlegung aus pragmatischen Gründen; weder der Container noch der Builder erfasst den Typ einzelner Elemente, daher gibt es auch keine Möglichkeit, überschüssigen Spread festzustellen. Zwar könnte der Client-Code diese Information besitzen, aber dann könnte er auch gleich richtig dimensionieren. Generell wird der Änderung des Spread keine besondere Bedeutung zugemessen — wenn es geht, wird's gemacht.

der Puffer sollte mit einem Initial-Wert beginnen, und dann in Verdoppelungs-Schriten wachsen; Verdoppelung setzt natürlich Kenntnis der aktuellen Puffergröße voraus; dabei gäbe es aber auch noch einen Maximalwert zu beachten, der vom Allokator abhängen kann.

...im Besonderen könnte man den std::common_type verwenden, um ein mögliches Interface zu inferieren, und zudem einen gemeinsamen Element-Typ zu erkennen...

...denn lib::Several ist ein ziemlich trickreicher low-level-Container; es könnte gefährlich werden, wenn man beliebige Typ-Parameter verwendet; insofern ist es nicht wünschenswert, dem User das explizite Wählen der Typ-Parameter zu ersparen (abgesehen von dem einfachen Fall mit der Initializer-list)

...also ist das keine gute Idee, da verwirrend

enthält ein nested template / typedef: Policy

Name: SetupSeveral

Schema: Policy<I,E>

Denn auch den Allocator verwendet man typischerweise an vielen Stellen, und nicht überall möchte man dann auch several-builder.hpp mit reinziehen; wäre also gut wenn es einfacher Template-Code ist, der mit entspr. Forward-Deklarationen auch »blank« vom Compiler akzeptiert wird

template<class I, class E, template<typename> class ALO>

struct AllocationPolicy;

template<template<typename> class ALO, typename...ARGS>

struct SetupSeveral;

SetupSeveral<std::void_t, lib::AllocationCluster&>

Die Definition der Builder-Methode withAllocator<ALO>(args...)  sollte dazu führen, daß das Konfigurations-Template SetupSeveral  genau mit diesen Argumenten instantiiert wird...

...ich hab diese Mechanik ganz bewußt janz domm implementiert — also muß praktisch alle Info von oben kommen

diese erweiterten Event-Log-Matches sind wohl etwas ad hoc

Ich habe nun beschlossen, sie zu bilden als Produkt

  • interne laufende ID der Allokation
  • nominelle Größe der Basis-Allokation

Name: TrackAlloc<TY>

verwende die Speicheradresse direkt als Hash-Wert; muß dazu eine Hasher-Funktion implementieren und in den Typ der Hashtable aufnehmen

...das ist die Art von bequemen Beschäftigungen, die sich nach viel Arbeit anfühlen, tatsächlich aber nur darin bestehen, ein gewohntes Schema durchzuziehen ... man erbt von lib::Sync, man bastelt die Guards in jede Methode, man zimmert einen geilen Test mit dem Threadwrapper. Dadurch wird der neue Code keinen Deut besser.

Der Zweck der TrackingFactory ist es, das saubere Verhalten eines Custom-Allocators zu belegen. In Grenzfällen könnte das zwar auch Concurrency involvieren — jedoch ist es aus heutigem Verständnis generell nicht mehr üblich, Allokationen in der »heißen Zone« zu machen. Typischerweise verwendet man genau dafür einen Builder oder einen Pool und teilt die Ressourcen schon im Vorhinen den Threads zu.

im Zweifelsfall wäre ein Adapter über der Factory einfacher

kann man gefälligst selber machen

denn dort rufen wir den Destruktor-Funktor explizit auf, anstatt den Destruktor von ArrayBucket aufzurufe

...denn auch am Ende bleibt ein use-cnt übrig, obwohl doch in diesem Fall letztlich der Destruktor des Funktors aufgerufen werden sollte, wenngleich auch bereits nach der de-Allokation (!)

...deshalb habe ich da so sonderbar darum herum gecodet. Ich dachte mir, kein Problem, ArrayBucket ist ja sowiso ein POD. Und dann bin ich »eingeknickt« und habe doch eine std::function genommen. Und deren Destruktor muß aufgerufen werden

weil vor jeder regulären Änderung der Checksumme auch ein Log-Aufruf steht. Und die Log-Einträge sehen allesamt korrekt und balanaciert aus

  • nicht die tatsächliche Speicheradresse wird bei Allokation geloggt, sondern die Addresse des Arbeitspointers
  • nicht die tatsächliche entry-ID wird bei Deallokation geloggt

...fälschlicherweise der laufende Allokations-Counter verwendet

...konkret, ich plane einen Satz an Steuer-Flags, und auf dieser Basis dann die Belegung weiterer Storage; im einfachsten Fall gibt es keinen Spread, keinen Deleter und einen Standard-Offset; es muß dann nur die Element-Zahl und Kapazität gespeichert werden.

  • provoziere mehrere re-Allokationen
  • prüfe die use-counts für die eingebetteten Allokator-Instanzen
  • move-Asignments räumen auch sauber auf

...da AllocationCluster eine starke Größenbeschränkgung hat

denn das eine zusätzliche Element würde locker reinpassen — und zudem sollte ja nun der Block in den nächsten Extent migrieren

einmal aus emplaceNewElm() und einmal aus reserve()

das ist es nämlich, worauf die Implementierung hinausläuft, und dieser Grebrauch ist auch naheliegend und korrekt, sofern es sich um das Einfügen neuer Datenelemente handelt. Insofern muß sich der andere Gebrauch in reserve() daran anpassen

...daß da immer noch high-level-Code darüber liegt, der eine inhaltliche Prüfung gemacht hat, so daß hier nur noch ein Konsistenzcheck vonnöten ist

...aber realistisch betrachtet ist der AllocationCluster viel zu einfach zu verwenden, als daß man da überhaupt daran denkt, noch explizit zu prüfen — und durch den standard-Allocator-Adapter gibt es unvermeidbar einen direkten Eingang über allot().

...aufgrund der Flexibilität ist das intendierte Verhalten nicht mehr einfach zu fassen; es ist eigens ein Zugangsweg zu finden, um die richtige Logik zu entwickeln---

Vorsicht Falle!

Ohne diesen Check würden wir uns in den Fuß schießen, wenn wir den Container in den wild-move-Modus schalten, denn Elemente vom Typ E oder I könnten stets ohne Weiteres noch dazukommen

...denn die default-Impl kopiert skalare Typen lediglich; hier müssen wir sie aber wirklich austauschen, damit nur eine Instanz den aktiven Pointer hält...

storageBytes wird nur in der Factory benötigt und ist die Größe der gesamten Allokation incl Admin-Overhead

....und das wird praktisch immer greifen, wenn Element-Typ und Interface-Typ identisch sind; es steht zu befürchten daß deshalb der triviale destruktur praktisch nie zum Tragen kommt. Anders herum bestünde auch keine Gefahr, daß ein erstes Element, das auch ein Subtyp sein könnte, eine Entscheidung für TRIVIAL fällen würde, denn der ELEMENT-Fall fordert ja grade, daß alle Elemente den gleichen Typ haben; also wäre eine solche Festlegung auch in diesem Fall sogar vorteilhaft, da sie den Destruktor-Aufruf einspart

...und zwar, weil man ein (language)-Array mit operator new[] allozieren muß

...✔ OK — selber schuld

und der von Dummy muß noexcept sein

Es ist nämlich so: wenn wir gleich die Move-Sperre setzen, können wir ansonsten das Objekt durchaus reinlassen — vorausgesetzt das mit dem Destruktor bleibt OK — und letzteres prüfen wir ja auch, nur eben später (aus Gründen der einfachen Formulierung im Code, weil wir den Rückgabewert von dieser Prüfung erst später brauchen)

das im Test beobachtete Verhalten ist korrekt

...es spricht tatsächlich nichts dagegen, diesen Fall auch nachträglich noch zuzulassen — man gibt dann eben die Möglichkeit für re-Allocations auf (und wenn das ein Problem darstellt, macht sich das zu gegebener Zeit eindeutig bemerkbar)

  • gleich zu Beginn — noch vor allen Storage-Prüfungen — stellen wir fest, ob wir (noch) Ojbekte verschieben können. Wenn das für ein einziges Objekt nicht mehr gilt, dann gilt es eben für den gesamten Container nicht mehr. Es ist zu dem Zeitpunkt noch nicht klar, ob das überhaupt ein Problem darstellt.
  • als Nächstes prüfen wir die Speicheranforderungen; wenn der Speicher nicht reicht und wir zudem nicht verschieben können, dann staubt's
  • nur wenn nötig und möglich wird die Allokation vergrößert. An dem Punkt ist alles wieder sauber für die bestehenden Elemente
  • erst danach müssen wir die Destruktor-Möglichkeit für das neue Element prüfen. Wenn hier ein Fehler auftritt, wurde zwar ggfs der Puffer (unnötigerweise) vergrößert, aber es hat noch keine Objekt-Konstruktion stattgefunden, und wir die Gesamtsituation ist weiterhin konsistent

kann keinen ganz anderen Typ platzieren (der nicht Subklasse ist)

dadurch daß alignas(void*) aber auch da ist, stellen wir mindestens »slot«-Alignment sicher; das löst zwar nicht das Problem wenn nur ein einzelner Element-Typ overaligned ist, aber schließt Probleme mit allen regulären Typen aus

Vorsicht Falle: der Allocator kann das gar nicht machen, wenn man ihm im Sinn von »placement new« explizit die Position vorgibt

versteckte Inkonsistenz mit der Deleter-Behandlung aufgedeckt

wenn eine pull()-Kette nicht bis zur tatsächlichen Quelle reicht, sondern an irgend einer Stelle bereits Ausgangs-Daten in einem Buffer liegen müssen; beispielsweise weil diese Daten von einem IO-Job gelesen wurden, oder von einem Decoder/Demultiplexer aufbereitet wurden — es gibt dann eine hand-over-Node, und diese braucht ein konkret passendes BuffHandle für jeden einzelnen Frame

es bekommt zusätzlch nur die Job-Parameter

  • WiringDescriptor, und dann eine Impl NodeWiring
  • aber die eigentliche Impl steckt dann in einem Parameter-Typ STATE
  • die ProcNode delegiert durch ein SAM-Interface
  • und der tatsächliche Aufruf greift aber wieder auf die In/Out-Arrays durch
  • der StateProxy schwirrt irgendwo dazwischen auch noch herum

aus dem NodeWiring-Konstruktor passieren jede Menge Callbacks in die WiringSituation

man geht von ProcNode durch Connectivity, dann NodeWiring ... und wenn man Glück hat, fällt einem der Header nodeoperation.hpp  auf...  ⚠ denn sonst ist der tatsächliche Einstiegspunkt mit üblichen Methoden der Code-Suche praktisch nicht zu finden: Er steckt in nodewiring.hpp in der konkreten Definition der virtuellen Funktion callDown()  — und von dort in eine dependent Method thisStep.retrieve(), die tatsächlich im Template ActualInvocationProcess an STRATEGY::step()  weitergereicht wird — und vermutlich ist nur mir klar, daß damit die verketteten step()-Methoden in nodeoperation.hpp  gemeint sind...

per Default mit absolute nominal Time zu erzeugen

Das muß so sein aus Gründen der logischen Konsistenz: Der Invocation-Mechanismus der Render-Engine ist generisch, und das bedeutet, er kann nichts implizit über das zu rendernde Modell wissen; zwar wird für den Build-Vorgang in absolute Placements reduziert, aber diese beziehen sich immer noch auf eine bestimmte Timeline — ebenso wie der Render-Vorgang, der auf einer Timeline abläuft. Das bedeutet, für den Rendervorgang ist das Koordinatensystem implizit, und er gibt nur eine absolute nominal Time relativ dazu an; jedoch wird dieser implizite Kontext in der Job-Planung übersetzt in den Zugriff auf eine bestimmte konkrete Exit-Node. Insofern kann dann ein Job komplett generisch auf der Render-Engine laufen, denn er tarnsportiert sowohl die absolute nominal Time, alsauch die konkrete ExitNode. Das Turnout-System selber ist ebenfalls generisch, und das heißt, es wird nur sinnvoll in Verbindung mit einer ExitNode zur Aufführung gebracht

und diese ist massiv (Millionen Zyklen, allerdings in Speicher, der dann im Cache liegt)

Vorsicht Falle: nicht in heißer Loop messen, sondern in einem realistischen Szenario unter Speicher-Druck

...oder werden sie von der Medien-Berechnung verdrängt?

WeavingPattern ⟶ TurnoutSystem (Stack) ⟶ Index-Tabelle (Buffer) ⟶ Storage (same Buffer)

  • der erste Slot ist stets günstig, denn er liegt grundsätzlich im TurnoutSystem selber (die nominal Time)
  • wenn es nur einen weiteren zusätzlichen Parameter gibt, wäre die Index-Tabelle ein nutzloser Overhead, sie muß ja auch aufgebaut werden
  • bei mehr als drei Slots sind wir im Vorteil (wobei der lokale Hebel erst mal gering sein dürfte, denn spätestens nach dem ersten Zugriff liegt der Buffer im Cache)

....die intrusive linked List habe ich nämlich fertig und getestet, zudem ist der Code dafür sehr robust zu verwenden, wenn man ohnehin einen privatenTyp für ein Bucket definiert. Dem gegenüber müßte man die Index-Tabellen-Lösung erst mal konzipieren und austesten, und zudem wäre die wohl eine Spezial-Implementierung und daher auch jedes Mal wieder zu verstehen. Damit ist die Entscheidung klar, man muß wirklich erst mal zeigen daß ein Problem besteht...

der Build-Prozeß belegt sukzessiv mehrere abstrakte Slots

Eine compiletime-Lösung setzt zwingend vorraus, daß der Code zur Belegung mehr oder weniger explizit und fest verdrahtet ist

Ich hab ein ungutes Gefühl, wenn man jetzt sagt, das muß dann halt im Einzelfall programmiert werden. Denn es ist nicht so, daß es hier einen »User« oder »Client« gäbe, wie bei einer klassischen Library. Vielmehr muß später, in einer weiteren Runde, ein Builder implementiert werden, der seinerseits wieder generisch sein soll. Demgegenüber kann das Library-Plug-in zwar Adapter bereitstellen, aber diese Adapter können keine Implementierung einer Parameter-Automation bereitstellen, denn das ist ein zur Medien-Berechnung komplett disjunkter Belang — das heißt, genauso wie es für eine Film-Timeline schon mal eine Video- und eine Audio-Renderpipeline geben wird, wird es zusätzlich auch noch eine Automations-Pipeline geben. Nur daß diese nun auch noch mit der Audio / Videoverarbeitung quer-verknüpft sein muß. .... hoppla — das ist eine neue Einsicht

und dann gibt es eben doch eine Render-Engine Domain-Ontologie

ganz analog wie ebenda gefordert werden kann: brauche N Eingabepuffer mit Format F₁ und M Ausgabepuffer mit Format F₂

das ist die Systematik die auf Level-2 gebraucht wird

...das wäre fatal, denn dann würde die Abstraktion zusammenbrechen; etweder der Builder, oder (noch schlimmer) das Library-Plug-in müßte Render-Engine-Internals instrumentieren

explizit ausgeführte

Parameterbehandlung

Es geht um die Verständlichkeit und Wartbarkeit des Codes. Wenn ein derart zentrales Thema überhaupt keinen Niederschlag in konkreten Strukturen findet, sondern nur über eine Konvention abgebildet wird, ist das verwirrend und fehleranfällig. Umso mehr, als hier nun auch noch auf transiente Datenpuffer verwiesen wird.

Damit wäre nämlich eine relativ sichere Storage auf dem Stack gegeben, und die Gegenwart der Parameter wäre trotzdem stets eindeutig dokumentiert. Auch im Hinblick darauf, daß vermutlich sehr häufig irgend welche Parameter fest gesetzt werden müssen (aber nicht per Automation bestimmt)

die explizite Behandlung würde damit in das konkrete Weaving-Pattern verlegt

in diesem Fall hätten alle Bindings die gleiche Port-Subklasse, würden aber beim Zugriff auf die Parameter einen virtual call machen

wichtigster Vorteil: es ist noch nicht festgelegt

Ich wollte mehr, und deshalb halte ich die Stelle für das TurnoutSystem offen — obwohl auf gegenwärtigem Stand seine verbleibende Funktionalität komplett in die interne Mechanik integriert werden könnte. Auf diesem gegenwärtigen Stand kann ich die Vorstellung noch nicht weiter entwickeln, weil mir der klare Blick auf den realen Gebrauch in den tatsächlichen Proportionen fehlt — aber ich hoffe, daß sich dann aus dem Einsatz eines Baukasten-Systems irgendwann klarere Muster codifizieren lassen

Fazit: zurück zum ersten Konzept

einen Satz Parameter an anderer Stelle platzieren und anhängen

Der Zugriff erfolgt unchecked, aber ein typsicherer Zugriff soll durch einen compile-time-Overlay gewährleistet sein. Essentiell ist, daß die typsicheren Accessoren erzeugt werden können bevor die konkrete Storage-Adressen bekannt sind

der Umweg über lib::LinkedElements ist überflüssig, zumal ich durch direkte rekursive Implementierung auch noch eine klare Fehlermeldung erzeugen kann, falls ein Nachfolge-Extent noch nicht alloziert wurde

kurz gesagt:  std::conditional soll zwischen zwei wohldefinierten Typen auswählen; es ist eigentlich nicht für Metaprogrammierung gedacht

Ein Traits-Template hat eingeschachtelte Typen, mit denen sich weitere Eigenschaften abgreifen lassen. Das Traits-Template ist wohldefiniert, wenn es mit den gegebenen Typ-Parametern instantiiert werden kann. Das heißt, eingeschachtelte Definitionen müssen syntaktisch korrekt sein, werden aber nur weiter ausgewertet, wenn man diese Definitionen dann tatsächlich verwendet. Das ist ein grundlegendes Verhalten des Instantiierungs-Modells von C++, das u.A. auch Forward-Deklarationen möglich macht. Das kann man sich hier zunutze machen ... allerdings muß ich nun dafür ein Helper-Template anlegen, damit auf beiden Zweigen so etwas wie ein Traits-Template ausgewählt werden kann. Ich führe also eine private Def ein: PickType<slot>. Dann ist das Elm_t<slot> wie üblich nur noch eine getemplatete Abkürzung, aber erst diese greift auf die nested types tatsächlich zu

Das ist aber der Knackpunkt bei diesem Design — ich mache den ganzen komplexen Typ-Overlay nur, damit ich zur Laufzeit keinerlei Metadaten speichern muß; Zugriff erfolgt über Accessoren, die sozusagen „eingebaute Leitplanken“ haben, dann aber einfach einen force-Cast machen. Pro Level möchte ich eigentlich nur eine einzige Indirektion haben

...obzwar möglich, kann man nicht sinnvollerweise vom Benutzer verlangen, daß der den Basis-Offset kennt — und obendrein wäre eine solche Angabe dann auch noch verwirrend im Code

...das ist hier im use-case ein wichtiges Sicherheits-Feature ... und auch generell halte ich eine solche get-index-Funktion für gefährlich, wenn sie im Fehlerfall -1 zurückgibt.

Konkret ist das Problem, daß die Fold-Expression von meinem Compiler nicht als constexpr akzeptiert wird (weil man ja über die Fold-Expression einen Index inkrementiert). Möglicherweise würde das dann mit C++20 gehen?

bei Aufruf wird dann der eigentliche Storage-Frame erzeugt und  registriert

das heißt, ich kann ihn nur per RVO aus einer Konstruktor-Funktion heraus „abwerfen“

aber danach erst ist die Speicher-Adresse bekannt

und diese muß nun in einen Vorgänger-Frame geschrieben werden

so ein Accessor kann fest auf dem TurnoutSystem eingebaut sein, also getNominalTime(), oder er ist anderweitig kontextuell gegeben und damit ein frei stehendes Accessor-Objekt, das in der Vorbereitungs-Phase von einem HeteroData-Chain generiert wurde

...diese ist ein Funktor, der von gans woanders stammt — nämlich von dem Builder aus dem High-Level-Model gewrappt, z.B. Auswertung einer Bézier-Kurve

gestern habe ich mich furchbar geplagt, weil ich fest davon überzeugt war, daß dieses Anhängen im Konstruktor untergebracht werden muß

das Anknüpfen könnte man rückwärts gehend einleiten

denn Runtime-Checks sind ausgeschlossen, da wir es uns nicht leisten können, die entsprechenden Metadaten (VTable oder Funktor) in jeder einzelnen Node-Invocation wieder zu speichern; die Info kann aber auch nicht im Aufruf-Typ untergebracht werden, denn dieser ist generisch und für alle Nodes gleich

denn dieser erstellt einerseits den lokalen Daten-Record, andererseits greift man von diesem auch die Accessoren ab

eben weil wir im HeteroData-Chain selber (also dem TurnoutSystem) keine Metadaten unterbringen wollen ⟹ damit aber drehen sich alle Verifikationen dieser Typ-Struktor im Kreis, auch die size()-Funktion ist ja constexpr und analysiert wieder nur die Typ-Signatur; ob tatsächlich die angebilchen Tupel-Felder im Speicher liegen kann man ohne RTTI nicht prüfen

...dieser Check würde prüfen, ob wir nach einer vorkonfigurierten Anzahl an Schritten exakt hinter dem Ende des aktuellen Chain stehen — denn genau dann kann ein damit konform konstruierter Accessor keinen Schaden mehr anrichten: der lokale Tupel-Typ steckt ja im Kontext des Konstruktors, und wurde deshalb soeben zum Anlegen des konkreten Datentupels verwendet; solange die Vorgänger-Chain in der Zahl der Segmente paßt, ist es im Grunde egal, was da sonst noch für Datenwerte liegen, so lange wir nur grade am Ende der Vorgänger-Chain auf den Pointer zu unserem neu erzeugten Datenwert landen

Konsequenz: die Funktion, die an den Chain anhängt, muß die Längen-Info haben

insofern ist es ja schon attraktiv, mit dem Konstruktor-Funktor zu »zaubern«

und könnt nur genau in einer einzigen Weise (und ein einziges Mal!)

an einen Chain der richtigen Länge angehängt werden

Daten-Record wird markiert mit Template-Parameter seg

Wir zählen ganz einfach die Nummer der Segmente mit, die wir anhängen. Der erste Storage-Frame wird mit 0 initialisiert, und jedesmal, wenn ein erweiterter Typ konstruiert wird, erhöht sich diese Segment-Nummer

...die kann unmittelbar die Länge prüfen und dann an's Ende gehen und den Pointer dort per Referenz exponieren; der eigentliche Check besteht lediglich darin, daß wir die Segment-Anzahl in der Typ-Definition mit den beobachteten »pointer-hops« vergleichen. Das mag nach verblüffend wenig aussehen, ist aber grade hinreichend. Wie oben schon gesagt: was in den früheren Segmenten konkret gespeichert wurde, kann uns für diesen Zugriffs-Schutz komplett egal sein, solange wir in unserem eigenen Segment bleiben, und die Zahl der Schritte bis dorthin stimmt.

weil es ja für den gesamten Teilbaum relevant ist

denn die weitere Verknüpfung wird explizit in einem zweiten Schritt gemacht, und das ist gut so

...der Standard-Fall nimmt N... Template-Argumente und macht daraus ein Tupel

...das ist dann die einzige Typ-Signatur, die der Benutzer selber schreiben muß: HeteroData<X,Y,Z>

Das ist jetzt eine gewisse Asymetrie, die aber m.E. weniger wiegt, als der Vorteil, daß der angeschriebene HeteroData-Typ auch tatsächlich das ist, was für das Front-End erzeugt wird — notwendig wäre das überhaupt nicht, aber ohne Kenntnis der Definitionen wäre ein solches Verhalten hochgradig verwirrend; immerhin gehe ich davon aus, daß 90% der Leser nur den Standard-Fall sehen, und sich dann merken „das ist so eine Art Tupel...“

denn wir haben ja alle expliziten Accessor-Funktionen direkt auf dem Objekt nochmal definiert, und zudem wird das C++ Tuple-Protokoll implementiert

Nicht von der Typ-Info blenden lassen: die wird hier nur einmal im Kreis geschoben. Wir nehmen zwar irgend ein Front-End, müssen das aber dann auf den vollen Chain-Typ force-casten (denn das ist das Grundkonzept, das uns Zugriff ohne virtuelle Funktionen ermöglicht). Die einzige runtime-Info  ist die Anzahl der »Hops« zum nächsten Segment, d.h. die Anzahl an Pointern, denen wir über die Kette folgen. Diese muß mit der Anzahl der Komponenten im Typ zusammenpassen

sollte im Prinzip ja einfach sein...

Brauche wirklich alle vier Varianten (&, const&, && und const&&). Und auch für jede einen explizit ausgeschribenen korrekten Return-Typ per std::tuple_element_t

heißt auch, man kann hier nicht mit SFINAE arbeiten — und exakt das hat sich am Ende auch als das Problem herausgestellt, die Compilation scheitert bevor unser Code überhaupt eingreifen kann

HeteroData darf dann gar keine Basisklasse haben. Denn die empty-baseclass-optimisation ist optional; letztlich läßt der Standard dem konkreten Compiler einigen Spielraum bezüglich des Speicher-Layouts

Die Definition mit dem Frame als Basisklasse erscheint mir sauberer, denn sie legt zweifelsfrei fest, wo der Frame sein soll und daß er mit dem Anfang von HeteroData zusammenfallen soll. Außerdem dokumentiert das eine is-a Beziehung (wenngleich auch die Sichtbarkeit eingeschränkt ist).

ja wirklich: der Code geht durch den Compiler und zur Laufzeit kann ich mit dem Debugger in unseren Overload steppen

ja wirklich: man sieht das am Error-Stack der Compile-Fehlermeldung; der Einstieg ging sofort in den Code aus der Stdlib (tuple.h, 1335)

exakt diese Funktionen unter anderem Namen

 ⟹ Fehler im Test-Setup reproduzierbar

durch decltype(auto) ersetzt ⟹ compiliert und unser Overload wird genommen

denn würde er dorthin kommen, dann würde er unseren Overload tatsächlich wählen, exakt wie erwartet; und unser Overload würde auch funktionieren.

Ich verwende ja einen uralten Compiler und eine OS-Version, die schon am Ende der Lebensdauer ist; gut möglich, daß dieses Problem anderswo längst erkannt und behoben wurde — es würde ja bereits genügen, die assertion erst im Body der Funktion zu haben, denn dann könnte die Overload-Resolution arbeiten und daran vorbeikommen. Andererseits ist die Assertion grundsätzlich in Ordnung, denn falls es hier keine custom-Version gäbe, wäre der Aufruf ja tatsächlich gefährlich und müßte verhindert werden

Fazit: Overload-Resolution ist gar nicht das Problem

unser Overload ist in Ordnung, wird vom Compiler gewählt und funktioniert fehlerfrei.... wenn da nur nicht die Implementierung von std::get wäre, welche bereits bei der Instantiierung mit konkreten Parametern eine Assertion triggert und damit die Compilierung stoppt.

damit funktionieren die structured bindings

der PortBuilder kann einen Adapter für klassische Parameter-Automation bauen; dieser muß intern auf die absolute-nominal-Time  zugreifen können

Für den Bau einer Param(Agent)Node wird ein Chain-Block vorbereitet, welcher später dann in einen Invocation-Stack-Frame gelegt werden soll. Die tatsächlichen Dateninhalte für diesen Block stammen aus dem Aufruf eines Funktors, der in diesen Vorgang hinein verdrahtet wird. Der Builder muß selber einen Funktor konstruieren, der später alle diese Aktionen während der Invocation ausführt, als Schritte des speziellen Weaving-Patterns, das hier zum Einsatz kommt. Dafür braucht er Zugang zu Typ-Konstruktoren, die prinzipiell bereits zur compile-Zeit vorliegen,  da das TurnoutSystem eine fest eingestellte Basis-Typisierung als Anker verwendet. Darauf aufbauend erzeugt die konkrete Builder-Instanz den Typ für konkrete Accessoren, und bettet sie in die vorbereitete Invocation ein

die Typisierung über Hetero-Data stellt grundsätzlich sicher, daß es die zuzugreifenden Struktur irgendwann, irgendwie in diesem Executable geben kann, aber nicht wann sie tatsächlich existiert, oder wie lange sie lebt. Auch der Zugang über die interne linked-List stellt nur sicher, daß die Datenstruktur bereits erzeugt wurde, nicht aber daß sie noch lebt

die konkreten Accessoren werden als spezielle Typisierung in einen privaten Kontext in ein invocation-λ eingebettet, so daß sie nur aus dem Scope der tatsächlichen Invocation heraus sichtbar sind; solange dieser Scope lebt, ist per Definitionem auch zumindest dieser Chain-Block valide und zugreifbar.

Der erste Entwurf von 2009 / 2012 baute im Grunde noch darauf auf, einen void*  etwas geschickter zu „verpacken“ — ich konnte mir Parametrisierung nicht anders vorstellen, als eine Ansammlung von Daten in einem Record; Type Erasure war damals für mich noch ein schwer zu fassendes Konzept, weil ich im Grunde die Idee von Typ-Kontext und higher kinded Types noch nicht wirklich erfaßt hatte. Ich sah zwar die Möglichkeit, mit Templates in einem »Baukasten-System« eine paßgenaue Aufruf-Mechanik zusammenzustellen, aber mein Entwurf lief sehr schnell gegen die „Schallmauer“ zwischen Laufzeit-Parametrisierung und Code-Generierung im Compiler. Zudem fehlte es mir noch an Phantasie, was man tatsächlich mit Templates anstellen kann, indem man eigenständige Code-Bausteine per Kontext aufgreift....

Inzwischen habe ich verstanden, daß es im Kern um Ontology-Mapping  geht, und der Typ-Kontext hierfür die Implementierung bereitstellt. Das bedeutet, ich denke den Typ-Kontext nicht mehr als eine feste Struktur, die bestimmte Parameter abliefern muß. Das hat aber auch zur Konsequenz, daß ich explizit darauf verzichte, eine feste Struktur oder gar eine Meta-Ontologie vorzugeben, auf der dann der Build-Prozeß als Interpreter läuft. Vielmehr muß bereits der Build-Prozeß auf einem hohen Level — nachdem der Render-Path grundsätzlich feststeht (und damit auch, welche Library die Arbeit machen muß) — alsbald in die Domäne eben dieser Library einsteigen und mit dem Ontology-Mapping interagieren, um das Baumuster der einzelnen Node festzulegen. Scheinbar wie zufällig entsteht genau dadurch für jede Library ein System von Code, welches exakt alle benötigten Code-Bausteine instantiiert und somit konkretisiert aufrufen kann. Ich brauche damit gar keine zentrale Registy von Invocation-Varianten mehr, und die Template-Instanzen liegen dann im Code des jewiligen Binding-Plug-ins für diese Library...

dann sind unendlich viele Template-Instanzen die Folge

ja... lang ist's her ☺

Zur Erinnerung:

  • an Zeit-Werten fummelt man nicht herum
  • sie sind opaque — will man „reinschauen“, dann braucht man einen Timecode
  • und dann eine QuantisedTime, welche ein Grid impliziert.
  • es gibt keinen natürlichen Default und aus diesem Grund gibt es auch keine »convenience method« für die armen Tester
  • So ist es, Ende der Diskussion! (die Interne Zeit ist eine Abstraktion, den „wirklichen Wert“ gibt es nicht)

...hab jetzt die Abkürzung eingebaut FrameNr::quant (time, "Grid")

.... somit spart man sich das Konstruiren der QuTime, und einer FrameNr instanz, bloß um dann letztlich in einen int64_t umzuwandeln. Ich halte das für vertretbar, da dennoch das Grid explizit genannt sein muß...

also das Thema »Quantisierung« war dann letztlichn doch nicht relevant für den Test, hat mich aber immerhin an den Sachverhalt erinnert, und dazu geführt, daß ich eine vertretbare Abkürzung in den FrameNo-Timecode eingabaut habe

    template<class X, typename...ARGS>

    inline void

    buildIntoBuffer (void* storageBuffer, ARGS&& ...args)

    {

      new(storageBuffer) X(forward<ARGS> (args)...);

    }

      template<class X, typename...ARGS>

      static TypeHandler

      create (ARGS&& ...args)

        {

          return TypeHandler ( bind (buildIntoBuffer<X,ARGS...>, _1, forward<ARGS> (args)...)

                             , destroyInBuffer<X>);

        }

no matching function for call to

  std::function<void(void*)>::function(std::_Bind<void (*(std::_Placeholder<1>, int))(void*, int&&)>&)

with CTOR = std::_Bind<void (*(std::_Placeholder<1>, int))(void*, int&&)>;

will sagen ... das ist nicht das Problem; der Compiler macht hier eine verwirrende Transformation, die den Typ eines Funktionspointers mit Template-Argumenten instaniiert. Leider hat das aber zur Folge, daß man das eigentliche Problem nicht sieht (selbst wenn man endlos drauf starrt...)

...aber aufgrund der sonderbaren Zuordnung praktisch nicht zu erkennen....

Tip: wir übergeben einen extra int-Parameter an eine Funktion, die auf einen in&& parametrisiert wurde

Aufgerufen: (std::_Bind<void (*(std::_Placeholder<1>, int))(void*, int&&)>) (void*&)

Candidate: _Result std::_Bind<_Functor(_Bound_args ...)>::operator()(_Args&& ...)

  _Functor = void (*)(void*, int&&)

  _Bound_args = {std::_Placeholder<1>, int}

Beobachtung: _Functor(_Bound_args ...)

void (*(std::_Placeholder<1>, int))(void*, int&&)

(void (*)(void*, int&&)) (std::_Placeholder<1>, int)

#include <functional>
#include <iostream>

inline void
fun (int&& a)
{
std::cout << a << std::endl;
}
int
main (int, char**)
{
auto bi = std::bind (fun, 55);
bi();
return 0;
}

Ursache: Binder liefert eine Kopie

also: func<ARGS& ...>

ERR: tracking-heap-block-provider.cpp:129: ~BlockPool: Block actively in use while shutting down BufferProvider allocation pool. This might lead to Segfault and memory leaks.

wirkt alles noch latent unfertig hier....

auch Implementierung von all_buffers_released() fehlt

Glorreich — testgetriebene Entwicklung und dann den wichtigen zentralen Test >10 Jahre lang auskommentiert rumstehen lassen

also muß ich im BlockPool alles als released markieren

schließlich soll man ja sauber arbeiten

das genügt mir hier — gehe davon aus daß es dann mit Tupeln auch funktioniert

Alle Parameter einer processing-function sind in ein einziges Tupel zusammengefaßt, aber typischerweise sind einige technisch, während andere die gestalterischen Steuermöglichkeiten der Operation repräsentieren. Beide Aspekte müssen auf dem gleichen NodeBuilder konfiguriert werden, aber aus verschiedenen Quellen (und vermutlich auch in verschiedenen Schritten)

implementiert auf Basis der partial-function-closure

verwende die gleiche Berechnungs-Struktur wie build_connectedNodes()

Ich hab das mit der »Domain-Onology« bisher nur auf einem abstrakt-logischen Level konzipiert — nach dem „müßte so hinhauen“-Schema. Auf dem Weg bin ich jetzt so weit, daß ich sehe, was für elementare Berechnugs-Primitive nützlich sein könnten. Also implementiert man die mal und sieht dann weiter....

»Dann weiter« läuft darauf hinaus, daß eine »Dummy-Media-Processing-Library« entsteht. Das heißt, die wird dann auch ihre eigene Beschreibung von Datentypen haben, wie.z.B. Anzahl Kanäle. Aktuell sind das erst mal Parameter für TestFrame, aber das würde dann längerfristig auf sowas hinauslaufen wie einen StreamType-ImplType.

Gut möglich, daß man das überhaupt erst im nächsten Vertical-Slice wird zusammenschalten können

...mit der man im Test sagen kann

  • ich möchte diese Art von Processing
  • mit diesen Datentypen
  • und diesen Varianten / Qualifiern

Dieser Test ist der Schlüssel zum Aufbau des Render-Node-Network — sowohl für mich selber in der Entwicklung, alsauch später zur Dokumentation. Der Aufbau sollte sorgfältig vorgehen und sich auf das Wesentliche beschränken

Die Analyse ergab mehrere Perspektiven auf das Thema, die sich zwar stark unterscheiden, wenn man sie als praktische Handlungsanweisungen interpretiert, die jedoch praktisch auf dasselbe hinauslaufen, wenn man sie gedanklich auffaßt. Meine Schlußfolgerung daraus ist, daß es stets einer Übersetzung bedarf, und es daher untunlich wäre, sich auf ein Modell festzulegen...

  • topologisch haben wir es mit einem DAG zu tun, und es kann Mehrfachverbindungen auch zwischen unmittelbaren Nachbarknoten geben
  • eine Node-Aktivierung erzeugt immer alle Ausgaben, die dann irgendwo aufgefangen werden müssen
  • im Allgemeinen ist ein Umordnen und ggfs Auswählen und Verteilen dieser Ausgaben erforderlich
  • der Berechnungs-Vorgang hat K Eingaben
  • jede Eingabe ist ein bestimmtes Ergebnis von einem bestimmten Vorläufer

faßt das Konzept eines »Port« enger

nämlich indem der Ergebnis-Datenframe paßgenau sein muß

Auch dort zeigt sich die Wahl zwischen einer high-level-Sicht, welche in den Ports noch Raum für sub-Differenzierung läßt, und somit näher an der Domänen-Modellierung liegt — und auf der anderen Seite einer radikalen low-Level-Lösung, die auf restlos explizit gemachten Möglichkeiten besteht

ich neige zunehmend Richtung Model-1

virtual BuffHandle weave(TurnoutSystem&)

Auf der Ebene der Implementierung ist die Domain-Ontology so etwas wie eine Typklasse — wie bildet man das in C++17 ab — und wie in C++20 ?

das heißt, zur Aufruf-Zeit sind keine Meta-Informationen mehr notwendig, zum Zeitpunkt des Zusammenstellens der Render-Nodes dagegen sehr wohl

in der Tat berühren sich hier zwei Ebenen, und das könnte man für eine Trennung in verschiedene Domänen nutzen. Und zwar zum Einen die introspektive selbst-Darstellung für eine Media-Library und zum Anderen der Aufruf einer Processing-Function mit paßgenauen Argumenten; zu bedenken ist zudem, daß nicht jede Library auch eine Form von Discovery bietet — insofern kann diese Mapping- und Adapter-Schicht einiges an Fleißarbeit erfordern

  • der Builder bezieht Introspektion von der Adapter-Schicht
  • Antwort ist eine generische Meta-Beschreibung
  • diese wird vom Builder interpretiert
  • und demzufolge werden direkte Bau-Instruktionen gegeben
  • der Builder triggert mit ID-Informationen den Binding-Layer an
  • dieser erzeugt daraufhin direkt die Ausführungs-Parameter
  • welche dann lediglich in die Nodes eingebettet werden

...insofern ist das eindeutig die elegantere Lösung. Auf den ersten Blick mag die komplette Trennung „sauberer“ wirken, jedoch muß dazu eine Meta-Repräsentation geschaffen werden, die für jedwede Art von Medium gilt, und es muß dafür ein Interpreter im Builder bereitstehen. Da auf der anderen Seite ohnehin ein Adapter für jede Library geschrieben werden muß, besteht die Möglichkeit, für jede Library einen eigenständige Weg zu verwenden, um letztlich die Aufrufparameter auszugeben; das bedeutet, man kann in der jeweiligen Fach-Ontologie bleiben, und das Ontology-Mapping beschränkt sich auf eine Übersetzung in das sehr rudimentäre Muster von Node-Aufrufen, wie von der Render-Engine gefordert

das hängt sehr stark davon ab, zu welchem Grad die jeweilige Media-Library überhaupt Flexibilität zuläßt. Im schlimmsten Fall nimmt die Library einen Dateinamen und liefert direkt eine Pipeline an, die in einer Library-internen Engine läuft. Eine etwas bessere Variante wäre, wenn die Library einen Filenamen (oder offenes File-Handle) akzeptiert und dann decodierte Daten in einer Folge von Buffern bereitstellt. Idealerweise kann man der Library einen Rohdatenblock geben, dann den Header interpretieren lassen, und daraufhin dann einen Teildatenblock an die LIbrary zum Decodieren geben.

⟹ wie viele Kanäle stehen bereit

welches Buffer-Format (Größe)

ggfs Metadaten-Adapter?

...aber diese Info muß aus dem Library-Adapter bezogen werden können, und zwar schon viel früher, wenn das entspr. Processing-Asset erstellt wird; das kann allerdings auch eine dynamischen Rückruf in die LIbrary implizieren

das ist ein besonders kniffeliges Thema...

  • welche der Eingabe-Buffer sind inplace? Anzahl, Position in der nominellen Ordnung?
  • die FeedManifold muß diese speziell behandeln und auf die Ausgabeseite durchmappen (wohin?)
  • besondere Komplikation: der Builder muß wissen, ob eine Node in-Place-fähig ist, muß das aber u.U auch noch auf sehr spezielle Weise für den Aufruf instruieren (besondere Funktions-Variante aufrufen, zusätzliche Flag-Parameter übergeben, spezielle Marker für die Buffer setzen)

ein ganz besonders lästiges Thema: in der C-Welt gilt es als Feature, und nicht als Bug, wenn eine Library dem Aufrufer das Memory-Management abnimmt; Libraries sind in der Hinsicht oft unsäglich „kreativ“. Typischerweise bedeutet das, daß man sich dann an ein ganz spezielles Protokoll halten muß; in Lumiera würden wir so etwas als besonderen BufferProvider verkapseln, mit einem speziellen »Inlay-Typ«, der dann lifecycle-Events an die Library weitergibt

Typ des Invocation-Adapters

Das ist ein spezielles Konstrukt, das sich direkt aus dem Setup mit BufferProvider ergibt: man sieht einen speziellen Buffer-»slot« vor, in den ein Adapter-Typ inplantiert wird; dieser Adapter bekommt eine Referenz auf die FeedManifold und wird dann getriggert, was die eigentliche externe Berechnung in der Library bewirkt. Da dieser Typ maßgeschneidert ist, kann er ein zusätzliches Kontroll- und Informations-API bieten

...das ein bestimmtes Schema für Funktionsaufrufe und Buffer-Arrays fest vorgibt; damit kann dann auch die FeedManifold Teil des InvocationAdapters werden und beide zusammen liegen beim Aufruf im Stack-Frame

...wenn ich will, daß das Ergebnis der Berechnung an der gewünschten Stelle anfällt. Falls das nicht geht, weil z.B. die Library selber den Speicher gemanaged hat, weil es sich um das Ergebnis eines Pipeline-Aufrufs handelt, dann muß das bereits vorher der Builder festgestellt haben, denn in dem Fall ist eine Adapter-Node notwendig, die das Ergebnis in meinen Zielpuffer umkopiert — oder zumindest muß die Ausgabe als spezieller BufferProvider verpackt werden (beispielsweise weil die Library mir einen von ihr gemanageten Buffer gibt, ich diesen aber an IO_URING weiterreichen möchte und dann folglich später asynchron die Freigabe an die Library zurücksignalisieren muß.

...was bedeutet, das ist ein Schema zur Vereinheitlichung

  • es gibt eine lineare Folge von N Eingaben
  • und ebenso M reguläre Ausgaben
  • bestimmte Eingaben sind tatsächlich in-Place-Buffer
  • diese erscheinen stets vor den regulären Ausgaben
  • es gibt einen Invocation-Adapter

...hab ich doch alles schon mehrfach durchdiskutiert, und sogar die Ergebnisse aufgeschrieben...

das sind Bindings ≙ »Weichenstellungen«

...man könnte ein System von nested scopes aufbauen, auf Basis einer persistenten Datenstruktur. Es ist aber noch nicht klar, ob soetwas jemals gebraucht wird (YAGNI); im Besonderen bräuchte man dann auch einen Propagations-Mechanismus für Bindings, und damit wird die Sache komplex und potentiell aufwendig. Als ein anderes Modell könnte man lediglich einen Verweis auf einen Key-Value-Store durchgeben, der dann auf dem Einstiegs-Stackframe liegen würde. Und als minimal-Version würden wir nur einen Koordinaten-Record per Funktionsparameter nach unten durchreichen — bräuchten dann aber einen out-of-band-channel um Muting und Aktivierungen (z.B. Switchboard) durchzugeben; letztere müssen übrigens auf Ebene der Job-Invocation atomar aufgegriffen werden (wie genau ist noch nicht klar)

...und zwar mit einer Gruppierung der Buffer nach Größe, und vermutlich auch pro Thread

ein Zwischending: BufferDescriptor

  • das bedeutet: dies ist die Quintessenz der Buffer-Konfiguration und damit die Minimal-Information, die wir für jeden Slot fest vorbelegen müssen.
  • allerdings könnte man noch über eine kompaktere Repräsentation im Speicher nachdenken...

...im Gegensatz zum Baumuster des Node-Graphen, der stets abwärts gerichtet ist: eine Node kennt nur ihre Vorläufer, aber der BufferDescriptor verweist aufwärts auf einen globalen Service

Es kann durchaus sein, daß das jeweilige Output-System einzelne Teil-Kanäle in einem planaren Format haben möchte, also als einzelne, zusammenhängende Datenblöcke für jeden Kanal (Hue, Sat, Lum, bzw. Sound-L/R oder WXYZ...)

warum? weil es die BufferManager-Abstraktion unterläuft

nämlich in den Kontext vom Render-Job; dort wird das OutputSlot-Protokoll zeitgebunden durchgespielt

diese Infos müssen bereitstehen ⟶ jeweils zum Aufbau  einer neuen Invocation

also einfach ein Zugriff via lib::Depend<Service>

hängt ganz stark mit der internen Repräsentation eines Turnout-System-Elements zusammen: wie werden die dynamischen Array-Container implementiert? das entscheidet, ob alles in einem Aufruf-Scope passieren muß

welcher hiermit nur noch über eine virtuelle Methode weave() zur Laufzeit eingebunden ist

...wäre demnach ehr eine Hintertür im Design

  • Ausgabe: ja die müssen wir wissen, weil wir die entsprechende Anzahl empfangender Buffer bereitstellen müssen
  • Eingabe: ähnliches Argument; es kann sein daß wir nicht alle Leeds für diesen Port verwenden, oder umgekehrt daß wir M der N Ports von K Leeds verwenden

Ich sage nicht, daß das immer so ist, oder gar daß es besonders relevant wäre. Aber leider ist das hier die Festlegung eines komplett generischen Schemas, mit Partizipanten, über die nahezu nichts bekannt ist. Rein grundsätzlich könnte es schon sein, daß einige dieser Konstruktor-Parameter etwas Spezielles aus dem konkreten Call-Kontext aufgreifen müssen, denn genau dazu sind sie da. Ich denke da an die Wirkung einer Schalterstellung, oder einen Automations-Parameter, oder auch eine spezielle Service-Instanz, die vom Buffer-Inlay benötigt wird, z.B. Allokator-Instanz, Handle auf einen Hardware-Prozessor etc.

das heißt, die Reihenfolge der »slots« entspricht genau der Anzahl der Eingangs/Ausgangs-»slots« der ProcNode; mithin wäre die FeedManifold eine Aufdoppelung der Node-Struktur, nur daß dann hier, auf dem Stack, die konkreten BuffHandles liegen

...dann wäre die FeedManifold etwas, was man direkt an den InvocationAdapter weiterreicht; möglicherweise wäre die Form der FeedManifold dann sogar von der DomänenOntologie zu bestimmen: sie würde das widerspiegeln, was die konkrete Funktion zum Aufruf braucht, und das wäre im Extremfall sogar ein Typ, der von der Media-Library vorgegeben ist (wenn diese beispielsweise erwartet, daß sie die Buffer-Pointer in einer bestimmten Struct bekommt, ggfs mit zusätzlichen Flags)

wenn man beide Gedanken zuende denkt...

  • dann brauchen wir sowiso und stets irgend eine Struktur auf dem Stack, in dem die BuffHandles liegen; möglicherweise sogar über mehrere Stack-Frames hinweg durchgereicht
  • andererseits kann eine Library ganz gewiß nix mit Lumiera-BuffHandles anfangen, sondern wird ein spezielles Datenformat wollen; das bedeutet, man muß das BuffHandle in jedem Fall belegen, dann dereferenzieren und den resultierenden Pointer irgendwohin kopieren, um die Zielfunktion aufrufen zu können (das war auch die Idee hinter der »BuffTable« im ersten Entwurf)

dieser wird am Ende weitergegeben und nicht geschlossen

...das ist ein interessanter Freiheitsgrad: wir können gleichartige Buffer als Array ausgestalten und damit in einen »Slot« zusammenfassen. Wir müssen das sogar tun, wenn wir Mehrfach-Outputs haben

hoppla: weiterer Freiheits-Grad entdeckt

Eigentlich hätte ich die Storage gerne »inline« — aber das geht nicht ohne Weiteres in C++, denn die Größe jeden Objektes muß zur Compilezeit bekannt sein. Es gibt eine non-Standard-Extension in GCC, so daß auch in C++ die »dynamischen Arrays« aus C erlaubt sind. Tatsächlich liegen diese aber nicht garantiert auf dem Stack, sondern der Compiler reserviert einen gewissen Puffer und wenn es mehr Elemente sind, macht er stillschweigend eine Heap-Allokation

damit würden wir ähnlich vorgehen, wie ein Garbage-Colector mit »Eden-Space«: man müllt hemmongslos voll, weil man weiß daß man alles nach der Rückkehr aus dem Aufruf komplett wegwerfen kann

man bekommt von jeder Kopie den gleichen Zielpuffer, und auch die Lebenszyklus-Methoden lassen sich von jedem Handle aufrufen

...während ein Ausgabepuffer explizit im shed()-Schritt belegt wird, kurz vor dem Aufruf der Processing-Function

...während Ausgabepuffer zwar an der Stelle committed werden, aber »der« Ausgabepuffer wird noch nicht freigegeben, denn er ist ja Eingabepuffer der Nachfolger-Node

kann man von Außen das Resultat-Array überhaupt sehen? Ist es ein Zugriff  oder wird das Resultat geliefert?

eingangsseitig / ausgangsseitig?

...dieses Schema nimmt an, daß man der Reihe nach alle Eingänge »pullt«. Tatsächlich ist es aber so, daß man nur einen bestimmten ersten Eingang einer Serie pullen muß, und dann liegen bereits mehrere Ergebnisse in den Ausgabepuffern; es fehlt aber das API, um auf diese direkt zugreifen zu können, und außerdem ist auch nicht automatisch klar, auf welche Eingangsposition des Nachfolgers diese gemappt werden

...und das ist ein besonders lästiger Grenzfall, der höchstwahrscheinlich sehr selten auftritt, dann aber ein knock-out sein könnte; das bedeutet, für diesen seltenen Fall muß die Möglichkeit für freies Mapping vorgesehen werden

...dazu müßten wir aber wissen, daß der Cache hinreichend zuverlässig funktioniert, und andererseits der Fallback auf wiederholte Berechnung nicht prohibitiv aufwendig wird. Daher erscheint diese Lösung zunächst sehr attraktiv (man muß nämlich erst mal gar nix machen und die Dinge regeln sich von selbst), aber bei genauerer Betrachtung erscheint das Risiko schwer quantifizierbar, schon weil die Bedeutung dieses Falles nicht eingeschätzt werden kann ohne in die Breite gehende, praktische Erfahrungen.

Als Analogie sehe ich das Thema »Renaming« in SCMs. Subversion hat versucht, dieses Thema explizit zu modellieren; das Ergebnis wurde komplex. Git hat stattdessen beschlossen, das Thema im Modell komplett zu ignorieren, und bietet nur eine Heuristik. Diese Lösung funktioniert erst mal erstaunlich gut, stellt sich aber längerfristig als ein permanentes Ärgernis heraus

Beschluß: nicht Mehrfach-Outputs, sondern Array-wertige Outputs

da diese zusammen anfallen und verwendet werden, sind sie ein Frame

für sie wird nur ein BuffHandle vorgehalten, nur im Buffer liegt eben ein Array

...wobei hier zwei Aspekte offen bleiben...

  • zum Einen wäre es auch möglich, das Array-wertige Resultat zu beziehen, aber nur einen (teil)Puffer zu verwenden
  • was dann auf das andere Problem hinweist: wie entscheidet der Builder, was zu tun ist?

das erscheint aber praktisch schwer umsetzbar....

  • es wäre eine beliebige Anzahl an Argumenten jeweils einzeln zu repräsentieren
  • man müßte entweder auch die Mapping-Position mit in den Typ-Kontext aufnehmen, wodurch eine gefährliche Kombinatorik entsteht — oder man müßte mit indizierten »Typ-Slots« arbeiten

Diese Lösung erscheint sehr viel einfacher und ziemlich elegant; allerdings führt sie zu einer erheblichen Zahl an indirekten Calls (und die Relevanz dieses Hebels ist schwer einschätzbar)

auch das Caching (als orthogonaler Belang) bestimmt mit,

was für ein BufferProvider konkret angebunden wird

dazu müßte man erst mal diverse komplexe Fälle im Detail durchspielen...

  • ist es wirklich möglich, diese Auflösung und Zuordnung sofort vom Builder-Call aus zu machen?
  • braucht man die tatsächlichen Koordinaten wirklich nicht mehr später, zur Laufzeit?

dieser führt den »shed«-Schritt aus

Das ist eine Entscheidung. Man könnte ganz im Gegenteil jetzt anfangen, einen generischen Media-Function-Invoker zu schreiben, der von einer Routing-Tabelle gesteuert wird. Das werde ich nicht tun, da dies den Schein einer Generizität erzeugt, die gar nicht gegeben ist. Vielmehr gehe ich davon aus, daß die meisten Fälle offensichtlich-einfach sind, und in komplexeren Fällen muß man ohnehin wissen, was die jeweilige Library braucht; es wäre reine Verschwendung, das in ein Meta-Routing-Format zu pressen... 

...weil der btr. Buffer anderweitig noch weitergereicht wird; das gilt im Besonderen für den Ergebnis-»Slot«, und es gilt aber auch für Eingabe-»Slots«, die tatsächlich InOut-Parameter darstellen und daher auf die Ausgabeseite aufgedoppelt wurden.

muß aber auch festlegen wie aufgerufen wird

will sagen: ich gehe erst mal im Prototyping von einer Test-Ontology aus, die sich aber informell bereits auf meine Kenntnis der Domäne (Video-Processing) abstützt

wie beide aus einer »Ontology« heraus angelegt und gesteuert werden können

und letztlich wie dann ein konkreter Aufruf ablaufen kann

sich nicht verrückt machen: das hier ist ein hermeneutischer Zirkel: Um ein gutes Werkzeug bauen zu können, muß ich die Sache verstehen — und das mache ich, indem ich auf den Werkzeuggebrauch hin stipuliere

Ziel: Top-down Realisierung des Builder-API

Vermöge Analyse und Architektur-Design habe ich mich schrittweise an eine Gliederung des Build-Vorganges in verschiedene Level herangearbeitet. Auf dem mittleren Level-2 laufen alle Fäden zusammen; daher ist der Kern der Aufgabe bewältigt, wenn ein sinnvolles und adäquates Builder-API steht

Weg: bottom-up überlegen wie gerendert werden soll

Um aber auf das notwendige Builder-API schließen zu können, muß ich wissen

  • welche Struktur zum Rendern notwendig und deshalb zu bauen ist
  • wie diese Struktur aus dem Kontext der Domain-Ontologie heraus spezifiziert wird

...den Code eines Builders liest man normalerweise gar nicht im Detail, sobald man verstanden hat, daß es sich um einen Builder / eine DSL handelt; wenn der Builder gut geschrieben ist, kann man direkt die gebaute Datenstruktur erkennen

wenn das ginge, dann könnte man einfach der Struktur Schritt für Schritt die Felder zuweisen, und dann würde sie irgendwann auf magische Weise »fixiert« im Speicher

...wir bauen hier eine sehr komplexe Struktur, und deshalb sollte vermieden werden, auch noch ein komplexes Protokoll neu zu erfinden

....es geht hier auch um die Allokation, die Daten sollen nachher kompakt liegen und minimal sein; außerdem geht es um das Beherrschen von Komplexität und die Wartbarkeit des Codes; komplex aufeinander abgestimmte Datenstrukturen sollten abgekapselt sein, um ein Verfallen der Architektur durch opportunistische Anpassungen zu verhindern

Ansatzpunkt: genau das habe ich für den SeveralBuilder bereits gelöst

warum ist das so schwer verflixt noch einmal!!!!!

nicht als template-template-Parameter...

...denn sonst wird man die ALO, INIT... - Parameter nicht los, sondern sie werden Teil des Builder-Typs (wir wollen aber, daß sie nur implizit in den Builder-Typ eingehen)

auf das muß sich der Builder abstützen, nicht auf den lib::SeveralBuilder oder dessen Policy

...nicht versuchen, die beiden zu verbinden oder irgendwie durch default-Parameter ausdrücken

tja... und jedes nested template braucht ein Präfix "template <fun>" in der Syntax

...und dann kann man es noch rund machen...

weil dieser Typ in die tatsächliche Linkage der Objektfelder eingeht

Alle Typen sind bekannt und bereits angeschrieben; die neulich gefundene Lösung mit dem decltype(<builder>)-Trick hat mich zwar zum Ziel gebracht, ist aber unnötig indirekt — man könnte durchaus die Typen ineinander einsetzen und dann in eine einzige Typedef reduzieren

Namens-Idee: AlloPolicySelector

es bieteten sich an: lib::several und lib::allo ⟵ letzteres ist logischer!

nach all den Vereinfachungen und Refactorings: das ist der ServeralBuilder  selber

und wichtig: hier ist noch eine Selector-Policy darüber gesetzt

diese Policy ist ein einfacher Typparameter

using AllocatorPolicy = AlloPolicySelector<ALO,INIT...>;

⟶ withAllocator<ALO> (INIT&& ...)

UseHeapAlloc default-Policy für alle Builder

...vermutlich wird es irgendwann so eine Art »Builder-Framework« geben, und dorthin gehören dann solche Definitionen. Derzeit kann ich hierfür noch keine sinnvolle Code-Struktur festlegen

im alten Design von 2012 war die »Buffer Table« gedacht als eine Entität, die zumindest operational eine gewisse Abstraktion leistet. Im neuen Entwurf dagegen bemerke ich eine starke Tendenz, dies nur noch als eine Laufzeit-Datenstruktur zu betrachten, die durch das »WeavingPattern« bespielt wird

das wäre KISS; man würde den gleichen Template-Parameter N heranziehen, der auch die FeedManifold steuert; dieser wird nach dem Schema »eins«, »zwei«, »viele« belegt. Zusätzlich gibt es für diesen Ansatz dann eine dynamische Spec über die tatsächliche Anzahl der Buffer In/Out (die stets <= N wäre)

Im einfachsten Fall wäre das genauso wie die erste Lösung, nur daß der Buffer nicht im Haupt-Objekt liegt (aber natürlich trotzdem im Allocation-Cluster). Allerdings gäbe es hier noch eine weitere, halb-dynamische Lösung: man würde explizit einen Buffer dynamisch allozieren (direkter Allokator-Aufruf, bei dem man die Anzahl Elemente angibt). Das läuft auf einen minimalistischen Spezial-Container hinaus, oder eine ScopedCollection mit Custom-Allocator, oder dann gleich die nächste Lösung....

...dies ist mit wenig Entwicklungs-Aufgand umzusetzen, erfordert dann aber trotzdem eine explizite Verdrahtung des Allocators und damit eine Vermischung zwischen Objekt-Konstruktion und Builder/Allocator.

Unabhängig davon, welche der drei vorgenannten Lösungen zum Zuge kommt, gibt es stets noch eine weitere, komplexere Variante, die aber sehr attraktiv erscheint: es ist nämlich in den weitaus meisten Fällen so, daß nur ein einziger, einheitlicher Buffer-Typ zum Einsatz kommt; daher läuft das Belegen eines Array mit einem expliziten Deskriptor für jeden »Slot« auf erhebliche Speicherverschwendung hinaus. Stattdessen könnte man versucht sein, die Belegung durch ein Stück Code machen zu lassen (eine Closure), und dafür eine Meta-Spec zu interpretieren. Das wird aber in jedem Fell recht komplexer Code, der dann auch zur Render-Zeit läuft (ja wirklich, in jedem Aufruf), und deshalb wieder das Problem aufwirft, woher man die Storage für temporäre Datenstrukturen nimmt. Denn solche wird es zwingend geben, schließlich muß man ja irgendwie markieren, was die bereits behandelten Spezialfälle sind, und welche »Slots« damit übrig bleiben und mit dem Standard-Fall behandelt werden müssen. Ja mei!

diese Lösung ist wohl etwas brachial

...vor allem gibt es kein »Sicherheitsnetz« — ein Umstand, der mich nachdenklich stimmt, da hier doch ein sehr komplexes Code-System entsteht, in dem die Ausführungs-Pfade keineswegs auf sicheren, festen Bahnen verlaufen... Sollte dann wohl doch in Betracht ziehen, einen passenden, speichersicheren »in-Place«-Container zu implementieren

...grundsätzlich besteht diese Möglichkeit, und sie könnte eine gewisse Relevanz bekommen, sobald wir es mit Hardware-Accelleration zu tun haben. Dennoch halte ich das nicht für den Regelfall, und zudem treibt es die Komplexität hoch

fange also nur mit einem BufferDescriptor-Array an

Und zwar, weil nun innerhalb vom »Turnout« ein Template sitzt, das als Ganzes auf Metaprogramming-Ebene zusammengestellt und konsolidiert wird. Die alte Idee ist also wieder zurück, nun aber »um 90° gedreht«. Denn die Template-Generierung ist nun in das Library-Plug-in verschoben, wo sie direkt auf Implementierungs-Datentypen aufsetzt.

Damit tut sich eine viel weitere Blickachse auf: die Library wird verstanden als Domain-Ontology, und entsprechend ist der Kern dieses Designs, für Lumiera keine Universelle-Domain-Ontology  auzubauen, sondern nur ein minimales Meta-Koordinierungs-Schema. Auch das ist eine Art von Domain-Ontology, aber für eine verschobene Domain: es geht um das Bauen von Modellen...

Die FeedManifold hätte in diesem Fall einen zusätzlichen Platz für ein spezielles BuffHandle. Dieses müßte so konfiguriert werden, daß der Typ des InvocationAdapters und seine Konstruktions-Parameter in dem zugehörigen BufferDescriptor „versteckt“ sind. Trotzdem müßte man dann auch noch für den Aufruf nochmal eine Referenz der FeedManifold an die Funktionen des InvocationAdapters durchgeben

denn die FeedManifold wird als »eins«, »zwei«, »viele« parametrisiert, und nun kommen hier möglicherweise noch weitere Kombinationen hinzu; ich erwarte allerdings kein volles cartesisiches Produkt, da es i.d.R. einen Zusammenhang gibt zwischen der konkreten Funktion und der Anzahl der Buffer

meine Einschätzung: diese Kombinatorik ist unvermeidbar

die FeedManifold ist stets fester Bestandteil des InvocationAdapters

...und zwar seit dem Anfang des Lumiera-Projekts. Vorstellungen haben ist schön; aber um zu bauen, brauche ich entweder ein Baumuster, oder ich brauche einen Plan und ein formal definiertes Ziel. Ich habe aber nur eine klar (negativ) abgegrenzte Finalität

...denn ich sehe genau wohin selbst die leiseste Festlegung führt:

 — zu einer gewaltigen universellen Klassifikation jedweden Medien-Processings

und zwar durch die Entscheidungen der letzten Wochen

⟹ muß dafür den Buffer-Typ kennen

der hier gecodete InvocationAdapter ist kein Inteface (sondern ein Concept)

Trennmauern zwischen allen Bauelementen im Baukastensystem

Stufe-1: zunächst einmal brauchen wir ein Array von BufferProvider(n)

der zentrale Test verifyStandardCase() ist auskommentiert, da er sich auf die BufferTable abstützt — und mit deren Implementierung bin ich seinerzeit irgendwo in einem ähnlich gelagerten Wald versumpft, in dem ich jetzt auch wieder herumrirre ...

 

der Turnout ist ein Level-1-Builder....

...da ja ohnehin im Zusammenhang mit dem BufferProvider überall diverse »conveninence shortcuts« implementiert sind — ehrlich gesagt, es war und ist noch nicht klar wohin die Reise geht...

mit der offensichtlichen forwarding-Impl

gehe mal davon aus,

daß nach kompletter Berechnung

ein emitBuffer() erfolgen soll

Processing-Funktion kann Parameter verlangen

Wert ist wichtig — keine Referenzen, keine Pointer. Das ist eine einfache Regel um Parameter von Buffern unterscheiden zu können, und ansonsten keinerlei weitere Einschränkungen zu machen

...weil wir SLOT_I ≔ SLOT_O setzen (wichtig!), wenn es keinen Input gibt; es gibt nur zwei Fälle ohne Parameter, und die lsassen sich damit in diesen Test zusammenfassen

da es nur in Traits selber verwendet wird

denn im Moment wird der ja wirklich gebraucht, da die FeedManifold noch eine fixed-size-Allocaiton macht

vorsicht Falle: Parameter werden oft per Referenz genommen

ich muß für die strukturierten Fälle eine eingene Trait-Spezialisierung anlegen

...dieser Umbau ist durchaus schwer zu stemmen....

Denn der weitere darauf aufbauende Code ist ebenfalls nicht ganz in Ordnung und benötigt die Generalisierung, um dann grade gezogen zu werden... und ich fürchte die Situation, wo mir ein Berg von Template-Code um die Ohren fliegt

...wenn möglich alles auf strukturierte Typen heben

man könnte nun sagen: det sollen se halt nit machen!

bedeutet, wir „biegen“ dann in einem Fall, nämlich wenn nur zwei Argumente gegeben sind, jeweils zwangsläufig in einer bestimmten Richtung ab, je nachdem ob eine leere Struct als wahr gilt;

Beispielsweise in der aktuellen Logik prüfen wir ob der 1.Slot ein »Valu« ist, was dann bei einer leeren Struct dazu führen würde, diese als ein leeres Param-Tupel zu behandeln. Kommt dann darauf an, ob das tatsächlich einen Schaden im Code anrichtet, oder auch leer durchläuft. Denn an sich sollte es ja egal sein, sofern dann nur dieser Slot auch tatsächlich keine Behandlung bekommt

Erläuterung: egal wie die interne Logik abläuft, am Ende muß sie für jeden Slot den Typ (kompatibel) ausweisen, der initial für diese Stelle in der Signatur angegeben wurde

jetzt bin ich bereits so weit gekommen, und hab den größten Teil der Komplexität in die FeedManifold verlagert....

Die Signatur des Parameter-Funktors folgt eigentlich zwansläufig aus der gegebenen Processing-Function: es muß ein TurnoutSystem& akzeptiert und ein Parameter(Tupel) geliefert werden (by-value). Seiteneffekte im TurnoutSystem sind möglich (aber die Ausnahme)

Dieser Trait erbringt keine so hohe Abstraktiosleistung wie _ProcFun; im Grunde könnte man auch allen diesen Code direkt in FeedPrototype schieben — es ist also mehr ein Vekikel zur Code-Organisation, räumt die low-level-Details weg und macht sie auch leichter testbar

das Template DirectFunctionInvocation<N,FUN>  ist der Hebel

Und ich sehe keinerlei Basis, um einen beliebigen Buffer-Mix ausschließen zu können. Denn wir wollen beliebige, nicht weiter bekannte Libraries adaptieren können. Die einzige verbleibende Barriere ist FUN selber, denn das kann auch ein generischer Adapter aus dem Library-Plugin sein; jedoch Vorgaben können wir keine machen.

Jede mögliche Funktions-Signatur erzeugt eine separate Turnout-Instanz

das Media-Lib-Plugin is Problemzone und Ansatzpunkt bzgl Template-Bloat

Trade-off: dafür dann eine runtime-Indirection

...denn exakt an dieser Stelle ist das Prototyping bis jetzt gescheitert ... wir konnten bisher nur eine „dämliche“ Dummy-Funktion binden; sobald eine echte Dummy-Funktion eingebunden werden sollte, bin ich darauf aufmerksam geworden, das eine solche Funktion zwangsläufig auch weitere Parameter brauchen wird, und nach einer weiteren, langen „Denk-Schleife“ bin ich hier wo ich grad bin

Der Name und die Vorstellung hat nun mehrfach gewechselt, aber es ging doch letztlich immer darum, eine unbekannte Library-Funktion so aufzurüsten, daß man sie mit einem generischen Schema zu fassen bekommt. Auch die Idee, die Buffer-Pointer in ein Array zu packen, gehört letztlich zu diesem Ansatz. Der darüber hinausgehende Teil der ursprünglichen Idee, dieses Strukur auch zu »bespielen« ist jetzt in der »weaving«-Metapher aufgegangen — und dieses fortschreitende Wechselspiel in der Speicherbelegung findet darüber ja nun tatsächlich statt.

beispielsweise könnte man den Parameter-Funktor im allgemeinen Funktor verstecken, und dann auch gleich noch den Scope, in dem der eigentliche Aufruf-Adapter gebaut wird. Man würde sich damit ein paar Bytes pro Node-Level sparen, um den Preis eines trickreichen Konstrukts ineinander geschachtelter Funktoren. Da gefällt es mir doch besser, die Funktoren und die Storage in einem Objekt nebeneinander abzulegen, zusammen mit Methoden, die von einem jeweiligen Weaving-Pattern genutzt werden könnten

hier zeigt sich immer wieder der gleiche Trade-off: zusätzliche Indirektion vs. Template-Bloat

...das wäre die billige Lösung: man erzeugt es einfach immer per Default, und dann macht man im ctor-body in FeedManifold eine conditional und eine Zuweisung. Das ist insofern unsauber, da wir Zuweisbarkeit der Werte im Param-Tupel nicht fordern (sondern nur default-Konstruierbarkeit).

...und die neueren Compiler können sich auch nicht beschweren, daß wir anonyme Typen in die Storage binden, und obendrein sind so die ganzen Meta-Definitionen wirklich downstream nicht mehr sichtbar

er versucht nämlich erst einmal, den getemplateten Ctor mit dem this-type zu instantiieren. Das ist dann hier F = struct Storage selber. In der enable-if-Klausel bilden wir aber _ProcFun<F> — und Storage ist ganz offensichtlich keine Funktion und löst die Assertion aus

enable_if_hasParam : verwendet statt Type statt type

Zwar war der Anlaß für diesen sehr kompleten Umbau ein Anderer, aber ich habe bei der Gelegenheit gerne auch noch eine komplette Überarbeitung des ersten Entwurfs gemacht. In diesem konnte und wollte ich mich zu Beginn noch nicht festlegen, und habe daher Extension-Points auf mehreren Ebenen vorgesehen; im Grunde bin ich mit diesem Ansatz stecken geblieben und habe dann einen »Durchmasch« per Prototyping gemacht. In dieser ersten Lösung war teilweise noch eine Steuerung aus dem Weaving-Pattern vorgesehen (in Anlehung an den Ausgangspunkt, den ersten Entwurf von 2009) — aber tatsächlich in der Implementierung auf der Strecke geblieben. Der Code war also so noch nicht ausreichend, er konnte nur einheitliche Buffer in wenigen, festen Konfigurationen handhaben, und eben (das war der Anlaß) überhaupt keine Parameter

beide Funktoren müssen in den Typ eingehen

wenngleich auch lediglich indirekt, denn der sichtbare Parameter ist FUN, der Typ der Processing-Function

neuer Name: FeedPrototype

Wenn es also ein Param-Tupel gibt, entscheidet es sich im Aufruf-Kontext, ob dafür ein Init-Wert geliefert wird. Wenn nicht, dann findet Default-Initialisierung statt. Ganz einfach

prototype.moveAdapted (paramFun) ⟶ move in neue FeedPrototype-Instanz

prototype.clone() ⟶ Kopie falls beide Funktoren das erlauben

....das heißt, alle wichtigen Eigenschaften aus _ProcFun<FUN> müssen delegiert bereitgestellt werden...

valide  ⟹  Funktor ist brauchbar

„glücklicherweise“ sollte man das seit C++11 auch stets so machen, was die diversen safe-bool idioms überflüssig gemacht hat

⟶ bool

conv

cons

*

trivial-λ

capture-λ

function

template<class PF>

using SigP = add_pointer_t<typename _Fun<PF>::Sig>;

template<class PF>

using isConfigurable = __and_<std::is_constructible<bool, PF&>

                             ,__not_<std::is_convertible<PF&, SigP<PF>>>

                             >;

...die werde ich voraussichtlich per Default (oder zumindest in wichtigen Fällen) einsetzen, um den Template-Bloat zu begrenzen. Das hat dann zur Folge, daß noch zur Laufzeit entschieden werden könnte, ob überhaupt eine Parameter-Konfiguration gemacht wird, oder ob default-konstruierte Parameter-Werte genügen

...das war der Name im Prototyping-Entwurf, und der ist viel besser!

Name: prototype.moveAdapted (paramFun)

FeedPrototype wird zur Gelenkstelle zum WeavingPatternBuilder

FeedPrototype wird zentraler Konfigurationspunkt

«InvocationAdapter» ist nun stets die FeedManifold

Output-Buffer-Typ-Default

muß von unten kommen

hatte im Juli 24 noch keinerlei Durchblick und ging außerdem davon aus, direkt auf den Ergebnis-Daten in lib::SeveralBuilder zu arbeiten; daher der Ansatz mit "Einfüllen"

Im Gegensatz zur Konfiguration der Lead-Ports ist diese ganze Buffer-Belegungs-Thematik weitgehend undifferenziert — es ist nur klar daß wir von irgendwoher einen Buffer brauchen, und daß es typisierte BufferDeskcriptoren gibt. Also hatte ich seinerzeit (Juli 24) die Behandlung symmetrisch zur Eingangs-Seite aufgezogen, und erst mal eine default-Konfiguration für jeden »Slot« gemacht. Dann, mit dem Umbau der FeedManifold (Dezember 24) ergab sich Möglichkeit (und Notwendigkeit), jeden Ausgabe-Slot individuell zu konfigurieren

diese Schachtel möchte ich nicht nach außen aufmachen...

Ich möchte definitiv nicht

  • daß auch nur der WeavingBuilder noch direkt mit der Typ-Repräsentation der FeedManifold herumfummelt
  • ich möchte aber auch nicht, daß der FeedPrototype nun maßgeschneiderte Daten für die Interna eines WeavingBuilders liefert

Lösung: Iteration über ein Buffer-Descriptor-Tupel

Hat noch so einige Tücken, wiewohl es im Prinzip einfach ist

die interne Logik ist so aufgebaut, daß sie sich dann durchgehend korrekt verhält, ohne daß dafür viel getan werden müßte; es muß lediglich eine Funktion sein, und kein brauchbares Ergebnis liefern

Dahinter verbirgt sich ein architektonisches Problem: die FeedManifold ist ein Bindeglied, und daher bewirkt jeder Konstruktor-Parameter eine Einbindung in Detail-Strukturen — problematisch dabei bleibt, daß die Einbindung nach beiden Seiten erfolgen kann. Konkret bedeutet dies....

  • von der usage-site her, also dem WeavingPattern muß die FeedManifold mit einer TurnoutSystem& versorgt werden
  • wohingegen von der Definitions-Seite her ggfs. Funktor-Objekte mit eingepackten Detail-Bindings übernommen werden müssen
  • und hier ist auch noch Variabilität gegeben: ein Funktor für Parameter ist optional, aber wenn er gegeben ist, dann kann (nicht muß) es sein, daß er ebenfalls Init-Argumente braucht, die dann aber ziemlich sicher aus einem anderen Struktur-Kontext stammen, nämlich aus der internen Logik des Builders, nicht aus dem Library-Plug-in

⟹ geht in eine Builder-Klasse FeedPrototype<FUN,PAM>

Hut ab für den Poster auf Stackoverflow: der hat diese Möglichkeit gesehen und ist dann nur an ein paar Kleinigkeiten gescheitert.

sonst dreht »man« (≙ich) noch durch...

...denn diese Mechanik ist im Grunde komplett unabhängig von Tuples (oder auch sonstigen Strukturen)

lib::meta::forEachIDX<T> ( λ )

MetaUtils_test::detect_tupleProtocol()

das war brutal

das ist doch der Teil der immer so mühsam ist....

extrahiert:  ElmTypes<X>  in variadic-helper.hpp

void dummyOp (NoArg in, SoloArg out)

rein logisch läßt sich das nicht lösen — die Entscheidung beruht dann auf einer willkürlichen Konvention; im Moment habe ich fesgelegt: »das können keine Buffer sein«

denn für einen Parameter-Slot muß weniger getan werden; im Zweifelsfall muß er nur default-konstruiert werden — und es wird dann halt auch kein Param-Funktor dazu aufgerufen

in den Turnout wird ein Prototyp der FeedManifold eingebettet

das heißt, idealerweise ist dieses ganze komplexe Konfigurations-Thema optional und transparent

...insofern dann alle Fälle mit gleicher Signatur zusammenfallen; für die häufigsten Fälle (wie z.B. ein einziger int-Parameter) erwarte ich einen starken Hebel

bekommt ggfs. ein zusätzliches Parameter-Tupel als ctor-Wert

Für die BuffTable war früher jede Menge Funktionalität geplant, die inzwischen im BufferProvider realisiert wurde; der alte Test hängt seit >10 Jahren bei den Engine-Tests unimplementiert mit rum — es ist sinnlos, ihn nun umzuwidmen

Strategie: während des Umbaues den alten downstream-Code compilierbar gehalten

und zwar, indem ich zunächst die Type-Traits umgestellt habe, und dann den alten Code auf das neue Traits-Inteface portiert. Damit konnte ich die alte Implementierung der FeedManifold (als "FoldManifeed" ☺) im Code erhalten — und alles was darunter hängt...

Haha! Nur ist das jetzt eines der zentralen Gelenkstellen im FeedPrototype geworden — oh oh oh wenn das alles bloß nicht so spannend wäre, könnte man ja glatt anderen Leuten davon erzählen

ich habe die Einbindung in ein fixed-size-Array der Größe N komplett aufgegeben, zugunsten einer flexiblen, Tuple-basierten Storage

alle Argumente ab dem 4.Argument gehen pauschal durch an INVO

  • der Prototype kann uns einen Tupel-Typ der Output-Buffer-Typen konstruieren
  • zusätzlich können wir ElmTypes<TUP>::Apply verwenden
  • damit kann man lokal im WeavingPatternBuilder ein »Handler-Template« für jeden Buffer-Typ instantiieren
  • in dieses könen wir die lokale Builder-Logik einbauen
  • und darauf auch laufzeit-indiziert zugreifen
  • oder statisch iterieren

Zur Erinnerung: HeteroData ist ein low-level Daten-Layout, das ich speziell für den Anwendungsfall im Turnout mir ausgedacht habe; es beruht auf einer single-linked-List von Datenblöcken, in denen jeweils ein Datentupel sitzt. Und hier liegt das Problem: diese Chain-Blöcke sollen nun schrittweise über eine Builder-Notation aufgebaut werden — aber die Konstruktor-Funktion in HeteroData erwartet die Angabe des ganzen Tupel-Typs auf einmal.

daher scheidet ein Typ-Konstruktor als Einstiegspunkt aus; man kann nämlich i.d.R. vom User nicht erwerten, daß der jeweilige Funktor-Typ explizit anggebbar ist

man baut ja schrittweise diese ParamBuildSpec auf ⟹ also muß es nicht gleich der final korrekte Tupel-Typ sein; erst wenn man damit tatsächlich alloziert, legen wir uns fest.

also ein Instanz-Typ allein legt alles fest; keine Indirektion oder Virtualisierung

damit liegt das unumgängliche Minimum an Information bereits fest; insofern wir explizit getypte Funktoren durch das System grundsätzlich durchreichen können wollen (nicht bloß std::function-Instanzen), bleibt keine Alternative, als diese Typen in ein Tupel zu packen....

...weil wir allein mit einer solchen ParamBuildSpec auf ein TurnoutSystem losgehen wollen; eine Hetero-Data prefix-Chain ist damit notwendig, als Anker-Punkt um einen konkreten Konstruktor für einen Chain-Block zu generieren, den wir in den aktuellen StackFrame legen wollen

also ParamBuildSpec<ANK, FUNZ...>

⟹ Ankerpunkt muß ein nested type TurnoutSystem::FrontBlock sein

also den gleichen Kniff, auf dem auch lib::meta::forEach und lib::meta::mapEach beruhen: man wendet per std::apply ein generisches variadisches Lambda auf die Tupel-Elemente an

denn ich kann nicht ein non-copyable-Member fertig per Konstruktor bekommen....

heißt, man müßte entweder explizit umkopieren, oder es bräuchte eine Konvention, wie man das (optional) gegebene BuffHandle dann eben doch frei gibt; letztlich wird ja ein BuffHandle zurückgegeben, und das wäre dann dasjenige, daß vom rekursiven Call belegt wurde. Könnte sogar klappen, ist aber als Trickserei zu werten.

Die Aufteilung in diese fünf Schritte dient vor allem der Gliederung der Abläufe; es ist aber nur bedingt ein »Protokoll«, in dem in bestimmten Schritten etwas Festgelegtes passieren muß...

Wenn man Strukturen biegen, leer machen oder umdeuten muß...

...da gibt es definitiv keinen anderen Ausweg, und eigentlich wird ja dadurch auch erst die Builder-Notation wirklich sinnvoll; daß man ein non-copyable-Objekt aus einer Funktion „abwerfen“ kann ist sowiso grenzwertig....

...damit man sieht, wie man's tatsächlich braucht;

oder anders herum, das sind die hexagonalen Gefahren der testgetriebenen Entwicklung — ich bastel jetzt schon seit 3 Wochen in diversen Inkrementen an einer Lösung herum, die in ihrer vollen Allgemeingültigkeit rein auf Verdacht geschaffen wurde. Und jetzt bin ich in einem Exkurs angekommen, der mal rein vorsorglich einen extremen Grenzfall schon durchspielt, und erst hier kommt der ganze tolle Entwurf zum ersten Mal zum Einsatz.....

trotzdem ziemlich durchgeknallt was ich hier mache

(und ich mache es dennoch, weil ich weiß, daß für einen Film-Editor des geplanten Kallibers alle diese abgefuckten Sonderfälle dann später auch tatsächlich gefordert und eingesetzt werden) 

mit dem Ziel, die Einteilung des Weaving-Schemas zu erhalten und universeller nutzbar zu machen

und all dies muß in einen verständliche Builder-DSL  verpackt werden

was bis jetzt feststeht: dem Weaving-Pattern werden BufferDescriptors gegeben

Begründung: der Aufrufer legt das zusammen mit der konkreten Funktion fest

Der Aufrufer ist Code im Kontext der Domain-Ontology, und nur von dort kann bekannt sein, was die eingebundene Funktion konkret auf jedem »output slot« liefert

Einschränkung: der Aufrufer kennt den benötigten Typ des Buffers

aber auch nur diesen

heißt konkret, ein BufferProdiver könnte einen BufferDescriptor eines anderen Providers übernehmen und re-interpretieren

wenn man nur ein opaques Handle herausgibt, dann läßt man sich Spiel für spätere Entwicklung im BufferProvider selber; wenn wir das aufgeben, dann wird die derzeitige Prototyp-Implementierung zum Standard; und es ist immer eine sehr schlechte Idee, eine Implementierung zum Standard werden zu lassen

und zwar, weil wir ja auf den Aufrufer / den Kontext der Domain-Ontology angewiesen sind, um überhaupt zu wissen, wie viele Slots es geben wird

zur Erinnerung: die vollständige Form lautet...

  template<typename BU, typename...ARGS>

  BuffDescr

  BufferProvider::getDescriptor (ARGS  ...args)

⟹ das müßte dann in den shed()-Aufruf gehen

...im aktuellen Stand brauchen wir die Info nur während dem Build-Vorgang; sobald man aber später übergeht zu einem ctor-λ, müßte man die Info in die Node (in den Turnout) materialisieren. Nun könnte man aktuell „einfach“ einen std::vector nehmen — oder aber, genauso „einfach“ einen weiteren SeveralBuilder mitlaufen lassen. Vorteil: der Speicher liegt im AllocationCluster / Nachteil: der Speicher wird verschwendet

die Policies und Adapter haben alle einen pass-through-ctor, welcher letztlich den ganz innen (protected) liegenden C++ -  Allokator initialisiert. Alles, was über diese Standard-C++-Mechanismen hinausgeht, muß über die Policy geregelt und eingebunden werden.  Relevantes Beispiel ist der AllocationCluster, für den in der Policy ein Zugangspunkt für dynamisches Justieren der aktuell letzten Allokation herausgeführt ist.

...denn es sind noch die flexiblen Strategy-Templates darüber gelegt, und außerdem hatte ich den eigentlichen Allocator nur als protected-Mix-in eingebettet — und deshalb wird der für einen normalen (generierten) copy-Konstruktor nicht in sichtbar; kopiert würde ja auf dem gesamten (zusammengesetzten) Strategy-Template, und das hängt nominell durchaus vom Element-Typ ab, der aber in diesem Fall, beim Weitergeben einer Initialisierung, durchaus variabel sein kann (nur der Basis-Allokator ist stets kompatibel)

denn das Standard-C++-Front-end für Allocation-Cluster ist ein std::allocator<byte>, und deshalb sind alle Varianten untereinander kompatibel (und das einzige verbleibende Problem wäre, daß der Allokator eine protected-Basisklasse ist)

damit man von einem existierenden SeveralBuilder ausgehend weiter verdrahten kann

Konzeptionell ist das Strategy-Template offen angelegt, aber in der tatsächlichen Implementierung dürfte seine Rolle inzwischen weitgehend festgelegt sein. Und in diesem aktuellen Gebrauch spielt der Element-Typ nur eine Rolle zur Ansteuerung des Allocators, und auch die Konstruktor-Argumente werden komplett von oben bis auf die Basis-Ebene durchgereicht. Da zudem der Basis-Allocator an den C++-Standard angelehnt ist, kann man unterstellen, daß sich praktisch immer ein passender Alocator für einen anderen als den gegebenen Element-Typ konstruieren läßt.

nein! jetzt direkt im Aufruf gelöst

ja: ist problematisch — und genau deshalb ziehe ich jetzt vor, den Output-Buffer direkt über den Aufruf bereitzustellen; dieser Aspekt spielt damit für das Thema Dependency-Injection keine Rolle mehr

im Codepfad des konkreten Aufrufs erfolgt ein lockBuffer() — und dieser muß exakt den Ausgabe-Puffer für diesen Aufruf liefern

heißt: wir vergeben diese nulläre Optimierung und legen fest, daß das Ergebnis einfach in einem Buffer im Arbeitsspeicher liegt. Jeder Render-Job bekommt dann einen explizit gecodeten Kopier-Schritt

das müßte eine Marke sein, die die spezielle BufferProvider-Implementierung — also der OutputBufferProvider — erkennt und daraufhin den konkreten extrnen Output-Buffer herausgibt

Das würde bedeuten, eine spezielle Ausnahme in die shed()-Funktion einzubauen, um das lockBuffer() fürden tatsächlichen »output-slot« zu unterdrücken und stattdessen hier einen aus dem Kontext bezogenen Buffer einzusetzen. Im Gegenzug würde der Rückgabewert wegfallen und das Ergebnis würde stattdessen per Seiteneffekt herausgeführt.

...diese ganze Überlegung übersieht, daß auf tieferen Ebenen ja ebenfalls ein Output-Buffer zurückgeliefert werden muß; nur dort unten ist dann kein spezieller BufferProvider notwendig

Diese Annahme kann man ziemlich sicher machen; eine Abweichung davon wäre nur möglich bei einem ziemlich speziellen Setup, bei dem dann setets mehrere Ergebnise auf verschiedenen Ebenen aber im gleichen Rechenprozeß anfallen

dieser assoziiert den Buffer-Header als LocalTag

...dieser eigentliche Buffer kann auch nochmal per TypeHandler einen »Inlay-Type« bekommen; aber hier beim TrackingHeapBlockProvider ist ja der entscheidenden Punkt, jede Allokation nochmal sekundär zu verzeichnen und nachzuverfolgen, um entsprechende Verifikationen in den Tests zu ermöglichen

siehe Key::forEntry

Das heißt, entweder man setzt ihn schon beim BufferDescriptor, oder man setzt ihn erst mit dem sub-Entry für den konkreten Buffer beim lock() — aber beides zusammen ist nicht erlaubt

deshalb habe ich genau für diesen Zweck bereits einen Mechanismus geschaffen

dieses LocalTag ist aber nicht überall auf das Buffer-Provider-API herausgeführt

im Besonderen an die Reihenfolge denken ... mir fällt auf, daß ich chainedHash  verwende; damit wäre der Hash-Key pfadabhängig

...und zwar je nachdem, in welcher Reihenfolge das LocalTag und die Buffer-Adresse gegeben waren: war das LocalTag zuerst da, dann würde das bereits Teil des Hash; wenn dann später jemand mit der gleichen Buffer-Adresse ankommt, aber ohne das LocalTag, dann wird der bestehende (schlimmstenfalls bereits gelockte) Eintrag nicht gefunden ⟹ es wird ein neuer Eintrag erzeugt und somit zweimal gelockt.

...da mit jedem Buffer-Typ auch eine bestimmte Memory-Block-Größe verbunden ist, könnte man dahinter mehrere kachelnde Basis-Allokatoren anordnen — und in einer solchen Struktur wäre es sinnvoll, zu erwartende Buffer-Allokationen im Voraus anzukündigen. Die Steuerung der Pool-Größe könnte dann im Hintergrund erfolgen, was möglicherweise eine relevante Optimierung auf Durchsatz ermöglicht

anhand der Hash-ID, die zum Entry gehört, welcher beim Erzeugen des Handles gespeichert wurde

der Entry wird aus der Metadaten-Tabelle gelöscht

  • DataSink liefert ein BuffHandle, hat dann aber eine eigne Methode DataSink::emit()
  • die BuffHandle::emit()-Methode ist nicht richtig integriert...
  • deshalb kann derzeit (12/2024) der Render-Node-Code nicht korrekt mit Output-Buffern umgehen!
  • Grund ist zu enge Verkopplung mit der Default-Implementierung. Siehe #1387
  • Da wird ein spezielle BufferProvider eingeführt, der dann doch gar kein richtiger BufferProvider ist (oder?)
  • und damit der funktionieren kann, muß man ein LocalTag explizit am bestehenden System vorbei schieben

zunächst hatte ich diese Idee verworfen, weil sie die einfache Symmetrie der Daten-Feeds durchkreuzt. Nun fällt mir aber auf, daß das ja nur auf dem Top-Level passieren muß, und daß genau da in jedem Fall ein Eingriff notwendig wird, denn man muß die Buffer-Info ja dort injizieren. Daran führt kein Weg vorbei

dieser würde dann nur von top-Level aus so erfolgen, womit der Spezialfall ganz natürlich ausgewählt wäre

...d.h. daß man das Output-Protokoll hinter einem BufferProvider versteckt, um einen uniformen Zugriff zu ermöglichen.

...sie entsteht dadurch, daß beide auf bewährten Lösungsmustern aufbauen; das heißt aber noch nicht, daß beide eine gemeinsame Natur haben

In beiden Fällen gibt es das Problem der Freigabe — aber meine Bewertung ist unterschiedlich ausgefallen: für einen Ausgabe-Vorgang gibt es verschiedene kritische Zustände, und die Möglichkeit einer Verklemmung; und genau weil man den Protokoll-Status über ein Timeout bestimmen kann, besteht auch die Gefahr, daß ein verspäteter Rechenprozeß in Verletzung des Protokolls weiterhin in die Ausgabe schreibt. Ganz anders beim BuffHandle, welches eine möglichst leichtgewichtige low-level-Abstraktion darstellt; die Methoden am dem Handle dienen nur dazu, Protokoll-Schritte auf eine höhere Ebene zurückzumelden, aber der Gebrauch des Handles wird nicht selber durch ein Protokoll geregelt. Hinzu kommt, daß ref-counting einen gewissen Performance-Overhead hat, der für ein BuffHandle vielleicht schon kritisch werden könnte; eine DataSink dagegen wird auf top-Level erstellt und wird dann nach unten nur ausgeliehen

Das ist das wichtigste Argument: sie finden auf verschiedenen Abstraktionsebenen statt. Der Ausgabevorgang kann möglicherweise  eine Buffer-Verwendung mit einschließen — letztere bekommt aber nur durch den Kontext ihre Bedeutung

das Problem ist nämlich: nach innen wird nur noch die TypeID weitergegeben, weil der eigentliche Entry, in dem das LocalTag dann zugänglich wäre, nur in der Metadaten-Tabelle liegt. Im Buffer-Descriptor liegt auch nur die Hash-ID des Typs

nebenbei: umbenennen ⟶ LocalTag

...es ist zwar gut, daß es das LocalTag gibt, aber für diesen Fall ist das eine unsinnige Trickserei; es ist viel besser, an der einzigen Stelle, wo das benötigt wird (nämlich auf top-Level) explizit eine Flag zu setzen und dann den Buffer zu entnehmen

nämlich den OutputBufferProvider explizit eine Ebene darüber verwenden und dann das BuffHandle direkt in den weave()-Aufruf geben

Den OutputBufferProvider handhaben wir jetzt eine Ebene höher; genauer, wir hanhaben ihn direkt, denn es ist nur ein einziger Buffer auf top-Level.

Alle anderen Use-Cases (Memory-Blöcke und Cache) sind ohnehin global für die gesamte RenderEngine, und damit handhabbar mit den normalen DI-Mechanismen

wenngleich auch dort Zweifel zum Namen bestehen

damit lib::Depend ohne Weiteres einfach funktioniert

Die Indirektion ist nur notwendig, wenn es einen für den Benutzer sichtbaren Lebenszyklus-Zustand gibt; andernfalls gilt es lediglich als Service-Konfiguration und der Client kann sich direkte Referenzen für die verwendeten Services ziehen

Denn nicht die Render-Engine hat einen Lebenszyklus, sondern das Core-System; die Abhängikeiten kaskadieren ausgehend von der Subsystem-Facade. Das bedeutet, sofern man überhaupt Zugang zu einer Core-Engine hat, ist diese bereits fertig konfiguriert und bleibt auch so; ein Austauschen oder Neustarten der Engine wird nicht vorgesehen. Mithin spielt diese Indirektion keine Rolle, denn die Verwendungen im low-level-Model können sich direkte Service-Referenzen ziehen; die Flexibilität wird lediglich für Applikations-Konfiguration und für das Testen benötigt

und zwar liegt die Indirektion hier in der VTable und ist damit bei jeder Verwendung wirksam

da es offensichtlicher ist, und zwischen Konstruktor und den von diesem erzeugten Services keine architektonische Trennung besteht

aber diese baut nur auf dem Typ der Funktoren auf

es könnte passieren, daß wir lediglich die Hash-ID generiern für generische Signaturen wie z.B. size_t(string); alle Funktoren mit dieser Signatur wären dann identisch

eine stabile Charakteriseierung wird benögigt

...bedeutet: eine Speicher-Adresse könnte bereits zu spezifisch sein....

irgendwie binden wir hier einen Handler ein, der »hinten rum« am Lib-Plugin hängt und dafür einen (privaten) Datentyp konstruiert ⟹ diese Information muß in den Prototyp eingehen

auf jeder Ebene gibt es genau eine Slot-ID, von der die Ergebnis-Daten an den Aufrufer zurückgegeben werden. Ohne explizite Konfig ist das der 1.Slot (Konvention). Von dieser Position wird das BuffHandle nicht geschlossen, sondern als Ergebnis geliefert und wandert damit auf die Eingabe-Seite der nächsthöheren Node

speziell für den top-Level wird ein OutputBufferProvider eingesetzt, der an dieser Stelle dann auch noch ein LocalTag bekommen muß

und diese müssen matchen

ich empfinde das als einen schrecklichen Hack

die Ausnahme überhaupt gar nicht über die Konfiguration/Verdrahtung einführen, sondern direkt in den Aufruf

im WeavingBuilder:

using TypeMarker = std::function<BuffDescr(BufferProvider&)>;

sollte »im Prinzip« auf das bestehende Schema aufgepflantzt werden

umbenannt in MediaWeavingPattern

muß irgendwie in den PortBuilder

man muß den Rückgabewert wirklich weiterverarbeiten; das ist aber eine unvermeidbare Einschränkung, wenn man überhaupt so ein Verhalten mit flexibel weiterentwickelten Typen realisieren will....

Der Prototype wurde selber zu einer Policy, weil er auch das Parameter-Binding mit steuern muß

zur Laufzeit haben wir Services wie den Cache und die BufferProvider; diese sollten beser nicht dirket auf dem Builder-API sichtbar sein, sondern magisch injiziert werden

jetzt habe ich einen WeavingBuilder geschrieben, dem man das alles explizit sagen muß

Ja .... aber (YAGNI)

...da es sich um C-Libraries handelt....

Unsicherheiten beim Durchreichen des Initialisierers.

Das notorische Problem; ich möchte in der eigentlichen ProcNode keine std::function haben, sondern idealerweise ein Lambda. Aber irgendwo im Turnout muß ein Prototyp dieses Funktors liegen, von dem wir für jeden Aufruf kopieren können. Das ist kniffelig, wenn der Initialisierer hierfür durch eine perfect-forwarding-Kette durchgereicht werden soll.

src/lib/several-builder.hpp:327:39: warning: parameter 'src' set but not used [-Wunused-but-set-parameter]

         moveElem (size_t idx, Bucket* src, Bucket* tar)

Turnout<SimpleWeavingPattern<

            Conf_DirectFunctionInvocation<1

                                         ,void(array<char*,1>,array<char*,1>) >>>

insofern verhält sich hier der Compiler formal und auch  inhaltlich logisch korrekt

Port ◁— Turnout

MoveOnly ◁— CONF  ◁— SimpleWeavingPattern  ◁— Turnout

Nachtrag: aus anderen Gründen habe ich Bulk-Allokation ermöglicht

Aber es führt kein Weg daran vorbei; das hier erstmals bemerkte Problem, daß Port non-copyable ist/sein sollte, führt tatsächlich dazu, daß man keine einfache dynamische Speicherbelegung bekommt. Genauere Analyse zeigt aber, daß das grundsätzlich nicht möglich ist (und auch nicht wünschenswert, da wir eine low-level-Struktur bauen und auch gute Cache-Kohärenz wollen)

Nun habe ich den Builder so umgebaut, daß alle Port-Konstruktoren als Lambda verzögert aufgesammelt werden; man kann dann am Ende alle Allokationen auf einmal »abwerfen«, so daß sie wunderbar kompakt liegen

indem man die Parameter in diesem Zweig voided

Da ProcNodes regelmäßig auf ihre Leads per direkter Referenz zugreifen, müssen sie sofort mit der Erzeugung im Speicher festgesetzt werden. Das war auch der Grund, warum ich den Builder in dieser limitierten Form aufgebaut habe: man muß sich zwangsläufig von den Quellen durch den Graphen aufwärts bewegen.

»one-shot«-Konstruktor, Objekte werden nur in den Definitionslisten erzeugt. Das erfordert entweder, extrem viele Detail-Konstruktorparameter durchzureichen (Problem der Verkopplung), oder eben ganze Teilkomponenten per RValue-Ref entgegenzunehmen und an den Zielpunkt zu schieben

Und dieser Compiler ist alt: ich sitze auf einer 4 Jahre veralteten Plattform.

Aufgrund der Komplexität der aufgebauten Strukturen ist nicht ersichtlich, ob der Compiler sich korrekt verhält; jedenfalls versucht er, den Copy-Konstruktor von engine::Turnout zu verwenden

das ist hinlänglich bekannt, und deshalb mache ich es routinemäßig so; habe es aber nochmal explizit versucht, und das Problem tritt auf. Das wäre erst gelöst, wenn wir auf Module umstellen würden

...sobald ein Copy-Konstruktor oder Destruktor explizit definiert ist, wird der Compiler keinen Move-Konstruktor mehr generieren (sondern ggfs. nur noch einen Copy-Konstruktor). Die Begründung dafür ist, daß in einem solchen Fall wahrscheinlich der default-generierte move-Konstruktor subtil oder gefährlich falsch sein könnte

man müßte lediglich der build()-Methode explizit einen SeveralBuilder übergeben; das ließe sich sogar generalisieren auf etwas Generisches, das eine emplace()-Methode bietet; in erster Näherung (C++17) wäre das ein offener Template-Parameter

mit »Hintertür« meine ich die Fähigkeit zum dynamischen Wachsen der Allokation speziell im Allocation-Cluster; das ist leicht möglich, aber nur indem wir die aktuell neueste Allokation im Rahmen eines Bucket nachjustieren

  • einfach: man hat eine Deque mit allen Buildern; erst zum Abschluß werden alle diese Builder in einem Lauf in das Several<Port> entladen; das könnte dann nur in der finalen build()-Methode für die Node stattfinden
  • trickreich? könnte man ggfs etwas mit Tail-Rekursion »zaubern«?

es könnte vielleicht sogar die Standard-Variante sein, aber so eine Änderung wäre massiv, und könnte mehrere Wochen Arbeit nach sich ziehen und am Ende doch scheitern. Also wäre das dann ein weiteres Ewigkeits-Projekt (als hätten wir nicht genug von der Sorte)

jedermann könnte jederzeit Port-Referenzen in anderen Umgständen erzeugen

Turnout ist sicher nicht trivially copyable

  • und zwar allein schon wegen dem virtuellen Destruktor
  • außerdem aus konzeptionellen Gründen: es soll ja ein Extension-Point sein, also können wir eigentlich nichts über diesen Typ sagen

wenn später in der Sequenz eine größere FeedManifold gebraucht wird, läßt sich der Spread nicht mehr durch Verschieben bereits bestehender Elemente korrigieren

...weil kein realloc() möglich ist; zudem muß auch die Extent-Größe des AllocationCluster bedacht werden (was sich jetzt hier, im Prototyp-Code noch gar nicht auswirkt, da wir ja Heap-Allokationen machen); wenn es der letzte aktive Builder ist und alle Anforderungen am Stück kommen,  dann könnte man im AllocationCluster dynamisch justieren (nicht aber bei Heap-Allokation)

die erhoffte »dynamische Belegung«

ist unter den vorgegebenen Beschränkungen

so nicht realisierbar

Vorgabe:

  • es ist eine Performance-kritische Datenstruktur
  • wir müssen eine Bulk-Allokation machen
  • Datenwerte sollen kompakt liegen (cache locality)

...denn beide Lösungen unterliegen den strengeren Beschränkungen bezüglich der Allokation; das hat aber auch den Vorteil, daß man diese Entscheidung rein auf Basis der sonstigen Vor- und Nachteile abwägen und treffen kann

Es besteht die Gefahr, daß für diese genau abgezirkelte Allokation ein festes Belegungsschema notwendig wird, und damit die Idee eines Builder-API im Kern negiert ist; denn in einem solchen Fall würde es zu einer Verkopplung zwischen der Logik in der Domain-Ontology und dem Datenlayout im Modell kommen

für alle problematischen Daten-Builder gilt.....

»problematisch« sind alle Daten-Builder, die

  • mit heterogenen Objekten belegt werden
  • und für die die Objekte nicht trivially copyable sind

es kommt hier nur zu einer gewissen Speicher-Verschwendung, wenn ein realloc() mit Umkopieren gemacht wird, da der AllocationCluster die alte Allokation einfach tot mitschleppt

Man würde sich damit von dem vertrauten und klaren Builder-API verabschieden, und stattdessen ein Aufrufmuster verwenden, welches von mindestens der Hälfte aller Programmierer als „schwierig“ empfunden wird. Und zwar rein aus Gründen der Implementierungs-Mechanik; nach der inhaltlichen Logik wäre das nicht notwendig

es kann nicht die eigentliche Processing-Function sein, denn die ist generisch. Sondern in dem Functor steckt eigentlich die verzögerte terminal-build-Operation

denn der konkrete Funktionstyp und das N (≙Manifold-Größe), sowie ggfs. ein alternativer »InvocationAdapter«-Typ sind zunächst im konkreten Typ des PortBuilders angelegt (als Typ-Kontext); das bedeutet, das Parameter-Satz muß aus lauter Wrappern bestehen, die die konkreten Typen bereits materialisiert und virtualisiert enthalten. Für lib::Several ist das definitiv der Fall (das verweist auf einen Extent, in dem die Metadaten und das Daten-Array liegen). Auch std::function ist eine Abstraktion. Also wäre wohl noch ein weiterer Generator-Functor notwendig, in dem man den konkreten InvocationAdapter versteckt und damit die genaue Parametrisierung des Turnout versteckt....

...wichtig: Wartbarkeit

und im getypten Kontext im Funktor wird das Ergebnis »abgeworfen«

...zwar könnte man jetzt wieder ein Tupel machen, in das den Builder-Functor legen und zusätzliche Steuer-Infos; diese Tupel würden dann in eine Collection auf dem Heap gehen und dort iteriert. Das wäre (scheinbar) simpel, aber nicht einfach und klar. Deshalb bleibt für mich als einzige Alternative, die notwendigen Schrite unmittelbar in eine verkettete Aufrufstruktur zu packen. Das ist dann zwar zunächst fordernd für den Leser (aber das ist die Implementierung des Builders sowiso schon); aber immerhin erschließt sich der Sinn unmittelbar lokal im jeweiligen Aufruf — genauer gesagt, man kann direkt beim lokalen Aufruf den Sinn in einem Kommentar erläutern, und zwar komplett

diese Datenstruktur ist die Sequenz der Builder selber

Builder sind generisch, d.h. tragen Typ-Parameter, und die konkreten Typen in diesen durchlaufen eine Sequenz, in welcher sich schrittweise die Datenstruktur aufbaut. Effektiv liegen die Daten in dem äußeren Stack-Frame, welcher das Builder-API aufruft; sie werden jeweils für einen Aufruf in den nächsten Stack-Frame geschoben, und dann wieder zurück geschoben, allerdings wie in einer russischen Puppe immer weiter verpackt

bekommt (später, zum Aufruf) einen DataBuilder<Port> &

...und zwar vor allem zur besseren Verständlichkeit und Wartbarkeit; da es sich um lauter nicht-virtuelle, direkte Aufrufe bekannter Funktionsdefinitionen handelt, kann der Optimiser jedweden Overhead problemlos entfernen

jeder spätere Aufruf legt sich oben über eine Vererbungs-Kette, wobei lokal ermittelte Größen-Parameter der benötigten Datenstruktur mit eingebettet werden; damit läßt sich dann später auch die erforderliche Gesamt-Größe der Storage bestimmen, bevor das die Several-Collection selber gebaut wird (und das ist der Zweck des ganzen Unterfangens)

Hier tritt das bekannte Problem der umgekehrten Reihenfolge für einfach verkettete Listen auf: das zuletzt hinzugefügte Element liegt zuoberst...

Man konstruiert einen (Meta)Typ für den eigentlichen Aufruf, und verwendet das bereits implementierte Anhängen an eine Typliste. Dieser Ansatz birgt zwei schwer abschätzbare Risiken

  • die Compilation könnte extrem lang dauern und ggfs sogar komplett scheitern, wenn es sehr viele Ports gibt
  • die Debug-Infos könnten extrem aufgebläht werden, falls für jeden Zwischenschritt eine Typ-Info generiert wird; das ist besonders gefährlich, da für sehr viele Nodes jeweils andere konkrete Typen generiert werden (andere Processing-Function, abweichende Parameter). Das sieht nach einem sehr gefährlichen Hebel aus!

In der eigentlichen Invocation macht man einfach etwas vor und etwas nach dem rekursiven Aufruf. Gefahr: Stack-Overflow bei sehr vielen Ports (Ambisonics etc....)

...damit die terminale Methode des PortBuilders nun nicht mehr den originalen NodeBuilder per slice-copy extrahiert, sondern stattdessen eine angepaßte Variante erzeugen kann, bei der ein »Zwiebelschalen-Layer« um die PatternData gelegt wird, während alle weiteren Daten per move weitergeschoben werden

danach überhaupt erst outTypes = DataBuilder<POL, BuffDescr>  erzeugen

der primäre Konstruktor nimmt Var-Args, und reicht sie zur Initialisierung des Allokators durch (was vor allem den wichtigen Fall eines monomorphic Allocators abdeckt, der gar keine Init-Argumente nimmt)....

der andere ctor würde dann nur vorgezogen, wenn er exakt paßt

Das sind die bekannten Regeln für die Overload-Selection: ein exakter Match ist stets besser als ein Match mit einer impliziten Konversion. Aber ein variadic Match ist schlechter als ein exakter Match.

hab jetzt zwei Stunden mit dem TypeDebugger die Instantiierungen auseinandergenommen, bis ich den Fehler gefunden hatte: die compile-time-Konstante ist eine static constexpr, wird also zu einer const&, und außerdem muß man dann auch noch das Strippen der RRef berücksichtigen, und eigentlich auch noch den Subtype match. Nee Danke!

...muß hier zweistufig vorgehen mit einer Helper-Metafunction, die dann den genauen Match der Argumente prüft.

das ewige Ärgenis bei Function-Referenzen, die auf einen Function-Pointer decayen. Das passiert dann irgendwo mittendrin; konkret beim Binden in die Lambda-Closure, wo ich leider überhaupt keinen Einfluß nehmen kann. Grrrrrrr

...weil diese Funktion in der Praxis sehr häufig ein Lambda sein dürfte, das dann direkt kopiert und ge-Inlined werden kann. Bekanntermaßen ist nämlich std::function nicht sonderlich Inline-freundlich; in meinen Experimenten hat es manchmal geklappt, manchmal nicht, und dann bleibt eine Indirektion durch einen Funktion-Pointer übrig, was ich hier, auf einem Performance-kritischen Pfad gerne vermeiden möchte. Mal ganz abgesehen davon, daß eine std::function auch ziemlich »fett« ist (bei mir 5 »Slots« ≙ 40 Byte) — klar, aktuell haben wir das gleiche Problem auch in lib::Several, aber dort weiß ich bereits, daß es sich nahezu optimal lösen läßt, wenn man nur die entsprechende Komplexität in der Definition in Kauf nimmt (was ich auf später verschoben habe, siehe Ticket #)

Denn std::decay macht genau das, was auch passiert, wenn man ein Argument  by-value nimmt. Und das ist hier auch aus anderen Gründen genau das, was wir brauchen: Egal ob Lambda oder Funktor order Funktions-Referenz, es wird erst mal im Builder materialisiert, und von dort weiter geschoben in die λ-closure, welche im Builder für den InvocationAdapter abgelegt ist; im Prototyp-Beispiel ist das die Klasse DirectFunctionInvocation. Das »Protokoll« für den Turnout und das SimpleWeavingPattern erwartet, daß in diesem Builder eine Fuktion oder ein Funktor buildFeed() gegeben ist. Das bedeutet, für jede Invocation wird eine Kopie der processing-Function gezogen und direkt in den code des InvocationAdapters geinlined. Typischerweise ist die processing-Function selber wiederum als Lamda definiert, und damit erfolgt ein sehr direkter und effizienter Aufruf der eigentlichen Library-Funktion

die Zwiebelschalen werden sukzessive darüber gelegt, also ist der Anfang ganz innen

...jeder einzelne Turnout-Builder liefert nebenbei auch (über einen compile-time-constant-Parameter) die Storage-Size des von ihm generierten Turnout ab; über eine max()-Kette finden wir die größte Anforderung, die dann auch als »spread« an den lib::SeveralBuilder übergeben wird

diese setzen den jeweiligen Turnout direkt per emplace() in die Ziel-Storage

weil nur der PortBuilder seine eigene Position erschließen kann (weil er auf den geerbten DataBuilder zugreifen kann)

d.h. der Default würde mit hochgezählt, kann aber per Setter auf andere Werte gesetzt werden

PRECONDITION: weaving-pattern-builder.hpp:415: thread_1: connectRemainingInputs: (leadPorts.size() + cnt <= knownLeads.size())

wir definieren eine Quell-Node ohne Vorgänger, aber binden eine Funktion mit einem Input-Slot

...doch dazu ist das ganze Introspection-Schema für die processing-Function (im Moment) viel zu starr

komplexe Typ-Verbindungen

noch nie wirklich getestet

...technisch ist das ein kniffeliger double-Dispatch; beide Seiten der Interaktion haben jeweils einen konkreten Typkontext, den man so nicht direkt als API-Funktion ausdrücken kann, und ich kann im Moment noch nicht vorhersagen, wie das zu lösen ist — es wird darauf ankommen, die Indirektion geschickt zu drehen, so daß sie auf etwas Generisches fällt.

mache hier (für den Prototyp) die Anahme: der Level-2-Builder kann konstruiert werden

...insofern die Library die richtigen Eingänge wählen muß und deshalb Annahmen (oder explizite Informationen) zu den Leed-Nodes benötigt; zumindest kann unterstellt werden, daß auch die Lead-Nodes durch die gleiche Library implementiert werden, oder Adapter darstellen, welche kompatible Stromdaten für diese Library produzieren

...das heißt, es kann mehr Leads als Slots geben

(oder eines InvocationAdapters oder Pattern Builders)

⟹ also muß der WeavingBuilder seine Typ-Parameter schwenken können

⟹ und der ihn umschließende PortBuilder muß diesen Schwenk mitgehen

schon das Ding mit PatternData ist grauenhaft — und s'werd ois no fui schmlimma

letztlich auch keine Katastrophe mehr. Da die darüber liegenden Ebenen eigentlich nichts mit der Funktion zu tun haben, läuft es darauf hinaus, zwei dämliche cross-build-Konstruktoren zu coden. Das ist lediglich lästig, weil man alle Datenfelder einzeln aufführen muß. Die sonstige Verwendung im Code wäre durchaus elegant: der äußere Port-Builder ist für das Große Ganze zuständig, und der WeavingBuilder bekommt nur noch einen vorkonfigurierten Parameter-Funktor und adressiert damit das cross-Builder-API des FeedPrototype....

Tja ... was mir daran definitiv nicht gefällt, ist die Vorstellung, mich in 5 Monaten in diese ganze Struktur von oben bis unten wieder hineindenken zu müssen, denn hier gibt es nichts, an dem man sich entlanghangeln könnte, sondern man sieht nur einzelne cross-Builder mit WTF-Faktor

das ist dann schon Arbeits-Aufwand, und auch jeweils eine Code-Duplikation

Denn das ist die zentrale Eigenschaft des lib::Several, die sich immer mehr als Glücksgriff herausstellt: man kann Detail-Typen unterbringen und sofort auf das Interface löschen

insofern ist PatternData tatsächlich eine Funktionale Datenstruktur

dagegen spricht daß es die schlimmste Stelle noch komplexer  macht

template<class RES>

using Builder = function<pair<RES, Ctx>(Ctx)>;

template<class RES>
pair<RES, Ctx>
runBuild(Builder<RES> buildr, Ctx c)
{
    return buildr(c);
}

eine Anpassung macht aus einem Resultat

einen weiteren Kontext-abhängigen Builder

template<class R1, class R2>

Extension = function<Builder<R2>(R1)>

template<class R1, class R2>
auto
StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
{
    return [b1, ext](Ctx c) {
        pair<R1, Ctx> inter = runBuild(b1, c);
        Builder<R1> extBuilder = extension(inter.val);
        return runBuild(extBuilder, inter.ctx);
    };
}

aber das wäre das eigentliche Problem

insofern hier nämlich bereits akzidentelle Komplexität generiert wird

Man kann die Struktur zwar extrahieren, aber die extrahierte Version läßt sich nicht sinnvoll in einfachereren Begriffen darstellen

...insofern hier Komplexität nicht durch eine Abstraktion reduziert wurde, sondern nur durch Modularisierbarkeit und Zwischenschritte beherrschbarer gemacht wird; aber dies wird durch Generieren zusätzlicher Komplexität erkauft, was den Ansatz insgesamt in Frage stellt

Fazit: bite the bullet...

dann exakt das gleiche Setup

per ParamAgentBuilder konstruieren

getrieben durch elaborierten Testfall

NodeFeed_test::feedParamNode()

...die dann später den eigentlichen Turnout produziert und in den DataBuilder für die Node-Ports „abwirft“

diese zusätzliche Komplexität ist hier überflüssig (auch wenn das nicht ganz symmetrisch ist...)

...aber was will man schon machen; das liegt in der Natur des Builder-Ansatzes, daß der Builder selber aus relativ inhaltsleerem mapping und forwarding besteht, und man zudem die Builder-Struktur im Kopf haben muß, um sich im Code zurecht zu finden, da aus jeder Sub-Klausel ein ganzer Sub-Builder wird. Und zu allem Überfluß kommt hier noch die Komplikation mit dem Memory-Management hinzu, die man überhaupt nur mit einem solchen Builder-Ansatz noch beherrschen kann.

Ich fürchte, dieser Code ist nun nahezu undurchdringbar für jeden Leser außer mir

  • ich weiß mir aber nicht anders zu helfen
  • die Komplexität bringt auch mich nahezu um
  • und das, was oberhalb dieses Builders aufsetzen wird, ist ebenfalls sehr komplex und dringend auf die Abstraktionsleistung der DSL angewiesen

rein nach Definition sollte es bereits funktionieren

naja ... einige technische Kleinigkeiten haben noch geklemmt

...bisweilen hat man eben doch einen Builder (z.B. wie hier, für Operationen in dem Builder selbst). Dann kann es ggfs gefährlich sein, eine Operation auf dem Builder aufzurufen, wenn danach ein neues Objekt konstruiert wird. Wir müssen ein neues Objekt konstruieren bei jedem Cross-Builder-Schritt, d.h. diese Gefahr läßt sich nicht bannen. Die normalen Operationen jedoch können eine RValue-Referenz zurückgeben (analog zu lib::SeveralBuilder). Denn eine RValue-Referenz ist auch eine generelle Referen, und erlaubt den Aufruf von Member-Funktionen, so daß dann deren Rückgabewert eigentlich bestimmt, was gebaut wird. Das ist kniffelig und kapp am Abgrund, aber nur durch einen solchen RValue-Builder sind Cross-Builder mit Weiterentwicklung der Typ-Parameter im Build-Prozeß möglich

...für sich genommen wäre das eine Festlegung auf das »simple weaving pattern« — das bedeutet, 1:1-Verdrahtung mit explizit anzugebenden Ausnahmen

Das Pattern heißt jetzt auch MediaWeavingPattern und ist zum Standard geworden; der Build-Mechanismus im Port-Builder könnte jedes dazu kompatible Pattern  ebenfalls handhaben

das bedeutet: ein Builder für eine bestimmte Art Weaving-Pattern kann letztlich auch direkt einen Turnout konstruieren, sofern nur das WeavingPattern die 5 Schritte als Member-Functions bereitstellt, welche notwendig sind, um das Port-API zu implementieren

...weil es immer mehr darauf hinaus läuft, daß der PortBuilder die high-level-Sicht auf die Verdrahtung hat, während der WeavingBuilder von ersterem explizit gesteuert werden muß

Da der Prototyp hier direkt durchmaschiert, und man trotzdem damit erst mal jede erdenkliche Medienberechnung per CPU erschlagen könnte (dank der Flexibilität, die ich mir in die Parameter-Signatur der Processing-function eingebaut habe), besteht vorerst wohl doch kein so starker Bedarf nach weiterer Flexibilität. Immerhin, der Extension-Point ist da, wie ich durch die Implementierung des »Param Agent Scheme« demonstrieren konnte.

Potentiell problematisch ist, daß die Verdrahtung im NodeBuilder / PortBuilder extrem technisch anspruchsvoll ist (man reicht mehrere Template-Parameter durch, hat cross-Builder, ein up-Slicing und einen deduction-Guide, der dann »nebenbei« auch noch eine funktionale Datenstruktur befüllt. Nicht daß ich das aus Spaß so gemacht hätte....

...derzeit verwendet der fest eingestellt das SimpleWeavingPattern — und ebenso eine fest verdrahtete Basis-Konfiguration. Richtig wäre es, entweder mehrere WeavingBuilder-Varianten zu haben, oder — wenn schon ein einziger WeavingBuilder genügt — diesen nicht nur mit der Funktion zu parametrisieren

Über das Prototyping hat eine Art Normierung stattgefunden; ich bin jetzt sicher, daß die allermeisten Aufgaben zur Medien-Berechnung in dieses Standard-Pattern abgebildet werden können, vor allem durch die stark erweiterte Flexibilität des Buffer- und Parameter-Bindings

Daß das in solchem Maß erfolgreich wird, hätte ich nicht erwartet. Zugegeben: ich hab's erst vor mir hergeschoben, und als ich dann den Umbau tatsächlich durchgezogen habe, war das eine der brutalsten Aktionen, die ich jemals gemacht habe. Also genau das Richtige für Weihnachten (!).

Und — oh Wunder — mir geht die Phantasie aus. Ich kann mir im Moment keine Berechnungs-Aufgabe ausdenken, die man nicht mit diesem Standard Weaving-Pattern + geschickter Verwendung von Parametern lösen kann. Also wird das vielleicht erst etwas für viel später — Hardware-Accelleration und so....

erfolgt als Teil der Konfiguration eines Ports

also nachdem man den Processing-Functor gegeben hat

...durch Delegieren an PAT::mount()  —  wobei PAT ≡ weaving pattern

aber festgelegt wird sie beim Bauen des Turnout

....und zwar durch Festlegen des konkreten Typs des Turnout, welcher dann das Weaving-Pattern als Mix-in einbindet, um auf dieser Basis das Turnout-Interface zu implementieren

Hier droht die typische Falle des white-box-Testing: daß man auf Implementierungsdetails abstellt — was ganz besonders gefährlich wird, wenn die Implementierung erwartbar eine hohe Kohäsivität aufweist. Daher sollte der Hauptfokus darauf gereichtet sein, eine Test-Ontologie aufzubauen, vermöge deren der erfolgte Berechnungsvorgang präzise ausgeleuchtet werden kann.

Verifikationen: Zugang über das Schema watch(X)

Analog zum BlockFlow, wo sich dieses Baumuster hervorragend bewährt hat:

  • es gibt ein generisches Zugangs-Prädikat: watch(X)
  • dieses konstruiert ein Diagnostics-Objekt, das entweder vom Target ableitet oder Friend ist
  • auf diesem gibt es einfache Accessoren und Verifikations-Methoden
  • damit kann man die OO-Kapsel für den Test durchdringen

siehe TestFrame_test

es gibt nur einen festen Seed-Mechanismus, der auf der Kanal- und Sequenz-Nummer beruht

kann Lifecycle markieren als CREATED, EMITTED, DISCARDED

bringt nicht wirklich was, aber zumindest sollte std::random mit kompatiblem Generator verwendet werden

...also zumindest eine globale Variable, die auslesbar und zuweisbar ist.

Es fehlt ja generell noch ein Framework für Zufallswerte in Tests, so daß jeder Test seinen definiten Seed bekommt, welcher zwar normalerweise zufällig, ansonsten aber feststellbar und reproduzierbar sein sollte

wirft auch den internen Daten-Cache weg und re-seeded den Basis-Hash, auf dem die Distinction-ID dann aufbaut

Das sind sie bereits in der bestehenden Implementierung, und das Prinzip ist auch bereits gut genug; allerdings sollten wir das Schema von std::random verwenden, d.h. jeweils einen Generator und darauf aufbauend eine Distribution

Das Daten-Array liegt nicht am Anfang, und ist deshalb plattform/implementierungsabhängig aligned; das erschwert auch die direkte Interpretation eines Datenblocks als TestFrame....

....und damit sollte man jede beliebige Speicherlocation als Testframe interpretieren können, ohne diese zu korrumpieren; das impliziert auch, daß der behandelnde Code erkennen kann, ob ein Metadatenblock überhaupt angelegt wurde — und wenn das nicht der Fall ist, sollte auch niemals implizit in den Speicher geschrieben werden

Dabei ist die Prüfsumme ein verknüpfter Hash, und man sollte diese jederzeit berechnen können; jedoch ein regulär erstellter TestFrame sollte die Prüfsumme explizit speichern, und auch für beliebigen Dateninhalt sollte sich die Prüfsumme explizit markiern (speichern) lassen, denn damit werden auch weitergehende Berechnungen auf den Daten möglich, die vom initialen Seed abweichen

neue Bedeutung: es ist ein TestFrame mit erkennbarem Metadatenblock (auch wenn ansonsten keine der Prüfsummen mit den Daten zusammenpaßt); es sollte recht unwahrscheinlich sein, daß ein zufälliger Datenblock diesen Test besteht.

Es ist ein TestFrame mit gültigem Metadaten-Block, und die dort gespeicherte Prüfsumme wird durch die Daten bestätigt

entspricht dem bisherigen isSane() — d.h. isValid() aber zusätzlich passen die Daten auf die gespeicherte distinction, und das heißt, die Daten wurden vom Standard-Schema erzeugt und nicht weiter bearbeitet

testframe.hpp (/zLumi/tests/core/steam/engine)

  • buffer-metadata-test.cpp (/zLumi/tests/core/steam/engine)
  • buffer-provider-protocol-test.cpp (/zLumi/tests/core/steam/engine)
  • diagnostic-output-slot.hpp (/zLumi/tests/core/steam/play)
  • output-slot-protocol-test.cpp (/zLumi/tests/core/steam/play)
  • testframe.cpp (/zLumi/tests/core/steam/engine)
  • testframe-test.cpp (/zLumi/tests/core/steam/engine)
  • test-rand-ontology.hpp (/zLumi/tests/core/steam/engine)
  • tracking-heap-block-provider-test.cpp (/zLumi/tests/core/steam/engine)
  • hängt nur von den Eingabedaten ab
  • für jeden Pixel auch durch Direktaufruf der Berechnung reproduzierbar
  • (optional) in jedem Schritt mit einer Instanz-ID verknüpfbar

...das bedeutet, sie läuft auf size_t (oder was dann letztlich die Wortbreite sein wird für Lumiera Hash-Vorgänge) und sie ist nicht kommutativ in den Argumenten

Jeder Funktionsaufruf ist „gefärbt“, aber ansonsten äquivalent. Das heißt, ein fester Parameterwert fließt mit in die Berechnung jedes einzelnen Datenpunktes, und dieser Wert ist an den jeweiligen Aufruf gebunden wird, wodurch auch eine Kette gleichartiger Aufrufe in der Reihenfolge unterscheidbar gemacht werden kann.

...zwar handelt es sich stets nur um eine Hash-Verknüpfung, aber die Arität (und ggfs. später auch noch ein Array-wertiger Input) kommen als Steuerparameter hinzu. Diese Parameter können als lesbarer Spec-String ausgegeben werden, und in einen Hash eingerechnet werden, der dann die Funktion markiert und unterscheidbar macht. Wenn ein Seed explizit angegeben wird, dann fließt er zusätzlich mit ein, und markiert außerdem auch die Berechnung an jedem Datenpunkt

...später, wenn ich abschließend geklärt habe, wie Automation funktionieren soll. Derzeit denke ich, daß solche Funktions-Parameter in einem Tree-walk vor dem eigentlichen pull() der Invocation gesetzt werden, also über das Turnout-System. Speziell für die Test-Ontology könnte man hier auch eine Node-ID oder einen Node-Struktur-Hash einfließen lassen (wobei mit jetzt aber nicht klar ist, ob diese Idee für das Testen überhaupt von Nutzen ist)

Ich will also eine laterale Vermischung der Daten vermeiden; erst über die Prüfsumme des Ergebnis-Datenblocks werden die einzelnen Datenpunkte zusammengeführt; theoretisch wäre es dadurch sogar möglich, die Korruption einzelner Datenpunkte zu erkennen, indem man die intendierte Berechnungskette explizit gecodet nochmal ausführt und dann die Ergebnisdaten vergleicht.

Also einen String, der die Operation mit Parametrisierung und Seed darstellt. Aufbauend darauf läßt sich dann auch eine Node-Spec generieren

⟹ als nächstes muß aus aus den Manipulationen eine richtige Funktion gemacht werden

d.h. das EventLog liegt in einem Singleton, und wenn es da ist, dann hinterläßt jeder Funktionsaufruf einen Log-Eintrag, so daß sich der gesamte Berechnungsweg mit allen Aufruf-Reihenfolgen nachvollziehen und verifizieren läßt

...im Grunde bräuchte es diese dummy-Funktion jetzt gar nicht mehr, da nun viel flexiblere Signaturen akzeptiert werden; zu Beginn des Prototyping hatte ich ja noch eine recht spezielle Konvention mit einem std::array...

Ich lasse diese Dummy-Operation dennoch bestehen, vor allem, weil auch dazu passende Specs gegeben sind

Wunsch: abgekürzte Darstellung der Vorläufer

...darüber hab ich damals viel nachgedacht: eine generische Implementierung der Blatt-Iteration ist nicht so einfach zu realisieren, da man einem Element nicht anhand generischer Eigenschaften ansehen kann, ob es ein Blatt ist oder noch weiter expandiert werden kann. Ein Ausweg wäre, das Element versuchsweise zu expandieren und es selber nur zurückzuliefern, wenn die Expansion leer ist. Davon habe ich damals aber Abstand genommen, da es (a) erfordert, den Aufwand für die Expansion stets und für jedes Element zu leisten und (b) dann irgendwie eine Interaktion mit dem internen Stack stattfinden muß, und das dann auch noch rekursiv oder repretitiv  (und das wurde mir dann alles zu kompliziert)

...und deshalb können wir erst expandAll() machen, und dann die inneren Blattknoten einfach nachträglich wegfiltern, woduch automatisch weiter repetitiv konsumiert wird.

....aus Sicherheits-Gründen: der Container soll irgendwo „daneben“ in sicherer Storage liegen

Der Trick ist: die Funktion nimmt ein Template-Argument, das aber als Default den forward-deklarierten STL-Container hat. Scheint mit meinem GCC zu klappen .... ich bin da aber sehr skeptisch, denn die Signatur mit den Template-Parametern könnte sich in Zukunft schon noch erweitern.....

auch da wollte ich einen pervasiven #include <map> vermeiden. Dann kam aber ein Defekt-Report von einem Clang-User auf BSD (und zwar der neueste Clang). Dort war der neue C++17 polymorphic-Allocator anders deklariert, so daß es zu einer ambiguity gekommen ist.

...da werde ich wohl noch darüber nachdenken müssen ... aber im Moment erscheint mir diese Frage unmittelbar auswegslos eindeutig: der Port ist die einzige Stelle, an der eine minimalistische, rigide Graph-Struktur virtualisiert verbunden ist mit einem unendlichen, amorphen offenen Raum der Möglichkeiten. Andererseits werde ich niemals mehr als eine opaque ID über dieses virtuelle Portal geben dürfen, denn sonst dringen Strukuren der semantischen Attributierung in das Graph-Modell ein, anstatt dieses nur zu markieren.

das ist und bleibt eine Frage der Ontology

insofern ist es nur ein Tag

jedes konkrete Projekt hat seine eigene kohärente Domain Ontology

Das folgt unter den Annahme, daß ein konkretes Projekt funktionieren muß. Also macht man nur Dinge, die kohärent aufgehen. Für Zweideutigkeiten findet man eine praktische Konvention, oder man weiß sie unsichtbar zu machen. Daher sollte dieses Problem für jedes konkrete Projekt in dem Sinn »lösbar« sein, daß die Cache-Steuerung fehlerfrei funktioniert. Der Beitrag der Lumiera-Infrastruktur dazu ist, für die notwendige Über-Differenzierung zu sorgen, so daß es niemals zu einer Kollision kommen kann zwischen Beiträgen, welche in disjunkten Libraries und Domain-Ontologies verankert sind.

...und zwar bewußt;

zunächst wollte ich ja einen Routing-Descriptor, um damit nochemal eine zweite Indirektion / Flexibilisierung gegenüber dem InvocationAdapter zu machen. Dann hat sich aber die Bedeutung des Invocation-Adapters so verschoben, daß ein Teil seiner Rolle mit der FeedManifold verschmolzen ist, dafür aber ein rigideres Belegungsschema dem Binding-Functor abverlangt wird. Damit wurde explizite oder schematische Routing-Info eigentlich irrelevant, und ich habe sie durch direkte Referenzen ersetzt. Und das ist nun das Problem.

Problem dabei: wer kennt den kokreten Typ,

um mit einer solchen Spezialisierung zu binden?

Lösung: per Registrierung

...wegen Template bloat. Umso schlimmer, wenn diese Funktionen dann fast nur von Unit-Tests genutzt werden. Aber selbst eine einzige zusätzliche Funktion für einen Hash oder konkreten Descriptor würde ich gerne vermeiden wollen, denn man generiert zwangsläufig eine große Menge redundanten codes, der vermutlich niemals aufgerufen wird (was man aber nie wissen kann...)

Man würde also ein System von Hash-IDs aufbauen, und dann würde jede Instanz des Turnout-Template bei der Generierung noch entsprechende Handler für die Diagnostik-Funktionen regisrieren. Allerdings nur, wenn die Funktion nicht bereits existiert. Dieser Ansatz würde darauf spekulieren, daß die meisten Diagnostik-Funktionen gar nicht anhand der konkreten Buffer- und Parameter-Typen differenziert sind. Beispielswese Funktionen, die auf Vorgänger-Ports zugreifen, müssen nur die Beschränkung auf die Anzahl der Vorgänger beachten. Aus der Processing-Spec, welche ja exakt den ausreichenden Differenzierungsgrad haben muß (sonst funktioniert das Caching nicht richtig) ließe sich ein Sub-Key für bestimmte Klassen von Diagnostik-Zugriffen gewinnen. Dieser würde dann ein Diagnostic-Objekt aus einer Hashtable holen (was dadurch dedupliziert wäre).

...das wäre durchaus denkbar für »operational control«, wenn es um spezielle Instrumentierung geht, also detailierte Zeitmessungen aufgezeichnet werden müssen, die sich nicht über einen einzigen Kamm scheren lassen

...aber immerhin habe ich mich damals so nobel geschlagen, daß ich daraus noch eine generische Library-Funktion gemacht habe, was mir jetzt zugute kommen könnte

Zugriff braucht nur die Differenzierung nach Weaving-Pattern, aber keine konkreten Typ-Signaturen. Denn das WeavingPattern legt fest, ob und wo die Sequenz der Vorgänger-Ports zugänglich ist. Das Layout von lib::Several wurde unabhängig von der konkreten Typisierung gehalten — aus ähnlichen Motiven. Selbst der Storage-Header beginnt mit einem einheitlichen Layout, und der size-Parameter liegt ganz vorne. Es erscheint sinnvoll, für diesen Zugriff eine ungetypte neue Library-Funktion bereitzustellen.

Und das muß auch so sein, denn sonnst wäre das kein freier Extension-Point. Jede Abweichung davon bedeutet effektiv eine Einschränkung der Implementierung — oder anders ausgedrückt, eine Erweiterung des Port-Interfaces. Das wäre durchaus denkbar; hier geht es allerdings um den sekundären Belang, wie man eine solche Erweiterung mit möglichst geringen Kosten realisiert

Die offensichtliche Lösung ohne Überraschungen: wenn das API tatsächlich breiter ist, sollte man es breiter machen — und zwar auf dem üblichen und etablierten Weg

Vorschlag wäre eine Klasse PortConnectivity, und eine Alternative als ProxyPort. Davon könnten dann die beiden (derzeit definierten) generischen Weaving-Patterns erben. Dadurch wäre dann eine uniforme Implementierung für gewisse Verhaltensmuster möglich, ohne für jede Detail-Typisierung erneut Code generieren zu müssen

Das wäre dann ein zusätzliches Differenzierungskriterium oder ein Qualifier; mutmaßlich zu realisieren als extended attributes, d.h. in der normalen Port-Spec unsichtbar oder nur durch einen kompakten Dekorator ausgewiesen. Ein solches zusätzliches Feature wird sehr wahrscheinlich notwendig sein, um eine hinreichend präzise differenzierte Hash-ID zu erzeugen, die auch konkrete Parameter-Werte mit einschließt, sofern diese einen Einfluß auf das Ergebnis und damit auf die Cache-Keys haben könnten.

Jede Subklasse mit nicht-trivialem Verhalten (also effektiv jede) muß diese virtuelle Funktion implementieren; mit jeder Template-Spezialisierung wird erneut Code »abgeworfen«, um einen Funktionspointer darauf dann in die VTable aufnehmen zu können. Theoretisch könnte ein cleverer Compiler hier eine de-Duplikation machen, es ist mir aber kein Standard bekannt, der so etwas auch nur nahelegt (wobei — es ist naheliegend, da Template-Bloat ein bekanntes Problem von generischem modernen C++ ist, und trotzdem für einen durchschnittlichen Entwickler schwer zu vermitteln)

Egal ob der Compiler de-Duplizieren könnte — wir machen es einfach explizit: Die jeweilige Basisklasse dürfte dann aber keine Abhängikgeit von Template-Parametern haben ⟹ für die beiden bisher definierten Fälle wäre komplette de-Duplikation so realisierbar. Der Preis dafür wäre aber leider eine deutliche Verschlechterung der Lesbarkeit im Weaving-Pattern selber; nicht nur daß man nun eine Basisklasse „im Kopf haben“ müßte, zudem wären dann alle Zugriffe auf diese Basis-Parameter mit dem self-Typ im Code zu qualifizieren. Deswegen habe ich eine starke Abneigung gegenüber dieser Lösung. Es ist der typische Pragmatismus, der den Code für alle nachfolgende Wartung und Erweiterung versaut....

Die Kosten werden hierdurch in die Laufzeit verschoben, genauer gesagt, aus dem Code-Segment in die Allokation für Nodes. Es ist weiterhin Compile-Time-Logik erforderlich, um für den Standard-Fall ein entsprechendes Tag in den extended attributes zu hinterlassen.

Zunächst einmal erzeugt diese Lösung einen erheblichen run-time-Storage-Overhead. Die dynamische Codierung ersetzt einen einzelnen Pointer in der VTable jeweils durch eine String-Spec mit vmtl. mehr als 8 char Länge, sowie dazu noch einen Eintrag in einer sekundären Dispatcher-Table, bestehend aus einer Hash-ID (size_t) plus ... genau einem Function-Pointer. So gesehen, erst mal ziemlich dämlich. ABER es gibt nun einen zusätzlichen Hebel durch de-Duplikation: zum Einen deduplizieren wir die Spec-Strings selber; was aber noch wichtiger ist, da die Detail-Operationen nur auf Teile der Spec-Strings differenzieren, wird es deutlich weniger Einträge in der sekundären Dispatcher-Table geben. Eine String-View kostet zwei «slot» — also wäre bei mehr als zwei Zusatz-Operationen der Overhead in der ProcID selber bereits reingeholt. Allerdings gibt es noch viel zusätzliche String-Storage, und die Dispatcher-Table-Einträge. Ob sich diese amortisieren, hängt von der Differenzierung in der Praxis ab. Entsprechend gibt es auch noch den Trade-off, ob man in der ProcID weniger oder mehr Teil-Komponenten des Spec-String separat speichert

...und das Bauchgefühl neigt Richtung erhebliches Potential, sobald die Projekte groß und komplex werden. Für einfache Projekte, schätze ich, wird diese Lösung verschwenderisch, aber sobald wir eine fünf- bis sechstellige Anzahl an Render Nodes haben, wird das Einsparpotential gewaltig. So viele verschiedene Typen kann man in einem Projekt gar nicht haben, als daß es nicht zu einer erheblichen Reduktion der Strings und noch mehr der Dispatcher führen würde...

...wenn es allerdings nur darum ginge, (also um die Cache-keys), könnte man genauso gut eine HashID direkt in die ProcID legen (die ja ohnehin embedded im Port liegt). Ich spekuliere also schon explizit auf das Einspar-Potential durch zusätzliche Operationen...

Aktuell geht es nur um Zusatz-Operationen, die rein für das Testing gebraucht werden; deshalb sollte auch über einen Lazy-Init-Ansatz nochmal nachgedacht werden. Längerfristig kommen Aufgaben für Instrumentierung und Job-Planung hinzu. Diese laufen nicht in der innersten, hoch-performanten Zone. Man sollte das Thema trotzdem im Auge behalten; möglicherweise kann man Ergebniswerte für einen ganzen Render-Chain in der Exit-Node als Abkürzung speichern, und so die re-Traversierung einsparen.

Es geht hier zunächst darum, die eigentliche Rechen-Funktionalität bereitzustellen. Erst in weiteren Schritten werde ich dann daraus eine Test-Manipulations-Umgebung aufmachen, die diese Funktionalität in sinnvolle Operationen und Verknüfpungen verpackt. Was im Moment die Parameter für TestFrame sind, könnte mal die Emulation eines StreamType-ImplType werden

Stelle eine Hash-Kette her, die jeweils „quer“ über die Datenpunkte in benachbarten Frames läuft. Ein »param«-Wert dient als Seed und könnte später vom Node-Hash genommen werden. Dann wird der jeweilige original-Datenwert eingehasht und durch das Ergebnis der Kette ersetzt.

herausfinden wie ein API für Tests sinnvollerweise aufgebaut werden kann...

Hab mich in den letzten Tagen wieder in einem Knoten festgefahren — der nun zum Glück wenigstens in meinem Kopf schon gelöst ist....  Trotzdem ist die Situation sehr schwierig, da ich mehrere »intuitiv geklärte« Sachverhalte gleichzeitig aufbauen muß, und nicht recht weiß, wo beginnen....

läuft darauf hinaus,

nun den Umbau anzugehen

NodeBuilder<POL,DAT>

in der friend-Deklaration, sowie im Argument des intendierten Konstruktors (wenigstens bin ich konsequent...)

...denn die einzelnen Klauseln werden nur instantiiert wenn angesprochen. In einem bisher nie aktivierten Codepfad können ganz banale Fehler lange überdauern ...

der Test muß zumindest soweit gehen,

alle Aspekte der Builder-Syntax auszuleuchten

Das ist ein realistisches Beispiel:

  • zwei Clips
  • der erste Quell-Clip ist noch mit einem Effekt belegt
  • allerdings ist der erste Clip auch nur in zwei Ports aufbereitet (warum auch immer)
  • der Mix soll jedoch für drei Ports bereitgestellt werden (warum auch immer — beispielsweise könnten die Unterschiede nur im 2.Chain tatsächlich bestehen, weil dieser ein anderes Farbformat hat)

das ist inhaltlich falsch!

⟹ jeder Lead erscheint als neu

  • also ohne Namespace
  • dafür aber mit Qualifier
  • ohne Argumentlisten

die Proc-ID ist hier nur der Implementierungs-Ort, weil es auf der Node selber gar keine entsprechende Einrichtung gibt — der Node-Name ist in die Proc-ID aufgedoppelt, um auch für einen Port noch eine sinnvolle Diagnostik zu bieten; diese Redundanz nehme ich in Kauf (ist jeweils ein Pointer); im Gegenzug verzichte ich aber auf eine separate Node-ID, indem die Proc-ID hilfsweise auch die Node-Spec mit implemenitert

müßte doch im Prinzip bereits alles funktionieren....

brauche also nur noch ein BuffHandle hier

das würde mir eigentlich gefallen ⟹ packe ich diesen Schritt JETZT?

die müssen dann auch ins default Turnout-System

im Scheduler ist nur noch ein freier Slot übrig

Es gibt N Timelines. Für jede und für jedes Segment habe ich eine Exit-Node.

⟹ Also ~ 2000 pro Timeline

Das multipliziert nochmal mit ~20 Ports (Video, Audio, R,G,B,α, Full-Screen, + Probe-Point(s), Audio-Subgruppe(n)...)

mit 8 Byte pro Pointer sind das ~ 1.5 MiB

...und zwar wegen den Subgruppen und Probe-Points, die eben keinesfalls beschränkt werden dürfen

...wegen der Concurrency, außerdem müßte dann dort ja auch wieder das cartesische Produkt gespeichert werden, denn es sind ja grade alle möglichen Ports möglich

das was alle „vernünftigen Leute“ sowiso machen — allerdings tun sie das nicht aus Vernunft, sondern aus Gedankenlosigkeit, weshalb das kein weiter relevanter Einwand ist

Also einen Post-Processing-Schritt, der die gleiche Ausführungs-Logik effizienter im Speicher codiert

  • Flyweight-Pattern zur Deduplikation anwenden
  • VTables o.ä. wo möglich durch Funktionspointer ersetzen

Das könnte ggfs. sogar eine automatische Transformation sein, auf Basis des jetzt definierten Node-Modells; man würde dann eine DAG ⟼ Tree -Transformation machen und dann die jeweilge ausführbare Node über ein Lambda-Binding über eine Prototyp-Node erzeugen.

alles andere würde eine Art Kollaboration oder Protokoll implizieren

Ich hatte schon angefangen, über der mögilchen Implementierung zu »brüten« und mich wieder in BufferProvider + OutputSlot eingelesen. Erst nach etwa einer Stunde ist mir aufgefallen, daß OutputSlot ja eine DataSink erzeugt, und daß diese bereits eine lockBufferFor(FrameID)-Funktion hat, die ein (TADAA!) BuffHandle liefert. Nicht wirklich überraschend, da ich ja beide Protokolle (Buffer Provider und Output Slot) kurze Zeit nacheinander entworfen habe. Daher konnte ich wohl damals auch einen Proof-of-Concept-Test ziemlich einfach „aus dem Ärmel schütteln“...

Heute aber kommt mir dieser ganze Zusammenhang ziemlich »anrüchig« vor. Ich war inzwischen x-mal in der Analyse in die Falle gegangen, den BufferProvider mit dem Output-Management zu verwechseln — und dann immer wieder festgestellt, daß beide auf komplett andere Architektur-Ebenen gehören....

Deshalb möchte ich nun doch einmal aus-implementieren, was denn erforderlich wäre, einen frei-stehenden Buffer-Provider neu zu implementieren, welcher an einen dahinter liegenden OutputSlot delegiert....

weil die tatsächlich zu unterstützenden Operationen die BufferMetadata brauchen

Konsequenz ⟹ was der User bekommt, ist kein BufferProvider

...und zwar schon längere Zeit bezüglich der Implementierung  des BufferProvider — das Konzept halte ich für sehr wichtig und auch gelngen. Aber immer wieder, wenn ich dann die Implementierung anschaue, dann springt man irgendwo zwischen dieser Default-Implementierung und dem TrackingHeapBlockProvider hin und her — und der ganze Code „riecht schief“, irgendwie (weiß aber nicht warum) ... Hinzu kommen Probleme (und, wie ich inzwischen weiß, tatsächliche Bugs) in der Typ-Registrierung, also BufferMetadata

...es sind ja nur vier abstrakte Methoden zu implementieren, die die tatsächlichen Zustandsübergänge markieren

die Idee war damals wohl, daß die BufferProvider-Basis-Impl einen Sicherheits-Layer  bietet

...will sagen, die Implementierung muß sich bloß noch um ihr eigenes Memory-Management kümmern, aber nicht mehr darum, Typen und Lifecycle-Phasen zu tracken. Das erscheint mir nun durchaus ein plausibler Ansatz zu sein (hätte man bloß besser dokumentieren sollen)

und das Ergebnis, das BuffHandle "ist es dann auch"

allerdings war das Design nie etwas anderes als vorläufig

...denn in all honesty, die Prämisse war und ist, daß man auf dieser Basis einen echten BufferProvider implementieren können dürfen sollte

NodeBuilder_test::build_connectedNodes()

da hat das aber bereits funktioniert...

ich wollte einen schnellen Test coden und hab daher immer wieder den gleichen Output-Buffer reingegeben; das ist so nicht erlaubt, denn der erste Aufruf hat diesen in den EMITTED-State gebracht.

Leit-Aufrabe für die »Test-Ontology«

Generator-Funktion bauen ⟸

dann würd ich's halt nicht machen.....

Jetzt mal ehrlich: auch eine Library kann korrumpierten internen State haben — und man sorgt eben dafür, daß das möglichst nicht passieren kann, indem man defensiv vorgeht.

Schlußfolgerung ⟹ alle spezielle Test-Konfiguration gehört dann eben in Spec-Instanzen

welches jedoch stimmig sein muß

ergibt hier keinen Sinn — berechnet wird eine einfache lineare Interpolation, zwar ohne Beschränkung des Wertebereichs, jedoch sind aus Sicht des Aufrufers nur Werte [0.0 ... 1.0] plausibel

...damit wird der Binding-Funktor ein Einzeiler

...da std::array auch das Tuple-Protocol unterstützt; und der Fall mit einem direkt gegebenen Einzel-Argument wird über eine Hilfsfunktion integriert

...denn in der Policy steckt die Vorgschichte des jeweiligen Build-Vorgangs — und auch der Allokator!

die DomainOntology muß jeweils nur einige Aspekte beitragen

  • der Node-Name
  • Funktion und Spec für einen Port

...dagegen andere Aspekte müssen frei bleiben

  • Verdrahtung der Vorgänger-Nodes für diesen Port
  • ggs andere Ports

...vor allem für Src-Nodes könnte die TestRand-Ontology gleich eine fertige ProcNode liefern....

Problem damit

  • dann muß TestRand etwas über ProcNode wissen

wie viele Fälle sind das jetzt tatsächlich

...müßte dazu viel mehr über den Builder wissen....

essentielle Aufgabe: Parameter in die Berechungsfunktion einspeisen

Also explizit für Kern-Funktionalität; das wäre ein naheliegender Lösungsansatz, der sich gewissermaßen unter der Struktur der Feed-Manifold »durchgräbt«. Hierzu würde man spezielle Buffer vereinbaren, in denen ein Adapter-Typ liegt, der dann irgendwie mit den Parametern versorgt wird.

Besonders fragwürdig ist die hohe Komplexität, und auch die Indirektion, die über mehrere Level des Builders hinweg durchgereicht werden muß

elegant, weil nichts gemacht wird an einer Stelle an der ohnehin nichts zu tun ist — denn die eigentliche Logik liegt auf Level-3

effizient, denn Buffer-Speicher wird gepoolt und damit gute Chancen auf Cache-Locality

die bisher bedachten Strukturen sind auf die Datenströme ausgerichtet — es wäre ungeschickt, hier explizit etwas zur Parameterversortung einzurichten; vielmehr kann der Apekt der Berechnungs-Verknüpfung hier mit abgebildet werden, aber die eigentliche Ansteuerung muß von der Invocation ausgehen, und kann daher nur durch das Turnout-System laufen, welches hierdurch seinen bisher nur abstrakt gefaßten Sinn bekommt.

Ein Teil der Parameter-Berechnung ist wohl spezifisch für den Einzelfall und gehört damit sinnigerweise in eine Node; diese wird aber speziell aufgebaut und direkt mit dem Turnout-System verbunden; letzteres wurde durch diese Überlegungen nun konkretisiert und wird verstanden als Mediator für den Austausch von Parameter-Daten; die ParamAgent-Node stellt damit die Brücke dar ⟹ folglich muß eine Test-Spec auch die Möglichkeit mit einschließen, solche ParamAgent-Nodes zu erzeugen

und ehrlich gesagt, ziemlich heftig — nach dem Motto »jetzt oder nie« habe ich effektiv die C-Arrays und void* über Bord geworfen und die gesamte Invocation-Storage auf typisierte Daten und vor allem Tupel  aufgebaut. Der Level an Metaprogramming ist nun zwar viel konzentrierter und tief in der Implementierung versteckt, geht aber an Heftigkeit noch weit über den allerersten Entwurf zur Render-Engine (von 2009) hinaus. Auch die Loki-Typlisten sind wieder mit dabei...

da drücke ich mich schon seit Jahren drum

»geliefert« ist das Wort das diese Debatte klärt

Hier im Rahmen der Render-Engine wird nach einem einheitlichen Webemuster vorgegangen: die Berechnung erfolgt lazy und schreitet in Wellen von der Quelle in Richtung des Resultats fort. Und, ganz wichtig, die Berechnungen sind hochgradig concurrent. Deshalb muß jedweder intemediäre Berechnungszustand externalisiert werden — wir brauchen Storage, die in Buffern organisiert ist und jweils für eine Node-Invocation bereitgestellt wird. Daher muß ein Berechnungsergebnis stets irgendwo abgestellt werden — und das heißt, es fällt als Wert an.

als Funktion der nominal Time

Es ist eine ungeklärte Frage, ob Abkürzungen in der Render-Engine sinnvoll sind. Diese Frage kann nur empirisch geklärt werden, und vermutlich niemals abschließend. Erfahrung im high-Performance-Computing zeigt, daß Schematisierung oft der Einzelfallbehandlung überlegen ist — es sei denn, der Einzelfalls stellt selbst ein Schema dar

Automation ist eine Domain-Ontology

nenne sie »Special Agent«

DataAgent: Übergabe von Daten aus einem anderen Job

ParamAgent: Einspielen von Steuerparametern

CacheAgent: Cache-Verwaltung

ControlAgent: Instrumentierung

Verbindung zwischen Invocation

und

»ParamAgent«-Nodes

und zwar, weil eine Referenz in den weave()-Aufruf gegeben wird, und dieser — dem WeavingPattern gemäß — an andere Nodes weitergereicht wird; insofern hier ein nested Scope im Callgraphen entsteht, kann auch ein adaptiertes Objekt erzeugt und überlagert werden

es bleibt zu klären: was stellt das Turnout-System dar?

das war mal klar ....

 und dann hat sich diese Vorstellung im Prototyping verflüchtigt

Mir schwebte ein verwobenes Wechselspiel vor: der Turnout schafft eine Sprosse des Turnout-Systems, und diese wiederum führt diesen Turout aus. Dann habe ich versucht, das in konkretes Speicher-Layout zu übersetzen und das uferte zunächst aus, bis ich alles auf den Stack gelegt habe, womit es aber mehrere Ebenen weit tiefer gerutscht ist, in die konkrete Implementierung des Aufrufs

Fahrweg — Weichenstraße — konkrete Spurführung

Denn die Render-Engine ist — genauso wie ein Schienenweg — ein vermittelndes Gebilde und nicht die Sache selber (genausowenig wie der Zug oder die Lokomotive ist das nicht, wenn sie auf dem Abstellgleis stehen).

Seitens der Nodes ist mir das wohl ganz gut gelungen, aber es besteht die Gefahr, sich letztlich doch noch irgendwo auf ein Über-System festzulegen; daher sollte auch auf der Seite der Kontrolle und Steuerung ein Erweiterungspunkt vorgesehen werden

Das ist hochgradig relevant, weil auf diesem Weg jetzt etwas gebaut werden kann, ohne die Gefahr von Architektur-Fehlern

die würde sich der SpecialAgent dann vom generischen TurnoutSystem holen um dann in einem speziellen Service einen hinterlegten Kontext aufzugreifen

das widerspricht jedoch dem Erkenntnisbild von Fahrweg ⟶ Weichenstraße ⟶ Spurführung

Festlegung: genau ein virtual call pro Node

Aber der Visitor von »damals« erscheint mir...

  • immer noch unrund
  • zu generisch und auf Hierarchien fixiert
  • viel zu indirekt für den Effizienz-Maßstab in der Render-Engine (wäre akzeptabel im Builder)
  • viel zu systemisch bezogen auf den hier essentiellen Grad der Offenheit

VTable-Träger ist der »opaque Gegenstand«

und zwar muß noch festgelegt werden, auf welche Art Parameter zugegriffen wird, und wo; das könnte allerdings Teil eines Parameter-Berechnungsfunktors sein, der dann ein TurnoutSystem& als Argument nimmt — damit wäre die Prekonfiguration auf einem vergleichbaren Level wie für die Medienberechnungs-Nodes

Unabhängig davon ob lediglich ein Basis-Parameter zugegriffen wird, oder ob ein vorher explizit berechneter Parameterwerd von einer ParamAgent-Node abgeholt wird: es ist eine Indirektion notwendig, um die die Konkrete Daten_Adresse zu bekommen, denn diese ist i.d.R. erst zum Zeitpunkt der Invocation feststellbar

Paramter werden in ParamAgent-Nodes berechnet, welche über den normalen Builder eingehängt werden — und zwar nur bei Bedarf. Sofern also spezielle Parameter-Berechnung notwendig ist, wird dies in der Belegung und Verschaltung der Nodes prekonfiguriert, so daß die eigentliche Invocation davon nichts wissen muß.

Obzwar weitgehende Flexibilität besteht, soll im Regelfall die weitergehende Parameter-Berechnung in einer speziellen Parameter-Aufbereitungs-Node gebündelt werden; diese ist als erster Lead unter der Exit-Node eingehängt und wird somit als erste aktiviert. Die Berechnungsfunktion in dieser Node bekommt eine Referenz auf das TurnoutSystem, und kann somit dort per Seiteneffekt zusätzliche Daten-Module registrieren. Als Storage für die zusätzlichen Datenmodule dient der Ausgabepuffer dieser Aufbereitungs-Node, welcher — gemäß allgemeinem Auswertungsschema — garantiert bis zum Ende der Render-Invocation im Speicher bestehen bleibt.

Das Turnout-System erlaubt es, einzelne Datenmodule zu registrieren und später über diese Registrierung auch wieder (mit integriertem Cast) abzugreifen. In der Grundausstattung bietet das Turnout-System nur Zugriff auf die Invocation-Koordinaten (vor allem: die absolute nominal Time). In die ParamAgentNodes (welche letztlich einen konkreten Parameter für eine nachfolgend aufgeschaltete Berechnungs-Node bereitstellen) wird ein konkret abgeschlossenes Zugriffs-λ gebunden, welches das TurnoutSystem als Referenz bekommt, und dann aber eine Template-Funktion für den konkreten Datenzugriff aufruft. An dieser Stelle finden keine Verifikationen mehr statt, aber das Turnout-System speichert die Indirektion auf den konkreten Datenpuffer

...denn wir haben nun (nach einem heftigen Umbau) direkt eine typisierte, strukturierte Storage in der FeedManifold geschaffen

Denn das Konzept der »AgentNode« ist ja generisch, und nicht aus der Welt zu schaffen — hängt es doch an der Wurzel zusammen mit dem Ansatz einer vereinfachenden Abstraktion in einen Node-Graphen, die dann das Einlassen komplexerer Zusammenhänge in die lokale Struktur innerhalb dieses Graphen impliziert, in Form von Bau- und Verschaltungsmustern...

Für die Parameter-Behandlung sind komplexe Spezialfälle vorhersehbar, welche die Pasis-Parameter, den Kontext-Zugriff über einen Prozeß-Key, sowie weitere, kontextuell zugängliche Zustandsdaten in einen erweiterten Satz konkreter Steuerparameter synthetisieren, um diese dann an anderer Stelle, rekursiv tiefer im Invocation-Tree, zur Parametrisierung der Medien-Berechnung heranzuziehen. Ein solches Muster ist in der Tat „speziell“ und ohne im Moment für mich erkennbaren konkreten Bezug; ich leite diese Möglichkeit lediglich aus allgemeinen Überlegungen zur Struktur her, und ziehe den Schluß, zumindest die Ansatzpunkte hierfür schaffen zu müssen....

...im Hinblick auf Reproduzierbarkeit sollte die Voraussetzung geschaffen werden, einen Testfall au einem einzigen Seed zu speisen (auch wenn das weitere Framework für derart reproduzierbare Testfälle noch nicht gegeben ist)

der Header sollte optional sein (und hinter den Daten liegen), aber die Gegenwart eines Headers sollte explizit erkennbar sein

bisher haben wir nur nach einem Standard generierte Serien von Zufallsdaten, das heißt, wir könnten eine Speicher-Korruption erkennen, oder eben daß diese Daten geändert wurden; was aber fehlt die die Möglichkeit, nach einer solchen Änderung den neuen Stand wieder durch eine Prüfsumme zu qualifizieren (wodurch man ein erwartetes Ergebnis feststellen könnte)

Im Datenblock liegt lediglich eine Sequenz von 1014 Bytes — man möchte darauf jedoch flexibel aber getypt zugreifen können

TrackingHeapBlockProvider_test

BufferProviderProtocol_test

OutputSlotProtocol_test

0000000515: INFO: testframe.cpp:168: thread_1: getFrame: Growing channel #0 of test frames 0 -> 1 elements.

0000000516: CHECK: buffer-provider-protocol-test.cpp:107: thread_1: verifySimpleUsage: (testData(0) == checker.accessMemory (0))

  • könnte daran liegen, daß ich für testData die Argumente verkehrt herum verdrahtet hatte; da alle drei Tests direkt mit testData arbeiten und selber etwas in den Speicher legen, muß sich dort diese Verdrehung auch irgendwo im Code niedergeschlagen haben. Ist natürlich jetzt nicht einfach zu finden, ohne diese Custom_Allocatoren zu verstehen....
  • es könnte aber auch am anderen Memory-Layout liegen....
  • oder am zusätzlichen Header-Check, der  nun stattfindet

...da war doch irgendwas mit der Sequenz-Nr und Channel-Nr, die habe ich entweder mit anderen Aufrufen harmonisiert oder die Anordnung im Cache verändert; wenn ich mich bloß an die Details erinnern könnte

Puh...

...endlos mit dem Debugger beobachtet, sieht immer alles völlig sauber aus; konnte schließlich belegen daß es nicht die de-duplzierten Strings selber sind ⟹ das hat mich dann auf die richtige Fährte gebracht: es muß einer der dazwischen liegenden String-Views sein, der in transientem Speicher liegt — und tatsächlich: in den Builder-λ erzeugen wir eine Proc-ID per Value, binden sie dann aber per Referenz in den neuen Port....

...was auch immer das heißt — es gibt nämlich dafür noch kein Konzept; nur die Vorstellung, daß dafür irgendwo ein Schieberegler existiert

demnach sollte auch die Param-Node zum Einsatz kommen

AUA!‼ Hammer auf den letzten Metern

...wie so oft:

Du sagst Dir, anstandshalber sollte es noch einen kompletten Integrationstest geben, hast noch ein schlechtes Gewissen, daß Du Dich „verspielst“ — und dann das: das sorgfälig schrittweise aufgebaute Design kann ein ganz und gar grundlegendes und sehr geläufiges Problem nicht handhaben!

Was mach ich jetzt bloß ... der NodeBuilder ist doch schon so komplex, daß ich ihn selber kaum noch stemmen kann..... Muß ich jetzt die grundelgende Systematik der Builder-DSL über den Haufen werfen und alles nochmal von Grund auf implementieren....???

definitiv nicht des Parameter-Funktors, denn letzterer empfängt immer das Turnout-System, kann also sowiso beliebige Anpassungen vornehmen; was aber gebraucht wird ist eine Processing-function, die andere (angepaßte) Parameter nimmt

das sollte über die bestehenden cross-Builder-Techniken lösbar sein (seufz)

das geht, weil durch das constexpr-if die Funktion jedesmal erneut instantiiert wird, mit dann jeweils anderem deduziertem auto-Returntyp

...denn im Grunde sind alle diese Bindings mit Lambdas relativ banal zu formulieren — es ist halt bloß relativ technisch, versaut den Code und erfordert vom Benutzer viel Präzision und Beachten von feinen Punkten. Da »der Benutzer« auf absehbare Zeit ich selbst bin, ist das — Määh. Ja ich kann das. Wääh

Das klingt nach einer Lösung, die billig zu haben wäre. Es sind ein paar Zeilen recht technischer Code im Node-Builder, der ohnehin in diesem Bereich „nicht mehr schön“ ist. Kann man also in jedem Fall mal einbauen — mein einziger Zweifel betrifft die Nützlichkeit und Relevanz; warum man ausgerechnet ein einziges erstes Argument schließen wollte, will sich mir nicht recht erschließen

Das ist pre-C++11-Code, nur oberflächlich modernisiert, und bisher nur in einem Bereich testhalber verwendet (für Proc-Commands). Grundsätzlich möchte ich diesen Code aber erhalten, nicht loswerden. Risiko: es gibt wohl einige »Untiefen« — es ist nicht klar, wo Heap-Allokationen stattfinden, und auch der Umgang mit non-copyable oder move-only-Typen ist nicht wirklich ausgeleuchtet. Wahrscheinlich deshalb habe ich vor wenigen Jahren die beiden convenience-Wrapper vom alten Implementierungs-Framework abgekoppelt und direkt per Lambda implementiert — und dann seither doch nicht mehr benutzt (bestätigt die Zweifel bezüglich partieller Applikation nur eines Arguments, das ist dann doch ehr so ein Haskell-Ding)

daß wir jetzt eine std::function erzeugen ist hier ganz furchbar

ein Schritt vor,

zwei Schritte zurück...

wenn mehr als ein Parameter closed wird ⟹ Heap Storage

die Arität muß ermittelt werden,

denn std::make_tuple ist ein Template

...weil wir den Processing-Functor jeweils aus dem Prototypen kopieren, dann aber in der Anwendung des Binders nochmal das gespeicherte Element ins Ergebnis kopieren

zwar könnte man auf die Idee kommen, den Adapter hinter den Parameter-Functor zu setzen. Das läßt sich aber in dem hier gewählten cross-Builder-API nicht realisieren, da der Parameter-Functor sofort auf Kompatibilität mit dem Parameter-Tuple-Typ geprüft wird, und obendrein auch sofort dahingehend festglegt wird, daß er ein TurnoutSystem& als Argument bekommt. Ein schrittweises Aufbauen einer Funktor-Kette ist also nur beim Processing-Functor möglich. Es sei denn, man würde eine wesentlich elaboriertere Darstellung finden können, bei der ein Gesamt-Functor-Typ für die Parameter irgendwo in den Template-Argumenten aufgebaut wird.

...da wir durch das Anwenden auf einen umgebauten Processing-Functor schwenken; deshalb kann auf diesen sodann erneut eine Adaptierung angewendet werden — selbst wenn diese dann schließlich das Parameter-Tupel complett schließt (denn ohne Parameter-Functor erfolgt der Aufruf implizit mit einem default-konstruierbaren Parameter-Tupel, und das leere Tupel ist uneingeschräntk default-konstruierbar)

RebindVariadic geht nur für Klassen-Templates

muß eingangsseitig Einzelwert oder Aggregatwert entgegennehmen

...denn wir müssen den generierten Binder in diesem Funktor speichern

wenngleich er auch eine std::function liefert — das würde ich temporär akzeptieren, bis die function-closure-Tools umgebaut sind (⟹ Ticket!)

das heißt, doch auch die Builder-Funktion selber schreiben — vermutlich eingebettet in ein Builder-Konfigurator-Template

d.h. man kann in der Praxis direkt mit einem Wert aufrufen, dann wird der eben implizit in ein 1-Tupel konvertiert

Das war vielleicht der größte Brocken, und ich hab mich da seit einigen Jahren nicht rangetraut, weil ich eine tagelange »Big Bang«-Aktion fürchtete. Nun mußte ich mir aber für dieses Thema die function-closure-Utils genauer anschauen, und hab verstanden, wie die Definitionen zusammenhängen. Hinzu kommt, daß inzwischen schon ein gewisses Kern-Ökosystem steht, das gleichermaßen mit den variadischen Sequenzen umgehen kann. Das hat mich auf die Idee gebracht, das Thema mit kreuzweisen Brücken zu entschärfen — bei genauerer Betrachtung zeigt sich nämlich, daß ein erheblicher Teil der eigentlichen Manipulations-Funktionen nicht explizit auf NullType angewiesen ist, sondern sich im Wesentlichen auf lib::meta::Prepend abstützt. Und da nun klar ist, daß in Zukunft einmal TySeq einfach die Rolle von Types übernehmen wird, per Umbenennung, ist es möglich, an vielen Stellen Spezialisierungen daneben zu stellen (markiert mit #987), die dann wieder über die richtige Brücke zurück führen. Habe nun gute Hoffnung, daß sich die explizit auf die alten Typlisten angewiesenen Verwendungen schritweise isolieren lassen

wie steht's mit perfect forwarding?

ohnehin limitiert: wir konstruieren stets Werte in den Binder

...das bedeutet, der ElmMapper greift auf Inhalte zu, so wie sie im ctor-Argument des TupleConstructors liegen. Aber, da das Tupel selber per L-Value-Referenz eingebunden ist, wird auch std::get nur eine LValue-Referenz herausgeben (selbst wenn im Tupel selber eine RValue-Referenz liegen würde...). Das wäre nur anders, wenn man das ganze Tupel als RValue-Referenz an std::get gäbe...

das ist hier sogar ein essentielles Feature

...denn nur auf diesem Weg hat der ElmMapper die komplette Freiheit und kann im Besonderen Werte selbst konstruieren, oder aber auch Werte mehrfach mappen. Denn wenn man das eingebundene Tupel als RValue an std::get geben würde, dann würde dieses eine RValue-Referenz von Value-Inhalten ziehen, und diese damit beim ersten Zugriff konsumieren.

Anmerkung (function-closure aufgeräumt, Juni 25):

  • habe jetzt in die alten Templates einen modernen Binder-Builder eingebaut
  • man kann jetzt Funktor und zu bindende Werte per Forwarding in den std::bind schieben
  • gebe jetzt eine Forwarding-Ref durch den TupleConstructor !

Das Ziel war, die ganzen Workarounds schrittweise zu reduzieren, damit letztlich alle Varianten wieder auf einen einzigen Implementierungspfad aufsetzen. Zwischenzeitlich waren nämlich bis zu drei verschiedene Implementierungslösungen aufgebaut worden.

Für den TupleConstructor war die wichtige Einsicht, daß eine Forwarding-Referenz nicht zwangsläufig eine RValue-Referenz ist; vielmehr ist sie flexibel, weil sie auf einem Template-Argument aufsetzt, das an die direkte Aufruf-Quelle andockt. Dieses Muster läßt sich in den ElmMapper hineintragen, da dieser die Quelle als Parameter SRC bekommt. Wurde die Quelle also initial als LValue-Referenz in den TupleConstructor gegeben, dann wird dieser Parameter ebenfalls zur Referenz; andernfalls wird er zur RValue-Referenz. Damit, und mit dem explizit gegebenen Zieltyp für jedes Element im Ergebnis-Tupel läßt sich dieser ElmMapper also komplett »fernsteuern«. Ob das aber dennoch eine Falle darstellt für unique-values muß sich erst in der Praxis zeigen (wenn denn überhaupt jemals ein so komplexer ElmMapper gebraucht wird, der einzelne Elemente dupliziert, und diese Elemente dann aber unique-ownership hätten....)

BuildFunSig

...was uns aber nix hilft, denn wir kommen gar nicht dorthin

in combineFrames() : out->markChecksum();

zunächst formal, aber habe das per Debugger bestätigt

aber —

warum funktioniert das dann

für Arbeit-Buffer in der Engine?

d.h. die source-Node alloziert hier einen Buffer vom generischen Provider

in einem Einzelfall von Hand nachgezählt...

Fazit: Weaving-Patterns

weder müssen wir nach derzeitigem Stand den Node-Datensatz anhand einer ID wiederfinden können, noch müssen wir von einer Node auf ihren Definitions-Kontext zurückschließen. Denn Nodes sind eine reine executiv-Repräsentation; sie werden durch Interpretation semantischer Attributierungen einmal erstellt, dann aber nur noch ausgeführt, und bei jeder Änderung verworfen und neu konstruiert

So manches Verfahren der symbolischen Repräsentation erscheint zunächst unglaublich elegant und auch effizient; und wenn man es dann in die Praxis übernimmt, und das Verfahren nicht funktioniert, steht man vor einem unlösbaren Rätsel. Also muß der Weg der Lösungsfindung mit aufgezeichnet werden. Und das zerstört die gesamte Eleganz und ist oft aufwendiger, als die eigentliche Lösungsfindung.

soll sich genau dann ändern, wenn das Ergebnis aus User-Sicht abweichen wird

Das ist ein wichtiger Neben-Nutzen; jedoch ist zu beachten, daß dieser die Haupt-Berechnung nicht belastet, denn auf dem kritischen Pfad ist dieser Fall niemals relevant. Er wird aber sicher sehr bedeutsam sein für lesbare Diagnose-Meldungen, und auch für die wenigen aber wesentlichen Testfälle, welche den Aufbau und Zugang zur Datenstruktur der Render-Engine absichern.

Es handelt sich auch um eine strategische Entscheidung, welche Fehler man routinemäßig zu identifizieren hat. Das System ist entworfen unter der Grundannahme, daß der Builder bereits jedwede Fehlkonfiguration  und Bereichsüberschreitung im Nutzen erfaßt und unterbunden hat, so daß während der Render-Operation nur noch (erwartbare) Zeitüberschreitungen und (unerwartete) Systemfehler und Entgleisungen in externen Libraries passieren können. Es wird deshalb in der Node-Invocation kein Fehlerausgang vorgesehen. Sollte einer der unerwarteten Fehlerfälle noch überhaupt faßbar sein (und nicht unmittelbar zum Absturz führen), so kann von überall eine Exception augeworfen werden; Ressourcen-Lecks schätze ich für diesen Fall als nicht relevant ein, da aller Invocation-State ohnehin auf dem Stack liegt und Buffer durch Timeouts geschützt sind.

Nun ist die Frage: will man in einer solchen Situation mehr leisten als »Fehler in Render-Engine«? Denn dann müßte Information über den direkten Invocation-Kontext mit aufgegriffen werden; und um diese Information überhaupt nutzen zu können, bedürfte es dann einer Rückübersetzung in Entitäten und Koordinaten des high-level-Models, um sinnvoll auf die Ursache schließen zu können. Alle mir bekannte Medien-Sofware leistet nicht diesen Grad der Fehlerdiagnose, vielmehr steht in einem solchen Fall der Benutzer allein da, und kann bestenfalls durch schrittweises Herantasten erraten, was das Problem verursacht hat. Insofern könnte man sich auf den Standpunkt stellen, daß Lumiera in dieser Hinsicht keine Probleme zu lösen hat, mit denen man in der Praxis auch anderweitig irgendwie überlebt...

ohne Differenzierungen auf der Implementierungs-Ebene durch die Ports

Beispiel: FFmpeg:gaussianBlur

das ist dann aber die NodeSpec

nicht das reine NodeSymbol

hier ist zu markieren, falls der Builder oder die Domain-Ontology konkret einen Entscheidungsspielraum in einem Fall so und in einem anderen Fall so nutzt, also z.B. eine spezielle Parametrisierung des Algorithmus, die vom Default abweicht. Wenn dagegen der Algorithmus selber sich inkompatibel geändert hat, so wird das bereits n der ID des Proc-Asset durch ein Postfix .v# markiert

denn in den allermeisten Fällen dürfte es entweder ein Feed sein, oder N gleiche Feeds

wäre nämlich zusätzlicher Coding-Aufwand — und dabei weitgehend redundant

Level-3 ist der Builder, der über die tatsächlich zu legenden Verbindungen entscheidet; dafür benötigt er noch die StreamType-Information, wohingegen Level-2 Verbindungen und Typinformationen ungeprüft anwendet. Daher muß zwangsläufig dieser gesamte Qualifier mit den Argumentslisten auf einem höheren Level im Builder etabliert werden.

...weil man es nicht erwarten kann, daß irgend ein Library-Plugin hier eine sinnvolle Systematik einführt

System schaffen

 nicht einfach ad hoc verdrahten

bedeutet, es braucht einen Zugriff auf die ID der Medien-Datenquelle, und diese wird zur Kennzeichnung des Eingangs-Feeds verwendet, und nicht rekursiv die vorläufer-Node-ID

zwar könnte man bereits im Builder festlegen, daß eine solche Komponente dazukommt; jedoch ist erst zum Zeitpunkt der konkreten Invocation klar, welche dynamischen Parameterwerte sich ergeben und noch in die Medien-Berechnung einfließen

...braucht einen aktuellen Port-Hash, berechnet aber auf dieser Basis direkt den Beitrag der aktuell erzeugten Parameter-Werte

...will sagen, diese Funktionen interpretieren dann zwar noch die Connectivity-Information, verwenden aber für alle tatsächlich zu inkorporierenden Texte eine externe Symbol-Tabelle, allein schon zur Deduplikation. Das bedeutet aber im Umkehrschluß: hier haben wir doch einen »Rückwärts-Zugriff« (auch wenn er funktional realisiert werden kann, indem eine Node-ID reproduzierbar aus der Connectivity errechnet wird)

Beschluß: als string_view durch den Builder durchreichen

das heißt, es handelt sich definitiv nicht um Literal-Strings; zudem wollen wir die Spec ggfs. noch zerlegen und dann in eine Symbol-Table internen; insofern ist std::string_view der naheliegende Ansatz, da wir keine Inline-Storage durch x-fache Builder-Objekte durchschieben wollen, bloß um am Ende doch nur einen Teilstring in die Symboltabelle zu kopieren

...also weiß, daß man nicht mutwillig mit Whitespace herumspielt, daß man eine stabile Spec am Besten generiert, daß man Unicode-Sonderzeichen vermeidet und sich generell um die Eindeutigeit der Syntax kümmert, selbst wenn einem niemand auf die Finger schaut

ich bohre jetzt schon mehr als einen Tag an dieser Stelle herum — was mir zeigt, daß ich den Hebel für so bedrohlich halte, daß ich micht nicht überwinden kann, eine „dumme“ Implementierung schnell reinzuklopfen

eine ID der Domain-Anbindung / Domain-Ontology / Library

high-Level-Beschreibung der Funktionalität dieser Node ... steht wahrscheinlich in Beziehung zur Asset-ID

...das nicht die allgemeine Symbol-Tabelle flutet; auch kann man damit später den tatsächlichen Speicherbedarf besser beurteilen

std::string hat ja inzwischen ebenfalls inline-Storage für ca. 20 Zeichen oder so; zudem könnte man in ProcID selber dann eine std::string_view darauf speichern, damit wäre der Zusammenhang effizient und klar dokumentiert

#include "lib/hash-standard.hpp"

und auch ansonsten äquivalent zur Implementierung von lib::Symbol

Ich sehe das als einen Laufzeit-Cache, der jederzeit regeneriert werden kann; er wird sich maximal mit den im Model verwendeten Processing-Funktions-Deskriptoren füllen

  • geplantes vollständiges Format:
     NodeSymb[.portQualifier](inType[/#][,inType[/#]])(outType[/#][,outType[/#]][ >N])
  • tatsächlich auf dieser Ebene implementiert
    NodeSymb[.portQualifier]<argumentList>

ich war blockiert mit dem testgetriebenen Ansatz, denn ich kann zwar jetzt Nodes bauen, aber nicht einfach verifizieren und dokumentieren, daß sie korrekt verdrahtet sind.

die gleiche Funktionalität wird später zur Problem-Diagnose benötigt; also nichts performance-kritisches

im Moment sieht's so aus, daß der erzeugende Code im Library-Plug-In ohnehin von internen strukturierten Daten ausgeht, um daraus die Node-Spec zu generieren. Diese wird dann einmal beim Builde per Parse zerlegt und ggfs noch zusätzlich dekoriert, und das war's dann.

Begründung: die Schwachstelle jedes Parsers liegt bei den Anknüpfungen. Eine Library wird unvermeidbar eine Objekt- oder Baumnotation mit sich bringen, und sei es bloß eine Konvention auf Basis von std::tuple. Eine gut ausgereifte und weit verbreitete Library wird vermutlich hier eine schwergewichtige Lösung bieten; rein nach Bauchgefühl erwarte ich, daß eine solche Lösung aufgebläht, schlecht in der Performence, extrem tricky, schwer lesbar ist, oder uns ungünstige Verhaltensmuster aufzwingt (Monaden!).

Demgegenüber erscheint mir eine minimalistische ad-hoc-Lösung viel attraktiver, sofern sie auf dem Niveau von »ein paar Abkürzungs-Notationen« bleibt. Das heißt, sie sollte die Mechanik des Parsens nicht zu verbergen versuchen.

Maßstab ist für mich der bereits etablierte Gebrauch von Regular-Expressions (header regexp.hpp): Im Grunde haben wir nur noch eine convenience-Verpackung eines Iterators, ein paar, Typ-Abkürzungen und Imports, sowie den Stil, jeweils im Implementierungs-Header die Grammatik in Form von Reg-Exp-Bausteienen zu definieren. Etwas Vergleichbares sollte auch für Parser-Kombinatoren möglich sein, wenn man sich diesem Gebrauch in mehreren Schritten nähert (und dabei Erfahrungen sammelt)

man könnte eine klammern-zählende Hilfsfunktion schreiben und dann den Inhalt der Klammern per RegExp zerlegen

Zielvorgabe: im Grunde zu Fuß machen (mit Abkürzugen)

...weil ich eigentlich keinen Aufwand in sowas stecken sollte, aber auch keine Bastel-Lösung an einer so zentralen Stelle möchte, und jetzt schon wieder zwei Tage daran herumprokrastiniere und den inneren Konflikt nicht lösen kann. Ja, es reizt mich, es besser zu lösen als all die Libraries, die ich gesehen habe und nicht im Projekt haben möchte. Und »Pragmatismus« empfinde ich als Kränkung und Niderlage hier

⟹ alle Deklarationen zum Spec / Model dorthin

d.h. ArgumentModel könnte eine Struct sein, und lediglich zusätzliche Informations-Funktionen bieten

...mußte dazu erst mal eine Runde raus, dann hab ichs rasch gesehen...

da schreibt man so eine Monster-Syntax einfach hin

und der Parser funktioniert auf Anhieb

...wenn man's genau bedenkt: wäre praktisch äquivalent

in einer LL-Syntax kann man nicht mit einer optionalen Struktur beginnen, die sich erst im Rückblick auf die ganze Zeile aufklärt

siehe text-template.cpp

der Aufwand zum Parsen eines gequoteten CSV ist im Regelfall kaum höher als die stupide Zerlegung an einem Escape-Zeichen; dafür aber ist die Notation als kommaseparierte Liste sehr intuitiv, was Testen und Diagnose erleichtern wird. Quotes müssen wir dann aber vorsehen, weil komplexe Typen möglicherweise Kommata enthalten können, und ich diese Tür nicht sofort schließen möchte (zwar wird es vermutlich darauf hinauslaufen, daß die Library-Binding-Plug-ins ihre eigenen, synthetischen Typ-Bezeichner einführen)

Das passiert, wenn man mit einer Implementierungtechnik anfängt, und nicht bei der Anforderungs-Analyse....

Ja, es würde definitiv gehen, und der User würde fluchen, denn die allgegenwärtigen Quotes haben die Nützlichkeit einer »einfachen Listen-Spec« wieder auf. Hinzu kommt, daß es einige spziell lästige Grenzfälle gibt, wie nested-Quotes , die man dann finden und escapen muß

weiß noch nicht recht, wohin das mit der Node-Spec noch führen wird; nach der Lösung mit einer textuellen Spech habe ich vor allem gegriffen, da ich eine Festlegung auf ein Meta-Modell vermeiden möchte — da noch auf lange Zeit das Projekt nicht im Stande sein wird, das Feld der Möglichkeiten hier zu überblicken (was für Medien werden überhaupt verarbeitet? Außer Video und Sound, meine ich....)

wir brauchen hier definitiv keinen Stack, sondern nur einen internen count-down

dafür wurde dieser Mechanismus ja grade geschaffen, und das State-Core-API ist leicht zu dekorieren

Aua, dieses Thema ist zu lange liegen geblieben; drei Monate später weiß ich nicht mehr, was ich überhaupt bezwecken wollte....  Kam das mit der Pipeline so zustande, weil ich mir vorgestllt hatte, einen Spec-String mit einer RegExp zu »scannen« — denn so einfach wird es nicht sein

doch! jetzt bietet sich tatsächlich eine

Pipeline zum Aufbereiten der Argumente an

und zwar zum Ausfalten der Abkürzungs-Syntax /#

Aber trotzdem immer noch nicht der leiseste Hauch von Erinnerung, was ich damals gemeint habe....

....und jetzt hab ich einen echten LL-Parser, der diese Aufgabe direkt und ohne Implementierungstricks handhaben kann

...deshalb habe ich ja auch die Node-Connectivity beibehalten (obwohl zum Rendern nur die Port-Connectivity gebraucht wird). Zusätzlich könnten noch Attribute in der ProcID eine Rolle spielen. Also insgesamt ehr kein separates Parsing

⟹ also so implementieren wie's am einfachsten geht

die Normalisierung hier nicht zu weit treiben; es ist ein low-Level-Interface und dient in 99% der Fälle dazu, Hash-Keys zu erzeugen

Extended Attributes sollen diverse anderweitige Beschränkungen auffangen

Wenn ein komplexer Sachverhalt überhaupt erst erschlossen werden soll, durch das Aufbauen einer orientierenden Struktur, so muß ein Vorgriff gemacht werden, der leider nur in den seltensten Fällen der Sache adäquat ist. Infolgedessen ist man dann an untaugliche Strukturen gebunden, deren Reparatur zu kostspielig wäre. Stattdessen hilft man sich mit darüber gelegten Layern, die Metadaten und erweiterte Attribute als Ankerpunkt verwenden

Design-Paradoxon: das Design besonders relevanter Funktionalität ist selten adäquat

zum Einen ist das komplex, und außerdem birgt es die Gefahr der Korruption durch Updates

keine offene Lösung

eine »offene« Lösung würde einer Erweiterung von Lumiera (plug-in) oder einer speziellen Library-Integration ermöglichen, Attribute durch das low-level-Model hindurch zu »tunneln«, ohne spezielle Unterstützung der Core-Implementierung. Ich halte das jedoch aktuell für einen nicht naheliegenden Ansatz, da es keinen »Rück-Kanal« vom low-level-Model in die Erweiterungen/Plug-ins gibt; generell hat sich das low-level-Model von einer Schnittstelle weg und zu einer internen Struktur hin entwickelt und kann nicht verstanden werden ohne implizite Kenntnis mancher Entwicklungs-Aspekte

Normalerweise verwendet man die klassischen OO-Interfaces (oder einen generischen aber getypten Kontext) auch grade dazu, die passenden Voraussetzungen durch einen Kontrakt sicherzustellen. Das ganze Unterfangen hier aber umgeht Interfaces und Kontrakt, um die Kosten dafür einzusparen. Stattdessen hinterlegen wir einen Direkt-Zugangsschlüssel, der ohne Prüfung angewendet wird.....

...also ist der einzige Schutz, den Schlüssel inhaltlich korrekt an die Features der ID zu binden. Diese Bindung ist aber nicht formal-logisch, sondern für eine bestimmte Intention vorkonfiguriert; ob diese Zuordnung korrekt ist und zu einem validen Aufruf führt, hängt allein daran, daß bei der Erstellung die dazu passenden Merkmale in der ID hinterlegt wurden. Da diese Klassifikation zusammen mit der Registrierung der Funktion erfolgt, ist diese Verbindung so sicher wie die Logik, die bei der Registrierung zum Tragen kommt. Das heißt, wenn es — bedingt durch eine Lücke im logischen Argument — zur Registrierung zweier inkompatibler Funktionen unter der gleichen ID kommen könnte, gibt es keinen weiteren Schutzmechanismus mehr

der Aufruf ist ein blinder cast

...weil eine offene Lösung zwar ohne Weiteres mögilch wäre, aber mit erheblich größerem Storage-Overhead einhergeht, der sich dann erst mal durch de-Duplikation amortisieren müßte. Daher sage ich erst einmal YAGNI!  und realisiere die einfache Lösung mit direkter Kollaboration, deren Overhead und Amortisierung leicht abschätzbar ist

das ist ein gradezu absurdes Unterfangen,

den Konsequenzen des gewählten Designs

zu entgehen....

aber all die damit ausgeschlossenen Möglichkeiten

will ich dann doch auch noch haben...

oder ein Trampolin? aber auf Basis welcher Laufzeit-Information?

Auf Stuktur-Kongruenz von Templates zurückführen

MediaWeavingPattern ⟵  Dummy-Funktion void(&)(NullType*)

ParamWeavingPattern ⟵  empty Spec buildParamSpec()

....da ich im Moment nur spekuliere, und ein konkreter Bezugspunkt noch in weiter Ferne liegt; wegen der de-Duplikation sollten alle Quellen bereits in den statischen Factory-Aufruf gehen; man könnte natürlich eine Builder-DSL davor setzen (nicht daß wir schon genug Builder in dem Bereich hätten....)

ich glaube nicht, daß es sinnvoll ist, eine Attribut-Map als Ganzes zu de-duplizieren. Empirisch entscheiden können wir das aber leider erst viel später. Ja aber, was machen wir dann bloß? Wenn-wäre-hätte?? Die Node-Storage muß unbedingt klein gehalten werden. Kann aber derzeit überhaupt nicht entscheiden, ob die Vorraussetzungen für eine persistente Datenstruktur (im Sinne der funktionalen Programmierung) gegeben sind....

Daher ziehe ich es vor, den Kopf in den Sand zu stecken....

erste Anwendung: PortDiagnostic::srcPorts()

Da Fall-1 am Wichtigsten ist, orientiert sich das Interface daran und reicht einfach die Collection der Vorgänger-Ports per Referenz heraus. Leider haben wir aber im Fall de ParamWeavingPattern gar keine Collection der Vorgänger-Ports, sondern nur eine direkte Referenz auf den Delegate des Proxy; und da es sich um einen einfachen Funktionsaufruf handelt, haben wir keinen Ort, an dem eine solche Collection hilfsweise konstruiert und abgelegt werden könnte. Deshalb bleibt nur, entweder in dem Fall gar nichts zu liefern, d.h. zwei verschiedene Methoden zu bieten, oder eben die Rekursion auf das Delegate (das dann hoffentlich ein Manifold-Pattern ist und deshalb eine solche Collection hat)

...entweder, indem es direkt im Turnout eine Querverbindung gibt, oder duch einen low-Level-Zugangsweg analog zu den Source-Ports

...wird eigentlich erst für die Hash / Cache-Key-Berechnung relevant; dann aber wären erst einige kniffelige technische Probleme zu lösen, die ich im Moment nicht recht bestimmen kann...

....die Vorraussetzungen hierfür sind gut, da der einzige Zugang über eine zentrale Builder-Schnittstelle erfolgt; dennoch wird ein solches Schema komplex, bedingt durch die Verteilung von Logik auf die verschiedenen Media-processing-Library-Plug-ins

gefährlicher Hebel durch potentiell

sehr große Anzahl an Nodes

...denn wir verweisen darauf per Referenz oder Pointer

Und zwar, weil die Rolle der Ablaufsteuerung grundsätzlich überdacht und reorientiert wurde: Der Engine-Code ist jetzt nicht mehr »Tabellengetrieben«, sondern besteht aus miteinander verwobenen Funktions-Closures. Damit wandert der wichtigste Teil dieser Steuerung in ein zukünftig zu entwickelndes Library Plug-in (erneut für jede Media-Handling Lib, wie z.B. FFmpeg).

Daher wird es keine zentrale Node-Factory als Abstraktionskomponente geben; vielmehr erwarte ich, daß der Builder-Level-3 über ein noch zu schaffendes API das Library Plug-in aktiviert, welches dann das API des NodeBuilders verwendet um einzelne Nodes zu konstruieren. Es läuft also auf ein relativ komplexes Wechselspiel zwischen den Builder-Leveln und der Delegation an das Library Plug-in hinaus. Letzteres kann nämlich nicht das Anlegen und Verschalten der Nodes übernehmen, aber muß die Funktion beisteuern und die dazu passende Node-Spec....

Lösung: Weaving-Pattern wird unmittelbar Teil des Turnout

verwendet einen Invocation-Adapter mit eingebetteter FeedManifold

Nein!

Da wir nun weitgehend auf explizite Typisierung und Templates setzen (Tupel als Argumente der Processing-Function), sind Detail-Implementierungen in den Spezialisierungen der Weaving-Pattern untergebracht, oder werden sogar als Funktoren aus dem Library-Plug-in eingebunden

Tatsächlich zeige ich nun alle drei Punkte

in einer einzigen Test-Node-Topologie

  • nahezu identisch verwendet in NodeLink_test und NodeMeta_test
  • in letzterem befindet sich die erschöpfende Abdeckung der Diagnose / Verifikations-Tools
  • als Processing-Funktionen verwende ich in den ersten Schritten einfache Algebra-Funktionen
  • in NodeBase_test habe ich die Korrektheit des Aufrufs solcher einfachen Algebra im Detail verifiziert

...weil es dämlich wäre, da so diverse (auch inhaltlich sinnvolle) Funktionen zu definieren, dann aber überhaupt nicht aufzurufen. Zumal diese Funktionen noch so einfach sind, daß man die Aufrufe direkt im Debugger nachvollziehen kann...

...im Grunde ist die Identität zunächst einmal, daß es die Node gibt, daß sie an einer Speicheradresse sitzt; im aktuellen Design ist es jedoch nicht notwendig, die Node anhand der Identität finden zu können, denn man findet sie über die Verschaltung auf eine Exit-Node

...weiterhin bleibt es dabei, daß der Zugang zum Buffer-Management über das Turnout-System (früher StateAdapter) läuft; aber dafür ist kein klassisches OO-Interface notwendig, sofern das Buffer-Handling seinerseits auf einem Interface aufbaut. Die Aufgabe, an der sich das entscheidet ist, wie der konkrete Turnout eine konkrete FeedManifold bauen kann (und ob er das überhaupt tut)

...das war mir lange Zeit nicht klar; tatsächlich ist das verborgen in der Richtung der Abstraktion, die auf einen top-down-Zugang hindeutet — ein Solcher ist aber nur zielführend, wenn man im Prinzip über ein Gesamtbild der Domäne verfügt. Das ist allerdings die ganz normale, professionelle Dreistigkeit des Entwicklers, getreu dem Motto „let's do the first step, and then we'll figure out the rest“.

Im Rückblick ist das, was mir dann passiert ist, also folgerichtig, und ich habe mich richtig verhalten, indem ich den Ansatz „hängen ließ“....

Rein technische Informationen wären z.B. ein Funktions-Symbol im Executable oder Quellcode oder eine Parametersignatur; von solchen Daten kommt man niemals direkt zu einem Urteil "äquivalent für den User"

mindestens Level-3, denn auf Level-2 gehen wir bereits davon aus, daß Buffer-Typen blindlings passen

Das ist ein Kompromiß und ein Trick, mit dem man (für Test und Diagnose) doch noch ein bischen hinter die Kulissen schauen kann. Auf dieser Basis habe ich einen Accessor gebaut: PortDiagnostic::getSrcNodes()  ⟼  lib::Several<PortRef>

...und dieser ist zwar global eindeutig, andererseits aber auch implementation-defined und für jeden Programmlauf anders

im Gegensatz zum Port, in den auch der aktuelle Implementierungszustand einfließt, aber keine Timeline-Koordinaten, nur die Quell-Koordinaten (wenn man einen Clip nur in der Timeline verschiebt, ohne die Berechnungs-Topologie zu ändern, soll der Cache-Key stabil bleiben)

hier darf kein logischer Zirkel entstehen! Man könnte nämlich auf die Idee kommen, das Proc-Asset durch seine Implementierung zu klassifizieren, aber das wollen wir nicht. Vielmehr brauchen wir hier ein semantisches Konzept, wie z.B. »gaussian blur«. Das bedeutet, diese Basis-IDs müssen vom Library Plug-in belegt werden, und sollen langfristig stabil bleiben (solange der in der LIbrary gebotene Implementierungs-Algorithmus kompatibel bleibt und auf Sicht äquivalente Ergebnisse liefert)

es sollte eine human-readable-Ausprägung geben

wenn wir für jede Node wieder eine texturelle ID speichern, oder auch nur mehrere Hash-Komponenten, wäre der Hebel gewaltig — irgendwo muß dedupliziert werden....

Zwar ist es eine Zusammenführung bereits abgeschlossener Konzepte, Planungen und Teilkomponenten, jedoch mußten diese zunächst in eine undefinierte Umgebung hinein entworfen werden, so daß sich ihre Form erst durch die Zusammenschaltung ausprägen kann. Und — aufgrund der zyklischen Natur des Unterfangens — diese Zusammenschaltung selbst kann nur vorläufig sein; gebaut wird die Demonstration eines Render-Vorganges, auf Basis einer vorläufigen Integration externer Render-Funktionalität; und dabei wird sich zeigen, was glatt aufgeht und was sich sperrt. Bewußt wird der Kern der gesamten Architektur, der Builder/Compiler, für dieses Konstrukt ausgelassen — denn das, was sich sperrt, soll über diesen Dreh- und Angelpunkt nach Außen transformiert und dort entfaltet werden.

Und das ganz bewußt nur auf Basis eines intuitiven Verständnisses der Anforderungen und API-Strukturen einer in C geschriebenen Media-Handling-Library — also explizit ohne direkte Analyse konkreter Libraries wie FFmpeg oder GStreamer. Denn mein Ziel ist, zunächst eine Hohlform zu bekommen, eine Ausprägung der inneren Anforderungen, aus meinem generellen Verständnis des Sachverhalts, sowie der bisher geschaffenen Implementierung der Render-Nodes. Die entstehende Struktur soll sich zunächst gradlinig entwickeln können, und dann erst später, in weiteren Prototyping-Schritten auf konkrete Anforderungen adapteiert werden.

Inkrementelles Prototyping schafft mir hier einen Mittelweg zwischen deduktivem top-down-Ansatz und einem induktiven bottom-up-Aufbau; ich gehe aus von einigen, intuitiv gewählten Formen in der Mitte des auszuspannenden Raumes und stelle von dort einen ersten Durchgang her. Die Belange der Performance und die Datenstruktur spielen dabei ebenso eine Rolle, wie das Streben nach Klarheit, welches die geschaffene Struktur nachhalltig macht.

Nodes werden hier noch direkt erzeugt und verwenden die automatische Heap-Allokation; denn es geht um die Zusammenarbeit der Funktionalität in den Render-Nodes.

Framework und Services aus dem produktiven Setup verwenden, möglichst auch den realen Memory-Buffer-Provider. Damit stellt sich die Frage, wie hier überhaupt verifiziert werden kann; vermutlich werde ich Instrumentierungs-Hilfsmittel einführen und dafür auch Zugangspunkte in die produktiven Services einführen müssen — ähnlich wie ich es erfolgreich für den Block-Flow-Allokator im Scheduler getan habe

Das war schon beim allerersten Entwurf 2009 ein Problem, daß ich immerfort das Bild eines komplexen Interaktions-Protokolls im Kopf hatte; im Bezug auf die tatsächlich zu realisierenden Abläufe mag das ja stimmen, aber es muß nicht explizit in Software-Strukturen repräsentiert werden. Jetzt, für das überarbeitete Schema habe ich zwar die Interaktionen genauer verstanden, und auch ein anderes Erkenntnisbild zugrundegelegt (ein Webe-Vorgang) — trotzdem unterliege ich immer wieder dem gleichen Denkfehler, diese per Analyse offengelegten Strukturen auch in Software-Komponenten verkörpern zu wollen.

Es ist offensichtlich, daß die Berechnung eines einzigen Video-Frames um Größenordnungen aufwendiger ist, als alle Einzel-Aspekte der Ablaufsteuerung. Aber, gilt das auch noch, wenn wir Sound berechnen? oder MIDI-Verarbeitung machen? Oder gar symbolische Repräsentationen generieren, wie z.B. Musik-Notation, API-Orchestrierung etc? Also die »nicht-offensichtlichen Medien«. Außerdem: wie feingranular wird die Zerlegung von Rechenvorgängen im Modell? Das ist auch eine Abwägung Code-Komplexität vs. Ausführungskosten. Eine sehr feingranulare Zerlegung könnte dazu führen, daß ein gefährlicher Hebel entsteht (im Besonderen, da es sich um eine Baumstruktur handelt). Dadurch könnten Einzel-Aufwände, die für sich allein genommen komplett irrelevant erscheinen, in der Gesamtsumme eben doch wirksam werden

Die Frage ist: kann es in dieser komplexen Verarbeitung dazu kommen, daß Buffer-Lebenszyklus-Übergänge redundant getriggert werden? Ein möglicher Problemfall wäre denkbar als Folge der Behandlung von InOut-Parametern durch Kopieren eines BuffHandle (was ansonsten eine sehr elegante Lösung wäre)

somit geht es vor allem darum,

Folgeschäden eines redundanten Aufrufs zu bedenken

...und zwar, weil sie nicht speichersicher ist!

Das ist genau die Sorte Code, die man zwar durchaus aus Performance-Gründen schreiben kann, aber dann besser nur in einem sorgsam abgezirkelten Implementierungs-Zusammenhang. Genau diese Eingrenzung ist hier aber nicht gegeben: vielmehr entsteht hier ein Baukasten-System, und es ist ohne Weiteres möglich, daß ein Turnout mit falschen Parametern initialisiert wird.

  ⚠ In einem solchen Fall könnte die Ausführung direkt in die Interpretation

        von uninitialisiertem Speicher laufen.

Der Node-Build sammelt Parameterdaten für jeden Port in einer verschachtelten Datenstruktur auf dem Heap, und arbeitet diese rekursiv ab; der so generierte Code ist sehr effizient (Optimiser), bedingt aber eine Limitierung auf eine kleine Anzahl an Ports

aber nur, falls wirklich Outputs für einzelne Komponenten gerendert werden sollen

es müßte dann auf Ebene der Fixture die möglichkeit Virtueller oder Iterativer Ports geschaffen werden, ohne dort die Storage ausufern zu lassen; solche Ports müßten dann geeignet in die Aufruf-Parameter repräsentiert werden, so daß das Turnout-System dann jeweils einen einzigen realen Port aktiviert, diesem aber einen iterativ generierten Selektor-Parameter auf dem Weg der Automation durchgibt, wodurch im Node-Graphen selber an geeigneter Stelle ein Kanal-Selektor bei ansonsten gleicher Verarbeitung koordiniert würde. Die eigentliche Schwierigkeit bei diesem Ansatz liegt allerdings darin, daß diese komplexe und globale Struktur im Builder erkannt und transformiert  werden muß

...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)

das Buffer-Protocol von 2012

betrachte ich als verbindlich

naja... ehrlich gesagt,

es gibt noch gar keine Verwendungen

alignof()-Operator und die Hilfsfunktion std::align()

muß hier leider eine schecklicke low-level-Trickserei machen; das ist die Konsequenz der Entscheidung, mit dem absolut minimalen Storage-Overhead zu arbeiten, und außerdem muß ich auch noch das Non-Copyable aushebeln...

auch: eine gradzahlige Anzahl an Overhead-Slots

...durch diesen Trick sparen wir uns einen zusätzlichen Pointer auf den aktuellen Block: da std::allign(pos,rest) den pos-Zeiger stets kohärent zusammen mit dem rest-cnt manipuliert, können wir stets aus beiden zusammen wieder zum Anfang des Blocks zurückfinden.

wie man's auch dreht und wendet: irgendwo muß die Typ-Information explizit untergrebracht werden, da wir sie vom Allocation-Cluster selber entfernt haben (dieser ist nun generisch und kann einen beliebigen Mix von Objekten/Typen allozieren). Das einzige, was man machen könnte, wäre diese Info komprimiert abzuspeichern....

...da man sich diesen Code ohnehin nur anschaut, wenn man muß, ist nichts mehr gewonnen, weitere Details nochmal durch eine Indirektion zu verbergen

...nämlich die Speicheradresse der Instanz

beim Umbau der Policy für LinkedElements habe ich zunächst einen Adapter geschrieben, der den std::allocator schön sauber als Member hält — nur wird dadurch dann sizeof(LinkedElements) ≡ 2 »slots«. Und damit kippt der ganze Layout-Trick weg und der Destruktor läuft Amok.

   Lessions Learned

weil dies auf einem Array beruht, also im Speicher kompakt liegen muß

In diesem Konflikt stehen zwei gleichermaßen bedeutsame Belange gegeneinander, ohne einen klaren Ansatz zur Entscheidung

  • ein erhebliches Wartungs-Risiko
  • ein erheblicher Performance-Overhead

Der Beschluß zur Lösung sieht vor, diese Belange markierbar zu machen, um dann später differenziert handeln zu können.

...somit kann auf Basis der einzelnen, konkreten Datenstruktur entschieden (und später auch korrigiert) werden, ob ein expliziter clean-up-Aufruf notwendig ist; für die einzelne Datenstruktur dürfte das lokal jeweils klar entscheidbar sein, und ich erwarte, daß durch die Anbindung an den Allocation-Cluster diese Entscheidungsmöglichkeit auch langfristig klar dokumentiert ist — und zwar sollte das von üblichen C++ Praktiken abweichende Verhalten auch als der Spezialfall dargestellt sein (wenngleich auch erwartet wird, daß die meisten Datenstrukturen von diesem Spezialfall gebrauch machen)

Und zwar weil sich aus der konkreten Implementierung diese Möglichkeit einfach ergibt

per Default ist stets eine absolute nominal Time gegeben

das folgt aus der Aufruf-Struktur der Render-Engine; diese Zeitangabe ist aber für sich sinnlos und wird erst interpretiert auf Basis einer konkreten ExitNode, welche sich dann implizit auf eine Timeline und durch den verwendeten Port auch auf ein konkretes FrameGrid bezieht; die Übersetzung der nominal Time in eine konkrete Frame-Nummer passiert innerhalb des Node-Graphen

Hab zunächst den alten Code von 2009 / 2012 analysiert und dann die Node-Struktur neu aufgebaut; beim Verschalten der Nodes stellte sich dann zwangsläufig wieder die Frage nach den Parametern (und sei es bloß die Frame-Nummer, die man irgendwo abgreifen muß). Aus den bereits geschaffenen Strukturen war Anfang Dezember 2024 ein Entwurf möglich, der die noch vorhandenen Freiräume nutzt und die Vorstellung von Parametern konkretisiert als Funktor-Aufrufe

es gibt einen commit

Einträge sind woanders alloziert

nur eine einzige, die Aktive

geplant: generisches front-End für Custom Allocator

Fazit: gar nicht

Render-Node (ProcNode)

Kurzfristig erscheint das als eine naheliegende Optimierung, die einem praktisch »in den Schoß fällt« (die Implementierung wird dadurch sogar drastisch einfacher). Aber längerfristig befürchte ich eine heimtücksiche Gefahr, denn die hier genommene Abkürzung kann leicht übersehen werden, da sie den üblichen Gepflogenheiten zuwiderläuft. Im Lauf der Zeit können sich so Speicher- und Ressourcen-Lecks einschleichen, die dann nur mit erheblichem und fokussiertem Aufwand aufzuräumen sind

Es handelt sich um eines der markanten Eigenschaften der Sprache C++ : Kontrolle und Determinismus bis ins kleinste Detail — und das prägt den alltäglichen Stil der Arbeit; weithin kann man sich auf Abstraktionen verlassen, weil diese sich wiederum auf Abstraktionen verlassen können; wenn alles genau und zuverlässig ist, dann werden auch weitreichende Aktionen planbar und handhabbar.

Hier geht es um das gesamte low-level-Model, sowie möglicherweise Teile des Build-Prozesses und des Regelwerks, die daran angeknüpft sein könnten — und das bedeutet, mit einer (wie es zunächst scheint) sehr lokalen und tief verborgenen Optimierung könnte der Grund-Kontrakt in einem erheblichen Teil der Applikation geändert werden

Der Aufwand, der allein für das Aufrufen der aller Destruktoren getrieben werden muß, ist nicht unerheblich, denn für jeden Typ muß eine Closure im Datensegment erzeugt werden und für jede einzelne Allokation muß diese per Funktionszeiger aufrufbar sein; außerdem muß die gesamte Allokation navigierbar gemacht werden — also zwei »Slots« zusätzlich für jede einzelne Allokation. Das ist sehr viel für eine Datenstruktur, die aus vielen kleinen und sehr flexiblen Descriptor-Elementen bestehen wird; die meisten Nodes haben erwartungsgemäß nur einen Eingang und einen Ausgang, was bedeutet, daß für jeweils nur eine einzige ID (ein »Slot«) zusätzlich ein Container (2 »Slot«) und dann noch 4 »Slot« Allokations-Overhead notwendig sind.

in der Regel sind es cold pages

Aus Performance-Sicht besonders fatal ist, daß zum Zeitpunkt der Bulk-de-Allokation mit hoher Wahrscheinlichkeit alle betroffenen memory pages bereits »cold« sind, d.h. aus dem Cache herausgefallen; wir müssen also eine Menge von Speicherseiten über den Bus ziehen, bloß um sie zu navigieren und dann...

...in den allermeisten Fällen nämlich exakt gar nichts  zu tun. Dies unter der Annahme, daß die Struktur größtenteils selbst-referentiell ist; zwar werden dadurch reihenweise verkettete Destruktor-Aufrufe stattfinden, welche aber alle letztlich beim Allocator enden, welcher dann (ganz bewußt) nichts tut, weil der gesamte Speicherblock anschließend ohnehin verworfen wird. Da es sich jedoch um dynamisch aufgebaute Datenstrukturen handelt, kann der Optimizer diesen Leerlauf nicht erkennen und beseitigen

Es steht zu befürchten, daß während der normalen Edit-Tätigkeit alle par 1/10-sec ein Builder-Lauf getriggert wird — und ich schätze, daß ein erheblicher Anteil der tatsächlichen Laufzeit in das Konstruieren der Datenstruktur geht, denn der zugrundeliegende trade-off ist ja grade  space-for-time. Wenngleich auch der Neubau ebenfalls schlecht für den Cache ist, so kann man doch zumindet in Teilen hoffen, daß die neu gebauten Strukturen zumindest bis zur ersten Berührung durch den Play-Prozeß im L3 bleiben. Für die alten Strukturen gilt das aber nicht, sie stellen rein nutzlosen Balast dar.

das ist »der Klassiker«.

Ich handle hier nur auf Basis eines Bauchgefühls, und alle Erfahrung zeigt, daß man dabei meist die Gewichte falsch setzt.

Angenommen, ich mache diese Optimierung jetzt nicht, bereite sie aber vor; später dann zeigt sich (mit guter Wahrscheinlichkeit) tatsächlich ein relevanter Overhead ⟹ dann ist der Druck zur Optimierung umso stärker, und man wird die vorbereitete Option »ziehen« und die weitreichenden Konsequenzen in Kauf nehmen, da die Behebung eines konkreten Problems immer alle strategischen und methodischen Erwägungen übersteuert. Das wäre der schlechtest mögliche Verlauf, denn zu eine so späten Zeitpunkt kann man kaum mehr etwas tun, um eine weitreichende Änderung der Konventionen abzufedern

alle Werte müssen zuverlässig in die Zieldatenstruktur kopiert  werden

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...

⟹ man könnte Argumente encodieren

denn damit wäre nicht nur die Segmentation zu erhalten, sondern auch der Play-Prozess-Translator-Record — solange bis kein Job mehr „anrufen“ kann

Fazit: nein und YAGNI

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

...und wird daher auf Implementierungs-Ebene im »ExecutionCtx« gesetzt und weitergegeben; im einzelnen Activity-Record ist diese Information nicht vorhanden und meist auch nicht relevant

...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

ansonsten bekommen wir nämlich zwei λ-post-Übergänge

⟹ all das zusammen legt nahe, direkt die Bedingungen des Target zu beachten

...denn das ist ein trickreicher Mechanismus, der zwar sehr einfach umzusetzen, aber nicht leicht zu verstehen ist; einerseits bin ich froh, das notfalls auch noch zur Verfügung zu haben, andererseits ist mir eine direkte Unterstützung im Mechanismus lieber

da es zusätzliche λ-post für jeden Aufruf verursacht, selbst wenn dieser Aufruf danach gleich am GATE scheitert — während der Zeit hält der Thread das Grooming-Token

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

beide Probleme lassen sich nicht lösen —

..aber entschärfen..

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 gehen die

Scheduling-Daten hervor

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

...oh wie wahr (seinerzeit nur aus allgemeiner Argumentation abgeleitet ... inzwischen geprüft und bestätigt gefunden)

nach vorläufiger Analyse: harmlos

...da nun wirklich jeder Dispatch am Grooming-Token „vorbei muß“  — theoretisch wäre es sogar darstellbar, die eigentliche Job-Planung außerhalb und ohne Grooming-Token zu machen; die neuen Jobs gehen dann eben in die Instruction-Queue. Auch eine Notification (welche das Gate dekrementieren könnte) geht durch das λ-post, welches direkt durch Layer2.postDispatch geht, und damit entweder versucht, das Grooming-Token zu erlangen, oder eben in die Instruct-Queue einstellt

...in diese Falle bin ich nun schon x-mal gegangen; das ist alles so schön hinter einem Builder verpackt, so daß man nicht sieht, wo der Effekt passiert; festzuhalten bleibt: bereits das Konstruieren der Activity-Records ist gefährlich, und das kann durchaus passieren, bevor wir überhaupt in den Scheduler selber eingesteigen sind

die ActivityLang steht gefährlich außerhalb des Scheduler-Kernsystems

...hängt aber hinten unten am Allokator, und ist deshalb darauf angewiesen, daß allozierende Operationen nur in einer sicheren »Management-Zone« passieren

wenig Raum für Diskussionen — und zwar wegen der Memory-Allokation

und hat damit garantiert das Grooming-Token

weil ja λ-post selbst im ExecutionCtx sitzt

wenn man nämlich als Funktor oder statische Funktion implementiert, bekommt man kein "this" in die Hand

...also solange ich nur mit Unit-Tests gearbeitet habe

  • von einer statischen Funktion kan man direkt den Typ aufgreifen, qualifiziert durch den Scope: decltype( Scope::fun )
  • von einem regulären Member-Function kann man den Typ nach einem Pseudo-Zugriff erfassen: decltype( std::declval<Scope>().fun )
  • bei einem Functor-Member geht das nicht, aber man kann hier entweder statisch qualifzieren (wie 1.Fall), oder einen Pointer nehmen: decltype( &Scope::fun )

im Besonderen λ-post ist im Grunde

der Scheduler-Service überhaupt

...nämlich die Fähigkeit, einen Activity-chain zu einem geplanten Zeitpunkt oder auf Signal hin auszuführen — und diese Fähigkeit muß selbstverständlich der Sprache selber zu Gebote stehen, damit sie komplexe Aktionsmuster flexibel ausdrücken kann

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?

hier spielt eine Deadline nur die Rolle einer zu prüfenden Bedingung

diese enthalten eine Activity, aber auch ein Zeitfenster sowie weitere Kontext-Parameter (ManifestationID, Flags wie isCompulsory)

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

da am Beginn der Kette ohnehin ein Ctx.post vorangegangen ist

vor dem Zugang zum GATE muß das Grooming-Token  erlangt werden

zweimaliger λ-post wäre die sauberste Lösung — bedingt aber inakzeptablen Overhead

wie gut ist es self-composable?

aber der Hook kann auch die Rolle jedes activate/notify übernehmen

bestimme: dies soll jetzt geschehen

prüfe das Fenster

activate-NOTIFY: macht λ-post mit target

dispatch-GATE:  ⟶ Spezialbehandlung NOTIFY Gate

wichtige Detail-Änderung: λ-post geht immer via Queue

....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

unklar: ist das gut?

fällt damit eine Ausdrucks-Variante weg?

oder doch nicht?

aber nur sofern man die Start-Zeit als Constraint deutet

es geht hier um Regel/Ausnahme

wenn etwas „sofort“ starten kann, setze ich die Startzeit auf jetzt

Im Zusammenspiel mit λ-post-Anpassung: signifikant bessere Parallelisierung

Anpassung: λ-post macht keinen direkten Dispatch (mehr)

...sondern stellt die gePOSTete Activity stets in die Queue, selbst wenn das Grooming-Token zu erlangen wäre. Das ändert die formale Semantik nicht, führt aber zu einer wesentlich besseren Parallelisierung

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

weil sie sinnigerweise auf dem Kopf steht

....das hat sich allerdings schon aus der Analyse des Pull-Processing im Node-Network so ergeben, denn dort geht man von der ExitNode rückwärts; damals konnte ich nicht vorhersehen, wie die Situation im Scheduler sich darstellen wird — möglicherweise verbirgt sich eine tiefere, strukturelle Konvergenz dahinter, daß das jetzt so schön aufgeht

das ist das ausschlaggebende Argument: Jemand, der erst mal nur den Scheduler-Header liest, wäre sonst sofort erschlagen von technischen Details, und würde den Wald vor lauter Bäumen nicht mehr sehen. Zumal Scheduler selber nochmal zerlegt wird in einen Header-Anteil und eine eigene translation-unit (scheduler.cpp)

...und das hatte ich an der Stelle gar nicht erwartet: die tatsächlichen Benutzer eines »Scheduler-Service« gehen nach wenigen festen Schemata vor und müssen überhaupt nichts von einer »Activity-Language« wissen

...und zwar wegen der Builder-Notation; wegen der Möglichkeit, später noch Dependencies aufzuschalten, muß der activity::Term im Builder eingebettet sein, also bei der Definition der Builder-Klasse bekannt. Einziger Ausweg wäre, in die äußere Builder-Klasse einen opaque buffer zu legen, und die Implementierung in scheduler.cpp nachzutragen. Diese Lösung halte ich für unnötig komplex (wiewohl sie keinen relevanten Performance-overhead erzeugt)

...denn die betreffenden Header sind nicht wirklich schwergewichtig  — im Besonderen da die typischen Clients des SchedulerService selber Implementierungs-Code sind

Letzten Endes sind das immer nur ein paar Dutzend Aufrufe, und die Zeitmessungen der letzten Tage zeigen wieder einmal mehr als deutlich, daß das Vermeiden virtueller Calls auf dem Level keine Rolle spielt. Es sollte locker möglich sein, 20 neue Jobs in 100µs an den Scheduler zu übergeben...

denn dabei handelt es sich nun wirklich um eine zentrale Implementierungs-Funktion, die auch aus der Implementierung heraus aufgerufen wird. Selbst wenn der Optimizer wahrscheinlich die monomorphic optimization beherrscht, macht man sowas einfach nicht.

...so ziemlich jede von denen greift in der Implementierung auf die Komponenten im Scheduler zu.

...der erhebliche techische Detailierungsgrad der übergreifenden Abläufe legt eine klare Abtrennung zu den Kollaborationsparnern nahe. Zwar bewegen sich voraussichtlich auch alle Clients im gleichen Code-Universums (so daß es keine Probleme mit Dependencies gibt), aber die integration im Scheduler selber, erweist sich bereits als herausfordernd komplex. Zudem zeichnet sich eine deutliche Abgrenzung im Sinne einer Domäne heraus: die tatsächlichen Clients müssen praktisch nichts vom Themenkomplex der »Activity-Language« wissen.

Nicht am falschen Ende sparen hier! Das kostet nur ein paar Nanosekunden... (und ein paar Fixes in den Tests, damit die am sanityCheck vorbeikommen)

Das könnte man machen, klingt sogar sinnvoll — ABER...

Der BlockFlow müßte dann seine eigene Config dynamischer auswerten. Das mit der Config war ein schwieriges Thema, denn ich wollte sie statisch haben, und das hat sich dann auch als sehr performance-relevant bestätigt. Daher möchte ich das Thema mit einer dynamischen Auswertung vertagen, bis wir insgesamt ein klareres Bild zum Thema System-Konfiguration haben.


Die Lösung müßte in etwa so aussehen: Der BlockFlow hat dann eine Informationsfuntion, die ebenfalls auf die statische Config zugreift, und eben jeden Überschlags-Berechnungen macht, die ich hier nun mit dem Taschenrechner gemacht habe. Das würde natürlich eine Requirement-Analyse vorraussetzen, d.h. welche Informationen werden benötigt? Im Moment ist der Scheduler der einzige »Kunde«.


Hinzu kommt, daß dies hier ohnehin ein zusätzliches Sicherheitsnetz ist: um den BlockFlow auf die minimale Blockgröße zu drücken, müßte man den Scheduler massivst überladen; er würde dann vermutlich die Deadline vom nächsten »Tick« überfahren, und dann eine »Scheduler-emergency« auslösen (da der Tick  compulsory ist). Das würde nach ca. 0.5 Sekunden passieren; hier aber reden wir über 20 Sekunden mit dieser Dichte. Es geht also im Grunde nur darum, daß das System kurzzeitige Lastsöße überlebt, ohne aus dem Gleichgewicht zu kommen

20sec ⟹ 366 MiB

also auf 20 Sekunden in die Zukunft limitieren

Im Moment stehe ich for der Aufgabe, einen Scheduler-Mechanismus zu bauen, dessen konkrete Einsatzszenarien derzeit bestenfalls abschätzbar sind. Daher muß ich mir vor allem Erweiterungspunkte und Flexibilität schaffen. Wenn später einmal die Verwendungen der Engine weitgehend komplett überschaubar sind (einschließlich Hardware-Einbindung), dann wäre es sehr wohl denkbar, die Builder-Syntax und ggfs. sogar Teile der Activity-Language zu konsolidieren und festzulegen in wenige, explizite Funktionen auf dem Scheduler-API...

⟶ Iter-explorer

⟶ Format-util

⟶ meta/trait

⟹ für Implementierungs-Code ist das nicht weiter auffällig (die halbe Code-Basis includiert indirekt diese Header)

also sehr großzügig

er ist stets compulsory

...ich hab nämlich nur an mich selber gedacht, wie ich mit dem Debugger noch in den ersten Planungs-Chunk hineinsteppen kann. Das ist doch cool, dachte ich. Diese Perspektive ist aber schräg — wenngleich es sicher gut ist, daß aus Konsistenzgründen sogar der aktuelle Thread ein Stück Arbeit machen könnte, so ist das letztlich doch nicht die Aufrgabe des aktuellen Thread (Player?), und es kostet (wie ich grade feststelle) gerne mal 5-10ms, die woanders besser angelegt sind. Zumal die Festlegung des »start-Ankers« logisch davon unabhängig ist, von wo aus das läuft; und wenn man den Anker explizit auf etwas setzen möchte, dann soll man ihn eben explizit setzen.

Genau so einen Fall beobachte ich grade:

  • es gibt einen weiteren Bug, weshalb der Meta-Job zunächst einmal leer durchfällt
  • demzufolge wird außer dem aktuellen Job nichts „abgeworfen“
  • und dann fährt der Scheduler überhaupt nicht hoch
  • und eine Logik, die letzteres vorraussetzt, läuft dann in den Deadlock ��

...für den internen Memory-Pool. Der kann zwar wachsen (was dann aber ggfs. blockt). Und: der Pool schrumpft nie!

...unterscheiden sich doch im Detail

  • einmal ist es der Scheduler selber, dessen Funktionalität abstrahiert als ExecutionCtx bereitzustellen ist
  • die WorkForce hingegen ist konzeptionell selbständig, wird aber vom Scheduler übernommen und gezielt parametrisiert, einschließlich der Abstraktion doWork(), welche dann doch vom Scheduler selbst zustammengestellt wird
  • nochmal anders ist die Situation beim LoadController, der so etwas wie die Steuerzentrale darstellt: konzeptionell ist er total vom Scheduler abhängig, aber seine Implementierung ist entkoppelt, wiewohl in beide Richtungen verdrahtet mit wechselseitiger Abhängigkeit. In diesem Fall muß also ein Abhängigkeitszyklus durchbrochen werden

Schwere Geburt...

Irgendwie hatte sich bei mir die Idee mit den Lambdas so festgefressen — obwohl ich doch schon eingesehen hatte, daß C++ in der Hinsicht (aus gutem Grund) sehr konsequent ist, und keine Hintertür bietet, um Laufzeit-Bindigns in die statische Ebene „zu schmuggeln“. Was mich dann aber auch extrem gestört hat, waren die std::function-Felder, in die man die Lambdas speichern müßte. Es wäre nämlich eine andere Lösung denkbar, bei der diese Function-Objekte schon in der Basis-Config für die WorkForce enthalten sind; damit wäre das Design auch komplett festgelgt (und der Test würde sogar einfacher). Trotzdem habe ich eine Abneigung gegen diese Function-Objekte, weil sie eben komplett unnötig sind, da es sich effektiv um ein statisches Binding handeln sollte. Und das war dann das rettende Stichwort: dann definiert man eben diese Binding-Forwarder als statische Funktionen in einer nested Class, und macht die Rückreferenz auf den Scheduler explizit. Und mit Aggregat-Initialisierung kann man dann sogar noch die Definition des Konstruktors auslassen

kann um fünf Ecken gehen; auch dispatchNotify landed irgend wann bei postDispatch

die Funktion heißt scatteredDelay() — aber sie kombiniert hier ziemlich übergreifende Funktionalität....

und nach dem Dispatch wäre das now und die headTime  grob falsch

Performance:

wie aufwendig ist der Zugriff

auf die high-resolution-Time?

Es ist nämlich überhaupt noch nicht klar, ob ich hier ein INTERNATIONALES oder ein NATIONALES Desaster programmiere, oder ob der Code bloß peinlich ist

zum einen kann das auch aus dem Activity-Dispatch kommen, und außerdem ist das genau die Bedeutung, die SKIP ausdrücken soll...

Ich programmiere hier unter der Annahme, daß es sich um »performance-relavanten« Code handelt, und deshalb verwende ich nur bestimmte Konstrukte, und vermeide andere Konstrukte. Das alles bewirkt eigentlich bereits viel zu viel Trickserei, die noch nicht durch Empirie gedeckt ist

...das ist ein kleiner Kniff und führt dazu, daß ein Thread ohne weiteres in einem Schwung alle Management-Aufgaben erledigt, bis er durch eine reguläre Transaktion in den Work-Modus geht, oder eben nichts mehr unmittelbar zu tun ist. Für die logische Konsistenz ist diese Ausnahme nicht notwendig, aber sie verhindert in einigen Fällen eine unnötige read-write-barrier und arbeitet weiter aus dem Cache der jeweiligen Core. Cache-Effekte können locker mal 50-100µs kosten

siehe der quer-verlinkte Test;

Normalerweise sollte das keine Gefahr darstellen, da ja targeted-sleep  nun eben explizit das Grooming-Token droppt. Gefahr bestünde nur dann, wenn eine interne Aktivität im Management-Modus eine Arbeits-Schleife aufmachen würde.

sonst könnte der Scheduler zwischendurch

 für Zeitspannen bis zu 20ms geblockt sein

...denn typischerweise passiert ja ein targeted sleep genau dann, wenn grade eben nichts zu tun ist; aber ein Teil dieser Verzögerun könnte eben doch über eine Zeitspanne reichen, in der Activities geplant sind (oder zwischenzeitlich eingespielt wurden)

passiert tatsächlich in SchedulerCommutator::postDispatch()

weil beide letztlich auf den unterliegenden Systemcall gehen; die Linux-Systemclock liefert bereits Nanosekunden-Auflösung

Erläuterung:

Die gesamte Entscheidungs-Logik baut auf dem Head der Priority-Queue auf. Sofern man aber das GroomingToken nicht erlangen kann (oder will), gehen neue ActivationEvent erst einmal in die Lock-free Instruct-queue. Sie werden von dort nebenbei mit weiterbefördert, und zwar immer dann, wenn jemand grade das Grooming-Token hat und sich an den Queues zu schaffen macht. Leider ist dieser Umstand nur garantiert der Fall im Scheduler-»Tick« — wenngleich auch im regulären Betrieb fast immer irgendwo was „unterwegs“ sein sollte, und deshalb auch das nebenbei Auskehren, praktisch immer funktioniert. Aufgefallen war mir das Problem nur in einem künstlichen Test-Setup SchedulerService_test::scheduleRenderJob(), wo ich die Worker bewußt außer Gefecht gesetzt habe. Aber es gibt eben durchaus mögliche Umstände, wo das auch passieren könnte:

  • wenn jemand ganz von Außen etwas einstellt
  • wenn grade ein anderer Worker mit Management-Aufgaben beschäftigt ist, aber am Auskehren der Eingangsqueue bereits vorbei ist.

Grade in letzterem Fall könnte es passieren, daß das dummer Weise die letzte Aktion ist, und dann eine lange Pause kommt, bis zum nächsten Tick. Man könnte zwar so etwas leicht beim Einplanen neuer Jobs kompensieren, aber das ist genau die Art von heimtückischer Verkoppelung, die ich um jeden Preis zu verhindern suche.

....also kein Locking etc; allerdings greift der LoadController jetzt per Lambda darauf zu.

ActivityLang gehört sinnvollerweise in den Scheduler

es genügte, an diesen Stellen die Ausführung der abstrahierten Aktionen zu loggen

der Scheduler selber kann diese Rolle generisch übernehmen

  • eine Subklasse, die aber private vom Scheduler erbt
  • sie bietet selber eine Downcast-Accessor-Funktion
  • eigene Datenfelder sind nicht erlaubt (Slicing)
  • aber beliebige Member-Funktionen, die sich frei aus dem (protected)-Scope des Schedulers bedienen können

...speziell wenn man λ-post als Funktor implementiert, bekommt man keinen this*

...entspricht außerdem dem generellen Schema, nach dem alle »downstream« benötigten Argumente durch die ganze Kette möglichst in gleicher Reihenfolge durchgegeben werden.

und ich habe im Scheduler nur noch λ-post als universellen Eingang

  • weil sie die Ausgabe aufbläht
  • weil sie i.d.R. zufällig ist
  • es gibt ja noch util::showPtr()
  • und in format-cout hab ich's drinnen gelassen (da möchte man es normalerweise schon sehen)

...denn sonst würde in jedem Fall der Zugriff komplizierter; zwar handelt es sich um ein Implementierungs-Detail, aber im ScheudlerService selber darf es durchaus Verkopplung auf die Implementierung geben

Wir sind hier in einem »inline«-Universum, und die wenigen Verwendungen machen entweder direkt die bool-conversion oder speichern die Datenwerte irgendwo auf den Stack. Da is nix „schwergewichtig“

neuer Haupt-Eingang: postChain(ActivationEvent)

...im Lastbetrieb möchte man das eigentlich auch nicht: ein ankommender Thread soll gemäß aktueller Situation klassifiziert werden, aber nicht erst mal um das Grooming-Token konkurrieren

....und das würde in dem Fall das Problem durch den ersten »Tick« nebenbei beheben — aber nicht, falls der Scheduler bereits läuft und leer gefallen ist

Ich finde dieses Problem ziemlich ärgerlich, und würde es am liebsten „unter den Teppich kehren“

Warum?

  • wie gesagt, im normalen Betrieb wird nebenbei die Eingangsqueue mit bedient
  • wenn man den Scheduler neu startet, läuft gleich einmal der »Tick« und bedient ebenfalls diese Queue
  • das Problem tritt also nur auf, wenn der Scheduler schon läuft, aber grade idle fällt
  • und auch nur dann, wenn man neue Tasks von außen einstellt, nicht aus einem Management-Job

Und — last but not least — die Lösung ist hässlich und redundant; effektiv müßte der Check in jedem Aufruf der Work-Function laufen, und würde auch das Grooming-Token benötigen....

Aber letztlich:  Augen zu und durch ! So eine Falle darf nicht in der Logik bleiben. Der Scheduler ist so komplex, daß ich jetzt Wochen gebraucht habe, um alle Zusammenhänge herzustellen, und der Code ist extra mit vielen Abstraktionen so geschrieben, daß man immer alles im Detail verstehen muß. Eine solche Lücke würde das untergraben. Daher baue ich jetzt den Fix unauffälig in die Kapazitäts-Behandlung mit ein. Und zwar sowohl pre-Dispatch, alsoauch post-Dispatch — denn auch bei letzterem hätten wir noch die gleiche Lücke (wenn jemand pre-Dispatch das Grooming-Token nicht erlangen kann)

Habe einige Zeit gezögert, aber nun werden die Verknüpfungen definitiv zu vielfältig. Also stellt der EngineObserver so etwas wie einen »Rück-Kanal« bereit. Leider stellen sich hiermit erneut die gesamten (hinlänglich bekannten) Probleme bezüglich der Domäne eines Benachrichtigungs-Systems....

  • Zu welchem Grad legt das System die Kommunikation fest?
  • Wie offen oder verbindlich ist der Kreis der Teilnehmer?
  • Wie stark vorherbestimmt sind die Nachrichten-Inhalte?
  • Wieviel Payload ist notwendig?

hier könnte man named arguments gebrauchen....

Parametrisierung: maxCapacity = work::Config::COMPUTATION_CAPACITY

schwierige Frage....

  • man würde eine subtile Falle vermeiden
  • man würde point-and-shot-API bekommen
  • aber eben auch eine ganz starke Kopplung auf Implementierungsdetails

...weil wir im Moment immer mit Vollast einsteigen und dann zwar im Idle-Fall herunterregeln; letzteres passiert typischerweise aber schnell. Also im Test sehe ich daher immer nur 1.0 oder keine Last

...das kann aber erst beurteilt werden, wenn wir echte Last-Szenarien kennen. Denkbar wäre, daß z.B. ein real-Time Render in etwa mit 3 Cores über die Runden kommt

  • erst einmal schon kommt man an den Wert nicht ohne Weiteres ran: das verdammte Ding läuft nämlich concurrent. Und ist performance-kritischer Code
  • und dann ist noch gar nicht klar, was dieser Wert überhaupt aussagt, abgesehen von "total überlastet"
  • was ich bräuchte wäre auch ein Dämpfungs-Faktor und ein Bezugspunkt

 Exponential MA:

Grundidee ist, daß man nach N Schritten nicht den exakten alten Wert abzieht (wie für lineares MA), sondern diesen durch den aktuellen MA ersetzt/abschätzt. Das heißt, wir berechnen ein gewichtetes Mittel mit (N-1) mal dem aktuellen MA, und 1/N mal dem neuen Beitrag

 mean ≔ mean · (N-1)/N  + newVal/N

⟹ das läßt sich in einen Faktor verwandeln, wenn man den neuen Beitrag relativ zum aktuellen MA setzt

 mean ≔ mean · 1/N · ( N-1  + newVal/mean)

Bei einem moving-Average interessiert es uns eigentlich nicht, wann genau der Wert gilt und für wen er gilt. Es interessiert uns nur, was am Ende rausgekommen ist, bzw. wie der Trend im Moment so ist

wir ziehen das aktuelle MA »jetzt«

aber wir rechnen den sich ergebenden Faktor

»irgendwann eventuell« in das MA ein

und das läßt sich mit zwei Atomic-Operationen realisieren

wenn man auch nur ansatzweise bedenkt, wie Assembly funktioniert

Microbenchmark der Meß-Funktion

Scheduler::connectMonitoring()

um das System nicht unnötig zu belasten

bedeutet ⟹ ein momentan nicht benötigter Worker darf duchaus mal 200µs schlafen

Das bedeutet: bei nur vereinzelt vorhandenen Tasks dürfen diese nicht zufällig verteilt werden (sonst bleiben alle Worker am Leben). Vielmehr muß bevorzugt derjenige den nächsten Task bekommen, der ohnehin  grade gearbeitet hat

Idler: findet direkt beim Eintritt in pullWork() nichts zu tun vor

Worker: findet nach einem Work-Zyklus nichts zu tun vor

wenn sich der Tick nicht mehr erneuert,

ist der Scheduler stehen geblieben

...weil der water-level ein internes Implementierungsdetail von Layer-1 ist, und der eigentliche Dispatch mit "now" erfolgt. Möglicherweise hab' ich mich da verrant — andererseits wollte ich ganz explizit nicht überall und in jeder Activity auch noch eine Startzeit mitschleppen, sondern habe mich darauf verlegt, diese Information kontextuell zu handhaben

...und ich hatte kürzlich noch solche Zweifel ob das Design komplett entgleist, habe mich dann aber entschieden, locker zu lassen

...diesen beobachten wir nun ohnehin, um einen Load-Indicator zu synthetisieren. Und zwar per moving-Average; neue sample-Werte werden concurrency-safe eingearbeitet, immer wenn ein Worker nach weiterer Arbeit fragt

damit im Schnitt jedes 1/Nte - Warte-Intervall sich ein Worker meldet

denn: innerhalb der zeitnah-Phase wird jeder verfügbare worker per gezieltem Schlaf auf die nächste headTime gesetzt, und damit ist er bis dahin geblockt. Also sollte es extrem unwahrscheinlich sein, daß inzwischen so kurzfristig noch was dazwischen geplant wird

....denn dann kommen die periodischen Worker sofort zum Zug

sent to work

reserved for next task

awaiting imminent activities

capacity for active processing required

typical stable work task rhythm expected

time to go to sleep

man schickt einen Worker in »scattered delay«

Die Zeitangaben im std::chrono-Framework reichen bis in den Nano-Bereich, und es gibt einen high-precision-Timer

...andererseits weiß ich, daß man schon einfachste Scheduling-Delays ab mindestens 400ns mißt, und daß das Starten eines Thread auf meinem System mindestens 100µs braucht. Der aktuelle Scheduler unter Linux (CFS) verwendet keine festen Time-Slices mehr, aber man versucht definitiv die Kontext-Switches zu minimieren. Latenzen oder Scheduling-Zyklen für normale (nicht-realtime)-Prozesse liegen im Bereich von Millisekunden. Andererseits arbeitet die C++-Chrono-Funktion sleep_for nachweislich im Schnitt bis in den zweistelligen µs-Bereich genau

die Funktion liefert eine Duration

...und spart einen zusätzlichen clamp-Schritt und den Absolutwert (den das OS sowiso machen wird)

es ist nicht auszuschließen, daß gewisse End-Bits häufiger auftreten, weil die OS-Aufrufe / Scheduler-Aktivitäten eben doch mit einer gewissen Regelmäßigkeit stattfinden

Im Zusammenhang mit dem JobTicket (in der Tat, da braucht man einen ordentlichen hash-Key aus beliebiger Zeit)

Siehe TimeValue_test::checkTimeHash()

die WORKTIME liegt nämlich grade jenseits des WORK_HORIZON, also der NEARTIME, aber noch innerhalb des SLEEP_HORIZON. Aber genau dieser Zwischenbereich ist der wichtigste Bereich überhaupt, denn über ihn wird die meiste Kapazität umverteilt werden.

...es ist ein Horizont, d.h. eine Grenze und eine Dimension...

...man könnte daraus ehr schließen, daß NOW_HORIZON inkonsistent benannt ist

in dem Sinn, daß es ein Horizont und eine Dimension ist; außerdem ist „now-horizon“ sprachlich ungelenk

...auch das eine Angleichung, schließlich wird für IDLEWAIT stets activity::WAIT signalisiert, d.h. der Worker wird schlafen gelegt

...und zwar deshalb, weil wir auf IDLEWAIT stets mit dem activity::WAIT-Ausgang reagieren, d.h. wir machen keine zufällige Redistribution; wenn man das so nicht wollte, müßte man eine weitere Kategorie einführen, die dann nur für die outgoing-capacity verwendet wird — das wäre denkbar, bedarf aber emprischer Untermauerung

wichtig falls der Scheduler leer fällt

d.h. was grade eben die Deadline „reißt“, kann auch von niemandem mehr gestartet werden

stelle fest: das wirkt (unbeabsichtigterweise) auch als Zünd-Barriere

Scheduler ⟶ laufend und genügend Kapazität bereitgestellt

warum....? weiß nicht, Bauchgefühl.

Ich möchte nicht mit Einzelfall-Analysen belegen, wann der Scheduler gestartet werden soll. Der Ruhe-Zustand, wie auch der Neuanlauf sollten von außen praktisch nicht erkennbar sein (bis auf die Verzögerung)

  • hab mir den jeweiligen Quellcode angeschaut und keine Probleme gesehen
  • auch auf Stackoverflow gibt es Hinweise, daß das so gehen soll (obwohl es kein richtiges API dafür gibt)
  • Timing-Messungen zeigen: braucht 100µs
  • danach kommt die gleiche Scheduler-Instanz wieder auf die Beine und verhält sich unauffällig.
  • auch ist der erneute Start nicht feststellbar langsamer; das Hochfahren der WorkForce braucht ohnehin 1ms

...denn wir wissen grundsätzlich nie, wieviel Kapazität wir „in der Rückhand“ haben; nur falls sich das grundsätzlich ändert, könnte man über derartige Priorisierungen nachdenken. Das wäre durchaus möglich, würde aber mit dem Design-Prinzip dieses Schedulers brechen: daß nämlich die Worker aktiv sind und nebenbei die Administration mit erledigen, und daß Kapazität über mehrere Stufen „fließt“

weil bei etwa 100µs die Granularität des OS-Schedulers liegt; man kann zwar deutlich kleinere Schlaf-Zeiten angeben und bekommt dann auch was Kürzeres und zwar im Schnitt. Aber im Einzelfall bekommt man dann eben doch x-mal wieder etwas über 100µs. Gemäß Zielabwägung habe ich daher die Grenze für spinning yield-wait auf 50µs gesetzt, denn wenn's mehr als 50µs sind, dann ist es auch schon egal wenn wir über 100µs bekommen

Ein einfaches Schedule läßt sich damit ohne weiteres aufbauen und betreiben; die Probleme beginnen, wenn dieser Plan unzulänglich ist oder in der Ausführung durch Verzögerungen bricht. Dann erweist es sich als schwierig, das Grooming-Token korrekt (und effizient) zu handhaben — und es kann im Extremfall zur Korruption im Memory-Management kommen

dies folgt aus der Eigenschaft, daß wir (bisher) eine Abkürzung an der Queue vorbei nehmen, wenn ein Job bereits hinter seiner Startzeit liegt — denn im Zuge der normalen Job-Verarbeitung wird der »Managment-Modus« regulär verlassen, in einem expliziten Prozeß-Schritt. Zusammen bewirkt das eine Unsicherheit, ob auf Ebene des Planungs-Jobs das Grooming-Token gehalten wird, und ob es dort gehalten werden muß. Diese Unsicherheit steht im Widerspruch zum Konzept, welches diesen Belang an die Zugehörigkeit zu Zonen gebunden hat, um auf eine aufwendige, feingranulare Steuerung verzichten zu können. Und die Beobachtungen in den ersten Lasttests scheinen diese Entscheidung zu bestätigen: anscheinend kann das Prüfen und Wechseln des Grooming-Tokens zu Verzögerungen im Bereich 100µs führen (wohl durch stall und Cache-Effekte), sobald das System concurrent läuft.

möchte gefühlsmäßig trotzdem daran festhalten

...und damit auch keine Möglichkeit, auf natürlichem Weg ein zweifach unterschiedliches Verhalten anzuordnen.

stattdessen zeigen,

daß die Deadline wirkt

Grooming-Token wird nur noch hier erlangt

Haupt-Eingang: postChain()

Work-Verteilung: dispatchCapacity()

Work-Funktion: doWork()

deshalb treffen hier ein Zustand im Epochen-Pool zusammen mit einem kontextuellen Zustand aus dem Handle, welches aus Performance-Gründen eben die internen Koordinaten aus dem Epochen-Pool verdeckt einbettet

...der untere Layer kann und darf nichts wissen davon, daß sein Zustand auf einem höheren Layer in eine Abstraktion materialisiert wird. Deshalb wird er im Zuge einer Überlauf-Behandlung seine internen Strukturen reorganisieren und damit das Handle auf dem höheren Layer indirekt invalidieren

...also nicht nur in dem Fall, an dem ich das Problem entdeckt habe; generell kann das Handle durch jede Allokation invalidiert werden, denn jede Allokation kann eine Kettenreaktion mit Überlauf nach sich ziehen, und dadurch viele Handles entwerten — der Effekt ist nicht lokal

es handelt sich nämlich hier um einen hoch problematischen Durchgriff durch die Layer-Struktur des Allokators — und zu allem Überfluß auch noch mitten aus einer anderen Operation heraus als Seiteneffekt, welcher explizit nicht Teil des Iterator-Konzepts ist und daran vorbei auf das Basis-API zugreift. Mit einem Wort, schrecklich.

und zwar wenn isWrapped() und idx im oberen Bereich hinter start_

Falls die Neu-Allokation nicht so groß ist, verschiebt sich dadurch idx „nur“ in eine Epoche mit früherer Deadline. Ab dem Punkt gehen alle weitere Allokationen in diese frühere Epoche, ohne daß der Aufrufkontext davon etwas mitbekommt.

...und dann wird die Lage noch schröcklicher: entweder der Extent ist noch uninitialisierte Storage und der Positionszeiger schickt uns dann irgendwohin  und in die allgemeine Speicherkorruption, oder es handelt sich um einen alten, re-cycleten Extent, dessen Werte noch plausibel sind und zu einer funktionierenden Allokation führen, die dann irgendwann später ohne Vorankündigung mit einer neuen Nutzung überschrieben wird.

selbst-Gefährdung könnte man reparieren

 — fremd-Gefährdung nicht

wenn aber etwas passiert, sind die Folgen destruktiv und nicht zu diagnostizieren

das Weiterverwenden der Handles ist der größte Hebel, mit dem der Allocator überhaupt in eine akzeptable Perfromance-Zone kommt

aber einen Check für jeden Zugriff erfordern (teuer)

und auch steady_clock statt system_clock verwendet, sowie einen Adapter um das Test-Subjekt gelegt (um flexibler zu sein in den akzeptierten Signaturen)

commit 28b39002846aba9ef3dab18dae10f67fa8b063dd

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Sat Jul 22 01:54:25 2023 +0200

    Block-Flow: final adjustments from performance test (closes: #1311)

   

    Further extensive testing with parameter variations,

    using the test setup in `BlockFlow_test::storageFlow()`

   

    - Tweaks to improve convergence under extreme overload;

      sudden load peaks are now accomodated typically < 5 sec

   

    - Make the test definition parametric, to simplify variations

   

    - Extract the generic microbenchmark helper function

   

    - Documentation

___Microbenchmark____

noAlloc     : 57.1625

heapAlloc   : 68.9836

sharedAlloc : 243.961

blockFlow   : 386.459

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 3

alloc pool... 10

___Microbenchmark____

noAlloc     : 9.52497

heapAlloc   : 22.5513

sharedAlloc : 96.7604

blockFlow   : 11.2576

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺226ms≻

cnt Epochs... 4

alloc pool... 10

inzwischen kann die GATE-Activity den Deadline-Check für eine Epoch machen; das kann ich nicht so ohne weiteres backporten

___Microbenchmark____

noAlloc     : 58.0486

heapAlloc   : 72.2743

sharedAlloc : 231.007

blockFlow   : 245.747

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 4

alloc pool... 10

___Microbenchmark____

noAlloc     : 9.49421

heapAlloc   : 23.7542

sharedAlloc : 97.4423

blockFlow   : 10.7804

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 3

alloc pool... 10

___Microbenchmark____

noAlloc     : 0.0551005

heapAlloc   : 0.0653065

sharedAlloc : 0.222995

blockFlow   : 0.251686

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 4

alloc pool... 10

___Microbenchmark____X

noAlloc     : 0.00914207

heapAlloc   : 0.0227815

sharedAlloc : 0.0985881

blockFlow   : 0.140562

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 4

alloc pool... 10

___Microbenchmark____

noAlloc     : 0.0540774

heapAlloc   : 0.0709439

sharedAlloc : 0.226855

blockFlow   : 0.401036

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺226ms≻

cnt Epochs... 4

alloc pool... 10

___Microbenchmark____

noAlloc     : 0.00942346

heapAlloc   : 0.0220902

sharedAlloc : 0.0973145

blockFlow   : 0.130734

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 3

alloc pool... 10

sowohl die Umstellung des Threadwrappers, alsauch das automatische Adaptieren der Test-Lambdas betrifft nur die anderen Funktionen; auch damals habe ich bereits per std::Chrono gemessen, und der Unterschied system_clock vs. steady_clock scheint keinen Einfluß zu haben

bestätigt: mit komplettem Release-Build läuft auch der aktuelle Code schnell(er)

___Microbenchmark____

noAlloc     : 0.00916021

heapAlloc   : 0.0228476

sharedAlloc : 0.0984637

blockFlow   : 0.0174424

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺224ms≻

cnt Epochs... 4

alloc pool... 10

___Microbenchmark____

noAlloc     : 0.00926561

heapAlloc   : 0.0232462

sharedAlloc : 0.0964142

blockFlow   : 0.0314077

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺223ms≻

cnt Epochs... 3

alloc pool... 10

die neuerlichen Änderungen führen zu einer deutlichen Verbesserung

___Microbenchmark____

noAlloc     : 0.00921847

heapAlloc   : 0.0234061

sharedAlloc : 0.0969127

blockFlow   : 0.0240378

_____________________

instances.... 360000

fps.......... 200

Activities/s. 2000

Epoch(expect) ≺225ms≻

Epoch  (real) ≺226ms≻

cnt Epochs... 4

alloc pool... 10

jetzt (nur noch) +22% Performance-Gewinn

...ich hatte diesen Fix nur oberflächlich getestet, und dabei übersehen, daß eine Assertion ansprechen kann (sogar sehr wahrscheinlich einmal ansprechen wird, sobald der Reparatur-Mechanismus eine größtere Strecke zurücklegt). Das ist aber kein Bug im eigentlichen Reparatur-/reLink-Mechanismus; dieser funktioniert präzise, wie ich nochmals im einzelnen mit dem Debugger nachvollziehen konnte.

wenn eine Deadline überfahren wurde, ist ein weiterer Zugriff auf den Extend als _undefined behaviour_ zu betrachten. Das gilt auch für das AllocatorHandle, das man früher mal für eine bestimmte Deadline bekommen hat; dieses kann man sehr wohl weiterhin verwenden (solange die Deadline noch in der Zukunft liegt). Konkreter Fall: später noch eine Dependency anhängen. Wenn der Anker dieser Dependecy zu dem Zeitpunkt bereits ausgeführt oder invalidiert wurde, ist man selber schuld!

Payload: ActivationEvent

zwar hätte man aus logischer Sicht gerne, daß alle Informationsfunktionen stets den aktuell gültigen Zustand reflektieren — aber das scheitert an der Natur einer Priority-Queue (welche nicht insgeheim mutable sein kann). Praktisch wirkt sich eine solche Diskrepanz aber fast nicht aus, denn das Ergebnis einer Informationsfunktion kann ohne Besitzt des Grooming-Tokens stets im nächsten Moment schon obsolet sein. Man muß nur aufpassen, daß im Fall einer echten Mutation die Bereinigungs-Funktion vor der Entscheidungslogik aufgerufen wird

Implementierung:

»zusätzliche Markierungen«

Habe im Vorgriff diverse Bedenken

  • eine Priority-Queue vertauscht Elemente; die Kosten dafür steigen mit der Größe
  • Spielt es eine Rolle, wenn Allokationen nicht eine 2-er-Potenz sind?

übergreifendes Thema: fliegende Änderungen in Play-Prozessen

brauche generische Informationsfunktion isOutdated()

  • ein solcher Task würde immer ausführbar bleiben und wäre somit für den Layer-1 neutral.
  • allerdings auf dem Scheduler-API muß die Deadline verpfichtend gemacht werden, denn sonst erstickt der Memory-Allocator irgendwann

diese spezielle Marker-ID wird implizit stets freigeschaltet, kann also später nicht verworfen werden; das erscheint mir ein sinnvoller Default

verpflichtende Activities sind ebenfalls ein Feature, das eigentlich erst auf Scheduler-Level angesiedelt ist, und hier nur den entsprechenden Support braucht

das heißt, das ist latent ein Design-Problem — welches ich derzeit nicht lösen kann, da mir der Gesamtüberblick noch fehlt, und ich genau deshalb dieses offene Design gewählt habe

kann es auch gar nicht, denn die Activity selber kennt i.A. keine Deadline

Umbau: Dispatch-mit-Kontext

der Queue-Entry wurde zum ActivationEvent als zentrale Austausch-Einheit

also gehört diese Operation in Layer-2

damit: direkt in findWork(now)

prüft explizit auf isOutdated (now) and not isOutOfTime(now)

...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

Denn jeder Aufruf des Fork-Funktors wird unterschiedlich lang dauern. Und selbst wenn alle Worker idle sind: dann gibt es bei der ersten Runde die Mega-Contention, und danach liegen die Wartezyklen aller Worker leicht gestaffelt. Contention wäre nur dann ein Problem, wenn jemand, der arbeiten könnte, vom Arbeiten abgehalten wird.

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)

das Erlangen des Token und die damit verbundene Barrier können durchaus 30µs Verzögerung für das ganze System bedingen. Auch die reinen Lesezugriffe werden unter Contention langsamer (was vermutlich an der x86-Architektur liegt)

....und dieser Zustand kann schon mal wenige ms lang andauern

Die Konstanten sind derzeit fest eincompiliert, aber man kann die Wirkzonen und die Stärke des Effekts in weiten Grenzen steuern

    const size_t CONTEND_SOFT_LIMIT  = 3;                         ///< zone for soft anti-contention measures, counting continued contention events

    const size_t CONTEND_STARK_LIMIT = CONTEND_SOFT_LIMIT + 5;    ///< zone for stark measures, performing a sleep with exponential stepping

    const size_t CONTEND_SATURATION  = CONTEND_STARK_LIMIT + 4;   ///< upper limit for the contention event count

    const size_t CONTEND_SOFT_FACTOR = 100;                       ///< base counter for a spinning wait loop

    const size_t CONTEND_RANDOM_STEP = 11;                        ///< stepping for randomisation of anti-contention measures

    const microseconds CONTEND_WAIT = 100us;                      ///< base time unit for the exponentially stepped-up sleep delay in case of contention

...bis die Situation in der Engine insgesamt klarer ist — das kann noch weit über das »Playback Vertical Slice« hinaus dauern, bis wir tatsächlich ein dynamisches Geschehen beobachten können

...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...

...wie so oft, vor allem getrieben durch die Tests: die Layer wurden zu einer Sammlung von Implementierungs-Bausteienen, welche auch aufeinander aufbauen. Aber die große Verdrahtung habe ich noch vor mir hergeschoben — sogar so weit, daß Layer-2 selbst keine Referenz auf Layer-1 besitzt (sondern diese für jede Funktion hereingereicht bekommt). Damit wird die Verdrahtung nun sternförmig, und der Binde-Code ist das, was den »Scheduler-Service« ausmacht

...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
  • das Grooming-Token wird stets on-demand im Scheduler-Code erlangt
  • Worker rufen diesen Code über die work-Function auf
  • diese hat einen maximal generischen catch(...)- Handler
  • 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

Weitere Beobachtungen im Scheduler ergaben, daß das polling  tatsächlich gefährlichen Overhead produziert; zudem ist es nach aktuellem Stand grundsätzlich nicht notwendig, da alle Gate-Änderungen per Notification kommen (und dann direkt durchsteuern können)

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()

Und zwar weil wir

  • ein Moving-Average im Spiel haben, das es sehr schwer macht, bestimmte Werte zu erzielen
  • das Moving-Average in einer privaten Variablen steckt (ist auch gut so, da es ein Atomic sein muß wegen concurrency)

Alternative wäre, den Test zum Friend zu machen. Das will ich aber möglichst nicht zur Gewohnheit werden lassen. Hier könnte man das umgehen, da wegen der Concurrency ein Setter sogar sinnvoll ist: der würde dann einen atomic-swap machen

...und das erscheint adäquat für moderne Maschinen

also optimized vs. Debug-Build; ja man sieht diesen Unterschied im "slip", weil einfach die Verarbeitung länger dauert

habe generell hier den Fall der leeren Queue nicht bedacht

...und das ist so auch plausibel (interessanterweise gibt es hier keinen Unterschied debug/O3 )...  hier wird ein Vector befüllt und jede Menge String-Konvertierungen gemacht, und auch ein virtual call

das bedeutet: in diesem Fall liegt der überwiegende Teil des extra-Aufwands vermutlich vor der Activity-Activation (weil die bekanntermaßen aufwendigen Log-Operationen erst nach dem Eintritt in die activation-Funktion der ActivityProbe erfolgen). ~50µs bedeutet ⟹ gut die Hälfte wird außerhalb aber im Dispatch verbrannt

...wir sind noch nicht so weit (nach Plan)

  • Grooming-Token soll nur per explizitem Übergang geDropped werden; beide Fälle sind noch nicht implementiert (work-Function und λ-work)
  • dieser Test läuft im Haupt-Thread, d.h. der erste Aufruf (in dem das post() auch direkt den Dispatch macht) erlangt das Grooming-Token

...zumal ich jetzt alles im Debug-Mode untersuche, und auch noch COUT-Statements und Clock-Aufrufe eingebaut habe

steckt in Activity::activate

steckt in Activity::callHook

|capacity=0

+++++++++++HO: 0µs

++++++++++ACT: 4µs

+++++++++Post: 10µs (0|+|10:dispatch)

+++++Dispatch: 17µs (2|+|15)

|capacity=5

res:2 delay=29µs

einziger Unterschied: Zeiten im Call-stack addieren sich weniger auf...

+++++++++++HO: 45µs

++++++++++ACT: 50µs

+++++++++Post: 52µs (0|+|52:dispatch)

+++++Dispatch: 54µs (0|+|54)

...sondern nur die wenigen Translation-Units, die ich für relevant halte. Das könnte dann insgesamt nochmal einen Unterschied machen

Optimierung hat darauf keinerlei Einfluß

ohne das wird nämlich gar kein Tick-Zyklus aufgemacht

...da ich das handleDutyCycle eben auch zum „zünden“ verwenden möchte, entsteht hier so etwas wie eine zusätzliche Zünd-Barriere; war nicht geplant, gefällt mir aber: man muß also auch Holz in den Ofen stecken, bevor man zündet...

Post+erfolgreiche Zündung: 1ms

gewaltsam Angehalten: 60ms

korrekt!

  • tend-next auf den Tick ≔ 50ms
  • WorkForce wartet beim Shutdown pauschal 20ms (IDLE_WAIT)

nochmal zünden: 1ms

Cache-Effekte können mehrere 100µs ausmchen. Dennoch ist dieses Pattern mit leichten Schwankungen reproduzierbar in vielen Läufen, und zeigt auch fast keinen Unterschied, wenn mit -O3 gebaut. Man beachte im Besonderen daß bei der ersten Fehlzündung die WorkForce nicht hochskaliert wird. Da steckt also die Zeit, und das ist auch mit dem WorkForce_test konsistent.

Fazit: Verhalten wie erwartet

  • der erste gestartete Thread macht alle Dispaches in Serie
  • danach wird er per tendNext gezielt auf den Tick +50ms geschickt
  • alle anderen Threads gehen in höchstens einen Wartezyklus
  • der WorkForce-Destruktor wartet jeweils auf den tendNext-Thread

Im Detail nachvollzogen durch Trace-Log der aktuellen Zeit, incl. Thread-Nummer

  • man konnte jeweils deutliche Cache-Effekte beobachten
  • abgesehen davon waren die Timings stabil
  • die separat im Test gemessenen Zeiten passen perfekt mit den Zeiten und dem Muster aus dem Trace zusammen
  • die Threads brauchen in der Tat eine Zeitspanne von 100-200µs, bis sie aktiv werden
  • insofern variiert es von Lauf zu Lauf erheblich, wie viele Threads überhaupt den Schlaf-Auftrag bekommen

...weil es eben ein Microbenchmark des reinen Dispatch-Aufrufs ist, und ohne das ganze Drumherum mit Grooming-Token, Queue-Verwaltung und Kapazitäts-Verteilung

Doch! spätere Messung ⟹ 3µs / Activity in einem Thread beobachtet

Aufgrund der Kapazitäts-Verteilung (genau wie implementiert) steht in einem so kurzen Peak im Schnitt nur 1/3 der Concurrency überhaupt zur Verfügung (2ms im Verhältnis zur Verteilung auf WORK_HORIZON = 5ms). Diese wenigen Threads machen aber den Cache ordentlich heiß, so daß wir sogar im Debug-Modus Werte von 3µs / Activity erreichen (also komplett Ende-zu-Ende mit allem Drumherum)

0000000599: INFO: suite.cpp:180: thread_1: invokeTestCase: ++------------------- invoking TEST: vault::gear::test::SchedulerService_test

#--◆--# wuff() ? = 9   ⟵—————————————————————————————————————————————————————————‖ current Time relative to time-axis anchor

#--◆--# wuff(scheduler.layer1_.headTime()) ? = 307445013520244830   ⟵————————————‖ not yet in the priority queue

>          createLoad (Offset{Time{5,0}}, fatPackage);

>          createLoad (Offset{Time{15,0}}, fatPackage);

#--◆--# wuff() ? = 1697   ⟵——————————————————————————————————————————————————————‖ 1.7ms

>          scheduler.ignite();

|oo|^ Sleep-->1811  <head:1811

|oo|^ Sleep-->4650  <head:1595

|oo|^ Sleep-->5705  <head:1466

|oo|^ Sleep-->3820  <head:1469   ⟵———————————————————————————————————————————————‖ 2.4ms behind head

#--◆--# wuff() ? = 3678   ⟵——————————————————————————————————————————————————————‖ ignite() costs 2ms

#--◆--# wuff(scheduler.layer1_.headTime()) ? = 5000

|oo|^ Sleep-->6265  <head:1298

3793 +++ Load: 0.274273 --- HT= 5000 -+- Lag -1141

|oo|^ Sleep-->1505  <head:1226   ⟵———————————————————————————————————————————————‖ 279µs behind head

|oo|^ Sleep-->3800  <head:1225   ⟵———————————————————————————————————————————————‖ 2.6ms behind head

|oo|^ Sleep-->6175  <head:1200

3972 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4083 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4194 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4304 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4415 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4526 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4636 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4746 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4856 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

4966 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

5076 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

5186 +++ Load: 0.271592 --- HT= 5000 -+- Lag -1141

|••|^   eep<--1811  <head:1811   ⟵———————————————————————————————————————————————‖ this one is the next-tending Thread

5297 +++ Load: 1.04 --- HT= 5014 -+- Lag 234

5410 +++ Load: 1.29 --- HT= 5058 -+- Lag 347

|••|^   eep<--1505  <head:1296   ⟵———————————————————————————————————————————————‖ this one was distributed 279µs behind head

                                 ⟵———————————————————————————————————————————————‖ now head is advanced by 1296-1226 = +70 entries

5522 +++ Load: 1.432 --- HT= 5101 -+- Lag 418

5636 +++ Load: 1.582 --- HT= 5139 -+- Lag 493

5747 +++ Load: 1.73  --- HT= 5178 -+- Lag 566

5859 +++ Load: 1.876 --- HT= 5216 -+- Lag 640

5971 +++ Load: 2.022 --- HT= 5255 -+- Lag 713

6082 +++ Load: 2.17  --- HT= 5293 -+- Lag 786

6194 +++ Load: 2.316 --- HT= 5332 -+- Lag 859

6306 +++ Load: 2.46  --- HT= 5371 -+- Lag 931

6417 +++ Load: 2.604 --- HT= 5409 -+- Lag 1004

6528 +++ Load: 2.748 --- HT= 5448 -+- Lag 1076

6640 +++ Load: 2.896 --- HT= 5488 -+- Lag 1149

6752 +++ Load: 3.038 --- HT= 5527 -+- Lag 1221

6863 +++ Load: 3.188 --- HT= 5566 -+- Lag 1295

6975 +++ Load: 3.328 --- HT= 5605 -+- Lag 1366

7047 +++ Load: 3.41  --- HT= 5627 -+- Lag 1408

7160 +++ Load: 3.574 --- HT= 5669 -+- Lag 1488

7271 +++ Load: 3.716 --- HT= 5710 -+- Lag 1558

7382 +++ Load: 3.856 --- HT= 5750 -+- Lag 1630

|••|^   eep<--3820  <head:2258   ⟵———————————————————————————————————————————————‖ head advanced by +789 entries

                                 ⟵———————————————————————————————————————————————‖ first Peak was completed here

7494 +++ Load: 3.998 --- HT= 15000 -+- Lag -327

                                

|oo|v Sleep-->7478  <head:7478   ⟵———————————————————————————————————————————————‖ tend-next to t=15ms (now = 7522ms)

|**|^ --------------------------------------------Deep-Sleep-->  <head:7477

|**|^ --------------------------------------------Deep-Sleep-->  <head:7442

 +++ Load: 0.191644 --- HT= 15000 -+- Lag -1909

|••|^   eep<--3800  <head:11225   ⟵——————————————————————————————————————————————‖ head now at start of 2.nd Peak at t=15ms

|**|^ --------------------------------------------Deep-Sleep-->  <head:7296

7760 +++ Load: 0.131337 --- HT= 15000 -+- Lag -3107

7872 +++ Load: 0.131337 --- HT= 15000 -+- Lag -3107

7982 +++ Load: 0.131337 --- HT= 15000 -+- Lag -3107

8091 +++ Load: 0.131337 --- HT= 15000 -+- Lag -3107

|••|^   eep<--4650  <head:11595

|**|^ --------------------------------------------Deep-Sleep-->  <head:6865

8201 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8313 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8424 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8534 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8644 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8754 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8864 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

8974 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

9084 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

9194 +++ Load: 0.107689 --- HT= 15000 -+- Lag -3943

9305 +++ Load: 0.107689 --- HT= 15000 -+- Lag -4331

|••|^   eep<--5705  <head:11466

|**|^ --------------------------------------------Deep-Sleep-->  <head:5688

9425 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

9536 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

9646 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

9757 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

9867 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

9977 +++ Load: 0.0993838 --- HT= 15000 -+- Lag -4331

|••|^   eep<--6265  <head:11298

|oo|^ Sleep-->7300  <head:4925   ⟵———————————————————————————————————————————————‖ t=10075µs targeted at 17375µs (head+2.3ms)

10086 +++ Load: 0.0968429 --- HT= 15000 -+- Lag -4463

|••|^   eep<--6175  <head:11200

|oo|^ Sleep-->8115  <head:4868   ⟵———————————————————————————————————————————————‖ t=10132µs targeted at 18247µs (head+3.2ms)

10221 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10332 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10442 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10552 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10662 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10772 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10882 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

10991 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11102 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11212 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11322 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11432 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11541 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11651 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11761 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

11883 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12002 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12120 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12238 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12356 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12473 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12591 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12709 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12826 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

12944 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13062 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13179 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13297 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13414 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13532 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13650 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13768 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

13886 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14004 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14121 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14239 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14357 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14474 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14592 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14709 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14827 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

14945 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -4553

15054 +++ Load: 0.0951837 --- HT= 15000 -+- Lag -3527

|••|v   eep<--7478  <head:7478   ⟵———————————————————————————————————————————————‖ this one is the next-tending Thread

15185 +++ Load: 0.883392  --- HT= 15026 -+- Lag 140

15306 +++ Load: 1.078     --- HT= 15051 -+- Lag 243

15427 +++ Load: 1.27      --- HT= 15077 -+- Lag 335

15547 +++ Load: 1.456     --- HT= 15103 -+- Lag 432

15668 +++ Load: 1.648     --- HT= 15128 -+- Lag 528

15788 +++ Load: 1.836     --- HT= 15154 -+- Lag 622

15909 +++ Load: 2.028     --- HT= 15180 -+- Lag 714

16030 +++ Load: 2.22      --- HT= 15206 -+- Lag 810

16151 +++ Load: 2.408     --- HT= 15232 -+- Lag 904

16271 +++ Load: 2.596     --- HT= 15257 -+- Lag 1002

16391 +++ Load: 2.786     --- HT= 15283 -+- Lag 1093

16511 +++ Load: 2.97      --- HT= 15309 -+- Lag 1189

16631 +++ Load: 3.158     --- HT= 15335 -+- Lag 1283

16752 +++ Load: 3.348     --- HT= 15361 -+- Lag 1378

16872 +++ Load: 3.538     --- HT= 15386 -+- Lag 1473

16992 +++ Load: 3.726     --- HT= 15412 -+- Lag 1567

17113 +++ Load: 3.916     --- HT= 15439 -+- Lag 1662

17234 +++ Load: 4.102     --- HT= 15465 -+- Lag 1755

17354 +++ Load: 4.292     --- HT= 15491 -+- Lag 1850

17474 +++ Load: 4.482     --- HT= 15517 -+- Lag 1945

|••|^   eep<--7300  <head:5454   ⟵———————————————————————————————————————————————‖ (was head+2.3ms) head now at +529 (15529)

17595 +++ Load: 4.686     --- HT= 15543 -+- Lag 2048

17717 +++ Load: 4.888     --- HT= 15567 -+- Lag 2148

17839 +++ Load: 5.082     --- HT= 15591 -+- Lag 2244

17959 +++ Load: 5.274     --- HT= 15615 -+- Lag 2342

18081 +++ Load: 5.468     --- HT= 15639 -+- Lag 2437

18202 +++ Load: 5.664     --- HT= 15664 -+- Lag 2535

18323 +++ Load: 5.854     --- HT= 15687 -+- Lag 2633

|••|^   eep<--8115  <head:5559   ⟵———————————————————————————————————————————————‖ (was head+3.2ms) head now at +691 (15691)

18445 +++ Load: 6.058     --- HT= 15711 -+- Lag 2733

18568 +++ Load: 6.25      --- HT= 15736 -+- Lag 2829

18689 +++ Load: 6.442     --- HT= 15760 -+- Lag 2927

18810 +++ Load: 6.638     --- HT= 15785 -+- Lag 3023

                                 ⟵———————————————————————————————————————————————‖ second Peak was completed here

|oo|v Sleep-->32824  <head:32824   ⟵—————————————————————————————————————————————‖ tend-next to t=51.706ms (now = 18.882ms)

|**|^ --------------------------------------------Deep-Sleep-->  <head:32824     ‖ note: all see the same head-time

|**|^ --------------------------------------------Deep-Sleep-->  <head:32824     ‖

18931 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19048 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19168 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19286 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19404 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19522 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19640 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19758 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19875 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

19993 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20111 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20234 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20352 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20471 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20590 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20707 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20825 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

20943 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21060 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21178 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21296 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21414 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21531 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21649 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21766 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

21884 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22001 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22119 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22236 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22354 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22472 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22589 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22707 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22824 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

22942 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23055 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23173 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23291 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23409 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23526 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23644 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23761 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23879 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

23997 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24114 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24232 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24349 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24467 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24585 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24702 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24820 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

24937 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25055 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25172 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25290 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25407 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25525 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25643 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25760 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25878 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

25995 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26113 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26230 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26347 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26465 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26583 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26701 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26818 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

26935 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27053 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27172 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27290 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27407 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27525 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

27643 +++ Load: 0.074162  --- HT= 51706 -+- Lag -6042

|**|^ --------------------------------------------Deep-Sleep-->  <head:24007

|**|^ --------------------------------------------Deep-Sleep-->  <head:23931

|**|^ --------------------------------------------Deep-Sleep-->  <head:23968

27761 +++ Load: 0.0353757 --- HT= 51706 -+- Lag -13434

27920 +++ Load: 0.0353757 --- HT= 51706 -+- Lag -13434

28041 +++ Load: 0.0353757 --- HT= 51706 -+- Lag -13434

28168 +++ Load: 0.0353757 --- HT= 51706 -+- Lag -13434

|**|^ --------------------------------------------Deep-Sleep-->  <head:23463

28314 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

28441 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

28565 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

28684 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

28802 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

28920 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

29038 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

29156 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

29274 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

29392 +++ Load: 0.0320636 --- HT= 51706 -+- Lag -14894

|**|^ --------------------------------------------Deep-Sleep-->  <head:22309

29520 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

29641 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

29760 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

29878 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

29996 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30114 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30231 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30349 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30467 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30585 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30703 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30821 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

30939 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31049 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31169 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31286 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31404 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31522 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31640 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31758 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31877 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

31994 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32112 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32230 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32348 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32466 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32583 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32701 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32820 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

32938 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33055 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33173 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33291 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33409 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33526 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33644 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33762 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33880 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

33998 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34116 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34233 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34351 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34469 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34587 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34704 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34822 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

34940 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35053 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35172 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35290 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35407 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35525 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35643 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35761 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35879 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

35997 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36115 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36233 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36350 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36468 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36586 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36704 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36822 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

36939 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37058 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37176 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37294 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37412 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37529 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37647 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37765 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

37883 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38001 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38120 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38237 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38355 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38473 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38591 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38708 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38826 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

38944 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

39054 +++ Load: 0.0298882 --- HT= 51706 -+- Lag -16029

|**|^ --------------------------------------------Deep-Sleep-->  <head:12672

|**|^ --------------------------------------------Deep-Sleep-->  <head:12640

39201 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39324 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39444 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39565 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39685 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39806 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

39926 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40046 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40166 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40285 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40406 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40526 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40647 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40767 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

40887 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41006 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41124 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41243 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41363 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41481 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41601 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41721 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41841 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

41961 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42080 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42200 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42320 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42440 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42560 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42679 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42800 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

42919 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43055 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43176 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43296 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43417 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43537 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43657 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43777 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

43897 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44016 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44135 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44254 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44374 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44495 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44615 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44733 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44852 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

44971 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45090 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45209 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45328 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45447 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45566 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45685 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45804 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

45922 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46041 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46159 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46278 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46397 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46516 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46635 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46754 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46873 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

46992 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47111 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47231 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47350 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47469 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47589 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47710 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

47828 +++ Load: 0.032476 --- HT= 51706 -+- Lag -14696

|oo|^ Sleep-->7711  <head:3832   ⟵———————————————————————————————————————————————‖ t=47.874ms targeted at head+3.3ms

|oo|^ Sleep-->6071  <head:3824   ⟵———————————————————————————————————————————————‖ t=47.882ms targeted at head+2.2ms

47948 +++ Load: 0.051878 --- HT= 51706 -+- Lag -8938

|oo|^ Sleep-->5496  <head:3809   ⟵———————————————————————————————————————————————‖ t=47.897ms targeted at head+1.7ms

48091 +++ Load: 0.051878 --- HT= 51706 -+- Lag -8938

48211 +++ Load: 0.051878 --- HT= 51706 -+- Lag -8938

|oo|^ Sleep-->5461  <head:3382   ⟵———————————————————————————————————————————————‖ t=48.324ms targeted at head+1.6ms

48330 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

48477 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

48597 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

48717 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

48837 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

48957 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

49079 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

49198 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

49317 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

49436 +++ Load: 0.0594955 --- HT= 51706 -+- Lag -7704

|oo|^ Sleep-->5366  <head:2223   ⟵———————————————————————————————————————————————‖ t=49.483ms targeted at head+3.1ms

49558 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

49678 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

49796 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

49914 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50032 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50150 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50268 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50393 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50511 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50629 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50747 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50865 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

50983 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51102 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51220 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51338 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51456 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51574 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

51693 +++ Load: 0.0695797 --- HT= 51706 -+- Lag -6486

|••|v   eep<--32824  <head:32824   ⟵—————————————————————————————————————————————‖ was tended to the »Tick« (from t=18.882ms)

51812 +++ Load: 0.0873668 --- HT= 307445013520244830 -+- Lag -5023

|oo|v Sleep-->6855  <head:307445013520193000   ⟵—————————————————————————————————‖ outgoing redistributed +6.9ms (before big sleep)

|••|^   eep<--5496  <head:307445013520196933

|••|^   eep<--5461  <head:307445013520196506

|••|^   eep<--6071  <head:307445013520196948

|••|^   eep<--5366  <head:307445013520195347

|••|^   eep<--7711  <head:307445013520196956

|••|v   eep<--6855  <head:307445013520193000

|**|^ --------------------------------------------Deep-Sleep-->  <head:307445013520185675

++++ WorkForce shut down by destructor ++++

Und zwar weil für jeden targeted-Sleep und jeden Rückgabewert != PASS ja doch automatisch das Token gedropped wird. Deshalb ist nicht einmal der »Tick« ein Problem (ja der droppt es nicht). Nur wenn eine Interne Verarbeitung in Schleifen gehen würde, hätten wir ein Problem. Dazu müßte sie sich selbst ohne Zeitversatz wieder einplanen (bzw. die Scheduler-Zeitachse müßte geflutet sein)

Fazit: works as designed

die Kapazitäts-Verteilung wurde expizit geprüft und arbeitet sehr gut

  • das gesamte Verhalten ist wie definiert
  • selbst unter den hier ungünstigen Umständen, wo Threads bündelweise in den deep-sleep geschickt werden, erfolgt spätestens bei Annäherung an den nächsten Peak eine Umverteilung

weil das Beladen und Starten des Schedulers erhebliche Zeit kostet (und nicht präzise ist)

Aus Sicht des Testaufbaues wäre es sehr wünschenswert. Aber da das Scheduler-API high-Level ist, sehe ich keine einfache Möglichkeit, für einen Test dazwischen zu gehen. Ein Mocking / Austauschen des Scheduler wäre hier nicht zielführend (weil es genau auf die Interaktion mit der integrierten Scheduler-Implementierung ankommt).

wie im Beispiel im Test: seed=62 ⟹ sehr schnell ein Expand und dann mehrere massive Wellen die bis an den Anschlag gehen

...denn normalerweise ist ein calibrate() ganz verträumt   (und sicher nicht zufällig) im ersten simpleUsage-Example versteckt

angeblich um zu demonstrieren, daß der Hash gleich ist...

bis zur Hälfte ist genügend »Luft«,

danach muß so gut wie möglich parallel gearbeitet werden,

und trotzdem wird das vorgegebene Schedule deutlich überfahren

2023-09-07 17:15:25

Workforce: configuration and initialisation of workers

es paßt nicht zum Stil im gesamten Kontext; wir sind hier in Implementierungs-Code, der grundsätzlich davon ausgeht, nur sinnvoll aufgerufen zu werden — es geht nicht darum, einen User „an die Hand zu nehmen“

ist das angemessen,

was ich hier mache?

so manches an diesen Tests hier funktioniert nur scheinbar „einfach“

Einen komplett leeren Job-Funktor wiederholt back-to-back (ohne Wartezeit) aufrufen; dabei das System nicht in die Sättigung treiben

z.B. 7 Cores auf meinem 8-Core-System — damit die sonstigen Sytem-aktivitäten nicht zu Stau-Zuständen führen

immer nur so viele Jobs einstellen, daß eine Epoche nicht ganz voll wird; aber die Startzeiten extrem dicht zusammen legen, so daß im alles effektiv sofort berechenbar wird; dann kann man sinnvoll messen, wenn alles abgearbeitet ist: dazu einen Zeitmesser-Job verwenden; man kann die Zeitmessung vor oder nach dem Einstellen der Jobs starten

Bei diesem Ansatz wird eine Dauerlast erzeugt, indem immer ein Planungs-Chunk eine Zahl an Jobs erzeugt, die im Verhältnis zur Epochen-Größe steht (dabei kann auch durchaus auf eine Überlastung des BlockFlow-Allokators abgezielt werden, indem man immer das N-Fache der Block-Kapazität einstellt). Diese Jobs werden getaktet gemäß der aktuellen Dichte, aber diese Dichte wird langsam gesteigert, bis ein Feedback-Signal auftritt: nämlich wenn der reale Zeitpunkt des nächsten Planungs-Chunks verspätet ist, dann elastisch zurückregeln. Bei richtiger Abstimmung sollte sich dann ein »Lockstate« einstellen, von dem man einen Meßwert für die erreichbare Dichte ablesen kann

Fazit: ist direkt nicht möglich

Sollte ausgeführt werden wie der normale Leerlauf-Stresstest, aber nun mit zusätzlichen und zunehmend exzessiven Dependencies/Notifications in der kette; beobachtet wird die Degeneration der Leerlauf-Kapazität

Anders als in den Leerlauf-Tests wird hier eine überprüfbare Berechnung ausgeführt, die aber auch Dependencies mit einschließt. Auch unter Vollast muß die Prüfsumme korrekt sein. Für diesen Test könnte man das System sogar kurzzeitig extrem in die Sättigung treiben (und trotzdem muß das Ergebnis stimmen)

Es wird aber kein Play-Prozess gebaut, sondern die entsprechende Funktionalität direkt aus dem Test aufgebaut

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

EMA(i) = α · value(i) + (1- α) · EMA(i-1)

Im Grunde gibt es keine Herleitung, denn das ganze Verfahren ist pragmatisch.

In meiner Rationalisierung wäre dann α ≔ 1/N  ⟹ 1-α = (N-1)/N

In der Wirtschaft (Chart-Analyse) findet man oft α ≔ damp / (N+1)  und damp ≔ 2 ist die geläufige Wahl. Damit kommt man auf die oft zitierte Faustregel, daß ein neuer Wert in einen 10-day EMA mit 18% eingeht (2/11)

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...

Idee: Angabe in »zusätzlichen Frames-per-second«

wende wieder die gleiche Technik an, die ich für das ZoomWindow erfolgreich entwickelt hatte...

  1. feststellen, um wie viele Stellen die Zahl zu groß ist (binär-logarithmus)
  2. den Nenner um diese Zahl nach rechts schieben ⟹ neuer quantisierer
  3. mit diesem die Zahl re-quantisieren
  4. daraus ein boost::rationall<uint>
  • aktuelle Framerate += zusätzliche Framerate
  • Framerate ⟹ neue Epochen-Dauer, setzen!

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

es gibt Situationen, in denen das Schedule »gebrochen« wird, und das führt u.U. zum Deadlock bzw. dazu, daß ein Berechnungvorgang stillschweigend stecken bleibt; diese Situationen können relativ leicht auf einem unteren Level im Code erkannt werden, aber von dort ist es nicht einfach, einen Alarm auf einem globalen Level auszulösen

Aufruf  aus der Berechnung heraus möglich, d.h. darf höchstens ein paar µs kosten

Empfang/Zustellung laufen single-Threaded

LeitbildTimingObservable

EngineEvent-Basisstruktur

Größenbeschränkung: 4 »Slots«

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)

...war nichts Grundsätzliches ... die Logik wollte nur zu viel mit zu geringen Mitteln; etwas auseinandergezogen und expliziter programmiert

Relevanz immer noch nicht klar; alle Erfahrungen bisher beruhen auf Testläufen mit eigentlich zu kurzen Jobs (200µs - 1ms)

bedeutet: im Grunde kann man wild jede Funktion aufrufen

...auch bezüglich Lifecycle: weil GUI depends-on Core

Das zeigt, daß ich damals das Prototyping durchaus richtig gemacht haben muß (finde keine Aufzeichnungen dazu mehr). Habe das erst mal irgendwie in funktionierenden Code gewürgt, und mir dann angeschaut, was paßt und was nicht paßt. Beibehalten habe ich die Idee eines »Play-Prozesses«. Aber Display-Connections sollen nicht mehr direkt vom Play-Prozess gemacht werden, sondern über einen OutputManager abstrahiert sein. Und als Schnittstelle hinter dem Prozeß-Handle bekommt man nun ein Controller-Interface (State Machine).

Klingt alles sehr plausibel — sollte diesem Design folgen

mal bei den Standard-Icons vom XFCE-Desktop nachschauen

... die Starter-Konfig-Box durchscrollen

.... mal bei Tango nachschauen...

..... XFCE hat doch diesen sehr sauber-minimalistischen Icon-Satz von »Elementary-OS« übernommen (den man dort wegwerfen wollte)

Ausgangspunkt sind verschiedene Icons für Musik-Player, die eine stilisierte Compact-Kasette zeigen. Das bringt mich auf die Idee, auf die Steenbeck-Schneidemaschinen anzuspielen, mit den großen Rollen, bzw. auf eine Magnetton-Maschine ... und dann könnte man ein »Playhead« aus einem »Play/Pause«-Symbol erzeugen

Tja ⟹ dann gibt's bloß Inspiration + Arbeit

Ideen sind immer noch frei (sofern sie nicht patentiert sind) — nur der konkrete gestalterische Ausruck steht unter Copyright...

siehe InteractionDirector::injectTimeline()

ist aber leider nicht implementiert...

Erfolg-1 : es wird irgendwas angezeigt

beziehe mich hier per Gedächtnis auf Sachverhalte, die ich irgenwann irgendwo mal gelesen habe; demnach kann XV mit irgend einer Art von »Compositor« zusammenarbeiten, notfalls aber seine sichtbare (clipping)-Region auch per Colour-Key herausfinden; dabei geht es um die Frage, welcher Teil des Videobildes tatsächlich auf dem Desktop zu sehen ist, denn das Fenster könnte partiell verdeckt sein

wenn man beispielsweise zwar einen XV_COLORKEY setzen kann, aber kein XV_AUTOPAINT_COLORKEY

...dann kann ich das ganze Fenster füllen, durchaus auch mit beliebigen Farben (wobei hier eine Falle wartet, da die Farbtiefe unterschiedlich sein könnte).

dixplay_x11.c, line 623

static inline void  xv_draw_colorkey(void) {

XSetForeground( xj_dpy, xj_gc,  0 );

if (xj_box[1] >  0 ) {

XFillRectangle( xj_dpy, xj_win,  xj_gc, 0, 0,  xj_box[2], xj_box[1]);

XFillRectangle( xj_dpy, xj_win,  xj_gc, 0, xj_box[1]+xj_box[3],  xj_box[2], xj_box[1]+xj_box[3]+xj_box[1]);

} /* else */

if (xj_box[0] >  0 ) {

XFillRectangle( xj_dpy, xj_win,  xj_gc, 0, 0,  xj_box[0], xj_box[3]);

XFillRectangle( xj_dpy, xj_win,  xj_gc, xj_box[0]+xj_box[2],  0, xj_box[0]+xj_box[2]+xj_box[0],  xj_box[3]);

}

}

... verwende dann in XSetForeground ggfs eine andere Farbe, wie 0x00FF00 für Grün

    number of attributes: 7

      "XV_SET_DEFAULTS" (range 0 to 0)

              client settable attribute

      "XV_ITURBT_709" (range 0 to 1)

              client settable attribute

              client gettable attribute (current value is 0)

      "XV_SYNC_TO_VBLANK" (range 0 to 1)

              client settable attribute

              client gettable attribute (current value is 1)

      "XV_BRIGHTNESS" (range -1000 to 1000)

              client settable attribute

              client gettable attribute (current value is 0)

      "XV_CONTRAST" (range -1000 to 1000)

              client settable attribute

              client gettable attribute (current value is 0)

      "XV_SATURATION" (range -1000 to 1000)

              client settable attribute

              client gettable attribute (current value is 0)

      "XV_HUE" (range -1000 to 1000)

              client settable attribute

              client gettable attribute (current value is 0)

    maximum XvImage size: 16384 x 16384

    Number of image formats: 4

      id: 0x32595559 (YUY2)

        guid: 59555932-0000-0010-8000-00aa00389b71

        bits per pixel: 16

        number of planes: 1

        type: YUV (packed)

      id: 0x32315659 (YV12)

        guid: 59563132-0000-0010-8000-00aa00389b71

        bits per pixel: 12

        number of planes: 3

        type: YUV (planar)

      id: 0x59565955 (UYVY)

        guid: 55595659-0000-0010-8000-00aa00389b71

        bits per pixel: 16

        number of planes: 1

        type: YUV (packed)

      id: 0x30323449 (I420)

        guid: 49343230-0000-0010-8000-00aa00389b71

        bits per pixel: 12

        number of planes: 3

        type: YUV (planar)

number of attributes: 5

"XV_COLORKEY" (range 0 to 16777215)

client settable attribute

client gettable attribute (current value is 2110)

"XV_BRIGHTNESS" (range -128 to 127)

client settable attribute

client gettable attribute (current value is 0)

"XV_CONTRAST" (range 0 to 255)

client settable attribute

client gettable attribute (current value is 128)

"XV_SATURATION" (range 0 to 255)

client settable attribute

client gettable attribute (current value is 128)

"XV_HUE" (range -180 to 180)

client settable attribute

client gettable attribute (current value is 0)

maximum XvImage size: 1024 x 1024

Number of image formats: 7

id: 0x32595559 (YUY2)

guid: 59555932-0000-0010-8000-00aa00389b71

bits per pixel: 16

number of planes: 1

type: YUV (packed)

id: 0x32315659 (YV12)

guid: 59563132-0000-0010-8000-00aa00389b71

bits per pixel: 12

number of planes: 3

type: YUV (planar)

id: 0x30323449 (I420)

guid: 49343230-0000-0010-8000-00aa00389b71

bits per pixel: 12

number of planes: 3

type: YUV (planar)

id: 0x36315652 (RV16)

guid: 52563135-0000-0000-0000-000000000000

bits per pixel: 16

number of planes: 1

type: RGB (packed)

depth: 0

red, green, blue masks: 0x1f, 0x3e0, 0x7c00

id: 0x35315652 (RV15)

guid: 52563136-0000-0000-0000-000000000000

bits per pixel: 16

number of planes: 1

type: RGB (packed)

depth: 0

red, green, blue masks: 0x1f, 0x7e0, 0xf800

id: 0x31313259 (Y211)

222

guid: 59323131-0000-0010-8000-00aa00389b71

bits per pixel: 6

number of planes: 3

type: YUV (packed)

id: 0x0

guid: 00000000-0000-0000-0000-000000000000

bits per pixel: 0

number of planes: 0

type: RGB (packed)

depth: 1

red, green, blue masks: 0x0, 0x0, 0x0

...was aber normalerweise passiert bei der Behandlung des »Expose«-Events von X

...und gehört in einen dedizierten OutputManager

er rechnet ja ohnehin zunächst in RGB, und macht dann eine YUV-Konvertierung

Tip: suche nach "image data"

sieht zwar nach nitpicking aus, aber da wir dann explizit per structured-Binding auf die Komponenten zugreifen, könnte der Code etwas klarer werden

...sonst ist es nicht möglich, das als virtuellen Zugriff für Input und Output zu verwenden

...was jetzt einfach an dem Test-Setup liegt, in dem die Komponenten nochmal gewrapped sind

...denn sonst endet man doch wieder mit z.B. einem Debian-Paket, das build-depends on the world of media processing

sie könnte zwar im System installiert und vorhanden sein, aber nicht richtig konfiguriert, vielleicht überhaupt nie nutzbar sein, oder aber derzeit grade nicht nutzbar (weil eine externe Verbindung oder Ressource fehlt)

Es ist ein sehr alter Standard, durchaus komplex und detailiertere Doku ist kaum noch zu finden (-> Einzelfragen auf Stackoverflow). Manpages sind, wie üblich, nur dann sinnvoll, wenn man schon weiß, worum es geht). Hinzu kommt, daß es wohl erhebliche Streubreite gibt bzgl. der Implementierung. »Beispielsweise« unterstützt meine uralt-Nvidia-Karte nicht das automatische Keying (Attribut XV_COLORKEY und XV_AUTOPAINT_COLORKEY sind nicht definiert, aber der X-Treiber macht ein Auto-Keying auf dem gesamten X-Window und das ist problematisch mit den Dockingpanels, die das gleiche XWindow verwenden)

An Libraries ist nur erlaubt, was ohnehin in Reichweite liegt (also elementare Video-Libraries und Cairo zum Zeichnen). Der Code wird Teil des Projekts, sollte daher in C++ stehen und allen Qualitätsstandards entsprechen. Sinnvollerweise wird der Code in ein ad-hoc-Plugin gepackt, um den Umgang mit Dependencies zu erleichtern

„viel“ ist relativ. Es darf schon Spaß machen, aber es sollte nicht in ein monatelanges Kunst-Projekt ausufern

Der Datengenerator bewegt sich in einem gewissen Rahmen: bei hinreichend kleiner Auflösung muß er zuverlässig und ohne Aussetzer in Echtzeit laufen, und zwar mit 30fps. Idealerweise gibt es Parameter, um die Last stark zu erhöhen, so daß man ihn auch zum Test von Qualitäts-Renders heranziehen kann. Weiterhin sollte es sich im Wesentlichen um eine computational load handeln; ein gewisser Speicherbedarf ist dabei akzeptabel, externe IO nicht.

Da das Projekt auf eine gewisse öffentliche Sichtbarkeit zusteuert, werden alle sichtbaren und leicht faßbaren Elemente auf subtile Weise zu einer Visitenkarte. Dieser Playback ist so ziemlich das Erste und Einzige seit vielen Jahren, das man sehen und erfassen kann. Es sollte daher klar sein, daß es in jeder Form einen Eindruck hinterläßt.

Beschreibt das Prinzip rekursiver fraktaler Funktionen, und stellt dann Erweiterungen vor, die in seinem »Flam3« implementiert sind. Auf dieser Basis hat er »Electric Sheep« aufgebaut, welches auch die Grafik-Definitionen selber durch einen generativen Prozeß weiterentwickelt.

  • es wird eine echte Hash-Funktion verwendet, die auch Vertauschungen erkennt
  • eingerechnet wird in jedem Berechnungsschritt jeweils ein lokaler Zahlenwert
  • dieser ergibt sich im einfachsten Fall aus einem laufenden Zähler

Ein lokal berechneter Hashwert kann als pseudo-Zufallszahl verwendet werden, um eine gewisse Zahl an Neben-Zweigen zu erzeugen; diese greifen in einem limitierten Rahmen auf vorausgehende Ergebnisse zurück. Diese werden dadurch zu Voraussetzungen.

die Hash-Funktion wird N-fach auf das Zwischenergebnis angewendet, für großes N. Damit kann CPU-Last erzeugt werden

Wenn man nicht immer auf den unmittelbaren Vorgänger zurückgreift, sondern auf zufällige Vorgänger, entsteht ein chaotischer DAG, der aber aufgrund der Limitierung stets Forschritt macht. Durch die teilweise lokale Entkoppelung der Zweige wird die Berechnung parallelisierbar; da sich jede Entscheidung aber aus den Eingangsdaten ergibt, muß das Ergebnis 100% deterministisch sein.

Das Ergebnis muß ohnehin einmal auf konventionellem Weg berechnet werden; an dieser Stelle kann man die sich ergebende Verknüpfungs-Struktur ausgeben, um daraus verknüpfte Render-Jobs zu machen

....denn sonst könnte sich diese Struktur nicht entfalten, ohne die Wahrscheinlichkeiten gezielt global zu steuern. Zu Beginn muß einmal fest verzweigt werden, und zum Abschluss müssen einmalig alle noch offenen Knoten in einen Endknoten zusammengeführt werden.

Und das legt auch nahe, die Knoten selber im Block zu allozieren. Die Struktur wird dann sukzessive durch Herstellen von Verknüpfungen aufgebaut, und jeweils neue Knoten werden fortschreitend „belegt“

...und zwar da sie wegen der möglicherweise chaotischen Verknüpfungsstruktur schwierig zu programmieren und maximal aufwendig wäre. Das gilt entsprechend auf das Erzeugen des Schedule

...denn auch die Topologie-Generierung braucht bereits eine Zufallszahl pro Node, und dafür nimmt man sinnvollerweise den berechneten Hash für diese Node, also ruft die Berechnungsfunktion der Node auf.

Wenn man das Scheduling nicht auf einmal macht, bleiben gewisse Querverbindungen undeklariert, auch auf den Nodes, die man schon in den Schedulter übergeben hat. Da jedoch auf jeder »Zeitebene« für jeden parallelen Strang jeweils eine Node sitzt, können maximal die Verknüpfungen zur unmittelbar vorausgehenden Zeitebene fehlen; sofern man das nächste Paket für's Scheduling behandelt, bevor diese unmittelbar vorhergehende Zeitebene tatsächlich schon ihre Aktivierung erreicht hat, können diese Verbindungen noch nachgetragen werden.

Begründung: man möchte mit dem Ding heftigen Druck erzeugen können. Daher muß es selbst performant implementiert sein; unkoordinierte Heap-Allokationen für Predecessor/Successor-Tabellen können wir uns da nicht gestatten.

weil per Festlegung die letzte Node alles zusammenführen soll, und ich keinen komplizierteren Algorithmus programmieren will

hatte den Basis-Offset von 8 übersehen, der ja sofort abgezogen wird, bevor das Fenster überhaupt zum Tragen kommt

aber nur N21,N23 und N24 können die realisieren,

denn dann haben wir alle bis zur vorletzen N30 verbraucht

das war der Fix von gestern

die letze vorhandene Node

ist das „gut so“ ?

der resultierende Baum ist grundlos unregelmäßig

und (bei meinem aktuellen Kentnissstand): ich kann es leicht fixen...

Der Baum sieht nun sinnig aus — für das Auge des Mathematikers (nota bene: nicht für mein subjektives Auge). Und der Code ist tatsächlich klarer geworden, weil in der Behandlung nicht mehr zwei getrennte Belange überlagert sind.

ich hab jetzt dreimal „draufgedroschen“, bis ich das Wahrscheinlichkeitsverhalten hatte, das ich wollte (wenngleich ich auch dabei einen Bug entdeckt habe...)

das bedingt eine dynamische Regel

mit Abstrichen: jetzt läufts...

Es ist schwierig — und ich weiß warum: es ist keine direkte Arbeit  am Berechnungs-Strom möglich; das wäre das mir vorschwebende Ideal (siehe Projekt Replantor). Stattdessen kann ich nur Wahrscheinlichkeiten nach Plausibilität wählen und dann per minimaler Variation nach Zufallstreffern suchen. Und sofern ein Solcher gefunden ist, gibt es kaum Möglichkeiten, ihn noch in einer gewissen Richtung auszugestalten, denn das Verhalten biegt nicht, es bricht.

das hat letztlich einige brauchbare Muster hervorgebracht

Ausgangspunkt: Cap ⟶ Ergebnis-Typ Draw

...dabei ist Cap die bereits geschaffene Hilfs-Komponente, die einen Zahlenwert mit einer Klammer von Iber/Untergrenze speichert und ihn in in diesen Rahmen hinein konditionieren kann. Das war ein relativ leichtgewichtiger erster Schritt, reicht aber nicht aus, um die techniche Komplexität aus dem Nutzkontext zu entfernen

will sagen: es ist nicht hilftreich,

das kombinieren zu wollen...

....aber irgendwie scheint mir genau das vorzuschweben...

...zumindest der Möglichkeit nach

hab das Gefühl daß ich schon wieder mal viel zu weit ausgreife, für ein Stück Infrastruktur, das einem einzigen Zweck dient (noch dazu lediglich für Tests) und dann wahrscheinlich nur noch rumliegt und zufällig vorbeikommende Leser verwirrt

mir sind die im Moment ganz klar, dagegen beim Leser kann man nur so etwas wie eine generelle Idee von einem DAG voraussetzen

aber Kaskadieren der Auswertung bleibt eine Möglichkeit

konsequenterweise sollte die ganze Freiheit

und Offenheit für beliebige Rechenregeln

nun komplett aufgegeben werden

und gemäß YAGNI werde ich nur 10% der Möglichkeiten  wirklich brauchen

...der müßte dann aber absolut gelten, und Teil der Spec werden. Ja warum nicht

...da sich alle auf den gleichen Hash beziehen, und daher auch korreliert ziehen werden

...diese Variante ist erst mit C++17 möglich, da nun der Template-Parameter inferiert werden kann

Fazit Design-Analyse

warum? eine zweckgebundene Lösung mit diesem Komplexitätsgrad wäre nicht zu rechtfertigen; man würde dann nach einer einfacheren Alternative suchen. Konkret komme ich allerdings von dieser einfacheren Alternative her, weil sie immer noch nicht einfach und klar genug war. Insofern bin ich froh, überhaupt eine Lösung zu haben, die im Rückblick sinnvoll darstellbar ist

Library-Komponente extrahieren: RandomDraw

geht heutzutage relativ kompakt mit generischen Lambdas

...ich hab nicht „lesbar“ gesagt...

muß die Funktionsargumente kopieren

Beispiel: -2 .. 0 ..+2  ⟹ Wahrscheinlichkeit definiert für Werte ≠ 0

natürliche Rundung is towards zero

...und das ist obendrein implementation defined...

und zwar unter den einfachen Fall

Konsequenz: speziellen Fall schaffen...

explizit org ≔ minResult_

dabei beachten: val ≡ 1.0 liegt am Rand und wird genau nicht mehr erreicht

der Fall, daß der neutrale Wert am oberen Rand liegt,

wird mehr zufälligerweise vom innenliegenden Fall

korrekt mit behandelt, indem die 1.Hälfte wegfällt

grade wenn die Maximalgrenze nahe an einer Zweierpotenz liegt (Beispiel 10 Werte) dann werden einige Werte deutlich wahrscheinlicher

...würde den DSL-Gebrauch stark einschränken. Das häufigste Idiom dürfte sein: auto draw = Draw().probaility(###);

verwende stattdessen eine Sperre, die »eintrastet«, sobald eine custom-defined-function installiert wurde. Ab dem Punkt könnte sonst jeder Aufruf zum SEGFAULT führen

zur Anwendung müssen Funktoren gebunden sein

In die eigentliche Auswertungsfunktion kann man eine »trojanische Funktion« installieren, die etwas völlig anderes macht, nämlich die Initialisierung. Danach überschreibt sie sich selbst mit der fertig gebundenen Funktion

...mit einem gewissen Grad an Type Erasure; sie haben eine bekannte, feste Größe, egal was man reinpackt, ggfs. aber verwalten sie Heap-Storage transparent

das ist praktisch das Gleiche, beide haben eine VTable, aber der OpaqueHolder baut darauf auf und unterstützt auch Zugriff über das Basis-Interface

init(this) ⟹ this wird fertig gemacht

Erfahrungswert: ein »Slot« geht immer

die Trap-Door muß irgendwo auf den Initialiser zugreifen, und dieser muß wiederum die vorläufige Funktion speichern. Alle diese Allokationen müssen direkt am Trap selber »aufgehängt« werden; wenn wir also den Trap verwerfen (durch Neuzuweisung), sägen wir den Ast ab, auf dem wir sitzen. Kann man machen, wenn man Flügel hat, oder sowieso schon auf dem Sprung ist

die Funktion ist dieses Mal ein Feld im abgeleiteten Objekt (yess!)

//          CHECK (1 == invoked);

//          CHECK (init);

die neue maybeInvoke()

previousInit

Details:<error reading variable: Cannot access memory at address 0x3000000030>

gedankenloser Schmuh :

 move (pendingInit_) .....

 und nachher pendingInit_.reset()

      __shared_ptr(__shared_ptr&& __r) noexcept

      : _M_ptr(__r._M_ptr), _M_refcount()

      {

_M_refcount._M_swap(__r._M_refcount);

__r._M_ptr = 0;

      }

... kein Problem mit der Logik

Ergebnis: funktioniert nicht mit shared_ptr

In der Tat mache ich mir da grade selber das Leben schwer, aber die billige Lösung würde zu Verkopplung der Belange beim Client führen, insofern der Funktor-Typ Bestandteil der Signatur von LazyInit werden müßte. Das bedeutet, da es ein mix-In ist, müßte man diesen Typ vor Instatiierung bereits kennen oder spätestens im Zuge der Instantiierung irgendwie (per Metaprogramming) ermitteln. Obwohl dies komplett unnötig wäre — denn für die gewünsche Funktionalität genügt es völlig, wenn man den target-Functor (und damit die Signatur) erst in dem Moment erfährt, in dem tatsächlich ein Initialiser vorzubereiten ist.

InPlaceBuffer mißbraucht

hier besteht ein latentes semantsiches Problem:

lazyInit ⟹ Objekt ist erst mal noch nicht ganz initialisiert

dieses Design ist MIST

darauf kommt es jetzt auch nicht mehr an

Funktor ist Funktor!

Deshalb sind sie ja stateless (und müssen auch so bleiben, also nicht mutable). Für jeweils eine Instanz wird ein Funktor einfach »verbrannt«, indem man ihn aufruft und danach das pendingInit leert. Und zwar auf *self  — der Funktor als Solcher bleibt stateless. Wenn ihn noch jemand anders referenziert, dann schön....

Beim Umkonfigurieren wird der pendingInit verschoben

und zwar aus dem LazyInit-Objekt in den Adapter hinein. Der use-count bleibt dadurch gleich.

          using lib::test::Tracker;

          auto& log = Tracker::log;

          log.clear (this);

          auto f1 = [t=Tracker{11}](int i){return i+1; };

          auto f2 = [t=Tracker{22}](int i){return i/2; };

          auto f3 = [t=Tracker{33}](int i){return i-1; };

...

...

          cout << "____Tracker-Log_______________\n"

               << util::join(Tracker::log,      "\n")

               << "\n───╼━━━━━━━━━━━╾──────────────"<<endl;

sondern irgend ein Speicherinhalt

...und zwar deshalb, weil ich wußte, daß ich über diese Zeile beim Umbau nicht nachgedacht habe...

es ist zu »funktional«

konkret hilft mir:

die »Manipulation« kann per Definitionem

auf einem leeren Objekt statfinden...

vom konkreten use-Case her gedacht, ist es typischerweise gar nicht hilfreich, wenn das zu manipulierende Objekt schon »Zustand« hat; man schafft viel klarere Verhältnisse, wenn es per Definition ein default-konstruiertes Objekt ist

das ist für den aktuellen Use-case unabdingbar

...im Gegensatz zu dem ganzen weiteren Schlonz, den ich die letzten Tage eingebaut habe, „weils grad gang“; ich brauche irgende einen Mechanismus, über den ein später hinzukonfigurierter Funktor in Abhängigkeit von aktuellen Parameter-Daten an der Parametrisierung des RandomDraw "drehen" kann...

dangling Reference ⟹ CRASH

Lösung-2: er bindet irgendwie eine Referenz darauf

das wäre schön sauber — ABER ...

läuft auf eine partielle Anwendung der Funktion hinaus

genausogut kann man hier einen const_cast machen

dynamische Manipulation versaut this

das war ja gradezu der Keim, aus dem sich RandomDraw entwickelt hat

TestChainLoad selber is parametrisiert auf die Maximalwerte der Topologie ⟹ alle Parameter haben typischerweise die Breiten-Limitierung als Obergrenze

löst das Problem fast...

nachdem man nämlich buildTopology() aufgerufen hat,

explodiert TestChainLoad am Ende der DSL-Methode.

Denn da steht ganz verträumt...

return move(*this)

nun gut: dann verwendet buildTopology() eben lokale Kopien

...da RandomDraw jetzt so wunderbar elegant kopierbar ist...

...während der Berechnung der Topologie (und das ist die einzige tatsächliche Verwendung der Regeln) arbeiten wir auf lokalen Kopien; eine dynamische Änderung aus der Berechnung heraus hinterläßt Spuren in der »schwebenden« globalen Regelkonfiguration — dringt aber grade nicht in die aktivierte Regel-Konfiguration durch.

das macht fast genau das hier Benötigte

...zum einen ist TestChainLoad selbst ein Test-Tool

...und außerdem ist der lib::test - Namespace weder verboten noch gefährlich

...und schließlich steht der Makro-Name TRANSIENTLY nicht wirklich in Konkurrenz zu irgendwas

...sondern nur eine Verbesserung in der Code-Struktur (deshalb belasse ich es jetzt auch dabei)....

Das eigentliche Problem zeigte sich erst einen Schritt weiter: es ist eben nicht möglich, eine bereits gebundene Mapping-Funktion nochmal zu „entbinden“ und separat aufzurufen..

mit Abstrichen: jetzt läufts...

Ab dem Zeitpunkt der ersten Anwendung müssen Regeln an einer festen Stelle im Speicher sitzen, da sie Referenzen intern binden. Deshalb macht buildTopology() sich nun jeweils eine lokale Kopie

....wir müssen hier niemandem etwas beweisen, über die Grenzen des Unit-Testing hinausgehend:

  • der Node-Hash beweist, daß die Berechnung einer Node stattgefunden hat und die richtigen Ergebnisse der Vorläufer aufgegriffen hat
  • ansonsten können wir annehmen, daß eine aufgerufene Funktion auch tatsächlich läuft
  • und daß eine Exception aus dem Job heraus einen Worker herunterfährt und eine Scheduler-Emergency auslöst
  • da die Topologie vom Hash abhängen soll, bekommen wir es hier mit Eierlegenden und Hennen zu tun...
  • außerdem würde dadurch bereits auch die Topologie-Konstruktion aufwendig
  • und jede Änderung der Gewichte würde die Topologie komplett ändern

▶ das zuletzt genannte Argument hat den Ausschlag gegeben, auf Proof-of-Work zu verzichten, denn es ist ausgesprochen mühsam, eine bestimmte gewünschte Topologie zu „finden“ — jedwede Änderung wirkt chaotisch (genauso wie bereits die geringste Seed-Änderung eine komplett andere Form zufolge hat)

wird dann von dem RandomChainCalcFunctor eingebunden — und nicht von der Node; es wird damit strikt ein Detail des tatsächlichen Scheduler-Laufs

also: beides

legt die Eichungs-Konstanten statisch ab

schwierig...

Einerseits möchte ich schon eine gewisse Genauigkeit sicherstellen, andererseits soll der Test natürlich keine Probleme machen. Grade bei den initialen Werten kann man GEWALTIG danebenliegen. Da muß ich also zumindest schon mal eine plausible Basis-Geschwindigkeit hartcodieren, und zwar in Tendenz schnellere Rechner. Und ich kann wohl kaum was anderes als -90% und +1000% annehmen. Dagegen für die anderen Grenzen muß man sehen....

Stelle schon mal fest: der Einzel-Lauf streut ganz deutlich. Und Cache-Effekte könnten auch noch ein gewisses Problem darstellen (wenngleich auch der vorangegangene Test grundsätzlich den Code schon vorgewärmt  hat. Also setze mal  ±30% für den Einzeltest an, aber nur ±10% für den Benchmark

bei..

sizeBase = 10000 ⟼ +5%

sizeBase = 100000 ⟼ +10-20%

repetitions/µs

Konsequenz ⟹ nicht idempotent

das ist sinnvoll denn....

  • dort haben wir eine einfache, aber nicht-triviale Struktur
  • genau in diesem Test (und nur dort) verifizieren wir explizit die Connectivity
  • und ebenfalls dort prüfen wir bereits jeden Einzel-Hash

NodeTab : für Verlinkungen und für die Topologie-Erstellung

Man möchte nämlich auch ganz normale Rendervorgänge  simulieren, nicht bloß extravagant komplexe Graphen. Denn im Normalfall ist der Abhängigkeitsgraph sehr klein, dafür aber werden immerfort neue Stränge begonnen. Wir brauchen also einen Mechanismus, der regelmäßig Stränge mitten im Graphen beendet.

kein Platz für eine r-Node ⟹

ein Vorgänger könnte mit allen Nachfolgern verbunden sein

die Situtation ist nur so lange logisch konsistent,

wie ich die carry-on-Node einfügen kann

...denn dann würden wir eine Kette unterbrechen, und das darf keinesfalls passieren (es sei denn, das ist per Ctrl-Rule so vorgegeben worden)

...das liegt an der Grundstrukture der Topologie-Generierung: wir iterieren ja über die Vorgänger. Solange wir einen Vorgänger in der Hand haben, kann dieser noch nicht per Reduction oder Carry-on an irgend einen Nachhänger verschaltet worden sein (aber der Vorgänger kann sehr wohl per Expansion mit allen Nachfolgern verbunden sein)

es gab dann keinen einzigen Crash mehr und auch das Verhalten war stets nach Sicht konstistent

stattet jede seed-Node sofort mit einem festen Branch-Faktor aus

diese ist speziell, und spielt eine große Rolle, um kontrolliert Seeds einzuspielen. Dafür muß sie an die Situation im Generierungs-Algorighmus angepaßt werden, denn dort sind zum Abfrage-Zeitpunkt die Nachfolger noch gar nicht etabliert. Daher kann auch ein Link und ein Exit nicht wirklich unterschieden werden.

Das ist eine if-else-Regel; man kann damit auf einen Join anders reagieren als auf sonstige Knoten. Letzten Endes hat mich diese Regel aber nicht weitergebracht, denn für gute Strukturen kommt es aussschließlich auf das feinfühlige Balancieren der Kräfte an — das ist eine grundsätzliche Einschränkung des Ansatzes, die es unmöglich macht, auf Struktur-Muster direkt zu reagieren

  • einfache default-Knödel
  • den Graphen von unten nach oben anordnen (rankdir="BT")
  • die Nodes der Zeit-Ebenen jeweils in einen Subgraphen mit rank=same
  • Seed-Nodes geeignet auszeichnen, die Endnode ebenso
  • Kurzform des Hash als Node-Label

Grundstruktur

digraph {

  // Nodes

  N1[label=1, shape=doublecircle]

  N2[label=2]

  N3[label=3]

  N4[label=4]

  N5[label=5, shape=circle]

  N6[label=6]

  N7[label=7, shape=box, style=rounded]

  //Layers

  { /*0*/ rank=min  N1 }

  { /*1*/ rank=same N2 N3 }

  { /*2*/ rank=same N4 N5 N6 }

  { /*3*/ rank=same N7 }

  // Topology

  N1 -> {N2,N3}

  N2 -> {N4,N6}

  N3 -> {N4}

  N4 -> {N7}

  N5 -> {N7}

  N6 -> {N7}

}

rank muß explizit in jedem Layer definiert sein

...und das zeigt sich in schwer verständlichem Fehlverhalten; er macht dann Nodes mehrfach...

DSL().group(1)

       .term("a")

       .term("b")

       .group(11)

         .term("x"

       .end()

     .end();

dsl(

  group(1,

    term("a"),

    term("b"),

    group(11,

      term("x"))))

DSL dsl;

  dsl.group(1,

    dsl.term("a"),

    dsl.term("b"),

    dsl.group(11,

      dsl.term("x"))))

struct Special : DSL

  {

    Special(Context ctx)

      {

        group(1,

          term("a"),

          term("b"),

          group(11,

            term("x")));

      }

  }

  process;

  process(myContext);

nur bodenständige Performance....

...aber sage erst mal YAGNI

...man könnte z.B. meinen PolymorphicValue verwenden (wenn ich mich nur nicht so für den verworrenen Implementierungs-Code schämen müßte... �� Ticket #1197)

Node mit mehr als einem Nachfolger

Node mit mehr als einem Vorläufer

Fortsetzung einer Kette mit genau einem Vorläufer und einem Nachfolger

Node mit mehr als einem Vorläufer und mehr als einem Nachfolger

die Zahl der ausgehenden Verbindungen minus der eingehenden Verbindungen;  ∑spread ≙ 0 (Konsistenz der Connectivity)

für einfache Topologien sollte das den konfigurierten Wahrscheinlichkeiten entsprechen; bei komplexeren Topologien beschreibt es das Gleichgewicht, und ist daher eine wesentliche Kennzahl

da in einem breiteren Graph die viel mehr Nodes pro Level konzentriert sind, wachsen alle Gemmetrie-Kenndaten auch mit der Breite, was die Beurteilung des Verlaufs erschwert; daher ist es sinnvoll, eine Variante der lokalen Kennzahlen zu erstellen, die auf die Einheitsbreite normiert ist; erst dadurch wird ein zeitlicher Tredn im Gleichgewicht sichtbar

man berechnet hierfür ein gewichtetes Mittel über die Level, gewichtet durch die jweilige Kennzahl; dadurch ergibt sich, wo die betr. Kennzahl schwerpunktmäßig aktiv ist. Das ist für alle Kennzahlen sinnvoll, selbst die Nodes (wo sind die meisten Nodes?). Die Standardabweichung könnte man dann auch gleich noch dazu berechnen (um die Breite der Verteilung zu kennzeichnen)

analog zum einfachen gewichteten Level, nur daß hier die auf die Breite normierten Geometrie-Daten betrachtet werden; dadurch werden tatsächlich zeitliche Verläufe sichtbar

Ziemlich sicher werden wir zu Begin nur einen Bruchteil der möglichen Statisktiken berechnen, und es ist überhaupt nicht klar, ob wir jemals mehr brauchen

Vector mit Zähldaten indiziert nach Level

Gesamt-Anzahl der Forks

Forks per Level ∅

Forks per Level-Width ∅

Forks Level-γ-Schwerpunkt (centre)

Forks Level-Width-γ -Schwerpunkt (centre)

lineare Regression der forks_pL über den Level

forks moving average über den Level

forks per width moving average (über den Level)

...grundsätzlich wären die wichtigsten Zeitreihen im Statistik-Report da, so daß man auch noch weitreichendere Auswertungen machen könnte — sofern das hier mal für mehr als für einige Lasttests verwendet wird...

Dies müßte in eine Prüf-Klammer eingebunden werden, die den Original-Graphen zu fassen bekommt, bevor der Scheduler gestartet wird

  • man müßte einen Vector aller Node-Hashes aufzeichnen — ist ein Einzeiler mit explore(allNodes)...effuse()
  • danach macht man das Gleiche und einen Vergleich in einem Pass ⟿ abweichende Nodes in geeigneter Datenstruktur markieren
  • zusätzlich bedarf es einer Erweiterung des generateTopologyDOT: ein zusätzliches Argument bringt mark-up per Node ein; der fließt dann in die Node-Generierung ein; konkret wäre das eine Farbmarkierung der fehlerhaften Nodes
  • will man überhaupt im Einzelnen wissen, wo der Hash abgewichen ist?
  • was kann man mit dieser Information anfangen, ohne einen kompletten Berechnungs-Trail genau dieses Laufes zu haben?

sollte vergleichsweise einfach zu implementieren sein

...will sagen, ich muß da nicht lang überlegen und kann es mehr oder weniger runterschreiben (auch wenn's ein paar Stunden braucht)

...es wäre zwar nicht notwendig, da in der Node-struct lauter pointer und einfache Werte gespeichert werden; aber eine Node-Kopie wäre ein hochgradig inkonsistentes Gebilde, da dann die Rück-Referenzen nicht mehr stimmen würden.

...das folgt aus dem Design von IterExplorer insgesamt. Es wäre hochgradig gefährlich, wenn man in der Gruppe Referenzen speichern würde. Wenn überhaupt, dann muß das explizit so angefordert werden, indem man in der Pipeline mit Pointern arbeitet.

Stand: noch nicht volltändig umgesetzt

für den eigentlichen Aufruf genügt ein Node*

....und das hängt vom Template-Parameter der TestChainLoad ab (size_t maxFan)

weiß ja noch nicht, wie das für ProcNode implementiert wird

formal-Logisch entspräche der Node-Index dem srcRef-Parameter

also nicht um den gesamten Playback-Prozeß. Es müssen keinerlei Deadline-Planungen gemacht werden; das kommt alles Fix per Test-Setup

das könnten Varianten des Test-Setup sein; weiß noch nicht was hier gebraucht wird. Rein intuitiv würde ich erst mal nur nach dem Level vorgehen

das paßt doch hervorragend; LevelNr ≡ nominal time

Promise muß wirklich vor dem Start-Trigger vorbereitet werden

...ich erinnere mich: man muß mit 0-Timeout darauf warten und das bedingt mindestens einen yield-wait

Gut daß wir darüber geredet haben.

Da offensichtlich die Länge des Test-Graphen keine Eigenschaft  der Teststruktur ist, landen wir bei einer rein dynamischen Allokation. Trotzdem war es eine gute Idee, das mit der uninitialised storage  endlich mal zu codifizierren...

wir wissen, daß es keine concurrent Aufrufe geben wird, und auch Allokation stellt kein Problem dar; wir können einfach State im »Test-Rahmen« liegen lassen

die Node-Anzahl ist inhaltlich keine Typ-Eigenschaft

...sie ist rein aus Bequemlichkeit der Implementierung zum Template-Parameter geworden: ich hab nämlich ein std::array genommen

aber dann muß er unter zwei Namen auftreten

...und zwar, weil ich im Klassen-Scope einen Template-Namen nicht verdecken darf. Leider verteilt sich der Node-bezogene Code zu ziemlich gleichen Teilen auf die Node-Klasse selber, und den umschließenen Scope

hier ist zwar die maxCapacity festgelegt, aber nur zur Erstellungs-Zeit. Das wäre ja genau, was hier gebraucht wird

weil wir aufbauen auf can_STL_ForEach.

  • hat nested-type "iterator"
  • hat member-Funktionen begin() und end()

....außerdem wird dann das Hilfs-Template StlRange auch noch initialisiert mit
RangeIter{begin(con), end(con} .... ��

...darauf deuten die Komentare zur Typ-Inferenz hin:

"when IT is just a pointer, we use the pointee as value type"

Template-Parameter numNodes zurückbauen

wird in den Adaptoren verwendet — und die sind statisch

das war mal eine offensichtlich einfache Idee

man könnte das anders interpretieren

targetNode : bis einschießlich diese Node-ID soll mindestens behandelt werden

soll per Schedule als Letztes laufen

so einen Job auf den Level nach dem letzten Level setzen

...und das ist gut so

Nebeneinsicht: darf nur ein wirklich leeres Signal kaputt schießen

Hilfsfunktion, die beim ersten Aufruf den Bezugspunkt setzt. Dafür sorgen, daß dieser erste Aufruf den Anker-Punkt anfragt. Hilfsfunktion gibt dann µs nach Anker aus

⟹ der pre-Roll ist wesentlich zu kurz

Graph als DOT anschauen, ist ja reproduzierbar und enthält die Node-IDs

Fazit: durch Rückbau des Gate-re-Scheduling behoben

das ist incoming capacity

dieser Fall ist in der Tat etwas „ausgefallen“: wenn die ersten planmäßigen Tasks jenseits des WORK_HORIZON liegen, muß man damit rechnen, daß die ganze Kapazität gebündelt einmal kurz hinter dem SLEEP_HORIZON auftaucht und „nachschaut“... 

...und solange keine Jobs im Nahfeld auftreten, kommt es auch erst mal nicht zu einer Umverteilung der Kapazität. Das ist eine Konsequenz der Entscheidung, das längerfristige Schlafen zu priorisieren. Letztlich ist auch der SLEEP_HORIZON so zu wählen, daß er noch nicht gefährlich lang ist im Verhältnis zum zu erwartenden Takt (den ich auf ca 25fps schätze)

das ist zwar logisch, erscheint mir aber dennoch nicht »zielführend«  — daher eine Anpassung des Standard-Verhaltens, so daß der erste Tick schneller erfolgt

das Grooming-Token wird erst im WORKSTART gedroppt. Soweit kommen wir hier gar nicht, weil das Gate früher steht

arbeitet nur ein Thread exclusiv

dieser Fall: ja das ist sinnvoll

habe ja schon mehrfach beobachtet, daß Cache-Effekte +100µs bewirken können. Insofern macht es absolut keinen Sinn, mehr als einen Thread in diese Zone reinzulassen, mit intensiver Queue-Interaktion

80µs für die reine Ausführung, wenn danach das Grooming-Token nicht wieder elangt werden kann (denn das dauert 20µs)

...das ist dann die Karenz-Zeit, nach der ein anderer Thread zum Zuge kommen könnte (der braucht aber auch noch mal 20µs um das Token zu erlangen

...das interpoliere ich aus mehreren Abläufen, wo sich jeweils eine Verzögerung in dem vorher festen poll-Zyklus zeigt, und zwar genau, wenn ein bisher erfolgloser Thread zum Zuge kommt (also an dem Punkt das Grooming-Token erlangt haben muß)

also reinkommen, am Grooming-Token scheitern, rausfallen wieder reinkommen

für den Zeithorizont < 100µs ist es sinnlos, nach Parallelisierung  zu streben

unklar bleibt, ob mehr als drei contendende Threads Schaden anrichten können

danach / sonst : läuft verdächtig gut

Genau betrachtet: diese Aussage stimmt nur in einem allgemeinen »work-horizon« von <20ms. Die Fokussierung ist nicht dynamisch, sondern mit einem Sprung, nämlich an der 5ms-Grenze

Theorie-2 deckt sich mit emprischen Beobachtungen

Fazit: es lief nur ordentlich,

der Fall ist aber speziell und

von unklarer Relevanz.

kurze Lücken im Schedule + freie Kapazität ⟹ Ablaufsteuerung funktioniert sehr gut

ein Beispiel: Dump-02

Abarbeiten eines Backlog muß fokussiert untersucht werden

sofern Contention auftritt ⟹ tendentiell problematisch

dort als Dump-03

...und zwar von 5000µs ... 5982

und zwar anscheinend der Vorlauf, bis die Meldung vom Job-Funktor selber erscheint.

Ein Beispiel: Dauer 400µs, Meldung nach > 300µs

nun läuft es wunderbar glatt

denn durch die genaue Schedule-Berechnung und den präzisen pre-Roll ist nun bereits der erste Job ahead of schedule.

...das dreht sich dann typischerweise kurz (1.Tick, erste Kaskade von Jobs), aber dadurch entsteht höchstens ein Lag von 500µs, den ein einziger Worker locker wieder aufholt, zumal der konkrete Test-Graph hier am Anfang auch noch nicht die volle Breite hat.

dokumentiert als Dump-01

164.458 / 1.016*100µs - 1

in einem Fall sehe ich das sogar in einer Zeile als "HT" (head time)

man sieht sofort wo das Problem liegt: die Deadlines

nebenbei bemerkt: jeder Dispatch geht auch nochmal durch ctx.post

...und zwar liegt die Wurzel in der Offenheit der Activity-Language; ich wollte (und will) diese nicht auf eine Implementierungs-Logik des Schedulers reduzieren; dadurch sind Redundanzen entstanden, und aus logischen Gründen müßte eingentlich das Zeitfenster [start,dead] vom initialen POST am Anfang der Kette gelten, zumindes »sinngemäß«

wenn überhaupt, sollte die Deadline des Target gelten

Deadlines betreffen die Aktivierung. Es ist die Aufgabe des Job-Planning, das per Verkettung zurückzuführen auf die gewünschte Ankunftszeit. Die Activity-Language könnte das gar nicht tun, denn ihr fehlt dazu die Information über Erfahrungswerte die Ausführungszeit betreffend

Der Code ist wirklich performance-kritisch, und bis jetzt hab ich richtig gute Werte erziehlt, durch genau diese Art Maßnahmen.

ich weiß nicht, wie gut die CPU-Lasterzeugung funktioniert; Wohl möglich, daß Cache-Effekte die tatsächliche Zeit in de Höhe treiben

da wir alle ScheduleSpec-Terme in einem Array halten

es gibt nur noch wenige Dependency-Ketten,

die von einem einzigen Worker durchgearbeietet werden

wäre ja so zu erwarten gemäß Topologie...

weil es stets ein letztes Prerequisite gibt, das dann die ganze Lawine an Berechnungen lostritt; der relevante Knoten ist Node-Nr.34(L20) — und die wiederum hat drei Vorläufer-Ketten der Länge 2, d.h. der finale Trigger liegt auf Level 18 oder gar 17.

~3ms — aber 3·400µs ≈ 1.5ms + 100µs hätte ich erwartet

bei genauerer Betrachtung löst sich die Feststellung auf....

ja, irgendwo geht hier Zeit verloren, aber im 2.Lauf ist alles nicht so stark ausgeprägt

im 2.Lauf mit genauerer Instrumentierung sind die auffälligen Abweichungen innerhalb der Node-Invocation, nicht im administrativen Teil

in vielen weiteren Läufen immer wieder beobachtet: sobald signifikante concurrent activity stattfindet, laufen alle Knoten mit weight ≠ 0 signifikant länger als kalibriert

beim Durchdenken nebenbei aufgefallen: unprotected concurrenct acccess

...weil wir Prioritäten gar nicht auf der Ebene des Schedulers repräsentieren, sondern in den übergreifenden Plan einarbeiten können; der Scheduler muß also nur im Bezug auf diesen Plan priorisieren

...denn diese zugrundeliegende Struktur ist durch einen vorgelagerten Plan grundsätzlich nicht aus der Welt zu schaffen, denn es geht dabei um eine Diskrepanz zwischen einer planäßigen Codifizierung der Priorität und einer real (sachbezogen) gegebenen Priorität

Zwischenstand:

nach Behebung der aufgedeckten Probleme

ist self-Inhibition nicht mehr notwendig,

und erzeugt sogar zusätzlichen Overhead;

der überarbeitete NOTIFY-Mechanismus

macht das jetzt viel besser

...ich hatte dabei ein ungutes Gefühl, weil ich mir nicht erklären konnte, warum um ursprünglichen Code diese doppelte Indirektion drinnen war; hab das dann vom Tisch gewischt mit der Vermutung, es sei aus formalen Gründen eingebaut. Jetzt sehe ich, hier gibt es auch einen Gefahrenübergang bezüglich Concurrency, der mich wahrscheinlich damals bewogen hat „vorsichtshalber“ einen POST zu machen...

wir haben jetzt nur die Priorisierung verbessert, aber auch vorher konnte ein Thread depth-first durchmarschieren...

und außerdem bin ich mit der Gesamtsituation unzufrieden

eine POST-Activity in der Mitte eines Chain; das wird derzeit nie benötigt, und am Anfang des Chain steigen wir ja anders ein, nämlich über ActivityLang::dispatch() -> POST.dispatch() -> activate(next)

wenn etwas zu tun ist, dann geht ein Worker nach 2-3 Runden Management-Arbeit wieder in den Work-Modus (genau so wie intendiert). Diese Situation mit massiver Contention tritt nur auf, wenn mehrere Worker idle sind, weil ein anderer Worker noch eine länger laufende Voraussetzung abzuarbeiten hat. Das Verhalten ist dann also sogar vorteilhaft, weil es die wartenden Worker am aktuellen Schedule hält (anstatt sie wegzuschicken).

Man könnte allerdings darüber nachdenken, die Prüf-Zyklen im Falle von Contention zu bremsen

   ·‖ 0B: @14133 HT:14000  -> ▶ 13000

‖PST‖ 72: @14173 ◒ start=13000⧐14000 dead:100000

   ·‖ 51: @14194 HT:14000  -> ∘

   ·‖ 72: @14193 HT:15000  -> ▶ 14000

!◆!   0B: @14188  ⚙  calc(i=18, lev:13)

‖PST‖ 72: @14253 ◒ start=14000⧐14000 dead:100000

!◆!   72: @14272  ⚙  calc(i=19, lev:14)

   ·‖ 51: @15064 HT:16000  -> ▶ 15000

‖PST‖ 51: @15095 ◒ start=15000⧐15000 dead:100000

..!   0B: @15109  ⤴       (i=18)

‖PST‖ 0B: @15177 ◒ start=13000⧐14000 dead:100000

!◆!   0B: @15205  ⚙  calc(i=20, lev:14)

..!   72: @15847  ⤴       (i=19)

‖PST‖ 72: @15887 ◒ start=14000⧐15000 dead:100000

   ·‖ 5E: @16030 HT:17000  -> ▶ 16000

‖PST‖ 5E: @16074 ◒ start=16000⧐16000 dead:100000

   ·‖ 5E: @17068 HT:18000  -> ▶ 17000

‖PST‖ 5E: @17103 ◒ start=17000⧐17000 dead:100000

..!   0B: @17251  ⤴       (i=20)

‖PST‖ 0B: @17313 ◒ start=14000⧐15000 dead:100000

!◆!   0B: @17336  ⚙  calc(i=21, lev:15)

..!   0B: @17389  ⤴       (i=21)

‖PST‖ 0B: @17431 ◒ start=15000⧐16000 dead:100000

!◆!   0B: @17451  ⚙  calc(i=22, lev:16)

   ·‖ 5E: @18070 HT:18000  -> ▶ 18000

‖PST‖ 5E: @18111 ◒ start=18000⧐18000 dead:100000

   ·‖ 5E: @18134 HT:18000  -> ▶ 18000

‖PST‖ 5E: @18163 ◒ start=18000⧐18000 dead:100000

   ·‖ 5E: @18185 HT:18000  -> ▶ 18000

‖PST‖ 5E: @18215 ◒ start=18000⧐18000 dead:100000

   ·‖ 72: @18187 HT:18000  -> ∘

   ·‖ 72: @18264 HT:18000  -> ∘

   ·‖ 5E: @18259 HT:18000  -> ▶ 18000

   ·‖ 72: @18278 HT:18000  -> ∘

   ·‖ 72: @18298 HT:18000  -> ∘

‖PST‖ 5E: @18296 ◒ start=18000⧐18000 dead:100000

   ·‖ 72: @18313 HT:18000  -> ∘

   ·‖ 72: @18335 HT:19000  -> ∘

   ·‖ 5E: @18333 HT:19000  -> ▶ 18000

‖PST‖ 5E: @18364 ◒ start=18000⧐18000 dead:100000

..!   0B: @18509  ⤴       (i=22)

‖PST‖ 0B: @18564 ◒ start=16000⧐17000 dead:100000

!◆!   0B: @18586  ⚙  calc(i=23, lev:17)

   ·‖ DE: @19065 HT:19000  -> ▶ 19000

   ·‖ 5E: @19071 HT:19000  -> ∘

‖PST‖ DE: @19105 ◒ start=19000⧐19000 dead:100000

   ·‖ 5E: @19116 HT:19000  -> ∘

   ·‖ 5E: @19140 HT:19000  -> ∘

   ·‖ DE: @19123 HT:19000  -> ▶ 19000

   ·‖ 5E: @19162 HT:19000  -> ∘

‖PST‖ DE: @19179 ◒ start=19000⧐19000 dead:100000

   ·‖ 5E: @19184 HT:19000  -> ∘

   ·‖ DE: @19198 HT:19000  -> ▶ 19000

   ·‖ 5E: @19207 HT:19000  -> ∘

‖PST‖ DE: @19220 ◒ start=19000⧐19000 dead:100000

   ·‖ 5E: @19229 HT:19000  -> ∘

   ·‖ DE: @19238 HT:19000  -> ▶ 19000

   ·‖ 5E: @19251 HT:19000  -> ∘

‖PST‖ DE: @19259 ◒ start=19000⧐19000 dead:100000

   ·‖ 5E: @19276 HT:19000  -> ∘

   ·‖ DE: @19288 HT:20000  -> ▶ 19000

‖PST‖ DE: @19309 ◒ start=19000⧐19000 dead:100000

   ·‖ DE: @20088 HT:20000  -> ▶ 20000

‖PST‖ DE: @20120 ◒ start=20000⧐20000 dead:100000

   ·‖ DE: @20139 HT:21000  -> ▶ 20000

‖PST‖ DE: @20159 ◒ start=20000⧐20000 dead:100000

   ·‖ DE: @21063 HT:21000  -> ▶ 21000

‖PST‖ DE: @21092 ◒ start=21000⧐21000 dead:100000

   ·‖ DE: @21108 HT:21000  -> ▶ 21000

‖PST‖ DE: @21128 ◒ start=21000⧐21000 dead:100000

   ·‖ DE: @21144 HT:22000  -> ▶ 21000

‖PST‖ DE: @21163 ◒ start=21000⧐21000 dead:100000

..!   0B: @21639  ⤴       (i=23)

‖PST‖ 0B: @21699 ◒ start=17000⧐18000 dead:100000

   ·‖ EE: @22030 HT:22000  -> ▶ 22000

‖PST‖ 0B: @118578 ◒ start=17000⧐18000 dead:100000

‖PST‖ EE: @118603 ◒ start=22000⧐22000 dead:100000

‖PST‖ 0B: @118637 ◒ start=17000⧐18000 dead:100000   ·‖ 5E: @26364 HT:22000  -> ∘

   ·‖ DE: @22063 HT:22000  -> ∘

   ·‖ 5E: @118727 HT:18000  -> ∘

   ·‖ EE: @118732 HT:22000  -> ▶ 22000

   ·‖ 51: @30823 HT:22000  -> ∘

   ·‖ 51: @118831 HT:22000  -> ∘

   ·‖ DE: @118778 HT:22000  -> ∘

   ·‖ 51: @118852 HT:22000  -> ∘

   ·‖ DE: @118863 HT:22000  -> ∘

‖PST‖ EE: @118784 ◒ start=22000⧐22000 dead:100000

   ·‖ 99: @23758 HT:22000  -> ∘   ·‖ 72: @22141 HT:22000  -> ∘

   ·‖ 99: @118940 HT:22000  -> ∘

   ·‖ 72: @118952 HT:22000  -> ∘

   ·‖ 99: @118964 HT:22000  -> ∘

   ·‖ 5E: @118756 HT:22000  -> ∘

   ·‖ 99: @118987 HT:22000  -> ∘

   ·‖ EE: @118929 HT:22000  -> ▶ 22000

   ·‖ 99: @119009 HT:22000  -> ∘

   ·‖ 51: @118870 HT:22000  -> ∘

   ·‖ 99: @119031 HT:22000  -> ∘

   ·‖ CB: @23660 HT:22000  -> ∘

   ·‖ 99: @119054 HT:22000  -> ∘

   ·‖ 99: @119077 HT:22000  -> ∘

   ·‖ 51: @119089 HT:22000  -> ∘

   ·‖ 99: @119100 HT:22000  -> ∘

   ·‖ 51: @119109 HT:22000  -> ∘

   ·‖ DE: @118889 HT:22000  -> ∘   ·‖ 5E: @119082 HT:22000  -> ∘   ·‖ CB: @119145 HT:22000  -> ∘

‖PST‖ 0B: @119165 ◒ start=17000⧐18000 dead:100000

   ·‖ 5E: @119196 HT:22000  -> ∘

‖PST‖ EE: @119204 ◒ start=22000⧐22000 dead:100000   ·‖ 51: @119150 HT:22000  -> ∘

   ·‖ 5E: @119225 HT:22000  -> ∘

   ·‖ 51: @119235 HT:22000  -> ∘

   ·‖ 5E: @119244 HT:22000  -> ∘

   ·‖ 51: @119255 HT:22000  -> ∘

   ·‖ 5E: @119275 HT:22000  -> ∘

   ·‖ DE: @119208 HT:22000  -> ∘

   ·‖ 5E: @119298 HT:22000  -> ∘

   ·‖ CB: @119169 HT:22000  -> ∘   ·‖ DE: @119310 HT:22000  -> ∘

   ·‖ 72: @118970 HT:22000  -> ∘   ·‖ DE: @119342 HT:22000  -> ∘

‖PST‖ 0B: @119295 ◒ start=17000⧐18000 dead:100000

   ·‖ 0B: @119416 HT:22000  -> ∘

   ·‖ 0B: @119433 HT:22000  -> ∘

   ·‖ 0B: @119449 HT:22000  -> ∘

   ·‖ 0B: @119466 HT:22000  -> ∘

   ·‖ 0B: @119484 HT:22000  -> ∘

   ·‖ 0B: @119500 HT:22000  -> ∘

   ·‖ 0B: @119517 HT:22000  -> ∘

   ·‖ 0B: @119535 HT:22000  -> ∘

   ·‖ 0B: @119552 HT:22000  -> ∘

   ·‖ 0B: @119568 HT:22000  -> ∘

   ·‖ 0B: @119585 HT:22000  -> ∘

   ·‖ 0B: @119602 HT:22000  -> ∘

   ·‖ 0B: @119619 HT:22000  -> ∘

   ·‖ 0B: @119637 HT:22000  -> ∘

   ·‖ 0B: @119653 HT:22000  -> ∘

   ·‖ 0B: @119671 HT:22000  -> ∘

   ·‖ 0B: @119688 HT:22000  -> ∘

   ·‖ 0B: @14133 HT:14000  -> ▶ 13000

!◆!   0B: @14188  ⚙  calc(i=18, lev:13)

..!   0B: @15109  ⤴       (i=18)

‖PST‖ 0B: @15177 ◒ start=13000⧐14000 dead:100000

!◆!   0B: @15205  ⚙  calc(i=20, lev:14)

..!   0B: @17251  ⤴       (i=20)

‖PST‖ 0B: @17313 ◒ start=14000⧐15000 dead:100000

!◆!   0B: @17336  ⚙  calc(i=21, lev:15)

..!   0B: @17389  ⤴       (i=21)

‖PST‖ 0B: @17431 ◒ start=15000⧐16000 dead:100000

!◆!   0B: @17451  ⚙  calc(i=22, lev:16)

..!   0B: @18509  ⤴       (i=22)

‖PST‖ 0B: @18564 ◒ start=16000⧐17000 dead:100000

!◆!   0B: @18586  ⚙  calc(i=23, lev:17)

..!   0B: @21639  ⤴       (i=23)

‖PST‖ 0B: @21699 ◒ start=17000⧐18000 dead:100000

‖PST‖ 0B: @118578 ◒ start=17000⧐18000 dead:100000

‖PST‖ 0B: @118637 ◒ start=17000⧐18000 dead:100000   ·‖ 5E: @26364 HT:22000  -> ∘

‖PST‖ 0B: @119165 ◒ start=17000⧐18000 dead:100000

‖PST‖ 0B: @119295 ◒ start=17000⧐18000 dead:100000

   ·‖ 0B: @119416 HT:22000  -> ∘

   ·‖ 0B: @119433 HT:22000  -> ∘

   ·‖ 0B: @119449 HT:22000  -> ∘

der Thread, aber auch andere warten warten endlos auf das Grooming-Token (wer hat das zu der Zeit?)

          Extent&

          access()

            {

              auto* rawStorage = this->get();

              ENSURE (rawStorage != nullptr); 

              return static_cast<Extent&> (rawStorage->array());

            }

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::Storage::access()+0xd6

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::access(unsigned long) const+0x139

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::IdxLink::yield() const+0x26

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor::yield() const+0x18

lib::IterableDecorator<vault::gear::blockFlow::Epoch<vault::mem::ExtentFamily<vault::gear::Activity, 500ul> >, vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor>::operator->() const+0x20

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::claimSlot()+0x2b

vault::gear::Activity& vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::create<vault::gear::Activity::Verb>(vault::gear::Activity::Verb&&)+0x2e

vault::gear::activity::Term::insertWorkBracket()+0x26

vault::gear::activity::Term::configureTemplate(vault::gear::activity::Term::Template)+0x3c

vault::gear::activity::Term::Term(vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle&&, vault::gear::activity::Term::Template, lib::time::Time, lib::time::Time, vault::gear::Job)+0xd9

vault::gear::ActivityLang::setupActivityScheme(vault::gear::activity::Term::Template, vault::gear::Job, lib::time::Time, lib::time::Time)+0x99

vault::gear::ActivityLang::buildCalculationJob(vault::gear::Job, lib::time::Time, lib::time::Time)+0x6d

vault::gear::ScheduleSpec::post()+0x8b

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::disposeStep(unsigned long, unsigned long)+0x242

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long)#1}::operator()(unsigned long, unsigned long) const+0x2e

std::_Function_handler<void (unsigned long, unsigned long), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long)#1}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&)+0x52

std::function<void (unsigned long, unsigned long)>::operator()(unsigned long, unsigned long) const+0x61

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x29d

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::getWork()::{lambda()#2}::operator()() const+0x3ce

vault::gear::Scheduler::WorkerInstruction vault::gear::Scheduler::WorkerInstruction::performStep<vault::gear::Scheduler::getWork()::{lambda()#2}>(vault::gear::Scheduler::getWork()::{lambda()#2})+0x26

vault::gear::Scheduler::getWork()+0x3d

vault::gear::Scheduler::Setup::doWork()+0x1c

vault::gear::work::Worker<vault::gear::Scheduler::Setup>::pullWork()+0x26

void std::__invoke_impl<void, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(std::__invoke_memfun_deref, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x67

std::__invoke_result<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>::type std::__invoke<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x37

std::invoke_result<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>::type std::invoke<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x37

void lib::thread::PolicyLaunchOnly<lib::thread::ThreadWrapper, void>::perform_thread_function<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x4d

void lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::invokeThreadFunction<void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x76

void std::__invoke_impl<void, void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(std::__invoke_memfun_deref, void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*&&)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*&&, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x95

std::__invoke_result<void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>::type std::__invoke<void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*>(void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*&&)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*&&, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&)+0x67

decltype (__invoke((_S_declval<0ul>)(), (_S_declval<1ul>)(), (_S_declval<2ul>)(), (_S_declval<3ul>)())) std::thread::_Invoker<std::tuple<void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*> >::_M_invoke<0ul, 1ul, 2ul, 3ul>(std::_Index_tuple<0ul, 1ul, 2ul, 3ul>)+0x7b

std::thread::_Invoker<std::tuple<void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*> >::operator()()+0x18

std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>::*)(void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*&&)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*&&), lib::thread::ThreadLifecycle<lib::thread::PolicyLaunchOnly, void>*, void (vault::gear::work::Worker<vault::gear::Scheduler::Setup>::*)(), vault::gear::work::Worker<vault::gear::Scheduler::Setup>*> > >::_M_run()+0x1c

0000000648: BACKTRACE: extent-family.hpp:115: thread_8: access: /usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0xbbb2f) [0x7f63cb791b2f]

0000000649: BACKTRACE: extent-family.hpp:115: thread_8: access: /lib/x86_64-linux-gnu/libpthread.so.0(+0x7fa3) [0x7f63cc19dfa3]

0000000650: BACKTRACE: extent-family.hpp:115: thread_8: access: /lib/x86_64-linux-gnu/libc.so.6(clone+0x3f) [0x7f63cb46706f]

                |n.(54,lev:24)

... dispose(i=54,lev:24) -> @12000

‖•△•‖     wof:8 HT:0

‖SCH‖ 25: @214 ○ start=12000 dead:10000

                |n.(55,lev:24)

... dispose(i=55,lev:24) -> @12000

‖•△•‖     wof:8 HT:33

‖SCH‖ 25: @283 ○ start=12000 dead:10000

   ·‖ EE: @233 HT:33  -> ▶ 0

                |n.(56,lev:24)

!◆!   EE: @375  ⚙  calc(i=0, lev:0)

... dispose(i=56,lev:24) -> @12000

..!   EE: @468  ⤴       (i=0)

‖•△•‖     wof:8 HT:33

‖PST‖ EE: @548 ◒ start=0⧐500 dead:10000

‖SCH‖ 25: @579 ○ start=12000 dead:10000

   ·‖ EE: @612 HT:500  -> ▶ 33

‖▷▷▷‖ EE: @ 645 HT:500 

                |n.(57,lev:24)

... dispose(i=57,lev:24) -> @12000 

   ·‖ EE: @814 HT:500  -> ▶ 500

   ·‖ EE: @848 HT:1000  -> ▶ 500

!◆!   EE: @887  ⚙  calc(i=1, lev:1)

..!   EE: @947  ⤴       (i=1)

0000000609: POSTCONDITION: extent-family.hpp:115: thread_8: access: (rawStorage != nullptr) 

‖PST‖ EE: @1004 ◒ start=500⧐1000 dead:10000

   ·‖ EE: @1039 HT:1000  -> ▶ 1000

   ·‖ EE: @1076 HT:1500  -> ▶ 1000

!◆!   EE: @1108  ⚙  calc(i=2, lev:2)

   ·‖ 00: @1733 HT:2000  -> ▶ 1500

   ·‖ 00: @2069 HT:2500  -> ▶ 2000

..!   EE: @2181  ⤴       (i=2)

‖PST‖ EE: @2231 ◒ start=1000⧐1500 dead:10000

Anfangsverdacht: Grooming-Token??

jedes NOTIFY führt sofort zu einem POST — mit (start,deadline) vom Target

dieses Prinzip könnte man aufweichen — ich halte das aber für eine schlechte Idee, denn der Scheduler ist auch so schon schwer genug zu verstehen, auch die Activity-Language beginnt schon zu »bröseln« ... gefährliche Tendenz. Meine Einschätzung ist, wir planen ehr so 10 Jobs pro Metajob-Lauf, d.h. das wären dann Größenordnung 500-1000µs Zeit, in der jedwede andere Management-Tätigkeit geblockt ist (renderjobs laufen trotzdem weiter). Sofern diese Annahme nicht zutrifft, möchte ich dafür konkrete Beweise sehen

commit 5c6354882de1be63724221022b374bd559a499b0

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Wed Nov 8 20:58:32 2023 +0100

    Scheduler: solve problem with transport from entrance-queue

   

    The test case "scheduleRenderJob()" -- while deliberately operated

    quite artificially with a disabled WorkForce (so the test can check

    the contents in the queue and then progress manually -- led to discovery

    of an open gap in the logic: in the (rare) case that a new task is

    added ''from the outside'' without acquiring the Grooming-Token, then

    the new task could sit in the entrace queue, in worst case for 50ms,

    until the next Scheduler-»Tick« routinely sweeps this queue. Under

    normal conditions however, each dispatch of another activity will

    also sweep the entrance queue, yet if there happens to be no other

    task right now, a new task could be stuck.

   

    Thinking through this problem also helped to amend some aspects

    of Grooming-Token handling and clarified the role of the API-functions.

aber sie dann auch zusätzlich in das post()  eingesetzt

...dann erlangt er es, und behält es

das ist nicht gut

...denn es führt dazu, daß ab der 2. Schleifen-Iteration die Allokation neuer Activities nicht mehr geschützt ist; das könnte den beobachteten Crash erklären

und: die Allokation findet hier statt

    term_ = move(

      theScheduler_->activityLang_

          .buildCalculationJob (job_, start_,death_));

...eben wenn jemand von außen kommt... das ist i.d.R. nur beim seedCalcStream() der Fall

zum einen ist das nicht genau das feste Raster, und außerdem braucht der OS-Scheduler sowiso immer etwas länger

sage bewußt »dokumentieren«, denn diese Logik hier ist derart einfach zu implementieren, daß eigentlich kein Test notwendig wäre

das ist nämlich unverhältnismäßig schwer im Test zu verifizieren, und steht in keinem Verhältnis zur Aufgabe

Assertion-Failure: der Aufrufer von dropGroomingToken() hält es nicht

das eigentliche Problem ist, daß der Planer sein eigenes Schedule überfährt

Forderung: ein echer Planer muß das erkennen und abbrechen

in entsprechend analogen Situationen wird jetzt einfach wirr bis zu Ende geplant und irgendwo bleibt die Berechnung stecken... so soll's sein

...und diese wird im aktuellen Test-Setup nicht von der Grooming-Token-Klammer erfaßt

durch die veränderte Grooming-Token-Behandlung läuft der Planungs-Job nun exclusiv (wie es sein sollte); da aber für den Debug-Modus das pre-Roll-Stepping zu knapp gewählt ist, ist er noch +3ms über den geplanten Startpunkt hinaus aktiv (für den späteren Teil des Zeitplans, insofern kein Problem). Da die calculation-structure zu Beginn noch dünn ist, holt der Scheduler das bis Node-23 locker wieder rein

trickreich: brauche schleppenden State

  • wenn das erste Element nicht den Default-Wert des Gruppierungskriteriums hat ⟹ muß einmal pullen und neu aufsetzen
  • wenn quelle bereits hinter dem letzten Element steht ⟹ gibt noch das letzte gruppierte Element aus
  • danach muß noch genau eine Iteration erlaubt werden

weil die Invariante eben auch erfüllt ist, wenn man hinter dem Ende steht; insofern ist die Auswertung ja tatsächlich »schleppend«, d.h. man gibt immer die vorhergehende Gruppe aus

und zwar der Umstand, daß die Nodes innerhalb eines Levels stets voneinander unabhängig sind; deshalb sind zumindest diese Nodes komplett parallelisierbar

und withAdaptedSchedule muß man explizit aufrufen (wegen Stress-Faktor)

Idee: logarithmische Suche nach dem breaking point

das ist aber nicht klar genug ausgeprägt, um daraus ein Kriterium zu machen

Mittelpunkt auswerten: test(m)

deshalb habe ich gezögert, diese Möglichkeit gleich vorzusehen; letztlich aber wiegt das »Ausleuchten« von Verhaltensmustern im Test mehr (und ich erwarte keinen westentlichen Einfluß auf die Performance)

Das war anfangs ein erhebliches Problem, bis ich mich dazu durchgerungen hatte, den unmittelbaren Dispatch aufzugeben. Seither kann ein Job auch verspätet abgesetzt werden, und wandert dann sofort an die Spitze der Queue. Und da wir das komplette Array mit den Activity-Termen Aufrufu-übergreifend erhalten, können dann eben Jobs auch »schleppend« abgesetzt werden. Genau das nutzen wir hier aus

anders kann ich mir das nicht erklären, denn es ist ja nun immer nur eine kleine Zahl an Einträgen in der Queue, d.h. es besteht durchaus die Gefahr, daß sich Worker schlafen legen, weil die Queue scheinbar leer ist. Tatsächlich passiert das aber nur in den Abschnitten mit geringer Parallelität (ja der Doppel-Strang wird nun nur von einem Worker bearbeitet)

bisher mußte er explizit per post() erzeugt werden — nun meint post() wirklich die Übergabe an den Scheduler, und der Term wird hier nur noch erzeugt, wenn er nicht schon besteht. Dies rechtfertigt dann auch das optional<ActitityTerm> und wirkt so viel natürlicher

muß dann die Info über zwei Ebenen durchreichen...?

...es ist nämlich redundant: wenn man keinen constraint setzen möchte, dann definiert man eben die Start-Zeit des Nachfolgers entsprechend früher.

ALLE Signalmarken haben einen vierstelligen Namen, und dabei soll es bleiben. Beim Formulieren der Dokumentation fällt mir auf, daß KICK klarer ist, und sich besser in Formulierungen fügt. Man könnte allerdings diesen Zustand auch DAVE nennen, weil wir dem Worker sagen „cant do that, Dave“

if ret != PASS könnte jetzt logisch falsch sein

und zwar wegen der jüngsten Änderung in λ-post; dieses stellt nun nur noch in die Queue und geht nicht mehr in den Dispatch — andererseits weiß ich, daß der neue Zustand nur an einer einzigen Stelle injiziert wird, nämlich wenn postChain zwar feststellt, daß es sofort in den Dispatch gehen soll, dann aber nichts von der Queue bekommt (weil das Grooming-Token nicht erlangt werden konnte)

man würde damit einen Regelbreich schaffen können, den man über die Parametrisierung abstimmt; allerdings wirft das das Problem auf, wie man sowas beobachten, messen und kontrollieren kann (da es stark systemabhängig ist)

Das läuft auf einen Effekt hinaus, der bei Wiederholung schnell so drastisch wird, daß der Worker mehr oder weniger »aus dem Verkehr« gezogen wird. Für diesen Ansatz spricht, daß er praktisch nicht von der konkreten System-Performance abhängig ist (mit Einschränkungen). Da wir ein Kurzzeit-Gedächtnis haben, wäre dieser Effekt dann aber auch nur mäßig »sticky«, was angemessen erscheint (im Grunde bräuchten wir eine Hysterese

wegen einigen wenigen Contentions dürfen wir kein G'summs machen, in diesem unteren Bereich ist eigentlich nur die Randomisierung wichtig; ab einem gewissen Punkt müssen dann aber drastischere Maßnahmen folgen, sonst kann das System ersticken (wie ich jetzt im konkreten Testfall sehe)

das ist doof ... weil unser Thread-Wrapper den Thread relativ spät erzeugt, und eben grade nicht aus der Initialiser-List; außerdem gibt es eine signifikante Verzögerung bis die Thread-ID da ist (hab da schon > 200µs gesehen)

67: INIT @0  rand=6

67: ◁ @148

67: △1  @196

67: ◁ @258

67: △2  @293

67: △2  @340

67: ◁ @387

67: △3  @420

67: △3  @470

67: △3  @520

67: ◁ @570

67: ▲4  @603

67: ◁ @893

67: ▲5  @948

67: ◁ @1387

67: ▲6  @1441

67: ◁ @2269

67: ▲7  @2417

67: ◁ @3888

67: ▲8  @4028

67: ◁ @6785

EA: INIT @0  rand=2

EA: ◁ @225

EA: △1  @280

EA: ◁ @368

EA: △2  @419

EA: △2  @482

EA: ◁ @544

EA: △3  @594

EA: △3  @657

EA: △3  @722

EA: ◁ @787

EA: ▲4  @836

EA: ◁ @1114

EA: ▲5  @1168

EA: ◁ @1529

EA: ▲6  @1581

EA: ◁ @2178

EA: ▲7  @2232

EA: ◁ @3398

EA: ▲8  @3521

EA: ◁ @5607

EA: ◁ @5719

EA: ◁ @5754

EA: ◁ @5787

EA: ◁ @5821

EA: ◁ @5852

EA: ◁ @5886

EF: INIT @0  rand=3

EF: ◁ @209

EF: △1  @260

EF: ◁ @333

EF: △2  @379

EF: △2  @440

EF: ◁ @499

EF: △3  @544

EF: △3  @607

EF: △3  @669

EF: ◁ @731

EF: ▲1  @794

EF: ◁ @1080

EF: ▲2  @1132

EF: ◁ @1508

EF: ▲3  @1556

EF: ◁ @2252

EF: ▲4  @2392

EF: ◁ @3615

EF: ▲5  @3789

EF: ◁ @6049

EF: ▲5  @6194

EF: ◁ @8410

EF: ▲5  @8509

EF: ◁ @10744

allein die Print-Statements können schon 100µs und mehr kosten, und vergiften den Cache. Trotzdem beobachte ich im Test(Debug-Mode) auch ohne die Print-statements etwa die gleiche Zahl Zyklen in 5ms

das ist spannend...

denn das bedeutet, daß der Scheduler eine zusätzliche Verzögerung von +3ms in diesem Fall einfach aufgefangen hat — vermutlich in der ersten Phase, in der noch »Luft« im Zeitplan ist, und daher die Abläufe im Wesentlichen durch die Trigger-Zeiten getacktet werden

»normal« bedeutet, daß die Planung eines Jobs 100-200µs dauert

dieser Worker taucht dann nur noch einmal auf, 20ms später; es ist aber nicht zu erkennen, warum (möglicherweise war er dazwischen da, aber es gab keine Arbeit)

kann man nicht genau sagen, weil er beim 3.Mal schon wieder was bekommt; jedenfalls macht er keine auffälligen Verzögerungen

das ist völlig unauffällig und etwa so wie vorher. An der Stelle sind im Doppelstrang die ersten Nodes mit einem höheren Gewicht; bis zu dem Punkt hat ein Worker die ganze Dependency-Kette allein abgearbeitet, während die beiden anderen Worker die restlichen (leeren) Jobs vom Schedule weggeräumt haben. Nun ist der erste Worker länger weg, und der 2. Worker greift mit geringer Verzögerung mit ein.

Fazit: Schema funktioniert und wirkt wie erhofft

...hab die Logik grade nochmal im Detail durchgeprüft: die headDistance ist von tendNext abhängig, aber der relevante Fall tritt nur ein wenn bereits tendedNext gilt. Des Weiteren gilt als der »near horizon« die Spanne zwischen 50µs und 5ms. Und genau in dem Bereich fokussieren wir nun; und zwar „in erster Näherung“ mal mit dem Abstand selber

...der OS-Scheduler bekommt selten einen genauen sleep unterhalb von 100µs in, und auch der Einstieg in den Chain bzw. die Koordination braucht so lang.

die Parallelisierung ist nur so hoch, wie grade Worker zur Verfügung stehen; sofern wir nun die Kapazität im Nahfeld stärker fokussieren, sollten im kritischen Moment (in dem plötzlich ein starkes Potential zur Parallelisierung besteht) auch im Schnitt mehr Worker verfügbar werden.Was dann allerdings auch zu stärkerer Contention führen kann ⟹ Umbau unter empirischer Kontrolle des Effekts

das ist aber keine dramatische Verschlechterung, vielmehr nur sichtbar, wenn man genau nachrechnet

... dispose(i=66,lev:25) -> @25000

‖•△•‖     wof:8 HT:0

‖SCH‖ 8D: @2984 ○ start=25000 dead:50000

   ·‖ FE: @3002 HT:0  -> ∘

                |n.(67,lev:25)

... dispose(i=67,lev:25) -> @25000

‖•△•‖     wof:8 HT:0

‖SCH‖ 8D: @3069 ○ start=25000 dead:50000

                |n.(68,lev:25)

... dispose(i=68,lev:25) -> @25000

‖•△•‖     wof:8 HT:0

‖SCH‖ 8D: @3141 ○ start=25000 dead:50000

                |n.(69,lev:25)

... dispose(i=69,lev:25) -> @25000

‖•△•‖     wof:8 HT:0

‖SCH‖ 8D: @3214 ○ start=25000 dead:50000

                |n.(70,lev:26)

   ·‖ 1C: @3257 HT:0  -> ∘

+++ 8D: Continuation(lastNode=69, levelDone=25, work_left:true)

--> reschedule to ...133

   ·‖ 63: @3563 HT:0  -> ∘

   ·‖ E0: @3601 HT:0  -> ∘

   ·‖ B1: @3611 HT:0  -> ∘

   ·‖ 63: @4258 HT:0  -> ∘

   ·‖ EC: @4625 HT:0  -> ∘

   ·‖ E0: @5129 HT:0  -> ∘

   ·‖ 92: @5247 HT:0  -> ∘

   ·‖ FE: @5476 HT:0  -> ∘

   ·‖ 1C: @6053 HT:0  -> ∘

   ·‖ 63: @6099 HT:0  -> ∘

   ·‖ 63: @6353 HT:0  -> ∘

   ·‖ B1: @6571 HT:0  -> ∘

   ·‖ EC: @6615 HT:0  -> ∘

   ·‖ 92: @7579 HT:0  -> ∘

   ·‖ 63: @7934 HT:0  -> ∘

   ·‖ FE: @7949 HT:0  -> ∘

   ·‖ E0: @8096 HT:0  -> ∘

extent-family.hpp line 105

extent-family.hpp l.201

          if (not canAccomodate (cnt))

            {//insufficient reserve => allocate 

              size_t oldSiz = slotCnt();

              size_t addSiz = cnt - freeSlotCnt()

                              + EXCESS_ALLOC;

              // add a strike of new extents at the end

              extents_.resize (oldSiz + addSiz);

block-flow.hpp, line 489

              if (lastEpoch().deadline() < deadline)

                {   // a deadline beyond the established Epochs...

                  //  create a grid of new epochs up to the requested point

                  TimeVar lastDeadline = lastEpoch().deadline();

                  auto distance = _raw(deadline) - _raw(lastDeadline);

                  EpochIter nextEpoch{alloc_.end()};

                  ENSURE (not nextEpoch);      // not valid yet, but we will allocate there...

                  auto requiredNew = distance / _raw(epochStep_);

                  if (distance % _raw(epochStep_) > 0)

                    ++requiredNew;  // fractional:  requested deadline lies within last epoch

                  alloc_.openNew(requiredNew);

Fazit: es ist ein einziger Call — der verlangt Unmögiches vom Memory-Manager

...es geht hier um Performance-Messungen, die allerdings auch schon im Debug-Build aussagekräftig sein sollen (den Release-Build betrachte ich dann als zusätzlichen Bonus); deshalb habe ich auf viele Sicherheitsmaßnamen und Diagnose-Hilfsmittel verzichtet, die ich normalerweise in Tests einsetze

nun schon mehrfach aufgefallen: die Allokationen passieren schon vor der Verarbeitung

ist gut, und setzt auch eine Begrenzung durch, die den Allokator vor Überforderung schützen soll, kommt aber leider zu spät für den Allokator, der ist dann schon tot

...hab ich in den letzten Tagen aufgeklärt und gefixt: das Grooming-Token wurde im Scheduler gedroppt, wird aber im Planungs-Job gebraucht, denn die Allokation erfolgt dort (also früher als gedacht)

commit 892099412cdf171d9a7f960c4a5e2d2b063bd5fc

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Tue Nov 7 18:37:20 2023 +0100

    Scheduler: integrate sanity check on timings

   

    ...especially to prevent a deadline way too far into the future,

    since this would provoke the BlockFlow (epoch based) memory manager

    to run out of space.

   

    Just based on gut feeling, I am now imposing a limit of 20seconds,

    which, given current parametrisation, with a minimum spacing of 6.6ms

    and 500 Activities per Block would at maximum require 360 MiB for

    the Activities, or 3000 Blocks. With *that much* blocks, the

    linear search would degrade horribly anyway...

der sanityCheck prüft also vornehmlich daß es eine Deadline gibt

...dazu müßte nämlich der BlockFlow viel dynamischer seine Config auswerten — lästiges Thema — YAGNI

wähle hier 8 GiB für die totale Allokation

begrenze hier konsistent mit dem Scheduler auf +3000 neue Blöcke pro Schritt

weil das Limit im BlockFlow-Allokator nur greift, wenn wir tatsächlich  auf Minimal-Blockggröße unten sind, während der Scheduler eine feste Obergrenze für die Deadlines erzwingt.

und ist jetzt sogar schenller als der single-threaded Fall

die veranschlagte Zeit ist viel zu kurz

Die Grundannahme war, daß die Planung in einem sicheren Environment erfolgt und nicht korrupt ist. Das gilt für die Lumiera-Engine insgesamt. Deshalb wird auf spezielle Härtung bei der Planung verzichtet (zusätzliche Konsistenz-Checks, Synchronisation, Commits/Transaktionen)

          Extent&

          access()

            {

              auto* rawStorage = this->get();

              ENSURE (rawStorage != nullptr); 

              return static_cast<Extent&> (rawStorage->array());

            }

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::access(unsigned long) const+0xdd

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::IdxLink::yield() const+0x26

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor::yield() const+0x18

lib::IterableDecorator<vault::gear::blockFlow::Epoch<vault::mem::ExtentFamily<vault::gear::Activity, 500ul> >, vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor>::operator->() const+0x20

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::claimSlot()+0x2b

vault::gear::Activity& vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::create<vault::gear::Activity::Verb>(vault::gear::Activity::Verb&&)+0x2e

vault::gear::activity::Term::appendNotificationTo(vault::gear::activity::Term&)+0x2a

vault::gear::ScheduleSpec::linkToSuccessor(vault::gear::ScheduleSpec&)+0x43

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::setDependency(vault::gear::test::TestChainLoad<16ul>::Node*, vault::gear::test::TestChainLoad<16ul>::Node*)+0xad

auto vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(auto:1*, auto:2*)#2}::operator()<vault::gear::test::TestChainLoad<16ul>::Node, {lambda(auto:1*, auto:2*)#2}::operator()>(vault::gear::test::TestChainLoad<16ul>::Node*, {lambda(auto:1*, auto:2*)#2}::operator()*) const+0x2e

std::_Function_handler<void (vault::gear::test::TestChainLoad<16ul>::Node*, vault::gear::test::TestChainLoad<16ul>::Node*), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(auto:1*, auto:2*)#2}>::_M_invoke(std::_Any_data const&, vault::gear::test::TestChainLoad<16ul>::Node*&&, std::_Any_data const&)+0x52

std::function<void (vault::gear::test::TestChainLoad<16ul>::Node*, vault::gear::test::TestChainLoad<16ul>::Node*)>::operator()(vault::gear::test::TestChainLoad<16ul>::Node*, vault::gear::test::TestChainLoad<16ul>::Node*) const+0x61

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x2fc

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::postChain(vault::gear::ActivationEvent)+0x2a9

vault::gear::Scheduler::continueMetaJob(lib::time::Time, vault::gear::Job, vault::gear::ManifestationID)+0x164

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::continuation(unsigned long, unsigned long, bool)+0x21a

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}::operator()(unsigned long, unsigned long, bool) const+0x34

std::_Function_handler<void (unsigned long, unsigned long, bool), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&, bool&&)+0x6e

std::function<void (unsigned long, unsigned long, bool)>::operator()(unsigned long, unsigned long, bool) const+0x77

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x463

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::postChain(vault::gear::ActivationEvent)+0x2a9

vault::gear::Scheduler::continueMetaJob(lib::time::Time, vault::gear::Job, vault::gear::ManifestationID)+0x164

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::continuation(unsigned long, unsigned long, bool)+0x21a

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}::operator()(unsigned long, unsigned long, bool) const+0x34

std::_Function_handler<void (unsigned long, unsigned long, bool), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&, bool&&)+0x6e

std::function<void (unsigned long, unsigned long, bool)>::operator()(unsigned long, unsigned long, bool) const+0x77

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x463

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::postChain(vault::gear::ActivationEvent)+0x2a9

vault::gear::Scheduler::continueMetaJob(lib::time::Time, vault::gear::Job, vault::gear::ManifestationID)+0x164

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::continuation(unsigned long, unsigned long, bool)+0x21a

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}::operator()(unsigned long, unsigned long, bool) const+0x34

std::_Function_handler<void (unsigned long, unsigned long, bool), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&, bool&&)+0x6e

std::function<void (unsigned long, unsigned long, bool)>::operator()(unsigned long, unsigned long, bool) const+0x77

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x463

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::postChain(vault::gear::ActivationEvent)+0x2a9

vault::gear::Scheduler::continueMetaJob(lib::time::Time, vault::gear::Job, vault::gear::ManifestationID)+0x164

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::continuation(unsigned long, unsigned long, bool)+0x21a

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}::operator()(unsigned long, unsigned long, bool) const+0x34

std::_Function_handler<void (unsigned long, unsigned long, bool), vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::ScheduleCtx(vault::gear::test::TestChainLoad<16ul>&, vault::gear::Scheduler&)::{lambda(unsigned long, unsigned long, bool)#3}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&, bool&&)+0x6e

std::function<void (unsigned long, unsigned long, bool)>::operator()(unsigned long, unsigned long, bool) const+0x77

vault::gear::test::RandomChainPlanFunctor<16ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x463

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::activity::Proc vault::gear::SchedulerCommutator::postDispatch<vault::gear::Scheduler::ExecutionCtx>(vault::gear::ActivationEvent, vault::gear::Scheduler::ExecutionCtx&, vault::gear::SchedulerInvocation&)+0xc1

vault::gear::Scheduler::getWork()::{lambda()#2}::operator()() const+0x3ce

vault::gear::Scheduler::WorkerInstruction vault::gear::Scheduler::WorkerInstruction::performStep<vault::gear::Scheduler::getWork()::{lambda()#2}>(vault::gear::Scheduler::getWork()::{lambda()#2})+0x26

vault::gear::Scheduler::getWork()+0x3d

vault::gear::Scheduler::Setup::doWork()+0x1c

vault::gear::work::Worker<vault::gear::Scheduler::Setup>::pullWork()+0x26

und diese re-entrance 4-Mal wiederholt

                |n.(412,lev:72)

... dispose(i=412,lev:72) -> @72000

‖•△•‖     wof:8 HT:154587

‖SCH‖ 0F: @154927 ○ start=72000 dead:100000

..!   FD: @154922  ⤴       (i=380)

   ·‖ 74: @154936 HT:154587  -> ∘

‖PST‖ FD: @154954 ◒ start=68000▹▹69000 dead:100000

   ·‖ 74: @154958 HT:307445672727859657  -> ▶ 154587

‖▷▷▷‖ 74: @ 154976 EMPTY

!◆!   0F: @154940  ⚙  calc(i=412, lev:72)

‖PST‖ FD: @154987 ◒ start=68000▹▹69000 dead:100000

..!   BF: @154998  ⤴       (i=381)

‖PST‖ FD: @155018 ◒ start=68000▹▹69000 dead:100000

‖PST‖ BF: @155029 ◒ start=68000▹▹69000 dead:100000

‖PST‖ FD: @155033 ◒ start=68000▹▹69000 dead:100000

‖PST‖ BF: @155047 ◒ start=68000▹▹69000 dead:100000

   ·‖ FD: @155056 HT:69000  -> ∘

‖PST‖ BF: @155063 ◒ start=68000▹▹69000 dead:100000

   ·‖ FD: @155071 HT:69000  -> ∘

‖PST‖ BF: @155079 ◒ start=68000▹▹69000 dead:100000

   ·‖ FD: @155087 HT:69000  -> ∘

   ·‖ FD: @155108 HT:69000  -> ∘

   ·‖ BF: @155114 HT:69000  -> ∘

   ·‖ 74: @155076 HT:69000  -> ▶ 69000

   ·‖ FD: @155131 HT:69000  -> ∘

   ·‖ BF: @155134 HT:69000  -> ∘

   ·‖ BF: @155178 HT:69000  -> ▶ 69000

!◆!   74: @155162  ⚙  calc(i=383, lev:69)

!◆!   BF: @155194  ⚙  calc(i=384, lev:69)

   ·‖ FD: @155354 HT:69000  -> ▶ 69000

!◆!   FD: @155371  ⚙  calc(i=387, lev:69)

..!   0F: @155533  ⤴       (i=412)

0000000615: PRECONDITION: extent-family.hpp:321: thread_4: access: (isValidPos (idx))

Das ist ein Problem mit der Steuerung des Grooming-Tokens

< now ⟹ re-entrant

== now ⟹ re-entrant

< now ⟹ re-entrant

< now ⟹ re-entrant

< now ⟹ re-entrant

...

...

...

< now ⟹ re-entrant

@36141 Continuation(lastNode=304, levelDone=43) --> to ...324  ○ start=45000

Level 19(lang) * 5 wird von 5 Threads bearbeitet

        scheduleCalcJob_(currIdx_, n->level);

        for (Node* pred: n->pred)

          markDependency_(pred,n);

... dispose(i=412,lev:72) -> @72000

‖•△•‖     wof:8 HT:154587

‖SCH‖ 0F: @154927 ○ start=72000 dead:100000

!◆!   0F: @154940  ⚙  calc(i=412, lev:72)

‖PST‖ FD: @154987 ◒ start=68000▹▹69000 dead:100000

..!   BF: @154998 ⤴       (i=381)

‖PST‖ FD: @155018 ◒ start=68000▹▹69000 dead:100000

‖PST‖ BF: @155029 ◒ start=68000▹▹69000 dead:100000

   ·‖ 74: @155076 HT:69000  -> ▶ 69000

!◆!   74: @155162  ⚙  calc(i=383, lev:69)

!◆!   BF: @155194  ⚙  calc(i=384, lev:69)

   ·‖ FD: @155354 HT:69000  -> ▶ 69000

!◆!   FD: @155371  ⚙  calc(i=387, lev:69)

..!   0F: @155533        (i=412)

PRECONDITION: extent-family.hpp:321: thread_4: access: (isValidPos (idx))

...und der Stack-Trace zeigt ja, daß der Call-Pfad über SchedulerCtx::setDependency()  lief

sichtbar wurde das durch eine zu späte Startzeit

der reale Planer muß dafür sogar elaborierte Kontroll-Logik  haben

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::access(unsigned long) const+0xdd

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::IdxLink::yield() const+0x26

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor::yield() const+0x18

lib::IterableDecorator<vault::gear::blockFlow::Epoch<vault::mem::ExtentFamily<vault::gear::Activity, 500ul> >, vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor>::operator->() const+0x20

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::claimSlot()+0x2b

vault::gear::Activity& vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::create<vault::gear::Activity::Verb>(vault::gear::Activity::Verb&&)+0x2e

vault::gear::activity::Term::appendNotificationTo(vault::gear::activity::Term&)+0x2a

vault::gear::ScheduleSpec::linkToSuccessor(vault::gear::ScheduleSpec&)+0x61

vault::gear::test::TestChainLoad<16ul>::ScheduleCtx::setDependency(vault::gear::test::TestChainLoad<16ul>::Node*, vault::gear::test::TestChainLoad<16ul>::Node*)+0xad

                |n.(361,lev:62)

... dispose(i=361,lev:62) -> @62000

|·?·|

|·◆·|

‖•△•‖     wof:8 HT:162454

‖SCH‖ 99: @161506 ○ start=62000 dead:100000

!◆!   99: @161518  ⚙  calc(i=361, lev:62)

..!   99: @162551  ⤴       (i=361)

|·○·|

|·?·|

   ·‖ D5: @162567 HT:307445574858905914  -> ▶ 162454

‖▷▷▷‖ D5: @ 162633 EMPTY

|·◆·|

|·◇·|

|·?·|

|·◆·|

|·◇·|

                |n.(362,lev:62)

... dispose(i=362,lev:62) -> @62000

|·?·|

|·◆·|

‖▷▷▷‖ 99: @ 162887 EMPTY

‖IGN‖     wof:8

‖SCH‖ 99: @162979 ○ start=62000 dead:100000

|·◇·|

|·?·|

|·◆·|

|↯↯↯| wrapped:true start=7 idx=6 after=2 slcnt:15

0000000620: PRECONDITION: extent-family.hpp:327: thread_6: access: (isValidPos (idx))

   ·‖ 8C: @167889 HT:167887  -> ∘

   ·‖ 0B: @167888 HT:167887  -> ∘

   ·‖ 8C: @167920 HT:167887  -> ∘

falsche Schlußfolgerung vmtl auch schon für die vorherigen Incidents

ich hatte bisher die konkreten Koordinaten nicht zu fassen gekriegt, und deshalb eine Daten-Korruption durch concurrent Access vermutet

ein use-after-free oder ein bereits überschriebener oder rotierter Block

isValid = isWrapped()? (start_ <= idx and idx < slotCnt())

                        or idx < after_

                      : (start_ <= idx and idx < after_);

auch eine Rotation invalidiert bestehende Alloc-Handle

Nachtrag: gestern lief erfolgreich ein Graph mit 65535 Nodes

die typische Test-Load ~ 500µs braucht im Scheduler-Kontext oft doppelt so lange als kalibriert

nachdem ich aber nur noch per NOTIFY triggere,

ist die beobachtbare Verzögerung nun im Gegenteil oft zu kurz.

Nur an einer Stelle mit vielen aktiven Workern, treten mal wieder 2ms auf

0.42 statt 0.56

ohne schedule-dependency ist die Performance aber minimal schlechter

ohne Limitierung der NOTIFY-Zeit ist die Performance minimal besser

die ganzen Test zur Integration und zum Aufbau der Testanordnung haben die »chained load bursts« verwendet, ein hochgradig unregelmäßiges und abschnittsweise stark verknüpftes Pattern. Damit konnte ich in etwa die erwartete Parallelisierung beobachten, aber die Computational Load ist typischerweise doppelt so lang gelaufen wie kalibriert, während gleichzeitig permanent Koordinations-Aufwand zu leisten war. Deshalb wähle ich nun einen anderen Blickwinkel: Wie gut können wir die theoretisch vorhandene »Rechenkapazität« zum Einsatz bringen? Dafür braucht es ein möglichst einfaches Pattern, das aber hinreichend breit sein muß, um alle Kerne auszulasten. Ziel ist es, einen gleichmäßigen »Flow« von länger laufenden Rechen-Jobs zu generieren

          TestChainLoad<8> testLoad{64};

          testLoad.seedingRule(testLoad.rule().probability(0.6).maxVal(2))

                  .pruningRule(testLoad.rule().probability(0.44))

                  .setSeed(62)

                  .buildTopology()

                  ;

          TestChainLoad<8> testLoad{64};

          testLoad.seedingRule(testLoad.rule().probability(0.6).maxVal(2))

                  .pruningRule(testLoad.rule().probability(0.44))

                  .setSeed(60)

                  .buildTopology()

                  ;

          TestChainLoad<8> testLoad{64};

          testLoad.seedingRule(testLoad.rule().probability(0.6).minVal(2))

                  .pruningRule(testLoad.rule().probability(0.44))

                  .setSeed(55)

                  .buildTopology()

                  ;

Fokus ⟹ Ziel ⟹ es geht um Halten ⟷ Brechen

es geht nicht um die Ermittlung einer empirischen »Performance«

sie muß gut genug im Sinn von tauglich sein

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

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

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

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

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

erster Versuch: überhaupt keinen Graph konstruieren

fast alle Threads sind im Contention-wait

das ist zwar ganz klar geplant, aber ich jetzt lasse ich die Tests erst mal so laufen...

Zwar beobachte ich die Memory-Corruption dann ehr beim Einfügen der Events, aber das kann sehr wohl ein Folge-Fehler sein. Ich sehe auch anderes Verhalten, das „eigentlich gar nicht möglich ist“

Hier passiert ein Vector-realloc. Wenn währenddessen der nächste Thread kommt, macht der ebenfalls ein re-Alloc und die Memory-corruption wäre garantiert.

Das ist ein Instrumentierungs-Hilfsmittel, und generell nur darauf ausgelegt, »richtig verwendet zu werden«

...waren aber geblockt, solange noch die eigentlichen Jobs mit t=0 in der Queue stehen

...und die Selbstreguliertung sorgt schntell dafür, daß der größte Teil der WorkForce in contention-Wait geht

zweiter Versuch: unconnected Nodes with weight (500µs)

das entspricht exakt der Concurrency

das ist effektiv concurrency ≡ 6

mit 400 Nodes  ⟼ real ∅ 12ms und Concurrency ≡ 7.9

...weil keine Festlegung auf eine bestimmte Zahl an Cores möglich ist und auch die Werte stark statistisch schwanken, besonders bei den relativ kurzen Laufzeiten, die hier aus praktischen Gründen erforderlich sind, schon um das Test-Setup einfach zu halten

Stichwort: Box stacking. Das ist ein NP-hartes Problem und wir lösen es hier nicht; vielmehr wird so getan, als könnte man um die durchschnittliche Concurrency elastisch stauchen. Das führt dann zum Auftreten eines weiteren »Form-Faktors«, der eben genau darin besteht, zu welchem Grad diese Annahme zu den strukturellen Beschränkugeneni im Widerspruch steht.

da wir die Überschreitung einer Grenze beobachten, kann bereits ein zufälliger "outlier" den Suchmechanismus in ein falsches Intervall „abbiegen lassen“. Denn eine einmal etablierte Intervall-Grenze wird grundsätzlich nicht nochmal geprüft. Ich hatte deswegen regelmäßig grob daneben liegende Ergebnisse beobachtet. Ein typisches Beispiel

  • aus vielen Tests ergibt sich, daß der breaking Point etwa bei 0.7 liegt
  • ein Testlauf mit 0.45 liefert zufällig bereits einen »Treffer« (insofern dieser Lauf eben dumm gelaufen ist und deshalb die Schwelle überschreitet)
  • ⟹ die binary search wird daher im Intervall ]0.45, 0.225] fortgesetzt....
  • alle weiteren Tests in diesem Intervall liefern  — erwartbar — keine Treffer mehr
  • ⟹ 0.45 - ε wird als »Ergebnis-Wert« geliefert
  • das ist logisch aber grob falsch, denn ~0.7 - 0.45 > ε

Mehraufwand ⟹ bereits ein entsprechend kleinerer Stress-Faktor hat die gleiche Stress-Wirkung

...das läßt dann die nominelle Concurrency als integral-Zahl intakt

denn das Schedule wird ja auf eine nominelle Concurrency ausgelegt

und diese wird ggfs aus strukturellen Gründen nicht ausgeschöpft

...die Berechnung läuft zwar genauso, nämlich über eine Gruppierung per Level, jedoch müssen dann nur die reinen Nodes pro Level berücksichtigt werden

weil die Gewichte entsprechend proportional auch in die durchschnittliche empirische Concurrency eingehen

...denn es ist tatsächlich sowas wie ein »Form-Faktor«, ergibt sich also aus der konkreten Dependency-Struktur und der konkreten Scheduling-Situation. Umso besser, daß dieser Wert sich auch nur leicht vom »Druck« abhängig erweist; es ist also etwas Situatives, und insofern arbeitet der »Streß« relativ dazu

ohne starke Abhängigkeiten ist durchaus elastisches Verhalten denkbar

Zwar ist die nun entwickelte Meßmethode durchaus elegant, wird aber zunehmend ungenau, wenn es kein klar definiertes »Brechen« mehr gibt, bzw. wenn man nur noch einen numerischen Grenzwert festlegen kann, der dann von den Dimensionen des konketen Falls abhängen. Für das wohldefinierte »Brechen« ist eine komplexe Abhängikeitsstruktur notewendig, die durch einen Rückstau komplett aus der Balance kippt. Das ist dann doch ein sehr spezieller Fall, denn in der Praxis erwarte ich im Durchschnitt ehr viele kleine 2-er und 3-er-Ketten ohne viel Verknüfpungen (ein IO-Job und ein Render-Job). Und bei länger laufenden Berechnungen wäre definitiv zu erwarten (ja sogar zwingend gefordert), daß sich freie Kapazität elastisch auf die nächsten Aufgaben verschieben läßt.

Der Scheduler ist ein komplexer Mechanismus, und allein das Hochfahren des Worker-Pools, alsauch das Erstellen des initialen Schedule erzeugt einen festen Overhead, der mehr oder weniger unkontrolliert in den Start der Arbeitsphase hineinwirkt. Je nachdem wann und wo der erste Eintrag im Schedule auftaucht, erscheinen zu einem bestimmten Zeitpunkt alle Worker und sorgen erst mal für deutlich meßbare Contention-Verzögerungen. Des Weiteren erzeugt auch die Abhängigkeit auf den Wake-up-Job einen erheblichen Overhead, sobald es nicht mehr einen einzigen stringenten Berchnungspfad gibt. Hinzu kommt, daß wir nicht auf einem Realtime-Betriebssystem arbeiten, und die vermutete Basis-Last stark von der Optimierung abhängig ist. Und außerdem hat der Scheduler nun einen speziellen Schutzmechanismus, der den Worker-Pool beim Auftreten von Contention effektiv herunterregelt. All dies zusammen macht eine Beobachtung des »Leerlauf-Aufwandes« sehr schwer...

Nebenbei sollten sich dabei auch nicht-lineare Basis-Effekte zeigen, aber idealerweise sollte das Verhalten ab einem gewissen Punkt linear werden; aus diesem näherungsweise linearen Verhalten ließe sich auf theoretischem Weg auch ein hypothetischer / amortisierter Basis-Aufwand ableiten. Die Hoffung ist, daß durch den Abgleich dieser verschiedenen Meßmethoden ein klareres Verständnis der Grenzen der Meßanordnung gewonnen werden kann.

verschiedene Parameter-Werte führen zu besserer statistischer Abdeckung

auto [stress,delta,time] = StressRig::with<Setup>()

                                     .perform<bench::BreakingPoint>();

  • legt Parameter-Typ und Ergebnis-Spalten fest
  • konfiguriert für jeden Lauf erneut einen Graphen
  • ist auch für das Aufsammeln der Ergebnisse zuständig

Darstellung als Gnuplot-Diagramm aufbereitet

was auch immer das ist....

empirisch(debug) ⟹ 17 Stellen

Das ist naheliegend und kompromittiert das Design nicht wesentlich; File-Operationen können ohnehin immer Fehler auslösen, und da ein Pfad invalid sein kann, ist auch das Original-Design der Tabelle in dieser Hinsicht inhärent stateful.

die Funktionalität selber steht nicht zur Debatte

...das ist grundsätzlicher Natur; da ich die eingebetteten Daten-Vectoren bewußt und explizit zugänglich gehalten habe (um auf weitergehende Zugriffsfunktionen verzichten zu können), kann man die Invarianten des Containers jederzeit unterlaufen. Ich habe jetzt nur eine Mitigation eingebaut, insofern das Hinzufügen neuer Zeilen die eingebetteten Daten beschneidet

Denn hier erfolgt der Feldzugriff sozusagen naïv, unter den Annahme daß niemand an den Einzeldaten manipuliert hat. Da die gesamte Semantik der Tabelle von der letzten Zeile her aufgebaut ist, wäre es korrekter, wenn man hier den Index an der nominellen DataFile::size() rückwärts verankern würde. Diese Überlegung zeigt aber, daß auch schon diese DataFile::size() unter der gleichen Annahme operiert, und damit zu einer Menge weiterere Inkonsistenzen führt

Das Design ist bewußt minimalistisch (und damit handwerklich orientiert): Es ist ein Werkzeug, keine Komponente aus dem Baukasten.

  • entweder man müßte die Daten-Vectoren komplett einkapseln, würde dann aber ein gutes Stück der Eleganz in Statistik-Berechnungen verlieren
  • zudem müßte man dann das Konzept eines »Cursors« einführen
  • oder man müßte überall komplexen Prüfcode einbauen, der auch explizit Performance kostet

...in der Hoffnung, daß ich sie dort dann demnächst wiederfinde...

...was nicht möglich ist, sofern man Gnuplot irgendwie den xrange automatisch bestimmen läßt (dann ist der xrange nämlich erst nach  dem Plotten bekannt)

mustache tmpl{"Hello {{what}}!"};
std::cout << tmpl.render({"what", "World"}) << std::en

dies ist eine Aufgabe, die in dynamischen Sprachen nahezu magisch lösbar ist

mir ist kein Vorschlag bekannt; vermutlich haben die Leute die gleiche Analyse vollzogen, wie ich sie hier grade mache

wenn man sich auf die Essenz beschränkt, kann man es selber schreiben

Keinerlei Annahmen über die Datenypen

ein situativ-funktionales Daten-Binding

als Einstellung des konkreten Templates? ⟹ das wäre einfach, aber unpraktisch für den Client

als Einstellung im Data-Binding? ⟹ das wäre einfach für den Client, wäre aber für Standard-Templates endgütlig festgelegt

über einen freien Erweiterungspunkt? ⟹ die optimal flexible Lösung, aber trickreich zu realisieren und schwer zuverlässig zu steuern

das bedeutet: nachdem ein nested-context geöffnet wurde, müssen wir einen State erlangen, auf dem transparent genauso gearbeitet werden kann, wie auf dem initialen / top-level-State

weil sie im Einzelfall auch komplexer sein kann, und im Besonderen eine parent-Verknüpfung beinhaltet

...dieser enthält die Informationen aus dem RegExp-Match bereits semantisch aufgeschlüsselt

...das heißt, die einzelne Auswertung ist keine pure function — aber der Seiteneffekt-Stat verbleibt in der Pipeline selber und merkt sich den Endpunkt des vorausgehenden Matches

will sagen, der Standardfall ist, lediglich auf eine Map per Key zuzugreifen — und die gesamte Datenstruktur ist hierauf zu optimieren

uuund fertig — C++ ist toll

naja...

Hat teilweise schon was gebracht, denn der Parser ist schön kompakt und klar geworden — aber für den Compiler bin ich genau deshalb in den Urwald geraten — das war eine „schwere Geburt“ — und in mehreren Schritten bin ich letztlich bei einer funktional-imperativen Formulierung angekommen, die jetzt einigermaßen gut lesbar ist...

man könnte wohl was basteln mit den Funktionen position(i) und length(i)

...und diese soll irgendwie auf eine Pipeline aufbauen. Das bedeutet, die Lösung sollte möglichst in der Verarbeitung selber zugänglich sein, und nicht über eine externe Zusatz-Information oder einen Seiteneffekt. Es wäre denkbar, auf das Ende des letzten Match aufzubauen — allerdings noch viel schöner wäre es, wenn der letzte Match den Quell-String komplett ausschöpft, so daß gar kein Rest übrig bleibt

erst in einem zweiten Schritt wird explizit eine spezifische Action für diese Syntax emittiert

in diesem speziellen Fall wird das verbleibende Postfix

vom letzten beobachteten Syntax-Match als TEXT-lead ausgegeben

zunächst einmal: Deklaration kann auto verwenden

Grund: die expliziten Spezialisierungen kommen überhaupt erst ins's Spiel, wenn der Compiler die konkreten Template-Argumente bereits erschlossen hat. Vorher schaut er nur in das primäre Template... und wenn das keinen Konstruktor hat, dann kennt der Compiler nur den Copy-Konstruktor. Die resultierende Fehlermeldung ist dann unglaublich hilfreich...

Das sind die Limitierungen bei Function-Overloads. Mit Template-Deduction-Guides hätten wir dieses Problem vermutlich nicht (wenn wir sie denn schreiben könnten).

Im Detail: Bei der Function Overload-Resolution werden die verschiedenen Kandidaten geordnet. Dabei werden zunächst alle überschüssigen und Default-Argumente weggestrichen. Konsequenz: ein enable-If kann zwar einen einzelnen Overload entfernen — wenn er aber nicht entfernt wurde, stehen zwei äquivalente Overloads da, und es gibt einen Compilation-Fehler. Rückgabewerte helfen hier auch nichts (die tragen nur zur const-ness-Auswahl bei). Das heißt, bei Function-Overload-Resolution muß das enable-If auf einem weiteren  Parameter stehen, der auch tatsächlich verwendet wird. Und an der Stelle wird's dann wirklich so trickreich, das es mir fragil erscheint.

GenNode wurde explizit entworfen

um »Programming by Reflection«

zu unterbinden

mit einer pure virtual String-Repräsentation

  template<typename TYPES>

  template<typename TY>

  Variant<TYPES>::Buff<TY>::operator string()  const

  {

    return util::typedString (this->access());

  }

OK ... nicht ganz das was wir brauchen

...seinerzeit bin ich da mit meinem Design an Grenzen gestoßen und konnte (oder wollte) mich nicht aus der Zwanslage befreien; das Problem ist, daß die Dispatch-Funktion selber wieder eine virtuelle Funktion sein muß, die auf der »inneren Hülle« des Variant-Record definiert ist (damit dann die äußere Hülle ohne weitere Vorkenntnisse den Visitor nach innen schieben kann). Mit dem bestehenden Code ist man also in der Signatur des Visitors nicht frei, sondern hat nur wenige, festgelegte Varianten zus Auswahl (für die jeweils ein kompletter Pfad nach innen definiert ist).

habe eine dritte Visitor-Variante eingeführt: den »Renderer«

das ist dann ein VisitorConstFunc<string>

Man könnte erwarten, daß diese string-view tatsächlich nur während der Iteration genutzt wird — letztlich möchte man ja das gerenderte Text-Template irgendwo integrieren...

...also auf Top-Level einen Record<GenNode> annehmen. Teilweise wird das im Diff-Framework und auch in diversen Tests so gehandhabt (da es einfacher ist)

...weil eine DataSource ein Referenz-Wrapper ist, und damit einen Pointer auf das Start-Element haben muß

....je länger ich darüber nachdenke —

umso mehr sehe ich diese Idee im Widerspruch zum GenNode-Design

Das ist gradezu die Grundannahme: der Verarbeiter hat die Struktur zu kennen — die Node hat keine introspektiven Fähigkeiten

DataSrc-Impl kann sich auf stabile Memory-Location verlassen

wir brauchen nur den Level traits + util + elementares Metaprogramming

...denn in der Praxis wird erwartet, daß im Scope sehr wohl wieder verschachtelte Records liegen; sonst würde diese Datenstruktur ja wenig Sinn machen

⟹ wenn Kind kein Record ist

es gibt ein Prädikat genNode.isNested()

watt soll der Jeitz

...wegen möglichen Style-Adjustments, die ggfs. eine spezielle Datenspalte auswerten könnten, um Entscheidungen zu treffen — allesamt Entscheidungen, die man notfalls auch hart-gecodet als Parameter durchgeben könnte

manches Problem kann man auch in Gnuplot lösen...

...relevant immer wenn die Daten als Linien-Plot (wie eine Funktion) dargestellt werden sollen, denn dann müssen die Zeilen nach Abszissenwert aufsteigend sortiert sein

nicht Header-Zeile, also Zeile 2.

Einzelne Datenpunkte können problemlos fehlen....

man kann hier eigentlich nur stichprobenartig verifizieren, daß das jeweilige Template zum Einsatz kam, und daß einige markante Werte per Text-Templating eingebaut wurden. Also z.B. die Datenheader, oder eine Achsenbeschriftung.

damit man verschiende Messungen mit dem geleichen Tool-Code machen kann

der Parameter bleibt in der Tat „offen“ — aber allzuviel kann man damit nicht anstellen, denn

  • der Parameter muß numerisch (und total geordnet) sein
  • er muß auf double konvertierbar sein und aus double erstellbar
  • und die tatsächlich vorzunehmenden Messungen sind auch fix
  • weil es gar keine Zeit wäre, sondern ein Prozentsatz oder Faktor
  • und wenn es eine Zeit wäre, weil es sich auf einer komplett anderen Skala bewegt
  • oder weil es einige dramatische Ausreißer gibt und kein Pattern erkennbar ist

Denn es gibt stets einige wenige ganz dramatische Ausreißer, die sonst die Skala so verschieben würden, daß man die eigentlichen Meßwerte nicht mehr sieht; ich müßte also eigens wieder Anpassungs-Code schreiben, und mir eine Heuristik einfallen lassen, um die Skala zu kappen — unnötiger Aufwand für eine nebenbei mit aufgenommene Größe

...denn der Umstand, daß der Overhead / Job weitgehend konstant ist, zeigt, daß wir hier einen separaten Setup-Effekt haben, vermutlich nämlich vor allem die Job-Planungs-Zeit. Da wir demgegenüber in der aktiven Phase sehr gut linear sind, kann man diesen zusätzlichen Overhead zunächst mal als Artefakt der Meßanordnung beiseite lassen (und später dann mal eigens untersuchen)

Das ist hier definitiv der Fall!

Ich habe weitere »Slots« im Diagramm „zu vergeben“ und ich habe noch eine Menge Statistik-Daten sowie die Laufzeit instgesamt, die ich bisher gar nicht auswerte

dort ist das Vorgeben eines Stress-Faktors für jeden Einzel-Lauf die zentrale Steuergröße, hier dagegen gehe ich eigentlich von einer Überlastung aus, und damit sollte das vorgefertigte Schedule nur zu dicht sein, ansonsten ist es egal

...kann mir im Moment aber keine Fall vorstellen, wo man tatsächlich ein Schedule vorgeben und abarbeiten lassen möchte (weil dann die Messung nur einen kritischen Pfad durch das Schedule ermittelt, nicht den Scheduler selbst ausleuchtet ... anderenfalls wäre man wieder bei der breaking-Point-Suche)

man könnte aber hier auch noch selber eingreifen

⟹ für das Tool »ParameterRange« wird per default kein (adapted) Schedule erstellt

...was nun relevant wird für das Tool »ParameterRange« — der feste default-Wert im Chain-Load-Schedule-Ctx ist 1ms, und das erscheint für diesen Zweck als zu hoch, weil damit die Gefahr besteht, den Worker-Pool gar nicht in die Voll-Auslastung zu bekommen

...man würde also bereits ganz von Grund auf einen Hüllen-Container bzw. eine virtuelle Schnittstelle brauchen — und ich wollte hier exakt den entgegengesetzten Weg gehen, mit lauter konkreten, weitgehend lokal definierten Typen und daher auch Template-Parametern für die Breite des Graphen

  • kann nicht einfach ausführbaren Code in einen Klassenrumpf schreiben (wie in Scala oder Python)
  • kann keine Parameter eines Basis-Typs dynamisch / late-binding modifizieren
  • kann keine virtuelle Funktion über generische Parameter arbeiten lassen
  • Typen aus Template-Basisklassen werden erst durch explizite Typedefs sichtbar ⟹ schwer lesbar
  • Template-Parameter einer Basis / Framework-Klasse müssen ganz zu Beginn schon feststehen, und können sich nicht aus konkreten Spezialisierungen ergeben
  • es sei denn... man würde ein sehr komplexes Framework mit Wrapper-Typen bauen, und darüber einen »Deckel« setzen, der dann alles zusammenlinkt; ein solcher Ansatz wäre vielleicht machbar (sicher Wochen an Arbeit) — dann aber wohl so komplex, daß selbst C++ -  Experten nicht mehr auf Anhieb sehen, was gespielt wird.

direkt im Test: using StressRig = StressTestRig<16>;

Fazit: knapp am Abgrund vorbei

Tests sehen einfach aus...

und sind leicht anpassbar

...und was dieser Kontroll-Parameter tatsächlich ist, wird allein durch das Setup festgelegt — denn dies erzeugt sowohl jedesmal eine neue Topologie

einmal prüfen ob man das GroomingToken hat, und dann ein Enqueue

aber nur, wenn man Dependencies nicht »unlimitiert« scheduled. Aber selbst dann werden die Notifications mit »now« gescheduled, und bei einer (hier angestrebten) Überlast-Situation sind alle derzeit zurückgestauten Jobs zeitlich vorher einsortiert

64*0.5 / 8 cores

und eigentlich hatte ich den Pool auf 4 Worker limitiert

da es nur noch diese eine letzte Node gibt, macht es keinen Sinn, die Expansion order Join-Regeln noch anzuwenden. Aber die Pruning-Regel kann sehr wohl für die Vorgänger angewendet werden, welch dann infolgedessen eben u.U nicht mit der letzten Node verbunden werden. Damit kann es sogar passieren, daß die letzte Node unverbunden bleibt — und in diesem Fall muß dann sogar ihr Seed eigens gesetzt werden

möglicherweise könne man diese Spezialbehandlung komplett in die normale Verarbeitungsschleife integrieren... das war mir aber zu schwer (ich bräuchte dafür mehr Formalisierung)

...da zurückkehrende Worker die Vorfahrt haben, und die typsiche Rüstzeit ~100µs beträgt, besteht noch hinreichend Wahrscheinlichkeit für Contention

Stand: nun bereit für eigentliche Messungen

...denn er ist stark von der Hardware abhängig und i.d.R wenig variierbar, und oft gibt es noch weitere, interne Beschränkungen (wie z.B. Hyperthreading, das eben doch nicht komplett transparent ist und mit Cache und Pipelining im Prozessor wechselwirkt)

andere Stack-Frames oder Thrads inspizieren ⟶ Debugger terminiert

Verdacht: Daten-Korruption durch ungesicherten Race

theoretisch sollte es auch so sein: sie befindet sich in einem anonymen Namespace...

Problem erkannt: re-Entrance

...das ist eine inhärente Limitierung, die ich jedoch als Grenzfall und damit nicht relevant betrachte; zum einen sind dann meistens die Laufzeiten so kurz, daß die statistischen Triggerschwellen gar nicht greifen können, und außerdem, wenn das generierte Schedule gar keine zeitliche Komponente enthält — also alle Jobs gleichzeitig starten sollen — dann läßt sich auch nichts verdichten und der binäre Suchalgorithmus konvergiert auf das erste gefundene Rand-Intervall

das ist eine im Rückblick ungeschickte Vereinfachung, die ich ganz zu Beginn für den SchedulerCtx gemacht habe; eigentlich müßte man die Implementierung jetzt wergwerfen und nochmal systematisch von Null auf neu aufbauen. Das versuche ich zu vermeiden, da es sich letztlich nur um Test / Expermental-Code handelt, und zudem gegenwärtig nur eine erste Grobeinschätzung durch die Streß-Tests gefordert ist (ich gehe davon aus, daß das ganze Test-Setup später für detailiertere Messungen ohnehin noch einmal neu implementiert werden muß, wenn wir denn auch dafür Zeit haben)

Die Mantisse des Double-Datentyps ist 52Bit lang (≈4.5e15). Das reicht nicht, um einen 64bit-Integer fehlerfrei mit voller Genauigkeit darzustellen

das heißt, es gibt einen Punkt ab dem die Standardabweichung deutlich ansteigt, und viele Einzel-Läufe deutlich länger brauchen

Er liegt tatsächlich etwas niedriger, nämlich 180µs ±5µs. Die Zahlen sind erstaunlich stabil und relativ genau, und passen auch mit dem statistischen Verhalten zusammen; insofer betrachte ich die 5.8ms Gesamtlaufzeit als einen empirischen Fakt. Und es paßt in's Bild, daß dieser Wert etwas niedriger liegt für eine lange Kette (Cache-Effekte)

Im Schnitt sind 3 Worker losgelaufen, wenn der erste Worker nach 500µs + X wieder zurückkehrt. Da ein zurückkehrender Worker vorrangig behandelt wird, kommen die restlichen Worker kaum zum Zug und regeln sich herunter

Einzelbeobachtung ⟹ keinerlei auffälliges Verhalten

sie werden per Instrumentation erfaßt, und nicht aus anderen Beobachtungen errechnet

  • die Anlauf-Phase ist abgesclossen, wenn der erste Worker einen weiteren Job übernimmt und zudem alle 4 Worker da sind
  • unter der Annahme, daß alle Worker während der Anlauf-Phase aktiv werden, zählt diese Anlauf-Phase also grundsätzlich immer einen abgeschlossenen Job
  • die Shutdown-Phase beginnt, wenn der erste Worker idle wird. Damit bleiben noch N-1 Worker übrig, die ihre Arbeit beenden müssen. Die Shutdown-Phase zählt daher 3 abgeschlossene Jobs

4,649/46  ( 46= 50 -4)

das entspricht genau dem empirisch beobachteten Scheduling-Overhead

...rein zufällig habe ich hier grade 46 Jobs in der Aktiven Phase, aber das Modell deckt Parameter im Bereich 10...100 Jobs ab

bessere Deutung: es sind ca 5ms — das enstspricht dem WORK_HORIZON

sehr schöner steady state ohne offensichtliche Inkonsistenzen

man kann ihn also deuten als generische Scheduler-Latenz

Begründung: mir kommt es auf das Modell an, und die Regressionslinie war durch die Outlier ersichtlich nach oben verschoben; Model war 0.25·p + 13

...hat wohl nichts mit der Contention zu tun, sondern damit, daß eine längere Pause war nach dem Ende der Planung. Alle Worker, die irgendwann auftauchen, wurden daraufhin weiter in die Zukunft geschoben. Dieses Muster habe ich bewußt so gewählt, um eine bessere strategische Verteilung der Kapazität zu erzielen

Ein Worker merkt sich, wenn er in Contention gelaufen ist; jedesmal wenn er wieder erfolgreich zum Zuge kommt, wird sein contention-Level nur reduziert (aber die Erinnerung nicht gelöscht). Das habe ich so eingebaut, um die Drosselung mit einer gewissen Trägheit zu erhalten; denn selbst unter starker Contention kommt ein Worker ab und zu doch mal im richtigen Moment, und würde dann sofort wieder den Contention-Druck erhöhen. Mein Ziel war, im Falle von andauernder Contention erst mal sehr effektiv »Druck rauszunehmen«

...der nicht gleichverteilt ist, sondern die Thread-ID verwendet. Es kann also passieren, daß die ganze WorkForce auf Extrem getrimmt ist

im Realbetrieb erwarte ich durchaus Job-Zeiten im Millisekunden-Bereich —ein zufälliges Contention-Event, das (wie im Beispiel hier) nicht notwendig problematisch ist, würde dennoch für eine Dauer von etlichen Frames die „Bremsen anziehen“

200 * 8ms = 1600ms / 8 Worker = 200ms, aber der letzte Job wird auf 200*0.2 = 40ms geplant

und zwar ganz regulär

und zwar if layer1.isOutOfTime(now)

der betreffende Fall in der Logik fällt leer durch und wird dann zwei Ebenen darüber als Contention-Kick gedeutet (was in 99% der Fälle auch korrekt ist)

genauer: eine externe Reaktion ist sicher nicht etws, was Layer-2 einfach machen kann — hier entstehe eine zunehmend komplexe Dependency-Injection, insofern der Layer-2 auch Entscheidungen für den Service als Ganzes trifft

Contention ist ein ernstes Problem

abgesehen davon: das Verhalten ist linear mit wenig Streuung

Sockel-Overhead: 5ms + Scaffolding-Effekte

...und zwar vorallem, weil die Kapazität zufällig verteilt ist (und am Ende nicht glatt aufgeht)

bei kurzen Job-Zeiten dagegen ist der Overhead bedenklich

und damit nicht über eine Abweichung der Job-Zeiten in den Formfaktor eingehen

naja... die ist unterirdisch

zur Erinnerung: in einer Serie machen wir ja eine Art Konvergenz auf einen effektiven Form-Faktor hin. Mit den Ergebnissen eines Laufes wird für den nächsten Lauf nachjustiert; der von außen vorgegebene (nominelle) Streß-Faktor bleibt, aber die tatsächliche Dichte wird so optimiert, daß die dann effektiv diesem Faktor entspricht. Im Zuge dieser Anpassung wird anscheinend das Schedule jeweils etwas verdichtet, und die erreichte Concurency fällt (von etwas über 2 auf 1.6 zuletzt)

zumindest nach den inzwischen vorliegenden Beobachtungen aus dem Param-Range-Setup

damit geht jetzt die Auslastung einigermaßen hoch

es ist und bleibt ein Kompromiß....

Ich versuche hier, eine sehr spezifische Meßmethode halbwegs generisch nutzbar zu machen, stecke dabei aber bereits gefährlich viel Vorannahmen über den Scheduler in den Meßprozeß

Kapazität wird normalerweise zufällig in eine aktive Zone verteilt; nur wenn wir hinter das Schedule fallen, werden alle Worker eingesetzt

...wenn aufgrund einer vorhergehend beobachteten, geringen Parallelität das Schedule gespreizt ist, dann ist die Abarbeitung eines Layers vorzeitig fertig, und die Worker werden hinter den Startpunkt des nächsten Levels verteilt. Damit geht auch dort wieder die Kapazität nur langsam hoch, und nach wenigen Runden hat sich eine kleine Zahl an aktiven Workern herauskristalisiert. Die weitere Nachregulierung sorgt dann genau dafür, daß das Schedule so großzügig ist, daß diese wenigen Worker es schaffen.

dieses Setup beobachtet nicht den »breaking Point«

— sondern das Erreichen eines Lastziels

Ursprünglich wurde die »breaking Point«-Methode an einem komplexen Lastmuster entwickelt, welches bei Überlastung sehr deutlich degeneriert, insofern dann zentrale Vorraussetzungs-Knoten erst spät erreicht werden, und damit das gesamte Schedule sich drastisch verspätet. Sowas ist hier nicht gegeben. Vielmehr verlängert sich die Laufzeit einfach elastisch und proportional, wenn ein einmal vorgegebenes Schedule ohne Puffer genau erfüllt wird. Da sich die Suche von der Seite geringer Last nähert, wird dabei nie genügend Druck aufgebaut, um die Concurrency hochzutreiben.

locker ⟹ geringe Concurrency ⟹ dieser Punkt wird als strenger klassifiziert und das Schedule wird noch lockerer ⟹ wenn nun wir an den vorherigen Testpunkt zurückkehren würden, dann wäre dort möglicherweise das Testziel (= Schedule gebrochen) gar nicht mehr erfüllt

extent-family.hpp:356

...damals allerdings aus einem ganz anderen Kontext, der inzwischen durch einen Umbau im Scheduler behoben ist/sein sollte.

»investigateWorkProcessing«

∅conc:7.29344

∅conc:6.53128

∅conc:6.24495

∅conc:6.082

PRECONDITION: extent-family.hpp:356: thread_9: access: (isValidPos (idx))

0000000609: PRECONDITION: extent-family.hpp:356: thread_9: access: (isValidPos (idx))

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::access(unsigned long) const+0xdd

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::IdxLink::yield() const+0x26

vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::IdxLink::validatePos(vault::mem::ExtentFamily<vault::gear::Activity, 500ul>::Extent*)+0x52

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor::iterNext()+0x23

lib::IterableDecorator<vault::gear::blockFlow::Epoch<vault::mem::ExtentFamily<vault::gear::Activity, 500ul> >, vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::StorageAdaptor>::operator++()+0x20

vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::claimSlot()+0x228

vault::gear::Activity& vault::gear::BlockFlow<vault::gear::blockFlow::RenderConfig>::AllocatorHandle::create<vault::gear::Activity::Verb>(vault::gear::Activity::Verb&&)+0x2e

vault::gear::activity::Term::appendNotificationTo(vault::gear::activity::Term&, bool)+0x2f

vault::gear::ScheduleSpec::linkToPredecessor(vault::gear::ScheduleSpec&, bool)+0x68

vault::gear::test::TestChainLoad<8ul>::ScheduleCtx::continuation(unsigned long, unsigned long, unsigned long, bool)+0x2ef

vault::gear::test::TestChainLoad<8ul>::ScheduleCtx::performRun()::{lambda(unsigned long, unsigned long, unsigned long, bool)#3}::operator()(unsigned long, unsigned long, unsigned long, bool) const+0x40

std::_Function_handler<void (unsigned long, unsigned long, unsigned long, bool), vault::gear::test::TestChainLoad<8ul>::ScheduleCtx::performRun()::{lambda(unsigned long, unsigned long, unsigned long, bool)#3}>::_M_invoke(std::_Any_data const&, unsigned long&&, std::_Any_data const&, std::_Any_data const&, bool&&)+0x86

std::function<void (unsigned long, unsigned long, unsigned long, bool)>::operator()(unsigned long, unsigned long, unsigned long, bool) const+0x90

vault::gear::test::RandomChainPlanFunctor<8ul>::invokeJobOperation(lumiera_jobParameter_struct const&)+0x2ca

vault::gear::Activity::invokeFunktor(lib::time::Time)+0x740

vault::gear::activity::Proc vault::gear::Activity::activate<vault::gear::Scheduler::ExecutionCtx>(lib::time::Time, vault::gear::Scheduler::ExecutionCtx&)+0x70

vault::gear::activity::Proc vault::gear::ActivityLang::activateChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x4e

vault::gear::activity::Proc vault::gear::ActivityLang::dispatchChain<vault::gear::Scheduler::ExecutionCtx>(vault::gear::Activity*, vault::gear::Scheduler::ExecutionCtx&)+0x68

vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}::operator()(vault::gear::ActivationEvent) const+0x4d

vault::gear::SchedulerCommutator::dispatchCapacity<vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2}>(vault::gear::SchedulerInvocation&, vault::gear::LoadController&, vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2})::{lambda()#2}::operator()() const+0x8f

vault::gear::SchedulerCommutator::WorkerInstruction vault::gear::SchedulerCommutator::WorkerInstruction::performStep<vault::gear::SchedulerCommutator::dispatchCapacity<vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2}>(vault::gear::SchedulerInvocation&, vault::gear::LoadController&, vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2})::{lambda()#2}>(vault::gear::LoadController)+0x1f

vault::gear::activity::Proc vault::gear::SchedulerCommutator::dispatchCapacity<vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2}>(vault::gear::SchedulerInvocation&, vault::gear::LoadController&, vault::gear::Scheduler::doWork()::{lambda(vault::gear::ActivationEvent)#1}, vault::gear::Scheduler::doWork()::{lambda()#2})+0xc5

vault::gear::Scheduler::doWork()+0x3d

vault::gear::Scheduler::Setup::doWork()+0x1c

vault::gear::work::Worker<vault::gear::Scheduler::Setup>::pullWork()+0x26

linkToPredecessor()

tritt reproduzierbar auf ab Load=4ms

alloziert wurde auf dem predecessor-Term

...das ist ohnehin etwas kreativ

dieser hängt an der Deadline

Der Allokator in sich ist robust; die Deadline beschreibt nur einen Nutzungs-Kontrakt; sie ist zwar im Gate des Blocks gespeichert, aber für den Allokator nur maßgeblich zur Suche des passenden Blocks. Das weitere Nutzungs-Muster muß auf einer höheren Ebene gewährleistet sein.

...ich muß auch daran denken, daß nicht jede Maschine 8 Kerne hat. Insofern erscheint es sinnvoller, die Concurrency auf 4 zu beschränken, und dann zu sehen, wo man landet

der Graph-3 hat denn doch immer wieder mehrstufige Dependency-Tries — und ich kann daher nicht entscheiden, ob die beobachtete ∅concurrency = 5.4 auf dependency-wait zurückzuführen ist, oder tatsächlich durch Abregeln der Kapazität durch den Scheduler zustande kam. Möglicherweise auch beides im Wechselspiel.

das ist ziemlich gut

...da die tatsächliche Last ehr bei 5.5ms liegt, können diese Durchschnittswerte nur erklärt werden, indem zeitweilig die zusätzlichen, freien Worker mithelfen; sie können aber nicht permanent voll ausgelastet werden, da der Graph eigentlich nicht so viel Last hergibt. Trotzdem ergibt sich noch ein auf 60% verdichtetes Schedule

Das deutet alles darauf hin, daß das Scheduling weitgehend effizient  ist

Abschließender Test: Laufzeit > 1 sec

Und zwar unabhängig davon, ob die Kalibrierung mit kurzen oder langen Zeiten und single- oder multithreaded erfolgte. Die Abweichtung tritt nur im realen Last-Kontext auf, und ist (visuell. den Diagrammen nach zu urteilen) korreliert mit dem Grad an contention und irregularität im Ablauf. Tendentiell nimmt sie für längere Testläufe ab, konvergiert aber — auch für ganz große Lasten und sehr lange Läufe — typischerweise zu einem Offset von ~ +1ms

Und das ist schon aus rein-logischen Gründen so zu erwarten. Bewußt habe ich beim Aufstellen der Heuristig für das Test-Schedule auf jedwede optimale Anordnung der Rechenwege verzichtet (kein Box-stacking problem lösen!). Hinzu kommen die tatsächlichen Beschränkungen des Worker-Pools. Daraus ergibt sich eine charakteristische Abweichung zwischen einem theoretisch berechneten concurrency-speed-up (wie er in's Schedule eingerechnet ist) und der empirisch beobachteten durchschnittlichen concurrency. Das wird als Form-Faktor gedeutet.

zwischen Load-Size und Laufzeit zum kompletten Abarbeiten der erzeugten Lastspitze

Gradient sehr nah am zu erwartenden Wert

wenn man die empirisch beobachtete, effektive Concurrency und reale durchschnittliche Job-Zeit ansetzt

das bedeutet: der tatsächlich beobachtete Sockel hängt von der Länge der Job-Last und der Concurrency ab: Grundsätzlich muß man einmal die ganze Worker-Pool-size zu Beginn und am Ende aufschlagen — mit reduzierter Concurrency. Das ergibt sich bereits aus einer rein logischen Überlegung: »Voll-Last« kann erst konstituiert werden, wenn der erste Worker sich den zweiten Job holt. Analog beginnt der spin-down, wenn der erste worker idle fällt.

Gefahr allgemein: reagiert sehr empfindlich auf Contention

Einschränkung: ausgepräfte Tendenz zum down-Scaling

ab einer gewissen Lastgröße: kein Overhead mehr feststellbar

über alles andere können wir keine Annahmen machen — frühere Nodes könnten schon „durch“ sein und dann würde der Wake-up für-immer warten

habe ich nicht gemacht, weil ich dachte, es sei eine gute Idee, wenn die Contuation »frühest-möglich« zurückkommt. An das Dependency-Problem habe ich nicht gedach.

sofern SCHED_NOTIFY wären wir theoretisch sicher

zwar kann (nach diesem ersten Fix) die Continuation erst starten, nachdem das von Anfang an geplante Schedule abgearbeitet war; jedoch laufen zu dem Zeitpunkt u.U noch eine Anzahl Nodes (Paralleitäts-Grad FAN_OUT). Und wenn diese dann fertig sind, könnten die weitere NOTIFY-Dependencies „abwerfen“ ⟹ SEGFAULT

  • es gab immer wieder Incidents, in denen Theads auffällig langsam waren (Contention? OS-Scheduler überlastet?)
  • unter concurrency läuft die (eigentlich kalibrierte) CPU-Load deutlich langsamer, oft um Faktor-2

...von dem nicht klar ist, ob es relevant wird. Wenn dieses Problem relevant sein sollte, dann ist es definitiv eine Folge des gewählten Designs — allerdings bräuchten wir dann eine Laststeuerung, die nicht ihrerseits wieder Contention erzeugt; das heißt: ein sehr effizientes und schnelles Messaging

es gab einige besorniserregende Einzel-Events

  • einmal eine Assertion-Failure bei Zugriff auf einen invaliden Extent
  • einmal einen Runaway mit massiver Allokation (mußte Prozeß vom Linux-VT killen, 56 GiB virtMem)

Das ist meine Einschätzung nach Bauchgefühl. Anfangs hatte ich den Eindruck einer Instabilität in diesem Bereich. Dann aber wurde klar, daß wir diverse Code-Pfade haben, in denen concurrent auf den Allokator zugegriffen wird, und weitere Code-Pfade, in denen auf bereits verworfene Blöcke referenziert wird. Das war den Ansporn, das Design des Scheduler-Eingangs zu reorganisieren. Damit geht die Verantwortung zum Einhalten der Bedingungen auf die Job-Planung über (und diese läuft nun definitiv single-threaded)

das beobachte ich mit dem DUMP-Logging immer wieder. Und zwar ohne eine ausgeprägte Contention-Spitze. Nach dem Absetzen des letzten regulären Node-Jobs sieht man das Log vom Continuation-Aufruf, dann lang nichts, dann die Sequenz der dependencies.

„weitreichend“ ⟶ besonders beim wake-up-Job, was zu dem Verdacht führt, daß hier eine Suche im Allokator das Problem ist; beim Hinzufügen wird das bestehende Allocator-Handle verwendet, welches aber in der Regel über mehrere Folge-Blöcke hinweg überläuft

gemeint ist: längerfristig und im Betrieb mit realer Last

...im Direkt-Vergleich mit jeweils wenigen Meß-Läufen liegt sie unter der Beobachtungsgenauigkeit von Zeitmessungen (welche aber durchaus 5-10% sein kann)

⟹ bedeutet für jedes Ereignis auch eine Kategorisierung zu erfassen, so daß dann in der Auswertung später proportionale Anteile beobachtbar werden

...das könnte tatsächlich schwierig herauszufinden sein, weil wir definitiv nicht jede Handhabung des Grooming-Tokens irgendwo als Event loggen wollen; zudem besteht die Schwierigkeit, daß wir das Grooming-Token nach Aufrufen der work-Funktion oft wieder droppen

works as designed

  • freut mich — das Monster ist zahm
  • Verhalten erscheint oft auf den ersten Blick sonderbar
  • man muß wirklich in diesen Zeit-Dimensionen denken
  • ob das wirklich brauchbar ist — muß sich erst noch zeigen

ist auch explizit so gewünscht

wenn er zur Berechnung „weg“ ist, und währenddessen taucht ein anderer Worker auf, dann schnappt sich letzterer das tend-next, und der aktuelle Thread wird erst mal weggeschickt

Das wurde explizit so eingerichtet, um eine bessere Parallelisierung zu erziehlen: NOTIFY wird nur »abgeworfen«, ohne an dieser Stelle das Grooming-Token zu erlangen; erst wenn der Worker zurückkommt, findet er u.U die NOTIFY-continuations mit Priorität in der Queue (und zwar genau dann, wenn ihr Startzeitpunkt bereits in der Vergangenheit liegt)

gemessen wurde das Abarbeiten einer homogenen Lastspitze, welche schlagartig eingebracht wurde, und die ohne Berücksichtigung von Abhängkeiten verarbeitet werden kann

gilt nur bei sauberem Lauf ohne Contention

und ohne Aufwand für Job-Planung

tatsächlich ... wenn die Datenfelder base values sind

allerdings haben wir jetzt praktisch immer einen direkten Eingang

denn im regulären Betrieb sollen Jobs immer aus einem Planungs-Job heraus eingestellt werden

...denn der Contender hat in dem Moment ohnehin nix anderes zu tun und ist auch von der Kapazität her definitiv für das Rendering belegt. Zudem belästigen wir durch aktives Polling niemanden sonst (abgesehen von dem produzierten CO₂); und der Fall sollte selten sein und idealerweise auch nur kurz dauern

Eine Wirkung können wir nämlich nicht messen ⟹ es läuft darauf hinaus, ob dieses Vorgehen verhältnismäßig ist, und das ist identisch mit der Frage, ob Contentions wirklich so extrem selten sind, wie vom Konzept her vermutet. Der wichtigste Ansatzpunkt wäre daher, nach zeitlichen Koinzidenzen zu suchen, was allerdings ohne weitere Instrumentierung auch nicht möglich sein dürfte

  • die eigentlichen Work-Jobs praktisch nix zu tun haben
  • mehr als 3 Worker um das Grooming-Token konkurrieren (bei 3 Workern ist ∅ keine Verzögerung verststellbar, auch wenn sie ziemlich viel herumeiern)
  • nacheinander mehrere Jobs geprüft werden, die dann jeweils nicht ausführbar sind

Der zuletzt genannte Punkt ist inhärent problematisch es würde sich nicht lohnen, das Grooming-Token zu droppen, da der bloße Check weniger als 1µs kostet, der Test auf das Grooming-Token danach aber wieder 10-20µs. Unter Contention allerdings kann sich eine solche Prüf-Kette auf 100-200µs ausdehnen, und während der Zeit passiert sonst praktisch nix.

unter »Zugriffs-Phase« verstehe ich die Zeit, während der die Queues modifiziert werden müssen, was nur exklusiv von einem Thread gemacht werden kann (erst danach könnte dieser mit eigentlicher Arbeit „weggehen“). Das Problem ist nun: wenn wir im Rückstau sind, dann werden alle Threads an die Head-Time geschickt. Dort drehen sie dann am Rad und machen dadurch alles viel langsamer

...wir reden hier über Abläufe in der Größenordnung der »Grund-Unschärfe« (100µs); und das Problem tritt nur auf, wenn die eigentliche Aktion in der gleichen (kurzen) Zeit erledigt sein könnte. Wenn dagegen 500µs gearbeitet werden müßte, sollte sich das Verhalten (theoretisch) von selbst entspannen

...und zwar durch die Anpassungen am Notify und die Klarstellungen / Verschärfungen für das Grooming-Token; es zeigt sich, daß unter Contention die Worker einen active-Spin machen, der das System erheblich belasten kann

...da wir nur einen Status-Code haben, aber nicht sagen können was und wie genau weiter verfahren werden soll. Dadurch ist zwar die Kopplung oberflächlich lose und explizit, tatsächlich aber führt es zu sehr komplexen Kollaborationen

...wenn diese naiv reingelassen werden und die gesamte Work-Kapazität ausschöpfen, führt der nächste »Tick«-Job zur Scheduler-Emergency.

des Weiteren: wir schützen den Planer durch externe Maßnahmen

Forderung: ein realer Planer muß das erkennen und abbrechen

wenn man einen Verarbeitungsvorkang darstellt als Jobs mit Dependency-Verkettung, dann kann diese Verarbeitung ohne Weiteres irgendwo stecken bleiben, ohne daß ein Fehler ersichtlich ist; der einzige Signalisierungs-Mechanismus sind compulsory-Jobs (und das ist ein relativ brachialer Mechanismus)

Es sind Render-jobs denkbar, die mehrere Stunden brauchen!

Da es keine übergeordnete Koordinierungs-Instanz gibt, muß in diesem Fall von der Job-Planung aus für frei bleibende Restkapazität für Admin-Aufgaben gesorgt werden — denn wenn alle verfügbaren Worker mit solchen langwierigen Berechnungen belegt sind, findet kein »Tick« mehr statt (und der nächste Tick hat seine Deadline überfahren und löst eine Scheduler-Emergency aus). Ohne eine übergeordnete Kapazitäts- und Engine-Steuerung ist diese Situation nicht adäquat zu handhaben: entweder wir bekommen permanent und erwartbar eine Fehler-Situation, oder wir können die Parallelität nicht ausschöpfen, weil wir eine ganze Core freihalten müssen, oder wir müssen überprovisionieren.

Tracking-Ticket #1228 Implement Vertical Slice: play a clip

Scheduler-Entwicklung: #1280 build Scheduler implementation

Render-Processing: #235 Render/Playback process high-level design

Engine-Steuerung: #1284 Render Engine operational control

...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)

  • BufferProvider: Zugang zu Arbeits-Speicherblöcken
  • OutputManagement: verwalten von Datenströmen

Achtung: nicht idempotent

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

ermöglicht, einen CalcStream inkompatibel zu verändern und die Reste der damit obsoleten Manifestation fliegend zu verwerfen

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

...wenn diese die gesamte Work-Kapazität ausschöpfen, führt der nächste »Tick«-Job zur Scheduler-Emergency.

...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

Gruß vom Vollmond in Pullach (noch nicht ganz voll, morgen ist Mondfinsternis), der geht das Tal entlang mit

Priorität bestimmt die Wahrscheinlichkeit,

Kapazität zufällig zu erlangen

Grundsätzlich darf ein Render-Prozeß nur bei vorhandener Kapazität zugelassen werden. Aber die Anwendung dieses Prinzips hat Abstufungen, die sich aus der Art des Prozesses bestimmen.

  • ein whatever it takes - Render bekommt die gesamte Kapazität. Punkt. (alles andere würde ihm schaden)
  • ein realtime-Playback muß im Stande sein, seine Termine zu halten; unter dieser Maßgabe muß er Vorfahrt bekommen, kann aber durchaus noch Raum freilassen, wenn er nicht außerdem auch noch die Leistungsfähigkeit des Systems komplett ausschöpft
  • demgegenüber kann man Background-Prozesse dem Wettbewerb überlassen, und dabei sogar noch verschiedene Level definieren. Es sind nur zwei Bedingungen einzuhalten:
    • Activities müssen feingranular sein, damit sie wirklich nur die Reste verbrauchen
    • es muß dafür gesorgt werden, daß der Prozeß tatsächlich fortschreitet

die aktuelle Situation bestimmt darüber, wie Kapazität zugeführt wird; aber der Querschnitt bestimmt, wie gute Chancen ein konkreter Task hat, davon etwas abzubekommen

Ein frei stehender Task bekommt durch den tend-Next-Mechanismus auch schon sehr viel früher die nächste frei werdende Kapazität zugeordnet; solange aber in der einfachen zeitlichen Ordnung noch etwas vor ihm steht (selbst wenn überfällig), dann zieht dieses die Priorität auf sich

sobald er aber frei steht, bestimmt seine Länge die Priorität

Und zwar durch das Ende (die Deadline), nach deren Überstreichen der Task effektiv unwirksam ist und im Vorrübergehen entnommen und verworfen wird. Wenn nun verschiedene Tasks jeweils in der Länge beschränkt sind, dann fällt ihnen diejenige Kapazität zu, die zufällig in ihrem Wirkradius „landet“

Da sind zunächst die Gates von Relevanz. Ein noch geschlossenes Gate kann einen Task nach hinten schieben und damit andere Tasks aufdecken. Ein getriggertes und endgültig geschlossenes Gate nimmt den Task aus der Konkurrenz komplett heraus. Und außerdem werden Tasks auch noch über eine Prozeß/Kontext-ID gekennzeichnet, wodurch eine Revision und Aktualisierung eines gesamten Planungsvorgangs möglich wird.

...das bedeutet, wenn wir einmal durch das Gate gegangen sind, ist es endgültig geschlossen. Deshalb können gewissermaßen mehrere »Instanzen« eingeplant werden, denn das Wegräumen von Müll ist in einer Priority-Queue relativ effizient, O(logₙ), und läuft bei uns im einstelligen µs-Bereich pro Einzelschritt

Ein Task der nur Restkapazität bekommen soll, darf niemals am Anfang des Fensters stehen, und auch möglichst nicht ganz am Ende. Je kürzer und je mehr in der Mitte, desto geringer seine Chancen. Ein Task der in jedem Fall Kapazität bekommen soll, muß nur hinreichend weit nach hinten reichen (aber unter der Einschränkung, unseren Epochen-basierten BlockFlow nicht zu überlasten). Oder er muß hinreichend oft wiederholt werden. Es wird also ein generelles Segment-Schema etabliert, und in diesem gibt es vorgefertigte »Slots«. Gemäß übergeordnete Kapazitätsplanung werden diese Slots in Anspruch genommen. Wenn wir beispielsweise eine Restkapazität 10-fach überprovisionieren, dann bedeutet das, jeden einzelnen Task (ggfs. mit Zeitabstand) 10+x-mal einzuplanen, wobei x einen empirisch bestimmten safety-margin darstellt, um die tatsächlichen Fluktuationen der Kapazität abzufedern...

dadurch übersetzen wir multidimensionale Zusammenhänge

von der übergeordneten Planungsebene

in ein low-Level-Ausführungsschema

Für die Implementierung des Schedulers sind Deadlines technisch irrelevant — deshalb habe ich aktuell auch die ganze Implementierung gecodet, und noch keine Deadline-Behandlung eingebaut. Aber sowohl logisch, als auch im Hinblick auf die Allokationen muß jede Activity eine Deadline haben. Eine Activity ohne Deadline wäre ultimativ verbindlich: irgendwann nach ihrem Startzeitpunkt muß sie aktiviert werden, und jede spätere Activity ist durch sie verdeckt

Problem: wenn Rückstau im Scheduler entsteht,

wird jede Deadline irgendwann überfahren

...es gibt zwar einen Block-Mechanismus (das Epoch-Gate), doch damit läßt man den Block-Pool anwachsen und die lineare Suche durch die Blöcke macht sich bald bemerkbar

...weil dann der Überlauf-Mechanismus zu weit in die Zukunft ausgreift, und sich die Steuerung in absehbarer Zeit nicht mehr fängt.

Daher ist es nicht möglich, eben sehr große/lange Tasks vorzuplanen; man erzeugt eine Flut leerer Blöcke dazwischen, und die Suchzeiten degenerieren

Damit scheiden die naheliegenden Reparatur-Ansätze weitgehend aus: es ist nicht möglich, eine „überfahrene“ Activity in die Zukunft zu kopieren, denn wenn die Deadline verstrichen ist, kann sie auch bereits verworfen worden sein, und es ist nicht mehr feststellbar, um was es sich handelte

der Tick selber ist eine verbindliche Aktivität

derzeit eingestellt auf 200ms

(das sind Tick-Zyklen — nicht klar ob das reicht, denn in den Streß-Tests habe ich gesehen, daß das Schedule durchaus leicht mal um mehrere 100ms weg-rutschen kann)

sollte automatisch passieren, wenn die internen Gesundheits-Checks scheitern

...wenn wir noch einen Worker-Pool haben, der mit langlaufenden berechnungen geblockt ist, dann wird jeder reguläre emergency-Shutdown im Destruktor hängen bleiben.Wenn es erst mal so weit gekommen ist, kann man nicht einmal mehr den User richtig benachrichtigen; ein hartes exit()  dagegen würde die bereits geleistete Arbeit einfach verwerfen.

Wie viel dedizierte Logik ist hierfür sinnvoll?

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

Der Scheduler arbeitet nur in einem begrenzten Bereich

Dieser kann durch alltägliche Umstände verletzt werden

Dann ist ein spezielle Notzustand zu aktivieren

Es kann auftreten, daß eine Lastspitze den nächsten Tick über seine Deadline befördert ⟹ dann bleibt das Scheduling stecken. Aber Layer-2 hat bisher keine Möglichkeit, die Emergency auszulösen (denn das würde gegen die Layer-Ordnung verstoßen)

Sowohl der Tick-Service, alsauch das pullWork müssen die Queue aktualisieren und obsolete Einträge wegwerfen; dabei kann aber eine Scheduler-Emergency auftreten (konkret: ein compulsory-task steht am Queue-Head, hat aber bereits seine Deadline überfahren, weshalb man ihn nur noch wegwerfen kann, denn der zugehörige Activity-Chain könnte bereits dealloziert bzw. recycled sein; und im Besonderen könnte dieser compulsory-task der nächste Tick sein).

Aber Scheduler-Emergency gehört ganz eindeutig auf den top-Level, und muß dann sogar nach außen signalisiert werden, weil der Scheduler dieses Problem nicht selbst lösen kann

...der gesamte Entwurf bisher beruht auf Ereignis-Verknüpfung und sieht keine generische Erfassung aller tatsächlich berechneten Schritte vor. Wenn also die Scheduler-Emergency ausgelöst wird, ist ein zufällig verteiltes Muster an Einzel-Jobs abgeschlossen, und der Rest fehlt. Wollte man hier einen generischen Ansatz für das Wiederanlaufen schaffen, so müßte man alle Berechnungsprozesse unter eine einzige Abstraktion subsummieren können. Das halte ich für gefährlich...

Beispiel: ein »final Render« bei dem pro Frame mehr als 30 Sekunden Rechenzeit notwendig sind; dies würde die Auslegung des Schedulers sprengen

Frame-Cache (Service)

Alles, was aus Sicht des Benutzers mit sich selbst identisch ist, bekommt die gleiche Namens-ID. Diese setzt sich zusammen aus....

  • der Domain-Ontology (z.B. FFmpeg)
  • der eindeutigen high-Level-Bezeichnung der Operation (z.B. gaussian blur)
  • (optional) eine Epochen-Versionsnummer (da es im Lauf der Zeit passieren kann, daß die Library-Implementierung modifiziert wird, mit sichtbar geändertem Verhalten)

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

was ich hier angebe, ist eine Grenz-Version; viele der Unterseiten wurden schon länger nicht geändert, und der letzte Capture in Archive.org liegt schon mehrere Jahre zurück; daher wird dann die nächst-neuere Version geliefert, die typischerweise aus 2021 stammt und bereits auf GTK-4 bezogen ist

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

...und zwar, weil GTK portabel ist, und man eigentlich nicht auf die unterliegende Window/Grafik-Technologie zugreifen sollte; mit Wayland gibt es nun sogar unter Linux eine zweite Variante, d.h. man kann nicht mehr sicher sein, daß man überhaupt an ein X-Window oder X-Display gebunden ist.

Interface: dasjenige Widget,

das die tatsächliche Realisierung macht

(typischerweise ein Gtk::Window)

...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

#include <gtkmm.h>

#include <iostream>

class MyApp

  : public Gtk::Application

  {

  protected:

    // install into the default startup handler

    void

    on_startup()  override

      {

        Gtk::Application::on_startup ();

        Glib::signal_timeout ().connect (sigc::ptr_fun (&MyApp::on_timeout), 1000);

      }

   

    static bool

    on_timeout()

      {

        std::cout << "Periodic task executed" << std::endl;

        return true; // Return true to continue calling this function

      }

  };

int main (int argc, char *argv[])

  {

    auto app = MyApp::create ();

    return app->run (argc, argv);

  }

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).

es ging darum, an die unterliegenden X-Windows ranzukommen, um sie dann auf dem Bidschirm zu positionierenl

Window window = GDK_WINDOW_XID (area_window->gobj());

Display* display = GDK_WINDOW_XDISPLAY (area_window->gobj());

dieser Code ist anscheinend nicht deprecated

Das GTK-Projekt führt seit Gtk-3 zunehmen weitere Abstraktionen ein, über die alle relevanten Aufgaben erledigt werden können (selbst low-Level-Aufgaben wie das Mapping von Device-Koordinaten). Siehe Gtk::Native, und von dort Gdk::Surface

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

man konnte sich nicht
zwischen zwei Auslegungen

entscheiden

Das bedeutet: ein Funktionspointer mit passender Signatur kann von einem λ initialisiert werden und kann dann die dahinter stehende Funktion aufrufen. Vermutlich als Kompatibilität zu C-Callbacks gedacht....

das ist problematisch wegen std::function

Denn std::function kann auch ⟘ sein — genauso übrigens wie ein Funktionspointer — und es ist ein durchaus übliches Pattern, auf diese Weise eine zusätzliche Flag-Info zu transportieren

normalerweise harmlos — aber gefährlich wenn man Traits darauf aufbaut

Solange man sich an die implizite Semantik eines nullptr hält, ist diese Konversion harmlos, wenngleich auch nicht sonderlich nützlich. Denn zwar ist ein λ grundsätzlich immer und ohne

Einschränkungen aufrufbar, aber diesen Umstand kann man nur ausnützen, solange das Lambda keine captures hat; denn andernfalls wird es ein wirkliches Objekt mit Storage und Lebenszyklus.

Gefährlich wird es aber, wenn man auf den Umkehrschluß aufbaut, mit der Annahme, eine bool-konvertierbare Funktion sei deaktivierbar. Also wenn man dieser Flag eine zusätzliche Bedeutung zuweist

template<

    class Rep,
    class Period =  std::ratio<1>

class duration;

Beispiel steady_clock

using time_point = chrono::time_point<steady_clock, steady_clock::duration>

Ticket #886

»Simple C++« Dimitrij Mijoski  2021-11

Letztlich werde ich aus der Diskussion zu diesem Aspekt nicht klug. Zumindest aber sind nicht alle möglichen Ergebniszahlen sofort zu Beginn gleich wahrscheinlich; es wird auch immer wieder ein Zusammenhang impliziert mit dem Umfang des internal state, der beim Mersenne-Twister extrem groß ist (624 int). Festzuhalten bleibt, daß ein PRNG eine gweisse predictability haben kann, und das heißt, man sollte sich niemals auf das Auftreten / nicht-auftreten bestimmter Zahlen verlassen. Die Eigenschaften zeigen sich erst im statistischen Durchschnitt. Will man eine echte Zufallszahl, muß man eine echte Zufallszahl nehmen

...das heißt, nur wenn man tatsächlich mit 624 ints seeded, erreicht man alle möglichen Sequenzen. Wobei aber bereits zwei ints (2^64) für die meisten praktischen Probleme mehr als genug sein sollte; problematisch wird das nur bei Simulationen und Lösungssuche durch Monte-Carlo.

...darauf wird nirgends hingeweisen, und es gibt ein Interface, um einen Allokator einzuführen. Kritisiert wird auch generell das Interface für std::seed_seq — die Stichhaltigkeit dieser Kritik kann ich nicht beurteilen (anders als daß es nicht „einfach“ ist)

Es läuft darauf hinaus, den Mersenne-Twister in Form von std::mt19937 als »default« zu verwenden. Und der ist langsam, braucht viel Speicher und ist kompliziert zu seeden. Außerdem sind die gebotenen Distributions-Funktionen allesamt nicht portabel (sondern implementation-defined). Leider werden dann in der Diskussion (Reddit, Stackoverflow) diverse Favoriten genannt, für die es dann stets auch wieder (ohne tiefere Expertise schwer nachprüfbare) Einwände gibt. Die einzige Alternative, gegen die niemand wirklich etwas einzuwenden hatte (außer daß es angeblich modernere / schnellere / coolere Alternativen gäbe) ist der allseits bekannte Jenkins jsf32

the corresponding unsigned type has the same rank

Auf den ersten Blick könnte man meinen, hier sollte die »is-a«-Relation wirksam werden. Allerdings kann die C++-Sprache das nicht leisten. Die funktionale Template-Subsprache ist rein statisch und komplett explizit: würde eine Template-Spezialisierung für einen Subtyp aktiviert, dann würde die Implementierung statisch auf der Basis-Klasse operieren, Value-Objekte würden geSLICEd und virtueller Dispatch würde aus dem Objekt heraus nicht stattfinden (weil es den this-Typ der Basisklasse verwenden muß). Wohingegen ein nach außen geleiteter Aufruf über eine Rück-Referenz dann doch wieder eine eventuell vorhandene VTable verwenden würde. Es gäbe somit keine zuverlässig wirksamen Erweiterungspunkte....

Angenommen, wir haben eine Klasse, die aus std::tuple ableitet. Man würde sich wünschen, diese würde automatisch am Tuple-Protocol partizipieren. Tut sie aber nicht, weil schon die Spezialisierung von std::tuple_size nicht greift. Das ist aber auch gut so, denn sonst wäre man auf die Implementierung von std::tuple festgelegt, und könnte z.B. keine Anpassungen am Storage-Management machen. Wenn man so etwas möchte, dann sollte man die klassische-OO verwenden.

Jede mögliche Template-Instantiierung wird als konkret gegebene Implementierung dem Overload-Resolution-Set hinzugefügt. Eine genau passende konkrete Implementierung wird gegenüber einer nur inferierten / synthetisierten Implementierung vorgezogen, und auch gegenüber einer konkreten Implementierung, für die eine Typ-Konvertierung notwendig ist.

lib::meta::disable_if_self

ABER nur wenn der Template-Parameter für diesen konkreten Aufruf  instantiiert wird; ein Typ-Parameter einer umschließenden Klasse gilt bereits als fest gebunden, also funktioniert mit damit kein »perfect forwarding«. Im Besonderen bei Konstruktoren ist das eine häufig anzutreffende Falle

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)

[](auto&& ...args) -> RetType { return fun (forward<decltype(args)> (args) ...); }

weil bei normaler Typinferenz ein decay stattfindet

„zum Beispiel...“ wenn die im Lamba verwendete Funktion eine Referenz zurückliefert, dann passiert ein decay auf den unterliegenden Objekttyp, d.h. man erzeugt eine schwebende Kopie....

hab mich grad geschnitten und 5 Stunden nach der Ursache gesucht

es genügt daß die Member unter dem Namen direkt zugreifbar sind; sie müssen auch in der gleichen Struct liegen (die aber durchaus eine Basisklasse sein kann). Bit-Felder werden unterstützt, Union-Member jedoch nicht

viel Geplänkel cool-getue; tatsächlich hat er schon einen eigenständigen Gedanken, braucht aber sehr lange, ihn auszuformulieren; und zu den schwierigen praktischen Fragen mit STL-Containern sagt er gar nichts

dann skizziert er alle wesentlichen Standard-Allocator-Patterns als  composable allocators

Begründung: es ist sinnlos, weil das einzige, was man dabei machen kann, ist Fehler machen. Der Standard ist extrem genau und elaboriert für dieses Thema, und wenn man sich an wirklich alle Vorgaben hält, hat man praktisch keinen Spielraum mehr....

...bei Bedarf kann man dort sogar einige optionale Methoden zusätzlich implementieren, z.B. construct (und das wird dann auch verwendet, anstatt der Standard-Implementierung in den Traits)

wenn ein Allocator nur seine eignen Allokationen hanhaben soll ⟹ Gefahr

...und das ist leider der Fall, sobald die Allokationen in irgend einer Form von spezieller Datenstruktur verwaltet werden, die wir auch für die de-Allokation wieder brauchen...

Allokatoren äquivalent ⟺ eine Instanz kann von der anderen Instanz erzeugte Objekte deallozieren

using propagate_on_container_copy_assignment = std::true_type;

using propagate_on_container_move_assignment = std::true_type;

using propagate_on_container_swap = std::true_type;

explicit-specifier(optional) template-name (  parameter-decl-clause ) -> simple-template-id  ;

Automatic Deduction setzt ctor im primären  Template vorraus

⟹ dann findet der Compiler ohne Hilfe keinen Construktor

bzw. er findet nur den Copy-Konstruktur

dazu im OS: clangd installieren

das behebt in der Tat eine sonderbare Asymmetrie und ermöglicht einen wichtigen Fall: wenn man transparent auf einer Kopie von *this arbeiten möchte (was stets der Fall sein dürfte, wenn man einen λ-Dispatch in einen anderen Thread macht)

strong_ordering::greater

strong_ordering::equal

strong_ordering::less

weak_ordering::greater

weak_ordering::equivalent

weak_ordering::less

partial_ordering::greater

partial_ordering::equivalent

partial_ordering::less

partial_ordering::unordered

  • man muß selber eigentlich nur noch einen Spaceship-Operator implementieren
  • oder für rein equality-comparables: einen operator==
  • Boost::Operators wird damit praktisch obsolet

aber nur wenn es eindeutig ist; typischerweise muß dazu die Vergleichs-Kategorie mit angegeben werden. Also z.B. std::strong_ordering

früher hat man oft einen Basis-Operator in der Klasse definiert, und dann einen freistehenden gemischten Operator (z.B. mit einem Integral-Datentyp) dazu. Oder umgekehrt: in der Klasse den Vergleich mit dem Integraltyp plus eine implizite Konvertierung. Solche Konstrukte führen jetzt oft entweder zu Ambivalenzen (Compile-Fehler), oder zur Endlos-Rekursion

Lösung: Basis-Operatoren oder Spaceship direkt implementieren

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

Konversion string_view ⟼ string kopiert die Daten

"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 mag ärgerlich sein, dient aber einem »guten Zweck«

Es ist eine der großen Errungenschaften, daß hier nun das Thema »Thread-Handling« in ein wirklich portables Framework gepackt wurde. Also mehr als „es ist POSIX“ !!!

std::stringstream ss;
ss << std::this_thread::get_id();
int id = std::stoi(ss.str());
std::hash<std::thread::id>{}(std::this_thread::get_id())
std::thread::id threadId = std::this_thread::get_id();    
unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<void*>(&threadId));

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!

es ist ein einmal-Kommunikationskanal

Endpunkte werden normalerweise vorher erstellt

...will sagen, bevor die »concurrent operation« überhaupt beginnt; also beispielsweise bevor man einen Worker-Thread startet, ist zumindest der Promise (und damit der shared-state) schon erzeugt. Ein Future kann man dann später davon ableiten (is clearly sequenced). Wenn man Endpunkte über Thread-Grenzen hinweg weitereichen möchte, ist das nicht durch den Future-Promise-Mechanismus gedeckt und muß anderweitig konventionell synchronisiert werden.

also ein shared_ptr != null

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.

 */

  • bestehende Lösungen analysiert
  • eine minimale Lösung selber implementiert (mit generischem Data-Binding)

sondern verwenden einen Parser-Builder

ein sub-Parser wird akzeptiert, aber es ist kein Fehler wenn er übrhaupt nicht greift; partielle Anwendbarkeit aber ist ein Fehler

repetitive Strukturen

  • Kleene-*
  • Kleene-+
  • delimited List

als Teil der Kombination wird auch Kombination der Ergebnisse  gehandhabt

Dazu gehören schon die sehr geläufigen Bausteine wie optional und sequenced-by. Mit erweiterter Vorschau und Modell-Interaktion ist man schnell nicht mehr kontextfrei.

Hier gibt es mehrere Faktoren zu beachten

  • die Eigenschaften der Sprache: wie leicht kann ein vom Parser erzeugtes Modell anschließend ausgewertet werden?
  • die Umstände der Anwendung: welche Struktur hat die zu gewinnende Information? Syntaxbaum, Steuerprädikat, strukturierte Daten, Befehlsvarianten,....
  • die Eindeutigkeit der Grammatik: mit Parser-Kombinatoren schafft man leicht fragile, hochgradig zweideutige Grammatik-Strukturen
  • die Art der Konfigurierbarkeit: starr mit festen Regeln für einen Ergebnisbaum vs. flexibel mit lokaler konfigurierbarkeit oder impliziter Logik
  • Umgang mit Backtracking, welches aus Alternativen in der Grammatik resultiert; wie gut ist die Ergebnis-Repräsentation im Stande, letztlich nicht erfüllbare Hypothesen wieder zurückzurollen?

die schöne Formulierung der Produktionen in der DSL täuscht meist darüber hinweg, daß es oft schwer verständlich ist, was der Parser-Code eigentlich macht, besonders wenn implizite Konventionen involviert sind. Fehlerbehandlung ist so schwer und mühsam wie stets im Parserbau, aber Fehlerbehandlung zusammen mit spezieller Steuerlogik in den Anknüpfungen führt schnell zu nahezu unwartbarem Code. Aber auch im anderen Extrem, bei einem Framework welches lediglich den Syntaxbaum als Algebraic-Type liefert, ist die dann folgende, eigentliche Auswertung per Tree-Walk oft extrem schwer zu verstehen, da man die gesamte Grammatik im Kopf haben muß

Der einzelne Parser im zusammengesetzten Parser-Ausdruck ist opaque und daher nicht der Optimierung zugänglich. Anders als für einen LR-Parser können verschiedene Alternativen nicht so einfach zugleich verfolgt werden. Dadurch entsteht viel Aufwand durch prüfen und Backtracking von Alternativen und die Kosten wachsen u.U. exponentiell in der Läge der Eingabe oder Umfang der Grammatik. Hinzu kommt, daß für ein Modell oft flexible Meta-Strukturen geschaffen werden müssen, nur um sie nach Traversierung wieder zu verwerfen

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'

Das ist essentiell; gute Icons zeichnen ist eine Kunst

Pfeile rückwärts / aufwärts

bottom to top

size="10!"
ratio="expand"

Beachte das Ausrufezeichen; auch expand ist wichtig, um die Apsect-Ratio zu erhalten. Wenn man stark verkleinert, sollte man ein generisches node-Template hinzufügen mit node[fontsize="16"]... dann Rendern mit -Tsvg

      plot-element:
           {<iteration>}
           <definition> | {sampling-range} <function> | <data source>
                        | keyentry
           {axes <axes>} {<title-spec>}
           {with <style>}
  • using only some rows in aribtrary order
  • only one data set in a multiple data set file
  • skipping lines or using only each Nth point

$RunData << _End_of_Data_

 name1 name2 name3

11 22 33

_End_of_Data_

stats $RunData nooutput

print STATS_columns

plot "data.1" using (tan($2)):($3/$4) smooth cspline

      set xrange [-10:10]
      set yrange [] writeback
      plot sin(x)
      set yrange restore
      replot x/2

(actually 2D projections, of course)

incl re-reading and computations

      set multiplot
      set size 0.4,0.4
      set origin 0.1,0.1
      plot sin(x)
      set size 0.2,0.2
      set origin 0.5,0.5
      plot cos(x)
      unset multiplot

this allows to change, reposition and re-style an existing arrow

plot 'data' with boxes, sin(x) with lines

in Debian: linkchecker

  • man verwendet eine lokale Instanz der Website (mit einem micro-httpd)
  • man excluded Lumiera.org und läßt externe URLs checken

Beispielaufruf:

linkchecker http://localhost:8888/ --check-extern --timeout=30 --ignore-url='.*lumiera.org'

Vorsicht mit dem user agent

....das auf dh basiert; dabei ist viel Detailwissen über bestehende Buildsysteme bereits eingebaut

und wird in Lumiera in das »Debian«-Git publiziert, als unabhängier Branch

Ubuntu war hier Debian voraus, und hat Debug-Symbole automatisch paketiert, allerdings dafür ein spzielles Format geschaffen. Debian hat dann später etwas ähnliches getan, allerdings paketiert Debian sie als normales DEB

this enforces symbol resolution at build time

Quellen

und

Dokumentation

zu
Themenbereichen

...im Besonderen die guten Diagramme für Pulse, ALSA und Jack

wichtiger Hinweis: OpenColorIO LUT statt ICC-Profilen  verwenden!

d.h. je nachdem, was man machen möchte: Skalieren, Positionieren, Farbraum-Mapping

...er ist inzwischen sehr tief in Ardour eingestiegen; xjadeo hat er schon „viele Jahre nicht mehr angeschaut“ — da hatte er vor langer Zeit mal beim konsolidieren geholfen; viel von dem Wissen ist dann in Ardour's video-Anzeige in der Timeline eingeflossen.

Nach dem letzten Event, im »le Sucre« an der confluence  von Rhône und Saône, sind dann Frank Neumeier, ich, Robin und Jörg Nettingsmeyer zusammen spät nachts quer durch Lyon zurück ins Hotel gewandert

Das ist der virale Mechanismus.

Er gilt nicht für die Hinzufügungen allein, denn diese können auch sonstwie unter beliebigen Bedingungen lizenziert werden (das heißt, man verliert nicht sein Copyright an seinem eigenen Werk, aber man erwirbt auch keine Rechte an den Werken andere Leute, jenseits dieser LIzenz)

...schließt den Fall ein daß man lediglich ein Compilat bereitstellt (auch das gilt als abgeleitetes Werk)

im Besonderen: klarstellen daß das Werk unter diese Lizenz fällt

Hier gibt es diverse Varianten in der Lizenz, denn die Software kann auch Teil eines Produktes sein, sogar Teil einer Hardware. In diesem Fall muß ein Service für mindestens 3 Jahre geboten werden (oder so lange wie man Wartung und Reparatur bietet), über den man erfahren kann, wie man an den Quellcode kommt. Der Quellcode kann auch duch jemand Anderes bereitgestellt werden, aber man selber hat sicherzustellen, daß dies der Fall ist, solange man das Produkt anbietet oder wartet

Aber die Einzel-Statements sind wichtig für den Fall, daß jemand einzelne Files aus der Distribution herausnimmt und separat weiterverbreitet. Solange die Files in der Gesamtdistribution bleiben, genügt ein einziges zentrales Lizenz-Statement, das jedoch mit allen Einzel-Lizenzen kompatibel sein muß.

Der Copyright-Claim weist auf das Urheberrecht hin, das man gemäß Konvention von Bern automatisch erlangt, für typischerweise 70Jahre nach dem Tod. Dieses Recht bildet das Fundament, warum der Code überhaupt unter eine Lizenz fallen kann. Deshalb muß mindestens ein tragfähriger Copyritght-Claim gegeben sein

Diese Regelung zielt erkenntlich auf die »industriselle Praxis«: dort ist den Leuten alles egal,  wofür sie nicht bezahlt und wozu sie nicht geprügelt wurden. Da bekommt man dann (ist mir selbst oft genug passiert) irdend ein Zip-File hingerotzt, und weiß nicht einmal, ob das Executable, das auf dem Server installiert wurde, wirklich aus diesem Quellcode stammt. Die GPL bietet in diesem Fall einen zusätzlichen Hebel, nicht zuletzt auch um die Reputation einer Quelle aus FreeSoftware zu schützen.

Man muß jederzeit erkennen können...

  • auf welchem Ursprungsrecht (Copyright-Claim) die Lizenz fußt
  • welche Lizenz nun effektiv gültig ist
  • wer sonst noch Änderungen vorgenommen hat und damit Copyright haben könnte
  • Datum der Änderungen summarisch (⟹ verlängert Gültigkeit der LIzenz 70Jahre nach dem Tod)

Jede Verletzung einer Pflicht terminiert die Lizenz, zunächst vorläufig. Allerdings handelt es sich um Vertragsrecht, und desshalb müßte die Aktion von einem Upstream-Rechteinhaber ausgehen...

Vorrausseztung ist, daß man schnellstmöglich die Versäumnisse nachholt (in weniger als 30 Tagen nach Bekanntwerden des Mißstandes). Dennoch können binnen 60 Tagen nach dem Start des Heilversuchs beliebige Upstream-Copyright-Holder ihre Lizenz terminieren und haben dann allerdings nur ihren eigenen Anteil am Werk als Hebel zur Verfügung. Sofern sie dies nicht (binnen 60 Tagen) von sich aus tun, wird die Lizenz durch den Heilungsversuch automatisch wiederhergestellt

selbst wenn ich Fehler mache, und dadurch meine Lizenz verliere, können die Copyright-Holder sich nur bei mir bedienen, aber nicht weitere User in die Pflicht nehmen, die ihre Lizenz durch mich erlangt haben, solange sie im Glauben waren, diese Lizenz sei gültig.

Das gilt nur für die GPL-3 und auch nur für Patent-Claims, die ein Nutzer oder Contributor geltend machen könnte; die Lizenz schützt im Besonderen nicht vor Patent-Claims Dritter

Design-Diskussion »Visitor«

Die Überlegungen zum Visitor und double-Dispatch tauchten bereits in meinen allerersten Design-Studien zu Lumiera auf. Zunächst war mir nur undeutlich ein Zusammenhang klar, warum ich in diese Richtung strebte, erst im Lauf der Jahre enthüllte sich ein größerer Zusammenhang: es ist die Suche nach einer Struktur, die weder von Anbeginn an feste Setzungen macht, aber auch nicht bloß vom Einzelfall getrieben ist. Wie kann man eine gestaltete Struktur bauen und halten, ohne mit einer gewaltsamen Setzung zu beginnen, die im Weitergehen doch zwansläufig zuschanden werden muß? Wie verhindert man, sich in eine Sackgasse zu bringen, indem man sich einfach am naheliegend gegebenen entlanghangelt? Wie vermeidet man Ideenflucht und das „we'll figure it out“-Syndrom?

wie kann man einen Compiler anlegen,

ohne bereits zu wissen, was compiliert wird?

Lumiera verarbeitet vor allem Metadaten

High-Level: die Session ist ein Syntax-Baum mit Attribution

Auf konzeptioneller Ebene

und für das UI : klar gegeben

das Command steht für einen Satz,

der einen Auftrag erteilt

diese Forderung kann in doppelter Hinsicht verstanden werden...

  • wenn ich schon eine Instruktion habe, muß ich vom Subjekt alle weiteren Beteiligten erreichen können; im Besonderen einen Kontext, in dem sich das Subjekt befindet, und welcher eigentlich eine koordinierte Aktion vollziehen muß, um die Instruktion zu verwirklichen
  • das Command und damit die Instruktion ist so anzuordnen, daß das Subjekt der Instruktion die notwendige Souveränität hat, um die Instruktion vollumfänglich zu verwirklichen

....denn andernfalls werden mit hoher Wahrscheinlichkeit fehlerhaft bzw. unvollständig definierte Commands auftreten

auf welcher Art Repräsentation

wird in der Session gearbeitet?

  • Wobei unter Business-Logik verstanden wird, daß gewisse Verarbeitungs-Schemata, die sich an der Target-Domain orientieren, direkt in Code zur Datenverarbeitung übersetzt worden sind. Man kann dann „im Code sehen was das System macht“.
  • Das andere Extrem wäre, wenn ein System lediglich eine Meta-Verarbeitung implementiert, wähend Logik und Inhalt der Verarbeitung in Parametern und Arbeitsdaten steckt.
  • Ein Zwischending wäre die »Pinball-Machine«: bei dieser steckt die Verarbeitungs-Struktur in der Verschaltung, über welche Ereignisse weitergeleitet und beantwortet werden

überwiegend geht es darum, verbundene oder betroffene weitere Objekte zu finden, oder einen Kontext zu erlangen

denn das Command bestimmt was getan werden soll — ob das dann tatsächlich passiert, oder gar weitere Konsequenzen hat, stellt sich in der Verarbeitung heraus und wird als Ergebnis-Feedback asynchron zurückgemeldet

und zwar „natürlich“ aus Sicht dessen, was hier geschieht, also aus Sicht der Implementierung. Dem würde eine technische / Meta-Schnittstelle entsprechen, auf der man eine Art »instruction code« absetzt

Es wird also definitiv nicht »der Clip« den Code enthalten, wie er sich selber rendert, oder »der Track« den Code enhalten, mit dem Mediendaten kombiniert und verarbeitet werden.

und zwar muß das einzelne Objekt im Stande sein, beliebige, lokal opaque aber stukturierte Daten zu transportieren

Und nicht mit Referenz-Semantik, wie ursprünglich beabsichtigt. All die smart-Pointer mit angebundenem Memory-Management fallen weg; stattdessen arbeiten wir pervasiv mit EntryIDs. Placements liegen nur noch im Placement-Index, werden aber extern ausschließlich als ID-basierte Referenzen gehandhabt

Objekte sind stets vergleichsweise klein. Wo das schwierig wird, arbeiten wir mit Proxies (z.B. lib::Literal oder lib::Symbol für Strings). Zu einer gegebenen EntryID kann man stets eine Objekt-Kopie bekommen; auch ein Command bezieht solche Kopien, um darauf eine Methode aufzurufen, welche intern, über Session-Services die entsprechenden Events auslösen. Auch der nachfolgende Builder-Lauf zieht sich eine Kopie des Session-Inhalts

Low-Level: die Nodes sind ein funktionaler Aufruf-Plan

In die Pipeline gehören im Besonderen auch Operationen, die spezifisch sind für ein bestimmtes Ausgabe-System...

  • für XV: das Skalieren und Umrechnen in den Ausgabe-Farbraum
  • für XGL: die Matrix-Operationen um die Textur für die Ausgabe aufzubereiten

Allerdings gibt es Schritte, die sind dann an die konkrete Ausgabe-Verbindung gebunden und insofern Zustands-behaftet:

  • XV: die shared-memory operationen und Kommunikation mit dem X-Server und flush
  • XGL auch hier erzeugt man ja einen Grafix-Kontext

Problem: »Event«-getriebene Struktur

multiple »Domain Ontologies«?

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

Konfiguration, Parametrisierung, Regeln und Defaults sind ein komplexes Thema — und es ist überaus schädlich, wenn sich dieses auf unklare Weise mit der Darlegung im Code vermischt. Der einzige Ausweg aus diesem Dilemma besteht in Klarheit und Grenzziehung: der Code muß die Entscheidungsmöglichkeiten deutlich machen, dann eine konkrete Anfrage stellen und der sich daraus ergebenden Entscheidung Folge leisten....

Die klare Trennung von Code und Entscheidungen bringt eine Schattenseite zum Vorschein: Die Beherrschbarkeit und Regelmäßigkeit  des Verhaltens steht in Spannung zum Fluß des Einzelfalles, und dem Wunsch nach Anpassungsfähigkeit. Das ist ein Problem, also nicht lösbar; es ist aber eingrenzbar, indem der Weg im Nachhinein aufgeklärt und modifiziert werden kann....

Query and Decision-System sind stateful —

insofern gehören »Session« und »Setup« zusammen

Entscheidungen fügen sich zusammen und bauen aufeinander auf — daraus entsteht ein Weg in das gegenwärtige Setup, welches seinerseits nahtlos übergeht in den Stand des Projekts, der sich in der »Session« niederschlägt. Für das Bindeglied, das sich hier als Teil einer Architektur abzeichnet, wähle ich den Begriff DecisionSystem.

Das gehört zur ersten Vision (auf der das Projekt oberflächlich segeln soll): Lumiera soll die Standard-Applikation für anspruchsvollere Medien-Arbeiten sein. Diese Applikation muß in allen major-Distros ohne große Komplikationen mitlaufen können; User installieren es einfach aus dem Paketmanager und können sofort loslegen (nachdem sie noch ein Tutorial gelesen oder angeschaut haben). Es darf hier keinesfalls irgendweilche Einstiegshürden oder Zusatzanforderungen geben.

keine problematischen Libraries

Libraries können in verschiedenster Hinsicht »problematisch« sein

  • sie können aufgegeben werden und verrotten
  • sie können Spielball kurzfristiger Moden und politischer Spielchen werden
  • sie können kommerziallisiert und dann ausgeschlachtet werden
  • sie können technologisch instabil sein / werden
  • sie können exzessive Abhängigkeiten nach sich ziehen
  • sie können die Entwicklung einer Distribution stören

Der durchschnittliche User möchte die geläufigen Medienformate einfach öffnen, lesen und schreiben können. Das ist die Kernkompetenz von FFmpeg; was von FFmpeg geöffnet werden kann, gitl als geläufig.  Auch Youtube setzt auf FFmpeg. Daher ist FFmpeg in jeder Distribution da und es gibt dafür jeweils ein eigenes Sicherheitsteam (weil es naturgemäß erhebliches Angriffspotential bietet).

Christian kannte bereits Burkart Pflaum, und hat uns auf dessen Libraries aufmerksam gemacht. Die Libraries sind vorbildlich in iherer Struktur und in dem beschränkten Scope. Wahrscheinlich sind sie problemloser einzubinden als FFmpeg. Aber das Kernproblem bleibt: das ist eine one-Man-Show, und wir werden trotzdem auf FFmpeg nicht verzichten können....

Also wenn es sich — wider Erwarten — herausstellt, daß FFmpeg die elementaren Grundfunktionalitäten nur unbefriedigend für alle Farbtiefen und Pixelformate bieten kann, dann könnten Gavl und Gmerlin leicht die Lücken füllen, denn sie sind einfach einzubinden.

GStreamer, MLT, libVLC, MPlayer

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

Das war eine Richtlinien-Entscheidung, nachdem wir den Vortrag »Video-Output« auf der FrOSCon gehalten haben.

Dazu gibt es mehrere Gründe

  • wir sind massiv zurückgefallen und haben noch sehr viel zu tun für eine minimal funktionsfähige Applikation
  • das Thema »Shader-Programmierung« ist ein dicker Brocken, und erfordert zusätzliche Infrastruktur; ja das wird relevant
  • Wayland ist »um die Ecke« (und dann müssen wir sowiso nochmal 'ran)

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)

git://git.lumiera.org/extra/froscon.git

Subdir: ichthyo

XSync leert die Event-Queue. Wenn sie leer ist, wird per IO nach weiteren Events gesucht. Was so gesammelt wurde, wird als Batch an den XServer gegeben und blockend gewartet, bis dieser diese Events abgearbeitet hat. Zwischenzeitlich können weitere Events aufgelaufen sein; diese kann man dann einfach wegwerfen (discard = true) oder in der Queue lassen (discard = false) ohne sie zu bearbeiten

war verlinkt von einer Stackoverflow-Antwort zum Thema "redraw unter X"

https://stackoverflow.com/a/17035752/444796

heute ist OpenGL ein Zugang zu einem computation device

EGL verwendet ein Extension-Modell: es wird für die jeweilige Plattform eine passende Extension installiert. Diese ist nicht komplett neutral, d.h. der Client-Code hängt noch von einem allgemeinen Zugriffschema ab: man bekommt irgendwie eine »Surface« — die dann aber an verschiedene moderne APIs gebunden werden kann: OpenGL core-profile, OpenGL ES, OpenVG (2D -Grafik), Vulkan

  • Eine GTK-3 - Applikation — alles noch unter X11
  • drei Callbacks, in denen die eigentliche Logik demonstriert wird
  • eine Commons-Lib mit einfachen Abstraktionen für RGB-Daten

Zusätzlich gibt es eine Skizze für SDL, die ich aber auf dem Legacy-Level belassen habe (aus Zeitgründen). Also SDL 1.x (weithin gebräuchlich ist SDL v2, aktuell 2025 ist sogar v3 erschienen)

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

Ich betrachte den Inhalt der RfC grundsätzlich als gültig und bindend. Aber seitdem ich alleine am Projekt arbeite, ist dieses Format nicht mehr sinnvoll, denn ich diskutiere nun pro forma mit mir selber, dafür aber in einer viel größeren Tiefe

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

Das erachte ich als dringend, denn wer schreibt, der bleibt:  der typische, durchschnittliche »Techie« hat keine Ahnung vom Arbeiten mit Medien und auch keine Ahnung von Interaction-Design; die typsichen Feature-Requests, mit denen wir geflutet werden, kann man weitgehend ignorieren, es wäre schädlich für die Applikation, sie umzusetzen, bevor ein vollständiges GUI/Workflow-Konzept mit strikten Richtlinien da ist. Und ein solches Konzept auszuarbeiten, kostet Zeit und Kraft. Es ist strategisch sinnvoll, diesen Aufwand von der laufenden Entwicklungstätigkeit abzuzweigen, hoffentlich bevor das Thema allgemein bemerkt wird.  Wenn man »die Leute« machen läßt, dann wird Lumiera die ursprüngliche Vision verfehlen. Denn man kennt heut nur, worauf einen der industrielle Prozeß konditioniert, und man will dann bloß das Gleiche, aber umsonst.

Das ist eine Sichtweise auf die Architektur, die ich inzwischen überwunden habe

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

und zwar für zwei Dinge

  • warum eine vollständige Lösung etwa 10 mal so viel Aufwand ist, wie mal eben runterhacken
  • für die Begriffsunterscheidung akzidentell vs essentiell, die Brooks in diesem Buch in den 70ern eingeführt hat (Essay »There is no Silver Bullet«)

nenne es »can be solved«

deute es als ein Festhalten an einer »einfachen« Lösung

dahinter steckt eine Form des Verfallens

⟹ auf Frederick Brooks zurückgreifen

Recherche: das war ja noch alles viel dramatischer

...und darin liegt der Wirkmechanismus: da die Plug-ins von jemandem Anderes geschrieben werden, kann ich jetzt bereits die komplette Lösung deklarieren (und alle Einwände werden pariert mit "can be solved as a Plug-in")

Das meine ich auf mehreren Ebenen

  • zunächst das „natürliche“ (das heißt zwanghaft verfallende Coden)
  • ich markiere aber durchaus bewußt den Anspruch, daß der Entwickler gestaltet und entscheidet was er baut (und nicht ein Entscheidungsträger in einem industriellen Prozeß)

Warnung: gefühlte Realität

...sinnlos dagegen zu argumentieren, man darf sie aber auch nicht einfach vom Tisch wischen und für „unreal“ erklären

Ich fand meinen Entwurf nicht sonderlich visionär, ehr naheliegend, und der Sache angemessen. Mein Entwurf wurde mit Begeisterung aufgenommen — sonst hatte nämlich niemand überhaupt einen Plan, oder auch nur einen Horizont, im HInblick auf Film, Medien und freie Software. Ich habe die Idee ernst genommen, daß man selber gestaltend handeln kann und sollte. Ich hatte mir erhofft, mit anderen zusammen gestaltend zu handeln

Ich fand mich in einer Bewegung und Gruppendynamik, die ich als widerwärtig und pupertär empfunden habe. Das was ich vorgeschlagen habe, wurde allerdings von den Filmemachern und Medien-Leuten sofort verstanden, nicht aber von all diesen »Techies«, auf deren Beitrag ich gerechnet hatte.

Aufgrund meiner auch damals schon erheblichen Erfahrung habe ich gesehen, daß mein Entwurf nicht mit allgemeinen Wünschen harmoniert (zumindest nicht anfangs, man muß einen Fokus setzen für ein derart großes Projekt). Ich habe daraufhin geschickt navigiert, und tatsächlich die anderen beteiligten Interessen ausmanövriert. Ich ging davon aus, daß mein Entwurf für das Projekt so offensichtlich ist, daß sich schon brauchbare Unterstützer finden werden. Dann hat sich aber das Klima gedreht, und jetzt sitze ich seit mehr als 10 Jahren allein in dem Projekt, und mußte mich jahrelang mit den Folgen dieser Manöver plagen. Es gab keine Möglichkeit mehr, den Konflikt auszutragen (und das Projekt ist sowiso niemals allein zu bewältigen, ich allein kann grade verhindern, daß es ganz untergeht)

Auseinandersetzung mit der Historie  ⟹  habe Liberalismus  dahinter entdeckt

Vor dem Hintergrund der veränderten Situation (Plan einer Stiftung) habe ich begonnen, Altlasten aufzuräumen; damit sind all diese lang begrabenen Themen wieder hochgekommen. Ich habe die aufgehobenen Dokumente und Protokolle durchgesehen, und die Erzählung zur Historie von Lumiera weiter geschrieben. Erst in dem Zusammenhang wurde mir klar, daß hinter dieser Spinnerei mit den Plug-ins eine konsistente Ideologie steckt, welche sich bei näherer Betrachtung als eine Spielart des liberalistischen Glaubens an unsichtbare Heilkräfte herausstellt. Im Rückblick erscheint das plausibel, das war (und ist) der Zeitgeist. Das kann ich aber nicht als Lösung akzeptieren, sondern empfinde es als ungerecht.

Darin steckt mein Verlangen nach Rache: ich habe zig mal die Erfahrung gemacht, daß ich meine Haltung und meinen Entwurf überhaupt nicht formulieren kann, weil man mir gar nicht zuhört, sonder wie verblödet immer nur seinem Aberglauben an die magischen Kräfte des Kollektivs frönt. Nun schaffe ich mir eine Konstellation, in der alle diese Kollektiv-Schafe ihr blödes Maul zu halten haben.

Meine Haltung war bisher — ehrlicherweise eingestanden — auch nur eine intuitive Einschätzung „das kann so nicht funktionieren was ihr euch da so vorstellt“. Damit allein werde ich keine Debatte bestehen können, und schon gar nicht gegen einen »Zeitgeist«. Also brauche ich eine bessere Position, die die Frage nach der konkreten Architektur und der Rolle von Plug-ins auf einen Boden stellt, auf dem überhaupt argumentiert werden kann. Wenn überhaupt, dann ist die Gelegenheit für strategische Weichenstellungen jetzt (auch bezüglich dessen, was ich für später offen lasse)

Das ist ein strategischer Ansatz

  • ich steige bewußt nicht in eine Argumentation ein
  • ich setzte einen Rahmen, der die Handelnden mit einbezieht und auf Reflexion verpflichtet
  • ich rücke ein betontes Verständnis der Architektur in den Mittelpunkt und mache es zur Aufgabe
  • damit entziehe ich eventuellen Diskussionen über Technologien und ihre Wirkungen bereits im Vorhinein den Boden (ich denke da auch an die sogenannte »künstliche Intelligenz«)

...das war am Ende eine erhebliche Schwierigkeit, und hat mich fast eine Woche Arbeit gekostet. Denn zunächst einmal bin ich induktiv vorgegangen, und damit meine ich, aus einem Verständnis des Stoffes — der Text ist nun sehr lang und mühsam zu lesen. Zwar geht es mir um das, was zwischen den Zeilen steht. Aber beim Lesen muß man dennoch das Gefühl haben, daß der Text wohin führt. Und zwar, da es sich um einen Essay handelt, und nicht um einen wissenschaftlichen Artikel, sollte der Text zum Anfang zurückführen.

habe nun alle RfC durchgesehen und verstehe einige Zusammenhänge besser

Erste Mail: Oktober 2006.

Diese Mail war versehentlich auf die Mailingliste geraten, und zeigte, daß damals Cehteh und Johannes Sixt (vom Cinnelerra-CV-Team) zusammen ein Git-Repo aufgesetzt hatten

zunächst bezogen auf ein Independent-Film Project, für das versucht wurde, auf Cinelerra zu editieren, weil Cinelerra damals die erste leicht zugängliche Methode war, HDV-Video zu editieren.

war aber bereits etwas skeptisch, da er Cinelerra länger kannte als Cehteh

Und zwar lediglich, weil er davon nichts wußte. Diese Initiative war nämlich nirgends angekündigt; man mußte viel auf IRC sein, um mitzubekommen, daß da was lief. Ichthyo hatte sogar Cehteh und Johannes Sixt chatten gesehen, und nicht recht verstanden, worum es ging: sie haben nämlich versucht, die neueste Version Cinelerra v2.1 mithilfe von Git nochmal gemerged zu bekommen. Cehteh und Ichthyo sind erst im Mai direkt ins Gespräch gekommen, und dann hat Cehteh sofort Ichthyo eingeladen, sich auf pipapo.org einzubringen (d.h. Ichthyo bekam Schreibrecht auf das Wiki). Allerdings hatte Cehteh bereits ein halbes Jahr vorher für Ichthyo ein Git-Repo auf pipapo.org eingerichtet (mit Schreibrecht), welches Ichthyo genutzt hat, um weitere Patches für Cinelerra vorzubereiten und mit Johannes Sixt abzustimmen. Ichthyo hat aber damals noch nicht verstanden, daß da eine Initiative entstand, die unabhängig von Cinelerra-CV war. Er dachte zunächst, dieses Git-Repo wäre eine neue Einrichtung von Jonannes Sixt, für Cinelerra-CV

...und zwar, im Bezug auf Änderungen, die sich von der Heroine-Version von Cinelerra wegbewegen. Der Grund war offensichtich, daß der die Situation kannte, und keine Möglichkeit sah, sie zu ändern. Johannes hatte einen full-time Job als Entwickler, und ohnehin wenig Freizeit, die er nicht komplett nur für Cinelerra verbraten wollte. Er war es auch, der die Merges überhaupt zustandegebracht hat, und damit eine ganz kleine Möglichkeit geschaffen hatte, neue Patches zu akzeptieren; aber jeder Patch hat ihm persönlich Probleme bereitet (weil er dann den nächsten Merge „ausbaaden“ durfte).

Und zwar, obwohl (oder weil) er ein extrem erfahrener Programmierer und Project-Lead war. Er wußte einfach, daß er keinen Code mehr anfassen würde.

Dahinter verbirgt sich eine tragische Geschichte: er hatte ADHS, war mit Ritalin behandelt worden, und Ritalin-süchtig geworden, hatte einen kompletten Zusammenbruch mit Burnout durchgemacht, und war von Opera in Frührente geschickt worden (mit 40 Jahren). Er war bereits mit bedrohlichen Nebenwirkungen vom Ritalin-Abusus konfrontiert, und die Ärzte hatten ihm vorhergesagt, daß er vermutlich in wenigen Jahren in eine Art Demenz gleiten würde.

hat aber — mangels Erfahrung — sich nicht getraut, viel Code beizutragen; er hat den Code gelesen, Typos in Kommentaren korrigiert, Formulierungen in den Wikis verbessert und viele (sehr gute!) Fragen gestellt.

...und hat dabei sehr viel gelernt, was im Umkehrschluß bedeutet, daß Cehteh ihn intensiv gementored hat, und ihm viele Programmiertechniken beibringen mußte, die auf der Uni nicht gelehrt wurden. Er hatte nur ein Semester "systemnahe Programmierung" gehabt, und Spaß daran gefunden, aber die Uni hat nicht mehr geboten, als ein paar Programmieraufgaben in C.

die C++ - Strukturen von Ichthyo hat er bewundert,

vieles aber nicht verstanden und wollte dort keine Last sein.

Und zwar überwiegend in der Rolle eines »Bewunderers«: er war bei allen Diskussionen dabei, hat sich oft nichteinmal getraut, Fragen gestellt, und dann anschließend in langen Chat-Sitzungen sich alles im Detail von Ichthyo erklären lassen. Er sagte damals immer wieder, daß er so gerne mit dabei sein wolle, aber was hier gemacht würde, sei um „mehrere Stockwerke zu hoch“ für ihn

Das war von Cehteh als „eigentlich ein Anfänger-Job“ eingestuft. Daher hat Cehteh ihn angehauen, „Siemon, das packst Du!“. Tatsächlich hat Simeon den größten Teil der Implementierung geschrieben, so wie sie dann viele Jahre in der Codebasis verblieb. Allerdings brauchte er permanente Hilfe von Cehteh; die beiden waren beinah täglich zusammen im Chat und Cehteh hat den Code von SimAV reviewed

Und zwar handelte es sich um Code von hoher Qualittät, rein als Library konzipiert, sauber strukturiert, gründlich getestet

...er ist aber nie selber eingestiegen, sondern hat darauf gewartet, daß Cehteh die entsprechenden Teile im Backend implementiert, in denen seine Library eingebunden würde; die Developer-Gruppe hate Anfangs in aller Form beschlossen, daß Lumiera auf dem Gmerlin/Gavl-Framework von Burkhard aufbauen sollte. Burkhard hat immer klar gemacht, daß er nicht direkt in das Lumiera-Projekt einsteigt, weil sein eigenes Projekt (der Gmerlin Videoplayer) bereits seine volle Kapazität braucht. Und da Christian kaum je etwas für das Backend getan hat, sondern Plugins und Frameworks gebaut hat, kam auch Burkhard nie weiter in das Projekt und verschwand irgendwann von der Bildfläche

Richard Spindler found his way through Parma thanks to the great

OpenStreetMap project.

Hatte damals die Idee,

die verschiedensten Video-Entwickler

in einem Meta-Projekt »Open Video«

zusammenzubringen

Die Mailingliste dazu hat Cehteh auf der Infrastruktur von pipapo.org und später von Lumiera gehostet; leider ist diese Initiative relativ schnell ausgetrocknet (es gab wenig zu besprechen, jeder hat sein Ding gemacht)

Fazit: es hat sich erübrigt

commit 13b963ba5bc39603c1d425752f07d8b3941f01ba

Author: Christian Thaeter <ct@pipapo.org>

Date:   Mon Jun 18 01:14:12 2007 +0200

    initial commit, just tiddlywiki tests

commit 0a9c2599dd49990b8ccf779a44abdaba626bdd86

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Tue Jul 3 00:13:12 2007 +0200

    some cleanup. Set Version=3+alpha.01, add a helloworld-main to make it compile

commit 0a9c2599dd49990b8ccf779a44abdaba626bdd86

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Tue Jul 3 00:13:12 2007 +0200

    some cleanup. Set Version=3+alpha.01, add a helloworld-main to make it compile

commit a313ea87a588241a1db72b6cac6e2aee2b512fc7

Author: Christian Thaeter <ct@pipapo.org>

Date:   Sun Jul 15 02:23:37 2007 +0200

    Work in progress, just for review

src/lib/plugin.c

src/lib/plugin.h

commit 471148b7db2e41f2c081760cc367710ce6999da9

Author: Christian Thaeter <ct@pipapo.org>

Date:   Thu Jul 19 05:10:14 2007 +0200

    basic automake setup

commit ebb4da6cc738392c015c7d66c54c6483331459f4

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Wed Aug 8 04:50:02 2007 +0200

    ** Start Coding ** Renderengine sources generated, reformatted and made compilable.

commit ed4decb5de9c6c22bb0f9173e3d239fefe9453e7

Author: Christian Thaeter <ct@pipapo.org>

Date:   Sun Aug 12 04:10:10 2007 +0200

    added notes from yesterday irc discussion

commit 45c21677009dfc733d0ecd6f26d783c99b2818d5

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Aug 13 09:55:32 2007 +0200

    wrote a very simple Test-Suite runner and provided a Tests source tree

commit ce3eb42131b0f8f809b00ef9a7759eb885e684d3

Author: Christian Thaeter <ct@pipapo.org>

Date:   Mon Aug 13 17:22:07 2007 +0200

    test suite works now basically

Die Git-Historie der ersten Wochen ist im Rückblick durchaus aufschlußreich. In der offiziellen Kommunikation haben Cehteh und Ichthyo über eine gemeinsame prototypsiche Applikation geredet, während gleichzeitig jeder auf seinem Branch »Fakten geschaffen« hat, die nicht koordiniert waren, und sich konzeptionell widersprechen. Das hier ist ein gutes Beispiel...

  • plötzlich schreibt Cehteh einen "ganz einfachen" Test in C, mit einem shellscript zum Starten. Jedwedes schaffolding muß man selber machen
  • vermutlich daraufhin arbeitet Ichthyo die ganze Nacht durch, und kippt am nächsten Morgen ein komplettes, in C++ geschriebenes Unit-Test-Framework ab.

Jeder integriert „seine“ Tests natürlich in „das“ Buildsystem (was auch bereits disjunkt war, Autotools für Cehteh, SCons für Ichthyo). Es ist definitiv klar daß man Unit-Tests wollte. So klar, daß seinerzeit nicht weiter darüber geredet wurde

Ein Rant von einem User aus Argentinien. Das "Cinelerra-3"-Projekt war offenbar nur wenigen bekannt. Christian und Hermann Robak haben irgendwann im Thread geantwortet

On Tue, 14 Aug 2007 23:32:01 +0200, Edouard Chalaron

<e.chalaron@xtra.co.nz> wrote:


Well I am sorry, but the way icons look is of the last relevance

I don't work better because icons look better. They could look better
but I could not care less either.

 Same here.  But people _will_ complain about the things they see,
perceive or understand.  So we will keep hearing complaints about
the colours and the icons until they become more in style with the
flavour of the month.

 The developers don't feel strongly motivated by that, though.
I am not shaming the developers for not caring about the end
users' complaints.  Nor am I shaming end users for complaining
about things that the developers never will consider urgent.
I am just pointing it out.  If you want to vent here anyway,
I don't mind. ��


 In light of this, I think Christian Thäter's protocols for
work on Cin3 are clever.  You have to hang around on IRC and
poke around with the git repositories, regularily.  If you
don't, you are out of the loop.
 People who are "talkers" and not "doers" will have to spend
a lot of energy just to stay in the loop.  They will either
get a more intimate insight into which ways things are going,
and why, or they will get fed up and leave.
 It makes trolling much more expensive, and it makes the
"doers" stand more clearly out.

 These are interesting times ��

-- 
Herman Robak

marquitux caballero wrote:
in the comunity very cool people tried to explain me thos things, but
they seems to be very focused in specific issues, and those BASIC
things, are not important  in this part of the coding process, and they
told me those things are BUGs... really? bugs? or bad plannig, or even
no global vision?
Few people from IRC gathered together to plan a rewrite/redesign of what
ought to become 'Cinelerra 3'.

Please take a look at:
http://www.pipapo.org/pipawiki/Cinelerra3/DesignProcess/Manifest
http://www.pipapo.org/pipawiki/Cinelerra3

So far we have very cool ideas about a new design which allows a lot of
things which are currently not possible, some coding has started but
this is rather in a experimental, preparation phase.

The downside is that we massively lack developers, unfortunally many
previous contributors fallen away because they finished university, got
new jobs or whatever. We aim to make cinelerra3 a open project where
anyone can join and help as much as possible! If you are coder and
interested, just join us.

I've send a http://www.pipapo.org/pipawiki/Cinelerra3/Announcement about
this 'cinelerra 3' project to all developers, so far the responses where
very sparse but postive.

A note to all 'users' reading this: Please refrain from sending feature
request and ideas to us, its way to early and only costs our time to
explain that we consider this things later. Ichthyo and me decided to
design cin3 from ground up. Interested people should start by checking
out the git repositories and review what is there. If you know how to do
things better ask the responsive author of the current thing on IRC or
via mail and do a discussion with the involved people about it. Speaking
for me, I would like to see improvements and new ideas, but I don't want
to become overthrown by people just dropping ideas and then disappear.

Further note about HV's involvement: I informed him at first about this
ideas, but his responses are sparse as usual. It is clear that this may
only become Cinelerra 3 if he acknowledge on this project at some time
and he is invited to join and contribute whenever and as much he wants
to do (we aim to reuse code and ideas from cin2 anyways). Cinelerra is a
heroinewarrior project, Cinelerra CV is a (friendly) fork of it, we
don't want to take over the project, our goal is just to make the best
free Linux Video editor in existence ��.

	Christian

_______________________________________________
Cinelerra mailing list
Cinelerra@skolelinux.no
https://init.linpro.no/mailman/skolelinux.no/listinfo/cinelerra

This my maybe arguable view how to hive Cinelerra CV out of its
develoment stall:

1) Change the focus of CinelerraCV
Currently CVs goal is repackaging the HV version and fixing bugs.
But a real community version should acknowledge progress and new
features which are contributed by the community.

2) Stop using SVN
Even if commit access is generously handled to people who ask, it's
still a big blocker as I explained earlier. As long we have only one
linear history everything has global impact and there is no easy way to
add new features without running in troubles. There is no easy way that
small groups of people try and review new features, no easy way to get
good but intrusive new ideas back into CV.

3) Make releases
Cinelerra CV has only this SVN there is no release schedule and no
defined point when the source is called to be stable (well we can't
define in a lack of testsuite and presense of many bugs anyways). This
yields the result that anyone (including distributors and packagers)
build on some (maybe recent?) svn revision. There are packages from many
different versions out there which makes it not really easy to track
reported bugs down. Users have doubts which is the best version for them
already just because this linear revision history without release
statements, which is imo more worse than a magnitude of git branches
with defined releases (and maybe bugfix revisions on them)

4) Make tracking HV less important
We want some branch which tracks heroines versions and refactors it into
smaller commits as we are doing now, but this should be considered as
tool and foundation of any work which is done on our releases. This
means the CV version should be maintained in another branch and new
features should be added on our development (or release) branches.
Finally we may provide a backporting branch where imminent bugfixes are
prepared to be mergeable with the hv-tracking version. So this becomes a
way how we can contribute back to HV which is currently not a easy case.
Maybe Adam once speaks about what he wants, so far he complained that
the community didn't provide much useable feedback .. and admitably he
was right, takeing HV less important will actually allow us to do more
work and thus may provide more benefits for HV getting some
contributions feed back.


	Christian

_______________________________________________
Cinelerra mailing list
Cinelerra@skolelinux.no
https://init.linpro.no/mailman/skolelinux.no/listinfo/cinelerra

...also der ganze Kreis an einführenden Seiten, die bis heute in meinem Renderengine-TiddlyWiki herumhängen. Und alle wesentlichen UML-Diagramme. Sogar über die Builder-Entities habe ich mir bereits ausführlich Gedanken gemacht

  • für die Renderengine (Hermann): ein Durchgang von EDL mit Placements, ExplicitPlacements, dem Builder und einem Render-Graphen
  • für das Backend (Christian): der komplette Inhalt des späteren RfC: Data Bakend. Plus eine Implementierungsskizze als Sammlung von Tiddlern, die man jeweils direkt runtercoden könnte

Error, Locking, Plugin und eine Linked List

Zunächst einmal, ich wußte mit UML umzugehen, Christian hat nach einem ersten Gehversuch aufgegeben. Daher habe ich per Generierung bereits einen großen Haufen Klassen angelegt, d.h. der C++ - Code dominierte absolut. Aber für Christian war das einfach durchschaubar (und ich habe das auch betont).

Abgesehen davon hatte ich ein Asset + MObject-Framework angelegt und Tests für CRUD-Operationen in der Session. Wobei die Session damals ein kompletter Mock war. Außerdem hatte ich eine Reihe von Test-Skeletten für den Builder und den Aufruf von Nodes, aber dort war alles praktisch nur Platzhalter-Code, und ich kam bereits nur noch langsam voran.

Im Vergleich hat Christian nur sehr wenig gebaut, und das waren elementare Sachen, die aber vollsändig und routiniert. Ich habe eine weit ausgreifende Struktur skizziert, die fast nur aus Dummies besteht

In diversen Diskussionen, die ich heute gelesen habe, ist immer nur von "CT" die Rede. Auch Herman Robak rehted immer nur von Christians Initiative. Ich sehe Antworten von mir, die wie "5. Rad am Wagen" rüberkommen, oder wie jemand, der sich wichtig machen möchte, und sehr akademisch redet. Meine wenigen Aussagen wurden in Diskussionen in diesem ersten Herbst praktisch nicht aufgegriffen. Allerdings bin ich in die Threads mit den Usern auch wenig eingestiegen, deren Argumente waren mir zu blöd, um darauf einzugehen. Das war ganz anders als sonst, ich finde viele Beiträge von mir, in denen ich Usern mit Cinelerra geholfen habe, und daraus ist ein Gespräch entstanden.

ich erinnere mich, daß andere Leute von "Deinem neuen Projekt" geredet haben, und Christian dann immer darauf hingewiesen hat, daß das nicht "sein" Projekt ist. Er wollte es auch nicht auf pipapo.org hosten.

Ich erinnere mich, argumentativ auf die Leute einzugehen, und zwar vor allem auf die Anfänger. Ich kann mich erinnern, daß Christian grade den Anfängern gegenüber oft von oben herab kam, und schnoddrig war. Ich erinnere mich auch, in Debatten auf IRC stark präsent gewesen zu sein, und sehr für unser Projekt geworben zuhaben. Ich hatte auch lange, lange Gespräche mit Raffa und Co. über allgemeine Themen und Film. Chistian dagegen hing auf dutzenden anderen Channels herum, und hat dem Cinelerra-Channel nur begrenzte Aufmerksamkeit gewidmet. Er war auf anderen Channels oft in routiniertes Ping-Pong mit unendlich vielen anderen Leuten involviert, die sich alle kannten. Demgegenüber war ich dort ein kompletter Außenseiter, und hab mich auf diesen anderen Channels (z.B. Debian, Free Software) auch tunlichst rausgehalten.

Also das Gefühl, daß das alles dermaßen gut aufgeht, und wir so unglaublich produktiv sind, daß sich ein laufendes System in ein paar Monaten hinstellen läßt, wenn man nur wirklich hart arbeitet.  Es bringt mir auch die Erinnerung zurück, daß ich nicht hinterfragt habe, wie das Verhältnis zu Cinelerra ist. Das hier war »Cinelerra-3« und im übrigen gab es ja meinen Projektplan, mit dem man das irgendwie den ersten Meilensteinen zuordnen könnte. Auch das Gefühl: wie wir dann weiter vorgehen und das in Cinelerra einbauen, überleg ich mir, wenn die Engine läuft. Denn eine laufende Engine kann ja schon mal nicht falsch sein.

Diese Erinnerung bringt erstmals das Gefühl einer auszehrenden Schwere. Ich bin einen ganzen Tag dagesessen, draußen regenete es. Von Zeit zu Zeit war ein rätselhaftes "Tuuut" auf 1kHz draußen zu hören, das ich nicht verstanden habe, nicht klar ob eine Glocke oder ein Signal. Währenddessen habe ich mit mit der Asset- und MObject-Hierarchie herumgeplagt, die Struktur und die Logik wollte nicht aufgehen, ich sah keine Möglichkeit, einen Test zu schreiben, und ich habe vergeblich nach einem Ankerpunkt gesucht, von dem her ich den Code aufrollen konnte. Es war ja letztlich eine Sammlung von UML-generierten Klassen-Skeletten, die ich nun versuchte, zusamenzuhängen.

Später, als es schon dämmerig wurde, bin ich in den Supermarkt gegangen (war damals noch nicht einmal der Rewe). Vor dem Eingang hab ich wieder das rätselhafte "Tuuut" gehört, bin dann im Regen die Aberlestraße entlanggefahren und durch den Park im Südbad. Auch dort war es zu hören, und ich konnte nicht orten, von woher es kam, oder was es war. Erst einige Tage später habe ich oben, hinter der Margarethen-Kirche eine Baustelle an der S-Bahn entdeckt. Es war also ein ganz banaler 1kHz-Sinuston aus Lautsprechern, die an der Strecke entlang standen.

Und zwar in den ersten dokumentierten Diskussionen, auf der Cinelerra-Mailingliste mitte August. (Beachte, Adam konnte das lesen — das zeigt, daß Christian unreflektiert gehandelt hat).

Des genaueren sagte Christian, er habe versucht, die Cinelerra-Codebasis zu refactorn und verbessern, und habe es aufgegeben, da "bottomless pit". Ich weiß aber definitiv, daß er das nicht im Sommer getan haben kann. Also muß er bereits im Frühjahr zu diesem Schluß gekommen sein, hat aber andererseits meinen Umbau-Plan zumindest verbal unterstützt (aber schon solche kommentare reingeschrieben, wie "wäre es nicht besser allses neu zu bauen?"

De facto hat Christian nur an seiner Applikations-Basis gearbeitet: das erste war der Plugin-Loader v1, dann kam das Errorhandling und die Tests. Es war alles von Anfang an ausschließlich auf C angelegt. Auch hat er nur kurz etwas mit SCons gespielt und dann nur noch mit seinem Autotools gearbeitet.

Ich weiß ganz sicher, daß ich niemals einem Projekt beigetreten wäre, einen Video-Editor komplett neu zu schreiben. Da hätte ich eine ganz andere Organisation vorausgesetzt, und eine echte Design-Phase. Ich kann mich auch erinnern, daß ich Christian's Glaube an die "Community" als naiv empfunden habe. Ich sah das, was wir machen, als ein alternatives Basissystem, mit dem man sich in eine bestehende, große Applikation einklinkt.

De facto habe ich an einer naiv objektorientierten Klassenhierarchie gearbeitet, und erst mal versucht, als Java-Entwickler mit C++ klar zu kommen. Um den C-Code von Christian habe ich mich kaum gekümmert, und mir gedacht, wird man dann schon irgendwie aufrufen können, schließlich kann C++ ja auch C. Ich erinnere mich auch, daß ich Angst vor der Systemprogrammierung hatte, und froh war, daß mir Christian das abnehmen wird. Ich dachte, die Beiträge von Christian werden schon noch kommen. Das was er anfangs gemacht hat, habe ich gar nicht erst genommen, und für Experimente gehalten.

die Plug-in-Kontroverse war bereits damals, in den ersten Wochen

wie kam es daß wir neu gebaut haben?

  • Erste Hypothese: diesen »Intent« habe bloß ich mir ausgedacht / eingebildet
    • ich war der einzige, der explizit einen Umbau-Plan durchdacht und ausformuliert hatte
    • die anderen „fanden das irgendwie gut“ — aber auch mühsam
    • Christian hatte vielleicht schon vorher insgeheim aufgegeben, und wollte was Neues bauen (dafür gibt es Hinweise)
    • es wurde die Resonanz durch eine Bewegung gerne aufgegriffen, verschiedene Akteure hatten aber ihre eigenen Interessen
  • Es war tatsächlich eine Gemeinschaft-stiftende Idee, blieb aber abstrakt
    • das Mission-Statement „we want a better Cinelerra“ hat sich auf der Ebene der Plausibilität festgesetzt
    • man hatte dadurch — gefühlt — ein Mandat und konnte loslegen
  • Es war der tatsächliche Plan — und man ist davon weggedriftet
    • der von mir konkret ausformulierte Plan hatte eine katalysierende Wirkung
    • der Plan sieht überraschenderweise tatsächlich (Milestone-2) gewisse Prototyping-Aktivitäten vor
    • der Plan ließ Spielräume offen (was an sich gut ist), und beide Haupt-Akteure haben ihn ein Stück weit in ihrem Sinn verstanden
    • sobald man an der Arbeit war, hat jeder getan, was er gut konnte und (auch nur ein ganz kleines Bisschen) was ihm wichtig war
    • es ist etwas Unerwartetes geschehen, wodurch das Projekt festgefahren ist....?
    • es blieb nur noch die äußere Form übrig, und dummerweise war einer der Akteure (das Reptil nämlich) ein hartnäckiges Arbeitstier

Einsicht: der Plugin-Streit war bereits in den ersten Wochen

...das habe ich in der Erinnerung komplett anders angebunden: mein Bild war, daß das erst Jahre später passiert ist, und sich langsam hochgeschaukelt hat

schon die erste lange Antwortmail ("how to proceed?") erscheint latent-feindselig. Und die zweite, grundsätzliche Mail ist eine Kriegserklärung.

  • war mir das nicht klar damals?
  • warum habe ich plötzlich so scharf reagiert?

Im Rückblick waren dafür die Anzeichen schon viel länger da, aber ich habe sie übersehen, bzw. als Joke abgetan. Hätte Christian gleich von Anfang klar gesagt, daß er sich gegen moderne Methoden definiert, und nur die Imperative Pogrammierung alten Schlages für sinnvoll hält, dann hätte ich mich vermutlich überhaupt nicht näher auf ihn eingelassen, und das gesamte Projekt wäre nie zustandegekommen. Christian aber war locker-eschmeidig, und ich war ebenso stets aufgeschlossen und interessiert und habe ebenso nicht gesagt, daß ich einen solchen Ansatz als "oldschool" komplett ablehnen würde. Hinzu kommt, daß Chistian selbstbewußt auftritt, und ich dagegen sehr stark meine eigenen Schwächen sehe, und daher stets vorsichtig bin und meine Position absichere.

Und das hat mehrere Quellen

  • er war/ist sehr gut vernetzt und wird von seinen Buddies geschätzt und bestätigt
  • er weiß um sein Geschick und die Fähigkeit, extrem pfiffige Lösungen in kurzer Zeit zum Fliegen zu bringen (siehe die Drohnen einige Jahre später)
  • er hatte diese Vision, die ihn über Details hinwegblicken ließ: man fängt halt mal an, und wenn nur genügend Vernetzung da ist, biegt das die Gruppe schon hin. Es ist stets wichtiger, irgendetwas in Bewegung zu setzen

Und zwar in zweierlei möglichen Richtungen (ich erinnere mich jetzt, nach Lektüre der ganzen Dokumente, daß ich das damals bereits so gesehen habe)

  • wenn ich Christian und seine Buddies »machen lasse«, entsteht eine Umgebung, in der ich mein anspruchsvolles Konzept ganz sicher nicht realisieren kann. Das wird ein Kampf gegen Windmühlen, und ich weiß, daß ich kein Power-Coder bin, sondern langsam, vorsichtig und vorbedenklich. Meine Beiträge werden dann alsbald nur noch lächerlich gemacht, und sind es dann auch, weil sie in einer solchen flachen Code-Suppe nicht in den Griff zu bekommen sind.
  • Wenn ich mich dagegen auf das Experiment einlasse, und Christian mit dem Anspruch meines Projektvorschlags zusammenspanne, dann wird daraus eine Quälerei, die schnell in persönliche Vorwürfe ausartet. Selbst wenn ich das durchfechte, wird am Ende Christian lediglich davonlaufen und mir die Schuld geben, und ich habe viel Lebenszeit vertan

Deshalb ist die Beurteilung dieser Kontroverse so schwierig

  • Christian wollte "iregendwie eine Plugin-Architektur" weil das cool ist, und er hielt das für sehr wichtig, und war der Meinung, er bekommt das mit seinem Ansatz schon gebacken. Christian war zu Konzessionen bereit, wie z.B. nur C++ Interfaces, oder dann eben keine Microkernel+Plugin-Architektur. Es ging ihm eigentlich nur um sein Visions-Ding, aber er hat sich damals noch nicht als "reiner C-Entwickler" definiert
  • Ich kannte mich damals noch nicht näher mit C++ aus. Ich sah potentielle Einschränkungen, aber ich wußte weder, wie bedeutsam Plug-ins wirklich werden, noch wußte ich welche Rolle moderne C++ - Features spielen könnten

Bedingt durch diese Diffusität, hat sich das Thema plötzlich in eine Debatte entladen, wurde aber nicht geklärt. Infolgedessen konnte Christian seine Vision nicht realisieren und hat letztlich mit Lumiera gefremdelt, aber jahrelang noch versucht, Elemente seiner Vision doch noch unterzubringen. Und ich habe jahrelang versucht, mit seinen Beiträgen zurecht zu kommen, die aber nicht wirklich mit der Applikation zusammenpassen, die real entstanden ist

8.7.

9.7.

Ersichtlich erst schon in den Mails, aber ganz schlagend in der einzigen Debatte, die ein direktes Gespräch war (auf IRC am 10.7):

  • Christian hat nie zugehört und immer nur auf einzelne Stichpunkte mit technologischen Lösungen oder Dementi reagiert
  • Ich habe Christian so behandelt, als wüßte er nicht, was er will. Ich habe nicht gesehen, warum ihm das so wichtig war
  • Ich habe zwar im Gespräch meine Punkte vorgegracht und versucht festzumachen, doch Christian hat zwar zugestimmt, aber offensichtlich gar nicht zugehört
  • mein Standpunkt war eigentlich: laßt uns das später klären. Ich war nicht gegen plug-Ins und deshalb konnte Christian glauben, meine Argumente widerlegt zu haben
  • es gab sowohl einen »Punktsieg« für Christian, insofern er pro forma sein Konzept akzeptiert bekommen hat. Aber es gab auch eine Nebenabrede, die er (zu mindest pro forma) zugesagt hat

Christian blieb im Projekt, das Projekt lief weiter, und ich habe von seinem Netzwerk profitiert

das eigentliche Projekt, das grade so hoffnungsvoll begonnen hatte, und das für mich so beglückend aussah, war mit einer Explosion untergegangen. Ich hatte gehofft, als einer von Gleichen, aufgrund meiner Fähigkeiten anerkannt zu werden, und gemeinsam etwas zu schaffen. Das war nun nicht mehr möglich; stattdessen mußte ich nun taktieren, lavieren und manipulieren.

Wow Du hast hast mächtig vorgelegtb mit deiner uml/wiki doc.

Wie Du vllt siehst hab ich ausser bisschen wiki kosmetik noch nicht viel
gemacht, ich schreib gerade mal was zum backend was ich hoffentlich
nacher noch einchecken werde. Deine sachen hab ich alle bei mir gemerged
und alles auch in den mob geschoben.

Ich hab noch ein paar fragen/vorschläge:

* Beim wiki mergen hatte ich den ersten conflikt der von hand aufgelöst
werden musste. Beim schreiben von tiddern sollten wir drauf achten das
sie mit einer 'newline' enden, damit das abschliessende <\pre> auf eine
eigene zeile kommt, das sollte wesentlich besser zu mergen sein.

* sollten wir nicht ein gemeinsames UML modell machen, dann kann man
komponenten des andern mitbenutzen und wir sehen gleich wie das mit
mergen der projectfiles klappt. Ausserdem muss man dann die
konfiguration nur einmal pflegen.

* Dein wiki-draft ist klasse (auch wenn ich noch nicht alles blicke,
unfertig), aber wie stellst du dir das vor das man die daten pflegt? Ich
wollte anfangs eigentlich nur 'source' files im git tracken und alles
andere inklusive dokumentation wird dann vom build system gebaut. Jetzt
wo ich dein wiki gesehn hab, bin ich aber überzeugt, das wir ruhig
einige generierte sachen mit versioniern sollten. das problem ist nur,
das so hinzubekommen das es sich immer noch einfach pflegen lässt.

Vorschlag währe das man Bouml nach doc/uml/ generiern lässt
....

(es flogt nur eine Diskussion wohin man Bilder und HTML generiert, und was man in Git eincheckt)

Voßeler Hermann wrote:
Hallo Christian,

gestern hab ich zum ersten mal aus den bisher im UML angelgten Klassen
Code generiert. Dazu bin ich erst mal auf einen eigenen Zweig
"prototype" gegangen, den Du auch auf cinelerra3/ichthyo findest.
Check generierten code bitte nicht ein solange er 'nur' generiert ist,
bzw bouml noch alles parsen kann (hatten wir schon mal drüber geredet).
Ansonsten wollte ich mir das auch mal anschauen. Nebenbei hab ich noch
einige probleme mich mit UML und Bouml anzufreunden.

Natürlich sind da jetzt jede Menge Fragen offen, so z.B. den 
Einrückungsstil betreffend, die Paketstruktur, wie wir die 
Namespaces handhaben etc. Kann man glaub ich alles besser 
anhand von einem konkreten Beispiel diskutieren ��
Sollte im pipapo wiki passieren, damit zumindest Plouj und auch andere
interessierte davon was mitbekommen anstatt privater mails.

(es flogt eine lange Diskussion über Details in meiner Mail...)

Bis dahin war Christian immer sehr ermutigend, fand alles Toll, hat zu allem weitere (sehr sinnvolle) Vorschläge gemacht....

das war mir damals zwar unbewußt aufgefallen,

ich hatte es aber schnell wieder verdrängt

Woher weiß ich das?

Ganz einfach, ich hatte diese gesammte Mail-Kommunikation komplett und restlos vergessen; ich hatte vielmehr die Vorstellung, der Streit über Plug-Ins sei erst ein Jahr später ausgebrochen, als wir schon im Lumiera-Projket waren, und wir hätten dann den Streit "ausgeräumt" bei meinem ersten Besuch in Karlsruhe.

Nun sehe ich diese Mails, und mir kommt sofort das Lebensgefühl von damals wieder, und ich kann mich an einzelne Details wieder erinnern, sogar was ich beim Schreiben einzelner Zeilen gedacht habe


    
Vorhin hab ich gesehen, daß Du auch grade die ersten Schritte
in UML gemacht hast; bin schon gespannt...
bin am fluchen, verdammt lange her das ich UML das letzte mal angeschaut
hab, das war 1.0 und da hat sich einiges getan, ausserdem fehlen mir
oder bouml einfach ein paar sachen um bestimmte dinge zu modelliern.

	Christian

Er hat auf einem separaten 'prototype' branch anscheinend damit schon einen UML-generierten Satz an Klassen gebaut. Von diesem Prototype-Branch gibt es keine Spur mehr (wichtig! denn das zeigt, daß bereits vor Ende Juni mit Experimenten zur Klassengenerierung begonnen wurde)

Ichthyostega wrote:
So, could you please have a look at this prototype buildsystem? I pushed out to cinelerra3/ichthyo
 #scons     - just the buildsystem
 #prototype - contains the same code plus the files of my experimental/prototype branch to compile
morning, trying it out now ��, next i'll write some DesignProcess
proposals about C nameing rules, plugins and interfaces. (i am back ;))

note about gnu style: (how I/emacs interpret it)

....

ganz klar eine Anspielung an "Terminator"

... wir hatten uns eingehend über die Filme unterhalten

After a talk on IRC ichthyo and me agreed on making lumiera a multi language project where each part can be written in the language which will fit it best. Language purists might disagree on such a mix, but I believe the benefits outweigh the drawbacks.

ct

2007-07-03 05:51:06

angeblich hätte er sich mir mir auf ein Multi-Language-Projekt geeinigt

ist das möglich oder eine dreiste Lüge?

Der 3.7 war ein Sonntag, das heißt, ich mußte am nächsten Tag ins Büro, bin jedoch in diesen Jahren in der Regel erst Mittags dort gewesen. Es war sehr typisch für mich, in der Nacht Sonntag ⟶ Montag noch (zu) lange wach zu sein...

Also eindeutig mit Smily, und ich habe entsprechend witzelnd (und wie ich denke, deutlich) geantwortet; Christian hat daraufhin sofort einen Rückzieher gemacht. Mir erscheint es allerings plausibel, daß eine solche Diskussion in einer früheren, lockereren Phase stattfand, nicht in diesem bereits angespannten Moment....

Beachte dazu auch folgendes: der erste (wichtigere) RfC »All Plugin Interfaces are C« ist datiert auf 26.9. Nur dieser erste Kommentar trägt den Timestamp 3.7. — außerdem steht dort im Pro/Contra-Teil unter "Alternatives"

  • Just only use C++

  • Maybe SWIG?

  • Implement lumiera in C instead C++

Das würde dazu passen, daß Christian vorher schon mal vorgefühlt hat

Und zwar in mehrerlei Hinsicht. Zum einen, Christian hat den wichtigeren grundsätzlichen RfC gar nicht erwähnt! (Ich hatte ihn wahrscheinlich trotzdem schon bemerkt, ich war und bin in entscheidenden Phasen immer sehr aufmerksam). Er hat im Mail-Austausch an eben jenem Morgen geschrieben:

morning, trying it out now ��, next i'll write some DesignProcess
proposals about C nameing rules, plugins and interfaces. (i am back ;))

Natürlich könnte diese Mail für mich der Anlaß gewesen sein, nochmal eben in den IRC zu schauen (es war 3 Uhr früh) und dann mit Christian locker zu chatten. Warum hätte ich dann aber die Diskussion über den GNU-Stil per Mail weitergeführt? Vielleicht, damit Plouj das auch mitbekommt? Wäre nicht meine Art gewesen (typischerweise habe ich wichtige Diskusionen explizit in einer Mail an alle zusammengefaßt). Und es wäre total unplausibel, daß ich in einem Thread über GNU-Stil mich ausbreite, aber das Thema »Multi-Language« überhaupt nicht erwähne, obwohl es ein Punkt in einer dazwischenliegenden IRC-Diskusion gewesen war.

Was aber überhaupt sehr gegen einen möglichen mündlichen Beschluß am frühen morgen auf IRC spricht: am nächsten Abend gehe ich auf das gesamte Thema mit einer eMail ein, und erwähne explizit die Einschränkungen durch plain-C als Bullet-point, mit einer grundsätzlich reservierten bis ablehnenden Haltung. Ebenso spricht dagegen, daß ich mir nur drei IRC-Logs aufgehoben habe, von denen das erste am 8.7. zwischen Herman Robak und Christian stattfand (aus den IRC-Logs geht auch hervor, daß ich zu der Zeit massiv unter Zeitdruck stand und grade ein Orgel-Tonaufnahme-Projekt lief)

ich habe C-Funktionen stets nur als eine Konzession betrachtet

Denn ich war bereits in den 90er Jahren ziemlich ablehnend gegenüber C (nicht C++). Ich hab die C-Kultur als eine Kultur der Schlaumeier und Taschenspieler betrachtet. Wenn ich also vor diesem Hintergrund nun sage, "es macht nicht Sinn, alles zwingend in Objekte zu packen", dann war das von mir als Ausdruck von Offenheit und Konzillianz gemeint. Ich wollte ausdrücken, daß ich kein OO-Fanatiker bin. Ich weiß auch sehr genau, daß meine Vorstellung ehr dain ging, daß man zwar ein *.cpp-File hat, in diesem aber nur Funktionen schreibt, die imperativ mit For-Schleifen etc. implementiert sind. Genau deshalb hat mich dann auch Christian's Versuch, reines C zu etablieren (später, wie es um main() und das Start-up ging), ziemlich empört. Ich fühlte mich ausgenutzt und betrogen, denn in meinem Verständnis hatte ich eben nur konzediert, daß man auch rein imperativen Code schreiben kann.

Nach dieser Leseart hätte es also zu dem Zeitpunkt keine entsprechende Diskussion auf IRC gegeben, aber Christian hat sich daran erinnert, daß ich einige Woche früher mal gesagt habe, es müsse ja nicht alles in Klassen gepackt werden, und für reine Video-Berechnung würde auch C gehen. Er hat das dann als Zustimmung aufgefaßt, und wollte jetzt ehr versuchen, das Gewicht insgesamt Richtung C zu verschieben, weil er sich eigentlich erhofft hatte, ein reines C-Projekt starten zu können, in das auch seine ganzen C-Tools gut reinpassen. Diese Lesart halte ich im Moment für die plausibelste Deutung. Insofern war es keine Lüge, sondern nur ein Manipulationsversuch, der mir zu dem Zeitpunkt sogar entgangen ist

Btw: seen my Interface / CStyleGuide proposal in the pipapo wiki? please
review it carefully if it looks ok, I straight go into make a referene
implementation, since this is a very low level building block.

	Christian

"review it carefully if it looks ok, I straight go into make a referene implementation...."

Ich lese das zwischen den Zeilen so

  • bitte segnet mir das möglichst gestern so ab wie es ist
  • schaut besser gar nicht so genau hin, ich scharre bereits mit den Hufen
  • macht das Faß bloß nicht später noch mal auf!!!!

"since this is a very low level building block."

Normalerweise verwendet Christian viel "very important". Hier verwendet er "very lowlevel", das klingt, als würde er die Sache herunterpspielen wollen. Hier könnte das noch ein Zufall sein, aber in dem nachfolgenden, sehr hitzingen Streit verwendet er exakt diesen Ansatz als Hauptverteidungunslinie (Ist ja nur ein Experiment, ist ja nur optional, ich will halt bloß keine Möglichkeiten abschneiden, wir können das alles noch diskutieren, es gilt ja nicht für das C++ Zeug)

Am Dienstag, den 03.07.2007, 20:51 +0200 schrieb Christian Thaeter:

Btw: seen my Interface / CStyleGuide proposal in the pipapo wiki? please
review it carefully if it looks ok, I straight go into make a referene
implementation, since this is a very low level building block.

yes, I have seen it this morning. I have to look at it more carefull
tonight, when at home. Basically, it looks OK for all external
interfaces, i.e. interfaces other external apps or components will use
to call to cinelerra or to embed within cinelerra. Good examples for 
this type of things are LADSPA plugins or some video codec interface

For use /within/ the application we should consider the following
questions first:
* how much "plugin architecture" do we want? Does a "micro
  kernel/plugin" aproach help? how then do we handle extension points?
* why should we constrain ourselfs to just C linkage? For example for
  the effects plugins it's just natural to require each plugin to define
  a GUI object as well and then proxy the communication (Cinelerra2
  basically does the same, just does it create two instances of each
  plugin class, one for the processing and one for the gui). Same for
  exceptions: they wouldn't be used so commonly if they weren't 
  helpfull ��. On the other hand: for a data storage plugin/interface
  I don't see much use in using classes at all because this is
  procedural by nature. (Thats the point where people start intventing
  all those silly singleton classes...)
* the internal interfaces shouldn't be fixed this much, because this
  hinders refacturing. Well, at least until we reach beta 0.98 ��

Hermann


effektiv ist das eine freundlich verpackte Ablehnung

hier sage ich explizit, und mir Argument,

daß ich C als Einschränkung empfinde

  • als ein Wink mit dem Zaunpfahl („Junge, ich sehe was Du vorhast“)
  • zugleich als Einladung für eine fachliche Debatte mit Argumenten
  • ich mache hier explizit die Tür noch nicht zu, mache aber klar, daß ich seinen Ansatz nicht einfach durchwinken werde

Das meine ich als echte Frage, und die ist wichtig, zum Verständnis dessen, was dann geschah.

  • ich war damals bereits seit mindestens 8 Jahren in einem Business-Kontext tätig und hatte jeden Tag diplomatischen E-Mail-Verkehr zu führen
  • ich ging aber naiver Weise davon aus, daß Christian (da er in meinem Alter ist), so etwas versteht
  • aus späteren Erfahrungen weiß ich inzwischen, daß viele Leute diese Sprachebene nur generell als bedrohlich empfinden, aber nicht verstehen

Christian antwortet am selben Abend völlig naiv und macht klar was er will

Voßeler Hermann wrote:
Am Dienstag, den 03.07.2007, 20:51 +0200 schrieb Christian Thaeter:

Btw: seen my Interface / CStyleGuide proposal in the pipapo wiki? please
review it carefully if it looks ok, I straight go into make a referene
implementation, since this is a very low level building block.

yes, I have seen it this morning. I have to look at it more carefull
tonight, when at home. Basically, it looks OK for all external
interfaces, i.e. interfaces other external apps or components will use
to call to cinelerra or to embed within cinelerra. Good examples for 
this type of things are LADSPA plugins or some video codec interface

For use /within/ the application we should consider the following
questions first:
* how much "plugin architecture" do we want? Does a "micro
  kernel/plugin" aproach help? how then do we handle extension points?
imo as much as possible, I stated that the cinelerra main app should be
only a skeleton using plugins. (do we want to start more monolithic and
then factor plugins out, or plugins from start on?)

Extensions shall be considered when designing this interfaces. I also
considered to make the plugin interface extensible without breaking
compatibility (naturally it will turn out whats needed during
development of new features)

consider my favorite example
 (based on cin2, cin3 might be little diffrent):
Cinelerra has tracks on the timeline, these tracks shall be plugins (we
provide plugins for audio and video tracks, but someone might provide
tracks for midi, 3D animation or such)

Tracks have some gui components
 1. patchbay
 2. timline drawing (thumbs for video, waveform for audio, notes for
midi, ...)

some internal components:
 1. list of clips on the track
 2. attached effects container

(and some more)

we now need to define/collect what interfaces are needed to implement
tracks. There will be not a single interface but a group of related
interfaces which in sum define the behaviour and gui rendering of a track.
 * cinelerra_track_gui_patchbay_interface
  defines the patchbay gui
 * cinelerra_track_gui_timeline_interface
  how timeline is rendered
 * cinelerra_track_effects_container_interface
  manage effects on the track
 * cinelerra_track_clips_interface
  manages clips (add/remove, order, ...)


these 'tracks' use in turn other interfaces we define, effects, codecs, ...


for effects plugins this is quite similar, we need at least a interface
for the gui component and one for the internal workings.


* why should we constrain ourselfs to just C linkage? For example for
  the effects plugins it's just natural to require each plugin to define
  a GUI object as well and then proxy the communication (Cinelerra2
  basically does the same, just does it create two instances of each
  plugin class, one for the processing and one for the gui). Same for
  exceptions: they wouldn't be used so commonly if they weren't 
  helpfull ��. On the other hand: for a data storage plugin/interface
  I don't see much use in using classes at all because this is
  procedural by nature. (Thats the point where people start intventing
  all those silly singleton classes...)
The Idea is here to make it possible to write plugins in any other
language, C bindings are the best thing to make this possible. The
downside is that glueing C++ is not effortless. I think it's still worth
it, but this is just a proposal.

* the internal interfaces shouldn't be fixed this much, because this
  hinders refacturing. Well, at least until we reach beta 0.98 ��
yes, my proposal only applies to 'external' interfaces. Unfortunally many
interfaces are considered external by this plugin architecture (whatever
we provide for us, we provide for people extenting it too)

Das geht mir jetzt so, und das ging mir vermutlich damals nicht anders.

Wenn ich darauf eingehen müßte: ich wüßte nicht, wo ich anfangen soll mit dem Argumentieren...

Und damit meine ich eine Applikation, nicht OS-level Code.

  • Man hat ein GUI-Framework, mit Widgets. Die muß man beim Start gemäß ganz bestimmten Regeln einhängen, danch gibt es nur noch Event-Handling
  • In einem Track liegt eine Datenstruktur, die mit der Engine/Core und (bei naiver Herangehensweise) auch mit dem GUI geteilt wird. Wie soll man das "mal eben" in ein Plugin packen? Und dann sollen andere Leute im Stande sein, das durch ihre Lösung zu ersetzen???

Er skizziert ja, wie er sich das vorstellt:

we now need to define/collect what interfaces are needed to implement tracks.

Das ist das gefürchtete "dann kann man" ... und andere Leute sollen sich gefälligst mal den Arsch aufreißen, ich hab euch jetzt das Prinzip gezeigt.

Chistians Vorschlag operiert auf einer formal-strukturellen Ebene: er definiert ein Schema der Machbarkeit, und da dieses offensichtlich aufgeht, ist er zufrieden und hält das für eine gute Idee — alles Weitere sind die "lots of details are still in progress to be worked out" wie er typischerweise schreibt. Dazu kommt bei Christian stets dieses Bild von der Community, die letztlich entscheidet, wohin es gehen soll, und insofern kommt es erst mal nur darauf an, etwas angefangen zu haben, was unfertig genug ist, damit andere Leute da einsteigen können. (Achtung: mir erscheint diese Haltung komplett unplausibel — und deshalb muß ich sehr vorsichtig sein, Christian nicht falsch zu „lesen“: er meint das Ernst und sieht seinen Beitrag als einen ordentlichen Schritt in die richtige Richtung)

Das macht diesen Vorschlag so »entwaffnend«:

Wenn Du gesehen hast, wie eine Code-Basis degeneriert und kaum noch zu handhaben ist, dann stellst Du einen Bezug her zu gefährlichen Methoden und Ansätzen. Das läßt sich aber niemals belegen. So jemand wie Christian kann das immer einfach vom Tisch wischen, und behaupten, das läge nur an XYZ (zum Beispiel, weil man C++ verwendet hat).

Wenn man einen Anfänger hat, der mit solchen Ideen um die Ecke gebogen kommt, dann sagt man (wenn man Zeit hat und freundlich ist): "setzt Dich mal hin und mach das so, und wir schauen uns das Ergebnis zusammen an...."  Dann läßt man den Junior rudern, bis er völlig verzweifelt ist, und reitet ihn immer tiefer rein. Und dann zeigt man ihm, wo er falsch abgebogen ist.

Aber das Problem ist, ein 40-jähriger Mann mit robustem Selbstbewußtsein, der sich selbst als "Coder" definiert, wird sich niemals auf ein solches Setting einlassen. Das war mir klar (und leider ließ sich nicht vermeiden, daß wir diese unapetittliche Erfahrung dann später auch noch konkret durchbuchstabieren mußten, als es um Thread-Wrapper, Lock-Checker und den MPool ging)

Fazit: ich stecke nun bis über die Ohren in der Scheiße

Er findet Macros gut, ist stolz auf sein Programmierschema mit "goto" und besteht darauf, daß man auf einem gemeinsamen Datenmodell arbeiten muß, weil was anderes mit C ja nicht geht. (Die beiden letztgenannten Punkte ergänze ich aus der Erinnerung, sie gehen nicht aus den Mails hervor. Es könnte auch sein, daß Christian diese Punkte erst später ausgeführt hat — aber es war zu diesem Zeitpunkt mit wünschenswerter Klarheit deutlich, daß er alle die Argumente gegen das imperative Programmieren entweder nicht kennt, oder nicht gelten läßt.

C++ Klassen sind immer "fett", C-Interfaces sind immer schlank und klar. Abstraktionen und Interfaces werden mit C++ assoziiert und als kompliziert und schwierig dargestellt

Wenn man diese Mail unvermittelt liest, kann man nur den Kopf schütteln. Ich greife mir einen Widerspruch in Christians Aussagen heraus, und leite daraus die Forderung nach Entscheidung ab; aber diese Entscheidung treffe ich sofort selber, auf argumentativer Ebene: Etwa so: Es gibt bei diesem Thema hier nur einen Ansatz, der nicht Unfug ist, und der ist sehr aufwendig und komplex. Spielraum für Abwägungen und Auslegungen lasse ich keinen.

In der Sache ist das tatsächlch zutreffend, aber weder gibt es einen allgemeinen Konsens in der Richtung (damals noch viel weniger als heute), noch ist es angemessen, ein solches Thema der Haltung und Präferenzen derart kagetorial zu behandeln....

Im Rückblick deute ich das wie folgt

  • tatsächlich sah ich dahinter eine Haltung, die mir zuwiderläuft, die ich aber nicht argumentativ zu fassen bekomme
  • ich wollte eine sich abzeichnende Tendenz mit einem Überraschungs-Schlag abschneiden, weil ich sonst keine Möglichkeit sah, das grade noch so hoffnungsvoll begonnene Projekt fortzusetzen

Das bedeutet: mein Handeln war aus meiner Sicht ein Befreiungsschlag, und zielte darauf, das ansonsten Unvermeidliche doch noch überraschend wenden zu können: daß ich nämlich das Projekt aufgeben muß, mit dem ich mich grade eben sehr stark identifiziert hatte (denn in 2007 gabe es mehrere dieser atmosphärischen Umschläge, auch in meinem eigenen Leben; zudem tauchte ein sehr ähnlich gelagerter Konflikt bereits sehr bedrohlich mit meinen Kollegen und meinem Chef auf)

Christian war einfach nicht zu stoppen. Er labert und labert und labert und hört nicht zu. Er hat mich auch etwas von oben herab behandelt, in dem Sinn "hehe, der Typ mit Java und der Bank, die arbeiten doch nur mit bloatware, also sind seine Urteile mit Vorsicht zu genießen..."

Dazu kam, daß Cehristian immer im IRC war und viel mit dutzenden Leuten geredet hat, während ich under Mehrfachbelastung stand, und auch generell nicht arbeiten konnte, während ich ein IRC-Log beobachte; Christian konnte das sehr gut, aber er hat nicht genau gelesen und selten wirklich mitgedacht, sondern nur auf Stichworte reagiert. Deshalb: ich mußte mir Gehör verschaffen.

Mein Argument war differenziert, Christian hatte eigentlich gar kein Argument, sondern eine Überzeugung.

Es könnte sein, daß Christian verstanden hat, daß ich angreife, daß er aber meiner Argumentation überhaupt nicht folgen kann, weil er gedanklich „anders abbiegt“ (z.B. weil er gewisse Argumentationsschritte von mir nicht versteht, weil ihm der Kontext fehlt, und er sie dann als „unverständlich“ abtut, und meinen Gedankengang anders „interpoliert“). Er macht in dieser Diskussion Statements, die man eigentlich gar nicht mehr so machen kann, wenn man auch nur einen Teil der Disussion der vorausgegangenen 10 Jahre zum Thema Architektur und Methoden bewußt gelesen und nachvollzogen hat. Diese Beobachtung ist mir damals komplett entgangen.

  • Du hast das in die falsche Kehle bekommen
  • Du unterstellst mir etwas, was ich niemals wollte (was eine dreiste Lüge ist)
  • geziehltes opportunistisches Mißverstehen
  • behaupten, das Argument des Gegeners wäre nicht schlüssig
  • ein Argument des Gegners konntern mit "kannst Du das mal erklären?"

Ich deutet das so, daß ich durch meinen Angriff einer Vision den Boden entzogen habe, die tatsächlich für Christian sehr bedeutend war. Im Rückblick gibt es dutzende Belege dafür in den Dokumenten. Mir war das damals jedoch nicht klar, und ich dachte, es würde helfen, das Thema nachzuschärfen; meine vorgetragene Problemanalyse war ja weithin bekannt und diskutiert worden in den vorausgegangenen Jahren. Vermutlich hatte ich sogar damit gerechnet, daß sich eine Diskussion über eine Plugin-Architektur besser führen läßt, als eine Diskussion um die grundlegende Haltung zum Programmieren.

Bei der Lektüre jetzt bekomme ich den Eindruck, daß das passiert, weil ich vor der Konsequenz ausgewichen bin, den dahinter liegenden Grundsatz-Streit direkt auszutragen. Ich habe mich stattdessen auf die Frage nach einer Plugin-Architektur konzentriert, was Christian damit gekonntert hat, daß er das ja gar nicht will — wobei jede seiner sachlichen Ausführungen dem explizit widerspricht. Dadurch war ich durch ein Double-Bind gefesselt (und das war nicht das erste Mal, daß mir das in meinem Leben passiert ist).

In den folgenden Jahren bin ich, in vielen ähnlich gelagerten Debatten-Situationen (vor allem in meiner Tätigkeit bei der Bank) graduell zu der Einsicht gekommen, daß es darauf ankommt, wer als erster die gemeinen Tricks anwendet, sobald eine Debattensituation eigentlich entschieden ist, aber keiner der Gegner aufgeben möchte.

Auf die Situation hier übertragen, würde dieser Ansatz etwa so funktionieren: (Christian): "Aber das will ich ja gar nicht" (Ich): OK, dann war dieses Proposal ein Irrtum und wir können es in aller Form verwerfen. Um es gleich in aller Form festzustellen, eine Plugin-Architektur geht nur »ganz oder gar nicht«, jede Lösung darunter ist gefährlich, und wir schließen das daher explzit aus. Plugins für einzelne Fälle werden wir später brauchen, und wir vertagen die gesamte Techologie bis auf diesen späteren Zeitpunkt" (ich weiß aus praktischer Erfahrung, daß man leider einen solchen Satz in einer persölichen Diskussion dem Gegenüber ins Gesicht brüllen muß, sonst gibt er nicht auf). Mit hoher Wahrscheinlichkeit ist eine konstruktive Zusammenarbeit danach aber nicht mehr möglich

Ich bin jetzt erstaunt, welche bedeutende, und auch besonnene Rolle dieser Mann in den ersten Wochen gespielt hat. Das war mir komplett entfallen.

Sein Vorschlag

  • Christian entwickelt tatsächlich mal seinen Plugin-Loader fertig und implementiert einige Beispiel-Plugins
  • Hermann baut eine Struktur mit Modulen + Abstraktionen
  • dann versucht man, diese mit Christian's System in Plug-ins zu verwandeln

Und (daran erinnere ich mich jetzt sogar wieder) das habe ich nicht als Taktik oder Heimtücke empfunden, denn ich hatte doch meine Argumente in der grundsätzlichen Mail komplett offen dargelegt; diesen Argumenten zufolge wird dieses Experiment vorhersagbar scheitern.

Sehr aufschlußreich wie Christian pariert: das wäre Zeitverschwendung. Er will die Grundsatz-Entscheidung jetzt (zu dem Zeitpunkt, wo wir, in gemeinsamen Verständnis, tatsächlich zu coden anfangen)

Christian will das System jetzt öffnen und die Entscheidung darüber fixieren

11.7 : Christian erklärt den Plugin-RfC einseitig für anerkannt

after a talk on irc, we decided to do it this way, further work will be documented in the repository (tiddlywiki/source)

ct

2007-07-11 13:10:07

Der Grund ist aus den IRC-Logs ersichtlich: Christian und ich hatten uns wiederholt auf IRC nicht getroffen (ich war extrem mit Arbeit überlastet in der Zeit, Orgel + Cin-2 + Baaderbank). Ich hatte am 10.7. eine Diskussion mit Plouj + Hermanr (aber Christian schlief um die Zeit). Plouj hat das IRC-Log an Christian weitergeleitet, der es dann selektiv in seiner Mail gequotet hat, aber nicht auf meine Argumente eingegangen ist

Chistian nimmt Auszüge aus IRC, gequotet in der Mail, und setzt darunter jeweils ein Statement, das das Gegenteil von dem argumentativen Stand einfach affirmiert.

Die Diskussion scheint allerdings in einem versönlichen Tonfall zu enden — ohne jedoch den Dissens auszuräumen

Das Log dazu habe ich aufgehoben in meinem privaten Trac.

Daraus geht auch klar hervor: das war die einzige Debatte mit Christian auf IRC. Somint ist das Bild vollständig.

Er hat mich selten überhaupt ausreden lassen. Er hat permanennt auf einzelne Stichworte reaagiert, und diese nach seiner Sicht »entkräftet«:

  • Gegenargument-A : ich will das ja gar nicht was Du mir unterstellst, aber wenn wir alles in Plugins verwandeln und jeder alles aufrufen kann, dann wird das System so toll
  • Gegenargument-B : aber das ist doch eine Trivialität, die kann man technisch lösen. Ich bau Dir das alles ein, nur mußt Du jetzt erst mal meiner Vision zustimmen

Nicht wirklich, aber im Zusammenhang war das der Eindruck

  • es gibt nie und nirgends ein Statement von Christian, in dem er einen Kompromiß bestätigt oder in seinen Worten affirmiert
  • aber er sagt permanennt, er wolle das ja alles gar nicht, und wir können das immer noch entscheiden
  • er schlägt stets technologische Lösungen vor gegen meine grundsätzlichen Einwände. Er verspricht dann sogar, das zu bauen (was er nie getan hat)

Christian hat wohl geglaubt, mit ein paar mündlichen Zusagen hat der diesen ängstlichen Typen jetzt ruhiggestellt, und sein Ding ist durch. Und es ging ihm vermutlich darum, Code vorlegen zu dürfen. Er glaubte wohl, wenn man erst mal seinen Code sieht, dann wären alle Mißverständnisse ausgeräumt, und es würden dann alsbald die Wunder geschehen, von denen er überzeugt war; daher war vermutlich das sein einziges Ziel, und deshalb war er auch mündlich bereit, so viele Zugeständnisse wie möglich zu machen, bis zu dem Punkt, daß er sich selber komplett widerspricht. Er dachte vermutlich, er muß dieses Ding jetzt nur reinbekommen, und alles wird gut, alles weitere wird sich dann schon von selber zeigen, Leute werden Plugins in Massen schreiben, die Creativität pur bricht aus, und dieser komische Bank-Mensch, wer redet dann noch über den...?

ich kann mich jetzt wieder erinnern,

das als eine Niederlage empfunden zu haben.

Ich hab mich elend gefühlt.

Im Rückblick halte ich es für wahrscheinlich, daß ich dieses ziemlich dreiste Vorgehen von Christian nicht sofort und auch (bis jetzt nicht) in vollem Umfang realisiert habe. Den Status des RfC als "final" habe ich natürlich irgendwann gesehen, möglicherweise hat mich Christian sogar darauf hingewiesen. Und ich habe dann wohl geschluckt, und gemerkt, daß mir nach all dem Streit jetzt praktisch nichts anderes übrig bleibt, als so zu tun, als wäre das belanglos

im Rückblick nicht klar: warum habe ich den Kompromiß nicht explizit klargestellt?

Ich hätte sofort eine Mail rumschicken können, in dem ich den Kompromiß aus meiner Sicht zusammenfasse, und die von Christian zugesagten Punkte festnagle.

...nachdem Christian den RfC unqualifiziert für angenommen erklärt hat, hätte ich entweder ihm gegenüber daruf bestehen können, daß er die Einschränkungen im Text aufführt (das wäre ein direkter Affront und Chistian würde dann weiter versuchen, zu manipulieren) oder ich hätte den RfC einfach editieren können, die Einschränkungen im Text vermerken, und das mit einem Kommentar bestätigen "ich habe diesem Vorschlag nur unter den genannten Bedingungen zugestimmt)

Das ist ausschließlich Spekulation bzw. Interpolation.

Meine Erinnerung hat sich überlagert mit den späteren Erfahrungen

...allein schon von dem Umstand, daß wir nun plötzlich diese Diskussion haben; möglicherweise war ich noch voller Begeisterung und Drive und habe das Verhältnis zu Christian als kollegial und wohlgesonnen empfunden (was es bis vor kurzem war). Dieser Hypothese zufolge habe ich den Streit aus Notwehr vom Zaun gebrochen und war bloß froh, daß der Tonfall am Ende versöhnlich war, und es weiterging. Ich wollte zu dem angenehmen Zustand zurück

Dieser Hypothese zufolge wäre es mir unangenehm gewesen, daß dieser Streit plötzlich so eskaliert ist. Ich wäre dann bloß froh gewesen, daß es nochmal "mit einem blauen Auge" abgegagen ist und kein Bruch daraus entstanden ist. Ich wäre froh gewesen, daß das Thema jetzt vom Tisch ist und wir uns mündlich auf einen vernünftigen Kompromiß geeinigt haben. Es hätte mich dann zwar gewurmt, daß Christian den RfC unqualifiziert als "angenommen" markiert, aber ich hätte dann gegen mich selber argumentiert, das solle man mal nicht zu wörtlich nehmen und darauf herumreiten, eigentlich hat er ja nix falsches gesagt. Insgesamt hätte ich damit geglaubt, wir hätten einen Dissens gehabt, und uns dann "unter Männern" geeinigt, und beide würden sich selbstverständlich an die Absprache halten.

Für diese Hypothese habe ich keinerlei Anhaltspunkt, sie wäre aber durchaus plausibel, gemessen an meinem allgemeinen Verhalten: wenn mir Wertschätzung entgegengebracht wird und wenn ersichtlich auf meinen Beitrag geachtet wird, dann erzeugt das bei mir ein starkes Verpflichtungsgefühl und eine starke emotionale Bindung (weil es mir in meinem Leben bis damels ehr selten zuteil geworden ist, jenseits der Familie). Demnach wäre mir emotional vor allem daran gelegen gewesen, den angenehmen Zustand aufrecht zu erhalten, und ich war froh, den (notwendigerweise gestarteteten) Konfilkt einigermaßen gut wieder losgeworden zu sein. (dafür würde auch sprechen, daß ich grade zu der Zeit von mehreren anderen Seiten erheblich unter Druck stand)

Dieser Hypothese zufolge hätte ich die Situation strategisch eingeschätzt, und mich auf mein professionelles Urteil verlassen, dem zufolge die Versprechungen von Christian zwangsläufig an der Wirklichkeit scheitern würden. Es wäre mir demnach lediglich darum gegeangen, genügend »Bremse« gegeben zu haben, so daß Christian nicht unmittelbar loslegt und ein Kettensägenmassaker anrichtet. Für den Rest hätte ich mich darauf verlassen, daß er nicht viel zustande bringen wird und daß ich eventuelle Restprobleme später schon noch abgebogen bekomme, z.B. indem ich dann im Einzelfall eben ekelhaft auf Qualitätsmerkmalen bestehe (die er mit seinem Plan niemals wird erfüllen können). Ich habe keinerlei Anhaltspunkt ob diese Hypothese irgendwie gerechtfertigt ist, und ich damals so gedacht haben könnte; jedenfalls im Rückblick wäre das eine realistische Einschätzung gewesen. Es ist ja exakt so gekommen, wie ich in meinem Einwand vorhergesagt habe: es werden immer Wunder versprochen, aber praktisch bleiben diese Art "leichtgewichtigen" Plug-ins praktisch bedeutungslos.

Dieser Hypothese zufolge hatte ich mich plötzlich in einem spannenden Projekt gefunden (was ich als Glücksfall betrachte) und hatte schon Ideen, mit denen ich mich identifiziere und die ich realisieren möchte. Daher wollte ich bloß diese sich plötzlich auftuhenden Hindernisse aus dem Weg haben, und nachdem das "nach Bauchgefühl" der Fall war, war mir alles egal, solange ich mit dem Zeug weiter machen konnte, was ich wollte. Dieser Hypothese zufolge hätte ich mich rein opportunistisch verhalten (interesssanterweise exakt genauso wie Christian, wenn man eine ähnlich gelagerte Leseart anwendet, was heißen würde, wir waren uns insgeheim einig) und hätte keinerlei Bewußtsein dafür gehabt, was eine solche Haltung für die Zukunft bedeutet. Mein Verhalten im nächsten halben Jahr würde ziemlich gut zu dieser Hypothese passen

dieser Hypothese zufolge wäre ich zu der Einschätzung gelangt, daß es unmöglich ist, im Augenblick mehr zu bekommen, als ich konkret hatte: nämlich die Möglichkeit, ungestört weiterzumachen plus eine mündliche Zusage von Christian, daß er jetzt nicht durchmarschiert und sich an die Einschränkungen hält. Im Besonderen wäre meine Einschätzung dieser Hypothese zufolge gewesen, daß Christian ein »Festnageln« als ungeheuerlichen Affront versteht, und mich entweder sofort vor die Tür setzt (er hatte die technische Hoheit über die ganze Infrastruktur), oder aber sich dann der Streit endlos fortsetzt, mit dem Ergebnis, daß wir anschließend auseinandergehen, und das schöne Projekt gestorben wäre, bevor es begonnen hat

ich kann jetzt den Zusammenhang gedanklich fassen

Einige erfahrene Männer, die allesamt ihr Handwerk verstehen, gehen mit einem gemeinsamen Werk vorran und leiten eine Bewegung ein.

allerdings nie irgend etwas zum DataBackend

er hat sich dann zu 150% auf das uWiki geworfen

und zwar passiert hier bereits der Streit über die Plug-in-zentrische Architektur

Ich hatte mir gemerkt, daß wir auf der Terrasse in Karlsruhe saßen, und das Thema angesprochen haben. Das war aber mindestens ein Jahr später. Außerdem habe ich mir gemerkt, daß es irgendwie mit dem Thema Application start-Up zusammenhing. Das wäre sogar zwei Jahre später.....

Wie sich nun eindeutig aus den Quellen ergibt, sind alle diese Erinnerungen zwar korrekt, ich habe aber die falschen Schlüsse gezogen. Der eigentliche Streit ist sofort ausgebrochen, nachdem wir begonnen haben, ernsthaft zusammezuarbeiten. Das ist auch plausibel so. Die späteren Erinnerungen hängen damit zusammen, daß wir wohl beide (Christian und ich) dem Streit ausgewichen sind, weil wir das Bauchgefühl hatten, daß er nicht lösbar ist. Auch diese Einschätzung ist wohl korrekt, es stehen Fragen der Weltanschauung dahinter.

....und hab auch tatsächlich Fortschritte gemacht, wenngleich die auch komplett im Mißverhältnis standen zu der Absicht, die ursprünglich hinter diesem Prototypen stand:

  • eine funktionale Skizze für eine Render-Engine zustande zu bekommen
  • beispielhaft einen neuen Coding- und Kollaborationsstil zu etablieren
  • dieses dann zu verwenden, um den Umbau von Cinelerra anzugehen
mark carter schrieb:
Maybe a lot of my problem is that I'm new to the code. Coming in from
fresh on such a big project is bound to be daunting,....
well, but my personal experience was that, contrary to other projects,
things get worse when you get accustomed. Trying to work with the
current codebase is just plain frustrating, you find yourself
fighting against windmills all the time.

Looking at the codebase, I realise that there's quite a lot of it, and
think that a ground-up rewrite is likely to be doomed. I think that we
must find a way of remoulding the existing code into a more stable form.
This indeed /is/ a big problem.
At the moment I just choose to ignore it and picked me one region, namely
the render engine / render pipeline and rewrite that one. Christian concentrates
on some aspects of file handling media loading.
At the moment I just choose to ignore it and picked me one region, ...

gemeint war, Lumiera ist ein Projekt, das auch heroischen Einsatz einfach spurlos aufsaugt, wie ein trockener Schwamm

»Weihnachten« ist insofern wichtig, als ich im Sommer das Gefühl hatte, bis Weinachten zusammen mit Christian eine laufähige Renderengine „auf die Beine stellen“ zu können. Ab August wurde die Arbeit bei mir „zäh“, jeodoch habe ich diese zeitliche Vorstellung immer noch irgendwie im Kopf gehabt, Stichwort »hart arbeiten«. Das war das Muster: wenn ich wirklich alle Kraft und Entschlossenheit einsetze, dann kann ich (toller Hecht) das doch nageln. Das war in früheren Projekten, seit meiner Studienzeit, immer wieder die Situation gewesen, und es war dann auch jeweils (mit gewissen Einschränkungen) erfolgreich; dieses Muster war Teil meines Selbstverständnisses, damals.

Insofern gibt der »Visitor« einen wichtigen Hinweis: der sollte ja ultimativ das Grundgerüst für den Builder sein — ein Thema, an dem ich laut offizieller Verlautbarung den ganzen Herbst gearbeitet habe. Tatsächlich hatte ich nur Objektmodell über Objektmodell geschichtet und getestet, und versucht, im Design Mechanismen und Primitive zu entwerfen, mit denen sich das Thema packen ließe (die Builder-Mould, der Operation-Point). Kurz vor Weihnachten habe ich mich dann gewissermaßen auf die Framework-Ebene geflüchtet, und dort eine Ersatz-Schlacht  geschlagen und „gewonnen“: ich habe das (bekanntermaßen problematische) GoF-Pattern (das mich immer schon fasziniert hatte) durch eine techologische Lösung ersetzt, die ich nach einiger Recherche im Internet aus bestehenden Ansätzen entwickelt habe: einen Tabellen-getriebenen Visitor, der „azyklisch“ ist, Subklassen-Semantik unterstützt (»is-a«) und durch Template-Metaprogramming realisiert wird. Das hat dann doch bis in den Januar gebraucht, ich hab meinen gesamten Feitertags-Urlaub pausenlos durchgecodet, das Ergebnis ist wohl tatsächlich eine Leistung (nach der bloß niemand gefragt hat) — und am Ende stellte sich heraus, daß ich damit den »Builder« nicht „packen“ kann, da die Objekte eingepackt in Smart-Pointer daherkommen. Danach habe ich den Sarg mit dem WrapperPtr zugenagelt und mich Scheiße gefühlt.

z.B. wir sollten doch alles in QT bauen, weil es dadurch viel einfacher wird, und obendrein auch gleich noch portabel

Wichtig: er hat sich nie in dieser Rolle „gesonnt“ — sondern sets die festgelegten Formeln wiederholt und auf meinen Beitrag eigens hingewiesen

Bis zum Frühjahr 2008 hat Christian absolut nichts mehr zum eigentlichen Coding beigetragen, sondern immer mehr seine Vision betont, daß durch distributed Tooling und ein offenes Projekt-Setup die Probleme von Open-Source (die sich damals schon sehr deutlich zeigten) aufgebrochen und gelöst werden könnten. Man muß nur „die Community enablen“, damit diese „als Benevolent Dicatator agieren kann“

Ich bekam Reichweite, die ich mir selber niemals hätte aufbauen können: denn bedingt durch den sonderbaren Zustand, daß ich allein weiter gecodet habe, wurde mir regelmäßig das Wort erteilt und ich konnte mich als Experte bewundern lassen

...das in diesen Monaten im Herbst vor allem dadurch getragen war, daß man nur hart genug arbeiten muß, dann kann man es nageln. Niemand hat mehr nach einem Projektplan gefragt, niemand wollte erklärt haben, wie das überhaupt zusammenpaßt (mit Cinelerra). Außerdem habe ich in der Zeit überhaupt erst C++ gelernt (das geht bei mir üblicherweise sehr schnell), viele neue Paradigmen aufgegriffen und mich bis Dezember sogar in das (damals total esoterische) Thema »Template-Metaprogramming« hineingebissen. Ich hatte nun in kurzer Zeit einen anderen Horizont gewonnen, und fand meine Ansätze aus dem Herbst (mit den Assets und MObjects) bereits problematisch, hab davon aber niemanden was gesagt (und es bisweilsen sogar vor mir selbst verborgen)

  • für Christian: einfache Lösungen + distributed Tooling wird es richten
  • für mich: man muß nur moderne Methoden einsetzen und hart arbeiten.

Zunächst einmal sogar die Frage, ob das noch Cinelerra ist, oder schon eine neue Applikation; auch die Frage, warum man überhaupt ein solches Projekt starten sollte („the world needs Lumiera“). Aber auch auf technischer Ebene, wurden Mystifikationen eingesetzt und durch stetige Wiederholung affirmiert: Da ist zum einen das DataBackend (für das, so muß man jetzt im Rückblick sagen, fast überhaupt nichts jemals implementiert wurde, mit Ausnahme der Memory-mapped Files), des Weiteren sind da die Placements, die auf viele Jahre hinaus aus „da kann man“ bestanden, die Config-Rules waren (für mich offensichtlich) ein Fernziel, das ich aber sehr oft in der öffentlichen Diskussion als Pluspunkt aufgeführt habe; ganz ähnlich steht es mit den Plug-ins: Christian hat über ein Jahr lang nichts Konkretes mehr zu dem Thema gesagt oder getan, aber die flexiblen Plugins waren weiterhin einer der immer wiederkehrenden Bullet-points. Und den Builder habe ich nach Januar 2008 erst mal liegen gelassen, und das auf verschiedene Weise plausibel gemacht. Damit war effektiv der Prototyp aufgegeben, und es wurde stattdessen die große Architektur gebaut.

Raffaella und Odin haben dieses Moment aufgegriffen —

und in die Naming- / Logo-Contests übersetzt

...das heißt, ich habe dazu keinen Beschluß gefaßt, das weiß ich ganz genau; vielmehr brauchte Joel nund einen Gegenpart, und ich hab mir sofort dafür den Arsch aufgerissen und geliefert.

August 2008 war ich zum ersten Mal in Karlsruhe (für mich ganz besonders magisch, denn ich hat aus meinem früheren Leben eine sehr tiefe Beziehung zu Karlsruhe), habe bei Christian übernachtet, und wir haben so manche Streitigkeiten auf der Terasse sitzend geregelt, „von Mann zu Mann“. Das hatte dann auch in vieler Hinsicht den Charakter eines Vertrages, wir haben Claims abgesteckt. Anschließend sind wir zusammen zur FrOSCon gefahren, für mich das erste Mal. Und anschließend habe ich den Kontakt mit meiner Verwandtschaft in Hagen wieder aufgenommen (Hagen hat für mich eine ähnlich tiefe Bedeutung, auch da reichen die Bezüge in meine Schulzeit zurück)

ohne sie explizit breitzutreten!

  • durch Kurzschließen entstehen Kräfte ... und mit modernen Tools verstärken sich diese Kräfte
  • da ist eine große Codebasis, die bereits was taugt und zu dem gemacht werden könnte, was sie immer schon sein wollte

Wir hatten den »Drive« und wir hatten ein Konzept, das in überschaubarer Zeit machbar erschien

Denn die beiden Elemente stammen aus einem komplett unterschiedlichen, und inkompatiblen Hintergrund; das Konzept, das (wenn ungefähr betrachtet) so plausibel erschien, läßt sich zwar realisieren, aber nur durch sorgfältiges, planvolles Vorgehen und Festhalten an einem Ziel. Also eine extrem lange Zeit ohne »Begeisterung« und »Inspiration«

Damit meine ich: ein »Mission Statement« und ein »Projektkonzept«, das plausibel und machbar erscheint, und nicht „völlig durchgeknallt“

...und anfangs auch tatsächlich plausibel war

hier meine ich mit normaler Dynamik: Man geht in einer freien Community auf Nahziele zu, baut, was man sich leicht vorstellen kann und was kurzfristig Spaß machen kann. All das konnte sich in der festgefahrenen Projektstruktur nicht entfalten. Das Projekt war „langweilig“ und hatte keine Drive

man hat sich auf einen Weg gemacht,

auf den man sich vernünftigerweise

niemals gemacht hätte

Erzählen auf dem Grundton: das kann doch nicht gutgehen...

das Thema der Flexibilität

...das ist tatsächlich die Vision, die sich jetzt abzeichnet; mit »vertikal« meine ich, daß sie von low-level bis high-level integriert sind und kohärent bleiben ⟹ »medium level of abstraction« ⟹ wir schaffen kein Wunderding, sondern ein Werkzeug mit Kraftverstärkung

Benny hatte schon vor Monaten gesagt,

ich möge ihn da einbeziehen

...und zwar hatte ich angedeutet, daß ich irgendwann den Disput über Plug-ins irgendwo als Text fassen muß; Benny sagte dann, es erscheine ihm plausibel, daß ich das möchte, aber ich solle bitte den Text von ihm gegenlesen lassen, bevor er irgendwo veröffentlicht wird; ich halte das auch für angemessen, und war/bin Benny sehr dankbar...

Damit meine ich: soweit der Impetus im Moment trägt, also bis in das 1.Jahr. Insgesamt möchte ich den Text noch weiter führen, kann das aber im Moment sicher nicht stemmen. Nun habe ich also den Text erst mal entworfen, dann die Quellen ausgearbeitet und dann den Text ausgefeilt. Er soll schließlich mit einem einzigen Commit online gestellt werden, ohne viel Aufhebens.

danke Benny!

we don't say this in this context. Does transliterate refer to 'meaning',

'spelling', ... but not 'format'. I'm not entirely sure; but it is not
_generally_ used like this. But I am nearly certain you will find it
as a special process in, for example, a group of Archeologists where
transliterate has a special meaning only to this group.

Fix because 'properly' is here coloquial because, again, it is

    ambiguous. Does 'properly' refer to the process of providing the
    credit, i.e., it was only written on a piece of paper, .. i.e.,
    'how' the credit was down; or is the creditation inappropriate?
    
    This kind of error is something similar to the many errors Trump
    makes and upsets many jurnalists, as the ambiguity is often
    critical and is much discussed on the BBC

also ist Benny's Vorschlag inhaltlich viel bessern

Lately i had almost no time to hack on cinelerra and it doesn't seem
that situation will improve in forseeable future.

ich muß hier eine Deutung machen, um den Sachverhalt klar zu fassen

Andraz hat es in seiner Mail "durch die Blume" gesagt, und die Community (oder zumindest die aufmerksamen Leser, hehe) haben verstanden, in welche Richtung das geht, ohne konkreter zu werden.

insofern: Benny's Formulierung ist sogar sehr gut, sie ist nämlich dezent

Christian hat nicht bloß mal ein Git-Repo eingerichtet, sondern er hat sich, so meine Deutung, davon erhofft, daß die Kombination von Technik und "kurzgeschlossener" Community von selber Heilungskräfte entfaltet. Das würde auch erklären, warum er...

  • grade eben keine Initiative vorschlägt
  • auch später sich stets weigert, "Cinelerra-3" als seine Initiative zu bezeichnen
  • "Cinelerra-3" nicht auf seinem Server hosten möchte, sondern alles in Git-Repos haben möchte
  • andauernd sagt: "the community shall be the benevolent dictator"

es sind jetzt ein paar Wochen vergangen; und ehrlich, für diese Einschätzung brauche ich Benny nicht. Es wäre nur schön gewesen, ihn mitzunehmen...

Andraz: "Leute, ich kann nicht mehr!"

User-1 sagt, "ich hab da mal nen Patch"

Cehteh: "und übrigens ich bastel an meinem Git-Branch, aber ich trage nix bei, sondern baue eine andere Infrastruktur"

auch die zweite Korrektur ist wohl stilistischer Natur (wäre interessant, Benny dazu zu befragen! Möglicherweise wieder so ein Fall, in dem sich Pidgin-English breit gemacht hat....)

ich kann nicht einschätzen,

ob Benny hier nur auf "gutem Englisch" herumreitet

Ich bin immer wieder am Zweifeln, inwiefern Benny mit verschiedenen Sprachebenen umgehen kann. Selbstverständlich kennt er das Konzept, er hat mir oft Beispiele genannt, wie ein Adeliger reden würde. Dann macht er mir aber anderseits immer wieder Vorschläge, die für mein Ohr sehr "literarisch" klingen, und auch sehr "brittisch". Er korrigiert auch Redewendungen, die in der gedruckten Fachliteratur weit verbreitet sind. Außerdem habe ich immer wieder gemerkt, daß Benny keinerlei Sinn für das Verkürzen von Formulierungen hat, und sachen klarstellen möchte, die ich bewußt zweideutig gehalten habe. Er sagt dann auch immer, das sei grammatikalisch, ist es aber nicht (in dem Sinn, daß es einem in der Schule als Fehler angestrichen würde, man aber durchaus so reden und schreiben kann)

eine Qualifikation ist m.E. hier komplett überflüssig, aber es sollte gesagt werden, daß es sich um Git Repos handelt, und nicht um Subversion

Please check: i think you mean 'general' here

    
'Common', here means 'working class' or cheap (and bad).
    But please check

Create our own toolset to track issues, tasks, bugs in a distributed manner.

Und mir fällt auf, daß dies sein erster inhaltlicher RfC war

Er "mag keine Mailinglisten", er mag keine Foren, er findet Bugtracker eine einzige Müllhalde und sagt, er will nicht damit arbeiten. Er lästert bei jeder Gelegenheit über Spreadsheets, er kotzt über Projektplanungstools ab, er findet Wikis nur eine Krücke und will sie schnell wieder loswerden, er findet die typischen Buildserver total daneben (Cruise-Control damals, dann Hudson, Jenkins). Und, was mich völlig von den Socken gehauen hat: vor zwei Jahren wurde er plötzlich ganz leidenschaftlich wegen Ethereum, er fand das System sowas von verkünstelt und overengineered, und gradezu gegen den "spirit" von Blockchain. Auch gegen Bitcoin ist er ehr negativ eingestellt, denn es wäre ja bloß "Kapitalismus pur" ... und dann fängt er leidenschaftlich an, seine Vision von einem Geld zu entwickeln, das auf Community-Tasks beruht und Austausch von Hilfe.

wäre schön wenn man das noch dikutieren könnte, aber eigentlich geht es nur um Nuancen in der Bedeutung. Ich bin mir ziemlich sicher, daß Christian nicht "Microsoct Projects" durch Git-Magic ablösen wollte, sonder ehr der Meinung war, wer MS Projects verwendet, ist sowiso krank

versuche die grammatikalische Verbesserung aufzunehmen,

bleibe aber bei meiner Formulierung "without a clear Resolution"

Benny hat sich jetzt 3 Wochen nicht mehr gemeldet, daher konzentriere ich mich nun auf das Wesentliche und räume solche Nebenthemen einfach ab

There appears to be widespread consensus that simple building blocks should be provided as free software, that “can be used to combine new functionality”

Die Frage ist: gehe ich mit dieser Mutmaßung zu weit? Es ist schon starker Tobak

Ohne ein solches Verzeichnis sieht man nur eine endlose Historie von Git-Commits über mehr als 10 Jahre, und kaum ein Thema „führt zu Ergebnissen“. Erst in den letzten Jahren ist durch die »Vertical Slices« soetwas wie eine Fürhungslinie gegeben

Vorausgegangen war Cehteh's letzter Vorstoß in Richtung einer Plug-in-Architektur, der damit endete, daß Ichthyo sein Konzept der Applikations-Struktur mit den Subsystemen durchgeprügelt hat. Das mündete in eine allgemeine Integrationsphase, in der die Code-Struktur und die Build-Systeme glattgezogen wurden, und das GUI als Plug-in integriert

und hat begonnen, dicke Bretter zu bohren:

  • Command-Handling Framework
  • Node-Wiring / Factory
  • Placement-Index
  • Placement Scope-Path und QueryFocus

man hat wohl ein ganzes Jahr lang zwar die Meetings aufrecht erhalten, aber nur sich informell ausgetauscht

Das ist wichtig für mich selber: denn ich habe sehr unter diesen Konflikten gelitten. Nur fanden sie gar nicht wirklich statt, sondern bestanden in einer Diskrepanz zwischen dem Stand, den ich mir erarbeitet habe, und den endlos-eintönig-immer-gleichen Klischees, die von den anderen Entwicklern und Usern kamen. Insofern habe ich noch eine Rechnung offen mit dieser »Community« — aber diese Rechnung hat hier nichts zu suchen. Im Grunde habe ich alles Wichtige bereits in meinem Essay »Complexity and Flexibility« gesagt....

die Darstellung könnte sich vor allem

auf die kollektiven Aspekte konzentrieren

Technologie wird dabei eine »Mystifikation« und wird zum »Fetisch« — aber das ist nur oberflächlich. Die Technologie (konket: Git, Automatisierung, Plug-ins) soll nämlich eine Art Marktplatz der Ideen herstellen. Und der eigentliche, weltanschauliche Kern ist, daß »die Community« wie eine unsichtbare Hand alle Probleme löst, und man deshalb sich gedanklich nicht weiter anstrengen muß, ja sogar gar nicht darf, weil man sonst das heilsame Wirken der Marktkräfte stört.

verstehe dies Verhalten als ein Anti-Pattern — dem man verfällt

Die Komplexität sorgt dafür, „daß die Bäume nicht in den Himmel wachsen“ — und demütigt den sich aus dem Anspruch der Technik ergebenden Herrschaftsanspruch. Der Komplexität kann man nur standhalten, durch Verzicht auf Wunder und durch Selbstbeschränkung auf einige wenige Themen.

Lumiera ist entstanden durch meine Verstrickung in diesen Konflikt

Ohne diese Verstrickung, und das damit verbundene Verfallen und die fehlende Reflexion, wäre dieses Projekt niemals zustande gekommen. Ich habe eine dialektisch-verhaftete Position eingenommen, die durch diesen Konflikt überhaupt erst ihre Form bekommen hat. Durch meine Hartnäckigkeit habe ich dem Projekt in der Anfangsphase den Weg zum »Erfolg« abgeschnitten. Dann aber passierte ein Wechsel des allgemeinen Klimas (der sich im Grunde bereits von Anfang an abgezeichnet hat). Das Resultat ist nun, daß ich, ganz allein, auf dieser Position stehe und über den damit verbundenen Anspruch bestimmen kann (solange niemand anders daherkommt — und auch nur wenn ich diesen Weg entschieden weiterverfolge).

ich sehe jetzt in dieser Position eine Chance

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

Geschrieben 10/2025 infolge der Auseinandersetzung mit den Anfängen des Lumiera-Projekts und implizit als Antwort auf den nie wirklich gelösten Architektur-Streit

Das ist entstanden, weil Benny zunächst allgemein helfen/beitragen wollte; ich habe vorgeschlagen, er solle sich mit dem Thema Video-Output beschäftigen, unter der Maßgabe, ob es da bereits fertige und einfach nutzbare Frameworks gibt. Er hat dann relativ bald sich auf GStreamer konzentrierte, sehr schnell ein Tutorial-Beispiel gehabt für einen Video-Player, ist an der Stelle aber stecken geblieben. Im Rückblick muß ich sagen, daß GStreamer für unser Thema vermutlich keine gute Quelle ist — man bekommt nur ein massives Framework zu fassen — allerdings hat sich Benny auch nicht selbständig lateral bewegt. Im Frühjahr 2025 haben wir überlegt, was wir zur FrOSCon machen könnten; es war bereits beschlossen, daß wir nun jedes Jahr mit mindestens einem Vortrag präsent sein wollen. Ich hatte dann die Idee, diese Recherche gemeinsam weiterzuführen, da ich die Ergebnisse nun absehbar demnächst brauche, um die Engine zu testen.

...der findet sich in der Seite project/credits.html und besteht aus einem speziellen CSS + ein JavaScript, das die Seite langsam scrollt.

Relevant ist hier vor allem der Content, denn das war der erste Versuch, Attribution zu geben (und bisher auch die Basis des AUTHORS file)

....anhand der Git-Logs nach Auslassungen suchen; dann aber auch die Darstellung etwas rebalancieren, denn die entspricht mehr dem Eindruck aus der Anfangszeit, als jeder kleine Beitrag bejubelt wurde — aus heutiger Sicht haben wir einige substantielle Beiträge, die in dieser ganzen Masse an Credits untergehen, während Sachen hervorgehoben werden, die letztlich nie zustandegekommen sind (uWiki, Builddrone, neues Website-Layout, Christian's DataBackend)

Wir sollten darauf achten, auf den oberen Ebenen die Anzahl der Kategorien knapp zu halten — um eine gewisse systematische Auffindbarkeit zu gewährleisten; im Gegenzug dazu sind weitere Unterkategorien auf tiefer geschachtelten Ebenen eine praktisch kostenlos verfügbare Ressource, da die Kapazität eines Baumes exponentiell mit der Tiefe wächst

Wenn die Design- und Archtektur-Bereiche zu sehr in die Details abgleiten, fächern sie sich in technische Belange auf, welche nicht mehr so recht systematisch in eine Kategorie passen wollen. Für die technische Dokumentation ist das kein Problem, denn diese ist ohnehin quantitativ ausgelegt.

Hier sollen Inhalte veröffentlicht werden, die über eine reine technische Dokumentation hinausgehen. Das reicht von einer Darstellung der Projekthistorie, über Recherche und gesammeltem thematischen Wissen bis zur Aufsätzen zu allgemeinen Begriffsbestimmungen und Maßstäben.

Da Lumiera.org explizit dafür sorgt, längerfristig im Internet-Archiv präsent zu sein, und zudem längerfristig auf Verlinkung hoffen kann, sollten hier abgelegte Texte stabil sein und könnten ggfs. extern als Quellen referenziert werden; im Besonderen denke ich hier an eine Sammlung von Wissen, Methoden und Verfahren.

das heißt, hier bewege ich mich auf einem schmalen Grat

  • erhoffe mir Sichtbarkeit für meine eigenen Einsichten
  • kann aber auch Richtlinien und Maßstäbe verbindlich setzen

da dieser Content unweigerlich wächst, ist die Historie des Website-Repos nicht für die Ewikeit und könnte rewritten werden

per hart gecodeter Mapping-Regel in menugen.py — addPredefined()

wie so oft hatte Christian eine ganz pfiffige Lösung, die zu kurz greift aber in die richtige Richtung zeigt (und auf die man erst mal kommen muß!)

es gibt einen RfC: »WebsiteSupportMarkup«

Die Implementierung braucht sehr wahrscheinlich einen kompletten Scan über alle Dokumente; das zu vermeiden führt direkt in ein DB-basiertes CMS. Daher, gemäß KISS sollte man erst mal versuchen das zu implementieren und beobachten, wie groß der Schmerz ist. Auch Menuegen selber war mal in zwei Tagen implementiert, ist schwer zu warten, aber erfüllt seinen Zweck inzwischen seit mehr als 10 Jahren

Das größte Problem ist wohl, daß wir nicht genau wissen, was wir brauchen (abgesehen von der Vorstellung, irgenwie magisch funktionierende Cross-Links zu bekommen)...

  • wir wollen ein Tag-System, welches um weitere Quellen erweitert werden kann
  • wir wollen möglichst mit dem Taggen anfangen können, bevor unser Konzept wirklich klar ist
  • es schwirren viele Ideen herum bezüglich generierter Übersichtsseiten; was uns aber tatsächlich weht tut, ist die Schwierigkeit, Cross-Links in der Dokumentation zu verwenden.

nun sind wohl 10 Jahre vergangen

und das Problem besteht unverändert

...was aber vor allem daran liegt, daß ich allein bin und für mich sowiso alles per Mindmap organisiere; daher konnte ich das Problem bisher »aussitzen« — was aber leider dazu geführt hat, daß das TiddlyWiki (und meine Mindmap) ins Unermessliche gewachsen sind. Dennoch ist das Problem eigentlich brennend ernst: außer mir blickt keiner durch, und ohne mich findet niemand die Ergebnisse der umfangreichen Konzeptionsarbeit.

Dieser Vorschlag stammt von Christian, und (selbst wenn der Vorschlag zunächst in zweifelhaftem Kontext stand) — es ist die einzige bisher vorgeschlagene Lösung, die mit einfachen Mitteln umsetzbar ist, letztlich sogar ohne jedwede Automatisierung

...all die weiteren seinerzeit hitzig diskutieren »Killer-Features« sind meines Erachtens Extras, die man oben drauf setzen kann; auch Übersichts- und Kategorieseiten erreicht man letztlich wieder über einen ID-Link. Der einzige Knackpunkt ist, das Eingangs-Format der Links so hinzubekommen, daß es tragfähig ist.

Micro-HTTPD expandiert nicht automatisch *.html

Aus mehrerlei Gründen

  • weil wir generell in der Adresszeile die kanonische URL sehen wollen
  • weil das Navigationsmenü nur mit der kanonischen URL funktioniert

»solange Vorrat reicht« — die ganze Frage der Duplikat-Resolution kann später auf technischer Ebene gelöst werden, solange es nur für jede verwendete ID einen Link gibt

Die aktuelle Struktur ist aus Kompromissen entstanden und gewachsen, erfüllt aber derzeit noch ihren Zweck. Trotzdem ist die Website-Infrastruktur etwas mühsam.

  • Es ist verwirrend, daß Content in mehreren Repos ist
  • Bilder sind in Git eingecheckt und stellen mithin ein latentes Problem dar
  • man ist daher gezwungen, Bilder in einen separaten Bereich zu legen
  • daher ist Content „kreuz und quer“ verteilt und oft nicht leicht zu finden

Bei der Diskussion (mit Benny, in Bernbach, August 2025) ist uns aufgefallen, daß die Website eigentlich gut auf das Muster von Git-Submodulen passen würde. Es werden wohl weitere Module dazukommen, vor allem durch die von mir geplante »Knowledge Base«. Allerdings bedeuten Git-Submodule dann aber auch ein Anheben der Komplexität im Umgang mit der Website; man muß wohl zusätzliche „Handgriffe“ sich einprägen, oder die Automatisierung weiter treiben. Wir haben beschlossen, damit so lange zu warten, wie der Zustand mit den Bildern/Medien im »website-Repository« noch tragbar ist; perspektivisch werden wir die »documentation«-Struktur dann wohl doch aus dem Haupt-Repository herauslösen, vielleicht auch nur das eigentliche User-Manual.

Die einzige Maßnahme, die wir nun unmittelbar umsetzen, ist, die Symlinks aus Git herauszunehmen — so daß man per manueller Einrichtung auf einer Maschine gleichzeitig mehrere Varianten der Website haben kann

...der Grund liegt in der Pflicht zur Bereitstellung der "Quellen". Das zwingt im Zusammenhang mit allgemeinen Inhalten wie Text, Bild und Video zur Auslegung, und dadurch wird die Situation rechtlich zweideutig. Theoretisch wäre das kein Problem, aber praktisch kann es ein Hindernis darstellen für jemanden, der lediglich gestalterischen Content  weiterverwenden und umgestalten möchte, also in unserem Fall Texte und Bilder. Daher bieten wir eine Wahlmögichkeite der Creatvice-Commons-By-SA, denn diese verpflichtet nicht zur Bereitstellung eines Quelltextes.

Einschätzung: ja

ABER: das verpflichtet UNS, die CC-By-SA einzuhalten

Und zwar deshalb, weil es nur einen kompatiblen Pfad gibt von CC-By-SA ⟶ GPL-3.

Deshalb müssen wir die Vereinigungsmenge der Forderungen beider Lizenzen erfüllen, was aber möglich ist....

...hier müssen wir aufpassen. Da die CC-By-SA nicht auf Quellcode abstellt, muß die Attributierungs-Information jeweils auch textuell nahe bei dem jeweiligen Content aufgeführt sein.

...das tun wir, unser Git-Repo ist öffentlich und die veröffentlichten Webseiten werden aus Asciidoc generiert. Für Bilder halten wir die bestmögliche Auflösung bereit, bzw. verwenden SVG, wo möglich

das heißt, mal eben den Spell-Checker laufen lassen, oder den Markup reparieren ist noch kein Beitrag, der eine Autorenschaft konstituiert.

ABER: jeder relevante Autor eines Textes / Bildes muß im Text selber aufgeführt sein

auf lange Sicht werde ich selber diese Arbeiten ausführen

....und deshalb muß das Schema nicht so elaboriert sein, wie es für einen Release-Train mit vielen Projekten notwendig wäre; wichtiger ist, daß das Ergebnis selbsterklärend ist. Später kann man diese Prozedur wohl komplett skripten

DEB-Paket: direkt <v%(version)s>-Tag von master verwenden

Nachdem die Code-Basis grundsätzlich reif ist für ein Release, wird hiermit die Konvergenz-Phase eingeleitet, und die Haupt-Entwicklung wieder freigegeben...

  • ein Release-Branch wird abgezweigt
  • auf dem Integration(Mainline)-Branch findet der Versions-Bump statt

Nachdem die Konvergenz-Phase abgeschlossen wurde, muß nun das Release nur noch formal vollzogen werden....

  • (idealerweise schreibt man vorher als letzten Commit die Release-Notes)
  • es findet ein back-Merge statt auf den Integration(Mainline)-Branch

Nachdem nichts mehr zu committen ist und auch der Build nochmal getestet wurde....

  • setzt ein letzter formalisierter Commit auf dem Release-Branch die Release-Version
  • der Release-Branch wird auf den Master-Branch geMerged
  • auf diesen Merge wird das Release-Tag gesetzt
  • der Release-Branch wird gelöscht
  • dieser Stand in Git wird publiziert
  • automatisierte CI-Aktionen werden initiiert

Ein Hotfix kann jederzeit ohne Vorbedingungen begonnen werden

  • ein Hotfix-Branch wird vom letzten Release-Commit abgezweigt
  • der Eröffnungs-Commit ist formalisiert und beinhaltet einen Version-bump < current-devel

Nachdem Test und Validierung abgeschlossen sind

  • erfolgt ein Merge-Commit normalerweise auf Integration(mainline)
  • sofern allerdings ein Release-Branch existiert, wird auf diesen gemerged; dabei kommt es zu einem Konflikt mit der Versionsnummer, welcher zugunsten der Release-Version aufzulösen ist (diese ist stets höher)

Nachdem alle Arbeiten abgeschlossen sind und der Code-Stand so publiziert werden könnte...

  • erfolgt direkt ein Merge auf den Master-Branch
  • auf diesen wird ein Tag gesetzt
  • der Hotfix-Branch wird gelöscht

Ein Feature kann jederzeit begonnen werden und soll regelmäßig re-based werden

  • ein formalisierter erster Commit setzt eine dekorierte Version und legt den Branch an

Nachdem ein Feature so weit ist, daß es den Stand auf Integration(mainline) nicht mehr gefährdet...

  • (sollte ggfs. zunächst nochmal manuell ein Rebase erfolgen)
  • erfolgt automatisch ein Rebase, welches den Feature-Versions-Commit wegläßt
  • sofern danach nur maximal zwei Commits übrigbleiben, kaönnen diese direkt per fast-forward übernommen werden
  • sonst wird ein no-fast-forward-Merge auf Integration(Mainline) gemacht

Wobei allerdings (gemäß meinen Erfahrungen bei der Bank) die CI explizit so aufgesetzt werden muß, daß sie verschiedene »Linien« nicht vermischt. Es ist nicht möglich, die CI-Builds allein durch die Versionsnummer zu steuern. Vielmehr muß es eine dedizierte CI geben für

  • Produktionsstand
  • Releases in Arbeit
  • Fixes in Arbeit
  • Integration
  • Feature-Development

...so daß die unqualifizierte Version auf dem Integration-Branch die geringste Prio hat (am ältesten sortiert), Features danach stehen, und mit Timestamp markierte explizite Snapshots die höchste Prio haben

Das Lumiera-Versionsnummern-Schema baut auf der Debian-Policy für Versionsnamen auf, und beinhaltet aber zusätzlich einige Entscheidungen, wie die ALPHA und BETA-Phase dargestellt wird, und wie man Development-Snapshots markiert

...da in Git-Tags keine Tilde erlaubt ist, hat git-buildpackage hier eine Übersetzung in '_' eingeführt

...denn dummerweise ist '_' ein word-constituent-char, d.h. die vorhergehenden Versions-Komponenten würden einen Trenner durch '_' einfach mit konsumieren

Gemäß Debian-Policy ist das nämlich der Trenner zum Paketnamen, und darf daher in der nachfolgenden Versionsnummer nicht nochmal vorkommen (d.h. der letzte Underscore trennt den Namen von der versionsnr)

ist so möglich, da Underscore anderweitig nicht erlaubt ist (und Grenzfälle können hier unberücksichtigt bleiben, da letztlich in Lumiera nur sinnvolle und valide Versionsnummern vergeben werden dürfen; als zusätzilcher Filter dient das mandatory-prefix 'v*' im Tag

Und zwar temporär, während man auf dem Release-Zweig ist, kann es ein RC-Tag geben. Wenngleich auch die (von mir intendierte) Git-flow policy verlangt, daß solche RC-Tags mit dem Release selber wieder gelöscht werden — man wird sie aber zulassen müssen, damit später mal eine CI korrekt mit Release-Candidates umgehen kann. Die Realität ist komplex. Seufz.

Drei Varianten wären denkbar, diese hier erscheint mir am praktischten, und natürlichsten (und ist einfach umzusetzen, ohne den Parser komplexer zu machen

  • man kann argparse.SUPPRESS verwenden
  • man nargs='?' setzen, und zudem einen Default angeben; damit ließe sich der Wert zur Option seinerseits optional machen...

Dadruch würde aber der Aufruf komplexer, da nun der 'suffix' key möglicherweise nicht mehr im resultierenden dictionary nach Parser-Auswertung enthalten wäre.

Das war praktisch der Anfang der Diskussion, Benny sagte, die Einleitung wirke steif, und hat einige Vorschläge im Chat skizziert

...aus dem, was ich geschrieben habe, aber nur der 2. Hälfte, und Benny's Formulierung, die um einiges glatter wirkt, da sie mehr an weithin aktzeptierte Formulierungen anknüpft. Das halte ich aber nun auch für besser, da es ja nur zum Thema hinführen soll.

"is established" ist zweifellos eine viel dezentere und neutralere Formulierung, aber weicht eben auch einer Aussage in der Sache aus. Das ist immer wieder der Konflikt zwischen uns Beiden; ich mache die Aussage in der Sache ja, weil ich einen Gedankengang entwickle, anstatt nur bekannte Umstände möglichst geläufig anzusprechen; damit ist meine Formulierung meist schwierig und herausfordernd. Nachdem dann Benny einmal alles geglättet hat, stößt er dann auf die Kernaussage und sagt: das ist jetzt aber komisch, wie kommst Du da drauf? Und in der Tat, nachdem der ganze Gedankengang in der Sache weg ist, ist die Aussage, bei der der Gedankengang ankommen sollte, nur noch beliebig und far fetched.

Da ich diese Erfahrung in der Zusammenarbeit mit Benny nun schon oft gemacht habe, weiß ich, daß ich an zentralen Stellen den Konflikt suchen muß. Denn letztlich bin ich es, der einen Gedanken sieht und einen Entwurf macht. Würde Benny, auf seine Weise, etwas Entsprechendes tun, ich wäre der letzte das anzugreifen...!

siehe deutsche Wikipedia

siehe englische Wikipedia

TODO this commit requires a review

    
'essence' does not work here. Don't know why, probably gramatical; but I
know what you'd like too say.
The problem with my suggestion, 'core', ist that core has several meanings.
One meaning refers to responsibility or location. So making an insigificant
code modification in this area; which is noot the meaning you intended with
'essence'. However 'core' can also mean 'essence'....so this is ambiguous using
core.
    
    'fundamental operation of the code base' might be better suited; but everyone
    gets 'core' immediately.

...will sagen, er hat den generischen Ausdruck gewählt, den ich ganz bewußt nicht genommen habe, der eigentlichen Aussage wegen. Das hatten wir schon mehrfach.

A complex integration process is also

required for large and intricately structured systems: changes will first be accommodated within a subsystem,

which is followed by joining subsystem branches into a new state of integration

ebenso hier: das meiste ist sehr gut,

aber manches versteht Benny anscheinend nicht

was für einen native speaker schwierig zu verstehen ist, läßt er nicht gelten

interessanterweise beharrt er darauf,

das sei rein grammatikalisch

Das passiert uns immer wieder mal, bei der Arbeit an einem Text. Und es handelt sich immer um Gedanken, die man sehr wohl im Englischen ausdrücken kann (siehe die Übersetzung philosophischer Werke, daher habe ich es ja) aber die Engländer und Amerikaner der Tendenz nach nicht gebrauchen; und das liegt wohl mehr in ihrer eigenen Kultur: die Esssenz, der Wesenskern, ein Begriff der diesen trifft — so etwas ist ihnen suspekt, das erscheint ihnen wie ein "Label" das man künstlich draufklebt

Dieser gegenwärtige Trend ist kein Pendelschlag in die andere Richtung, und es geht nicht darum, die bewährten alten Methoden gegen Neuerungen zu positionieren. Allerdings laufen die propagierten Methoden im Grunde auf eine Restauration hinaus; es werden wieder mal Tools und Technologien propagiert, allerdings unter dem Thema einer aggressiven Beschleunigung und Steigerung

Rein der Sache nach wäre auch nicht mehr notwendig; was derzeit an Argumenten vorgebracht wird ist vor allem dummes Zeug, formuliert von Leuten, die keine Erfahrung haben, und wieder einem naiven Technik-Glauben anhängen....

ich vermute aber, dahinter steht eine ernst zu nehmende Tendenz: Industrialisierung

Das ist ein Geschehen, das nicht „absichtlich betrieben“ wird, sondern sich den Menschen aufdrängt, in der Form eines Anspruches: Sachverhalte und Umstände erscheinen plötzlich in einem anderen Licht, in einem Zusammenhang, und diejenigen, die das nicht sehen, nimmt man plötzlich als unmoralisch wahr.

und impliziert eine Gegenposition — ohne sie direkt zu bezeichnen

Ich würde diese Gegenposition wie folgt charakerisieren: „egal was von Deiner Industrialisierung zu halten ist — Erfahrung brauchst Du trotzdem, Entscheidungen wirst Du weiterhin treffen müssen, und den Grenzen Deiner Fähigkeiten entgehst Du nicht“

Und zwar aus mehrerlei Gründen

  • didaktisch: mein Text operiert auf einem abstrakteren Level als der Original-Text von Vincent Driessen; daher gehe ich auf viele Details nicht ein, arbeite aber die Grundstrukturen und Motivationen stärker heraus
  • Abweichungen in der Methode: ich setzte die Tags etwas woanders, und ich schlage vor, die back-Merges sets vom Tag aus zu machen
  • Darstellung: ich möchte den Zeitablauf von unten nach oben darstellen, so wie er auch in fast allen Git-UIs gezeigt wird

nach einigem Herumknobeln: es paßt neben den einleitenden Text, ab dem 2. Absatz ⟹ daraus ergibt sich eine Beschränkung der Zeichnungsgröße

wenn wir den master vorerst stehen lassen, dann kann er nach dem nächsten Release nahtlos in die neue Rolle wechseln; d.h. wir mergen das neue release noch in den alten release-Branch, und dann spulen wir master einfach dorthin

rein praktisch gesehen bringt das keinen Mehrwert;

wir hatten bisher nur »preview«-Releases, wlche nicht wirklich einen greifbaren Stand verkörpern. Zudem liegt das letzte solche Release jetzt (Räusper)  zehn Jahre zurück... wir würden also jetzt über 10 Jahre einen leeren Branch ziehen. Da erscheint es mir besser, das Schema jetzt neu aufzusetzen, mit Master von dem Punkt an dem Git-flow eingeführt wird

Weil da nun die Kopfzeile (Docking-Panel) nicht richtig gezeichnet wird, bzw. pseudo-transparent wird.

Abbruch. Es zeigt sich, daß meine Grafikkarte das Auto-Keying von XV nicht unterstützt, d.h. die Attribute XV_AUTOPAINT_COLORKEY und XV_COLORKEY sind nicht definiert. Habe noch mind. 2 Stunden herumexperimentiert, bin aber auf keinen »grünen Zweig« gekommen. Diese Erfahrung läßt es für mich sehr fraglich erscheinen, ob wir XV unterstützen sollen....?

  • project/news/DevReport-16-11.txt : by cutting a new ``preview release'' (Version `Lumiera-0.pre.03`), and, in the process, updating the packages...
  • project/news/old_news.txt : We published the next »preview release« `0.pre.03`
  • project/background/history/Resources.txt : Erwähnung in der Zeittafel

Fazit: OK

Das ist ein Thema mit langer Historie: theoretisch sollten Root-relative Links auf jeder Website funktionieren, in der Praxis gab es damit immer wieder Probleme. Gilt im Besonderen, wenn man die Website lokal mit einem Mini-HTTPD laufen läßt. Fazit: sollte eigens getestet werden, auch mit einem lokalen Webserver

Das widerstrebt mir sofort — denn um das angemessen zu machen, bräuchte man mehr als ein »pfiffiges bash-script«. Schon nach den ersten zwei Zusatzfeatures beginnen die Probleme und Wechselwirkungen.

STOP! Das bisherige Setup war so genial, weil es minimalistisch ist und genau eine Sache macht; im Grunde war es bereits problematisch, Menugen zu integrieren (aber dennoch sinnvoll).

Wenn überhaupt, dann sollte man das alles durch ein kleines Python-System ersetzen, das die Website-Sources scannt, Asciidoc anstößt und sonstige Infrastruktur generiert.

Tagger: Ichthyostega <prg@ichthyostega.de>

Date:   Mon Sep 1 02:27:12 2025 +0200

Review: Website infrastructure improvements by Cehteh from 2018

Regrouped thematically:

- generic improvements of build_website.sh

- additional features (Linkchecking, cleaning)

- website maintainance and fixing of broken links

-----BEGIN PGP SIGNATURE-----

iHUEABYKAB0WIQTVnM7D++M2pMSFPUOUEG/3stxoAQUCaLToYAAKCRCUEG/3stxo

Ad9LAP9rje8hIMWOY6gV5UnrbJ0+wnopy4j6GxRMWxMSoPpWVwD5AQ4HRKh9tdKe

cz2r08O5G0ofTTc5fnV0GSXTZZq02g0=

=ZDyL

-----END PGP SIGNATURE-----

bei genauerer Betrachtung ist dieses Skript auch vorher bereits extrem pfiffig — es ist ein minimales Build-System in wenigen Zeilen Bash

wirlich überprüfen kann ich das nicht, ohne das Skript analtyisch auseinanderzunehmen

...ich vermute, daß diese früher automatisch den Pfadnamen "gitweb" aufgegriffen hat, und jetzt (durch Christian's Umbenennung) unter einem anderen Namen in der Datenstruktur steht.

Resultat: es sind nun zwei Nodes in dem Submenü, und einer davon hat eine falsche URL

Stichworte allein sind ein flat namespace

Das wäre eigentlich eine schöne Lösung, die uns weiterhin ein Content-Management-System erspart: wir erzeugen beim Seiten-Rendern eine Linkfarm, und die Links werden anhand von Tags aufgelöst, die in den Seiten als Kommentar stehen (ähnlich wie derzeit die Steuerung des Menüs)

Seinerzeit wollte Christian das mal eben schnell in Lua  implementieren, war aber am nächsten Tag zurückgerudert (als ihm klar wurde, daß die eigentliche Aufgabe schon etwas komplexer ist). Dann wollte sich Benny darum kümmern, ist aber bei einem Glossary-Generator steckengeblieben. Und ich — ich würde das wohl in 1-2 Wochen hinbekommen, würde dafür aber auch Menuegen neu schreiben, weil beide Aufgaben gleichermaßen eine Traversierung aller Sources erfordern. Meine Sorge dabei ist, daß das ein Performance-Bottleneck wird; denn dann brauchen wir inkrementelle Verarbeitung und damit eine Datenbank — und würden dann selber ein Content-Management-System schreiben.

Und das Problem hierbei ist so typisch Christian: „man kann dann ja mit Tags arbeiten!“ — Junge, ein System von Tags aufbauen, das für unsere Zwecke funktionert, das ist die eigentliche Aufgabe.

Lumiera ist keine Plattform und kein Framework — wir haben lediglich Framework-artige Infrastruktur wie in jeder größeren Applikation

...da hatte anfangs niemand von uns dran gedacht; inzwischen haben wir dutzende  RfCs, und so manche gute griffige Namen sind bereis weg; generell sollte man

  • RfCs mit einem Nummern-Schema versehen
  • im RfC-Subindex auch gewisse qualifizierte Schlagworte mit speichern

man kann Exclusions von Exclusions definieren; das klappt aber nur, wenn diese doppel-Exclusions spezieller sind als die allgemeine ignore-Regel. Insofern wird empfohlen, solche Regeln paarweise zu definieren

hier stimmt was nicht mit dem Link auf das allgemeine Essay

...so meine Schlußfolgerung; denn es fällt auf, daß ich keinen  der noch bestehenden Links in der Wayback-Maschine finde...

einige meiner Texte habe ich als HTML gespeichert

andererseits: alle wichtigen Ideen für Lumiera waren von Anfang an da

...denn das ist das ist dann irgend ein Text, der 15 Jahre später in Git auftaucht; für den »Cinelerra_woes«-Text habe ich das wenigstens zeitnah gemacht

einigermaßen beweiskräftig ist Publikation, welche

  • entweder auf Archive.org aufgezeichnet wurde
  • oder zeitnah in einem public-Git-Repo lag
  • oder in einem »geschlossenen Format« (wie PDF) vorliegt

...denn in diesen Fällen kann man zumindest schon durch den Kontext eine gewisse Beweiskraft ableiten. Für alle anderen Formate gilt sets, daß man sie im Grunde jederzeit erstellen könnte, im Besonderen wenn die eigentliche Formatierung viel später oder in einem neuen Format stattfindet. Solche Dokumente haben dann nur einen Wert wie eine Zeugenaussage

...außer »Cinelerra woes« ...

ich bin halt ein unverbesserlicher Sammler, und nachdem ich wußte, wonach ich suchen muß, hab ich sie auch ganz schnell gefunden

...ich frage mich nämlich inzwischen oft, wann ich mir diese Ideen zugezogen habe; diese Seiten belegen für mich, daß es ich bei Lumiera im Kern um eine »Vision« handelt, die ich 2007 hatte. Und nicht um ein Konzept, welches ich mir durch endloses Nachdenken über viele Jahre ohne Realitätskontakt »zusammengesponnen« hätte.
Also im Grunde sehr ähnlich, wie bei Christian, der ja wohl auch damals bereits die komplette Plugin-Applikation vor seinem geistigen Auge gesehen hat

Sofern Lumiera scheitert, oder zu etwas ganz anderem wird, oder überhaupt jemand sich die Mühe macht, die Historie auszuleuchten — dann könnten solche Seiten auch erheblich negativ ausgedeutet werden: so eine absurde Idee, und sowas von weltfern... und dann hält man daran auch noch jahrelang fest, anstatt sich »vernünftiger Methoden« zu bedienen oder sich »wertvolle Ziele« zu setzen.

dann sind es nämlich wohl nur drei Seiten; alle anderen Seiten finden sich nahezu vollständig als erste Versionen späterer RfCs

2008-03-06T06:22:27+01:00 : ProcPlacementMetaphor.html

2008-03-06T06:22:56+01:00 : ProcBuilder.html

2008-03-06T06:23:09+01:00 : ArchitectureOverview.html

2008-03-06T06:23:38+01:00 : Cinelerra_woes.html

2008-03-06T06:23:48+01:00 : best_practices.html

2008-03-06T06:24:05+01:00 : Cin3_Project_Proposal.html

2008-03-06T06:24:22+01:00 : Possibilities_at_hand.html

...im zweiten Teil hat dieser Capture vom 2008 deutlich überarbeitete Inhalte, im Vergleich zu der Version, die ich 2011 für die Historien-Seite publiziert habe. Überdies bin ich (lt. Git) damals 2011 von Moin-Moin-Markup ausgegangen. Also  mußte ich irgendwo eine noch ältere Version gespeichert haben, als dieser capture hier von 2008.

Beschluß: für die Historien-Seite bleibe ich bei der älteren (roheren) Fassung, aber ich dokumentiere diese Fassung trotzdem hier in der Git-Historie von dem Seitenbranch, den ich im Moment aufbaue

Taucht erstmals in einem Merge-Commit im Mai 2008 auf (da habe ich das SVG wohl noch mit reingestopft)

commit c0d7ae1aa2073e4e5b29f864d32a639c2864e9a1

Merge: b5d2e9486 2e58b02b8

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Tue May 27 02:11:35 2008 +0200

    Merge added builder documentation

...sie stammen alle (mit Ausnahme von »Cinelerra woes«, von dem ich noch original-MoinMoin-Markup hatte) aus einem HTML-Snapshot im März 2008. Tatsächlich habe an ganz wenigen Stellen nun doch die Grammatik repariert, denn manche Sätze waren nahezu unverständlich in der Originalform. Außerdem habe ich manuell auf Asciidoc umgeschrieben, und dabei auch bisweilen die Formatierung minimal angepaßt (Umbrüche, Titel-Level vereinheitlicht, an einer Stelle eine Bullet-List aus einer Aufzählung gemacht)

das war nun eine

intensive Auseinandersetzung

diese greift, wenn die mtime neuer wäre als die angegebene Unix-Epoch. Damit kann man einen Build reproducible machen

  • <link rel="license" href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" />
  • <link rel="license" href="https://www.gnu.org/licenses/gpl-3.0" />
  • <link rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/" />

Grundsätzlich kann ich Content von anderen Leuten nicht ohne Weiteres aktualisieren. Aber Seiten, zu denen ich später Ergänzungen beigetragen habe, stellen ein abgeleitetes Werk dar, und in dem Zuge kann ich von dem Upgrade-Recht (das CC-Lizenzen stets beinhalten) Gebrauch machen. Das geht aber nur mit einem explizit genannten Stichdatum (oder wenn man sicherstellt, daß jede Seite tatsächlich bearbeitet wurde (Git-Commit) und einen entsprechenden Vermerk trägt

Vor allem relevant in der EU: die Datenbank als Ganzes (oder in großen Teilen) bleibt geschützt, da sie ein erhebliches Investment darstellt. CC-Lizenzen gelten nur für den Abruf und Gebrauch von Einzeldaten

Das beseitigt eine gefährliche Falle. Manche Länder (z.B. England, Frankreich, Japan) haben ein striktes moral rights regime, das ohne ensprechende Klarstellung dafür sorgen könnte, daß die per CC gegebenen Rechte durch einen Beitrag abgeschwächt werden könnten. Die neue Lizenz hat eine spezielle »Tunell-Klausel«, der zufolge per übersteuerndem Recht eingeschränkte Rechte wieder aufleben, sofern die Bearbeitung in einen anderen Rechtsraum gelangt. Zu den moral rights gehört das Recht eines Authors, über die Art der Repräsentation seines Werkes vollumfänglich zu bestimmen. Das bedeutet, daß ein Author das Recht hat, einem abgeleiteten Werk oder einer Übersetzung nicht zuzustimmen  und verlangen kann, daß entsprechende Werke effektiv aus dem Verkehr gezogen werden. Das würde auch greifen, wenn ein bisher substantieller Anteil marginalisiert wird, oder die Art und der Stil der Darstellung sich ändert.

Es gibt jetzt einen Pfad CC By SA 4 ⟶ GPL 3+ (aber nicht umgekehrt!)

Adapted Material: means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor.

Es geht nach meiner Einschätzung vor allem um die Protokolle, die andere Leute geschrieben haben, die RfCs und die Workflow/GUI-Proposals. Alle diese werden mit klarer Author-Angabe reproduziert

und die neue Version schränkt nach meiner Einschätzung keine Rechte ein, sondern stellt nur Rechte klar. Der größte Knackpunkt könnten moral rights sein, aber da stelle ich mich auf den Standpunkt, daß ich eine Adaption/Collection mache, und daher gar nicht über die moral rights von Dritten verfügen kann. Insofern bestehen diese weiter

bisweilen mit Benny als Co-Author

Wobei in diesem Kontext zweifelhaft ist, was ein maschinenlesbarer Code auf einer allgemeinen Lizenz-Seite bringen soll. Das würde eigentlich nur Sinn machen, wenn der Button auf jeder Seite wäre

So stellt es zumindest ChatGPT dar. Demnach habe man die Empfehlungen bei CC vereinfacht, und biete nun einfach Grafiken an, die die User in ihre Website packen können.

CSS, falscher Link

....und jeweils sehen ob sich das bewährt und entsprechend klar ist

Denn damit kann man vermeiden, Attribution für Dinge zu geben, die nicht vorwärts gekommen sind

...die wollten mal was beitragen, haben sich auch in der Diskussion engagiert, aber nie etwas geliefert

  • ich hatte vor vielen Jahren in Asciidoc-Markup umgewandelt, was hier mit minimalem Aufwand möglich ist (einziges Problem: ich bekomme Ziffern  in der Aufzählung, statt Buchstaben wie im Original)
  • der eigentliche Text ist komplett identisch geblieben
  • geändert hat sich, daß jetzt auf die FSF-Website verlinkt wird, und daß ein Beispieltext angepaßt wurde

analog

Die LGPL hatte ich ohnehin nur der Vollständigkeit halber mit eingeschlossen; sie spielt für Lumiera derzeit keine Rolle. Daher kann ich auch gleich die aktuellste Version LGPL-3 nehmen. Auch diese transcodiere ich ganz oberflächlich nach Asciidoc

Okt.2009 hatten wir eine Kooperation mit der ffis.de vereinbart; in den nächsten Jahren gab es ein paar Klein-Spenden, die wir aber nicht von der ffis abgerufen haben, da wir damals keinen Bedarf hatten (Idee war z.B. immer gewesen, einem Entwickler die Reise zum Treffen zu zahlen — aber die Beteiligten konnten die Reise zur FrOSCon immer problemlos selber zahlen). Das ist zwar schön für uns .... aber eine derart veraltete Spenden-Seite wirft ein schlechtes Licht auf uns!

klar machen: Diskusssion ist abgeschlossen

Da die RfCs immer noch als eine wichtige Ebene in unserer Dokumentation gelten, sollte ihr Inhalt nicht gleichgültig sein, sondern auf Relevanz geprüft werden.

bisweilen ist die Formulierung so unbeholfen, daß das Gemeinte kaum verständlich ist; manchmal fehlt etwas Kontext und ein paar Hinweise wären sinnvoll

Es gibt einige RfCs aus der allerersten Zeit, die im Rückblick gradezu visionär wirken — oft aber ist bei diesen Entwürfen die Tragweite nicht klar. Daher sollten solche RfCs behutsam durch Kommentare und Textredaktion nachgeschärft und eingeordnet werden. Nicht zuletzt auch deshalb, weil man weiterhin auf diese Einträge wird verlinken müssen.

eine Menge RfCs waren politisch aufgeladen

Es gab eine Zeit erheblicher Spannungen zwischen mir und Christian, die gekennzeichnet war von einem Ringen um den Stli des Projekts. Stil hier im weiten Sinn. Dem entsprechend finden sich viele doppelbödige Formulierungen, oder Vorschläge, die — nüchtern betrachtet — nachgerade durchgeknallt wirken. Auch für diese RfCs halte ich eine gewisse Einordnung für angezeigt. Das kann durch einen historischen Zusatz erfolgen, oder dadruch, daß ich sie in aller Form verwerfe und mir dadurch die Deutungshoheit in der Sache aneigne. Das kann bisweilen eine Gratwanderung sein — denn es geht mir nicht darum, zu siegen oder Recht zu haben, sondern es geht mir darum, das Projekt der Sache angemessen zu navigieren. Allerdings ist das, „was Sache ist“, ein Urteil, das ich fälle, nachdem ich duch die Sachverhalte durchgegangen bin, und zwar, sein vielen Jahren, allein.

Eine ganze Reihe der RfCs aus der allerersten Zeit gehören in diese Kategorie: sie entwerfen einen Arbeitsstil im Projekt, oft auch mit einer bestimmten Erwarung und Vorstellung bezüglich Formalismen und Automatisierung. Bezeichnenderweise stammen alle diese RfC von Christian. Einige wenige beschreiben Projekt-Konventionen, die sich tatsächlich auch so gehalten haben (z.B. die Anordnung der Repositories). Einge ganze Reihe weiterer Vorschläge erscheint mir komplett weltfremd (wie z.B. daß ein größeres Projekt funktionieren könnte, indem einfach jeder in Git eincheckt und nach belieben merged was gefällt, oder daß man jedwede Projektorganisation auf Git-commits zurückführen kann). Dem entsprechend hat auch nie jemand ernsthaft versucht, im Projekt so zu arbeiten. Das sollte dann auch im jeweiligen RfC vermerkt sein. Und schließlich gibt es einige wenige RfC, die die Organisation aus der Anfangszeit beschreiben; diese sind dadruch obsolet geworden, daß ich nun schon so ewig lange allein bin, und dem entsprechend anders vorgehen kann und muß.

explizit wieder in die Design-Phase zurück

habe ich schon vor einigen Jahren explizit verworfen

Dieser RfC ist wieder so typisch Christian: er besteht aus einem sehr naheliegenden und sinnigen Vorschlag (Tags verwenden, Tags automatisch generieren, Tag-Verzeichnis generieren). Aber dann besteht er größtenteils aus einer teils genialen, teils unausgegorenen Idee, die sofort absolut gesetzt wird: eine Ontology über Tags definieren, mit logischen Operationen darauf.

Diese Art Vorschläge bringen mich immer wieder in das gleiche Dilemma

  • dem einfachen Teil würde ich gerne zustimmen, er müßte aber konkret ausgearbeitet werden
  • zu dem weitgreifenden Vorschschlag wäre wohl erst mal ein Prototyp notwendig, aber in eienr Art, die nicht gleich endgülitig Fakten schafft und andere Möglichkeiten verbaut.

Und mit beiden Problemen läßt einen Christian dann allein; er hätte nur noch Interesse daran, einen extrem pfiffigen Prototypen für den weitreichenden Vorschlag ganz minimalistisch zu implementieren und dann das Thema abzuhaken. Ich finde mich dann immer wieder in dem ärgerlichen Dilemma: entweder ich lasse alle möglichen Vorschläge offen hängen, und dann laufe ich Gefahr, daß Christian irgendwann einfordert, das nun einfach mal zu akzeptieren (ohne weiter was für zu tun). Oder ich bin derjenige, der seine Vorschläge komplett verwirft, oder ich bin derjenige, der seine Vorschläge dann runter-strippt und implementiert, und mit den sich dann zeigenden konzeptionellen Problemen allein gelassen wird (so geschehen beim Lock-Cycle-detector, bei den Thread-Wrappern, seinem Interface/Plugin-System oder der Configuration).

Hier entscheide ich mich dafür, den Vorschlag komplett zu verwerfen, mit den Argumenten, die ich damals schon als Diskussionsbeitrag geschrieben habe. Und zwar mache ich das aus Sorge, daß später irgend jemand auf die Idee kommt, den Vorschlag mal zu implementieren — was bestenfalls nutzlos wäre, schlimmstenfalls aber andere, wichtige Einrichtungen behindert oder verdirbt (man denke nur mal daran, wenn ein Website-Cross-Link-Generator implementiert wird, aber nur limitiert auf die hier vorgeschlagene Semantik)

Dieser RfC ist ein Kuriosum — eine »Duftmarke« aus der ersten Zeit.

Christian hat das vorgeschlagen im März 2008; damals waren wir schon in einer festen Projekt-Organisation als »Lumiera« unterwegs. Ich kann mich definitiv nicht mehr erinnern, was ich damals von dem Vorschlag gehalten habe. Ich weiß noch, das Christian eine heftige Aversion gegen Bugzilla hatte (und wohl auch gegen jede Art von Ticket-Management).

Diese Erinnerung schreibe ich jetzt auch in das Ticket rein, denn was soll man sonst damit machen? So für sich betrachtet (aus gegenwärtigem Kontext) erscheint es nur absurd, einen solchen RfC überhaupt zu formulieren, aber ich sollte Christian den Kredit geben, daß er ein Problem gesehen hat.

Jedenfalls klar ist, wenig später hatten wir ein Trac, dafür habe ich gesorgt, und Christian hat es zwar anfangs auch genutzt, aber letztlich immer für "Ichthyos Ding" gehalten.

daher müssen die meisten RfCs aus heutiger Sicht neu beurteilt  werden

Diese dokumentieren den Stil und das »Mindset« aus der Zeit der entstehenden Bewegung, aus der später das Lumiera-Projekt wurde. Typisch für diese RfCs ist, daß sie ein wesentliches Element des Stils im Projekt bereits vorgreifend darstellen, aber typischwerise auch total „über das Ziel“ hinausschießen, indem Methoden und Verfahren vorgeschlagen werden, oder gar schon als gelebte Praxis deklariert werden, welche — nüchtern betrachtet — in unserer gegenwärtigen Zeit nicht funktionieren und ihren Zweck niemals erfüllen können. Solche RfC müssen explizit eingeordnet werden

⟹ heißt: hier hat er eine Vision, die ihm wichtiger als alles andere ist

...ich fand Christian's Vorschläge in diese Richtung immer nur etwas „sonderbar“ und realtitätsfern, meist so völlig überzogen, daß ich nur „ja ja“ gesagt habe (wohl wissend, daß nichts konkretes folgend wird). Dumm gelaufen. Jetzt haben wir ein ganzes Bündel von solchen RfCs, und alle wurden entweder durchgepresst, oder durchgewunken, ohne weitere Diskussion. Ausnahme: die »Semantic Tags« von 2012. Da habe ich im Dev-Meeting kontra gegeben, und es kam bereits zueiner überraschend hitzigen Diskussion.

"points define a periphery" ...  möglicherweise hilft es, nicht an den jeweiligen Vorschlägen hängen zu bleiben, sondern mich zu fragen: worauf will Christian hinaus?

  • sein allererster RfC war also, daß wir alle Bugtracker, und sonstigen Orga-Tools durch Git + Automatisierung ersetzen
  • das nächste war das Manifest. Darin ging es inhaltlich nicht um Cinelerra, sondern darum, ein Projekt "möglichst einfach und zugänglich" zu machen. Jeder soll tun was er für richtig hält, und dadurch lösen sich alle Probleme von selber (wenn man die Leute nur durch clevere Automatisierung und Git zusammenschaltet ⟹ das ist die Idee der »Unsichtbaren Hand« aus dem Liberalismus)
  • dann hat er x weitere Aktionen und RfCs gestartet, die alle darauf hinauslaufen, eine Infrastruktur zu schaffen (dem Projekt hat das enorm geholfen)
  • und dann hat er monatelang überhaupt nichts beigetragen, sondern hat sich mit vollem Eifer in das uWiki-Projekt geworfen, aber es exakt in dem Moment aufgegeben, als klar war, daß es nicht genügt, alles über einen eleganten Bootstrap zu verschalten, sondern daß tatsächliche Knochenarbeit (Webentwicklung) notwendig wäre. Nicht daß Christian nicht hart arbeiten kann, aber wenn man sich durch dicke Bretter bohren muß und Mengen von Details ordnen, dann ist das nicht seine Vision, sondern wohl ehr eine Bankrotterkärung.
  • danach hat er eine Menge Infrastruktur im Backend hochgezogen, die nur darauf zielt, die Arbeit für den Entwickler möglichst einfach zu machen, und dafür bestimmte Schemata global enforced. Er hat dann immer eingefordert, daß sich alle von jetzt an daran halten müssen, denn sonst wirkt das ja nicht
  • als nächstes kam das Thema "Plug-ins" + Lua. Auch das zielt darauf, alles möglichst modular, flach und einfach zugänglich zu machen, um aufgrund jeder neu auftauchenden Idee alles mit allem neu verschalten zu können
  • kurz darauf hat Christian mehrere Initiativen gestartet, den globalen Applikations-Rahmen "in den Griff" zu bekommen. Mich hat das sehr empört (und ich hab entsprechend giftig reagier), denn er hat meine bereits geleistete Arbeit komplett ignoriert und dann nach meinem Hinweis als "Overengineering" beiseite geschoben. Ich hab das nicht einordnen können, denn es war offensichtlich nicht persönlich gemeint. Sondern er wollte Dinge beiseite schieben, die seine Vision stören
  • sehr ähnlich lief's dann beim Config-System, wie ich jetzt aus den Meeting-Summaries rekonstruieren konnte. Alle anderen Devs haben hin darauf hingewiesen, daß es zu dem Thema zig ausgereifte Lösungen gibt. Darauf ist er nicht einmal eingegangen (und hat sich auch nicht erkenntlich mit diesen Lösungen auseinandergesetzt). Was ich so deute, daß er an das Thema grundsätzlich ganz anders rangehen wollte. Christian zeigte auch keinerlei Interesse an einer Requirement-Analyse, was ich so deute, daß er das Thema als Vehikel gesehen hat, um seine Vision voranzubringen. Auch der bestehende Implementation-Draft bestätigt diese Lesart: er ist bezüglich der Nutzbarkeit derart rudimentär, daß man ihn grade eben mit Tricks durch einen Unit-Test prügeln kann. Aber ein elaboriertes System von Matching- und Ersetzungregeln direkt in der Stringverarbeitung ist sehr detailliert durchdacht und angelegt worden.
  • Auch später noch, nachdem Christian schon nicht mehr aktiv beigetragen hat, kamen als Vorschläge zu aktuellen Problemen manchmal unglaublich elegante Abkürzungs-Lösungen (wie die Linkfarm), deren Stärke darin besteht, das eigentliche Problem aus den Angeln heben. Meist aber kamen irgendwelche Vorschläge für Automatisierung und clevere Textersetzungs-Regeln (Semantic Tags, Git commit message format, Vorschläge, Metadata-Hooks als Overlay direkt in die Datenstrukturen im Backend einzubauen, Vorschläge für Caching an allen möglichen Stellen, oder die Idee, Tests elaboriert automatisch zu generieren)

Ich war jetzt lange Zeit allein, und insofern waren die RfC egal, denn ich weiß ja, was geschehen ist. Da ich aber nun das Ziel ins Auge fasse, ein Team aufzubauen, ändert sich die Situation. Ich muß nun dafür sorgen, meine Deutungshoheit zu ratifizieren. Ich habe diese Deutungshoheit durch harte Arbeit erlangt, nicht durch Magie. Alle entscheidenden Durchbrüche in den letzten Jahren beruhen vor allem auf Denkarbeit, also viele Stunden Reflexion, die auch aufgeschrieben und wieder gelesen wird.

Meine Haltung zu dem, was Christian's Vision sein könnte (letztlich meine Auslegung!), hat sich in den letzten Jahren geändert. Und zwar von „schaug' mer mal was es in Beweung setzt“ auf „ich sehe jetzt wohin das führt und ich will nicht in dieser Zukunft leben, sondern in einer anderen“.

Konsequenzen

  • Ich *DEUTE* Christians Vision als Teil eines Trends zur Industrialisierung. Das ist etwas, was mit den Menschen geschieht, ohne daß es absichtsvoll geplant wird.
  • Ich *NAVIGIERE* das Projekt in Richtung einer möglichen Dialektik dazu (⚠ Achtung: nicht in Richtung von Tradition und Konservativismus; ich gebrauche den Begriff »Handwerk« als Vehikel).

Diese forumulieren wesentliche Elemente der Vision aus, unter der das Projekt begonnen wurde. Einige dieser RfC wurden sofort beschlossen — und die Realität in der Codebasis hat sich dann ganz anders entwickelt. Andere RfC wurden erst mal auf „noch mehr Arbeit erforderlich“ gestellt — was in jedem Fall realisitischer ist, denn in der Regel ist ein weiter Weg notwendig, um dieser Vision auch nur nahe zu kommen. Diese RfC müssen also explizit von mir neu beurteilt werden

Diese RfC gibt es in verschiedensten Formen, von einer kurz hingeworfenen Idee bis zu einem elaboriert ausgearbeiteten Konzept. Allen gemeinsam ist, daß man sie alsbald beiseite geschoben hat, da den Entwicklern eigentlich von Anfang an klar war, was für ein weiter Weg vor uns liegt. Diese RfC können getrost liegen bleiben, so wie sie sind.

Diese RfC sind allesamt mit Konflikten verbunden, und wurden nur deshalb formuliert, weil der Initiator schon ahnte, daß es Widerstand gegen diese Idee geben würde. Hier kann man zwei Klassen beobachten

  • einige RfC sind so verschwurbelt, daß ohne Kenntnis der damaligen Situation überhaupt nicht erkenntlich ist, worum es überhaupt geht; diese RfC wurden meist sehr kontrovers diskutiert, bisweilen sogar per Winkelzug durchgeprügelt und als Konsens dargestellt.
  • die sonstigen RfC dieser Art sind extrem elaboriert geschrieben, so daß man eigentlich nichts mehr dagegen sagen kann; verräterisch ist nur, daß der Diskussionsteil komplett fehlt, oder nicht ernst gemeint ist.

Der Beschluß-Status dieser RfC ist fragwürdig. Tatsächlich wurde über alle diese Vorschläge eine faktische Entscheidung von mir getroffen, indem ich eine Richtung eingschlagen habe, die ich für richtig halte und begründen kann. Die notwendige Diskussion mußte ich dazu mit mir selber führen und ein formaler Beschluß war nicht mehr möglich. Im Ergebnis sind nun praktisch alle charakteristischen Vorschläge von Christian von mir verworfen worden und die allermeisten meiner Vorschläge wurden weiterentwickelt und dabei jedoch substantiell verändert. Insofern ist für diese RfC eine Klarstellung notwendig

Der eigentliche RfC schlägt nur vor, Lua zur verbindlichen Scriptsparche zu machen, und C-Bindings einzurichten. Die Kommentare ergeben dann aber, daß Christian diesen RfC sehr wohl im Zusammenhant mit seiner Plugin-Vision gesehen hat, und tatsächliche alle internen Interfaces öffnen und von Lua aufrufbar machen wollte.

Dieser Zusammenhang haftet dem RfC nun an, und auch meine Ablehnung stellt darauf ab ⟹ es ist nicht sinnvoll, diesen RfC zu „reparieren“

Effektiv habe ich die »Plugin-Architektur« nun beerdigt. Es wird kein generelles Interface-System geben, sondern nur ein neues Konzept für Plugins und Feature-Bundles

Ich habe vor 2 Jahren beschlossen, daß das System der C Error-States nur noch mitgeführt wird, aber den Exceptions untergeordnet ist. Das bedeutet, perspektivisch werden Ausnahmen nur noch per Exception signalisiert, und es ist nicht mehr akzeptabel einfach mal so eine Flag zu setzen (auch wenn sie Thread-local ist). Man sollte nicht mehr erwarten, daß irgendjemand einen Lumiera-Error-State prüft

Dieser Content erweckt ein falsches Bild. Ich werde dafür sorgen, daß niemand mehr ein »C-Ökosystem« aufmacht. Imperativ programmieren kann man auch in C++, und mithilfe der Standardlibrary. Die wenigen Verwendungen der hier aufgeführten Library-Datenstrukturen von Christian werden mit dem Config-Loader wegfallen. Diese C-Library ist aus der Dynamik der Anfangszeit entstanden, aber seit 2010 trage ich nahezu die gesamte Entwicklung allein, und setze daher die Maßstäbe.

Name + Datum als Asciidoc-Delimited-Block

also mit + einleiten und dann mit -- umschließen

...das ist vermutlich ein Fehler in der MoinMoin-2-Asciidoc-Konvertierung gewesen: viele Diskussionsbeiträge beginnen mit einem "--", was von Asciidoc u.U. als Beginn eines »delimited block« interpretiert wird

...wodurch sie Asciidoc nicht erkennt, sondern dem vorhergehenden Absatz zuschlägt

...das geht vermutlich zurück auf das Moin-Moin-Wiki von Cehteh: Dort war es üblich, Stichworte mal vorsorglich als Link zu schreiben; in wiki-typischer Weise wurden daraus dann nicht-existente Seiten, die man sukzessive erstellen konnte. Duch die automatische Umwandlung in Asciidoc sind daraus leider sehr viele Links nach dem Muster link:blablubb[]  enstanden.

...die dann aber dem gekennzeichneten Inhalt widersprechen können

After a talk on IRC ichthyo and me agreed on making lumiera a multi language

project where each part can be written in the language which will fit it best.

Language purists might disagree on such a mix, but I believe the benefits

outweigh the drawbacks.


ct:: '2007-07-03 05:51:06'

In die kleine Tabelle im Kopf des RfC wird beim Anlegen ein Timestamp gesetzt; dieses Datum erscheint stets plausibel, Timestamps in Kommentaren sind zeitnah, aber etwas später. Oft wurden RfC auch in developer-meetings auf IRC besprochen; auch das ergibt ein schlüssiges Bild.

⟹ alle RfC lassen sich grob einer Phase des Projekts zuordnen

war trotzdem ein Monster-Aufwand

....aber mußte mal sein; der Zeitpunkt erscheint mir richtig, denn ich ziehe anscheinend nun einige Trennlinien explizit und spreche Entscheidungen aus. Denn wenn es mir gelingt in dem Projekt wieder etwas in Bewegung zu setzen, werde ich alsbald für diese art Arbeit keine Zeit mehr haben!

Also es ist definitiv so, und zwar in jeder Installation die ich sehe. Jetzt kann ich mich aber gar nicht mehr erinnern, wie Benny auf diesen Vorschlag kam. War das nur eine rein-theoretische Überlegung, ist es in einer Diskussion passiert, ohne daß wir die Website gesehen haben (ich erinnere mich ganz dunkel, daß wir das in Bernbach besprochen haben)

⚠ Achtung: im Website-Repo

commit a32d3e0f7caf8d905e3203608c426953f85fd6e4

Author: Ichthyostega <prg@ichthyostega.de> 2013-10-26 23:55:23

Committer: Ichthyostega <prg@ichthyostega.de> 2013-10-26 23:55:23


Menu: attach the Doxygen node also into the documentation subdir

Da ich "autobrief" verwende, ist der erste Satz stets auch die Kurzbeschreibung. Zudem stelle ich die Langbeschreibung stets an den Anfang der Seite, weil ich dies für die nützlichste Info halte. Die ganzen sonstigen Member-Listen sind ehr verwirrend

Das ist im Grunde die Formulierung aus der DEB-Paketbeschreibung, also eine knappe Charakterisierung was Lumiera ist, aus Sicht eines prospektiven Benutzers.... Aber der erste Satz wirkt aufgebläht und auch der Hinweis auf auf den Status eines Development-Snapshot erscheint aufgebläht und etwas redundant.

...denn das erscheint mir immer noch ziemlich aus der Zeit gefallen; mit gegenwärtiger Software kann man nichts sinnvolles anfangen ohne Internet-Connection; daher ist eine ausführliche Bauanleitung viel angebrachter auf der Website. Das README gibt nur eine Zusammenfassung...

  • mit was man es zu tun hat
  • was man weiter noch braucht um den Code zu bauen
  • wo man mehr Informationen und Anleitungen findet

...ein Commit-Log einer größeren Applikation ist für sich allein nicht aussagekräftig; es muß in den Kontext gestellt werden durch ein offiziell verbindlich vorgegebenes Urteil wo das Projekt steht und wohin es sich bewegt.

alle diese Preview-Releases wurden bisher bedingt durch Änderungen im Kontext, wie z.B. der Umstand, daß ich allein die Zügel tatsächlich übernommen habe, oder der Upgrade auf eine neue Refernz-Plattform und höhere Sprachstandards. Zwar wollte ich das immer, aber es ist mir nie gelungen, diese Releases auch mit Erreichen eines Meilensteins zu koppeln. Zudem haben wir noch keinen der großen Meilensteine erreicht.

NEWS sollte neben einem Datum auch die Versionsnummer tragen

Das hab ich neulich schon im Rahmen der Website überlegt und so beschlossen; ich kann das Aggregat re-lizensieren unter einer kompatiblen Lizenz (eigentlich könnte man das auch für GPL2+ ⟶ GPL3+ so machen). Auch Stackoverfow hat seine Lizenz für »Subscriber Content« auf CC-By-SA 4.0 gehoben

Denn ich mußte mir das heute schon wieder zusammensuchen...

  • die SCons-Kommando-Argumente werden im Konstruktor des Environment-Objekts geparst
  • die erwartete Argument-Definition kommt dorthin im Keyword-Argument 'variables'
  • wir definieren sie in unserem Options-Objekt
  • weitere key=value-Bindings können als zusätzliche KW-Args mitgegeben werden
  • im Besonderen diejenigen, die aus 'optcache' geladen wurden
  • alle diese Variablen (incl. der von der Kommandozeile) werden 'build variables'
  • diese können per env.subst(string) interpoliert werden
  • bei eingebauten Kommando-Aufrufen macht das SCons automatisch

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

Daher hieß das Projekt anfangs auch Gnome-Design-Library, und enthielt verschiedene Zusatzfunktionen, die dann vom Toolbuilder (»Glade«) direkt in den generierten Code integriert wurden. In Vorbereitung der Umstellung auf CSS für GTK-3 hat man bereits einige Jahre vorher in dem Bereich „aufgeräumt“ — und dann blieb nur Funktionalität übrig, die von diversen GTK2-Projekten extern genutzt wurde: die flexiblen Docks

Und in dem Zusammenhang hat unser damaliger GUI-Entwickler, Joel Holdsworth, diese Lösung mitgebracht. Er war auch im Kontakt mit den Anjuta-Leuten, und hat an der Vorbereitung für einen GTK-3-Port mitgeholfen.

gdl no longer has any reverse dependencies in Debian and isn't really
actively maintained upstream. Please remove gdl from Debian.

2021-09-22

...und zwar, weil GTK-3 bereits stabilisiert und im Wartungsmodus ist; die Entwickler haben längst kein Interesse mehr daran, während ein Großteil des etablierten Ökosystems noch mit dem Übergang von GTK-2 auf GTK-3 hadert....

diese sind "distclean", also autoconf bereits ausgeführt

Also die Paketierung selber nicht ändern (ich hatte die ohnehin von Glibmm abgeschaut, ohne mich in die Details einzuarbeiten) ... somit bleibt dieses Paket auf CDBS

irgend etwas im Build führt folgendes Kommando aus:

echo 7 >debian/compat

Daraufhin bricht der Build ab, da nun zwei widersprüchliche Compat-Level angegeben sind

Im Sinne von Redundanz und Transparenz sollte ich im Lumiera-Git-Repo auf jeden Fall die ganze Master-Historie mit einschließen ... (ka man könnte, aber ich lasse die Historien unverbunden so bestehen wie sie waren, nicht zuletzt auch der signierten Build-Tags wegen)

viele DD betrachten CDBS inzwischen als einen »code smell« — der neue dh-Sequencer hat die Herzen im Sturm erobert....

Als einzige Ausnahme gilt das Haskell-Ökosystem, in dem wirklich wichtige Einrichtungen nur in CDBS gepflegt waren

...soweit ich sehen kann sind nämlich alle relevanten Pakte aus dem Gnome-Umfeld auf Meson umgestellt, selbst wenn sie dann doch irgendwie noch auf Autotools delegieren; das sind Komplexitäten die hier unangemessen scheinen, da das Quellpaket ja ein funktionsfähiges Buildsystem hat und debhelper direkt mit Autotools umgehen kann

override_dh_auto_configure:
    dh_auto_configure -- --prefix=/opt/rawau

override_dh_autoreconf:

               dh_autoreconf --as-needed

   dh_auto_test

make -j8 check "TESTSUITEFLAGS=-j8 --verbose" VERBOSE=1

make[1]: Entering directory '/pack/gdlmm3-3.7.3'

Making check in gdl/gdlmm

make[2]: Entering directory '/pack/gdlmm3-3.7.3/gdl/gdlmm'

make[2]: Nothing to be done for 'check'.

make[2]: Leaving directory '/pack/gdlmm3-3.7.3/gdl/gdlmm'

Making check in examples

make[2]: Entering directory '/pack/gdlmm3-3.7.3/examples'

make  dock

make[3]: Entering directory '/pack/gdlmm3-3.7.3/examples'

....

Arguments passed directly to dh_makeshlibs, for a particular package <package>

konkret dienen die Zusatz-Argumente dazu, bestimmte Paket+Versions-Constraints in das Shlibs-File zu bekommen, da bisher für dh_makeshlibs als default galt -VNone — aber ab Standard 11 wurde der Default geändert in -VUpstream-Version — was konkret einen Eintrag erzeugt der Form »Paketname  (>= Paketversion)«

...das ist eine generelle Regel: man sollte sich nur um die Kernbelange kümmern, aber nicht versuchen, auf allen möglichen Nebenschauplätzen seine eigenen Akzente zu setzen. Mag ja sein, daß hier xz etwas besser komprimiert, aber ist das nicht ehr ein Thema für die Betreuer des DEB-Paketvormats?

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

streng logisch hat der Compiler recht: ich kann ein non-copyable-Objekt nicht aus einer Build-Funktion erzeugen, weil ja die Rückgabe eine »Kopie« ist. Aber die Sprache C++ garantiert die RVO, und deshalb wird das Objekt zwar im Code der Build-Funktion erzeugt, aber tatsächlich bereits an der endgülrigen Storage-Location am Zielort im aufrufenden Kontext.

Es ist nämlich tatsächlich nicht ganz korrekt:

  • ich habe ein Member-Filed (im TurnoutSystem), das auf den front-end-Typ lautet, nicht auf den technischen Basistyp — also HeteroData<A,B,C>, und nicht HeteroData<Node<StorageFrame, NullType>> 
  • aber die Builder-Funktion mit RVO erzeugt den technischen Basis-Typ
  • das bedeutet: beide sind zwar storage-kompatibel, aber streng genommen muß eine Initialisierung des abgeleiteten Typs von einem Objekt des Basistyps stattfinden, und das kann man drehen und Wenden wie man will, das ist eine Copy-Operation

...und dafür auch den Basis-Konstruktor explizit sichtbar machen

...das war ein Punkt, den ich neulich für Yoshimi gelernt habe: gewisse Header sind letzten Endes doch pervasiv vorhanden, und man gewinnt nichts durch ein fragiles Konstrukt mit Forward-Deklaration. Also Augen zu und das Ding fressen

Feststellung: das ist eine deutlich überarbeitete Version vom gleichen Code

Da hier offensichtlich eine Kontinuität besteht und die öffentliche SCons-Website (wenngleich um mehrere Ecken) auf dieses Tool verweist, erscheint es mir angezeigt, eine vollständige Historie bereitzustellen — wenngleich auch der Code selber direkt von Lumiera in-Tree ausgeliefert wird.

alle diese Commits sind von Richard van der Hoff<richard@richvdh.org> am 12.8.2013

wenn man sich die Zeiten und Änderungen etwas anschaut, dann waren das experimentelle Fixes oder Verbesserungen, die am Ende des Tages irgendwie zusammengehängt wurden.

Denn der erste Code vor 2007 war im Scons-Wiki gespeichert, auf einer frei editierbaren Seite, auf der verschiedene Leute Fixes und neue Features vorgeschlagen haben. Es gab wohl mal ein eMail-Archiv, aber das ist schon vor langer Zeit verlorengegangen....

Commit Roussel Wilder on Mar 7, 2010

Dieses Commit-Datum geht hervor aus einem Mirror in https://github.com/ptomulik/scons_doxygen/commits/master/notes.txt

The original plugin file is the file posted to http://www.scons.org/wiki/DoxygenBuilder by Matthew Nicolson
2006-03-31.  Attached to this wiki page were two other files, both of which are in the repository history
although removed from being in the working tree: doxygen_reiners_2007-02-26.py, and
doxygen_boehme_2007-07-18.py.  Boehme's version seems to include many of the changes in Reiners but many of
Reiners changes are missing from Boehme.  It is not clear why Reiner's changes have been reverted by Boehme.
it seems appropriate to merge in Boehme's directly rather than Reiner's and then Boehme's.

Robert Smallshire in his email of
http://scons.tigris.org/ds/viewMessage.do?dsForumId=1272&dsMessageId=2383574 supports the move to use
Boehme's version as the next iteration.  It may then be that a version he has can be merged in.

Apart from one change to the builder command line, Norton's version seems to be Boehme's version with all
the TAGFILE stuff removed.

Since then others have made contributions, see the log for the history.

Inhalt aus dem Archive.org-Snapshot

ich verwende einen Zwichen-Branch, um nur den Inhalt von admin/scons mit dem Subtree-Branch sconsDoxy zu verbinden

git+ssh://ichthyo@git.lumiera.org/git/libs/scons-contrib

das wirkt zwar offensichtlich im ersten Teil, wenn die source-Files erstmals geparst werden, denn dann werden (nur mit diesem Setting) alle Cores zu 100% ausgelastet

...und zwar in der der _speziellen Form mit Unterstrich._  (meint: der Unterstrich steht hinter dem Punkt, und damit nicht mehr im \brief -Teil

(wird als HTML-Trag geparst)

das Ergebnis ist letztlich immer wieder enttäuschend

Doxygen (wie auch diverse andere Sourcecode-Scanner und Validatoren) passen konzeptionell nicht sonderlich gut auf die spezielle Struktur von C++ Code, welcher sehr stark auf Scopes und Querbezüge setzt. Und mein eigener Code-Stil trägt dazu auch noch einiges bei. Meistens sind nur die Texte in den Header-Kommentaren brauchbar. Es folgt dann eine mehr-oder-weniger willkürlich wirkende Liste von Klassen und namespace-Membern. Eine solche Liste erzählt keine Geschichte (der Code tut es schon). Und der Umstand, daß in Doxygen die Struktur flach geklopft wird, tut ein Übriges

das heißt, um Generator-Kostrukte an der Aufruf-Stelle ein list(gen) wickeln...
das ist OK, aber oftmals nicht schön — geht es besser?

Beschluß: wir gehen auf 3.10

Was war denn überhaupt der Sinn der vgsuppression? Welche Aufrufe sollten damit ausgenommen werden? warum brauchen die die Tests???

Die »klassischen Hacker« waren seinerzeit unbedingt davon überzeugt, daß wir viel mit Valgrind arbeiten müssen, weil wir ja sonst Probleme mit Memory-Leaks „nie in den Griff bekommen“. Ich dagegen wollte immer ein deterministisches Memory-Management, und hab mich mit diesem Ansatz durchgeführt. Erste Versuche mit Valgrind waren nicht sonderlich hilfreich. Vor allem wegen Dingen wie dem "MPool", der nicht deterministisch ist. Ebenso hat Christian irgendwann einmal einen "Leak-Checker" geschrieben, und wollte den eingebunden haben. Dann kam heraus, daß sein C-Code leakt, aber der C++-Code nicht, weil die smart-Pointer per Konstruktion »wasserdicht« sind. Daraufhin hat Christian schlagartig das Interesse an dem Thema verloren. Und sich auch nie um eine brauchbare Konfiguration der vgsuppression gekümmert. Ich hatte darauf auch keinen Bock, denn das ist eine endlose Knobelei, und wozu? Ich weiß ja daß die smart-pointer nicht leaken.

searchpath.cpp :  replaceMagicLinkerTokens()

SCons würde nämlich diese $ORIGIN ebenfalls versuchen, zu expandieren

...dies dient dazu, Defaults aus dem 'optcache' zu laden

...hab heute schon wieder ganzschön lang gebraucht, um mir das zusammenzupuzzeln (und ich kenne SCons und unser Buildsystem)

Hauptsache, man hat irgendwas total trickreich eingefädelt, so daß man sich später von der linken Tasche in die rechte Tasche spielen kann

und exakt so machen wir es bereits

das Config-System ist nur noch pro forma da

lumiera_get_plugin_path_default()

Nein! denn unser Buildsystem ist nicht Autotools oder CMake

das beruht aber auf einer Mystifikation

...weil es dadurch zusätzliche Seiteneffekt-Abhängigeiten gibt, die man dem System nicht »ansieht«

...das dann auch die üblichen Overlay-Ebenen hat (Paket, System, User)

Für den normalen Build ist das irrelevant; erst wenn wir das Install-Target aufrufen, passiert da etwas. Und dann sollte das auf Betriebssystem-Ebene geregelt sein (und das erfordert i.d.R. auch Root-Rechte)

Das ist so eine ganz typische Paketbau-Funktionalität: prüfen ob das Zielverzeichnis existiert, richtige Rechte hat und leer ist — oder eben einige dieser Kriterien grade nicht prüftn

gdl-dock-item.c: In function 'gdl_dock_item_class_init':

gdl-dock-item.c:358:44: error: passing argument 1 of 'gdl_dock_object_class_set_is_compound' from incompatible pointer type [-Wincompatible-pointer-types]

  358 |     gdl_dock_object_class_set_is_compound (object_class, FALSE);

      |                                            ^~~~~~~~~~~~

      |                                            |

      |                                            GObjectClass * {aka struct _GObjectClass *}

In file included from gdl-dock.h:26,

                 from gdl-dock-item.c:38:

../gdl/gdl-dock-object.h:354:74: note: expected 'GdlDockObjectClass *' {aka 'struct _GdlDockObjectClass *'} but argument is of type 'GObjectClass *' {aka 'struct _GObjectClass *'}

  354 | void          gdl_dock_object_class_set_is_compound (GdlDockObjectClass *object_class,

      |                                                      ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~

gdl-dock-item.c: In function 'gdl_dock_item_set_property':

gdl-dock-item.c:747:36: error: initialization of 'GObject *' {aka 'struct _GObject *'} from incompatible pointer type '

 *' {aka 'struct _GtkWidget *'} [-Wincompatible-pointer-types]

  747 |                 GObject * parent = gtk_widget_get_parent (GTK_WIDGET (item));

      |                                    ^~~~~~~~~~~~~~~~~~~~~

gdl-dock-bar.c: In function 'gdl_dock_bar_set_master':

gdl-dock-item.c: In function 'gdl_dock_item_realize':

gdl-dock-bar.c:428:31: error: assignment to 'GdlDockMaster *' {aka 'struct _GdlDockMaster *'} from incompatible pointer type 'GObject *' {aka 'struct _GObject *'} [-Wincompatible-pointer-types]

  428 |         dockbar->priv->master = g_object_ref (master);

      |                               ^

und dann wird ein GdlDockMaster* darauf gesetzt mit Refcount

und zwar als Feld dockbar->priv->master

gdl-dock-layout.c: In function 'gdl_dock_layout_set_master':

gdl-dock-layout.c:623:30: error: assignment to 'GdlDockMaster *' {aka 'struct _GdlDockMaster *'} from incompatible pointer type 'GObject *' {aka 'struct _GObject *'} [-Wincompatible-pointer-types]

  623 |         layout->priv->master = g_object_ref (master);

      |                              ^

Es ist halt C, und früher war mal der Kontrakt, daß einen C nicht nervt wenn man eh weiß, wohin ein Pointer zeigt. Aber die Zeiten ändern sich; jetzt wird sogar C penibel, und das führt dann dazu, daß man jede Menge herumcasten muß, weil C keine Subtypen-Relation kennt. Die angezeigten Fehler passen allesamt auf dieses Muster; ein kurzer Blick in den Code genügt, und man sieht, daß das gleiche Objekt gemeint ist

diese Deprecations werden alle zuschlagen, wenn man auf GTK-4 migrieren möchte. Vor allem das Wegfallen der Stock-Icons wird vmtl. einige Reorganisation nach sich ziehen....

(seufz). Ob es dann doch ein Problem gibt, sieht man bei diesem GTK-Zeug leider erst zur Laufzeit; die angezeigten deprecations allerdings betreffen lediglich einige Zugriffe auf die private-Properties eines Objekts (dafür gibt es m.W. jetzt einen anderen Zugang), sowie den Wegfall des »Stock-Icon«-systems

wie lib/regex.hpp, lib/result.hpp, lib/thread.hpp, lib/parse.hpp ....

...das war vermutlich mein erster Versuch, eine Iterator-Klasse in C++ zu bauen — hatte mich dabei offensichtlich an Java orientiert

Und zwar schon vor einem Jahr.

Grrr..... wo ist die Zeit hin??

nix auskommentiert oder deaktiviert — und das heißt, die aktuellen Dependencies sind hierfür ausreichend

jeder Displayer versucht eine minimale Initialisierung im Konstruktor und signalisiert damit, daß er im Prinzip Video ausgeben könnte

zeigt an, in welchem Format die Daten angeliefert werden müssen

der Displayer macht eine Vorgabe, kann aber durchaus (wohl in gewissen Grenzen) noch skalieren

virtual void put (void* const)

window = GDK_WINDOW_XID (area_window->gobj());

display = GDK_WINDOW_XDISPLAY (area_window->gobj());

Implementierung war extrem einfach

Nichts weiter als etwas Bit-Blitting.

Das heißt, ein Pixmap wurde via GDK zur Anzeige gebracht

die Anzeige erscheint nur marginal gebrochen

Mit dem DummyPlayer wollte ich damals demonstrieren, wie eine Zusammenarbeit über Facade-Interfaces funktionieren kann; das hat die ganzen Schwachstellen an Christian's Interface-Konzept schonungslos offengelegt — und war ein ziemliches Gewürge. Außerdem ist der Code alt, und verwendet einige Dinge wie den ScopedPtrVect, die ich gerne mal begraben würde.

man könnte den DummyImageGenerator

 + TickService wieder ins GUI ziehen

steht fest: wir können Video anzeigen

vor allem...

  • wie man tatsächlich einen Darstellungsvorgang erzwingt
  • wie das Compositing mit dem Window-Manager integriert wird

man hat ein time::Control (was auch immer das in Zukunft sein wird), welches mit einem Change-Signal verbunden wird. GUI-seitig verdrahtet man dieses Signal mit dem Code, der die tatsächliche Änderung des Presentation-State macht

ja ... der code ist anrüchig

Ursprünglich war das mal so eine Idee von Code-Reuse. Klingt plausibel, schließlich hat Ardour einen guten Ruf....

Bei genauerer Betrachtung allerdings zeigt sich, daß die Qualität von Ardour auf einer sehr gründlichen QA beruht, ein anderes Projekt (wie Lumiera) aber nicht davon profitieren kann, denn der Code enthält keine besonderen strukturell gefaßten Einsichten; ganz im Gegenteil, man würde sich eine Menge ungesunder Strukturmuster einhandeln, wenn man an solchen Code anbauen würde.

Abgesehen davon ist der Code inzwischen auch technologisch überholt und wird mit GTK-4 nicht mehr funktionieren

Anfangs waren wir von der Vorstellung ausgegengen, daß Lumiera zumindest ein elementares Raw-Video-Processing machen wird, und sich dabei an der Klassifikation und den Funktionen von Gavl orientieren sollte, also dessen »Domain Ontology«.

Inzwischen haben wir eine Architektur, die komplett »Library-agnostic« ist — jedwedes Processing wird an eine LIbrary weitergereicht. In der Basis-Ausstattung wird es sich bei dieser Library sehr wahrscheinlich um FFmpeg handeln, nicht um Gavl und Gmerlin. So schade das ist.

inzwischen nirgends mehr includiert — lasse diesen Header und die Library-Abhängigkeit dennoch vorerst bestehen — als Platzhalter (für eine »Domain-Ontology«)

raw_time_64

schon seit langem will ich von der "C-Time-Library" weg, denn diese ist gradezu eine Einladung, Timecode-Operationen ad-hoc zu implementieren

....weil eine solche Library gradezu dazu einläd, sich nicht mit den „mühsamen“ Abstraktionen des Time-Handling-Frameworks herumzuschlagen, sondern stattessen einfach mit Zahlen zu rechnen.

...nachdem gavl_time_t durch einen Typedef ersetzt ist, und ich das Thema durch #1261 bereits abgesteckt habe, könnte der Bestand einer eigenständigen Basis-Library durch wenige Umordnungen aufgehoben werden. Denn darum geht es mir: ich will in Lumiera eine Ordnung schaffen, in der gedankenloses einfach-mal-Machen keinen Raum findet.

...möglicherweise lassen sich diese Funktionen nämlich in Gruppen einteilen und dann direkt in einen anonymen namespace in die jeweilige Translation-Unit schieben....

Der Test täuscht: weil er in C geschrieben ist, sieht er so komplex aus. Dabei besteht die erste Hälfte lediglich draus, eine Konstruktor-Funktion zu testen. Der sinnvolle Kern daran ist die decimator-Sequenz zum Herunterbrechen von Zeiten. Die wird hier aber nur oberflächlich getestet. Da sollte man, wenn schon, wirklich auf die Grenzfälle losgehen ⟹ Test neu schreiben!

Der zweite Teil besteht im Antesten der drop-Frame-Umwandlung ⟹ auch das in eigenständigen neuen Test packen

zeigt besonders deutlich warum ich diesen Ansatz ablehne

und könnten vewendet werden,

das Framework zu umgehen

diese dokumentieren einen gewissen Standard-Algorithmus, mit dem man einen SMPTE-drop-frame-Timecode aufbauen könnte

Bekanntlich war »drop-frame« vor langer Zeit in USA eingeführt worden, um ein Elektronik-Problem mit dem NTSC-Farbfernseh-Standard zu entschärfen. Klassischer Pragmatismus: was uns schadet, kann doch keine Wahrheit sein!

Also handelt es sich im Kern darum, wie ein Frame-Count in ein klassisches Zeit-Schema gemappt wird. Das paßt nicht in den gedanklichen Rahmen, den ich ansonsten für Zeiten und Timecodes errichtet habe. Andererseits ist drop-Frame ein practical hack, und tritt meines Wissens nur auf im Zusammenhang mit SMPTE-Timecode — gehört also in eine vergangene Welt, in der die Verhältnisse einfach waren.

es ist dadurch entstanden,

daß man ausgehend von

»Basis-Operationen«

gedacht hat

Also zeigt sich wieder einmal: die zugrundeliegende »pragmatische Denkweise« ist zutieft fehlgeleitet. Die Realität ist kein Baukasten-System.

Hinter dem pragmatischen »hands-on« steckt ein Glaube an eine verborgene einfache Wahrheit. Nur unter dieser Annahme macht die Heuristik Sinn, daß man mal mit einem ausgedachten Exempel anfängt, dieses in Einzelschritte zerlegt, und dann behauptet, diese Einzelschritte wären Basis-Elemente.

Und exakt so wurde bei dieser C-Library vorgegangen: Exempel-1: wir bauen uns mal eine Zeit. Einmal mit alles-auf-Null, dann einmal mit »random numbers« (return 47). Dabei stößt man dann auf die Dezimator-Schritte, und da diese irgendwie elementar aussehen, verpackt man sie in Funktionen, und behauptet, man habe das Atom gefunden. Das führt dazu, daß man eine Rechnung in Schein-Abstraktionen verpackt, welche man in einer Implementierung auch ohne Weiteres direkt anschreiben könnte. Tatsächlich ist die Modulus-Funktion bereits eine echte Abstraktion, d.h. sie ist ausdrucksstärker als eine lumiera_get_seconds()-Funktion. Und im Hintergrund macht sich auf diesem Weg wieder die geläufige Vorstellung breit, eine Zeit „bestehe“ aus Stunden, Minuten und Sekunden. Das ist bereits ein Irrtum, Time is fleeting. 

  • auch hier sollte vom tatsächlichen Nutzen ausgegangen werden...
  • Vorrausetzung wäre, das Verhältnis zu ISO8601 zu klären, d.h. verwenden wir H:M:S tatsächlich, um auch Millisekunden darzustellen? möglicherweise sogar konfigurierbar bis auf das µ-Grid hinunter

Solche Komponenten wären z.B. ein Editor für Marker-Positionen, für Clip-Längen, für Keyframes, für Effekte mit temporalem Aspekt. Aus diesem Kontext würde sich dann ergeben, wie mit der Format-Auswahl eines Timecode-Widget umgegangen werden soll, und daraus würde sich ergeben, wieein Timecode-Widget mit einem bestimmten Timecode verschaltet wird (und auf dessen Digxel zugreifen könnte). Erst von dort würde klar, welches API ein Timecode tatsächlich bieten muß. Im Moment wissen wir nur, daß ein Timecode auch als String gerendert werden kann....

...wenngleich auch durch die Hintertür — was aber egal ist, wenn man drin ist, is man drin

und markiert als @deprecated

gebrochen ist es ja ohnehin und sowiso, und weiter damit arbeiten möchte ich nicht — ganz ehrlich, das war ein »Griff ins Klo«, das ist kein Code, den man erhalten sollte.

Zeiten sind bereits µ-Tick-quantisiert, d.h. eine einfache Division über ein Grid ist stets im Integer-Value range und ohne Fehler ausführbar

und zwar wegen der Gefahr numerischer Overflows; die eingebaute Limitierung der Lumiera-Time ist nicht ausreichend: denn die Quantisierung muß die Framerate durch das µ-Grid dividieren, also den Zähler mal 10^6 nehmen

...und das macht durchaus Sinn, grade wegen der starken Limitierung, die ich bisher in diesem Fall demonstriert habe; außerdem ist diese grid-local-Time ohnehin etwas sonderbar, und auch daher ist es sinnvoll, diese als Offset (gegenüber dem Origin) zu modellieren. Sofern der Benutzer in eine normale Lumiera-Time speichert, sind wir wieder zurück bei den alten Limitierungen, aber man kann eben mit diesem Offset auch weiterrechnen, und ihn z.B. zum Origin dazuaddieren (und würde dann im Beispiel wieder bei ganz kleinen Zeiten ankommen)

...und die Grenzen ausreizen, die das Zeit-Framework (bewußt) erlaubt, wenn man die Typisierung geschickt ausnutzt: denn ein TimeValue wird aus einer anderen Zeit-Enität (absichtlich) ohne weiteren Bounds-Check übernommen. Es sind mithin durchaus TimeValue möglich, die größer sind als Time::MAX

das ist der Ansatz mit den Feature-Flags in Metaprogramming.

Dieser Weg ist definitiv obsolet!

Habe stattdessen die Feed-Manifold, und der flexible Teil besteht nun Richtung Weaving Pattern und Domain Ontologies

»Commands« sind selber im Schwebezustand

Zwar halte ich Commands nicht für insgesamt obsolet — wir werden so etwas definitiv brauchen, da unser GUI per Messaging angebunden ist. Aber es ist bisher unklar geblieben, wie die Schnittstelle zur Sessinon tatsächlich aussehen wird, auf der die Command-Funktoren dann einmal arbeiten sollen. Zudem waren die Commands auch als Teil eines »Command-Systems« gedacht, welches auch UNDO und REDO ermöglicht. Und in dieser Hinsicht bewege ich mich schon seit längerer Zeit in Richtung auf Events und Event-sourcing. Damit wäre ein Großteil der Komplexität im bestehenden Command-Framework hinfällig

pfui ... einen Konstruktor-Fehler unterdrücken und stattdessen ein default-konstruiertes Objekt unterschieben, und das auch noch in einer Funktion, die emplace() heißt...

ReplacableIterm ist schlampig definiert

...denn er würde ReplacableItem in ein ReplacableItem einpflanzen — also ein Stockwerk zu viel

wollte Time, Duration etc. in einem Command als Memento binden

...hat bereits keinerlei Verwendungen mehr (hab es wohl schrittweise durch std::ref() ersetzt ... ohne eigens darauf zu achten)

...was man auch daran sieht, daß es das gleiche Konezpt in Boost gibt; wäre da nicht Boost-serialisation, dann könnte man das als Ersatz nehmen

außerdem habe ich hier bereits pervasiv mit einem Typ-Präfix _Vect::  dekoriert

...was darauf hindeutet, daß ich bereits Probleme mit der Eindeutigkeit von Namen hatte, bzw. die Notation ohnehin verwirrend war.

allein schon wenn man so viel Kommentar zur Rechtfertigung schreiben muß; und dann ist das Ganze auch noch stateful, und es führte zu einer Erweiterun, ScopedHolderTransfer, mit noch viel mehr Kommentar und Erläuterung und umständlichen Tests (und dann wird das am Ende doch fast nicht verwendet)

hatte das zwar geschaffen, um non-copyable Objekte in einen Vector packen zu können — aber es ist kein Container, sondern lediglich ein inline-Buffer

für einen Test / Diagnose-Container ist das in der Tat sinnvoll, denn man möchte da nicht die Komplexität mit der Verwaltung von Extents  haben (größere Speicherblöcke, aus denen dann kleinere Allokationen ausgegeben werden)

dann könnte man ihn nämlich ohne weiteres direkt in jedem STL-Container per emplace einbringen, selbst in std::vector

unique_ptr kann das ja auc

ein Müll-Header mit einem Dummy-Test, der seit > 10 Jahren herumliegt, und alle wichtigen Probleme nicht löst ...

dann kann man auch gleich von Null anfangen

der nächste »Vertical Slice« wird das entscheiden müssen

denn im nächsten Slice wird das Agens festgelegt, auf dem der Builder arbeitet....

Daran wird sich dann anmessen, was für eine Repräsentation in der Session liegt 

man könnte also RefArray durch eine const vector&  ersetzen

weil zum Glück

die Abstraktions-Eigenschaft

gar nicht verwendet wurde

Zwar wird daran die Compilation mit C++20 nicht scheitern, aber danach dürfte doch ein gewisser Aufbruch stattfinden — und solche problematischen Reste der Zeit vor C++11 geraten dann zunehmend zum Hindernis.

...indem ich den bisherigen »Workaround« in den wichtigsten Definitionen unmittelbar daneben gestellt habe; teilweise können beide Fälle bereits koexistieren

Vorbereitung: alten Typ umbenennen: Types<TY...> in TyOLD<TY...>

bisher wurde das durch Delegieren an die alte Loki-Implementierung bewerkstelligt; das nun direkt auf der Basis von Variadics zu machen, wäre der zentrale Schritt, der das neue Ökosystem der variadischen Typlisten autonom macht (so daß man am Ende die alte nicht-variadische Definition entfernen kann)

in typeseq-util (etwas versteckt zwischen den Spezialisierungen von Prepend, was wiederum Vorraussetzung ist, so einen Rückweg konstruieren zu können)

d.h arbeitet ausschließlich auf Typlisten und bezieht sich nirgends auf Typ-Sequenzen

es stellt nämllich nur auf einen nested  X::List ab

das ist m.W. der einzige Use-Case, der das vollständige Feature  der partiellen Funktions-Closure mit N argumenten verwendet; meist wird nur der einfache convenience-Fall mit dem ersten oder letzten Argument genommen. Das bedeutet, hier wird auch eine Typ-Sequenz generiert, und daraus ein Tupel... (und da ich dieses Feature erst 2025 entwickelt habe, arbeitet es bereits ausschließlich mit den neuen variadischen Sequenzen)

Das sieht auf den ersten Blick dämlich aus, aber vorsicht, der Template-Parameter ist kein Variadic, sondern eine Loki-Liste; damit greift hier eine Spezialisierung, die im Header typeseq-util.hpp definiert ist: die variadische Sequenz wird eigens per Prepend<...> wieder aufgebaut... (hab lange gebraucht bist ich das geschnallt habe)

...aber nur wenn's einfach geht; eigentlich ist das außerhalb vom Scope und könnte auch später mal gemacht werden, sofern nur die bestehende Impl mit den neuen Typlisten arbeitet.


Tatsächlich hab ich das jetzt doch angehen müssen, da ich im Zuge der Umstellung in compile-Fehler gelaufen bin. Und das ist die Art Blocker, die ich für dieses Refactoring stets befürchtet habe; letztlich war es aber doch nicht so schlimm, denn bedingt durch den inkrementellen Ansatz konnte ich stückweise zurückgehen und hab dann gesehen, daß der Fehler „nur“ darauf zurückgeht, daß meine neue Ersatz-Implementierung nun voll generisch ist, und deshalb einen out-of-bounds-Zugriff nicht mehr stillschweigend abschneidet, sondern an std::bind durchreicht (welches dann die nicht passenden Argumenttypen bemerkt). Insgesamt war das nun etwa ein Tag full-time-Arbeit, und nun ist dieser bedrohliche Header wenigstens schon mal so weit reduziert, daß man die Redundanzen klar sehen kann

da es auf std::get<i> mit frest verdrahtetem Index aufsetzt

d.h.: für die Back-Berchnung

wird die Länge der Overlay-Liste

nicht richtig gerechnet

» off by one «

ECHT JETZT!

Es geht um Intervall-Relationen — bekanntermaßen eines der (fast) ekelhaftesten Fallunterscheidungen (Lage von Raumgebieten zueinander oder teilweise ähnlichen Topologien zueinander ist noch schrecklicher)

da als rekursive Berechnung implementiert und der pos-Parameter auch korrekt als uint definiert

Warum hab ich das nicht gleich gesehen?????

  • Ich hatte alle relevanten Informationen bereits gestern Nacht, aus dem Compile-Fehler
  • hab dann wohl einen Tunellblick gehabt, und dann sofort nach der Splice-Implementierung als »Strohhalm« gegriffen, denn das ist was Systematisches
  • und heute hab ich jetzt ein paar Stunden damit verbracht, einen systematischen Testfall runterzuklopfen (was nun immerhin eine vertrauensbildende Maßname ist, denn die Splice-Metafunktion ist definitiv komplex)

also vermutlich war mir das damals so völlig klar, daß ich es einfach gemacht habe, ohne einen Kommentar zu hinterlassen; es war ja auch eine Spezialanfertigung für diesen einen Fall, und explizit für 1..9 Parameter so ausgeklopft

...und zwar habe ich, mithilfe eines λ-generic, teilweise die Fähigkeit von std::bind nachgebaut, einen Functor per Forwarding in die Kopie zu nehmen; das war damals für mich ein ziemlicher Schritt vorwärts — wiewohl eigentlich auch die alte Implementierung auf std::bind aufbaut, und somit eigentlich dazu fähig sein müßte (wenn man sie denn druchdringen könnte....)

wir greifen den aus dem Original-Tupel-Typ heraus; d.h. wenn std::get<i> eine Referenz liefert, wird diese an den Konstruktor dieses Zieltyps gegeben

  • wenn es sein Argument, also das Tupel normal bekommt, nimmt es eine Referenz und liefert auch eine Referenz auf das Einzelargument
  • wenn es dagegen sein Argument-Tupel per forwarding oder RValue bekommt, dann liefert es eine Forwarding-Referenz

....und auch der wurde noch nicht auf den Fall perfect-forwarding mit möglicherweise Referenzen im Tupel vorbereitet

hier sollte alles komplett auf perfect forwarding bleiben; egal ob nun ein Einzel-Argument gegeben ist, oder (später mal) ein Tupel

wenn in C++ ein Funktionsaufruf dazu dienen soll, eine Objektinstanz(Kopie) zu erstellen, dann sollte man nicht die Forwarding-Fälle durchreichen, sondern diese Handhabung besser dem Compiler überlassen, und die Kopie also sofort ganz vorne auf den Funktionsargumenten passieren lassen. Ab diesem Punkt kann man dieses Objekt per move ans Ziel schieben

...denn sie bekommt hier, was auf der Eingangs-Seite im Konstruktor gebunden wurde. Wurde also eingangsseitig ein perfect-forwarding gemacht, dann schwenkt dieser Parameter je nach gegebener Objekt-Art.  Man kann von sowas nun entweder kopieren, oder sich das selber als Forwarding-Referenz speichern

commit 32b740cd400127b1d119b1e4bab490e063aa4f3e

Author: Ichthyostega <prg@ichthyostega.de>

Date:   Wed Nov 22 22:11:59 2023 +0100

    Library: RandomDraw - dynamic configuration requires partial application

   

    Investigation in test setup reveals that the intended solution

    for dynamic configuration of the RandomDraw can not possibly work.

    The reason is: the processing function binds back into the object instance.

    This implies that RandomDraw must be *non-copyable*.

   

    So we have to go full circle.

    We need a way to pass the current instance to the configuration function.

    And the most obvious and clear way would be to pass it as function argument.

    Which however requires to *partially apply* this function.

   

    So -- again -- we have to resort to one of the functor utilities

    written several years ago; and while doing so, we must modernise

    these tools further, to support perfect forwarding and binding

    of reference arguments.

in diesem Test wird

  • einmal eine Referenz auf eine lokale Variable in die closure gespeichert
  • eine Referenz auf ein std::function-Object als Funktor gespeichert

dann werden beide Referenzen manipuliert, und dadurch ändert sich (per Seiteneffekt) das Ergebnis, das man aus der den Funktoren für Funktions-Komposition und für partielle closure bekommt. Das heißt, die in diesem Testfall konstruierten Funktoren / Closures sind auf extreme Weise von verdecktem Kontext abhängig.

Die neue Implementierung verwendet wieder die alte Template-Infrastruktur, aber gründlich modernisiert, mit variadics und std::apply. Damit können keine Referenzen in einen Binder gespeichert werden

Dabei ging es darum, einzelne Parameter in einem Tupel bereits »technisch« vorbinden zu können, während der Rest erst später von der Automation gesetzt wird.

Beobachtung: das Thema Referenzen spielte in diesen Überlegungen keine Rolle. Vielmehr ging es um Performance: ich hatte Sorgen, weil das partial-application-Framework std::function-Objekte erzeugt; das bedeutet, daß jedes Lambda mit mehr als einem kontextuell gebundenen Wert in Heap-Storage wandert. Das  war die Hauptsorge hier

...hatte damals aber keine Lösung gesehen, und war zu dem Schluß gekommen, daß dieser per Value arbeiten muß, weil sonst der ElmMapper heimtückischen Berschränkungen unterliegen könnte, wenn unique-ownership im Spiel ist. Weiß nicht, ob diese Sorge berechtigt ist. Jedenfalls ist mir damals der Unterschied zwischen RValue-Referenz und Universeller-Referenz(forwarding, template) nicht aufgefallen.

Hier ist der Zusammenhang im Rückblick (auch nach längerer Recherche) nebulös. Ich habe mich damals wohl „verhackt“, d.h. ich stand unter Druck, weil das ganze ja nur ein Test-Hilfsmittel sein sollte (warum hat sich dann der ganze Scheduler-Stress-Test so extrem über mehrere Monate »gezogen«....?). Das RandomDraw, mit dieser Graph-Generierung ist wohl so eine Idee, die mich im Hintergrund fasziniert, und die eigentlich über das Ziel hinausschießt.....

Wie dem auch sei, ich habe das durchgeprügelt, und dann bei den abschließenden Tests gemerkt, daß ich mit meinen Referenz-gebundenen Lambdas, auf die ich die ganze Lösung (für die Konfiguration dieses Zufalls-Generators) aufgebaut habe, ganz wunderbar in's eigene Bein geschossen habe: Das Ding soll ein Value-Objekt sein, darf aber nicht bewegt werden, sonst sind alle Referenzen in den Lambdas kaputt. Daraufhin habe ich mich mit einer noch wilderen Hack-Lösung gerettet...  (das LazyInit). Irgendwo in dieser Verzweiflungs-Aktion habe ich partielle Anwendung gebraucht; in der endgülitgen Lösung wird sie aber gar nicht mehr verwendet, sondern nur Funktions-Komposition (die Zufalls-Quelle wird dem Zieh-Mechanismus vorgeschaltet). Ich war damals (Winter 23/24) wieder sehr nah am low-level-Hacking, und hab extrem steilen Code geschrieben (über den ich mich jetzt, 1 Jahr später, schon wieder schäme.

der ganz alte Code konnte noch keine Typ-Infrerenz verwenden und mußte deshalb stets einen Ergebnis-Typ definieren, der dann zwangsläufig eine std::function war. Das war einer der Hauptgründe, warum ich zwischenzeitlich mit diesem Framework in Probleme gelaufen bin, und deshalb angefangen habe, Teile „daneben auf der grünen Wiese“ zu bauen. (Erstaunlich, daß ich das alles nach 5 Montaten bereits restlos vergessen hatte)

hat nur eine public Typedef return_type

no matching function for call to '

__invoke(

AdaptInvokable<int, double>::buildWrapper<BINDER >(BINDER&&)::<lambda(int, double)> const&

, __tuple_element_t<0, tuple<int, double> >&

, __tuple_element_t<1, tuple<int, double> >&

)'

das const& erscheint verdächtig

damit die reguläre Variante des Funktions-Operators genommen wird

weil man nämlich damit versucht, die Probleme mit const / volatile wegzubügeln, so daß sich der Binder in jedem Fall aufrufen läßt

es geht darum, die alten nicht-variadischen (pre-C++11)-Konstrukte loszuwerden, bevor wir auf C++20 schalten. Es geht nicht darum, ein generells Funktion/Binding-Framework aufzubauen; das kann man schrittweise weiterentwickeln. Siehe das Ticket #1394

...und wird definitiv für den allgemeinsten Fall verwendet. Das Backend ist in beiden Fällen bindArgTuple() und damit std::bind. Und die Implementierung ist eigentlich gleich (da hab ich letztlich das Problem x-mal gelöst).

Man könnte also BindToArgument so verallgemeinern, daß es eine typ-sequenz für mehrere Parameter nimmt, und die Werte dann als Tupel. Und dann könnte man ein function-style API als front-End davor stellen.

Trigger...

jeder vollständige Aufruf von

func::bindFirst(f, val)

— oder —

func::bindLast(f, val)

lag vermutlich an dem λ-generic,

welches inline definiert und per

auto-Rückgabetyp geliefert wurde

hier droht das Function-Closure-Monster

weil *this tatsächlich von boost::rational<uint> erbt; der Check soll ja eine division-by-Zero verhindern, also *this soll nicht 0 fps sein

dafür gibt es eigentlich keine Entschuldigung bei Typen, die auch arithmetisch sein sollen; denn leider wird int(0) implizit in CStr promoted, und von dort weiter in Literal bzw Symbol

boost::rational<unsigned int>::bool_type

sollte nicht eben dieser fehlgeleitete Vergleich durch ein save-bool vermieden werden...?

...wobei, das Ergebnis wäre hier auch korrekt (da 0 auch false)

wobei vor allem relevant wären die Vergleiche via "compatible integer types".

//

// Non-member operators: previously these were provided by Boost.Operator, but these had a number of

// drawbacks, most notably, that in order to allow inter-operability with IntType code such as this:

//

// rational<int> r(3);

// assert(r == 3.5); // compiles and passes!!

//

// Happens to be allowed as well :-(

//

// There are three possible cases for each operator:

// 1) rational op rational.

// 2) rational op integer

// 3) integer op rational

// Cases (1) and (2) are folded into the one function.

//

...und nicht (mehr) per Vererbung.

Erläuterung:

Die Implementierung von is_compatible_integer  kombiniert mehrere compile-Time Trait-Checks. Grundsätzlich gilt für alle Traits, daß sie nur direkt auf dem jeweiligen Template-Argument arbeiten, nicht aber eine eventuelle Basisklasse heranziehen. Das Problem ist mir bekannt; die Sprache C++ gibt das einfach nicht her...  (Kann mich an die genaue Begründung im Moment nicht erinnern, müßte irgendwo in dieser Mindmap hier stehen ... war das beim Parser?)

/** derive total ordering from base class */

std::strong_ordering operator<=>(FrameRate  const&) const =  default;

std::memory_order_release = std::memory_order::release

__is_tuple_like_v<typename std::remove_cvref<_Tp>::type> [with _Tp = lib::meta::ArrayAdapt<unsigned int, unsigned int, unsigned int, unsigned int>&]

...with

with _Tuple = lib::meta::ArrayAdapt<uint, uint, uint, uint>&

Problem: »tuple_like« ist exposition-only

Ärgernis-1 : das exposition-only concept »tuple_like«

Anscheinend wollte man da bessere Fehlermeldungen und dann wurden nur die Stakeholder aus dem Standard bzw. der Stdlib berücksichtigt; der Standard ist in der Tat so formuliert, daß er eigentlich nur auf eine feste Liste von bekannten Tuple-likes abstellt. Weiß nicht ob man sich überhaupt einen Gedanken gemacht hat, ein generisches tuple_like  bereitzustellen; so wie man das Mindset der Kommittee-Mitglieder kennt, scheitert sowas daran, daß man definitiv nicht alles erfassen kann, was per Structured Binding greifbar ist.

Ärgernis-2 : die std::apply Impl.  ruft explizit std::get

Das ist ein weiteres Problem, und bestand bereits in C++17; und es zwingt dazu, eine eigene Spezialisierung von get in den Namespace std:: einzufügen — was nun mit C++20 explizit verboten wird. Was dann bedeutet, daß die restriktive Auslegung rückwirkend als "immer schon beabsichtigt" erscheint (und ich trau mir aber wetten, daß das Problem anfänglich nicht gesehen wurde)

dort könnte dann auch das Concept bereitgestellt werden, und eine Ausweitung auf diverse weitere Verwendungen (forEach etc...)

allerdings dann korrekt für einen Erweiterungspunkt get

das limitiert die Nutzbarkeit ohnehin sehr stark, da man mit unlimitiert generischen Typen wenig Gemeinsames anfangen kann

...auch der Standard gibt keine klaren Ziele an — vermutlich deshalb auch diese Diskrepanz, daß »tuple-like« nun doch nur für bestimmte STL-Klassen gelten soll; für diese engere Auswahl synthetisiert der Compiler nun (C++20) auch Vergleichsoperatoren. Aber darüber hinaus? es bleibt eine Art Parameter-Satz oder generischer Datensatz, mit dem man letztlich wenig anfangen kann ohne zusätzliche Anhaltspunkte (Referenzen, Kopierbarkeit, semantisches...)

aber nur auf den einen Fall, der allgemeine Typen betrifft...

d.h. wir unterstützen auch eine Memberfunktion get<i>()

mit duck-typing

...wenn man selber Typen für das »tuple-protocol« einrichtet, hat man i.d.R nicht die Zeit, sich um alle CV-Varianten + RValues zu kümmern (es gibt ja einen konkreten Use-case); allerdings spielen die CV-Varianten nur für eine get<i>()-Funktion tatsächlich eine Rolle, denn dort muß sich diese Variante auf den Ergebnistyp auswirken; man könnte diese Varianten komplett genersich per Library-Funktion aus einer Basis-Impl ableiten...

und doch nur einen fest verdrahteten Spezialfall definiere, bloß weil's jetzt grade convenient ernscheint

in der FeedManifold (spielt dort aber eine essentielle Rolle, weil nur auf diesem Weg die völlige Flexibilität in allen Argumenten erreicht wird)

ElmTypes<TUP>::Tup macht ein variadic rebind von einer Typ-Sequenz in ein std::tuple

...das bedeutet, der Fall einer reinen Loki-Typliste fällt erst mal weg (wird ja derzeit nicht verwendet), und der sonderbare »fallback« ändert nun sein Veralten und würde zu einem 1-Tupel. Dafür kann die gesamte constexpr-if-Logik wegfallen, weil ElmTypes<Tup> bereits eine Index-Sequenz fertig bereitstellt.

siehe Entdeckung, die ich im Test dokumentiert habe: eine reine Compile-Time evaluation kann man zwar so machen, das wäre aber von hinten durch die Brust ins Auge...

das ist nämlich eine Spezialisierung, die dann bereits auf dieses Concept aufsetzen würde...

und die interne Impl ist mehr oder weniger genau dasselbe, als wir zu Fuß auch machen würden, nur daß wir dann an die Index-Seq gar nicht rankommen

nützlich für Tests

...und jetzt weiß ich, warum das Pendant für std::apply so kompliziert implementiert ist!!!

und zwar bildet das das Verhalten nach, das die Sprache automatisch bei Funktions-Anwendung vollzieht: wenn konkrete Parameter eine im Scope sichtbare Variable bezeichnen, dann bekommt die Funktion automatisch eine Referenz

class Base

{

protected:

Base() = default;

};


struct Feed

: Base

{

};


int

main (int, char**)

{

Feed f1;

// Feed f1{}; /// does not compile with GCC 14.2

return 0;

}

vermutlich ein sehr spezieller Compiler-Bug

Parsing(...).should_yield()

hier wird die expectation als Zeitangabe gesetzt, und dann nur zur Fehlermeldung gerendert

Symbol::empty() ist direkt inline definiert

...eine Frage der Code-Anordnung;

ich hab das so gemacht, weil es kompakter und lesbarer ist, die Definition diretkt in den Klassen zu sehen; das geht nur, weil dann eben hilfsweise der Vergleich mit einem CStr genommen wird, in den sich ein Literal oder Symbol implizit stets konvertieren läßt. Effektiv läuft damit ohnehin der gleich Code, nämlich ein einfacher Pointer-Vergleich.

...der ganz regulär als freie Funktion definiert ist; und zwar verschieden für den Vergleich von zwei Literals und zwei Symbols.

...und jetzt kommt C++20 und will hilfreich sein

... und merkt dabei, daß diese implizite Konvertierung auf beide Seiten anwendbar wäre

Sauber wäre es, diese Definition erst nach der Definition der Vergleichsoperatoren zu geben, als inline-Funktion. Denn dann würde der Compiler den direkten Vergleichsoperator sehen. Aber der menschliche Leser könnte übersehen daß Symbol::empty() sich unterscheidet von Literal::empty(). Es soll nämlich auch das Symbol{"⟂"} als »empty« gelten

grrrr... das Design von MObject ist planlos

weiß nicht, wozu die Vergleichsoperatoren überhaupt gut sein sollen; das sieht nach Value-Semantik aus, aber andererseits hat doch ein MObject wohl eine klar definierte Identität?

...denn das zahlt sich nur bei komplexeren Relationen aus; eine Negation wird man ja wohl noch schreiben können, ohne daß ein Zacken aus der Krone bricht

...warum wird das erst jetzt mit C++20 sichtbar??

......anscheined wird hier direkt der Conversion-Operator nach raw_time_64 genommen (und von dort in einen char)

indem man string{val} konstruiert

narrowing conversion of '(& val)->lib::time::TimeVar::operator lib::time::raw_time_64()' from 'lib::time::raw_time_64' {aka 'long int'} to 'char'

eigentlich sollte es diesen Konversionspfad auch nicht geben

...erscheint mir als ein Überbleibsel von den Anfängen, wo ich dachte, TimeVar wäre eine Ausnahme, und es wäre sinnvoll, diese Ausnahme bequem zu gestalten und auch gleich noch eine Hintertür einzubauen — keine gute Idee...

also dann ⟹ weg damit (und  _raw(var) explizit verwenden)

src/vault/gear/work-force.cpp: In function 'void vault::gear::work::performRandomisedSpin(size_t, size_t)':

src/vault/gear/work-force.cpp:69:43: warning: '++' expression of 'volatile'-qualified type is deprecated [-Wvolatile]

69 | for (volatile size_t i=0; i<degree; ++i) {/*SPIN*/}

Motivation war: irregeleitete Verwendungen von volatile verhindern

Insgesamt bleibt volatile erhalten, ist aber nur noch an wenigen Stellen erlaubt, und zwingt dazu, Zugriffe explizit auszuschreiben (keine compound-assignment-Operatoren mehr). Einige dieser Einschränkungen wurden (nach Kritik aus der Embedded-community) in C++23 wieder zurückgenomm

...weil es eine "discarded value expression" ist; wenn man die gleiche Zuweisung im Schleifenrumpf schreibt, gibt es keine Warnung (da erkennt der Compiler das) — nur in der for-expression erkennt er es nicht

Zeile 220:

CHECK (5 == *pX);  // implicit conversion from X to long

...von diesem speziellen shared-ptr möchte ich nur noch wegrennen...

denn nur so entsteht die 2. Variante, die für die Intention des Testsfalles hier völlig daneben liegt (unbeschadet der Tatsache, daß der Testfall selber dämlich ist, und das, was ich hier teste (den lumiear::P), schleunigst auf den Müll gehörte)

in beiden Fällen der gleiche Fehler, man sollte hier per Referenz capturen, da wir ohnehin direkt im Callstack bleiben

Digxel<long, digxel::CountFormatter>::operator long() at /Werk/devel/lumi/src/lib/time/digxel.hpp:257 0x7ffff48f62b4

Der erste Entwurf für eine Infrastruktur für den Builder/Compiler (der Kern von Lumiera) hängt völlig in der Luft, und war von Phantasie-Vorstellungen getragen, wie ich mir diesen Kern gerne vorstellen würde. Ich war (und bin eigentlich immer noch) vom Visitor fasziniert, hab aber den Verdacht, daß das gar nicht das ist, was ich darunter verstehe. Soweit so gut. Habe dann seinerzeit rein auf Verdacht einen eigenen Visitor implementiert, und dann beim Integrationsversuch gemerkt, daß er gar nicht auf mein Modell paßt. Die Notrettung war ein »wrapper-pointer«, und der brauchte eine Variant-Storage. Boost zieht die ganze MPL mit rein, keine Ahnung warum, aber ich kann das besser. Der zweite Versuch, das Gleiche zu bauen wurde dann nur noch eine halb-Katastrofe und ist jetzt in unserer Support-Lib (und hie und da im Einsatz)

....und zwar sehr bald, wenn ich den Builder in Angriff nehme — was ich nun nicht mehr hinausschieben kann. Aber dieses aktuelle »Vertical Slice« geht noch ohne, und damit lasse ich die Finger davon; es zeichnet sich bereits ab, daß ich mein high-Level-Model nochmal komplett umkrempele und nicht mehr auf Shared-Pointer aufbauen werde, und so könnte das mit dem Visitor ja vielleicht doch noch klappen.

...denn das ist ein wichtiges Argument; speziell Literal soll die CStr eigentlich überall verdrängen können, und zwar ohne viel Zeremonie. Denn für std::string verhält es sich genauso

...Solange es keine automatische Konvertierung gibt, die eine Brücke in ein anderes Ökosystem baut. Also (wie im Beispiel, das diese Diskussion auslöst) eine implizite Konvertierung von etwas Arrithmetischem in etwas String-artiges

aber Literal kann komplett constexpr sein

immerhin: konnte alle komplexen Vergleichsoperatoren viel einfacher per Spaceship darstellen...

Trotzdem verwenden wir weiterhin boost::operators (und das ist nicht wirklich problematisch, da header-only)

Template-Parameter POA

denn was bedeutet es schon, wenn eine Query »kleiner« ist als eine andere — und dann im Bezug dazu, wenn zwei Queries »gleich« sind?

und zwar weil letztlich das Prädikaten-Symbol und dann noch zusätzlich die Typ-ID die Entscheidung fällt

d.h. die drei explizit definieren Operatoren können weg, und dafür wird ein defaulted Spaceship eingeführt

bisher wurde die Kategorie für die Gleichheit ignoriert, d.h. es wurde nur die Prädikation verglichen; das ist inkonsistent mit der Ordnung, welche diesbezüglich noch differenziert (gleiche Prädikation bei verschiedener Kategorie ist nicht äquivalent)

...weil wir stets die 2-Argument-Konstruktoren von BareEntryID aufrufen, und getTypeHash<TY>() dabei als Seed injizieren

/**

* Identification tuple for addressing frames unambiguously.

*

* @todo currently (7/08) this is a dummy implementation to find out

* what interface the Steam layer needs. Probably the vault layer will

* later on define what is actually needed; this header should then

* be replaced by a combined C/C++ header

*/

class FrameID : boost::totally_ordered<FrameID>  ////////////TODO it seems we don't need total ordering, only comparison. Clarify this!

{

long dummy;

public:

FrameID(long dum=0)  : dummy(dum) {}

operator long () { return  dummy; }

bool operator< (const  FrameID& other) const  { return dummy <  other.dummy; }

bool operator== (const  FrameID& other) const  { return dummy ==  other.dummy; }

};

STOP: wir verwenden auch boost::additive und boost::multipliable

verwendet boost::unit_steppable<SmpteTC>

  • was ist ein »äquivalentes« Memento?
  • wozu würde man das brauchen?

normalerweise interessiert nur der Zustand, ob überhaupt ein Memento gespeichert wurde

...und er verhält sich wie typischweise in den funktionalen Sprachen; daher ist der Spaceship-Operator ein drop-in replacement, da dessen Ergebnis ja auch mit dem Literal 0 verglichen werden kann

additive und multipliable

Und diese sind mit die massivsten Definition in boost::operators — also sicherlich nichts, was man von Hand nachimplementieren möchte

* @todo with C++20 the body of the implementation can be replaced by std::invoke_r //////////////////////TICKET  #1245

...weil wir nun nämlich den gewünschten Return-Typ von oben durchgeben können, und nicht mehr per Type-Trait inferieren müssen; außerdem ist das ganze Thema Invoke / convert nun in die Standardlibrary ausgelagert (und ja, die hat deswegen ganz schön zu rudern)

hier fasse ich mit constexpr-if die zwei Fälle zusammen (void-Funktion und Rückgabewert)

...wir brauchen diesen Aufruf nämlich nie, aber wir brauchen ihn, damit die Funktion durch den Compiler geht. Die einzige Alternative, die ich sehe, wäre, die Invocation wieder in die Hauptklasse zu integrieren, incl. try/catch. Dann müßte ich aber das Ergebnis auch stets an einen zunächst leer konstruierten ItemWrapper zuweisen — was dank der speziellen Konstruktion des ItemWrappers tatsächlich möglich wäre (er mach in diesem Fall eine Move-Konstruktion)

...denn diese war ja nur notwendig geworden, um den void-Fall mit zu integrieren; also genau das, was nun invoke_r aus der STDLib auch leistet. Damit hätten wir dann zweimal das try-catch-Konstrukt, aber das verbessert hier eigentlich die Lesbarkeit, da man nun direkt sehen kann, was passiert.

Damit wird der Aufruf ein Einzeiler, und das Problem mit der void-Fallunterscheidung fällt ebenfalls flach

viele typename und template -Qualifier werden überflüssig

Use 'rsvg_handle_get_intrinsic_size_in_pixels' instead

Use 'rsvg_handle_render_document' instead

typically this was used as mix-in base class to get the typedefs argument_type and result_type — which are no longer needed by modern code (and the STL), since traits or even concepts are commonplace nowadays.

src/lib/time/timevalue.hpp:303:13: note: because 'lib::time::Time' has user-provided 'lib::time::Time& lib::time::Time::operator=(lib::time::Time)'

bedeutet: hier sollte der (erlaubte) copy-ctor explizit deklariert werden als =default 

MoveSupport<I, D, B>::moveInto(void*) was hidden by FullCopySupport<I, D, B>::moveInto(I&)

Das bedeutet, der Scope, in dem dem der Funktionsaufruf interpretiert wird, ist gegeben durch den Typ der Referenz, über die aufgerufen wird. Das kann das Interface sein, es kann aber auch die konkrete Klasse sein, oder irgend was dazwischen. In diesem Scope wird nach dem reinen Namen der Funktion gesucht. Für diesen könnten mehrere Overloads in dieser Funktion sichtbar sein, und nur auf diesen wird die Overload-Resolution gemacht. Nur falls die konkrete Subklasse die Funktion gar nicht definiert, wird dann über die Vererbungshierarchie gesucht (und bei Templates gar nicht). Das heißt, hier kollidiert das Standard-Verhalten von C++ (das von C abstammt), mit dem Konzept eines virtuellen Dispatch. Im Zweifelsfall macht der Compiler immer einen direkten Aufruf, und versucht dann ggfs. sogar, die Argumente automatisch zu konvertieren. Deshalb könnte das tatsächlich eine Falle sein, und die Warnung ist grundsätzlich angebracht.....

Der konkrete Fall ist der »VirtualCopySupport«, und noch konkreter, er wird verwendet in unserem Variant-Typ. Dafür bauen wir eine Vererbungs-Kette, in der das VirtualCopySupportInterface irgendwo unter dem Interface liegt, und dann das per Policy gewählte Implementierungs-Mix-in irgendwo unter dem konkreten Typ. Aber die eigentliche Verwendung steckt im Variant-Container, der eben nichts über den konkreten Typ in seinem Inline-Buffer weiß. Deshalb erfolgt der Dispatch stets über das Basis-Interface des Buffers in das VirtualCopySupportInterface. Insofern wird hier vor einem potentiellen Problem gewarnt in dem Moment, in dem der konkrete Typ konstruiert wird, obwohl der gefährliche Aufruf niemals stattfinden kann

Das VirtualCopySupportInterface bietet zwei Sätze von Zugriffsfunktionen

  • einen direkten Zugang über eine Referenz auf das Interface
  • einen indirekten Zugang, in dem der Empfänger einen Storage-Buffer anbietet (das wird zur Implementierung von Konstruktoren benötigt, weil in diesem Fall noch gar nichts im Buffer liegen kann)

Und das bedeutet: die Argument-Typen für die beiden Funktions-Varianten sind total inkompatibel. Würde man also versehentlich mal tatsächlich über die konkrete Klasse aufrufen, dann wäre die u.U. die Variante mit dem Storage-Buffer shaddowed (und davor warnt der Compiler), aber man könnte einen Aufruf darauf auch gar nicht auflösen, da die Argumente nicht kompatibel sind

...auch wenn sie nicht dem entspricht, was man intuitiv erwarten würde (aber gleiches hier: eine Lösung stünde in keinem Verhältnis zum Nutzen)

denn hier kann man gar nicht die Basis-Impl in scope bringen

d.h. wenn ich hinzufüge using BASE::resolve,  dann bekomme ich einen echten Compile-Fehler, weil das Symbol zweideutig ist. Und zwar by design, wir bauen das ja über eine Typ-Sequenz auf

siehe Zyn.mm, suche nach "std::move ungeschickt verwendet"

weil std::move() einen Cast macht, und damit das Ergebnis der delegate-Funktion nicht mehr den gleichen Value-Typ hat, den wir grade zurückkeben wollen. Denn bei gleichem Value-Typ, kann der Compiler die ganze Kette auflösen und erstellt den Ergebniswert bereits am endgültigen Zielort (das ist die RVO). Aber durch unseren Cast sieht er einen RValue, und muß damit ein neues Objekt konstruieren. Weil Herrchen das so verlangt hat ;-)

ui-location-solver.hpp , l.280

Das move ist hier aber nicht überflüssig, sondern soll tatsächlich die Terminal-Operation des UICoord::Builder auslösen (von dem UILocationSolver erbt). Also mißdeutet der Compiler die Situation komplett, denn es handelt sich nicht um eine RVO, sondern um einen speziellen Konstruktor-Aufruf von UICoord, in dem noch eine Normalisierung stattfindet

UICoord hat nämlich einen variadischen Konstruktor; hier tritt wieder das notorische Problem auf, daß dieser andere konkrete Definitionen verdeckt. Ich mußte deshalb auch schon die ganzen Copy-Operationen nochmal anschreiben (inzwischen gäbe es dafür den disable_if_self-Helper)

Ich hab auch erst selbst nicht verstanden, was hier vorgeht, zumal die Terminal-Operation im UICoord::Builder selber gar nicht zu sehen ist (obzwar die tatsächliche Implementierung direkt dahinter steht, und auch erst an der Stelle gegeben werden kann). Ein weiterer design smell  ist, daß ich dazu UICoord zum friend machen mußte. Was war nochmal die Motivation für dieses Design?  UICoord selber ist doch kopierbar, also warum nicht eine explzite Terminal-Operation, in der die Normalisierung passiert, und das Ergebnis dann direkt in ein neue UICoord-Objekt schieben?

Frage: wirklich?

und jetzt umsetzen?

Ich wollte zwar auf der technischen Ebene die Klasse UICoord immutable machen, aber für die Verwendung sollte sich das transparent anfühlen, ähnlich wie die moderne funktionale Programmierung mit immutable-containers umgeht:

  • gewisse Spezifikations-Funktionen steigen in einen Builder ein, der auf einer Kopie aufbaut
  • dieser Builder kann aber nahtlos überall dort verwendet werden, wo auch UI-Coordinaten verwendet werden können

ich wollte definitiv kein Terminal in der Notation

TEST test stderr, cat'ing noonexistant file: ,nonexistent_file .. FAILED

cat: ,nonexistent_file: No such file or directory

unexpected data on stderr

more output than expected: 'cat: ,nonexistent_file: No such file or directory':1

stderr was:

cat: ,nonexistent_file: No such file or directory

END

TEST Filesystem manipulations: FileSupport_test .. FAILED

unexpected return value 5, expected 0

stderr was:

0000000514: INFO: suite.cpp:193: thread_1: invokeTestCase: ++------------------- invoking TEST: stat::test::FileSupport_test

*** Test Failure «stat::test::FileSupport_test»

***            : LUMIERA_ERROR_CONFIG:misconfiguration (Program environment doesn't define $HOME (Unix home directory).).

0000000515: ERR: suite.cpp:202: thread_1: invokeTestCase: Error state (null)

0000000516: WARNING: suite.cpp:203: thread_1: invokeTestCase: Caught exception LUMIERA_ERROR_CONFIG:misconfiguration (Program environment doesn't define $HOME (Unix home directory).).

END

Record.hpp, 289

/**

* copy-initialise (or convert) from the given Mutator instance.

* @remarks need to code this explicitly, otherwise Record's

* build-from sequence templated ctor would kick in.

* @warning beware of initialiser lists. Record has a dedicated

* ctor to accept an initialiser list of GenNode elements,

* and GenNode's ctor is deliberately _not explicit,_ to ease

* building argument lists wrapped in GenNodes. When writing

* initialisation expressions inline, the compiler might pick

* the conversion path through initialiser list, which means

* the contents of the Mutator end up wrapped into a GenNode,

* which in turn becomes the only child of the new Record.

*/

Record (Mutator const&  mut)

: Record((Record const&)  mut)

{ }

...zumindest würde ich das aus dem Kontext und dem Kommentar darüber so deuten

* After performing

* the desired changes, the altered copy can either

* be sliced out (by conversion), or moved overwriting

* an existing other Record instance (implemented as swap)

...und nicht über einen Umweg; ich bin ja mit dem Debugger durchgesteppt. Obwohl theoretisch der getemplatete Konstruktor gepaßt hätte, war nichts von zwei Iterationen über Attribute und Kinder zu sehen. Und mehr noch: wie würden wir wieder zu einem Mutator-Typ kommen, mit dem wir wieder in den selben Konstruktor einsteigen

die allesamt noch gar nicht initialisiert sind

dann scheitert die Compilation an anderer Stelle, nämlich an der Initialisierung einer Referenz Rec& root = target. Aber generic-record-test.cpp compiliert ohne Fehler

Überraschung: wer sich in den Fuß schießt, schießt sich in den Fuß

...ohne die implizite Annahme eines Layout, einfach indem ich den Overflow-Frame gleich per placement-New erzeuge; dann kann man trotzdem immer noch zeigen, daß die Daten weiterhin in der UninitialisedStorage liegen und dort verwendet werden können

sollte da ... die Topologie nicht stabil sein....?

Dann wäre der gesamte aufwendige Scheduler-Stress-Test (für den ich Monate an Arbeit versenkt habe) »auf Sand gebaut«

...als damals in 1.67, von wo ich die interne Implementierung für Lumiera genommen und eingefrohren hatte

SCHWEIN gehabt

   1 import sys
   2 
   3 try:
   4     untrusted.execute()
   5 except:  # catch *all* exceptions
   6     e = sys.exc_info()[0]
   7     write_to_page("<p>Error: %s</p>" % e)

das ist Python-uralt()

bzw allgemein fn(*arg,**kw)

hiv@flaucher:~/devel/doku/LumiWeb$ ./menugen.py -p -s -w >menu.html.tmp

--WARNING--   DSL-method "discover" not applicable for Node(root)

--WARNING--   DSL-method "discover" not applicable for Node(documentation)

--WARNING--   DSL-method "discover" not applicable for Node(documentation)

--WARNING--   DSL-method "link" not applicable for Node(documentation)

--WARNING--   DSL-method "link" not applicable for Node(project)

--WARNING--   DSL-method "link" not applicable for Node(devs-vault)

--WARNING--   DSL-method "link" not applicable for Node(devs-vault)

--WARNING--   DSL-method "sortChildren" not applicable for Node(news)

Traceback (most recent call last):

  File "/Werk/devel/doku/LumiWeb/./menugen.py", line 1183, in <module>

    parseAndDo()

    ~~~~~~~~~~^^

  File "/Werk/devel/doku/LumiWeb/./menugen.py", line 141, in parseAndDo

    addPredefined()

    ~~~~~~~~~~~~~^^

  File "/Werk/devel/doku/LumiWeb/./menugen.py", line 102, in addPredefined

    .putChildLast('old_news')

     ^^^^^^^^^^^^

er ist nämlich wirklich point-and-shot geschrieben, in etwa zwei Tagen, und das merkt man

  • er verwendet sehr viel Funktoren
  • einige dieser sind auch als Klassen definiert, mit Polymorphismus
  • jede beshandelte Resource wird in ein Node-Objekt transformiert
  • Processing-Instructions (entweder vom Scannen, oder in den predefined() elements) werden in Placement-Objekte übersetzt
  • Placement-Objekte sind selber Funktoren, die in einer Kette angewendet werden (ja das ist das gleiche Konzept wie in Lumiera selber)
  • es gibt nur eine fest konfigurierte Liste möglicher Placements
  • und hier verwenden wir committed choice,  d.h. das erste Placement, das die DSL-Spec oder die processing-Instruction parsen kann, wird angewendet, und kann den aktuellen Discovery-status manipulieren
  • das ist ein flexibles Baukastensystem, mit dem man direkt im Discovery-Prozeß eingreifen kann und bestimmen kann, was mit der aktuellen Node passiert und was als ihre Kinder rekursiv verarbeitet wird

Am Ende haben wir einen DAG aus Node-Objekten, die wir traversiern und rendern

...immer da, wo ich selber "woot?" sage; es genügt, Variable und Funktionen klarer zu benennen

mußte dabei die scons-Soup zusammenführen; trivial ⟶ ich hab zwei neue Funktionen im V1-Stil (Resultat wird zwar nicht funktionieren, aber das ist mir egal)

in der dependency-Liste steht es korrekt drinnen

standard hardening-flags setzen #971

wähle Kompatibiltät genau so, daß Ubuntu-Noble noch unterstützt wird, ansonsten den Level für »Trixie«

...das war ein Beschluß auf dem Entwickler-Meeting im September: auch wenn wir (Benny+ich) jetzt viel Know-How aufgesammelt haben, verschieben wir das Thema »Shader-Programmierung« dann doch noch in die Zukunft, weil es nicht strikt notwendig ist, um die einfachste Form von Playback zu bekommen. Demnach könnten wir sogar mit dem bestehenden XVideo-Code weitermachen (oder eben Legacy GL nehmen, sofern noch Motivation dafür da ist)

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

...heißt nämlich, daß man Lumiera derzeit nicht nach /usr/local/ installieren könnte!

Das wenn ein »richtiger« Unix-Hacker mitbekommt, dann haben wir uns ziemlich lächerlich gemacht: „mit Autotools wär das nicht passiert...“

Ich würde nie auf die Idee kommen, etwas anders ins System zu installieren, als via DEB-Paket. Und für das Debian-Paketieren brauchen wir ja einen Pfad relativ zum Build-Root (nämlich im debian/lumiera -  Unterverzeichnis, denn dort wird der Content für das neue DEB-Paket nach dem Build zusammengestellt....)

wie viele Build-Systeme beruht auch der SCons-Build darauf, rekursiv definierte Sub-Builds aus Unterverzeichnissen zu aggregieren. Das bedeutet, daß für den eigentlichen Bauvorgang jeweils in das Unterverzeichnis gewechselt wird. Das ist aber problematisch für Aktionen im Build-Tree, und deshalb bietet SCons diese spezielle Konvention mit dem '#'-Präfix, das den Pfad dann relativ zum Build-Root verankert

Effektiv ist das '#'-Präfix jetzt in $INSTALLDIR und $TARGDIR hineingewandert. Das ist aber eine SCons-spezifische Konvention, mit der der Icon-Renderer natürlich nix anfangen kann

Nov.2025 beim Erweitern/Debuggen entdeckt: da stößt das Setup mit der Hilfsfunktion getDirname() an ihre Grenzen. Gilt vermutlich auch für den ConfigData()-Builder. So belassen. Also: besser immer ein Präfix angeben, es ist ja auch kein optionales Argument

...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...

...mit etwas Hilfe von mir; ich war in der Zeit in Bernbach, dort haben wir dann mal einen Vormittag lang zusammen gehackt und den Build wieder weitgehend flott bekommen (bis auf den Scons-BuilderDoxygen)

Einige lasse ich bewußt offen, weil es sich um Code handelt, der mit dem nächsten »Vertical Slice« komplett überarbeitet wird

  • diverse Warnings zu Vergleichs-Operatoren über Assets und MObjects, im Besonderen mit dem speziellen Smart-Ptr lumiera::P (den will ich loswerden)
  • Warnungen zur pessimisation durch std::move im Return. Das betrifft vor allem die View-Spec-DSL im GUI, und dort ist die Warnung sogar fehlgeleitet (d.h. der Code würde brechen, wenn man das std::move entfernt); deutet das als Hinweis auf ein Design-Problem, das aber derzeit nicht auf der Agenda steht

tatsächlich hat GCC das bereits im Vorgriff, im C++17 Modus teilweise unterstützt, und ich habe beonnen, es zu nutzen

Nach Sprachstandard können die allermeisten "typename" und "template" Qualifier wegfallen; notfalls mithilfe einer weiteren, dazwischengeschalteten Typedef....

Diese redundanten typename und template -Qualifier sind einer der Hauptgründe, warum Metaprogramming-Code in C++ so schwer lesbar ist...

Wenn eine Codebasis als Sammlung von Quellen veröffentlicht wird, muß  der Lizenzhinweis prominent sichtbar direkt beim Zugang zur Quelle gegeben sein. Die Veröffentlichung ist rechtlich bindend. Wenn die Veröffentlichung in Form eines Quellbaumes erfolgt, dann muß das Lizenz-Statement direkt in der Wurzel liegen. Es deckt damit automatisch alle anderen Resourcen in dieser Distribution mit ab, auch wenn in diesen Ressourcen nichts weiter steht. Darin liegt aber eine latente Gefahr: wenn eine Resource ohne direkt angehefteten Lizenz-Marker in ein anderes Projekt kopiert wird, fällt sie unter dessen allgemeine Lizenzbestimmung. Fehlt jedwede allgemeine Lizenzbestimmung, dann tritt das jeweilige Copyright-Gesetz des Landes in Kraft, und das bedeutet praktisch überall auf der Welt: der Original-Autor hat die exclusiven Veröffentlichungsrechte. Auf rechtlicher Ebene gilt stets, was sich feststellen und belegen läßt, nicht was jemand beabsichtigt hat oder haben könnte.

...und zwar für den Fall, daß jemand eine in-sich geschlossene Quelldatei aus dem Projekt heraus kopiert; alle Hinweise direkt in der Datei erhöhen die Wahrscheinlichkeit, daß die Lizenzinformation nicht verlorengeht. Hinweise über Haftungsausschluß und wo man die Lizenz bekommt sind nur eine Empfehlung und haben keine rechtliche Wirkung. Diese ensteht durch die Veröffentlichung, und dazu genügt ein Statement im Root des Quellbaumes. Diese Einschätzung geht sogar aus den Hinweisen im Appendix der GPL selber hervor. Auch auf den Seiten der FSF wird zwar stark nahegelegt, den kopletten Präambel-Text einzufügen, aber es handelt sich daber ohne jeden Zweifel nur um eine Empfehlung

Wenn dieser Claim in jeder Quelldatei eigens gepflegt wird, erhöht das die Sicherheit der Verbindung von Copyright und Lizensierung. Allerdings auch nur, wenn dieser Claim korrekt ist. Es genügt, den Zeitpunkt der allerersten signifikanten Schöpfung und Veröffentlichung aufzuführen, denn alle weiteren Beiträge sind dann bereits durch die GPL gedeckt. Empfohlen wird, spätere signifikante Beiträge gesondert mit Jahreszahl aufzuführen. Jahresbereiche sind nur sinnvoll, wenn in jedem der eingeschlossenen Jahre tatsächlich etwas beigetragen wurde.

Notwendig wäre das nicht, aber hilfreich, wenn die Datei herausgelöst wird. Es genügt ein Hinweis auf die exakte Form der Lizenz. Man kenn "this Program" durch den expliziten Namen ersetzen. Man kann dazu noch auf die COPYING-Datei verweisen. Alle weiteren Infos, z.B. ein Haftungsausschluß sind nur empfohlen und ändern nicht die rechtliche Wirkung der Lizenz, unter der veröffentlicht wurde

/*

  INDEX-ITER.hpp  -  iterator with indexed random-access to referred container

  Copyright (C)

    2024,            Hermann Vosseler <Ichthyostega@web.de>

  **Lumiera** is free software; you can redistribute it and/or modify it

  under the terms of the GNU General Public License as published by the

  Free Software Foundation; either version 2 of the License, or (at your

  option) any later version. See the file COPYING for further details.

*/

das ist wichtig, denn damit ist das stets das nächstgelegene Release-Tag, was man für Skripting ausnützen kann.

Guten Einschnitt finden; abgeschlossene Features landen, möglichst noch obsoleten Code wegräumen. Und wichtig: rechtzeitig aufhören mit den tiefgreifenden Änderungen; besser Tests schreiben, an Details feilen, Untiefen ausloten.

Die aktuelle Version, die nun released werden soll; sie sollte zu dem Zeitpunkt noch mit einem ~dev -  Suffix versehen sein, aber so in-tree selber eingecheckt sein (im Buildsystem oder einer Konfigdatei).

Manchmal kommt es vor, daß erst kurz vor einem Release feststeht, daß man nun eine minor oder gar eine major-Version machen wird; daher muß man dann die eingecheckte Konfiguration anpassen und hochziehen. In einem solchen Fall ist es dann auch nicht möglich, die nächste Version einfach aus dem letzten Git-Tag zu gewinnen. (TODO: später mal ein elaborierteres Scripting, das auch die Version in-tree berücksichtigt)

Hiermit wird der Release-Stand(Features) festgesetzt und die Entwicklung ist wieder freigegeben

Klarstellung: das wird die nächste Version nach diesem Release

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

der Release-branch sollte zuletzt direkt auf dem Merge-Commit stehen; Grund ist: wir wollen dann von dort nach integration mergen (und automatisch die richtige Commit-Msg bekommen: merging rel/#.#.# into integration)

...das heißt bauen und hochladen

Wichtig: auf der Doku-Seite zum Paket nur was wirklich gebaut ist und funktioniert!

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.

As of _1/2011_ (0.pre.01)::

the project has created and documented a fairly consistent design,

partially coded up -- starting from the technical foundations and working up.

The code base is approaching 65k LOC. Roughly half of this is test code.

The Application can be installed and started to bring up a GTK GUI framework,

but the GUI is very preliminary and not connected to core functionality.

The video processing pipeline exists only in the blueprints.

As of _10/2013_ (0.pre.02)::

the data models have been elaborated and some significant parts of the session

are finished. Work has continued with time handling, a draft of the output

connection framework, a draft of the player subsystem and interfaces to the

engine and processing network. Unfortunately there was a considerable slowdown

and decrease in team size, yet still the code base is growing towards 90k LOC.

No tangible progress regarding the GUI and the backend.

As of _11/2015_ (0.pre.03)::

a lot of long standing maintennance work has been done. The Project switched

to C++11 and in the end even to C++14 and Debian/Jessie as reference platform,

followed by clean-up of now obsolete workarounds. On the GUI side, we largely

made the transition to GTK-3, which lead to rework of our timeline widget, not

finished yet. This work also spured an effort the connection and communication

between Proc and the UI, which is expected to be asynchroneous. Due to the

limited developer resources, work on the Engine and Player part is stalled.

debuild -S -sd --sign-key=11FDF5D2DBD7BBD7F4D9D9C42CF2539262382557
  • Über die Launchpad-Oberfläche, dort den Abschnitt mit den Keys anklicken.
  • auf der nächsten Seite gibt man den Fingerprint ein; der Pub-Key wird dann von Keyserver.ubuntu.com geholt
  • Launchpad verschlüsselt eine Token-Nachricht mit diesem Key und schickt ihn an die markierte eMail-Adresse
  • diese Nachricht entschlüsseln und den Token-Link im Browser öffnen ⟹ Key ist verifiziert

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

hat sich durch den Umzug auf Gitea geändert

den hatte ich früher einfach auf den DEB-Branch genommen; jetzt sauber als Patch repräsentieren

commit 0cb862810701e1b28a443917e02c6c604c469967

Author: Christian Thaeter <ct@pipapo.org>

Date:   Tue Apr 5 23:58:15 2011 +0200

    Fix: missing NOBUG_USE_PTHREAD conditional

commit d2fac4617da8cab64709e7fc7e229af181ae82db

Author: Christian Thaeter <ct@pipapo.org>

Date:   Thu May 26 02:00:13 2011 +0200

    remove one final newline from a log message

   

    Normally NoBug messages should not end in a newline, but in case NoBug

    hooks up other logging functions it is not completely avoidable that

    the log messages there have trailing newline characters.

commit 661f654b8660eac4bc77f32e4047366c5e2a9770

Author: Christian Thaeter <ct@pipapo.org>

Date:   Thu May 26 02:32:37 2011 +0200

    export nobug_log_va_ and macros defining limits for log lines

commit 9a609a3c36a5a6af3b0d6317c03f9edf044cf4ff

Author: Christian Thaeter <ct@pipapo.org>

Date:   Mon Jun 20 14:45:16 2011 +0200

    FIX: whitespace formatting in logging messages

commit 9ba2e948a7c95e28d156823174bdca187db679f9 (lumi/fix_c11, fix)

Author: Hermann Vosseler <deb@ichthyostega.de>

Date:   Mon Mar 17 02:12:37 2014 +0100

    FIX: macro string concatenation for C++11 compliance

   

    Since C++11 supports user defined string literals,

    there was the need to clarify string tokenisation

    in macro expansion.

   

    For NoBug, unfortunately this means that

   

    ""__VA_ARGS__

   

    is no longer treated as two tokens, but as a single token

    starting a user defined string literal. The fix is simple

    to insert a whitespace to clarify we want this parsed as

    two tokens as previously.

   

    http://stackoverflow.com/questions/11909806/g-4-7-evaluates-operator-as-sibling-to-macro-expansion

10.2

Libtool .la files should not be installed for public libraries. If they’re required (for libltdl, for instance), the dependency_libs  setting should be emptied. Library packages historically including .la  files must continue to include them (with dependency_libs emptied) until all libraries that depend on that library have removed or emptied their .la files.

Im Copyright/Lizenz muß nur aufgeführt sein, was für die Distribution in Debian relevant ist. Autogenerierte Build-Skripte werden nur während dem Paketbau erzeugt und sind nicht Bestandteil der Distribution. Steht so im Policy-Manual

...entweder direkt im LIcense: - Feld, oder unten, in einem Sammelfeld für diese Lizenz.

Hierbei versteht man unter »grant« die wörtliche textuelle Formulierung aus der Upstream-Distribution oder einem konkreten zentralen Quelltext, mit einer rechtlich bindenden Aussage die den Quelltext unter eine Lizenz stellt. („This code is Free Software, and can be .... under the terms and conditions....“). Der volle Text der Lizenz muß nur folgen, wenn es sich um eine spezielle LIzenz handelt, die nicht schon in der Debian-Distro mit ausgeliefert wird

Christian war lediglich schlampig und deshalb gibt es viele Files mit fehlender oder nur partieller Info. Aber ich sehe keine abweichende Lizenz-Statements

statt autoconf, automake, libtool

das ist nämlich der alte Standard bevor dieses Feld eingeführt wurde; dieser Fall gilt, falls ein Teil des Build-prozesses Root-Credentials braucht. Wenn das nicht der Fall ist, soll Rules-Requires-Root: no gesetzt werden

...denn fakeroot wird inzwischen per Default verwendet; das Paket muß keine Systemdateien anfassen, nur Dateien in einem lokalen Buildverzeichnis erstellen; die Permissions(root) setzt später der Installer

das Paket ist nicht darauf eingerichtet (obwohl es vermutlich funktionieren würde mit Multiarch: same für das Library-Paket); mir ist nicht klar wie das nobug-dev-Paket dann deklariert wird (Multiarch: foreign? aber was ist dann mit den statischen Libs). Das ganze Kapitel im Debian-Manual ist komplex und sieht ehr nach Work-in-Progress aus; und ich müßte wahrscheinlich den Aufbau der Pakete reorganisieren, und dann auch testen....

Bei aller Liebe — dafür erscheint mir der Nutzen doch zu peripher (YAGNI)

dh_makeshlibs is a debhelper program that automatically scans for shared libraries, and generates a shlibs file for the libraries it finds.

It will also ensure that ldconfig is invoked during install and removal when it finds shared libraries. Since debhelper 9.20151004, this is done via a dpkg trigger. In older versions of debhelper, dh_makeshlibs would generate a maintainer script for this purpose.

# Triggers added by dh_makeshlibs/13.24.1

activate-noawait ldconfig

override_dh_<command>:

    dh_command

    make something-else

execute_after_dh_<command>:

    make something-else

Im debmake-autogenerierten nobug-Package...

   dh_testdir

   dh_update_autotools_config

   dh_autoreconf

   dh_auto_configure

   dh_auto_build

   dh_auto_test

   create-stamp debian/debhelper-build-stamp

   dh_testroot

   dh_prep

   dh_installdirs

   dh_auto_install --destdir=debian/nobug/

   dh_install

   dh_installdocs

   dh_installchangelogs

   dh_installexamples

   dh_installman

   dh_installcatalogs

   dh_installcron

   dh_installdebconf

   dh_installemacsen

   dh_installifupdown

   dh_installinfo

   dh_installinit

   dh_installtmpfiles

   dh_installsystemd

   dh_installsystemduser

   dh_installmenu

   dh_installmime

   dh_installmodules

   dh_installlogcheck

   dh_installlogrotate

   dh_installpam

   dh_installppp

   dh_installudev

   dh_installgsettings

   dh_installinitramfs

   dh_installalternatives

   dh_bugfiles

   dh_ucf

   dh_lintian

   dh_icons

   dh_perl

   dh_usrlocal

   dh_link

   dh_installwm

   dh_installxfonts

   dh_strip_nondeterminism

   dh_compress

   dh_fixperms

   dh_missing

   dh_dwz -a

   dh_strip -a

   dh_makeshlibs -a

   dh_shlibdeps -a

   dh_installdeb

   dh_gencontrol

   dh_md5sums

   dh_builddeb

erzeugt eine Analyse der Hook-  und override-Targets, die dh sehen wird

Hiermit ist das Git-Repo gemeint, in dem die Debianisierung verfügbar ist, nicht upstream; (in unserem Fall ist da aber auch Upstream mit in der Historie)

das ist eine non-standard GNU-Extension (wobei size = 1byte angenommen wird).

Das hier ist eine aufgegebene Codebasis von einem »old-styler« — Umerziehung sinnlos

wegen git-buildpackage bauen wir nicht im Arbeitsverzeichnis, aus dem der Build startet; brauche also eine portable und saubere Lösung, um den Build für das Manual zu starten

Autoconf gibt zwar eine Warnung aus, macht es aber trotzdem

...da Ubuntu nur ein Nebenschauplatz ist; für Ubuntu gäbe es Launchpad und die PPAs (und ich kenne mich mit dieser Infrastruktur aus) — da *.ddeb die Ubuntu-spezifische Lösung für Debug-Pakete ist, könnte man dorthin ausweichen...

  • master fungiert direkt als upstream-Branch
  • das Versionsschema angepaßt: upstream-tag = v%(version)s

wird veröffentlicht in einem separaten Git-Repo debian/lumiera, ist aber auch in meinem 'ichthyo'-Repository

die stammt eigentlich aus der Lumiera-Webiste und wurde umgeschrieben in ein eigenständiges HTML.... unbedingt per Diff/Merge  aktualisieren vom Website-Content!

Dort bin ich bereits vor ½ Jahr durch die ganze Serie von Neuerungen im Debian-Standard gegangen und habe viele Detail-Fragen geklärt

Soll die Dokumentationsgenerierung überspringen, aber ein leeres Doumentations-Paket bauen

...und wenn das problematisch wird, sollte das DEB-Packaging die DEB_BUILD_OPTIONS "terse" unterstützen

war letztlich eine Sackgasse,

hat aber wichtige Umstände geklärt

Speziell GLib ist bekanntermaßen buggy, wenngliech auch sich das in den letzten Jahren verbessert hat. Aber die Leute ändern und modernisieren auch ständig ... also gibt es nicht wirklich einen »Kompatibilitäts-Level«

deshalb ist es ja »Referenz-Platform«, was auch bedeutet, wir orientieren uns nach Vorne, und die Referenz-Platform ist eigentlich der Mindest-Level (mit ein klein wenig Wasser unter dem Kiel, wegen Ubuntu)

...nur mit dem Unterschied, daß wir hier nun die Aufrufe direkt im debian/rules stehen haben; im Grunde hat uns die "magic" gar nicht viel gebracht, nachdem man sich erst mal damit beschäftigt hat.

override_dh_auto_clean: (hier zusätzlich optcache und configure-cache weglöschen)

override_dh_auto_build

override_dh_auto_test

override_dh_auto_install

DEB_SCONS_OPTIONS = \

BUILDLEVEL=ALPHA \

DEBUG=True       \

OPTIMIZE=False   \

VALGRIND=False   \

ARCHFLAGS=" -fstack-protector-strong"

SCons verwendet eine MD5-Summe über den Quellcode und außerdem  auch über alle Compiler-Schalter und Environment-Settings

sollte daher alle drei Targets mit den gleichen Settings aufrufen

Folgeproblem: *** Directory path for variable 'INSTALLDIR' does not exist: debian/lumiera

prüft und Syntax, aber nicht ob das Filesystem-Element existiert

NodeDevel_test : hier werden Checksummen über Datenblöcke gebildet und eine dafür präparierte Render-Pipeline geschickt.

Dieser Test enthält keine concurrency — also deutet ein (nicht reproduzierbarer) Fehler hier auf ein Hardware-Problem hin (den Verdacht hab ich schon länger)

Unterschied: -fstack-protector-strong

Buffer clone[50];

for (uint i=0;  i<channels; ++i)

CHECK (not clone[i]->isSane());

ist Sane() prüft nur header_.isPlausible() ⟹ das prüft ob ein das Marker-Wort im Header liegt — was durchaus der Fall sein kann, wenn exakt an der gleichen Stelle vorher schon mal ein Buffer-Header lag....

...vermutlich mit dem temporären TestFrame, der beim vorhergehenden Test während der Verifikation erzeugt wird. Dieser dürfte ja in den Bereich fallen, der in dieser Methode vom zweiten Array abgedeckt wird

alle Verwendungen (nur in diesem Test) brauchen einen sauberen Buffer

Dieses Konstrukt wird (augenscheinlich, habe dies stichprobenhaft geprüft) nur in diesem einen Test verwendet. Es werden jeweils ein/mehrere Buffer auf den Stack gelegt. In den meisten Fällen wir dann in diesen Buffer etwas generiert. In einigen Testfällen wird vorher geprüft, daß der Buffer keinen validen Testframe enthält, und nachher, daß dies der Fall ist. Die Prüfung vorher scheitert im vorliegenden Problemfall, vermutlich weil im Stack-Speicher exakt an der gleichen Stelle vorher das gleiche Verarbeitungsmuster stattfand

TestFrame selber generiert in seinem Konstruktor stets eine valide Buffer-Füllung und belegt alle Metadaten. Möglicherweise hatte ich die Idee, den »Buffer« über beliebige Storage legen zu können, um sie dann zu begutachten. Diese Verwendung fand dann aber nicht statt

das erspart mir das Gewürge, da std::byte kein numerischer Datentyp ist

hab gesehen daß der Speicher jetzt mit NULL gefüllt wird

Seltsam....

Ich sehe keinerlei generische Platzhalter in meinem File...  Allerdings werde ich das demnächst ohnehin umstellen auf das maschinenlesbare Format

g++ -o target/modules/libtest-vault.so -Wl,--no-undefined -Wl,--as-needed -Wl,-soname=libtest-vault.so -Wl,-rpath=\$ORIGIN/../modules,--enable-new-dtags -shared tests/vault/mem/extent-family-test.os tests/vault/gear/activity-detector-test.os tests/vault/gear/scheduler-usage-test.os tests/vault/gear/test-chain-load-test.os tests/vault/gear/scheduler-commutator-test.os tests/vault/gear/scheduler-activity-test.os tests/vault/gear/scheduler-invocation-test.os tests/vault/gear/work-force-test.os tests/vault/gear/block-flow-test.os tests/vault/gear/scheduler-stress-test.os tests/vault/gear/scheduler-service-test.os tests/vault/gear/scheduler-load-control-test.os tests/vault/gear/special-job-fun-test.os -lm -ldl -lpthread -lrt -lnobugmt -lstdc++fs -lboost_program_options -lgavl target/modules/liblumieravault.so target/modules/liblumieracommon.so target/modules/liblumierasupport.so

Da wir ja ein durchaus spezielles Versionsnummernschema haben. Die Fehlermeldung sieht so aus, als würde das als ein RC für eine Version v0 gedeutet

Für die Installation per DEB-Paket brauchen wir kein allgemeines README, da vor allem die Bau- und Installations-Vorrausetzungen bereits erfüllt sind, und auch die Lizenz anderweitig deklariert wird. Daher sollte hier im README.debian alles für den reinen User Wissenswerte stehen

  • auf die NEWS
  • Auf die Website
  • (geplant: User-Manual)

/documentation/user/manual.html

das war eine Scheiß-Arbeit...

  • das Savepage-Plugin schreibt das DOM komplett neu
  • es fügt <tbody>-Elemente ein
  • es löst HTML-Entities auf (was ich definitiv nicht will, obschon es wegen UTF-8 eigentlich möglich wäre)
  • es fügt hinter jedem schließenden Tag noch whitespace ein (Windows???)

und den Output in doc/devel/LumieraHelpLandingPage.html einchecken

Denn in Zukunft sollt das Buildsystem auch irgendwann mal ein User-Manual generieren und korrekt installieren; diese Platzhalter-Seite markiert mithin bereits den Ort dieser Installation und dient auch als Anker für diese zukünftige Funktionalität

....naja... das ist relativ; wie bei jeder DSL, wenn man mal das Schema verstanden hat, dann ist es konzis und deklarativ, und wenn man das Schema wieder vergessen hat, dann ist es »magisch«

(gemäß FHS) ⟹ <prefix>/share/doc/<paktename>/

Das hier ist ein Buildsystem für ein Projekt von überschaubarem Umfang. Ganz ehrlich, ich erwarte nicht, daß irgendjemand außer mir  das SCons mag und pflegt. Also geht es höchstens darum, nach bestehendem Schema die eine oder andere Datei hinzuzufügen. Überdies frage ich mich, wie lange wir bei SCons bleiben können (hoffentlich noch lange, und hoffentlich darf dann nicht ich einen Ersatz programmieren, oder mich mit CMake herumärgern, das bei Weitem nicht so deklarativ ist

dann wird subdir = <die letzte Pfadkomponente>, und das ist nicht, was man erwartet, sofern die Directories mehr als eine Ebene tief liegen. Kann ich jetzt nicht so ohne Weiteres ändern, ohne die Hilfsfunktion getDirname() (BuildHelper.py) umzuschreiben. Das ist es mir dann doch nicht wert!

Installationsziel: <prefix>/share/doc/lumiera/manual-html/index.html

Was aber auch daran liegen könnte, daß XFCE nicht Gnome ist

Bei den GUI-Icons gibt es ein app-icon.svg — aber das sieht unfertig/kaputt aus; vermutlich hat sich da Joel damals daran versucht, aber er ist daran gescheitert, daß das Lumiera-Logo sich nicht ohne Weiteres in ein Icon übersetzen läßt. Außerdem folgen diese SVGs ja einem ganz bestimmten Schema, und enthalten daher mehrere Varianten auf einem gemeinsamen »Canvas«

Tja... damals hat man zwar ein Lumiera-Logo designt — aber dann würde noch die Arbeit ausstehen, daraus eine Design-Linie zu entwickeln, die in verschiedensten Größen und in verschiedenem Umfeld konsistent funktioniert. Und ersatzweise hat dann jeder für jeden Einzelfall seine Variante gebastelt (und ich mach ja auch nix Anderes)

Deklarativität setzt etablierte Kategorien vorraus

Some works meticulously keep track of what copyrights and licenses apply to each particular file when combining source from multiple origins. Some instead apply

all licenses and all copyrights of component parts to the entire combined work. Both are generally accepted by the open source community, as long as it's clear that an effort is being made to identify and comply with the original licenses.

Das ist expizit erlaubt und stellt ein Statement für die Codebasis als Sammel-Veröffentlichung dar; diese Angabe ersetzt nicht die Angaben für die Files, aber stellt eine (damit zwingend kompatible) Veröffentlichung eines Sammelwerks dar, welches damit ein vereinfachtes Handle bietet für die Weiterverbreitung (und insofern dann zumindest die Einzel-Lizenzen überlagert). Bezüglich der Attributierung ist die GPL ja nicht so streng wie die CC oder Apache

...ist mir aber schon neulich aufgefallen, beim Aufbereiten der Daten für die Historien-Seite. Joel war also noch gut 1 1/2 Jahre formal im Projekt mit dabei, und hat an Meetings teilgenommen, bis er sich offiziell verabschiedet hat

das Alsa-Experiment, was unfertig ist und nur funktioniert wenn die Soundkarte zwei Kanäle hat und 16 Bit erwartet (also bei mir funktioniert es schon mal nicht deshalb). Allerdings ist das Experiment bis heute im Code, als lumiera-output-probe.

In der aktuellen Codebasis ist kein Code von ihm (mehr) enthalten, und sein Code wurde nie verwendet. Sein Beitrag ist auf der »Credits« page gewürdigt, allerdings auch da nur unter "IRC crew"

Die Authorschaft am Code hat rechtliche Relevanz, denn sie begründet eine Lizenz und auch ein Veto-Recht im Falle einer intendierten Lizenz-Änderung (z.B. Kommerzialisierung). Auf diesem Hintergrund halte ich es nicht sinnvoll, jemanden als "Code-Author" zu führen, der keinen signifikante  Beitrag zum Code geleistet hat, denn Copyright setzt eine gewisse »Schöpfungshöhe« vorraus.

Die Zeiträume des Copyright mit aufgeführt, und explizit auf das Git-Repo verwiesen, den allgemeinen Einsichten gemäß, die ich heute über die Wirkungsweise des GPL-Copyright gewonnen habe: Zeitpunkt und Autoren-Namen müssen für die GPL verbindlich mitgeführt werden, es ist aber nicht eigens eine »Attribution« notwendig (jedoch die Git-Historie allein genügt nicht, sondern die summarische Info muß auch irgendwo im Content zu finden sein)

Derzeit(2025) ist noch überhaupt nicht klar, in welcher Form wir Dokumentation ausliefern; naheliegend wäre es, unser (nicht vorhandenes) User-Manual aus den Asciidoc-Quellen zu bauen und damit als HTML lokal auszuliefern; das funktioniert aber nicht so ohne Weiteres, da die Seiten auf unsere Website abgestellt sind, und daher die volle Struktur vorraussetzen, und im Besonderen einen Webserver. Diesbezüglich fällt mir natürlich sofort das Stichwort »HTML-Help« ein — kann Asciidoc sowas generieren?

Anmerkung

  • git clone in ein Unterverzeichnis vom Paketverzeichnis
  • Paketverzeichnis in den Container gemounted
    podman run -v /Werk/Gang/pack:/pack -it ichthyo/pack-debian-trixie-20250317
  • im Container....
    • (ggfs dort equivs installieren)
    • cd /pack
    • mk-build-dep --install --remove
    • gbp buildpackage --git-tag

Vorbereitung:

./build-pack-dock debian:trixie-2025####

Dieses Release ist nur notwendig, um auch auf neueren Plattformen ein NoBug-Paket bereitzustellen; ich repräsentiere es lediglich als eine neue Debian-Version zum Release 2008.1

Christian hatte 2008 ... 2011 noch mit einigen Aufräum-Arbeiten und Dokumentation begonnen und dabei auch ein paar Bugs gefixt; 2017 gab es nochmal zwei Commits zur Dokumentation. Es ist aber offensichtlich, daß Christian das Interesse an NoBug (und generell C) verloren hat; er hat jetzt eine NoBug-Variante für Rust.

Ich habe lediglich diejenigen Changesets identifiziert, die nach meiner Einschätzung Bugs fixen. Die Anpassungen zum FNV-Hash nehme ich nicht mit (da wird eine 32-bit-Variante eingeführt), und auch reine Dokumentations-Ergänzungen lasse ich weg, da es sich fast nur um TODOs handelt

vielmehr mache ich nun den *.orig-Tarball sauber und liefere alle nachträglichen Fixes als quilt-Patches aus

  • git clone in ein Unterverzeichnis vom Paketverzeichnis
  • Paketverzeichnis in den Docker gemounted
    docker run -v /Werk/Gang/pack:/pack -it ichthyo/pack-debian-buster-20230919
  • (ggfs dorts equivs installieren)
  • mk-build-dep --install --remove
  • gbp buildpackage im Docker

Vorbereitung:

./build-pack-dock debian:trixie-2025####

Historien sowohl für Upstream, alsauch für das DEB-Paket finden sich zum Glück noch als archivierte Git-Repos; das letzte Upstream-Release 3.40 war August 2021, das Debian-Pakete wurde bis Juli 2024 weitergeführt. Die Debian-Historie reicht zurück bis September 2008, wobei anfangs nur das debian-Unterverzeichnis eingecheckt wurde, bis zur Umstellung auf git-buildpackage Dezember 2017, mit Version 3.26. Ab dieser Stelle ist die Debian-Git-Historie über einen upstream-Zwischenbranch regelmäßig mit der Upstream-Git-Historie verbunden (das freut mich; auch andere Leute sind auf die gleiche Idee gekommen)

...und zwar war der Grund, daß in den vorausgegangenen Jahren unser GUI-Entwickler Joel Holdsworth einige Verbesserungen und Fixes gemacht hatte, die zunächst upstream nicht übernommen wurden (man war damals überwiegend mit dem Port auf GTK-3 beschäftigt). Später, mit Lumiera's Umstellung auf GTK-3 konnten wir auf das offizielle Paket schwenken, in das Joel's Fixes inzwischen eingeflossen waren.

markiere die Historien-Übernahme für Lumiera

mit einem separaten Merge-Commit (JETZT)

— verbindet die Debian-Linien —

in einem Seitenzweig 2021

  • git clone in ein Unterverzeichnis vom Paketverzeichnis
  • Paketverzeichnis in den Container gemounted
    podman run -v /Werk/Gang/pack:/pack -it ichthyo/pack-debian-trixie-20250317
  • im Container....
    • (ggfs dort equivs installieren)
    • cd /pack
    • mk-build-dep --install --remove
    • gbp buildpackage --git-tag

Vorbereitung:

./build-pack-dock debian:trixie-2025####

Die GDLmm-Bindings haben es nie geschafft, in Debian aufgenommen zu werden; sie sind aber in anderen Linux-Distros paketiert, und irgendwann in das GNOME-Archiv überführt worden.

Seinerzeit habe ich nur irgendwo publizierte Tarballs in eine kurze Historie eingecheckt, aber wie sich nun herausstellt, war das praktisch vollständig; auch habe ich bereits das letzte jemals publizierte Release genommen, um darauf ein DEB-Paket neu aufzubauen. Dabei habe ich mich an das Glibmm-Paket als Vorlage gehalten (und nicht viel Aufwand investiert). Insofern sollte dieses Paket lediglich minimal modernisiert werden, so daß es auf aktuellen Debian-Distros noch akzeptabel ist. Ich bleibe bei CDBS

Die Historie reicht zurück bis 2009 und markiert 5 Releases, das erste davon noch für GTK-2.

Zur Dokumentation nehme ich die Upstream-Historie unverknüpft mit in das Lumiera-Repo auf; aber mein DEB-Paket bleibt auf einem »release«-Branch, der den Inhalt der Tarballs dokumentiert

läßt sich jenseits von Standard 10 (post Buster) praktisch nicht mehr verwenden, da viel einfach nicht mehr nachgeführt wurde; CDBS ist inzwischen weitgehend aufgegeben....

ebenfalls von mir lokal gepflegt

commit 4a5affc3f7db1490568fd2aeb199d8411e7ea5a5

Author: Christian Hergert <christian@hergert.me>

Date:   Fri Oct 16 18:45:45 2015 -0700

    build-fix for recent gtkmm

  • git clone in ein Unterverzeichnis vom Paketverzeichnis
  • Paketverzeichnis in den Container gemounted
    podman run -v /Werk/Gang/pack:/pack -it ichthyo/pack-debian-trixie-20250317
  • im Container....
    • (ggfs dort equivs installieren)
    • cd /pack
    • mk-build-dep --install --remove
    • gbp buildpackage --git-tag

Vorbereitung:

./build-pack-dock debian:trixie-2025####

Zehn Jahre Reise ins Ungewisse....

  • jetzt auf Debian/Trixie angekommen und C++23
  • mitten im »Playback Vertical Slice«
  • master fungiert direkt als upstream-Branch
  • das Versionsschema angepaßt: upstream-tag = v%(version)s
  • git clone in ein Unterverzeichnis vom Paketverzeichnis
  • Paketverzeichnis in den Container gemounted
    podman run -v /Werk/Gang/pack:/pack -it ichthyo/pack-debian-trixie-20250317
  • im Container....
    • (ggfs dort equivs installieren)
    • cd /pack
    • mk-build-dep --install --remove
    • gbp buildpackage --git-tag

Vorbereitung:

./build-pack-dock debian:trixie-2025####

Ticket #722

seit gcc-4.8 ist kein static_assert mehr in der STDlib

Das Log ist geflutet mit Warnungen, und viele Querverweise funktionieren nicht. Leider sind Probleme mit Doxygen schwer zu diagnostizieren, da der Lauf lange dauert, und die Codebasis riesig ist. Es gibt daher keine klar definierte Agenda, die man sinnvoll abarbeiten könnte. Ich kann immer nur von Zeit zu Zeit stichprobenartige Kontrollen machen

...obwohl ich diese Warnung abgeschaltet habe; wahrscheinlich handelt es sich um Fälle, wo ich (gemäß Clean Code) nur einen Parameter dokumentiert habe, der nicht selbsterklärend ist.

Ich finde die Seiten meist total verwirrend; es sind eigentlich nur die File-Kommentare sinnvoll. Selbst die Typ-Kommentare sind oft verwirrend, weil die Typen irgendwo stehen und der Kontext nicht so ersichtlich ist wie im Code. Hinzu kommt, daß der allgemeine Scope (auch Namespace) oft geflutet ist mit Metaprogramming-Definitionen, die nicht im richtigen Zusammenhang dargestellt werden. Es ist in dem Zusammenhang total unpraktisch, daß die Template-Parameter mit angegeben werden, und daß zusamengehörige Spezialisierungen nicht aufgesammelt werden. Das ist aber vermutlich gar nicht möglich.

Insgesamt scheint das Prinzip einer API-Doc nicht auf einen modernen, funktionalen Programmierstil zu passen.

Aber Vorsicht: es wird noch gar nicht verwendet

typed-allocation-manager.hpp 217

dumme Heap-Allokation eines char[]

!!!!!11!!

Da alignof(char) ≡ 1, ist es gradezu eine »Steilvorlage« für Probleme, wenn man einen Allocation-Buffer per char[] deklariert. GCC macht das nicht (er fängt Allokationen immer an 64bit-Grenzen an), aber grundsätzlich dürfte ein Compiler ein char[] anfangen, wo er grad' lustig ist. Besonders gefährlich, wenn das Array in ein anderes Objekt eingebettet wird. Nur den zuletzt genannten Fall habe ich 2019 abgeklärt; gegen alle sonstigen Schnaps-Ideen gibt es keinen Schutz — es sei denn, man hält sich an die Sprachmittel

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.

sieht aber soweit sauber aus...

Wenngleich auch ziemlich elaboriert; all diese Tracking-Funktionalität war seinerzeit angelegt worden, aber nur oberflächlich getestet, weil der Render-Engine-Entwurf von 2012 letztlich steckengeblieben ist. Jetzt, 2024 beginne ich, den TrackingHeapBlockProvider zu für Tests zu nutzen, einfach weil er da ist — und stelle fest, daß einige Details unfertig und etwas unausgereift wirken....

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?

Es ist also im Besonderen nicht notwendig, daß irgend ein gewerblicher »Dienst« angeboten wird, da auch alle Arten von Publikationen mit erfaßt werden sollen.

Auf eine Telefonnummer kann man verzichten. Aber die eMail muß regelmäßig gelesen werden (Reaktionszeit: Stunden). Und für die Adresse gilt: man muß dort persönlich einen Verantwortlichen antreffen können. Beispielsweise für eine gerichtliche  Ladung oder einen Durchsuchungsbeschluß. Es ist also eine Postbox explizit nicht ausreichend.

Umsatzsteuernummer, Name der Zulassungsbehörde, bei Freiberuflern der Name der Stelle die den Titel oder das Diplom ausgestellt hat. Bei Unternehmen die Rechtsform. Bei journalistischen Publikationen, die regelmäßige Aktualisierungen beinhalten und regelmäßig publizierte Druckerzeugnisse wiedergeben (d.h. Zeitungen, Nachrichtendienste) muß ein Verantwortlicher benannt werden, der sofort reaktionsfähig ist (es genügt nicht die Angabe eines Firmensitzes)

Konkret heißt das: es muß ein Link in den Footer, der »Impressum« heißt und mit höchstens einem Klick zum Ziel führt. Verlinken auf eine Hompage genügt nicht

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