Rewrite memory quota management in C++ with Promises library (#27327)
* first pass new memory quota * forget the ee stuff - it should be a wrapper on top * beginning to finalize * compiles * basic tests pass * fixes * Automated change: Fix sanity tests * merge * Automated change: Fix sanity tests * add rebind test * flesh out the rest * Automated change: Fix sanity tests * add increase * prog * Automated change: Fix sanity tests * allow cancellation during run * fixes * clang-format * better allocation strategy * better allocation strategy * Automated change: Fix sanity tests * Update memory_quota.cc * format * better comment * remove block size - this is probably unnecessary complexity * fmt * cleanup * size_t * Automated change: Fix sanity tests * fixes * move makeslice into memoryallocator * move makeslice into memoryallocator * add container allocator, tests * Automated change: Fix sanity tests * fixes * Automated change: Fix sanity tests * add some docs * Automated change: Fix sanity tests * fix doc * comment vector * Automated change: Fix sanity tests * fixes * ditch the thread * exec_ctx integration * Automated change: Fix sanity tests * progress! * fuzzer * initial_corpora * Automated change: Fix sanity tests * bigger_objects * better-fuzzer * add stress test * Automated change: Fix sanity tests * Remove unused header * Iwyu * Automated change: Fix sanity tests * Portability fix, comment * Fix unused arg * Remove unused names * Removed unused name * Automated change: Fix sanity tests * windows * Automated change: Fix sanity tests * cleanup * fix-mac * cleanup, eliminate atomicbarrier * exclude some platforms * Automated change: Fix sanity tests Co-authored-by: ctiller <ctiller@users.noreply.github.com>pull/27499/head
parent
bed585bdcb
commit
82c99362b5
19 changed files with 2380 additions and 62 deletions
@ -0,0 +1,426 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
|
||||||
|
#include "src/core/lib/gpr/useful.h" |
||||||
|
#include "src/core/lib/promise/exec_ctx_wakeup_scheduler.h" |
||||||
|
#include "src/core/lib/promise/loop.h" |
||||||
|
#include "src/core/lib/promise/race.h" |
||||||
|
#include "src/core/lib/promise/seq.h" |
||||||
|
#include "src/core/lib/slice/slice_refcount.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
// Maximum number of bytes an allocator will request from a quota in one step.
|
||||||
|
// Larger allocations than this will require multiple allocation requests.
|
||||||
|
static constexpr size_t kMaxReplenishBytes = 1024 * 1024; |
||||||
|
|
||||||
|
// Minimum number of bytes an allocator will request from a quota in one step.
|
||||||
|
static constexpr size_t kMinReplenishBytes = 4096; |
||||||
|
|
||||||
|
//
|
||||||
|
// Reclaimer
|
||||||
|
//
|
||||||
|
|
||||||
|
ReclamationSweep::~ReclamationSweep() { |
||||||
|
if (memory_quota_ != nullptr) { |
||||||
|
memory_quota_->FinishReclamation(sweep_token_); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// ReclaimerQueue
|
||||||
|
//
|
||||||
|
|
||||||
|
const ReclaimerQueue::Index ReclaimerQueue::kInvalidIndex; |
||||||
|
|
||||||
|
void ReclaimerQueue::Insert(RefCountedPtr<MemoryAllocator> allocator, |
||||||
|
ReclamationFunction reclaimer, Index* index) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
if (*index < entries_.size() && entries_[*index].allocator == allocator) { |
||||||
|
entries_[*index].reclaimer.swap(reclaimer); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (free_entries_.empty()) { |
||||||
|
*index = entries_.size(); |
||||||
|
entries_.emplace_back(std::move(allocator), std::move(reclaimer)); |
||||||
|
} else { |
||||||
|
*index = free_entries_.back(); |
||||||
|
free_entries_.pop_back(); |
||||||
|
Entry& entry = entries_[*index]; |
||||||
|
entry.allocator = std::move(allocator); |
||||||
|
entry.reclaimer = std::move(reclaimer); |
||||||
|
} |
||||||
|
if (queue_.empty()) waker_.Wakeup(); |
||||||
|
queue_.push(*index); |
||||||
|
} |
||||||
|
|
||||||
|
ReclamationFunction ReclaimerQueue::Cancel(Index index, |
||||||
|
MemoryAllocator* allocator) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
if (index >= entries_.size()) return nullptr; |
||||||
|
Entry& entry = entries_[index]; |
||||||
|
if (entry.allocator.get() != allocator) return {}; |
||||||
|
entry.allocator.reset(); |
||||||
|
return std::move(entry.reclaimer); |
||||||
|
} |
||||||
|
|
||||||
|
Poll<ReclamationFunction> ReclaimerQueue::PollNext() { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
while (true) { |
||||||
|
if (queue_.empty()) { |
||||||
|
waker_ = Activity::current()->MakeNonOwningWaker(); |
||||||
|
return Pending{}; |
||||||
|
} |
||||||
|
Index index = queue_.front(); |
||||||
|
queue_.pop(); |
||||||
|
free_entries_.push_back(index); |
||||||
|
Entry& entry = entries_[index]; |
||||||
|
if (entry.allocator != nullptr) { |
||||||
|
entry.allocator.reset(); |
||||||
|
return std::move(entry.reclaimer); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// MemoryAllocator
|
||||||
|
//
|
||||||
|
|
||||||
|
MemoryAllocator::MemoryAllocator(RefCountedPtr<MemoryQuota> memory_quota) |
||||||
|
: InternallyRefCounted("MemoryAllocator"), memory_quota_(memory_quota) { |
||||||
|
Reserve(sizeof(MemoryQuota)); |
||||||
|
} |
||||||
|
|
||||||
|
MemoryAllocator::~MemoryAllocator() { |
||||||
|
GPR_ASSERT(free_bytes_.load(std::memory_order_acquire) + |
||||||
|
sizeof(MemoryQuota) == |
||||||
|
taken_bytes_); |
||||||
|
memory_quota_->Return(taken_bytes_); |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::Orphan() { |
||||||
|
ReclamationFunction old_reclaimers[kNumReclamationPasses]; |
||||||
|
{ |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
for (size_t i = 0; i < kNumReclamationPasses; i++) { |
||||||
|
old_reclaimers[i] = |
||||||
|
memory_quota_->reclaimers_[i].Cancel(reclamation_indices_[i], this); |
||||||
|
} |
||||||
|
} |
||||||
|
InternallyRefCounted<MemoryAllocator>::Unref(); |
||||||
|
} |
||||||
|
|
||||||
|
size_t MemoryAllocator::Reserve(MemoryRequest request) { |
||||||
|
while (true) { |
||||||
|
// Attempt to reserve memory from our pool.
|
||||||
|
auto reservation = TryReserve(request); |
||||||
|
if (reservation.has_value()) return *reservation; |
||||||
|
// If that failed, grab more from the quota and retry.
|
||||||
|
Replenish(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
absl::optional<size_t> MemoryAllocator::TryReserve(MemoryRequest request) { |
||||||
|
// How much memory should we request? (see the scaling below)
|
||||||
|
size_t scaled_size_over_min = request.max() - request.min(); |
||||||
|
// Scale the request down according to memory pressure if we have that
|
||||||
|
// flexibility.
|
||||||
|
if (scaled_size_over_min != 0) { |
||||||
|
double pressure; |
||||||
|
{ |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
pressure = memory_quota_->InstantaneousPressure(); |
||||||
|
} |
||||||
|
// Reduce allocation size proportional to the pressure > 80% usage.
|
||||||
|
if (pressure > 0.8) { |
||||||
|
scaled_size_over_min = |
||||||
|
std::min(scaled_size_over_min, |
||||||
|
static_cast<size_t>((request.max() - request.min()) * |
||||||
|
(1.0 - pressure) / 0.2)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// How much do we want to reserve?
|
||||||
|
const size_t reserve = request.min() + scaled_size_over_min; |
||||||
|
// See how many bytes are available.
|
||||||
|
size_t available = free_bytes_.load(std::memory_order_acquire); |
||||||
|
while (true) { |
||||||
|
// Does the current free pool satisfy the request?
|
||||||
|
if (available < reserve) { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
// Try to reserve the requested amount.
|
||||||
|
// If the amount of free memory changed through this loop, then available
|
||||||
|
// will be set to the new value and we'll repeat.
|
||||||
|
if (free_bytes_.compare_exchange_weak(available, available - reserve, |
||||||
|
std::memory_order_acq_rel, |
||||||
|
std::memory_order_acquire)) { |
||||||
|
return reserve; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::Replenish() { |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
// Attempt a fairly low rate exponential growth request size, bounded between
|
||||||
|
// some reasonable limits declared at top of file.
|
||||||
|
auto amount = Clamp(taken_bytes_ / 3, kMinReplenishBytes, kMaxReplenishBytes); |
||||||
|
// Take the requested amount from the quota.
|
||||||
|
gpr_log(GPR_DEBUG, "%p: take %" PRIdMAX " bytes from quota", this, amount); |
||||||
|
memory_quota_->Take(amount); |
||||||
|
// Record that we've taken it.
|
||||||
|
taken_bytes_ += amount; |
||||||
|
// Add the taken amount to the free pool.
|
||||||
|
free_bytes_.fetch_add(amount, std::memory_order_acq_rel); |
||||||
|
// See if we can add ourselves as a reclaimer.
|
||||||
|
MaybeRegisterReclaimerLocked(); |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::MaybeRegisterReclaimer() { |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
MaybeRegisterReclaimerLocked(); |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::MaybeRegisterReclaimerLocked() { |
||||||
|
// If the reclaimer is already registered, then there's nothing to do.
|
||||||
|
if (reclamation_indices_[0] != ReclaimerQueue::kInvalidIndex) return; |
||||||
|
// Grab references to the things we'll need
|
||||||
|
auto self = Ref(DEBUG_LOCATION, "reclaimer"); |
||||||
|
gpr_log(GPR_DEBUG, "%p: register reclaimer; idx=%" PRIdMAX, this, |
||||||
|
reclamation_indices_[0]); |
||||||
|
memory_quota_->reclaimers_[0].Insert( |
||||||
|
self, |
||||||
|
[self](ReclamationSweep) { |
||||||
|
MutexLock lock(&self->memory_quota_mu_); |
||||||
|
// Figure out how many bytes we can return to the quota.
|
||||||
|
size_t return_bytes = |
||||||
|
self->free_bytes_.exchange(0, std::memory_order_acq_rel); |
||||||
|
gpr_log(GPR_DEBUG, "%p: sweep reclaimer - return %" PRIdMAX, self.get(), |
||||||
|
return_bytes); |
||||||
|
if (return_bytes == 0) return; |
||||||
|
// Subtract that from our outstanding balance.
|
||||||
|
self->taken_bytes_ -= return_bytes; |
||||||
|
// And return them to the quota.
|
||||||
|
self->memory_quota_->Return(return_bytes); |
||||||
|
}, |
||||||
|
&reclamation_indices_[0]); |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::Rebind(RefCountedPtr<MemoryQuota> memory_quota) { |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
if (memory_quota_ == memory_quota) return; |
||||||
|
// Return memory to the original memory quota.
|
||||||
|
memory_quota_->Return(taken_bytes_); |
||||||
|
// Fetch back any reclaimers that are queued.
|
||||||
|
ReclamationFunction reclaimers[kNumReclamationPasses]; |
||||||
|
for (size_t i = 0; i < kNumReclamationPasses; i++) { |
||||||
|
reclaimers[i] = |
||||||
|
memory_quota_->reclaimers_[i].Cancel(reclamation_indices_[i], this); |
||||||
|
} |
||||||
|
// Switch to the new memory quota, leaving the old one in memory_quota so that
|
||||||
|
// when we unref it, we are outside of lock.
|
||||||
|
memory_quota_.swap(memory_quota); |
||||||
|
// Drop our freed memory down to zero, to avoid needing to ask the new
|
||||||
|
// quota for memory we're not currently using.
|
||||||
|
taken_bytes_ -= free_bytes_.exchange(0, std::memory_order_acq_rel); |
||||||
|
// And let the new quota know how much we're already using.
|
||||||
|
memory_quota_->Take(taken_bytes_); |
||||||
|
// Reinsert active reclaimers.
|
||||||
|
for (size_t i = 0; i < kNumReclamationPasses; i++) { |
||||||
|
if (reclaimers[i] == nullptr) continue; |
||||||
|
memory_quota_->reclaimers_[i].Insert(Ref(DEBUG_LOCATION, "rebind"), |
||||||
|
std::move(reclaimers[i]), |
||||||
|
&reclamation_indices_[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryAllocator::PostReclaimer(ReclamationPass pass, |
||||||
|
ReclamationFunction fn) { |
||||||
|
MutexLock lock(&memory_quota_mu_); |
||||||
|
auto pass_num = static_cast<int>(pass); |
||||||
|
memory_quota_->reclaimers_[pass_num].Insert( |
||||||
|
Ref(DEBUG_LOCATION, "post_reclaimer"), std::move(fn), |
||||||
|
&reclamation_indices_[pass_num]); |
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
// Reference count for a slice allocated by MemoryAllocator::MakeSlice.
|
||||||
|
// Takes care of releasing memory back when the slice is destroyed.
|
||||||
|
class SliceRefCount { |
||||||
|
public: |
||||||
|
static void Destroy(void* p) { |
||||||
|
auto* rc = static_cast<SliceRefCount*>(p); |
||||||
|
rc->~SliceRefCount(); |
||||||
|
gpr_free(rc); |
||||||
|
} |
||||||
|
SliceRefCount(RefCountedPtr<MemoryAllocator> allocator, size_t size) |
||||||
|
: base_(grpc_slice_refcount::Type::REGULAR, &refs_, Destroy, this, |
||||||
|
&base_), |
||||||
|
allocator_(std::move(allocator)), |
||||||
|
size_(size) { |
||||||
|
// Nothing to do here.
|
||||||
|
} |
||||||
|
~SliceRefCount() { allocator_->Release(size_); } |
||||||
|
|
||||||
|
grpc_slice_refcount* base_refcount() { return &base_; } |
||||||
|
|
||||||
|
private: |
||||||
|
grpc_slice_refcount base_; |
||||||
|
RefCount refs_; |
||||||
|
RefCountedPtr<MemoryAllocator> allocator_; |
||||||
|
size_t size_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
grpc_slice MemoryAllocator::MakeSlice(MemoryRequest request) { |
||||||
|
auto size = Reserve(request.Increase(sizeof(SliceRefCount))); |
||||||
|
void* p = gpr_malloc(size); |
||||||
|
new (p) SliceRefCount(Ref(DEBUG_LOCATION, "slice"), size); |
||||||
|
grpc_slice slice; |
||||||
|
slice.refcount = static_cast<SliceRefCount*>(p)->base_refcount(); |
||||||
|
slice.data.refcounted.bytes = |
||||||
|
static_cast<uint8_t*>(p) + sizeof(SliceRefCount); |
||||||
|
slice.data.refcounted.length = size - sizeof(SliceRefCount); |
||||||
|
return slice; |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// MemoryQuota
|
||||||
|
//
|
||||||
|
|
||||||
|
class MemoryQuota::WaitForSweepPromise { |
||||||
|
public: |
||||||
|
WaitForSweepPromise(WeakRefCountedPtr<MemoryQuota> memory_quota, |
||||||
|
uint64_t token) |
||||||
|
: memory_quota_(memory_quota), token_(token) {} |
||||||
|
|
||||||
|
struct Empty {}; |
||||||
|
Poll<Empty> operator()() { |
||||||
|
if (memory_quota_->reclamation_counter_.load(std::memory_order_relaxed) != |
||||||
|
token_) { |
||||||
|
return Empty{}; |
||||||
|
} else { |
||||||
|
return Pending{}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
WeakRefCountedPtr<MemoryQuota> memory_quota_; |
||||||
|
uint64_t token_; |
||||||
|
}; |
||||||
|
|
||||||
|
MemoryQuota::MemoryQuota() : DualRefCounted("MemoryQuota") { |
||||||
|
auto self = WeakRef(); |
||||||
|
|
||||||
|
// Reclamation loop:
|
||||||
|
// basically, wait until we are in overcommit (free_bytes_ < 0), and then:
|
||||||
|
// while (free_bytes_ < 0) reclaim_memory()
|
||||||
|
// ... and repeat
|
||||||
|
auto reclamation_loop = Loop(Seq( |
||||||
|
[self]() -> Poll<int> { |
||||||
|
// If there's free memory we no longer need to reclaim memory!
|
||||||
|
if (self->free_bytes_.load(std::memory_order_acquire) > 0) { |
||||||
|
return Pending{}; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
}, |
||||||
|
[self]() { |
||||||
|
// Race biases to the first thing that completes... so this will
|
||||||
|
// choose the highest priority/least destructive thing to do that's
|
||||||
|
// available.
|
||||||
|
return Race(self->reclaimers_[0].Next(), self->reclaimers_[1].Next(), |
||||||
|
self->reclaimers_[2].Next(), self->reclaimers_[3].Next()); |
||||||
|
}, |
||||||
|
[self](ReclamationFunction reclaimer) { |
||||||
|
// One of the reclaimer queues gave us a way to get back memory.
|
||||||
|
// Call the reclaimer with a token that contains enough to wake us
|
||||||
|
// up again.
|
||||||
|
const uint64_t token = |
||||||
|
self->reclamation_counter_.fetch_add(1, std::memory_order_relaxed) + |
||||||
|
1; |
||||||
|
reclaimer(ReclamationSweep(self, token)); |
||||||
|
// Return a promise that will wait for our barrier. This will be
|
||||||
|
// awoken by the token above being destroyed. So, once that token is
|
||||||
|
// destroyed, we'll be able to proceed.
|
||||||
|
return WaitForSweepPromise(self, token); |
||||||
|
}, |
||||||
|
[]() -> LoopCtl<absl::Status> { |
||||||
|
// Continue the loop!
|
||||||
|
return Continue{}; |
||||||
|
})); |
||||||
|
|
||||||
|
reclaimer_activity_ = |
||||||
|
MakeActivity(std::move(reclamation_loop), ExecCtxWakeupScheduler(), |
||||||
|
[](absl::Status status) { |
||||||
|
GPR_ASSERT(status.code() == absl::StatusCode::kCancelled); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryQuota::SetSize(size_t new_size) { |
||||||
|
size_t old_size = quota_size_.exchange(new_size, std::memory_order_relaxed); |
||||||
|
if (old_size < new_size) { |
||||||
|
// We're growing the quota.
|
||||||
|
Return(new_size - old_size); |
||||||
|
} else { |
||||||
|
// We're shrinking the quota.
|
||||||
|
Take(old_size - new_size); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryQuota::Take(size_t amount) { |
||||||
|
// If there's a request for nothing, then do nothing!
|
||||||
|
if (amount == 0) return; |
||||||
|
GPR_DEBUG_ASSERT(amount <= std::numeric_limits<intptr_t>::max()); |
||||||
|
// Grab memory from the quota.
|
||||||
|
auto prior = free_bytes_.fetch_sub(amount, std::memory_order_acq_rel); |
||||||
|
// If we push into overcommit, awake the reclaimer.
|
||||||
|
if (prior >= 0 && prior < static_cast<intptr_t>(amount)) { |
||||||
|
reclaimer_activity_->ForceWakeup(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryQuota::Orphan() { reclaimer_activity_.reset(); } |
||||||
|
|
||||||
|
void MemoryQuota::FinishReclamation(uint64_t token) { |
||||||
|
uint64_t current = reclamation_counter_.load(std::memory_order_relaxed); |
||||||
|
if (current != token) return; |
||||||
|
if (reclamation_counter_.compare_exchange_strong(current, current + 1, |
||||||
|
std::memory_order_relaxed, |
||||||
|
std::memory_order_relaxed)) { |
||||||
|
reclaimer_activity_->ForceWakeup(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void MemoryQuota::Return(size_t amount) { |
||||||
|
free_bytes_.fetch_add(amount, std::memory_order_relaxed); |
||||||
|
} |
||||||
|
|
||||||
|
size_t MemoryQuota::InstantaneousPressure() const { |
||||||
|
double free = free_bytes_.load(); |
||||||
|
if (free < 0) free = 0; |
||||||
|
double size = quota_size_.load(); |
||||||
|
if (size < 1) return 1.0; |
||||||
|
double pressure = (size - free) / size; |
||||||
|
if (pressure < 0.0) pressure = 0.0; |
||||||
|
if (pressure > 1.0) pressure = 1.0; |
||||||
|
return pressure; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace grpc_core
|
@ -0,0 +1,405 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef GRPC_CORE_LIB_RESOURCE_QUOTA_MEMORY_QUOTA_H |
||||||
|
#define GRPC_CORE_LIB_RESOURCE_QUOTA_MEMORY_QUOTA_H |
||||||
|
|
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <cstddef> |
||||||
|
#include <limits> |
||||||
|
#include <memory> |
||||||
|
#include <queue> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include <grpc/slice.h> |
||||||
|
|
||||||
|
#include "src/core/lib/gprpp/dual_ref_counted.h" |
||||||
|
#include "src/core/lib/gprpp/orphanable.h" |
||||||
|
#include "src/core/lib/gprpp/ref_counted_ptr.h" |
||||||
|
#include "src/core/lib/gprpp/sync.h" |
||||||
|
#include "src/core/lib/promise/activity.h" |
||||||
|
#include "src/core/lib/promise/poll.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
class Reclaimer; |
||||||
|
class MemoryAllocator; |
||||||
|
class MemoryQuota; |
||||||
|
|
||||||
|
// Reclamation passes.
|
||||||
|
// When memory is tight, we start trying to claim some back from memory
|
||||||
|
// reclaimers. We do this in multiple passes: if there is a less destructive
|
||||||
|
// operation available, we do that, otherwise we do something more destructive.
|
||||||
|
enum class ReclamationPass { |
||||||
|
// Non-empty reclamation ought to take index 0, but to simplify API we don't
|
||||||
|
// expose that publicly (it's an internal detail), and hence index zero is
|
||||||
|
// here unnamed.
|
||||||
|
|
||||||
|
// Benign reclamation is intended for reclamation steps that are not
|
||||||
|
// observable outside of gRPC (besides maybe causing an increase in CPU
|
||||||
|
// usage).
|
||||||
|
// Examples of such reclamation would be resizing buffers to fit the current
|
||||||
|
// load needs, rather than whatever was the peak usage requirement.
|
||||||
|
kBenign = 1, |
||||||
|
// Idle reclamation is intended for reclamation steps that are observable
|
||||||
|
// outside of gRPC, but do not cause application work to be lost.
|
||||||
|
// Examples of such reclamation would be dropping channels that are not being
|
||||||
|
// used.
|
||||||
|
kIdle = 2, |
||||||
|
// Destructive reclamation is our last resort, and is these reclamations are
|
||||||
|
// allowed to drop work - such as cancelling in flight requests.
|
||||||
|
kDestructive = 3, |
||||||
|
}; |
||||||
|
static constexpr size_t kNumReclamationPasses = 4; |
||||||
|
|
||||||
|
// Reservation request - how much memory do we want to allocate?
|
||||||
|
class MemoryRequest { |
||||||
|
public: |
||||||
|
// Request a fixed amount of memory.
|
||||||
|
// NOLINTNEXTLINE(google-explicit-constructor)
|
||||||
|
MemoryRequest(size_t n) : min_(n), max_(n) {} |
||||||
|
// Request a range of memory.
|
||||||
|
MemoryRequest(size_t min, size_t max) : min_(std::min(min, max)), max_(max) {} |
||||||
|
|
||||||
|
// Increase the size by amount
|
||||||
|
MemoryRequest Increase(size_t amount) const { |
||||||
|
return MemoryRequest(min_ + amount, max_ + amount); |
||||||
|
} |
||||||
|
|
||||||
|
size_t min() const { return min_; } |
||||||
|
size_t max() const { return max_; } |
||||||
|
|
||||||
|
private: |
||||||
|
size_t min_; |
||||||
|
size_t max_; |
||||||
|
}; |
||||||
|
|
||||||
|
// For each reclamation function run we construct a ReclamationSweep.
|
||||||
|
// When this object is finally destroyed (it may be moved several times first),
|
||||||
|
// then that reclamation is complete and we may continue the reclamation loop.
|
||||||
|
class ReclamationSweep { |
||||||
|
public: |
||||||
|
ReclamationSweep(WeakRefCountedPtr<MemoryQuota> memory_quota, |
||||||
|
uint64_t sweep_token) |
||||||
|
: memory_quota_(std::move(memory_quota)), sweep_token_(sweep_token) {} |
||||||
|
~ReclamationSweep(); |
||||||
|
|
||||||
|
ReclamationSweep(const ReclamationSweep&) = delete; |
||||||
|
ReclamationSweep& operator=(const ReclamationSweep&) = delete; |
||||||
|
ReclamationSweep(ReclamationSweep&&) = default; |
||||||
|
ReclamationSweep& operator=(ReclamationSweep&&) = default; |
||||||
|
|
||||||
|
// Has enough work been done that we would not be called upon again
|
||||||
|
// immediately to do reclamation work if we stopped and requeued. Reclaimers
|
||||||
|
// with a variable amount of work to do can use this to ascertain when they
|
||||||
|
// can stop more efficiently than going through the reclaimer queue once per
|
||||||
|
// work item.
|
||||||
|
bool IsSufficient() const; |
||||||
|
|
||||||
|
private: |
||||||
|
WeakRefCountedPtr<MemoryQuota> memory_quota_; |
||||||
|
uint64_t sweep_token_; |
||||||
|
}; |
||||||
|
|
||||||
|
using ReclamationFunction = std::function<void(ReclamationSweep)>; |
||||||
|
|
||||||
|
class ReclaimerQueue { |
||||||
|
public: |
||||||
|
using Index = size_t; |
||||||
|
|
||||||
|
// An invalid index usable as an empty value.
|
||||||
|
// This value will not be returned from Insert ever.
|
||||||
|
static constexpr Index kInvalidIndex = std::numeric_limits<Index>::max(); |
||||||
|
|
||||||
|
// Insert a new element at the back of the queue.
|
||||||
|
// If there is already an element from allocator at *index, then it is
|
||||||
|
// replaced with the new reclaimer and *index is unchanged. If there is not,
|
||||||
|
// then *index is set to the index of the newly queued entry.
|
||||||
|
// Associates the reclamation function with an allocator, and keeps that
|
||||||
|
// allocator alive, so that we can use the pointer as an ABA guard.
|
||||||
|
void Insert(RefCountedPtr<MemoryAllocator> allocator, |
||||||
|
ReclamationFunction reclaimer, Index* index) |
||||||
|
ABSL_LOCKS_EXCLUDED(mu_); |
||||||
|
// Cancel a reclamation function - returns the function if cancelled
|
||||||
|
// successfully, or nullptr if the reclamation was already begun and could not
|
||||||
|
// be cancelled. allocator must be the same as was passed to Insert.
|
||||||
|
ReclamationFunction Cancel(Index index, MemoryAllocator* allocator) |
||||||
|
ABSL_LOCKS_EXCLUDED(mu_); |
||||||
|
// Poll to see if an entry is available: returns Pending if not, or the
|
||||||
|
// removed reclamation function if so.
|
||||||
|
Poll<ReclamationFunction> PollNext() ABSL_LOCKS_EXCLUDED(mu_); |
||||||
|
|
||||||
|
// This callable is the promise backing Next - it resolves when there is an
|
||||||
|
// entry available. This really just redirects to calling PollNext().
|
||||||
|
class NextPromise { |
||||||
|
public: |
||||||
|
explicit NextPromise(ReclaimerQueue* queue) : queue_(queue) {} |
||||||
|
Poll<ReclamationFunction> operator()() { return queue_->PollNext(); } |
||||||
|
|
||||||
|
private: |
||||||
|
// Borrowed ReclaimerQueue backing this promise.
|
||||||
|
ReclaimerQueue* queue_; |
||||||
|
}; |
||||||
|
NextPromise Next() { return NextPromise(this); } |
||||||
|
|
||||||
|
private: |
||||||
|
// One entry in the reclaimer queue
|
||||||
|
struct Entry { |
||||||
|
Entry(RefCountedPtr<MemoryAllocator> allocator, |
||||||
|
ReclamationFunction reclaimer) |
||||||
|
: allocator(allocator), reclaimer(reclaimer) {} |
||||||
|
// The allocator we'd be reclaiming for.
|
||||||
|
RefCountedPtr<MemoryAllocator> allocator; |
||||||
|
// The reclamation function to call.
|
||||||
|
ReclamationFunction reclaimer; |
||||||
|
}; |
||||||
|
// Guarding mutex.
|
||||||
|
Mutex mu_; |
||||||
|
// Entries in the queue (or empty entries waiting to be queued).
|
||||||
|
// We actually queue indices into this vector - and do this so that
|
||||||
|
// we can use the memory allocator pointer as an ABA protection.
|
||||||
|
std::vector<Entry> entries_ ABSL_GUARDED_BY(mu_); |
||||||
|
// Which entries in entries_ are not allocated right now.
|
||||||
|
std::vector<size_t> free_entries_ ABSL_GUARDED_BY(mu_); |
||||||
|
// Allocated entries waiting to be consumed.
|
||||||
|
std::queue<Index> queue_ ABSL_GUARDED_BY(mu_); |
||||||
|
// Potentially one activity can be waiting for new entries on the queue.
|
||||||
|
Waker waker_ ABSL_GUARDED_BY(mu_); |
||||||
|
}; |
||||||
|
|
||||||
|
// MemoryAllocator grants the owner the ability to allocate memory from an
|
||||||
|
// underlying resource quota.
|
||||||
|
class MemoryAllocator final : public InternallyRefCounted<MemoryAllocator> { |
||||||
|
public: |
||||||
|
explicit MemoryAllocator(RefCountedPtr<MemoryQuota> memory_quota); |
||||||
|
~MemoryAllocator() override; |
||||||
|
|
||||||
|
void Orphan() override; |
||||||
|
|
||||||
|
// Rebind - Swaps the underlying quota for this allocator, taking care to
|
||||||
|
// make sure memory allocated is moved to allocations against the new quota.
|
||||||
|
void Rebind(RefCountedPtr<MemoryQuota> memory_quota) |
||||||
|
ABSL_LOCKS_EXCLUDED(memory_quota_mu_); |
||||||
|
|
||||||
|
// Reserve bytes from the quota.
|
||||||
|
// If we enter overcommit, reclamation will begin concurrently.
|
||||||
|
// Returns the number of bytes reserved.
|
||||||
|
size_t Reserve(MemoryRequest request) ABSL_LOCKS_EXCLUDED(memory_quota_mu_); |
||||||
|
|
||||||
|
// Release some bytes that were previously reserved.
|
||||||
|
void Release(size_t n) ABSL_LOCKS_EXCLUDED(memory_quota_mu_) { |
||||||
|
// Add the released memory to our free bytes counter... if this increases
|
||||||
|
// from 0 to non-zero, then we have more to do, otherwise, we're actually
|
||||||
|
// done.
|
||||||
|
if (free_bytes_.fetch_add(n, std::memory_order_release) != 0) return; |
||||||
|
MaybeRegisterReclaimer(); |
||||||
|
} |
||||||
|
|
||||||
|
// An automatic releasing reservation of memory.
|
||||||
|
class Reservation { |
||||||
|
public: |
||||||
|
Reservation() = default; |
||||||
|
Reservation(const Reservation&) = delete; |
||||||
|
Reservation& operator=(const Reservation&) = delete; |
||||||
|
Reservation(Reservation&&) = default; |
||||||
|
Reservation& operator=(Reservation&&) = default; |
||||||
|
~Reservation() { |
||||||
|
if (allocator_ != nullptr) allocator_->Release(size_); |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
friend class MemoryAllocator; |
||||||
|
Reservation(RefCountedPtr<MemoryAllocator> allocator, size_t size) |
||||||
|
: allocator_(allocator), size_(size) {} |
||||||
|
|
||||||
|
RefCountedPtr<MemoryAllocator> allocator_ = nullptr; |
||||||
|
size_t size_ = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
// Reserve bytes from the quota and automatically release them when
|
||||||
|
// Reservation is destroyed.
|
||||||
|
Reservation MakeReservation(MemoryRequest request) { |
||||||
|
return Reservation(Ref(DEBUG_LOCATION, "Reservation"), Reserve(request)); |
||||||
|
} |
||||||
|
|
||||||
|
// Post a reclaimer for some reclamation pass.
|
||||||
|
void PostReclaimer(ReclamationPass pass, |
||||||
|
std::function<void(ReclamationSweep)>); |
||||||
|
|
||||||
|
// Allocate a new object of type T, with constructor arguments.
|
||||||
|
// The returned type is wrapped, and upon destruction the reserved memory
|
||||||
|
// will be released to the allocator automatically. As such, T must have a
|
||||||
|
// virtual destructor so we can insert the necessary hook.
|
||||||
|
template <typename T, typename... Args> |
||||||
|
absl::enable_if_t<std::has_virtual_destructor<T>::value, T*> New( |
||||||
|
Args&&... args) ABSL_LOCKS_EXCLUDED(memory_quota_mu_) { |
||||||
|
// Wrap T such that when it's destroyed, we can release memory back to the
|
||||||
|
// allocator.
|
||||||
|
class Wrapper final : public T { |
||||||
|
public: |
||||||
|
explicit Wrapper(RefCountedPtr<MemoryAllocator> allocator, Args&&... args) |
||||||
|
: T(std::forward<Args>(args)...), allocator_(std::move(allocator)) {} |
||||||
|
~Wrapper() override { allocator_->Release(sizeof(*this)); } |
||||||
|
|
||||||
|
private: |
||||||
|
const RefCountedPtr<MemoryAllocator> allocator_; |
||||||
|
}; |
||||||
|
Reserve(sizeof(Wrapper)); |
||||||
|
return new Wrapper(Ref(DEBUG_LOCATION, "Wrapper"), |
||||||
|
std::forward<Args>(args)...); |
||||||
|
} |
||||||
|
|
||||||
|
// Construct a unique ptr immediately.
|
||||||
|
template <typename T, typename... Args> |
||||||
|
std::unique_ptr<T> MakeUnique(Args&&... args) |
||||||
|
ABSL_LOCKS_EXCLUDED(memory_quota_mu_) { |
||||||
|
return std::unique_ptr<T>(New<T>(std::forward<Args>(args)...)); |
||||||
|
} |
||||||
|
|
||||||
|
// Allocate a slice, using MemoryRequest to size the number of returned bytes.
|
||||||
|
// For a variable length request, check the returned slice length to verify
|
||||||
|
// how much memory was allocated.
|
||||||
|
// Takes care of reserving memory for any relevant control structures also.
|
||||||
|
grpc_slice MakeSlice(MemoryRequest request); |
||||||
|
|
||||||
|
// A C++ allocator for containers of T.
|
||||||
|
template <typename T> |
||||||
|
class Container { |
||||||
|
public: |
||||||
|
// Construct the allocator: \a underlying_allocator is borrowed, and must
|
||||||
|
// outlive this object.
|
||||||
|
explicit Container(MemoryAllocator* underlying_allocator) |
||||||
|
: underlying_allocator_(underlying_allocator) {} |
||||||
|
|
||||||
|
MemoryAllocator* underlying_allocator() const { |
||||||
|
return underlying_allocator_; |
||||||
|
} |
||||||
|
|
||||||
|
using value_type = T; |
||||||
|
template <typename U> |
||||||
|
explicit Container(const Container<U>& other) |
||||||
|
: underlying_allocator_(other.underlying_allocator()) {} |
||||||
|
T* allocate(size_t n) { |
||||||
|
underlying_allocator_->Reserve(n * sizeof(T)); |
||||||
|
return static_cast<T*>(::operator new(n * sizeof(T))); |
||||||
|
} |
||||||
|
void deallocate(T* p, size_t n) { |
||||||
|
::operator delete(p); |
||||||
|
underlying_allocator_->Release(n * sizeof(T)); |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
MemoryAllocator* underlying_allocator_; |
||||||
|
}; |
||||||
|
|
||||||
|
private: |
||||||
|
// Primitive reservation function.
|
||||||
|
absl::optional<size_t> TryReserve(MemoryRequest request) GRPC_MUST_USE_RESULT; |
||||||
|
// Replenish bytes from the quota, without blocking, possibly entering
|
||||||
|
// overcommit.
|
||||||
|
void Replenish() ABSL_LOCKS_EXCLUDED(memory_quota_mu_); |
||||||
|
// If we have not already, register a reclamation function against the quota
|
||||||
|
// to sweep any free memory back to that quota.
|
||||||
|
void MaybeRegisterReclaimer() ABSL_LOCKS_EXCLUDED(memory_quota_mu_); |
||||||
|
void MaybeRegisterReclaimerLocked() |
||||||
|
ABSL_EXCLUSIVE_LOCKS_REQUIRED(memory_quota_mu_); |
||||||
|
|
||||||
|
// Amount of memory this allocator has cached for its own use: to avoid quota
|
||||||
|
// contention, each MemoryAllocator can keep some memory in addition to what
|
||||||
|
// it is immediately using, and the quota can pull it back under memory
|
||||||
|
// pressure.
|
||||||
|
std::atomic<size_t> free_bytes_{0}; |
||||||
|
// Mutex guarding the backing resource quota.
|
||||||
|
Mutex memory_quota_mu_; |
||||||
|
// Backing resource quota.
|
||||||
|
RefCountedPtr<MemoryQuota> memory_quota_ ABSL_GUARDED_BY(memory_quota_mu_); |
||||||
|
// Amount of memory taken from the quota by this allocator.
|
||||||
|
size_t taken_bytes_ ABSL_GUARDED_BY(memory_quota_mu_) = 0; |
||||||
|
// Indices into the various reclaimer queues, used so that we can cancel
|
||||||
|
// reclamation should we shutdown or get rebound.
|
||||||
|
ReclaimerQueue::Index |
||||||
|
reclamation_indices_[kNumReclamationPasses] ABSL_GUARDED_BY( |
||||||
|
memory_quota_mu_) = { |
||||||
|
ReclaimerQueue::kInvalidIndex, ReclaimerQueue::kInvalidIndex, |
||||||
|
ReclaimerQueue::kInvalidIndex, ReclaimerQueue::kInvalidIndex}; |
||||||
|
}; |
||||||
|
|
||||||
|
// Wrapper type around std::vector to make initialization against a
|
||||||
|
// MemoryAllocator based container allocator easy.
|
||||||
|
template <typename T> |
||||||
|
class Vector : public std::vector<T, MemoryAllocator::Container<T>> { |
||||||
|
public: |
||||||
|
explicit Vector(MemoryAllocator* allocator) |
||||||
|
: std::vector<T, MemoryAllocator::Container<T>>( |
||||||
|
MemoryAllocator::Container<T>(allocator)) {} |
||||||
|
}; |
||||||
|
|
||||||
|
// MemoryQuota tracks the amount of memory available as part of a ResourceQuota.
|
||||||
|
class MemoryQuota final : public DualRefCounted<MemoryQuota> { |
||||||
|
public: |
||||||
|
MemoryQuota(); |
||||||
|
|
||||||
|
OrphanablePtr<MemoryAllocator> MakeMemoryAllocator() { |
||||||
|
return MakeOrphanable<MemoryAllocator>( |
||||||
|
Ref(DEBUG_LOCATION, "MakeMemoryAllocator")); |
||||||
|
} |
||||||
|
|
||||||
|
// Resize the quota to new_size.
|
||||||
|
void SetSize(size_t new_size); |
||||||
|
|
||||||
|
private: |
||||||
|
friend class MemoryAllocator; |
||||||
|
friend class ReclamationSweep; |
||||||
|
class WaitForSweepPromise; |
||||||
|
|
||||||
|
void Orphan() override; |
||||||
|
|
||||||
|
// Forcefully take some memory from the quota, potentially entering
|
||||||
|
// overcommit.
|
||||||
|
void Take(size_t amount); |
||||||
|
// Finish reclamation pass.
|
||||||
|
void FinishReclamation(uint64_t token); |
||||||
|
// Return some memory to the quota.
|
||||||
|
void Return(size_t amount); |
||||||
|
// Instantaneous memory pressure approximation.
|
||||||
|
size_t InstantaneousPressure() const; |
||||||
|
|
||||||
|
static constexpr intptr_t kInitialSize = std::numeric_limits<intptr_t>::max(); |
||||||
|
|
||||||
|
// The amount of memory that's free in this quota.
|
||||||
|
// We use intptr_t as a reasonable proxy for ssize_t that's portable.
|
||||||
|
// We allow arbitrary overcommit and so this must allow negative values.
|
||||||
|
std::atomic<intptr_t> free_bytes_{kInitialSize}; |
||||||
|
// The total number of bytes in this quota.
|
||||||
|
std::atomic<size_t> quota_size_{kInitialSize}; |
||||||
|
|
||||||
|
// Reclaimer queues.
|
||||||
|
ReclaimerQueue reclaimers_[kNumReclamationPasses]; |
||||||
|
// The reclaimer activity consumes reclaimers whenever we are in overcommit to
|
||||||
|
// try and get back under memory limits.
|
||||||
|
ActivityPtr reclaimer_activity_; |
||||||
|
// Each time we do a reclamation sweep, we increment this counter and give it
|
||||||
|
// to the sweep in question. In this way, should we choose to cancel a sweep
|
||||||
|
// we can do so and not get confused when the sweep reports back that it's
|
||||||
|
// completed.
|
||||||
|
// We also increment this counter on completion of a sweep, as an indicator
|
||||||
|
// that the wait has ended.
|
||||||
|
std::atomic<uint64_t> reclamation_counter_{0}; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_RESOURCE_QUOTA_MEMORY_QUOTA_H
|
@ -0,0 +1,27 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include "src/core/lib/resource_quota/resource_quota.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
ResourceQuota::ResourceQuota() |
||||||
|
: memory_quota_(MakeRefCounted<MemoryQuota>()), |
||||||
|
thread_quota_(MakeRefCounted<ThreadQuota>()) {} |
||||||
|
|
||||||
|
ResourceQuota::~ResourceQuota() = default; |
||||||
|
|
||||||
|
} // namespace grpc_core
|
@ -0,0 +1,48 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef GRPC_CORE_LIB_RESOURCE_QUOTA_RESOURCE_QUOTA_H |
||||||
|
#define GRPC_CORE_LIB_RESOURCE_QUOTA_RESOURCE_QUOTA_H |
||||||
|
|
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
#include "src/core/lib/resource_quota/thread_quota.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
class ResourceQuota : public RefCounted<ResourceQuota> { |
||||||
|
public: |
||||||
|
ResourceQuota(); |
||||||
|
~ResourceQuota() override; |
||||||
|
|
||||||
|
ResourceQuota(const ResourceQuota&) = delete; |
||||||
|
ResourceQuota& operator=(const ResourceQuota&) = delete; |
||||||
|
|
||||||
|
const RefCountedPtr<MemoryQuota>& memory_quota() const { |
||||||
|
return memory_quota_; |
||||||
|
} |
||||||
|
|
||||||
|
const RefCountedPtr<ThreadQuota>& thread_quota() const { |
||||||
|
return thread_quota_; |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota_; |
||||||
|
RefCountedPtr<ThreadQuota> thread_quota_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_RESOURCE_QUOTA_RESOURCE_QUOTA_H
|
@ -0,0 +1,43 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include "src/core/lib/resource_quota/thread_quota.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
ThreadQuota::ThreadQuota() = default; |
||||||
|
|
||||||
|
ThreadQuota::~ThreadQuota() = default; |
||||||
|
|
||||||
|
void ThreadQuota::SetMax(size_t new_max) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
max_ = new_max; |
||||||
|
} |
||||||
|
|
||||||
|
bool ThreadQuota::Reserve(size_t num_threads) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
if (allocated_ + num_threads > max_) return false; |
||||||
|
allocated_ += num_threads; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void ThreadQuota::Release(size_t num_threads) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
GPR_ASSERT(num_threads <= allocated_); |
||||||
|
allocated_ -= num_threads; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace grpc_core
|
@ -0,0 +1,55 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#ifndef GRPC_CORE_LIB_RESOURCE_QUOTA_THREAD_QUOTA_H |
||||||
|
#define GRPC_CORE_LIB_RESOURCE_QUOTA_THREAD_QUOTA_H |
||||||
|
|
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include <cstddef> |
||||||
|
|
||||||
|
#include "src/core/lib/gprpp/ref_counted.h" |
||||||
|
#include "src/core/lib/gprpp/sync.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
// Tracks the amount of threads in a resource quota.
|
||||||
|
class ThreadQuota : public RefCounted<ThreadQuota> { |
||||||
|
public: |
||||||
|
ThreadQuota(); |
||||||
|
~ThreadQuota() override; |
||||||
|
|
||||||
|
ThreadQuota(const ThreadQuota&) = delete; |
||||||
|
ThreadQuota& operator=(const ThreadQuota&) = delete; |
||||||
|
|
||||||
|
// Set the maximum number of threads that can be used by this quota.
|
||||||
|
// If there are more, new reservations will fail until the quota is available.
|
||||||
|
void SetMax(size_t new_max); |
||||||
|
|
||||||
|
// Try to allocate some number of threads.
|
||||||
|
// Returns true if the allocation succeeded, false otherwise.
|
||||||
|
bool Reserve(size_t num_threads); |
||||||
|
|
||||||
|
// Release some number of threads.
|
||||||
|
void Release(size_t num_threads); |
||||||
|
|
||||||
|
private: |
||||||
|
Mutex mu_; |
||||||
|
size_t allocated_ ABSL_GUARDED_BY(mu_) = 0; |
||||||
|
size_t max_ ABSL_GUARDED_BY(mu_) = std::numeric_limits<size_t>::max(); |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_RESOURCE_QUOTA_THREAD_QUOTA_H
|
@ -0,0 +1,94 @@ |
|||||||
|
# Copyright 2021 gRPC 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 |
||||||
|
# |
||||||
|
# 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. |
||||||
|
|
||||||
|
load("//bazel:grpc_build_system.bzl", "grpc_cc_test", "grpc_package") |
||||||
|
|
||||||
|
licenses(["notice"]) |
||||||
|
|
||||||
|
grpc_package(name = "test/core/promise") |
||||||
|
|
||||||
|
load("//test/core/util:grpc_fuzzer.bzl", "grpc_proto_fuzzer") |
||||||
|
|
||||||
|
grpc_cc_test( |
||||||
|
name = "memory_quota_test", |
||||||
|
srcs = ["memory_quota_test.cc"], |
||||||
|
external_deps = [ |
||||||
|
"gtest", |
||||||
|
], |
||||||
|
language = "c++", |
||||||
|
uses_polling = False, |
||||||
|
deps = [ |
||||||
|
"//:memory_quota", |
||||||
|
"//test/core/util:grpc_suppressions", |
||||||
|
], |
||||||
|
) |
||||||
|
|
||||||
|
grpc_cc_test( |
||||||
|
name = "thread_quota_test", |
||||||
|
srcs = ["thread_quota_test.cc"], |
||||||
|
external_deps = [ |
||||||
|
"gtest", |
||||||
|
], |
||||||
|
language = "c++", |
||||||
|
uses_polling = False, |
||||||
|
deps = [ |
||||||
|
"//:thread_quota", |
||||||
|
"//test/core/util:grpc_suppressions", |
||||||
|
], |
||||||
|
) |
||||||
|
|
||||||
|
grpc_cc_test( |
||||||
|
name = "resource_quota_test", |
||||||
|
srcs = ["resource_quota_test.cc"], |
||||||
|
external_deps = [ |
||||||
|
"gtest", |
||||||
|
], |
||||||
|
language = "c++", |
||||||
|
uses_polling = False, |
||||||
|
deps = [ |
||||||
|
"//:resource_quota", |
||||||
|
"//test/core/util:grpc_suppressions", |
||||||
|
], |
||||||
|
) |
||||||
|
|
||||||
|
grpc_cc_test( |
||||||
|
name = "memory_quota_stress_test", |
||||||
|
srcs = ["memory_quota_stress_test.cc"], |
||||||
|
language = "c++", |
||||||
|
# We only run this test under Linux, and really only care about the |
||||||
|
# TSAN results. |
||||||
|
tags = [ |
||||||
|
"no_mac", |
||||||
|
"no_windows", |
||||||
|
], |
||||||
|
uses_polling = False, |
||||||
|
deps = [ |
||||||
|
"//:memory_quota", |
||||||
|
"//test/core/util:grpc_suppressions", |
||||||
|
], |
||||||
|
) |
||||||
|
|
||||||
|
grpc_proto_fuzzer( |
||||||
|
name = "memory_quota_fuzzer", |
||||||
|
srcs = ["memory_quota_fuzzer.cc"], |
||||||
|
corpus = "memory_quota_fuzzer_corpus", |
||||||
|
language = "C++", |
||||||
|
proto = "memory_quota_fuzzer.proto", |
||||||
|
tags = ["no_windows"], |
||||||
|
uses_polling = False, |
||||||
|
deps = [ |
||||||
|
"//:memory_quota", |
||||||
|
"//test/core/util:grpc_test_util", |
||||||
|
], |
||||||
|
) |
@ -0,0 +1,169 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 <map> |
||||||
|
|
||||||
|
#include "src/core/lib/gpr/useful.h" |
||||||
|
#include "src/core/lib/iomgr/exec_ctx.h" |
||||||
|
#include "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
#include "src/libfuzzer/libfuzzer_macro.h" |
||||||
|
#include "test/core/resource_quota/memory_quota_fuzzer.pb.h" |
||||||
|
|
||||||
|
bool squelch = true; |
||||||
|
bool leak_check = true; |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
namespace { |
||||||
|
ReclamationPass MapReclamationPass(memory_quota_fuzzer::Reclaimer::Pass pass) { |
||||||
|
switch (pass) { |
||||||
|
case memory_quota_fuzzer::Reclaimer::BENIGN: |
||||||
|
return ReclamationPass::kBenign; |
||||||
|
case memory_quota_fuzzer::Reclaimer::IDLE: |
||||||
|
return ReclamationPass::kIdle; |
||||||
|
case memory_quota_fuzzer::Reclaimer::DESTRUCTIVE: |
||||||
|
return ReclamationPass::kDestructive; |
||||||
|
default: |
||||||
|
return ReclamationPass::kBenign; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Fuzzer { |
||||||
|
public: |
||||||
|
void Run(const memory_quota_fuzzer::Msg& msg) { |
||||||
|
grpc_core::ExecCtx exec_ctx; |
||||||
|
RunMsg(msg); |
||||||
|
memory_quotas_.clear(); |
||||||
|
memory_allocators_.clear(); |
||||||
|
allocations_.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
void RunMsg(const memory_quota_fuzzer::Msg& msg) { |
||||||
|
for (int i = 0; i < msg.actions_size(); ++i) { |
||||||
|
const auto& action = msg.actions(i); |
||||||
|
switch (action.action_type_case()) { |
||||||
|
case memory_quota_fuzzer::Action::kFlushExecCtx: |
||||||
|
ExecCtx::Get()->Flush(); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kCreateQuota: |
||||||
|
memory_quotas_.emplace(action.quota(), |
||||||
|
RefCountedPtr<MemoryQuota>(new MemoryQuota())); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kDeleteQuota: |
||||||
|
memory_quotas_.erase(action.quota()); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kCreateAllocator: |
||||||
|
WithQuota(action.quota(), |
||||||
|
[this, action](RefCountedPtr<MemoryQuota> q) { |
||||||
|
memory_allocators_.emplace(action.allocator(), |
||||||
|
q->MakeMemoryAllocator()); |
||||||
|
}); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kDeleteAllocator: |
||||||
|
memory_allocators_.erase(action.allocator()); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kSetQuotaSize: |
||||||
|
WithQuota(action.quota(), [action](RefCountedPtr<MemoryQuota> q) { |
||||||
|
q->SetSize(Clamp(action.set_quota_size(), uint64_t{0}, |
||||||
|
uint64_t{std::numeric_limits<ssize_t>::max()})); |
||||||
|
}); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kRebindQuota: |
||||||
|
WithQuota(action.quota(), |
||||||
|
[this, action](RefCountedPtr<MemoryQuota> q) { |
||||||
|
WithAllocator(action.allocator(), |
||||||
|
[q](MemoryAllocator* a) { a->Rebind(q); }); |
||||||
|
}); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kCreateAllocation: { |
||||||
|
auto min = action.create_allocation().min(); |
||||||
|
auto max = action.create_allocation().max(); |
||||||
|
if (min > max) break; |
||||||
|
MemoryRequest req(min, max); |
||||||
|
WithAllocator( |
||||||
|
action.allocator(), [this, action, req](MemoryAllocator* a) { |
||||||
|
auto alloc = a->MakeReservation(req); |
||||||
|
allocations_.emplace(action.allocation(), std::move(alloc)); |
||||||
|
}); |
||||||
|
} break; |
||||||
|
case memory_quota_fuzzer::Action::kDeleteAllocation: |
||||||
|
allocations_.erase(action.allocation()); |
||||||
|
break; |
||||||
|
case memory_quota_fuzzer::Action::kPostReclaimer: { |
||||||
|
std::function<void(ReclamationSweep)> reclaimer; |
||||||
|
auto cfg = action.post_reclaimer(); |
||||||
|
if (cfg.synchronous()) { |
||||||
|
reclaimer = [this, cfg](ReclamationSweep) { RunMsg(cfg.msg()); }; |
||||||
|
} else { |
||||||
|
reclaimer = [cfg, this](ReclamationSweep sweep) { |
||||||
|
struct Args { |
||||||
|
ReclamationSweep sweep; |
||||||
|
memory_quota_fuzzer::Msg msg; |
||||||
|
Fuzzer* fuzzer; |
||||||
|
}; |
||||||
|
auto* args = new Args{std::move(sweep), cfg.msg(), this}; |
||||||
|
auto* closure = GRPC_CLOSURE_CREATE( |
||||||
|
[](void* arg, grpc_error*) { |
||||||
|
auto* args = static_cast<Args*>(arg); |
||||||
|
args->fuzzer->RunMsg(args->msg); |
||||||
|
delete args; |
||||||
|
}, |
||||||
|
args, nullptr); |
||||||
|
ExecCtx::Get()->Run(DEBUG_LOCATION, closure, GRPC_ERROR_NONE); |
||||||
|
}; |
||||||
|
auto pass = MapReclamationPass(cfg.pass()); |
||||||
|
WithAllocator(action.allocator(), |
||||||
|
[pass, reclaimer](MemoryAllocator* a) { |
||||||
|
a->PostReclaimer(pass, reclaimer); |
||||||
|
}); |
||||||
|
} |
||||||
|
} break; |
||||||
|
case memory_quota_fuzzer::Action::ACTION_TYPE_NOT_SET: |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
template <typename F> |
||||||
|
void WithQuota(int quota, F f) { |
||||||
|
auto it = memory_quotas_.find(quota); |
||||||
|
if (it == memory_quotas_.end()) return; |
||||||
|
f(it->second); |
||||||
|
} |
||||||
|
|
||||||
|
template <typename F> |
||||||
|
void WithAllocator(int allocator, F f) { |
||||||
|
auto it = memory_allocators_.find(allocator); |
||||||
|
if (it == memory_allocators_.end()) return; |
||||||
|
f(it->second.get()); |
||||||
|
} |
||||||
|
|
||||||
|
std::map<int, RefCountedPtr<MemoryQuota>> memory_quotas_; |
||||||
|
std::map<int, OrphanablePtr<MemoryAllocator>> memory_allocators_; |
||||||
|
std::map<int, MemoryAllocator::Reservation> allocations_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
static void dont_log(gpr_log_func_args* /*args*/) {} |
||||||
|
|
||||||
|
DEFINE_PROTO_FUZZER(const memory_quota_fuzzer::Msg& msg) { |
||||||
|
if (squelch) gpr_set_log_function(dont_log); |
||||||
|
gpr_log_verbosity_init(); |
||||||
|
grpc_tracer_init(); |
||||||
|
grpc_core::Fuzzer().Run(msg); |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
// Copyright 2021 gRPC 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 |
||||||
|
// |
||||||
|
// 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. |
||||||
|
|
||||||
|
syntax = "proto3"; |
||||||
|
|
||||||
|
package memory_quota_fuzzer; |
||||||
|
|
||||||
|
message Empty {} |
||||||
|
|
||||||
|
message Reclaimer { |
||||||
|
enum Pass { |
||||||
|
BENIGN = 0; |
||||||
|
IDLE = 1; |
||||||
|
DESTRUCTIVE = 2; |
||||||
|
} |
||||||
|
bool synchronous = 1; |
||||||
|
Pass pass = 2; |
||||||
|
Msg msg = 3; |
||||||
|
} |
||||||
|
|
||||||
|
message AllocationRequest { |
||||||
|
uint32 min = 1; |
||||||
|
uint32 max = 2; |
||||||
|
} |
||||||
|
|
||||||
|
message Action { |
||||||
|
int32 quota = 1; |
||||||
|
int32 allocator = 2; |
||||||
|
int32 allocation = 3; |
||||||
|
oneof action_type { |
||||||
|
Empty flush_exec_ctx = 7; |
||||||
|
Empty create_quota = 10; |
||||||
|
Empty delete_quota = 11; |
||||||
|
Empty create_allocator = 12; |
||||||
|
Empty delete_allocator = 13; |
||||||
|
uint64 set_quota_size = 14; |
||||||
|
Empty rebind_quota = 15; |
||||||
|
AllocationRequest create_allocation = 16; |
||||||
|
Empty delete_allocation = 17; |
||||||
|
Reclaimer post_reclaimer = 18; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
message Msg { |
||||||
|
repeated Action actions = 2; |
||||||
|
} |
@ -0,0 +1,210 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 <random> |
||||||
|
#include <thread> |
||||||
|
|
||||||
|
#include "src/core/lib/iomgr/exec_ctx.h" |
||||||
|
#include "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
namespace { |
||||||
|
class StressTest { |
||||||
|
public: |
||||||
|
// Create a stress test with some size.
|
||||||
|
StressTest(size_t num_quotas, size_t num_allocators) { |
||||||
|
for (size_t i = 0; i < num_quotas; ++i) { |
||||||
|
quotas_.emplace_back(new MemoryQuota()); |
||||||
|
} |
||||||
|
std::random_device g; |
||||||
|
std::uniform_int_distribution<size_t> dist(0, num_quotas - 1); |
||||||
|
for (size_t i = 0; i < num_allocators; ++i) { |
||||||
|
allocators_.emplace_back(quotas_[dist(g)]->MakeMemoryAllocator()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Run the thread for some period of time.
|
||||||
|
void Run(int seconds) { |
||||||
|
std::vector<std::thread> threads; |
||||||
|
|
||||||
|
// A few threads constantly rebinding allocators to different quotas.
|
||||||
|
threads.reserve(2); |
||||||
|
for (int i = 0; i < 2; i++) threads.push_back(Run(Rebinder)); |
||||||
|
// And another few threads constantly resizing quotas.
|
||||||
|
for (int i = 0; i < 2; i++) threads.push_back(Run(Resizer)); |
||||||
|
|
||||||
|
// For each (allocator, pass), start a thread continuously allocating from
|
||||||
|
// that allocator. Whenever the first allocation is made, schedule a
|
||||||
|
// reclaimer for that pass.
|
||||||
|
for (size_t i = 0; i < allocators_.size(); i++) { |
||||||
|
auto* allocator = allocators_[i].get(); |
||||||
|
for (ReclamationPass pass : |
||||||
|
{ReclamationPass::kBenign, ReclamationPass::kIdle, |
||||||
|
ReclamationPass::kDestructive}) { |
||||||
|
threads.push_back(Run([allocator, pass](StatePtr st) mutable { |
||||||
|
if (st->RememberReservation( |
||||||
|
allocator->MakeReservation(st->RandomRequest()))) { |
||||||
|
allocator->PostReclaimer( |
||||||
|
pass, [st](ReclamationSweep) { st->ForgetReservations(); }); |
||||||
|
} |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// All threads started, wait for the alloted time.
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(seconds)); |
||||||
|
|
||||||
|
// Toggle the completion bit, and then wait for the threads.
|
||||||
|
done_.store(true, std::memory_order_relaxed); |
||||||
|
while (!threads.empty()) { |
||||||
|
threads.back().join(); |
||||||
|
threads.pop_back(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
// Per-thread state.
|
||||||
|
// Not everything is used on every thread, but it's not terrible having the
|
||||||
|
// extra state around and it does simplify things somewhat.
|
||||||
|
class State { |
||||||
|
public: |
||||||
|
explicit State(StressTest* test) |
||||||
|
: test_(test), |
||||||
|
quotas_distribution_(0, test_->quotas_.size() - 1), |
||||||
|
allocators_distribution_(0, test_->allocators_.size() - 1), |
||||||
|
size_distribution_(1, 4 * 1024 * 1024), |
||||||
|
quota_size_distribution_(1024 * 1024, size_t(8) * 1024 * 1024 * 1024), |
||||||
|
choose_variable_size_(1, 100) {} |
||||||
|
|
||||||
|
// Choose a random quota, and return an owned pointer to it.
|
||||||
|
// Not thread-safe, only callable from the owning thread.
|
||||||
|
RefCountedPtr<MemoryQuota> RandomQuota() { |
||||||
|
return test_->quotas_[quotas_distribution_(g_)]; |
||||||
|
} |
||||||
|
|
||||||
|
// Choose a random allocator, and return a borrowed pointer to it.
|
||||||
|
// Not thread-safe, only callable from the owning thread.
|
||||||
|
MemoryAllocator* RandomAllocator() { |
||||||
|
return test_->allocators_[allocators_distribution_(g_)].get(); |
||||||
|
} |
||||||
|
|
||||||
|
// Random memory request size - 1% of allocations are chosen to be variable
|
||||||
|
// sized - the rest are fixed (since variable sized create some contention
|
||||||
|
// problems between allocator threads of different passes on the same
|
||||||
|
// allocator).
|
||||||
|
// Not thread-safe, only callable from the owning thread.
|
||||||
|
MemoryRequest RandomRequest() { |
||||||
|
size_t a = size_distribution_(g_); |
||||||
|
if (choose_variable_size_(g_) == 1) { |
||||||
|
size_t b = size_distribution_(g_); |
||||||
|
return MemoryRequest(std::min(a, b), std::max(a, b)); |
||||||
|
} |
||||||
|
return MemoryRequest(a); |
||||||
|
} |
||||||
|
|
||||||
|
// Choose a new size for a backing quota.
|
||||||
|
// Not thread-safe, only callable from the owning thread.
|
||||||
|
size_t RandomQuotaSize() { return quota_size_distribution_(g_); } |
||||||
|
|
||||||
|
// Remember a reservation, return true if it's the first remembered since
|
||||||
|
// the last reclamation.
|
||||||
|
// Thread-safe.
|
||||||
|
bool RememberReservation(MemoryAllocator::Reservation reservation) |
||||||
|
ABSL_LOCKS_EXCLUDED(mu_) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
bool was_empty = reservations_.empty(); |
||||||
|
reservations_.emplace_back(std::move(reservation)); |
||||||
|
return was_empty; |
||||||
|
} |
||||||
|
|
||||||
|
// Return all reservations made until this moment, so that they can be
|
||||||
|
// dropped.
|
||||||
|
std::vector<MemoryAllocator::Reservation> ForgetReservations() |
||||||
|
ABSL_LOCKS_EXCLUDED(mu_) { |
||||||
|
MutexLock lock(&mu_); |
||||||
|
return std::move(reservations_); |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
// Owning test.
|
||||||
|
StressTest* const test_; |
||||||
|
// Random number generator.
|
||||||
|
std::mt19937 g_{std::random_device()()}; |
||||||
|
// Distribution to choose a quota.
|
||||||
|
std::uniform_int_distribution<size_t> quotas_distribution_; |
||||||
|
// Distribution to choose an allocator.
|
||||||
|
std::uniform_int_distribution<size_t> allocators_distribution_; |
||||||
|
// Distribution to choose an allocation size.
|
||||||
|
std::uniform_int_distribution<size_t> size_distribution_; |
||||||
|
// Distribution to choose a quota size.
|
||||||
|
std::uniform_int_distribution<size_t> quota_size_distribution_; |
||||||
|
// Distribution to choose whether to make a variable-sized allocation.
|
||||||
|
std::uniform_int_distribution<size_t> choose_variable_size_; |
||||||
|
|
||||||
|
// Mutex to protect the reservation list.
|
||||||
|
Mutex mu_; |
||||||
|
// Reservations remembered by this thread.
|
||||||
|
std::vector<MemoryAllocator::Reservation> reservations_ |
||||||
|
ABSL_GUARDED_BY(mu_); |
||||||
|
}; |
||||||
|
// Type alias since we always pass around these shared pointers.
|
||||||
|
using StatePtr = std::shared_ptr<State>; |
||||||
|
|
||||||
|
// Choose one allocator, one quota, rebind the allocator to the quota.
|
||||||
|
static void Rebinder(StatePtr st) { |
||||||
|
MemoryAllocator* allocator = st->RandomAllocator(); |
||||||
|
RefCountedPtr<MemoryQuota> quota = st->RandomQuota(); |
||||||
|
allocator->Rebind(std::move(quota)); |
||||||
|
} |
||||||
|
|
||||||
|
// Choose one allocator, resize it to a randomly chosen size.
|
||||||
|
static void Resizer(StatePtr st) { |
||||||
|
RefCountedPtr<MemoryQuota> quota = st->RandomQuota(); |
||||||
|
size_t size = st->RandomQuotaSize(); |
||||||
|
quota->SetSize(size); |
||||||
|
} |
||||||
|
|
||||||
|
// Create a thread that repeatedly runs a function until the test is done.
|
||||||
|
// We create one instance of State that we pass as a StatePtr to said
|
||||||
|
// function as the current overall state for this thread.
|
||||||
|
// Monitors done_ to see when we should stop.
|
||||||
|
// Ensures there's an ExecCtx for each iteration of the loop.
|
||||||
|
template <typename Fn> |
||||||
|
std::thread Run(Fn fn) { |
||||||
|
return std::thread([this, fn]() mutable { |
||||||
|
auto state = std::make_shared<State>(this); |
||||||
|
while (!done_.load(std::memory_order_relaxed)) { |
||||||
|
ExecCtx exec_ctx; |
||||||
|
fn(state); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Flag for when the test is completed.
|
||||||
|
std::atomic<bool> done_{false}; |
||||||
|
|
||||||
|
// Memory quotas to test against. We build this up at construction time, but
|
||||||
|
// then don't resize, so we can load from it continuously from all of the
|
||||||
|
// threads.
|
||||||
|
std::vector<RefCountedPtr<MemoryQuota>> quotas_; |
||||||
|
// Memory allocators to test against. Similarly, built at construction time,
|
||||||
|
// and then the shape of this vector is not changed.
|
||||||
|
std::vector<OrphanablePtr<MemoryAllocator>> allocators_; |
||||||
|
}; |
||||||
|
} // namespace
|
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
int main(int, char**) { grpc_core::StressTest(16, 64).Run(8); } |
@ -0,0 +1,176 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 "src/core/lib/resource_quota/memory_quota.h" |
||||||
|
|
||||||
|
#include <gtest/gtest.h> |
||||||
|
|
||||||
|
#include "absl/synchronization/notification.h" |
||||||
|
|
||||||
|
#include "src/core/lib/iomgr/exec_ctx.h" |
||||||
|
#include "src/core/lib/slice/slice_refcount.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
namespace testing { |
||||||
|
|
||||||
|
//
|
||||||
|
// Helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
template <size_t kSize> |
||||||
|
struct Sized { |
||||||
|
char blah[kSize]; |
||||||
|
virtual ~Sized() {} |
||||||
|
}; |
||||||
|
|
||||||
|
//
|
||||||
|
// MemoryRequestTest
|
||||||
|
//
|
||||||
|
|
||||||
|
TEST(MemoryRequestTest, ConversionFromSize) { |
||||||
|
MemoryRequest request = 3; |
||||||
|
EXPECT_EQ(request.min(), 3); |
||||||
|
EXPECT_EQ(request.max(), 3); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryRequestTest, MinMax) { |
||||||
|
MemoryRequest request(3, 7); |
||||||
|
EXPECT_EQ(request.min(), 3); |
||||||
|
EXPECT_EQ(request.max(), 7); |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// MemoryQuotaTest
|
||||||
|
//
|
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, NoOp) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, CreateAllocatorNoOp) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, CreateObjectFromAllocator) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
auto object = memory_allocator->MakeUnique<Sized<4096>>(); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, CreateSomeObjectsAndExpectReclamation) { |
||||||
|
ExecCtx exec_ctx; |
||||||
|
|
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
memory_quota->SetSize(4096); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
auto object = memory_allocator->MakeUnique<Sized<2048>>(); |
||||||
|
|
||||||
|
memory_allocator->PostReclaimer( |
||||||
|
ReclamationPass::kDestructive, |
||||||
|
[&object](ReclamationSweep) { object.reset(); }); |
||||||
|
auto object2 = memory_allocator->MakeUnique<Sized<2048>>(); |
||||||
|
exec_ctx.Flush(); |
||||||
|
EXPECT_EQ(object.get(), nullptr); |
||||||
|
|
||||||
|
memory_allocator->PostReclaimer( |
||||||
|
ReclamationPass::kDestructive, |
||||||
|
[&object2](ReclamationSweep) { object2.reset(); }); |
||||||
|
auto object3 = memory_allocator->MakeUnique<Sized<2048>>(); |
||||||
|
exec_ctx.Flush(); |
||||||
|
EXPECT_EQ(object2.get(), nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, BasicRebind) { |
||||||
|
ExecCtx exec_ctx; |
||||||
|
|
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
memory_quota->SetSize(4096); |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota2 = MakeRefCounted<MemoryQuota>(); |
||||||
|
memory_quota2->SetSize(4096); |
||||||
|
|
||||||
|
auto memory_allocator = memory_quota2->MakeMemoryAllocator(); |
||||||
|
auto object = memory_allocator->MakeUnique<Sized<2048>>(); |
||||||
|
|
||||||
|
memory_allocator->Rebind(memory_quota); |
||||||
|
auto memory_allocator2 = memory_quota2->MakeMemoryAllocator(); |
||||||
|
|
||||||
|
memory_allocator2->PostReclaimer(ReclamationPass::kDestructive, |
||||||
|
[](ReclamationSweep) { |
||||||
|
// Taken memory should be reassigned to
|
||||||
|
// memory_quota, so this should never be
|
||||||
|
// reached.
|
||||||
|
abort(); |
||||||
|
}); |
||||||
|
|
||||||
|
memory_allocator->PostReclaimer(ReclamationPass::kDestructive, |
||||||
|
[&object](ReclamationSweep) { |
||||||
|
// The new memory allocator should reclaim
|
||||||
|
// the object allocated against the previous
|
||||||
|
// quota because that's now part of this
|
||||||
|
// quota.
|
||||||
|
object.reset(); |
||||||
|
}); |
||||||
|
|
||||||
|
auto object2 = memory_allocator->MakeUnique<Sized<2048>>(); |
||||||
|
exec_ctx.Flush(); |
||||||
|
EXPECT_EQ(object.get(), nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, ReserveRangeNoPressure) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
size_t total = 0; |
||||||
|
for (int i = 0; i < 10000; i++) { |
||||||
|
auto n = memory_allocator->Reserve(MemoryRequest(100, 40000)); |
||||||
|
EXPECT_EQ(n, 40000); |
||||||
|
total += n; |
||||||
|
} |
||||||
|
memory_allocator->Release(total); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, MakeSlice) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
std::vector<grpc_slice> slices; |
||||||
|
for (int i = 1; i < 1000; i++) { |
||||||
|
int min = i; |
||||||
|
int max = 10 * i - 9; |
||||||
|
slices.push_back(memory_allocator->MakeSlice(MemoryRequest(min, max))); |
||||||
|
} |
||||||
|
for (grpc_slice slice : slices) { |
||||||
|
grpc_slice_unref_internal(slice); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
TEST(MemoryQuotaTest, ContainerAllocator) { |
||||||
|
RefCountedPtr<MemoryQuota> memory_quota = MakeRefCounted<MemoryQuota>(); |
||||||
|
auto memory_allocator = memory_quota->MakeMemoryAllocator(); |
||||||
|
Vector<int> vec(memory_allocator.get()); |
||||||
|
for (int i = 0; i < 100000; i++) { |
||||||
|
vec.push_back(i); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace testing
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
// Hook needed to run ExecCtx outside of iomgr.
|
||||||
|
void grpc_set_default_iomgr_platform() {} |
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
gpr_log_verbosity_init(); |
||||||
|
return RUN_ALL_TESTS(); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 "src/core/lib/resource_quota/resource_quota.h" |
||||||
|
|
||||||
|
#include <gtest/gtest.h> |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
namespace testing { |
||||||
|
|
||||||
|
TEST(ResourceQuotaTest, Works) { |
||||||
|
auto q = MakeRefCounted<ResourceQuota>(); |
||||||
|
EXPECT_NE(q->thread_quota(), nullptr); |
||||||
|
EXPECT_NE(q->memory_quota(), nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace testing
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
// Hook needed to run ExecCtx outside of iomgr.
|
||||||
|
void grpc_set_default_iomgr_platform() {} |
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
return RUN_ALL_TESTS(); |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
// Copyright 2021 gRPC 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
|
||||||
|
//
|
||||||
|
// 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 "src/core/lib/resource_quota/thread_quota.h" |
||||||
|
|
||||||
|
#include <gtest/gtest.h> |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
namespace testing { |
||||||
|
|
||||||
|
TEST(ThreadQuotaTest, Works) { |
||||||
|
auto q = MakeRefCounted<ThreadQuota>(); |
||||||
|
EXPECT_TRUE(q->Reserve(128)); |
||||||
|
q->SetMax(10); |
||||||
|
EXPECT_FALSE(q->Reserve(128)); |
||||||
|
EXPECT_FALSE(q->Reserve(1)); |
||||||
|
q->Release(118); |
||||||
|
EXPECT_FALSE(q->Reserve(1)); |
||||||
|
q->Release(1); |
||||||
|
EXPECT_TRUE(q->Reserve(1)); |
||||||
|
EXPECT_FALSE(q->Reserve(1)); |
||||||
|
q->Release(10); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace testing
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
// Hook needed to run ExecCtx outside of iomgr.
|
||||||
|
void grpc_set_default_iomgr_platform() {} |
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
return RUN_ALL_TESTS(); |
||||||
|
} |
Loading…
Reference in new issue