From ce731043dae60a01efd6d46f8aec07e06766d3ad Mon Sep 17 00:00:00 2001 From: muit Date: Sun, 30 Aug 2026 22:18:19 +0200 Subject: [PATCH 01/15] Added strings, improved memory stats --- Include/Misc/PipeDebug.h | 22 +- Include/Pipe/Core/Guid.h | 2 +- Include/Pipe/Core/Log.h | 8 +- Include/Pipe/Core/Map.h | 4 +- Include/Pipe/Core/String.h | 174 --- Include/Pipe/Core/Tag.h | 2 +- Include/Pipe/Files/Files.h | 17 +- Include/Pipe/Files/Paths.h | 2 +- Include/Pipe/Memory/MemoryStats.h | 16 +- Include/PipeColor.h | 10 +- Include/PipeContainers.h | 219 ++-- Include/PipeECS.h | 8 +- Include/PipeReflect.h | 2 +- Include/PipeSerialize.h | 2 +- Include/PipeStrings.h | 1452 ++++++++++++++++++++++ Include/PipeTime.h | 2 +- Include/PipeVectors.h | 2 +- Src/Core/Checks.cpp | 6 +- Src/Core/Guid.cpp | 12 +- Src/Core/Log.cpp | 6 +- Src/Core/Subprocess.cpp | 2 +- Src/Files/Files.cpp | 4 +- Src/Files/Paths.cpp | 2 +- Src/Files/PlatformPaths.cpp | 4 +- Src/Memory/MemoryStats.cpp | 108 +- Src/PipeContainers.cpp | 42 +- Src/PipeFiles.cpp | 37 +- Src/PipeSerialize.cpp | 2 +- Src/{Core/String.cpp => PipeStrings.cpp} | 19 +- Src/PipeTime.cpp | 42 +- Tests/Containers/Arrays.spec.cpp | 186 +++ Tests/Core/String.spec.cpp | 957 +++++++++++++- Tests/Core/StringView.spec.cpp | 2 +- Tests/Memory/MemoryStats.spec.cpp | 122 +- Tests/Reflection/TypeName.spec.cpp | 2 +- Tests/Serialization/Json.spec.cpp | 16 +- 36 files changed, 2966 insertions(+), 549 deletions(-) delete mode 100644 Include/Pipe/Core/String.h create mode 100644 Include/PipeStrings.h rename Src/{Core/String.cpp => PipeStrings.cpp} (88%) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index f72d1601..ea18ee1b 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -25,7 +25,7 @@ static_assert(false, "Imgui v" IMGUI_VERSION " found but PipeDebug requires v1.9 #include "Pipe/Core/Checks.h" #include "Pipe/Core/Map.h" #include "Pipe/Core/Set.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Memory/MemoryStats.h" #include "PipeColor.h" #include "PipeContainers.h" @@ -666,7 +666,7 @@ namespace p for (i32 i = 0; i < size; ++i) { tmpLabel.clear(); - Strings::FormatTo(tmpLabel, "Index {}", i); + FormatTo(tmpLabel, "Index {}", i); Inspect(tmpLabel, ops->GetItem(data, i), ops->itemType); ++ins.propStack.Last().index; @@ -888,7 +888,7 @@ namespace p } static p::String typeLabel; typeLabel.clear(); - p::Strings::FormatTo(typeLabel, "{}###{}", entry.name, entry.id.GetId()); + FormatTo(typeLabel, "{}###{}", entry.name, entry.id.GetId()); if (ImGui::Selectable(typeLabel, true)) { selectedTypeId = entry.id; @@ -936,7 +936,7 @@ namespace p for (TypeId typeId : poolTypes) { typeName.clear(); - p::Strings::FormatTo(typeName, "{}###{}", GetTypeName(typeId), typeId.GetId()); + FormatTo(typeName, "{}###{}", GetTypeName(typeId), typeId.GetId()); if (ImGui::Button(typeName.data())) { typesToRemove.Add(typeId); @@ -1313,7 +1313,7 @@ namespace p bool wantAddComponent = false; String name; - Strings::FormatTo(name, "{}: {}{}###inspector{}", label, inspector.id, + FormatTo(name, "{}: {}{}###inspector{}", label, inspector.id, removed ? " (removed)" : "", inspector.uniqueId); if (inspector.pendingFocus) @@ -1412,7 +1412,7 @@ namespace p componentLabel.clear(); - Strings::FormatTo(componentLabel, "{}", + FormatTo(componentLabel, "{}", RemoveNamespace(GetTypeName(poolInstance.componentId))); if (!inspector.filter.PassFilter(componentLabel.c_str())) @@ -1473,7 +1473,7 @@ namespace p { static String modalTitle; modalTitle.clear(); - p::Strings::FormatTo(modalTitle, "Remove {}?", + FormatTo(modalTitle, "Remove {}?", RemoveNamespace(GetTypeName(pendingDeleteType))); ImGui::Text(modalTitle); ImGui::NewLine(); @@ -1756,7 +1756,7 @@ namespace p inspectLabel.clear(); const bool inspected = IsInspectingId(ecsDbg, id); const char* icon = inspected ? " × " : "-->"; - p::Strings::FormatTo(inspectLabel, "{}##{}", icon, id); + FormatTo(inspectLabel, "{}##{}", icon, id); ImGui::PushTextColor( inspected ? ImGui::GetTextColor() : ImGui::GetTextColor().Translucency(0.3f)); ImGui::PushStyleCompact(); @@ -1792,7 +1792,7 @@ namespace p { static String idText; idText.clear(); - Strings::FormatTo(idText, "{}", type); + FormatTo(idText, "{}", type); StringView name = property.name.AsString(); if (!ctx.filter.PassFilter(name.data(), name.data() + name.size())) @@ -1825,7 +1825,7 @@ namespace p static String idText; idText.clear(); - Strings::FormatTo(idText, "{}", type); + FormatTo(idText, "{}", type); bool passedFilter = true; StringView rawName = GetTypeName(type); @@ -3478,7 +3478,7 @@ namespace p ImGui::ProgressBar(usedPct / 100.0f); } detailsLabel.clear(); - Strings::FormatTo(detailsLabel, "{} blocks", selectedArena->blocks.Size()); + FormatTo(detailsLabel, "{} blocks", selectedArena->blocks.Size()); ImGui::SeparatorText(detailsLabel.data()); for (i32 i = 0; i < selectedArena->blocks.Size(); ++i) { diff --git a/Include/Pipe/Core/Guid.h b/Include/Pipe/Core/Guid.h index 7c0b5f13..75492c52 100644 --- a/Include/Pipe/Core/Guid.h +++ b/Include/Pipe/Core/Guid.h @@ -2,7 +2,7 @@ #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "PipeAlgorithms.h" #include "PipeSerializeFwd.h" diff --git a/Include/Pipe/Core/Log.h b/Include/Pipe/Core/Log.h index 0c07f621..0140e847 100644 --- a/Include/Pipe/Core/Log.h +++ b/Include/Pipe/Core/Log.h @@ -2,7 +2,7 @@ #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include @@ -28,7 +28,7 @@ namespace p { if (!format.empty()) { - Info(Strings::Format(format, std::forward(args)...)); + Info(Format(format, std::forward(args)...)); } } @@ -37,7 +37,7 @@ namespace p { if (!format.empty()) { - Warning(Strings::Format(format, std::forward(args)...)); + Warning(Format(format, std::forward(args)...)); } } @@ -46,7 +46,7 @@ namespace p { if (!format.empty()) { - Error(Strings::Format(format, std::forward(args)...)); + Error(Format(format, std::forward(args)...)); } } }; // namespace p diff --git a/Include/Pipe/Core/Map.h b/Include/Pipe/Core/Map.h index 34e9e349..f3d50279 100644 --- a/Include/Pipe/Core/Map.h +++ b/Include/Pipe/Core/Map.h @@ -108,13 +108,13 @@ namespace p } template - TPair InsertOrAssign(const KeyType& key, OtherT&& value) + std::pair InsertOrAssign(const KeyType& key, OtherT&& value) { return map.insert_or_assign(key, std::forward(value)); } template - TPair InsertOrAssign(KeyType&& key, OtherT&& value) + std::pair InsertOrAssign(KeyType&& key, OtherT&& value) { return map.insert_or_assign(Move(key), Fwd(value)); } diff --git a/Include/Pipe/Core/String.h b/Include/Pipe/Core/String.h deleted file mode 100644 index e2136580..00000000 --- a/Include/Pipe/Core/String.h +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2015-2026 Piperift. All Rights Reserved. - -#pragma once - -#include "Pipe/Core/Hash.h" -#include "Pipe/Core/STDFormat.h" -#include "Pipe/Core/StringView.h" -#include "Pipe/Core/Utility.h" -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wcharacter-conversion" -#endif -#include "Pipe/Extern/utf8/unchecked.h" -#if defined(__clang__) -#pragma clang diagnostic pop -#endif -#include "PipeContainers.h" -#include "PipeMemory.h" -#include "PipePlatform.h" -#include "PipeSerializeFwd.h" - -#include - - -#pragma warning(push) -#pragma warning(disable:4996) - - -namespace p -{ - template - using TString = - std::basic_string, std::allocator>; - using String = TString; - using WString = TString; - - template - using FormatString = std::basic_format_string; - - namespace Strings - { - template - inline StringType Format(StringView format, Args... args) - { - String str; - std::vformat_to(std::back_inserter(str), format, std::make_format_args(args...)); - return Move(str); - } - - template - inline void FormatTo(StringType& buffer, StringView format, Args... args) - { - std::vformat_to(std::back_inserter(buffer), format, std::make_format_args(args...)); - } - - // Format an string using a compile time format - template - inline StringType Format(FormatString format, Args... args) - { - String str; - std::format_to(std::back_inserter(str), format, p::Fwd(args)...); - return Move(str); - } - - // Format into an existing string using a compile time format - template - inline void FormatTo(StringType& buffer, FormatString format, Args... args) - { - std::format_to(std::back_inserter(buffer), format, p::Fwd(args)...); - } - - template - inline void ToString(StringType& buffer, T value, FormatString format = "{}") - { - std::format_to(std::back_inserter(buffer), format, p::Fwd(value)); - } - - template - inline StringType ToString(T value) - { - StringType str; - ToString(str, value); - return str; - } - - P_API String ToSentenceCase(StringView value); - - /** - * Breaks up a delimited string into elements of a string array. - * - * @param InArray The array to fill with the string pieces - * @param pchDelim The string to delimit on - * @param InCullEmpty If 1, empty strings are not added to the array - * - * @return The number of elements in InArray - */ - P_API i32 ParseIntoArray(const String& str, TArray& OutArray, const char* pchDelim, - bool InCullEmpty = true); - - P_API void RemoveFromStart(String& str, sizet size); - P_API void RemoveFromEnd(String& str, sizet size); - P_API void RemoveFromEnd(String& str, StringView subStr); - - P_API bool RemoveCharFromEnd(String& str, char c); - - P_API i32 Split(const String& str, TArray& tokens, const char delim); - - P_API bool Split(const String& str, String& a, String& b, const char* delim); - - P_API bool IsNumeric(const String& str); - P_API bool IsNumeric(const char* Str); - - P_API String ParseMemorySize(sizet size); - - template - inline void ConvertTo(TStringView source, ToStringType& dest) - { - using ToChar = typename ToStringType::value_type; - static_assert( - std::is_integral_v, "FromChar is not integral (so it is not a char)"); - static_assert( - std::is_integral_v, "ToChar is not integral (so it is not a char)"); - - if constexpr (IsSame) - { - dest += source; - } - else if constexpr (sizeof(FromChar) == 1 && sizeof(ToChar) == 2) - { - utf8::unchecked::utf8to16(source.begin(), source.end(), std::back_inserter(dest)); - } - else if constexpr (sizeof(FromChar) == 2 && sizeof(ToChar) == 1) - { - utf8::unchecked::utf16to8(source.begin(), source.end(), std::back_inserter(dest)); - } - else if constexpr (sizeof(FromChar) == 1 && sizeof(ToChar) == 4) - { - utf8::unchecked::utf8to32(source.begin(), source.end(), std::back_inserter(dest)); - } - else if constexpr (sizeof(FromChar) == 4 && sizeof(ToChar) == 1) - { - utf8::unchecked::utf32to8(source.begin(), source.end(), std::back_inserter(dest)); - } - else - { - // TODO: Find a way to assert at compile time except on the previous cases - // static_assert(false, "Unknown char conversion"); - } - } - - template - inline ToStringType Convert(TStringView source) - { - ToStringType dest; - ConvertTo(source, dest); - return Move(dest); - } - template - inline ToStringType Convert(const TString& source) - { - ToStringType dest; - ConvertTo(TStringView{source}, dest); - return Move(dest); - } - }; // namespace Strings - - - inline sizet GetHash(const String& str) - { - return GetStringHash(str.data()); - } -} // namespace p - -#pragma warning(pop) diff --git a/Include/Pipe/Core/Tag.h b/Include/Pipe/Core/Tag.h index 15ed029e..b6740c5a 100644 --- a/Include/Pipe/Core/Tag.h +++ b/Include/Pipe/Core/Tag.h @@ -1,7 +1,7 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" #include "PipeSerializeFwd.h" diff --git a/Include/Pipe/Files/Files.h b/Include/Pipe/Files/Files.h index 47ef04f0..1d426e92 100644 --- a/Include/Pipe/Files/Files.h +++ b/Include/Pipe/Files/Files.h @@ -2,8 +2,9 @@ #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" +#include "Pipe/Files/Paths.h" #include "Pipe/Files/STDFileSystem.h" #include "PipeContainers.h" @@ -16,6 +17,20 @@ namespace p P_API bool ExistsAsFolder(const Path& path); P_API SpaceInfo Space(const Path& target); + // StringView overloads — create Path internally + inline bool ExistsAsFile(StringView path) + { + return ExistsAsFile(ToSTDPath(path)); + } + inline bool ExistsAsFolder(StringView path) + { + return ExistsAsFolder(ToSTDPath(path)); + } + inline SpaceInfo Space(StringView path) + { + return Space(ToSTDPath(path)); + } + /** String API */ diff --git a/Include/Pipe/Files/Paths.h b/Include/Pipe/Files/Paths.h index 745a72e7..f74f2d1e 100644 --- a/Include/Pipe/Files/Paths.h +++ b/Include/Pipe/Files/Paths.h @@ -2,7 +2,7 @@ #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Export.h" #include "Pipe/Files/STDFileSystem.h" diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index 37afa6dd..461a484e 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -4,6 +4,7 @@ #include "Pipe/Core/EnumFlags.h" #include "Pipe/Core/Hash.h" +#include "Pipe/Core/Map.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" #include "PipeContainers.h" @@ -93,10 +94,23 @@ namespace p // can classify events via a bit-test instead of MemoryStatsEvent. mutable BitArray frees; - mutable sizet used = 0; mutable sizet totalAllocated = 0; + private: + // --- Incremental CollectStats state (consumer thread only) --- + // Events are append-only, so classification of old events never + // changes. Only events past collectedEvents are classified per call. + mutable i32 collectedEvents = 0; + // Head of the unmatched-alloc chain per event key. Chains are + // intrusively linked through prevLiveIdx, newest first. + mutable TMap liveIdx; + // For each alloc event index, the previous unmatched alloc index + // sharing the same key (NO_INDEX if none). Consumed on free. + mutable TArray prevLiveIdx; + + public: + MemoryStats(); ~MemoryStats(); diff --git a/Include/PipeColor.h b/Include/PipeColor.h index da58d55d..55f4b0ca 100644 --- a/Include/PipeColor.h +++ b/Include/PipeColor.h @@ -3,7 +3,7 @@ #pragma once #include "Pipe/Core/FixedString.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "PipeMath.h" #include "PipeVectors.h" @@ -712,9 +712,9 @@ namespace p { if constexpr (mode == ColorMode::HSV) { - return Strings::Format("(h={},s={},v={},a={})", this->h, this->s, this->v, this->a); + return Format("(h={},s={},v={},a={})", this->h, this->s, this->v, this->a); } - return Strings::Format("(r={},g={},b={},a={})", this->r, this->g, this->b, this->a); + return Format("(r={},g={},b={},a={})", this->r, this->g, this->b, this->a); } /** @@ -727,10 +727,10 @@ namespace p { if (includeAlpha) { - return Strings::Format( + return Format( "{:02X}{:02X}{:02X}{:02X}", this->r, this->g, this->b, this->a); } - return Strings::Format("{:02X}{:02X}{:02X}", this->r, this->g, this->b); + return Format("{:02X}{:02X}{:02X}", this->r, this->g, this->b); } // Common colors diff --git a/Include/PipeContainers.h b/Include/PipeContainers.h index f183a432..7d72b02f 100644 --- a/Include/PipeContainers.h +++ b/Include/PipeContainers.h @@ -668,6 +668,19 @@ namespace p #pragma endregion Search +#pragma region Subviews + constexpr Type& First() const; + constexpr Type& Last() const; + + constexpr TView FirstUnsafe(i32 n) const; + constexpr TView First(i32 n) const; + constexpr TView LastUnsafe(i32 n) const; + constexpr TView Last(i32 n) const; + constexpr TView SliceUnsafe(i32 offset, i32 n) const; + constexpr TView Slice(i32 offset, i32 n) const; +#pragma endregion Subviews + + template bool operator==(const IArray& other) const { @@ -1323,12 +1336,7 @@ namespace p */ bool RemoveAt(i32 index, const Shrink shouldShrink = Shrink::Yes) { - if (Super::IsValidIndex(index)) - { - RemoveAtUnsafe(index, shouldShrink); - return true; - } - return false; + return RemoveAt(index, 1, shouldShrink); } /** @@ -1352,12 +1360,7 @@ namespace p */ bool RemoveAtSwap(i32 index, const Shrink shouldShrink = Shrink::Yes) { - if (Super::IsValidIndex(index)) - { - RemoveAtSwapUnsafe(index, shouldShrink); - return true; - } - return false; + return RemoveAtSwap(index, 1, shouldShrink); } /** @@ -1382,17 +1385,7 @@ namespace p */ void RemoveAtUnsafe(i32 index, const Shrink shouldShrink = Shrink::Yes) { - const i32 lastIndex = index + 1; - const i32 countToPull = Super::size - lastIndex; - DestroyItems(Super::data + index, 1); - MoveItems(Super::data + index, countToPull, Super::data + lastIndex); - --Super::size; - - /// @OPTIMIZE: Shrinking can be combined to avoid moving trailing elements twice - if (shouldShrink == Shrink::Yes) - { - Shrink(); - } + RemoveAtUnsafe(index, 1, shouldShrink); } /** @@ -1423,9 +1416,7 @@ namespace p */ void RemoveAtSwapUnsafe(i32 index, const Shrink shouldShrink = Shrink::Yes) { - const i32 lastIndex = Super::size - 1; - Super::SwapUnsafe(index, lastIndex); - RemoveAtUnsafe(lastIndex, shouldShrink); + RemoveAtSwapUnsafe(index, 1, shouldShrink); } /** @@ -1687,51 +1678,6 @@ namespace p #pragma endregion Storage -#pragma region Subviews - constexpr Type& First() const - { - P_Check(Super::size != 0); - return Super::data[0]; - } - constexpr Type& Last() const - { - P_Check(Super::size != 0); - return Super::data[Super::size - 1]; - } - - constexpr TView FirstUnsafe(i32 n) const - { - return {Super::data, n}; - } - constexpr TView First(i32 n) const - { - n = Clamp(n, 0, Super::size); - return FirstUnsafe(n); - } - - constexpr TView LastUnsafe(i32 n) const - { - return {Super::data + (Super::size - n), n}; - } - constexpr TView Last(i32 n) const - { - n = Clamp(n, 0, Super::size); - return LastUnsafe(n); - } - - constexpr TView SliceUnsafe(i32 offset, i32 n) const - { - return {Super::data + offset, n}; - } - constexpr TView Slice(i32 offset, i32 n) const - { - offset = Clamp(offset, 0, Super::size - 1); - n = Clamp(n, 0, Super::size); - return LastUnsafe(n); - } -#pragma endregion Subviews - - protected: void CopyFrom(const IArray& other); @@ -1811,51 +1757,64 @@ namespace p Super::size = other.Size(); return *this; } + }; -#pragma region Subviews - constexpr Type& First() const - { - P_Check(Super::size != 0); - return Super::data[0]; - } - constexpr Type& Last() const - { - P_Check(Super::size != 0); - return Super::data[Super::size - 1]; - } - constexpr TView FirstUnsafe(i32 n) const - { - return {Super::data, n}; - } - constexpr TView First(i32 n) const - { - n = Clamp(n, 0, Super::size); - return FirstUnsafe(n); - } + //////////////////////////////// + // IArray Implementation +#pragma region IArray Implementation - constexpr TView LastUnsafe(i32 n) const - { - return {Super::data + (Super::size - n), n}; - } - constexpr TView Last(i32 n) const - { - n = Clamp(n, 0, Super::size); - return LastUnsafe(n); - } + template + constexpr Type& IArray::First() const + { + P_Check(size != 0); + return data[0]; + } + template + constexpr Type& IArray::Last() const + { + P_Check(size != 0); + return data[size - 1]; + } - constexpr TView SliceUnsafe(i32 offset, i32 n) const - { - return {Super::data + offset, n}; - } - constexpr TView Slice(i32 offset, i32 n) const - { - offset = Clamp(offset, 0, Super::size - 1); - n = Clamp(n, 0, Super::size); - return LastUnsafe(n); - } -#pragma endregion Subviews - }; + template + constexpr TView IArray::FirstUnsafe(i32 n) const + { + return {data, n}; + } + template + constexpr TView IArray::First(i32 n) const + { + n = Clamp(n, 0, size); + return FirstUnsafe(n); + } + + template + constexpr TView IArray::LastUnsafe(i32 n) const + { + return {data + (size - n), n}; + } + template + constexpr TView IArray::Last(i32 n) const + { + n = Clamp(n, 0, size); + return LastUnsafe(n); + } + + template + constexpr TView IArray::SliceUnsafe(i32 offset, i32 n) const + { + return {data + offset, n}; + } + template + constexpr TView IArray::Slice(i32 offset, i32 n) const + { + offset = Clamp(offset, 0, size); + n = Clamp(n, 0, size - offset); + return SliceUnsafe(offset, n); + } + +#pragma endregion IArray Implementation struct P_API BitArray @@ -1930,10 +1889,8 @@ namespace p // @return index of previous set bit in array (wraps around) i32 GetPreviousSet(i32 index) const; - /** - * Count the number of set bits in this array FromIndex <= bit < ToIndex - */ - i32 CountSetBits(i32 fromIndex = 0, i32 toIndex = NO_INDEX) const; + /** @return number of set bits in the whole array. */ + i32 CountSetBits() const; constexpr u32* Data() const { @@ -2037,6 +1994,10 @@ namespace p { return ((bitSize - 1) >> 5) + 1; } + + private: + + void SetFromBools(const bool* data, i32 count); }; @@ -2082,15 +2043,25 @@ namespace p } else if (atIndex != oldSize) { - // Imagine we insert 1 element at the start of "A B": - // First we move last trailing elements to unconstructed positions - // "A B #" -> "A # B" - MoveConstructItems(Super::data + oldSize, count, Super::data + oldSize - count); + const i32 trailing = oldSize - atIndex; + if (count <= trailing) + { + // Imagine we insert 1 element at the start of "A B": + // First we move last trailing elements to unconstructed positions + // "A B #" -> "A # B" + MoveConstructItems(Super::data + oldSize, count, Super::data + oldSize - count); - // Then we push the other trailing elements - // "A # B" -> "# A B" - Type* const ptrToPush = Super::data + atIndex; - MoveItemsBackwards(ptrToPush + count, oldSize - atIndex - count, ptrToPush); + // Then we push the other trailing elements + // "A # B" -> "# A B" + Type* const ptrToPush = Super::data + atIndex; + MoveItemsBackwards(ptrToPush + count, oldSize - atIndex - count, ptrToPush); + } + else + { + // All trailing elements end up past the old end (in uninitialized slots) + MoveConstructItems( + Super::data + atIndex + count, trailing, Super::data + atIndex); + } } else { diff --git a/Include/PipeECS.h b/Include/PipeECS.h index dde886bc..87630d5f 100644 --- a/Include/PipeECS.h +++ b/Include/PipeECS.h @@ -153,11 +153,11 @@ namespace p } else if (auto version = id.GetVersion(); version > 0) { - Strings::FormatTo(str, "{}:{}", id.GetIndex(), version); + FormatTo(str, "{}:{}", id.GetIndex(), version); } else { - Strings::FormatTo(str, "{}", id.GetIndex()); + FormatTo(str, "{}", id.GetIndex()); } } @@ -2511,7 +2511,7 @@ namespace p { const Id id = ids[i]; key.clear(); - Strings::FormatTo(key, "{}", i); + FormatTo(key, "{}", i); if (EnterNext(key)) { @@ -2581,7 +2581,7 @@ namespace p for (auto id : typeIds) { key.clear(); - Strings::FormatTo(key, "{}", id.first); + FormatTo(key, "{}", id.first); if constexpr (std::is_empty_v) { diff --git a/Include/PipeReflect.h b/Include/PipeReflect.h index cd6908e8..c615fcdc 100644 --- a/Include/PipeReflect.h +++ b/Include/PipeReflect.h @@ -5,7 +5,7 @@ #include "Pipe/Core/EnumFlags.h" #include "Pipe/Core/Guid.h" #include "Pipe/Core/Macros.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Tag.h" #include "Pipe/Core/TypeId.h" diff --git a/Include/PipeSerialize.h b/Include/PipeSerialize.h index 423e742e..e2f55ef0 100644 --- a/Include/PipeSerialize.h +++ b/Include/PipeSerialize.h @@ -1,7 +1,7 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #pragma once -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Templates.h" #include "Pipe/Core/TypeFlags.h" diff --git a/Include/PipeStrings.h b/Include/PipeStrings.h new file mode 100644 index 00000000..00251287 --- /dev/null +++ b/Include/PipeStrings.h @@ -0,0 +1,1452 @@ +// Copyright 2015-2026 Piperift. All Rights Reserved. + +#pragma once + +#include "Pipe/Core/Hash.h" +#include "Pipe/Core/STDFormat.h" +#include "Pipe/Core/StringView.h" +#include "Pipe/Core/Utility.h" +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wcharacter-conversion" +#endif +#include "Pipe/Extern/utf8/unchecked.h" +#if defined(__clang__) + #pragma clang diagnostic pop +#endif +#include "PipeContainers.h" +#include "PipeMemory.h" +#include "PipePlatform.h" +#include "PipeSerializeFwd.h" + +#include +#include + + +namespace p +{ + /** + * A std-independent dynamic string built on top of TArray. + * Mirrors the std::basic_string API. + * The internal array always contains a null terminator as its last element. + * + * @param CharType of the characters stored + * @param InlineCapacity of characters stored inside the object itself (SSO). + * Defaults to ~24 bytes worth of characters. + */ + template + struct TString + { + public: + using traits_type = std::char_traits; + using value_type = CharType; + using size_type = sizet; + using difference_type = ptrdiff_t; + using reference = CharType&; + using const_reference = const CharType&; + using pointer = CharType*; + using const_pointer = const CharType*; + using iterator = CharType*; + using const_iterator = const CharType*; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + using View = TStringView; + + static constexpr sizet npos = sizet(-1); + + + private: + /** Contains [chars..., '\0']. Size() is always Len() + 1 */ + TArray chars; + + + public: + TString() + { + InitEmpty(); + } + + /** Arena constructors. Allocations are done on the given arena */ + explicit TString(Arena& arena) + { + chars.arena = &arena; + InitEmpty(); + } + TString(Arena& arena, size_type count, CharType ch) + { + chars.arena = &arena; + AppendCount(count, ch); + } + explicit TString(Arena& arena, const CharType* str) + { + chars.arena = &arena; + SetFrom(str, traits_type::length(str)); + } + TString(Arena& arena, const CharType* str, size_type count) + { + chars.arena = &arena; + SetFrom(str, count); + } + explicit TString(Arena& arena, View view) + { + chars.arena = &arena; + SetFrom(view.data(), view.size()); + } + TString(Arena& arena, const TString& other) + { + chars.arena = &arena; + SetFrom(other.Buf(), other.Len()); + } + + TString(size_type count, CharType ch) + { + AppendCount(count, ch); + } + + TString(const TString& other, size_type pos) + { + P_Check(pos <= other.Len()); + SetFrom(other.Buf() + pos, other.Len() - i32(pos)); + } + TString(const TString& other, size_type pos, size_type count) + { + P_Check(pos <= other.Len()); + SetFrom(other.Buf() + pos, Min(count, other.Len() - pos)); + } + + TString(const CharType* str, size_type count) + { + SetFrom(str, count); + } + TString(const CharType* str) + { + SetFrom(str, traits_type::length(str)); + } + + template + TString(It first, It last) + { + AppendIter(first, last); + } + + TString(std::initializer_list initList) + { + AppendIter(initList.begin(), initList.end()); + } + + /** StringView-like constructor (not convertible to const CharType*) */ + template + requires(std::convertible_to + && !std::convertible_to) + explicit TString(const SVLike& view) : TString{View(view)} + {} + template + requires(std::convertible_to + && !std::convertible_to) + explicit TString(const SVLike& view, size_type pos) + { + View v = view; + P_Check(pos <= v.size()); + SetFrom(v.data() + pos, v.size() - pos); + } + template + requires(std::convertible_to + && !std::convertible_to) + explicit TString(const SVLike& view, size_type pos, size_type count) + { + View v = view; + P_Check(pos <= v.size()); + SetFrom(v.data() + pos, Min(count, v.size() - pos)); + } + explicit TString(View view) + { + SetFrom(view.data(), view.size()); + } + + TString(std::nullptr_t) = delete; + + TString(const TString& other) : chars{other.chars} {} + TString(TString&& other) noexcept : chars{Move(other.chars)} + { + other.InitEmpty(); + } + + ~TString() = default; + + +#pragma region Assignment + TString& operator=(const TString& other) + { + if (this != &other) + { + SetFrom(other.Buf(), other.Len()); + } + return *this; + } + TString& operator=(TString&& other) noexcept + { + if (this != &other) + { + chars = Move(other.chars); + other.InitEmpty(); + } + return *this; + } + TString& operator=(const CharType* str) + { + SetFrom(str, traits_type::length(str)); + return *this; + } + TString& operator=(CharType ch) + { + SetFrom(&ch, 1); + return *this; + } + TString& operator=(std::nullptr_t) = delete; + TString& operator=(std::initializer_list initList) + { + clear(); + AppendIter(initList.begin(), initList.end()); + return *this; + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& operator=(const SVLike& view) + { + View v = view; + SetFrom(v.data(), v.size()); + return *this; + } + + TString& assign(const TString& other) + { + return *this = other; + } + TString& assign(const TString& other, size_type pos, size_type count = npos) + { + P_Check(pos <= other.Len()); + SetFrom(other.Buf() + pos, Min(count, other.Len() - pos)); + return *this; + } + TString& assign(TString&& other) noexcept + { + return *this = Move(other); + } + TString& assign(const CharType* str, size_type count) + { + SetFrom(str, count); + return *this; + } + TString& assign(const CharType* str) + { + SetFrom(str, traits_type::length(str)); + return *this; + } + TString& assign(size_type count, CharType ch) + { + clear(); + AppendCount(count, ch); + return *this; + } + template + TString& assign(It first, It last) + { + clear(); + AppendIter(first, last); + return *this; + } + TString& assign(std::initializer_list initList) + { + clear(); + AppendIter(initList.begin(), initList.end()); + return *this; + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& assign(const SVLike& view) + { + View v = view; + SetFrom(v.data(), v.size()); + return *this; + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& assign(const SVLike& view, size_type pos, size_type count = npos) + { + View v = view; + P_Check(pos <= v.size()); + SetFrom(v.data() + pos, Min(count, v.size() - pos)); + return *this; + } +#pragma endregion Assignment + + +#pragma region Element access + reference at(size_type pos) + { + P_Check(pos < Len()); + return Buf()[pos]; + } + const_reference at(size_type pos) const + { + P_Check(pos < Len()); + return Buf()[pos]; + } + + reference operator[](size_type pos) + { + P_Check(pos <= Len()); + return Buf()[pos]; + } + const_reference operator[](size_type pos) const + { + P_Check(pos <= Len()); + return Buf()[pos]; + } + + reference front() + { + P_Check(!IsEmpty()); + return Buf()[0]; + } + const_reference front() const + { + P_Check(!IsEmpty()); + return Buf()[0]; + } + + reference back() + { + P_Check(!IsEmpty()); + return Buf()[Len() - 1]; + } + const_reference back() const + { + P_Check(!IsEmpty()); + return Buf()[Len() - 1]; + } + + pointer data() + { + return Buf(); + } + const_pointer data() const + { + return Buf(); + } + const_pointer c_str() const + { + return Buf(); + } + + operator View() const + { + return {Buf(), size()}; + } +#pragma endregion Element access + + +#pragma region Iterators + iterator begin() + { + return Buf(); + } + const_iterator begin() const + { + return Buf(); + } + const_iterator cbegin() const + { + return Buf(); + } + + iterator end() + { + return Buf() + Len(); + } + const_iterator end() const + { + return Buf() + Len(); + } + const_iterator cend() const + { + return Buf() + Len(); + } + + reverse_iterator rbegin() + { + return reverse_iterator{end()}; + } + const_reverse_iterator rbegin() const + { + return const_reverse_iterator{end()}; + } + const_reverse_iterator crbegin() const + { + return const_reverse_iterator{end()}; + } + + reverse_iterator rend() + { + return reverse_iterator{begin()}; + } + const_reverse_iterator rend() const + { + return const_reverse_iterator{begin()}; + } + const_reverse_iterator crend() const + { + return const_reverse_iterator{begin()}; + } +#pragma endregion Iterators + + +#pragma region Capacity + bool empty() const + { + return IsEmpty(); + } + size_type size() const + { + return sizet(Len()); + } + size_type length() const + { + return sizet(Len()); + } + size_type max_size() const + { + return sizet(INT32_MAX - 1); + } + + void reserve(size_type newCapacity) + { + P_CheckMsg(newCapacity <= sizet(max_size()), "String capacity exceeds max_size"); + chars.Reserve(i32(newCapacity) + 1); + } + size_type capacity() const + { + return sizet(Max(1, chars.Capacity()) - 1); + } + + void shrink_to_fit() + { + chars.Shrink(Len() + 1); + } +#pragma endregion Capacity + + +#pragma region Modifiers + void clear() + { + chars.Resize(1, Shrink::No); + Buf()[0] = CharType{}; + } + + void push_back(CharType ch) + { + EnsureInit(); + const i32 len = Len(); + chars.AddUninitialized(1); + Buf()[len] = ch; + Buf()[len + 1] = CharType{}; + } + void pop_back() + { + P_Check(!IsEmpty()); + chars.RemoveLast(1, Shrink::No); + Buf()[Len()] = CharType{}; + } + + TString& append(size_type count, CharType ch) + { + AppendCount(count, ch); + return *this; + } + TString& append(const TString& other) + { + Append(other.Buf(), other.Len()); + return *this; + } + TString& append(const TString& other, size_type pos, size_type count = npos) + { + P_Check(pos <= other.Len()); + Append(other.Buf() + pos, Min(count, other.Len() - pos)); + return *this; + } + TString& append(const CharType* str, size_type count) + { + Append(str, count); + return *this; + } + TString& append(const CharType* str) + { + Append(str, traits_type::length(str)); + return *this; + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& append(const SVLike& view) + { + View v = view; + Append(v.data(), v.size()); + return *this; + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& append(const SVLike& view, size_type pos, size_type count = npos) + { + View v = view; + P_Check(pos <= v.size()); + Append(v.data() + pos, Min(count, v.size() - pos)); + return *this; + } + template + TString& append(It first, It last) + { + AppendIter(first, last); + return *this; + } + TString& append(std::initializer_list initList) + { + AppendIter(initList.begin(), initList.end()); + return *this; + } + + TString& operator+=(const TString& other) + { + return append(other); + } + TString& operator+=(const CharType* str) + { + return append(str); + } + TString& operator+=(CharType ch) + { + push_back(ch); + return *this; + } + TString& operator+=(std::initializer_list initList) + { + return append(initList); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& operator+=(const SVLike& view) + { + return append(view); + } + + TString& insert(size_type pos, size_type count, CharType ch) + { + P_Check(pos <= Len()); + P_CheckMsg(count <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(count); + const i32 oldLen = Len(); + chars.AddUninitialized(num); + traits_type::move(Buf() + pos + num, Buf() + pos, i32(oldLen - pos) + 1); + traits_type::assign(Buf() + pos, num, ch); + return *this; + } + TString& insert(size_type pos, const CharType* str) + { + return insert(pos, str, traits_type::length(str)); + } + TString& insert(size_type pos, const CharType* str, size_type count) + { + P_Check(pos <= Len()); + P_CheckMsg(count <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(count); + TArray temp = ResolveAliased(str, num); + const i32 oldLen = Len(); + chars.AddUninitialized(num); + traits_type::move(Buf() + pos + num, Buf() + pos, i32(oldLen - pos) + 1); + traits_type::copy(Buf() + pos, str, num); + return *this; + } + TString& insert(size_type pos, const TString& other) + { + return insert(pos, other.Buf(), other.Len()); + } + TString& insert( + size_type pos, const TString& other, size_type otherPos, size_type count = npos) + { + P_Check(otherPos <= other.Len()); + return insert(pos, other.Buf() + otherPos, Min(count, other.Len() - otherPos)); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& insert(size_type pos, const SVLike& view) + { + View v = view; + return insert(pos, v.data(), v.size()); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& insert( + size_type pos, const SVLike& view, size_type otherPos, size_type count = npos) + { + View v = view; + P_Check(otherPos <= v.size()); + return insert(pos, v.data() + otherPos, Min(count, v.size() - otherPos)); + } + + iterator insert(const_iterator pos, CharType ch) + { + const i32 index = i32(pos - Buf()); + insert(sizet(index), 1, ch); + return Buf() + index; + } + /** Integral first parameters are excluded so that insert(0, 3, '=') is not ambiguous */ + template + requires(!std::is_integral_v) + iterator insert(T pos, size_type count, CharType ch) + { + const i32 index = i32(pos - Buf()); + insert(sizet(index), count, ch); + return Buf() + index; + } + template + iterator insert(const_iterator pos, It first, It last) + { + if constexpr (requires(It a, It b) { a - b; }) // Random access (or pointer) + { + const i32 index = i32(pos - Buf()); + const i32 count = i32(last - first); + if (count > 0) + { + const i32 oldLen = Len(); + chars.AddUninitialized(count); + traits_type::move( + Buf() + index + count, Buf() + index, i32(oldLen - index) + 1); + CharType* dest = Buf() + index; + for (It it = first; it != last; ++it, ++dest) + { + *dest = *it; + } + } + return Buf() + index; + } + else // Single pass iterators (input iterators) + { + const i32 index = i32(pos - Buf()); + TString temp(first, last); + insert(sizet(index), temp.Buf(), temp.Len()); + return Buf() + index; + } + } + iterator insert(const_iterator pos, std::initializer_list initList) + { + return insert(pos, initList.begin(), initList.end()); + } + + TString& erase(size_type pos = 0, size_type count = npos) + { + P_Check(pos <= Len()); + const i32 len = Len(); + const i32 num = i32(Min(count, len - pos)); + if (num > 0) + { + traits_type::move( + Buf() + pos, Buf() + pos + num, i32(len - pos - num) + 1); // + terminator + chars.RemoveLast(num, Shrink::No); + Buf()[Len()] = CharType{}; + } + return *this; + } + iterator erase(const_iterator pos) + { + P_Check(pos >= begin() && pos < end()); + const i32 index = i32(pos - Buf()); + erase(sizet(index), 1); + return Buf() + index; + } + iterator erase(const_iterator first, const_iterator last) + { + P_Check(first >= begin() && first <= end() && last >= first && last <= end()); + const i32 index = i32(first - Buf()); + erase(sizet(index), sizet(last - first)); + return Buf() + index; + } + + TString& replace(size_type pos, size_type count, const TString& other) + { + return ReplaceRange(pos, count, other.Buf(), other.Len()); + } + TString& replace(size_type pos, size_type count, const TString& other, size_type otherPos, + size_type otherCount = npos) + { + P_Check(otherPos <= other.Len()); + return ReplaceRange( + pos, count, other.Buf() + otherPos, i32(Min(otherCount, other.Len() - otherPos))); + } + TString& replace(size_type pos, size_type count, const CharType* str) + { + return ReplaceRange(pos, count, str, traits_type::length(str)); + } + TString& replace(size_type pos, size_type count, const CharType* str, size_type strCount) + { + return ReplaceRange(pos, count, str, strCount); + } + TString& replace(size_type pos, size_type count, size_type num, CharType ch) + { + P_Check(pos <= Len()); + const TString temp(num, ch); + return ReplaceRange(pos, count, temp.Buf(), temp.Len()); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& replace(size_type pos, size_type count, const SVLike& view) + { + View v = view; + return ReplaceRange(pos, count, v.data(), v.size()); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& replace(size_type pos, size_type count, const SVLike& view, size_type otherPos, + size_type otherCount = npos) + { + View v = view; + P_Check(otherPos <= v.size()); + return ReplaceRange( + pos, count, v.data() + otherPos, Min(otherCount, v.size() - otherPos)); + } + TString& replace(const_iterator first, const_iterator last, const TString& other) + { + return ReplaceRange(first - Buf(), last - first, other.Buf(), other.Len()); + } + TString& replace(const_iterator first, const_iterator last, const CharType* str) + { + return ReplaceRange(first - Buf(), last - first, str, traits_type::length(str)); + } + TString& replace( + const_iterator first, const_iterator last, const CharType* str, size_type count) + { + return ReplaceRange(first - Buf(), last - first, str, count); + } + TString& replace(const_iterator first, const_iterator last, size_type num, CharType ch) + { + const TString temp(num, ch); + return ReplaceRange(first - Buf(), last - first, temp.Buf(), temp.Len()); + } + template + requires(std::convertible_to + && !std::convertible_to) + TString& replace(const_iterator first, const_iterator last, const SVLike& view) + { + View v = view; + return ReplaceRange(first - Buf(), last - first, v.data(), v.size()); + } + template + TString& replace(const_iterator first, const_iterator last, It first2, It last2) + { + const i32 pos = i32(first - Buf()); + const i32 count = i32(last - first); + const TString temp(first2, last2); + return ReplaceRange(sizet(pos), sizet(count), temp.Buf(), temp.Len()); + } + + void resize(size_type newSize, CharType ch = CharType{}) + { + P_CheckMsg(newSize <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(newSize); + const i32 len = Len(); + if (num < len) + { + chars.RemoveLast(len - num, Shrink::No); + Buf()[num] = CharType{}; + } + else if (num > len) + { + append(num - len, ch); + } + } + + void swap(TString& other) + { + p::Swap(chars, other.chars); + } + + friend void swap(TString& a, TString& b) + { + a.swap(b); + } +#pragma endregion Modifiers + + +#pragma region String operations + TString substr(size_type pos = 0, size_type count = npos) const + { + P_Check(pos <= Len()); + return TString{*this, pos, count}; + } + + size_type copy(CharType* dest, size_type count, size_type pos = 0) const + { + P_Check(pos <= Len()); + const i32 num = i32(Min(count, Len() - pos)); + traits_type::copy(dest, Buf() + pos, num); + return sizet(num); + } + + i32 compare(const TString& other) const + { + return AsView().compare(other.AsView()); + } + i32 compare(size_type pos, size_type count, const TString& other) const + { + return substr(pos, count).compare(other); + } + i32 compare(size_type pos, size_type count, const TString& other, size_type otherPos, + size_type otherCount = npos) const + { + return substr(pos, count).compare(other.substr(otherPos, otherCount)); + } + i32 compare(const CharType* str) const + { + return AsView().compare(View{str}); + } + i32 compare(size_type pos, size_type count, const CharType* str) const + { + return substr(pos, count).compare(View{str}); + } + i32 compare(size_type pos, size_type count, const CharType* str, size_type strCount) const + { + return substr(pos, count).compare(View{str, strCount}); + } + template + requires(std::convertible_to + && !std::convertible_to) + i32 compare(const SVLike& view) const + { + return AsView().compare(view); + } + template + requires(std::convertible_to + && !std::convertible_to) + i32 compare(size_type pos, size_type count, const SVLike& view) const + { + return substr(pos, count).compare(view); + } + template + requires(std::convertible_to + && !std::convertible_to) + i32 compare(size_type pos, size_type count, const SVLike& view, size_type otherPos, + size_type otherCount = npos) const + { + View v = view; + P_Check(otherPos <= v.size()); + return substr(pos, count) + .compare(v.substr(otherPos, Min(otherCount, v.size() - otherPos))); + } + + bool starts_with(View view) const + { + return Strings::StartsWith(AsView(), view); + } + bool starts_with(CharType ch) const + { + return !IsEmpty() && front() == ch; + } + bool starts_with(const CharType* str) const + { + return starts_with(View{str}); + } + + bool ends_with(View view) const + { + return Strings::EndsWith(AsView(), view); + } + bool ends_with(CharType ch) const + { + return !IsEmpty() && back() == ch; + } + bool ends_with(const CharType* str) const + { + return ends_with(View{str}); + } + + bool contains(View view) const + { + return find(view) != npos; + } + bool contains(CharType ch) const + { + return traits_type::find(Buf(), Len(), ch) != nullptr; + } + bool contains(const CharType* str) const + { + return contains(View{str}); + } + + size_type find(View view, size_type pos = 0) const + { + return AsView().find(view, pos); + } + size_type find(CharType ch, size_type pos = 0) const + { + return AsView().find(ch, pos); + } + size_type find(const CharType* str, size_type pos = 0) const + { + return AsView().find(View{str}, pos); + } + size_type find(const CharType* str, size_type pos, size_type count) const + { + return AsView().find(View{str, count}, pos); + } + + size_type rfind(View view, size_type pos = npos) const + { + return AsView().rfind(view, pos); + } + size_type rfind(CharType ch, size_type pos = npos) const + { + return AsView().rfind(ch, pos); + } + size_type rfind(const CharType* str, size_type pos = npos) const + { + return AsView().rfind(View{str}, pos); + } + size_type rfind(const CharType* str, size_type pos, size_type count) const + { + return AsView().rfind(View{str, count}, pos); + } + + size_type find_first_of(View view, size_type pos = 0) const + { + return AsView().find_first_of(view, pos); + } + size_type find_first_of(CharType ch, size_type pos = 0) const + { + return AsView().find_first_of(ch, pos); + } + size_type find_first_of(const CharType* str, size_type pos = 0) const + { + return AsView().find_first_of(View{str}, pos); + } + size_type find_first_of(const CharType* str, size_type pos, size_type count) const + { + return AsView().find_first_of(View{str, count}, pos); + } + + size_type find_last_of(View view, size_type pos = npos) const + { + return AsView().find_last_of(view, pos); + } + size_type find_last_of(CharType ch, size_type pos = npos) const + { + return AsView().find_last_of(ch, pos); + } + size_type find_last_of(const CharType* str, size_type pos = npos) const + { + return AsView().find_last_of(View{str}, pos); + } + size_type find_last_of(const CharType* str, size_type pos, size_type count) const + { + return AsView().find_last_of(View{str, count}, pos); + } + + size_type find_first_not_of(View view, size_type pos = 0) const + { + return AsView().find_first_not_of(view, pos); + } + size_type find_first_not_of(CharType ch, size_type pos = 0) const + { + return AsView().find_first_not_of(ch, pos); + } + size_type find_first_not_of(const CharType* str, size_type pos = 0) const + { + return AsView().find_first_not_of(View{str}, pos); + } + size_type find_first_not_of(const CharType* str, size_type pos, size_type count) const + { + return AsView().find_first_not_of(View{str, count}, pos); + } + + size_type find_last_not_of(View view, size_type pos = npos) const + { + return AsView().find_last_not_of(view, pos); + } + size_type find_last_not_of(CharType ch, size_type pos = npos) const + { + return AsView().find_last_not_of(ch, pos); + } + size_type find_last_not_of(const CharType* str, size_type pos = npos) const + { + return AsView().find_last_not_of(View{str}, pos); + } + size_type find_last_not_of(const CharType* str, size_type pos, size_type count) const + { + return AsView().find_last_not_of(View{str, count}, pos); + } +#pragma endregion String operations + + + View AsView() const + { + return {Buf(), size()}; + } + + /** @return the arena this string allocates from */ + Arena& GetArena() const + { + return *chars.arena; + } + +#pragma region Operators + friend bool operator==(const TString& a, const TString& b) + { + return a.size() == b.size() && traits_type::compare(a.Buf(), b.Buf(), a.Len()) == 0; + } + friend auto operator<=>(const TString& a, const TString& b) + { + const i32 result = traits_type::compare(a.Buf(), b.Buf(), Min(a.Len(), b.Len()) + 1); + if (result != 0) [[likely]] + { + return result <=> 0; + } + return a.Len() <=> b.Len(); + } + + friend bool operator==(const TString& a, const CharType* str) + { + return a.AsView() == View{str}; + } + friend auto operator<=>(const TString& a, const CharType* str) + { + return a.AsView() <=> View{str}; + } + + friend bool operator==(const TString& a, View b) + { + return a.AsView() == b; + } + friend auto operator<=>(const TString& a, View b) + { + return a.AsView() <=> b; + } + + friend TString operator+(const TString& a, const TString& b) + { + TString result{a}; + result += b; + return result; + } + friend TString operator+(const TString& a, const CharType* str) + { + TString result{a}; + result += str; + return result; + } + friend TString operator+(const CharType* str, const TString& b) + { + TString result; + result.reserve(traits_type::length(str) + b.size()); + result += str; + result += b; + return result; + } + friend TString operator+(const TString& a, CharType ch) + { + TString result{a}; + result += ch; + return result; + } + friend TString operator+(CharType ch, const TString& b) + { + TString result; + result.reserve(b.size() + 1); + result += ch; + result += b; + return result; + } + friend TString operator+(const TString& a, View b) + { + TString result{a}; + result += b; + return result; + } + friend TString operator+(View a, const TString& b) + { + TString result; + result.reserve(a.size() + b.size()); + result += a; + result += b; + return result; + } + + friend TString operator+(TString&& a, const TString& b) + { + a += b; + return Move(a); + } + friend TString operator+(TString&& a, const CharType* str) + { + a += str; + return Move(a); + } + friend TString operator+(TString&& a, CharType ch) + { + a += ch; + return Move(a); + } + friend TString operator+(TString&& a, View b) + { + a += b; + return Move(a); + } + friend TString operator+(const CharType* str, TString&& b) + { + b.insert(sizet(0), str); + return Move(b); + } + friend TString operator+(CharType ch, TString&& b) + { + b.insert(b.begin(), ch); + return Move(b); + } + friend TString operator+(View a, TString&& b) + { + b.insert(sizet(0), a); + return Move(b); + } +#pragma endregion Operators + + + private: + constexpr i32 Len() const + { + return chars.Size() - 1; + } + constexpr bool IsEmpty() const + { + return chars.Size() <= 1; + } + + constexpr CharType* Buf() + { + return chars.Data(); + } + constexpr const CharType* Buf() const + { + return chars.Data(); + } + + /** Ensures the string contains its terminator. Allows safe Len() on fresh objects */ + void EnsureInit() + { + if (chars.Size() == 0) + { + chars.Add(CharType{}); + } + } + + /** Ensures the string starts with its terminator, empty */ + void InitEmpty() + { + chars.Clear(Shrink::No); + chars.Add(CharType{}); + } + + /** + * Self-referential sources must be copied out before any buffer reallocation. + * Replaces the source with a pointer to a safe temporary buffer if it aliases this string. + * @return the temporary buffer, kept alive by the caller until the source is consumed + */ + TArray ResolveAliased(const CharType*& str, i32 count) + { + TArray temp; + if (count > 0 && str && chars.Size() > 0 && str >= Buf() && str < Buf() + chars.Size()) + { + temp.arena = chars.arena; + temp.AddUninitialized(count); + traits_type::copy(temp.Data(), str, count); + str = temp.Data(); + } + return temp; + } + + void SetFrom(const CharType* str, size_type count) + { + P_CheckMsg(count <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(count); + if (str && num > 0) + { + TArray temp = ResolveAliased(str, num); + chars.Clear(Shrink::No); + chars.Reserve(num + 1); + chars.AddUninitialized(num + 1); + traits_type::copy(Buf(), str, num); + Buf()[num] = CharType{}; + } + else + { + clear(); + } + } + + void Append(const CharType* str, size_type count) + { + P_CheckMsg(count <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(count); + if (str && num > 0) + { + TArray temp = ResolveAliased(str, num); + EnsureInit(); + const i32 len = Len(); + chars.AddUninitialized(num); + traits_type::copy(Buf() + len, str, num); + Buf()[len + num] = CharType{}; + } + } + + void AppendCount(size_type count, CharType ch) + { + P_CheckMsg(count <= sizet(max_size()), "String size exceeds max_size"); + const i32 num = i32(count); + if (num > 0) + { + EnsureInit(); + const i32 len = Len(); + chars.AddUninitialized(num); + traits_type::assign(Buf() + len, num, ch); + Buf()[len + num] = CharType{}; + } + } + + template + void AppendIter(It first, It last) + { + if constexpr (requires(It a, It b) { a - b; }) // Random access (or pointer) + { + const i32 num = i32(last - first); + if (num > 0) + { + EnsureInit(); + const i32 len = Len(); + chars.AddUninitialized(num); + CharType* dest = Buf() + len; + for (It it = first; it != last; ++it, ++dest) + { + *dest = *it; + } + Buf()[len + num] = CharType{}; + } + } + else // Single pass iterators (input iterators) + { + for (; first != last; ++first) + { + push_back(*first); + } + } + } + + TString& ReplaceRange(size_type pos, size_type count, const CharType* str, size_type strNum) + { + P_Check(pos <= Len()); + P_CheckMsg(strNum <= sizet(max_size()), "String size exceeds max_size"); + const i32 n = i32(strNum); + TArray temp = ResolveAliased(str, n); + const i32 len = Len(); + const i32 num = i32(Min(count, len - pos)); + const i32 tail = len - i32(pos) - num; + if (n > num) // Grow first so we don't overlap while moving + { + chars.AddUninitialized(n - num); + traits_type::move(Buf() + pos + n, Buf() + pos + num, tail + 1); // + terminator + } + else if (n < num) + { + traits_type::move(Buf() + pos + n, Buf() + pos + num, tail + 1); + chars.RemoveLast(num - n, Shrink::No); + Buf()[Len()] = CharType{}; + } + traits_type::move(Buf() + pos, str, n); + return *this; + } + }; + + + using String = TString; + using WString = TString; + + template + using FormatString = std::basic_format_string; + + template + inline StringType Format(StringView format, Args... args) + { + StringType str; + std::vformat_to(std::back_inserter(str), format, std::make_format_args(args...)); + return Move(str); + } + + template + inline void FormatTo(StringType& buffer, StringView format, Args... args) + { + std::vformat_to(std::back_inserter(buffer), format, std::make_format_args(args...)); + } + + // Format a string using a compile time format + template + inline StringType Format(FormatString format, Args... args) + { + StringType str; + std::format_to(std::back_inserter(str), format, p::Fwd(args)...); + return Move(str); + } + + // Format into an existing string using a compile time format + template + inline void FormatTo(StringType& buffer, FormatString format, Args... args) + { + std::format_to(std::back_inserter(buffer), format, p::Fwd(args)...); + } + + namespace Strings + { + + template + inline void ToString(StringType& buffer, T value, FormatString format = "{}") + { + std::format_to(std::back_inserter(buffer), format, p::Fwd(value)); + } + + template + inline StringType ToString(T value) + { + StringType str; + ToString(str, value); + return str; + } + + P_API String ToSentenceCase(StringView value); + + /** + * Breaks up a delimited string into elements of a string array. + * + * @param InArray The array to fill with the string pieces + * @param pchDelim The string to delimit on + * @param InCullEmpty If 1, empty strings are not added to the array + * + * @return The number of elements in InArray + */ + P_API i32 ParseIntoArray(const String& str, TArray& OutArray, const char* pchDelim, + bool InCullEmpty = true); + + P_API void RemoveFromStart(String& str, sizet size); + P_API void RemoveFromEnd(String& str, sizet size); + P_API void RemoveFromEnd(String& str, StringView subStr); + + P_API bool RemoveCharFromEnd(String& str, char c); + + P_API i32 Split(const String& str, TArray& tokens, const char delim); + + P_API bool Split(const String& str, String& a, String& b, const char* delim); + + P_API bool IsNumeric(const String& str); + P_API bool IsNumeric(const char* Str); + + P_API String ParseMemorySize(sizet size); + + template + inline void ConvertTo(TStringView source, ToStringType& dest) + { + using ToChar = typename ToStringType::value_type; + static_assert( + std::is_integral_v, "FromChar is not integral (so it is not a char)"); + static_assert( + std::is_integral_v, "ToChar is not integral (so it is not a char)"); + + if constexpr (IsSame) + { + dest += source; + } + else if constexpr (sizeof(FromChar) == 1 && sizeof(ToChar) == 2) + { + utf8::unchecked::utf8to16(source.begin(), source.end(), std::back_inserter(dest)); + } + else if constexpr (sizeof(FromChar) == 2 && sizeof(ToChar) == 1) + { + utf8::unchecked::utf16to8(source.begin(), source.end(), std::back_inserter(dest)); + } + else if constexpr (sizeof(FromChar) == 1 && sizeof(ToChar) == 4) + { + utf8::unchecked::utf8to32(source.begin(), source.end(), std::back_inserter(dest)); + } + else if constexpr (sizeof(FromChar) == 4 && sizeof(ToChar) == 1) + { + utf8::unchecked::utf32to8(source.begin(), source.end(), std::back_inserter(dest)); + } + else + { + // TODO: Find a way to assert at compile time except on the previous cases + // static_assert(false, "Unknown char conversion"); + } + } + + template + inline ToStringType Convert(TStringView source) + { + ToStringType dest; + ConvertTo(source, dest); + return Move(dest); + } + template + inline ToStringType Convert(const TString& source) + { + ToStringType dest; + ConvertTo(TStringView{source}, dest); + return Move(dest); + } + }; // namespace Strings + + + inline sizet GetHash(const String& str) + { + return GetStringHash(str.data()); + } + + template + inline std::basic_ostream>& operator<<( + std::basic_ostream>& os, + const TString& str) + { + os << str.AsView(); + return os; + } +} // namespace p + +template +struct std::formatter, CharType> + : std::formatter, CharType> +{ + template + auto format(const p::TString& str, FormatContext& ctx) const + { + return std::formatter, CharType>::format( + std::basic_string_view{str.data(), str.size()}, ctx); + } +}; + +template +struct std::hash> +{ + size_t operator()(const p::TString& str) const noexcept + { + return p::GetStringHash(str.data()); + } +}; diff --git a/Include/PipeTime.h b/Include/PipeTime.h index 01db243c..1437a136 100644 --- a/Include/PipeTime.h +++ b/Include/PipeTime.h @@ -3,7 +3,7 @@ #pragma once #include "Pipe/Core/Checks.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "PipePlatform.h" #include diff --git a/Include/PipeVectors.h b/Include/PipeVectors.h index 9025b14d..36c74e43 100644 --- a/Include/PipeVectors.h +++ b/Include/PipeVectors.h @@ -8,7 +8,7 @@ /// @OPTIMIZE: Try to remove this include -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include diff --git a/Src/Core/Checks.cpp b/Src/Core/Checks.cpp index 13ae83b8..dd8f8aae 100644 --- a/Src/Core/Checks.cpp +++ b/Src/Core/Checks.cpp @@ -3,7 +3,7 @@ #include "Pipe/Core/Checks.h" #include "Pipe/Core/Log.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" @@ -14,12 +14,12 @@ namespace p::details p::String text{inText}; if (text.empty()) { - p::Strings::FormatTo(text, "Failed check \"{}\" at {}:{}", p::StringView{expr}, + FormatTo(text, "Failed check \"{}\" at {}:{}", p::StringView{expr}, p::StringView{file}, line); } else { - p::Strings::FormatTo(text, "\n(Failed check \"{}\" at {}:{})", p::StringView{expr}, + FormatTo(text, "\n(Failed check \"{}\" at {}:{})", p::StringView{expr}, p::StringView{file}, line); } Error(text); diff --git a/Src/Core/Guid.cpp b/Src/Core/Guid.cpp index b4f2df21..5d8a4481 100644 --- a/Src/Core/Guid.cpp +++ b/Src/Core/Guid.cpp @@ -18,27 +18,27 @@ namespace p switch (Format) { case EGuidFormats::DigitsWithHyphens: - return Strings::Format("{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}", a, b >> 16, + return ::p::Format("{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}", a, b >> 16, b & 0xFFFF, c >> 16, c & 0xFFFF, d); case EGuidFormats::DigitsWithHyphensInBraces: - return Strings::Format("{{{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}}}", a, b >> 16, + return ::p::Format("{{{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}}}", a, b >> 16, b & 0xFFFF, c >> 16, c & 0xFFFF, d); case EGuidFormats::DigitsWithHyphensInParentheses: - return Strings::Format("({:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X})", a, b >> 16, + return ::p::Format("({:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X})", a, b >> 16, b & 0xFFFF, c >> 16, c & 0xFFFF, d); case EGuidFormats::HexValuesInBraces: - return Strings::Format( + return ::p::Format( "{{0x{:08X},0x{:04X},0x{:04X},{{0x{:02X},0x{:02X},0x{:02X}," "0x{:02X},0x{:02X},0x{:02X},0x{:02X},0x{:02X}}}}}", a, b >> 16, b & 0xFFFF, c >> 24, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0XFF, d >> 24, (d >> 16) & 0XFF, (d >> 8) & 0XFF, d & 0XFF); case EGuidFormats::UniqueObjectGuid: - return Strings::Format("{:08X}-{:08X}-{:08X}-{:08X}", a, b, c, d); - default: return Strings::Format("{:08X}{:08X}{:08X}{:08X}", a, b, c, d); + return ::p::Format("{:08X}-{:08X}-{:08X}-{:08X}", a, b, c, d); + default: return ::p::Format("{:08X}{:08X}{:08X}{:08X}", a, b, c, d); } } diff --git a/Src/Core/Log.cpp b/Src/Core/Log.cpp index b000bc5a..17fc2c92 100644 --- a/Src/Core/Log.cpp +++ b/Src/Core/Log.cpp @@ -17,21 +17,21 @@ namespace p String text; auto now = DateTime::Now(); now.ToString("[%Y/%m/%d %H:%M:%S]", text); - Strings::FormatTo(text, "[Info] {}\n", msg); + FormatTo(text, "[Info] {}\n", msg); std::cout << text; }, .warningCallback = [](StringView msg) { String text; auto now = DateTime::Now(); now.ToString("[%Y/%m/%d %H:%M:%S]", text); - Strings::FormatTo(text, "[Warning] {}\n", msg); + FormatTo(text, "[Warning] {}\n", msg); std::cout << text; }, .errorCallback = [](StringView msg) { String text; auto now = DateTime::Now(); now.ToString("[%Y/%m/%d %H:%M:%S]", text); - Strings::FormatTo(text, "[Error] {}\n", msg); + FormatTo(text, "[Error] {}\n", msg); std::cout << text; } }; diff --git a/Src/Core/Subprocess.cpp b/Src/Core/Subprocess.cpp index 0fd76400..fdafea3d 100644 --- a/Src/Core/Subprocess.cpp +++ b/Src/Core/Subprocess.cpp @@ -3,7 +3,7 @@ #include "Pipe/Core/Log.h" #include "Pipe/Core/Optional.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "PipeContainers.h" #include "PipePlatform.h" diff --git a/Src/Files/Files.cpp b/Src/Files/Files.cpp index 64705ab7..c5ac19a1 100644 --- a/Src/Files/Files.cpp +++ b/Src/Files/Files.cpp @@ -100,7 +100,7 @@ namespace p // Clean string and reserve it result = {}; - std::ifstream file(String{path}); + std::ifstream file(String{path}.c_str()); file.seekg(0, std::ios::end); const sizet size = sizet(file.tellg()); if (size > 0) @@ -120,7 +120,7 @@ namespace p return false; } - std::basic_ofstream file(String{path}); + std::basic_ofstream file(String{path}.c_str()); file.write(data.data(), data.size()); file.close(); return true; diff --git a/Src/Files/Paths.cpp b/Src/Files/Paths.cpp index da9e022a..990f1baf 100644 --- a/Src/Files/Paths.cpp +++ b/Src/Files/Paths.cpp @@ -656,7 +656,7 @@ namespace p String ToString(const Path& path) { - return path.string, std::allocator>(); + return Strings::Convert(PathView{path.c_str(), path.native().size()}); } Path ToSTDPath(StringView pathStr) diff --git a/Src/Files/PlatformPaths.cpp b/Src/Files/PlatformPaths.cpp index b39310b1..5ba99d65 100644 --- a/Src/Files/PlatformPaths.cpp +++ b/Src/Files/PlatformPaths.cpp @@ -4,7 +4,7 @@ #include "Pipe/Core/FixedString.h" #include "Pipe/Core/Log.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Files/Files.h" #include "Pipe/Files/Paths.h" @@ -241,7 +241,7 @@ namespace p } else if (IsFile(path)) { - const String parameters = Strings::Format("/select,{}", path); + const String parameters = Format("/select,{}", path); ::ShellExecuteA( nullptr, "open", "explorer.exe", parameters.data(), nullptr, SW_SHOWNORMAL); } diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index dcf5a2ce..f7455189 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -2,9 +2,9 @@ #include "Pipe/Memory/MemoryStats.h" -#include "Pipe/Core/Set.h" -#include "Pipe/Core/String.h" +#include "Pipe/Core/Map.h" #include "PipeMath.h" +#include "PipeStrings.h" namespace p @@ -59,10 +59,10 @@ namespace p void PrintAllocationError(StringView error, const MemoryStatsEvent* allocation) { String msg; - Strings::FormatTo(msg, error); + FormatTo(msg, error); if (allocation) { - Strings::FormatTo(msg, " ({} {})", static_cast(allocation->GetPtr()), + FormatTo(msg, " ({} {})", static_cast(allocation->GetPtr()), Strings::ParseMemorySize(allocation->GetSize())); } std::puts(msg.data()); @@ -70,7 +70,11 @@ namespace p } // namespace MemoryStats::MemoryStats() - : events{GetStatsArena()}, live{GetStatsArena()}, frees{GetStatsArena()} + : events{GetStatsArena()} + , live{GetStatsArena()} + , frees{GetStatsArena()} + , liveIdx{GetStatsArena()} + , prevLiveIdx{GetStatsArena()} {} MemoryStats::~MemoryStats() @@ -186,9 +190,14 @@ namespace p { // Drain all thread buffers and reset state. CollectStats(); - used = 0; - totalAllocated = 0; + used = 0; + totalAllocated = 0; + collectedEvents = 0; events.Clear(); + live.Clear(); + frees.Clear(); + liveIdx.Clear(); + prevLiveIdx.Clear(); } void MemoryStats::CollectStats() const @@ -234,6 +243,17 @@ namespace p } + // --- Incremental classification of drained events --- + // live[i]: events[i] is an alloc never matched by a free. + // frees[i]: events[i] is a free event. + // Events are append-only, so bits computed in previous calls remain + // valid; only classify events drained since the last call. A free + // matches the most recent unmatched alloc with the same key (LIFO), + // mirroring a full reverse scan. + live.Resize(events.Size()); + frees.Resize(events.Size()); + prevLiveIdx.Resize(events.Size()); + // Fast (ptr,size) key: XOR ptr with mixed size to produce a // single u64. Cheaper to hash than the full 16-byte event. auto EventKey = [](const MemoryStatsEvent& ev) -> u64 @@ -242,63 +262,49 @@ namespace p ^ (static_cast(ev.GetSize()) * 0x9E3779B97F4A7C15ULL); }; - // Reverse scan: build live bitmask + set of unmatched free keys. - live.Resize(events.Size()); - live.SetAllFalse(); - TSet freeKeys{GetStatsArena()}; - for (i32 i = events.Size() - 1; i >= 0; --i) + for (i32 i = collectedEvents; i < events.Size(); ++i) { const auto& ev = events[i]; + const u64 key = EventKey(ev); + auto it = liveIdx.FindIt(key); if (ev.IsFree()) { - freeKeys.Insert(EventKey(ev)); - } - else - { - auto it = freeKeys.FindIt(EventKey(ev)); - if (it != freeKeys.end()) + frees.SetTrue(i); + if (it != liveIdx.end()) { - freeKeys.RemoveIt(it); - } - else - { - live.SetTrue(i); + // Unmark the newest unmatched alloc and pop it off the + // chain, promoting its predecessor as chain head. + const i32 node = it->second; + live.SetFalse(node); + const i32 prev = prevLiveIdx[node]; + if (prev == NO_INDEX) + { + liveIdx.RemoveIt(it); + } + else + { + *const_cast(&it->second) = prev; + } } + // Else a stray free: recorded, nothing to unmark. } - } - - // In-place compaction + bitmask rebuild (single pass). - // Drops matched alloc/free pairs, keeps leaks + stray frees. - frees.Resize(events.Size()); - frees.SetAllFalse(); - i32 writeIdx = 0; - for (i32 i = 0; i < events.Size(); ++i) - { - const MemoryStatsEvent& ev = events[i]; - const bool keep = - ev.IsFree() ? freeKeys.Contains(EventKey(ev)) : live.IsSet(i); - if (keep) + else { - const bool isFree = ev.IsFree(); - if (writeIdx != i) - { - events[writeIdx] = ev; - } - if (isFree) + // Push this alloc as the newest node of the key's chain. + if (it != liveIdx.end()) { - frees.SetTrue(writeIdx); - live.SetFalse(writeIdx); + prevLiveIdx[i] = it->second; + *const_cast(&it->second) = i; } else { - live.SetTrue(writeIdx); + prevLiveIdx[i] = NO_INDEX; + liveIdx.Insert(key, i); } - ++writeIdx; + live.SetTrue(i); } } - events.Resize(writeIdx); - frees.Resize(writeIdx); - live.Resize(writeIdx); + collectedEvents = events.Size(); } void MemoryStats::CheckLeaks() const @@ -310,7 +316,7 @@ namespace p } String errorMsg; - Strings::FormatTo(errorMsg, "{}: {} allocs were not freed!", name, numLeaks); + FormatTo(errorMsg, "{}: {} allocs were not freed!", name ? name : "MemoryStats", numLeaks); const i32 shown = Min(64, numLeaks); i32 i = -1; @@ -327,7 +333,7 @@ namespace p } if (numLeaks > shown) { - Strings::FormatTo(errorMsg, "\n...\n{} more not shown.", numLeaks - shown); + FormatTo(errorMsg, "\n...\n{} more not shown.", numLeaks - shown); } std::puts(errorMsg.data()); } diff --git a/Src/PipeContainers.cpp b/Src/PipeContainers.cpp index 7e25571f..8a5d15fb 100644 --- a/Src/PipeContainers.cpp +++ b/Src/PipeContainers.cpp @@ -16,13 +16,7 @@ namespace p BitArray::BitArray(const bool* data, i32 newSize) : size{newSize}, bits(CalculateDataSize(newSize)) { - for (i32 i = 0; i < newSize; ++i) - { - if (data[i]) - { - SetTrue(i); - } - } + SetFromBools(data, newSize); } BitArray::BitArray(Arena& arena, i32 newSize) @@ -38,13 +32,7 @@ namespace p BitArray::BitArray(Arena& arena, const bool* data, i32 newSize) : size{newSize}, bits(arena, CalculateDataSize(newSize)) { - for (i32 i = 0; i < newSize; ++i) - { - if (data[i]) - { - SetTrue(i); - } - } + SetFromBools(data, newSize); } BitArray::BitArray(BitArray&& other) noexcept @@ -186,24 +174,24 @@ namespace p return NO_INDEX; } - i32 BitArray::CountSetBits(i32 fromIndex, i32 toIndex) const + i32 BitArray::CountSetBits() const { - if (toIndex == NO_INDEX) - { - toIndex = size; - } - - P_Check(fromIndex >= 0); - P_Check(toIndex >= fromIndex && toIndex <= size); - i32 numSetBits = 0; - // To data indices - fromIndex = fromIndex >> 5; - toIndex = toIndex >> 5; - for (i32 i = fromIndex; i < toIndex; ++i) + for (i32 i = 0; i < bits.Size(); ++i) { numSetBits += CountBits(bits[i]); } return numSetBits; } + + void BitArray::SetFromBools(const bool* data, i32 count) + { + for (i32 i = 0; i < count; ++i) + { + if (data[i]) + { + SetTrue(i); + } + } + } } // namespace p diff --git a/Src/PipeFiles.cpp b/Src/PipeFiles.cpp index 7b73a059..59e4e2dc 100644 --- a/Src/PipeFiles.cpp +++ b/Src/PipeFiles.cpp @@ -21,16 +21,16 @@ namespace p { #pragma region FileDialogs - std::vector ParseFilters(const TArray& filters) + std::vector ParseFilters(const TArray& filters) { - std::vector rawFilters; + std::vector rawFilters; rawFilters.reserve(sizet(filters.Size()) * 2); for (const DialogFileFilter& filter : filters) { rawFilters.emplace_back(filter.first); rawFilters.emplace_back(filter.second); } - return p::Move(rawFilters); + return rawFilters; } String SelectFileDialog(StringView title, StringView defaultPath, @@ -41,9 +41,10 @@ namespace p { options = options | pfd::opt::force_path; } - pfd::open_file dialog(String{title}, String{defaultPath}, ParseFilters(filters), options); + pfd::open_file dialog( + std::string{title}, std::string{defaultPath}, ParseFilters(filters), options); - std::vector files = dialog.result(); + const std::vector files = dialog.result(); if (files.size() > 0) { return String{files[0]}; @@ -59,9 +60,10 @@ namespace p { options = options | pfd::opt::force_path; } - pfd::open_file dialog(String{title}, String{defaultPath}, ParseFilters(filters), options); + pfd::open_file dialog( + std::string{title}, std::string{defaultPath}, ParseFilters(filters), options); - std::vector files = dialog.result(); + const std::vector files = dialog.result(); outFiles.Resize(i32(files.size())); for (u32 i = 0; i < files.size(); ++i) { @@ -76,7 +78,7 @@ namespace p { options = options | pfd::opt::force_path; } - pfd::select_folder dialog{String{title}, String{defaultPath}, options}; + pfd::select_folder dialog{std::string{title}, std::string{defaultPath}, options}; return String{dialog.result()}; } @@ -88,7 +90,8 @@ namespace p { options = options | pfd::opt::force_path; } - pfd::save_file dialog{String{title}, String{defaultPath}, ParseFilters(filters), options}; + pfd::save_file dialog{ + std::string{title}, std::string{defaultPath}, ParseFilters(filters), options}; String path{dialog.result()}; p::ReplaceExtension(path, "rf"); return path; @@ -136,16 +139,16 @@ namespace p switch (error) { case FWE_FileNotFound: - lastFileWatcherError = Strings::Format("File not found ({})", log); + lastFileWatcherError = Format("File not found ({})", log); break; case FWE_FileRepeated: - lastFileWatcherError = Strings::Format("File repeated in watches ({})", log); + lastFileWatcherError = Format("File repeated in watches ({})", log); break; case FWE_FileOutOfScope: - lastFileWatcherError = Strings::Format("Symlink file out of scope ({})", log); + lastFileWatcherError = Format("Symlink file out of scope ({})", log); break; case FWE_FileRemote: - lastFileWatcherError = Strings::Format( + lastFileWatcherError = Format( "File is located in a remote file system, use a generic watcher ({})", log); break; case FWE_Unspecified: @@ -349,8 +352,8 @@ namespace p void DirectorySnapshot::InitFiles() { files.Clear(); - for (auto& it : - DirectoryIterator(path, std::filesystem::directory_options::follow_directory_symlink)) + for (auto& it : DirectoryIterator( + ToSTDPath(path), std::filesystem::directory_options::follow_directory_symlink)) { files.Insert(Tag{ToString(it.path().filename())}, it.status()); } @@ -393,8 +396,8 @@ namespace p } FileStatusMap currentFiles; - for (auto& it : - DirectoryIterator(path, std::filesystem::directory_options::follow_directory_symlink)) + for (auto& it : DirectoryIterator( + ToSTDPath(path), std::filesystem::directory_options::follow_directory_symlink)) { currentFiles.Insert(Tag{ToString(it.path().filename())}, it.status()); } diff --git a/Src/PipeSerialize.cpp b/Src/PipeSerialize.cpp index 04aefaaf..9349063b 100644 --- a/Src/PipeSerialize.cpp +++ b/Src/PipeSerialize.cpp @@ -5,7 +5,7 @@ #include "Pipe/Core/Checks.h" #include "Pipe/Core/Guid.h" #include "Pipe/Core/Log.h" -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/Tag.h" #include "Pipe/Extern/yyjson.h" #include "PipeMath.h" diff --git a/Src/Core/String.cpp b/Src/PipeStrings.cpp similarity index 88% rename from Src/Core/String.cpp rename to Src/PipeStrings.cpp index 7da6844f..a43ce02b 100644 --- a/Src/Core/String.cpp +++ b/Src/PipeStrings.cpp @@ -1,6 +1,6 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. -#include "Pipe/Core/String.h" +#include "PipeStrings.h" #include "Pipe/Core/Char.h" #include "PipeMath.h" @@ -11,12 +11,12 @@ namespace p::Strings String ToSentenceCase(StringView value) { + String result; if (value.empty()) { - return {}; + return result; } - String result; result.reserve(value.size()); const char* p = value.data(); @@ -47,7 +47,8 @@ namespace p::Strings void RemoveFromEnd(String& str, sizet size) { - str.resize(str.size() - size); + // Clamp to the string length to prevent underflowing the size + str.resize(str.size() - Min(size, str.size())); } void RemoveFromEnd(String& str, StringView subStr) { @@ -71,7 +72,7 @@ namespace p::Strings { sizet current, previous = 0; current = str.find(delim); - while (current != std::string::npos) + while (current != String::npos) { tokens.Add(str.substr(previous, current - previous)); previous = current + 1; @@ -129,7 +130,7 @@ namespace p::Strings String ParseMemorySize(sizet size) { - if (size <= 0) + if (size == 0) { return "0B"; } @@ -140,11 +141,13 @@ namespace p::Strings const u32 scale = u32(FloorToI64(scaleD)); const double finalSize = double(size) / Pow(1024, scale); - String sizeStr = Format("{:.1f}", finalSize); + String sizeStr = Format(StringView{"{:.1f}"}, finalSize); // Remove trailing zeros RemoveFromEnd(sizeStr, sizeStr.size() - Find(sizeStr, '0', FindDir::Back, true) - 1); RemoveFromEnd(sizeStr, sizeStr.size() - Find(sizeStr, '.', FindDir::Back, true) - 1); - return Format("{}{}", sizeStr, sizes[scale]); + String result = sizeStr; + result += sizes[scale]; + return result; } } // namespace p::Strings diff --git a/Src/PipeTime.cpp b/Src/PipeTime.cpp index e06806e5..4f510183 100644 --- a/Src/PipeTime.cpp +++ b/Src/PipeTime.cpp @@ -39,15 +39,15 @@ namespace p { switch (*format) { - case 'd': Strings::FormatTo(result, "{}", Abs(GetDays())); break; - case 'D': Strings::FormatTo(result, "{:08i}", Abs(GetDays())); break; - case 'h': Strings::FormatTo(result, "{:02i}", Abs(GetHours())); break; - case 'm': Strings::FormatTo(result, "{:02i}", Abs(GetMinutes())); break; - case 's': Strings::FormatTo(result, "{:02i}", Abs(GetSeconds())); break; - case 'f': Strings::FormatTo(result, "{:03i}", Abs(GetFractionMilli())); break; - case 'u': Strings::FormatTo(result, "{:06i}", Abs(GetFractionMicro())); break; - case 't': Strings::FormatTo(result, "{:07i}", Abs(GetFractionTicks())); break; - case 'n': Strings::FormatTo(result, "{:09i}", Abs(GetFractionNano())); break; + case 'd': FormatTo(result, "{}", Abs(GetDays())); break; + case 'D': FormatTo(result, "{:08i}", Abs(GetDays())); break; + case 'h': FormatTo(result, "{:02i}", Abs(GetHours())); break; + case 'm': FormatTo(result, "{:02i}", Abs(GetMinutes())); break; + case 's': FormatTo(result, "{:02i}", Abs(GetSeconds())); break; + case 'f': FormatTo(result, "{:03i}", Abs(GetFractionMilli())); break; + case 'u': FormatTo(result, "{:06i}", Abs(GetFractionMicro())); break; + case 't': FormatTo(result, "{:07i}", Abs(GetFractionTicks())); break; + case 'n': FormatTo(result, "{:09i}", Abs(GetFractionNano())); break; default: result += *format; } } @@ -296,7 +296,7 @@ namespace p case MonthOfYear::December: MonthStr = "Dec"; break; } - return Strings::Format("{}, {:02d} {} {} {:02i}:{:02i}:{:02i} GMT", DayStr, GetDay(), + return Format("{}, {:02d} {} {} {:02i}:{:02i}:{:02i} GMT", DayStr, GetDay(), MonthStr, GetYear(), GetHour(), GetMinute(), GetSecond()); } @@ -314,7 +314,7 @@ namespace p String DateTime::ToString(const char* format) const { - // return Strings::Format(format, *value); + // return Format(format, *value); String result; ToString(format, result); return result; @@ -332,16 +332,16 @@ namespace p { case 'a': result += IsMorning() ? "am" : "pm"; break; case 'A': result += IsMorning() ? "AM" : "PM"; break; - case 'd': Strings::FormatTo(result, "{:02}", GetDay()); break; - case 'D': Strings::FormatTo(result, "{:03}", GetDayOfYear()); break; - case 'm': Strings::FormatTo(result, "{:02}", GetMonth()); break; - case 'y': Strings::FormatTo(result, "{:02}", GetYear() % 100); break; - case 'Y': Strings::FormatTo(result, "{:04}", GetYear()); break; - case 'h': Strings::FormatTo(result, "{:02}", GetHour12()); break; - case 'H': Strings::FormatTo(result, "{:02}", GetHour()); break; - case 'M': Strings::FormatTo(result, "{:02}", GetMinute()); break; - case 'S': Strings::FormatTo(result, "{:02}", GetSecond()); break; - case 's': Strings::FormatTo(result, "{:03}", GetMillisecond()); break; + case 'd': FormatTo(result, "{:02}", GetDay()); break; + case 'D': FormatTo(result, "{:03}", GetDayOfYear()); break; + case 'm': FormatTo(result, "{:02}", GetMonth()); break; + case 'y': FormatTo(result, "{:02}", GetYear() % 100); break; + case 'Y': FormatTo(result, "{:04}", GetYear()); break; + case 'h': FormatTo(result, "{:02}", GetHour12()); break; + case 'H': FormatTo(result, "{:02}", GetHour()); break; + case 'M': FormatTo(result, "{:02}", GetMinute()); break; + case 'S': FormatTo(result, "{:02}", GetSecond()); break; + case 's': FormatTo(result, "{:03}", GetMillisecond()); break; default: result += *format; } } diff --git a/Tests/Containers/Arrays.spec.cpp b/Tests/Containers/Arrays.spec.cpp index 61e57a1a..a97fbc81 100644 --- a/Tests/Containers/Arrays.spec.cpp +++ b/Tests/Containers/Arrays.spec.cpp @@ -29,6 +29,20 @@ struct MoveType } }; +struct CopyType +{ + i32 value = 0; + + CopyType() = default; + CopyType(i32 value) : value(value) {} + CopyType(const CopyType& other) : value{other.value} {} + CopyType& operator=(const CopyType& other) + { + value = other.value; + return *this; + } +}; + go_bandit([]() { @@ -566,6 +580,70 @@ go_bandit([]() AssertThat(data[7], Equals(9)); }); + it("Can insert many values inline", [&]() + { + TArray data{1, 2}; + data.Insert(1, 3, 9); // More values than trailing elements + AssertThat(data.Size(), Equals(5)); + AssertThat(data[0], Equals(1)); + AssertThat(data[1], Equals(9)); + AssertThat(data[2], Equals(9)); + AssertThat(data[3], Equals(9)); + AssertThat(data[4], Equals(2)); + + data.Insert(0, 2, 7); // Fewer values than trailing elements + AssertThat(data.Size(), Equals(7)); + AssertThat(data[0], Equals(7)); + AssertThat(data[1], Equals(7)); + AssertThat(data[2], Equals(1)); + AssertThat(data[3], Equals(9)); + AssertThat(data[4], Equals(9)); + AssertThat(data[5], Equals(9)); + AssertThat(data[6], Equals(2)); + + data.Insert(5, 3, 4); // One more value than trailing elements + AssertThat(data.Size(), Equals(10)); + AssertThat(data[5], Equals(4)); + AssertThat(data[6], Equals(4)); + AssertThat(data[7], Equals(4)); + AssertThat(data[8], Equals(9)); + AssertThat(data[9], Equals(2)); + + data.Insert(3, 8, 6); // One more value than trailing elements + AssertThat(data.Size(), Equals(18)); + AssertThat(data[3], Equals(6)); + AssertThat(data[10], Equals(6)); + AssertThat(data[11], Equals(9)); + AssertThat(data[17], Equals(2)); + }); + + it("Can insert buffer inline", [&]() + { + TArray data{1, 2, 3}; + i32 src[]{4, 5, 6, 7}; + data.Insert(1, src, 4); // More values than trailing elements + AssertThat(data.Size(), Equals(7)); + AssertThat(data[0], Equals(1)); + AssertThat(data[1], Equals(4)); + AssertThat(data[4], Equals(7)); + AssertThat(data[5], Equals(2)); + AssertThat(data[6], Equals(3)); + }); + + it("Can insert many non trivial values inline", [&]() + { + TArray data; + data.Add(CopyType{1}); + data.Add(CopyType{2}); + data.Insert(1, 3, CopyType{9}); // More values than trailing elements + AssertThat(data.Size(), Equals(5)); + AssertThat(data[0].value, Equals(1)); + AssertThat(data[1].value, Equals(9)); + AssertThat(data[2].value, Equals(9)); + AssertThat(data[3].value, Equals(9)); + AssertThat(data[4].value, Equals(2)); + }); + it("Can insert moved value", [&]() { TArray data; @@ -806,6 +884,43 @@ go_bandit([]() AssertThat(data.Size(), Equals(7)); }); + it("Can slice", [&]() + { + TArray data{1, 2, 3, 4, 5}; + + auto mid = data.Slice(1, 2); // Elements 1 to 3 + AssertThat(mid.Size(), Equals(2)); + AssertThat(mid[0], Equals(2)); + AssertThat(mid[1], Equals(3)); + + auto tail = data.Slice(3, 100); // Clamped to available elements + AssertThat(tail.Size(), Equals(2)); + AssertThat(tail[0], Equals(4)); + AssertThat(tail[1], Equals(5)); + + auto none = data.Slice(2, 0); // Zero length + AssertThat(none.IsEmpty(), Is().True()); + + auto end = data.Slice(5, 2); // Offset clamped to size + AssertThat(end.IsEmpty(), Is().True()); + }); + + it("Can slice views", [&]() + { + TArray data{1, 2, 3, 4, 5}; + TView view = data; + + auto mid = view.Slice(2, 2); // Elements 2 to 4 + AssertThat(mid.Size(), Equals(2)); + AssertThat(mid[0], Equals(3)); + AssertThat(mid[1], Equals(4)); + + auto head = view.Slice(0, 3); + AssertThat(head.Size(), Equals(3)); + AssertThat(head[0], Equals(1)); + AssertThat(head[2], Equals(3)); + }); + describe("Iterate", []() { it("Can iterate empty", [&]() @@ -949,6 +1064,77 @@ go_bandit([]() AssertThat(source.Data(), Equals(nullptr)); AssertThat(target.Data(), Equals(sourceData)); }); + + it("Can bitwise operate", [&]() + { + BitArray a{true, true, false, false}; + BitArray b{true, false, true, false}; + + const BitArray anded = a & b; + const BitArray ored = a | b; + const BitArray xored = a ^ b; + const BitArray negged = ~a; + + // a & b: only bit 0 is set in both + AssertThat(anded.IsSet(0), Is().True()); + AssertThat(anded.IsSet(1), Is().False()); + AssertThat(anded.IsSet(2), Is().False()); + AssertThat(anded.IsSet(3), Is().False()); + + // a | b: all bits set + AssertThat(ored.IsSet(0), Is().True()); + AssertThat(ored.IsSet(1), Is().True()); + AssertThat(ored.IsSet(2), Is().True()); + AssertThat(ored.IsSet(3), Is().False()); + + // a ^ b: bits 1 and 2 differ + AssertThat(xored.IsSet(0), Is().False()); + AssertThat(xored.IsSet(1), Is().True()); + AssertThat(xored.IsSet(2), Is().True()); + AssertThat(xored.IsSet(3), Is().False()); + + // ~a: all bits flipped + AssertThat(negged.IsSet(0), Is().False()); + AssertThat(negged.IsSet(1), Is().False()); + AssertThat(negged.IsSet(2), Is().True()); + AssertThat(negged.IsSet(3), Is().True()); + + // Compound operations + BitArray compound = a; + compound &= b; + AssertThat(compound.IsSet(0), Is().True()); + AssertThat(compound.IsSet(1), Is().False()); + compound |= b; + AssertThat(compound.IsSet(2), Is().True()); + compound ^= b; + AssertThat(compound.IsSet(0), Is().False()); + AssertThat(compound.IsSet(2), Is().False()); + }); + + it("Can bitwise operate with different sizes", [&]() + { + BitArray small{false}; + BitArray big{true, true, true}; + + const BitArray anded = big & small; + AssertThat(anded.Size(), Equals(1)); + AssertThat(anded.IsSet(0), Is().False()); + + const BitArray ored = big | small; + AssertThat(ored.Size(), Equals(1)); // Sized to the smallest operand + AssertThat(ored.IsSet(0), Is().True()); + + // Only whole words are operated on. Bits past the smallest word count + // keep their value. Bits within a cleared word are cleared with it. + BitArray large{false}; + large.Resize(40, true); + large &= small; // small has a single (zeroed) word + AssertThat(large.Size(), Equals(40)); + AssertThat(large.IsSet(0), Is().False()); + AssertThat(large.IsSet(31), Is().False()); // Same word as bit 0 + AssertThat(large.IsSet(32), Is().True()); // Next word, unaffected + AssertThat(large.IsSet(39), Is().True()); + }); }); }); }); diff --git a/Tests/Core/String.spec.cpp b/Tests/Core/String.spec.cpp index a89ac433..16aa1884 100644 --- a/Tests/Core/String.spec.cpp +++ b/Tests/Core/String.spec.cpp @@ -1,90 +1,937 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #include -#include #include +#include +#include + +#include +#include using namespace snowhouse; using namespace bandit; using namespace p; +// Longer than the inline capacity, forcing heap allocations +static const StringView longText = "0123456789ABCDEFGHIJ0123456789ABC"; + go_bandit([]() { - describe("Core.String", []() + describe("Strings.String", []() { - it("Can assign from literal", [&]() + describe("Construction", []() + { + it("Can default construct", [&]() + { + String v{}; + AssertThat(v.size(), Equals(0u)); + AssertThat(v.empty(), Is().True()); + AssertThat(v.length(), Equals(0u)); + // c_str() must always return a valid pointer to a null terminator + AssertThat(v.c_str() != nullptr, Is().True()); + AssertThat(v.c_str()[0], Equals('\0')); + AssertThat(v.data() != nullptr, Is().True()); + AssertThat(v.data()[0], Equals('\0')); + }); + + it("Can construct from literal", [&]() + { + String v{"Kiwi"}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); + + it("Can construct from literal with count", [&]() + { + String v{"KiwiApple", 4}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); + + it("Can construct from count and char", [&]() + { + String v(5, 'x'); + AssertThat(v, Equals("xxxxx")); + AssertThat(v.size(), Equals(5u)); + }); + + it("Can construct from string view", [&]() + { + StringView str{"Kiwi"}; + String v{str}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); + + it("Can construct from string view with pos and count", [&]() + { + StringView str{"KiwiApple"}; + String v{str, 4, 5}; + AssertThat(v, Equals("Apple")); + }); + + it("Can construct from substring", [&]() + { + String str{"KiwiApple"}; + String v{str, 4}; + AssertThat(v, Equals("Apple")); + String v2{str, 4, 3}; + AssertThat(v2, Equals("App")); + }); + + it("Can construct from iterators", [&]() + { + std::string_view sv = "Kiwi"; + String v{sv.begin(), sv.end()}; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can construct from initializer list", [&]() + { + String v{'K', 'i', 'w', 'i'}; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can copy construct", [&]() + { + String v{"Kiwi"}; + String v2{v}; + AssertThat(v2, Equals("Kiwi")); + AssertThat(v, Equals("Kiwi")); + }); + + it("Can move construct", [&]() + { + String v{"Kiwi"}; + String v2{Move(v)}; + AssertThat(v2, Equals("Kiwi")); + // Moved-from string is valid and empty + AssertThat(v.size(), Equals(0u)); + AssertThat(v.empty(), Is().True()); + AssertThat(v.c_str()[0], Equals('\0')); + }); + }); + + describe("Assignment", []() + { + it("Can assign from literal", [&]() + { + String v; + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can copy assign", [&]() + { + String vKiwi{"Kiwi"}; + String vApple{"Apple"}; + String vCopy = vKiwi; + AssertThat(vCopy, Equals("Kiwi")); + vCopy = vApple; + AssertThat(vCopy, Equals("Apple")); + AssertThat(vCopy, Equals(vApple)); + }); + + it("Can move assign", [&]() + { + String vKiwi{"Kiwi"}; + String vApple{"Apple"}; + String vMove = Move(vKiwi); + AssertThat(vKiwi.size(), Equals(0u)); + AssertThat(vMove, Equals("Kiwi")); + vMove = Move(vApple); + AssertThat(vApple.size(), Equals(0u)); + AssertThat(vMove, Equals("Apple")); + }); + + it("Can assign char", [&]() + { + String v; + v = 'x'; + AssertThat(v, Equals("x")); + }); + + it("Can assign initializer list", [&]() + { + String v; + v = {'K', 'i', 'w', 'i'}; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can assign string view", [&]() + { + String v; + StringView sv{"Kiwi"}; + v = sv; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can assign", [&]() + { + String v; + v.assign("Kiwi"); + AssertThat(v, Equals("Kiwi")); + v.assign("KiwiApple", 4); + AssertThat(v, Equals("Kiwi")); + v.assign(3, 'x'); + AssertThat(v, Equals("xxx")); + String other{"Apple"}; + v.assign(other); + AssertThat(v, Equals("Apple")); + v.assign(other, 2, 2); + AssertThat(v, Equals("pl")); + StringView sv{"KiwiApple"}; + v.assign(sv, 4, 5); + AssertThat(v, Equals("Apple")); + v.assign({'a', 'b', 'c'}); + AssertThat(v, Equals("abc")); + }); + + it("Can self assign", [&]() + { + String v{"Kiwi"}; + const String& ref = v; + v = ref; + AssertThat(v, Equals("Kiwi")); + }); + + it("Can self assign substrings", [&]() + { + String v{longText}; + v.assign(v.c_str() + 10); + AssertThat(v, Equals("ABCDEFGHIJ0123456789ABC")); + }); + + it("Can self assign substrings with count", [&]() + { + String v{longText}; + v.assign(v.c_str() + 5, 10); + AssertThat(v, Equals("56789ABCDE")); + }); + }); + + describe("Element access", []() + { + it("Can index", [&]() + { + String v{"Kiwi"}; + AssertThat(v[0], Equals('K')); + AssertThat(v[3], Equals('i')); + v[0] = 'k'; + AssertThat(v, Equals("kiwi")); + // pos == size() returns reference to null char + AssertThat(v[4], Equals('\0')); + }); + + it("Can access at", [&]() + { + String v{"Kiwi"}; + AssertThat(v.at(0), Equals('K')); + AssertThat(v.at(3), Equals('i')); + v.at(0) = 'k'; + AssertThat(v, Equals("kiwi")); + }); + + it("Can access front and back", [&]() + { + String v{"Kiwi"}; + AssertThat(v.front(), Equals('K')); + AssertThat(v.back(), Equals('i')); + v.front() = 'P'; + v.back() = 's'; + AssertThat(v, Equals("Piws")); + }); + + it("Can retrieve data", [&]() + { + String v{"Kiwi"}; + AssertThat(v.data(), Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + AssertThat(strlen(v.data()), Equals(4u)); + }); + + it("Can convert to string view", [&]() + { + String v{"Kiwi"}; + StringView sv = v; + AssertThat(sv.size(), Equals(4u)); + AssertThat(sv, Equals(StringView{"Kiwi"})); + StringView wsv{v}; + AssertThat(wsv, Equals(StringView{"Kiwi"})); + }); + }); + + describe("Iterators", []() + { + it("Can iterate", [&]() + { + String v{"Kiwi"}; + u32 i = 0; + for (char c : v) + { + AssertThat(c, Equals("Kiwi"[i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); + + it("Can iterate const", [&]() + { + const String v{"Kiwi"}; + u32 i = 0; + for (char c : v) + { + AssertThat(c, Equals("Kiwi"[i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); + + it("Can iterate manually", [&]() + { + String v{"Kiwi"}; + auto it = v.begin(); + auto end = v.end(); + AssertThat(end - it, Equals(4)); + AssertThat(*it, Equals('K')); + AssertThat(it[2], Equals('w')); + ++it; + AssertThat(*it, Equals('i')); + it += 2; + AssertThat(*it, Equals('i')); + --it; + AssertThat(*it, Equals('w')); + AssertThat(it == v.begin() + 2, Is().True()); + AssertThat(it != v.begin(), Is().True()); + }); + + it("Can iterate reverse", [&]() + { + String v{"Kiwi"}; + u32 i = 0; + for (auto rit = v.rbegin(); rit != v.rend(); ++rit) + { + AssertThat(*rit, Equals("Kiwi"[3 - i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); + + it("Can iterate c-variants", [&]() + { + String v{"Kiwi"}; + AssertThat(*v.cbegin(), Equals('K')); + AssertThat(*(v.cend() - 1), Equals('i')); + AssertThat(*v.crbegin(), Equals('i')); + AssertThat(*(v.crend() - 1), Equals('K')); + }); + + it("Can mutate through iterators", [&]() + { + String v{"Kiwi"}; + std::transform(v.begin(), v.end(), v.begin(), [](char c) + { + return char(c + 1); + }); + AssertThat(v, Equals("Ljxj")); + }); + }); + + describe("Capacity", []() { - String v{"Kiwi"}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); + it("Can query size and length", [&]() + { + String v{"Kiwi"}; + AssertThat(v.size(), Equals(4u)); + AssertThat(v.length(), Equals(4u)); + AssertThat(v.empty(), Is().False()); + }); + + it("Has short string optimization", [&]() + { + String v{"Kiwi"}; + // Short strings must fit in the internal buffer + AssertThat(v.capacity() >= 15u, Is().True()); + AssertThat(v.capacity() <= 32u, Is().True()); + }); + + it("Can reserve", [&]() + { + String v; + v.reserve(100); + AssertThat(v.capacity() >= 100u, Is().True()); + AssertThat(v.size(), Equals(0u)); + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() >= 100u, Is().True()); + }); + + it("Can shrink to fit", [&]() + { + String v; + v.reserve(100); + v = "Kiwi"; + v.shrink_to_fit(); + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() >= 4u, Is().True()); + AssertThat(v.capacity() < 100u, Is().True()); + }); + + it("Has max size", [&]() + { + String v; + // Lengths are stored internally as i32 + AssertThat(v.max_size(), Equals(sizet(Limits::Max() - 1))); + }); }); - it("Can assign from string view", [&]() + describe("Modifiers", []() { - StringView str{"Kiwi"}; - String v{str}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); + it("Can clear", [&]() + { + String v{"Kiwi"}; + v.clear(); + AssertThat(v.empty(), Is().True()); + AssertThat(v.size(), Equals(0u)); + AssertThat(v.c_str()[0], Equals('\0')); + }); + + it("Can push and pop back", [&]() + { + String v{"Ki"}; + v.push_back('w'); + v.push_back('i'); + AssertThat(v, Equals("Kiwi")); + AssertThat(v.back(), Equals('i')); + v.pop_back(); + AssertThat(v, Equals("Kiw")); + v.pop_back(); + v.pop_back(); + v.pop_back(); + AssertThat(v, Equals("")); + AssertThat(v.empty(), Is().True()); + }); + + it("Can append", [&]() + { + String v{"Kiwi"}; + v.append("Apple"); + AssertThat(v, Equals("KiwiApple")); + v.append("Orange", 3); + AssertThat(v, Equals("KiwiAppleOra")); + v.append(3, '-'); + AssertThat(v, Equals("KiwiAppleOra---")); + String other{"End"}; + v.append(other); + AssertThat(v, Equals("KiwiAppleOra---End")); + v.append(other, 1, 2); + AssertThat(v, Equals("KiwiAppleOra---Endnd")); + StringView sv{"View"}; + v.append(sv); + AssertThat(v, Equals("KiwiAppleOra---EndndView")); + v.append(sv, 2, 2); + AssertThat(v, Equals("KiwiAppleOra---EndndViewew")); + v.append({'!', '?'}); + AssertThat(v, Equals("KiwiAppleOra---EndndViewew!?")); + }); + + it("Can append with operator+=", [&]() + { + String v{"Kiwi"}; + v += "Apple"; + AssertThat(v, Equals("KiwiApple")); + v += '!'; + AssertThat(v, Equals("KiwiApple!")); + String other{"End"}; + v += other; + AssertThat(v, Equals("KiwiApple!End")); + v += StringView{"View"}; + AssertThat(v, Equals("KiwiApple!EndView")); + v += {'a', 'b'}; + AssertThat(v, Equals("KiwiApple!EndViewab")); + }); + + it("Can insert", [&]() + { + String v{"KiwiApple"}; + v.insert(4, "Orange"); + AssertThat(v, Equals("KiwiOrangeApple")); + v.insert(0, "-"); + AssertThat(v, Equals("-KiwiOrangeApple")); + v.insert(v.size(), "!"); + AssertThat(v, Equals("-KiwiOrangeApple!")); + v.insert(0, 3, '='); + AssertThat(v, Equals("===-KiwiOrangeApple!")); + String other{"XX"}; + v.insert(3, other); + AssertThat(v, Equals("===XX-KiwiOrangeApple!")); + StringView sv{"YY"}; + v.insert(5, sv); + AssertThat(v, Equals("===XXYY-KiwiOrangeApple!")); + v.insert(0, 2, 'Z'); + AssertThat(v, Equals("ZZ===XXYY-KiwiOrangeApple!")); + }); + + it("Can insert with iterator", [&]() + { + String v{"Kiwi"}; + auto it = v.insert(v.begin() + 2, '-'); + AssertThat(*it, Equals('-')); + AssertThat(v, Equals("Ki-wi")); + v.insert(v.end(), 3, '!'); + AssertThat(v, Equals("Ki-wi!!!")); + String other{"AB"}; + v.insert(v.begin(), other.begin(), other.end()); + AssertThat(v, Equals("ABKi-wi!!!")); + v.insert(v.begin() + 2, {'x', 'y'}); + AssertThat(v, Equals("ABxyKi-wi!!!")); + }); + + it("Can erase", [&]() + { + String v{"KiwiApple"}; + v.erase(4, 5); + AssertThat(v, Equals("Kiwi")); + v.erase(2); + AssertThat(v, Equals("Ki")); + v.erase(0, 1); + AssertThat(v, Equals("i")); + v.erase(0, 10); + AssertThat(v, Equals("")); + }); + + it("Can erase with iterator", [&]() + { + String v{"Kiwi"}; + auto it = v.erase(v.begin()); + AssertThat(*it, Equals('i')); + AssertThat(v, Equals("iwi")); + v.erase(v.begin() + 1, v.end()); + AssertThat(v, Equals("i")); + }); + + it("Can replace", [&]() + { + String v{"KiwiApple"}; + v.replace(0, 4, "Orange"); + AssertThat(v, Equals("OrangeApple")); + v.replace(0, 6, "X"); + AssertThat(v, Equals("XApple")); + v.replace(v.size() - 3, 3, "Z"); + AssertThat(v, Equals("XApZ")); + String other{"Kiwi"}; + v.replace(0, 4, other); + AssertThat(v, Equals("Kiwi")); + StringView sv{"Two"}; + v.replace(0, 4, sv); + AssertThat(v, Equals("Two")); + v.replace(0, 3, 2, 'y'); + AssertThat(v, Equals("yy")); + }); + + it("Can replace with iterators", [&]() + { + String v{"KiwiApple"}; + v.replace(v.begin(), v.begin() + 4, "Orange"); + AssertThat(v, Equals("OrangeApple")); + }); + + it("Can resize", [&]() + { + String v{"Kiwi"}; + v.resize(2); + AssertThat(v, Equals("Ki")); + v.resize(4); + AssertThat(v.size(), Equals(4u)); + AssertThat(v[2], Equals('\0')); + AssertThat(v[3], Equals('\0')); + v.resize(6, 'x'); + AssertThat(v[4], Equals('x')); + AssertThat(v[5], Equals('x')); + AssertThat(v.size(), Equals(6u)); + }); + + it("Can swap", [&]() + { + String a{"Kiwi"}; + String b{"Apple"}; + a.swap(b); + AssertThat(a, Equals("Apple")); + AssertThat(b, Equals("Kiwi")); + }); + + it("Can append from self", [&]() + { + String v{longText}; + v.append(v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText})); + }); + + it("Can append self substring", [&]() + { + String v{longText}; + v.append(v.c_str() + 5); + AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(5)})); + }); + + it("Can insert from self", [&]() + { + String v{longText}; + v.insert(0, v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText})); + }); + + it("Can insert self substring", [&]() + { + String v{longText}; + v.insert(4, v.c_str() + 5); + AssertThat( + v, Equals(std::string{longText.substr(0, 4)} + std::string{longText.substr(5)} + + std::string{longText.substr(4)})); + }); + + it("Can replace with self", [&]() + { + String v{longText}; + v.replace(0, 4, v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(4)})); + }); + + it("Can replace self substring with count", [&]() + { + String v{longText}; + v.replace(5, 10, v.c_str() + 2, 5); + AssertThat(v, Equals(std::string{longText.substr(0, 5)} + "23456" + + std::string{longText.substr(15)})); + }); }); - it("Can copy empty", [&]() + describe("Operations", []() { - String str{}; - String str2{" "}; - AssertThat(str.empty(), Equals(true)); - AssertThat(str2.empty(), Equals(false)); - str2 = str; - AssertThat(str2.empty(), Equals(true)); + it("Can get substr", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.substr(), Equals("KiwiApple")); + AssertThat(v.substr(4), Equals("Apple")); + AssertThat(v.substr(4, 3), Equals("App")); + AssertThat(v.substr(0, 100), Equals("KiwiApple")); + }); + + it("Can copy out", [&]() + { + String v{"KiwiApple"}; + char buffer[16]{}; + const auto count = v.copy(buffer, 4, 4); + AssertThat(count, Equals(4u)); + AssertThat(buffer, Equals("Appl")); + buffer[count] = '\0'; + }); + + it("Can compare", [&]() + { + String v{"Kiwi"}; + String other{"Kiwi"}; + String apple{"Apple"}; + AssertThat(v.compare(other), Equals(0)); + AssertThat(v.compare(apple) > 0, Is().True()); + AssertThat(apple.compare(v) < 0, Is().True()); + AssertThat(v.compare("Kiwi"), Equals(0)); + AssertThat(v.compare("Kiwi2") < 0, Is().True()); + AssertThat(v.compare(StringView{"Kiwi"}), Equals(0)); + AssertThat(v.compare(0, 2, String{"Ki"}), Equals(0)); + AssertThat(v.compare(2, 2, String{"wi"}), Equals(0)); + }); + + it("Can check prefix and suffix", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.starts_with("Kiwi"), Is().True()); + AssertThat(v.starts_with('K'), Is().True()); + AssertThat(v.starts_with(StringView{"Ki"}), Is().True()); + AssertThat(v.starts_with("Apple"), Is().False()); + AssertThat(v.ends_with("Apple"), Is().True()); + AssertThat(v.ends_with('e'), Is().True()); + AssertThat(v.ends_with(StringView{"le"}), Is().True()); + AssertThat(v.ends_with("Kiwi"), Is().False()); + }); + + it("Can check contains", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.contains("wiA"), Is().True()); + AssertThat(v.contains('A'), Is().True()); + AssertThat(v.contains(StringView{"zzz"}), Is().False()); + AssertThat(v.contains('z'), Is().False()); + }); + + it("Can find", [&]() + { + String v{"KiwiKiwi"}; + AssertThat(v.find("Kiwi"), Equals(0u)); + AssertThat(v.find("Kiwi", 1), Equals(4u)); + AssertThat(v.find("Kiwi", 5), Equals(String::npos)); + AssertThat(v.find('i'), Equals(1u)); + AssertThat(v.find('i', 6), Equals(7u)); + AssertThat(v.find('z'), Equals(String::npos)); + AssertThat(v.find(String{"Kiwi"}), Equals(0u)); + AssertThat(v.find(StringView{"Kiwi"}), Equals(0u)); + }); + + it("Can rfind", [&]() + { + String v{"KiwiKiwi"}; + AssertThat(v.rfind("Kiwi"), Equals(4u)); + AssertThat(v.rfind("Kiwi", 3), Equals(0u)); + AssertThat(v.rfind('i'), Equals(7u)); + AssertThat(v.rfind('i', 5), Equals(5u)); + AssertThat(v.rfind('z'), Equals(String::npos)); + AssertThat(v.rfind(String{"Kiwi"}), Equals(4u)); + AssertThat(v.rfind(StringView{"Kiwi"}), Equals(4u)); + }); + + it("Can find first of", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.find_first_of("pl"), Equals(5u)); + AssertThat(v.find_first_of("pl", 6), Equals(6u)); + AssertThat(v.find_first_of('z'), Equals(String::npos)); + AssertThat(v.find_first_of("xyz"), Equals(String::npos)); + AssertThat(v.find_first_of(StringView{"Ap"}), Equals(4u)); + }); + + it("Can find last of", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.find_last_of("pl"), Equals(7u)); + AssertThat(v.find_last_of("pl", 6), Equals(6u)); + AssertThat(v.find_last_of('z'), Equals(String::npos)); + AssertThat(v.find_last_of(StringView{"Ap"}), Equals(6u)); + }); + + it("Can find first not of", [&]() + { + String v{"aaab"}; + AssertThat(v.find_first_not_of("a"), Equals(3u)); + AssertThat(v.find_first_not_of("ab"), Equals(String::npos)); + AssertThat(v.find_first_not_of('a'), Equals(3u)); + AssertThat(v.find_first_not_of("ab", 3), Equals(String::npos)); + }); + + it("Can find last not of", [&]() + { + String v{"baaa"}; + AssertThat(v.find_last_not_of("a"), Equals(0u)); + AssertThat(v.find_last_not_of("ab"), Equals(String::npos)); + AssertThat(v.find_last_not_of('a'), Equals(0u)); + AssertThat(v.find_last_not_of("ab", 0), Equals(String::npos)); + }); + + it("Has npos", [&]() + { + AssertThat(String::npos, Equals(sizet(-1))); + AssertThat(StringView::npos, Equals(String::npos)); + }); }); - it("Can retrieve string data", [&]() + describe("Operators", []() { - String v{"Kiwi"}; - AssertThat(v.data(), Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); - String v2{}; - AssertThat(v2.size(), Equals(0)); + it("Can concatenate", [&]() + { + String a{"Kiwi"}; + String b{"Apple"}; + AssertThat(a + b, Equals("KiwiApple")); + AssertThat(a + "X", Equals("KiwiX")); + AssertThat("X" + a, Equals("XKiwi")); + AssertThat(a + '!', Equals("Kiwi!")); + AssertThat('!' + a, Equals("!Kiwi")); + AssertThat(a + StringView{"V"}, Equals("KiwiV")); + AssertThat(StringView{"V"} + a, Equals("VKiwi")); + }); + + it("Can chain concatenate", [&]() + { + String a{"Kiwi"}; + String result = a + " " + "Apple" + '!'; + AssertThat(result, Equals("Kiwi Apple!")); + }); + + it("Can compare with other types", [&]() + { + String v{"Kiwi"}; + AssertThat(v == String{"Kiwi"}, Is().True()); + AssertThat(v != String{"Apple"}, Is().True()); + AssertThat(v == "Kiwi", Is().True()); + AssertThat(v != "Apple", Is().True()); + AssertThat("Kiwi" == v, Is().True()); + AssertThat("Apple" != v, Is().True()); + AssertThat(v < "Lime", Is().True()); + AssertThat("Lime" > v, Is().True()); + AssertThat(v <= String{"Kiwi"}, Is().True()); + AssertThat(v >= String{"Kiwi"}, Is().True()); + AssertThat(v == StringView{"Kiwi"}, Is().True()); + AssertThat(StringView{"Kiwi"} == v, Is().True()); + AssertThat(v != StringView{"Apple"}, Is().True()); + AssertThat(StringView{"Apple"} != v, Is().True()); + AssertThat(v < StringView{"Lime"}, Is().True()); + AssertThat(StringView{"Lime"} > v, Is().True()); + }); + + it("Can three-way compare", [&]() + { + String a{"Kiwi"}; + String b{"Lime"}; + AssertThat((a <=> b) < 0, Is().True()); + AssertThat((b <=> a) > 0, Is().True()); + AssertThat((a <=> String{"Kiwi"}) == 0, Is().True()); + AssertThat((a <=> "Kiwi") == 0, Is().True()); + }); }); - it("Can compare", [&]() + describe("Memory", []() { - String vKiwi{"Kiwi"}; - String vKiwi2{"Kiwi"}; - String vApple{"Apple"}; - AssertThat(vKiwi, Equals(vKiwi2)); - AssertThat(vKiwi, !Equals(vApple)); + it("Keeps data valid when growing", [&]() + { + String v; + for (char c = 'a'; c <= 'z'; ++c) + { + v.push_back(c); + } + AssertThat(v.size(), Equals(26u)); + AssertThat(v, Equals("abcdefghijklmnopqrstuvwxyz")); + AssertThat(v.c_str()[26], Equals('\0')); + }); + + it("Can reuse capacity", [&]() + { + String v; + v.reserve(1000); + const auto cap = v.capacity(); + for (u32 i = 0; i < 100; ++i) + { + v.assign("KiwiAppleOrangeBanana"); + v.clear(); + } + AssertThat(v.capacity(), Equals(cap)); + }); + + it("Is valid after move assignment", [&]() + { + String a{"Kiwi"}; + String b; + b = Move(a); + AssertThat(b, Equals("Kiwi")); + a = "Reused"; + AssertThat(a, Equals("Reused")); + }); }); - it("Can copy", [&]() + describe("Format & Hash", []() { - String vKiwi{"Kiwi"}; - String vApple{"Apple"}; - String vCopy = vKiwi; - AssertThat(vCopy, Equals("Kiwi")); - AssertThat(vCopy, Equals(vKiwi)); - AssertThat(vCopy, !Equals(vApple)); - vCopy = vApple; - AssertThat(vCopy, Equals("Apple")); - AssertThat(vCopy, !Equals(vKiwi)); - AssertThat(vCopy, Equals(vApple)); + it("Can be formatted", [&]() + { + String v{"Kiwi"}; + AssertThat(std::format("{}", v), Equals("Kiwi")); + AssertThat(Format("{}-{}", v, 5), Equals("Kiwi-5")); + String out; + FormatTo(out, "{}!", v); + AssertThat(out, Equals("Kiwi!")); + }); + + it("Can be hashed", [&]() + { + String v{"Kiwi"}; + AssertThat(GetHash(v), Equals(GetStringHash("Kiwi"))); + AssertThat(GetHash(StringView{"Kiwi"}), Equals(GetHash(v))); + }); }); - it("Can move", [&]() + describe("Arena", []() { - String vKiwi{"Kiwi"}; - String vApple{"Apple"}; - String vMove = Move(vKiwi); - AssertThat(vKiwi.size(), Equals(0)); - AssertThat(vMove, Equals("Kiwi")); - vMove = Move(vApple); - AssertThat(vApple.size(), Equals(0)); - AssertThat(vMove, Equals("Apple")); + const char* longText = "This string is long enough to exceed the inline capacity"; + + it("Can default construct on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena}; + AssertThat(v.empty(), Is().True()); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + // Short strings still use the inline buffer + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() <= 32u, Is().True()); + }); + + it("Can allocate on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena, longText}; + AssertThat(v, Equals(longText)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + // Long strings must allocate on the arena, not the current arena + AssertThat(v.capacity() >= v.size(), Is().True()); + }); + + it("Can construct with count and char on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena, 64, 'x'}; + AssertThat(v.size(), Equals(64u)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + }); + + it("Can copy into an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String original{longText}; + String v{arena, original}; + AssertThat(v, Equals(original)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + }); + + it("Keeps its arena when assigned", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena}; + v.assign(longText); + v.append(" with some extra content to force a reallocation"); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + AssertThat(v.starts_with("This string"), Is().True()); + }); }); - describe("Strings", []() + describe("Strings helpers", []() { + it("RemoveFromStart", [&]() + { + String v{"KiwiApple"}; + Strings::RemoveFromStart(v, 4); + AssertThat(v, Equals("Apple")); + Strings::RemoveFromStart(v, 100); + AssertThat(v.empty(), Is().True()); + }); + + it("RemoveFromEnd", [&]() + { + String v{"KiwiApple"}; + Strings::RemoveFromEnd(v, 5); + AssertThat(v, Equals("Kiwi")); + Strings::RemoveFromEnd(v, StringView{"wi"}); + AssertThat(v, Equals("Ki")); + Strings::RemoveFromEnd(v, 100); + AssertThat(v.empty(), Is().True()); + }); + + it("RemoveCharFromEnd", [&]() + { + String v{"Kiwi!"}; + AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().True()); + AssertThat(v, Equals("Kiwi")); + AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().False()); + AssertThat(v, Equals("Kiwi")); + }); + it("ToSentenceCase", [&]() { AssertThat(Strings::ToSentenceCase(""), Equals("")); @@ -98,14 +945,14 @@ go_bandit([]() { TString utf16string{0x41, 0x0448, 0x65e5, 0xd834, 0xdd1e}; TString u = Strings::Convert>(utf16string); - AssertThat(u.size(), Equals(10)); + AssertThat(u.size(), Equals(10u)); }); it("Convert u8 to u16", [&]() { TString utf8_with_surrogates = "\xe6\x97\xa5\xd1\x88\xf0\x9d\x84\x9e"; TString utf16result = Strings::Convert>(utf8_with_surrogates); - AssertThat(utf16result.size(), Equals(4)); + AssertThat(utf16result.size(), Equals(4u)); AssertThat(utf16result[2] == 0xd834, Is().True()); AssertThat(utf16result[3] == 0xdd1e, Is().True()); }); @@ -113,13 +960,13 @@ go_bandit([]() { TString utf32string = {0x448, 0x65E5, 0x10346}; TString utf8result = Strings::Convert>(utf32string); - AssertThat(utf8result.size(), Equals(9)); + AssertThat(utf8result.size(), Equals(9u)); }); it("Convert u8 to u32", [&]() { TString twochars = "\xe6\x97\xa5\xd1\x88"; TString utf32result = Strings::Convert>(twochars); - AssertThat(utf32result.size(), Equals(2)); + AssertThat(utf32result.size(), Equals(2u)); }); }); }); diff --git a/Tests/Core/StringView.spec.cpp b/Tests/Core/StringView.spec.cpp index 938874e6..359321c9 100644 --- a/Tests/Core/StringView.spec.cpp +++ b/Tests/Core/StringView.spec.cpp @@ -1,7 +1,7 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #include -#include +#include #include diff --git a/Tests/Memory/MemoryStats.spec.cpp b/Tests/Memory/MemoryStats.spec.cpp index 2e390734..86005ad6 100644 --- a/Tests/Memory/MemoryStats.spec.cpp +++ b/Tests/Memory/MemoryStats.spec.cpp @@ -213,6 +213,113 @@ go_bandit([]() AssertThat(AllocCount(s), Is().EqualTo(1)); }); + it("CheckLeaks with null name does not crash", [&]() + { + { + // detectLeaks defaults to true and name defaults to null. + MemoryStats s; + s.Add((void*)0x1000, 64); + s.CollectStats(); + // Destructor runs CheckLeaks with leaks and a null name. + } + }); + + it("live and frees bitsets match events", [&]() + { + MemoryStats s; + s.detectLeaks = false; + // allocs: 2 live, 1 matched. frees: 2 (one matches, one stray). + s.Add((void*)0x1000, 64); + s.Add((void*)0x2000, 32); + s.Add((void*)0x3000, 16); + s.Remove((void*)0x3000, 16); + s.Remove((void*)0xDEAD, 16); + s.CollectStats(); + + AssertThat(s.events.Size(), Is().EqualTo(5)); + AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); + AssertThat(s.frees.IsSet(3), Is().EqualTo(true)); + AssertThat(s.frees.IsSet(4), Is().EqualTo(true)); + AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); + AssertThat(s.live.IsSet(0), Is().EqualTo(true)); + AssertThat(s.live.IsSet(1), Is().EqualTo(true)); + AssertThat(s.live.IsSet(2), Is().EqualTo(false)); + AssertThat(s.live.IsSet(3), Is().EqualTo(false)); + AssertThat(s.live.IsSet(4), Is().EqualTo(false)); + + // Re-collecting must rebuild bitsets identically. + s.CollectStats(); + AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); + AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); + }); + + it("Alternating instances on one thread", [&]() + { + // Exercises thread context reuse when the owner switches. + MemoryStats a; + MemoryStats b; + a.detectLeaks = false; + b.detectLeaks = false; + + a.Add((void*)0x1000, 64); + b.Add((void*)0x2000, 32); + a.Add((void*)0x3000, 16); + b.Remove((void*)0x2000, 32); + + a.CollectStats(); + b.CollectStats(); + + AssertThat(a.used, Is().EqualTo(64 + 16)); + AssertThat(AllocCount(a), Is().EqualTo(2)); + AssertThat(FreeCount(a), Is().EqualTo(0)); + AssertThat(b.used, Is().EqualTo(0)); + AssertThat(AllocCount(b), Is().EqualTo(1)); + AssertThat(FreeCount(b), Is().EqualTo(1)); + }); + + it("Add after Release works", [&]() + { + MemoryStats s; + s.detectLeaks = false; + s.Add((void*)0x1000, 64); + s.Release(); + AssertThat(AllocCount(s), Is().EqualTo(0)); + + s.Add((void*)0x2000, 32); + s.CollectStats(); + AssertThat(s.used, Is().EqualTo(32)); + AssertThat(s.totalAllocated, Is().EqualTo(32)); + AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(s.live.CountSetBits(), Is().EqualTo(1)); + }); + + it("Frees across collects unmark live allocs (LIFO, duplicate keys)", [&]() + { + MemoryStats s; + s.detectLeaks = false; + + // Collect 1: two allocs sharing a key (same ptr and size). + s.Add((void*)0x1000, 64); + s.Add((void*)0x1000, 64); + s.CollectStats(); + AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); + AssertThat(s.live.IsSet(0), Is().EqualTo(true)); + AssertThat(s.live.IsSet(1), Is().EqualTo(true)); + + // Collect 2: one free must unmark the latest alloc (LIFO). + s.Remove((void*)0x1000, 64); + s.CollectStats(); + AssertThat(s.live.CountSetBits(), Is().EqualTo(1)); + AssertThat(s.live.IsSet(0), Is().EqualTo(true)); + AssertThat(s.live.IsSet(1), Is().EqualTo(false)); + + // Collect 3: second free pops the olderLive spill entry. + s.Remove((void*)0x1000, 64); + s.CollectStats(); + AssertThat(s.live.CountSetBits(), Is().EqualTo(0)); + AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); + }); + it("Ignores null ptr in Remove", [&]() { MemoryStats s; @@ -323,19 +430,22 @@ go_bandit([]() } s.CollectStats(); AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(s.live.CountSetBits(), Is().EqualTo(N)); for (sizet i = 0; i < N / 2; ++i) { s.Remove(&buf[i * 8], 8); } s.CollectStats(); AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(s.live.CountSetBits(), Is().EqualTo(N / 2)); + AssertThat(s.frees.CountSetBits(), Is().EqualTo(N / 2)); }); }); - describe("SPSC stress", [&]() + describe("Multithreading", [&]() { - it("Producer and consumer work concurrently", [&]() + it("One thread adds, another collects", [&]() { MemoryStats s; const sizet N = 1000; @@ -375,12 +485,8 @@ go_bandit([]() // Suppress leak warnings at destruction (test buffers are stack). s.Release(); }); - }); - - describe("MPMC stress", [&]() - { - it("Many producers push, one consumer collects", [&]() + it("Many threads add, then collects", [&]() { MemoryStats s; const sizet N_PER_THREAD = 1000; @@ -439,7 +545,7 @@ go_bandit([]() s.Release(); }); - it("Producers push and free, one consumer collects", [&]() + it("Many threads add and remove, then collects", [&]() { MemoryStats s; const sizet N_PER_THREAD = 1000; diff --git a/Tests/Reflection/TypeName.spec.cpp b/Tests/Reflection/TypeName.spec.cpp index 84f86f8e..73ec0b94 100644 --- a/Tests/Reflection/TypeName.spec.cpp +++ b/Tests/Reflection/TypeName.spec.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/Tests/Serialization/Json.spec.cpp b/Tests/Serialization/Json.spec.cpp index e85fb4a8..372fedaf 100644 --- a/Tests/Serialization/Json.spec.cpp +++ b/Tests/Serialization/Json.spec.cpp @@ -181,9 +181,9 @@ go_bandit([]() it("Can read i16 values", [&]() { // Test inbounds and out of bounds values - JsonFormatReader reader{Strings::Format( - "{{\"a\":{},\"b\":{},\"c\":{},\"d\":{}}}", Limits::Max(), - Limits::Lowest(), Limits::Max(), Limits::Lowest())}; + JsonFormatReader reader{ + Format("{{\"a\":{},\"b\":{},\"c\":{},\"d\":{}}}", Limits::Max(), + Limits::Lowest(), Limits::Max(), Limits::Lowest())}; Reader ct = reader; ct.BeginObject(); i16 value = 0; @@ -199,7 +199,7 @@ go_bandit([]() it("Can read u16 values", [&]() { - JsonFormatReader reader{Strings::Format("{{\"a\":{},\"b\":{},\"c\":{}}}", + JsonFormatReader reader{Format("{{\"a\":{},\"b\":{},\"c\":{}}}", Limits::Max(), Limits::Lowest(), -32)}; Reader ct = reader; ct.BeginObject(); @@ -215,9 +215,9 @@ go_bandit([]() it("Can read i32 values", [&]() { // Test inbounds and out of bounds values - JsonFormatReader reader{Strings::Format( - "{{\"a\":{},\"b\":{},\"c\":{},\"d\":{}}}", Limits::Max(), - Limits::Lowest(), Limits::Max(), Limits::Lowest())}; + JsonFormatReader reader{ + Format("{{\"a\":{},\"b\":{},\"c\":{},\"d\":{}}}", Limits::Max(), + Limits::Lowest(), Limits::Max(), Limits::Lowest())}; Reader ct = reader; ct.BeginObject(); i32 value = 0; @@ -233,7 +233,7 @@ go_bandit([]() it("Can read u32 values", [&]() { - JsonFormatReader reader{Strings::Format("{{\"a\":{},\"b\":{},\"c\":{}}}", + JsonFormatReader reader{Format("{{\"a\":{},\"b\":{},\"c\":{}}}", Limits::Max(), Limits::Lowest(), -32)}; Reader ct = reader; ct.BeginObject(); From 0824fda9ad5c77e7038113a3fa5daf97c55784c9 Mon Sep 17 00:00:00 2001 From: muit Date: Mon, 31 Aug 2026 11:28:36 +0200 Subject: [PATCH 02/15] Optimized memory stats --- Include/Misc/PipeDebug.h | 2 - Include/Pipe/Memory/MemoryStats.h | 68 +- Src/Memory/MemoryStats.cpp | 272 +++-- Tests/Core/String.spec.cpp | 1673 +++++++++++++++-------------- Tests/Core/StringView.spec.cpp | 171 +-- Tests/Memory/MemoryStats.spec.cpp | 16 +- 6 files changed, 1176 insertions(+), 1026 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index ea18ee1b..e26ca876 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -277,7 +277,6 @@ namespace p Tag typeName; const TArray* events = nullptr; const BitArray* live = nullptr; - const BitArray* frees = nullptr; }; TArray snapshots; }; @@ -2220,7 +2219,6 @@ namespace p snapshot.used = stats->used; snapshot.events = &stats->events; snapshot.live = &stats->live; - snapshot.frees = &stats->frees; } memoryDbg.snapshots.Add(snapshot); diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index 461a484e..cb013219 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -4,7 +4,6 @@ #include "Pipe/Core/EnumFlags.h" #include "Pipe/Core/Hash.h" -#include "Pipe/Core/Map.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" #include "PipeContainers.h" @@ -35,10 +34,10 @@ namespace p public: MemoryStatsEvent() = default; - MemoryStatsEvent(u8* ptr, sizet size) : ptr{ptr}, size{size} {} + MemoryStatsEvent(void* ptr, sizet size) : ptr{static_cast(ptr)}, size{size} {} // Construct with size and flag (for the free ring). - MemoryStatsEvent(u8* ptr, sizet size, MemoryStatsEventFlags flags) - : ptr{ptr}, size{size | *flags} + MemoryStatsEvent(void* ptr, sizet size, MemoryStatsEventFlags flags) + : ptr{static_cast(ptr)}, size{size | *flags} {} u8* GetPtr() const @@ -90,21 +89,45 @@ namespace p // CollectStats, had not been matched by a corresponding free. mutable BitArray live; - // Bit i set when events[i] is a free event. Cached so consumers - // can classify events via a bit-test instead of MemoryStatsEvent. - mutable BitArray frees; - mutable sizet used = 0; mutable sizet totalAllocated = 0; private: + // Open-addressed linear-probe map from event hash to the newest + // unmatched alloc event index for that hash. Keys are pre-mixed + // hashes (from GetHash), indexed directly without re-hashing. + // No per-insert allocation; grows at 75% load. + class LiveIndex + { + static constexpr i32 Empty = -1; + static constexpr i32 Tombstone = -2; + + Arena* arena = nullptr; + TArray keys; + // Parallel to keys: the node index, or Empty/Tombstone. + TArray nodes; + u64 mask = 0; + i32 count = 0; + i32 tombCount = 0; + + void Grow(); + + public: + explicit LiveIndex(Arena& inArena) : arena{&inArena}, keys{inArena}, nodes{inArena} {} + + i32* Find(u64 hash); + i32* FindOrInsert(u64 hash, i32 node); + void EraseAt(i32* node); + void Clear(); + }; + // --- Incremental CollectStats state (consumer thread only) --- // Events are append-only, so classification of old events never // changes. Only events past collectedEvents are classified per call. mutable i32 collectedEvents = 0; - // Head of the unmatched-alloc chain per event key. Chains are + // Newest unmatched alloc index per event key. Chains are // intrusively linked through prevLiveIdx, newest first. - mutable TMap liveIdx; + mutable LiveIndex liveIdx; // For each alloc event index, the previous unmatched alloc index // sharing the same key (NO_INDEX if none). Consumed on free. mutable TArray prevLiveIdx; @@ -114,8 +137,20 @@ namespace p MemoryStats(); ~MemoryStats(); - void Add(void* ptr, sizet size); - void Remove(void* ptr, sizet size); + // Tracks an allocation. Never blocks; writes one 16B event. + inline void Add(void* ptr, sizet size) + { + PushEvent(MemoryStatsEvent{ptr, size}); + } + + // Tracks a free. Ignored for null pointers. + inline void Remove(void* ptr, sizet size) + { + if (ptr) + { + PushEvent(MemoryStatsEvent{ptr, size, MemoryStatsEventFlags::IsFree}); + } + } // Empty memory stats. No diagnostics. void Release(); @@ -133,7 +168,7 @@ namespace p // from head. The chain is append-only. struct Chunk { - static constexpr u32 capacity = 4096; + static constexpr u32 capacity = 1024; std::atomic next{nullptr}; MemoryStatsEvent slots[capacity]; std::atomic writeIdx{0}; @@ -144,7 +179,10 @@ namespace p // consumer advances to next chunk (relaxed) as it drains. std::atomic head{nullptr}; // Producer's current chunk. Consumer does not access. - Chunk* tail = nullptr; + Chunk* tail = nullptr; + // Drained chunk returned by the consumer for producer reuse. + std::atomic spare{nullptr}; + MemoryStats* owner = nullptr; ThreadContext* nextCtx = nullptr; }; @@ -153,7 +191,7 @@ namespace p ThreadContext* GetOrCreateContext(); - void PushEvent(void* ptr, sizet size, bool isFree); + void PushEvent(const MemoryStatsEvent& ev); }; P_API Arena& GetStatsArena(); diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index f7455189..39bd90b3 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -50,6 +50,132 @@ namespace p } + // --------------------------------------------------------------------------- + // MemoryStats::LiveIndex + // --------------------------------------------------------------------------- + + void MemoryStats::LiveIndex::Grow() + { + TArray oldKeys = Move(keys); + TArray oldNodes = Move(nodes); + const i32 newCap = oldKeys.IsEmpty() ? 64 : oldKeys.Size() * 2; + + keys = TArray{*arena}; + nodes = TArray{*arena}; + keys.AddUninitialized(newCap); + nodes.AddUninitialized(newCap); + for (i32 i = 0; i < newCap; ++i) + { + keys[i] = 0; + nodes[i] = Empty; + } + mask = u64(newCap - 1); + count = 0; + tombCount = 0; + + for (i32 i = 0; i < oldKeys.Size(); ++i) + { + if (oldNodes[i] >= 0) + { + // Insert without grow or duplicates + const u64 hash = oldKeys[i]; + u64 idx = hash & mask; + while (nodes[idx] != Empty) + { + idx = (idx + 1) & mask; + } + keys[idx] = hash; + nodes[idx] = oldNodes[i]; + ++count; + } + } + } + + i32* MemoryStats::LiveIndex::Find(u64 hash) + { + if (count + tombCount <= 0) + { + return nullptr; + } + u64 idx = hash & mask; + while (true) + { + const i32 node = nodes[idx]; + if (node == Empty) + { + return nullptr; + } + if (node != Tombstone && keys[idx] == hash) + { + return &nodes[idx]; + } + idx = (idx + 1) & mask; + } + } + + i32* MemoryStats::LiveIndex::FindOrInsert(u64 hash, i32 node) + { + // Grow up front when the table is empty or this insert would exceed + // load. A probe on an empty table would read out of bounds, and + // growing after a probe would invalidate its result. The rare cost + // is growing on a find-hit when load is already at the limit. + if ((count + tombCount + 1) * 4 > i64(keys.Size()) * 3) + { + Grow(); + } + + constexpr u64 noTomb = ~u64{0}; + u64 tombstone = noTomb; + u64 idx = hash & mask; + while (true) + { + const i32 n = nodes[idx]; + if (n == Empty) + { + break; + } + if (n == Tombstone) + { + if (tombstone == noTomb) + { + tombstone = idx; + } + } + else if (keys[idx] == hash) + { + return &nodes[idx]; + } + idx = (idx + 1) & mask; + } + + if (tombstone != noTomb) + { + idx = tombstone; + --tombCount; + } + keys[idx] = hash; + nodes[idx] = node; + ++count; + return &nodes[idx]; + } + + void MemoryStats::LiveIndex::EraseAt(i32* node) + { + *node = Tombstone; + --count; + ++tombCount; + } + + void MemoryStats::LiveIndex::Clear() + { + keys.Clear(); + nodes.Clear(); + mask = 0; + count = 0; + tombCount = 0; + } + + // --------------------------------------------------------------------------- // MemoryStats // --------------------------------------------------------------------------- @@ -72,7 +198,6 @@ namespace p MemoryStats::MemoryStats() : events{GetStatsArena()} , live{GetStatsArena()} - , frees{GetStatsArena()} , liveIdx{GetStatsArena()} , prevLiveIdx{GetStatsArena()} {} @@ -91,15 +216,23 @@ namespace p ThreadContext* c = contexts.exchange(nullptr, std::memory_order_acq_rel); while (c) { - ThreadContext* next = c->nextCtx; - if (ThreadContext::Chunk* chunk = c->head.load(std::memory_order_relaxed)) + ThreadContext* nextCtx = c->nextCtx; + ThreadContext::Chunk* chunk = c->head.load(std::memory_order_relaxed); + while (chunk) { + ThreadContext::Chunk* next = chunk->next.load(std::memory_order_relaxed); chunk->~Chunk(); p::Free(GetStatsArena(), chunk, 1); + chunk = next; + } + if (ThreadContext::Chunk* spare = c->spare.load(std::memory_order_relaxed)) + { + spare->~Chunk(); + p::Free(GetStatsArena(), spare, 1); } c->~ThreadContext(); p::Free(GetStatsArena(), c, 1); - c = next; + c = nextCtx; } } @@ -125,65 +258,46 @@ namespace p return ctx; } - void MemoryStats::PushEvent(void* ptr, sizet size, bool isFree) + void MemoryStats::PushEvent(const MemoryStatsEvent& ev) { - auto* ctx = GetOrCreateContext(); - ThreadContext::Chunk* c = ctx->tail; - if (!c || c->writeIdx.load(std::memory_order_relaxed) >= ThreadContext::Chunk::capacity) + ThreadContext* ctx = GetOrCreateContext(); + if (ThreadContext::Chunk* chunk = ctx->tail) { - // Allocate a new chunk. Initialize it with the event already - // written so the consumer sees a complete slot on first read. - ThreadContext::Chunk* newC = p::Alloc(GetStatsArena(), 1); - new (newC) ThreadContext::Chunk{}; - if (isFree) - { - newC->slots[0] = {static_cast(ptr), size, MemoryStatsEventFlags::IsFree}; - } - else - { - newC->slots[0] = {static_cast(ptr), size}; - } - newC->writeIdx.store(1, std::memory_order_release); - if (c) - { - // Publish new chunk via the old chunk's next. Consumer - // discovers it after we've fully initialized newC. - c->next.store(newC, std::memory_order_release); - } - else + const u32 idx = chunk->writeIdx.load(std::memory_order_relaxed); + if (idx < ThreadContext::Chunk::capacity) { - // First chunk: publish via head. - ctx->head.store(newC, std::memory_order_release); + chunk->slots[idx] = ev; + // Release the write so the consumer sees the slot data + // before the new writeIdx. + chunk->writeIdx.store(idx + 1, std::memory_order_release); + return; } - ctx->tail = newC; - return; } - const u32 idx = c->writeIdx.load(std::memory_order_relaxed); - if (isFree) + + // Cold path: no chunk yet or current chunk is full. Allocate a new + // chunk, reusing the spare if the consumer left one. The event is + // written into slot 0 before publishing so the consumer sees a + // complete slot on first read. + ThreadContext::Chunk* newC = ctx->spare.exchange(nullptr, std::memory_order_acquire); + if (!newC) { - c->slots[idx] = {static_cast(ptr), size, MemoryStatsEventFlags::IsFree}; + newC = p::Alloc(GetStatsArena(), 1); } - else + new (newC) ThreadContext::Chunk{}; + newC->slots[0] = ev; + newC->writeIdx.store(1, std::memory_order_release); + if (ThreadContext::Chunk* const oldTail = ctx->tail) { - c->slots[idx] = {static_cast(ptr), size}; + // Publish new chunk via the old chunk's next. Consumer + // discovers it after we've fully initialized newC. + oldTail->next.store(newC, std::memory_order_release); } - // Release the write so the consumer sees the slot data before the - // new writeIdx. - c->writeIdx.store(idx + 1, std::memory_order_release); - } - - void MemoryStats::Add(void* ptr, sizet size) - { - PushEvent(ptr, size, false); - } - - void MemoryStats::Remove(void* ptr, sizet size) - { - if (!ptr) + else { - return; + // First chunk: publish via head. + ctx->head.store(newC, std::memory_order_release); } - PushEvent(ptr, size, true); + ctx->tail = newC; } void MemoryStats::Release() @@ -195,7 +309,6 @@ namespace p collectedEvents = 0; events.Clear(); live.Clear(); - frees.Clear(); liveIdx.Clear(); prevLiveIdx.Clear(); } @@ -203,8 +316,8 @@ namespace p void MemoryStats::CollectStats() const { // Walk all thread contexts and drain their chunk chains. For each - // chunk, process all available events, then free the chunk if the - // producer has already linked a successor. + // chunk, process all available events, then recycle or free the + // chunk if the producer has already linked a successor. ThreadContext* c = contexts.load(std::memory_order_acquire); for (; c != nullptr; c = c->nextCtx) { @@ -214,7 +327,7 @@ namespace p const u32 writeIdx = chunk->writeIdx.load(std::memory_order_acquire); while (chunk->readIdx < writeIdx) { - const MemoryStatsEvent& ev = chunk->slots[chunk->readIdx]; + const auto& ev = chunk->slots[chunk->readIdx]; if (ev.IsFree()) { used -= ev.GetSize(); @@ -234,10 +347,17 @@ namespace p // Producer hasn't allocated a successor yet. Stop. break; } - // Producer has moved on. Safe to free this chunk. + // Producer has moved on. Safe to recycle or free this chunk. c->head.store(next, std::memory_order_relaxed); chunk->~Chunk(); - p::Free(GetStatsArena(), chunk, 1); + if (!c->spare.load(std::memory_order_relaxed)) + { + c->spare.store(chunk, std::memory_order_relaxed); + } + else + { + p::Free(GetStatsArena(), chunk, 1); + } chunk = next; } } @@ -245,61 +365,49 @@ namespace p // --- Incremental classification of drained events --- // live[i]: events[i] is an alloc never matched by a free. - // frees[i]: events[i] is a free event. // Events are append-only, so bits computed in previous calls remain // valid; only classify events drained since the last call. A free // matches the most recent unmatched alloc with the same key (LIFO), - // mirroring a full reverse scan. + // mirroring a full reverse scan. Free events are classified by + // their flag in the event itself. live.Resize(events.Size()); - frees.Resize(events.Size()); prevLiveIdx.Resize(events.Size()); - // Fast (ptr,size) key: XOR ptr with mixed size to produce a - // single u64. Cheaper to hash than the full 16-byte event. - auto EventKey = [](const MemoryStatsEvent& ev) -> u64 - { - return reinterpret_cast(ev.GetPtr()) - ^ (static_cast(ev.GetSize()) * 0x9E3779B97F4A7C15ULL); - }; - for (i32 i = collectedEvents; i < events.Size(); ++i) { - const auto& ev = events[i]; - const u64 key = EventKey(ev); - auto it = liveIdx.FindIt(key); + const MemoryStatsEvent& ev = events[i]; + const u64 hash = GetHash(ev); if (ev.IsFree()) { - frees.SetTrue(i); - if (it != liveIdx.end()) + if (i32* nodePtr = liveIdx.Find(hash)) { // Unmark the newest unmatched alloc and pop it off the // chain, promoting its predecessor as chain head. - const i32 node = it->second; + const i32 node = *nodePtr; live.SetFalse(node); const i32 prev = prevLiveIdx[node]; if (prev == NO_INDEX) { - liveIdx.RemoveIt(it); + liveIdx.EraseAt(nodePtr); } else { - *const_cast(&it->second) = prev; + *nodePtr = prev; } } // Else a stray free: recorded, nothing to unmark. } else { - // Push this alloc as the newest node of the key's chain. - if (it != liveIdx.end()) + i32* headPtr = liveIdx.FindOrInsert(hash, i); + if (*headPtr != i) { - prevLiveIdx[i] = it->second; - *const_cast(&it->second) = i; + prevLiveIdx[i] = *headPtr; + *headPtr = i; } else { prevLiveIdx[i] = NO_INDEX; - liveIdx.Insert(key, i); } live.SetTrue(i); } diff --git a/Tests/Core/String.spec.cpp b/Tests/Core/String.spec.cpp index 16aa1884..70c0c500 100644 --- a/Tests/Core/String.spec.cpp +++ b/Tests/Core/String.spec.cpp @@ -19,954 +19,957 @@ static const StringView longText = "0123456789ABCDEFGHIJ0123456789ABC"; go_bandit([]() { - describe("Strings.String", []() + describe("Strings", []() { - describe("Construction", []() + describe("String", []() { - it("Can default construct", [&]() + describe("Construction", []() { - String v{}; - AssertThat(v.size(), Equals(0u)); - AssertThat(v.empty(), Is().True()); - AssertThat(v.length(), Equals(0u)); - // c_str() must always return a valid pointer to a null terminator - AssertThat(v.c_str() != nullptr, Is().True()); - AssertThat(v.c_str()[0], Equals('\0')); - AssertThat(v.data() != nullptr, Is().True()); - AssertThat(v.data()[0], Equals('\0')); - }); + it("Can default construct", [&]() + { + String v{}; + AssertThat(v.size(), Equals(0u)); + AssertThat(v.empty(), Is().True()); + AssertThat(v.length(), Equals(0u)); + // c_str() must always return a valid pointer to a null terminator + AssertThat(v.c_str() != nullptr, Is().True()); + AssertThat(v.c_str()[0], Equals('\0')); + AssertThat(v.data() != nullptr, Is().True()); + AssertThat(v.data()[0], Equals('\0')); + }); - it("Can construct from literal", [&]() - { - String v{"Kiwi"}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4u)); - }); + it("Can construct from literal", [&]() + { + String v{"Kiwi"}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); - it("Can construct from literal with count", [&]() - { - String v{"KiwiApple", 4}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4u)); - }); + it("Can construct from literal with count", [&]() + { + String v{"KiwiApple", 4}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); - it("Can construct from count and char", [&]() - { - String v(5, 'x'); - AssertThat(v, Equals("xxxxx")); - AssertThat(v.size(), Equals(5u)); - }); + it("Can construct from count and char", [&]() + { + String v(5, 'x'); + AssertThat(v, Equals("xxxxx")); + AssertThat(v.size(), Equals(5u)); + }); - it("Can construct from string view", [&]() - { - StringView str{"Kiwi"}; - String v{str}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4u)); - }); + it("Can construct from string view", [&]() + { + StringView str{"Kiwi"}; + String v{str}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + }); - it("Can construct from string view with pos and count", [&]() - { - StringView str{"KiwiApple"}; - String v{str, 4, 5}; - AssertThat(v, Equals("Apple")); - }); + it("Can construct from string view with pos and count", [&]() + { + StringView str{"KiwiApple"}; + String v{str, 4, 5}; + AssertThat(v, Equals("Apple")); + }); - it("Can construct from substring", [&]() - { - String str{"KiwiApple"}; - String v{str, 4}; - AssertThat(v, Equals("Apple")); - String v2{str, 4, 3}; - AssertThat(v2, Equals("App")); - }); + it("Can construct from substring", [&]() + { + String str{"KiwiApple"}; + String v{str, 4}; + AssertThat(v, Equals("Apple")); + String v2{str, 4, 3}; + AssertThat(v2, Equals("App")); + }); - it("Can construct from iterators", [&]() - { - std::string_view sv = "Kiwi"; - String v{sv.begin(), sv.end()}; - AssertThat(v, Equals("Kiwi")); - }); + it("Can construct from iterators", [&]() + { + std::string_view sv = "Kiwi"; + String v{sv.begin(), sv.end()}; + AssertThat(v, Equals("Kiwi")); + }); - it("Can construct from initializer list", [&]() - { - String v{'K', 'i', 'w', 'i'}; - AssertThat(v, Equals("Kiwi")); - }); + it("Can construct from initializer list", [&]() + { + String v{'K', 'i', 'w', 'i'}; + AssertThat(v, Equals("Kiwi")); + }); - it("Can copy construct", [&]() - { - String v{"Kiwi"}; - String v2{v}; - AssertThat(v2, Equals("Kiwi")); - AssertThat(v, Equals("Kiwi")); - }); + it("Can copy construct", [&]() + { + String v{"Kiwi"}; + String v2{v}; + AssertThat(v2, Equals("Kiwi")); + AssertThat(v, Equals("Kiwi")); + }); - it("Can move construct", [&]() - { - String v{"Kiwi"}; - String v2{Move(v)}; - AssertThat(v2, Equals("Kiwi")); - // Moved-from string is valid and empty - AssertThat(v.size(), Equals(0u)); - AssertThat(v.empty(), Is().True()); - AssertThat(v.c_str()[0], Equals('\0')); + it("Can move construct", [&]() + { + String v{"Kiwi"}; + String v2{Move(v)}; + AssertThat(v2, Equals("Kiwi")); + // Moved-from string is valid and empty + AssertThat(v.size(), Equals(0u)); + AssertThat(v.empty(), Is().True()); + AssertThat(v.c_str()[0], Equals('\0')); + }); }); - }); - describe("Assignment", []() - { - it("Can assign from literal", [&]() + describe("Assignment", []() { - String v; - v = "Kiwi"; - AssertThat(v, Equals("Kiwi")); - }); + it("Can assign from literal", [&]() + { + String v; + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + }); - it("Can copy assign", [&]() - { - String vKiwi{"Kiwi"}; - String vApple{"Apple"}; - String vCopy = vKiwi; - AssertThat(vCopy, Equals("Kiwi")); - vCopy = vApple; - AssertThat(vCopy, Equals("Apple")); - AssertThat(vCopy, Equals(vApple)); - }); + it("Can copy assign", [&]() + { + String vKiwi{"Kiwi"}; + String vApple{"Apple"}; + String vCopy = vKiwi; + AssertThat(vCopy, Equals("Kiwi")); + vCopy = vApple; + AssertThat(vCopy, Equals("Apple")); + AssertThat(vCopy, Equals(vApple)); + }); - it("Can move assign", [&]() - { - String vKiwi{"Kiwi"}; - String vApple{"Apple"}; - String vMove = Move(vKiwi); - AssertThat(vKiwi.size(), Equals(0u)); - AssertThat(vMove, Equals("Kiwi")); - vMove = Move(vApple); - AssertThat(vApple.size(), Equals(0u)); - AssertThat(vMove, Equals("Apple")); - }); + it("Can move assign", [&]() + { + String vKiwi{"Kiwi"}; + String vApple{"Apple"}; + String vMove = Move(vKiwi); + AssertThat(vKiwi.size(), Equals(0u)); + AssertThat(vMove, Equals("Kiwi")); + vMove = Move(vApple); + AssertThat(vApple.size(), Equals(0u)); + AssertThat(vMove, Equals("Apple")); + }); - it("Can assign char", [&]() - { - String v; - v = 'x'; - AssertThat(v, Equals("x")); - }); + it("Can assign char", [&]() + { + String v; + v = 'x'; + AssertThat(v, Equals("x")); + }); - it("Can assign initializer list", [&]() - { - String v; - v = {'K', 'i', 'w', 'i'}; - AssertThat(v, Equals("Kiwi")); - }); + it("Can assign initializer list", [&]() + { + String v; + v = {'K', 'i', 'w', 'i'}; + AssertThat(v, Equals("Kiwi")); + }); - it("Can assign string view", [&]() - { - String v; - StringView sv{"Kiwi"}; - v = sv; - AssertThat(v, Equals("Kiwi")); - }); + it("Can assign string view", [&]() + { + String v; + StringView sv{"Kiwi"}; + v = sv; + AssertThat(v, Equals("Kiwi")); + }); - it("Can assign", [&]() - { - String v; - v.assign("Kiwi"); - AssertThat(v, Equals("Kiwi")); - v.assign("KiwiApple", 4); - AssertThat(v, Equals("Kiwi")); - v.assign(3, 'x'); - AssertThat(v, Equals("xxx")); - String other{"Apple"}; - v.assign(other); - AssertThat(v, Equals("Apple")); - v.assign(other, 2, 2); - AssertThat(v, Equals("pl")); - StringView sv{"KiwiApple"}; - v.assign(sv, 4, 5); - AssertThat(v, Equals("Apple")); - v.assign({'a', 'b', 'c'}); - AssertThat(v, Equals("abc")); - }); + it("Can assign", [&]() + { + String v; + v.assign("Kiwi"); + AssertThat(v, Equals("Kiwi")); + v.assign("KiwiApple", 4); + AssertThat(v, Equals("Kiwi")); + v.assign(3, 'x'); + AssertThat(v, Equals("xxx")); + String other{"Apple"}; + v.assign(other); + AssertThat(v, Equals("Apple")); + v.assign(other, 2, 2); + AssertThat(v, Equals("pl")); + StringView sv{"KiwiApple"}; + v.assign(sv, 4, 5); + AssertThat(v, Equals("Apple")); + v.assign({'a', 'b', 'c'}); + AssertThat(v, Equals("abc")); + }); - it("Can self assign", [&]() - { - String v{"Kiwi"}; - const String& ref = v; - v = ref; - AssertThat(v, Equals("Kiwi")); - }); + it("Can self assign", [&]() + { + String v{"Kiwi"}; + const String& ref = v; + v = ref; + AssertThat(v, Equals("Kiwi")); + }); - it("Can self assign substrings", [&]() - { - String v{longText}; - v.assign(v.c_str() + 10); - AssertThat(v, Equals("ABCDEFGHIJ0123456789ABC")); - }); + it("Can self assign substrings", [&]() + { + String v{longText}; + v.assign(v.c_str() + 10); + AssertThat(v, Equals("ABCDEFGHIJ0123456789ABC")); + }); - it("Can self assign substrings with count", [&]() - { - String v{longText}; - v.assign(v.c_str() + 5, 10); - AssertThat(v, Equals("56789ABCDE")); + it("Can self assign substrings with count", [&]() + { + String v{longText}; + v.assign(v.c_str() + 5, 10); + AssertThat(v, Equals("56789ABCDE")); + }); }); - }); - describe("Element access", []() - { - it("Can index", [&]() + describe("Element access", []() { - String v{"Kiwi"}; - AssertThat(v[0], Equals('K')); - AssertThat(v[3], Equals('i')); - v[0] = 'k'; - AssertThat(v, Equals("kiwi")); - // pos == size() returns reference to null char - AssertThat(v[4], Equals('\0')); - }); + it("Can index", [&]() + { + String v{"Kiwi"}; + AssertThat(v[0], Equals('K')); + AssertThat(v[3], Equals('i')); + v[0] = 'k'; + AssertThat(v, Equals("kiwi")); + // pos == size() returns reference to null char + AssertThat(v[4], Equals('\0')); + }); - it("Can access at", [&]() - { - String v{"Kiwi"}; - AssertThat(v.at(0), Equals('K')); - AssertThat(v.at(3), Equals('i')); - v.at(0) = 'k'; - AssertThat(v, Equals("kiwi")); - }); + it("Can access at", [&]() + { + String v{"Kiwi"}; + AssertThat(v.at(0), Equals('K')); + AssertThat(v.at(3), Equals('i')); + v.at(0) = 'k'; + AssertThat(v, Equals("kiwi")); + }); - it("Can access front and back", [&]() - { - String v{"Kiwi"}; - AssertThat(v.front(), Equals('K')); - AssertThat(v.back(), Equals('i')); - v.front() = 'P'; - v.back() = 's'; - AssertThat(v, Equals("Piws")); - }); + it("Can access front and back", [&]() + { + String v{"Kiwi"}; + AssertThat(v.front(), Equals('K')); + AssertThat(v.back(), Equals('i')); + v.front() = 'P'; + v.back() = 's'; + AssertThat(v, Equals("Piws")); + }); - it("Can retrieve data", [&]() - { - String v{"Kiwi"}; - AssertThat(v.data(), Equals("Kiwi")); - AssertThat(v.size(), Equals(4u)); - AssertThat(strlen(v.data()), Equals(4u)); - }); + it("Can retrieve data", [&]() + { + String v{"Kiwi"}; + AssertThat(v.data(), Equals("Kiwi")); + AssertThat(v.size(), Equals(4u)); + AssertThat(strlen(v.data()), Equals(4u)); + }); - it("Can convert to string view", [&]() - { - String v{"Kiwi"}; - StringView sv = v; - AssertThat(sv.size(), Equals(4u)); - AssertThat(sv, Equals(StringView{"Kiwi"})); - StringView wsv{v}; - AssertThat(wsv, Equals(StringView{"Kiwi"})); + it("Can convert to string view", [&]() + { + String v{"Kiwi"}; + StringView sv = v; + AssertThat(sv.size(), Equals(4u)); + AssertThat(sv, Equals(StringView{"Kiwi"})); + StringView wsv{v}; + AssertThat(wsv, Equals(StringView{"Kiwi"})); + }); }); - }); - describe("Iterators", []() - { - it("Can iterate", [&]() + describe("Iterators", []() { - String v{"Kiwi"}; - u32 i = 0; - for (char c : v) - { - AssertThat(c, Equals("Kiwi"[i])); - ++i; - } - AssertThat(i, Equals(4u)); - }); + it("Can iterate", [&]() + { + String v{"Kiwi"}; + u32 i = 0; + for (char c : v) + { + AssertThat(c, Equals("Kiwi"[i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); - it("Can iterate const", [&]() - { - const String v{"Kiwi"}; - u32 i = 0; - for (char c : v) - { - AssertThat(c, Equals("Kiwi"[i])); - ++i; - } - AssertThat(i, Equals(4u)); - }); + it("Can iterate const", [&]() + { + const String v{"Kiwi"}; + u32 i = 0; + for (char c : v) + { + AssertThat(c, Equals("Kiwi"[i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); - it("Can iterate manually", [&]() - { - String v{"Kiwi"}; - auto it = v.begin(); - auto end = v.end(); - AssertThat(end - it, Equals(4)); - AssertThat(*it, Equals('K')); - AssertThat(it[2], Equals('w')); - ++it; - AssertThat(*it, Equals('i')); - it += 2; - AssertThat(*it, Equals('i')); - --it; - AssertThat(*it, Equals('w')); - AssertThat(it == v.begin() + 2, Is().True()); - AssertThat(it != v.begin(), Is().True()); - }); + it("Can iterate manually", [&]() + { + String v{"Kiwi"}; + auto it = v.begin(); + auto end = v.end(); + AssertThat(end - it, Equals(4)); + AssertThat(*it, Equals('K')); + AssertThat(it[2], Equals('w')); + ++it; + AssertThat(*it, Equals('i')); + it += 2; + AssertThat(*it, Equals('i')); + --it; + AssertThat(*it, Equals('w')); + AssertThat(it == v.begin() + 2, Is().True()); + AssertThat(it != v.begin(), Is().True()); + }); - it("Can iterate reverse", [&]() - { - String v{"Kiwi"}; - u32 i = 0; - for (auto rit = v.rbegin(); rit != v.rend(); ++rit) - { - AssertThat(*rit, Equals("Kiwi"[3 - i])); - ++i; - } - AssertThat(i, Equals(4u)); - }); + it("Can iterate reverse", [&]() + { + String v{"Kiwi"}; + u32 i = 0; + for (auto rit = v.rbegin(); rit != v.rend(); ++rit) + { + AssertThat(*rit, Equals("Kiwi"[3 - i])); + ++i; + } + AssertThat(i, Equals(4u)); + }); - it("Can iterate c-variants", [&]() - { - String v{"Kiwi"}; - AssertThat(*v.cbegin(), Equals('K')); - AssertThat(*(v.cend() - 1), Equals('i')); - AssertThat(*v.crbegin(), Equals('i')); - AssertThat(*(v.crend() - 1), Equals('K')); - }); + it("Can iterate c-variants", [&]() + { + String v{"Kiwi"}; + AssertThat(*v.cbegin(), Equals('K')); + AssertThat(*(v.cend() - 1), Equals('i')); + AssertThat(*v.crbegin(), Equals('i')); + AssertThat(*(v.crend() - 1), Equals('K')); + }); - it("Can mutate through iterators", [&]() - { - String v{"Kiwi"}; - std::transform(v.begin(), v.end(), v.begin(), [](char c) + it("Can mutate through iterators", [&]() { - return char(c + 1); + String v{"Kiwi"}; + std::transform(v.begin(), v.end(), v.begin(), [](char c) + { + return char(c + 1); + }); + AssertThat(v, Equals("Ljxj")); }); - AssertThat(v, Equals("Ljxj")); }); - }); - describe("Capacity", []() - { - it("Can query size and length", [&]() + describe("Capacity", []() { - String v{"Kiwi"}; - AssertThat(v.size(), Equals(4u)); - AssertThat(v.length(), Equals(4u)); - AssertThat(v.empty(), Is().False()); - }); + it("Can query size and length", [&]() + { + String v{"Kiwi"}; + AssertThat(v.size(), Equals(4u)); + AssertThat(v.length(), Equals(4u)); + AssertThat(v.empty(), Is().False()); + }); - it("Has short string optimization", [&]() - { - String v{"Kiwi"}; - // Short strings must fit in the internal buffer - AssertThat(v.capacity() >= 15u, Is().True()); - AssertThat(v.capacity() <= 32u, Is().True()); - }); + it("Has short string optimization", [&]() + { + String v{"Kiwi"}; + // Short strings must fit in the internal buffer + AssertThat(v.capacity() >= 15u, Is().True()); + AssertThat(v.capacity() <= 32u, Is().True()); + }); - it("Can reserve", [&]() - { - String v; - v.reserve(100); - AssertThat(v.capacity() >= 100u, Is().True()); - AssertThat(v.size(), Equals(0u)); - v = "Kiwi"; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.capacity() >= 100u, Is().True()); - }); + it("Can reserve", [&]() + { + String v; + v.reserve(100); + AssertThat(v.capacity() >= 100u, Is().True()); + AssertThat(v.size(), Equals(0u)); + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() >= 100u, Is().True()); + }); - it("Can shrink to fit", [&]() - { - String v; - v.reserve(100); - v = "Kiwi"; - v.shrink_to_fit(); - AssertThat(v, Equals("Kiwi")); - AssertThat(v.capacity() >= 4u, Is().True()); - AssertThat(v.capacity() < 100u, Is().True()); - }); + it("Can shrink to fit", [&]() + { + String v; + v.reserve(100); + v = "Kiwi"; + v.shrink_to_fit(); + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() >= 4u, Is().True()); + AssertThat(v.capacity() < 100u, Is().True()); + }); - it("Has max size", [&]() - { - String v; - // Lengths are stored internally as i32 - AssertThat(v.max_size(), Equals(sizet(Limits::Max() - 1))); + it("Has max size", [&]() + { + String v; + // Lengths are stored internally as i32 + AssertThat(v.max_size(), Equals(sizet(Limits::Max() - 1))); + }); }); - }); - describe("Modifiers", []() - { - it("Can clear", [&]() + describe("Modifiers", []() { - String v{"Kiwi"}; - v.clear(); - AssertThat(v.empty(), Is().True()); - AssertThat(v.size(), Equals(0u)); - AssertThat(v.c_str()[0], Equals('\0')); - }); + it("Can clear", [&]() + { + String v{"Kiwi"}; + v.clear(); + AssertThat(v.empty(), Is().True()); + AssertThat(v.size(), Equals(0u)); + AssertThat(v.c_str()[0], Equals('\0')); + }); - it("Can push and pop back", [&]() - { - String v{"Ki"}; - v.push_back('w'); - v.push_back('i'); - AssertThat(v, Equals("Kiwi")); - AssertThat(v.back(), Equals('i')); - v.pop_back(); - AssertThat(v, Equals("Kiw")); - v.pop_back(); - v.pop_back(); - v.pop_back(); - AssertThat(v, Equals("")); - AssertThat(v.empty(), Is().True()); - }); + it("Can push and pop back", [&]() + { + String v{"Ki"}; + v.push_back('w'); + v.push_back('i'); + AssertThat(v, Equals("Kiwi")); + AssertThat(v.back(), Equals('i')); + v.pop_back(); + AssertThat(v, Equals("Kiw")); + v.pop_back(); + v.pop_back(); + v.pop_back(); + AssertThat(v, Equals("")); + AssertThat(v.empty(), Is().True()); + }); - it("Can append", [&]() - { - String v{"Kiwi"}; - v.append("Apple"); - AssertThat(v, Equals("KiwiApple")); - v.append("Orange", 3); - AssertThat(v, Equals("KiwiAppleOra")); - v.append(3, '-'); - AssertThat(v, Equals("KiwiAppleOra---")); - String other{"End"}; - v.append(other); - AssertThat(v, Equals("KiwiAppleOra---End")); - v.append(other, 1, 2); - AssertThat(v, Equals("KiwiAppleOra---Endnd")); - StringView sv{"View"}; - v.append(sv); - AssertThat(v, Equals("KiwiAppleOra---EndndView")); - v.append(sv, 2, 2); - AssertThat(v, Equals("KiwiAppleOra---EndndViewew")); - v.append({'!', '?'}); - AssertThat(v, Equals("KiwiAppleOra---EndndViewew!?")); - }); + it("Can append", [&]() + { + String v{"Kiwi"}; + v.append("Apple"); + AssertThat(v, Equals("KiwiApple")); + v.append("Orange", 3); + AssertThat(v, Equals("KiwiAppleOra")); + v.append(3, '-'); + AssertThat(v, Equals("KiwiAppleOra---")); + String other{"End"}; + v.append(other); + AssertThat(v, Equals("KiwiAppleOra---End")); + v.append(other, 1, 2); + AssertThat(v, Equals("KiwiAppleOra---Endnd")); + StringView sv{"View"}; + v.append(sv); + AssertThat(v, Equals("KiwiAppleOra---EndndView")); + v.append(sv, 2, 2); + AssertThat(v, Equals("KiwiAppleOra---EndndViewew")); + v.append({'!', '?'}); + AssertThat(v, Equals("KiwiAppleOra---EndndViewew!?")); + }); - it("Can append with operator+=", [&]() - { - String v{"Kiwi"}; - v += "Apple"; - AssertThat(v, Equals("KiwiApple")); - v += '!'; - AssertThat(v, Equals("KiwiApple!")); - String other{"End"}; - v += other; - AssertThat(v, Equals("KiwiApple!End")); - v += StringView{"View"}; - AssertThat(v, Equals("KiwiApple!EndView")); - v += {'a', 'b'}; - AssertThat(v, Equals("KiwiApple!EndViewab")); - }); + it("Can append with operator+=", [&]() + { + String v{"Kiwi"}; + v += "Apple"; + AssertThat(v, Equals("KiwiApple")); + v += '!'; + AssertThat(v, Equals("KiwiApple!")); + String other{"End"}; + v += other; + AssertThat(v, Equals("KiwiApple!End")); + v += StringView{"View"}; + AssertThat(v, Equals("KiwiApple!EndView")); + v += {'a', 'b'}; + AssertThat(v, Equals("KiwiApple!EndViewab")); + }); - it("Can insert", [&]() - { - String v{"KiwiApple"}; - v.insert(4, "Orange"); - AssertThat(v, Equals("KiwiOrangeApple")); - v.insert(0, "-"); - AssertThat(v, Equals("-KiwiOrangeApple")); - v.insert(v.size(), "!"); - AssertThat(v, Equals("-KiwiOrangeApple!")); - v.insert(0, 3, '='); - AssertThat(v, Equals("===-KiwiOrangeApple!")); - String other{"XX"}; - v.insert(3, other); - AssertThat(v, Equals("===XX-KiwiOrangeApple!")); - StringView sv{"YY"}; - v.insert(5, sv); - AssertThat(v, Equals("===XXYY-KiwiOrangeApple!")); - v.insert(0, 2, 'Z'); - AssertThat(v, Equals("ZZ===XXYY-KiwiOrangeApple!")); - }); + it("Can insert", [&]() + { + String v{"KiwiApple"}; + v.insert(4, "Orange"); + AssertThat(v, Equals("KiwiOrangeApple")); + v.insert(0, "-"); + AssertThat(v, Equals("-KiwiOrangeApple")); + v.insert(v.size(), "!"); + AssertThat(v, Equals("-KiwiOrangeApple!")); + v.insert(0, 3, '='); + AssertThat(v, Equals("===-KiwiOrangeApple!")); + String other{"XX"}; + v.insert(3, other); + AssertThat(v, Equals("===XX-KiwiOrangeApple!")); + StringView sv{"YY"}; + v.insert(5, sv); + AssertThat(v, Equals("===XXYY-KiwiOrangeApple!")); + v.insert(0, 2, 'Z'); + AssertThat(v, Equals("ZZ===XXYY-KiwiOrangeApple!")); + }); - it("Can insert with iterator", [&]() - { - String v{"Kiwi"}; - auto it = v.insert(v.begin() + 2, '-'); - AssertThat(*it, Equals('-')); - AssertThat(v, Equals("Ki-wi")); - v.insert(v.end(), 3, '!'); - AssertThat(v, Equals("Ki-wi!!!")); - String other{"AB"}; - v.insert(v.begin(), other.begin(), other.end()); - AssertThat(v, Equals("ABKi-wi!!!")); - v.insert(v.begin() + 2, {'x', 'y'}); - AssertThat(v, Equals("ABxyKi-wi!!!")); - }); + it("Can insert with iterator", [&]() + { + String v{"Kiwi"}; + auto it = v.insert(v.begin() + 2, '-'); + AssertThat(*it, Equals('-')); + AssertThat(v, Equals("Ki-wi")); + v.insert(v.end(), 3, '!'); + AssertThat(v, Equals("Ki-wi!!!")); + String other{"AB"}; + v.insert(v.begin(), other.begin(), other.end()); + AssertThat(v, Equals("ABKi-wi!!!")); + v.insert(v.begin() + 2, {'x', 'y'}); + AssertThat(v, Equals("ABxyKi-wi!!!")); + }); - it("Can erase", [&]() - { - String v{"KiwiApple"}; - v.erase(4, 5); - AssertThat(v, Equals("Kiwi")); - v.erase(2); - AssertThat(v, Equals("Ki")); - v.erase(0, 1); - AssertThat(v, Equals("i")); - v.erase(0, 10); - AssertThat(v, Equals("")); - }); + it("Can erase", [&]() + { + String v{"KiwiApple"}; + v.erase(4, 5); + AssertThat(v, Equals("Kiwi")); + v.erase(2); + AssertThat(v, Equals("Ki")); + v.erase(0, 1); + AssertThat(v, Equals("i")); + v.erase(0, 10); + AssertThat(v, Equals("")); + }); - it("Can erase with iterator", [&]() - { - String v{"Kiwi"}; - auto it = v.erase(v.begin()); - AssertThat(*it, Equals('i')); - AssertThat(v, Equals("iwi")); - v.erase(v.begin() + 1, v.end()); - AssertThat(v, Equals("i")); - }); + it("Can erase with iterator", [&]() + { + String v{"Kiwi"}; + auto it = v.erase(v.begin()); + AssertThat(*it, Equals('i')); + AssertThat(v, Equals("iwi")); + v.erase(v.begin() + 1, v.end()); + AssertThat(v, Equals("i")); + }); - it("Can replace", [&]() - { - String v{"KiwiApple"}; - v.replace(0, 4, "Orange"); - AssertThat(v, Equals("OrangeApple")); - v.replace(0, 6, "X"); - AssertThat(v, Equals("XApple")); - v.replace(v.size() - 3, 3, "Z"); - AssertThat(v, Equals("XApZ")); - String other{"Kiwi"}; - v.replace(0, 4, other); - AssertThat(v, Equals("Kiwi")); - StringView sv{"Two"}; - v.replace(0, 4, sv); - AssertThat(v, Equals("Two")); - v.replace(0, 3, 2, 'y'); - AssertThat(v, Equals("yy")); - }); + it("Can replace", [&]() + { + String v{"KiwiApple"}; + v.replace(0, 4, "Orange"); + AssertThat(v, Equals("OrangeApple")); + v.replace(0, 6, "X"); + AssertThat(v, Equals("XApple")); + v.replace(v.size() - 3, 3, "Z"); + AssertThat(v, Equals("XApZ")); + String other{"Kiwi"}; + v.replace(0, 4, other); + AssertThat(v, Equals("Kiwi")); + StringView sv{"Two"}; + v.replace(0, 4, sv); + AssertThat(v, Equals("Two")); + v.replace(0, 3, 2, 'y'); + AssertThat(v, Equals("yy")); + }); - it("Can replace with iterators", [&]() - { - String v{"KiwiApple"}; - v.replace(v.begin(), v.begin() + 4, "Orange"); - AssertThat(v, Equals("OrangeApple")); - }); + it("Can replace with iterators", [&]() + { + String v{"KiwiApple"}; + v.replace(v.begin(), v.begin() + 4, "Orange"); + AssertThat(v, Equals("OrangeApple")); + }); - it("Can resize", [&]() - { - String v{"Kiwi"}; - v.resize(2); - AssertThat(v, Equals("Ki")); - v.resize(4); - AssertThat(v.size(), Equals(4u)); - AssertThat(v[2], Equals('\0')); - AssertThat(v[3], Equals('\0')); - v.resize(6, 'x'); - AssertThat(v[4], Equals('x')); - AssertThat(v[5], Equals('x')); - AssertThat(v.size(), Equals(6u)); - }); + it("Can resize", [&]() + { + String v{"Kiwi"}; + v.resize(2); + AssertThat(v, Equals("Ki")); + v.resize(4); + AssertThat(v.size(), Equals(4u)); + AssertThat(v[2], Equals('\0')); + AssertThat(v[3], Equals('\0')); + v.resize(6, 'x'); + AssertThat(v[4], Equals('x')); + AssertThat(v[5], Equals('x')); + AssertThat(v.size(), Equals(6u)); + }); - it("Can swap", [&]() - { - String a{"Kiwi"}; - String b{"Apple"}; - a.swap(b); - AssertThat(a, Equals("Apple")); - AssertThat(b, Equals("Kiwi")); - }); + it("Can swap", [&]() + { + String a{"Kiwi"}; + String b{"Apple"}; + a.swap(b); + AssertThat(a, Equals("Apple")); + AssertThat(b, Equals("Kiwi")); + }); - it("Can append from self", [&]() - { - String v{longText}; - v.append(v.c_str()); - AssertThat(v, Equals(std::string{longText} + std::string{longText})); - }); + it("Can append from self", [&]() + { + String v{longText}; + v.append(v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText})); + }); - it("Can append self substring", [&]() - { - String v{longText}; - v.append(v.c_str() + 5); - AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(5)})); - }); + it("Can append self substring", [&]() + { + String v{longText}; + v.append(v.c_str() + 5); + AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(5)})); + }); - it("Can insert from self", [&]() - { - String v{longText}; - v.insert(0, v.c_str()); - AssertThat(v, Equals(std::string{longText} + std::string{longText})); - }); + it("Can insert from self", [&]() + { + String v{longText}; + v.insert(0, v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText})); + }); - it("Can insert self substring", [&]() - { - String v{longText}; - v.insert(4, v.c_str() + 5); - AssertThat( - v, Equals(std::string{longText.substr(0, 4)} + std::string{longText.substr(5)} - + std::string{longText.substr(4)})); - }); + it("Can insert self substring", [&]() + { + String v{longText}; + v.insert(4, v.c_str() + 5); + AssertThat(v, + Equals(std::string{longText.substr(0, 4)} + std::string{longText.substr(5)} + + std::string{longText.substr(4)})); + }); - it("Can replace with self", [&]() - { - String v{longText}; - v.replace(0, 4, v.c_str()); - AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(4)})); - }); + it("Can replace with self", [&]() + { + String v{longText}; + v.replace(0, 4, v.c_str()); + AssertThat(v, Equals(std::string{longText} + std::string{longText.substr(4)})); + }); - it("Can replace self substring with count", [&]() - { - String v{longText}; - v.replace(5, 10, v.c_str() + 2, 5); - AssertThat(v, Equals(std::string{longText.substr(0, 5)} + "23456" - + std::string{longText.substr(15)})); + it("Can replace self substring with count", [&]() + { + String v{longText}; + v.replace(5, 10, v.c_str() + 2, 5); + AssertThat(v, Equals(std::string{longText.substr(0, 5)} + "23456" + + std::string{longText.substr(15)})); + }); }); - }); - describe("Operations", []() - { - it("Can get substr", [&]() + describe("Operations", []() { - String v{"KiwiApple"}; - AssertThat(v.substr(), Equals("KiwiApple")); - AssertThat(v.substr(4), Equals("Apple")); - AssertThat(v.substr(4, 3), Equals("App")); - AssertThat(v.substr(0, 100), Equals("KiwiApple")); - }); + it("Can get substr", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.substr(), Equals("KiwiApple")); + AssertThat(v.substr(4), Equals("Apple")); + AssertThat(v.substr(4, 3), Equals("App")); + AssertThat(v.substr(0, 100), Equals("KiwiApple")); + }); - it("Can copy out", [&]() - { - String v{"KiwiApple"}; - char buffer[16]{}; - const auto count = v.copy(buffer, 4, 4); - AssertThat(count, Equals(4u)); - AssertThat(buffer, Equals("Appl")); - buffer[count] = '\0'; - }); + it("Can copy out", [&]() + { + String v{"KiwiApple"}; + char buffer[16]{}; + const auto count = v.copy(buffer, 4, 4); + AssertThat(count, Equals(4u)); + AssertThat(buffer, Equals("Appl")); + buffer[count] = '\0'; + }); - it("Can compare", [&]() - { - String v{"Kiwi"}; - String other{"Kiwi"}; - String apple{"Apple"}; - AssertThat(v.compare(other), Equals(0)); - AssertThat(v.compare(apple) > 0, Is().True()); - AssertThat(apple.compare(v) < 0, Is().True()); - AssertThat(v.compare("Kiwi"), Equals(0)); - AssertThat(v.compare("Kiwi2") < 0, Is().True()); - AssertThat(v.compare(StringView{"Kiwi"}), Equals(0)); - AssertThat(v.compare(0, 2, String{"Ki"}), Equals(0)); - AssertThat(v.compare(2, 2, String{"wi"}), Equals(0)); - }); + it("Can compare", [&]() + { + String v{"Kiwi"}; + String other{"Kiwi"}; + String apple{"Apple"}; + AssertThat(v.compare(other), Equals(0)); + AssertThat(v.compare(apple) > 0, Is().True()); + AssertThat(apple.compare(v) < 0, Is().True()); + AssertThat(v.compare("Kiwi"), Equals(0)); + AssertThat(v.compare("Kiwi2") < 0, Is().True()); + AssertThat(v.compare(StringView{"Kiwi"}), Equals(0)); + AssertThat(v.compare(0, 2, String{"Ki"}), Equals(0)); + AssertThat(v.compare(2, 2, String{"wi"}), Equals(0)); + }); - it("Can check prefix and suffix", [&]() - { - String v{"KiwiApple"}; - AssertThat(v.starts_with("Kiwi"), Is().True()); - AssertThat(v.starts_with('K'), Is().True()); - AssertThat(v.starts_with(StringView{"Ki"}), Is().True()); - AssertThat(v.starts_with("Apple"), Is().False()); - AssertThat(v.ends_with("Apple"), Is().True()); - AssertThat(v.ends_with('e'), Is().True()); - AssertThat(v.ends_with(StringView{"le"}), Is().True()); - AssertThat(v.ends_with("Kiwi"), Is().False()); - }); + it("Can check prefix and suffix", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.starts_with("Kiwi"), Is().True()); + AssertThat(v.starts_with('K'), Is().True()); + AssertThat(v.starts_with(StringView{"Ki"}), Is().True()); + AssertThat(v.starts_with("Apple"), Is().False()); + AssertThat(v.ends_with("Apple"), Is().True()); + AssertThat(v.ends_with('e'), Is().True()); + AssertThat(v.ends_with(StringView{"le"}), Is().True()); + AssertThat(v.ends_with("Kiwi"), Is().False()); + }); - it("Can check contains", [&]() - { - String v{"KiwiApple"}; - AssertThat(v.contains("wiA"), Is().True()); - AssertThat(v.contains('A'), Is().True()); - AssertThat(v.contains(StringView{"zzz"}), Is().False()); - AssertThat(v.contains('z'), Is().False()); - }); + it("Can check contains", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.contains("wiA"), Is().True()); + AssertThat(v.contains('A'), Is().True()); + AssertThat(v.contains(StringView{"zzz"}), Is().False()); + AssertThat(v.contains('z'), Is().False()); + }); - it("Can find", [&]() - { - String v{"KiwiKiwi"}; - AssertThat(v.find("Kiwi"), Equals(0u)); - AssertThat(v.find("Kiwi", 1), Equals(4u)); - AssertThat(v.find("Kiwi", 5), Equals(String::npos)); - AssertThat(v.find('i'), Equals(1u)); - AssertThat(v.find('i', 6), Equals(7u)); - AssertThat(v.find('z'), Equals(String::npos)); - AssertThat(v.find(String{"Kiwi"}), Equals(0u)); - AssertThat(v.find(StringView{"Kiwi"}), Equals(0u)); - }); + it("Can find", [&]() + { + String v{"KiwiKiwi"}; + AssertThat(v.find("Kiwi"), Equals(0u)); + AssertThat(v.find("Kiwi", 1), Equals(4u)); + AssertThat(v.find("Kiwi", 5), Equals(String::npos)); + AssertThat(v.find('i'), Equals(1u)); + AssertThat(v.find('i', 6), Equals(7u)); + AssertThat(v.find('z'), Equals(String::npos)); + AssertThat(v.find(String{"Kiwi"}), Equals(0u)); + AssertThat(v.find(StringView{"Kiwi"}), Equals(0u)); + }); - it("Can rfind", [&]() - { - String v{"KiwiKiwi"}; - AssertThat(v.rfind("Kiwi"), Equals(4u)); - AssertThat(v.rfind("Kiwi", 3), Equals(0u)); - AssertThat(v.rfind('i'), Equals(7u)); - AssertThat(v.rfind('i', 5), Equals(5u)); - AssertThat(v.rfind('z'), Equals(String::npos)); - AssertThat(v.rfind(String{"Kiwi"}), Equals(4u)); - AssertThat(v.rfind(StringView{"Kiwi"}), Equals(4u)); - }); + it("Can rfind", [&]() + { + String v{"KiwiKiwi"}; + AssertThat(v.rfind("Kiwi"), Equals(4u)); + AssertThat(v.rfind("Kiwi", 3), Equals(0u)); + AssertThat(v.rfind('i'), Equals(7u)); + AssertThat(v.rfind('i', 5), Equals(5u)); + AssertThat(v.rfind('z'), Equals(String::npos)); + AssertThat(v.rfind(String{"Kiwi"}), Equals(4u)); + AssertThat(v.rfind(StringView{"Kiwi"}), Equals(4u)); + }); - it("Can find first of", [&]() - { - String v{"KiwiApple"}; - AssertThat(v.find_first_of("pl"), Equals(5u)); - AssertThat(v.find_first_of("pl", 6), Equals(6u)); - AssertThat(v.find_first_of('z'), Equals(String::npos)); - AssertThat(v.find_first_of("xyz"), Equals(String::npos)); - AssertThat(v.find_first_of(StringView{"Ap"}), Equals(4u)); - }); + it("Can find first of", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.find_first_of("pl"), Equals(5u)); + AssertThat(v.find_first_of("pl", 6), Equals(6u)); + AssertThat(v.find_first_of('z'), Equals(String::npos)); + AssertThat(v.find_first_of("xyz"), Equals(String::npos)); + AssertThat(v.find_first_of(StringView{"Ap"}), Equals(4u)); + }); - it("Can find last of", [&]() - { - String v{"KiwiApple"}; - AssertThat(v.find_last_of("pl"), Equals(7u)); - AssertThat(v.find_last_of("pl", 6), Equals(6u)); - AssertThat(v.find_last_of('z'), Equals(String::npos)); - AssertThat(v.find_last_of(StringView{"Ap"}), Equals(6u)); - }); + it("Can find last of", [&]() + { + String v{"KiwiApple"}; + AssertThat(v.find_last_of("pl"), Equals(7u)); + AssertThat(v.find_last_of("pl", 6), Equals(6u)); + AssertThat(v.find_last_of('z'), Equals(String::npos)); + AssertThat(v.find_last_of(StringView{"Ap"}), Equals(6u)); + }); - it("Can find first not of", [&]() - { - String v{"aaab"}; - AssertThat(v.find_first_not_of("a"), Equals(3u)); - AssertThat(v.find_first_not_of("ab"), Equals(String::npos)); - AssertThat(v.find_first_not_of('a'), Equals(3u)); - AssertThat(v.find_first_not_of("ab", 3), Equals(String::npos)); - }); + it("Can find first not of", [&]() + { + String v{"aaab"}; + AssertThat(v.find_first_not_of("a"), Equals(3u)); + AssertThat(v.find_first_not_of("ab"), Equals(String::npos)); + AssertThat(v.find_first_not_of('a'), Equals(3u)); + AssertThat(v.find_first_not_of("ab", 3), Equals(String::npos)); + }); - it("Can find last not of", [&]() - { - String v{"baaa"}; - AssertThat(v.find_last_not_of("a"), Equals(0u)); - AssertThat(v.find_last_not_of("ab"), Equals(String::npos)); - AssertThat(v.find_last_not_of('a'), Equals(0u)); - AssertThat(v.find_last_not_of("ab", 0), Equals(String::npos)); - }); + it("Can find last not of", [&]() + { + String v{"baaa"}; + AssertThat(v.find_last_not_of("a"), Equals(0u)); + AssertThat(v.find_last_not_of("ab"), Equals(String::npos)); + AssertThat(v.find_last_not_of('a'), Equals(0u)); + AssertThat(v.find_last_not_of("ab", 0), Equals(String::npos)); + }); - it("Has npos", [&]() - { - AssertThat(String::npos, Equals(sizet(-1))); - AssertThat(StringView::npos, Equals(String::npos)); + it("Has npos", [&]() + { + AssertThat(String::npos, Equals(sizet(-1))); + AssertThat(StringView::npos, Equals(String::npos)); + }); }); - }); - describe("Operators", []() - { - it("Can concatenate", [&]() + describe("Operators", []() { - String a{"Kiwi"}; - String b{"Apple"}; - AssertThat(a + b, Equals("KiwiApple")); - AssertThat(a + "X", Equals("KiwiX")); - AssertThat("X" + a, Equals("XKiwi")); - AssertThat(a + '!', Equals("Kiwi!")); - AssertThat('!' + a, Equals("!Kiwi")); - AssertThat(a + StringView{"V"}, Equals("KiwiV")); - AssertThat(StringView{"V"} + a, Equals("VKiwi")); - }); + it("Can concatenate", [&]() + { + String a{"Kiwi"}; + String b{"Apple"}; + AssertThat(a + b, Equals("KiwiApple")); + AssertThat(a + "X", Equals("KiwiX")); + AssertThat("X" + a, Equals("XKiwi")); + AssertThat(a + '!', Equals("Kiwi!")); + AssertThat('!' + a, Equals("!Kiwi")); + AssertThat(a + StringView{"V"}, Equals("KiwiV")); + AssertThat(StringView{"V"} + a, Equals("VKiwi")); + }); - it("Can chain concatenate", [&]() - { - String a{"Kiwi"}; - String result = a + " " + "Apple" + '!'; - AssertThat(result, Equals("Kiwi Apple!")); - }); + it("Can chain concatenate", [&]() + { + String a{"Kiwi"}; + String result = a + " " + "Apple" + '!'; + AssertThat(result, Equals("Kiwi Apple!")); + }); - it("Can compare with other types", [&]() - { - String v{"Kiwi"}; - AssertThat(v == String{"Kiwi"}, Is().True()); - AssertThat(v != String{"Apple"}, Is().True()); - AssertThat(v == "Kiwi", Is().True()); - AssertThat(v != "Apple", Is().True()); - AssertThat("Kiwi" == v, Is().True()); - AssertThat("Apple" != v, Is().True()); - AssertThat(v < "Lime", Is().True()); - AssertThat("Lime" > v, Is().True()); - AssertThat(v <= String{"Kiwi"}, Is().True()); - AssertThat(v >= String{"Kiwi"}, Is().True()); - AssertThat(v == StringView{"Kiwi"}, Is().True()); - AssertThat(StringView{"Kiwi"} == v, Is().True()); - AssertThat(v != StringView{"Apple"}, Is().True()); - AssertThat(StringView{"Apple"} != v, Is().True()); - AssertThat(v < StringView{"Lime"}, Is().True()); - AssertThat(StringView{"Lime"} > v, Is().True()); - }); + it("Can compare with other types", [&]() + { + String v{"Kiwi"}; + AssertThat(v == String{"Kiwi"}, Is().True()); + AssertThat(v != String{"Apple"}, Is().True()); + AssertThat(v == "Kiwi", Is().True()); + AssertThat(v != "Apple", Is().True()); + AssertThat("Kiwi" == v, Is().True()); + AssertThat("Apple" != v, Is().True()); + AssertThat(v < "Lime", Is().True()); + AssertThat("Lime" > v, Is().True()); + AssertThat(v <= String{"Kiwi"}, Is().True()); + AssertThat(v >= String{"Kiwi"}, Is().True()); + AssertThat(v == StringView{"Kiwi"}, Is().True()); + AssertThat(StringView{"Kiwi"} == v, Is().True()); + AssertThat(v != StringView{"Apple"}, Is().True()); + AssertThat(StringView{"Apple"} != v, Is().True()); + AssertThat(v < StringView{"Lime"}, Is().True()); + AssertThat(StringView{"Lime"} > v, Is().True()); + }); - it("Can three-way compare", [&]() - { - String a{"Kiwi"}; - String b{"Lime"}; - AssertThat((a <=> b) < 0, Is().True()); - AssertThat((b <=> a) > 0, Is().True()); - AssertThat((a <=> String{"Kiwi"}) == 0, Is().True()); - AssertThat((a <=> "Kiwi") == 0, Is().True()); + it("Can three-way compare", [&]() + { + String a{"Kiwi"}; + String b{"Lime"}; + AssertThat((a <=> b) < 0, Is().True()); + AssertThat((b <=> a) > 0, Is().True()); + AssertThat((a <=> String{"Kiwi"}) == 0, Is().True()); + AssertThat((a <=> "Kiwi") == 0, Is().True()); + }); }); - }); - describe("Memory", []() - { - it("Keeps data valid when growing", [&]() + describe("Memory", []() { - String v; - for (char c = 'a'; c <= 'z'; ++c) - { - v.push_back(c); - } - AssertThat(v.size(), Equals(26u)); - AssertThat(v, Equals("abcdefghijklmnopqrstuvwxyz")); - AssertThat(v.c_str()[26], Equals('\0')); - }); + it("Keeps data valid when growing", [&]() + { + String v; + for (char c = 'a'; c <= 'z'; ++c) + { + v.push_back(c); + } + AssertThat(v.size(), Equals(26u)); + AssertThat(v, Equals("abcdefghijklmnopqrstuvwxyz")); + AssertThat(v.c_str()[26], Equals('\0')); + }); - it("Can reuse capacity", [&]() - { - String v; - v.reserve(1000); - const auto cap = v.capacity(); - for (u32 i = 0; i < 100; ++i) + it("Can reuse capacity", [&]() { - v.assign("KiwiAppleOrangeBanana"); - v.clear(); - } - AssertThat(v.capacity(), Equals(cap)); - }); + String v; + v.reserve(1000); + const auto cap = v.capacity(); + for (u32 i = 0; i < 100; ++i) + { + v.assign("KiwiAppleOrangeBanana"); + v.clear(); + } + AssertThat(v.capacity(), Equals(cap)); + }); - it("Is valid after move assignment", [&]() - { - String a{"Kiwi"}; - String b; - b = Move(a); - AssertThat(b, Equals("Kiwi")); - a = "Reused"; - AssertThat(a, Equals("Reused")); + it("Is valid after move assignment", [&]() + { + String a{"Kiwi"}; + String b; + b = Move(a); + AssertThat(b, Equals("Kiwi")); + a = "Reused"; + AssertThat(a, Equals("Reused")); + }); }); - }); - describe("Format & Hash", []() - { - it("Can be formatted", [&]() + describe("Format & Hash", []() { - String v{"Kiwi"}; - AssertThat(std::format("{}", v), Equals("Kiwi")); - AssertThat(Format("{}-{}", v, 5), Equals("Kiwi-5")); - String out; - FormatTo(out, "{}!", v); - AssertThat(out, Equals("Kiwi!")); - }); + it("Can be formatted", [&]() + { + String v{"Kiwi"}; + AssertThat(std::format("{}", v), Equals("Kiwi")); + AssertThat(Format("{}-{}", v, 5), Equals("Kiwi-5")); + String out; + FormatTo(out, "{}!", v); + AssertThat(out, Equals("Kiwi!")); + }); - it("Can be hashed", [&]() - { - String v{"Kiwi"}; - AssertThat(GetHash(v), Equals(GetStringHash("Kiwi"))); - AssertThat(GetHash(StringView{"Kiwi"}), Equals(GetHash(v))); + it("Can be hashed", [&]() + { + String v{"Kiwi"}; + AssertThat(GetHash(v), Equals(GetStringHash("Kiwi"))); + AssertThat(GetHash(StringView{"Kiwi"}), Equals(GetHash(v))); + }); }); - }); - - describe("Arena", []() - { - const char* longText = "This string is long enough to exceed the inline capacity"; - it("Can default construct on an arena", [&]() + describe("Arena", []() { - MonoLinearArena arena{Memory::KB * 4}; - String v{arena}; - AssertThat(v.empty(), Is().True()); - AssertThat(&v.GetArena(), Equals(static_cast(&arena))); - // Short strings still use the inline buffer - v = "Kiwi"; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.capacity() <= 32u, Is().True()); - }); + const char* longText = "This string is long enough to exceed the inline capacity"; - it("Can allocate on an arena", [&]() - { - MonoLinearArena arena{Memory::KB * 4}; - String v{arena, longText}; - AssertThat(v, Equals(longText)); - AssertThat(&v.GetArena(), Equals(static_cast(&arena))); - // Long strings must allocate on the arena, not the current arena - AssertThat(v.capacity() >= v.size(), Is().True()); - }); + it("Can default construct on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena}; + AssertThat(v.empty(), Is().True()); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + // Short strings still use the inline buffer + v = "Kiwi"; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.capacity() <= 32u, Is().True()); + }); - it("Can construct with count and char on an arena", [&]() - { - MonoLinearArena arena{Memory::KB * 4}; - String v{arena, 64, 'x'}; - AssertThat(v.size(), Equals(64u)); - AssertThat(&v.GetArena(), Equals(static_cast(&arena))); - }); + it("Can allocate on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena, longText}; + AssertThat(v, Equals(longText)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + // Long strings must allocate on the arena, not the current arena + AssertThat(v.capacity() >= v.size(), Is().True()); + }); - it("Can copy into an arena", [&]() - { - MonoLinearArena arena{Memory::KB * 4}; - String original{longText}; - String v{arena, original}; - AssertThat(v, Equals(original)); - AssertThat(&v.GetArena(), Equals(static_cast(&arena))); - }); + it("Can construct with count and char on an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena, 64, 'x'}; + AssertThat(v.size(), Equals(64u)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + }); - it("Keeps its arena when assigned", [&]() - { - MonoLinearArena arena{Memory::KB * 4}; - String v{arena}; - v.assign(longText); - v.append(" with some extra content to force a reallocation"); - AssertThat(&v.GetArena(), Equals(static_cast(&arena))); - AssertThat(v.starts_with("This string"), Is().True()); - }); - }); + it("Can copy into an arena", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String original{longText}; + String v{arena, original}; + AssertThat(v, Equals(original)); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + }); - describe("Strings helpers", []() - { - it("RemoveFromStart", [&]() - { - String v{"KiwiApple"}; - Strings::RemoveFromStart(v, 4); - AssertThat(v, Equals("Apple")); - Strings::RemoveFromStart(v, 100); - AssertThat(v.empty(), Is().True()); + it("Keeps its arena when assigned", [&]() + { + MonoLinearArena arena{Memory::KB * 4}; + String v{arena}; + v.assign(longText); + v.append(" with some extra content to force a reallocation"); + AssertThat(&v.GetArena(), Equals(static_cast(&arena))); + AssertThat(v.starts_with("This string"), Is().True()); + }); }); - it("RemoveFromEnd", [&]() + describe("Strings helpers", []() { - String v{"KiwiApple"}; - Strings::RemoveFromEnd(v, 5); - AssertThat(v, Equals("Kiwi")); - Strings::RemoveFromEnd(v, StringView{"wi"}); - AssertThat(v, Equals("Ki")); - Strings::RemoveFromEnd(v, 100); - AssertThat(v.empty(), Is().True()); - }); + it("RemoveFromStart", [&]() + { + String v{"KiwiApple"}; + Strings::RemoveFromStart(v, 4); + AssertThat(v, Equals("Apple")); + Strings::RemoveFromStart(v, 100); + AssertThat(v.empty(), Is().True()); + }); - it("RemoveCharFromEnd", [&]() - { - String v{"Kiwi!"}; - AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().True()); - AssertThat(v, Equals("Kiwi")); - AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().False()); - AssertThat(v, Equals("Kiwi")); - }); + it("RemoveFromEnd", [&]() + { + String v{"KiwiApple"}; + Strings::RemoveFromEnd(v, 5); + AssertThat(v, Equals("Kiwi")); + Strings::RemoveFromEnd(v, StringView{"wi"}); + AssertThat(v, Equals("Ki")); + Strings::RemoveFromEnd(v, 100); + AssertThat(v.empty(), Is().True()); + }); - it("ToSentenceCase", [&]() - { - AssertThat(Strings::ToSentenceCase(""), Equals("")); - AssertThat(Strings::ToSentenceCase("papa"), Equals("Papa")); - AssertThat(Strings::ToSentenceCase("papa "), Equals("Papa ")); - AssertThat(Strings::ToSentenceCase("papa3"), Equals("Papa 3")); - AssertThat(Strings::ToSentenceCase("MisterPotato"), Equals("Mister Potato")); - }); + it("RemoveCharFromEnd", [&]() + { + String v{"Kiwi!"}; + AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().True()); + AssertThat(v, Equals("Kiwi")); + AssertThat(Strings::RemoveCharFromEnd(v, '!'), Is().False()); + AssertThat(v, Equals("Kiwi")); + }); - it("Convert u16 to u8", [&]() - { - TString utf16string{0x41, 0x0448, 0x65e5, 0xd834, 0xdd1e}; - TString u = Strings::Convert>(utf16string); - AssertThat(u.size(), Equals(10u)); - }); - it("Convert u8 to u16", [&]() - { - TString utf8_with_surrogates = "\xe6\x97\xa5\xd1\x88\xf0\x9d\x84\x9e"; - TString utf16result = - Strings::Convert>(utf8_with_surrogates); - AssertThat(utf16result.size(), Equals(4u)); - AssertThat(utf16result[2] == 0xd834, Is().True()); - AssertThat(utf16result[3] == 0xdd1e, Is().True()); - }); - it("Convert u32 to u8", [&]() - { - TString utf32string = {0x448, 0x65E5, 0x10346}; - TString utf8result = Strings::Convert>(utf32string); - AssertThat(utf8result.size(), Equals(9u)); - }); - it("Convert u8 to u32", [&]() - { - TString twochars = "\xe6\x97\xa5\xd1\x88"; - TString utf32result = Strings::Convert>(twochars); - AssertThat(utf32result.size(), Equals(2u)); + it("ToSentenceCase", [&]() + { + AssertThat(Strings::ToSentenceCase(""), Equals("")); + AssertThat(Strings::ToSentenceCase("papa"), Equals("Papa")); + AssertThat(Strings::ToSentenceCase("papa "), Equals("Papa ")); + AssertThat(Strings::ToSentenceCase("papa3"), Equals("Papa 3")); + AssertThat(Strings::ToSentenceCase("MisterPotato"), Equals("Mister Potato")); + }); + + it("Convert u16 to u8", [&]() + { + TString utf16string{0x41, 0x0448, 0x65e5, 0xd834, 0xdd1e}; + TString u = Strings::Convert>(utf16string); + AssertThat(u.size(), Equals(10u)); + }); + it("Convert u8 to u16", [&]() + { + TString utf8_with_surrogates = "\xe6\x97\xa5\xd1\x88\xf0\x9d\x84\x9e"; + TString utf16result = + Strings::Convert>(utf8_with_surrogates); + AssertThat(utf16result.size(), Equals(4u)); + AssertThat(utf16result[2] == 0xd834, Is().True()); + AssertThat(utf16result[3] == 0xdd1e, Is().True()); + }); + it("Convert u32 to u8", [&]() + { + TString utf32string = {0x448, 0x65E5, 0x10346}; + TString utf8result = Strings::Convert>(utf32string); + AssertThat(utf8result.size(), Equals(9u)); + }); + it("Convert u8 to u32", [&]() + { + TString twochars = "\xe6\x97\xa5\xd1\x88"; + TString utf32result = Strings::Convert>(twochars); + AssertThat(utf32result.size(), Equals(2u)); + }); }); }); }); diff --git a/Tests/Core/StringView.spec.cpp b/Tests/Core/StringView.spec.cpp index 359321c9..1e54e70d 100644 --- a/Tests/Core/StringView.spec.cpp +++ b/Tests/Core/StringView.spec.cpp @@ -1,8 +1,8 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #include -#include #include +#include using namespace snowhouse; @@ -12,101 +12,104 @@ using namespace p; go_bandit([]() { - describe("Core.StringView", []() + describe("Strings", []() { - it("Can assign from literal", [&]() + describe("StringView", []() { - StringView v{"Kiwi"}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); - }); + it("Can assign from literal", [&]() + { + StringView v{"Kiwi"}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4)); + }); - it("Can assign from string", [&]() - { - String str{"Kiwi"}; - StringView v{str}; - AssertThat(v, Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); - }); + it("Can assign from string", [&]() + { + String str{"Kiwi"}; + StringView v{str}; + AssertThat(v, Equals("Kiwi")); + AssertThat(v.size(), Equals(4)); + }); - it("Can copy empty", [&]() - { - StringView str{}; - StringView str2{" "}; - AssertThat(str.empty(), Equals(true)); - AssertThat((u8*)str.data(), Equals(nullptr)); - AssertThat(str2.empty(), Equals(false)); - AssertThat((u8*)str2.data(), !Equals(nullptr)); - str2 = str; - AssertThat(str2.empty(), Equals(true)); - AssertThat((u8*)str2.data(), Equals(nullptr)); - }); + it("Can copy empty", [&]() + { + StringView str{}; + StringView str2{" "}; + AssertThat(str.empty(), Equals(true)); + AssertThat((u8*)str.data(), Equals(nullptr)); + AssertThat(str2.empty(), Equals(false)); + AssertThat((u8*)str2.data(), !Equals(nullptr)); + str2 = str; + AssertThat(str2.empty(), Equals(true)); + AssertThat((u8*)str2.data(), Equals(nullptr)); + }); - it("Can retrieve string data", [&]() - { - StringView v{"Kiwi"}; - AssertThat(v.data(), Equals("Kiwi")); - AssertThat(v.size(), Equals(4)); - StringView v2{}; - AssertThat((u8*)v2.data(), Equals(nullptr)); - AssertThat(v2.size(), Equals(0)); - }); + it("Can retrieve string data", [&]() + { + StringView v{"Kiwi"}; + AssertThat(v.data(), Equals("Kiwi")); + AssertThat(v.size(), Equals(4)); + StringView v2{}; + AssertThat((u8*)v2.data(), Equals(nullptr)); + AssertThat(v2.size(), Equals(0)); + }); - it("Can compare", [&]() - { - StringView vKiwi{"Kiwi"}; - StringView vKiwi2{"Kiwi"}; - StringView vApple{"Apple"}; - AssertThat(vKiwi, Equals(vKiwi2)); - AssertThat(vKiwi, !Equals(vApple)); - }); + it("Can compare", [&]() + { + StringView vKiwi{"Kiwi"}; + StringView vKiwi2{"Kiwi"}; + StringView vApple{"Apple"}; + AssertThat(vKiwi, Equals(vKiwi2)); + AssertThat(vKiwi, !Equals(vApple)); + }); - it("Can copy", [&]() - { - StringView vKiwi{"Kiwi"}; - StringView vApple{"Apple"}; - StringView vCopy = vKiwi; - AssertThat(vCopy, Equals("Kiwi")); - AssertThat(vCopy, Equals(vKiwi)); - AssertThat(vCopy, !Equals(vApple)); - vCopy = vApple; - AssertThat(vCopy, Equals("Apple")); - AssertThat(vCopy, !Equals(vKiwi)); - AssertThat(vCopy, Equals(vApple)); - }); + it("Can copy", [&]() + { + StringView vKiwi{"Kiwi"}; + StringView vApple{"Apple"}; + StringView vCopy = vKiwi; + AssertThat(vCopy, Equals("Kiwi")); + AssertThat(vCopy, Equals(vKiwi)); + AssertThat(vCopy, !Equals(vApple)); + vCopy = vApple; + AssertThat(vCopy, Equals("Apple")); + AssertThat(vCopy, !Equals(vKiwi)); + AssertThat(vCopy, Equals(vApple)); + }); - it("Can move", [&]() - { - StringView vKiwi{"Kiwi"}; - StringView vApple{"Apple"}; - StringView vMove = Move(vKiwi); - AssertThat(vMove, Equals("Kiwi")); - vMove = Move(vApple); - AssertThat(vMove, Equals("Apple")); - }); + it("Can move", [&]() + { + StringView vKiwi{"Kiwi"}; + StringView vApple{"Apple"}; + StringView vMove = Move(vKiwi); + AssertThat(vMove, Equals("Kiwi")); + vMove = Move(vApple); + AssertThat(vMove, Equals("Apple")); + }); - describe("Strings", []() - { - it("Can Find", [&]() + describe("Strings", []() { - StringView v{"Kiwiwi"}; + it("Can Find", [&]() + { + StringView v{"Kiwiwi"}; - // Find Chars - AssertThat(Strings::Find(v, 'K', FindDir::Front), Equals(0)); - AssertThat(Strings::Find(v, 'K', FindDir::Back), Equals(0)); - AssertThat(Strings::Find(v, 'i', FindDir::Front), Equals(1)); - AssertThat(Strings::Find(v, 'i', FindDir::Back), Equals(5)); - // Find last chars - AssertThat(Strings::Find(v, 'w', FindDir::Front, true), Equals(0)); // 'K' - AssertThat(Strings::Find(v, 'w', FindDir::Back, true), Equals(5)); // 'i' - AssertThat(Strings::Find(v, 'K', FindDir::Front, true), Equals(1)); // 'i' - AssertThat(Strings::Find(v, 'i', FindDir::Back, true), Equals(4)); // 'w' + // Find Chars + AssertThat(Strings::Find(v, 'K', FindDir::Front), Equals(0)); + AssertThat(Strings::Find(v, 'K', FindDir::Back), Equals(0)); + AssertThat(Strings::Find(v, 'i', FindDir::Front), Equals(1)); + AssertThat(Strings::Find(v, 'i', FindDir::Back), Equals(5)); + // Find last chars + AssertThat(Strings::Find(v, 'w', FindDir::Front, true), Equals(0)); // 'K' + AssertThat(Strings::Find(v, 'w', FindDir::Back, true), Equals(5)); // 'i' + AssertThat(Strings::Find(v, 'K', FindDir::Front, true), Equals(1)); // 'i' + AssertThat(Strings::Find(v, 'i', FindDir::Back, true), Equals(4)); // 'w' - // Find Sub-strings - AssertThat(Strings::Find(v, "Ki", FindDir::Front), Equals(0)); - AssertThat(Strings::Find(v, "Ki", FindDir::Back), Equals(0)); - AssertThat(Strings::Find(v, "wi", FindDir::Front), Equals(2)); - AssertThat(Strings::Find(v, "wi", FindDir::Back), Equals(4)); + // Find Sub-strings + AssertThat(Strings::Find(v, "Ki", FindDir::Front), Equals(0)); + AssertThat(Strings::Find(v, "Ki", FindDir::Back), Equals(0)); + AssertThat(Strings::Find(v, "wi", FindDir::Front), Equals(2)); + AssertThat(Strings::Find(v, "wi", FindDir::Back), Equals(4)); + }); }); }); }); diff --git a/Tests/Memory/MemoryStats.spec.cpp b/Tests/Memory/MemoryStats.spec.cpp index 86005ad6..b717ca1e 100644 --- a/Tests/Memory/MemoryStats.spec.cpp +++ b/Tests/Memory/MemoryStats.spec.cpp @@ -224,7 +224,7 @@ go_bandit([]() } }); - it("live and frees bitsets match events", [&]() + it("live bitset and free flags match events", [&]() { MemoryStats s; s.detectLeaks = false; @@ -237,9 +237,9 @@ go_bandit([]() s.CollectStats(); AssertThat(s.events.Size(), Is().EqualTo(5)); - AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); - AssertThat(s.frees.IsSet(3), Is().EqualTo(true)); - AssertThat(s.frees.IsSet(4), Is().EqualTo(true)); + AssertThat(FreeCount(s), Is().EqualTo(2)); + AssertThat(s.events[3].IsFree(), Is().EqualTo(true)); + AssertThat(s.events[4].IsFree(), Is().EqualTo(true)); AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); AssertThat(s.live.IsSet(0), Is().EqualTo(true)); AssertThat(s.live.IsSet(1), Is().EqualTo(true)); @@ -247,10 +247,10 @@ go_bandit([]() AssertThat(s.live.IsSet(3), Is().EqualTo(false)); AssertThat(s.live.IsSet(4), Is().EqualTo(false)); - // Re-collecting must rebuild bitsets identically. + // Re-collecting must rebuild the bitset identically. s.CollectStats(); AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); - AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); + AssertThat(FreeCount(s), Is().EqualTo(2)); }); it("Alternating instances on one thread", [&]() @@ -317,7 +317,7 @@ go_bandit([]() s.Remove((void*)0x1000, 64); s.CollectStats(); AssertThat(s.live.CountSetBits(), Is().EqualTo(0)); - AssertThat(s.frees.CountSetBits(), Is().EqualTo(2)); + AssertThat(FreeCount(s), Is().EqualTo(2)); }); it("Ignores null ptr in Remove", [&]() @@ -438,7 +438,7 @@ go_bandit([]() s.CollectStats(); AssertThat(AllocCount(s), Is().EqualTo(N)); AssertThat(s.live.CountSetBits(), Is().EqualTo(N / 2)); - AssertThat(s.frees.CountSetBits(), Is().EqualTo(N / 2)); + AssertThat(FreeCount(s), Is().EqualTo(N / 2)); }); }); From a1fbc246b1697bb425585f5cb5aac00d75ee9741 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 1 Sep 2026 21:47:29 +0200 Subject: [PATCH 03/15] Memory Stats fixes and optimizations --- Include/Misc/PipeDebug.h | 6 +- Include/Pipe/Core/Guid.h | 2 +- Include/Pipe/Core/SpinLock.h | 304 +++++ Include/Pipe/Core/Tag.h | 2 +- Include/Pipe/Extern/sparse_growth_policy.h | 643 +++++----- Include/Pipe/Extern/sparse_hash.h | 14 +- Include/Pipe/Extern/utf8.h | 6 +- Include/Pipe/Extern/utf8/checked.h | 730 ++++++------ Include/Pipe/Extern/utf8/core.h | 1223 +++++++++++--------- Include/Pipe/Extern/utf8/cpp11.h | 61 +- Include/Pipe/Extern/utf8/cpp17.h | 124 +- Include/Pipe/Extern/utf8/cpp20.h | 180 +-- Include/Pipe/Extern/utf8/unchecked.h | 500 ++++---- Include/Pipe/Files/Files.h | 2 +- Include/Pipe/Files/Paths.h | 2 +- Include/Pipe/Memory/MemoryStats.h | 50 +- Src/Core/Checks.cpp | 6 +- Src/Core/Subprocess.cpp | 2 +- Src/Files/PlatformPaths.cpp | 2 +- Src/Memory/MemoryStats.cpp | 210 ++-- Src/Pipe.cpp | 2 +- Src/PipeMemoryArenas.cpp | 4 +- Tests/Core/SpinLock.spec.cpp | 187 +++ Tests/Memory/MemoryStats.spec.cpp | 16 +- Tests/Reflection/TypeName.spec.cpp | 2 +- 25 files changed, 2498 insertions(+), 1782 deletions(-) create mode 100644 Include/Pipe/Core/SpinLock.h create mode 100644 Tests/Core/SpinLock.spec.cpp diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index e26ca876..fdb7470e 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -25,11 +25,11 @@ static_assert(false, "Imgui v" IMGUI_VERSION " found but PipeDebug requires v1.9 #include "Pipe/Core/Checks.h" #include "Pipe/Core/Map.h" #include "Pipe/Core/Set.h" -#include "PipeStrings.h" #include "Pipe/Memory/MemoryStats.h" #include "PipeColor.h" #include "PipeContainers.h" #include "PipeECS.h" +#include "PipeStrings.h" namespace p @@ -2673,8 +2673,8 @@ namespace p // HEX and String values if ((hexStripW > 0.0f || stringStripW > 0.0f) && !memoryDbg.snapshots.IsEmpty()) { - const sizet viewLo = static_cast(viewStart); - const sizet viewHi = static_cast(viewStart + viewRange); + const sizet viewLo = static_cast(viewStart); + const sizet viewHi = static_cast(viewStart + viewRange); if (pixelsPerByte * bytesPerLine >= 13) { for (i32 a = 0; a < memoryDbg.snapshots.Size(); ++a) diff --git a/Include/Pipe/Core/Guid.h b/Include/Pipe/Core/Guid.h index 75492c52..4262baa5 100644 --- a/Include/Pipe/Core/Guid.h +++ b/Include/Pipe/Core/Guid.h @@ -2,9 +2,9 @@ #pragma once -#include "PipeStrings.h" #include "PipeAlgorithms.h" #include "PipeSerializeFwd.h" +#include "PipeStrings.h" namespace p diff --git a/Include/Pipe/Core/SpinLock.h b/Include/Pipe/Core/SpinLock.h new file mode 100644 index 00000000..bdeddfd7 --- /dev/null +++ b/Include/Pipe/Core/SpinLock.h @@ -0,0 +1,304 @@ +// Copyright 2015-2026 Piperift. All Rights Reserved. +// Based on https://github.com/fpikus/ConcurrentCpp + +#pragma once + +#include "PipePlatform.h" + +#include +#include +#include + + +namespace p +{ + P_FORCEINLINE static void YieldLockThread() noexcept + { + std::this_thread::yield(); + } + + P_FORCEINLINE static void SleepLockBackoff() noexcept + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + /** Spinlock with test-and-test-and-set (TTAS) and two-tier backoff. */ + class SpinLock + { + private: + std::atomic flag{0}; + + public: + SpinLock() = default; + SpinLock(const SpinLock&) = delete; + SpinLock& operator=(const SpinLock&) = delete; + + // Acquire the lock, blocking (spinning, then sleeping) until held. + void Lock() noexcept + { + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + if (!(flag.load(std::memory_order_relaxed) + || flag.exchange(1, std::memory_order_acquire))) + { + return; + } + } + if (spin_count < 8) // first 8 spin_count: yield, stay hot + { + YieldLockThread(); + } + else // then escalate to a real sleep and start the yield budget over + { + spin_count = 0; + SleepLockBackoff(); + } + } // spin/back-off loop + } + + /** Bounded acquire: returns true holding the lock, false if it could not be taken within + * the short-spin budget. + */ + bool TryLock() noexcept + { + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + if (!(flag.load(std::memory_order_relaxed) + || flag.exchange(1, std::memory_order_acquire))) + { + return true; + } + } + if (spin_count < 8) // spin/yield through the short tier... + { + YieldLockThread(); + } + else // ...then give up instead of long-sleeping + { + return false; + } + } // bounded spin loop + return true; // for-condition saw the lock free and the exchange won it + } + + // Release the lock. + void Unlock() noexcept + { + flag.store(0, std::memory_order_release); + } + + // Advisory, non-synchronizing peek. The answer may be stale the instant + // it returns, so only use for asserts/diagnostics, never for exclusion. + bool Locked() const noexcept + { + return flag.load(std::memory_order_relaxed) == 1; + } + }; + + /** Shared (read-write) spinlock. + * + * Many readers may hold concurrently, but an exclusive writer excludes everyone. + * Writers wait for all readers to drain; readers wait for any pending writer. + */ + class SharedSpinLock + { + private: + // Top bit of the reader counter is the exclusive (writer) flag. The + // remaining bits count shared (reader) holders, so at most + // 2^31 - 1 readers may hold simultaneously. + static constexpr u32 exclusiveBit = 1u << 31; + + std::atomic flag{0}; + + public: + SharedSpinLock() = default; + SharedSpinLock(const SharedSpinLock&) = delete; + SharedSpinLock& operator=(const SharedSpinLock&) = delete; + + // Exclusive (writer) acquire. Waits while readers or a writer hold. + void LockExclusive() noexcept + { + u32 current = 0; + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + current = flag.load(std::memory_order_relaxed); + if (current == 0 + && flag.compare_exchange_strong(current, exclusiveBit, + std::memory_order_acquire, std::memory_order_relaxed)) + { + return; + } + } + if (spin_count < 8) // first 8 spin_count: yield, stay hot + { + YieldLockThread(); + } + else // then escalate to a real sleep and start the yield budget over + { + spin_count = 0; + SleepLockBackoff(); + } + } // spin/back-off loop + } + + // Bounded exclusive acquire, same semantics as SpinLock::TryLock: spins + // and yields through the short budget before giving up. + bool TryLockExclusive() noexcept + { + u32 current = 0; + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + current = flag.load(std::memory_order_relaxed); + if (current == 0 + && flag.compare_exchange_strong(current, exclusiveBit, + std::memory_order_acquire, std::memory_order_relaxed)) + { + return true; + } + } + if (spin_count < 8) // spin/yield through the short tier... + { + YieldLockThread(); + } + else // ...then give up instead of long-sleeping + { + return false; + } + } // bounded spin loop + } + + void UnlockExclusive() noexcept + { + flag.store(0, std::memory_order_release); + } + + // Shared (reader) acquire. Waits while a writer holds or is pending. + void LockShared() noexcept + { + u32 current = 0; + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + current = flag.load(std::memory_order_relaxed); + if ((current & exclusiveBit) == 0 + && flag.compare_exchange_strong(current, current + 1, + std::memory_order_acquire, std::memory_order_relaxed)) + { + return; + } + } + if (spin_count < 8) // first 8 spin_count: yield, stay hot + { + YieldLockThread(); + } + else // then escalate to a real sleep and start the yield budget over + { + spin_count = 0; + SleepLockBackoff(); + } + } // spin/back-off loop + } + + // Bounded shared acquire, same semantics as SpinLock::TryLock. + bool TryLockShared() noexcept + { + u32 current = 0; + for (int spin_count = 0;; ++spin_count) + { + for (i32 i = 0; i < 8; ++i) + { + current = flag.load(std::memory_order_relaxed); + if ((current & exclusiveBit) == 0 + && flag.compare_exchange_strong(current, current + 1, + std::memory_order_acquire, std::memory_order_relaxed)) + { + return true; + } + } + if (spin_count < 8) // spin/yield through the short tier... + { + YieldLockThread(); + } + else // ...then give up instead of long-sleeping + { + return false; + } + } // bounded spin loop + return true; // for-condition saw the lock free and the exchange won it + } + + void UnlockShared() noexcept + { + flag.fetch_sub(1, std::memory_order_release); + } + }; + + // RAII guard for SpinLock. + class ScopedLock + { + public: + explicit ScopedLock(SpinLock& lock) noexcept : lock(lock) + { + lock.Lock(); + } + ~ScopedLock() + { + lock.Unlock(); + } + + ScopedLock(const ScopedLock&) = delete; + ScopedLock& operator=(const ScopedLock&) = delete; + + private: + SpinLock& lock; + }; + + // RAII guard for the exclusive (writer) side of SharedSpinLock. + class ExclusiveScopedLock + { + public: + explicit ExclusiveScopedLock(SharedSpinLock& lock) noexcept : lock(lock) + { + lock.LockExclusive(); + } + ~ExclusiveScopedLock() + { + lock.UnlockExclusive(); + } + + ExclusiveScopedLock(const ExclusiveScopedLock&) = delete; + ExclusiveScopedLock& operator=(const ExclusiveScopedLock&) = delete; + + private: + SharedSpinLock& lock; + }; + + // RAII guard for the shared (reader) side of SharedSpinLock. + class SharedScopedLock + { + public: + explicit SharedScopedLock(SharedSpinLock& lock) noexcept : lock(lock) + { + lock.LockShared(); + } + ~SharedScopedLock() + { + lock.UnlockShared(); + } + + SharedScopedLock(const SharedScopedLock&) = delete; + SharedScopedLock& operator=(const SharedScopedLock&) = delete; + + private: + SharedSpinLock& lock; + }; +} // namespace p diff --git a/Include/Pipe/Core/Tag.h b/Include/Pipe/Core/Tag.h index b6740c5a..0ef0e676 100644 --- a/Include/Pipe/Core/Tag.h +++ b/Include/Pipe/Core/Tag.h @@ -1,10 +1,10 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #pragma once -#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" #include "PipeSerializeFwd.h" +#include "PipeStrings.h" #include diff --git a/Include/Pipe/Extern/sparse_growth_policy.h b/Include/Pipe/Extern/sparse_growth_policy.h index 0b7622b5..a35342b7 100644 --- a/Include/Pipe/Extern/sparse_growth_policy.h +++ b/Include/Pipe/Extern/sparse_growth_policy.h @@ -35,307 +35,360 @@ #ifndef TSL_NO_EXCEPTIONS -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ - (defined(_MSC_VER) && defined(_CPPUNWIND))) -#define TSL_NO_EXCEPTIONS 0 -#else -#define TSL_NO_EXCEPTIONS 1 -#endif + #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) \ + || (defined(_MSC_VER) && defined(_CPPUNWIND))) + #define TSL_NO_EXCEPTIONS 0 + #else + #define TSL_NO_EXCEPTIONS 1 + #endif #endif #if TSL_NO_EXCEPTIONS -#include -#ifdef TSL_DEBUG -#include -#define TSL_SH_THROW_OR_ABORT(ex, msg) \ - do { \ - std::fprintf(stderr, "error: %s\n", msg); \ - std::abort(); \ - } while (0) + #include + #ifdef TSL_DEBUG + #include + #define TSL_SH_THROW_OR_ABORT(ex, msg) \ + do \ + { \ + std::fprintf(stderr, "error: %s\n", msg); \ + std::abort(); \ + } while (0) + #else + #define TSL_SH_THROW_OR_ABORT(ex, msg) std::abort() + #endif + #define TSL_SH_TRY if (true) + #define TSL_SH_CATCH(x) if (false) + #define TSL_SH_RETRHOW #else -#define TSL_SH_THROW_OR_ABORT(ex, msg) std::abort() -#endif -#define TSL_SH_TRY if (true) -#define TSL_SH_CATCH(x) if (false) -#define TSL_SH_RETRHOW -#else -#include + #include -#define TSL_SH_THROW_OR_ABORT(ex, msg) throw ex(msg) -#define TSL_SH_TRY try -#define TSL_SH_CATCH(x) catch (x) -#define TSL_SH_RETRHOW throw + #define TSL_SH_THROW_OR_ABORT(ex, msg) throw ex(msg) + #define TSL_SH_TRY try + #define TSL_SH_CATCH(x) catch (x) + #define TSL_SH_RETRHOW throw #endif -namespace tsl { -namespace sh { - -/** - * Grow the hash table by a factor of GrowthFactor keeping the bucket count to a - * power of two. It allows the table to use a mask operation instead of a modulo - * operation to map a hash to a bucket. - * - * GrowthFactor must be a power of two >= 2. - */ -template -class power_of_two_growth_policy { - public: - /** - * Called on the hash table creation and on rehash. The number of buckets for - * the table is passed in parameter. This number is a minimum, the policy may - * update this value with a higher value if needed (but not lower). - * - * If 0 is given, min_bucket_count_in_out must still be 0 after the policy - * creation and bucket_for_hash must always return 0 in this case. - */ - explicit power_of_two_growth_policy(std::size_t &min_bucket_count_in_out) { - if (min_bucket_count_in_out > max_bucket_count()) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - if (min_bucket_count_in_out > 0) { - min_bucket_count_in_out = - round_up_to_power_of_two(min_bucket_count_in_out); - m_mask = min_bucket_count_in_out - 1; - } else { - m_mask = 0; - } - } - - /** - * Return the bucket [0, bucket_count()) to which the hash belongs. - * If bucket_count() is 0, it must always return 0. - */ - std::size_t bucket_for_hash(std::size_t hash) const noexcept { - return hash & m_mask; - } - - /** - * Return the number of buckets that should be used on next growth. - */ - std::size_t next_bucket_count() const { - if ((m_mask + 1) > max_bucket_count() / GrowthFactor) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - return (m_mask + 1) * GrowthFactor; - } - - /** - * Return the maximum number of buckets supported by the policy. - */ - std::size_t max_bucket_count() const { - // Largest power of two. - return (std::numeric_limits::max() / 2) + 1; - } - - /** - * Reset the growth policy as if it was created with a bucket count of 0. - * After a clear, the policy must always return 0 when bucket_for_hash is - * called. - */ - void clear() noexcept { m_mask = 0; } - - private: - static std::size_t round_up_to_power_of_two(std::size_t value) { - if (is_power_of_two(value)) { - return value; - } - - if (value == 0) { - return 1; - } - - --value; - for (std::size_t i = 1; i < sizeof(std::size_t) * CHAR_BIT; i *= 2) { - value |= value >> i; - } - - return value + 1; - } - - static constexpr bool is_power_of_two(std::size_t value) { - return value != 0 && (value & (value - 1)) == 0; - } - - protected: - static_assert(is_power_of_two(GrowthFactor) && GrowthFactor >= 2, - "GrowthFactor must be a power of two >= 2."); - - std::size_t m_mask; -}; - -/** - * Grow the hash table by GrowthFactor::num / GrowthFactor::den and use a modulo - * to map a hash to a bucket. Slower but it can be useful if you want a slower - * growth. - */ -template > -class mod_growth_policy { - public: - explicit mod_growth_policy(std::size_t &min_bucket_count_in_out) { - if (min_bucket_count_in_out > max_bucket_count()) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - if (min_bucket_count_in_out > 0) { - m_mod = min_bucket_count_in_out; - } else { - m_mod = 1; - } - } - - std::size_t bucket_for_hash(std::size_t hash) const noexcept { - return hash % m_mod; - } - - std::size_t next_bucket_count() const { - if (m_mod == max_bucket_count()) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - const double next_bucket_count = - std::ceil(double(m_mod) * REHASH_SIZE_MULTIPLICATION_FACTOR); - if (!std::isnormal(next_bucket_count)) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - if (next_bucket_count > double(max_bucket_count())) { - return max_bucket_count(); - } else { - return std::size_t(next_bucket_count); - } - } - - std::size_t max_bucket_count() const { return MAX_BUCKET_COUNT; } - - void clear() noexcept { m_mod = 1; } - - private: - static constexpr double REHASH_SIZE_MULTIPLICATION_FACTOR = - 1.0 * GrowthFactor::num / GrowthFactor::den; - static const std::size_t MAX_BUCKET_COUNT = - std::size_t(double(std::numeric_limits::max() / - REHASH_SIZE_MULTIPLICATION_FACTOR)); - - static_assert(REHASH_SIZE_MULTIPLICATION_FACTOR >= 1.1, - "Growth factor should be >= 1.1."); - - std::size_t m_mod; -}; - -/** - * Grow the hash table by using prime numbers as bucket count. Slower than - * tsl::sh::power_of_two_growth_policy in general but will probably distribute - * the values around better in the buckets with a poor hash function. - * - * To allow the compiler to optimize the modulo operation, a lookup table is - * used with constant primes numbers. - * - * With a switch the code would look like: - * \code - * switch(iprime) { // iprime is the current prime of the hash table - * case 0: hash % 5ul; - * break; - * case 1: hash % 17ul; - * break; - * case 2: hash % 29ul; - * break; - * ... - * } - * \endcode - * - * Due to the constant variable in the modulo the compiler is able to optimize - * the operation by a series of multiplications, substractions and shifts. - * - * The 'hash % 5' could become something like 'hash - (hash * 0xCCCCCCCD) >> 34) - * * 5' in a 64 bits environment. - */ -class prime_growth_policy { - public: - explicit prime_growth_policy(std::size_t &min_bucket_count_in_out) { - auto it_prime = std::lower_bound(primes().begin(), primes().end(), - min_bucket_count_in_out); - if (it_prime == primes().end()) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - m_iprime = - static_cast(std::distance(primes().begin(), it_prime)); - if (min_bucket_count_in_out > 0) { - min_bucket_count_in_out = *it_prime; - } else { - min_bucket_count_in_out = 0; - } - } - - std::size_t bucket_for_hash(std::size_t hash) const noexcept { - return mod_prime()[m_iprime](hash); - } - - std::size_t next_bucket_count() const { - if (m_iprime + 1 >= primes().size()) { - TSL_SH_THROW_OR_ABORT(std::length_error, - "The hash table exceeds its maximum size."); - } - - return primes()[m_iprime + 1]; - } - - std::size_t max_bucket_count() const { return primes().back(); } - - void clear() noexcept { m_iprime = 0; } - - private: - static const std::array &primes() { - static const std::array PRIMES = { - {1ul, 5ul, 17ul, 29ul, 37ul, - 53ul, 67ul, 79ul, 97ul, 131ul, - 193ul, 257ul, 389ul, 521ul, 769ul, - 1031ul, 1543ul, 2053ul, 3079ul, 6151ul, - 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, - 393241ul, 786433ul, 1572869ul, 3145739ul, 6291469ul, - 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, - 402653189ul, 805306457ul, 1610612741ul, 3221225473ul, 4294967291ul}}; - - static_assert( - std::numeric_limits::max() >= PRIMES.size(), - "The type of m_iprime is not big enough."); - - return PRIMES; - } - - static const std::array &mod_prime() { - // MOD_PRIME[iprime](hash) returns hash % PRIMES[iprime]. This table allows - // for faster modulo as the compiler can optimize the modulo code better - // with a constant known at the compilation. - static const std::array MOD_PRIME = { - {&mod<0>, &mod<1>, &mod<2>, &mod<3>, &mod<4>, &mod<5>, &mod<6>, - &mod<7>, &mod<8>, &mod<9>, &mod<10>, &mod<11>, &mod<12>, &mod<13>, - &mod<14>, &mod<15>, &mod<16>, &mod<17>, &mod<18>, &mod<19>, &mod<20>, - &mod<21>, &mod<22>, &mod<23>, &mod<24>, &mod<25>, &mod<26>, &mod<27>, - &mod<28>, &mod<29>, &mod<30>, &mod<31>, &mod<32>, &mod<33>, &mod<34>, - &mod<35>, &mod<36>, &mod<37>, &mod<38>, &mod<39>}}; - - return MOD_PRIME; - } - - template - static std::size_t mod(std::size_t hash) { - return hash % primes()[IPrime]; - } - - private: - unsigned int m_iprime; -}; - -} // namespace sh -} // namespace tsl +namespace tsl +{ + namespace sh + { + + /** + * Grow the hash table by a factor of GrowthFactor keeping the bucket count to a + * power of two. It allows the table to use a mask operation instead of a modulo + * operation to map a hash to a bucket. + * + * GrowthFactor must be a power of two >= 2. + */ + template + class power_of_two_growth_policy + { + public: + /** + * Called on the hash table creation and on rehash. The number of buckets for + * the table is passed in parameter. This number is a minimum, the policy may + * update this value with a higher value if needed (but not lower). + * + * If 0 is given, min_bucket_count_in_out must still be 0 after the policy + * creation and bucket_for_hash must always return 0 in this case. + */ + explicit power_of_two_growth_policy(std::size_t& min_bucket_count_in_out) + { + if (min_bucket_count_in_out > max_bucket_count()) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + if (min_bucket_count_in_out > 0) + { + min_bucket_count_in_out = round_up_to_power_of_two(min_bucket_count_in_out); + m_mask = min_bucket_count_in_out - 1; + } + else + { + m_mask = 0; + } + } + + /** + * Return the bucket [0, bucket_count()) to which the hash belongs. + * If bucket_count() is 0, it must always return 0. + */ + std::size_t bucket_for_hash(std::size_t hash) const noexcept + { + return hash & m_mask; + } + + /** + * Return the number of buckets that should be used on next growth. + */ + std::size_t next_bucket_count() const + { + if ((m_mask + 1) > max_bucket_count() / GrowthFactor) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + return (m_mask + 1) * GrowthFactor; + } + + /** + * Return the maximum number of buckets supported by the policy. + */ + std::size_t max_bucket_count() const + { + // Largest power of two. + return (std::numeric_limits::max() / 2) + 1; + } + + /** + * Reset the growth policy as if it was created with a bucket count of 0. + * After a clear, the policy must always return 0 when bucket_for_hash is + * called. + */ + void clear() noexcept + { + m_mask = 0; + } + + private: + static std::size_t round_up_to_power_of_two(std::size_t value) + { + if (is_power_of_two(value)) + { + return value; + } + + if (value == 0) + { + return 1; + } + + --value; + for (std::size_t i = 1; i < sizeof(std::size_t) * CHAR_BIT; i *= 2) + { + value |= value >> i; + } + + return value + 1; + } + + static constexpr bool is_power_of_two(std::size_t value) + { + return value != 0 && (value & (value - 1)) == 0; + } + + protected: + static_assert(is_power_of_two(GrowthFactor) && GrowthFactor >= 2, + "GrowthFactor must be a power of two >= 2."); + + std::size_t m_mask; + }; + + /** + * Grow the hash table by GrowthFactor::num / GrowthFactor::den and use a modulo + * to map a hash to a bucket. Slower but it can be useful if you want a slower + * growth. + */ + template> + class mod_growth_policy + { + public: + explicit mod_growth_policy(std::size_t& min_bucket_count_in_out) + { + if (min_bucket_count_in_out > max_bucket_count()) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + if (min_bucket_count_in_out > 0) + { + m_mod = min_bucket_count_in_out; + } + else + { + m_mod = 1; + } + } + + std::size_t bucket_for_hash(std::size_t hash) const noexcept + { + return hash % m_mod; + } + + std::size_t next_bucket_count() const + { + if (m_mod == max_bucket_count()) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + const double next_bucket_count = + std::ceil(double(m_mod) * REHASH_SIZE_MULTIPLICATION_FACTOR); + if (!std::isnormal(next_bucket_count)) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + if (next_bucket_count > double(max_bucket_count())) + { + return max_bucket_count(); + } + else + { + return std::size_t(next_bucket_count); + } + } + + std::size_t max_bucket_count() const + { + return MAX_BUCKET_COUNT; + } + + void clear() noexcept + { + m_mod = 1; + } + + private: + static constexpr double REHASH_SIZE_MULTIPLICATION_FACTOR = + 1.0 * GrowthFactor::num / GrowthFactor::den; + static const std::size_t MAX_BUCKET_COUNT = std::size_t(double( + std::numeric_limits::max() / REHASH_SIZE_MULTIPLICATION_FACTOR)); + + static_assert( + REHASH_SIZE_MULTIPLICATION_FACTOR >= 1.1, "Growth factor should be >= 1.1."); + + std::size_t m_mod; + }; + + /** + * Grow the hash table by using prime numbers as bucket count. Slower than + * tsl::sh::power_of_two_growth_policy in general but will probably distribute + * the values around better in the buckets with a poor hash function. + * + * To allow the compiler to optimize the modulo operation, a lookup table is + * used with constant primes numbers. + * + * With a switch the code would look like: + * \code + * switch(iprime) { // iprime is the current prime of the hash table + * case 0: hash % 5ul; + * break; + * case 1: hash % 17ul; + * break; + * case 2: hash % 29ul; + * break; + * ... + * } + * \endcode + * + * Due to the constant variable in the modulo the compiler is able to optimize + * the operation by a series of multiplications, substractions and shifts. + * + * The 'hash % 5' could become something like 'hash - (hash * 0xCCCCCCCD) >> 34) + * * 5' in a 64 bits environment. + */ + class prime_growth_policy + { + public: + explicit prime_growth_policy(std::size_t& min_bucket_count_in_out) + { + auto it_prime = + std::lower_bound(primes().begin(), primes().end(), min_bucket_count_in_out); + if (it_prime == primes().end()) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + m_iprime = static_cast(std::distance(primes().begin(), it_prime)); + if (min_bucket_count_in_out > 0) + { + min_bucket_count_in_out = *it_prime; + } + else + { + min_bucket_count_in_out = 0; + } + } + + std::size_t bucket_for_hash(std::size_t hash) const noexcept + { + return mod_prime()[m_iprime](hash); + } + + std::size_t next_bucket_count() const + { + if (m_iprime + 1 >= primes().size()) + { + TSL_SH_THROW_OR_ABORT( + std::length_error, "The hash table exceeds its maximum size."); + } + + return primes()[m_iprime + 1]; + } + + std::size_t max_bucket_count() const + { + return primes().back(); + } + + void clear() noexcept + { + m_iprime = 0; + } + + private: + static const std::array& primes() + { + static const std::array PRIMES = { + {1ul, 5ul, 17ul, 29ul, 37ul, 53ul, 67ul, 79ul, 97ul, 131ul, 193ul, 257ul, 389ul, + 521ul, 769ul, 1031ul, 1543ul, 2053ul, 3079ul, 6151ul, 12289ul, 24593ul, + 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, 1572869ul, 3145739ul, + 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, + 402653189ul, 805306457ul, 1610612741ul, 3221225473ul, 4294967291ul} + }; + + static_assert(std::numeric_limits::max() >= PRIMES.size(), + "The type of m_iprime is not big enough."); + + return PRIMES; + } + + static const std::array& mod_prime() + { + // MOD_PRIME[iprime](hash) returns hash % PRIMES[iprime]. This table allows + // for faster modulo as the compiler can optimize the modulo code better + // with a constant known at the compilation. + static const std::array MOD_PRIME = { + {&mod<0>, &mod<1>, &mod<2>, &mod<3>, &mod<4>, &mod<5>, &mod<6>, &mod<7>, + &mod<8>, &mod<9>, &mod<10>, &mod<11>, &mod<12>, &mod<13>, &mod<14>, + &mod<15>, &mod<16>, &mod<17>, &mod<18>, &mod<19>, &mod<20>, &mod<21>, + &mod<22>, &mod<23>, &mod<24>, &mod<25>, &mod<26>, &mod<27>, &mod<28>, + &mod<29>, &mod<30>, &mod<31>, &mod<32>, &mod<33>, &mod<34>, &mod<35>, + &mod<36>, &mod<37>, &mod<38>, &mod<39>} + }; + + return MOD_PRIME; + } + + template + static std::size_t mod(std::size_t hash) + { + return hash % primes()[IPrime]; + } + + private: + unsigned int m_iprime; + }; + + } // namespace sh +} // namespace tsl #endif diff --git a/Include/Pipe/Extern/sparse_hash.h b/Include/Pipe/Extern/sparse_hash.h index 23871daf..14c8dbc9 100644 --- a/Include/Pipe/Extern/sparse_hash.h +++ b/Include/Pipe/Extern/sparse_hash.h @@ -1355,8 +1355,8 @@ namespace tsl } sparse_hash(const sparse_hash& other) - : Allocator( - std::allocator_traits::select_on_container_copy_construction(other)) + : Allocator(std::allocator_traits::select_on_container_copy_construction( + other)) , Hash(other) , KeyEqual(other) , GrowthPolicy(other) @@ -1376,11 +1376,11 @@ namespace tsl } sparse_hash(sparse_hash&& other) noexcept( - std::is_nothrow_move_constructible::value&& - std::is_nothrow_move_constructible::value&& - std::is_nothrow_move_constructible::value&& - std::is_nothrow_move_constructible::value&& - std::is_nothrow_move_constructible::value) + std::is_nothrow_move_constructible::value + && std::is_nothrow_move_constructible::value + && std::is_nothrow_move_constructible::value + && std::is_nothrow_move_constructible::value + && std::is_nothrow_move_constructible::value) : Allocator(std::move(other)) , Hash(std::move(other)) , KeyEqual(std::move(other)) diff --git a/Include/Pipe/Extern/utf8.h b/Include/Pipe/Extern/utf8.h index b5135309..5239e897 100644 --- a/Include/Pipe/Extern/utf8.h +++ b/Include/Pipe/Extern/utf8.h @@ -34,8 +34,8 @@ and set it to one of the values used by the __cplusplus predefined macro. For instance, #define UTF_CPP_CPLUSPLUS 199711L -will cause the UTF-8 CPP library to use only types and language features available in the C++ 98 standard. -Some library features will be disabled. +will cause the UTF-8 CPP library to use only types and language features available in the C++ 98 +standard. Some library features will be disabled. If you leave UTF_CPP_CPLUSPLUS undefined, it will be internally assigned to __cplusplus. */ @@ -43,4 +43,4 @@ If you leave UTF_CPP_CPLUSPLUS undefined, it will be internally assigned to __cp #include "utf8/checked.h" #include "utf8/unchecked.h" -#endif // header guard +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/checked.h b/Include/Pipe/Extern/utf8/checked.h index 96ceb4d5..a7a8f10d 100644 --- a/Include/Pipe/Extern/utf8/checked.h +++ b/Include/Pipe/Extern/utf8/checked.h @@ -29,331 +29,413 @@ DEALINGS IN THE SOFTWARE. #define UTF8_FOR_CPP_CHECKED_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #include "core.h" + #include namespace utf8 { - // Base for the exceptions that may be thrown from the library - class exception : public ::std::exception { - }; - - // Exceptions that may be thrown from the library functions. - class invalid_code_point : public exception { - utfchar32_t cp; - public: - invalid_code_point(utfchar32_t codepoint) : cp(codepoint) {} - virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE { return "Invalid code point"; } - utfchar32_t code_point() const {return cp;} - }; - - class invalid_utf8 : public exception { - utfchar8_t u8; - public: - invalid_utf8 (utfchar8_t u) : u8(u) {} - invalid_utf8 (char c) : u8(static_cast(c)) {} - virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE { return "Invalid UTF-8"; } - utfchar8_t utf8_octet() const {return u8;} - }; - - class invalid_utf16 : public exception { - utfchar16_t u16; - public: - invalid_utf16 (utfchar16_t u) : u16(u) {} - virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE { return "Invalid UTF-16"; } - utfchar16_t utf16_word() const {return u16;} - }; - - class not_enough_room : public exception { - public: - virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE { return "Not enough space"; } - }; - - /// The library API - functions intended to be called by the users - - template - octet_iterator append(utfchar32_t cp, octet_iterator result) - { - if (!utf8::internal::is_code_point_valid(cp)) - throw invalid_code_point(cp); - - return internal::append(cp, result); - } - - inline void append(utfchar32_t cp, std::string& s) - { - append(cp, std::back_inserter(s)); - } - - template - word_iterator append16(utfchar32_t cp, word_iterator result) - { - if (!utf8::internal::is_code_point_valid(cp)) - throw invalid_code_point(cp); - - return internal::append16(cp, result); - } - - template - output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out, utfchar32_t replacement) - { - while (start != end) { - octet_iterator sequence_start = start; - internal::utf_error err_code = utf8::internal::validate_next(start, end); - switch (err_code) { - case internal::UTF8_OK : - for (octet_iterator it = sequence_start; it != start; ++it) - *out++ = *it; - break; - case internal::NOT_ENOUGH_ROOM: - out = utf8::append (replacement, out); - start = end; - break; - case internal::INVALID_LEAD: - out = utf8::append (replacement, out); - ++start; - break; - case internal::INCOMPLETE_SEQUENCE: - case internal::OVERLONG_SEQUENCE: - case internal::INVALID_CODE_POINT: - out = utf8::append (replacement, out); - ++start; - // just one replacement mark for the sequence - while (start != end && utf8::internal::is_trail(*start)) - ++start; - break; - } - } - return out; - } - - template - inline output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out) - { - static const utfchar32_t replacement_marker = static_cast(utf8::internal::mask16(0xfffd)); - return utf8::replace_invalid(start, end, out, replacement_marker); - } - - inline std::string replace_invalid(const std::string& s, utfchar32_t replacement) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); - return result; - } - - inline std::string replace_invalid(const std::string& s) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - template - utfchar32_t next(octet_iterator& it, octet_iterator end) - { - utfchar32_t cp = 0; - internal::utf_error err_code = utf8::internal::validate_next(it, end, cp); - switch (err_code) { - case internal::UTF8_OK : - break; - case internal::NOT_ENOUGH_ROOM : - throw not_enough_room(); - case internal::INVALID_LEAD : - case internal::INCOMPLETE_SEQUENCE : - case internal::OVERLONG_SEQUENCE : - throw invalid_utf8(static_cast(*it)); - case internal::INVALID_CODE_POINT : - throw invalid_code_point(cp); - } - return cp; - } - - template - utfchar32_t next16(word_iterator& it, word_iterator end) - { - utfchar32_t cp = 0; - internal::utf_error err_code = utf8::internal::validate_next16(it, end, cp); - if (err_code == internal::NOT_ENOUGH_ROOM) - throw not_enough_room(); - return cp; - } - - template - utfchar32_t peek_next(octet_iterator it, octet_iterator end) - { - return utf8::next(it, end); - } - - template - utfchar32_t prior(octet_iterator& it, octet_iterator start) - { - // can't do much if it == start - if (it == start) - throw not_enough_room(); - - octet_iterator end = it; - // Go back until we hit either a lead octet or start - while (utf8::internal::is_trail(*(--it))) - if (it == start) - throw invalid_utf8(*it); // error - no lead byte in the sequence - return utf8::peek_next(it, end); - } - - template - void advance (octet_iterator& it, distance_type n, octet_iterator end) - { - const distance_type zero(0); - if (n < zero) { - // backward - for (distance_type i = n; i < zero; ++i) - utf8::prior(it, end); - } else { - // forward - for (distance_type i = zero; i < n; ++i) - utf8::next(it, end); - } - } - - template - typename std::iterator_traits::difference_type - distance (octet_iterator first, octet_iterator last) - { - typename std::iterator_traits::difference_type dist; - for (dist = 0; first < last; ++dist) - utf8::next(first, last); - return dist; - } - - template - octet_iterator utf16to8 (u16bit_iterator start, u16bit_iterator end, octet_iterator result) - { - while (start != end) { - utfchar32_t cp = static_cast(utf8::internal::mask16(*start++)); - // Take care of surrogate pairs first - if (utf8::internal::is_lead_surrogate(cp)) { - if (start != end) { - const utfchar32_t trail_surrogate = static_cast(utf8::internal::mask16(*start++)); - if (utf8::internal::is_trail_surrogate(trail_surrogate)) - cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; - else - throw invalid_utf16(static_cast(trail_surrogate)); - } - else - throw invalid_utf16(static_cast(cp)); - - } - // Lone trail surrogate - else if (utf8::internal::is_trail_surrogate(cp)) - throw invalid_utf16(static_cast(cp)); - - result = utf8::append(cp, result); - } - return result; - } - - template - u16bit_iterator utf8to16 (octet_iterator start, octet_iterator end, u16bit_iterator result) - { - while (start < end) { - const utfchar32_t cp = utf8::next(start, end); - if (cp > 0xffff) { //make a surrogate pair - *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); - *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); - } - else - *result++ = static_cast(cp); - } - return result; - } - - template - octet_iterator utf32to8 (u32bit_iterator start, u32bit_iterator end, octet_iterator result) - { - while (start != end) - result = utf8::append(*(start++), result); - - return result; - } - - template - u32bit_iterator utf8to32 (octet_iterator start, octet_iterator end, u32bit_iterator result) - { - while (start < end) - (*result++) = utf8::next(start, end); - - return result; - } - - // The iterator class - template - class iterator { - octet_iterator it; - octet_iterator range_start; - octet_iterator range_end; - public: - typedef utfchar32_t value_type; - typedef utfchar32_t* pointer; - typedef utfchar32_t& reference; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - iterator () {} - explicit iterator (const octet_iterator& octet_it, - const octet_iterator& rangestart, - const octet_iterator& rangeend) : - it(octet_it), range_start(rangestart), range_end(rangeend) - { - if (it < range_start || it > range_end) - throw std::out_of_range("Invalid utf-8 iterator position"); - } - // the default "big three" are OK - octet_iterator base () const { return it; } - utfchar32_t operator * () const - { - octet_iterator temp = it; - return utf8::next(temp, range_end); - } - bool operator == (const iterator& rhs) const - { - if (range_start != rhs.range_start || range_end != rhs.range_end) - throw std::logic_error("Comparing utf-8 iterators defined with different ranges"); - return (it == rhs.it); - } - bool operator != (const iterator& rhs) const - { - return !(operator == (rhs)); - } - iterator& operator ++ () - { - utf8::next(it, range_end); - return *this; - } - iterator operator ++ (int) - { - iterator temp = *this; - utf8::next(it, range_end); - return temp; - } - iterator& operator -- () - { - utf8::prior(it, range_start); - return *this; - } - iterator operator -- (int) - { - iterator temp = *this; - utf8::prior(it, range_start); - return temp; - } - }; // class iterator - -} // namespace utf8 - -#if UTF_CPP_CPLUSPLUS >= 202002L // C++ 20 or later -#include "cpp20.h" -#elif UTF_CPP_CPLUSPLUS >= 201703L // C++ 17 or later -#include "cpp17.h" -#elif UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later -#include "cpp11.h" -#endif // C++ 11 or later - -#endif //header guard - + // Base for the exceptions that may be thrown from the library + class exception : public ::std::exception + {}; + + // Exceptions that may be thrown from the library functions. + class invalid_code_point : public exception + { + utfchar32_t cp; + + public: + invalid_code_point(utfchar32_t codepoint) : cp(codepoint) {} + virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE + { + return "Invalid code point"; + } + utfchar32_t code_point() const + { + return cp; + } + }; + + class invalid_utf8 : public exception + { + utfchar8_t u8; + + public: + invalid_utf8(utfchar8_t u) : u8(u) {} + invalid_utf8(char c) : u8(static_cast(c)) {} + virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE + { + return "Invalid UTF-8"; + } + utfchar8_t utf8_octet() const + { + return u8; + } + }; + + class invalid_utf16 : public exception + { + utfchar16_t u16; + + public: + invalid_utf16(utfchar16_t u) : u16(u) {} + virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE + { + return "Invalid UTF-16"; + } + utfchar16_t utf16_word() const + { + return u16; + } + }; + + class not_enough_room : public exception + { + public: + virtual const char* what() const UTF_CPP_NOEXCEPT UTF_CPP_OVERRIDE + { + return "Not enough space"; + } + }; + + /// The library API - functions intended to be called by the users + + template + octet_iterator append(utfchar32_t cp, octet_iterator result) + { + if (!utf8::internal::is_code_point_valid(cp)) + { + throw invalid_code_point(cp); + } + + return internal::append(cp, result); + } + + inline void append(utfchar32_t cp, std::string& s) + { + append(cp, std::back_inserter(s)); + } + + template + word_iterator append16(utfchar32_t cp, word_iterator result) + { + if (!utf8::internal::is_code_point_valid(cp)) + { + throw invalid_code_point(cp); + } + + return internal::append16(cp, result); + } + + template + output_iterator replace_invalid( + octet_iterator start, octet_iterator end, output_iterator out, utfchar32_t replacement) + { + while (start != end) + { + octet_iterator sequence_start = start; + internal::utf_error err_code = utf8::internal::validate_next(start, end); + switch (err_code) + { + case internal::UTF8_OK: + for (octet_iterator it = sequence_start; it != start; ++it) + { + *out++ = *it; + } + break; + case internal::NOT_ENOUGH_ROOM: + out = utf8::append(replacement, out); + start = end; + break; + case internal::INVALID_LEAD: + out = utf8::append(replacement, out); + ++start; + break; + case internal::INCOMPLETE_SEQUENCE: + case internal::OVERLONG_SEQUENCE: + case internal::INVALID_CODE_POINT: + out = utf8::append(replacement, out); + ++start; + // just one replacement mark for the sequence + while (start != end && utf8::internal::is_trail(*start)) + { + ++start; + } + break; + } + } + return out; + } + + template + inline output_iterator replace_invalid( + octet_iterator start, octet_iterator end, output_iterator out) + { + static const utfchar32_t replacement_marker = + static_cast(utf8::internal::mask16(0xfffd)); + return utf8::replace_invalid(start, end, out, replacement_marker); + } + + inline std::string replace_invalid(const std::string& s, utfchar32_t replacement) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); + return result; + } + + inline std::string replace_invalid(const std::string& s) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + template + utfchar32_t next(octet_iterator& it, octet_iterator end) + { + utfchar32_t cp = 0; + internal::utf_error err_code = utf8::internal::validate_next(it, end, cp); + switch (err_code) + { + case internal::UTF8_OK: break; + case internal::NOT_ENOUGH_ROOM: throw not_enough_room(); + case internal::INVALID_LEAD: + case internal::INCOMPLETE_SEQUENCE: + case internal::OVERLONG_SEQUENCE: throw invalid_utf8(static_cast(*it)); + case internal::INVALID_CODE_POINT: throw invalid_code_point(cp); + } + return cp; + } + + template + utfchar32_t next16(word_iterator& it, word_iterator end) + { + utfchar32_t cp = 0; + internal::utf_error err_code = utf8::internal::validate_next16(it, end, cp); + if (err_code == internal::NOT_ENOUGH_ROOM) + { + throw not_enough_room(); + } + return cp; + } + + template + utfchar32_t peek_next(octet_iterator it, octet_iterator end) + { + return utf8::next(it, end); + } + + template + utfchar32_t prior(octet_iterator& it, octet_iterator start) + { + // can't do much if it == start + if (it == start) + { + throw not_enough_room(); + } + + octet_iterator end = it; + // Go back until we hit either a lead octet or start + while (utf8::internal::is_trail(*(--it))) + { + if (it == start) + { + throw invalid_utf8(*it); // error - no lead byte in the sequence + } + } + return utf8::peek_next(it, end); + } + + template + void advance(octet_iterator& it, distance_type n, octet_iterator end) + { + const distance_type zero(0); + if (n < zero) + { + // backward + for (distance_type i = n; i < zero; ++i) + { + utf8::prior(it, end); + } + } + else + { + // forward + for (distance_type i = zero; i < n; ++i) + { + utf8::next(it, end); + } + } + } + + template + typename std::iterator_traits::difference_type distance( + octet_iterator first, octet_iterator last) + { + typename std::iterator_traits::difference_type dist; + for (dist = 0; first < last; ++dist) + { + utf8::next(first, last); + } + return dist; + } + + template + octet_iterator utf16to8(u16bit_iterator start, u16bit_iterator end, octet_iterator result) + { + while (start != end) + { + utfchar32_t cp = static_cast(utf8::internal::mask16(*start++)); + // Take care of surrogate pairs first + if (utf8::internal::is_lead_surrogate(cp)) + { + if (start != end) + { + const utfchar32_t trail_surrogate = + static_cast(utf8::internal::mask16(*start++)); + if (utf8::internal::is_trail_surrogate(trail_surrogate)) + { + cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; + } + else + { + throw invalid_utf16(static_cast(trail_surrogate)); + } + } + else + { + throw invalid_utf16(static_cast(cp)); + } + } + // Lone trail surrogate + else if (utf8::internal::is_trail_surrogate(cp)) + { + throw invalid_utf16(static_cast(cp)); + } + + result = utf8::append(cp, result); + } + return result; + } + + template + u16bit_iterator utf8to16(octet_iterator start, octet_iterator end, u16bit_iterator result) + { + while (start < end) + { + const utfchar32_t cp = utf8::next(start, end); + if (cp > 0xffff) + { // make a surrogate pair + *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); + *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); + } + else + { + *result++ = static_cast(cp); + } + } + return result; + } + + template + octet_iterator utf32to8(u32bit_iterator start, u32bit_iterator end, octet_iterator result) + { + while (start != end) + { + result = utf8::append(*(start++), result); + } + + return result; + } + + template + u32bit_iterator utf8to32(octet_iterator start, octet_iterator end, u32bit_iterator result) + { + while (start < end) + { + (*result++) = utf8::next(start, end); + } + + return result; + } + + // The iterator class + template + class iterator + { + octet_iterator it; + octet_iterator range_start; + octet_iterator range_end; + + public: + typedef utfchar32_t value_type; + typedef utfchar32_t* pointer; + typedef utfchar32_t& reference; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + iterator() {} + explicit iterator(const octet_iterator& octet_it, const octet_iterator& rangestart, + const octet_iterator& rangeend) + : it(octet_it), range_start(rangestart), range_end(rangeend) + { + if (it < range_start || it > range_end) + { + throw std::out_of_range("Invalid utf-8 iterator position"); + } + } + // the default "big three" are OK + octet_iterator base() const + { + return it; + } + utfchar32_t operator*() const + { + octet_iterator temp = it; + return utf8::next(temp, range_end); + } + bool operator==(const iterator& rhs) const + { + if (range_start != rhs.range_start || range_end != rhs.range_end) + { + throw std::logic_error("Comparing utf-8 iterators defined with different ranges"); + } + return (it == rhs.it); + } + bool operator!=(const iterator& rhs) const + { + return !(operator==(rhs)); + } + iterator& operator++() + { + utf8::next(it, range_end); + return *this; + } + iterator operator++(int) + { + iterator temp = *this; + utf8::next(it, range_end); + return temp; + } + iterator& operator--() + { + utf8::prior(it, range_start); + return *this; + } + iterator operator--(int) + { + iterator temp = *this; + utf8::prior(it, range_start); + return temp; + } + }; // class iterator + +} // namespace utf8 + +#if UTF_CPP_CPLUSPLUS >= 202002L // C++ 20 or later + #include "cpp20.h" +#elif UTF_CPP_CPLUSPLUS >= 201703L // C++ 17 or later + #include "cpp17.h" +#elif UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later + #include "cpp11.h" +#endif // C++ 11 or later + +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/core.h b/Include/Pipe/Extern/utf8/core.h index 7313e20e..0415648e 100644 --- a/Include/Pipe/Extern/utf8/core.h +++ b/Include/Pipe/Extern/utf8/core.h @@ -28,8 +28,8 @@ DEALINGS IN THE SOFTWARE. #ifndef UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 #define UTF8_FOR_CPP_CORE_H_2675DCD0_9480_4c0c_B92A_CC14C027B731 -#include #include +#include #include // Determine the C++ standard version. @@ -37,565 +37,676 @@ DEALINGS IN THE SOFTWARE. // Otherwise, trust the unreliable predefined macro __cplusplus #if !defined UTF_CPP_CPLUSPLUS - #define UTF_CPP_CPLUSPLUS __cplusplus + #define UTF_CPP_CPLUSPLUS __cplusplus #endif -#if UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later - #define UTF_CPP_OVERRIDE override - #define UTF_CPP_NOEXCEPT noexcept - #define UTF_CPP_STATIC_ASSERT(condition) static_assert(condition, "UTFCPP static assert"); -#else // C++ 98/03 - #define UTF_CPP_OVERRIDE - #define UTF_CPP_NOEXCEPT throw() - // Not worth simulating static_assert: - #define UTF_CPP_STATIC_ASSERT(condition) (void)(condition); -#endif // C++ 11 or later +#if UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later + #define UTF_CPP_OVERRIDE override + #define UTF_CPP_NOEXCEPT noexcept + #define UTF_CPP_STATIC_ASSERT(condition) static_assert(condition, "UTFCPP static assert"); +#else // C++ 98/03 + #define UTF_CPP_OVERRIDE + #define UTF_CPP_NOEXCEPT throw() + // Not worth simulating static_assert: + #define UTF_CPP_STATIC_ASSERT(condition) (void)(condition); +#endif // C++ 11 or later namespace utf8 { // The typedefs for 8-bit, 16-bit and 32-bit code units -#if UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later - #if UTF_CPP_CPLUSPLUS >= 202002L // C++ 20 or later - typedef char8_t utfchar8_t; - #else // C++ 11/14/17 - typedef unsigned char utfchar8_t; - #endif - typedef char16_t utfchar16_t; - typedef char32_t utfchar32_t; -#else // C++ 98/03 - typedef unsigned char utfchar8_t; - typedef unsigned short utfchar16_t; - typedef unsigned int utfchar32_t; -#endif // C++ 11 or later - -// Helper code - not intended to be directly called by the library users. May be changed at any time -namespace internal -{ - // Unicode constants - // Leading (high) surrogates: 0xd800 - 0xdbff - // Trailing (low) surrogates: 0xdc00 - 0xdfff - const utfchar16_t LEAD_SURROGATE_MIN = 0xd800u; - const utfchar16_t LEAD_SURROGATE_MAX = 0xdbffu; - const utfchar16_t TRAIL_SURROGATE_MIN = 0xdc00u; - const utfchar16_t TRAIL_SURROGATE_MAX = 0xdfffu; - const utfchar16_t LEAD_OFFSET = 0xd7c0u; // LEAD_SURROGATE_MIN - (0x10000 >> 10) - const utfchar32_t SURROGATE_OFFSET = 0xfca02400u; // 0x10000u - (LEAD_SURROGATE_MIN << 10) - TRAIL_SURROGATE_MIN - - // Maximum valid value for a Unicode code point - const utfchar32_t CODE_POINT_MAX = 0x0010ffffu; - - template - inline utfchar8_t mask8(octet_type oc) - { - return static_cast(0xff & oc); - } - - template - inline utfchar16_t mask16(u16_type oc) - { - return static_cast(0xffff & oc); - } - - template - inline bool is_trail(octet_type oc) - { - return ((utf8::internal::mask8(oc) >> 6) == 0x2); - } - - inline bool is_lead_surrogate(utfchar32_t cp) - { - return (cp >= static_cast(LEAD_SURROGATE_MIN) && cp <= static_cast(LEAD_SURROGATE_MAX)); - } - - inline bool is_trail_surrogate(utfchar32_t cp) - { - return (cp >= static_cast(TRAIL_SURROGATE_MIN) && cp <= static_cast(TRAIL_SURROGATE_MAX)); - } - - inline bool is_surrogate(utfchar32_t cp) - { - return (cp >= static_cast(LEAD_SURROGATE_MIN) && cp <= static_cast(TRAIL_SURROGATE_MAX)); - } - - inline bool is_code_point_valid(utfchar32_t cp) - { - return (cp <= CODE_POINT_MAX && !utf8::internal::is_surrogate(cp)); - } - - inline bool is_in_bmp(utfchar32_t cp) - { - return cp < utfchar32_t(0x10000); - } - - template - int sequence_length(octet_iterator lead_it) - { - const utfchar8_t lead = utf8::internal::mask8(*lead_it); - if (lead < 0x80) - return 1; - else if ((lead >> 5) == 0x6) - return 2; - else if ((lead >> 4) == 0xe) - return 3; - else if ((lead >> 3) == 0x1e) - return 4; - else - return 0; - } - - inline bool is_overlong_sequence(utfchar32_t cp, int length) - { - if (cp < 0x80) { - if (length != 1) - return true; - } - else if (cp < 0x800) { - if (length != 2) - return true; - } - else if (cp < 0x10000) { - if (length != 3) - return true; - } - return false; - } - - enum utf_error {UTF8_OK, NOT_ENOUGH_ROOM, INVALID_LEAD, INCOMPLETE_SEQUENCE, OVERLONG_SEQUENCE, INVALID_CODE_POINT}; - - /// Helper for get_sequence_x - template - utf_error increase_safely(octet_iterator& it, const octet_iterator end) - { - if (++it == end) - return NOT_ENOUGH_ROOM; - - if (!utf8::internal::is_trail(*it)) - return INCOMPLETE_SEQUENCE; - - return UTF8_OK; - } - - #define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} - - /// get_sequence_x functions decode utf-8 sequences of the length x - template - utf_error get_sequence_1(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) - { - if (it == end) - return NOT_ENOUGH_ROOM; - - code_point = static_cast(utf8::internal::mask8(*it)); - - return UTF8_OK; - } - - template - utf_error get_sequence_2(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) - { - if (it == end) - return NOT_ENOUGH_ROOM; - - code_point = static_cast(utf8::internal::mask8(*it)); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = ((code_point << 6) & 0x7ff) + ((*it) & 0x3f); - - return UTF8_OK; - } - - template - utf_error get_sequence_3(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) - { - if (it == end) - return NOT_ENOUGH_ROOM; - - code_point = static_cast(utf8::internal::mask8(*it)); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = ((code_point << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = static_cast(code_point + ((*it) & 0x3f)); - - return UTF8_OK; - } - - template - utf_error get_sequence_4(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) - { - if (it == end) - return NOT_ENOUGH_ROOM; - - code_point = static_cast(utf8::internal::mask8(*it)); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = ((code_point << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = static_cast(code_point + ((utf8::internal::mask8(*it) << 6) & 0xfff)); - - UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) - - code_point = static_cast(code_point + ((*it) & 0x3f)); - - return UTF8_OK; - } - - #undef UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR - - template - utf_error validate_next(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) - { - if (it == end) - return NOT_ENOUGH_ROOM; - - // Save the original value of it so we can go back in case of failure - // Of course, it does not make much sense with i.e. stream iterators - octet_iterator original_it = it; - - utfchar32_t cp = 0; - // Determine the sequence length based on the lead octet - const int length = utf8::internal::sequence_length(it); - - // Get trail octets and calculate the code point - utf_error err = UTF8_OK; - switch (length) { - case 0: - return INVALID_LEAD; - case 1: - err = utf8::internal::get_sequence_1(it, end, cp); - break; - case 2: - err = utf8::internal::get_sequence_2(it, end, cp); - break; - case 3: - err = utf8::internal::get_sequence_3(it, end, cp); - break; - case 4: - err = utf8::internal::get_sequence_4(it, end, cp); - break; - } - - if (err == UTF8_OK) { - // Decoding succeeded. Now, security checks... - if (utf8::internal::is_code_point_valid(cp)) { - if (!utf8::internal::is_overlong_sequence(cp, length)){ - // Passed! Return here. - code_point = cp; - ++it; - return UTF8_OK; - } - else - err = OVERLONG_SEQUENCE; - } - else - err = INVALID_CODE_POINT; - } - - // Failure branch - restore the original value of the iterator - it = original_it; - return err; - } - - template - inline utf_error validate_next(octet_iterator& it, octet_iterator end) { - if (it == end) - return NOT_ENOUGH_ROOM; - - octet_iterator original_it = it; - const utfchar8_t lead = utf8::internal::mask8(*it); - - if (lead < 0x80) { - ++it; - return UTF8_OK; - } else if ((lead & 0xE0) == 0xC0) { - // two-byte sequence - if (lead == 0xC0 || lead == 0xC1) { - it = original_it; - return OVERLONG_SEQUENCE; - } - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail1 = utf8::internal::mask8(*it); - if ((trail1 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - ++it; - return UTF8_OK; - } else if ((lead & 0xF0) == 0xE0) { - // three-byte sequence - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail1 = utf8::internal::mask8(*it); - if ((trail1 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail2 = utf8::internal::mask8(*it); - if ((trail2 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - if (lead == 0xE0 && trail1 < 0xA0) { - it = original_it; - return OVERLONG_SEQUENCE; - } - if (lead == 0xED && trail1 > 0x9F) { - it = original_it; - return INVALID_CODE_POINT; - } - ++it; - return UTF8_OK; - } else if ((lead & 0xF8) == 0xF0) { - // four-byte sequence - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail1 = utf8::internal::mask8(*it); - if ((trail1 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail2 = utf8::internal::mask8(*it); - if ((trail2 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - if (++it == end) { - it = original_it; - return NOT_ENOUGH_ROOM; - } - const utfchar8_t trail3 = utf8::internal::mask8(*it); - if ((trail3 & 0xC0) != 0x80) { - it = original_it; - return INCOMPLETE_SEQUENCE; - } - if (lead == 0xF0 && trail1 < 0x90) { - it = original_it; - return OVERLONG_SEQUENCE; - } - if (lead == 0xF4 && trail1 > 0x8F) { - it = original_it; - return INVALID_CODE_POINT; - } - if (lead >= 0xF5) { - it = original_it; - return INVALID_CODE_POINT; - } - ++it; - return UTF8_OK; - } else { - it = original_it; - return INVALID_LEAD; - } - } - - template - utf_error validate_next16(word_iterator& it, word_iterator end, utfchar32_t& code_point) - { - // Make sure the iterator dereferences a large enough type - typedef typename std::iterator_traits::value_type word_type; - UTF_CPP_STATIC_ASSERT(sizeof(word_type) >= sizeof(utfchar16_t)); - // Check the edge case: - if (it == end) - return NOT_ENOUGH_ROOM; - // Save the original value of it so we can go back in case of failure - // Of course, it does not make much sense with i.e. stream iterators - word_iterator original_it = it; - - utf_error err = UTF8_OK; - - const utfchar16_t first_word = *it++; - if (!is_surrogate(first_word)) { - code_point = first_word; - return UTF8_OK; - } - else { - if (it == end) - err = NOT_ENOUGH_ROOM; - else if (is_lead_surrogate(first_word)) { - const utfchar16_t second_word = *it++; - if (is_trail_surrogate(static_cast(second_word))) { - code_point = static_cast(first_word << 10) + static_cast(second_word) + SURROGATE_OFFSET; - return UTF8_OK; - } else - err = INCOMPLETE_SEQUENCE; - - } else { - err = INVALID_LEAD; - } - } - // error branch - it = original_it; - return err; - } - - // Internal implementation of both checked and unchecked append() function - // This function will be invoked by the overloads below, as they will know - // the octet_type. - template - octet_iterator append(utfchar32_t cp, octet_iterator result) { - if (cp < 0x80) // one octet - *(result++) = static_cast(cp); - else if (cp < 0x800) { // two octets - *(result++) = static_cast((cp >> 6) | 0xc0); - *(result++) = static_cast((cp & 0x3f) | 0x80); - } - else if (cp < 0x10000) { // three octets - *(result++) = static_cast((cp >> 12) | 0xe0); - *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); - *(result++) = static_cast((cp & 0x3f) | 0x80); - } - else { // four octets - *(result++) = static_cast((cp >> 18) | 0xf0); - *(result++) = static_cast(((cp >> 12) & 0x3f)| 0x80); - *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); - *(result++) = static_cast((cp & 0x3f) | 0x80); - } - return result; - } - - // One of the following overloads will be invoked from the API calls - - // A simple (but dangerous) case: the caller appends byte(s) to a char array - inline char* append(utfchar32_t cp, char* result) { - return append(cp, result); - } - - // Hopefully, most common case: the caller uses back_inserter - // i.e. append(cp, std::back_inserter(str)); - template - std::back_insert_iterator append - (utfchar32_t cp, std::back_insert_iterator result) { - return append, - typename container_type::value_type>(cp, result); - } - - // The caller uses some other kind of output operator - not covered above - // Note that in this case we are not able to determine octet_type - // so we assume it's utfchar8_t; that can cause a conversion warning if we are wrong. - template - octet_iterator append(utfchar32_t cp, octet_iterator result) { - return append(cp, result); - } - - // Internal implementation of both checked and unchecked append16() function - // This function will be invoked by the overloads below, as they will know - // the word_type. - template - word_iterator append16(utfchar32_t cp, word_iterator result) { - UTF_CPP_STATIC_ASSERT(sizeof(word_type) >= sizeof(utfchar16_t)); - if (is_in_bmp(cp)) - *(result++) = static_cast(cp); - else { - // Code points from the supplementary planes are encoded via surrogate pairs - *(result++) = static_cast(LEAD_OFFSET + (cp >> 10)); - *(result++) = static_cast(TRAIL_SURROGATE_MIN + (cp & 0x3FF)); - } - return result; - } - - // Hopefully, most common case: the caller uses back_inserter - // i.e. append16(cp, std::back_inserter(str)); - template - std::back_insert_iterator append16 - (utfchar32_t cp, std::back_insert_iterator result) { - return append16, - typename container_type::value_type>(cp, result); - } - - // The caller uses some other kind of output operator - not covered above - // Note that in this case we are not able to determine word_type - // so we assume it's utfchar16_t; that can cause a conversion warning if we are wrong. - template - word_iterator append16(utfchar32_t cp, word_iterator result) { - return append16(cp, result); - } - -} // namespace internal - - /// The library API - functions intended to be called by the users - - // Byte order mark - const utfchar8_t bom[] = {0xef, 0xbb, 0xbf}; - - template - octet_iterator find_invalid(octet_iterator start, octet_iterator end) - { - octet_iterator result = start; - while (result != end) { - utf8::internal::utf_error err_code = utf8::internal::validate_next(result, end); - if (err_code != internal::UTF8_OK) - return result; - } - return result; - } - - inline const char* find_invalid(const char* str) - { - const char* end = str + std::strlen(str); - return find_invalid(str, end); - } - - inline std::size_t find_invalid(const std::string& s) - { - std::string::const_iterator invalid = find_invalid(s.begin(), s.end()); - return (invalid == s.end()) ? std::string::npos : static_cast(invalid - s.begin()); - } - - template - inline bool is_valid(octet_iterator start, octet_iterator end) - { - return (utf8::find_invalid(start, end) == end); - } - - inline bool is_valid(const char* str) - { - return (*(utf8::find_invalid(str)) == '\0'); - } - - inline bool is_valid(const std::string& s) - { - return is_valid(s.begin(), s.end()); - } - - - - template - inline bool starts_with_bom (octet_iterator it, octet_iterator end) - { - return ( - ((it != end) && (utf8::internal::mask8(*it++)) == bom[0]) && - ((it != end) && (utf8::internal::mask8(*it++)) == bom[1]) && - ((it != end) && (utf8::internal::mask8(*it)) == bom[2]) - ); - } - - inline bool starts_with_bom(const std::string& s) - { - return starts_with_bom(s.begin(), s.end()); - } -} // namespace utf8 - -#endif // header guard - +#if UTF_CPP_CPLUSPLUS >= 201103L // C++ 11 or later + #if UTF_CPP_CPLUSPLUS >= 202002L // C++ 20 or later + typedef char8_t utfchar8_t; + #else // C++ 11/14/17 + typedef unsigned char utfchar8_t; + #endif + typedef char16_t utfchar16_t; + typedef char32_t utfchar32_t; +#else // C++ 98/03 + typedef unsigned char utfchar8_t; + typedef unsigned short utfchar16_t; + typedef unsigned int utfchar32_t; +#endif // C++ 11 or later + + // Helper code - not intended to be directly called by the library users. May be changed at any + // time + namespace internal + { + // Unicode constants + // Leading (high) surrogates: 0xd800 - 0xdbff + // Trailing (low) surrogates: 0xdc00 - 0xdfff + const utfchar16_t LEAD_SURROGATE_MIN = 0xd800u; + const utfchar16_t LEAD_SURROGATE_MAX = 0xdbffu; + const utfchar16_t TRAIL_SURROGATE_MIN = 0xdc00u; + const utfchar16_t TRAIL_SURROGATE_MAX = 0xdfffu; + const utfchar16_t LEAD_OFFSET = 0xd7c0u; // LEAD_SURROGATE_MIN - (0x10000 >> 10) + const utfchar32_t SURROGATE_OFFSET = + 0xfca02400u; // 0x10000u - (LEAD_SURROGATE_MIN << 10) - TRAIL_SURROGATE_MIN + + // Maximum valid value for a Unicode code point + const utfchar32_t CODE_POINT_MAX = 0x0010ffffu; + + template + inline utfchar8_t mask8(octet_type oc) + { + return static_cast(0xff & oc); + } + + template + inline utfchar16_t mask16(u16_type oc) + { + return static_cast(0xffff & oc); + } + + template + inline bool is_trail(octet_type oc) + { + return ((utf8::internal::mask8(oc) >> 6) == 0x2); + } + + inline bool is_lead_surrogate(utfchar32_t cp) + { + return (cp >= static_cast(LEAD_SURROGATE_MIN) + && cp <= static_cast(LEAD_SURROGATE_MAX)); + } + + inline bool is_trail_surrogate(utfchar32_t cp) + { + return (cp >= static_cast(TRAIL_SURROGATE_MIN) + && cp <= static_cast(TRAIL_SURROGATE_MAX)); + } + + inline bool is_surrogate(utfchar32_t cp) + { + return (cp >= static_cast(LEAD_SURROGATE_MIN) + && cp <= static_cast(TRAIL_SURROGATE_MAX)); + } + + inline bool is_code_point_valid(utfchar32_t cp) + { + return (cp <= CODE_POINT_MAX && !utf8::internal::is_surrogate(cp)); + } + + inline bool is_in_bmp(utfchar32_t cp) + { + return cp < utfchar32_t(0x10000); + } + + template + int sequence_length(octet_iterator lead_it) + { + const utfchar8_t lead = utf8::internal::mask8(*lead_it); + if (lead < 0x80) + { + return 1; + } + else if ((lead >> 5) == 0x6) + { + return 2; + } + else if ((lead >> 4) == 0xe) + { + return 3; + } + else if ((lead >> 3) == 0x1e) + { + return 4; + } + else + { + return 0; + } + } + + inline bool is_overlong_sequence(utfchar32_t cp, int length) + { + if (cp < 0x80) + { + if (length != 1) + { + return true; + } + } + else if (cp < 0x800) + { + if (length != 2) + { + return true; + } + } + else if (cp < 0x10000) + { + if (length != 3) + { + return true; + } + } + return false; + } + + enum utf_error + { + UTF8_OK, + NOT_ENOUGH_ROOM, + INVALID_LEAD, + INCOMPLETE_SEQUENCE, + OVERLONG_SEQUENCE, + INVALID_CODE_POINT + }; + + /// Helper for get_sequence_x + template + utf_error increase_safely(octet_iterator& it, const octet_iterator end) + { + if (++it == end) + { + return NOT_ENOUGH_ROOM; + } + + if (!utf8::internal::is_trail(*it)) + { + return INCOMPLETE_SEQUENCE; + } + + return UTF8_OK; + } + +#define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) \ + { \ + utf_error ret = increase_safely(IT, END); \ + if (ret != UTF8_OK) \ + return ret; \ + } + + /// get_sequence_x functions decode utf-8 sequences of the length x + template + utf_error get_sequence_1(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + code_point = static_cast(utf8::internal::mask8(*it)); + + return UTF8_OK; + } + + template + utf_error get_sequence_2(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + code_point = static_cast(utf8::internal::mask8(*it)); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = ((code_point << 6) & 0x7ff) + ((*it) & 0x3f); + + return UTF8_OK; + } + + template + utf_error get_sequence_3(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + code_point = static_cast(utf8::internal::mask8(*it)); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = + ((code_point << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = static_cast(code_point + ((*it) & 0x3f)); + + return UTF8_OK; + } + + template + utf_error get_sequence_4(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + code_point = static_cast(utf8::internal::mask8(*it)); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = + ((code_point << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = + static_cast(code_point + ((utf8::internal::mask8(*it) << 6) & 0xfff)); + + UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) + + code_point = static_cast(code_point + ((*it) & 0x3f)); + + return UTF8_OK; + } + +#undef UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR + + template + utf_error validate_next(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + // Save the original value of it so we can go back in case of failure + // Of course, it does not make much sense with i.e. stream iterators + octet_iterator original_it = it; + + utfchar32_t cp = 0; + // Determine the sequence length based on the lead octet + const int length = utf8::internal::sequence_length(it); + + // Get trail octets and calculate the code point + utf_error err = UTF8_OK; + switch (length) + { + case 0: return INVALID_LEAD; + case 1: err = utf8::internal::get_sequence_1(it, end, cp); break; + case 2: err = utf8::internal::get_sequence_2(it, end, cp); break; + case 3: err = utf8::internal::get_sequence_3(it, end, cp); break; + case 4: err = utf8::internal::get_sequence_4(it, end, cp); break; + } + + if (err == UTF8_OK) + { + // Decoding succeeded. Now, security checks... + if (utf8::internal::is_code_point_valid(cp)) + { + if (!utf8::internal::is_overlong_sequence(cp, length)) + { + // Passed! Return here. + code_point = cp; + ++it; + return UTF8_OK; + } + else + { + err = OVERLONG_SEQUENCE; + } + } + else + { + err = INVALID_CODE_POINT; + } + } + + // Failure branch - restore the original value of the iterator + it = original_it; + return err; + } + + template + inline utf_error validate_next(octet_iterator& it, octet_iterator end) + { + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + + octet_iterator original_it = it; + const utfchar8_t lead = utf8::internal::mask8(*it); + + if (lead < 0x80) + { + ++it; + return UTF8_OK; + } + else if ((lead & 0xE0) == 0xC0) + { + // two-byte sequence + if (lead == 0xC0 || lead == 0xC1) + { + it = original_it; + return OVERLONG_SEQUENCE; + } + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail1 = utf8::internal::mask8(*it); + if ((trail1 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + ++it; + return UTF8_OK; + } + else if ((lead & 0xF0) == 0xE0) + { + // three-byte sequence + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail1 = utf8::internal::mask8(*it); + if ((trail1 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail2 = utf8::internal::mask8(*it); + if ((trail2 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + if (lead == 0xE0 && trail1 < 0xA0) + { + it = original_it; + return OVERLONG_SEQUENCE; + } + if (lead == 0xED && trail1 > 0x9F) + { + it = original_it; + return INVALID_CODE_POINT; + } + ++it; + return UTF8_OK; + } + else if ((lead & 0xF8) == 0xF0) + { + // four-byte sequence + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail1 = utf8::internal::mask8(*it); + if ((trail1 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail2 = utf8::internal::mask8(*it); + if ((trail2 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + if (++it == end) + { + it = original_it; + return NOT_ENOUGH_ROOM; + } + const utfchar8_t trail3 = utf8::internal::mask8(*it); + if ((trail3 & 0xC0) != 0x80) + { + it = original_it; + return INCOMPLETE_SEQUENCE; + } + if (lead == 0xF0 && trail1 < 0x90) + { + it = original_it; + return OVERLONG_SEQUENCE; + } + if (lead == 0xF4 && trail1 > 0x8F) + { + it = original_it; + return INVALID_CODE_POINT; + } + if (lead >= 0xF5) + { + it = original_it; + return INVALID_CODE_POINT; + } + ++it; + return UTF8_OK; + } + else + { + it = original_it; + return INVALID_LEAD; + } + } + + template + utf_error validate_next16(word_iterator& it, word_iterator end, utfchar32_t& code_point) + { + // Make sure the iterator dereferences a large enough type + typedef typename std::iterator_traits::value_type word_type; + UTF_CPP_STATIC_ASSERT(sizeof(word_type) >= sizeof(utfchar16_t)); + // Check the edge case: + if (it == end) + { + return NOT_ENOUGH_ROOM; + } + // Save the original value of it so we can go back in case of failure + // Of course, it does not make much sense with i.e. stream iterators + word_iterator original_it = it; + + utf_error err = UTF8_OK; + + const utfchar16_t first_word = *it++; + if (!is_surrogate(first_word)) + { + code_point = first_word; + return UTF8_OK; + } + else + { + if (it == end) + { + err = NOT_ENOUGH_ROOM; + } + else if (is_lead_surrogate(first_word)) + { + const utfchar16_t second_word = *it++; + if (is_trail_surrogate(static_cast(second_word))) + { + code_point = static_cast(first_word << 10) + + static_cast(second_word) + SURROGATE_OFFSET; + return UTF8_OK; + } + else + { + err = INCOMPLETE_SEQUENCE; + } + } + else + { + err = INVALID_LEAD; + } + } + // error branch + it = original_it; + return err; + } + + // Internal implementation of both checked and unchecked append() function + // This function will be invoked by the overloads below, as they will know + // the octet_type. + template + octet_iterator append(utfchar32_t cp, octet_iterator result) + { + if (cp < 0x80) // one octet + { + *(result++) = static_cast(cp); + } + else if (cp < 0x800) + { // two octets + *(result++) = static_cast((cp >> 6) | 0xc0); + *(result++) = static_cast((cp & 0x3f) | 0x80); + } + else if (cp < 0x10000) + { // three octets + *(result++) = static_cast((cp >> 12) | 0xe0); + *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast((cp & 0x3f) | 0x80); + } + else + { // four octets + *(result++) = static_cast((cp >> 18) | 0xf0); + *(result++) = static_cast(((cp >> 12) & 0x3f) | 0x80); + *(result++) = static_cast(((cp >> 6) & 0x3f) | 0x80); + *(result++) = static_cast((cp & 0x3f) | 0x80); + } + return result; + } + + // One of the following overloads will be invoked from the API calls + + // A simple (but dangerous) case: the caller appends byte(s) to a char array + inline char* append(utfchar32_t cp, char* result) + { + return append(cp, result); + } + + // Hopefully, most common case: the caller uses back_inserter + // i.e. append(cp, std::back_inserter(str)); + template + std::back_insert_iterator append( + utfchar32_t cp, std::back_insert_iterator result) + { + return append, + typename container_type::value_type>(cp, result); + } + + // The caller uses some other kind of output operator - not covered above + // Note that in this case we are not able to determine octet_type + // so we assume it's utfchar8_t; that can cause a conversion warning if we are wrong. + template + octet_iterator append(utfchar32_t cp, octet_iterator result) + { + return append(cp, result); + } + + // Internal implementation of both checked and unchecked append16() function + // This function will be invoked by the overloads below, as they will know + // the word_type. + template + word_iterator append16(utfchar32_t cp, word_iterator result) + { + UTF_CPP_STATIC_ASSERT(sizeof(word_type) >= sizeof(utfchar16_t)); + if (is_in_bmp(cp)) + { + *(result++) = static_cast(cp); + } + else + { + // Code points from the supplementary planes are encoded via surrogate pairs + *(result++) = static_cast(LEAD_OFFSET + (cp >> 10)); + *(result++) = static_cast(TRAIL_SURROGATE_MIN + (cp & 0x3FF)); + } + return result; + } + + // Hopefully, most common case: the caller uses back_inserter + // i.e. append16(cp, std::back_inserter(str)); + template + std::back_insert_iterator append16( + utfchar32_t cp, std::back_insert_iterator result) + { + return append16, + typename container_type::value_type>(cp, result); + } + + // The caller uses some other kind of output operator - not covered above + // Note that in this case we are not able to determine word_type + // so we assume it's utfchar16_t; that can cause a conversion warning if we are wrong. + template + word_iterator append16(utfchar32_t cp, word_iterator result) + { + return append16(cp, result); + } + + } // namespace internal + + /// The library API - functions intended to be called by the users + + // Byte order mark + const utfchar8_t bom[] = {0xef, 0xbb, 0xbf}; + + template + octet_iterator find_invalid(octet_iterator start, octet_iterator end) + { + octet_iterator result = start; + while (result != end) + { + utf8::internal::utf_error err_code = utf8::internal::validate_next(result, end); + if (err_code != internal::UTF8_OK) + { + return result; + } + } + return result; + } + + inline const char* find_invalid(const char* str) + { + const char* end = str + std::strlen(str); + return find_invalid(str, end); + } + + inline std::size_t find_invalid(const std::string& s) + { + std::string::const_iterator invalid = find_invalid(s.begin(), s.end()); + return (invalid == s.end()) ? std::string::npos + : static_cast(invalid - s.begin()); + } + + template + inline bool is_valid(octet_iterator start, octet_iterator end) + { + return (utf8::find_invalid(start, end) == end); + } + + inline bool is_valid(const char* str) + { + return (*(utf8::find_invalid(str)) == '\0'); + } + + inline bool is_valid(const std::string& s) + { + return is_valid(s.begin(), s.end()); + } + + + template + inline bool starts_with_bom(octet_iterator it, octet_iterator end) + { + return (((it != end) && (utf8::internal::mask8(*it++)) == bom[0]) + && ((it != end) && (utf8::internal::mask8(*it++)) == bom[1]) + && ((it != end) && (utf8::internal::mask8(*it)) == bom[2])); + } + + inline bool starts_with_bom(const std::string& s) + { + return starts_with_bom(s.begin(), s.end()); + } +} // namespace utf8 + +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/cpp11.h b/Include/Pipe/Extern/utf8/cpp11.h index 691633c8..cb93e83f 100644 --- a/Include/Pipe/Extern/utf8/cpp11.h +++ b/Include/Pipe/Extern/utf8/cpp11.h @@ -32,39 +32,38 @@ DEALINGS IN THE SOFTWARE. namespace utf8 { - inline void append16(utfchar32_t cp, std::u16string& s) - { - append16(cp, std::back_inserter(s)); - } + inline void append16(utfchar32_t cp, std::u16string& s) + { + append16(cp, std::back_inserter(s)); + } - inline std::string utf16to8(const std::u16string& s) - { - std::string result; - utf16to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } + inline std::string utf16to8(const std::u16string& s) + { + std::string result; + utf16to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } - inline std::u16string utf8to16(const std::string& s) - { - std::u16string result; - utf8to16(s.begin(), s.end(), std::back_inserter(result)); - return result; - } + inline std::u16string utf8to16(const std::string& s) + { + std::u16string result; + utf8to16(s.begin(), s.end(), std::back_inserter(result)); + return result; + } - inline std::string utf32to8(const std::u32string& s) - { - std::string result; - utf32to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } + inline std::string utf32to8(const std::u32string& s) + { + std::string result; + utf32to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } - inline std::u32string utf8to32(const std::string& s) - { - std::u32string result; - utf8to32(s.begin(), s.end(), std::back_inserter(result)); - return result; - } -} // namespace utf8 - -#endif // header guard + inline std::u32string utf8to32(const std::string& s) + { + std::u32string result; + utf8to32(s.begin(), s.end(), std::back_inserter(result)); + return result; + } +} // namespace utf8 +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/cpp17.h b/Include/Pipe/Extern/utf8/cpp17.h index 07587300..626a6e5c 100644 --- a/Include/Pipe/Extern/utf8/cpp17.h +++ b/Include/Pipe/Extern/utf8/cpp17.h @@ -32,65 +32,65 @@ DEALINGS IN THE SOFTWARE. namespace utf8 { - inline std::string utf16to8(std::u16string_view s) - { - std::string result; - utf16to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u16string utf8to16(std::string_view s) - { - std::u16string result; - utf8to16(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::string utf32to8(std::u32string_view s) - { - std::string result; - utf32to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u32string utf8to32(std::string_view s) - { - std::u32string result; - utf8to32(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::size_t find_invalid(std::string_view s) - { - std::string_view::const_iterator invalid = find_invalid(s.begin(), s.end()); - return (invalid == s.end()) ? std::string_view::npos : static_cast(invalid - s.begin()); - } - - inline bool is_valid(std::string_view s) - { - return is_valid(s.begin(), s.end()); - } - - inline std::string replace_invalid(std::string_view s, char32_t replacement) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); - return result; - } - - inline std::string replace_invalid(std::string_view s) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline bool starts_with_bom(std::string_view s) - { - return starts_with_bom(s.begin(), s.end()); - } - -} // namespace utf8 - -#endif // header guard - + inline std::string utf16to8(std::u16string_view s) + { + std::string result; + utf16to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u16string utf8to16(std::string_view s) + { + std::u16string result; + utf8to16(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::string utf32to8(std::u32string_view s) + { + std::string result; + utf32to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u32string utf8to32(std::string_view s) + { + std::u32string result; + utf8to32(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::size_t find_invalid(std::string_view s) + { + std::string_view::const_iterator invalid = find_invalid(s.begin(), s.end()); + return (invalid == s.end()) ? std::string_view::npos + : static_cast(invalid - s.begin()); + } + + inline bool is_valid(std::string_view s) + { + return is_valid(s.begin(), s.end()); + } + + inline std::string replace_invalid(std::string_view s, char32_t replacement) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); + return result; + } + + inline std::string replace_invalid(std::string_view s) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline bool starts_with_bom(std::string_view s) + { + return starts_with_bom(s.begin(), s.end()); + } + +} // namespace utf8 + +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/cpp20.h b/Include/Pipe/Extern/utf8/cpp20.h index 07b61d0f..a824c9a0 100644 --- a/Include/Pipe/Extern/utf8/cpp20.h +++ b/Include/Pipe/Extern/utf8/cpp20.h @@ -32,93 +32,93 @@ DEALINGS IN THE SOFTWARE. namespace utf8 { - inline std::u8string utf16tou8(const std::u16string& s) - { - std::u8string result; - utf16to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u8string utf16tou8(std::u16string_view s) - { - std::u8string result; - utf16to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u16string utf8to16(const std::u8string& s) - { - std::u16string result; - utf8to16(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u16string utf8to16(const std::u8string_view& s) - { - std::u16string result; - utf8to16(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u8string utf32tou8(const std::u32string& s) - { - std::u8string result; - utf32to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u8string utf32tou8(const std::u32string_view& s) - { - std::u8string result; - utf32to8(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u32string utf8to32(const std::u8string& s) - { - std::u32string result; - utf8to32(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::u32string utf8to32(const std::u8string_view& s) - { - std::u32string result; - utf8to32(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline std::size_t find_invalid(const std::u8string& s) - { - std::u8string::const_iterator invalid = find_invalid(s.begin(), s.end()); - return (invalid == s.end()) ? std::string_view::npos : static_cast(invalid - s.begin()); - } - - inline bool is_valid(const std::u8string& s) - { - return is_valid(s.begin(), s.end()); - } - - inline std::u8string replace_invalid(const std::u8string& s, char32_t replacement) - { - std::u8string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); - return result; - } - - inline std::u8string replace_invalid(const std::u8string& s) - { - std::u8string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result)); - return result; - } - - inline bool starts_with_bom(const std::u8string& s) - { - return starts_with_bom(s.begin(), s.end()); - } - -} // namespace utf8 - -#endif // header guard - + inline std::u8string utf16tou8(const std::u16string& s) + { + std::u8string result; + utf16to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u8string utf16tou8(std::u16string_view s) + { + std::u8string result; + utf16to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u16string utf8to16(const std::u8string& s) + { + std::u16string result; + utf8to16(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u16string utf8to16(const std::u8string_view& s) + { + std::u16string result; + utf8to16(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u8string utf32tou8(const std::u32string& s) + { + std::u8string result; + utf32to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u8string utf32tou8(const std::u32string_view& s) + { + std::u8string result; + utf32to8(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u32string utf8to32(const std::u8string& s) + { + std::u32string result; + utf8to32(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::u32string utf8to32(const std::u8string_view& s) + { + std::u32string result; + utf8to32(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline std::size_t find_invalid(const std::u8string& s) + { + std::u8string::const_iterator invalid = find_invalid(s.begin(), s.end()); + return (invalid == s.end()) ? std::string_view::npos + : static_cast(invalid - s.begin()); + } + + inline bool is_valid(const std::u8string& s) + { + return is_valid(s.begin(), s.end()); + } + + inline std::u8string replace_invalid(const std::u8string& s, char32_t replacement) + { + std::u8string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); + return result; + } + + inline std::u8string replace_invalid(const std::u8string& s) + { + std::u8string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result)); + return result; + } + + inline bool starts_with_bom(const std::u8string& s) + { + return starts_with_bom(s.begin(), s.end()); + } + +} // namespace utf8 + +#endif // header guard diff --git a/Include/Pipe/Extern/utf8/unchecked.h b/Include/Pipe/Extern/utf8/unchecked.h index 173d0302..f23c5cab 100644 --- a/Include/Pipe/Extern/utf8/unchecked.h +++ b/Include/Pipe/Extern/utf8/unchecked.h @@ -32,255 +32,293 @@ DEALINGS IN THE SOFTWARE. namespace utf8 { - namespace unchecked - { - template - octet_iterator append(utfchar32_t cp, octet_iterator result) - { - return internal::append(cp, result); - } + namespace unchecked + { + template + octet_iterator append(utfchar32_t cp, octet_iterator result) + { + return internal::append(cp, result); + } - template - word_iterator append16(utfchar32_t cp, word_iterator result) - { - return internal::append16(cp, result); - } + template + word_iterator append16(utfchar32_t cp, word_iterator result) + { + return internal::append16(cp, result); + } - template - output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out, utfchar32_t replacement) - { - while (start != end) { - octet_iterator sequence_start = start; - internal::utf_error err_code = utf8::internal::validate_next(start, end); - switch (err_code) { - case internal::UTF8_OK : - for (octet_iterator it = sequence_start; it != start; ++it) - *out++ = *it; - break; - case internal::NOT_ENOUGH_ROOM: - out = utf8::unchecked::append(replacement, out); - start = end; - break; - case internal::INVALID_LEAD: - out = utf8::unchecked::append(replacement, out); - ++start; - break; - case internal::INCOMPLETE_SEQUENCE: - case internal::OVERLONG_SEQUENCE: - case internal::INVALID_CODE_POINT: - out = utf8::unchecked::append(replacement, out); - ++start; - // just one replacement mark for the sequence - while (start != end && utf8::internal::is_trail(*start)) - ++start; - break; - } - } - return out; - } + template + output_iterator replace_invalid( + octet_iterator start, octet_iterator end, output_iterator out, utfchar32_t replacement) + { + while (start != end) + { + octet_iterator sequence_start = start; + internal::utf_error err_code = utf8::internal::validate_next(start, end); + switch (err_code) + { + case internal::UTF8_OK: + for (octet_iterator it = sequence_start; it != start; ++it) + { + *out++ = *it; + } + break; + case internal::NOT_ENOUGH_ROOM: + out = utf8::unchecked::append(replacement, out); + start = end; + break; + case internal::INVALID_LEAD: + out = utf8::unchecked::append(replacement, out); + ++start; + break; + case internal::INCOMPLETE_SEQUENCE: + case internal::OVERLONG_SEQUENCE: + case internal::INVALID_CODE_POINT: + out = utf8::unchecked::append(replacement, out); + ++start; + // just one replacement mark for the sequence + while (start != end && utf8::internal::is_trail(*start)) + { + ++start; + } + break; + } + } + return out; + } - template - inline output_iterator replace_invalid(octet_iterator start, octet_iterator end, output_iterator out) - { - static const utfchar32_t replacement_marker = static_cast(utf8::internal::mask16(0xfffd)); - return utf8::unchecked::replace_invalid(start, end, out, replacement_marker); - } + template + inline output_iterator replace_invalid( + octet_iterator start, octet_iterator end, output_iterator out) + { + static const utfchar32_t replacement_marker = + static_cast(utf8::internal::mask16(0xfffd)); + return utf8::unchecked::replace_invalid(start, end, out, replacement_marker); + } - inline std::string replace_invalid(const std::string& s, utfchar32_t replacement) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); - return result; - } + inline std::string replace_invalid(const std::string& s, utfchar32_t replacement) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result), replacement); + return result; + } - inline std::string replace_invalid(const std::string& s) - { - std::string result; - replace_invalid(s.begin(), s.end(), std::back_inserter(result)); - return result; - } + inline std::string replace_invalid(const std::string& s) + { + std::string result; + replace_invalid(s.begin(), s.end(), std::back_inserter(result)); + return result; + } - template - utfchar32_t next(octet_iterator& it) - { - utfchar32_t cp = utf8::internal::mask8(*it); - switch (utf8::internal::sequence_length(it)) { - case 1: - break; - case 2: - ++it; - cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); - break; - case 3: - ++it; - cp = ((cp << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); - ++it; - cp = static_cast(cp + ((*it) & 0x3f)); - break; - case 4: - ++it; - cp = ((cp << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); - ++it; - cp = static_cast(cp + ((utf8::internal::mask8(*it) << 6) & 0xfff)); - ++it; - cp = static_cast(cp + ((*it) & 0x3f)); - break; - } - ++it; - return cp; - } + template + utfchar32_t next(octet_iterator& it) + { + utfchar32_t cp = utf8::internal::mask8(*it); + switch (utf8::internal::sequence_length(it)) + { + case 1: break; + case 2: + ++it; + cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); + break; + case 3: + ++it; + cp = ((cp << 12) & 0xffff) + ((utf8::internal::mask8(*it) << 6) & 0xfff); + ++it; + cp = static_cast(cp + ((*it) & 0x3f)); + break; + case 4: + ++it; + cp = ((cp << 18) & 0x1fffff) + ((utf8::internal::mask8(*it) << 12) & 0x3ffff); + ++it; + cp = static_cast(cp + ((utf8::internal::mask8(*it) << 6) & 0xfff)); + ++it; + cp = static_cast(cp + ((*it) & 0x3f)); + break; + } + ++it; + return cp; + } - template - utfchar32_t peek_next(octet_iterator it) - { - return utf8::unchecked::next(it); - } + template + utfchar32_t peek_next(octet_iterator it) + { + return utf8::unchecked::next(it); + } - template - utfchar32_t next16(word_iterator& it) - { - utfchar32_t cp = utf8::internal::mask16(*it++); - if (utf8::internal::is_lead_surrogate(cp)) - return (cp << 10) + *it++ + utf8::internal::SURROGATE_OFFSET; - return cp; - } + template + utfchar32_t next16(word_iterator& it) + { + utfchar32_t cp = utf8::internal::mask16(*it++); + if (utf8::internal::is_lead_surrogate(cp)) + { + return (cp << 10) + *it++ + utf8::internal::SURROGATE_OFFSET; + } + return cp; + } - template - utfchar32_t prior(octet_iterator& it) - { - while (utf8::internal::is_trail(*(--it))) ; - octet_iterator temp = it; - return utf8::unchecked::next(temp); - } + template + utfchar32_t prior(octet_iterator& it) + { + while (utf8::internal::is_trail(*(--it))) + ; + octet_iterator temp = it; + return utf8::unchecked::next(temp); + } - template - void advance(octet_iterator& it, distance_type n) - { - const distance_type zero(0); - if (n < zero) { - // backward - for (distance_type i = n; i < zero; ++i) - utf8::unchecked::prior(it); - } else { - // forward - for (distance_type i = zero; i < n; ++i) - utf8::unchecked::next(it); - } - } + template + void advance(octet_iterator& it, distance_type n) + { + const distance_type zero(0); + if (n < zero) + { + // backward + for (distance_type i = n; i < zero; ++i) + { + utf8::unchecked::prior(it); + } + } + else + { + // forward + for (distance_type i = zero; i < n; ++i) + { + utf8::unchecked::next(it); + } + } + } - template - typename std::iterator_traits::difference_type - distance(octet_iterator first, octet_iterator last) - { - typename std::iterator_traits::difference_type dist; - for (dist = 0; first < last; ++dist) - utf8::unchecked::next(first); - return dist; - } + template + typename std::iterator_traits::difference_type distance( + octet_iterator first, octet_iterator last) + { + typename std::iterator_traits::difference_type dist; + for (dist = 0; first < last; ++dist) + { + utf8::unchecked::next(first); + } + return dist; + } - template - octet_iterator utf16to8(u16bit_iterator start, u16bit_iterator end, octet_iterator result) - { - while (start != end) { - utfchar32_t cp = utf8::internal::mask16(*start++); - // Take care of surrogate pairs first - if (utf8::internal::is_lead_surrogate(cp)) { - if (start == end) - return result; - utfchar32_t trail_surrogate = utf8::internal::mask16(*start++); - cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; - } - result = utf8::unchecked::append(cp, result); - } - return result; - } + template + octet_iterator utf16to8(u16bit_iterator start, u16bit_iterator end, octet_iterator result) + { + while (start != end) + { + utfchar32_t cp = utf8::internal::mask16(*start++); + // Take care of surrogate pairs first + if (utf8::internal::is_lead_surrogate(cp)) + { + if (start == end) + { + return result; + } + utfchar32_t trail_surrogate = utf8::internal::mask16(*start++); + cp = (cp << 10) + trail_surrogate + internal::SURROGATE_OFFSET; + } + result = utf8::unchecked::append(cp, result); + } + return result; + } - template - u16bit_iterator utf8to16(octet_iterator start, octet_iterator end, u16bit_iterator result) - { - while (start < end) { - utfchar32_t cp = utf8::unchecked::next(start); - if (cp > 0xffff) { //make a surrogate pair - *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); - *result++ = static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); - } - else - *result++ = static_cast(cp); - } - return result; - } + template + u16bit_iterator utf8to16(octet_iterator start, octet_iterator end, u16bit_iterator result) + { + while (start < end) + { + utfchar32_t cp = utf8::unchecked::next(start); + if (cp > 0xffff) + { // make a surrogate pair + *result++ = static_cast((cp >> 10) + internal::LEAD_OFFSET); + *result++ = + static_cast((cp & 0x3ff) + internal::TRAIL_SURROGATE_MIN); + } + else + { + *result++ = static_cast(cp); + } + } + return result; + } - template - octet_iterator utf32to8(u32bit_iterator start, u32bit_iterator end, octet_iterator result) - { - while (start != end) - result = utf8::unchecked::append(*(start++), result); + template + octet_iterator utf32to8(u32bit_iterator start, u32bit_iterator end, octet_iterator result) + { + while (start != end) + { + result = utf8::unchecked::append(*(start++), result); + } - return result; - } + return result; + } - template - u32bit_iterator utf8to32(octet_iterator start, octet_iterator end, u32bit_iterator result) - { - while (start < end) - (*result++) = utf8::unchecked::next(start); + template + u32bit_iterator utf8to32(octet_iterator start, octet_iterator end, u32bit_iterator result) + { + while (start < end) + { + (*result++) = utf8::unchecked::next(start); + } - return result; - } + return result; + } - // The iterator class - template - class iterator { - octet_iterator it; - public: - typedef utfchar32_t value_type; - typedef utfchar32_t* pointer; - typedef utfchar32_t& reference; - typedef std::ptrdiff_t difference_type; - typedef std::bidirectional_iterator_tag iterator_category; - iterator () {} - explicit iterator (const octet_iterator& octet_it): it(octet_it) {} - // the default "big three" are OK - octet_iterator base () const { return it; } - utfchar32_t operator * () const - { - octet_iterator temp = it; - return utf8::unchecked::next(temp); - } - bool operator == (const iterator& rhs) const - { - return (it == rhs.it); - } - bool operator != (const iterator& rhs) const - { - return !(operator == (rhs)); - } - iterator& operator ++ () - { - ::std::advance(it, utf8::internal::sequence_length(it)); - return *this; - } - iterator operator ++ (int) - { - iterator temp = *this; - ::std::advance(it, utf8::internal::sequence_length(it)); - return temp; - } - iterator& operator -- () - { - utf8::unchecked::prior(it); - return *this; - } - iterator operator -- (int) - { - iterator temp = *this; - utf8::unchecked::prior(it); - return temp; - } - }; // class iterator + // The iterator class + template + class iterator + { + octet_iterator it; - } // namespace utf8::unchecked -} // namespace utf8 + public: + typedef utfchar32_t value_type; + typedef utfchar32_t* pointer; + typedef utfchar32_t& reference; + typedef std::ptrdiff_t difference_type; + typedef std::bidirectional_iterator_tag iterator_category; + iterator() {} + explicit iterator(const octet_iterator& octet_it) : it(octet_it) {} + // the default "big three" are OK + octet_iterator base() const + { + return it; + } + utfchar32_t operator*() const + { + octet_iterator temp = it; + return utf8::unchecked::next(temp); + } + bool operator==(const iterator& rhs) const + { + return (it == rhs.it); + } + bool operator!=(const iterator& rhs) const + { + return !(operator==(rhs)); + } + iterator& operator++() + { + ::std::advance(it, utf8::internal::sequence_length(it)); + return *this; + } + iterator operator++(int) + { + iterator temp = *this; + ::std::advance(it, utf8::internal::sequence_length(it)); + return temp; + } + iterator& operator--() + { + utf8::unchecked::prior(it); + return *this; + } + iterator operator--(int) + { + iterator temp = *this; + utf8::unchecked::prior(it); + return temp; + } + }; // class iterator -#endif // header guard + } // namespace unchecked +} // namespace utf8 +#endif // header guard diff --git a/Include/Pipe/Files/Files.h b/Include/Pipe/Files/Files.h index 1d426e92..a14eb8ac 100644 --- a/Include/Pipe/Files/Files.h +++ b/Include/Pipe/Files/Files.h @@ -2,11 +2,11 @@ #pragma once -#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Files/Paths.h" #include "Pipe/Files/STDFileSystem.h" #include "PipeContainers.h" +#include "PipeStrings.h" namespace p diff --git a/Include/Pipe/Files/Paths.h b/Include/Pipe/Files/Paths.h index f74f2d1e..02dd8b79 100644 --- a/Include/Pipe/Files/Paths.h +++ b/Include/Pipe/Files/Paths.h @@ -2,12 +2,12 @@ #pragma once -#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "Pipe/Export.h" #include "Pipe/Files/STDFileSystem.h" #include "PipeContainers.h" #include "PipePlatform.h" +#include "PipeStrings.h" namespace p diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index cb013219..66a4fe92 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -4,6 +4,7 @@ #include "Pipe/Core/EnumFlags.h" #include "Pipe/Core/Hash.h" +#include "Pipe/Core/SpinLock.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" #include "PipeContainers.h" @@ -11,8 +12,6 @@ #include "PipeMemory.h" #include "PipePlatform.h" -#include - namespace p { @@ -122,9 +121,6 @@ namespace p }; // --- Incremental CollectStats state (consumer thread only) --- - // Events are append-only, so classification of old events never - // changes. Only events past collectedEvents are classified per call. - mutable i32 collectedEvents = 0; // Newest unmatched alloc index per event key. Chains are // intrusively linked through prevLiveIdx, newest first. mutable LiveIndex liveIdx; @@ -137,7 +133,7 @@ namespace p MemoryStats(); ~MemoryStats(); - // Tracks an allocation. Never blocks; writes one 16B event. + // Tracks an allocation. Writes one 16B event. inline void Add(void* ptr, sizet size) { PushEvent(MemoryStatsEvent{ptr, size}); @@ -153,7 +149,7 @@ namespace p } // Empty memory stats. No diagnostics. - void Release(); + void Reset(); // Update stats so that latest stats and events are reflected void CollectStats() const; @@ -162,34 +158,24 @@ namespace p void CheckLeaks() const; private: - struct ThreadContext + // Fixed-size event block. Chunks form a single shared queue guarded + // by `lock`: all producers append through `tail`, the collector + // drains from `head` and recycles chunks via `spare`. + struct EventChunk { - // Linked list of chunks. Producer writes to tail; consumer reads - // from head. The chain is append-only. - struct Chunk - { - static constexpr u32 capacity = 1024; - std::atomic next{nullptr}; - MemoryStatsEvent slots[capacity]; - std::atomic writeIdx{0}; - u32 readIdx = 0; // consumer-only - }; - - // First chunk with unread events. Producer sets once (release); - // consumer advances to next chunk (relaxed) as it drains. - std::atomic head{nullptr}; - // Producer's current chunk. Consumer does not access. - Chunk* tail = nullptr; - // Drained chunk returned by the consumer for producer reuse. - std::atomic spare{nullptr}; - - MemoryStats* owner = nullptr; - ThreadContext* nextCtx = nullptr; - }; + static constexpr u32 capacity = 1024; - mutable std::atomic contexts{nullptr}; + EventChunk* next = nullptr; + u32 size = 0; + MemoryStatsEvent slots[capacity]; + }; - ThreadContext* GetOrCreateContext(); + // Queue head/tail and a single recycled chunk. All access is + // guarded by `lock` (producers on append, collector on drain). + mutable SpinLock lock; + mutable EventChunk* firstChunk = nullptr; + mutable EventChunk* lastChunk = nullptr; + mutable EventChunk* spareChunk = nullptr; void PushEvent(const MemoryStatsEvent& ev); }; diff --git a/Src/Core/Checks.cpp b/Src/Core/Checks.cpp index dd8f8aae..638e57ce 100644 --- a/Src/Core/Checks.cpp +++ b/Src/Core/Checks.cpp @@ -3,8 +3,8 @@ #include "Pipe/Core/Checks.h" #include "Pipe/Core/Log.h" -#include "PipeStrings.h" #include "Pipe/Core/StringView.h" +#include "PipeStrings.h" namespace p::details @@ -14,8 +14,8 @@ namespace p::details p::String text{inText}; if (text.empty()) { - FormatTo(text, "Failed check \"{}\" at {}:{}", p::StringView{expr}, - p::StringView{file}, line); + FormatTo(text, "Failed check \"{}\" at {}:{}", p::StringView{expr}, p::StringView{file}, + line); } else { diff --git a/Src/Core/Subprocess.cpp b/Src/Core/Subprocess.cpp index fdafea3d..26766e66 100644 --- a/Src/Core/Subprocess.cpp +++ b/Src/Core/Subprocess.cpp @@ -3,10 +3,10 @@ #include "Pipe/Core/Log.h" #include "Pipe/Core/Optional.h" -#include "PipeStrings.h" #include "Pipe/Core/StringView.h" #include "PipeContainers.h" #include "PipePlatform.h" +#include "PipeStrings.h" #include #include diff --git a/Src/Files/PlatformPaths.cpp b/Src/Files/PlatformPaths.cpp index 5ba99d65..4756ee14 100644 --- a/Src/Files/PlatformPaths.cpp +++ b/Src/Files/PlatformPaths.cpp @@ -4,9 +4,9 @@ #include "Pipe/Core/FixedString.h" #include "Pipe/Core/Log.h" -#include "PipeStrings.h" #include "Pipe/Files/Files.h" #include "Pipe/Files/Paths.h" +#include "PipeStrings.h" #if P_PLATFORM_WINDOWS diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index 39bd90b3..9ec5b0b5 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -210,103 +210,68 @@ namespace p { CheckLeaks(); } - // Delete all thread contexts. The producer is no longer running + // Free the remaining queue chunks. The producer is no longer running // (the MemoryStats is being destroyed), so it's safe to free any // remaining chunks. - ThreadContext* c = contexts.exchange(nullptr, std::memory_order_acq_rel); - while (c) + ScopedLock guard(lock); + EventChunk* chunk = firstChunk; + while (chunk) { - ThreadContext* nextCtx = c->nextCtx; - ThreadContext::Chunk* chunk = c->head.load(std::memory_order_relaxed); - while (chunk) - { - ThreadContext::Chunk* next = chunk->next.load(std::memory_order_relaxed); - chunk->~Chunk(); - p::Free(GetStatsArena(), chunk, 1); - chunk = next; - } - if (ThreadContext::Chunk* spare = c->spare.load(std::memory_order_relaxed)) - { - spare->~Chunk(); - p::Free(GetStatsArena(), spare, 1); - } - c->~ThreadContext(); - p::Free(GetStatsArena(), c, 1); - c = nextCtx; + EventChunk* const next = chunk->next; + chunk->~EventChunk(); + p::Free(GetStatsArena(), chunk, 1); + chunk = next; } - } - - MemoryStats::ThreadContext* MemoryStats::GetOrCreateContext() - { - // Per-thread, per-MemoryStats context. The thread_local cache holds - // the most recently used context; we replace it if the owner changed. - thread_local ThreadContext* ctx = nullptr; - if (!ctx || ctx->owner != this) + if (EventChunk* freed = spareChunk) { - ctx = p::Alloc(GetStatsArena(), 1); - new (ctx) ThreadContext{}; - ctx->owner = this; - // Link into the global list. Append-only, so no synchronization - // needed with the consumer beyond the CAS. - ThreadContext* old = contexts.load(std::memory_order_relaxed); - do - { - ctx->nextCtx = old; - } while (!contexts.compare_exchange_weak( - old, ctx, std::memory_order_release, std::memory_order_relaxed)); + freed->~EventChunk(); + p::Free(GetStatsArena(), freed, 1); } - return ctx; + firstChunk = nullptr; + lastChunk = nullptr; + spareChunk = nullptr; } void MemoryStats::PushEvent(const MemoryStatsEvent& ev) { - ThreadContext* ctx = GetOrCreateContext(); - if (ThreadContext::Chunk* chunk = ctx->tail) + ScopedLock guard(lock); + + EventChunk* chunk = lastChunk; + if (!chunk || chunk->size >= EventChunk::capacity) [[unlikely]] { - const u32 idx = chunk->writeIdx.load(std::memory_order_relaxed); - if (idx < ThreadContext::Chunk::capacity) + // Cold path: no chunk yet or current chunk is full. Allocate a new + // chunk, reusing the spareChunk if the collector left one. + chunk = spareChunk; + if (chunk) { - chunk->slots[idx] = ev; - // Release the write so the consumer sees the slot data - // before the new writeIdx. - chunk->writeIdx.store(idx + 1, std::memory_order_release); - return; + spareChunk = nullptr; } - } + else + { + chunk = p::Alloc(GetStatsArena(), 1); + } + new (chunk) EventChunk{}; - // Cold path: no chunk yet or current chunk is full. Allocate a new - // chunk, reusing the spare if the consumer left one. The event is - // written into slot 0 before publishing so the consumer sees a - // complete slot on first read. - ThreadContext::Chunk* newC = ctx->spare.exchange(nullptr, std::memory_order_acquire); - if (!newC) - { - newC = p::Alloc(GetStatsArena(), 1); - } - new (newC) ThreadContext::Chunk{}; - newC->slots[0] = ev; - newC->writeIdx.store(1, std::memory_order_release); - if (ThreadContext::Chunk* const oldTail = ctx->tail) - { - // Publish new chunk via the old chunk's next. Consumer - // discovers it after we've fully initialized newC. - oldTail->next.store(newC, std::memory_order_release); - } - else - { - // First chunk: publish via head. - ctx->head.store(newC, std::memory_order_release); + if (lastChunk) + { + lastChunk->next = chunk; + } + else + { + firstChunk = chunk; + } + lastChunk = chunk; } - ctx->tail = newC; + chunk->slots[chunk->size] = ev; + ++chunk->size; } - void MemoryStats::Release() + void MemoryStats::Reset() { // Drain all thread buffers and reset state. CollectStats(); - used = 0; - totalAllocated = 0; - collectedEvents = 0; + used = 0; + totalAllocated = 0; events.Clear(); live.Clear(); liveIdx.Clear(); @@ -315,65 +280,41 @@ namespace p void MemoryStats::CollectStats() const { - // Walk all thread contexts and drain their chunk chains. For each - // chunk, process all available events, then recycle or free the - // chunk if the producer has already linked a successor. - ThreadContext* c = contexts.load(std::memory_order_acquire); - for (; c != nullptr; c = c->nextCtx) - { - ThreadContext::Chunk* chunk = c->head.load(std::memory_order_acquire); - while (chunk != nullptr) + const i32 lastEventsSize = events.Size(); + + { // Drain the shared event queue + ScopedLock guard(lock); + + EventChunk* chunk = firstChunk; + while (chunk) { - const u32 writeIdx = chunk->writeIdx.load(std::memory_order_acquire); - while (chunk->readIdx < writeIdx) - { - const auto& ev = chunk->slots[chunk->readIdx]; - if (ev.IsFree()) - { - used -= ev.GetSize(); - totalAllocated -= ev.GetSize(); - } - else - { - used += ev.GetSize(); - totalAllocated += ev.GetSize(); - } - events.Add(ev); - ++chunk->readIdx; - } - ThreadContext::Chunk* next = chunk->next.load(std::memory_order_acquire); - if (next == nullptr) + for (u32 i = 0; i < chunk->size; ++i) { - // Producer hasn't allocated a successor yet. Stop. - break; + events.Add(chunk->slots[i]); } - // Producer has moved on. Safe to recycle or free this chunk. - c->head.store(next, std::memory_order_relaxed); - chunk->~Chunk(); - if (!c->spare.load(std::memory_order_relaxed)) + + EventChunk* const next = chunk->next; + + chunk->~EventChunk(); + if (!spareChunk) { - c->spare.store(chunk, std::memory_order_relaxed); + spareChunk = chunk; } else { - p::Free(GetStatsArena(), chunk, 1); + p::Free(GetStatsArena(), chunk, 1); } chunk = next; } + firstChunk = nullptr; + lastChunk = nullptr; } - - // --- Incremental classification of drained events --- - // live[i]: events[i] is an alloc never matched by a free. - // Events are append-only, so bits computed in previous calls remain - // valid; only classify events drained since the last call. A free - // matches the most recent unmatched alloc with the same key (LIFO), - // mirroring a full reverse scan. Free events are classified by - // their flag in the event itself. + // --- Incremental classification of drained events + counters --- live.Resize(events.Size()); prevLiveIdx.Resize(events.Size()); - for (i32 i = collectedEvents; i < events.Size(); ++i) + for (i32 i = lastEventsSize; i < events.Size(); ++i) { const MemoryStatsEvent& ev = events[i]; const u64 hash = GetHash(ev); @@ -382,7 +323,7 @@ namespace p if (i32* nodePtr = liveIdx.Find(hash)) { // Unmark the newest unmatched alloc and pop it off the - // chain, promoting its predecessor as chain head. + // chain, promoting its predecessor as chain firstChunk. const i32 node = *nodePtr; live.SetFalse(node); const i32 prev = prevLiveIdx[node]; @@ -399,11 +340,11 @@ namespace p } else { - i32* headPtr = liveIdx.FindOrInsert(hash, i); - if (*headPtr != i) + i32* idxPtr = liveIdx.FindOrInsert(hash, i); + if (*idxPtr != i) { - prevLiveIdx[i] = *headPtr; - *headPtr = i; + prevLiveIdx[i] = *idxPtr; + *idxPtr = i; } else { @@ -412,7 +353,22 @@ namespace p live.SetTrue(i); } } - collectedEvents = events.Size(); + + // Update stats + for (i32 i = lastEventsSize; i < events.Size(); ++i) + { + const MemoryStatsEvent& ev = events[i]; + const sizet size = ev.GetSize(); + if (ev.IsFree()) + { + used -= size; + } + else + { + used += size; + totalAllocated += size; + } + } } void MemoryStats::CheckLeaks() const diff --git a/Src/Pipe.cpp b/Src/Pipe.cpp index 137888b8..3fa71c07 100644 --- a/Src/Pipe.cpp +++ b/Src/Pipe.cpp @@ -1,6 +1,6 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. -//#include "PipeNewDelete.h" +#include "PipeNewDelete.h" // New/Delete must be first include #include "Pipe.h" diff --git a/Src/PipeMemoryArenas.cpp b/Src/PipeMemoryArenas.cpp index a49ce67e..55c89967 100644 --- a/Src/PipeMemoryArenas.cpp +++ b/Src/PipeMemoryArenas.cpp @@ -98,7 +98,7 @@ namespace p void MonoLinearArena::Release(bool keepIfSelfAllocated) { - stats.Release(); + stats.Reset(); insert = block.data; count = 0; if (selfAllocated && !keepIfSelfAllocated) @@ -261,7 +261,7 @@ namespace p void MultiLinearArena::Release() { - stats.Release(); + stats.Reset(); smallPool.Release(GetParentArena()); mediumPool.Release(GetParentArena()); bigPool.Release(GetParentArena()); diff --git a/Tests/Core/SpinLock.spec.cpp b/Tests/Core/SpinLock.spec.cpp new file mode 100644 index 00000000..793fca38 --- /dev/null +++ b/Tests/Core/SpinLock.spec.cpp @@ -0,0 +1,187 @@ +// Copyright 2015-2026 Piperift. All Rights Reserved. + +#include +#include + +#include +#include +#include + + +using namespace snowhouse; +using namespace bandit; +using namespace p; + + +go_bandit([]() +{ + describe("Core.SpinLock", []() + { + describe("SpinLock", [&]() + { + it("Acquires and releases exclusively", [&]() + { + SpinLock lock; + ScopedLock guard(lock); + + AssertThat(lock.Locked(), Is().True()); + AssertThat(lock.TryLock(), Is().False()); + }); + + it("Allows serialized writers to increment a counter", [&]() + { + SpinLock lock; + i32 counter = 0; + + constexpr i32 kThreads = 4; + constexpr i32 kPerThread = 10'000; + + std::vector threads; + std::atomic start{false}; + for (i32 t = 0; t < kThreads; ++t) + { + threads.emplace_back([&]() + { + while (!start.load(std::memory_order_acquire)) + {} + for (i32 i = 0; i < kPerThread; ++i) + { + ScopedLock guard(lock); + ++counter; + } + }); + } + + start.store(true, std::memory_order_release); + for (auto& thread : threads) + { + thread.join(); + } + + AssertThat(counter, Is().EqualTo(kThreads * kPerThread)); + }); + }); + + describe("SharedSpinLock", [&]() + { + it("Exclusive lock excludes a second exclusive lock", [&]() + { + SharedSpinLock lock; + ExclusiveScopedLock writer(lock); + + AssertThat(lock.TryLockExclusive(), Is().False()); + }); + + it("Exclusive lock excludes shared locks", [&]() + { + SharedSpinLock lock; + ExclusiveScopedLock writer(lock); + + AssertThat(lock.TryLockShared(), Is().False()); + }); + + it("Shared lock excludes an exclusive lock", [&]() + { + SharedSpinLock lock; + SharedScopedLock reader(lock); + + AssertThat(lock.TryLockExclusive(), Is().False()); + }); + + it("Allows multiple overlapping shared locks", [&]() + { + SharedSpinLock lock; + + SharedScopedLock r1(lock); + SharedScopedLock r2(lock); + SharedScopedLock r3(lock); + + // Readers coexist: shared still acquirable. + AssertThat(lock.TryLockShared(), Is().True()); + lock.UnlockShared(); + + AssertThat(lock.TryLockExclusive(), Is().False()); + }); + + it("Writers exclude each other", [&]() + { + SharedSpinLock lock; + + ExclusiveScopedLock w1(lock); + AssertThat(lock.TryLockExclusive(), Is().False()); + }); + + it("Writes under exclusive lock are mutually excluded", [&]() + { + SharedSpinLock lock; + i32 counter = 0; + + constexpr i32 kThreads = 4; + constexpr i32 kPerThread = 10'000; + + std::vector threads; + std::atomic start{false}; + for (i32 t = 0; t < kThreads; ++t) + { + threads.emplace_back([&]() + { + while (!start.load(std::memory_order_acquire)) + {} + for (i32 i = 0; i < kPerThread; ++i) + { + ExclusiveScopedLock writer(lock); + ++counter; + } + }); + } + + start.store(true, std::memory_order_release); + for (auto& thread : threads) + { + thread.join(); + } + + AssertThat(counter, Is().EqualTo(kThreads * kPerThread)); + }); + + it("Shared readers run concurrently without tearing shared state", [&]() + { + SharedSpinLock lock; + i32 value = 0; + + constexpr i32 kThreads = 4; + constexpr i32 kIterations = 10'000; + + // Shared-side readers are allowed to overlap, so they must only + // read. This just checks that many threads can take the shared + // side simultaneously without deadlocking or corrupting the lock. + std::vector threads; + std::atomic start{false}; + std::atomic reads{0}; + for (i32 t = 0; t < kThreads; ++t) + { + threads.emplace_back([&]() + { + while (!start.load(std::memory_order_acquire)) + {} + for (i32 i = 0; i < kIterations; ++i) + { + SharedScopedLock reader(lock); + const i32 v = value; + (void)v; + reads.fetch_add(1, std::memory_order_relaxed); + } + }); + } + + start.store(true, std::memory_order_release); + for (auto& thread : threads) + { + thread.join(); + } + + AssertThat(reads.load(), Is().EqualTo(kThreads * kIterations)); + }); + }); + }); +}); diff --git a/Tests/Memory/MemoryStats.spec.cpp b/Tests/Memory/MemoryStats.spec.cpp index b717ca1e..e5f0f268 100644 --- a/Tests/Memory/MemoryStats.spec.cpp +++ b/Tests/Memory/MemoryStats.spec.cpp @@ -277,12 +277,12 @@ go_bandit([]() AssertThat(FreeCount(b), Is().EqualTo(1)); }); - it("Add after Release works", [&]() + it("Add after Reset works", [&]() { MemoryStats s; s.detectLeaks = false; s.Add((void*)0x1000, 64); - s.Release(); + s.Reset(); AssertThat(AllocCount(s), Is().EqualTo(0)); s.Add((void*)0x2000, 32); @@ -340,7 +340,7 @@ go_bandit([]() AssertThat(AllocCount(s), Is().EqualTo(1)); }); - it("Release resets state", [&]() + it("Reset resets state", [&]() { MemoryStats s; s.Add((void*)0x1000, 64); @@ -348,7 +348,7 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo(96)); - s.Release(); + s.Reset(); AssertThat(s.used, Is().EqualTo(0)); AssertThat(s.totalAllocated, Is().EqualTo(0)); AssertThat(AllocCount(s), Is().EqualTo(0)); @@ -483,7 +483,7 @@ go_bandit([]() AssertThat(s.used, Is().EqualTo(N * 8)); // Suppress leak warnings at destruction (test buffers are stack). - s.Release(); + s.Reset(); }); it("Many threads add, then collects", [&]() @@ -542,7 +542,7 @@ go_bandit([]() AssertThat(s.totalAllocated, Is().EqualTo(N * 8)); // Suppress leak warnings at destruction (test buffers are stack). - s.Release(); + s.Reset(); }); it("Many threads add and remove, then collects", [&]() @@ -606,7 +606,7 @@ go_bandit([]() AssertThat(s.totalAllocated, Is().EqualTo((N / 2) * 8)); // Suppress leak warnings at destruction (test buffers are stack). - s.Release(); + s.Reset(); }); }); @@ -673,7 +673,7 @@ go_bandit([]() AssertThat(s.used, Is().EqualTo(s.totalAllocated)); // Suppress leak warnings at destruction (test buffers are stack). - s.Release(); + s.Reset(); }); }); }); diff --git a/Tests/Reflection/TypeName.spec.cpp b/Tests/Reflection/TypeName.spec.cpp index 73ec0b94..6b947ffb 100644 --- a/Tests/Reflection/TypeName.spec.cpp +++ b/Tests/Reflection/TypeName.spec.cpp @@ -3,10 +3,10 @@ #include #include #include -#include #include #include #include +#include using namespace snowhouse; From 0c2ee2cf337076a33450ca172088f9eb16e95f07 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 1 Sep 2026 21:57:49 +0200 Subject: [PATCH 04/15] Renamed liveAllocIdx in MemoryStats --- Include/Pipe/Memory/MemoryStats.h | 6 +++--- Src/Memory/MemoryStats.cpp | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index 66a4fe92..c0582aca 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -122,11 +122,11 @@ namespace p // --- Incremental CollectStats state (consumer thread only) --- // Newest unmatched alloc index per event key. Chains are - // intrusively linked through prevLiveIdx, newest first. + // intrusively linked through liveAllocIdx, newest first. mutable LiveIndex liveIdx; // For each alloc event index, the previous unmatched alloc index // sharing the same key (NO_INDEX if none). Consumed on free. - mutable TArray prevLiveIdx; + mutable TArray liveAllocIdx; public: @@ -166,7 +166,7 @@ namespace p static constexpr u32 capacity = 1024; EventChunk* next = nullptr; - u32 size = 0; + u32 size = 0; MemoryStatsEvent slots[capacity]; }; diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index 9ec5b0b5..1b3929f8 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -199,7 +199,7 @@ namespace p : events{GetStatsArena()} , live{GetStatsArena()} , liveIdx{GetStatsArena()} - , prevLiveIdx{GetStatsArena()} + , liveAllocIdx{GetStatsArena()} {} MemoryStats::~MemoryStats() @@ -275,7 +275,7 @@ namespace p events.Clear(); live.Clear(); liveIdx.Clear(); - prevLiveIdx.Clear(); + liveAllocIdx.Clear(); } void MemoryStats::CollectStats() const @@ -312,7 +312,7 @@ namespace p // --- Incremental classification of drained events + counters --- live.Resize(events.Size()); - prevLiveIdx.Resize(events.Size()); + liveAllocIdx.Resize(events.Size()); for (i32 i = lastEventsSize; i < events.Size(); ++i) { @@ -326,7 +326,7 @@ namespace p // chain, promoting its predecessor as chain firstChunk. const i32 node = *nodePtr; live.SetFalse(node); - const i32 prev = prevLiveIdx[node]; + const i32 prev = liveAllocIdx[node]; if (prev == NO_INDEX) { liveIdx.EraseAt(nodePtr); @@ -343,12 +343,12 @@ namespace p i32* idxPtr = liveIdx.FindOrInsert(hash, i); if (*idxPtr != i) { - prevLiveIdx[i] = *idxPtr; - *idxPtr = i; + liveAllocIdx[i] = *idxPtr; + *idxPtr = i; } else { - prevLiveIdx[i] = NO_INDEX; + liveAllocIdx[i] = NO_INDEX; } live.SetTrue(i); } From bef75d973f0659b3d8d6baff10835b0feaf6fac4 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 1 Sep 2026 23:10:18 +0200 Subject: [PATCH 05/15] PipeDebug arena selection --- Include/Misc/PipeDebug.h | 155 ++++++++++++++++++++++++--------------- Include/PipeContainers.h | 8 +- Src/PipeContainers.cpp | 25 ++++--- 3 files changed, 116 insertions(+), 72 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index fdb7470e..5d8614d8 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -2219,6 +2219,28 @@ namespace p snapshot.used = stats->used; snapshot.events = &stats->events; snapshot.live = &stats->live; + + // Union live allocation addresses into the arena range so + // arenas without blocks (e.g. HeapArena) still report the + // memory they actually use. + if (snapshot.live && snapshot.events) + { + i32 i = -1; + while ((i = snapshot.live->GetNextSet(i)) != NO_INDEX) + { + const auto& ev = (*snapshot.events)[i]; + const u8* allocBegin = ev.GetPtr(); + if (!snapshot.begin || allocBegin < snapshot.begin) + { + snapshot.begin = allocBegin; + } + const u8* allocEnd = allocBegin + ev.GetSize(); + if (!snapshot.end || allocEnd > snapshot.end) + { + snapshot.end = allocEnd; + } + } + } } memoryDbg.snapshots.Add(snapshot); @@ -2879,6 +2901,8 @@ namespace p #pragma endregion Draw // ----- Arena columns loop (blocks, markers, click, tooltip) ----- + const ImVec2 mousePos = ImGui::GetIO().MousePos; + const bool inGraph = graphRect.Contains(mousePos); for (i32 i = 0; i < memoryDbg.snapshots.Size(); ++i) { const auto& snapshot = memoryDbg.snapshots[i]; @@ -2957,22 +2981,32 @@ namespace p // Walk live allocs if (snapshot.live && snapshot.events) { + // Filter visible allocations const float padding = colW * 0.25f; const sizet viewStartS = static_cast(viewStart); const sizet viewEndS = static_cast(viewStart + viewRange); - i32 lastI = NO_INDEX; - for (i32 j = snapshot.live->GetNextSet(NO_INDEX); j > lastI; - lastI = j, j = snapshot.live->GetNextSet(j)) + TArray liveInRange; + for (i32 j = snapshot.live->GetNextSet(NO_INDEX); j != NO_INDEX; + j = snapshot.live->GetNextSet(j)) { const auto& ev = (*snapshot.events)[j]; const sizet addr = reinterpret_cast(ev.GetPtr()); const sizet size = ev.GetSize(); - if (addr + size <= viewStartS || addr >= viewEndS) + if (addr >= viewEndS || addr + size <= viewStartS) { continue; } - const float ty = AddrToY(addr); - const float ty2 = AddrToY(addr + size); + liveInRange.Add(j); + } + + // Draw allocations + for (i32 i : liveInRange) + { + const auto& ev = (*snapshot.events)[i]; + const sizet addr = reinterpret_cast(ev.GetPtr()); + const sizet size = ev.GetSize(); + const float ty = AddrToY(addr); + const float ty2 = AddrToY(addr + size); if (ty >= addressY0 && ty2 <= addressY1) { drawList->AddRectFilled(ImVec2(colX + padding, ty - 0.5f), @@ -2983,12 +3017,54 @@ namespace p } } - // Column click selects arena + // Column click selects the arena (even without blocks), or the + // specific block when clicking inside one. const ImRect colRect(ImVec2(colX, addressY0), ImVec2(colRight, addressY1)); - if (colRect.Contains(ImGui::GetIO().MousePos) && ImGui::IsMouseClicked(0)) + if (inGraph && colRect.Contains(mousePos) && ImGui::IsMouseClicked(0) + && !memoryDbg.isSelecting) { - memoryDbg.hasSelection = true; - memoryDbg.selectionArenaIdx = i; + i32 blockIdx = NO_INDEX; + if (snapshot.begin && snapshot.capacity > 0) + { + for (i32 e = 0; e < snapshot.blocks.Size() && blockIdx == NO_INDEX; ++e) + { + const auto& block = snapshot.blocks[e]; + const sizet bStart = reinterpret_cast(block.data); + const sizet bEnd = bStart + block.size; + if (bEnd <= bStart) + { + continue; + } + const float y0 = AddrToY(bStart); + const float y1 = AddrToY(bEnd); + if (mousePos.y >= y0 && mousePos.y <= y1) + { + blockIdx = e; + } + } + } + + if (blockIdx != NO_INDEX) + { + const auto& block = snapshot.blocks[blockIdx]; + memoryDbg.isSelecting = false; + memoryDbg.hasSelection = true; + memoryDbg.selectionStart = sizet(block.data); + memoryDbg.selectionEnd = memoryDbg.selectionStart + block.size; + memoryDbg.selectionArenaIdx = i; + memoryDbg.selectionBlockIdx = blockIdx; + } + else // Empty part of the column: select the arena itself + { + memoryDbg.isSelecting = false; + memoryDbg.hasSelection = true; + memoryDbg.selectionArenaIdx = i; + memoryDbg.selectionBlockIdx = NO_INDEX; + // Select the arena's full memory range (min/max over + // all its allocations and blocks). + memoryDbg.selectionStart = reinterpret_cast(snapshot.begin); + memoryDbg.selectionEnd = reinterpret_cast(snapshot.end); + } } // Column tooltip @@ -3132,62 +3208,22 @@ namespace p } // ----- Selection (left-click any column, block-click sets block range, drag) - const ImVec2 mousePos = ImGui::GetIO().MousePos; - const bool inGraph = graphRect.Contains(mousePos); { // Selection Logic - // Start selection on left-click + // Start selection on left-click. Clicks inside an arena column + // are handled above (arena/block selection), so only start a + // drag-range when clicking outside all columns (ruler/hex/ascii). if (ImGui::IsMouseClicked(0) && inGraph && !memoryDbg.isSelecting) { - i32 arenaIdx = NO_INDEX; - i32 blockIdx = NO_INDEX; - for (i32 i = 0; i < memoryDbg.snapshots.Size(); ++i) + bool overColumn = false; + for (i32 i = 0; i < memoryDbg.snapshots.Size() && !overColumn; ++i) { - const auto& snapshot = memoryDbg.snapshots[i]; - if (!snapshot.begin || snapshot.capacity == 0) - { - continue; - } const float cx = ArenaColumnX(i); - if (mousePos.x < cx || mousePos.x >= cx + colW) - { - continue; - } - for (i32 e = 0; e < snapshot.blocks.Size(); ++e) - { - const auto& block = snapshot.blocks[e]; - const sizet bStart = reinterpret_cast(block.data); - const sizet bEnd = bStart + block.size; - if (bEnd <= bStart) - { - continue; - } - const float y0 = AddrToY(bStart); - const float y1 = AddrToY(bEnd); - if (mousePos.y >= y0 && mousePos.y <= y1) - { - arenaIdx = i; - blockIdx = e; - break; - } - } - - if (blockIdx != NO_INDEX) + if (mousePos.x >= cx && mousePos.x < cx + colW) { - break; + overColumn = true; } } - - if (blockIdx != NO_INDEX) - { - memoryDbg.isSelecting = false; - const auto& block = memoryDbg.snapshots[arenaIdx].blocks[blockIdx]; - memoryDbg.hasSelection = true; - memoryDbg.selectionStart = sizet(block.data); - memoryDbg.selectionEnd = memoryDbg.selectionStart + block.size; - memoryDbg.selectionArenaIdx = arenaIdx; - memoryDbg.selectionBlockIdx = blockIdx; - } - else + if (!overColumn) { memoryDbg.isSelecting = true; memoryDbg.selectionFirstAddr = ScreenYToAddr(mousePos.y); @@ -3455,6 +3491,9 @@ namespace p static String sizeStr; if (selectedArena) { + detailsLabel = selectedArena->name.AsString(); + ImGui::Text("%s", detailsLabel.c_str()); + detailsLabel = GetTypeName(selectedArena->typeId); ImGui::Text("Type: %s", detailsLabel.c_str()); ImGui::Text("Range: 0x%llX - 0x%llX", diff --git a/Include/PipeContainers.h b/Include/PipeContainers.h index 7d72b02f..6ff71cf5 100644 --- a/Include/PipeContainers.h +++ b/Include/PipeContainers.h @@ -1883,11 +1883,11 @@ namespace p void Clear(); - // Returns index of next set bit in array (wraps around) - i32 GetNextSet(i32 index) const; + // Returns index of next set bit in array (wraps around only if loops) + i32 GetNextSet(i32 index, bool loops = false) const; - // @return index of previous set bit in array (wraps around) - i32 GetPreviousSet(i32 index) const; + // @return index of previous set bit in array (wraps around only if loops) + i32 GetPreviousSet(i32 index, bool loops = false) const; /** @return number of set bits in the whole array. */ i32 CountSetBits() const; diff --git a/Src/PipeContainers.cpp b/Src/PipeContainers.cpp index 8a5d15fb..ce74fd6a 100644 --- a/Src/PipeContainers.cpp +++ b/Src/PipeContainers.cpp @@ -126,27 +126,29 @@ namespace p bits.Clear(); } - i32 BitArray::GetNextSet(i32 index) const + i32 BitArray::GetNextSet(i32 index, bool loops) const { - i32 i; - for (i = index + 1; i < size; ++i) + for (i32 i = index + 1; i < size; ++i) { if (IsSet(i)) { return i; } } - for (i = 0; i < index - 1; ++i) + if (loops) { - if (IsSet(i)) + for (i32 i = 0; i < index - 1; ++i) { - return i; + if (IsSet(i)) + { + return i; + } } } return NO_INDEX; } - i32 BitArray::GetPreviousSet(i32 index) const + i32 BitArray::GetPreviousSet(i32 index, bool loops) const { i32 i; if (index != 0) @@ -164,11 +166,14 @@ namespace p } } - for (i = size - 1; i > index; --i) + if (loops) { - if (IsSet(i)) + for (i = size - 1; i > index; --i) { - return i; + if (IsSet(i)) + { + return i; + } } } return NO_INDEX; From 6222ac4e239d438c2d2bd388e48ce986ddfa0534 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 1 Sep 2026 23:57:04 +0200 Subject: [PATCH 06/15] Setting to enable/disable Pipe's own new delete override --- CMakeLists.txt | 5 +++++ Src/Pipe.cpp | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6f794ae..71b46354 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ endif() option(PIPE_BUILD_SHARED "Build shared libraries" ON) option(PIPE_BUILD_TESTS "Build Pipe tests" ${PIPE_IS_PROJECT}) option(PIPE_ENABLE_ALLOCATION_STACKS "Should allocation call stacks be tracked?" OFF) +option(PIPE_OVERRIDE_NEWDELETE "Should pipe's compilation unit override new/delete" ON) option(PIPE_BUILD_WARNINGS "Enable compiler warnings" OFF) option(PIPE_ENABLE_CLANG_TOOLS "Enable clang-tidy and clang-format" ${PIPE_IS_PROJECT}) @@ -69,6 +70,10 @@ if(PIPE_ENABLE_ALLOCATION_STACKS) target_compile_definitions(Pipe PUBLIC P_ENABLE_ALLOCATION_STACKS=0) endif() +if(PIPE_OVERRIDE_NEWDELETE) + target_compile_definitions(Pipe PUBLIC P_OVERRIDE_NEWDELETE=1) +endif() + pipe_target_enable_CPP20(Pipe) pipe_add_sanitizers(Pipe) pipe_target_shared_output_directory(Pipe) diff --git a/Src/Pipe.cpp b/Src/Pipe.cpp index 3fa71c07..821f3b58 100644 --- a/Src/Pipe.cpp +++ b/Src/Pipe.cpp @@ -1,7 +1,9 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. -#include "PipeNewDelete.h" +#if defined(P_OVERRIDE_NEWDELETE) + #include "PipeNewDelete.h" // New/Delete must be first include +#endif #include "Pipe.h" #include "Pipe/Core/Log.h" From 08b51c5260733c7511a874d98dcffbfa52f6c2d8 Mon Sep 17 00:00:00 2001 From: muit Date: Wed, 2 Sep 2026 16:17:39 +0200 Subject: [PATCH 07/15] Memory debugger: Added timeline, better zoom and scroll, better HEX and ASCII, and other fixes --- Include/Misc/PipeDebug.h | 1513 +++++++++++++++++++++++------ Include/Misc/PipeImGui.h | 47 + Include/Pipe/Memory/MemoryStats.h | 25 +- Include/PipeStrings.h | 5 +- Src/Memory/MemoryStats.cpp | 100 +- Src/PipeStrings.cpp | 51 +- Tests/Memory/MemoryStats.spec.cpp | 189 ++-- 7 files changed, 1419 insertions(+), 511 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index 5d8614d8..711f3eec 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -36,6 +36,7 @@ namespace p { /////////////////////////////////////////////////////////// // Definition + struct DebugContext; #pragma region Inspection struct TypeInspection @@ -171,9 +172,16 @@ namespace p namespace details { + // Capture transport icons (raw UTF-8, FontAwesome5 solid) + // Avoids including IconsFontAwesome5.h in Pipe headers. + constexpr const char* ICON_CI_CAPTURE_BACK = "\xef\x81\x88"; // fa-step-backward + constexpr const char* ICON_CI_CAPTURE_FWD = "\xef\x81\x91"; // fa-step-forward + constexpr const char* ICON_CI_CAPTURE_RECORD = "\xef\x84\x91"; // fa-circle + constexpr const char* ICON_CI_CAPTURE_TRASH = "\xef\x87\xb8"; // fa-trash + void DrawEntityInspector(StringView label, DebugECSInspector& inspector, bool* open = nullptr, ImGuiWindowFlags flags = 0); - } + } // namespace details void DrawIdRegistry( const char* label = "Id Registry", bool* open = nullptr, ImGuiWindowFlags flags = 0); @@ -203,9 +211,9 @@ namespace p #pragma region Memory struct DebugMemoryContext { - bool showHex = true; - bool showAscii = true; - i32 bytesPerLine = 4; + bool showHEX = true; + bool showASCII = true; + i32 bytesPerLine = 8; bool showDetails = true; bool resetLayout = true; @@ -275,13 +283,74 @@ namespace p Tag name; TypeId typeId; Tag typeName; - const TArray* events = nullptr; - const BitArray* live = nullptr; + // When captured, live allocs are owned by this snapshot + // (ownedLiveAllocs). When live, live points to the live + // stats data and is only valid during the current live rebuild. + bool captured = false; + TArray ownedLiveAllocs; + const TArray* live = nullptr; + // Index of the parent arena snapshot (the Arena a ChildArena allocates from). + i32 parentArenaIdx = NO_INDEX; + }; + + // A single view of all arenas: either the live state or a captured + // frame at a point in time. Owns its snapshots (captured frames own a + // deep copy of each arena's data). + struct MemorySnapshot + { + TArray snapshots; + }; + + // The live snapshot, rebuilt every frame while in live mode. + MemorySnapshot liveSnapshot; + // Points to the snapshot currently being displayed: liveSnapshot when + // live, or one of `captures` when viewing a capture. Never copies. + const MemorySnapshot* curSnapshot = nullptr; + + // Capture timeline + TArray captures; + i32 captureIndex = NO_INDEX; + + // Per-frame recorded stats, keyed by stable Arena pointer. Used by the + // timeline graph. Recorded only while the debugger is open. + struct TimelineSample + { + double time = 0.0; + sizet used = 0; + sizet capacity = 0; + }; + struct ArenaTimeline + { + const Arena* arena = nullptr; + TArray samples; }; - TArray snapshots; + // Ring-buffer of recent per-frame samples, one entry per arena. + TArray timelines; + // Clock for the timeline, accumulated from ImGui::GetIO().DeltaTime. + double timelineCurrentTime = 0.0; + // How many seconds of history the timeline retains (configurable). + float timelineBufferSeconds = 15.0f; + // Fixed pixel height of the timeline panel. + float timelineHeight = 100.0f; + + bool IsLive() const + { + return captureIndex < 0; + } + + bool HasCaptures() const + { + return !captures.IsEmpty(); + } }; + void DrawMemory(const char* label = "Memory", bool* open = nullptr, ImGuiWindowFlags flags = 0); + + // Capture the current state of all memory arenas. The capture is + // appended to the capture timeline and becomes the current view. + void CaptureMemory(DebugContext& ctx); + #pragma endregion Memory struct DebugContext @@ -412,6 +481,7 @@ namespace p constexpr LinearColor includeColor = LinearColor::FromHex(0x40A832); constexpr LinearColor excludeColor = LinearColor::FromHex(0xA83632); constexpr LinearColor previewColor = LinearColor::FromHex(0x3265A8); + constexpr Color selectionColor{255, 200, 80}; // For internal use only @@ -2056,14 +2126,9 @@ namespace p // Bottom-to-top: pos is the screen position of the BOTTOM-LEFT corner // of the first glyph. The text rises upward from there. Each glyph's // top face points RIGHT (tilt head right to read). - static void AddTextVertical(ImDrawList* draw, ImVec2 pos, ImU32 col, const char* text_begin, - const char* text_end = nullptr) + static void AddTextVertical(ImDrawList* draw, ImVec2 pos, ImU32 col, const StringView text) { - if (!text_end) - { - text_end = text_begin + strlen(text_begin); - } - if (text_begin == text_end || draw == nullptr) + if (text.empty() || draw == nullptr) { return; } @@ -2080,7 +2145,8 @@ namespace p } const float scale = imguiFontSize / font->Size; v2 cursor = v2{RoundToZero(pos.x), RoundToZero(pos.y)}; - const char* s = text_begin; + const char* s = text.data(); + const char* text_end = text.data() + text.size(); i32 chars_exp = (i32)(text_end - s); i32 chars_rnd = 0; const i32 vtx_count_max = chars_exp * 4; @@ -2169,97 +2235,739 @@ namespace p } } // namespace details - void DrawMemory(const char* label, bool* open, ImGuiWindowFlags flags) + // Records one timeline sample per arena from the last live snapshot and + // prunes samples older than the configured buffer window. Called once per + // frame while the debugger window is open, in both live and capture modes. + static void RecordMemoryTimeline(DebugMemoryContext& memoryDbg) { - if (!EnsureInsideDebug) + memoryDbg.timelineCurrentTime += ImGui::GetIO().DeltaTime; + const double now = memoryDbg.timelineCurrentTime; + + for (const auto& snapshot : memoryDbg.liveSnapshot.snapshots) { - return; + if (!snapshot.arena) + { + continue; + } + // Find or create the arena's timeline. + DebugMemoryContext::ArenaTimeline* timeline = nullptr; + for (auto& t : memoryDbg.timelines) + { + if (t.arena == snapshot.arena) + { + timeline = &t; + break; + } + } + if (!timeline) + { + DebugMemoryContext::ArenaTimeline nt; + nt.arena = snapshot.arena; + nt.samples.Add( + DebugMemoryContext::TimelineSample{now, snapshot.used, snapshot.capacity}); + memoryDbg.timelines.Add(Move(nt)); + continue; + } + + timeline->samples.Add( + DebugMemoryContext::TimelineSample{now, snapshot.used, snapshot.capacity}); + + // Prune samples older than the buffer window, keeping at least one. + const double window = static_cast(memoryDbg.timelineBufferSeconds); + while (timeline->samples.Size() > 1 && now - timeline->samples[0].time > window) + { + timeline->samples.RemoveAt(0, 1, Shrink::No); + } + } + } + + + static void DrawMemoryTimeline(DebugMemoryContext& memoryDbg) + { + ImDrawList* tlDraw = ImGui::GetWindowDrawList(); + const ImVec2 tlPos = ImGui::GetCursorScreenPos(); + const float tlW = ImGui::GetContentRegionAvail().x; + const float tlH = memoryDbg.timelineHeight; + const ImRect tlRect(tlPos, ImVec2(tlPos.x + tlW, tlPos.y + tlH)); + const bool tlHovered = tlRect.Contains(ImGui::GetIO().MousePos); + + // Background + tlDraw->AddRectFilled(tlRect.Min, tlRect.Max, ImGui::GetColorU32(ImGuiCol_WindowBg)); + ImGui::Dummy(ImVec2(tlW, tlH + 2.0f)); // reserve vertical space + + // Gather the arenas currently shown: heap always, plus selections. + // Newest-time = right edge. Determine visible time window. + double newestTime = memoryDbg.timelineCurrentTime; + double oldestTime = newestTime - static_cast(memoryDbg.timelineBufferSeconds); + if (oldestTime < 0.0) + { + oldestTime = 0.0; } + const double span = (newestTime - oldestTime) > 0.0 ? (newestTime - oldestTime) : 1.0; - auto& memoryDbg = currentContext->memory; + struct VisibleArena + { + const Arena* arena; + bool isHeap; + }; + TArray visible; + // Global selection: one selected arena shared by the memory graph + // and the timeline. + if (memoryDbg.selectionArenaIdx != NO_INDEX && memoryDbg.curSnapshot + && memoryDbg.selectionArenaIdx >= 0 + && memoryDbg.selectionArenaIdx < memoryDbg.curSnapshot->snapshots.Size()) + { + const auto& snap = memoryDbg.curSnapshot->snapshots[memoryDbg.selectionArenaIdx]; + if (snap.arena) + { + visible.Add(VisibleArena{snap.arena, false}); + } + } + // Heap arena always shown (found in liveSnapshot) unless it's already + // present as the selected arena. + for (const auto& snap : memoryDbg.liveSnapshot.snapshots) + { + if (snap.arena && snap.typeId == GetTypeId()) + { + bool alreadyShown = false; + for (const auto& v : visible) + { + if (v.arena == snap.arena) + { + alreadyShown = true; + break; + } + } + if (!alreadyShown) + { + visible.Add(VisibleArena{snap.arena, true}); + } + break; + } + } + + // Sort arena labels by their current used size (largest first). + visible.Sort([&](const VisibleArena& a, const VisibleArena& b) + { + const auto latestUsed = [&](const Arena* arena) -> sizet + { + for (const auto& t : memoryDbg.timelines) + { + if (t.arena == arena && !t.samples.IsEmpty()) + { + return t.samples.Last().used; + } + } + return 0; + }; + return latestUsed(a.arena) > latestUsed(b.arena); + }); + + // Compute Y scale: max used/capacity across visible arenas in window. + sizet yMax = 1; + for (const auto& va : visible) + { + for (const auto& t : memoryDbg.timelines) + { + if (t.arena != va.arena) + { + continue; + } + for (const auto& s : t.samples) + { + if (s.time < oldestTime) + { + continue; + } + if (!va.isHeap && s.capacity > yMax) + { + yMax = s.capacity; + } + if (s.used > yMax) + { + yMax = s.used; + } + } + } + } + + // Plot area fills the whole timeline rect (no padding/border). + const float plotX0 = tlRect.Min.x; + const float plotX1 = tlRect.Max.x; + const float plotY0 = tlRect.Min.y; + const float plotY1 = tlRect.Max.y; + const float plotW = (plotX1 - plotX0) > 0.0f ? (plotX1 - plotX0) : 1.0f; + const float plotH = (plotY1 - plotY0) > 0.0f ? (plotY1 - plotY0) : 1.0f; + + const auto XFor = [&](double t) -> float + { + const double rel = (t - oldestTime) / span; + return plotX0 + static_cast(rel) * plotW; + }; + // Round the top of the log scale up to a power of two, with 10% + // headroom above the real maximum (e.g. max 1MB -> top 2MB). + i32 maxLog = static_cast(p::Log2(static_cast(yMax))); + const sizet scaledMax = static_cast(static_cast(yMax) * 1.1); + if (scaledMax > (sizet{1} << maxLog)) + { + maxLog += 1; + } + const sizet roundMax = sizet{1} << maxLog; + + const auto YFor = [&](sizet v) -> float + { + // Semi-log Y axis: blend a linear and a log mapping so the + // scale is less aggressively exponential. Values run from 1 + // (bottom) to roundMax (top, ~90% height). + const double maxD = static_cast(roundMax); + const double linRel = (v <= 1) ? 0.0 : (static_cast(v) - 1.0) / (maxD - 1.0); + const double lMax = p::Log2(maxD); + const double logRel = (v <= 1) ? 0.0 : p::Log2(static_cast(v)) / lMax; + const double blended = p::Lerp(linRel, logRel, 0.5); + const double rel = p::Min(blended, 1.0) * 0.9; + return plotY1 - static_cast(rel) * plotH; + }; + + // Draw plot area background. + tlDraw->AddRectFilled( + ImVec2(plotX0, plotY0), ImVec2(plotX1, plotY1), IM_COL32(20, 20, 20, 255)); + const ImU32 gridCol = p::Color{255, 255, 255, 18}.DWColor(); + + // Scale guides: one per power-of-two value tier, drawn from the + // top down. Only tiers kept that are at least a minimum pixel gap + // apart, so guides span the whole height — including reaching + // small sizes near the bottom of the (semi-log) axis. + const float labelH = ImGui::GetTextLineHeight(); + String sizeLabel; + const float minGap = 16.0f; + float lastY = -FLT_MAX; + for (i32 log2v = maxLog; log2v >= 1; log2v -= 2) + { + const sizet v = sizet{1} << log2v; + const float ty = YFor(v); + if (ty > plotY1 - labelH) + { + continue; // below the bottom of the graph + } + if (lastY != -FLT_MAX && ty - lastY < minGap) + { + continue; // too close to the previous guide + } + lastY = ty; + tlDraw->AddLine(ImVec2(plotX0, ty), ImVec2(plotX1, ty), gridCol); + sizeLabel.clear(); + Strings::ParseMemorySizeTo(sizeLabel, v); + tlDraw->AddText(ImVec2(plotX0 + 8.0f, ty - labelH), p::Color{150, 150, 150}.DWColor(), + sizeLabel.c_str()); + } - // Rebuild arena info cache - memoryDbg.snapshots.Clear(); - TArray arenas; - GetAllArenas(arenas); - for (const auto* arena : arenas) + // Draw one used line and one capacity line per visible arena. + for (const auto& va : visible) { - if (!arena) + DebugMemoryContext::ArenaTimeline* timeline = nullptr; + for (auto& t : memoryDbg.timelines) + { + if (t.arena == va.arena) + { + timeline = &t; + break; + } + } + if (!timeline || timeline->samples.IsEmpty()) { continue; } - DebugMemoryContext::ArenaSnapshot snapshot; - snapshot.arena = arena; - snapshot.typeId = arena->GetTypeId(); - snapshot.typeName = GetTypeName(snapshot.typeId); + const p::Color arenaColor = details::GetArenaColor(va.arena->GetTypeId()); - // Get blocks - arena->GetBlocks(snapshot.blocks); - for (const auto& block : snapshot.blocks) + // Capacity line (darker) — never for heap, only if capacity > 0. + if (!va.isHeap && va.arena->GetAvailableMemory() > 0) { - if (!snapshot.begin || block.data < snapshot.begin) + const p::Color capColor = arenaColor.Shade(0.4f); + for (i32 i = 1; i < timeline->samples.Size(); ++i) { - snapshot.begin = (u8*)block.data; + const auto& a = timeline->samples[i - 1]; + const auto& b = timeline->samples[i]; + if (b.time < oldestTime) + { + continue; + } + if (a.capacity > 0 && b.capacity > 0) + { + tlDraw->AddLine(ImVec2(XFor(a.time), YFor(a.capacity)), + ImVec2(XFor(b.time), YFor(b.capacity)), capColor.DWColor(), 1.5f); + } } - const u8* blockEnd = (u8*)block.data + block.size; - if (!snapshot.end || blockEnd > snapshot.end) + } + + // Used line (arena color). + const ImU32 usedCol = arenaColor.DWColor(); + for (i32 i = 1; i < timeline->samples.Size(); ++i) + { + const auto& a = timeline->samples[i - 1]; + const auto& b = timeline->samples[i]; + if (b.time < oldestTime) { - snapshot.end = blockEnd; + continue; } - snapshot.capacity += block.size; + tlDraw->AddLine(ImVec2(XFor(a.time), YFor(a.used)), + ImVec2(XFor(b.time), YFor(b.used)), usedCol, 1.5f); } + } - // Get stats if available - const auto* stats = arena->GetStats(); - if (stats) + // Hover vertical line + dots + tooltip. Snaps to the nearest + // sample point across all visible timelines. + if (tlHovered) + { + const float mx = ImGui::GetIO().MousePos.x; + const double mt = oldestTime + static_cast(mx - plotX0) / plotW * span; + // Find the nearest sample time across all visible arenas. + double bestTime = mt; + double bestDist = 1e30; + for (const auto& va : visible) + { + for (const auto& t : memoryDbg.timelines) + { + if (t.arena != va.arena) + { + continue; + } + for (i32 s = 0; s < t.samples.Size(); ++s) + { + if (t.samples[s].time < oldestTime) + { + continue; + } + const double d = p::Abs(t.samples[s].time - mt); + if (d < bestDist) + { + bestDist = d; + bestTime = t.samples[s].time; + } + } + break; + } + } + if (bestDist < 1e30) { - stats->CollectStats(); - snapshot.name = Tag(stats->name); - snapshot.used = stats->used; - snapshot.events = &stats->events; - snapshot.live = &stats->live; + const float snapX = XFor(bestTime); + // Vertical line. + tlDraw->AddLine(ImVec2(snapX, plotY0), ImVec2(snapX, plotY1), + IM_COL32(255, 255, 255, 80), 1.0f); + // Dots + tooltip content: for EACH visible arena, take its + // nearest sample to the snapped time and show its value. + String tooltip; + for (const auto& va : visible) + { + const p::Color ac = details::GetArenaColor(va.arena->GetTypeId()); + const ImU32 dotCol = ac.DWColor(); + sizet arenaUsed = 0; + bool found = false; + double arenaDist = 1e30; + for (const auto& t : memoryDbg.timelines) + { + if (t.arena != va.arena) + { + continue; + } + for (i32 s = 0; s < t.samples.Size(); ++s) + { + if (t.samples[s].time < oldestTime) + { + continue; + } + const double d = p::Abs(t.samples[s].time - bestTime); + if (d < arenaDist) + { + arenaDist = d; + arenaUsed = t.samples[s].used; + found = true; + } + } + break; + } + if (!found) + { + continue; + } + const float dotY = YFor(arenaUsed); + tlDraw->AddCircleFilled(ImVec2(snapX, dotY), 3.5f, dotCol); + const char* nm = nullptr; + for (const auto& snap : memoryDbg.liveSnapshot.snapshots) + { + if (snap.arena == va.arena) + { + nm = snap.name.Data(); + break; + } + } + if (!nm || nm[0] == '\0') + { + nm = "Arena"; + } + static String tmpSize; + tmpSize.clear(); + Strings::ParseMemorySizeTo(tmpSize, arenaUsed); + if (tooltip.size() > 0) + { + tooltip += "\n"; + } + p::FormatTo(tooltip, "{}: {}", nm, tmpSize.c_str()); + } + if (tooltip.size() > 0) + { + ImGui::BeginTooltip(); + // Parse lines and draw colored. + const char* p = tooltip.c_str(); + while (*p) + { + const char* nl = strchr(p, '\n'); + const size_t len = nl ? static_cast(nl - p) : strlen(p); + // Extract arena name (before ":"). + const char* colon = reinterpret_cast(memchr(p, ':', len)); + if (colon) + { + const size_t nameLen = static_cast(colon - p); + // Find arena color. + p::Color ac{200, 200, 200}; + for (const auto& va2 : visible) + { + const char* nm2 = nullptr; + for (const auto& snap : memoryDbg.liveSnapshot.snapshots) + { + if (snap.arena == va2.arena) + { + nm2 = snap.name.Data(); + break; + } + } + if (nm2 && strlen(nm2) == nameLen && memcmp(nm2, p, nameLen) == 0) + { + ac = details::GetArenaColor(va2.arena->GetTypeId()); + break; + } + } + ImGui::TextColored( + ImVec4{ac.r / 255.0f, ac.g / 255.0f, ac.b / 255.0f, 1.0f}, "%.*s", + static_cast(len), p); + } + else + { + ImGui::TextUnformatted(p, p + len); + } + p += len; + if (*p == '\n') + { + ++p; + } + } + ImGui::EndTooltip(); + } + } + } + + // Right-side legend: right-aligned block per arena. + // Arena [] + // 5MB/20% <- used/capacity% only when the arena has capacity + // Other Arena [] + // 15KB + // The color square sits after the name; all lines right-align to + // the same right edge. Clicking a legend entry deselects (heap exempt). + float ly = plotY0 + 8.0f; + const float legendX = plotX1 - 8.0f; + const float legendLineH = ImGui::GetTextLineHeight(); + const float sqSize = 8.0f; + String legendUsed; + for (const auto& va : visible) + { + const p::Color lc = details::GetArenaColor(va.arena->GetTypeId()); - // Union live allocation addresses into the arena range so - // arenas without blocks (e.g. HeapArena) still report the - // memory they actually use. - if (snapshot.live && snapshot.events) + const char* name = nullptr; + sizet used = 0; + sizet capacity = 0; + DebugMemoryContext::ArenaTimeline* tl = nullptr; + for (auto& t : memoryDbg.timelines) + { + if (t.arena == va.arena) + { + tl = &t; + break; + } + } + if (tl && !tl->samples.IsEmpty()) + { + used = tl->samples.Last().used; + capacity = tl->samples.Last().capacity; + } + for (const auto& snap : memoryDbg.liveSnapshot.snapshots) + { + if (snap.arena == va.arena) { - i32 i = -1; - while ((i = snapshot.live->GetNextSet(i)) != NO_INDEX) + name = snap.name.Data(); + break; + } + } + const char* dispName = (name && name[0]) ? name : "Arena"; + // Whether this legend entry is the globally selected arena. + const bool isSel = + (va.arena && memoryDbg.selectionArenaIdx != NO_INDEX && memoryDbg.curSnapshot + && memoryDbg.selectionArenaIdx >= 0 + && memoryDbg.selectionArenaIdx < memoryDbg.curSnapshot->snapshots.Size() + && memoryDbg.curSnapshot->snapshots[memoryDbg.selectionArenaIdx].arena + == va.arena); + // Value line (used, plus % when capacity exists). + String valueLine; + bool hasValue = (used > 0 || capacity > 0); + if (hasValue) + { + legendUsed.clear(); + Strings::ParseMemorySizeTo(legendUsed, used); + valueLine = legendUsed; + if (capacity > 0) + { + p::FormatTo(valueLine, "/{:.0f}%", + 100.0 * static_cast(used) / static_cast(capacity)); + } + } + + const float nameW = ImGui::CalcTextSize(dispName).x; + const float lineH = legendLineH; + // Square sits at the far right, vertically centered on the name + // line; name ends just left of it (with a small gap). + const float sqX0 = legendX - sqSize; + const float nameX0 = sqX0 - nameW - 6.0f; + const float sqY0 = ly + (lineH - sqSize) * 0.5f; + tlDraw->AddText(ImVec2(nameX0, ly), lc.DWColor(), dispName); + tlDraw->AddRectFilled(ImVec2(sqX0, sqY0), ImVec2(legendX, sqY0 + sqSize), lc.DWColor()); + ly += legendLineH; + + if (hasValue) + { + const float valW = ImGui::CalcTextSize(valueLine).x; + tlDraw->AddText(ImVec2(legendX - valW, ly), lc.DWColor(), valueLine.c_str()); + ly += legendLineH; + } + + // Click legend entry to select/deselect globally (heap exempt). + if (!va.isHeap && tlHovered && ImGui::IsMouseClicked(0)) + { + const ImVec2 mpos = ImGui::GetIO().MousePos; + if (mpos.x >= nameX0 && mpos.x <= legendX && mpos.y >= ly + && mpos.y <= ly + legendLineH) + { + i32 snapIdx = NO_INDEX; + if (memoryDbg.curSnapshot) { - const auto& ev = (*snapshot.events)[i]; - const u8* allocBegin = ev.GetPtr(); - if (!snapshot.begin || allocBegin < snapshot.begin) + for (i32 s = 0; s < memoryDbg.curSnapshot->snapshots.Size(); ++s) { - snapshot.begin = allocBegin; + if (memoryDbg.curSnapshot->snapshots[s].arena == va.arena) + { + snapIdx = s; + break; + } } - const u8* allocEnd = allocBegin + ev.GetSize(); - if (!snapshot.end || allocEnd > snapshot.end) + } + if (snapIdx != NO_INDEX) + { + if (isSel) + { + memoryDbg.hasSelection = false; + memoryDbg.selectionArenaIdx = NO_INDEX; + memoryDbg.selectionBlockIdx = NO_INDEX; + } + else { - snapshot.end = allocEnd; + const auto& snap = memoryDbg.curSnapshot->snapshots[snapIdx]; + memoryDbg.hasSelection = true; + memoryDbg.selectionArenaIdx = snapIdx; + memoryDbg.selectionBlockIdx = NO_INDEX; + memoryDbg.selectionStart = reinterpret_cast(snap.begin); + memoryDbg.selectionEnd = reinterpret_cast(snap.end); } } } } + } - memoryDbg.snapshots.Add(snapshot); + // Tooltip on hover. + if (tlHovered) + { + String tipBuf; + p::FormatTo(tipBuf, "Timeline: last {:.0f}s", memoryDbg.timelineBufferSeconds); + ImGui::SetTooltip("%s", tipBuf.c_str()); } - // Sort by block address for consistent rendering - std::sort(memoryDbg.snapshots.begin(), memoryDbg.snapshots.end(), - [](const auto& a, const auto& b) + ImGui::Separator(); + } + + void DrawMemory(const char* label, bool* open, ImGuiWindowFlags flags) + { + if (!EnsureInsideDebug) { - return a.begin < b.begin; - }); + return; + } + + auto& memoryDbg = currentContext->memory; + + // Rebuild arena info cache. curSnapshot points at either the live + // snapshot (rebuilt every frame) or a captured frame (never copied). + if (!memoryDbg.IsLive()) + { + // Viewing a captured frame — point at it directly. + memoryDbg.curSnapshot = &memoryDbg.captures[memoryDbg.captureIndex]; + } + else + { + // Live mode — rebuild from current arena state. + memoryDbg.liveSnapshot.snapshots.Clear(); + memoryDbg.curSnapshot = &memoryDbg.liveSnapshot; + TArray arenas; + GetAllArenas(arenas); + for (const auto* arena : arenas) + { + if (!arena || !arena->GetStats()) + { + continue; + } + + DebugMemoryContext::ArenaSnapshot snapshot; + snapshot.arena = arena; + snapshot.typeId = arena->GetTypeId(); + snapshot.typeName = GetTypeName(snapshot.typeId); + + // Get blocks + arena->GetBlocks(snapshot.blocks); + for (const auto& block : snapshot.blocks) + { + if (!snapshot.begin || block.data < snapshot.begin) + { + snapshot.begin = (u8*)block.data; + } + const u8* blockEnd = (u8*)block.data + block.size; + if (!snapshot.end || blockEnd > snapshot.end) + { + snapshot.end = blockEnd; + } + snapshot.capacity += block.size; + } + + // Get stats if available + if (const auto* stats = arena->GetStats()) + { + stats->CollectStats(); + snapshot.name = Tag(stats->name); + snapshot.used = stats->used; + snapshot.live = &stats->live; + + // Union live allocation addresses into the arena range so + // arenas without blocks (e.g. HeapArena) still report the + // memory they actually use. + if (snapshot.live) + { + for (const auto& ev : *snapshot.live) + { + const u8* allocBegin = ev.GetPtr(); + if (!snapshot.begin || allocBegin < snapshot.begin) + { + snapshot.begin = allocBegin; + } + const u8* allocEnd = allocBegin + ev.GetSize(); + if (!snapshot.end || allocEnd > snapshot.end) + { + snapshot.end = allocEnd; + } + } + } + } + + memoryDbg.liveSnapshot.snapshots.Add(snapshot); + } + + // Second pass: find each arena's parent. ChildArena subclasses + // know the arena they allocate from; we match that parent by + // arena pointer so the result holds for captured data too. + // Indices refer to the current order and are remapped if the + // snapshots get sorted below. + { + auto& gatheredSnaps = memoryDbg.liveSnapshot.snapshots; + const TypeId childArenaType = p::GetTypeId(); + for (i32 i = 0; i < gatheredSnaps.Size(); ++i) + { + const Arena* arena = gatheredSnaps[i].arena; + if (!arena || !p::IsTypeParentOf(childArenaType, arena->GetTypeId())) + { + continue; + } + const Arena& parentArena = + static_cast(arena)->GetParentArena(); + for (i32 j = 0; j < gatheredSnaps.Size(); ++j) + { + if (gatheredSnaps[j].arena == &parentArena) + { + gatheredSnaps[i].parentArenaIdx = j; + break; + } + } + } + } + } + + const auto& snapshots = memoryDbg.curSnapshot->snapshots; + + // Sort by block address for consistent rendering. Sort through an + // index permutation so stored parentArenaIdx values are remapped to + // the new positions (live arenas and captured frames alike). + { + auto& sortSnaps = const_cast&>( + memoryDbg.curSnapshot->snapshots); + const i32 sn = sortSnaps.Size(); + TArray order; + order.Reserve(sn); + for (i32 i = 0; i < sn; ++i) + { + order.Add(i); + } + std::sort(order.begin(), order.end(), [&sortSnaps](i32 a, i32 b) + { + return sortSnaps[a].begin < sortSnaps[b].begin; + }); + TArray sorted; + sorted.Reserve(sn); + for (i32 i = 0; i < sn; ++i) + { + sorted.Add(p::Move(sortSnaps[order[i]])); + } + for (i32 i = 0; i < sn; ++i) + { + const i32 oldIdx = sorted[i].parentArenaIdx; + if (oldIdx == NO_INDEX) + { + continue; + } + i32 newIdx = NO_INDEX; + for (i32 k = 0; k < sn; ++k) + { + if (order[k] == oldIdx) + { + newIdx = k; + break; + } + } + sorted[i].parentArenaIdx = newIdx; + } + sortSnaps = p::Move(sorted); + } // Determine selected arena - DebugMemoryContext::ArenaSnapshot* selectedArena = nullptr; - if (memoryDbg.snapshots.IsValidIndex(memoryDbg.selectionArenaIdx)) + const DebugMemoryContext::ArenaSnapshot* selectedArena = nullptr; + if (memoryDbg.curSnapshot->snapshots.IsValidIndex(memoryDbg.selectionArenaIdx)) { - selectedArena = &memoryDbg.snapshots[memoryDbg.selectionArenaIdx]; + selectedArena = &memoryDbg.curSnapshot->snapshots[memoryDbg.selectionArenaIdx]; } + // Timeline recording (only runs while this window is open). + RecordMemoryTimeline(memoryDbg); + if (!ImGui::Begin(label, open, flags | ImGuiWindowFlags_MenuBar)) { ImGui::End(); @@ -2270,21 +2978,21 @@ namespace p if (ImGui::BeginMenuBar()) { // Selection range (read-only inputs) - char startBuf[32] = ""; - char endBuf[32] = ""; + String startBuf; + String endBuf; if (memoryDbg.hasSelection) { - snprintf(startBuf, sizeof(startBuf), "0x%llX", - static_cast(memoryDbg.selectionStart)); - snprintf(endBuf, sizeof(endBuf), "0x%llX", - static_cast(memoryDbg.selectionEnd)); + p::FormatTo( + startBuf, "0x{:X}", static_cast(memoryDbg.selectionStart)); + p::FormatTo( + endBuf, "0x{:X}", static_cast(memoryDbg.selectionEnd)); } ImGui::SetNextItemWidth(110.0f); - ImGui::InputText("##selStart", startBuf, sizeof(startBuf), + ImGui::InputText("##selStart", startBuf, ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_AutoSelectAll); ImGui::SameLine(); ImGui::SetNextItemWidth(110.0f); - ImGui::InputText("##selEnd", endBuf, sizeof(endBuf), + ImGui::InputText("##selEnd", endBuf, ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_AutoSelectAll); ImGui::SameLine(); if (ImGui::Button("Focus selection")) @@ -2310,7 +3018,91 @@ namespace p } } ImGui::SameLine(); - ImGui::TextDisabled("Arenas: %d", i32(memoryDbg.snapshots.Size())); + ImGui::TextDisabled("Arenas: %d", i32(snapshots.Size())); + + // ----- Capture transport buttons ----- + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + + // Back + const bool canGoBack = memoryDbg.IsLive() ? !memoryDbg.captures.IsEmpty() + : (memoryDbg.captureIndex - 1) >= 0; + ImGui::BeginDisabled(!canGoBack); + if (ImGui::TextButton(details::ICON_CI_CAPTURE_BACK, "Previous Capture") && canGoBack) + { + if (memoryDbg.IsLive()) + { + memoryDbg.captureIndex = memoryDbg.captures.Size() - 1; + } + else + { + memoryDbg.captureIndex = memoryDbg.captureIndex - 1; + } + } + ImGui::EndDisabled(); + + if (memoryDbg.IsLive()) + { + ImGui::TextDisabled("live"); + } + else + { + ImGui::TextDisabled("%i/%i", memoryDbg.captureIndex + 1, memoryDbg.captures.Size()); + } + + // Forward + const bool canGoForward = + !memoryDbg.IsLive() && (memoryDbg.captureIndex + 1) <= memoryDbg.captures.Size(); + ImGui::BeginDisabled(!canGoForward); + if (ImGui::TextButton(details::ICON_CI_CAPTURE_FWD, "Next Capture")) + { + const i32 newIndex = memoryDbg.captureIndex + 1; + if (memoryDbg.captures.IsValidIndex(newIndex)) + { + memoryDbg.captureIndex = newIndex; + } + else + { + memoryDbg.captureIndex = NO_INDEX; // Return to live + } + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + // Capture (record) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.86f, 0.16f, 0.16f, 1.0f)); + if (ImGui::TextButton(details::ICON_CI_CAPTURE_RECORD, "Capture")) + { + CaptureMemory(*currentContext); + memoryDbg.captureIndex = i32(memoryDbg.captures.Size()) - 1; + } + ImGui::PopStyleColor(); + + ImGui::SameLine(); + + // Erase + const bool canErase = !memoryDbg.IsLive(); + ImGui::BeginDisabled(!canErase); + if (ImGui::TextButton(details::ICON_CI_CAPTURE_TRASH, "Erase Capture") && canErase) + { + memoryDbg.captures.RemoveAt(memoryDbg.captureIndex); + if (memoryDbg.captures.IsEmpty()) + { + memoryDbg.captureIndex = -1; // Back to live + } + else if (memoryDbg.captureIndex >= i32(memoryDbg.captures.Size())) + { + memoryDbg.captureIndex = i32(memoryDbg.captures.Size()) - 1; + } + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { @@ -2331,8 +3123,6 @@ namespace p if (ImGui::BeginMenu(settingsLabel)) { ImGui::SeparatorText("Graph"); - ImGui::MenuItem("Hex", nullptr, &memoryDbg.showHex); - ImGui::MenuItem("ASCII", nullptr, &memoryDbg.showAscii); // Bytes/line combo: powers of 2 up to 32 const i32 allowed[] = {1, 2, 4, 8, 16, 32}; const char* labels[] = {"1", "2", "4", "8", "16", "32"}; @@ -2352,8 +3142,18 @@ namespace p { memoryDbg.bytesPerLine = allowed[current]; } + ImGui::SeparatorText("Timeline"); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat( + "Buffer (s)", &memoryDbg.timelineBufferSeconds, 1.0f, 5.0f, "%.1f"); + if (memoryDbg.timelineBufferSeconds < 0.0f) + { + memoryDbg.timelineBufferSeconds = 0.0f; + } ImGui::SeparatorText("View"); ImGui::MenuItem("Details", nullptr, &memoryDbg.showDetails); + ImGui::MenuItem("HEX", nullptr, &memoryDbg.showHEX); + ImGui::MenuItem("ASCII", nullptr, &memoryDbg.showASCII); ImGui::SeparatorText("Layout"); if (ImGui::MenuItem("Reset")) { @@ -2364,6 +3164,10 @@ namespace p ImGui::EndMenuBar(); } + // ----- Memory timeline graph ----- + DrawMemoryTimeline(memoryDbg); + + // DockSpace — splits 0.5 (right) for Details, the rest for the View graph. const ImGuiID dockspaceId = ImGui::GetID("MemoryDebuggerDockspace"); if (memoryDbg.resetLayout || ImGui::DockBuilderGetNode(dockspaceId) == nullptr) @@ -2375,6 +3179,8 @@ namespace p &memoryDbg.graphDockId); ImGui::DockBuilderGetNode(memoryDbg.graphDockId)->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar; + ImGui::DockBuilderGetNode(memoryDbg.detailsDockId)->LocalFlags |= + ImGuiDockNodeFlags_AutoHideTabBar; // Re-dock windows (SetNextWindowDockID FirstUseEver is one-shot) ImGui::DockBuilderDockWindow("View", memoryDbg.graphDockId); if (memoryDbg.showDetails) @@ -2393,18 +3199,21 @@ namespace p const float colGap = 4.0f; constexpr float colMaxW = 32.0f; const float rulerStripW = 32.0f; - const float hexStripW = - memoryDbg.showHex ? (charTextSize.x * 2.0f * bytesPerLine + stripPad * 2.0f) : 0.0f; - const float stringStripW = - memoryDbg.showAscii ? (charTextSize.x * 1.0f * bytesPerLine + stripPad * 2.0f) : 0.0f; + // Full (configured) strip widths, used only to pre-size the first-use window. + // The actual rendered strip widths are gated on value visibility in the layout + // block below. + const float hexStripWFull = + memoryDbg.showHEX ? (charTextSize.x * 2.0f * bytesPerLine + stripPad * 2.0f) : 0.0f; + const float stringStripWFull = + memoryDbg.showASCII ? (charTextSize.x * 1.0f * bytesPerLine + stripPad * 2.0f) : 0.0f; // Compute desired graph width so the View window first-use size fits the // strips + all arena columns without horizontal scrolling. { - const i32 arenaCount = memoryDbg.snapshots.Size(); - const float desiredGraphW = rulerStripW + leftGap + hexStripW - + (hexStripW > 0 ? leftGap : 0.0f) + stringStripW - + (stringStripW > 0 ? leftGap : 0.0f) + const i32 arenaCount = snapshots.Size(); + const float desiredGraphW = rulerStripW + leftGap + hexStripWFull + + (hexStripWFull > 0 ? leftGap : 0.0f) + stringStripWFull + + (stringStripWFull > 0 ? leftGap : 0.0f) + (colMaxW + colGap) * arenaCount + colGap + 32.0f; ImGui::SetNextWindowSize(ImVec2(desiredGraphW, 400.0f), ImGuiCond_FirstUseEver); } @@ -2422,32 +3231,9 @@ namespace p ImGui::InvisibleButton("##graph_input", canvasSize); ImGui::SetCursorPos(ImVec2(0.0f, 0.0f)); - // Full interactable area of the View (declared early for hit-tests) - - // ----- Layout vars ----- - const float colAreaX0 = canvasPos.x + rulerStripW + leftGap + hexStripW - + (hexStripW > 0 ? leftGap : 0.0f) + stringStripW - + (stringStripW > 0 ? leftGap : 0.0f); - const float colAreaW = (canvasPos.x + canvasSize.x) - colAreaX0; - const i32 arenaCount = memoryDbg.snapshots.Size(); - const float colW = - (arenaCount > 0 && colAreaW > 0.0f) - ? p::Min((colAreaW - colGap * (arenaCount + 1)) / (float)arenaCount, colMaxW) - : 0.0f; - const float addressY0 = canvasPos.y; - const float addressY1 = canvasPos.y + canvasSize.y; - const float addressH = (addressY1 > addressY0) ? (addressY1 - addressY0) : 1.0f; - const float hexX0 = canvasPos.x + rulerStripW + leftGap; - const float stringX0 = hexX0 + hexStripW + (hexStripW > 0 ? leftGap : 0.0f); - const float colX0 = stringX0 + stringStripW + (stringStripW > 0 ? leftGap : 0.0f); - const float colTotalW = colW + colGap; - const float graphX0 = canvasPos.x; - const float graphW = canvasSize.x; - const float rulerW = rulerStripW; - // ----- Address range ----- sizet addrMin = 0, addrMax = 0; - for (const auto& snapshot : memoryDbg.snapshots) + for (const auto& snapshot : snapshots) { if (!addrMin || sizet(snapshot.begin) < addrMin) { @@ -2533,6 +3319,34 @@ namespace p safeEffectiveViewRange = 1.0; } + // ----- Layout vars ----- + const float addressY0 = canvasPos.y; + const float addressY1 = canvasPos.y + canvasSize.y; + const float addressH = (addressY1 > addressY0) ? (addressY1 - addressY0) : 1.0f; + const double pixelsPerByte = addressH / viewRange; + const bool valuesVisible = (pixelsPerByte * bytesPerLine >= 13); + // Only show the HEX/ASCII strips while zoomed in enough to read values. + const float hexStripW = (memoryDbg.showHEX && valuesVisible) + ? (charTextSize.x * 2.0f * bytesPerLine + stripPad * 2.0f) + : 0.0f; + const float stringStripW = (memoryDbg.showASCII && valuesVisible) + ? (charTextSize.x * 1.0f * bytesPerLine + stripPad * 2.0f) + : 0.0f; + // Order left->right: ruler | arena columns | HEX | ASCII + const float colX0 = canvasPos.x + rulerStripW + leftGap; + const float stringX0 = (canvasPos.x + canvasSize.x) - stringStripW; + const float hexX0 = stringX0 - (stringStripW > 0.0f ? leftGap : 0.0f) - hexStripW; + const i32 arenaCount = snapshots.Size(); + const float colAreaW = hexX0 - colX0 - (hexStripW > 0.0f ? leftGap : 0.0f); + const float colW = + (arenaCount > 0 && colAreaW > 0.0f) + ? p::Min((colAreaW - colGap * (arenaCount + 1)) / (float)arenaCount, colMaxW) + : 0.0f; + const float colTotalW = colW + colGap; + const float graphX0 = canvasPos.x; + const float graphW = canvasSize.x; + const float rulerW = rulerStripW; + // ----- Mapping helpers (address <-> vertical pixel) ----- auto AddrToY = [&](sizet a) -> float { @@ -2598,10 +3412,9 @@ namespace p #pragma region Compute // ----- Compute ----- - const double pixelsPerByte = addressH / viewRange; - const double majorStep = SnapToScale(100.0 / pixelsPerByte); - const double halfStep = majorStep * 0.5; - const double quarterStep = majorStep * 0.25; + const double majorStep = SnapToScale(100.0 / pixelsPerByte); + const double halfStep = majorStep * 0.5; + const double quarterStep = majorStep * 0.25; TArray majorTickYs; TArray majorTickAddrs; @@ -2620,24 +3433,25 @@ namespace p // Find the common prefix of the PRINTED hex strings across all // visible major ticks (not the normalized 64-bit form) - char firstLabel[32] = ""; + String firstLabel; if (majorTickAddrs.Size() > 0) { - snprintf(firstLabel, sizeof(firstLabel), "0x%llX", - static_cast(majorTickAddrs[0])); + p::FormatTo( + firstLabel, "0x{:X}", static_cast(majorTickAddrs[0])); } size_t commonHexChars = 0; if (majorTickAddrs.Size() >= 2) { - const size_t firstLen = strlen(firstLabel); + String ib; + const size_t firstLen = firstLabel.size(); for (size_t c = 2; c < firstLen; ++c) // skip "0x" { bool allMatch = true; for (i32 i = 1; i < majorTickAddrs.Size(); ++i) { - char ib[32]; - snprintf(ib, sizeof(ib), "0x%llX", - static_cast(majorTickAddrs[i])); + ib.clear(); + p::FormatTo( + ib, "0x{:X}", static_cast(majorTickAddrs[i])); if (ib[c] != firstLabel[c]) { allMatch = false; @@ -2688,20 +3502,46 @@ namespace p ImVec2(stringX0 + stringStripW, canvasPos.y + canvasSize.y), borderLgtCol, 1.0f); } + + // Horizontal line guides: data row boundaries, drawn behind everything. + // Only when zoomed in far enough to show data values, and only across the + // visible address range. + if (pixelsPerByte * bytesPerLine >= 13 && (hexStripW > 0.0f || stringStripW > 0.0f)) + { + const sizet bpl = static_cast(bytesPerLine); + const sizet gvLo = static_cast(viewStart); + const sizet gvHi = static_cast(viewStart + viewRange); + const sizet row0 = (gvLo / bpl) * bpl; + const float guideX0 = canvasPos.x; + const float guideX1 = canvasPos.x + canvasSize.x; + for (sizet a2 = row0 + bpl; a2 < gvHi; a2 += bpl) + { + const float y = AddrToY(a2); + if (y < addressY0 - 1.0f || y > addressY1 + 1.0f) + { + continue; + } + drawList->AddLine(ImVec2(guideX0, y), ImVec2(guideX1, y), borderLgtCol, 1.0f); + } + } #pragma endregion DrawBgs #pragma region DrawValues { // ---- Values ---- // HEX and String values - if ((hexStripW > 0.0f || stringStripW > 0.0f) && !memoryDbg.snapshots.IsEmpty()) + if ((hexStripW > 0.0f || stringStripW > 0.0f) && !snapshots.IsEmpty()) { const sizet viewLo = static_cast(viewStart); const sizet viewHi = static_cast(viewStart + viewRange); if (pixelsPerByte * bytesPerLine >= 13) { - for (i32 a = 0; a < memoryDbg.snapshots.Size(); ++a) + // Vertical centering offset: shift each value down so it sits in + // the middle of its line (precomputed once per frame). + const float rowH = static_cast(pixelsPerByte * bytesPerLine); + const float valueOffset = (rowH - charTextSize.y) * 0.5f; + for (i32 a = 0; a < snapshots.Size(); ++a) { - const auto& snapshot = memoryDbg.snapshots[a]; + const auto& snapshot = snapshots[a]; if (!snapshot.begin || snapshot.capacity == 0) { continue; @@ -2722,9 +3562,12 @@ namespace p } const u8* data = static_cast(block.data); const sizet bpl = static_cast(bytesPerLine); - // Global row grid anchored at viewLo - const sizet gridOff = (firstByte - viewLo) % bpl; - for (sizet a2 = firstByte - gridOff; a2 < lastByte; a2 += bpl) + // Row grid anchored to absolute memory so each line holds a + // contiguous bytesPerLine chunk starting at a bpl-aligned + // address (stable across pan/zoom). + const sizet gridOff = (firstByte / bpl) * bpl; + String hexLabel; + for (sizet a2 = gridOff; a2 < lastByte; a2 += bpl) { if (a2 + bpl <= bs) { @@ -2740,8 +3583,8 @@ namespace p const sizet lineByteIdx = b2 - a2; if (hexStripW > 0.0f) { - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", byte); + hexLabel.clear(); + p::FormatTo(hexLabel, "{:02X}", static_cast(byte)); const ImU32 hexCol = (byte == 0) ? p::Color{90, 90, 90, 255}.DWColor() @@ -2749,7 +3592,8 @@ namespace p const float xOff = static_cast(lineByteIdx) * (charTextSize.x * 2.0f); drawList->AddText( - ImVec2(hexX0 + stripPad + xOff, y), hexCol, hex); + ImVec2(hexX0 + stripPad + xOff, y + valueOffset), + hexCol, hexLabel.c_str()); } if (stringStripW > 0.0f) { @@ -2761,7 +3605,8 @@ namespace p : p::Color{90, 90, 90, 255}.DWColor(); const float xOff = static_cast(lineByteIdx) * charTextSize.x; - drawList->AddText(ImVec2(stringX0 + stripPad + xOff, y), + drawList->AddText( + ImVec2(stringX0 + stripPad + xOff, y + valueOffset), asciiCol, &c, &c + 1); } } @@ -2811,6 +3656,7 @@ namespace p } // Major ticks + labels (gray common prefix, white changing suffix) + String majorTickLabel; for (i32 t = 0; t < majorTickAddrs.Size(); ++t) { const sizet addr = majorTickAddrs[t]; @@ -2819,30 +3665,26 @@ namespace p ImVec2(canvasPos.x + rulerW - 10.0f, ty), p::Color{220, 220, 220}.DWColor(), 1.5f); - char fullBuf[32]; - snprintf( - fullBuf, sizeof(fullBuf), "0x%llX", static_cast(addr)); - const float fullWidth = ImGui::CalcTextSize(fullBuf).x; + majorTickLabel.clear(); + p::FormatTo(majorTickLabel, "0x{:X}", static_cast(addr)); + const float fullWidth = ImGui::CalcTextSize(majorTickLabel).x; // Split label into common (gray) and changing (white). - const size_t fullLen = strlen(fullBuf); + const size_t fullLen = majorTickLabel.size(); const size_t splitAt = p::Min(commonStrLen, fullLen); - char commonBuf[32] = {}; - char changingBuf[32] = {}; - if (splitAt > 0) - { - memcpy(commonBuf, fullBuf, splitAt); - } - if (splitAt < fullLen) + StringView commonBuf = majorTickLabel.substr(0, splitAt); + StringView changingBuf = + (splitAt < fullLen) ? majorTickLabel.substr(splitAt) : String{}; + if (splitAt == fullLen) { - memcpy(changingBuf, fullBuf + splitAt, fullLen - splitAt + 1); + changingBuf = {}; } // Bottom-to-top text: the FIRST char sits at the BOTTOM. // Common prefix at the bottom (read first), changing // suffix on top. Total vertical extent = fullWidth. const float baseY = ty + fullWidth; - if (commonBuf[0] != '\0') + if (!commonBuf.empty()) { const float commonW = ImGui::CalcTextSize(commonBuf).x; details::AddTextVertical(drawList, ImVec2(canvasPos.x + 2.0f, baseY), @@ -2854,7 +3696,7 @@ namespace p else { details::AddTextVertical(drawList, ImVec2(canvasPos.x + 2.0f, baseY), - p::Color{230, 230, 230, 255}.DWColor(), fullBuf); + p::Color{230, 230, 230, 255}.DWColor(), majorTickLabel); } } } @@ -2866,11 +3708,13 @@ namespace p // Label background const ImU32 bgCol = ImGui::GetColorU32(ImGuiCol_TableHeaderBg, 0.9f); drawList->AddRectFilled(canvasPos, - ImVec2(colX0, canvasPos.y + charTextSize.y + (padding.y * 2.f)), bgCol); + ImVec2(canvasPos.x + canvasSize.x, + canvasPos.y + charTextSize.y + (padding.y * 2.f)), + bgCol); // Ruler label (scale) const String scaleStr = Strings::ParseMemorySize(static_cast(majorStep)); - const ImVec2 rulerSize = ImGui::CalcTextSize(scaleStr.c_str()); + const ImVec2 rulerSize = ImGui::CalcTextSize(scaleStr); drawList->AddText( ImVec2(canvasPos.x + (rulerW - rulerSize.x) * 0.5f, canvasPos.y + padding.y), p::Color{220, 220, 220}.DWColor(), scaleStr.data()); @@ -2903,13 +3747,15 @@ namespace p // ----- Arena columns loop (blocks, markers, click, tooltip) ----- const ImVec2 mousePos = ImGui::GetIO().MousePos; const bool inGraph = graphRect.Contains(mousePos); - for (i32 i = 0; i < memoryDbg.snapshots.Size(); ++i) + for (i32 i = 0; i < snapshots.Size(); ++i) { - const auto& snapshot = memoryDbg.snapshots[i]; + const auto& snapshot = snapshots[i]; const bool isSelected = (i == memoryDbg.selectionArenaIdx); const p::Color arenaColor = details::GetArenaColor(snapshot.typeId); const p::Color arenaBg = arenaColor.Translucency(30); - const p::Color blockFillColor = isSelected ? arenaColor.Shade(0.65f) : arenaColor; + const p::Color blockFillColor = isSelected + ? arenaColor.Shade(0.65f).Translucency(200) + : arenaColor.Translucency(140); const p::Color blockLineColor = isSelected ? p::Color::Orange() : blockFillColor.Shade(0.5f); const p::Color allocLiveColor = arenaColor.Tint(0.1f); @@ -2924,6 +3770,45 @@ namespace p ImVec2(colX, addressY0), ImVec2(colRight, addressY1), arenaBg.DWColor()); } + // Link strips: translucent fills from each block toward + // its parent arena's alloc edge. Drawn before blocks so + // they render underneath. + if (snapshot.parentArenaIdx != NO_INDEX && snapshot.begin && snapshot.capacity > 0) + { + const i32 pi = snapshot.parentArenaIdx; + const bool left = (pi < i); + const float parentPad = colW * 0.25f; + const auto& parentSnap = snapshots[pi]; + const ImU32 stripCol = blockFillColor.Translucency(25).DWColor(); + for (const auto& block : snapshot.blocks) + { + const sizet blockStart = reinterpret_cast(block.data); + const float y0Raw = AddrToY(blockStart); + const float y1Raw = AddrToY(blockStart + block.size); + if (y1Raw < addressY0 || y0Raw > addressY1) + { + continue; + } + float y0 = y0Raw; + float y1 = y1Raw; + if (y1 - y0 < 2.0f) + { + const float mid = (y0 + y1) * 0.5f; + y0 = mid - 1.0f; + y1 = mid + 1.0f; + } + y0 = (y0 > addressY0) ? y0 : addressY0; + y1 = (y1 < addressY1) ? y1 : addressY1; + + const float parentEdge = left ? ArenaColumnX(pi) + colW - parentPad + : ArenaColumnX(pi) + parentPad; + const float childEdge = left ? colX : colRight; + const float fillX0 = left ? parentEdge : childEdge; + const float fillX1 = left ? childEdge : parentEdge; + drawList->AddRectFilled(ImVec2(fillX0, y0), ImVec2(fillX1, y1), stripCol); + } + } + // Block draw + double-click focus if (snapshot.begin && snapshot.capacity > 0) { @@ -2950,7 +3835,6 @@ namespace p ImVec2(colX, y0), ImVec2(colRight, y1), blockFillColor.DWColor()); drawList->AddRect( ImVec2(colX, y0), ImVec2(colRight, y1), blockLineColor.DWColor()); - // Double-click a block to focus it if (ImGui::IsMouseDoubleClicked(0)) { const ImRect blockRect(ImVec2(colX, y0), ImVec2(colRight, y1)); @@ -2959,9 +3843,9 @@ namespace p const double blkSize = static_cast(block.size); if (blkSize > 0.0) { - const double minViewRange = p::Max(1.0, - addressH - / (24.0 * double(bytesPerLine > 0 ? bytesPerLine : 1))); + const double minViewRange = p::Max( + 1.0, addressH * double(bytesPerLine > 0 ? bytesPerLine : 1) + / (static_cast(charTextSize.y))); double newViewRange(block.size); newViewRange = p::Max(newViewRange, minViewRange); newViewRange = p::Min(newViewRange, range); @@ -2979,17 +3863,18 @@ namespace p { // Walk live allocs - if (snapshot.live && snapshot.events) + const auto* live = + snapshot.captured ? &snapshot.ownedLiveAllocs : snapshot.live; + if (live) { // Filter visible allocations const float padding = colW * 0.25f; const sizet viewStartS = static_cast(viewStart); const sizet viewEndS = static_cast(viewStart + viewRange); TArray liveInRange; - for (i32 j = snapshot.live->GetNextSet(NO_INDEX); j != NO_INDEX; - j = snapshot.live->GetNextSet(j)) + for (i32 j = 0; j < live->Size(); ++j) { - const auto& ev = (*snapshot.events)[j]; + const auto& ev = (*live)[j]; const sizet addr = reinterpret_cast(ev.GetPtr()); const sizet size = ev.GetSize(); if (addr >= viewEndS || addr + size <= viewStartS) @@ -3000,17 +3885,20 @@ namespace p } // Draw allocations - for (i32 i : liveInRange) + for (i32 id : liveInRange) { - const auto& ev = (*snapshot.events)[i]; + const auto& ev = (*live)[id]; const sizet addr = reinterpret_cast(ev.GetPtr()); const sizet size = ev.GetSize(); const float ty = AddrToY(addr); const float ty2 = AddrToY(addr + size); - if (ty >= addressY0 && ty2 <= addressY1) + // Clamp to visible range so full-span allocations draw. + const float dy0 = (ty > addressY0) ? ty : addressY0; + const float dy1 = (ty2 < addressY1) ? ty2 : addressY1; + if (dy1 > dy0) { - drawList->AddRectFilled(ImVec2(colX + padding, ty - 0.5f), - ImVec2(colRight - padding, ty2 + 0.5f), + drawList->AddRectFilled(ImVec2(colX + padding, dy0 - 0.5f), + ImVec2(colRight - padding, dy1 + 0.5f), allocLiveColor.DWColor()); } } @@ -3067,42 +3955,58 @@ namespace p } } - // Column tooltip + // Column tooltip (lazy-built: only rebuilt when the hovered arena changes) + // Cached outside the loop so it survives across frames. if (colRect.Contains(ImGui::GetIO().MousePos)) { - ImGui::BeginTooltip(); - ImGui::SeparatorText("Arena"); - ImGui::Text("Name: %s (%s)", snapshot.name.Data(), snapshot.typeName.Data()); - ImGui::Text("Range: 0x%llX - 0x%llX", reinterpret_cast(snapshot.begin), - reinterpret_cast(snapshot.begin) + snapshot.capacity); - static String sizeStr; - if (snapshot.capacity > 0) - { - sizeStr = Strings::ParseMemorySize(snapshot.capacity); - ImGui::Text("Capacity: %s (%zuB)", sizeStr.data(), snapshot.capacity); - } - if (snapshot.used > 0) + static i32 cachedTipArena = -1; + static String colTipBuf; + if (cachedTipArena != i) { - sizeStr = Strings::ParseMemorySize(snapshot.used); - const float cof = - (snapshot.capacity > 0) ? snapshot.used / snapshot.capacity : 0; - ImGui::Text("Used: %s (%.1f%% %zuB)", sizeStr.c_str(), 100.0f * cof, - snapshot.used); + cachedTipArena = i; + colTipBuf.clear(); + p::FormatTo(colTipBuf, "{} ({})\n", + snapshot.name.Data() ? snapshot.name.Data() : "(unnamed)", + snapshot.typeName.Data() ? snapshot.typeName.Data() : ""); + p::FormatTo(colTipBuf, "Range: 0x{:X} - 0x{:X}\n", + static_cast( + reinterpret_cast(snapshot.begin)), + static_cast( + reinterpret_cast(snapshot.begin) + snapshot.capacity)); + static String memStr1; + memStr1.clear(); + Strings::ParseMemorySizeTo(memStr1, snapshot.capacity); + p::FormatTo(colTipBuf, "Capacity: {} ({}B)\n", memStr1.c_str(), + static_cast(snapshot.capacity)); + if (snapshot.used > 0) + { + static String memStr2; + memStr2.clear(); + Strings::ParseMemorySizeTo(memStr2, snapshot.used); + const float cof = + (snapshot.capacity > 0) ? snapshot.used / snapshot.capacity : 0; + p::FormatTo(colTipBuf, "Used: {} ({:.1f}% {}B)\n", memStr2.c_str(), + 100.0f * cof, static_cast(snapshot.used)); + } } - ImGui::EndTooltip(); + ImGui::SetTooltip("%s", colTipBuf.c_str()); } // Column header (vertical, drawn LAST so rects/markers don't cover it) - const char* name = snapshot.name.Data(); - bool nameIsType = false; - if (!name) + StringView name = snapshot.name.AsString(); + bool nameIsType = false; + if (name.empty()) { - name = snapshot.typeName.Data(); + name = snapshot.typeName.AsString(); nameIsType = true; } - if (name) + if (!name.empty()) { p::Color color{220, 220, 220}; + if (isSelected) + { + color = selectionColor; + } const float nameExtent = ImGui::CalcTextSize(name).x; const float fontHeight = ImGui::GetTextLineHeight(); // Center horizontally: strip extends right by ~fontHeight from pos.x @@ -3160,50 +4064,59 @@ namespace p } } - // ----- Graph-wide tooltip (address always, block info if hit) ----- + // ----- Graph-wide tooltip (address always, arena info if hit). Lazy-built: + // only rebuilt when the hovered address changes. ----- { const ImVec2 mp = ImGui::GetIO().MousePos; if (graphRect.Contains(mp)) { - const sizet hoverAddr = ScreenYToAddr(mp.y); - const DebugMemoryContext::ArenaSnapshot* hit = nullptr; - for (i32 i = 0; i < memoryDbg.snapshots.Size() && !hit; ++i) + static sizet cachedHoverAddr = static_cast(-1); + static i32 cachedHoverX = -1; + static String gTipBuf; + const sizet hoverAddr = ScreenYToAddr(mp.y); + const i32 hoverX = static_cast(mp.x); + if (hoverAddr != cachedHoverAddr || hoverX != cachedHoverX) { - const auto& snapshot = memoryDbg.snapshots[i]; - if (!snapshot.begin || snapshot.capacity == 0) - { - continue; - } - const float cx = ArenaColumnX(i); - if (mp.x < cx || mp.x >= cx + colW) + cachedHoverAddr = hoverAddr; + cachedHoverX = hoverX; + // Identify the arena by column (x), not by forcing the + // hover address inside the summed block span (which has gaps). + const DebugMemoryContext::ArenaSnapshot* hit = nullptr; + for (i32 i = 0; i < snapshots.Size(); ++i) { - continue; - } - const sizet aStart = reinterpret_cast(snapshot.begin); - const sizet aEnd = aStart + snapshot.capacity; - if (hoverAddr >= aStart && hoverAddr < aEnd) - { - hit = &snapshot; + const float cx = ArenaColumnX(i); + if (mp.x < cx || mp.x >= cx + colW) + { + continue; + } + hit = &snapshots[i]; + break; } - } - ImGui::BeginTooltip(); - ImGui::Text("Address: 0x%llX", static_cast(hoverAddr)); - if (hit) - { - ImGui::Text("Arena: %s", GetTypeName(hit->typeId).data()); - const sizet offset = hoverAddr - reinterpret_cast(hit->begin); - ImGui::Text("Offset: 0x%llX (%zuB)", - static_cast(offset), static_cast(offset)); - if (hit->used > 0) + gTipBuf.clear(); + p::FormatTo( + gTipBuf, "Address: 0x{:X}", static_cast(hoverAddr)); + if (hit) { - static String sizeStr; - sizeStr = Strings::ParseMemorySize(hit->used); - const float cof = (hit->capacity > 0) ? hit->used / hit->capacity : 0; - ImGui::Text("Used: %s (%.1f%% %zuB)", sizeStr.c_str(), 100.0f * cof, - hit->used); + const StringView typeName = GetTypeName(hit->typeId); + p::FormatTo( + gTipBuf, "\nArena: {}", typeName.data() ? typeName.data() : "?"); + const sizet offset = hoverAddr - reinterpret_cast(hit->begin); + p::FormatTo(gTipBuf, "\nOffset: 0x{:X} ({}B)", + static_cast(offset), + static_cast(offset)); + if (hit->used > 0) + { + static String usedStr; + usedStr.clear(); + Strings::ParseMemorySizeTo(usedStr, hit->used); + const float cof = + (hit->capacity > 0) ? hit->used / hit->capacity : 0; + p::FormatTo(gTipBuf, "\nUsed: {} ({:.1f}% {}B)", usedStr.c_str(), + 100.0f * cof, static_cast(hit->used)); + } } } - ImGui::EndTooltip(); + ImGui::SetTooltip("%s", gTipBuf.c_str()); } } @@ -3215,7 +4128,7 @@ namespace p if (ImGui::IsMouseClicked(0) && inGraph && !memoryDbg.isSelecting) { bool overColumn = false; - for (i32 i = 0; i < memoryDbg.snapshots.Size() && !overColumn; ++i) + for (i32 i = 0; i < snapshots.Size() && !overColumn; ++i) { const float cx = ArenaColumnX(i); if (mousePos.x >= cx && mousePos.x < cx + colW) @@ -3251,19 +4164,18 @@ namespace p } // Draw selection - constexpr Color selectionCol(255, 200, 80); if (memoryDbg.isSelecting) { const float sy0 = AddrToY(memoryDbg.selectionFirstAddr); const float sy1 = AddrToY(memoryDbg.selectionSecondAddr); // Selection box drawList->AddLine(ImVec2(canvasPos.x, sy0), ImVec2(canvasPos.x + canvasSize.x, sy0), - selectionCol.Translucency(220).DWColor()); + selectionColor.Translucency(220).DWColor()); drawList->AddLine(ImVec2(canvasPos.x, sy1), ImVec2(canvasPos.x + canvasSize.x, sy1), - selectionCol.Translucency(220).DWColor()); + selectionColor.Translucency(220).DWColor()); drawList->AddRectFilled(ImVec2(canvasPos.x + rulerW, sy0), ImVec2(canvasPos.x + canvasSize.x, sy1), - selectionCol.Translucency(90).DWColor()); + selectionColor.Translucency(30).DWColor()); } if (memoryDbg.hasSelection) { @@ -3272,18 +4184,18 @@ namespace p const float sy0 = AddrToY(memoryDbg.selectionStart); const float sy1 = AddrToY(memoryDbg.selectionEnd); drawList->AddLine(ImVec2(canvasPos.x, sy0), - ImVec2(canvasPos.x + canvasSize.x, sy0), selectionCol.DWColor()); + ImVec2(canvasPos.x + canvasSize.x, sy0), selectionColor.DWColor()); drawList->AddLine(ImVec2(canvasPos.x, sy1), - ImVec2(canvasPos.x + canvasSize.x, sy1), selectionCol.DWColor()); + ImVec2(canvasPos.x + canvasSize.x, sy1), selectionColor.DWColor()); drawList->AddRectFilled(ImVec2(canvasPos.x + rulerW, sy0), ImVec2(canvasPos.x + canvasSize.x, sy1), - selectionCol.Translucency(45).DWColor()); + selectionColor.Translucency(15).DWColor()); if (memoryDbg.selectionArenaIdx != NO_INDEX && memoryDbg.selectionBlockIdx != NO_INDEX) // Block selection box { const float cx = ArenaColumnX(memoryDbg.selectionArenaIdx); drawList->AddRectFilled(ImVec2(cx, sy0), ImVec2(cx + colW, sy1), - selectionCol.Translucency(90).DWColor()); + selectionColor.Translucency(90).DWColor()); } } else @@ -3294,10 +4206,10 @@ namespace p // clicked, regardless of which strip it was in. drawList->AddLine(ImVec2(canvasPos.x, sy0), ImVec2(canvasPos.x + canvasSize.x, sy0), - p::Color{255, 200, 80, 240}.DWColor(), 2.0f); + selectionColor.Translucency(240).DWColor(), 2.0f); drawList->AddRectFilled(ImVec2(canvasPos.x, sy0 - 1.5f), ImVec2(canvasPos.x + canvasSize.x, sy0 + 2.5f), - p::Color{255, 200, 80, 70}.DWColor()); + selectionColor.Translucency(70).DWColor()); } } @@ -3305,7 +4217,7 @@ namespace p if (inGraph && memoryDbg.hasSelection && ImGui::IsMouseClicked(1)) { i32 selectedColumn = -1; - for (i32 c = 0; c < memoryDbg.snapshots.Size(); ++c) + for (i32 c = 0; c < snapshots.Size(); ++c) { const float cx = ArenaColumnX(c); if (mousePos.x >= cx && mousePos.x < cx + colW) @@ -3407,52 +4319,42 @@ namespace p + ((static_cast(my) - wAddressY0) / wAddressH) * wViewRange; } - const double minViewRange = p::Max( - 1.0, wAddressH / (24.0 * static_cast(wBytesPerLine))); + // Max zoom: each data line (bytesPerLine bytes) must not span + // more than 1.5 text line heights. + const float lineH = charTextSize.y * 1.5f; + const double minViewRange = + p::Max(1.0, wAddressH * static_cast(wBytesPerLine) + / static_cast(lineH)); double newViewRange = (wheel > 0) ? (wViewRange / 1.15) : (wViewRange * 1.15); - if (newViewRange < minViewRange) - { - newViewRange = minViewRange; - } - if (newViewRange > range) - { - newViewRange = range; - } - const double zoomEpsilon = 0.5; - const bool atMinZoom = (newViewRange <= minViewRange + zoomEpsilon); - const bool atMaxZoom = (newViewRange >= range - zoomEpsilon); - if (atMinZoom || atMaxZoom) - { - memoryDbg.viewRange = atMinZoom ? minViewRange : range; - memoryDbg.isZooming = false; - memoryDbg.smoothViewRange = memoryDbg.viewRange; - memoryDbg.smoothViewStart = - static_cast(memoryDbg.viewStart); - } - else - { - // Zoom coupling: anchor stays at cursor - const double relX = - (wAddressH > 0.0) - ? ((static_cast(my) - wAddressY0) / wAddressH) - : 0.0; - const double newStart = addrAtCursor - relX * newViewRange; - memoryDbg.viewStart = static_cast(newStart); - memoryDbg.viewRange = newViewRange; - memoryDbg.smoothViewStart = newStart; - memoryDbg.smoothViewRange = newViewRange; - memoryDbg.zoomAnchorAddr = static_cast(addrAtCursor); - memoryDbg.zoomAnchorRelX = static_cast(relX); - memoryDbg.isZooming = true; - } + newViewRange = p::Max(newViewRange, minViewRange); + newViewRange = p::Min(newViewRange, range); + // Zoom coupling: anchor stays at cursor + const double relX = + (wAddressH > 0.0) + ? ((static_cast(my) - wAddressY0) / wAddressH) + : 0.0; + const double newStart = addrAtCursor - relX * newViewRange; + memoryDbg.viewStart = static_cast(newStart); + memoryDbg.viewRange = newViewRange; + memoryDbg.smoothViewStart = newStart; + memoryDbg.smoothViewRange = newViewRange; + memoryDbg.zoomAnchorAddr = static_cast(addrAtCursor); + memoryDbg.zoomAnchorRelX = static_cast(relX); + memoryDbg.isZooming = true; } else // Pan { // Pan: wheel up → higher addresses const double addrDelta = -static_cast(wheel) * wViewRange * 0.15; - memoryDbg.viewStart = static_cast(wViewStart + addrDelta); + // Snap the pan target to line (bytesPerLine) boundaries so the + // view settles on whole data lines; the smooth value lerps to it. + const sizet pBpl = static_cast( + memoryDbg.bytesPerLine > 0 ? memoryDbg.bytesPerLine : 1); + const sizet lineAligned = + (static_cast(wViewStart + addrDelta) / pBpl) * pBpl; + memoryDbg.viewStart = lineAligned; } } } @@ -3469,9 +4371,14 @@ namespace p { const double addrDelta = -static_cast(dy) / addressH * memoryDbg.smoothViewRange; - memoryDbg.viewStart = - static_cast(memoryDbg.smoothViewStart + addrDelta); - memoryDbg.smoothViewStart = static_cast(memoryDbg.viewStart); + // Snap the pan target to line (bytesPerLine) boundaries so the + // view settles on whole data lines; the smooth value lerps to it. + const sizet pBpl = static_cast( + memoryDbg.bytesPerLine > 0 ? memoryDbg.bytesPerLine : 1); + const sizet lineAligned = + (static_cast(memoryDbg.smoothViewStart + addrDelta) / pBpl) + * pBpl; + memoryDbg.viewStart = lineAligned; } } } @@ -3492,7 +4399,9 @@ namespace p if (selectedArena) { detailsLabel = selectedArena->name.AsString(); - ImGui::Text("%s", detailsLabel.c_str()); + ImGui::TextColored(ImVec4{selectionColor.r / 255.0f, selectionColor.g / 255.0f, + selectionColor.b / 255.0f, selectionColor.a / 255.0f}, + "%s", detailsLabel.c_str()); detailsLabel = GetTypeName(selectedArena->typeId); ImGui::Text("Type: %s", detailsLabel.c_str()); @@ -3502,7 +4411,8 @@ namespace p ImGui::SeparatorText("Usage"); if (selectedArena->capacity > 0) { - sizeStr = Strings::ParseMemorySize(selectedArena->capacity); + sizeStr.clear(); + Strings::ParseMemorySizeTo(sizeStr, selectedArena->capacity); ImGui::Text("Capacity: %s (%zuB)", sizeStr.data(), selectedArena->capacity); } if (selectedArena->used > 0) @@ -3510,7 +4420,8 @@ namespace p const float usedPct = (selectedArena->capacity > 0) ? selectedArena->used / selectedArena->capacity : 0; - sizeStr = Strings::ParseMemorySize(selectedArena->used); + sizeStr.clear(); + Strings::ParseMemorySizeTo(sizeStr, selectedArena->used); ImGui::Text("Used: %s (%.1f%%)", sizeStr.c_str(), 100.f * usedPct); ImGui::ProgressBar(usedPct / 100.0f); } @@ -3520,12 +4431,13 @@ namespace p for (i32 i = 0; i < selectedArena->blocks.Size(); ++i) { const auto& block = selectedArena->blocks[i]; - char blockLabel[128]; - sizeStr = Strings::ParseMemorySize(block.size); - snprintf(blockLabel, sizeof(blockLabel), "Block %i: 0x%llX | %s (%zuB)", i, + String blockLabel; + sizeStr.clear(); + Strings::ParseMemorySizeTo(sizeStr, block.size); + p::FormatTo(blockLabel, "Block {}: 0x{:X} | {} ({}B)", i, static_cast(reinterpret_cast(block.data)), sizeStr.data(), block.size); - ImGui::BulletText("%s", blockLabel); + ImGui::BulletText("%s", blockLabel.c_str()); } ImGui::Separator(); if (ImGui::Button("Deselect")) @@ -3546,6 +4458,41 @@ namespace p ImGui::End(); // Parent window (closes the ImGuiWindowFlags_MenuBar window) } + + void CaptureMemory(DebugContext& ctx) + { + auto& memoryDbg = ctx.memory; + const auto& currentSnapshot = memoryDbg.curSnapshot ? memoryDbg.curSnapshot->snapshots + : memoryDbg.liveSnapshot.snapshots; + DebugMemoryContext::MemorySnapshot frame; + frame.snapshots.Reserve(static_cast(currentSnapshot.Size())); + for (const auto& src : currentSnapshot) + { + DebugMemoryContext::ArenaSnapshot dst; + dst.arena = src.arena; + dst.begin = src.begin; + dst.end = src.end; + dst.capacity = src.capacity; + dst.used = src.used; + dst.blocks = src.blocks; + dst.blockSizes = src.blockSizes; + dst.name = src.name; + dst.typeId = src.typeId; + dst.typeName = src.typeName; + dst.parentArenaIdx = src.parentArenaIdx; + + // Deep-copy live allocs so the capture owns its data. + dst.ownedLiveAllocs = src.captured + ? src.ownedLiveAllocs + : (src.live ? *src.live : TArray{}); + dst.captured = true; + dst.live = nullptr; + + frame.snapshots.Add(p::Move(dst)); + } + memoryDbg.captures.Add(p::Move(frame)); + } + #pragma endregion Memory bool BeginDebug(DebugContext& context) diff --git a/Include/Misc/PipeImGui.h b/Include/Misc/PipeImGui.h index baa139dd..a12064da 100644 --- a/Include/Misc/PipeImGui.h +++ b/Include/Misc/PipeImGui.h @@ -126,6 +126,13 @@ namespace ImGui TextUnformatted(text.data(), text.data() + text.size()); } + inline ImVec2 CalcTextSize( + p::StringView text, bool hide_text_after_double_hash = false, float wrap_width = 0.0f) + { + return CalcTextSize( + text.data(), text.data() + text.size(), hide_text_after_double_hash, wrap_width); + } + inline void TextDisabled(p::StringView text) { PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); @@ -163,6 +170,46 @@ namespace ImGui return TextLink(label.data()); } + // Text-only button: The text/icon glyph is the only visible content, no background. + inline bool TextButton(const char* text, const char* id = nullptr) + { + const ImVec2 padding = GetStyle().FramePadding; + const ImVec2 text_size = CalcTextSize(text); + const ImVec2 size(text_size.x + padding.x * 2.0f, text_size.y + padding.y * 2.0f); + + PushID(id ? id : text); + const bool clicked = InvisibleButton("textbtn", size); + + const bool disabled = (GetItemFlags() & ImGuiItemFlags_Disabled) != 0; + const bool hovered = IsItemHovered(); + const bool active = IsItemActive(); + + // Base text color, dimmed automatically via the disabled text color. + const ImVec4& baseStyle = + GetStyle().Colors[disabled ? ImGuiCol_TextDisabled : ImGuiCol_Text]; + const p::LinearColor base{baseStyle.x, baseStyle.y, baseStyle.z, baseStyle.w}; + + // Per-channel tint of the button background from idle to state. + const ImVec4& btnStyle = GetStyle().Colors[ImGuiCol_Button]; + const p::LinearColor btn{btnStyle.x, btnStyle.y, btnStyle.z, btnStyle.w}; + + p::LinearColor col = base; + if (!disabled) + { + const ImVec4& stateStyle = active ? GetStyle().Colors[ImGuiCol_ButtonActive] + : hovered ? GetStyle().Colors[ImGuiCol_ButtonHovered] + : btnStyle; + const p::LinearColor state{stateStyle.x, stateStyle.y, stateStyle.z, stateStyle.w}; + col = base + (state - btn); + } + + PushStyleColor(ImGuiCol_Text, col); + RenderText(GetItemRectMin() + padding, text); + PopStyleColor(); + PopID(); + return clicked; + } + // ImGui::InputText() with String // Because text input needs dynamic resizing, we need to setup a callback to grow the capacity diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index c0582aca..4b4c602f 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -82,20 +82,16 @@ namespace p // Mutable so it can be flipped through a const GetStats() pointer. mutable bool detectLeaks = true; - mutable TArray events; - - // Bit i set when events[i] is an allocation that, by the last - // CollectStats, had not been matched by a corresponding free. - mutable BitArray live; + mutable TArray live; mutable sizet used = 0; mutable sizet totalAllocated = 0; private: - // Open-addressed linear-probe map from event hash to the newest - // unmatched alloc event index for that hash. Keys are pre-mixed - // hashes (from GetHash), indexed directly without re-hashing. - // No per-insert allocation; grows at 75% load. + // Open-addressed linear-probe map from event hash to the index of + // the newest unmatched alloc in `live` for that hash. + // Keys are pre-mixed hashes (from GetHash), indexed directly without + // re-hashing. No per-insert allocation; grows at 75% load. class LiveIndex { static constexpr i32 Empty = -1; @@ -103,7 +99,7 @@ namespace p Arena* arena = nullptr; TArray keys; - // Parallel to keys: the node index, or Empty/Tombstone. + // Parallel to keys: the live index, or Empty/Tombstone. TArray nodes; u64 mask = 0; i32 count = 0; @@ -121,12 +117,11 @@ namespace p }; // --- Incremental CollectStats state (consumer thread only) --- - // Newest unmatched alloc index per event key. Chains are - // intrusively linked through liveAllocIdx, newest first. + // Index of the newest unmatched alloc per event key. mutable LiveIndex liveIdx; - // For each alloc event index, the previous unmatched alloc index - // sharing the same key (NO_INDEX if none). Consumed on free. - mutable TArray liveAllocIdx; + // Classifier scratch: drained events awaiting classification. + // CollectStats is not reentrant; owned by the consuming thread. + mutable TArray pending; public: diff --git a/Include/PipeStrings.h b/Include/PipeStrings.h index 00251287..f355d830 100644 --- a/Include/PipeStrings.h +++ b/Include/PipeStrings.h @@ -1358,9 +1358,10 @@ namespace p P_API bool Split(const String& str, String& a, String& b, const char* delim); P_API bool IsNumeric(const String& str); - P_API bool IsNumeric(const char* Str); + P_API bool IsNumeric(const char* str); - P_API String ParseMemorySize(sizet size); + P_API String ParseMemorySize(sizet size, bool asBits = false); + P_API void ParseMemorySizeTo(String& str, sizet size, bool asBits = false); template inline void ConvertTo(TStringView source, ToStringType& dest) diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index 1b3929f8..b68d349e 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -196,10 +196,7 @@ namespace p } // namespace MemoryStats::MemoryStats() - : events{GetStatsArena()} - , live{GetStatsArena()} - , liveIdx{GetStatsArena()} - , liveAllocIdx{GetStatsArena()} + : live{GetStatsArena()}, liveIdx{GetStatsArena()}, pending{GetStatsArena()} {} MemoryStats::~MemoryStats() @@ -272,26 +269,22 @@ namespace p CollectStats(); used = 0; totalAllocated = 0; - events.Clear(); live.Clear(); liveIdx.Clear(); - liveAllocIdx.Clear(); } void MemoryStats::CollectStats() const { - const i32 lastEventsSize = events.Size(); - - { // Drain the shared event queue + // Phase 1: drain the shared event queue into a scratch buffer, + // keeping the lock hold time to just the memcpy. + pending.Clear(); + { ScopedLock guard(lock); EventChunk* chunk = firstChunk; while (chunk) { - for (u32 i = 0; i < chunk->size; ++i) - { - events.Add(chunk->slots[i]); - } + pending.Append(chunk->slots, chunk->size); EventChunk* const next = chunk->next; @@ -310,61 +303,44 @@ namespace p lastChunk = nullptr; } - // --- Incremental classification of drained events + counters --- - live.Resize(events.Size()); - liveAllocIdx.Resize(events.Size()); - - for (i32 i = lastEventsSize; i < events.Size(); ++i) + // Phase 2: classify drained events outside the lock. Alloc events + // become entries in `live`; matched frees swap-remove them. + // Nothing else is retained, so memory stays O(live). + for (const MemoryStatsEvent& ev : pending) { - const MemoryStatsEvent& ev = events[i]; - const u64 hash = GetHash(ev); + const u64 hash = GetHash(ev); + const sizet size = ev.GetSize(); if (ev.IsFree()) { if (i32* nodePtr = liveIdx.Find(hash)) { - // Unmark the newest unmatched alloc and pop it off the - // chain, promoting its predecessor as chain firstChunk. - const i32 node = *nodePtr; - live.SetFalse(node); - const i32 prev = liveAllocIdx[node]; - if (prev == NO_INDEX) + // Swap-remove the matched alloc from the live list, + // then point its map slot at the moved-in element. + const i32 p = *nodePtr; + const i32 lastIdx = live.Size() - 1; + if (lastIdx != p) { - liveIdx.EraseAt(nodePtr); - } - else - { - *nodePtr = prev; + live[p] = live[lastIdx]; + if (i32* movedSlot = liveIdx.Find(GetHash(live[p]))) + { + if (*movedSlot == lastIdx) + { + *movedSlot = p; + } + } } + live.RemoveLast(1, Shrink::No); + liveIdx.EraseAt(nodePtr); + used -= size; } - // Else a stray free: recorded, nothing to unmark. - } - else - { - i32* idxPtr = liveIdx.FindOrInsert(hash, i); - if (*idxPtr != i) - { - liveAllocIdx[i] = *idxPtr; - *idxPtr = i; - } - else - { - liveAllocIdx[i] = NO_INDEX; - } - live.SetTrue(i); - } - } - - // Update stats - for (i32 i = lastEventsSize; i < events.Size(); ++i) - { - const MemoryStatsEvent& ev = events[i]; - const sizet size = ev.GetSize(); - if (ev.IsFree()) - { - used -= size; + // Else a stray free: no matching live alloc, ignore. } else { + live.Add(ev); + const i32 idx = live.Size() - 1; + i32* const slot = liveIdx.FindOrInsert(hash, idx); + *slot = idx; used += size; totalAllocated += size; } @@ -373,7 +349,7 @@ namespace p void MemoryStats::CheckLeaks() const { - const i32 numLeaks = live.CountSetBits(); + const i32 numLeaks = live.Size(); if (numLeaks <= 0) { return; @@ -383,16 +359,14 @@ namespace p FormatTo(errorMsg, "{}: {} allocs were not freed!", name ? name : "MemoryStats", numLeaks); const i32 shown = Min(64, numLeaks); - i32 i = -1; i32 printed = 0; - while (printed < shown) + for (const auto& ev : live) { - i = live.GetNextSet(i); - if (i == NO_INDEX) + if (printed >= shown) { break; } - PrintAllocationError("", &events[i]); + PrintAllocationError("", &ev); ++printed; } if (numLeaks > shown) diff --git a/Src/PipeStrings.cpp b/Src/PipeStrings.cpp index a43ce02b..dfb0f8f6 100644 --- a/Src/PipeStrings.cpp +++ b/Src/PipeStrings.cpp @@ -99,17 +99,17 @@ namespace p::Strings return IsNumeric(str.data()); } - bool IsNumeric(const char* Str) + bool IsNumeric(const char* str) { - if (*Str == '-' || *Str == '+') + if (*str == '-' || *str == '+') { - Str++; + str++; } bool bHasDot = false; - while (*Str != '\0') + while (*str != '\0') { - if (*Str == '.') + if (*str == '.') { if (bHasDot) { @@ -117,37 +117,52 @@ namespace p::Strings } bHasDot = true; } - else if (!FChar::IsDigit(*Str)) + else if (!FChar::IsDigit(*str)) { return false; } - ++Str; + ++str; } return true; } - String ParseMemorySize(sizet size) + String ParseMemorySize(sizet size, bool asBits) + { + String result; + ParseMemorySizeTo(result, size, asBits); + return result; + } + + void ParseMemorySizeTo(String& str, sizet size, bool asBits) { if (size == 0) { - return "0B"; + str.append(asBits ? "0b" : "0B"); + return; } - static StringView sizes[]{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; + constexpr StringView byteSizes[]{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; + constexpr StringView bitSizes[]{"b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"}; - const double scaleD = Log(double(size), 1024.l); - const u32 scale = u32(FloorToI64(scaleD)); + const StringView* sizes = byteSizes; + double dSize(size); + if (asBits) + { + sizes = bitSizes; + dSize *= 0.125; // /8 + } + const double dScale = Log(dSize, 1024.l); + const u32 scale = u32(FloorToI64(dScale)); const double finalSize = double(size) / Pow(1024, scale); - String sizeStr = Format(StringView{"{:.1f}"}, finalSize); + FormatTo(str, StringView{"{:.1f}"}, finalSize); // Remove trailing zeros - RemoveFromEnd(sizeStr, sizeStr.size() - Find(sizeStr, '0', FindDir::Back, true) - 1); - RemoveFromEnd(sizeStr, sizeStr.size() - Find(sizeStr, '.', FindDir::Back, true) - 1); + RemoveFromEnd(str, str.size() - Find(str, '0', FindDir::Back, true) - 1); + RemoveFromEnd(str, str.size() - Find(str, '.', FindDir::Back, true) - 1); - String result = sizeStr; - result += sizes[scale]; - return result; + str.append(sizes[scale]); // Suffix } + } // namespace p::Strings diff --git a/Tests/Memory/MemoryStats.spec.cpp b/Tests/Memory/MemoryStats.spec.cpp index e5f0f268..7a555173 100644 --- a/Tests/Memory/MemoryStats.spec.cpp +++ b/Tests/Memory/MemoryStats.spec.cpp @@ -13,61 +13,13 @@ using namespace bandit; using namespace p; -static i32 AllocCount(const MemoryStats& s) +static i32 LiveCount(const MemoryStats& s) { - i32 n = 0; - for (const auto& ev : s.events) - { - if (!ev.IsFree()) - { - ++n; - } - } - return n; -} -static i32 FreeCount(const MemoryStats& s) -{ - i32 n = 0; - for (const auto& ev : s.events) - { - if (ev.IsFree()) - { - ++n; - } - } - return n; + return s.live.Size(); } static MemoryStatsEvent LiveAt(const MemoryStats& s, i32 i) { - for (const auto& ev : s.events) - { - if (ev.IsFree()) - { - continue; - } - if (i == 0) - { - return ev; - } - --i; - } - return MemoryStatsEvent{}; -} -static MemoryStatsEvent FreeAt(const MemoryStats& s, i32 i) -{ - for (const auto& ev : s.events) - { - if (!ev.IsFree()) - { - continue; - } - if (i == 0) - { - return ev; - } - --i; - } - return MemoryStatsEvent{}; + return s.live[i]; } @@ -83,8 +35,7 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo(0)); AssertThat(s.totalAllocated, Is().EqualTo(0)); - AssertThat(AllocCount(s), Is().EqualTo(0)); - AssertThat(FreeCount(s), Is().EqualTo(0)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); it("Tracks a single add", [&]() @@ -95,7 +46,7 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo(64)); AssertThat(s.totalAllocated, Is().EqualTo(64)); - AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(1)); AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); AssertThat(LiveAt(s, 0).IsFree(), Is().EqualTo(false)); @@ -109,11 +60,9 @@ go_bandit([]() s.Remove((void*)0x1000, 64); s.CollectStats(); AssertThat(s.used, Is().EqualTo(0)); - AssertThat(s.totalAllocated, Is().EqualTo(0)); - AssertThat(AllocCount(s), Is().EqualTo(1)); - AssertThat(FreeCount(s), Is().EqualTo(1)); - AssertThat(FreeAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); - AssertThat(FreeAt(s, 0).IsFree(), Is().EqualTo(true)); + AssertThat(LiveCount(s), Is().EqualTo(0)); + // totalAllocated is cumulative alloc bytes ever. + AssertThat(s.totalAllocated, Is().EqualTo(64)); }); it("Tracks multiple adds", [&]() @@ -126,7 +75,7 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo(16 + 32 + 64)); AssertThat(s.totalAllocated, Is().EqualTo(16 + 32 + 64)); - AssertThat(AllocCount(s), Is().EqualTo(3)); + AssertThat(LiveCount(s), Is().EqualTo(3)); }); it("Tracks many adds and frees", [&]() @@ -147,12 +96,11 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo((N / 2) * 16)); - AssertThat(s.totalAllocated, Is().EqualTo((N / 2) * 16)); - AssertThat(AllocCount(s), Is().EqualTo(N)); - AssertThat(FreeCount(s), Is().EqualTo(N / 2)); + AssertThat(s.totalAllocated, Is().EqualTo(N * 16)); + AssertThat(LiveCount(s), Is().EqualTo(N / 2)); }); - it("Records double-free", [&]() + it("Ignores double-free", [&]() { MemoryStats s; s.detectLeaks = false; @@ -160,22 +108,19 @@ go_bandit([]() s.Remove((void*)0x1000, 64); s.Remove((void*)0x1000, 64); s.CollectStats(); - // Every free is recorded chronologically, so used underflows. - AssertThat(s.used, Is().EqualTo((sizet)-64)); - AssertThat(AllocCount(s), Is().EqualTo(1)); - AssertThat(FreeCount(s), Is().EqualTo(2)); + // The second free matches no live alloc and is ignored. + AssertThat(s.used, Is().EqualTo(0)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); - it("Records free of unknown ptr", [&]() + it("Ignores free of unknown ptr", [&]() { MemoryStats s; s.detectLeaks = false; s.Remove((void*)0xDEAD, 64); s.CollectStats(); - // Free of unknown ptr is still recorded, so used underflows. - AssertThat(s.used, Is().EqualTo((sizet)-64)); - AssertThat(AllocCount(s), Is().EqualTo(0)); - AssertThat(FreeCount(s), Is().EqualTo(1)); + AssertThat(s.used, Is().EqualTo(0)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); it("Records duplicate allocs", [&]() @@ -185,8 +130,8 @@ go_bandit([]() s.Add((void*)0x1000, 64); s.Add((void*)0x1000, 128); s.CollectStats(); - // Every alloc is recorded chronologically, both survive. - AssertThat(AllocCount(s), Is().EqualTo(2)); + // Same ptr, different size: distinct keys, both survive. + AssertThat(LiveCount(s), Is().EqualTo(2)); AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); AssertThat(LiveAt(s, 1).GetSize(), Is().EqualTo(128)); }); @@ -198,7 +143,7 @@ go_bandit([]() s.Add((void*)0x1000, 64); s.CollectStats(); s.CheckLeaks(); - AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(1)); AssertThat(s.used, Is().EqualTo(64)); }); @@ -210,7 +155,7 @@ go_bandit([]() s.Remove((void*)0x1000, 64); s.CollectStats(); AssertThat(s.used, Is().EqualTo(0)); - AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); it("CheckLeaks with null name does not crash", [&]() @@ -224,7 +169,7 @@ go_bandit([]() } }); - it("live bitset and free flags match events", [&]() + it("live list only keeps unmatched allocs", [&]() { MemoryStats s; s.detectLeaks = false; @@ -236,21 +181,16 @@ go_bandit([]() s.Remove((void*)0xDEAD, 16); s.CollectStats(); - AssertThat(s.events.Size(), Is().EqualTo(5)); - AssertThat(FreeCount(s), Is().EqualTo(2)); - AssertThat(s.events[3].IsFree(), Is().EqualTo(true)); - AssertThat(s.events[4].IsFree(), Is().EqualTo(true)); - AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); - AssertThat(s.live.IsSet(0), Is().EqualTo(true)); - AssertThat(s.live.IsSet(1), Is().EqualTo(true)); - AssertThat(s.live.IsSet(2), Is().EqualTo(false)); - AssertThat(s.live.IsSet(3), Is().EqualTo(false)); - AssertThat(s.live.IsSet(4), Is().EqualTo(false)); - - // Re-collecting must rebuild the bitset identically. + AssertThat(LiveCount(s), Is().EqualTo(2)); + AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); + AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); + AssertThat(LiveAt(s, 1).GetPtr(), Is().EqualTo((u8*)0x2000)); + AssertThat(LiveAt(s, 1).GetSize(), Is().EqualTo(32)); + + // Re-collecting must preserve the live list identically. s.CollectStats(); - AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); - AssertThat(FreeCount(s), Is().EqualTo(2)); + AssertThat(LiveCount(s), Is().EqualTo(2)); + AssertThat(s.used, Is().EqualTo(64 + 32)); }); it("Alternating instances on one thread", [&]() @@ -270,11 +210,9 @@ go_bandit([]() b.CollectStats(); AssertThat(a.used, Is().EqualTo(64 + 16)); - AssertThat(AllocCount(a), Is().EqualTo(2)); - AssertThat(FreeCount(a), Is().EqualTo(0)); + AssertThat(LiveCount(a), Is().EqualTo(2)); AssertThat(b.used, Is().EqualTo(0)); - AssertThat(AllocCount(b), Is().EqualTo(1)); - AssertThat(FreeCount(b), Is().EqualTo(1)); + AssertThat(LiveCount(b), Is().EqualTo(0)); }); it("Add after Reset works", [&]() @@ -283,14 +221,13 @@ go_bandit([]() s.detectLeaks = false; s.Add((void*)0x1000, 64); s.Reset(); - AssertThat(AllocCount(s), Is().EqualTo(0)); + AssertThat(LiveCount(s), Is().EqualTo(0)); s.Add((void*)0x2000, 32); s.CollectStats(); AssertThat(s.used, Is().EqualTo(32)); AssertThat(s.totalAllocated, Is().EqualTo(32)); - AssertThat(AllocCount(s), Is().EqualTo(1)); - AssertThat(s.live.CountSetBits(), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(1)); }); it("Frees across collects unmark live allocs (LIFO, duplicate keys)", [&]() @@ -302,22 +239,18 @@ go_bandit([]() s.Add((void*)0x1000, 64); s.Add((void*)0x1000, 64); s.CollectStats(); - AssertThat(s.live.CountSetBits(), Is().EqualTo(2)); - AssertThat(s.live.IsSet(0), Is().EqualTo(true)); - AssertThat(s.live.IsSet(1), Is().EqualTo(true)); + AssertThat(LiveCount(s), Is().EqualTo(2)); // Collect 2: one free must unmark the latest alloc (LIFO). s.Remove((void*)0x1000, 64); s.CollectStats(); - AssertThat(s.live.CountSetBits(), Is().EqualTo(1)); - AssertThat(s.live.IsSet(0), Is().EqualTo(true)); - AssertThat(s.live.IsSet(1), Is().EqualTo(false)); + AssertThat(LiveCount(s), Is().EqualTo(1)); + AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); - // Collect 3: second free pops the olderLive spill entry. + // Collect 3: second free pops the remaining alloc. s.Remove((void*)0x1000, 64); s.CollectStats(); - AssertThat(s.live.CountSetBits(), Is().EqualTo(0)); - AssertThat(FreeCount(s), Is().EqualTo(2)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); it("Ignores null ptr in Remove", [&]() @@ -337,7 +270,7 @@ go_bandit([]() // Add has no null check (unlike Remove), so the event is // recorded and processed. Add's size is still tracked. AssertThat(s.used, Is().EqualTo(64)); - AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(1)); }); it("Reset resets state", [&]() @@ -351,8 +284,7 @@ go_bandit([]() s.Reset(); AssertThat(s.used, Is().EqualTo(0)); AssertThat(s.totalAllocated, Is().EqualTo(0)); - AssertThat(AllocCount(s), Is().EqualTo(0)); - AssertThat(FreeCount(s), Is().EqualTo(0)); + AssertThat(LiveCount(s), Is().EqualTo(0)); }); it("CollectStats is additive", [&]() @@ -364,7 +296,7 @@ go_bandit([]() s.Add((void*)0x2000, 32); s.CollectStats(); AssertThat(s.used, Is().EqualTo(96)); - AssertThat(AllocCount(s), Is().EqualTo(2)); + AssertThat(LiveCount(s), Is().EqualTo(2)); }); it("Re-collecting preserves state", [&]() @@ -375,7 +307,7 @@ go_bandit([]() s.CollectStats(); s.CollectStats(); AssertThat(s.used, Is().EqualTo(64)); - AssertThat(AllocCount(s), Is().EqualTo(1)); + AssertThat(LiveCount(s), Is().EqualTo(1)); }); }); @@ -395,7 +327,7 @@ go_bandit([]() s.CollectStats(); AssertThat(s.used, Is().EqualTo(N * 8)); AssertThat(s.totalAllocated, Is().EqualTo(N * 8)); - AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(LiveCount(s), Is().EqualTo(N)); }); it("Handles add/free across chunks", [&]() @@ -414,8 +346,8 @@ go_bandit([]() } s.CollectStats(); AssertThat(s.used, Is().EqualTo((N / 2) * 8)); - AssertThat(AllocCount(s), Is().EqualTo(N)); - AssertThat(FreeCount(s), Is().EqualTo(N / 2)); + AssertThat(s.totalAllocated, Is().EqualTo(N * 8)); + AssertThat(LiveCount(s), Is().EqualTo(N / 2)); }); it("Frees chunks between CollectStats calls", [&]() @@ -429,16 +361,14 @@ go_bandit([]() s.Add(&buf[i * 8], 8); } s.CollectStats(); - AssertThat(AllocCount(s), Is().EqualTo(N)); - AssertThat(s.live.CountSetBits(), Is().EqualTo(N)); + AssertThat(LiveCount(s), Is().EqualTo(N)); for (sizet i = 0; i < N / 2; ++i) { s.Remove(&buf[i * 8], 8); } s.CollectStats(); - AssertThat(AllocCount(s), Is().EqualTo(N)); - AssertThat(s.live.CountSetBits(), Is().EqualTo(N / 2)); - AssertThat(FreeCount(s), Is().EqualTo(N / 2)); + AssertThat(LiveCount(s), Is().EqualTo(N / 2)); + AssertThat(s.used, Is().EqualTo((N / 2) * 8)); }); }); @@ -468,7 +398,7 @@ go_bandit([]() { while (!start.load(std::memory_order_acquire)) {} - while (!producerDone.load(std::memory_order_acquire) || AllocCount(s) < N) + while (!producerDone.load(std::memory_order_acquire) || LiveCount(s) < N) { s.CollectStats(); std::this_thread::yield(); @@ -479,7 +409,7 @@ go_bandit([]() producer.join(); consumer.join(); - AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(LiveCount(s), Is().EqualTo(N)); AssertThat(s.used, Is().EqualTo(N * 8)); // Suppress leak warnings at destruction (test buffers are stack). @@ -537,7 +467,7 @@ go_bandit([]() } consumer.join(); - AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(LiveCount(s), Is().EqualTo(N)); AssertThat(s.used, Is().EqualTo(N * 8)); AssertThat(s.totalAllocated, Is().EqualTo(N * 8)); @@ -601,9 +531,9 @@ go_bandit([]() } consumer.join(); - AssertThat(AllocCount(s), Is().EqualTo(N)); + AssertThat(LiveCount(s), Is().EqualTo(N / 2)); AssertThat(s.used, Is().EqualTo((N / 2) * 8)); - AssertThat(s.totalAllocated, Is().EqualTo((N / 2) * 8)); + AssertThat(s.totalAllocated, Is().EqualTo(N * 8)); // Suppress leak warnings at destruction (test buffers are stack). s.Reset(); @@ -668,13 +598,12 @@ go_bandit([]() } consumer.join(); - // s.used reflects net adds/frees chronologically. - AssertThat(s.used, Is().EqualTo((AllocCount(s) - FreeCount(s)) * 8)); - AssertThat(s.used, Is().EqualTo(s.totalAllocated)); + // s.used reflects the net remaining live set. + AssertThat(s.used, Is().EqualTo(LiveCount(s) * 8)); // Suppress leak warnings at destruction (test buffers are stack). s.Reset(); }); }); }); -}); +}); \ No newline at end of file From 4b76f478a67dd8a0c4b574d54acd74a40fd73650 Mon Sep 17 00:00:00 2001 From: muit Date: Wed, 2 Sep 2026 22:35:35 +0200 Subject: [PATCH 08/15] Small fixes for UE --- Include/Misc/PipeDebug.h | 6 +++--- Include/Misc/PipeImGui.h | 3 ++- Src/Pipe.cpp | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index 711f3eec..aadd4f76 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -3775,10 +3775,10 @@ namespace p // they render underneath. if (snapshot.parentArenaIdx != NO_INDEX && snapshot.begin && snapshot.capacity > 0) { - const i32 pi = snapshot.parentArenaIdx; - const bool left = (pi < i); + const i32 parentIdx = snapshot.parentArenaIdx; + const bool left = (parentIdx < i); const float parentPad = colW * 0.25f; - const auto& parentSnap = snapshots[pi]; + const auto& parentSnap = snapshots[parentIdx]; const ImU32 stripCol = blockFillColor.Translucency(25).DWColor(); for (const auto& block : snapshot.blocks) { diff --git a/Include/Misc/PipeImGui.h b/Include/Misc/PipeImGui.h index a12064da..a72598e5 100644 --- a/Include/Misc/PipeImGui.h +++ b/Include/Misc/PipeImGui.h @@ -204,7 +204,8 @@ namespace ImGui } PushStyleColor(ImGuiCol_Text, col); - RenderText(GetItemRectMin() + padding, text); + const ImVec2 rectMin = GetItemRectMin(); + RenderText({rectMin.x + padding.x, rectMin.y + padding.y}, text); PopStyleColor(); PopID(); return clicked; diff --git a/Src/Pipe.cpp b/Src/Pipe.cpp index 821f3b58..fc0de957 100644 --- a/Src/Pipe.cpp +++ b/Src/Pipe.cpp @@ -1,6 +1,9 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. -#if defined(P_OVERRIDE_NEWDELETE) +#ifndef P_OVERRIDE_NEWDELETE + #define P_OVERRIDE_NEWDELETE 1 +#endif +#if P_OVERRIDE_NEWDELETE #include "PipeNewDelete.h" // New/Delete must be first include #endif From fbbed96272bb7ae1f9e6d8aebce905007ed39cd1 Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 01:39:29 +0200 Subject: [PATCH 09/15] Fixed crash on invalid memory stats allocs/frees, tweaked multi linear arena --- Include/Misc/PipeDebug.h | 138 +++++++--------- Include/Pipe/Memory/MemoryStats.h | 56 +++---- Include/PipeMemoryArenas.h | 15 +- Src/Memory/MemoryStats.cpp | 251 +++++++++--------------------- Tests/Memory/MemoryStats.spec.cpp | 78 +++++++--- 5 files changed, 226 insertions(+), 312 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index aadd4f76..2152d342 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -287,8 +287,8 @@ namespace p // (ownedLiveAllocs). When live, live points to the live // stats data and is only valid during the current live rebuild. bool captured = false; - TArray ownedLiveAllocs; - const TArray* live = nullptr; + TSet ownedLiveAllocs; + const TSet* live = nullptr; // Index of the parent arena snapshot (the Arena a ChildArena allocates from). i32 parentArenaIdx = NO_INDEX; }; @@ -2554,16 +2554,29 @@ namespace p // Vertical line. tlDraw->AddLine(ImVec2(snapX, plotY0), ImVec2(snapX, plotY1), IM_COL32(255, 255, 255, 80), 1.0f); - // Dots + tooltip content: for EACH visible arena, take its - // nearest sample to the snapped time and show its value. - String tooltip; + // Dots + tooltip: for EACH visible arena, take its nearest + // sample to the snapped time, draw a dot on the plot, and + // queue a colored tooltip row (legend order). + struct TipRow + { + p::Color col; + sizet used; + sizet capacity; + }; + TipRow rows[32]; + i32 rowCount = 0; for (const auto& va : visible) { + if (rowCount >= 32) + { + break; + } const p::Color ac = details::GetArenaColor(va.arena->GetTypeId()); const ImU32 dotCol = ac.DWColor(); - sizet arenaUsed = 0; - bool found = false; - double arenaDist = 1e30; + sizet arenaUsed = 0; + sizet arenaCapacity = 0; + bool found = false; + double arenaDist = 1e30; for (const auto& t : memoryDbg.timelines) { if (t.arena != va.arena) @@ -2579,9 +2592,10 @@ namespace p const double d = p::Abs(t.samples[s].time - bestTime); if (d < arenaDist) { - arenaDist = d; - arenaUsed = t.samples[s].used; - found = true; + arenaDist = d; + arenaUsed = t.samples[s].used; + arenaCapacity = t.samples[s].capacity; + found = true; } } break; @@ -2592,73 +2606,42 @@ namespace p } const float dotY = YFor(arenaUsed); tlDraw->AddCircleFilled(ImVec2(snapX, dotY), 3.5f, dotCol); - const char* nm = nullptr; - for (const auto& snap : memoryDbg.liveSnapshot.snapshots) - { - if (snap.arena == va.arena) - { - nm = snap.name.Data(); - break; - } - } - if (!nm || nm[0] == '\0') - { - nm = "Arena"; - } - static String tmpSize; - tmpSize.clear(); - Strings::ParseMemorySizeTo(tmpSize, arenaUsed); - if (tooltip.size() > 0) - { - tooltip += "\n"; - } - p::FormatTo(tooltip, "{}: {}", nm, tmpSize.c_str()); + rows[rowCount++] = TipRow{ac, arenaUsed, arenaCapacity}; } - if (tooltip.size() > 0) + if (rowCount > 0) { ImGui::BeginTooltip(); - // Parse lines and draw colored. - const char* p = tooltip.c_str(); - while (*p) + static String usedStr; + static String capStr; + static String line; + const float h = ImGui::GetTextLineHeight(); + const float sq = h * 0.7f; + const float pad = (h - sq) * 0.5f; + for (i32 r = 0; r < rowCount; ++r) { - const char* nl = strchr(p, '\n'); - const size_t len = nl ? static_cast(nl - p) : strlen(p); - // Extract arena name (before ":"). - const char* colon = reinterpret_cast(memchr(p, ':', len)); - if (colon) + const TipRow& row = rows[r]; + usedStr.clear(); + Strings::ParseMemorySizeTo(usedStr, row.used); + const ImVec4 col{row.col.r / 255.0f, row.col.g / 255.0f, + row.col.b / 255.0f, 1.0f}; + const ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(p.x, p.y + pad), + ImVec2(p.x + sq, p.y + pad + sq), row.col.DWColor()); + ImGui::SetCursorScreenPos(ImVec2(p.x + sq + 4.0f, p.y)); + if (row.capacity > 0) { - const size_t nameLen = static_cast(colon - p); - // Find arena color. - p::Color ac{200, 200, 200}; - for (const auto& va2 : visible) - { - const char* nm2 = nullptr; - for (const auto& snap : memoryDbg.liveSnapshot.snapshots) - { - if (snap.arena == va2.arena) - { - nm2 = snap.name.Data(); - break; - } - } - if (nm2 && strlen(nm2) == nameLen && memcmp(nm2, p, nameLen) == 0) - { - ac = details::GetArenaColor(va2.arena->GetTypeId()); - break; - } - } - ImGui::TextColored( - ImVec4{ac.r / 255.0f, ac.g / 255.0f, ac.b / 255.0f, 1.0f}, "%.*s", - static_cast(len), p); + capStr.clear(); + Strings::ParseMemorySizeTo(capStr, row.capacity); + line.clear(); + p::FormatTo(line, "{}/{:.0f}%/{}", usedStr.c_str(), + 100.0 * static_cast(row.used) + / static_cast(row.capacity), + capStr.c_str()); + ImGui::TextColored(col, "%s", line.c_str()); } else { - ImGui::TextUnformatted(p, p + len); - } - p += len; - if (*p == '\n') - { - ++p; + ImGui::TextColored(col, "%s", usedStr.c_str()); } } ImGui::EndTooltip(); @@ -2857,7 +2840,7 @@ namespace p stats->CollectStats(); snapshot.name = Tag(stats->name); snapshot.used = stats->used; - snapshot.live = &stats->live; + snapshot.live = &stats->liveAllocations; // Union live allocation addresses into the arena range so // arenas without blocks (e.g. HeapArena) still report the @@ -3871,23 +3854,22 @@ namespace p const float padding = colW * 0.25f; const sizet viewStartS = static_cast(viewStart); const sizet viewEndS = static_cast(viewStart + viewRange); - TArray liveInRange; - for (i32 j = 0; j < live->Size(); ++j) + TArray liveInRange; + for (const auto& ev : *live) { - const auto& ev = (*live)[j]; const sizet addr = reinterpret_cast(ev.GetPtr()); const sizet size = ev.GetSize(); if (addr >= viewEndS || addr + size <= viewStartS) { continue; } - liveInRange.Add(j); + liveInRange.Add(&ev); } // Draw allocations - for (i32 id : liveInRange) + for (const auto* evPtr : liveInRange) { - const auto& ev = (*live)[id]; + const auto& ev = *evPtr; const sizet addr = reinterpret_cast(ev.GetPtr()); const sizet size = ev.GetSize(); const float ty = AddrToY(addr); @@ -4484,7 +4466,7 @@ namespace p // Deep-copy live allocs so the capture owns its data. dst.ownedLiveAllocs = src.captured ? src.ownedLiveAllocs - : (src.live ? *src.live : TArray{}); + : (src.live ? *src.live : TSet{}); dst.captured = true; dst.live = nullptr; diff --git a/Include/Pipe/Memory/MemoryStats.h b/Include/Pipe/Memory/MemoryStats.h index 4b4c602f..7b743187 100644 --- a/Include/Pipe/Memory/MemoryStats.h +++ b/Include/Pipe/Memory/MemoryStats.h @@ -4,6 +4,7 @@ #include "Pipe/Core/EnumFlags.h" #include "Pipe/Core/Hash.h" +#include "Pipe/Core/Set.h" #include "Pipe/Core/SpinLock.h" #include "Pipe/Core/StringView.h" #include "Pipe/Core/Utility.h" @@ -62,18 +63,33 @@ namespace p friend bool operator==(const MemoryStatsEvent& a, const MemoryStatsEvent& b) noexcept { - return a.ptr == b.ptr && a.GetSize() == b.GetSize(); + return a.ptr == b.ptr; } }; #pragma pack(pop) inline sizet GetHash(const MemoryStatsEvent& ev) noexcept { - return HashCombine(GetHash(ev.GetPtr()), ev.GetSize()); + return GetHash(ev.GetPtr()); } static_assert(sizeof(MemoryStatsEvent) == 16); + enum class MemoryStatsErrorType : u8 + { + None, + UnknownFree, + SizeMismatch, // free(ptr, X) but live has same ptr with size != X + UnfreedRealloc, // alloc(ptr, X) but live already has same ptr + }; + + struct P_API MemoryStatsError + { + MemoryStatsEvent event; + MemoryStatsErrorType kind; + }; + + struct P_API MemoryStats { mutable const char* name = nullptr; @@ -82,46 +98,20 @@ namespace p // Mutable so it can be flipped through a const GetStats() pointer. mutable bool detectLeaks = true; - mutable TArray live; + // Live allocs keyed by pointer. At most one entry per pointer; + // duplicate allocs are reported as UnfreedRealloc errors. + mutable TSet liveAllocations; + mutable TArray errors; mutable sizet used = 0; mutable sizet totalAllocated = 0; private: - // Open-addressed linear-probe map from event hash to the index of - // the newest unmatched alloc in `live` for that hash. - // Keys are pre-mixed hashes (from GetHash), indexed directly without - // re-hashing. No per-insert allocation; grows at 75% load. - class LiveIndex - { - static constexpr i32 Empty = -1; - static constexpr i32 Tombstone = -2; - - Arena* arena = nullptr; - TArray keys; - // Parallel to keys: the live index, or Empty/Tombstone. - TArray nodes; - u64 mask = 0; - i32 count = 0; - i32 tombCount = 0; - - void Grow(); - - public: - explicit LiveIndex(Arena& inArena) : arena{&inArena}, keys{inArena}, nodes{inArena} {} - - i32* Find(u64 hash); - i32* FindOrInsert(u64 hash, i32 node); - void EraseAt(i32* node); - void Clear(); - }; - // --- Incremental CollectStats state (consumer thread only) --- - // Index of the newest unmatched alloc per event key. - mutable LiveIndex liveIdx; // Classifier scratch: drained events awaiting classification. // CollectStats is not reentrant; owned by the consuming thread. mutable TArray pending; + mutable TArray pendingErrors; public: diff --git a/Include/PipeMemoryArenas.h b/Include/PipeMemoryArenas.h index 23a1dbeb..49ae7b86 100644 --- a/Include/PipeMemoryArenas.h +++ b/Include/PipeMemoryArenas.h @@ -69,6 +69,7 @@ namespace p { return &stats; } + protected: TypeId ProvideTypeId() const override { @@ -135,6 +136,7 @@ namespace p { return &stats; } + protected: TypeId ProvideTypeId() const override { @@ -216,22 +218,22 @@ namespace p } }; - struct P_API LinearSmallPool : public LinearBasePool<1 * Memory::MB> + struct P_API LinearSmallPool : public LinearBasePool<512 * Memory::KB> { static constexpr sizet minSize = 0; - static constexpr sizet maxSize = 8 * Memory::KB; + static constexpr sizet maxSize = 4 * Memory::KB; }; struct P_API LinearMediumPool : public LinearBasePool<4 * Memory::MB> { static constexpr sizet minSize = LinearSmallPool::maxSize; - static constexpr sizet maxSize = 512 * Memory::KB; + static constexpr sizet maxSize = 32 * Memory::KB; }; - struct P_API LinearBigPool : public LinearBasePool<16 * Memory::MB> + struct P_API LinearBigPool : public LinearBasePool<32 * Memory::MB> { static constexpr sizet minSize = LinearMediumPool::maxSize; - static constexpr sizet maxSize = 4 * Memory::MB; + static constexpr sizet maxSize = 256 * Memory::KB; // Block size is the size of the allocation }; } // namespace Details @@ -288,6 +290,7 @@ namespace p { return &stats; } + protected: TypeId ProvideTypeId() const override { @@ -396,6 +399,7 @@ namespace p { return &stats; } + private: i32 FindSmallestSlot(sizet neededSize); void ReduceSlot( @@ -507,6 +511,7 @@ namespace p { return &stats; } + private: AllocationHeader* GetHeader(void* ptr) const { diff --git a/Src/Memory/MemoryStats.cpp b/Src/Memory/MemoryStats.cpp index b68d349e..b206989c 100644 --- a/Src/Memory/MemoryStats.cpp +++ b/Src/Memory/MemoryStats.cpp @@ -50,132 +50,6 @@ namespace p } - // --------------------------------------------------------------------------- - // MemoryStats::LiveIndex - // --------------------------------------------------------------------------- - - void MemoryStats::LiveIndex::Grow() - { - TArray oldKeys = Move(keys); - TArray oldNodes = Move(nodes); - const i32 newCap = oldKeys.IsEmpty() ? 64 : oldKeys.Size() * 2; - - keys = TArray{*arena}; - nodes = TArray{*arena}; - keys.AddUninitialized(newCap); - nodes.AddUninitialized(newCap); - for (i32 i = 0; i < newCap; ++i) - { - keys[i] = 0; - nodes[i] = Empty; - } - mask = u64(newCap - 1); - count = 0; - tombCount = 0; - - for (i32 i = 0; i < oldKeys.Size(); ++i) - { - if (oldNodes[i] >= 0) - { - // Insert without grow or duplicates - const u64 hash = oldKeys[i]; - u64 idx = hash & mask; - while (nodes[idx] != Empty) - { - idx = (idx + 1) & mask; - } - keys[idx] = hash; - nodes[idx] = oldNodes[i]; - ++count; - } - } - } - - i32* MemoryStats::LiveIndex::Find(u64 hash) - { - if (count + tombCount <= 0) - { - return nullptr; - } - u64 idx = hash & mask; - while (true) - { - const i32 node = nodes[idx]; - if (node == Empty) - { - return nullptr; - } - if (node != Tombstone && keys[idx] == hash) - { - return &nodes[idx]; - } - idx = (idx + 1) & mask; - } - } - - i32* MemoryStats::LiveIndex::FindOrInsert(u64 hash, i32 node) - { - // Grow up front when the table is empty or this insert would exceed - // load. A probe on an empty table would read out of bounds, and - // growing after a probe would invalidate its result. The rare cost - // is growing on a find-hit when load is already at the limit. - if ((count + tombCount + 1) * 4 > i64(keys.Size()) * 3) - { - Grow(); - } - - constexpr u64 noTomb = ~u64{0}; - u64 tombstone = noTomb; - u64 idx = hash & mask; - while (true) - { - const i32 n = nodes[idx]; - if (n == Empty) - { - break; - } - if (n == Tombstone) - { - if (tombstone == noTomb) - { - tombstone = idx; - } - } - else if (keys[idx] == hash) - { - return &nodes[idx]; - } - idx = (idx + 1) & mask; - } - - if (tombstone != noTomb) - { - idx = tombstone; - --tombCount; - } - keys[idx] = hash; - nodes[idx] = node; - ++count; - return &nodes[idx]; - } - - void MemoryStats::LiveIndex::EraseAt(i32* node) - { - *node = Tombstone; - --count; - ++tombCount; - } - - void MemoryStats::LiveIndex::Clear() - { - keys.Clear(); - nodes.Clear(); - mask = 0; - count = 0; - tombCount = 0; - } - - // --------------------------------------------------------------------------- // MemoryStats // --------------------------------------------------------------------------- @@ -196,7 +70,10 @@ namespace p } // namespace MemoryStats::MemoryStats() - : live{GetStatsArena()}, liveIdx{GetStatsArena()}, pending{GetStatsArena()} + : liveAllocations{GetStatsArena()} + , errors{GetStatsArena()} + , pending{GetStatsArena()} + , pendingErrors{GetStatsArena()} {} MemoryStats::~MemoryStats() @@ -269,8 +146,8 @@ namespace p CollectStats(); used = 0; totalAllocated = 0; - live.Clear(); - liveIdx.Clear(); + liveAllocations.Clear(); + errors.Clear(); } void MemoryStats::CollectStats() const @@ -278,69 +155,93 @@ namespace p // Phase 1: drain the shared event queue into a scratch buffer, // keeping the lock hold time to just the memcpy. pending.Clear(); - { + EventChunk* chunk; + { // Guard detaches the chunk list; producers can no longer reach it. ScopedLock guard(lock); - - EventChunk* chunk = firstChunk; - while (chunk) + chunk = firstChunk; + firstChunk = nullptr; + lastChunk = nullptr; + if (chunk && !spareChunk) // Spare chunk still needs lock and draining { pending.Append(chunk->slots, chunk->size); + spareChunk = chunk; + chunk = chunk->next; + spareChunk->~EventChunk(); + } + } - EventChunk* const next = chunk->next; + // Drain safely events from all detached chunks + while (chunk) + { + pending.Append(chunk->slots, chunk->size); + + EventChunk* const next = chunk->next; + chunk->~EventChunk(); + p::Free(GetStatsArena(), chunk, 1); + chunk = next; + } + pendingErrors.Resize(pending.Size(), MemoryStatsErrorType::None); - chunk->~EventChunk(); - if (!spareChunk) + // Iterate events to track live allocations and errors + for (i32 i = 0; i < pending.Size(); ++i) + { + const MemoryStatsEvent& ev = pending[i]; + if (ev.IsFree()) + { + if (MemoryStatsEvent* liveEv = liveAllocations.Find(ev)) { - spareChunk = chunk; + if (liveEv->GetSize() != ev.GetSize()) + { + pendingErrors[i] = MemoryStatsErrorType::SizeMismatch; + } + else + { + liveAllocations.Remove(ev); + } } else { - p::Free(GetStatsArena(), chunk, 1); + pendingErrors[i] = MemoryStatsErrorType::UnknownFree; + } + } + else + { + if (liveAllocations.Contains(ev)) + { + pendingErrors[i] = MemoryStatsErrorType::UnfreedRealloc; + } + else + { + liveAllocations.Insert(ev); } - chunk = next; } - firstChunk = nullptr; - lastChunk = nullptr; } - // Phase 2: classify drained events outside the lock. Alloc events - // become entries in `live`; matched frees swap-remove them. - // Nothing else is retained, so memory stays O(live). - for (const MemoryStatsEvent& ev : pending) + // Record errors and remove events so that stats are calculated correctly.\ + // (Order of events is no longer needed) + for (i32 i = 0; i < pendingErrors.Size(); ++i) { - const u64 hash = GetHash(ev); - const sizet size = ev.GetSize(); + if (pendingErrors[i] != MemoryStatsErrorType::None) + { + errors.Add({pending[i], pendingErrors[i]}); + + pending.RemoveAtSwapUnsafe(i); + pendingErrors.RemoveAtSwapUnsafe(i); + --i; + } + } + + // Record stats + for (i32 i = 0; i < pending.Size(); ++i) + { + const MemoryStatsEvent& ev = pending[i]; + const sizet size = ev.GetSize(); if (ev.IsFree()) { - if (i32* nodePtr = liveIdx.Find(hash)) - { - // Swap-remove the matched alloc from the live list, - // then point its map slot at the moved-in element. - const i32 p = *nodePtr; - const i32 lastIdx = live.Size() - 1; - if (lastIdx != p) - { - live[p] = live[lastIdx]; - if (i32* movedSlot = liveIdx.Find(GetHash(live[p]))) - { - if (*movedSlot == lastIdx) - { - *movedSlot = p; - } - } - } - live.RemoveLast(1, Shrink::No); - liveIdx.EraseAt(nodePtr); - used -= size; - } - // Else a stray free: no matching live alloc, ignore. + used -= size; } else { - live.Add(ev); - const i32 idx = live.Size() - 1; - i32* const slot = liveIdx.FindOrInsert(hash, idx); - *slot = idx; used += size; totalAllocated += size; } @@ -349,7 +250,7 @@ namespace p void MemoryStats::CheckLeaks() const { - const i32 numLeaks = live.Size(); + const i32 numLeaks = liveAllocations.Size(); if (numLeaks <= 0) { return; @@ -360,7 +261,7 @@ namespace p const i32 shown = Min(64, numLeaks); i32 printed = 0; - for (const auto& ev : live) + for (const auto& ev : liveAllocations) { if (printed >= shown) { diff --git a/Tests/Memory/MemoryStats.spec.cpp b/Tests/Memory/MemoryStats.spec.cpp index 7a555173..27edffad 100644 --- a/Tests/Memory/MemoryStats.spec.cpp +++ b/Tests/Memory/MemoryStats.spec.cpp @@ -15,11 +15,12 @@ using namespace p; static i32 LiveCount(const MemoryStats& s) { - return s.live.Size(); + return s.liveAllocations.Size(); } -static MemoryStatsEvent LiveAt(const MemoryStats& s, i32 i) +static const MemoryStatsEvent* LiveFind(const MemoryStats& s, void* ptr) { - return s.live[i]; + // Lookup is keyed by pointer only; size is irrelevant for matching. + return s.liveAllocations.Find(MemoryStatsEvent{ptr, 0}); } @@ -47,9 +48,9 @@ go_bandit([]() AssertThat(s.used, Is().EqualTo(64)); AssertThat(s.totalAllocated, Is().EqualTo(64)); AssertThat(LiveCount(s), Is().EqualTo(1)); - AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); - AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); - AssertThat(LiveAt(s, 0).IsFree(), Is().EqualTo(false)); + AssertThat(LiveFind(s, (void*)0x1000) != nullptr, Is().True()); + AssertThat(LiveFind(s, (void*)0x1000)->GetSize(), Is().EqualTo(64)); + AssertThat(LiveFind(s, (void*)0x1000)->IsFree(), Is().EqualTo(false)); }); it("Tracks add plus free", [&]() @@ -123,17 +124,21 @@ go_bandit([]() AssertThat(LiveCount(s), Is().EqualTo(0)); }); - it("Records duplicate allocs", [&]() + it("Records duplicate allocs as UnfreedRealloc", [&]() { MemoryStats s; s.detectLeaks = false; s.Add((void*)0x1000, 64); s.Add((void*)0x1000, 128); s.CollectStats(); - // Same ptr, different size: distinct keys, both survive. - AssertThat(LiveCount(s), Is().EqualTo(2)); - AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); - AssertThat(LiveAt(s, 1).GetSize(), Is().EqualTo(128)); + // Same ptr twice: the second alloc is an error and the + // live set is left untouched. + AssertThat(LiveCount(s), Is().EqualTo(1)); + AssertThat(LiveFind(s, (void*)0x1000)->GetSize(), Is().EqualTo(64)); + AssertThat(s.errors.Size(), Is().EqualTo(1)); + AssertThat(s.errors[0].kind == MemoryStatsErrorType::UnfreedRealloc, Is().True()); + AssertThat(s.errors[0].event.GetSize(), Is().EqualTo(128)); + AssertThat(s.used, Is().EqualTo(64)); }); it("CheckLeaks always runs when called directly", [&]() @@ -182,10 +187,8 @@ go_bandit([]() s.CollectStats(); AssertThat(LiveCount(s), Is().EqualTo(2)); - AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); - AssertThat(LiveAt(s, 0).GetSize(), Is().EqualTo(64)); - AssertThat(LiveAt(s, 1).GetPtr(), Is().EqualTo((u8*)0x2000)); - AssertThat(LiveAt(s, 1).GetSize(), Is().EqualTo(32)); + AssertThat(LiveFind(s, (void*)0x1000)->GetSize(), Is().EqualTo(64)); + AssertThat(LiveFind(s, (void*)0x2000)->GetSize(), Is().EqualTo(32)); // Re-collecting must preserve the live list identically. s.CollectStats(); @@ -230,27 +233,60 @@ go_bandit([]() AssertThat(LiveCount(s), Is().EqualTo(1)); }); - it("Frees across collects unmark live allocs (LIFO, duplicate keys)", [&]() + it("Duplicate allocs record UnfreedRealloc and live stays usable", [&]() { MemoryStats s; s.detectLeaks = false; - // Collect 1: two allocs sharing a key (same ptr and size). + // Collect 1: two allocs sharing the same ptr. The second is + // an UnfreedRealloc error; the live set keeps only the first. s.Add((void*)0x1000, 64); s.Add((void*)0x1000, 64); s.CollectStats(); - AssertThat(LiveCount(s), Is().EqualTo(2)); + AssertThat(LiveCount(s), Is().EqualTo(1)); + AssertThat(s.errors.Size(), Is().EqualTo(1)); + AssertThat(s.errors[0].kind == MemoryStatsErrorType::UnfreedRealloc, Is().True()); + AssertThat(s.used, Is().EqualTo(64)); - // Collect 2: one free must unmark the latest alloc (LIFO). + // Collect 2: freeing the original alloc still works. s.Remove((void*)0x1000, 64); s.CollectStats(); + AssertThat(LiveCount(s), Is().EqualTo(0)); + AssertThat(s.used, Is().EqualTo(0)); + }); + + it("Free with wrong size records SizeMismatch", [&]() + { + MemoryStats s; + s.detectLeaks = false; + s.Add((void*)0x1000, 64); + s.Remove((void*)0x1000, 32); // size mismatch + s.CollectStats(); AssertThat(LiveCount(s), Is().EqualTo(1)); - AssertThat(LiveAt(s, 0).GetPtr(), Is().EqualTo((u8*)0x1000)); + AssertThat(s.errors.Size(), Is().EqualTo(1)); + AssertThat(s.errors[0].kind == MemoryStatsErrorType::SizeMismatch, Is().True()); + AssertThat(s.errors[0].event.GetSize(), Is().EqualTo(32)); + AssertThat(s.used, Is().EqualTo(64)); - // Collect 3: second free pops the remaining alloc. + // Correcting the size frees the alloc normally. s.Remove((void*)0x1000, 64); s.CollectStats(); AssertThat(LiveCount(s), Is().EqualTo(0)); + AssertThat(s.used, Is().EqualTo(0)); + }); + + it("Free of unknown ptr records UnknownFree", [&]() + { + MemoryStats s; + s.detectLeaks = false; + s.Add((void*)0x1000, 64); + s.Remove((void*)0xDEAD, 64); + s.CollectStats(); + AssertThat(LiveCount(s), Is().EqualTo(1)); + AssertThat(s.errors.Size(), Is().EqualTo(1)); + AssertThat(s.errors[0].kind == MemoryStatsErrorType::UnknownFree, Is().True()); + AssertThat(s.errors[0].event.GetPtr(), Is().EqualTo((u8*)0xDEAD)); + AssertThat(s.used, Is().EqualTo(64)); }); it("Ignores null ptr in Remove", [&]() From 26479d0305449f98b3b2524693ee8feefdec0d2e Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 02:00:06 +0200 Subject: [PATCH 10/15] Tags using spinlock --- Src/Core/Tag.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Src/Core/Tag.cpp b/Src/Core/Tag.cpp index 74131ca0..7237aa11 100644 --- a/Src/Core/Tag.cpp +++ b/Src/Core/Tag.cpp @@ -1,12 +1,10 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #include "Pipe/Core/Tag.h" +#include "Pipe/Core/SpinLock.h" #include "PipeMemoryArenas.h" -#include -#include - namespace p { @@ -53,7 +51,7 @@ namespace p static TagStringTable table{}; // Makes sure the hashes & keys lists are thread-safe - std::shared_mutex stringsListMutex; + SharedSpinLock stringsListMutex; Tag::Tag(StringView value) @@ -142,7 +140,7 @@ namespace p i32 Tag::FlushInactiveTags() { - std::unique_lock lock{stringsListMutex}; + ExclusiveScopedLock lock{stringsListMutex}; const i32 initialSize = table.strings.Size(); for (i32 i = initialSize - 1; i >= 0; --i) { @@ -197,7 +195,7 @@ namespace p { i32 index; { - std::shared_lock lock{stringsListMutex}; + SharedScopedLock lock{stringsListMutex}; index = strings.LowerBound(hash); if (index != NO_INDEX) { @@ -221,7 +219,7 @@ namespace p header->hash = hash; // Copy string data auto* const data = const_cast(header->Data()); - p::CopyMem(data, (void*)value.data(), sizeof(char) * size); + p::CopyMem(data, value.data(), sizeof(char) * size); data[header->size] = '\0'; std::unique_lock lock{stringsListMutex}; @@ -231,7 +229,7 @@ namespace p void TagStringTable::FreeTagString(TagHeader& str) { - std::unique_lock lock{stringsListMutex}; + ExclusiveScopedLock lock{stringsListMutex}; strings.RemoveSorted(str.hash, {}, Shrink::No); arena.Free(&str, GetAllocSize(str.size)); } From 3be74844ba61b972c93a571a6e9088fd46229150 Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 02:00:21 +0200 Subject: [PATCH 11/15] Fixed BestFitArena tests --- Tests/Memory/BestFitArena.spec.cpp | 12 +++++++++--- Tests/Memory/BigBestFitArena.spec.cpp | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Tests/Memory/BestFitArena.spec.cpp b/Tests/Memory/BestFitArena.spec.cpp index 31b6d83a..4f93ef4f 100644 --- a/Tests/Memory/BestFitArena.spec.cpp +++ b/Tests/Memory/BestFitArena.spec.cpp @@ -278,15 +278,21 @@ go_bandit([]() AssertThat(p2, Is().Not().Null()); AssertThat(arena.GetFreeSize(), Equals(112)); - AssertThat(arena.GetFreeSlots().Size(), Equals(2)); + // Alignment is absolute, so the gap between p and p2 is zero + // when the block base lands on a matching 64B boundary. + const bool hasGap = p2 > (u8*)p + 8; + AssertThat(arena.GetFreeSlots().Size(), Equals(hasGap ? 2 : 1)); // Slot contains the rest if the block AssertThat(arena.GetFreeSlots()[0].start, Equals((u8*)p2 + 8)); AssertThat(arena.GetFreeSlots()[0].End(), Equals(arena.GetBlock().End())); // Slot contains the alignment gap - AssertThat(arena.GetFreeSlots()[1].start, Equals((u8*)p + 8)); - AssertThat(arena.GetFreeSlots()[1].End(), Equals(p2)); + if (hasGap) + { + AssertThat(arena.GetFreeSlots()[1].start, Equals((u8*)p + 8)); + AssertThat(arena.GetFreeSlots()[1].End(), Equals(p2)); + } }); }); }); diff --git a/Tests/Memory/BigBestFitArena.spec.cpp b/Tests/Memory/BigBestFitArena.spec.cpp index 351904b1..26abafc6 100644 --- a/Tests/Memory/BigBestFitArena.spec.cpp +++ b/Tests/Memory/BigBestFitArena.spec.cpp @@ -288,7 +288,11 @@ go_bandit([]() new (p2) TypeOfSize<8>(); AssertThat(p2, Is().Not().Null()); AssertThat(arena.GetFreeSize(), Equals(96)); - AssertThat(arena.GetFreeSlots().Size(), Equals(2)); + + // Alignment is absolute, so the gap between p and p2 is zero + // when the block base lands on a matching 64B boundary. + const bool hasGap = arena.GetAllocationStart(p2) > arena.GetAllocationEnd(p); + AssertThat(arena.GetFreeSlots().Size(), Equals(hasGap ? 2 : 1)); // Slot contains the rest if the block auto slot0 = arena.GetFreeSlots()[0]; @@ -298,10 +302,13 @@ go_bandit([]() slot0Start + slot0.size, Equals(static_cast(arena.GetBlock().End()))); // Slot contains the alignment gap - auto slot1 = arena.GetFreeSlots()[1]; - u8* slot1Start = (u8*)arena.GetBlock().data + slot1.offset; - AssertThat(slot1Start, Equals(arena.GetAllocationEnd(p))); - AssertThat(slot1Start + slot1.size, Equals(arena.GetAllocationStart(p2))); + if (hasGap) + { + auto slot1 = arena.GetFreeSlots()[1]; + u8* slot1Start = (u8*)arena.GetBlock().data + slot1.offset; + AssertThat(slot1Start, Equals(arena.GetAllocationEnd(p))); + AssertThat(slot1Start + slot1.size, Equals(arena.GetAllocationStart(p2))); + } }); }); }); From 7d5e3c088cdde177c718db70e7653fa38931df84 Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 02:02:59 +0200 Subject: [PATCH 12/15] Small misspelling error --- Include/Misc/PipeDebug.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/Misc/PipeDebug.h b/Include/Misc/PipeDebug.h index 2152d342..a6cb9836 100644 --- a/Include/Misc/PipeDebug.h +++ b/Include/Misc/PipeDebug.h @@ -3783,8 +3783,8 @@ namespace p y0 = (y0 > addressY0) ? y0 : addressY0; y1 = (y1 < addressY1) ? y1 : addressY1; - const float parentEdge = left ? ArenaColumnX(pi) + colW - parentPad - : ArenaColumnX(pi) + parentPad; + const float parentEdge = left ? ArenaColumnX(parentIdx) + colW - parentPad + : ArenaColumnX(parentIdx) + parentPad; const float childEdge = left ? colX : colRight; const float fillX0 = left ? parentEdge : childEdge; const float fillX1 = left ? childEdge : parentEdge; From a1e17a7e33aff2ee5bcf2bee079dc43665e93a3f Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 02:04:00 +0200 Subject: [PATCH 13/15] Missing lock change --- Src/Core/Tag.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/Core/Tag.cpp b/Src/Core/Tag.cpp index 7237aa11..42de9fee 100644 --- a/Src/Core/Tag.cpp +++ b/Src/Core/Tag.cpp @@ -1,8 +1,8 @@ // Copyright 2015-2026 Piperift. All Rights Reserved. #include "Pipe/Core/Tag.h" -#include "Pipe/Core/SpinLock.h" +#include "Pipe/Core/SpinLock.h" #include "PipeMemoryArenas.h" @@ -222,7 +222,7 @@ namespace p p::CopyMem(data, value.data(), sizeof(char) * size); data[header->size] = '\0'; - std::unique_lock lock{stringsListMutex}; + ExclusiveScopedLock lock{stringsListMutex}; strings.Insert(index, {hash, header}); return *header; } From eb74e2a6f53714796afe3c3ed1166cebbcfa7e9b Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 02:50:30 +0200 Subject: [PATCH 14/15] Fixed segfault Caused by initializing nested TArray with {arena} in InitReflection --- Include/PipeContainers.h | 2 +- Src/PipeECS.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Include/PipeContainers.h b/Include/PipeContainers.h index 6ff71cf5..4b68b92c 100644 --- a/Include/PipeContainers.h +++ b/Include/PipeContainers.h @@ -810,7 +810,7 @@ namespace p Assign(first, std::distance(first, last)); } - constexpr TArray(Arena& arena) : arena{&arena} {} + explicit constexpr TArray(Arena& arena) : arena{&arena} {} constexpr TArray(Arena& arena, i32 initialSize) : arena{&arena} { Assign(initialSize); diff --git a/Src/PipeECS.cpp b/Src/PipeECS.cpp index e6d99746..607c2bb4 100644 --- a/Src/PipeECS.cpp +++ b/Src/PipeECS.cpp @@ -444,8 +444,8 @@ namespace p ComponentPool& ComponentPool::operator=(const ComponentPool& other) noexcept { typeId = other.typeId; - idIndices = {*other.arena}; - idList = {*other.arena}; + idIndices = TPageBuffer{*other.arena}; + idList = TArray{*other.arena}; arena = other.arena; removePolicy = other.removePolicy; typeId = other.typeId; From 5740d02f6fc53864198690b32d04def48b2ef1a2 Mon Sep 17 00:00:00 2001 From: muit Date: Thu, 3 Sep 2026 03:03:10 +0200 Subject: [PATCH 15/15] Fixed segfault on windows release (clang-21) --- Src/PipeMemory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/PipeMemory.cpp b/Src/PipeMemory.cpp index 17308cd6..50d3ac71 100644 --- a/Src/PipeMemory.cpp +++ b/Src/PipeMemory.cpp @@ -120,7 +120,7 @@ namespace p void* HeapAlloc(sizet size) { #if P_PLATFORM_WINDOWS - return _aligned_malloc(size, alignof(std::max_align_t)); + return _aligned_malloc(size, __STDCPP_DEFAULT_NEW_ALIGNMENT__); #else return malloc(size); #endif @@ -138,7 +138,7 @@ namespace p void* HeapRealloc(void* ptr, sizet size) { #if P_PLATFORM_WINDOWS - return _aligned_realloc(ptr, size, alignof(std::max_align_t)); + return _aligned_realloc(ptr, size, __STDCPP_DEFAULT_NEW_ALIGNMENT__); #else return realloc(ptr, size); #endif