diff --git a/src/vault/mem/buffer-metadata.hpp b/src/vault/mem/buffer-metadata.hpp index c12efba4d..def1c6d70 100644 --- a/src/vault/mem/buffer-metadata.hpp +++ b/src/vault/mem/buffer-metadata.hpp @@ -20,7 +20,7 @@ ** expose the correct buffer for each request. Typically -- and independent of the actual ** implementation -- the following properties need to be tracked ** - that overall storage size available within the buffer - ** - a pair of custom \em creator and \em destructor functions to use together with this buffer + ** - a pair of custom _creator_ and _destructor_ functions to use together with this buffer ** - an additional client key to distinguish otherwise otherwise identical client requests ** These three distinctions are applied in sequence, thus forming a type tree with 3 levels. ** Only the first distinguishing level (the size) is mandatory. The others are provided, @@ -34,7 +34,10 @@ ** [Buffer Descriptor](\ref BuffDescr), embedded into each BuffHandle. While the engine ** mostly uses these handles in the way of a pointer, the buffer descriptor acts as a ** configuration tag attached to the buffer access, allowing to re-access a context - ** within the buffer provider implementation. + ** within the buffer provider implementation. Notably the Render Engine employs + ** several BufferProvider services (Memory, Cache, Output) that share the metadata + ** table, albeit with thread-local caches. For this reason, the registered types + ** are anchored at a root-hash, allowing to keep those several clients apart. ** ** @todo 2/2026 After refactoring the BufferProvider and splitting up the implementation ** in two realms (state and storage), the purpose of this code her became much @@ -54,7 +57,6 @@ #include "lib/error.hpp" -#include "lib/symbol.hpp" #include "lib/hash-value.h" #include "lib/util-foreach.hpp" #include "include/logging.h" @@ -70,7 +72,6 @@ namespace vault{ namespace mem { using lib::HashVal; - using lib::Literal; using util::for_each; namespace err = lumiera::error; @@ -557,23 +558,25 @@ namespace mem { * Registry for managing buffer metadata. * This is an implementation level service, * used by the standard BufferProvider implementation. - * Each metadata registry (instance) defines and maintains - * a family of "buffer types"; beyond the buffer storage size, - * the concrete meaning of those types is tied to the corresponding - * BufferProvider implementation and remains opaque. These types are - * represented as hierarchically linked hash keys. The implementation - * may bind a TypeHandler to a specific type, allowing automatic invocation - * of a "constructor" and "destructor" function on each buffer of this type, - * when _locking_ or _freeing_ the corresponding buffer. - * @todo 2/2026 must extract an interface here..... //////////////////////////////////////////////////////TICKET #1410 : clean-up BufferProvider base implementation - * + * A metadata registry defines and maintains several families + * of "buffer types"; beyond the buffer storage size, the concrete + * meaning of those types is tied to the corresponding BufferProvider + * implementation and remains opaque. These types are represented as + * hierarchically linked hash keys. The implementation may bind a + * TypeHandler to a specific type, allowing automatic invocation + * of a "constructor" and "destructor" function on each buffer + * of this type, when _locking_ or _freeing_ the corresponding + * buffer. When creating or accessing a top-level (type)-Key, + * a hash-ID of the "family" must be provided; this is a + * root anchor and allows several clients to use a common + * metadata table + * @remark in the Render Engine there can be several + * BufferProvider services, while the BufferMetadata + * uses a common setup with thread-local caches. */ class BufferMetadata : util::NonCopyable { - Literal id_; - HashVal family_; - metadata::Table table_; ///////////////////////////TICKET #854 : ensure proper locking happens "somewhere" when mutating metadata @@ -588,9 +591,8 @@ namespace mem { * @param implementationID to distinguish families of * type keys belonging to different registries. */ - BufferMetadata (Literal implementationID) - : id_(implementationID) - , family_(hash_value(id_)) + BufferMetadata() + : table_{} { } /** combine the distinguishing properties @@ -602,14 +604,16 @@ namespace mem { * the third level. All these levels describe abstract type * keys, not entries for concrete buffers. The latter are * always created as children of a known type key. + * @param familyID root anchor to keep mandators apart */ Key - key (size_t storageSize + key (HashVal familyID + ,size_t storageSize ,TypeHandler instanceFunc =TypeHandler::RAW ,LocalTag specifics =LocalTag::UNKNOWN) { REQUIRE (storageSize); - Key typeKey = trackKey (family_, storageSize); + Key typeKey = trackKey (familyID, storageSize); if (nontrivial(instanceFunc)) typeKey = trackKey (typeKey, instanceFunc); @@ -826,6 +830,9 @@ namespace mem { { if (isKnown (key)) return; table_.store (Entry{key, nullptr}); + // stored a type marker entry with this key + ENSURE (HashVal(key) == HashVal(get(key))); + ENSURE (get(key).isTypeKey()); } /** store a fully populated entry immediately starting with locked state diff --git a/src/vault/mem/simple-buffer-state-registry.hpp b/src/vault/mem/simple-buffer-state-registry.hpp index 610755ca3..190d58a0b 100644 --- a/src/vault/mem/simple-buffer-state-registry.hpp +++ b/src/vault/mem/simple-buffer-state-registry.hpp @@ -42,6 +42,7 @@ namespace mem { class SimpleBufferStateRegistry : public BufferProviderSetup::Stage { + HashVal familyID_; BufferMetadata metadata_; /* === BufferStage interface === */ @@ -68,8 +69,8 @@ namespace mem { ID defineBufferType (size_t buffSiz, TypeHandler handlerFunctions, LocalTag localTag) { - return lookup (metadata_.key (buffSiz, move (handlerFunctions), localTag)); - } // deliberately: create storage, and return reference to it + return lookup (metadata_.key (familyID_, buffSiz, move (handlerFunctions), localTag)); + } // deliberately: maybe create storage, and return reference to it ID mark_locked (ID typeKey, BuffAlloc alloc) override @@ -111,7 +112,8 @@ namespace mem { public: SimpleBufferStateRegistry (Literal implementationID) - : metadata_{implementationID} + : familyID_{hash_value (implementationID)} + , metadata_{} { } }; diff --git a/tests/vault/mem/buffer-metadata-test.cpp b/tests/vault/mem/buffer-metadata-test.cpp index 5ed75079c..27a50bc67 100644 --- a/tests/vault/mem/buffer-metadata-test.cpp +++ b/tests/vault/mem/buffer-metadata-test.cpp @@ -22,6 +22,7 @@ #include "test/test-helper.hpp" #include "vault/mem/buffhandle.hpp" #include "vault/mem/buffer-metadata.hpp" +#include "lib/symbol.hpp" #include "lib/util.hpp" #include @@ -34,6 +35,7 @@ using test::testData; using util::isSameObject; using util::isnil; using lib::randStr; +using lib::Literal; namespace vault { namespace mem { @@ -85,7 +87,10 @@ namespace test { size_t SIZE_A{0}; size_t SIZE_B{0}; - /** common Metadata table to be tested */ + /// root anchor for all type-keys, similar to a mandator ID + HashVal FAM{hash_value (Literal{"BufferMetadata_test"})}; + + /// common Metadata table to be tested unique_ptr meta_; virtual void @@ -106,11 +111,11 @@ namespace test { ensure_proper_fixture() { if (!meta_) - meta_.reset(new BufferMetadata("BufferMetadata_test")); + meta_.reset(new BufferMetadata); return (SIZE_A != SIZE_B) - and (JUST_SOMETHING != meta_->key(SIZE_A)) - and (JUST_SOMETHING != meta_->key(SIZE_B)) + and (JUST_SOMETHING != meta_->key(FAM, SIZE_A)) + and (JUST_SOMETHING != meta_->key(FAM, SIZE_B)) ; } @@ -119,11 +124,11 @@ namespace test { verifyBasicProperties() { // retrieve some type keys - metadata::Key key = meta_->key(SIZE_A); + metadata::Key key = meta_->key(FAM, SIZE_A); CHECK (key); - metadata::Key key1 = meta_->key(SIZE_A); - metadata::Key key2 = meta_->key(SIZE_B); + metadata::Key key1 = meta_->key(FAM, SIZE_A); + metadata::Key key2 = meta_->key(FAM, SIZE_B); CHECK (key1); CHECK (key2); CHECK (key == key1); @@ -204,10 +209,10 @@ namespace test { { // to build a descriptor for a buffer holding a TestFrame TypeHandler attachTestFrame = TypeHandler::create(); - metadata::Key bufferType1 = meta_->key(sizeof(TestFrame), attachTestFrame); + metadata::Key bufferType1 = meta_->key(FAM, sizeof(TestFrame), attachTestFrame); // to build a descriptor for a raw buffer of size SIZE_B - metadata::Key rawBuffType = meta_->key(SIZE_B); + metadata::Key rawBuffType = meta_->key(FAM, SIZE_B); // to announce using a number of buffers of this type LocalTag transaction1(1); @@ -321,7 +326,7 @@ namespace test { verifyStateMachine() { // start with building a type key.... - metadata::Key key = meta_->key(SIZE_A); + metadata::Key key = meta_->key(FAM, SIZE_A); CHECK (NIL == meta_->get(key).state()); CHECK (meta_->get(key).isTypeKey()); CHECK (not meta_->isLocked(key)); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index cab8cd522..15cd42299 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -77868,6 +77868,63 @@ + + + + + + + + + +

+ enthält implizit ein System der Bedingtheiten: bestimmte »Arten« von Daten können bearbeitet und transformiert werden, bisweileilen ist eine Adaptierung möglich, aber manche Verbindungen sind „ganz offensichtlich“ nicht möglich. Im Rahmen einer Domain-Ontology sind deratige Zusammenhänge zumeist unmittelbar gegeben, und werden daher oft gar nicht richtig ausformuliert +

+ +
+
+ + + + +

+ Hier werden nur symbolische Repräsentationen der Bezüge in eine Domain-Ontology festgehalten, und hinter der Konzeption eines »Asset« zusammengefaßt. In der Regel ist das hier nur ein indirekter Verweis: von einer Medien, oder Processor-ID kommt man auf die dahinter liegende Domain-Ontology, die letztlich die Frage beantworden muß, ob »etwas geht« +

+ +
+
+ + + + +

+ Das ist die einzige Stelle, and der alle Bezüge konkret aufgelöst und neu verknüpft werden müssen; wenn im Rahmen einer Processing-Pipeline ein Abschnitt »auf« einem bestimmten StreamType »laufen« muß, dann wird ein StreamType-ImplementationType benötigt; dieser legt u.A. die benöigten Buffer fest ⟹ der Builder hält dies in Form eines Buffer-Descriptors fest, und bettet diesen in die Render-Nodes ein +

+ +
+
+ + + + +

+ Hier entspricht der Buffer-Descriptor einer Buffer typeID, welche irgenwann einmal von „jemandem“ aufgelöst und zugeordnet wurde; diese Zuordnung ist auf dieser Ebene in der Engine bereits nur noch implizit in das Node-Network »gebacken«, und zeigt sich nur in Form der hinterlegten Buffer-Descriptoren und Render-Funktionen. +

+ +
+
+ + + + +

+ Dieser läuft im Wesentlichen Thread-local; er erzeugt im Moment der tatsächlichen Buffer-Zuordnung einen sub-Key, den »stateKey« — und im zugehörigen Metadaten-Eintrag speichert der BufferStore wichtige Verwaltungsinformationen, wie MRU-Daten +

+ +
+
+
+
@@ -91695,6 +91752,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+
@@ -99651,6 +99709,18 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + +

+ ...denn dort ist nicht nur die Buffer-size, sonder ggfs. auch ein TypeHandler hinterlegt. Alle diese Informationen zusammen werden abstrakt repräsentiert als Buffer-Descriptor, und in dieser Form auch schon vom Builder angelegt und in die Nodes eingebettet +

+ +
+ + +
@@ -106146,8 +106216,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -106175,15 +106246,23 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + + + + - + + + + + @@ -106197,28 +106276,63 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + +

+ ...hier hat sich einiges geklärt im Zuge der Überarbeitung BufferProvider / OutputSlot im Frühjahr 2026. Die Buffer-Metadaten sind hierarchisch, und das übersetzt sich zur Laufzeit in eine layered structure +

+
    +
  • + ein allgemeinter type-Key liegt in einer zentralen Metadaten-Tabelle +
  • +
  • + thread-local gibt es einen Eintrag mit einem Sub-Typ, der auf der konkreten Buffer-Adresse basiert (und die kann es nur einmal geben) +
  • +
+ +
+
- - + + + - - + + - + + + + +

+ aber BufferProvider ist nicht(war nicht) auf Delegieren ausgelegt +

+ +
- + + + + + + + + +
@@ -122946,7 +123060,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -123854,6 +123968,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + @@ -143539,7 +143654,38 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + +

+ Nach erster Bestandsaufnahme konnte ich bestätigen, daß das Buffer Provider Protocol im aktuellen Horizont gut genug ist und keiner Revision unterzogen werden muß. Die massiven Probleme steckten in der Implementierung, die sich tatsächlich als eine Test-Implementierung mit Support für Tracking und nachträgliche Verifikationen herausstellt — zudem bestand hier eine giftige Verkopplung mit dem OutputSlot, für den es nichteinmal ein klares Abstraktions-Interface gab, und dessen Implementierung ein reiner Test-Entwurf war, der direkt in die BufferProvider-Implementierung eingesprungen ist. +

+

+ +

+

+ Daher habe ich beschlossen, den BufferProvider komplett zu »entkernen«. Die bestehende Implementierungs-Logik wurde auf zwei Säulen aufgeteilt: +

+
    +
  • + die Metadaten-Verwaltung (Interface: BufferStage) +
  • +
  • + die Storage-Verwaltung  (Interface: BufferStore) +
  • +
+

+ Sodann wurde der (vorerst beiseite gestellte) alte Implementierungs-Code in dieses Schema eingefügt, und alle Teile ausgelassen und neu implementiert, die ersichtlich mit Diagnose/Tracking vermischt waren. Damit konnte die Funktionsfähigkeit auf Interface-Ebene demonstriert werden (BufferProviderProtocol_test). Danach wurden die Diagnose-Tools komplett als neue Erweiterung über diese aufgeräumte Struktur gesetzt, und als sehr viel einfachere, eingekapselte, eigenständige Komponente neu entwickelt. In einem letzten Schritt konnte ich den alten Backend-Code (mit dem inzwischen stillgelegten Tracking) komplett ablösen und ebenfalls durch eine sehr einfache, »naive« Implementierung ersetzen. +

+

+ +

+

+ Im Ergebnis gibt es nun eine viel flexiblere interne Struktur, in der Sub-Classing auf beiden Säulen mögich ist, oder auch Anpassung per Proxying. Die Buffer-Metadaten-Implementierung konnte dabei nahezu komplett so übernommen und bestätigt werden, wie sie seit 2024 vorliegt; es wurden lediglich einige API-Methoden etwas versäubert. +

+ +
@@ -143764,6 +143910,458 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
+ + + + + + +

+ nach den Aufräum-Arbeiten liegt nun eine saubere default-Implementierung vor +

+ +
+ + + + + + + + + + + + + + +

+ ...um die Komplexität beherrschbar zu machen; ich habe gemerkt, daß das Durchdenken des Buffer-Provider-Protocols mich bereits an gedankliche Kapazitätsgrenzen bringt +

+ +
+
+ + + + +

+ also: front-end-Handle ⟶ function-call-chain ⟶ Implementierungs-Backend ⟲ Response +

+ +
+
+ + + + + + + + + +

+ ⟹ die Aufgabe : Thread-lokale Slices schaffen +

+ +
+ + + + + + + + + + + + + + + + + + + +

+ dazu müßten sie allerdings die gleiche Buffer-Adresse registrieren; +

+

+ insofern die Buffer-Adressen vom Store-Backend stammen, und konkret einem thread-lokalen Store gehören müssen, kann dieser Fall nicht auftreten. +

+ +
+ +
+ + + + +

+ Aber das stellt insofern eine Gefahr dar, als daß in den dadurch erzeugten duplikaten Einträgen tatsächlich verschiedene Daten liegen könnten. Wenn dagegen zwei Threads eine neu-Registrierung machen würden, dann könnte diese einfach in die Zentrale propagiert, und dort dedupliziert werden +

+ +
+
+
+ + + + + + + + + + + + + + +

+ Das heißt, auf zwei Komponenten +

+
    +
  • + einen C++ - Funktor-Typ +
  • +
  • + die konkrete Speicheradresse eines Funktors, der als Closure fungiert. +
  • +
+

+ Sofern man diese beiden Informationen in den Hash kombiniert, würde ein »Aliasing« verschiedener Type-Handler vermieden. +

+ +
+
+ + + + +

+ Nehmen zwar Speicher weg, sind aber ansonsten harmlos, da sie funktional äquivalent sind. Vermutlich können solche Duplikate auf einer höheren Ebene vermieden werden, sofern dort ebenfalls die Metadaten-typeIDs verknüpft werden — z.B. als Teil des StreamType-Implementation-Type. Wenn letztlich der gleiche Implementation-Type auch immer den gleichen Buffer-Type referenziert, wären wir sauber. +

+ +
+ +
+
+
+ + + + + +

+ eine Anfrage nur mit gegebenem typeKey muß dagegen blockend  aufgelöst werden +

+ +
+
+
+
+ + + + + + + + +

+ möglicherweise auch praktisch, weil das Buffer-Metadaten-System offen angelegt ist; ich kann jetzt nicht alle Nutzungsmuster überblocken, und kann deshalb auch nicht entscheiden, ob garantiert nur einziger Knoten benötigt wird (was aber in der Regel der Fall sein wird, da wir derzeit noch keine Type-Refinements machen) +

+ +
+
+
+
+
+ + + + + + + + + + + + + + + +

+ Wir bekommen also u.U einen oder mehrere Function-Calls, zusätzlich zu einer Indirektion durch einen Pointer. Nach Bauchgefühl klingt das für mich nach einem Overhead ~ 500ns. Laß uns das, angenommen, 10 mal machen müssen pro Verwemdung eines Buffer-Cycle ⟹ dann wären das 5µs Overhead. Andererseits weiß ich von meinen Scheduler-Untersuchungen, daß ein einziger Zugang zu einer atomic-Flag schon mal 30µs kosten kann. Also allein das würde den Trade-off (thread_local zu verwenden) schon rechtfertigen. Laß uns dann 50 Buffer in einem Job verwenden ⟹ dann wären das 50*5µs ≡ 250µs für einen Job der mindestens 5ms lang sein sollte, damit mein Scheduler-Konzept aufgeht.... +

+ +
+
+ + + + +

+ ...ich meine, weil ich jetzt, kummulativ, monatelang daran herumgedoktort habe, nur um jetzt nachträglich das Probem mit den per-Thread-Allokatoren zu sehen? +

+ +
+
+ + + + +

+ Klar ist: ich spüre Gegenwind +

+ +
+ + + + + +

+ besonders bei den BufferMetadata +

+ +
+
+ + + + +

+ Hier meine ich sogar das weitere Problem, nämlich daß das Buffer-Management in einem massiv-concurrent-Environment funktionieren muß +

+ +
+
+
+
+ + + + + + + + + + +

+ das größte Problem ist daß es eine solche immer noch nicht gibt +

+ +
+
+ + + + + +

+ und auch warum: ich wollte an den offensichtlichen Stellen nicht »durchmarschieren«. +

+

+ Ich weiß aber nicht, was dann passiert wäre, ob ich von der „offensichtlich einfachen“ Lösung weitergekommen wäre, ohne diese naheliegende Lösung dann doch letztlich komplet wieder einzureissen und das zu bauen, was ich jetzt zu bauen versuche? +

+

+ +

+

+ Und noch etwas hat stets eine Rolle gespielt: ich habe mir immer die Frage gestellt, was würde passieren, wenn Du zusammen mit einem »Pragmatiker« allein im Boot sitzt? Dann stürzt sich der Pragmatiker auf das Offensichtliche und verbaut Dir alles, was Dir wichtig ist und warum Du die ganzen Mühen auf Dich nimmst. Mehr noch, ich befand mich die ganzen ersten Jahre in dieser Position, und habe nach außen hin vorgegeben, daß ich an der »pragmatischen« Lösung arbeite, wohl wissend, daß ich tatsächlich der Einzige mit entsprechendem Stehvermögen bin. +

+

+ +

+

+ Insofern bin ich noch nicht out of the woods, und sollte keine Zeit mit Zweifeln verbringen +

+ +
+
+
+ + + + +

+ Fazit: wenn thread_local wie erwartet funktioneren würde, gäbe es kein Problem +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ ...weil dann potentiell der State der Thread-Local-Instance davon abhängt, wann der Thread gestartet wurde im Verhältnis zur Änderung des Instance-State im Zugriffs-Pfad; zudem geht aus dem Standard nicht hervor, wann konkret eine einzelne thread-Lokale Instanz erzeugt wird +

+ +
+
+ + + + + +

+ Ich weiß nicht, ob das sogar vom Standard so verlangt wird (hab ihn nicht gelesen, und CPPReference sagt nichts dazu). Aber da die Lebensspanne nur indirekt definiert wird (lebt länger als der Thread), würde alles andere letztlich auf undefined behavour  hinauslaufen +

+ +
+
+ + + + +

+ ⟹ muß in den Typ repräsentiert werden +

+ +
+
+
+ + + + + + + + + + + +

+ ...denn effektiv definiert jede Kombination von Template-Parametern einen neuen Typ-Scope; aus Gründen der klaren Formulierung sollte diese Diversität in einen einzigen, benannten Parameter zusammengefaßt werden +

+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + +

+ diese Redundanz war entscheidend für die neue Implementierung des BufferProvider. Denn vermöge der Metadaten-Tabelle können wir jederzeit von einem gegebenen Hash wieder auf den Entry kommen, und somit die Storage, Size und den TypeHandler wieder zugänglich machen +

+ +
+
+ + + + + +

+ nämlich wenn zusätzlich zur Storage-Size auch schon ein TypeHandler und gar ein LocalTag gegeben wird +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Konkret: +

+
    +
  • + OutputSlot ist (neuerdings) ein ref-counting front-End-Handle. Es kann per Ref oder Kopie weitergereicht werden +
  • +
  • + wo die Connection-Impl gespeichert wird, bestimmt der OutputManager +
  • +
  • + die jeweilige JobInvocation gibt das BuffHandle vom OutputSlot als Ausgabe-Buffer in den Node-pull() +
  • +
+ +
+
+ + + + + + + + + +