From f660d30138e07afe81e6cbcedb1aab2a4fc36b41 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Sat, 15 Mar 2014 17:30:08 +0100 Subject: [PATCH] util/CircularBuffer: new buffer class --- Makefile.am | 2 + src/util/CircularBuffer.hxx | 165 ++++++++++++++++++++++++++++++++++++ test/TestCircularBuffer.hxx | 141 ++++++++++++++++++++++++++++++ test/test_util.cxx | 2 + 4 files changed, 310 insertions(+) create mode 100644 src/util/CircularBuffer.hxx create mode 100644 test/TestCircularBuffer.hxx diff --git a/Makefile.am b/Makefile.am index 700511338..42b4169ec 100644 --- a/Makefile.am +++ b/Makefile.am @@ -362,6 +362,7 @@ libutil_a_SOURCES = \ src/util/DynamicFifoBuffer.hxx \ src/util/ConstBuffer.hxx \ src/util/WritableBuffer.hxx \ + src/util/CircularBuffer.hxx \ src/util/LazyRandomEngine.cxx src/util/LazyRandomEngine.hxx \ src/util/SliceBuffer.hxx \ src/util/HugeAllocator.cxx src/util/HugeAllocator.hxx \ @@ -1832,6 +1833,7 @@ test_run_inotify_LDADD = \ endif test_test_util_SOURCES = \ + test/TestCircularBuffer.hxx \ test/test_util.cxx test_test_util_CPPFLAGS = $(AM_CPPFLAGS) $(CPPUNIT_CFLAGS) -DCPPUNIT_HAVE_RTTI=0 test_test_util_CXXFLAGS = $(AM_CXXFLAGS) -Wno-error=deprecated-declarations diff --git a/src/util/CircularBuffer.hxx b/src/util/CircularBuffer.hxx new file mode 100644 index 000000000..60b6b3337 --- /dev/null +++ b/src/util/CircularBuffer.hxx @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2014 Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef CIRCULAR_BUFFER_HPP +#define CIRCULAR_BUFFER_HPP + +#include "WritableBuffer.hxx" + +#include +#include + +/** + * A circular buffer. + * + * This class does not manage buffer memory. It will not allocate or + * free any memory, it only manages the contents of an existing + * buffer given to the constructor. + * + * Everything between #head and #tail is valid data (may wrap around). + * If both are equal, then the buffer is empty. Due to this + * implementation detail, the buffer is empty when #size-1 items are + * stored; the last buffer cell cannot be used. + */ +template +class CircularBuffer { +public: + typedef WritableBuffer Range; + typedef typename Range::pointer_type pointer_type; + typedef typename Range::size_type size_type; + +protected: + /** + * The next index to be read. + */ + size_type head; + + /** + * The next index to be written to. + */ + size_type tail; + + const size_type size; + const pointer_type data; + +public: + constexpr CircularBuffer(pointer_type _data, size_type _size) + :head(0), tail(0), size(_size), data(_data) {} + + CircularBuffer(const CircularBuffer &other) = delete; + +protected: + constexpr size_type Next(size_type i) const { + return i + 1 == size + ? 0 + : i + 1; + } + +public: + void Clear() { + head = tail = 0; + } + + size_type GetSize() const { + return size; + } + + constexpr bool IsEmpty() const { + return head == tail; + } + + constexpr bool IsFull() const { + return Next(tail) == head; + } + + /** + * Prepares writing. Returns a buffer range which may be written. + * When you are finished, call Append(). + */ + Range Write() { + assert(head < size); + assert(tail < size); + + size_type end = tail < head + ? head - 1 + /* the "head==0" is there so we don't write + the last cell, as this situation cannot be + represented by head/tail */ + : size - (head == 0); + + return Range(data + tail, end - tail); + } + + /** + * Expands the tail of the buffer, after data has been written + * to the buffer returned by Write(). + */ + void Append(size_type n) { + assert(head < size); + assert(tail < size); + assert(n < size); + assert(tail + n <= size); + assert(head <= tail || tail + n < head); + + tail += n; + + if (tail == size) { + assert(head > 0); + tail = 0; + } + } + + /** + * Return a buffer range which may be read. The buffer pointer is + * writable, to allow modifications while parsing. + */ + Range Read() { + assert(head < size); + assert(tail < size); + + return Range(data + head, (tail < head ? size : tail) - head); + } + + /** + * Marks a chunk as consumed. + */ + void Consume(size_type n) { + assert(head < size); + assert(tail < size); + assert(n < size); + assert(head + n <= size); + assert(tail < head || head + n <= tail); + + head += n; + if (head == size) + head = 0; + } +}; + +#endif diff --git a/test/TestCircularBuffer.hxx b/test/TestCircularBuffer.hxx new file mode 100644 index 000000000..e4285484b --- /dev/null +++ b/test/TestCircularBuffer.hxx @@ -0,0 +1,141 @@ +/* + * Unit tests for class CircularBuffer. + */ + +#include "config.h" +#include "util/CircularBuffer.hxx" + +#include +#include +#include +#include + +#include +#include + +class TestCircularBuffer : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(TestCircularBuffer); + CPPUNIT_TEST(TestIt); + CPPUNIT_TEST_SUITE_END(); + +public: + void TestIt() { + static size_t N = 8; + int data[N]; + CircularBuffer buffer(data, N); + + /* '.' = empty; 'O' = occupied; 'X' = blocked */ + + /* checks on empty buffer */ + /* [.......X] */ + CPPUNIT_ASSERT_EQUAL(true, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(true, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[0], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(7), buffer.Write().size); + + /* append one element */ + /* [O......X] */ + buffer.Append(1); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(1), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[0], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[1], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(6), buffer.Write().size); + + /* append 6 elements, buffer is now full */ + /* [OOOOOOOX] */ + buffer.Append(6); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(true, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(7), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[0], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(true, buffer.Write().IsEmpty()); + + /* consume [0]; can append one at [7] */ + /* [XOOOOOO.] */ + buffer.Consume(1); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(6), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[1], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[7], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(1), buffer.Write().size); + + /* append one element; [0] is still empty but cannot + be written to because head==1 */ + /* [XOOOOOOO] */ + buffer.Append(1); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(true, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(7), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[1], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(true, buffer.Write().IsEmpty()); + + /* consume [1..3]; can append [0..2] */ + /* [...XOOOO] */ + buffer.Consume(3); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(4), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[4], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[0], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(3), buffer.Write().size); + + /* append [0..1] */ + /* [OO.XOOOO] */ + buffer.Append(2); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(4), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[4], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[2], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(1), buffer.Write().size); + + /* append [2] */ + /* [OOOXOOOO] */ + buffer.Append(1); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(true, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(4), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[4], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(true, buffer.Write().IsEmpty()); + + /* consume [4..7] */ + /* [OOO....X] */ + buffer.Consume(4); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(size_t(3), buffer.Read().size); + CPPUNIT_ASSERT_EQUAL(&data[0], buffer.Read().data); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[3], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(4), buffer.Write().size); + + /* consume [0..2]; after that, we can only write 5, + because the CircularBuffer class doesn't have + special code to rewind/reset an empty buffer */ + /* [..X.....] */ + buffer.Consume(3); + CPPUNIT_ASSERT_EQUAL(true, buffer.IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.IsFull()); + CPPUNIT_ASSERT_EQUAL(true, buffer.Read().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(false, buffer.Write().IsEmpty()); + CPPUNIT_ASSERT_EQUAL(&data[3], buffer.Write().data); + CPPUNIT_ASSERT_EQUAL(size_t(5), buffer.Write().size); + } +}; diff --git a/test/test_util.cxx b/test/test_util.cxx index 07bcacbe9..aaadec68f 100644 --- a/test/test_util.cxx +++ b/test/test_util.cxx @@ -4,6 +4,7 @@ #include "config.h" #include "util/UriUtil.hxx" +#include "TestCircularBuffer.hxx" #include #include @@ -50,6 +51,7 @@ public: }; CPPUNIT_TEST_SUITE_REGISTRATION(UriUtilTest); +CPPUNIT_TEST_SUITE_REGISTRATION(TestCircularBuffer); int main(gcc_unused int argc, gcc_unused char **argv)