Pool LogMessageData instances in a 16-slot lock-free ObjectPool. Every active `LOG(...)` statement previously allocated and deallocated a ~30.5 KB `LogMessageData` buffer from the global heap (`std::make_unique<LogMessageData>`) and constructed and destroyed its `std::ostream` state whenever `OstreamView` was used. Introduce `absl::log_internal::ObjectPool<T, kCapacity>` in `log/internal/object_pool.h`, a cacheline-aligned, `ABSL_CONST_INIT`-compatible, trivially destructible lock-free object pool probed from a `thread_local` slot hint (or `GetCachedTID() % kCapacity` when `ABSL_HAVE_THREAD_LOCAL` is unavailable), falling back to `std::make_unique<T>` and `delete` when all `kCapacity` slots are concurrently in use. Use `ObjectPool<LogMessageData, 16>` (`LogMessageData::pool`) and `LogMessageData::Reset` to reuse `LogMessageData` instances across `LogMessage` lifetimes without touching the two 15 KB buffers (`encoded_buf` and `string_buf`), and track per-message `manipulated_used` state so a pooled `std::ostream` is constructed at most once per slot and reset (`clear`, `flags`, `precision`, `width`, `fill`) once per `LogMessage` when `OstreamView` is entered. PiperOrigin-RevId: 1004864407
diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake index 5b24d67..adf98dc 100644 --- a/CMake/AbseilDll.cmake +++ b/CMake/AbseilDll.cmake
@@ -195,6 +195,7 @@ "log/internal/nullguard.cc" "log/internal/nullguard.h" "log/internal/nullstream.h" + "log/internal/object_pool.h" "log/internal/proto.cc" "log/internal/proto.h" "log/internal/strip.h"
diff --git a/absl/log/CMakeLists.txt b/absl/log/CMakeLists.txt index 1da38e4..58cad49 100644 --- a/absl/log/CMakeLists.txt +++ b/absl/log/CMakeLists.txt
@@ -206,6 +206,7 @@ absl::log_internal_globals absl::log_internal_log_sink_set absl::log_internal_nullguard + absl::log_internal_object_pool absl::log_internal_proto absl::log_internal_structured_proto absl::log_severity @@ -286,6 +287,22 @@ absl_cc_library( NAME + log_internal_object_pool + HDRS + "internal/object_pool.h" + COPTS + ${ABSL_DEFAULT_COPTS} + LINKOPTS + ${ABSL_DEFAULT_LINKOPTS} + DEPS + absl::base + absl::config + absl::core_headers + absl::nullability +) + +absl_cc_library( + NAME log_internal_strip HDRS "internal/strip.h" @@ -1239,3 +1256,20 @@ absl::strings GTest::gmock_main ) + +absl_cc_test( + NAME + internal_object_pool_test + SRCS + "internal/object_pool_test.cc" + COPTS + ${ABSL_TEST_COPTS} + LINKOPTS + ${ABSL_DEFAULT_LINKOPTS} + DEPS + absl::config + absl::core_headers + absl::log_internal_object_pool + GTest::gmock_main +) +
diff --git a/absl/log/internal/BUILD.bazel b/absl/log/internal/BUILD.bazel index 42114ed..e1a64ba 100644 --- a/absl/log/internal/BUILD.bazel +++ b/absl/log/internal/BUILD.bazel
@@ -201,6 +201,7 @@ ":globals", ":log_sink_set", ":nullguard", + ":object_pool", ":proto", ":structured_proto", "//absl/base", @@ -293,6 +294,33 @@ ) cc_library( + name = "object_pool", + hdrs = ["object_pool.h"], + copts = ABSL_DEFAULT_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + "//absl/base", + "//absl/base:config", + "//absl/base:core_headers", + "//absl/base:nullability", + ], +) + +cc_test( + name = "object_pool_test", + srcs = ["object_pool_test.cc"], + copts = ABSL_TEST_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + ":object_pool", + "//absl/base:config", + "//absl/base:core_headers", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_library( name = "strip", hdrs = ["strip.h"], copts = ABSL_DEFAULT_COPTS,
diff --git a/absl/log/internal/log_message.cc b/absl/log/internal/log_message.cc index e56ef25..6e6e515 100644 --- a/absl/log/internal/log_message.cc +++ b/absl/log/internal/log_message.cc
@@ -25,9 +25,11 @@ #include <atomic> #include <ios> #include <memory> +#include <optional> #include <ostream> #include <string> #include <string_view> +#include <utility> #include "absl/base/attributes.h" #include "absl/base/config.h" @@ -44,6 +46,7 @@ #include "absl/log/internal/log_format.h" #include "absl/log/internal/log_sink_set.h" #include "absl/log/internal/nullguard.h" +#include "absl/log/internal/object_pool.h" #include "absl/log/internal/proto.h" #include "absl/log/internal/structured_proto.h" #include "absl/log/log_entry.h" @@ -148,11 +151,19 @@ } // namespace struct LogMessage::LogMessageData final { - LogMessageData(absl::string_view file, int line, - absl::LogSeverity severity, absl::Time timestamp); + static constexpr size_t kPoolCapacity = 16; + ABSL_CONST_INIT static ObjectPool<LogMessageData, kPoolCapacity> pool; + + LogMessageData(absl::string_view file, int line, absl::LogSeverity severity, + absl::Time timestamp) { + Reset(file, line, severity, timestamp); + } LogMessageData(const LogMessageData&) = delete; LogMessageData& operator=(const LogMessageData&) = delete; + void Reset(absl::string_view file, int line, absl::LogSeverity severity, + absl::Time timestamp); + // `LogEntry` sent to `LogSink`s; contains metadata. absl::LogEntry entry; @@ -169,7 +180,9 @@ // non-sink targets (e.g. stderr, log files). bool extra_sinks_only; - std::ostream manipulated; // ostream with IO manipulators applied + // Lazily initialized ostream with IO manipulators applied. + std::optional<std::ostream> manipulated; + bool manipulated_used; // A `logging.proto.Event` proto message is built into `encoded_buf`. std::array<char, kLogMessageBufferSize> encoded_buf; @@ -200,12 +213,21 @@ void FinalizeEncodingAndFormat(); }; -LogMessage::LogMessageData::LogMessageData(absl::string_view file, - int line, absl::LogSeverity severity, - absl::Time timestamp) - : extra_sinks_only(false), manipulated(nullptr) { - // Legacy defaults for LOG's ostream: - manipulated.setf(std::ios_base::showbase | std::ios_base::boolalpha); +ABSL_CONST_INIT ObjectPool<LogMessage::LogMessageData, + LogMessage::LogMessageData::kPoolCapacity> + LogMessage::LogMessageData::pool; + +void LogMessage::LogMessageData::Reset(absl::string_view file, int line, + absl::LogSeverity severity, + absl::Time timestamp) { + first_fatal = false; + fail_quietly = false; + is_perror = false; + extra_sinks.clear(); + extra_sinks.shrink_to_fit(); + extra_sinks_only = false; + manipulated_used = false; + encoded_remaining_actual_do_not_use_directly = absl::Span<char>(); entry.full_filename_ = file; entry.base_filename_ = Basename(file); entry.line_ = line; @@ -214,6 +236,13 @@ entry.verbose_level_ = absl::LogEntry::kNoVerbosityLevel; entry.timestamp_ = timestamp; entry.tid_ = absl::base_internal::GetCachedTID(); + entry.text_message_with_prefix_and_newline_and_nul_ = + absl::Span<const char>(); + entry.prefix_len_ = 0; + entry.encoding_ = absl::string_view(); + if (!entry.stacktrace_.empty()) { + entry.stacktrace_ = std::string(); + } } void LogMessage::LogMessageData::InitializeEncodingAndFormat() { @@ -278,12 +307,7 @@ : LogMessage(absl::string_view(file), line, severity) {} LogMessage::LogMessage(absl::string_view file, int line, absl::LogSeverity severity) - : data_( - std::make_unique<LogMessageData>(file, line, severity, absl::Now())) { - data_->first_fatal = false; - data_->is_perror = false; - data_->fail_quietly = false; - + : data_(LogMessageData::pool.Acquire(file, line, severity, absl::Now())) { // This logs a backtrace even if the location is subsequently changed using // AtLocation. This quirk, and the behavior when AtLocation is called twice, // are fixable but probably not worth fixing. @@ -298,7 +322,7 @@ : LogMessage(file, line, absl::LogSeverity::kError) {} // This cannot go in the header since LogMessageData is defined in this file. -LogMessage::~LogMessage() = default; +LogMessage::~LogMessage() { LogMessageData::pool.Release(std::move(data_)); } LogMessage& LogMessage::AtLocation(absl::string_view file, int line) { data_->entry.full_filename_ = file; @@ -437,14 +461,16 @@ return *this; } -LogMessage& LogMessage::operator<<(std::ostream& (*m)(std::ostream& os)) { +LogMessage& LogMessage::operator<<( + std::ostream& (*absl_nonnull m)(std::ostream& os)) { OstreamView view(*data_); - data_->manipulated << m; + view.stream() << m; return *this; } -LogMessage& LogMessage::operator<<(std::ios_base& (*m)(std::ios_base& os)) { +LogMessage& LogMessage::operator<<( + std::ios_base& (*absl_nonnull m)(std::ios_base& os)) { OstreamView view(*data_); - data_->manipulated << m; + view.stream() << m; return *this; } // NOLINTBEGIN(runtime/int) @@ -515,11 +541,23 @@ &encoded_remaining_copy_); setp(encoded_remaining_copy_.data(), encoded_remaining_copy_.data() + encoded_remaining_copy_.size()); - data_.manipulated.rdbuf(this); + if (!std::exchange(data_.manipulated_used, true)) { + if (!data_.manipulated.has_value()) { + data_.manipulated.emplace(nullptr); + } + data_.manipulated->clear(); + data_.manipulated->flags(std::ios_base::dec | std::ios_base::skipws | + std::ios_base::showbase | + std::ios_base::boolalpha); + data_.manipulated->precision(6); + data_.manipulated->width(0); + data_.manipulated->fill(' '); + } + data_.manipulated->rdbuf(this); } LogMessage::OstreamView::~OstreamView() { - data_.manipulated.rdbuf(nullptr); + data_.manipulated->rdbuf(nullptr); if (!string_start_.data()) { // The second field header didn't fit. Whether the first one did or not, we // shouldn't commit `encoded_remaining_copy_`, and we also need to zero the @@ -536,7 +574,7 @@ data_.encoded_remaining() = encoded_remaining_copy_; } -std::ostream& LogMessage::OstreamView::stream() { return data_.manipulated; } +std::ostream& LogMessage::OstreamView::stream() { return *data_.manipulated; } bool LogMessage::IsFatal() const { return data_->entry.log_severity() == absl::LogSeverity::kFatal &&
diff --git a/absl/log/internal/object_pool.h b/absl/log/internal/object_pool.h new file mode 100644 index 0000000..7ad2337 --- /dev/null +++ b/absl/log/internal/object_pool.h
@@ -0,0 +1,140 @@ +// Copyright 2026 The Abseil Authors. +// +// 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 +// +// https://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. +// +// ----------------------------------------------------------------------------- +// File: log/internal/object_pool.h +// ----------------------------------------------------------------------------- +// +// This file declares `class absl::log_internal::ObjectPool<T, kCapacity>`, a +// fixed-capacity, lock-free object pool for reusable objects with static +// storage duration. + +#ifndef ABSL_LOG_INTERNAL_OBJECT_POOL_H_ +#define ABSL_LOG_INTERNAL_OBJECT_POOL_H_ + +#include <atomic> +#include <cstddef> +#include <memory> +#include <utility> + +#include "absl/base/attributes.h" +#include "absl/base/config.h" +#include "absl/base/internal/sysinfo.h" // IWYU pragma: keep +#include "absl/base/nullability.h" +#include "absl/base/optimization.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace log_internal { + +// A fixed-capacity, lock-free object pool of up to `kCapacity` reusable `T` +// instances. +// +// `ObjectPool` is `constexpr`-constructible (supporting `ABSL_CONST_INIT` +// static storage duration) and trivially destructible; pooled instances are +// intentionally never destroyed at process exit so that usage during static +// destruction remains safe. +// +// `T` must be constructible from the arguments passed to `Acquire(args...)` and +// must provide a `void Reset(args...)` member function with a matching +// signature to reinitialize a previously pooled instance. +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable \ + : 4324) // structure was padded due to alignment specifier +#endif +template <typename T, size_t kCapacity> +class ObjectPool final { + static_assert(kCapacity > 0, "ObjectPool capacity must be positive."); + + public: + constexpr ObjectPool() = default; + ObjectPool(const ObjectPool&) = delete; + ObjectPool& operator=(const ObjectPool&) = delete; + + // Acquires a `T` instance initialized with `args...`. + // + // If a pool slot is available, either lazily allocates a new `T` in that + // slot or calls `ptr->Reset(args...)` on the existing instance. If all + // `kCapacity` slots are concurrently in use, allocates a temporary overflow + // `T` on the heap. + template <typename... Args> + absl_nonnull std::unique_ptr<T> Acquire(Args&&... args) { + const size_t start = GetSlotHint(); + for (size_t i = 0; i < kCapacity; ++i) { + const size_t idx = (start + i) % kCapacity; + Slot& slot = slots_[idx]; + if (!slot.in_use.load(std::memory_order_relaxed) && + !slot.in_use.exchange(true, std::memory_order_acquire)) { + SetSlotHint(idx); + T* ptr = slot.ptr.load(std::memory_order_relaxed); + if (ptr == nullptr) { + ptr = new T(std::forward<Args>(args)...); + slot.ptr.store(ptr, std::memory_order_relaxed); + } else { + ptr->Reset(std::forward<Args>(args)...); + } + return std::unique_ptr<T>(ptr); + } + } + return std::make_unique<T>(std::forward<Args>(args)...); + } + + // Returns `item` to the pool if it belongs to one of the pool's slots, or + // deletes it if it was an overflow allocation. + void Release(std::unique_ptr<T> item) { + if (item == nullptr) return; + const size_t start = GetSlotHint(); + for (size_t i = 0; i < kCapacity; ++i) { + Slot& slot = slots_[(start + i) % kCapacity]; + if (slot.ptr.load(std::memory_order_relaxed) == item.get()) { + item.release(); + slot.in_use.store(false, std::memory_order_release); + return; + } + } + } + + private: + struct alignas(ABSL_CACHELINE_SIZE) Slot { + constexpr Slot() : in_use(false), ptr(nullptr) {} + std::atomic<bool> in_use; + std::atomic<T*> ptr; + }; + +#ifdef ABSL_HAVE_THREAD_LOCAL + static size_t& SlotHint() { + ABSL_CONST_INIT thread_local size_t slot_hint = 0; + return slot_hint; + } + static size_t GetSlotHint() { return SlotHint(); } + static void SetSlotHint(size_t idx) { SlotHint() = idx; } +#else + static size_t GetSlotHint() { + return static_cast<size_t>(absl::base_internal::GetCachedTID()) % kCapacity; + } + static void SetSlotHint(size_t) {} +#endif + + Slot slots_[kCapacity]; +}; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +} // namespace log_internal +ABSL_NAMESPACE_END +} // namespace absl + +#endif // ABSL_LOG_INTERNAL_OBJECT_POOL_H_
diff --git a/absl/log/internal/object_pool_test.cc b/absl/log/internal/object_pool_test.cc new file mode 100644 index 0000000..ac23954 --- /dev/null +++ b/absl/log/internal/object_pool_test.cc
@@ -0,0 +1,177 @@ +// Copyright 2026 The Abseil Authors. +// +// 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 +// +// https://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. + +#include "absl/log/internal/object_pool.h" + +#include <atomic> +#include <cstddef> +#include <memory> +#include <string> +#include <thread> // NOLINT(build/c++11) +#include <type_traits> +#include <utility> +#include <vector> + +#include "gtest/gtest.h" +#include "absl/base/attributes.h" +#include "absl/base/config.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace log_internal { +namespace { + +struct TestItem { + struct Counters { + std::atomic<int> constructions{0}; + std::atomic<int> resets{0}; + std::atomic<int> destructions{0}; + }; + + TestItem(Counters* counters_arg, int value_arg, std::string label_arg) + : counters(counters_arg), value(value_arg), label(std::move(label_arg)) { + counters->constructions.fetch_add(1, std::memory_order_relaxed); + } + + TestItem(const TestItem&) = delete; + TestItem& operator=(const TestItem&) = delete; + + ~TestItem() { + counters->destructions.fetch_add(1, std::memory_order_relaxed); + } + + void Reset(Counters* counters_arg, int value_arg, std::string label_arg) { + counters = counters_arg; + value = value_arg; + label = std::move(label_arg); + counters->resets.fetch_add(1, std::memory_order_relaxed); + } + + Counters* counters; + int value; + std::string label; +}; + +static_assert(std::is_trivially_destructible_v<ObjectPool<TestItem, 4>>); + +ABSL_CONST_INIT ObjectPool<TestItem, 4> const_init_pool; + +TEST(ObjectPoolTest, AcquireAndReleaseReusesPooledObject) { + TestItem::Counters counters; + std::unique_ptr<TestItem> first = + const_init_pool.Acquire(&counters, 10, "first"); + ASSERT_NE(first, nullptr); + const TestItem* first_ptr = first.get(); + EXPECT_EQ(first->value, 10); + EXPECT_EQ(first->label, "first"); + EXPECT_EQ(counters.constructions.load(), 1); + EXPECT_EQ(counters.resets.load(), 0); + EXPECT_EQ(counters.destructions.load(), 0); + + const_init_pool.Release(std::move(first)); + EXPECT_EQ(counters.destructions.load(), 0); + + std::unique_ptr<TestItem> second = + const_init_pool.Acquire(&counters, 20, "second"); + ASSERT_NE(second, nullptr); + EXPECT_EQ(second.get(), first_ptr); + EXPECT_EQ(second->value, 20); + EXPECT_EQ(second->label, "second"); + EXPECT_EQ(counters.constructions.load(), 1); + EXPECT_EQ(counters.resets.load(), 1); + EXPECT_EQ(counters.destructions.load(), 0); + + const_init_pool.Release(std::move(second)); + EXPECT_EQ(counters.destructions.load(), 0); +} + +TEST(ObjectPoolTest, OverflowAllocatesAndDeletesWhenPoolExhausted) { + ABSL_CONST_INIT static ObjectPool<TestItem, 2> pool; + TestItem::Counters counters; + + std::unique_ptr<TestItem> first = pool.Acquire(&counters, 1, "one"); + std::unique_ptr<TestItem> second = pool.Acquire(&counters, 2, "two"); + std::unique_ptr<TestItem> overflow = pool.Acquire(&counters, 3, "three"); + + EXPECT_NE(first.get(), second.get()); + EXPECT_NE(first.get(), overflow.get()); + EXPECT_NE(second.get(), overflow.get()); + EXPECT_EQ(counters.constructions.load(), 3); + EXPECT_EQ(counters.destructions.load(), 0); + + // Releasing the overflow item must destroy it immediately because it does not + // belong to any of the pool's slots. + pool.Release(std::move(overflow)); + EXPECT_EQ(counters.destructions.load(), 1); + + const TestItem* first_ptr = first.get(); + const TestItem* second_ptr = second.get(); + pool.Release(std::move(first)); + pool.Release(std::move(second)); + EXPECT_EQ(counters.destructions.load(), 1); + + // Subsequent acquisition reuses one of the pooled items. + std::unique_ptr<TestItem> reused = pool.Acquire(&counters, 4, "four"); + EXPECT_TRUE(reused.get() == first_ptr || reused.get() == second_ptr); + EXPECT_EQ(reused->value, 4); + EXPECT_EQ(reused->label, "four"); + EXPECT_EQ(counters.constructions.load(), 3); + EXPECT_EQ(counters.resets.load(), 1); + pool.Release(std::move(reused)); +} + +TEST(ObjectPoolTest, ReleaseNullptrIsNoOp) { + ABSL_CONST_INIT static ObjectPool<TestItem, 2> pool; + pool.Release(nullptr); +} + +TEST(ObjectPoolTest, ConcurrentAcquireAndRelease) { + ABSL_CONST_INIT static ObjectPool<TestItem, 4> pool; + TestItem::Counters counters; + + constexpr size_t kNumThreads = 16; + static constexpr int kIterationsPerThread = 200; + std::vector<std::thread> threads; + threads.reserve(kNumThreads); + for (size_t t = 0; t < kNumThreads; ++t) { + threads.emplace_back([&counters, t]() { + const std::string expected_label = "thread_" + std::to_string(t); + for (int i = 0; i < kIterationsPerThread; ++i) { + const int expected_val = static_cast<int>(t) * 1000 + i; + std::unique_ptr<TestItem> item = + pool.Acquire(&counters, expected_val, expected_label); + EXPECT_EQ(item->value, expected_val); + EXPECT_EQ(item->label, expected_label); + item->value = -expected_val; + EXPECT_EQ(item->value, -expected_val); + pool.Release(std::move(item)); + } + }); + } + for (std::thread& thread : threads) { + thread.join(); + } + + const int total_acquires = + static_cast<int>(kNumThreads) * kIterationsPerThread; + EXPECT_EQ(counters.constructions.load() + counters.resets.load(), + total_acquires); + // Up to 4 items remain live in the pool; any overflow items were destroyed. + EXPECT_LE(counters.constructions.load() - counters.destructions.load(), 4); +} + +} // namespace +} // namespace log_internal +ABSL_NAMESPACE_END +} // namespace absl
diff --git a/absl/log/log_benchmark.cc b/absl/log/log_benchmark.cc index 50a64ea..878001b 100644 --- a/absl/log/log_benchmark.cc +++ b/absl/log/log_benchmark.cc
@@ -95,6 +95,29 @@ } BENCHMARK(BM_EnabledLogOverhead); +class LocalNullLogSink : public absl::LogSink { + public: + void Send(const absl::LogEntry&) override {} +}; + +static void BM_EnabledLogToSinkString(benchmark::State& state) { + absl::SetMinLogLevel(absl::LogSeverityAtLeast::kInfo); + LocalNullLogSink sink; + for (auto _ : state) { + LOG(INFO).ToSinkOnly(&sink) << "hello world"; + } +} +BENCHMARK(BM_EnabledLogToSinkString)->ThreadRange(1, 16); + +static void BM_EnabledLogToSinkInt(benchmark::State& state) { + absl::SetMinLogLevel(absl::LogSeverityAtLeast::kInfo); + LocalNullLogSink sink; + for (auto _ : state) { + LOG(INFO).ToSinkOnly(&sink) << "value: " << 42; + } +} +BENCHMARK(BM_EnabledLogToSinkInt)->ThreadRange(1, 16); + static void BM_VlogIsOnOverhead(benchmark::State& state) { // It would make sense to do this only when state.thread_index == 0, // but thread_index is an int on some platforms (e.g. Android) and a
diff --git a/absl/log/log_format_test.cc b/absl/log/log_format_test.cc index b792d38..48cb4db 100644 --- a/absl/log/log_format_test.cc +++ b/absl/log/log_format_test.cc
@@ -25,6 +25,7 @@ #include <string> #include <string_view> #include <type_traits> +#include <vector> #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -1950,6 +1951,47 @@ LOG(INFO) << std::setfill('0') << std::setw(8) << value; } +TEST(ManipulatorLogFormatTest, ManipulatorsDoNotLeakAcrossPooledMessages) { + struct BufferAndMessageCaptureSink : public absl::LogSink { + void Send(const absl::LogEntry& entry) override { + buffers.push_back(entry.text_message_with_prefix_and_newline().data()); + messages.emplace_back(entry.text_message()); + } + std::vector<const char*> buffers; + std::vector<std::string> messages; + } sink; + + // 1. Log without touching OstreamView (leaves std::optional<std::ostream> + // uninitialized or unused for this message). + constexpr absl::string_view kPlainMessage = "plain string_view"; + LOG(INFO).ToSinkOnly(&sink) << kPlainMessage; + + // 2. Log with non-default stream manipulators and stream error state bits on + // the same pooled buffer. + LOG(INFO).ToSinkOnly(&sink) + << std::hex << std::uppercase << std::noshowbase << std::noboolalpha + << std::scientific << std::setprecision(2) << std::setfill('*') + << std::left << std::setw(8) << 255 << " " << true << " " << 1.5 + << +[](std::ostream& os) -> std::ostream& { + os.setstate(std::ios_base::failbit | std::ios_base::badbit | + std::ios_base::eofbit); + return os; + }; + + // 3. Log again without manipulators; verify the same pooled LogMessageData + // buffer is reused and all default ostream flags/precision/width/fill/rdstate + // are restored. + LOG(INFO).ToSinkOnly(&sink) + << 255 << " " << true << " " << 1.5 << " " << std::setw(4) << 7; + + ASSERT_EQ(sink.messages.size(), 3u); + EXPECT_EQ(sink.buffers[0], sink.buffers[1]); + EXPECT_EQ(sink.buffers[1], sink.buffers[2]); + EXPECT_EQ(sink.messages[0], "plain string_view"); + EXPECT_EQ(sink.messages[1], "FF****** 1 1.50E+00"); + EXPECT_EQ(sink.messages[2], "255 true 1.5 7"); +} + class FromCustomClass {}; std::ostream& operator<<(std::ostream& os, const FromCustomClass&) { return os << "FromCustomClass{}" << std::hex;