blob: 5ac0883d9d08488af2990ba0724f313b02975fff [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef BASE_TASK_SEQUENCE_MANAGER_LAZILY_DEALLOCATED_DEQUE_H_
#define BASE_TASK_SEQUENCE_MANAGER_LAZILY_DEALLOCATED_DEQUE_H_
#include <algorithm>
#include <cmath>
#include <memory>
#include <vector>
#include "base/gtest_prod_util.h"
#include "base/logging.h"
#include "base/time/time.h"
namespace base {
namespace sequence_manager {
namespace internal {
// A LazilyDeallocatedDeque specialized for the SequenceManager's usage
// patterns. The queue generally grows while tasks are added and then removed
// until empty and the cycle repeats.
//
// The main difference between sequence_manager::LazilyDeallocatedDeque and
// others is memory management. For performance (memory allocation isn't free)
// we don't automatically reclaiming memory when the queue becomes empty.
// Instead we rely on the surrounding code periodically calling
// MaybeShrinkQueue, ideally when the queue is empty.
//
// We keep track of the maximum recent queue size and rate limit
// MaybeShrinkQueue to avoid unnecessary churn.
//
// NB this queue isn't by itself thread safe.
template <typename T>
class LazilyDeallocatedDeque {
public:
enum {
// Minimum allocation for a ring. Note a ring of size 4 will only hold up to
// 3 elements.
kMinimumRingSize = 4,
// Maximum "wasted" capacity allowed when considering if we should resize
// the backing store.
kReclaimThreshold = 16,
// Used to rate limit how frequently MaybeShrinkQueue actually shrinks the
// queue.
kMinimumShrinkIntervalInSeconds = 5
};
LazilyDeallocatedDeque() {}
~LazilyDeallocatedDeque() { clear(); }
bool empty() const { return size_ == 0; }
size_t max_size() const { return max_size_; }
size_t size() const { return size_; }
size_t capacity() const {
size_t capacity = 0;
for (const Ring* iter = head_.get(); iter; iter = iter->next_.get()) {
capacity += iter->capacity();
}
return capacity;
}
void clear() {
while (head_) {
head_ = std::move(head_->next_);
}
tail_ = nullptr;
size_ = 0;
}
// Assumed to be an uncommon operation.
void push_front(T t) {
if (!head_) {
DCHECK(!tail_);
head_ = std::make_unique<Ring>(kMinimumRingSize);
tail_ = head_.get();
}
// Grow if needed, by the minimum amount.
if (!head_->CanPush()) {
std::unique_ptr<Ring> new_ring = std::make_unique<Ring>(kMinimumRingSize);
new_ring->next_ = std::move(head_);
head_ = std::move(new_ring);
}
head_->push_front(std::move(t));
max_size_ = std::max(max_size_, ++size_);
}
// Assumed to be a common operation.
void push_back(T t) {
if (!head_) {
DCHECK(!tail_);
head_ = std::make_unique<Ring>(kMinimumRingSize);
tail_ = head_.get();
}
// Grow if needed.
if (!tail_->CanPush()) {
tail_->next_ = std::make_unique<Ring>(tail_->capacity() * 2);
tail_ = tail_->next_.get();
}
tail_->push_back(std::move(t));
max_size_ = std::max(max_size_, ++size_);
}
T& front() {
DCHECK(head_);
return head_->front();
}
const T& front() const {
DCHECK(head_);
return head_->front();
}
T& back() {
DCHECK(tail_);
return tail_->back();
}
const T& back() const {
DCHECK(tail_);
return tail_->back();
}
void pop_front() {
DCHECK(head_);
DCHECK(!head_->empty());
DCHECK(tail_);
DCHECK_GT(size_, 0u);
head_->pop_front();
// If the ring has become empty and we have several rings then, remove the
// head one (which we expect to have lower capacity than the remaining
// ones).
if (head_->empty() && head_->next_) {
head_ = std::move(head_->next_);
}
--size_;
}
void swap(LazilyDeallocatedDeque& other) {
std::swap(head_, other.head_);
std::swap(tail_, other.tail_);
std::swap(size_, other.size_);
std::swap(max_size_, other.max_size_);
std::swap(next_resize_time_, other.next_resize_time_);
}
void MaybeShrinkQueue() {
if (!tail_)
return;
DCHECK_GE(max_size_, size_);
// Rate limit how often we shrink the queue because it's somewhat expensive.
TimeTicks current_time = TimeTicks::Now();
if (current_time < next_resize_time_)
return;
// Due to the way the Ring works we need 1 more slot than is used.
size_t new_capacity = max_size_ + 1;
if (new_capacity < kMinimumRingSize)
new_capacity = kMinimumRingSize;
// Reset |max_size_| so that unless usage has spiked up we will consider
// reclaiming it next time.
max_size_ = size_;
// Only realloc if the current capacity is sufficiently greater than the
// observed maximum size for the previous period.
if (new_capacity + kReclaimThreshold >= capacity())
return;
SetCapacity(new_capacity);
next_resize_time_ =
current_time + TimeDelta::FromSeconds(kMinimumShrinkIntervalInSeconds);
}
void SetCapacity(size_t new_capacity) {
std::unique_ptr<Ring> new_ring = std::make_unique<Ring>(new_capacity);
DCHECK_GE(new_capacity, size_ + 1);
// Preserve the |size_| which counts down to zero in the while loop.
size_t real_size = size_;
while (!empty()) {
DCHECK(new_ring->CanPush());
new_ring->push_back(std::move(head_->front()));
pop_front();
}
size_ = real_size;
DCHECK_EQ(head_.get(), tail_);
head_ = std::move(new_ring);
tail_ = head_.get();
}
private:
FRIEND_TEST_ALL_PREFIXES(LazilyDeallocatedDequeTest, RingPushFront);
FRIEND_TEST_ALL_PREFIXES(LazilyDeallocatedDequeTest, RingPushBack);
FRIEND_TEST_ALL_PREFIXES(LazilyDeallocatedDequeTest, RingCanPush);
FRIEND_TEST_ALL_PREFIXES(LazilyDeallocatedDequeTest, RingPushPopPushPop);
struct Ring {
explicit Ring(size_t capacity)
: capacity_(capacity),
front_index_(0),
back_index_(0),
data_(reinterpret_cast<T*>(new char[sizeof(T) * capacity])),
next_(nullptr) {
DCHECK_GE(capacity_, kMinimumRingSize);
}
~Ring() {
while (!empty()) {
pop_front();
}
delete[] reinterpret_cast<char*>(data_);
}
bool empty() const { return back_index_ == front_index_; }
size_t capacity() const { return capacity_; }
bool CanPush() const {
return front_index_ != CircularIncrement(back_index_);
}
void push_front(T&& t) {
// Mustn't appear to become empty.
DCHECK_NE(CircularDecrement(front_index_), back_index_);
new (&data_[front_index_]) T(std::move(t));
front_index_ = CircularDecrement(front_index_);
}
void push_back(T&& t) {
back_index_ = CircularIncrement(back_index_);
DCHECK(!empty()); // Mustn't appear to become empty.
new (&data_[back_index_]) T(std::move(t));
}
bool CanPop() const { return front_index_ != back_index_; }
void pop_front() {
DCHECK(!empty());
front_index_ = CircularIncrement(front_index_);
data_[front_index_].~T();
}
T& front() {
DCHECK(!empty());
return data_[CircularIncrement(front_index_)];
}
const T& front() const {
DCHECK(!empty());
return data_[CircularIncrement(front_index_)];
}
T& back() {
DCHECK(!empty());
return data_[back_index_];
}
const T& back() const {
DCHECK(!empty());
return data_[back_index_];
}
size_t CircularDecrement(size_t index) const {
if (index == 0)
return capacity_ - 1;
return index - 1;
}
size_t CircularIncrement(size_t index) const {
DCHECK_LT(index, capacity_);
++index;
if (index == capacity_)
return 0;
return index;
}
size_t capacity_;
size_t front_index_;
size_t back_index_;
T* data_;
std::unique_ptr<Ring> next_;
DISALLOW_COPY_AND_ASSIGN(Ring);
};
public:
class Iterator {
public:
using value_type = T;
using pointer = const T*;
using reference = const T&;
const T& operator->() const { return ring_->data_[index_]; }
const T& operator*() const { return ring_->data_[index_]; }
Iterator& operator++() {
if (index_ == ring_->back_index_) {
ring_ = ring_->next_.get();
index_ = ring_ ? ring_->CircularIncrement(ring_->front_index_) : 0;
} else {
index_ = ring_->CircularIncrement(index_);
}
return *this;
}
operator bool() const { return !!ring_; }
private:
explicit Iterator(const Ring* ring) {
if (!ring || ring->empty()) {
ring_ = nullptr;
index_ = 0;
return;
}
ring_ = ring;
index_ = ring_->CircularIncrement(ring->front_index_);
}
const Ring* ring_;
size_t index_;
friend class LazilyDeallocatedDeque;
};
Iterator begin() const { return Iterator(head_.get()); }
Iterator end() const { return Iterator(nullptr); }
private:
// We maintain a list of Ring buffers, to enable us to grow without copying,
// but most of the time we aim to have only one active Ring.
std::unique_ptr<Ring> head_;
Ring* tail_ = nullptr;
size_t size_ = 0;
size_t max_size_ = 0;
TimeTicks next_resize_time_;
DISALLOW_COPY_AND_ASSIGN(LazilyDeallocatedDeque);
};
} // namespace internal
} // namespace sequence_manager
} // namespace base
#endif // BASE_TASK_SEQUENCE_MANAGER_LAZILY_DEALLOCATED_DEQUE_H_