From 228afa18e6931c1fc9e803faea08516e25170488 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 7 Jan 2019 20:28:39 -0800 Subject: [PATCH] Initial commit --- .travis.yml | 15 +++ CHANGELOG.md | 3 + CONTRIBUTING.md | 31 ++++++ README.md | 194 ++++++++++++++++++++++++++++++++++- UPDATING_QUEUE.md | 33 ++++++ api_test.go | 121 ++++++++++++++++++++++ benchmark_test.go | 242 ++++++++++++++++++++++++++++++++++++++++++++ doc_test.go | 20 ++++ go.mod | 3 + integration_test.go | 184 +++++++++++++++++++++++++++++++++ queue.go | 180 ++++++++++++++++++++++++++++++++ testdata/queue.jpg | Bin 0 -> 11287 bytes unit_test.go | 175 ++++++++++++++++++++++++++++++++ 13 files changed, 1199 insertions(+), 2 deletions(-) create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 UPDATING_QUEUE.md create mode 100644 api_test.go create mode 100644 benchmark_test.go create mode 100644 doc_test.go create mode 100644 go.mod create mode 100644 integration_test.go create mode 100644 queue.go create mode 100644 testdata/queue.jpg create mode 100644 unit_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..408a8ed --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: go + +go: + - "1.10.x" + - "1.11.x" + - tip + +before_install: + - go get -t -v ./... + +script: + - go test -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8038ea0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 + +* First stable release, production ready. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9c8169e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributions +There's a number of ways you can contribute to queue: + +- Use it in your projects and let us know your experience +- Let others know about it +- Propose changes by posting [issues](https://github.com/ef-ds/queue/issues) +- Propose changes by creating [PRs](https://github.com/ef-ds/queue/pulls) +- Help with issues labeled ["help wanted"](https://github.com/ef-ds/queue/labels/help%20wanted) + +If you have suggestions, open issues and do your best to describe your idea. We'll do our best to read and try to understand it. Just make sure to be clear about what you are proposing. + +If you feel confident about the proposed change, and the proposed change require small changes to queue's package, feel free to open PRs directly. + +We strongly encourage all changes to be benchmarked before sending PRs/merging. To check the impact of the changes to queue, please refer [here](UPDATING_QUEUE.md). + +A big part of the decision whether to accept suggestions and changes through PRs and issues are based on the feedback of the queue users. The rule we use to gather feedback is below: +- If you agree with a suggestion, thumb it up +- If you don't agree with a suggestion, thumb it down +- If you feel strongly about a suggestion, please consider leaving comments + +We're 100% committed to below software development rules: + +- Correctness +- Performance +- Efficiency +- Simplicity +- Testable code +- Tests, tests, tests! + - Strong test suite covering all code routes/branches + - Strong focus to achieve 100% code coverage everywhere + - Regression tests are a must diff --git a/README.md b/README.md index e6dbc9e..7a918bb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,192 @@ -# queue -Package queue implements a very fast and efficient general purpose First-In-First-out (FIFO) queue data structure that is specifically optimized to perform when used by Microservices and serverless services running in production environments. +# queue [![Build Status](https://travis-ci.com/ef-ds/queue.svg?branch=master)](https://travis-ci.com/ef-ds/queue) [![codecov](https://codecov.io/gh/ef-ds/queue/branch/master/graph/badge.svg)](https://codecov.io/gh/ef-ds/queue) [![Go Report Card](https://goreportcard.com/badge/github.com/ef-ds/queue)](https://goreportcard.com/report/github.com/ef-ds/queue) [![GoDoc](https://godoc.org/github.com/ef-ds/queue?status.svg)](https://godoc.org/github.com/ef-ds/queue) + +Package queue implements a very fast and efficient general purpose First-In-First-Out (FIFO) queue data structure that is specifically optimized to perform when used by Microservices and serverless services running in production environments. Internally, queue stores the elements in a dynamic growing circular singly linked list of arrays. + + +## Install +From a configured [Go environment](https://golang.org/doc/install#testing): +```sh +go get -u github.com/ef-ds/queue +``` + +If you are using dep: +```sh +dep ensure -add github.com/ef-ds/queue@1.0.0 +``` + +We recommend to target only released versions for production use. + + +## How to Use +```go +package main + +import ( + "fmt" + + "github.com/ef-ds/queue" +) + +func main() { + var q queue.Queue + + for i := 1; i <= 5; i++ { + q.Push(i) + } + for q.Len() > 0 { + v, _ := q.Pop() + fmt.Println(v) + } +} +``` + +Output: +``` +1 +2 +3 +4 +5 +``` + +Also refer to the [integration](integration_test.go) and [API](api_test.go) tests. + + + +## Tests +Besides having 100% code coverage, queue has an extensive set of [unit](unit_test.go), [integration](integration_test.go) and [API](api_test.go) tests covering all happy, sad and edge cases. + +When considering all tests, queue has over 4x more lines of testing code when compared to the actual, functional code. + +Performance and efficiency are major concerns, so queue has an extensive set of benchmark tests as well comparing the queue performance with a variety of high quality open source queue implementations. + +See the [benchmark tests](https://github.com/ef-ds/queue-bench-tests/blob/master/BENCHMARK_TESTS.md) for details. + + +## Performance +Queue has constant time (O(1)) on all its operations (Push/Pop/Back/Len). It's not amortized constant because queue never copies more than 64 (maxInternalSliceSize/sliceGrowthFactor) items and when it expands or grow, it never does so by more than 256 (maxInternalSliceSize) items in a single operation. + +Queue offers either the best or very competitive performance across all test sets, suites and ranges. + +As a general purpose FIFO queue, queue offers, by far, the most balanced and consistent performance of all tested data structures. + +See [performance](https://github.com/ef-ds/queue-bench-tests/blob/master/PERFORMANCE.md) for details. + + +## Design +The Efficient Data Structures (ef-ds) queue employs a new, modern queue design: a dynamic growing circular singly linked list of arrays. + +That means the [FIFO queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is a [singly-linked list](https://en.wikipedia.org/wiki/Singly_linked_list) where each node value is a fixed size [slice](https://tour.golang.org/moretypes/7). It is ring in shape because the linked list is a [circular one](https://en.wikipedia.org/wiki/Circular_buffer), where the last node always points to the first one in the ring. + +![ns/op](testdata/queue.jpg?raw=true "queue Design") + + +### Design Considerations +Queue uses linked slices as its underlying data structure. The reason for the choice comes from two main observations of slice based queues: + +1. When the queue needs to expand to accommodate new values, [a new, larger slice needs to be allocated](https://en.wikipedia.org/wiki/Dynamic_array#Geometric_expansion_and_amortized_cost) and used +2. Allocating and managing large slices is expensive, especially in an overloaded system with little available physical memory + +To help clarify the scenario, below is what happens when a slice based queue that already holds, say 1bi items, needs to expand to accommodate a new item. + +Slice based implementation. + +- Allocate a new, twice the size of the previous allocated one, say 2 billion positions slice +- Copy over all 1 billion items from the previous slice into the new one +- Add the new value into the first unused position in the new slice, position 1000000001 + +The same scenario for queue plays out like below. + +- Allocate a new 256 size slice +- Set the previous and next pointers +- Add the value into the first position of the new slice, position 0 + +The decision to use linked slices was also the result of the observation that slices goes to great length to provide predictive, indexed positions. A hash table, for instance, absolutely need this property, but not a queue. So queue completely gives up this property and focus on what really matters: add and retrieve from the edges (front/back). No copying around and repositioning of elements is needed for that. So when a slice goes to great length to provide that functionality, the whole work of allocating new arrays, copying data around is all wasted work. None of that is necessary. And this work costs dearly for large data sets as observed in the tests. + +While linked slices design solve the slice expansion problem very effectively, it doesn't help with many real world usage scenarios such as in a stable processing environment where small amount of items are pushed and popped from the queue in a sequential way. This is a very common scenario for [Microservices](https://en.wikipedia.org/wiki/Microservices) and [serverless](https://en.wikipedia.org/wiki/Serverless_computing) services, for instance, where the service is able to handle the current traffic without stress. + +To address the stable scenario in an effective way, queue keeps its internal linked arrays in a circular, ring shape. This way when items are pushed to the queue after some of them have been removed, the queue will automatically move over its tail slice back to the old head of the queue, effectively reusing the same already allocated slice. The result is a queue that will run through its ring reusing the ring to store the new values, instead of allocating new slices for the new values. + + + +## Supported Data Types +Similarly to Go's standard library list, [list](https://github.com/golang/go/tree/master/src/container/list), +[ring](https://github.com/golang/go/tree/master/src/container/ring) and [heap](https://github.com/golang/go/blob/master/src/container/heap/heap.go) packages, queue supports "interface{}" as its data type. This means it can be used with any Go data types, including int, float, string and any user defined structs and pointers to interfaces. + +The data types pushed into the queue can even be mixed, meaning, it's possible to push ints, floats and struct instances into the same queue. + + +## Safe for Concurrent Use +Queue is not safe for concurrent use. However, it's very easy to build a safe for concurrent use version of the queue. Impl7 design document includes an example of how to make impl7 safe for concurrent use using a mutex. queue can be made safe for concurret use using the same technique. Impl7 design document can be found [here](https://github.com/golang/proposal/blob/master/design/27935-unbounded-queue-package.md). + + +## Range Support +Just like the current container data structures such as [list](https://github.com/golang/go/tree/master/src/container/list), +[ring](https://github.com/golang/go/tree/master/src/container/ring) and [heap](https://github.com/golang/go/blob/master/src/container/heap/heap.go), queue doesn't support the range keyword for navigation. + +However, the API offers two ways to iterate over the queue items. Either use "PopFront"/"PopBack" to retrieve the first current element and the second bool parameter to check for an empty queue. + +```go +for v, ok := s.Pop(); ok; v, ok = s.Pop() { + // Do something with v +} +``` + +Or use "Len" and "Pop" to check for an empty queue and retrieve the first current element. +```go +for s.Len() > 0 { + v, _ := s.Pop() + // Do something with v +} +``` + + + +## Why +We feel like this world needs improving. Our goal is to change the world, for the better, for everyone. + +As software engineers at ef-ds, we feel like the best way we can contribute to a better world is to build amazing systems, +systems that solve real world problems, with unheard performance and efficiency. + +We believe in challenging the status-quo. We believe in thinking differently. We believe in progress. + +What if we could build queues, queues, lists, arrays, hash tables, etc that are much faster than the current ones we have? What if we had a dynamic array data structure that offers near constant time deletion (anywhere in the array)? Or that could handle 1 million items data sets using only 1/3 of the memory when compared to all known current implementations? And still runs 2x as fast? + +One sofware engineer can't change the world him/herself, but a whole bunch of us can! Please join us improving this world. All the work done here is made 100% transparent and is 100% free. No strings attached. We only require one thing in return: please consider benefiting from it; and if you do so, please let others know about it. + + +## Competition +We're extremely interested in improving queue and we're on an endless quest for better efficiency and more performance. Please let us know your suggestions for possible improvements and if you know of other high performance queues not tested here, let us know and we're very glad to benchmark them. + + +## Releases +We're committed to a CI/CD lifecycle releasing frequent, but only stable, production ready versions with all proper tests in place. + +We strive as much as possible to keep backwards compatibility with previous versions, so breaking changes are a no-go. + +For a list of changes in each released version, see [CHANGELOG.md](CHANGELOG.md). + + +## Supported Go Versions +See [supported_go_versions.md](https://github.com/ef-ds/docs/blob/master/supported_go_versions.md). + + +## License +MIT, see [LICENSE](LICENSE). + +"Use, abuse, have fun and contribute back!" + + +## Contributions +See [CONTRIBUTING.md](CONTRIBUTING.md). + + +## Roadmap +- Build tool to help find out the combination of firstSliceSize, sliceGrowthFactor, maxFirstSliceSize and maxInternalSliceSize that will yield the best performance +- Find the fastest open source queues and add them the bench tests +- Improve queue performance and/or efficiency by improving its design and/or implementation +- Build a high performance safe for concurrent use version of queue + + +## Contact +Suggestions, bugs, new queues to benchmark, issues with the queue, please let us know at ef-ds@outlook.com. diff --git a/UPDATING_QUEUE.md b/UPDATING_QUEUE.md new file mode 100644 index 0000000..06eda01 --- /dev/null +++ b/UPDATING_QUEUE.md @@ -0,0 +1,33 @@ +## Updating Queue and Checking the Results + +If you want to make changes to queue and run the tests to check the effect on performance and memory, +we suggest you run all the benchmark tests locally once using below command. + +``` +go test -benchmem -count 10 -timeout 60m -bench="queue*" -run=^$ > testdata/queue.txt +``` + +Then make the changes and re-run the tests using below command (notice the output file now is queue2.txt). + +``` +go test -benchmem -count 10 -timeout 60m -bench="queue*" -run=^$ > testdata/queue2.txt +``` + +Then run the [test-splitter](https://github.com/ef-ds/tools/tree/master/testsplitter) tool as follow: + +``` +go run *.go --file PATH_TO_TESTDATA/queue2.txt --suffix 2 +``` + +Test-splitter should create each file with the "2" suffix, so now we have the test file for both, the old and this new +test run. Use below commands to test the effect of the changes for each test suite. + +``` +benchstat testdata/BenchmarkMicroservice.txt testdata/BenchmarkMicroservice2.txt +benchstat testdata/BenchmarkFill.txt testdata/BenchmarkFill2.txt +benchstat testdata/BenchmarkRefill.txt testdata/BenchmarkRefill2.txt +benchstat testdata/BenchmarkRefillFull.txt testdata/BenchmarkRefillFull2.txt +benchstat testdata/BenchmarkSlowIncrease.txt testdata/BenchmarkSlowIncrease2.txt +benchstat testdata/BenchmarkSlowIncrease.txt testdata/BenchmarkSlowIncrease2.txt +benchstat testdata/BenchmarkStable.txt testdata/BenchmarkStable2.txt +``` diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..49244af --- /dev/null +++ b/api_test.go @@ -0,0 +1,121 @@ +// Copyright (c) 2018 ef-ds +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package queue_test + +import ( + "testing" + + "github.com/ef-ds/queue" +) + +func TestPopWithZeroValueShouldReturnReadyToUsequeue(t *testing.T) { + var q queue.Queue + q.Push(1) + q.Push(2) + + v, ok := q.Front() + if !ok || v.(int) != 1 { + t.Errorf("Expected: 1; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v.(int) != 1 { + t.Errorf("Expected: 1; Got: %d", v) + } + v, ok = q.Front() + if !ok || v.(int) != 2 { + t.Errorf("Expected: 2; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v.(int) != 2 { + t.Errorf("Expected: 2; Got: %d", v) + } + _, ok = q.Front() + if ok { + t.Error("Expected: empty slice (ok=false); Got: ok=true") + } + _, ok = q.Pop() + if ok { + t.Error("Expected: empty slice (ok=false); Got: ok=true") + } +} + +func TestWithZeroValueAndEmptyShouldReturnAsEmpty(t *testing.T) { + var q queue.Queue + + if _, ok := q.Front(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if _, ok := q.Front(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if _, ok := q.Pop(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if l := q.Len(); l != 0 { + t.Errorf("Expected: 0 as the queue is empty; Got: %d", l) + } +} + +func TestInitShouldReturnEmptyqueue(t *testing.T) { + var q queue.Queue + q.Push(1) + + q.Init() + + if _, ok := q.Front(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if _, ok := q.Pop(); ok { + t.Error("Expected: false as the queue is empty; Got: true") + } + if l := q.Len(); l != 0 { + t.Errorf("Expected: 0 as the queue is empty; Got: %d", l) + } +} + +func TestPopWithNilValuesShouldReturnAllValuesInOrder(t *testing.T) { + q := queue.New() + q.Push(1) + q.Push(nil) + q.Push(2) + q.Push(nil) + + v, ok := q.Pop() + if !ok || v.(int) != 1 { + t.Errorf("Expected: 1; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != nil { + t.Errorf("Expected: nil; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v.(int) != 2 { + t.Errorf("Expected: 2; Got: %d", v) + } + v, ok = q.Pop() + if !ok || v != nil { + t.Errorf("Expected: nil; Got: %d", v) + } + _, ok = q.Pop() + if ok { + t.Error("Expected: empty slice (ok=false); Got: ok=true") + } +} diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..c0dddc7 --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,242 @@ +// Copyright (c) 2018 ef-ds +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Below tests are used mostly for comparing the result of changes to queue and +// are not necessarily a replication of the comparison testq. For instance, +// the queue tests here use Push/Pop instead of Push/PopBack. +// +// For comparing queue performance with other queues, refer to +// https://github.com/ef-ds/queue-bench-tests + +package queue_test + +import ( + "strconv" + "testing" + + "github.com/ef-ds/queue" +) + +// testData contains the number of items to add to the queues in each test. +type testData struct { + count int +} + +var ( + tests = []testData{ + {count: 0}, + {count: 1}, + {count: 10}, + {count: 100}, + {count: 1000}, // 1k + {count: 10000}, //10k + {count: 100000}, // 100k + {count: 1000000}, // 1mi + } + + // Used to store temp values, avoiding any compiler optimizationq. + tmp interface{} + tmp2 bool + + fillCount = 10000 + refillCount = 10 +) + +func BenchmarkMicroservice(b *testing.B) { + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + q := queue.New() + + // Simulate stable traffic + for i := 0; i < test.count; i++ { + q.Push(nil) + q.Pop() + } + + // Simulate slowly increasing traffic + for i := 0; i < test.count; i++ { + q.Push(nil) + q.Push(nil) + q.Pop() + } + + // Simulate slowly decreasing traffic, bringing traffic Front to normal + for i := 0; i < test.count; i++ { + q.Pop() + if q.Len() > 0 { + q.Pop() + } + q.Push(nil) + } + + // Simulate quick traffic spike (DDOS attack, etc) + for i := 0; i < test.count; i++ { + q.Push(nil) + } + + // Simulate stable traffic while at high traffic + for i := 0; i < test.count; i++ { + q.Push(nil) + q.Pop() + } + + // Simulate going Front to normal (DDOS attack fended off) + for i := 0; i < test.count; i++ { + q.Pop() + } + + // Simulate stable traffic (now that is Front to normal) + for i := 0; i < test.count; i++ { + q.Push(nil) + q.Pop() + } + } + }) + } +} + +func BenchmarkFill(b *testing.B) { + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + q := queue.New() + for i := 0; i < test.count; i++ { + q.Push(nil) + } + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } + } + }) + } +} + +func BenchmarkRefill(b *testing.B) { + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + q := queue.New() + for n := 0; n < b.N; n++ { + for n := 0; n < refillCount; n++ { + for i := 0; i < test.count; i++ { + q.Push(nil) + } + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } + } + } + }) + } +} + +func BenchmarkRefillFull(b *testing.B) { + q := queue.New() + for i := 0; i < fillCount; i++ { + q.Push(nil) + } + + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + for k := 0; k < refillCount; k++ { + for i := 0; i < test.count; i++ { + q.Push(nil) + } + for i := 0; i < test.count; i++ { + tmp, tmp2 = q.Pop() + } + } + } + }) + } + + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } +} + +func BenchmarkStable(b *testing.B) { + q := queue.New() + for i := 0; i < fillCount; i++ { + q.Push(nil) + } + + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + for i := 0; i < test.count; i++ { + q.Push(nil) + tmp, tmp2 = q.Pop() + } + } + }) + } + + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } +} + +func BenchmarkSlowIncrease(b *testing.B) { + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + q := queue.New() + for i := 0; i < test.count; i++ { + q.Push(nil) + q.Push(nil) + tmp, tmp2 = q.Pop() + } + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } + } + }) + } +} + +func BenchmarkSlowDecrease(b *testing.B) { + q := queue.New() + for _, test := range tests { + items := test.count / 2 + for i := 0; i <= items; i++ { + q.Push(nil) + } + } + + for _, test := range tests { + b.Run(strconv.Itoa(test.count), func(b *testing.B) { + for n := 0; n < b.N; n++ { + for i := 0; i < test.count; i++ { + q.Push(nil) + tmp, tmp2 = q.Pop() + if q.Len() > 0 { + tmp, tmp2 = q.Pop() + } + } + } + }) + } + + for q.Len() > 0 { + tmp, tmp2 = q.Pop() + } +} diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..0cb38e9 --- /dev/null +++ b/doc_test.go @@ -0,0 +1,20 @@ +package queue_test + +import ( + "fmt" + + "github.com/ef-ds/queue" +) + +func Example() { + var q queue.Queue + + for i := 1; i <= 5; i++ { + q.Push(i) + } + for q.Len() > 0 { + v, _ := q.Pop() + fmt.Print(v) + } + // Output: 12345 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9595701 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ef-ds/queue + +go 1.11 diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..9bcc497 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,184 @@ +// Copyright (c) 2018 ef-ds +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package queue_test + +import ( + "testing" + + "github.com/ef-ds/queue" +) + +const ( + pushCount = 256 * 3 // Push to fill at least 3 internal slices +) + +func TestFillQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + + for i := 0; i < pushCount; i++ { + q.Push(i) + } + for i := 0; i < pushCount; i++ { + if v, ok := q.Pop(); !ok || v.(int) != i { + t.Errorf("Expected: %d; Got: %d", i, v) + } + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } +} + +func TestRefillQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + + for i := 0; i < refillCount; i++ { + for j := 0; j < pushCount; j++ { + q.Push(j) + } + for j := 0; j < pushCount; j++ { + if v, ok := q.Pop(); !ok || v.(int) != j { + t.Errorf("Expected: %d; Got: %d", i, v) + } + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } + } +} + +func TestRefillFullQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + for i := 0; i < pushCount; i++ { + q.Push(i) + } + + for i := 0; i < refillCount; i++ { + for j := 0; j < pushCount; j++ { + q.Push(j) + } + for j := 0; j < pushCount; j++ { + if v, ok := q.Pop(); !ok || v.(int) != j { + t.Errorf("Expected: %d; Got: %d", j, v) + } + } + if q.Len() != pushCount { + t.Errorf("Expected: %d; Got: %d", pushCount, q.Len()) + } + } +} + +func TestSlowIncreaseQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + + count := 0 + for i := 0; i < pushCount; i++ { + count++ + q.Push(count) + count++ + q.Push(count) + if v, ok := q.Pop(); !ok || v.(int) != i+1 { + t.Errorf("Expected: %d; Got: %d", i, v) + } + } + if q.Len() != pushCount { + t.Errorf("Expected: %d; Got: %d", pushCount, q.Len()) + } +} + +func TestSlowDecreaseQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + push := 0 + for i := 0; i < pushCount; i++ { + q.Push(push) + push++ + } + + count := -1 + for i := 0; i < pushCount-1; i++ { + count++ + if v, ok := q.Pop(); !ok || v.(int) != count { + t.Errorf("Expected: %d; Got: %d", count, v) + } + count++ + if v, ok := q.Pop(); !ok || v.(int) != count { + t.Errorf("Expected: %d; Got: %d", count, v) + } + + q.Push(push) + push++ + } + count++ + if v, ok := q.Pop(); !ok || v.(int) != count { + t.Errorf("Expected: %d; Got: %d", count, v) + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } +} + +func TestStableQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + + for i := 0; i < pushCount; i++ { + q.Push(i) + if v, ok := q.Pop(); !ok || v.(int) != i { + t.Errorf("Expected: %d; Got: %d", i, v) + } + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } +} + +func TestStableFullQueueShouldRetrieveAllElementsInOrder(t *testing.T) { + var q queue.Queue + + for i := 0; i < pushCount; i++ { + q.Push(i) + if v, ok := q.Pop(); !ok || v.(int) != i { + t.Errorf("Expected: %d; Got: %d", i, v) + } + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } +} + +func TestPushFrontPopRefillWith0ToPushCountItemsShouldReturnAllValuesInOrder(t *testing.T) { + var q queue.Queue + + for i := 0; i < refillCount; i++ { + for k := 0; k < pushCount; k++ { + for j := 0; j < k; j++ { + q.Push(j) + } + for j := 0; j < k; j++ { + v, ok := q.Pop() + if !ok || v == nil || v.(int) != j { + t.Errorf("Expected: %d; Got: %d", j, v) + } + } + if q.Len() != 0 { + t.Errorf("Expected: %d; Got: %d", 0, q.Len()) + } + } + } +} diff --git a/queue.go b/queue.go new file mode 100644 index 0000000..acaabf8 --- /dev/null +++ b/queue.go @@ -0,0 +1,180 @@ +// Copyright (c) 2018 ef-ds +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Package queue implements a very fast and efficient general purpose +// First-In-First-Out (FIFO) queue data structure that is specifically optimized +// to perform when used by Microservices and serverless services running in +// production environments. +package queue + +const ( + // firstSliceSize holds the size of the first slice. + firstSliceSize = 4 + + // sliceGrowthFactor determines by how much and how fast the first internal + // slice should grow. A growth factor of 4, firstSliceSize = 4 and maxFirstSliceSize = 64, + // the first slice will start with size 4, then 16 (4*4), then 64 (16*4). + // The growth factor should be tweaked together with firstSliceSize and specially, + // maxFirstSliceSize for maximum efficiency. + // sliceGrowthFactor only applies to the very first slice created. All other + // subsequent slices are created with fixed size of maxInternalSliceSize. + sliceGrowthFactor = 4 + + // maxFirstSliceSize holds the maximum size of the first slice. + maxFirstSliceSize = 64 + + // maxInternalSliceSize holds the maximum size of each internal slice. + maxInternalSliceSize = 256 +) + +// Queue implements an unbounded, dynamically growing double-ended-queue (queue). +// The zero value for queue is an empty queue ready to use. +type Queue struct { + // Head points to the first node of the linked list. + head *node + + // Tail points to the last node of the linked list. + // In an empty queue, head and tail points to the same node. + tail *node + + // Hp is the index pointing to the current first element in the queue + // (i.e. first element added in the current queue values). + hp int + + // hlp points to the last index in the head slice. + hlp int + + // tp is the index pointing one beyond the current last element in the queue + // (i.e. last element added in the current queue values). + tp int + + // Len holds the current queue values length. + len int +} + +// Node represents a queue node. +// Each node holds a slice of user managed values. +type node struct { + // v holds the list of user added values in this node. + v []interface{} + + // n points to the next node in the linked list. + n *node +} + +// New returns an initialized queue. +func New() *Queue { + return new(Queue) +} + +// Init initializes or clears queue d. +func (d *Queue) Init() *Queue { + *d = Queue{} + return d +} + +// Len returns the number of elements of queue d. +// The complexity is O(1). +func (d *Queue) Len() int { return d.len } + +// Front returns the first element of queue d or nil if the queue is empty. +// The second, bool result indicates whether a valid value was returned; +// if the queue is empty, false will be returned. +// The complexity is O(1). +func (d *Queue) Front() (interface{}, bool) { + if d.len == 0 { + return nil, false + } + return d.head.v[d.hp], true +} + +// Push adds value v to the the back of the queue. +// The complexity is O(1). +func (d *Queue) Push(v interface{}) { + switch { + case d.head == nil: + // No nodes present yet. + h := &node{v: make([]interface{}, firstSliceSize)} + h.n = h + d.head = h + d.tail = h + d.tail.v[0] = v + d.hlp = firstSliceSize - 1 + d.tp = 1 + case d.tp < len(d.tail.v): + // There's room in the tail slice. + d.tail.v[d.tp] = v + d.tp++ + case d.tp < maxFirstSliceSize: + // We're on the first slice and it hasn't grown large enough yet. + nv := make([]interface{}, len(d.tail.v)*sliceGrowthFactor) + copy(nv, d.tail.v) + d.tail.v = nv + d.tail.v[d.tp] = v + d.tp++ + d.hlp = len(nv) - 1 + case d.tail.n != d.head: + // There's at least one spare link between head and tail nodes. + n := d.tail.n + d.tail = n + d.tail.v[0] = v + d.tp = 1 + default: + // No available nodes, so make one. + n := &node{v: make([]interface{}, maxInternalSliceSize)} + n.n = d.head + d.tail.n = n + d.tail = n + d.tail.v[0] = v + d.tp = 1 + } + d.len++ +} + +// Pop retrieves and removes the current element from the front of the queue. +// The second, bool result indicates whether a valid value was returned; +// if the queue is empty, false will be returned. +// The complexity is O(1). +func (d *Queue) Pop() (interface{}, bool) { + if d.len == 0 { + return nil, false + } + + vp := &d.head.v[d.hp] + v := *vp + *vp = nil // Avoid memory leaks + d.len-- + switch { + case d.hp < d.hlp: + // The head isn't at the end of the slice, so just + // move on one place. + d.hp++ + case d.head == d.tail: + // There's only a single element at the end of the slice + // so we can't increment hp, so change tp instead. + d.tp = d.hp + default: + // Move to the next slice. + d.hp = 0 + d.head = d.head.n + d.hlp = len(d.head.v) - 1 + } + return v, true +} diff --git a/testdata/queue.jpg b/testdata/queue.jpg new file mode 100644 index 0000000000000000000000000000000000000000..feccecf86d5b1a29dcc2a7ee7f61c5107c2b1332 GIT binary patch literal 11287 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 k-Q