TimeCheck: Use FixedString

Avoids unnecessary malloc when constructing strings.

Test: simpleperf using Oboetester
Test: atest mediautils_fixedstring_tests
Bug: 238654698
Merged-In: I6b5239a0599de0cd0a60c55d9120e5bfaab7e6b9
Change-Id: I6b5239a0599de0cd0a60c55d9120e5bfaab7e6b9
(cherry picked from commit 35f9615b0299672a51e02884847dfa0800bfc9f4)
diff --git a/media/utils/TimeCheck.cpp b/media/utils/TimeCheck.cpp
index 0848eb3..e53d70f 100644
--- a/media/utils/TimeCheck.cpp
+++ b/media/utils/TimeCheck.cpp
@@ -21,6 +21,7 @@
 #include <android-base/logging.h>
 #include <audio_utils/clock.h>
 #include <mediautils/EventLog.h>
+#include <mediautils/FixedString.h>
 #include <mediautils/MethodStatistics.h>
 #include <mediautils/TimeCheck.h>
 #include <utils/Log.h>
@@ -138,11 +139,11 @@
     return getTimeCheckThread().toString();
 }
 
-TimeCheck::TimeCheck(std::string tag, OnTimerFunc&& onTimer, uint32_t timeoutMs,
+TimeCheck::TimeCheck(std::string_view tag, OnTimerFunc&& onTimer, uint32_t timeoutMs,
         bool crashOnTimeout)
-    : mTimeCheckHandler(new TimeCheckHandler{
-            std::move(tag), std::move(onTimer), crashOnTimeout,
-            std::chrono::system_clock::now(), gettid()})
+    : mTimeCheckHandler{ std::make_shared<TimeCheckHandler>(
+            tag, std::move(onTimer), crashOnTimeout,
+            std::chrono::system_clock::now(), gettid()) }
     , mTimerHandle(timeoutMs == 0
               ? getTimeCheckThread().trackTask(mTimeCheckHandler->tag)
               : getTimeCheckThread().scheduleTask(
@@ -231,14 +232,14 @@
             mediautils::getStatisticsForClass(className);
     if (!statistics) return {}; // empty TimeCheck.
     return mediautils::TimeCheck(
-            std::string(className).append("::").append(methodName),
-            [ clazz = std::string(className), method = std::string(methodName),
+            FixedString62(className).append("::").append(methodName),
+            [ safeMethodName = FixedString30(methodName),
               stats = std::move(statistics) ]
             (bool timeout, float elapsedMs) {
                     if (timeout) {
                         ; // ignored, there is no timeout value.
                     } else {
-                        stats->event(method, elapsedMs);
+                        stats->event(safeMethodName.asStringView(), elapsedMs);
                     }
             }, 0 /* timeoutMs */);
 }
diff --git a/media/utils/TimerThread.cpp b/media/utils/TimerThread.cpp
index 6de6b13..09783ed 100644
--- a/media/utils/TimerThread.cpp
+++ b/media/utils/TimerThread.cpp
@@ -31,17 +31,15 @@
 extern std::string_view timeSuffix(std::string_view time1, std::string_view time2);
 
 TimerThread::Handle TimerThread::scheduleTask(
-        std::string tag, std::function<void()>&& func, std::chrono::milliseconds timeout) {
+        std::string_view tag, std::function<void()>&& func, std::chrono::milliseconds timeout) {
     const auto now = std::chrono::system_clock::now();
-    std::shared_ptr<const Request> request{
-            new Request{ now, now + timeout, gettid(), std::move(tag) }};
+    auto request = std::make_shared<const Request>(now, now + timeout, gettid(), tag);
     return mMonitorThread.add(std::move(request), std::move(func), timeout);
 }
 
-TimerThread::Handle TimerThread::trackTask(std::string tag) {
+TimerThread::Handle TimerThread::trackTask(std::string_view tag) {
     const auto now = std::chrono::system_clock::now();
-    std::shared_ptr<const Request> request{
-            new Request{ now, now, gettid(), std::move(tag) }};
+    auto request = std::make_shared<const Request>(now, now, gettid(), tag);
     return mNoTimeoutMap.add(std::move(request));
 }
 
@@ -106,10 +104,10 @@
 //
 /* static */
 bool TimerThread::isRequestFromHal(const std::shared_ptr<const Request>& request) {
-    const size_t hidlPos = request->tag.find("Hidl");
+    const size_t hidlPos = request->tag.asStringView().find("Hidl");
     if (hidlPos == std::string::npos) return false;
     // should be a separator afterwards Hidl which indicates the string was in the class.
-    const size_t separatorPos = request->tag.find("::", hidlPos);
+    const size_t separatorPos = request->tag.asStringView().find("::", hidlPos);
     return separatorPos != std::string::npos;
 }
 
diff --git a/media/utils/include/mediautils/FixedString.h b/media/utils/include/mediautils/FixedString.h
new file mode 100644
index 0000000..047aa82
--- /dev/null
+++ b/media/utils/include/mediautils/FixedString.h
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <string>
+#include <string_view>
+
+namespace android::mediautils {
+
+/*
+ * FixedString is a stack allocatable string buffer that supports
+ * simple appending of other strings and string_views.
+ *
+ * It is designed for no-malloc operation when std::string
+ * small buffer optimization is insufficient.
+ *
+ * To keep code small, use asStringView() for operations on this.
+ *
+ * Notes:
+ * 1) Appending beyond the internal buffer size results in truncation.
+ *
+ * Alternatives:
+ * 1) If you want a sharable copy-on-write string implementation,
+ *    consider using the legacy android::String8().
+ * 2) Using std::string with a fixed stack allocator may suit your needs,
+ *    but exception avoidance is tricky.
+ * 3) Using C++20 ranges https://en.cppreference.com/w/cpp/ranges if you don't
+ *    need backing store.  Be careful about allocation with ranges.
+ *
+ * Good small sizes are multiples of 16 minus 2, e.g. 14, 30, 46, 62.
+ *
+ * Implementation Notes:
+ * 1) No iterators or [] for FixedString - please convert to std::string_view.
+ * 2) For small N (e.g. less than 62), consider a change to always zero fill and
+ *    potentially prevent always zero terminating (if one only does append).
+ *
+ * Possible arguments to create/append:
+ * 1) A FixedString.
+ * 2) A single char.
+ * 3) A char * pointer.
+ * 4) A std::string.
+ * 5) A std::string_view (or something convertible to it).
+ *
+ * Example:
+ *
+ * FixedString s1(std::string("a"));    // ctor
+ * s1 << "bc" << 'd' << '\n';           // streaming append
+ * s1 += "hello";                       // += append
+ * ASSERT_EQ(s1, "abcd\nhello");
+ */
+template <uint32_t N>
+struct FixedString
+{
+    // Find the best size type.
+    using strsize_t = std::conditional_t<(N > 255), uint32_t, uint8_t>;
+
+    // constructors
+    FixedString() { // override default
+        buffer_[0] = '\0';
+    }
+
+    FixedString(const FixedString& other) { // override default.
+        copyFrom<N>(other);
+    }
+
+    // The following constructor is not explicit to allow
+    // FixedString<8> s = "abcd";
+    template <typename ...Types>
+    FixedString(Types&&... args) {
+        append(std::forward<Types>(args)...);
+    }
+
+    // copy assign (copyFrom checks for equality and returns *this).
+    FixedString& operator=(const FixedString& other) { // override default.
+        return copyFrom<N>(other);
+    }
+
+    template <typename ...Types>
+    FixedString& operator=(Types&&... args) {
+        size_ = 0;
+        return append(std::forward<Types>(args)...);
+    }
+
+    // operator equals
+    bool operator==(const char *s) const {
+        return strncmp(c_str(), s, capacity() + 1) == 0;
+    }
+
+    bool operator==(std::string_view s) const {
+        return size() == s.size() && memcmp(data(), s.data(), size()) == 0;
+    }
+
+    // operator not-equals
+    template <typename T>
+    bool operator!=(const T& other) const {
+        return !operator==(other);
+    }
+
+    // operator +=
+    template <typename ...Types>
+    FixedString& operator+=(Types&&... args) {
+        return append(std::forward<Types>(args)...);
+    }
+
+    // conversion to std::string_view.
+    operator std::string_view() const {
+        return asStringView();
+    }
+
+    // basic observers
+    size_t buffer_offset() const { return offsetof(std::decay_t<decltype(*this)>, buffer_); }
+    static constexpr uint32_t capacity() { return N; }
+    uint32_t size() const { return size_; }
+    uint32_t remaining() const { return size_ >= N ? 0 : N - size_; }
+    bool empty() const { return size_ == 0; }
+    bool full() const { return size_ == N; }  // when full, possible truncation risk.
+    char * data() { return buffer_; }
+    const char * data() const { return buffer_; }
+    const char * c_str() const { return buffer_; }
+
+    inline std::string_view asStringView() const {
+        return { buffer_, static_cast<size_t>(size_) };
+    }
+    inline std::string asString() const {
+        return { buffer_, static_cast<size_t>(size_) };
+    }
+
+    void clear() { size_ = 0; buffer_[0] = 0; }
+
+    // Implementation of append - using templates
+    // to guarantee precedence in the presence of ambiguity.
+    //
+    // Consider C++20 template overloading through constraints and concepts.
+    template <typename T>
+    FixedString& append(const T& t) {
+        using decayT = std::decay_t<T>;
+        if constexpr (is_specialization_v<decayT, FixedString>) {
+            // A FixedString<U>
+            if (size_ == 0) {
+                // optimization to erase everything.
+                return copyFrom(t);
+            } else {
+                return appendStringView({t.data(), t.size()});
+            }
+        } else if constexpr(std::is_same_v<decayT, char>) {
+            if (size_ < N) {
+                buffer_[size_++] = t;
+                buffer_[size_] = '\0';
+            }
+            return *this;
+        } else if constexpr(std::is_same_v<decayT, char *>) {
+            // Some char* ptr.
+            return appendString(t);
+        } else if constexpr (std::is_convertible_v<decayT, std::string_view>) {
+            // std::string_view, std::string, or some other convertible type.
+            return appendStringView(t);
+        } else /* constexpr */ {
+            static_assert(dependent_false_v<T>, "non-matching append type");
+        }
+    }
+
+    FixedString& appendStringView(std::string_view s) {
+        uint32_t total = std::min(static_cast<size_t>(N - size_), s.size());
+        memcpy(buffer_ + size_, s.data(), total);
+        size_ += total;
+        buffer_[size_] = '\0';
+        return *this;
+    }
+
+    FixedString& appendString(const char *s) {
+        // strncpy zero pads to the end,
+        // strlcpy returns total expected length,
+        // we don't have strncpy_s in Bionic,
+        // so we write our own here.
+        while (size_ < N && *s != '\0') {
+            buffer_[size_++] = *s++;
+        }
+        buffer_[size_] = '\0';
+        return *this;
+    }
+
+    // Copy initialize the struct.
+    // Note: We are POD but customize the copying for acceleration
+    // of moving small strings embedded in a large buffers.
+    template <uint32_t U>
+    FixedString& copyFrom(const FixedString<U>& other) {
+        if ((void*)this != (void*)&other) { // not a self-assignment
+            if (other.size() == 0) {
+                size_ = 0;
+                buffer_[0] = '\0';
+                return *this;
+            }
+            constexpr size_t kSizeToCopyWhole = 64;
+            if constexpr (N == U &&
+                    sizeof(*this) == sizeof(other) &&
+                    sizeof(*this) <= kSizeToCopyWhole) {
+                // As we have the same str size type, we can just
+                // memcpy with fixed size, which can be easily optimized.
+                memcpy(static_cast<void*>(this), static_cast<const void*>(&other), sizeof(*this));
+                return *this;
+            }
+            if constexpr (std::is_same_v<strsize_t, typename FixedString<U>::strsize_t>) {
+                constexpr size_t kAlign = 8;  // align to a multiple of 8.
+                static_assert((kAlign & (kAlign - 1)) == 0); // power of 2.
+                // we check any alignment issues.
+                if (buffer_offset() == other.buffer_offset() && other.size() <= capacity()) {
+                    // improve on standard POD copying by reducing size.
+                    const size_t mincpy = buffer_offset() + other.size() + 1 /* nul */;
+                    const size_t maxcpy = std::min(sizeof(*this), sizeof(other));
+                    const size_t cpysize = std::min(mincpy + kAlign - 1 & ~(kAlign - 1), maxcpy);
+                    memcpy(static_cast<void*>(this), static_cast<const void*>(&other), cpysize);
+                    return *this;
+                }
+            }
+            size_ = std::min(other.size(), capacity());
+            memcpy(buffer_, other.data(), size_);
+            buffer_[size_] = '\0';  // zero terminate.
+        }
+        return *this;
+    }
+
+private:
+    //  Template helper methods
+
+    template <typename Test, template <uint32_t> class Ref>
+    struct is_specialization : std::false_type {};
+
+    template <template <uint32_t> class Ref, uint32_t UU>
+    struct is_specialization<Ref<UU>, Ref>: std::true_type {};
+
+    template <typename Test, template <uint32_t> class Ref>
+    static inline constexpr bool is_specialization_v = is_specialization<Test, Ref>::value;
+
+    // For static assert(false) we need a template version to avoid early failure.
+    template <typename T>
+    static inline constexpr bool dependent_false_v = false;
+
+    // POD variables
+    strsize_t size_ = 0;
+    char buffer_[N + 1 /* allow zero termination */];
+};
+
+// Stream operator syntactic sugar.
+// Example:
+// s << 'b' << "c" << "d" << '\n';
+template <uint32_t N, typename ...Types>
+FixedString<N>& operator<<(FixedString<N>& fs, Types&&... args) {
+    return fs.append(std::forward<Types>(args)...);
+}
+
+// We do not use a default size for fixed string as changing
+// the default size would lead to different behavior - we want the
+// size to be explicitly known.
+
+// FixedString62 of 62 chars fits in one typical cache line.
+using FixedString62 = FixedString<62>;
+
+// Slightly smaller
+using FixedString30 = FixedString<30>;
+
+// Since we have added copy and assignment optimizations,
+// we are no longer trivially assignable and copyable.
+// But we check standard layout here to prevent inclusion of unacceptable members or virtuals.
+static_assert(std::is_standard_layout_v<FixedString62>);
+static_assert(std::is_standard_layout_v<FixedString30>);
+
+}  // namespace android::mediautils
diff --git a/media/utils/include/mediautils/TimeCheck.h b/media/utils/include/mediautils/TimeCheck.h
index ef03aef..038e7dd 100644
--- a/media/utils/include/mediautils/TimeCheck.h
+++ b/media/utils/include/mediautils/TimeCheck.h
@@ -60,7 +60,7 @@
      *                  the TimeCheck object is destroyed or leaves scope.
      * \param crashOnTimeout true if the object issues an abort on timeout.
      */
-    explicit TimeCheck(std::string tag, OnTimerFunc&& onTimer = {},
+    explicit TimeCheck(std::string_view tag, OnTimerFunc&& onTimer = {},
             uint32_t timeoutMs = kDefaultTimeOutMs, bool crashOnTimeout = true);
 
     TimeCheck() = default;
@@ -79,7 +79,17 @@
     // The usage here is const safe.
     class TimeCheckHandler {
     public:
-        const std::string tag;
+        template <typename S, typename F>
+        TimeCheckHandler(S&& _tag, F&& _onTimer, bool _crashOnTimeout,
+            const std::chrono::system_clock::time_point& _startTime,
+            pid_t _tid)
+            : tag(std::forward<S>(_tag))
+            , onTimer(std::forward<F>(_onTimer))
+            , crashOnTimeout(_crashOnTimeout)
+            , startTime(_startTime)
+            , tid(_tid)
+            {}
+        const FixedString62 tag;
         const OnTimerFunc onTimer;
         const bool crashOnTimeout;
         const std::chrono::system_clock::time_point startTime;
diff --git a/media/utils/include/mediautils/TimerThread.h b/media/utils/include/mediautils/TimerThread.h
index ffee602..349bffd 100644
--- a/media/utils/include/mediautils/TimerThread.h
+++ b/media/utils/include/mediautils/TimerThread.h
@@ -27,6 +27,8 @@
 
 #include <android-base/thread_annotations.h>
 
+#include <mediautils/FixedString.h>
+
 namespace android::mediautils {
 
 /**
@@ -54,7 +56,7 @@
      * \returns       a handle that can be used for cancellation.
      */
     Handle scheduleTask(
-            std::string tag, std::function<void()>&& func, std::chrono::milliseconds timeout);
+            std::string_view tag, std::function<void()>&& func, std::chrono::milliseconds timeout);
 
     /**
      * Tracks a task that shows up on toString() until cancelled.
@@ -62,7 +64,7 @@
      * \param tag     string associated with the task.
      * \returns       a handle that can be used for cancellation.
      */
-    Handle trackTask(std::string tag);
+    Handle trackTask(std::string_view tag);
 
     /**
      * Cancels a task previously scheduled with scheduleTask()
@@ -129,12 +131,22 @@
     // To minimize movement of data, we pass around shared_ptrs to Requests.
     // These are allocated and deallocated outside of the lock.
     struct Request {
+        Request(const std::chrono::system_clock::time_point& _scheduled,
+                const std::chrono::system_clock::time_point& _deadline,
+                pid_t _tid,
+                std::string_view _tag)
+            : scheduled(_scheduled)
+            , deadline(_deadline)
+            , tid(_tid)
+            , tag(_tag)
+            {}
+
         const std::chrono::system_clock::time_point scheduled;
         const std::chrono::system_clock::time_point deadline; // deadline := scheduled + timeout
                                                               // if deadline == scheduled, no
                                                               // timeout, task not executed.
         const pid_t tid;
-        const std::string tag;
+        const FixedString62 tag;
 
         std::string toString() const;
     };
diff --git a/media/utils/tests/Android.bp b/media/utils/tests/Android.bp
index 1024018..232cc4e 100644
--- a/media/utils/tests/Android.bp
+++ b/media/utils/tests/Android.bp
@@ -124,6 +124,27 @@
 }
 
 cc_test {
+    name: "mediautils_fixedstring_tests",
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wextra",
+    ],
+
+    shared_libs: [
+        "libaudioutils",
+        "liblog",
+        "libmediautils",
+        "libutils",
+    ],
+
+    srcs: [
+        "mediautils_fixedstring_tests.cpp",
+    ],
+}
+
+cc_test {
     name: "mediautils_scopedstatistics_tests",
 
     cflags: [
diff --git a/media/utils/tests/mediautils_fixedstring_tests.cpp b/media/utils/tests/mediautils_fixedstring_tests.cpp
new file mode 100644
index 0000000..7ab9a9f
--- /dev/null
+++ b/media/utils/tests/mediautils_fixedstring_tests.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "mediautils_fixedstring_tests"
+
+#include <mediautils/FixedString.h>
+
+#include <gtest/gtest.h>
+#include <utils/Log.h>
+
+using namespace android::mediautils;
+
+TEST(mediautils_fixedstring_tests, ctor) {
+    FixedString<8> s0("abcde");
+
+    ASSERT_FALSE(s0.empty());
+    ASSERT_EQ(8U, s0.capacity());
+
+    ASSERT_EQ(5U, s0.size());
+    ASSERT_EQ(3U, s0.remaining());
+    ASSERT_EQ(0, strcmp(s0.c_str(), "abcde"));
+
+    ASSERT_EQ(0, strcmp(s0.data(), "abcde"));
+
+    // overflow
+    FixedString<8> s1("abcdefghijk");
+    ASSERT_EQ(8U, s1.size());
+    ASSERT_TRUE(s1.full());
+    ASSERT_EQ(0U, s1.remaining());
+    ASSERT_EQ(0, strcmp(s1.c_str(), "abcdefgh"));
+
+    // overflow
+    FixedString<8> s2(std::string("abcdefghijk"));
+    ASSERT_TRUE(s2.full());
+
+    ASSERT_EQ(8U, s2.size());
+    ASSERT_EQ(0, strcmp(s2.c_str(), "abcdefgh"));
+
+    // complex
+    ASSERT_EQ(s1, s2);
+    ASSERT_EQ(FixedString<12>().append(s1), s2);
+    ASSERT_NE(s1, "bcd");
+
+    // string and stringview
+    ASSERT_EQ(s1.asString(), s1.asStringView());
+
+    FixedString30 s3;
+    s3 = std::string("abcd");
+    ASSERT_EQ(s3, "abcd");
+
+    s3.clear();
+    ASSERT_EQ(s3, "");
+    ASSERT_NE(s3, "abcd");
+    ASSERT_EQ(0U, s3.size());
+}
+
+TEST(mediautils_fixedstring_tests, append) {
+    FixedString<8> s0;
+    ASSERT_EQ(0U, s0.size());
+    ASSERT_EQ(0, strcmp(s0.c_str(), ""));
+    ASSERT_TRUE(s0.empty());
+    ASSERT_FALSE(s0.full());
+
+    s0.append("abc");
+    ASSERT_EQ(3U, s0.size());
+    ASSERT_EQ(0, strcmp(s0.c_str(), "abc"));
+
+    s0.append(std::string("d"));
+    ASSERT_EQ(4U, s0.size());
+    ASSERT_EQ(0, strcmp(s0.c_str(), "abcd"));
+
+    // overflow
+    s0.append("efghijk");
+    ASSERT_EQ(8U, s0.size());
+    ASSERT_EQ(0, strcmp(s0.c_str(), "abcdefgh"));
+    ASSERT_TRUE(s0.full());
+
+    // concatenated
+    ASSERT_EQ(FixedString62("abcd"),
+            FixedString<8>("ab").append("c").append(FixedString<8>("d")));
+    ASSERT_EQ(FixedString<12>("a").append(FixedString<12>("b")), "ab");
+}
+
+TEST(mediautils_fixedstring_tests, plus_equals) {
+    FixedString<8> s0;
+    ASSERT_EQ(0U, s0.size());
+    ASSERT_EQ(0, strcmp(s0.c_str(), ""));
+
+    s0 += "abc";
+    s0 += "def";
+    ASSERT_EQ(s0, "abcdef");
+}
+
+TEST(mediautils_fixedstring_tests, stream_operator) {
+    FixedString<8> s0('a');
+
+    s0 << 'b' << "c" << "d" << '\n';
+    ASSERT_EQ(s0, "abcd\n");
+}
+
+TEST(mediautils_fixedstring_tests, examples) {
+    FixedString30 s1(std::string("a"));
+    s1 << "bc" << 'd' << '\n';
+    s1 += "hello";
+
+    ASSERT_EQ(s1, "abcd\nhello");
+
+    FixedString30 s2;
+    for (const auto &c : s1.asStringView()) {
+        s2.append(c);
+    };
+    ASSERT_EQ(s1, s2);
+
+    FixedString30 s3(std::move(s1));
+}
+
+// Ensure type alias works fine as well.
+using FixedString1024 = FixedString<1024>;
+
+TEST(mediautils_fixedstring_tests, copy) {
+    FixedString1024 s0("abc");
+    FixedString62 s1(s0);
+
+    ASSERT_EQ(3U, s1.size());
+    ASSERT_EQ(0, strcmp(s1.c_str(), "abc"));
+    ASSERT_EQ(s0, s1);
+
+    FixedString<1024> s2(s1);
+    ASSERT_EQ(3U, s2.size());
+    ASSERT_EQ(0, strcmp(s2.c_str(), "abc"));
+    ASSERT_EQ(s2, "abc");
+    ASSERT_NE(s2, "def");
+    ASSERT_EQ(s2, std::string("abc"));
+    ASSERT_NE(s2, std::string("def"));
+    ASSERT_EQ(s1, s2);
+    ASSERT_EQ(s0, s2);
+    ASSERT_EQ(s2, FixedString62(FixedString1024("abc")));
+}