From b60a8aa657f62a596e528f244f1c101dc2f054b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Feb 2023 18:45:05 +0100 Subject: [PATCH] Add two-step write feature to bytebuf If there is exactly one producer, then it can assume that the remaining space in the buffer will only increase until it writes something. This assumption may allow the producer to write to the buffer (up to a known safe size) without any synchronization mechanism, thus allowing to read and write different parts of the buffer in parallel. The producer can then commit the write with a lock held, and update its knowledge of the safe empty remaining space. PR #3757 --- app/src/util/bytebuf.c | 39 ++++++++++++++++++++++++++++++----- app/src/util/bytebuf.h | 24 ++++++++++++++++++++++ app/tests/test_bytebuf.c | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c index 61814376..eac69e9c 100644 --- a/app/src/util/bytebuf.c +++ b/app/src/util/bytebuf.c @@ -56,11 +56,9 @@ sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { buf->tail = (buf->tail + len) % buf->alloc_size; } -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { - assert(len); - assert(len <= sc_bytebuf_write_available(buf)); - +static inline void +sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from, + size_t len) { size_t right_len = buf->alloc_size - buf->head; if (len < right_len) { right_len = len; @@ -70,6 +68,37 @@ sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { if (len > right_len) { memcpy(buf->data, from + right_len, len - right_len); } +} +static inline void +sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) { buf->head = (buf->head + len) % buf->alloc_size; } + +void +sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { + assert(len); + assert(len <= sc_bytebuf_write_available(buf)); + + sc_bytebuf_write_step0(buf, from, len); + sc_bytebuf_write_step1(buf, len); +} + +void +sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, + size_t len) { + // *This function MUST NOT access buf->tail (even in assert()).* + // The purpose of this function is to allow a reader and a writer to access + // different parts of the buffer in parallel simultaneously. It is intended + // to be called without lock (only sc_bytebuf_commit_write() is intended to + // be called with lock held). + + assert(len < buf->alloc_size - 1); + sc_bytebuf_write_step0(buf, from, len); +} + +void +sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) { + assert(len <= sc_bytebuf_write_available(buf)); + sc_bytebuf_write_step1(buf, len); +} diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h index fcebc2d3..e8279ef8 100644 --- a/app/src/util/bytebuf.h +++ b/app/src/util/bytebuf.h @@ -56,6 +56,30 @@ sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len); void sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len); +/** + * Copy the user-provided array to the bytebuf, but do not advance the cursor + * + * The caller must check that len <= sc_bytebuf_write_available() (it is an + * error to write more bytes than the remaining available space). + * + * After this function is called, the write must be committed with + * sc_bytebuf_commit_write(). + * + * The purpose of this mechanism is to acquire a lock only to commit the write, + * but not to perform the actual copy. + * + * This function is guaranteed not to access buf->tail. + */ +void +sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, + size_t len); + +/** + * Commit a prepared write + */ +void +sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len); + /** * Return the number of bytes which can be read * diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c index fbb33765..75af3073 100644 --- a/app/tests/test_bytebuf.c +++ b/app/tests/test_bytebuf.c @@ -71,12 +71,56 @@ void test_bytebuf_boundaries(void) { sc_bytebuf_destroy(&buf); } +void test_bytebuf_two_steps_write(void) { + struct sc_bytebuf buf; + uint8_t data[20]; + + bool ok = sc_bytebuf_init(&buf, 20); + assert(ok); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 6); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet + + sc_bytebuf_read(&buf, data, 9); + assert(!strncmp((char *) data, "hello hel", 3)); + assert(sc_bytebuf_read_available(&buf) == 3); + + sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 9); + + sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1); + assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet + + sc_bytebuf_commit_write(&buf, sizeof("world") - 1); + assert(sc_bytebuf_read_available(&buf) == 14); + + sc_bytebuf_write(&buf, (uint8_t *) "!", 1); + assert(sc_bytebuf_read_available(&buf) == 15); + + sc_bytebuf_skip(&buf, 3); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_read(&buf, data, 12); + data[12] = '\0'; + assert(!strcmp((char *) data, "hello world!")); + assert(sc_bytebuf_read_available(&buf) == 0); + + sc_bytebuf_destroy(&buf); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; test_bytebuf_simple(); test_bytebuf_boundaries(); + test_bytebuf_two_steps_write(); return 0; }