Skip to content

Commit 25be585

Browse files
committed
[pubsub] Fix lag jumpahead logic
The existing behavior when the subscriber lags (behind the write head by queue size, since the buffer is circular, new data will have been written to the location the subscriber is trying to read from) was to move to the oldest valid location. However, since the jumpahead was happening outside of any locks, between the time of jumping ahead and acquring the read lock for that location, the publisher(s) could have written past the new read location. The fix was to jump ahead optimistically, rather than to the last valid location. Currently, this is mid-way between the write head and the last valid location. Another issue that surfaced was that since the jumpahead happens outside any lock, in the case of particularly fast publishers, even here the same issue can be seen, where the write head wraps around and writes past the new (optimistic) read location. The second fix was to do a similar jump ahead (and associated check) while under read lock. This is an expensive operation, since we might potentially by readig from two buffer elements. However, this is a (very rate) slow path. To ensure this logic is correct, the benchmark for pubsub has an assert to check that each subsequent subscribed elements is increasing. The benchmark has also been rewritten to make it much cleaner.
1 parent ae0405a commit 25be585

4 files changed

Lines changed: 100 additions & 59 deletions

File tree

benchmark/pubsub.cpp

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
SOFTWARE.
2222
==============================================================================*/
2323

24+
#include <cassert>
2425
#include <chrono>
2526
#include <iostream>
2627

@@ -35,66 +36,69 @@ SOFTWARE.
3536
#endif
3637

3738
const char topic[] = "raw_benchmark_topic";
38-
const int SECONDS = 10;
39-
const int VECTOR_SIZE = 32 * 1024;
4039

4140
shm::stats::Welford per_second_lag;
4241

4342
struct Message {
43+
uint64_t count;
4444
uint64_t timestamp;
4545
uint8_t *data;
4646
};
4747

48+
uint64_t current_count = 0;
49+
4850
void callback(shm::memory::Memblock *memblock) {
51+
if (memblock->is_empty()) {
52+
return;
53+
}
4954
auto *msg = reinterpret_cast<Message *>(memblock->ptr);
5055
auto lag = shm::current_time() - msg->timestamp;
56+
// assert(current_count == msg->count - 1);
57+
assert(current_count < msg->count);
58+
current_count = msg->count;
5159
per_second_lag.add(lag);
5260
}
5361

54-
void subscribe_loop() {
55-
std::this_thread::sleep_for(std::chrono::seconds(1));
62+
void subscribe_loop(int seconds) {
5663
shm::pubsub::Subscriber sub(topic, callback, nullptr);
57-
int seconds = 0;
58-
59-
auto start = shm::current_time();
60-
while (true) {
61-
sub.spin_once();
62-
auto end = shm::current_time();
63-
auto diff = end - start;
64-
65-
if (diff > TIMESCALE_COUNT) {
66-
std::cout << "Lag (" << TIMESCALE_NAME << "): " << per_second_lag
67-
<< std::endl;
68-
if (++seconds == SECONDS) break;
69-
start = shm::current_time();
70-
per_second_lag.clear();
64+
65+
for (int sec = 0; sec < seconds; ++sec) {
66+
auto start = std::chrono::steady_clock::now();
67+
for (auto now = start; now < start + std::chrono::seconds(1);
68+
now = std::chrono::steady_clock::now()) {
69+
sub.spin_once();
7170
}
71+
std::cout << per_second_lag << std::endl;
72+
per_second_lag.clear();
7273
}
7374
}
7475

75-
void publish_loop() {
76-
shm::pubsub::Publisher pub(topic, nullptr);
76+
void publish_loop(int seconds, int vector_size) {
77+
std::cout << "Number of bytes = " << vector_size << std::endl;
78+
std::cout << "Time unit = " << TIMESCALE_NAME << std::endl;
7779

78-
auto *rawptr = malloc(VECTOR_SIZE);
79-
std::memset(rawptr, 255, VECTOR_SIZE);
80+
auto *rawptr = malloc(vector_size);
81+
std::memset(rawptr, 255, vector_size);
8082
Message *msg = reinterpret_cast<Message *>(rawptr);
83+
msg->count = 0;
8184

82-
std::cout << "Number of bytes = " << VECTOR_SIZE << std::endl;
85+
shm::pubsub::Publisher pub(topic, nullptr);
8386

84-
auto start = shm::current_time();
85-
while (true) {
87+
auto start = std::chrono::steady_clock::now();
88+
for (auto now = start; now < start + std::chrono::seconds(seconds);
89+
now = std::chrono::steady_clock::now()) {
90+
msg->count++;
8691
msg->timestamp = shm::current_time();
87-
pub.publish(msg, VECTOR_SIZE);
88-
auto end = shm::current_time();
89-
auto diff = end - start;
90-
if (diff > (SECONDS + 1) * TIMESCALE_COUNT) break;
92+
pub.publish(msg, vector_size);
9193
}
9294
free(msg);
9395
}
9496

9597
int main() {
96-
std::thread subscribe_thread(subscribe_loop);
97-
std::thread publish_thread(publish_loop);
98+
const int SECONDS = 10;
99+
const int VECTOR_SIZE = 32 * 1024;
100+
std::thread subscribe_thread(subscribe_loop, SECONDS);
101+
std::thread publish_thread(publish_loop, SECONDS, VECTOR_SIZE);
98102

99103
subscribe_thread.join();
100104
publish_thread.join();

include/shadesmar/pubsub/subscriber.h

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,29 @@ memory::Memblock Subscriber::get_message() {
8383
return memory::Memblock();
8484
}
8585

86-
if (topic_->counter() - counter_ > topic_->queue_size()) {
86+
if (topic_->counter() - counter_ >= topic_->queue_size()) {
8787
/*
88+
* Why is the check >= (not >)? This is because in topic's
89+
* `write` we do an `inc` at the end, so the write head
90+
* (topic_->counter()) is currently under-write.
91+
*
8892
* If we have fallen behind by the size of the queue
8993
* in the case of overlap, we go to last existing
9094
* element in the queue.
9195
*
92-
* Why is the check > (not >=)? This is because in topic's
93-
* `write` we do a `fetch_add`, so the queue pointer is already
94-
* ahead of where it last wrote.
96+
*
97+
* Rather than going to the last valid location:
98+
* topic_->counter() - topic_->queue_size()
99+
* We move to a more optimistic location (see `jumpahead`), to prevent
100+
* hitting a case of always trying to keep up with the publisher.
95101
*/
96-
counter_ = topic_->counter() - topic_->queue_size();
102+
counter_ = jumpahead(topic_->counter(), topic_->queue_size());
97103
}
98104

99105
memory::Memblock memblock;
100106
memblock.free = true;
101107

102-
if (!topic_->read(&memblock, counter_)) {
108+
if (!topic_->read(&memblock, &counter_)) {
103109
return memory::Memblock();
104110
}
105111

include/shadesmar/pubsub/topic.h

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ SOFTWARE.
3737

3838
namespace shm::pubsub {
3939

40+
// The logic for optimistically jumping ahead, if the current read
41+
// logic has fallen behind (circular wrap around). `counter` is the
42+
// current write head location.
43+
inline uint32_t jumpahead(uint32_t counter, uint32_t queue_size) {
44+
return counter - queue_size / 2;
45+
}
46+
4047
template <class LockT>
4148
struct TopicElemT : public memory::Element {
4249
LockT mutex;
@@ -81,7 +88,7 @@ class Topic {
8188
std::cerr << "Increase buffer_size" << std::endl;
8289
return false;
8390
}
84-
uint32_t q_pos = fetch_add_counter() & (queue_size() - 1);
91+
uint32_t q_pos = counter() & (queue_size() - 1);
8592
TopicElem *elem = &(memory_.shared_queue_->elements[q_pos]);
8693

8794
/*
@@ -120,6 +127,7 @@ class Topic {
120127
while (!memory_.allocator_->free(old_address)) {
121128
std::this_thread::sleep_for(std::chrono::microseconds(100));
122129
}
130+
inc_counter();
123131
return true;
124132
}
125133

@@ -129,41 +137,62 @@ class Topic {
129137
* defaults to the head of the queue. Reads requires an explicit
130138
* position in the queue to read(`pos`). We use this position
131139
* argument since we need to support multiple subscribers each
132-
* reading at their own pace. Look at `subscriber.h` to see the
133-
* logic to handle circular queue wrap around logic.
140+
* reading at their own pace. Between picking the element at pos to
141+
* read from and acquiring a read lock, the publisher may write a new
142+
* value at pos. In this case, we implement a slow path to jump ahead.
134143
*/
135144

136-
bool read(memory::Memblock *memblock, uint32_t pos) {
137-
/*
138-
* Read into a raw array.
139-
*/
145+
bool read(memory::Memblock *memblock, std::atomic<uint32_t> *pos) {
140146
TopicElem *elem =
141-
&(memory_.shared_queue_->elements[pos & (queue_size() - 1)]);
147+
&(memory_.shared_queue_->elements[*pos & (queue_size() - 1)]);
142148

143149
/*
144-
* Code path:
150+
* Code path (without the slow path for lag):
145151
* 1. Acquire sharable lock
146152
* 2. Check for emptiness
147153
* 3. Copy from shared memory to input param `msg`
148154
* 4. Release sharable lock
149155
*/
150156
Scope<concurrent::SHARED> _(&elem->mutex);
151157

152-
if (elem->empty) {
153-
return false;
158+
// Using a lambda for this reduced throughput.
159+
#define MOVE_ELEM(_elem) \
160+
if (_elem->empty) { \
161+
return false; \
162+
} \
163+
auto *dst = memory_.allocator_->handle_to_ptr(_elem->address_handle); \
164+
memblock->size = _elem->size; \
165+
memblock->ptr = copier_->alloc(memblock->size); \
166+
copier_->shm_to_user(memblock->ptr, dst, memblock->size);
167+
168+
if (queue_size() > counter() - *pos) {
169+
// Fast path.
170+
MOVE_ELEM(elem);
171+
return true;
154172
}
155173

156-
auto *dst = memory_.allocator_->handle_to_ptr(elem->address_handle);
157-
memblock->size = elem->size;
158-
memblock->ptr = copier_->alloc(memblock->size);
159-
160-
copier_->shm_to_user(memblock->ptr, dst, memblock->size);
161-
174+
// See comment in `pubsub/subscriber.h`, in function `get_function()` for
175+
// more info. *pos is outdated, the publisher has already written here
176+
// before the reader lock was held. Jump ahead optimisically.
177+
//
178+
// Q: Why no lock on `next_best_elem`?
179+
// A: `elem` is behind `next_best_elem`. With a lock on the former, the
180+
// publisher cannot cross `elem` to get to `next_best_elem`.
181+
//
182+
// Q: Why is the jump ahead implemented again in `get_message()`?
183+
// A: `get_message()` can jump ahead outside of holding a lock. If a
184+
// lag can be detected there, it is more performant. This is a slow
185+
// path under a read lock.
186+
*pos = jumpahead(counter(), queue_size());
187+
TopicElem *next_best_elem =
188+
&(memory_.shared_queue_->elements[*pos & (queue_size() - 1)]);
189+
MOVE_ELEM(next_best_elem);
162190
return true;
191+
#undef MOVE_ELEM
163192
}
164193

165-
inline __attribute__((always_inline)) uint32_t fetch_add_counter() const {
166-
return memory_.shared_queue_->counter.fetch_add(1);
194+
inline __attribute__((always_inline)) void inc_counter() const {
195+
memory_.shared_queue_->counter++;
167196
}
168197

169198
inline __attribute__((always_inline)) uint32_t counter() const {

test/pubsub_test.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,13 @@ TEST_CASE("sub_counter_jump") {
289289
}
290290

291291
sub.spin_once();
292-
REQUIRE(answer == 0);
292+
int lookback =
293+
shm::pubsub::jumpahead(shm::memory::QUEUE_SIZE, shm::memory::QUEUE_SIZE);
294+
REQUIRE(answer == lookback);
293295

294-
int moveahead = 0.5 * shm::memory::QUEUE_SIZE;
296+
int moveahead = shm::memory::QUEUE_SIZE - lookback + 1;
295297
for (int i = 0; i < moveahead; ++i) {
296-
int message = shm::memory::QUEUE_SIZE + i;
298+
int message = lookback + i;
297299
pub.publish(reinterpret_cast<void *>(&message), sizeof(int));
298300
}
299301

0 commit comments

Comments
 (0)