diff --git a/src/lib/allocation-cluster.hpp b/src/lib/allocation-cluster.hpp index c5c4555e7..b179e83d2 100644 --- a/src/lib/allocation-cluster.hpp +++ b/src/lib/allocation-cluster.hpp @@ -366,7 +366,7 @@ namespace lib { { if (not bucket) return false; size_t currSize = bucket->getAllocSize(); - size_t delta = request - bucket->buffSiz; + long delta = long(request) - long(bucket->buffSiz); return this->mother_->canAdjust (bucket,currSize, currSize+delta); } diff --git a/src/lib/several-builder.hpp b/src/lib/several-builder.hpp index 5e1aa51b0..a5709364f 100644 --- a/src/lib/several-builder.hpp +++ b/src/lib/several-builder.hpp @@ -49,24 +49,24 @@ ** data storage, especially when the payload data type has alignment requirements ** beyond `alignof(void*)`, which is typically used by the standard heap allocator; ** additional headroom is added proactively in this case, to be able to shift the - ** storage buffer ahead to the next alignment boundrary. + ** storage buffer ahead to the next alignment boundary. ** ** # Handling of data elements ** ** The ability to emplace a mixture of data types into the storage exposed through ** the lib::Several front-end creates some complexities related to element handling. - ** The implementation relies on a rules and criteria based approach to decide on + ** The implementation uses generic rules and criteria based approach to decide on ** a case by case base if some given data content is still acceptable. This allows ** for rather tricky low-level usages, but has the downside to detect errors only ** at runtime — which in this case is ameliorated by the limitation that elements ** must be provided completely up-front, through the SeveralBuilder. ** - in order to handle any data element, we must be able to invoke its destructor - ** - an arbitrary mixture of types can thus only be accepted if either we can + ** - an arbitrary mixture of types can thus only be accepted if we can either ** rely on a common virtual base class destructor, or if all data elements ** are trivially destructible; these properties can be detected at compile ** time with the help of the C++ `` library ** - this container can accommodate _non-copyable_ data types, under the proviso - ** that the complete storage required is pre-allocated (using `reserve()` from + ** that the all the necessary storage is pre-allocated (using `reserve()` from ** the builder API) ** - otherwise, data can be filled in dynamically, expanding the storage as needed, ** given that all existing elements can be safely re-located by move or copy @@ -92,9 +92,8 @@ ** relevant when the _interface type_ has only lower alignment requirement, ** but an individual element is added with higher alignment requirements. ** In this case, while the spread is increased, still the placement of - ** the interface-type is used as anchor, possibly leading to misalignment. + ** the element-type \a E is used as anchor, possibly leading to misalignment. ** @see several-builder-test.cpp - ** */ @@ -166,6 +165,7 @@ namespace lib { return req; } + /** determine size of a reserve buffer to place with proper alignment */ size_t inline constexpr alignRes (size_t alignment) { @@ -173,6 +173,13 @@ namespace lib { } + /** + * Generic factory to manage objects within an ArrayBucket storage, + * delegating to a custom allocator \a ALO for memory handling. + * - #create a storage block for a number of objects + * - #createAt construct a single payload object at index position + * - #destroy a storage block with proper clean-up (invoke dtors) + */ template class ALO> class ElementFactory : protected ALO @@ -220,7 +227,7 @@ namespace lib { using BucketAlloT = typename AlloT::template rebind_traits; auto bucketAllo = adaptAllocator(); - // Step-2 : construct the Bucket metadata | ▽ ArrayBucket ctor arg ▽ + // Step-2 : construct the Bucket metadata | ▽ ArrayBucket ctor arg ▽ try { BucketAlloT::construct (bucketAllo, bucket, storageBytes, offset, spread); } catch(...) { @@ -274,6 +281,12 @@ namespace lib { }; + /** + * Policy Mix-In used to adapt to the ElementFactory and Allocator. + * @tparam I Interface type (also used in the lib::Several front-end + * @tparam E a common _element type_ to use by default + * @tparam ALO custom allocator template + */ template class ALO> struct AllocationPolicy : ElementFactory @@ -286,6 +299,7 @@ namespace lib { /** by default assume that memory is practically unlimited... */ size_t static constexpr ALLOC_LIMIT = size_t(-1) / sizeof(E); + /// Extension point: able to adjust dynamically to the requested size? bool canExpand(Bucket*, size_t){ return false; } Bucket* @@ -335,15 +349,35 @@ namespace lib { } }; + /** Default configuration to use heap memory for lib::Several */ template using HeapOwn = AllocationPolicy; }//(End)implementation details - /** - * Wrap a vector holding objects of a subtype and - * provide array-like access using the interface type. + + + + /*************************************************//** + * Builder to create and populate a lib::Several. + * Content elements can be of the _interface type_ \a I, + * or the _default element type_ \a E. When possible, even + * elements of an ad-hoc given, unrelated type can be used. + * The expected standard usage is to place elements of a + * subclass of \a I — but in fact the only limitation is that + * later, when using the created lib::Several, all content + * will be accessed through a (forced) cast to type \a I. + * Data (and metadata) will be placed into an _extent,_ which + * lives at a different location, as managed by an Allocator + * (With default configuration, data is heap allocated). + * The expansion behaviour is similar to std::vector, meaning + * that the buffer grows with exponential stepping. However, + * other than std::vector, even non-copyable objects can be + * handled, using #reserve to prepare a suitable allocation. + * @warning due to the flexibility and possible low-level usage + * patterns, consistency checks may throw at runtime, + * when attempting to add an unsuitable element. */ template ,class E =I ///< a subclass element element type (relevant when not trivially movable and destructible) @@ -370,6 +404,7 @@ namespace lib { { } + /* ===== Builder API ===== */ /** cross-builder to use a custom allocator for the lib::Several container */ @@ -392,6 +427,18 @@ namespace lib { return move(*this); } + /** discard excess reserve capacity. + * @warning typically this requires re-allocation and copy + */ + SeveralBuilder&& + shrinkFit() + { + if (not Coll::empty() + or size() < capacity()) + fitStorage(); + return move(*this); + } + /** append copies of one or several arbitrary elements */ template SeveralBuilder&& @@ -444,7 +491,7 @@ namespace lib { } - /** + /***********************************************************//** * Terminal Builder: complete and lock the collection contents. * @note the SeveralBuilder is sliced away, effectively * returning only the pointer to the ArrayBucket. @@ -461,7 +508,7 @@ namespace lib { size_t capReserve() const { return capacity() - size(); } - private: + private: /* ========= Implementation of element placement ================ */ template void emplaceCopy (IT& dataSrc) @@ -475,7 +522,7 @@ namespace lib { emplaceNewElm (ARGS&& ...args) { static_assert (is_object_v and not (is_const_v or is_volatile_v)); - + probeMoveCapability(); // mark when target type is not (trivially) movable ensureElementCapacity(); // sufficient or able to adapt spread ensureStorageCapacity(); // sufficient or able to grow buffer @@ -520,7 +567,7 @@ namespace lib { { if (not (Coll::empty() or Coll::hasReserve (requiredSiz, newElms) - or POL::canExpand (Coll::data_, requiredSiz*newElms) + or POL::canExpand (Coll::data_, requiredSiz*(Coll::size() + newElms)) or canDynGrow())) throw err::Invalid{_Fmt{"Several-container is unable to accommodate further element of type %s; " "storage reserve (%d bytes ≙ %d elms) exhausted and unable to move " @@ -565,9 +612,9 @@ namespace lib { void fitStorage() { - if (not Coll::data) - return; - if (not canDynGrow()) + REQUIRE (not Coll::empty()); + if (not (POL::canExpand (Coll::data_, Coll::size()) + or canDynGrow())) throw err::Invalid{"Unable to shrink storage for Several-collection, " "since at least one element can not be moved."}; Coll::data_ = POL::realloc (Coll::data_, Coll::size(), Coll::spread()); @@ -751,6 +798,8 @@ namespace lib { * `ALO` to create and destroy the memory buffer for content data * @param args optional dependency wiring arguments, to be passed to the allocator * @return a new empty SeveralBuilder, configured to use the custom allocator. + * @see lib::AllocationCluster (which provides a custom adaptation) + * @see SeveralBuilder_test::check_CustomAllocator() */ template template class ALO, typename...ARGS> diff --git a/src/lib/several.hpp b/src/lib/several.hpp index bf882df67..1c04411bf 100644 --- a/src/lib/several.hpp +++ b/src/lib/several.hpp @@ -52,8 +52,6 @@ ** The container is single-ownership (move-asignable); some additional metadata ** and the data storage reside within an `ArrayBucket`, managed by the allocator. ** In its simplest form, this storage is heap allocated and automatically deleted. - ** - ** @warning WIP-WIP in rework 6/2025 ** @see several-builder.hpp */ @@ -256,8 +254,6 @@ namespace lib { return data_ and data_->buffSiz >= size()*spread() + extraSize; } - - private: }; diff --git a/tests/library/several-builder-test.cpp b/tests/library/several-builder-test.cpp index 53d2c8b6d..baba90255 100644 --- a/tests/library/several-builder-test.cpp +++ b/tests/library/several-builder-test.cpp @@ -31,7 +31,6 @@ #include "lib/test/tracking-allocator.hpp" #include "lib/test/test-coll.hpp" #include "lib/test/test-helper.hpp" -#include "lib/test/diagnostic-output.hpp"////////////////TODO #include "lib/allocation-cluster.hpp" #include "lib/iter-explorer.hpp" #include "lib/format-util.hpp" @@ -39,11 +38,9 @@ #include "lib/several-builder.hpp" -#include #include using ::test::Test; -using std::vector; using std::array; using std::rand; @@ -58,7 +55,7 @@ using util::join; namespace lib { namespace test{ - namespace { // diagnostic test types + namespace { // invocation tracking diagnostic subclass... /** * Instance tracking sub-dummy @@ -85,7 +82,7 @@ namespace test{ { setVal (getVal() - explore(ext_).resultSum()); } - + long calc (int ii) override { @@ -163,7 +160,6 @@ namespace test{ /** @test demonstrate basic behaviour - * @todo WIP 6/24 ✔ define ⟶ ✔ implement */ void simpleUsage() @@ -184,12 +180,11 @@ namespace test{ * *unchecked wild cast* will happen on access; while certainly dangerous, * this behaviour allows for special low-level data layout tricks. * - the results from an iterator can be used to populate by copy - * @todo WIP 6/24 ✔ define ⟶ ✔ implement */ void check_Builder() { - // prepare to verify proper invocation of all constructors / destructors + // prepare to verify proper invocation of all constructors / destructors Dummy::checksum() = 0; { // Scenario-1 : Baseclass and arbitrary subclass elements @@ -274,7 +269,6 @@ namespace test{ * grow dynamically. * - all these failure conditions are handled properly, including exceptions emanating * from element constructors; the container remains sane and no memory is leaked. - * @todo WIP 6/24 ✔ define ⟶ ✔ implement */ void check_ErrorHandling() @@ -472,6 +466,7 @@ namespace test{ } + /** @test verify correct placement of instances within storage * - use a low-level pointer calculation for this test to * draw conclusions regarding the spacing of objects accepted @@ -482,7 +477,6 @@ namespace test{ * move-assign the lib::Several container instance; this * demonstrates that the latter is just a access front-end, * while the data elements reside in a fixed storage buffer - * @todo WIP 6/24 ✔ define ⟶ ✔ implement */ void check_ElementStorage() @@ -553,15 +547,15 @@ namespace test{ - /** @test TODO demonstrate integration with a custom allocator + /** @test demonstrate integration with a custom allocator * - use the TrackingAllocator to verify balanced handling * of the underlying raw memory allocations * - use an AllocationCluster instance to manage the storage - * @todo WIP 6/24 🔁 define ⟶ ✔ implement */ void check_CustomAllocator() { + // Setup-1: use the TrackingAllocator CHECK (0 == Dummy::checksum()); CHECK (0 == TrackingAllocator::checksum()); @@ -607,72 +601,75 @@ namespace test{ CHECK (0 == TrackingAllocator::use_count()); CHECK (0 == TrackingAllocator::checksum()); - AllocationCluster clu; - { - auto builder = makeSeveral() - .withAllocator(clu) - .reserve(4) // use a limited pre-reservation - .fillElm(4); // fill all the allocated space with 4 new elements - - size_t buffSiz = sizeof(Dummy) * builder.capacity(); - size_t headerSiz = sizeof(ArrayBucket); - expectedAlloc = headerSiz + buffSiz; -SHOW_EXPR(expectedAlloc) -SHOW_EXPR(builder.size()) - CHECK (4 == builder.size()); -SHOW_EXPR(builder.capacity()) - CHECK (4 == builder.capacity()); -SHOW_EXPR(clu.numExtents()) - CHECK (1 == clu.numExtents()); // AllocationCluster has only opened one extent thus far -SHOW_EXPR(clu.numBytes()) - CHECK (expectedAlloc == clu.numBytes()); // and the allocated space matches the demand precisely + + + {// Setup-2: use an AllocationCLuster instance + AllocationCluster clu; + size_t allotted = clu.numBytes(); + CHECK (allotted == 0); + { + auto builder = makeSeveral() + .withAllocator(clu) + .reserve(4) // use a limited pre-reservation + .fillElm(4); // fill all the allocated space with 4 new elements + + size_t buffSiz = sizeof(Dummy) * builder.capacity(); + size_t headerSiz = sizeof(ArrayBucket); + expectedAlloc = headerSiz + buffSiz; + CHECK (4 == builder.size()); + CHECK (4 == builder.capacity()); + CHECK (1 == clu.numExtents()); // AllocationCluster has only opened one extent thus far + CHECK (expectedAlloc == clu.numBytes()); // and the allocated space matches the demand precisely - builder.append (Dummy{23}); // now request to add just one further element -SHOW_EXPR(builder.capacity()) - CHECK (8 == builder.capacity()); // ...which causes the builder to double up the reserve capacity + builder.append (Dummy{23}); // now request to add just one further element + CHECK (8 == builder.capacity()); // ...which causes the builder to double up the reserve capacity - buffSiz = sizeof(Dummy) * builder.capacity(); - expectedAlloc = headerSiz + buffSiz; -SHOW_EXPR(buffSiz) -SHOW_EXPR(buffSiz + expectedAlloc) -SHOW_EXPR(buffSiz + headerSiz) -SHOW_EXPR(builder.size()) -SHOW_EXPR(builder.capacity()) -SHOW_EXPR(clu.numExtents()) - CHECK (1 == clu.numExtents()); // However, AllocationCluster was able to adjust allocation in-place -SHOW_EXPR(clu.numBytes()) - CHECK (expectedAlloc == clu.numBytes()); // and thus the new increased buffer is still placed in the first extent + buffSiz = sizeof(Dummy) * builder.capacity(); + expectedAlloc = headerSiz + buffSiz; + CHECK (1 == clu.numExtents()); // However, AllocationCluster was able to adjust allocation in-place + CHECK (expectedAlloc == clu.numBytes()); // and thus the new increased buffer is still placed in the first extent - // now perform another unrelated allocation - Dummy& extraDummy = clu.create(55); -SHOW_EXPR(clu.numExtents()) - CHECK (1 == clu.numExtents()); -SHOW_EXPR(clu.numBytes()) - CHECK (clu.numBytes() > expectedAlloc + sizeof(Dummy)); // but now we've used some further space behind that point - - builder.reserve(9); // ...which means that the AllocationCluster can no longer adjust dynamically -SHOW_EXPR(builder.size()) - CHECK (5 == builder.size()); // .....because this is only possible on the latest allocation opened -SHOW_EXPR(builder.capacity()) - CHECK (9 <= builder.capacity()); // And while we still got out increased capacity, -SHOW_EXPR(clu.numExtents()) - CHECK (2 == clu.numExtents()); // this was only possible by wasting space and copying into a new extent -SHOW_EXPR(clu.numBytes()) - buffSiz = sizeof(Dummy) * builder.capacity(); - expectedAlloc = headerSiz + buffSiz; - CHECK (expectedAlloc <= AllocationCluster::max_size()); - CHECK (clu.numBytes() == AllocationCluster::max_size() - + expectedAlloc); - - elms = builder.build(); // Note: assigning to the existing front-end (which is storage agnostic) - CHECK (5 == elms.size()); + // perform another unrelated allocation + Dummy& extraDummy = clu.create(55); + CHECK (1 == clu.numExtents()); + CHECK (clu.numBytes() > expectedAlloc+sizeof(Dummy)); // but now we've used some further space behind that point + + builder.reserve(9); // ...which means that the AllocationCluster can no longer adjust dynamically + CHECK (5 == builder.size()); // .....because this is only possible on the latest allocation opened + CHECK (9 <= builder.capacity()); // And while we still got the increased capacity as desired, + CHECK (2 == clu.numExtents()); // this was only possible by wasting space and copying into a new extent + buffSiz = sizeof(Dummy) * builder.capacity(); + expectedAlloc = headerSiz + buffSiz; + CHECK (expectedAlloc <= AllocationCluster::max_size()); + CHECK (clu.numBytes() == AllocationCluster::max_size() + + expectedAlloc); + + allotted = clu.numBytes(); + // request to throw away excess reserve + builder.shrinkFit(); + CHECK (5 == builder.size()); + CHECK (5 == builder.capacity()); + CHECK (allotted > clu.numBytes()); // dynamic adjustment was possible, since it is the latest allocation + allotted = clu.numBytes(); + + elms = builder.build(); // Note: assigning to the existing front-end (which is storage agnostic) + CHECK (5 == elms.size()); + CHECK (23 == elms.back().getVal()); + CHECK (55 == extraDummy.getVal()); + } // Now the Builder and the ExtraDummy is gone... + CHECK (5 == elms.size()); // while all created elements are still there, sitting in the Allocationcluster CHECK (23 == elms.back().getVal()); - CHECK (55 == extraDummy.getVal()); - } // Now the Builder and the ExtraDummy is gone... - CHECK (5 == elms.size()); // while all created elements are still there, sitting in the Allocationcluster - CHECK (23 == elms.back().getVal()); - CHECK (2 == clu.numExtents()); - } + CHECK (2 == clu.numExtents()); + CHECK (clu.numBytes() == allotted); + + CHECK (Dummy::checksum() > 0); + elms = move(Several{}); + CHECK (Dummy::checksum() == 55); // all elements within Several were cleaned-up... + CHECK (2 == clu.numExtents()); // but the base allocation itself lives as long as the AllocationCluster + CHECK (clu.numBytes() == allotted); + }// AllocationCluster goes out of scope... + CHECK (Dummy::checksum() == 0); // now the (already unreachable) extraDummy was cleaned up + } // WARNING: contents in Several would now be dangling (if we haden't killed them) }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index bb3e25099..8edde2b2a 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -81073,7 +81073,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -81081,7 +81082,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -81337,12 +81339,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + - - + + @@ -81530,7 +81533,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -81564,8 +81567,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + @@ -81624,7 +81628,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -81733,9 +81737,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + + + @@ -81767,6 +81773,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ @@ -81778,8 +81785,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -82016,7 +82023,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82229,7 +82236,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82284,7 +82291,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82427,7 +82434,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82442,7 +82449,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82557,9 +82564,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + + @@ -82807,8 +82815,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -82835,14 +82843,17 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + + + +
-
- - + + @@ -82858,21 +82869,24 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + + + - + + - + @@ -82892,7 +82906,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -82912,9 +82926,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + @@ -83002,16 +83016,20 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ +
- + + + - + @@ -83041,7 +83059,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83104,7 +83122,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83187,8 +83205,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + @@ -83203,9 +83222,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - +
@@ -83328,7 +83347,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83393,8 +83412,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -83413,19 +83432,22 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - + + + + @@ -83472,8 +83494,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -83503,9 +83525,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + + @@ -83521,7 +83545,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83570,7 +83594,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + + @@ -83672,7 +83698,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -83742,8 +83768,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...ich hab diese Mechanik ganz bewußt janz domm implementiert — also muß praktisch alle Info von oben kommen

- - +
@@ -83777,9 +83802,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
-
- - + + @@ -84210,9 +84234,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + @@ -84226,20 +84250,19 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...da AllocationCluster eine starke Größenbeschränkgung hat

- - +
- + - + - + @@ -84247,11 +84270,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
denn das eine zusätzliche Element würde locker reinpassen — und zudem sollte ja nun der Block in den nächsten Extent migrieren

- -
+
- + + @@ -84308,8 +84331,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...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

- - +
@@ -84319,8 +84341,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
...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().

- -
+
@@ -84339,7 +84360,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -84358,8 +84379,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + +
@@ -84368,8 +84389,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -84382,7 +84403,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -84509,8 +84530,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -84758,7 +84779,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -84811,8 +84832,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + +