This is an automated email from the ASF dual-hosted git repository.
chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 2f7775b93 Support RSS trimming for idle bthread stacks (#3668)
2f7775b93 is described below
commit 2f7775b936d487817e805120026fec28e1c9c1a7
Author: Bright Chen <[email protected]>
AuthorDate: Fri Oct 30 00:09:46 2026 +0800
Support RSS trimming for idle bthread stacks (#3668)
* Support RSS trimming for idle bthread stacks
* Disable by default
---
config_brpc.sh | 3 +-
src/bthread/stack.cpp | 126 +++++++-
src/bthread/stack.h | 14 +
src/bthread/stack_inl.h | 14 +-
src/bthread/task_control.cpp | 3 +
src/butil/object_pool.h | 18 ++
src/butil/object_pool_inl.h | 86 +++++-
test/bthread_stack_trim_unittest.cpp | 557 +++++++++++++++++++++++++++++++++++
test/object_pool_unittest.cpp | 174 +++++++++++
9 files changed, 975 insertions(+), 20 deletions(-)
diff --git a/config_brpc.sh b/config_brpc.sh
index e940e486f..b77f81bed 100755
--- a/config_brpc.sh
+++ b/config_brpc.sh
@@ -685,8 +685,9 @@ else
GTEST_HDR=$(find_dir_of_header_or_die gtest/gtest.h)
append_to_output_libs $GTEST_LIB " "
append_to_output_headers $GTEST_HDR " "
- append_to_output_linkings $GTEST_LIB gtest " "
+ # gtest_main depends on gtest, including when all tests are compiled out.
append_to_output_linkings $GTEST_LIB gtest_main " "
+ append_to_output_linkings $GTEST_LIB gtest " "
fi
append_to_output "endif"
diff --git a/src/bthread/stack.cpp b/src/bthread/stack.cpp
index 64608cd5e..2b25e77b0 100644
--- a/src/bthread/stack.cpp
+++ b/src/bthread/stack.cpp
@@ -20,11 +20,12 @@
// Date: Sun Sep 7 22:37:39 CST 2014
#include <unistd.h> // getpagesize
-#include <sys/mman.h> // mmap, munmap, mprotect
+#include <sys/mman.h> // mmap, munmap, mprotect,
madvise
#include <algorithm> // std::max
#include <stdlib.h> // posix_memalign
#include "butil/macros.h" // BAIDU_CASSERT
-#include "butil/memory/singleton_on_pthread_once.h"
+#include "butil/reloadable_flags.h"
+#include "butil/threading/platform_thread.h"
#include "butil/third_party/dynamic_annotations/dynamic_annotations.h" //
RunningOnValgrind
#include "butil/third_party/valgrind/valgrind.h" // VALGRIND_STACK_REGISTER
#include "bvar/passive_status.h"
@@ -37,6 +38,14 @@ DEFINE_int32(stack_size_large, 8388608, "size of large
stacks");
DEFINE_int32(guard_page_size, 4096, "size of guard page, allocate stacks by
malloc if it's 0(not recommended)");
DEFINE_int32(tc_stack_small, 32, "maximum small stacks cached by each thread");
DEFINE_int32(tc_stack_normal, 8, "maximum normal stacks cached by each
thread");
+DEFINE_int32(bthread_stack_trim_idle_s, 0,
+ "Release memory of stacks idle in the global pool for this many "
+ "seconds, 0 disables (Linux only, unsupported under ASan)");
+BUTIL_VALIDATE_GFLAG(bthread_stack_trim_idle_s, butil::NonNegativeInteger);
+DEFINE_int32(bthread_stack_trim_batch_size, 64,
+ "Object budget per idle stack trimming batch; whole chunks may "
+ "exceed it by up to 63 stacks (must be positive)");
+BUTIL_VALIDATE_GFLAG(bthread_stack_trim_batch_size, butil::PositiveInteger);
namespace bthread {
@@ -150,4 +159,117 @@ int* SmallStackClass::stack_size_flag =
&FLAGS_stack_size_small;
int* NormalStackClass::stack_size_flag = &FLAGS_stack_size_normal;
int* LargeStackClass::stack_size_flag = &FLAGS_stack_size_large;
+#if defined(BTHREAD_STACK_TRIM_SUPPORTED)
+
+#if defined(UNIT_TEST)
+// Replace only when no other thread is trimming stacks.
+int (*g_stack_madvise)(void*, size_t, int) = ::madvise;
+#endif
+
+namespace internal {
+
+static butil::static_atomic<int64_t> s_trimmed_count =
BUTIL_STATIC_ATOMIC_INIT(0);
+static int64_t get_trimmed_count(void*) {
+ return s_trimmed_count.load(butil::memory_order_relaxed);
+}
+static bvar::PassiveStatus<int64_t> bvar_trimmed_count(
+ "bthread_stack_trimmed_count", get_trimmed_count, nullptr);
+
+template <typename StackClass>
+static size_t trim_idle_stacks(int64_t idle_us) {
+ typedef typename StackFactory<StackClass>::Wrapper Wrapper;
+ size_t released = 0;
+ int error = 0;
+ auto fn = [&released, &error](Wrapper* w) noexcept {
+ StackStorage& s = w->storage;
+ // malloc owns unguarded stacks. Already processed stacks can return
+ // to the free list when a borrowed chunk is flushed from TLS.
+ if (w->context == nullptr || s.guardsize == 0) {
+ return;
+ }
+ void* low = (char*)s.bottom - s.stacksize;
+#if defined(UNIT_TEST)
+ int rc = g_stack_madvise(low, s.stacksize, MADV_DONTNEED);
+#else
+ int rc = madvise(low, s.stacksize, MADV_DONTNEED);
+#endif
+ // An error can follow partial reclamation across VMAs. Never resume
+ // the old continuation, even on failure; get_stack rebuilds it.
+ w->context = nullptr;
+ if (rc == 0) {
+ ++released;
+ } else {
+ error = errno;
+ }
+ };
+ size_t n = butil::process_idle_objects<Wrapper>(
+ idle_us, FLAGS_bthread_stack_trim_batch_size, fn);
+ if (released != 0) {
+ s_trimmed_count.fetch_add(released, butil::memory_order_relaxed);
+ }
+ if (error != 0) {
+ errno = error;
+ PLOG_EVERY_SECOND(ERROR) << "Fail to madvise idle stacks";
+ }
+ return n;
+}
+
+template <typename StackClass>
+static size_t trim_one_batch() {
+ // Runtime changes take effect at the next batch, including after sleeping.
+ int idle_s = FLAGS_bthread_stack_trim_idle_s;
+ if (idle_s <= 0) {
+ return 0;
+ }
+ size_t n = trim_idle_stacks<StackClass>(
+ static_cast<int64_t>(idle_s) * 1000000);
+ if (n != 0) {
+ // Spread madvise work across batches when the idle backlog is large.
+ butil::PlatformThread::Sleep(butil::TimeDelta::FromMilliseconds(10));
+ }
+ return n;
+}
+
+static void* stack_trim_thread(void*) {
+ butil::PlatformThread::SetNameSimple("bthread_trim");
+ for (;;) {
+ size_t n = trim_one_batch<SmallStackClass>();
+ n += trim_one_batch<NormalStackClass>();
+ n += trim_one_batch<LargeStackClass>();
+ if (n == 0) {
+ butil::PlatformThread::Sleep(butil::TimeDelta::FromSeconds(1));
+ }
+ }
+ return nullptr;
+}
+
+void init_stack_trimming() {
+ static pthread_once_t once = PTHREAD_ONCE_INIT;
+ pthread_once(&once, [] {
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_t tid;
+ int rc = pthread_create(&tid, &attr, stack_trim_thread, nullptr);
+ pthread_attr_destroy(&attr);
+ if (rc != 0) {
+ LOG(ERROR) << "Fail to start stack trimmer: " << berror(rc);
+ }
+ });
+}
+
+#if defined(UNIT_TEST)
+// Synchronous test entries must not overlap background reclamation.
+size_t trim_idle_stacks_for_test(int64_t idle_us) {
+ size_t n = trim_idle_stacks<SmallStackClass>(idle_us);
+ n += trim_idle_stacks<NormalStackClass>(idle_us);
+ n += trim_idle_stacks<LargeStackClass>(idle_us);
+ return n;
+}
+#endif
+
+} // namespace internal
+
+#endif // BTHREAD_STACK_TRIM_SUPPORTED
+
} // namespace bthread
diff --git a/src/bthread/stack.h b/src/bthread/stack.h
index d703566e9..10e1a3fe1 100644
--- a/src/bthread/stack.h
+++ b/src/bthread/stack.h
@@ -26,8 +26,15 @@
#include <gflags/gflags.h> // DECLARE_int32
#include "bthread/types.h"
#include "bthread/context.h" // bthread_fcontext_t
+#include "butil/build_config.h"
+#include "butil/compiler_specific.h"
#include "butil/object_pool.h"
+// Discarding the continuation would leak ASan's saved fake stack.
+#if defined(OS_LINUX) && !defined(BUTIL_USE_ASAN)
+#define BTHREAD_STACK_TRIM_SUPPORTED
+#endif
+
namespace bthread {
struct StackStorage {
@@ -53,6 +60,13 @@ int allocate_stack_storage(StackStorage* s, int stacksize,
int guardsize);
// corresponding allocate_stack_storage() otherwise behavior is undefined.
void deallocate_stack_storage(StackStorage* s);
+#if defined(BTHREAD_STACK_TRIM_SUPPORTED)
+namespace internal {
+// Called from TaskControl::init, never during command-line parsing.
+void init_stack_trimming();
+}
+#endif
+
enum StackType {
STACK_TYPE_MAIN = 0,
STACK_TYPE_PTHREAD = BTHREAD_STACKTYPE_PTHREAD,
diff --git a/src/bthread/stack_inl.h b/src/bthread/stack_inl.h
index 6c313ca36..e60c28968 100644
--- a/src/bthread/stack_inl.h
+++ b/src/bthread/stack_inl.h
@@ -136,7 +136,8 @@ template <typename StackClass> struct StackFactory {
BTHREAD_ASAN_POISON_MEMORY_REGION(storage);
}
~Wrapper() {
- if (context) {
+ // A trimmed stack has no context but still owns its mapping.
+ if (storage.bottom) {
context = nullptr;
// Unpoison to avoid affecting other allocator.
BTHREAD_ASAN_UNPOISON_MEMORY_REGION(storage);
@@ -147,9 +148,18 @@ template <typename StackClass> struct StackFactory {
};
static ContextualStack* get_stack(void (*entry)(intptr_t)) {
- ContextualStack* cs = butil::get_object<Wrapper>(entry);
+ Wrapper* cs = butil::get_object<Wrapper>(entry);
+ if (cs == nullptr) {
+ return nullptr;
+ }
// Marks stack as addressable.
BTHREAD_ASAN_UNPOISON_MEMORY_REGION(cs->storage);
+ // Reclamation may discard the saved continuation, even on failure.
+ // The mapping is kept, so restarting at entry needs no allocation.
+ if (cs->context == nullptr) {
+ cs->context = bthread_make_fcontext(
+ cs->storage.bottom, cs->storage.stacksize, entry);
+ }
return cs;
}
diff --git a/src/bthread/task_control.cpp b/src/bthread/task_control.cpp
index 6da8a4ab8..eeec8d5d1 100644
--- a/src/bthread/task_control.cpp
+++ b/src/bthread/task_control.cpp
@@ -283,6 +283,9 @@ int TaskControl::init(int concurrency) {
LOG(ERROR) << "Fail to get global_timer_thread";
return -1;
}
+#if defined(BTHREAD_STACK_TRIM_SUPPORTED)
+ internal::init_stack_trimming();
+#endif
#ifdef BRPC_BTHREAD_TRACER
if (!_task_tracer.Init()) {
diff --git a/src/butil/object_pool.h b/src/butil/object_pool.h
index 40bd28e7a..f0a66cc32 100644
--- a/src/butil/object_pool.h
+++ b/src/butil/object_pool.h
@@ -23,6 +23,7 @@
#define BUTIL_OBJECT_POOL_H
#include <cstddef> // size_t
+#include <cstdint>
#include "butil/type_traits.h"
// ObjectPool is a derivative class of ResourcePool to allocate and
@@ -109,6 +110,23 @@ template <typename T> ObjectPoolInfo describe_objects() {
return ObjectPool<T>::singleton()->describe_objects();
}
+// Process objects cached globally for at least idle_us (monotonic time).
+// Exclusively borrow whole chunks until at least max_items objects are
selected
+// or no eligible chunks remain, then invoke fn outside the pool lock. The
count
+// may exceed max_items by at most ObjectPool<T>::FREE_CHUNK_NITEM - 1 objects.
+// Use SIZE_MAX to process all eligible global chunks in one call.
+// fn must be noexcept, preserve each object's lifetime/address,
+// and leave it reusable. Borrowers restore any resources released by fn.
+// Processed chunks become cold and are reused only after the regular global
+// free list is empty. They are not processed again until borrowed and returned
+// to the global free list.
+// Return the number of objects passed to fn, regardless of its effects.
+// TLS caches are untouched. Finish all calls before clearing a pool in tests.
+template <typename T, typename Fn>
+inline size_t process_idle_objects(int64_t idle_us, size_t max_items, Fn& fn) {
+ return ObjectPool<T>::singleton()->process_idle_objects(idle_us,
max_items, fn);
+}
+
} // namespace butil
#endif // BUTIL_OBJECT_POOL_H
diff --git a/src/butil/object_pool_inl.h b/src/butil/object_pool_inl.h
index ea78ee5a0..15a95870b 100644
--- a/src/butil/object_pool_inl.h
+++ b/src/butil/object_pool_inl.h
@@ -31,6 +31,7 @@
#include "butil/macros.h" // BAIDU_CACHELINE_ALIGNMENT
#include "butil/scoped_lock.h" // BAIDU_SCOPED_LOCK
#include "butil/thread_local.h" // BAIDU_THREAD_LOCAL
+#include "butil/time.h" // monotonic_time_us
#include "butil/memory/aligned_memory.h" // butil::AlignedMemory
#include "butil/debug/address_annotations.h"
@@ -117,7 +118,11 @@ public:
// Free objects are batched in a FreeChunk before they're added to
// global list(_free_chunks).
typedef ObjectPoolFreeChunk<T, FREE_CHUNK_NITEM> FreeChunk;
- typedef ObjectPoolFreeChunk<T, 0> DynamicFreeChunk;
+ struct GlobalFreeChunk {
+ int64_t push_us;
+ size_t nfree;
+ T* ptrs[0];
+ };
#ifdef BUTIL_USE_ASAN
// According to
https://github.com/google/sanitizers/wiki/AddressSanitizerManualPoisoning ,
// The allocated chunks should start with 8-aligned addresses,
@@ -307,6 +312,44 @@ public:
return (n < FREE_CHUNK_NITEM ? n : FREE_CHUNK_NITEM);
}
+ template <typename Fn>
+ size_t process_idle_objects(int64_t idle_us, size_t max_items, Fn& fn) {
+ static_assert(noexcept(std::declval<Fn&>()(std::declval<T*>())),
+ "idle object callback must be noexcept");
+ if (idle_us < 0 || max_items == 0) {
+ return 0;
+ }
+ std::vector<GlobalFreeChunk*> batch;
+ size_t nchunk = 0;
+ size_t nitem = 0;
+ int64_t deadline = monotonic_time_us() - idle_us;
+ {
+ BAIDU_SCOPED_LOCK(_free_chunks_mutex);
+ while (nchunk < _free_chunks.size() &&
+ nitem < max_items && _free_chunks[nchunk]->push_us <=
deadline) {
+ nitem += _free_chunks[nchunk]->nfree;
+ ++nchunk;
+ }
+ batch.assign(_free_chunks.begin(), _free_chunks.begin() + nchunk);
+ _free_chunks.erase(_free_chunks.begin(), _free_chunks.begin() +
nchunk);
+ publish_nchunks_locked();
+ }
+ for (size_t i = 0; i < nchunk; ++i) {
+ for (size_t j = 0; j < batch[i]->nfree; ++j) {
+ T* obj = batch[i]->ptrs[j];
+ OBJECT_POOL_ASAN_UNPOISON_MEMORY_REGION(obj);
+ fn(obj);
+ OBJECT_POOL_ASAN_POISON_MEMORY_REGION(obj);
+ }
+ }
+ if (nchunk != 0) {
+ BAIDU_SCOPED_LOCK(_free_chunks_mutex);
+ _cold_chunks.insert(_cold_chunks.end(), batch.begin(),
batch.end());
+ publish_nchunks_locked();
+ }
+ return nitem;
+ }
+
// Number of all allocated objects, including being used and free.
ObjectPoolInfo describe_objects() const {
ObjectPoolInfo info;
@@ -355,9 +398,8 @@ public:
}
private:
- ObjectPool() {
+ ObjectPool() : _nchunks(0) {
_free_chunks.reserve(OP_INITIAL_FREE_LIST_SIZE);
- _nfree_chunks.store(0, butil::memory_order_relaxed);
pthread_mutex_init(&_free_chunks_mutex, nullptr);
#if defined(BUTIL_USE_ASAN) && \
!defined(BAIDU_CLEAR_OBJECT_POOL_AFTER_ALL_THREADS_QUIT)
@@ -536,21 +578,28 @@ private:
}
private:
+ void publish_nchunks_locked() {
+ _nchunks.store(_free_chunks.size() + _cold_chunks.size(),
+ butil::memory_order_relaxed);
+ }
+
bool pop_free_chunk(FreeChunk& c) {
// This relaxed load is only a hint that avoids contending on the
- // mutex when the shared free list is empty. The list itself remains
- // protected by _free_chunks_mutex below.
- if (_nfree_chunks.load(butil::memory_order_relaxed) == 0) {
+ // mutex when both shared free lists are empty. Both lists remain
+ // protected by `_free_chunks_mutex` below.
+ if (_nchunks.load(butil::memory_order_relaxed) == 0) {
return false;
}
pthread_mutex_lock(&_free_chunks_mutex);
- if (_free_chunks.empty()) {
+ std::vector<GlobalFreeChunk*>& list =
+ _free_chunks.empty() ? _cold_chunks : _free_chunks;
+ if (list.empty()) {
pthread_mutex_unlock(&_free_chunks_mutex);
return false;
}
- DynamicFreeChunk* p = _free_chunks.back();
- _free_chunks.pop_back();
- _nfree_chunks.fetch_sub(1, butil::memory_order_relaxed);
+ GlobalFreeChunk* p = list.back();
+ list.pop_back();
+ publish_nchunks_locked();
pthread_mutex_unlock(&_free_chunks_mutex);
c.nfree = p->nfree;
memcpy(c.ptrs, p->ptrs, sizeof(*p->ptrs) * p->nfree);
@@ -559,16 +608,21 @@ private:
}
bool push_free_chunk(const FreeChunk& c) {
- DynamicFreeChunk* p = (DynamicFreeChunk*)malloc(
- offsetof(DynamicFreeChunk, ptrs) + sizeof(*c.ptrs) * c.nfree);
+ if (c.nfree == 0) {
+ return true;
+ }
+ GlobalFreeChunk* p = (GlobalFreeChunk*)malloc(
+ offsetof(GlobalFreeChunk, ptrs) + sizeof(*c.ptrs) * c.nfree);
if (!p) {
return false;
}
p->nfree = c.nfree;
memcpy(p->ptrs, c.ptrs, sizeof(*c.ptrs) * c.nfree);
pthread_mutex_lock(&_free_chunks_mutex);
+ // Timestamp under the lock so the list is ordered by idle age.
+ p->push_us = monotonic_time_us();
_free_chunks.push_back(p);
- _nfree_chunks.fetch_add(1, butil::memory_order_relaxed);
+ publish_nchunks_locked();
pthread_mutex_unlock(&_free_chunks_mutex);
return true;
}
@@ -582,9 +636,11 @@ private:
static pthread_mutex_t _change_thread_mutex;
static butil::static_atomic<BlockGroup*>
_block_groups[OP_MAX_BLOCK_NGROUP];
- std::vector<DynamicFreeChunk*> _free_chunks;
+ std::vector<GlobalFreeChunk*> _free_chunks;
+ // Processed idle chunks, reused only when `_free_chunks` is empty.
+ std::vector<GlobalFreeChunk*> _cold_chunks;
+ butil::atomic<size_t> _nchunks;
pthread_mutex_t _free_chunks_mutex;
- butil::static_atomic<size_t> _nfree_chunks;
#ifdef BUTIL_OBJECT_POOL_NEED_FREE_ITEM_NUM
static butil::static_atomic<size_t> _global_nfree;
diff --git a/test/bthread_stack_trim_unittest.cpp
b/test/bthread_stack_trim_unittest.cpp
new file mode 100644
index 000000000..db7547088
--- /dev/null
+++ b/test/bthread_stack_trim_unittest.cpp
@@ -0,0 +1,557 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+#include <algorithm>
+#include <atomic>
+#include <csignal>
+#include <cstdio>
+#include <cstring>
+#include <cstdlib>
+#include <sys/mman.h>
+#include <thread>
+#include <vector>
+#include <gtest/gtest.h>
+#include "bthread/bthread.h"
+#include "bthread/butex.h"
+#include "bthread/stack.h"
+#include "bthread/task_group.h"
+#include "butil/build_config.h"
+#include "butil/threading/platform_thread.h"
+#include "bvar/variable.h"
+
+DECLARE_int32(bthread_stack_trim_idle_s);
+DECLARE_int32(bthread_stack_trim_batch_size);
+DECLARE_int32(stack_size_small);
+DECLARE_int32(stack_size_normal);
+DECLARE_int32(stack_size_large);
+
+#if defined(BTHREAD_STACK_TRIM_SUPPORTED)
+namespace bthread {
+extern int (*g_stack_madvise)(void*, size_t, int);
+namespace internal {
+size_t trim_idle_stacks_for_test(int64_t idle_us);
+}
+}
+#endif
+
+namespace {
+#if defined(BTHREAD_STACK_TRIM_SUPPORTED)
+
+typedef bthread::StackFactory<bthread::NormalStackClass> NormalFactory;
+
+int64_t counter(const char* name) {
+ return strtoll(bvar::Variable::describe_exposed(name).c_str(), nullptr,
10);
+}
+
+void sleep_ms(int ms) {
+ butil::PlatformThread::Sleep(butil::TimeDelta::FromMilliseconds(ms));
+}
+
+void trim_all() {
+ while (bthread::internal::trim_idle_stacks_for_test(0) != 0) {}
+}
+
+int64_t process_rss_kb() {
+ FILE* file = fopen("/proc/self/smaps_rollup", "r");
+ if (file == nullptr) { file = fopen("/proc/self/smaps", "r"); }
+ if (file == nullptr) {
+ ADD_FAILURE() << "Cannot read process RSS: " << strerror(errno);
+ return -1;
+ }
+ char line[256];
+ long long rss = -1;
+ long long value;
+ while (fgets(line, sizeof(line), file) != nullptr) {
+ if (sscanf(line, "Rss: %lld kB", &value) == 1) {
+ if (rss < 0) { rss = 0; }
+ rss += value;
+ }
+ }
+ fclose(file);
+ EXPECT_GE(rss, 0);
+ return rss;
+}
+
+// Excludes batches held by the trimmer. Use only after synchronous trimming.
+template <typename StackClass>
+size_t untrimmed_global_stacks() {
+ typedef typename bthread::StackFactory<StackClass>::Wrapper Wrapper;
+ auto* pool = butil::ObjectPool<Wrapper>::singleton();
+ BAIDU_SCOPED_LOCK(pool->_free_chunks_mutex);
+ size_t n = 0;
+ for (auto* chunk : pool->_free_chunks) {
+ for (size_t i = 0; i < chunk->nfree; ++i) {
+ n += chunk->ptrs[i]->context != nullptr;
+ }
+ }
+ return n;
+}
+
+size_t resident_pages(bthread::ContextualStack* s) {
+ std::vector<unsigned char> pages(s->storage.stacksize / getpagesize());
+ if (mincore(static_cast<char*>(s->storage.bottom) - s->storage.stacksize,
+ s->storage.stacksize, pages.data()) != 0) {
+ ADD_FAILURE() << "mincore failed: " << strerror(errno);
+ return SIZE_MAX;
+ }
+ return std::count_if(pages.begin(), pages.end(),
+ [](unsigned char p) { return (p & 1) != 0; });
+}
+
+void flush_stacks(bthread::StackType type = bthread::STACK_TYPE_NORMAL) {
+ switch (type) {
+ case bthread::STACK_TYPE_SMALL:
+
butil::clear_objects<bthread::StackFactory<bthread::SmallStackClass>::Wrapper>();
+ break;
+ case bthread::STACK_TYPE_LARGE:
+
butil::clear_objects<bthread::StackFactory<bthread::LargeStackClass>::Wrapper>();
+ break;
+ default:
+ butil::clear_objects<NormalFactory::Wrapper>();
+ break;
+ }
+}
+
+std::vector<bthread::ContextualStack*> cache_stacks(
+ size_t n, bthread::StackType type = bthread::STACK_TYPE_NORMAL) {
+ std::vector<bthread::ContextualStack*> stacks;
+ for (size_t i = 0; i < n; ++i) {
+ auto* s = bthread::get_stack(type, bthread::TaskGroup::task_runner);
+ EXPECT_NE(nullptr, s);
+ if (!s) { break; }
+ // Touch several pages; merely reserving an mmap does not consume RSS.
+ memset(static_cast<char*>(s->storage.bottom) - 16384, 0x5a, 8192);
+ stacks.push_back(s);
+ }
+ for (auto* s : stacks) { bthread::return_stack(s); }
+ flush_stacks(type);
+ return stacks;
+}
+
+struct Burst {
+ int depth;
+ std::atomic<int> ready{0};
+ std::atomic<int> errors{0};
+ butil::atomic<int>* gate =
bthread::butex_create_checked<butil::atomic<int>>();
+ explicit Burst(int stack_depth) : depth(stack_depth) { gate->store(0); }
+ ~Burst() { bthread::butex_destroy(gate); }
+};
+
+NOINLINE void touch_stack(Burst* burst, int depth) {
+ volatile char page[4096];
+ page[0] = static_cast<char>(depth);
+ if (depth != 0) {
+ touch_stack(burst, depth - 1);
+ } else {
+ ++burst->ready;
+ while (burst->gate->load() == 0) {
+ bthread::butex_wait(burst->gate, 0, nullptr);
+ }
+ bthread_usleep(1000);
+ bthread_yield();
+ }
+ if (page[0] != depth) { ++burst->errors; }
+}
+
+bool run_burst(size_t n, int64_t* ready_rss_kb = nullptr,
+ bthread_attr_t attr = BTHREAD_ATTR_NORMAL, int depth = 16) {
+ Burst burst(depth);
+ std::vector<bthread_t> tids;
+ for (size_t i = 0; i < n; ++i) {
+ bthread_t tid;
+ if (bthread_start_background(&tid, &attr,
+ [](void* p) -> void* {
+ auto* burst = static_cast<Burst*>(p);
+ touch_stack(burst, burst->depth);
+ return nullptr;
+ }, &burst) != 0) {
+ break;
+ }
+ tids.push_back(tid);
+ }
+ int64_t deadline = butil::monotonic_time_us() + 10000000;
+ while (burst.ready.load() != static_cast<int>(tids.size()) &&
+ butil::monotonic_time_us() < deadline) {
+ sleep_ms(1);
+ }
+ bool ready = burst.ready.load() == static_cast<int>(n);
+ if (ready_rss_kb != nullptr) { *ready_rss_kb = process_rss_kb(); }
+ burst.gate->store(1);
+ bthread::butex_wake_all(burst.gate);
+ for (bthread_t tid : tids) { bthread_join(tid, nullptr); }
+ return ready && burst.errors.load() == 0;
+}
+
+struct MadviseOverride {
+ explicit MadviseOverride(int (*fn)(void*, size_t, int))
+ : old(bthread::g_stack_madvise) { bthread::g_stack_madvise = fn; }
+ ~MadviseOverride() { bthread::g_stack_madvise = old; }
+ int (*old)(void*, size_t, int);
+};
+
+class StackTrimTest : public ::testing::Test {
+protected:
+ void SetUp() override {
+ FLAGS_bthread_stack_trim_idle_s = 0;
+ flush_stacks();
+ trim_all();
+ }
+ GFLAGS_NAMESPACE::FlagSaver flags;
+};
+
+// Re-exec isolates tests that enable background reclamation.
+class StackTrimDeathTest : public StackTrimTest {
+ void SetUp() override {
+ StackTrimTest::SetUp();
+#ifdef GTEST_FLAG_SET
+ GTEST_FLAG_SET(death_test_style, "threadsafe");
+#else
+ ::testing::GTEST_FLAG(death_test_style) = "threadsafe";
+#endif
+ // Otherwise a failed CHECK only logs and the child still exits with 0.
+ ASSERT_FALSE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "crash_on_fatal_log", "true").empty());
+ }
+};
+
+TEST_F(StackTrimTest, trim_and_rebuild_all_stack_types) {
+ for (auto type : {bthread::STACK_TYPE_SMALL, bthread::STACK_TYPE_NORMAL,
+ bthread::STACK_TYPE_LARGE}) {
+ auto stacks = cache_stacks(80, type);
+ ASSERT_EQ(80u, stacks.size());
+ std::vector<bthread::StackStorage> storage;
+ for (auto* s : stacks) {
+ ASSERT_GT(resident_pages(s), 0u);
+ storage.push_back(s->storage);
+ }
+ int64_t before = counter("bthread_stack_count");
+ int64_t released_before = counter("bthread_stack_trimmed_count");
+ trim_all();
+ EXPECT_EQ(before, counter("bthread_stack_count"));
+ EXPECT_EQ(released_before + 80,
counter("bthread_stack_trimmed_count"));
+ for (size_t i = 0; i < stacks.size(); ++i) {
+ auto* s = stacks[i];
+ EXPECT_EQ(nullptr, s->context);
+ EXPECT_EQ(storage[i].bottom, s->storage.bottom);
+ EXPECT_EQ(storage[i].stacksize, s->storage.stacksize);
+ EXPECT_EQ(storage[i].guardsize, s->storage.guardsize);
+ EXPECT_EQ(storage[i].valgrind_stack_id,
s->storage.valgrind_stack_id);
+ EXPECT_EQ(0u, resident_pages(s));
+ }
+ // Rebuilding only touches the existing mapping, including its guard.
+ std::vector<bthread::ContextualStack*> reused;
+ for (int i = 0; i < 80; ++i) {
+ auto* s = bthread::get_stack(type,
bthread::TaskGroup::task_runner);
+ ASSERT_NE(nullptr, s);
+ EXPECT_NE(nullptr, s->context);
+ EXPECT_GT(s->storage.guardsize, 0u);
+ EXPECT_EQ(type, s->stacktype);
+ auto found = std::find(stacks.begin(), stacks.end(), s);
+ ASSERT_NE(stacks.end(), found);
+ EXPECT_EQ(storage[found - stacks.begin()].bottom,
s->storage.bottom);
+ reused.push_back(s);
+ }
+ EXPECT_EQ(before, counter("bthread_stack_count"));
+ for (auto* s : reused) { bthread::return_stack(s); }
+ flush_stacks(type);
+ trim_all();
+ }
+}
+
+TEST_F(StackTrimTest, batch_size_updates_on_next_batch) {
+ // LARGE stacks have one object per chunk, so the budget is exact here.
+ ASSERT_EQ(256u, cache_stacks(256, bthread::STACK_TYPE_LARGE).size());
+ int64_t before = counter("bthread_stack_trimmed_count");
+ ASSERT_FALSE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_batch_size", "1").empty());
+ EXPECT_EQ(1u, bthread::internal::trim_idle_stacks_for_test(0));
+ EXPECT_EQ(before + 1, counter("bthread_stack_trimmed_count"));
+ ASSERT_FALSE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_batch_size", "128").empty());
+ EXPECT_EQ(128u, bthread::internal::trim_idle_stacks_for_test(0));
+ EXPECT_EQ(before + 129, counter("bthread_stack_trimmed_count"));
+ ASSERT_FALSE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_batch_size", "256").empty());
+ EXPECT_EQ(127u, bthread::internal::trim_idle_stacks_for_test(0));
+ EXPECT_EQ(before + 256, counter("bthread_stack_trimmed_count"));
+ EXPECT_EQ(0u, bthread::internal::trim_idle_stacks_for_test(0));
+}
+
+TEST_F(StackTrimDeathTest, malloc_stacks_are_not_trimmed) {
+ ASSERT_EXIT({
+ FLAGS_guard_page_size = 0;
+ auto stacks = cache_stacks(80);
+ CHECK_EQ(80u, stacks.size());
+ std::vector<bthread_fcontext_t> contexts;
+ for (auto* s : stacks) { contexts.push_back(s->context); }
+ int64_t before = counter("bthread_stack_count");
+ trim_all();
+ CHECK_EQ(before, counter("bthread_stack_count"));
+ CHECK_EQ(0, counter("bthread_stack_trimmed_count"));
+ for (size_t i = 0; i < stacks.size(); ++i) {
+ CHECK_EQ(0u, stacks[i]->storage.guardsize);
+ CHECK_EQ(contexts[i], stacks[i]->context);
+ CHECK_EQ(0x5a, *(static_cast<unsigned char*>(
+ stacks[i]->storage.bottom) - 16384));
+ }
+ _exit(0);
+ }, ::testing::ExitedWithCode(0), "");
+}
+
+void check_failed_trim(bool partial) {
+ auto stacks = cache_stacks(256);
+ std::vector<bthread::StackStorage> storage;
+ std::vector<size_t> resident;
+ for (auto* s : stacks) {
+ storage.push_back(s->storage);
+ resident.push_back(resident_pages(s));
+ }
+ int64_t before = counter("bthread_stack_count");
+ int64_t released_before = counter("bthread_stack_trimmed_count");
+ {
+ auto fail = [](void*, size_t, int) { errno = EINVAL; return -1; };
+ auto partial_fail = [](void* low, size_t size, int advice) {
+ // Discard the page containing the saved context, then report an
+ // error. An error alone cannot prove that the old context is safe.
+ size_t page = getpagesize();
+ if (madvise(static_cast<char*>(low) + size - page, page, advice)
!= 0) {
+ return -1;
+ }
+ errno = EINVAL;
+ return -1;
+ };
+ MadviseOverride hook(partial ? +partial_fail : +fail);
+ trim_all();
+ }
+ EXPECT_EQ(before, counter("bthread_stack_count"));
+ EXPECT_EQ(released_before, counter("bthread_stack_trimmed_count"));
+ for (size_t i = 0; i < stacks.size(); ++i) {
+ EXPECT_EQ(nullptr, stacks[i]->context);
+ EXPECT_EQ(storage[i].bottom, stacks[i]->storage.bottom);
+ EXPECT_EQ(storage[i].stacksize, stacks[i]->storage.stacksize);
+ EXPECT_EQ(storage[i].guardsize, stacks[i]->storage.guardsize);
+ EXPECT_EQ(storage[i].valgrind_stack_id,
stacks[i]->storage.valgrind_stack_id);
+ EXPECT_EQ(resident[i] - (partial ? 1 : 0), resident_pages(stacks[i]));
+ }
+ EXPECT_EQ(0u, bthread::internal::trim_idle_stacks_for_test(0));
+ ASSERT_EQ(0, bthread_setconcurrency(8));
+ ASSERT_TRUE(run_burst(256)); // Must rebuild even when madvise failed.
+ int64_t deadline = butil::monotonic_time_us() + 2000000;
+ do {
+ trim_all();
+ if (counter("bthread_stack_trimmed_count") > released_before) { break;
}
+ sleep_ms(1);
+ } while (butil::monotonic_time_us() < deadline);
+ EXPECT_GT(counter("bthread_stack_trimmed_count"), released_before);
+}
+
+TEST_F(StackTrimTest, failed_trim_rebuilds_context_and_retries_after_reuse) {
+ check_failed_trim(false);
+}
+
+TEST_F(StackTrimTest,
partial_failure_rebuilds_context_and_retries_after_reuse) {
+ check_failed_trim(true);
+}
+
+TEST_F(StackTrimTest, burst_reclaim_and_second_burst) {
+ const char* stress_count = getenv("BRPC_STACK_TRIM_TEST_BURST");
+ size_t n = stress_count ? strtoul(stress_count, nullptr, 10) : 2048;
+ const char* stress_workers = getenv("BRPC_STACK_TRIM_TEST_WORKERS");
+ int workers = stress_workers ? strtol(stress_workers, nullptr, 10) : 8;
+ const char* stress_type = getenv("BRPC_STACK_TRIM_TEST_TYPE");
+ std::string type = stress_type ? stress_type : "normal";
+ bthread_attr_t attr = BTHREAD_ATTR_NORMAL;
+ int depth = 16;
+ int stack_size = FLAGS_stack_size_normal;
+ int cached_per_worker = FLAGS_tc_stack_normal;
+ auto count_untrimmed = untrimmed_global_stacks<bthread::NormalStackClass>;
+ if (type == "small") {
+ attr = BTHREAD_ATTR_SMALL;
+ depth = 3;
+ stack_size = FLAGS_stack_size_small;
+ cached_per_worker = FLAGS_tc_stack_small;
+ count_untrimmed = untrimmed_global_stacks<bthread::SmallStackClass>;
+ } else if (type == "large") {
+ attr = BTHREAD_ATTR_LARGE;
+ depth = 64;
+ stack_size = FLAGS_stack_size_large;
+ cached_per_worker = 1;
+ count_untrimmed = untrimmed_global_stacks<bthread::LargeStackClass>;
+ } else {
+ ASSERT_EQ("normal", type);
+ }
+ ASSERT_GE(n, 256u);
+ ASSERT_EQ(0, bthread_setconcurrency(workers));
+ int64_t released_before = counter("bthread_stack_trimmed_count");
+ int64_t baseline_rss_kb = process_rss_kb();
+ int64_t ready_rss_kb = 0;
+ ASSERT_TRUE(run_burst(n, &ready_rss_kb, attr, depth));
+ ASSERT_EQ(workers, counter("bthread_worker_count"));
+ int64_t peak = counter("bthread_stack_count");
+ EXPECT_GE(peak, static_cast<int64_t>(n));
+ int64_t before_trim_rss_kb = process_rss_kb();
+ int64_t trim_start_us = butil::monotonic_time_us();
+ int64_t deadline = trim_start_us + 3000000;
+ do {
+ trim_all();
+ if (counter("bthread_stack_trimmed_count") - released_before >=
+ static_cast<int64_t>(n) - workers * cached_per_worker &&
+ count_untrimmed() == 0) { break; }
+ sleep_ms(1);
+ } while (butil::monotonic_time_us() < deadline);
+ EXPECT_GE(counter("bthread_stack_trimmed_count") - released_before,
+ static_cast<int64_t>(n) - workers * cached_per_worker);
+ EXPECT_EQ(0u, count_untrimmed());
+ EXPECT_EQ(peak, counter("bthread_stack_count"));
+ int64_t trim_elapsed_us = butil::monotonic_time_us() - trim_start_us;
+ int64_t after_trim_rss_kb = process_rss_kb();
+ RecordProperty("stack_type", type);
+ RecordProperty("worker_count", std::to_string(workers));
+ RecordProperty("stack_size_kb", std::to_string(stack_size / 1024));
+ RecordProperty("touched_stack_kb", std::to_string((depth + 1) * 4));
+ RecordProperty("peak_stacks", std::to_string(peak));
+ RecordProperty("trimmed_stacks", std::to_string(
+ counter("bthread_stack_trimmed_count") - released_before));
+ RecordProperty("baseline_rss_kb", std::to_string(baseline_rss_kb));
+ RecordProperty("ready_rss_kb", std::to_string(ready_rss_kb));
+ RecordProperty("before_trim_rss_kb", std::to_string(before_trim_rss_kb));
+ RecordProperty("after_trim_rss_kb", std::to_string(after_trim_rss_kb));
+ RecordProperty("trim_elapsed_us", std::to_string(trim_elapsed_us));
+ ASSERT_TRUE(run_burst(n, nullptr, attr, depth));
+ EXPECT_GE(counter("bthread_stack_count"), static_cast<int64_t>(n));
+}
+
+TEST_F(StackTrimTest, reclaim_while_a_second_burst_is_running) {
+ cache_stacks(2048);
+ ASSERT_EQ(0, bthread_setconcurrency(8));
+ std::atomic<bool> stop(false);
+ std::thread trimmer([&] {
+ while (!stop.load()) {
+ bthread::internal::trim_idle_stacks_for_test(0);
+ std::this_thread::yield();
+ }
+ });
+ bool ok = run_burst(2048);
+ stop = true;
+ trimmer.join();
+ ASSERT_TRUE(ok);
+}
+
+TEST_F(StackTrimDeathTest, rebuilt_stack_guard_is_protected) {
+ ASSERT_EXIT({
+ auto stacks = cache_stacks(1);
+ CHECK_EQ(1u, stacks.size());
+ trim_all();
+ CHECK(stacks[0]->context == nullptr);
+ auto* s = NormalFactory::get_stack(bthread::TaskGroup::task_runner);
+ CHECK(s && s->storage.guardsize != 0);
+ *static_cast<volatile char*>(
+ static_cast<void*>(static_cast<char*>(s->storage.bottom) -
+ s->storage.stacksize - 1)) = 1;
+ _exit(0);
+ }, ::testing::KilledBySignal(SIGSEGV), "");
+}
+
+TEST_F(StackTrimTest, destructor_releases_mapping_without_context) {
+ int64_t before = counter("bthread_stack_count");
+ {
+ NormalFactory::Wrapper stack(bthread::TaskGroup::task_runner);
+ ASSERT_NE(nullptr, stack.context);
+ ASSERT_EQ(0, madvise(static_cast<char*>(stack.storage.bottom) -
+ stack.storage.stacksize,
+ stack.storage.stacksize, MADV_DONTNEED));
+ stack.context = nullptr;
+ EXPECT_EQ(before + 1, counter("bthread_stack_count"));
+ }
+ EXPECT_EQ(before, counter("bthread_stack_count"));
+}
+
+void check_background(bool runtime_enable) {
+ // This thread keeps exactly one warm stack in TLS. A producer puts a known
+ // number of other stacks in the global pool; no bthread borrows them.
+ auto* warm = NormalFactory::get_stack(bthread::TaskGroup::task_runner);
+ CHECK(warm != nullptr);
+ bthread_fcontext_t warm_context = warm->context;
+ NormalFactory::return_stack(warm);
+ if (!runtime_enable) {
+ CHECK(!GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_idle_s", "1").empty());
+ }
+ std::thread producer([] { CHECK_EQ(256u, cache_stacks(256).size()); });
+ producer.join();
+ int64_t before = counter("bthread_stack_count");
+ sleep_ms(1200);
+ CHECK_EQ(0, counter("bthread_stack_trimmed_count")); // Not initialized
yet.
+
+ CHECK_EQ(0, bthread_setconcurrency(8));
+ bthread_t tid;
+ CHECK_EQ(0, bthread_start_background(&tid, &BTHREAD_ATTR_PTHREAD,
+ [](void*) -> void* { return nullptr; }, nullptr));
+ CHECK_EQ(0, bthread_join(tid, nullptr));
+ if (runtime_enable) {
+ sleep_ms(1200);
+ CHECK_EQ(before, counter("bthread_stack_count"));
+ CHECK_EQ(0, counter("bthread_stack_trimmed_count"));
+ // No validator callback: the thread must already exist after init.
+ FLAGS_bthread_stack_trim_idle_s = 1;
+ }
+ int64_t deadline = butil::monotonic_time_us() + 10000000;
+ // The counter is published after a batch completes. Waiting for all 256
+ // stacks also accounts for batches already removed from the global list.
+ while (counter("bthread_stack_trimmed_count") < 256 &&
+ butil::monotonic_time_us() < deadline) {
+ sleep_ms(20);
+ }
+ CHECK_EQ(256, counter("bthread_stack_trimmed_count"));
+ CHECK_EQ(before, counter("bthread_stack_count"));
+ // Repeated TLS use stays warm across several idle periods.
+ for (int i = 0; i < 120; ++i) {
+ auto* s = NormalFactory::get_stack(bthread::TaskGroup::task_runner);
+ CHECK_EQ(warm, s);
+ CHECK_EQ(warm_context, s->context);
+ NormalFactory::return_stack(s);
+ sleep_ms(20);
+ }
+ CHECK_EQ(256, counter("bthread_stack_trimmed_count"));
+}
+
+TEST_F(StackTrimDeathTest, startup_enable_waits_for_bthread_initialization) {
+ ASSERT_EXIT({ check_background(false); _exit(0); },
+ ::testing::ExitedWithCode(0), "");
+}
+
+TEST_F(StackTrimDeathTest, runtime_enable_uses_existing_thread) {
+ ASSERT_EXIT({ check_background(true); _exit(0); },
+ ::testing::ExitedWithCode(0), "");
+}
+
+TEST_F(StackTrimTest, reject_negative_idle_timeout) {
+ EXPECT_TRUE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_idle_s", "-1").empty());
+}
+
+TEST_F(StackTrimTest, reject_nonpositive_batch_size) {
+ int before = FLAGS_bthread_stack_trim_batch_size;
+ EXPECT_TRUE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_batch_size", "0").empty());
+ EXPECT_TRUE(GFLAGS_NAMESPACE::SetCommandLineOption(
+ "bthread_stack_trim_batch_size", "-1").empty());
+ EXPECT_EQ(before, FLAGS_bthread_stack_trim_batch_size);
+}
+
+#endif // BTHREAD_STACK_TRIM_SUPPORTED
+} // namespace
diff --git a/test/object_pool_unittest.cpp b/test/object_pool_unittest.cpp
index 10ad931ab..367bb8a0e 100644
--- a/test/object_pool_unittest.cpp
+++ b/test/object_pool_unittest.cpp
@@ -16,6 +16,8 @@
// under the License.
#include <memory>
+#include <atomic>
+#include <thread>
#include <gtest/gtest.h>
#include "butil/time.h"
#include "butil/macros.h"
@@ -419,3 +421,175 @@ TEST_F(ObjectPoolTest, verify_get) {
clear_objects<int>();
}
} // namespace
+
+namespace {
+std::atomic<int> idle_objects_alive(0);
+size_t idle_chunk_size = 4;
+struct IdleObject {
+ IdleObject() : in_use(false), processed(false) { ++idle_objects_alive; }
+ ~IdleObject() { --idle_objects_alive; }
+ std::atomic<bool> in_use;
+ bool processed;
+};
+}
+
+namespace butil {
+template <> struct ObjectPoolBlockMaxItem<IdleObject> {
+ static const size_t value = 4;
+};
+template <> struct ObjectPoolFreeChunkMaxItem<IdleObject> {
+ static size_t value() { return idle_chunk_size; }
+};
+}
+
+namespace {
+typedef butil::ObjectPool<IdleObject> IdlePool;
+
+class ObjectPoolIdleTest : public ::testing::Test {
+protected:
+ void TearDown() override {
+ butil::clear_objects<IdleObject>();
+ EXPECT_EQ(0, idle_objects_alive.load());
+ idle_chunk_size = 4;
+ }
+ void fill(size_t n) {
+ std::vector<IdleObject*> objects;
+ for (size_t i = 0; i < n; ++i) {
+ objects.push_back(butil::get_object<IdleObject>());
+ }
+ for (IdleObject* obj : objects) {
+ ASSERT_EQ(0, butil::return_object(obj));
+ }
+ }
+ void check_count() {
+ IdlePool* pool = IdlePool::singleton();
+ BAIDU_SCOPED_LOCK(pool->_free_chunks_mutex);
+ EXPECT_EQ(pool->_free_chunks.size() + pool->_cold_chunks.size(),
+ pool->_nchunks.load(butil::memory_order_relaxed));
+ }
+};
+
+TEST_F(ObjectPoolIdleTest, age_budget_and_tls) {
+ fill(13); // Three global chunks and one TLS object.
+ IdlePool* pool = IdlePool::singleton();
+ size_t calls = 0;
+ auto process = [&calls](IdleObject* obj) noexcept {
+ EXPECT_FALSE(obj->in_use.load());
+ EXPECT_FALSE(obj->processed);
+ obj->processed = true;
+ ++calls;
+ };
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(INT64_MAX, 64,
process));
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(0, 0, process));
+ {
+ BAIDU_SCOPED_LOCK(pool->_free_chunks_mutex);
+ ASSERT_EQ(3u, pool->_free_chunks.size());
+ EXPECT_LE(pool->_free_chunks[0]->push_us,
pool->_free_chunks[1]->push_us);
+ EXPECT_LE(pool->_free_chunks[1]->push_us,
pool->_free_chunks[2]->push_us);
+ pool->_free_chunks[0]->push_us -= 2000000;
+ }
+ EXPECT_EQ(4u, butil::process_idle_objects<IdleObject>(1000000, 1,
process));
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(1000000, 64,
process));
+ // Whole chunks may overshoot the budget by fewer than FREE_CHUNK_NITEM.
+ EXPECT_EQ(8u, butil::process_idle_objects<IdleObject>(0, 5, process));
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(0, 64, process));
+ EXPECT_EQ(12u, calls);
+ IdleObject* local = butil::get_object<IdleObject>();
+ EXPECT_FALSE(local->processed);
+ butil::return_object(local);
+ check_count();
+ // TearDown drains both global lists and destroys every object exactly
once.
+}
+
+TEST_F(ObjectPoolIdleTest, prefer_unprocessed_and_reuse_without_construction) {
+ fill(13);
+ auto process = [](IdleObject* obj) noexcept { obj->processed = true; };
+ EXPECT_EQ(12u, butil::process_idle_objects<IdleObject>(0, 64, process));
+ std::vector<IdleObject*> held;
+ for (int i = 0; i < 5; ++i) {
+ held.push_back(butil::get_object<IdleObject>());
+ held.back()->processed = false;
+ }
+ for (IdleObject* obj : held) { butil::return_object(obj); }
+ held.clear();
+ // The five returned objects (TLS + global) precede the eight processed
ones.
+ for (int i = 0; i < 13; ++i) {
+ IdleObject* obj = butil::get_object<IdleObject>();
+ EXPECT_EQ(i >= 5, obj->processed);
+ held.push_back(obj);
+ }
+ EXPECT_EQ(13, idle_objects_alive.load());
+ for (IdleObject* obj : held) { butil::return_object(obj); }
+ check_count();
+}
+
+TEST_F(ObjectPoolIdleTest, zero_cache_does_not_publish_empty_chunks) {
+ idle_chunk_size = 0;
+ fill(9);
+ auto process = [](IdleObject*) noexcept {};
+ EXPECT_EQ(8u, butil::process_idle_objects<IdleObject>(0, 64, process));
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(0, 64, process));
+ check_count();
+}
+
+TEST_F(ObjectPoolIdleTest, honors_item_budget_across_many_chunks) {
+ idle_chunk_size = 1;
+ fill(301); // 300 global chunks and one TLS object.
+ size_t calls = 0;
+ auto process = [&calls](IdleObject* obj) noexcept {
+ EXPECT_FALSE(obj->processed);
+ obj->processed = true;
+ ++calls;
+ };
+ EXPECT_EQ(200u, butil::process_idle_objects<IdleObject>(0, 200, process));
+ EXPECT_EQ(200u, calls);
+ check_count();
+ EXPECT_EQ(100u, butil::process_idle_objects<IdleObject>(0, SIZE_MAX,
process));
+ EXPECT_EQ(300u, calls);
+ EXPECT_EQ(0u, butil::process_idle_objects<IdleObject>(0, SIZE_MAX,
process));
+ check_count();
+}
+
+TEST_F(ObjectPoolIdleTest, concurrent_borrow_return_and_process) {
+ fill(1024);
+ std::atomic<bool> stop(false);
+ std::atomic<size_t> calls(0);
+ auto process = [&calls](IdleObject* obj) noexcept {
+ EXPECT_FALSE(obj->in_use.exchange(true));
+ obj->processed = true;
+ EXPECT_TRUE(obj->in_use.exchange(false));
+ ++calls;
+ };
+ std::thread processor([&] {
+ // Run at least once: the workers may finish before this is scheduled.
+ do {
+ butil::process_idle_objects<IdleObject>(0, 32, process);
+ check_count();
+ std::this_thread::yield();
+ } while (!stop.load());
+ });
+ std::vector<std::thread> workers;
+ for (int i = 0; i < 4; ++i) {
+ workers.emplace_back([] {
+ for (int j = 0; j < 1000; ++j) {
+ IdleObject* held[16];
+ for (IdleObject*& obj : held) {
+ obj = butil::get_object<IdleObject>();
+ EXPECT_FALSE(obj->in_use.exchange(true));
+ obj->processed = false;
+ }
+ for (IdleObject* obj : held) {
+ EXPECT_TRUE(obj->in_use.exchange(false));
+ butil::return_object(obj);
+ }
+ }
+ });
+ }
+ for (std::thread& worker : workers) { worker.join(); }
+ stop = true;
+ processor.join();
+ EXPECT_GT(calls.load(), 0u);
+ EXPECT_EQ(1024, idle_objects_alive.load());
+ check_count();
+}
+} // namespace
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]