Skip to content

Commit ffdf6ed

Browse files
feat: add breadcrumb ringbuffer to avoid O(n) memmove (#1060)
* feat: initial ringbuffer implementation * chore: cleanup code * chore: added todo * removed unnecessary buffer end value * changed buffer start_idx storage to [0] * fixed issues in new storing method * updated test to new ringbuffer append logic * refactor: renamed to sentry__value_append_ringbuffer * test: removed old bounded append test * chore: update CHANGELOG.md * chore: update CHANGELOG.md * chore: linting * increase refcount of ringbuffer-to-list items * apply suggestion from code review * added ringbuffer test * fixed ringbuffer to list conversion * direct access to ringbuffer items * updated test with proper refcount check * removed unnecessary decref from test * added decref of temporary ringbuffer list * removed double cloning * changing types from int32_t to size_t * moved declaration from public to private header * added decref * type conversion * applied suggestions from code review
1 parent 341a64f commit ffdf6ed

File tree

7 files changed

+130
-88
lines changed

7 files changed

+130
-88
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
6+
**Fixes**:
7+
8+
- Add breadcrumb ringbuffer to avoid O(n) memmove on adding more than max breadcrumbs ([#1060](https://github.com/getsentry/sentry-native/pull/1060))
9+
310
## 0.7.11
411

512
**Fixes**:

src/sentry_core.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ sentry_add_breadcrumb(sentry_value_t breadcrumb)
652652
// the `no_flush` will avoid triggering *both* scope-change and
653653
// breadcrumb-add events.
654654
SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) {
655-
sentry__value_append_bounded(
655+
sentry__value_append_ringbuffer(
656656
scope->breadcrumbs, breadcrumb, max_breadcrumbs);
657657
}
658658
}

src/sentry_scope.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,10 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope,
337337
sentry_value_decref(contexts);
338338

339339
if (mode & SENTRY_SCOPE_BREADCRUMBS) {
340-
PLACE_CLONED_VALUE("breadcrumbs", scope->breadcrumbs);
340+
sentry_value_t l
341+
= sentry__value_ring_buffer_to_list(scope->breadcrumbs);
342+
PLACE_VALUE("breadcrumbs", l);
343+
sentry_value_decref(l);
341344
}
342345

343346
if (mode & SENTRY_SCOPE_MODULES) {

src/sentry_value.c

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -629,40 +629,47 @@ sentry__value_clone(sentry_value_t value)
629629
}
630630
}
631631

632+
/**
633+
* This appends `v` to the List `value`.
634+
* To make this work properly as a ring buffer, the value list needs to have
635+
* the ring buffer start index as the first element
636+
* (e.g, 1 until max is exceeded, then it will update for each added item)
637+
*
638+
* It will remove the oldest value in the list, in case the total number of
639+
* items would exceed `max`.
640+
*
641+
* The list is of size `max + 1` to store the start index.
642+
*
643+
* Returns 0 on success.
644+
*/
632645
int
633-
sentry__value_append_bounded(sentry_value_t value, sentry_value_t v, size_t max)
646+
sentry__value_append_ringbuffer(
647+
sentry_value_t value, sentry_value_t v, size_t max)
634648
{
635649
thing_t *thing = value_as_unfrozen_thing(value);
636650
if (!thing || thing_get_type(thing) != THING_TYPE_LIST) {
637651
goto fail;
638652
}
639653

640654
list_t *l = thing->payload._ptr;
641-
642-
if (l->len < max) {
655+
if (l->len == 0) {
656+
sentry_value_append(value, sentry_value_new_int32(1));
657+
}
658+
if (l->len < max + 1) {
643659
return sentry_value_append(value, v);
644660
}
661+
if (l->len > max + 1) {
662+
SENTRY_WARNF("Cannot reduce Ringbuffer list size from %d to %d.",
663+
l->len - 1, max);
664+
goto fail;
665+
}
666+
const int32_t start_idx = sentry_value_as_int32(l->items[0]);
645667

646-
// len: 120
647-
// max: 100
648-
// move to 0
649-
// move 99 items (len - 1)
650-
// from 20
668+
sentry_value_decref(l->items[start_idx]);
669+
l->items[start_idx] = v;
670+
l->items[0] = sentry_value_new_int32((start_idx % (int32_t)max) + 1);
651671

652-
size_t to_move = max >= 1 ? max - 1 : 0;
653-
size_t to_shift = l->len - to_move;
654-
for (size_t i = 0; i < to_shift; i++) {
655-
sentry_value_decref(l->items[i]);
656-
}
657-
if (to_move) {
658-
memmove(l->items, l->items + to_shift, to_move * sizeof(l->items[0]));
659-
}
660-
if (max >= 1) {
661-
l->items[max - 1] = v;
662-
} else {
663-
sentry_value_decref(v);
664-
}
665-
l->len = max;
672+
l->len = max + 1;
666673
return 0;
667674

668675
fail:
@@ -840,6 +847,28 @@ sentry_value_as_string(sentry_value_t value)
840847
}
841848
}
842849

850+
sentry_value_t
851+
sentry__value_ring_buffer_to_list(const sentry_value_t rb)
852+
{
853+
const thing_t *thing = value_as_thing(rb);
854+
if (!thing || thing_get_type(thing) != THING_TYPE_LIST) {
855+
return sentry_value_new_null();
856+
}
857+
const list_t *rb_list = thing->payload._ptr;
858+
if (rb_list->len == 0) {
859+
return sentry_value_new_list();
860+
}
861+
const size_t start_idx = sentry_value_as_int32(rb_list->items[0]);
862+
863+
sentry_value_t rv = sentry_value_new_list();
864+
for (size_t i = 0; i < rb_list->len - 1; i++) {
865+
const size_t idx = (start_idx - 1 + i) % (rb_list->len - 1) + 1;
866+
sentry_value_incref(rb_list->items[idx]);
867+
sentry_value_append(rv, rb_list->items[idx]);
868+
}
869+
return rv;
870+
}
871+
843872
int
844873
sentry_value_is_true(sentry_value_t value)
845874
{

src/sentry_value.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,23 @@ sentry_value_t sentry__value_clone(sentry_value_t value);
8181

8282
/**
8383
* This appends `v` to the List `value`.
84-
* It will remove the first value of the list, is case the total number if items
85-
* would exceed `max`.
84+
*
85+
* On non-full lists, exponentially reallocate space to accommodate new values
86+
* (until we reach `max`). After reaching max, the oldest value is removed to
87+
* make space for the new one.
88+
*
89+
* `max` should stay fixed over multiple invocations.
8690
*
8791
* Returns 0 on success.
8892
*/
89-
int sentry__value_append_bounded(
93+
int sentry__value_append_ringbuffer(
9094
sentry_value_t value, sentry_value_t v, size_t max);
9195

96+
/**
97+
* Converts ring buffer to linear list
98+
*/
99+
sentry_value_t sentry__value_ring_buffer_to_list(sentry_value_t rb);
100+
92101
/**
93102
* Deep-merges object src into dst.
94103
*

tests/unit/test_value.c

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -176,21 +176,68 @@ SENTRY_TEST(value_list)
176176
sentry_value_decref(val);
177177

178178
val = sentry_value_new_list();
179+
sentry_value_append(val, sentry_value_new_int32(1));
179180
for (int32_t i = 1; i <= 10; i++) {
180-
sentry_value_append(val, sentry_value_new_int32(i));
181+
sentry__value_append_ringbuffer(val, sentry_value_new_int32(i), 5);
181182
}
182-
sentry__value_append_bounded(val, sentry_value_new_int32(1010), 5);
183+
sentry__value_append_ringbuffer(val, sentry_value_new_int32(1010), 5);
183184
#define CHECK_IDX(Idx, Val) \
184185
TEST_CHECK_INT_EQUAL( \
185186
sentry_value_as_int32(sentry_value_get_by_index(val, Idx)), Val)
186-
CHECK_IDX(0, 7);
187-
CHECK_IDX(1, 8);
188-
CHECK_IDX(2, 9);
189-
CHECK_IDX(3, 10);
190-
CHECK_IDX(4, 1010);
187+
CHECK_IDX(1, 1010);
188+
CHECK_IDX(2, 7);
189+
CHECK_IDX(3, 8);
190+
CHECK_IDX(4, 9);
191+
CHECK_IDX(5, 10);
191192
sentry_value_decref(val);
192193
}
193194

195+
SENTRY_TEST(value_ringbuffer)
196+
{
197+
sentry_value_t val = sentry_value_new_list();
198+
sentry_value_append(val, sentry_value_new_int32(1)); // start index
199+
200+
const sentry_value_t v0 = sentry_value_new_object();
201+
sentry_value_set_by_key(v0, "key", sentry_value_new_int32((int32_t)0));
202+
const sentry_value_t v1 = sentry_value_new_object();
203+
sentry_value_set_by_key(v1, "key", sentry_value_new_int32((int32_t)1));
204+
const sentry_value_t v2 = sentry_value_new_object();
205+
sentry_value_set_by_key(v2, "key", sentry_value_new_int32((int32_t)2));
206+
const sentry_value_t v3 = sentry_value_new_object();
207+
sentry_value_set_by_key(v3, "key", sentry_value_new_int32((int32_t)3));
208+
209+
sentry__value_append_ringbuffer(val, v0, 3);
210+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v0), 1);
211+
sentry_value_incref(v0);
212+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v0), 2);
213+
214+
sentry__value_append_ringbuffer(val, v1, 3);
215+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v1), 1);
216+
sentry__value_append_ringbuffer(val, v2, 3);
217+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v2), 1);
218+
sentry__value_append_ringbuffer(val, v3, 3);
219+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v3), 1);
220+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v0), 1);
221+
222+
const sentry_value_t l = sentry__value_ring_buffer_to_list(val);
223+
TEST_CHECK_INT_EQUAL(sentry_value_get_length(l), 3);
224+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v3), 2);
225+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v2), 2);
226+
TEST_CHECK_INT_EQUAL(sentry_value_refcount(v1), 2);
227+
#define CHECK_KEY_IDX(List, Idx, Val) \
228+
TEST_CHECK_INT_EQUAL(sentry_value_as_int32(sentry_value_get_by_key( \
229+
sentry_value_get_by_index(List, Idx), "key")), \
230+
Val)
231+
232+
CHECK_KEY_IDX(l, 0, 1);
233+
CHECK_KEY_IDX(l, 1, 2);
234+
CHECK_KEY_IDX(l, 2, 3);
235+
236+
sentry_value_decref(l);
237+
sentry_value_decref(val);
238+
sentry_value_decref(v0); // one manual incref
239+
}
240+
194241
SENTRY_TEST(value_object)
195242
{
196243
sentry_value_t val = sentry_value_new_object();
@@ -509,59 +556,6 @@ SENTRY_TEST(value_wrong_type)
509556
TEST_CHECK(sentry_value_get_length(val) == 0);
510557
}
511558

512-
SENTRY_TEST(value_collections_leak)
513-
{
514-
// decref the value correctly on error
515-
sentry_value_t obj = sentry_value_new_object();
516-
sentry_value_t null_v = sentry_value_new_null();
517-
518-
sentry_value_incref(obj);
519-
sentry_value_set_by_key(null_v, "foo", obj);
520-
521-
sentry_value_incref(obj);
522-
sentry_value_set_by_index(null_v, 123, obj);
523-
524-
sentry_value_incref(obj);
525-
sentry_value_append(null_v, obj);
526-
527-
TEST_CHECK_INT_EQUAL(sentry_value_refcount(obj), 1);
528-
529-
sentry_value_t list = sentry_value_new_list();
530-
531-
sentry_value_incref(obj);
532-
sentry_value_append(list, obj);
533-
sentry_value_incref(obj);
534-
sentry_value_append(list, obj);
535-
sentry_value_incref(obj);
536-
sentry_value_append(list, obj);
537-
sentry_value_incref(obj);
538-
sentry_value_append(list, obj);
539-
sentry_value_incref(obj);
540-
sentry_value_append(list, obj);
541-
542-
// decref the existing values correctly on bounded append
543-
sentry_value_incref(obj);
544-
sentry__value_append_bounded(list, obj, 2);
545-
sentry_value_incref(obj);
546-
sentry__value_append_bounded(list, obj, 2);
547-
548-
TEST_CHECK_INT_EQUAL(sentry_value_refcount(obj), 3);
549-
550-
sentry_value_incref(obj);
551-
sentry__value_append_bounded(list, obj, 1);
552-
TEST_CHECK_INT_EQUAL(sentry_value_refcount(obj), 2);
553-
554-
sentry_value_incref(obj);
555-
sentry__value_append_bounded(list, obj, 0);
556-
TEST_CHECK_INT_EQUAL(sentry_value_refcount(obj), 1);
557-
TEST_CHECK_INT_EQUAL(sentry_value_get_length(list), 0);
558-
559-
sentry_value_decref(list);
560-
561-
TEST_CHECK_INT_EQUAL(sentry_value_refcount(obj), 1);
562-
sentry_value_decref(obj);
563-
}
564-
565559
SENTRY_TEST(value_set_by_null_key)
566560
{
567561
sentry_value_t value = sentry_value_new_object();

tests/unit/tests.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ XX(user_feedback_with_null_args)
115115
XX(uuid_api)
116116
XX(uuid_v4)
117117
XX(value_bool)
118-
XX(value_collections_leak)
119118
XX(value_double)
120119
XX(value_freezing)
121120
XX(value_get_by_null_key)
@@ -132,6 +131,7 @@ XX(value_object)
132131
XX(value_object_merge)
133132
XX(value_object_merge_nested)
134133
XX(value_remove_by_null_key)
134+
XX(value_ringbuffer)
135135
XX(value_set_by_null_key)
136136
XX(value_set_stacktrace)
137137
XX(value_string)

0 commit comments

Comments
 (0)