// A runs-of-slots memory allocator. class RosAlloc { private: // Represents a run of free pages. class FreePageRun { public:
uint8_t magic_num_; // The magic number used for debugging only.
// The slot header. class Slot { public:
Slot* Next() const { return next_;
} void SetNext(Slot* next) {
next_ = next;
} // The slot right before this slot in terms of the address.
Slot* Left(size_t bracket_size) { returnreinterpret_cast<Slot*>(reinterpret_cast<uintptr_t>(this) - bracket_size);
} void Clear() {
next_ = nullptr;
}
private:
Slot* next_; // Next slot in the list. friendclass RosAlloc;
};
// We use the tail (kUseTail == true) for the bulk or thread-local free lists to avoid the need to // traverse the list from the head to the tail when merging free lists. // We don't use the tail (kUseTail == false) for the free list to avoid the need to manage the // tail in the allocation fast path for a performance reason. template<bool kUseTail = true> class SlotFreeList { public:
SlotFreeList() : head_(0U), tail_(0), size_(0), padding_(0) {}
Slot* Head() const { returnreinterpret_cast<Slot*>(head_);
}
Slot* Tail() const {
CHECK(kUseTail); returnreinterpret_cast<Slot*>(tail_);
}
size_t Size() const { return size_;
} // Removes from the head of the free list.
Slot* Remove() {
Slot* slot; if (kIsDebugBuild) {
Verify();
}
Slot** headp = reinterpret_cast<Slot**>(&head_);
Slot** tailp = kUseTail ? reinterpret_cast<Slot**>(&tail_) : nullptr;
Slot* old_head = *headp; if (old_head == nullptr) { // List was empty. if (kUseTail) {
DCHECK(*tailp == nullptr);
} return nullptr;
} else { // List wasn't empty. if (kUseTail) {
DCHECK(*tailp != nullptr);
}
Slot* old_head_next = old_head->Next();
slot = old_head;
*headp = old_head_next; if (kUseTail && old_head_next == nullptr) { // List becomes empty.
*tailp = nullptr;
}
}
slot->Clear();
--size_; if (kIsDebugBuild) {
Verify();
} return slot;
} void Add(Slot* slot) { if (kIsDebugBuild) {
Verify();
}
DCHECK(slot != nullptr);
DCHECK(slot->Next() == nullptr);
Slot** headp = reinterpret_cast<Slot**>(&head_);
Slot** tailp = kUseTail ? reinterpret_cast<Slot**>(&tail_) : nullptr;
Slot* old_head = *headp; if (old_head == nullptr) { // List was empty. if (kUseTail) {
DCHECK(*tailp == nullptr);
}
*headp = slot; if (kUseTail) {
*tailp = slot;
}
} else { // List wasn't empty. if (kUseTail) {
DCHECK(*tailp != nullptr);
}
*headp = slot;
slot->SetNext(old_head);
}
++size_; if (kIsDebugBuild) {
Verify();
}
} // Merge the given list into this list. Empty the given list. // Deliberately support only a kUseTail == true SlotFreeList parameter because 1) we don't // currently have a situation where we need a kUseTail == false SlotFreeList parameter, and 2) // supporting the kUseTail == false parameter would require a O(n) linked list traversal to do // the merge if 'this' SlotFreeList has kUseTail == false, which we'd like to avoid. void Merge(SlotFreeList<true>* list) { if (kIsDebugBuild) {
Verify();
CHECK(list != nullptr);
list->Verify();
} if (list->Size() == 0) { return;
}
Slot** headp = reinterpret_cast<Slot**>(&head_);
Slot** tailp = kUseTail ? reinterpret_cast<Slot**>(&tail_) : nullptr;
Slot* old_head = *headp; if (old_head == nullptr) { // List was empty.
*headp = list->Head(); if (kUseTail) {
*tailp = list->Tail();
}
size_ = list->Size();
} else { // List wasn't empty.
DCHECK(list->Head() != nullptr);
*headp = list->Head();
DCHECK(list->Tail() != nullptr);
list->Tail()->SetNext(old_head); // if kUseTail, no change to tailp.
size_ += list->Size();
}
list->Reset(); if (kIsDebugBuild) {
Verify();
}
}
private: // A pointer (Slot*) to the head of the list. Always 8 bytes so that we will have the same // layout between 32 bit and 64 bit, which is not strictly necessary, but we do so for 1) // uniformity, 2) we won't need to change this code if we move to a non-low 4G heap in the // future, and 3) the space savings by using 32 bit fields in 32 bit would be lost in noise // (won't open up enough space to cause an extra slot to be available).
uint64_t head_; // A pointer (Slot*) to the tail of the list. Always 8 bytes so that we will have the same // layout between 32 bit and 64 bit. The tail is stored to speed up merging of lists. // Unused if kUseTail is false.
uint64_t tail_; // The number of slots in the list. This is used to make it fast to check if a free list is all // free without traversing the whole free list.
uint32_t size_;
[[maybe_unused]] uint32_t padding_; friendclass RosAlloc;
};
// Represents a run of memory slots of the same size. // // A run's memory layout: // // +-------------------+ // | magic_num | // +-------------------+ // | size_bracket_idx | // +-------------------+ // | is_thread_local | // +-------------------+ // | to_be_bulk_freed | // +-------------------+ // | | // | free list | // | | // +-------------------+ // | | // | bulk free list | // | | // +-------------------+ // | | // | thread-local free | // | list | // | | // +-------------------+ // | padding due to | // | alignment | // +-------------------+ // | slot 0 | // +-------------------+ // | slot 1 | // +-------------------+ // | slot 2 | // +-------------------+ // ... // +-------------------+ // | last slot | // +-------------------+ // class Run { public:
uint8_t magic_num_; // The magic number used for debugging.
uint8_t size_bracket_idx_; // The index of the size bracket of this run.
uint8_t is_thread_local_; // True if this run is used as a thread-local run. bool to_be_bulk_freed_; // Used within BulkFree() to flag a run that's involved with // a bulk free.
[[maybe_unused]] uint32_t padding_; // Use a tailless free list for free_list_ so that the alloc fast path does not manage the tail.
SlotFreeList<false> free_list_;
SlotFreeList<true> bulk_free_list_;
SlotFreeList<true> thread_local_free_list_; // Padding due to alignment // Slot 0 // Slot 1 // ...
// Returns the byte size of the header. static size_t fixed_header_size() { returnsizeof(Run);
}
Slot* FirstSlot() const { const uint8_t idx = size_bracket_idx_; returnreinterpret_cast<Slot*>(reinterpret_cast<uintptr_t>(this) + headerSizes[idx]);
}
Slot* LastSlot() { const uint8_t idx = size_bracket_idx_; const size_t bracket_size = bracketSizes[idx];
uintptr_t end = reinterpret_cast<uintptr_t>(End());
Slot* last_slot = reinterpret_cast<Slot*>(end - bracket_size);
DCHECK_LE(FirstSlot(), last_slot); return last_slot;
}
SlotFreeList<false>* FreeList() { return &free_list_;
}
SlotFreeList<true>* BulkFreeList() { return &bulk_free_list_;
}
SlotFreeList<true>* ThreadLocalFreeList() { return &thread_local_free_list_;
} void* End() { returnreinterpret_cast<uint8_t*>(this) + gPageSize * numOfPages[size_bracket_idx_];
} void SetIsThreadLocal(bool is_thread_local) {
is_thread_local_ = is_thread_local ? 1 : 0;
} bool IsThreadLocal() const { return is_thread_local_ != 0;
} // Set up the free list for a new/empty run. void InitFreeList() { const uint8_t idx = size_bracket_idx_; const size_t bracket_size = bracketSizes[idx];
Slot* first_slot = FirstSlot(); // Add backwards so the first slot is at the head of the list. for (Slot* slot = LastSlot(); slot >= first_slot; slot = slot->Left(bracket_size)) {
free_list_.Add(slot);
}
} // Merge the thread local free list to the free list. Used when a thread-local run becomes // full. bool MergeThreadLocalFreeListToFreeList(bool* is_all_free_after_out); // Merge the bulk free list to the free list. Used in a bulk free. void MergeBulkFreeListToFreeList(); // Merge the bulk free list to the thread local free list. In a bulk free, as a two-step // process, GC will first record all the slots to free in a run in the bulk free list where it // can write without a lock, and later acquire a lock once per run to merge the bulk free list // to the thread-local free list. void MergeBulkFreeListToThreadLocalFreeList(); // Allocates a slot in a run.
ALWAYS_INLINE void* AllocSlot(); // Frees a slot in a run. This is used in a non-bulk free. void FreeSlot(void* ptr); // Add the given slot to the bulk free list. Returns the bracket size.
size_t AddToBulkFreeList(void* ptr); // Add the given slot to the thread-local free list. void AddToThreadLocalFreeList(void* ptr); // Returns true if all the slots in the run are not in use. bool IsAllFree() const { return free_list_.Size() == numOfSlots[size_bracket_idx_];
} // Returns the number of free slots.
size_t NumberOfFreeSlots() { return free_list_.Size();
} // Returns true if all the slots in the run are in use.
ALWAYS_INLINE bool IsFull(); // Returns true if the bulk free list is empty. bool IsBulkFreeListEmpty() const { return bulk_free_list_.Size() == 0;
} // Returns true if the thread local free list is empty. bool IsThreadLocalFreeListEmpty() const { return thread_local_free_list_.Size() == 0;
} // Zero the run's data. void ZeroData(); // Zero the run's header and the slot headers. void ZeroHeaderAndSlotHeaders(); // Iterate over all the slots and apply the given function. void InspectAllSlots(void (*handler)(void* start, void* end, size_t used_bytes, void* callback_arg), void* arg); // Dump the run metadata for debugging.
std::string Dump(); // Verify for debugging. void Verify(Thread* self, RosAlloc* rosalloc, bool running_on_memory_tool)
REQUIRES(Locks::mutator_lock_)
REQUIRES(Locks::thread_list_lock_);
private: // The common part of AddToBulkFreeList() and AddToThreadLocalFreeList(). Returns the bracket // size.
size_t AddToFreeListShared(void* ptr, SlotFreeList<true>* free_list, constchar* caller_name); // Turns a FreeList into a string for debugging. template<bool kUseTail>
std::string FreeListToStr(SlotFreeList<kUseTail>* free_list); // Check a given pointer is a valid slot address and return it as Slot*.
Slot* ToSlot(void* ptr) { const uint8_t idx = size_bracket_idx_; const size_t bracket_size = bracketSizes[idx]; const size_t offset_from_slot_base = reinterpret_cast<uint8_t*>(ptr)
- reinterpret_cast<uint8_t*>(FirstSlot());
DCHECK_EQ(offset_from_slot_base % bracket_size, static_cast<size_t>(0));
size_t slot_idx = offset_from_slot_base / bracket_size;
DCHECK_LT(slot_idx, numOfSlots[idx]); returnreinterpret_cast<Slot*>(ptr);
}
size_t SlotIndex(Slot* slot) const { const uint8_t idx = size_bracket_idx_; const size_t bracket_size = bracketSizes[idx]; const size_t offset_from_slot_base = reinterpret_cast<uint8_t*>(slot)
- reinterpret_cast<uint8_t*>(FirstSlot());
DCHECK_EQ(offset_from_slot_base % bracket_size, 0U);
size_t slot_idx = offset_from_slot_base / bracket_size;
DCHECK_LT(slot_idx, numOfSlots[idx]); return slot_idx;
}
// TODO: DISALLOW_COPY_AND_ASSIGN(Run);
};
// The magic number for a run. static constexpr uint8_t kMagicNum = 42; // The magic number for free pages. static constexpr uint8_t kMagicNumFree = 43; // The number of size brackets. static constexpr size_t kNumOfSizeBrackets = 42; // The sizes (the slot sizes, in bytes) of the size brackets. static size_t bracketSizes[kNumOfSizeBrackets]; // The numbers of pages that are used for runs for each size bracket. static size_t numOfPages[kNumOfSizeBrackets]; // The numbers of slots of the runs for each size bracket.
EXPORT static size_t numOfSlots[kNumOfSizeBrackets]; // The header sizes in bytes of the runs for each size bracket. static size_t headerSizes[kNumOfSizeBrackets];
// Initialize the run specs (the above arrays). staticvoid Initialize(); staticbool initialized_;
// Returns the page map index from an address. Requires that the // address is page size aligned.
size_t ToPageMapIndex(constvoid* addr) const {
DCHECK_LE(base_, addr);
DCHECK_LT(addr, base_ + capacity_);
size_t byte_offset = reinterpret_cast<const uint8_t*>(addr) - base_;
DCHECK_EQ(ModuloPageSize(byte_offset), static_cast<size_t>(0)); return DivideByPageSize(byte_offset);
} // Returns the page map index from an address with rounding.
size_t RoundDownToPageMapIndex(constvoid* addr) const {
DCHECK(base_ <= addr && addr < reinterpret_cast<uint8_t*>(base_) + capacity_); return DivideByPageSize(reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(base_));
}
// A memory allocation request larger than this size is treated as a large object and allocated // at a page-granularity. static constexpr size_t kLargeSizeThreshold = 2048;
// If true, check that the returned memory is actually zero. static constexpr bool kCheckZeroMemory = kIsDebugBuild; // Do not check memory when running under a memory tool. In a normal // build with kCheckZeroMemory the whole test should be optimized away. // TODO: Unprotect before checks.
ALWAYS_INLINE bool ShouldCheckZeroMemory();
// If true, log verbose details of operations. static constexpr bool kTraceRosAlloc = false;
public: // Different page release modes. enum PageReleaseMode {
kPageReleaseModeNone, // Release no empty pages.
kPageReleaseModeEnd, // Release empty pages at the end of the space.
kPageReleaseModeSize, // Release empty pages that are larger than the threshold.
kPageReleaseModeSizeAndEnd, // Release empty pages that are larger than the threshold or // at the end of the space.
kPageReleaseModeAll, // Release all empty pages.
};
// The default value for page_release_size_threshold_. static constexpr size_t kDefaultPageReleaseSizeThreshold = 4 * MB;
// We use thread-local runs for the size brackets whose indexes // are less than this index. We use shared (current) runs for the rest. // Sync this with the length of Thread::rosalloc_runs_. static constexpr size_t kNumThreadLocalSizeBrackets = 16;
static_assert(kNumThreadLocalSizeBrackets == kNumRosAllocThreadLocalSizeBracketsInThread, "Mismatch between kNumThreadLocalSizeBrackets and " "kNumRosAllocThreadLocalSizeBracketsInThread");
// The size of the largest bracket we use thread-local runs for. // This should be equal to bracketSizes[kNumThreadLocalSizeBrackets - 1]. static constexpr size_t kMaxThreadLocalBracketSize = 128;
// We use regular (8 or 16-bytes increment) runs for the size brackets whose indexes are less than // this index. staticconst size_t kNumRegularSizeBrackets = 40;
// The size of the largest regular (8 or 16-byte increment) bracket. Non-regular brackets are the // 1 KB and the 2 KB brackets. This should be equal to bracketSizes[kNumRegularSizeBrackets - 1]. static constexpr size_t kMaxRegularBracketSize = 512;
// The bracket size increment for the thread-local brackets (<= kMaxThreadLocalBracketSize bytes). static constexpr size_t kThreadLocalBracketQuantumSize = 8;
// Equal to Log2(kThreadLocalBracketQuantumSize). static constexpr size_t kThreadLocalBracketQuantumSizeShift = 3;
// The bracket size increment for the non-thread-local, regular brackets (of size <= // kMaxRegularBracketSize bytes and > kMaxThreadLocalBracketSize bytes). static constexpr size_t kBracketQuantumSize = 16;
// Equal to Log2(kBracketQuantumSize). static constexpr size_t kBracketQuantumSizeShift = 4;
private: // The base address of the memory region that's managed by this allocator.
uint8_t* base_;
// The footprint in bytes of the currently allocated portion of the // memory region.
size_t footprint_;
// The maximum footprint. The address, base_ + capacity_, indicates // the end of the memory region that's currently managed by this allocator.
size_t capacity_;
// The maximum capacity. The address, base_ + max_capacity_, indicates // the end of the memory region that's ever managed by this allocator.
size_t max_capacity_;
template<class Key, AllocatorTag kTag, class Compare = std::less<Key>> using AllocationTrackingSet = std::set<Key, Compare, TrackingAllocator<Key, kTag>>;
// The run sets that hold the runs whose slots are not all // full. non_full_runs_[i] is guarded by size_bracket_locks_[i].
AllocationTrackingSet<Run*, kAllocatorTagRosAlloc> non_full_runs_[kNumOfSizeBrackets]; // The run sets that hold the runs whose slots are all full. This is // debug only. full_runs_[i] is guarded by size_bracket_locks_[i].
std::unordered_set<Run*, hash_run, eq_run, TrackingAllocator<Run*, kAllocatorTagRosAlloc>>
full_runs_[kNumOfSizeBrackets]; // The set of free pages.
AllocationTrackingSet<FreePageRun*, kAllocatorTagRosAlloc> free_page_runs_ GUARDED_BY(lock_); // The dedicated full run, it is always full and shared by all threads when revoking happens. // This is an optimization since enables us to avoid a null check for revoked runs. static Run* dedicated_full_run_; // Using size_t to ensure that it is at least word aligned. static size_t dedicated_full_run_storage_[]; // The current runs where the allocations are first attempted for // the size brackes that do not use thread-local // runs. current_runs_[i] is guarded by size_bracket_locks_[i].
Run* current_runs_[kNumOfSizeBrackets]; // The mutexes, one per size bracket.
Mutex* size_bracket_locks_[kNumOfSizeBrackets]; // Bracket lock names (since locks only have char* names).
std::string size_bracket_lock_names_[kNumOfSizeBrackets]; // The types of page map entries. enum PageMapKind {
kPageMapReleased = 0, // Zero and released back to the OS.
kPageMapEmpty, // Zero but probably dirty.
kPageMapRun, // The beginning of a run.
kPageMapRunPart, // The non-beginning part of a run.
kPageMapLargeObject, // The beginning of a large object.
kPageMapLargeObjectPart, // The non-beginning part of a large object.
}; // The table that indicates what pages are currently used for. volatile uint8_t* page_map_; // No GUARDED_BY(lock_) for kReadPageMapEntryWithoutLockInBulkFree.
size_t page_map_size_;
size_t max_page_map_size_;
MemMap page_map_mem_map_;
// The table that indicates the size of free page runs. These sizes // are stored here to avoid storing in the free page header and // release backing pages.
std::vector<size_t, TrackingAllocator<size_t, kAllocatorTagRosAlloc>> free_page_run_size_map_
GUARDED_BY(lock_); // The global lock. Used to guard the page map, the free page set, // and the footprint.
Mutex lock_ DEFAULT_MUTEX_ACQUIRED_AFTER; // The reader-writer lock to allow one bulk free at a time while // allowing multiple individual frees at the same time. Also, this // is used to avoid race conditions between BulkFree() and // RevokeThreadLocalRuns() on the bulk free list.
ReaderWriterMutex bulk_free_lock_ DEFAULT_MUTEX_ACQUIRED_AFTER;
// The page release mode. const PageReleaseMode page_release_mode_; // Under kPageReleaseModeSize(AndEnd), if the free page run size is // greater than or equal to this value, release pages. const size_t page_release_size_threshold_;
// Whether this allocator is running on a memory tool. bool is_running_on_memory_tool_;
// The base address of the memory region that's managed by this allocator.
uint8_t* Begin() { return base_; } // The end address of the memory region that's managed by this allocator.
uint8_t* End() { return base_ + capacity_; }
// Page-granularity alloc/free void* AllocPages(Thread* self, size_t num_pages, uint8_t page_map_type)
REQUIRES(lock_); // Returns how many bytes were freed.
size_t FreePages(Thread* self, void* ptr, bool already_zero) REQUIRES(lock_);
// Allocate/free a run slot.
EXPORT void* AllocFromRun(Thread* self,
size_t size,
size_t* bytes_allocated,
size_t* usable_size,
size_t* bytes_tl_bulk_allocated) REQUIRES(!lock_); // Allocate/free a run slot without acquiring locks. // TODO: REQUIRES(Locks::mutator_lock_) void* AllocFromRunThreadUnsafe(Thread* self, size_t size, size_t* bytes_allocated,
size_t* usable_size, size_t* bytes_tl_bulk_allocated)
REQUIRES(!lock_); void* AllocFromCurrentRunUnlocked(Thread* self, size_t idx) REQUIRES(!lock_);
// Used to allocate a new thread local run for a size bracket.
Run* AllocRun(Thread* self, size_t idx) REQUIRES(!lock_);
// Used to acquire a new/reused run for a size bracket. Used when a // thread-local or current run gets full.
Run* RefillRun(Thread* self, size_t idx) REQUIRES(!lock_);
// The internal of non-bulk Free().
size_t FreeInternal(Thread* self, void* ptr) REQUIRES(!lock_);
// If kThreadUnsafe is true then the allocator may avoid acquiring some locks as an optimization. // If used, this may cause race conditions if multiple threads are allocating at the same time. template<bool kThreadSafe = true> void* Alloc(Thread* self, size_t size, size_t* bytes_allocated, size_t* usable_size,
size_t* bytes_tl_bulk_allocated)
REQUIRES(!lock_);
size_t Free(Thread* self, void* ptr)
REQUIRES(!bulk_free_lock_, !lock_);
size_t BulkFree(Thread* self, void** ptrs, size_t num_ptrs)
REQUIRES(!bulk_free_lock_, !lock_);
// Returns true if the given allocation request can be allocated in // an existing thread local run without allocating a new run.
ALWAYS_INLINE bool CanAllocFromThreadLocalRun(Thread* self, size_t size); // Allocate the given allocation request in an existing thread local // run without allocating a new run.
ALWAYS_INLINE void* AllocFromThreadLocalRun(Thread* self, size_t size, size_t* bytes_allocated);
// Returns the maximum bytes that could be allocated for the given // size in bulk, that is the maximum value for the // bytes_allocated_bulk out param returned by RosAlloc::Alloc().
ALWAYS_INLINE size_t MaxBytesBulkAllocatedFor(size_t size);
// Returns the size of the allocated slot for a given allocated memory chunk.
size_t UsableSize(constvoid* ptr) REQUIRES(!lock_); // Returns the size of the allocated slot for a given size.
size_t UsableSize(size_t bytes) { if (UNLIKELY(bytes > kLargeSizeThreshold)) { return RoundUp(bytes, gPageSize);
} else { return RoundToBracketSize(bytes);
}
} // Try to reduce the current footprint by releasing the free page // run at the end of the memory region, if any. bool Trim() REQUIRES(!lock_); // Iterates over all the memory slots and apply the given function. void InspectAll(void (*handler)(void* start, void* end, size_t used_bytes, void* callback_arg), void* arg)
REQUIRES(!lock_);
// Release empty pages.
size_t ReleasePages() REQUIRES(!lock_); // Returns the current footprint.
size_t Footprint() REQUIRES(!lock_); // Returns the current capacity, maximum footprint.
size_t FootprintLimit() REQUIRES(!lock_); // Update the current capacity. void SetFootprintLimit(size_t bytes) REQUIRES(!lock_);
// Releases the thread-local runs assigned to the given thread back to the common set of runs. // Returns the total bytes of free slots in the revoked thread local runs. This is to be // subtracted from Heap::num_bytes_allocated_ to cancel out the ahead-of-time counting.
size_t RevokeThreadLocalRuns(Thread* thread) REQUIRES(!lock_, !bulk_free_lock_); // Releases the thread-local runs assigned to all the threads back to the common set of runs. // Returns the total bytes of free slots in the revoked thread local runs. This is to be // subtracted from Heap::num_bytes_allocated_ to cancel out the ahead-of-time counting.
size_t RevokeAllThreadLocalRuns() REQUIRES(!Locks::thread_list_lock_, !lock_, !bulk_free_lock_); // Assert the thread local runs of a thread are revoked. void AssertThreadLocalRunsAreRevoked(Thread* thread) REQUIRES(!bulk_free_lock_); // Assert all the thread local runs are revoked. void AssertAllThreadLocalRunsAreRevoked() REQUIRES(!Locks::thread_list_lock_, !bulk_free_lock_);
// Callbacks for InspectAll that will count the number of bytes // allocated and objects allocated, respectively. staticvoid BytesAllocatedCallback(void* start, void* end, size_t used_bytes, void* arg); staticvoid ObjectsAllocatedCallback(void* start, void* end, size_t used_bytes, void* arg);
// Callback from rosalloc when it needs to increase the footprint. Must be implemented somewhere // else (currently rosalloc_space.cc). void* ArtRosAllocMoreCore(allocator::RosAlloc* rosalloc, intptr_t increment);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.