reftable: memory optimizations for reflog iteration
cover reftable: memory optimizations for reflog iteration
Hi,
this patch series does the same as all the preceding patch series that optimized how the reftable library iterates through refs, but for reflogs instead.
The goal of this patch series is to arrive at a constant number of allocations when iterating refs. This is achieved in mostly the same way we did it for ref iteration, namely by reusing already-allocated memory. Overall, this brings us down from 8 allocations per reflog record to essentially 0 allocations per reflog. Iterating through 1 million reflogs with git reflog list thus goes down from 8.068m allocations to only around 68.5k.
This series is built on top of "master" at b387623c12 (The third batch, 2024-03-01) with Junio's "ps/reftable-iteration-perf-part2" at 43f70eaea0 (refs/reftable: precompute prefix length, 2024-03-04) merged into it.
Patrick
Patrick Steinhardt (7):
refs/reftable: reload correct stack when creating reflog iter reftable/record: convert old and new object IDs to arrays reftable/record: avoid copying author info reftable/record: reuse refnames when decoding log records reftable/record: reuse message when decoding log records reftable/record: use scratch buffer when decoding records refs/reftable: track last log record name via strbuf
refs/reftable-backend.c | 52 +++++---------- reftable/block.c | 4 +- reftable/block.h | 2 + reftable/merged_test.c | 11 ++-- reftable/readwrite_test.c | 62 +++++++----------- reftable/record.c | 129 ++++++++++++++----------------------- reftable/record.h | 5 +- reftable/record_test.c | 68 ++++++++++--------- reftable/reftable-record.h | 6 +- reftable/stack_test.c | 26 ++++---- 10 files changed, 154 insertions(+), 211 deletions(-)
Signature · 1 line
2.44.0
PATCH 1/7 refs/reftable: reload correct stack when creating reflog iter
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent writes to the stack are reflected. But reflog_iterator_for_stack() always reloads the main stack, which is wrong.
Fix this and reload the correct stack.
--- refs/reftable-backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
refs/reftable-backend.c +1 -1
@@ -1682,7 +1682,7 @@ static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftablif (ret) goto done; - ret =reftable_stack_reload (refs->main_stack); + ret =reftable_stack_reload (stack);if (ret <0 ) goto done;
Signature · 1 line
2.44.0
Re: [PATCH 1/7] refs/reftable: reload correct stack when creating reflog iter
Quoted 2 lines · Patrick Steinhardt
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent
Nit: s/created./created for.
Re: [PATCH 1/7] refs/reftable: reload correct stack when creating reflog iter
Quoted 6 lines · Karthik Nayak
Quoted 2 lines · Patrick Steinhardt
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent
Nit: s/created./created for.
Yeah, I couldn't grok that sentence while reviewing it.
Thanks.
Re: [PATCH 1/7] refs/reftable: reload correct stack when creating reflog iter
In reply to Karthik NayakQuoted 6 lines · Karthik Nayak
Quoted 2 lines · Patrick Steinhardt
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent
Nit: s/created./created for.
Indeed, thanks. I've fixed this locally and will wait for more comments before sending a v2.
Patrick
Re: [PATCH 1/7] refs/reftable: reload correct stack when creating reflog iter
In reply to Patrick SteinhardtQuoted 28 lines · Patrick Steinhardt
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent writes to the stack are reflected. But
reflog_iterator_for_stack()always reloads the main stack, which is wrong.Fix this and reload the correct stack.
Signed-off-by Patrick Steinhardt <[email protected]>--- refs/reftable-backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)refs/reftable-backend.c +1
-1@@ -1682,7 +1682,7 @@ static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftablif (ret) goto done; - ret =reftable_stack_reload (refs->main_stack); + ret =reftable_stack_reload (stack);if (ret <0 ) goto done;Signature · 1 line
2.44.0
Is it possible to write a test to demonstrate the bug that was fixed here, or is it too much of a race condition to reliably trigger?
Re: [PATCH 1/7] refs/reftable: reload correct stack when creating reflog iter
Quoted 26 lines · Josh Steadmon
Quoted 25 lines · Patrick Steinhardt
When creating a new reflog iterator, we first have to reload the stack that the iterator is being created. This is done so that any concurrent writes to the stack are reflected. But
reflog_iterator_for_stack()always reloads the main stack, which is wrong.Fix this and reload the correct stack.
Signed-off-by Patrick Steinhardt <[email protected]>--- refs/reftable-backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)refs/reftable-backend.c +1
-1@@ -1682,7 +1682,7 @@ static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftablif (ret) goto done; - ret =reftable_stack_reload (refs->main_stack); + ret =reftable_stack_reload (stack);if (ret <0 ) goto done;
Quoted 6 lines
Quoted 3 lines
Signature · 1 line
2.44.0Is it possible to write a test to demonstrate the bug that was fixed here, or is it too much of a race condition to reliably trigger?
I wouldn't really know how to test for this in a way that is even somewhat reliably, unfortunately. You have to have at least two concurrent commands, one reading and one writing, where the first command loads the worktree stack and then tries to create an iter as above.
Now if this was part of the reftable library we could do it rather easily via t0032, which contains unit tests for the reftable library. But in the reftable backend it's much harder.
Patrick
PATCH 2/7 reftable/record: convert old and new object IDs to arrays
In reply to Patrick SteinhardtIn 7af607c58d (reftable/record: store "val1" hashes as static arrays, 2024-01-03) and b31e3cc620 (reftable/record: store "val2" hashes as static arrays, 2024-01-03) we have converted ref records to store their object IDs in a static array. Convert log records to do the same so that their old and new object IDs are arrays, too.
This change results in two allocations less per log record that we're iterating over. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 8,068,495 allocs, 8,068,373 frees, 401,011,862 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 6,068,489 allocs, 6,068,367 frees, 361,011,822 bytes allocated
--- refs/reftable-backend.c | 39 ++++++------------------ reftable/merged_test.c | 11 +++---- reftable/readwrite_test.c | 62 +++++++++++++++----------------------- reftable/record.c | 59 ++++++------------------------------ reftable/record_test.c | 11 ------- reftable/reftable-record.h | 4 +-- reftable/stack_test.c | 26 +++++++--------- 7 files changed, 61 insertions(+), 151 deletions(-)
refs/reftable-backend.c +9 -30
@@ -171,23 +171,6 @@ static int should_write_log(struct ref_store *refs, const char *refname) } } -static void clear_reftable_log_record (struct reftable_log_record *log) -{ -switch (log->value_type) { -case REFTABLE_LOG_UPDATE : -/* -* When we write log records, the hashes are owned by the -* caller and thus shouldn't be free'd. -*/ - log->value.update.old_hash =NULL ; - log->value.update.new_hash =NULL ; -break ; -case REFTABLE_LOG_DELETION : -break ; - } -reftable_log_record_release (log); -} -static void fill_reftable_log_record (struct reftable_log_record *log) {const char *info =git_committer_info (0 );
@@ -1102,8 +1085,8 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_datafill_reftable_log_record (log); log->update_index = ts; log->refname =xstrdup (u->refname); - log->value.update.new_hash = u->new_oid.hash; - log->value.update.old_hash = tx_update->current_oid.hash; +memcpy (log->value.update.new_hash, u->new_oid.hash,GIT_MAX_RAWSZ ); +memcpy (log->value.update.old_hash, tx_update->current_oid.hash,GIT_MAX_RAWSZ ); log->value.update.message =xstrndup (u->msg, arg->refs->write_options.block_size /2 ); }
@@ -1158,7 +1141,7 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data done:assert (ret !=REFTABLE_API_ERROR );for (i =0 ; i < logs_nr; i++) -clear_reftable_log_record (&logs[i]); +reftable_log_record_release (&logs[i]);free (logs);return ret; }
@@ -1275,13 +1258,13 @@ static int write_create_symref_table(struct reftable_writer *writer, void *cb_da log.update_index = ts; log.value.update.message =xstrndup (create->logmsg, create->refs->write_options.block_size /2 ); - log.value.update.new_hash = new_oid.hash; +memcpy (log.value.update.new_hash, new_oid.hash,GIT_MAX_RAWSZ );if (refs_resolve_ref_unsafe (&create->refs->base, create->refname,RESOLVE_REF_READING , &old_oid,NULL )) - log.value.update.old_hash = old_oid.hash; +memcpy (log.value.update.old_hash, old_oid.hash,GIT_MAX_RAWSZ ); ret =reftable_writer_add_log (writer, &log); -clear_reftable_log_record (&log); +reftable_log_record_release (&log);return ret; }
@@ -1420,7 +1403,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data) logs[logs_nr].update_index = deletion_ts; logs[logs_nr].value.update.message =xstrndup (arg->logmsg, arg->refs->write_options.block_size /2 ); - logs[logs_nr].value.update.old_hash = old_ref.value.val1; +memcpy (logs[logs_nr].value.update.old_hash, old_ref.value.val1,GIT_MAX_RAWSZ ); logs_nr++; ret =read_ref_without_reload (arg->stack,"HEAD" , &head_oid, &head_referent, &head_type);
@@ -1452,7 +1435,7 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data) logs[logs_nr].update_index = creation_ts; logs[logs_nr].value.update.message =xstrndup (arg->logmsg, arg->refs->write_options.block_size /2 ); - logs[logs_nr].value.update.new_hash = old_ref.value.val1; +memcpy (logs[logs_nr].value.update.new_hash, old_ref.value.val1,GIT_MAX_RAWSZ ); logs_nr++;/*
@@ -1515,10 +1498,6 @@ static int write_copy_table(struct reftable_writer *writer, void *cb_data)for (i =0 ; i < logs_nr; i++) {if (!strcmp (logs[i].refname,"HEAD" ))continue ; -if (logs[i].value.update.old_hash == old_ref.value.val1) - logs[i].value.update.old_hash =NULL ; -if (logs[i].value.update.new_hash == old_ref.value.val1) - logs[i].value.update.new_hash =NULL ; logs[i].refname =NULL ;reftable_log_record_release (&logs[i]); }
@@ -2180,7 +2159,7 @@ static int reftable_be_reflog_expire(struct ref_store *ref_store, dest->value_type =REFTABLE_LOG_DELETION ; }else {if ((flags &EXPIRE_REFLOGS_REWRITE ) && last_hash) - dest->value.update.old_hash = last_hash; +memcpy (dest->value.update.old_hash, last_hash,GIT_MAX_RAWSZ ); last_hash = logs[i].value.update.new_hash; } }
reftable/merged_test.c +4 -7
@@ -289,16 +289,13 @@ merged_table_from_log_records(struct reftable_log_record **logs,static void test_merged_logs (void ) { -uint8_t hash1[GIT_SHA1_RAWSZ ] = {1 }; -uint8_t hash2[GIT_SHA1_RAWSZ ] = {2 }; -uint8_t hash3[GIT_SHA1_RAWSZ ] = {3 };struct reftable_log_record r1[] = { { .refname ="a" , .update_index =2 , .value_type =REFTABLE_LOG_UPDATE , .value.update = { - .old_hash = hash2, + .old_hash = {2 },/* deletion */ .name ="jane doe" , .email ="jane@invalid" ,
@@ -310,8 +307,8 @@ static void test_merged_logs(void) .update_index = 1, .value_type = REFTABLE_LOG_UPDATE, .value.update = { - .old_hash = hash1, - .new_hash = hash2, + .old_hash = { 1 }, + .new_hash = { 2 }, .name = "jane doe", .email = "jane@invalid", .message = "message1",
@@ -324,7 +321,7 @@ static void test_merged_logs(void) .update_index = 3, .value_type = REFTABLE_LOG_UPDATE, .value.update = { - .new_hash = hash3, + .new_hash = { 3 }, .name = "jane doe", .email = "jane@invalid", .message = "message3",
reftable/readwrite_test.c +25 -37
128 lines not on this page · raw message
reftable/record.c +10 -49
113 lines not on this page · raw message
reftable/record_test.c +0 -11
@@ -183,8 +183,6 @@ static void test_reftable_log_record_roundtrip(void) .value_type = REFTABLE_LOG_UPDATE, .value = { .update = { - .old_hash = reftable_malloc(GIT_SHA1_RAWSZ), - .new_hash = reftable_malloc(GIT_SHA1_RAWSZ), .name = xstrdup("han-wen"), .email = xstrdup("[email protected]"), .message = xstrdup("test"),
@@ -202,13 +200,6 @@ static void test_reftable_log_record_roundtrip(void) .refname = xstrdup("branch"), .update_index = 33, .value_type = REFTABLE_LOG_UPDATE, - .value = { - .update = { - .old_hash = reftable_malloc(GIT_SHA1_RAWSZ), - .new_hash = reftable_malloc(GIT_SHA1_RAWSZ), - /* rest of fields left empty. */ - }, - }, } };set_test_hash (in[0 ].value.update.new_hash,1 );
@@ -231,8 +222,6 @@ static void test_reftable_log_record_roundtrip(void) .value_type = REFTABLE_LOG_UPDATE, .value = { .update = { - .new_hash = reftable_calloc(GIT_SHA1_RAWSZ, 1), - .old_hash = reftable_calloc(GIT_SHA1_RAWSZ, 1), .name = xstrdup("old name"), .email = xstrdup("old@email"), .message = xstrdup("old message"),
reftable/reftable-record.h +2 -2
@@ -88,8 +88,8 @@ struct reftable_log_record {union {struct { -uint8_t *new_hash; -uint8_t *old_hash; +unsigned char new_hash[GIT_MAX_RAWSZ ]; +unsigned char old_hash[GIT_MAX_RAWSZ ];char *name;char *email;uint64_t time;
reftable/stack_test.c +11 -15
@@ -468,8 +468,6 @@ static void test_reftable_stack_add(void) logs[i].refname =xstrdup (buf); logs[i].update_index =N + i +1 ; logs[i].value_type =REFTABLE_LOG_UPDATE ; - - logs[i].value.update.new_hash =reftable_malloc (GIT_SHA1_RAWSZ ); logs[i].value.update.email =xstrdup ("identity@invalid" );set_test_hash (logs[i].value.update.new_hash, i); }
@@ -547,16 +545,17 @@ static void test_reftable_stack_log_normalize(void) };struct reftable_stack *st =NULL ;char *dir =get_tmp_dir (__LINE__); - -uint8_t h1[GIT_SHA1_RAWSZ ] = {0x01 }, h2[GIT_SHA1_RAWSZ ] = {0x02 }; - -struct reftable_log_record input = { .refname ="branch" , - .update_index =1 , - .value_type =REFTABLE_LOG_UPDATE , - .value = { .update = { - .new_hash = h1, - .old_hash = h2, - } } }; +struct reftable_log_record input = { + .refname ="branch" , + .update_index =1 , + .value_type =REFTABLE_LOG_UPDATE , + .value = { + .update = { + .new_hash = {1 }, + .old_hash = {2 }, + }, + }, + };struct reftable_log_record dest = { .update_index =0 , };
@@ -627,8 +626,6 @@ static void test_reftable_stack_tombstone(void) logs[i].update_index =42 ;if (i %2 ==0 ) { logs[i].value_type =REFTABLE_LOG_UPDATE ; - logs[i].value.update.new_hash = -reftable_malloc (GIT_SHA1_RAWSZ );set_test_hash (logs[i].value.update.new_hash, i); logs[i].value.update.email =xstrdup ("identity@invalid" );
@@ -810,7 +807,6 @@ static void test_reflog_expire(void) logs[i].update_index = i; logs[i].value_type =REFTABLE_LOG_UPDATE ; logs[i].value.update.time = i; - logs[i].value.update.new_hash =reftable_malloc (GIT_SHA1_RAWSZ ); logs[i].value.update.email =xstrdup ("identity@invalid" );set_test_hash (logs[i].value.update.new_hash, i); }
Signature · 1 line
2.44.0
PATCH 3/7 reftable/record: avoid copying author info
In reply to Patrick SteinhardtEach reflog entry contains information regarding the authorship of who has made the change. This authorship information is not the same as that of any of the commits that the reflog entry references, but instead corresponds to the local user that has executed the command. Thus, it is almost always the case that all reflog entries have the same author.
We can make use of this fact when decoding reftable records: instead of freeing and then reallocating the authorship information of log records, we can special-case when the next record during an iteration has the exact same authorship as the preceding record. If so, then there is no need to reallocate the respective fields.
This change results in two allocations less per log record that we're iterating over in the most common case. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 6,068,489 allocs, 6,068,367 frees, 361,011,822 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 4,068,487 allocs, 4,068,365 frees, 332,011,793 bytes allocated
An alternative would be to store the capacity of both name and email and then use REFTABLE_ALLOC_GROW() to conditionally reallocate the array. But reftable records are copied around quite a lot, and thus we need to be a bit mindful of the overall record size. Furthermore, a memory comparison should also be more efficient than having to copy over memory even if we wouldn't have to allocate a new array every time.
--- reftable/record.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-)
reftable/record.c +21 -8
@@ -896,10 +896,19 @@ static int reftable_log_record_decode(void *rec, struct strbuf key, goto done;string_view_consume (&in, n); - r->value.update.name = -reftable_realloc (r->value.update.name, dest.len +1 ); -memcpy (r->value.update.name, dest.buf, dest.len); - r->value.update.name[dest.len] =0 ; +/* +* In almost all cases we can expect the reflog name to not change for +* reflog entries as they are tied to the local identity, not to the +* target commits. As an optimization for this common case we can thus +* skip copying over the name in case it's accurate already. +*/ +if (!r->value.update.name || +strcmp (r->value.update.name, dest.buf)) { + r->value.update.name = +reftable_realloc (r->value.update.name, dest.len +1 ); +memcpy (r->value.update.name, dest.buf, dest.len); + r->value.update.name[dest.len] =0 ; + }strbuf_reset (&dest); n =decode_string (&dest, in);
@@ -907,10 +916,14 @@ static int reftable_log_record_decode(void *rec, struct strbuf key, goto done;string_view_consume (&in, n); - r->value.update.email = -reftable_realloc (r->value.update.email, dest.len +1 ); -memcpy (r->value.update.email, dest.buf, dest.len); - r->value.update.email[dest.len] =0 ; +/* Same as above, but for the reflog email. */ +if (!r->value.update.email || +strcmp (r->value.update.email, dest.buf)) { + r->value.update.email = +reftable_realloc (r->value.update.email, dest.len +1 ); +memcpy (r->value.update.email, dest.buf, dest.len); + r->value.update.email[dest.len] =0 ; + } ts =0 ; n =get_var_int (&ts, &in);
Signature · 1 line
2.44.0
Re: [PATCH 3/7] reftable/record: avoid copying author info
Quoted 5 lines · Patrick Steinhardt
Each reflog entry contains information regarding the authorship of who has made the change. This authorship information is not the same as that of any of the commits that the reflog entry references, but instead corresponds to the local user that has executed the command. Thus, it is almost always the case that all reflog entries have the same author.
What are your thoughts on simplifying this explanation a little bit? I gave it a try below:
Each reflog entry contains authorship information indicating who has made the change. The author here corresponds to the local user who has executed the command rather than the author of the referenced commits. Thus, it is almost always the case that all reflog entries have the same author.
Cheers,
James
Re: [PATCH 3/7] reftable/record: avoid copying author info
Quoted 7 lines · James Liu
Quoted 5 lines · Patrick Steinhardt
Each reflog entry contains information regarding the authorship of who has made the change. This authorship information is not the same as that of any of the commits that the reflog entry references, but instead corresponds to the local user that has executed the command. Thus, it is almost always the case that all reflog entries have the same author.
Quoted 7 lines
What are your thoughts on simplifying this explanation a little bit? I gave it a try below:
Each reflog entry contains authorship information indicating who has made the change. The author here corresponds to the local user who has executed the command rather than the author of the referenced commits. Thus, it is almost always the case that all reflog entries have the same author.
That would've been a bit shorter indeed. But the patch series has been merged to next by now, so I'll leave it at that.
Thanks!
Patrick
PATCH 4/7 reftable/record: reuse refnames when decoding log records
In reply to Patrick SteinhardtWhen decoding a log record we always reallocate their refname arrays. This results in quite a lot of needless allocation churn.
Refactor the code to grow the array as required only. Like this, we should usually only end up reallocating the array a small handful of times when iterating over many refs. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 4,068,487 allocs, 4,068,365 frees, 332,011,793 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 3,068,488 allocs, 3,068,366 frees, 307,122,961 bytes allocated
--- reftable/record.c | 2 +- reftable/reftable-record.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-)
reftable/record.c +1 -1
@@ -861,7 +861,7 @@ static int reftable_log_record_decode(void *rec, struct strbuf key,if (key.len <=9 || key.buf[key.len -9 ] !=0 )return REFTABLE_FORMAT_ERROR ; - r->refname =reftable_realloc (r->refname, key.len -8 ); +REFTABLE_ALLOC_GROW (r->refname, key.len -8 , r->refname_cap);memcpy (r->refname, key.buf, key.len -8 ); ts =get_be64 (key.buf + key.len -8 );
reftable/reftable-record.h +1 -0
@@ -74,6 +74,7 @@ int reftable_ref_record_equal(const struct reftable_ref_record *a,/* reftable_log_record holds a reflog entry */ struct reftable_log_record {char *refname; +size_t refname_cap;uint64_t update_index;/* logical timestamp of a transactional update. */
Signature · 1 line
2.44.0
PATCH 5/7 reftable/record: reuse message when decoding log records
In reply to Patrick SteinhardtSame as the preceding commit we can allocate log messages as needed when decoding log records, thus further reducing the number of allocations. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 3,068,488 allocs, 3,068,366 frees, 307,122,961 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 2,068,487 allocs, 2,068,365 frees, 305,122,946 bytes allocated
--- reftable/record.c | 5 +++-- reftable/reftable-record.h | 1 + 2 files changed, 4 insertions(+), 2 deletions(-)
reftable/record.c +3 -2
@@ -871,6 +871,7 @@ static int reftable_log_record_decode(void *rec, struct strbuf key,switch (r->value_type) {case REFTABLE_LOG_UPDATE :FREE_AND_NULL (r->value.update.message); + r->value.update.message_cap =0 ;FREE_AND_NULL (r->value.update.email);FREE_AND_NULL (r->value.update.name);break ;
@@ -943,8 +944,8 @@ static int reftable_log_record_decode(void *rec, struct strbuf key, goto done;string_view_consume (&in, n); - r->value.update.message = -reftable_realloc (r->value.update.message, dest.len +1 ); +REFTABLE_ALLOC_GROW (r->value.update.message, dest.len +1 , + r->value.update.message_cap);memcpy (r->value.update.message, dest.buf, dest.len); r->value.update.message[dest.len] =0 ;
reftable/reftable-record.h +1 -0
@@ -96,6 +96,7 @@ struct reftable_log_record {uint64_t time;int16_t tz_offset;char *message; +size_t message_cap; } update; } value; };
Signature · 1 line
2.44.0
PATCH 6/7 reftable/record: use scratch buffer when decoding records
In reply to Patrick SteinhardtWhen decoding log records we need a temporary buffer to decode the reflog entry's name, mail address and message. As this buffer is local to the function we thus have to reallocate it for every single log record which we're about to decode, which is inefficient.
Refactor the code such that callers need to pass in a scratch buffer, which allows us to reuse it for multiple decodes. This reduces the number of allocations when iterating through reflogs. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 2,068,487 allocs, 2,068,365 frees, 305,122,946 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 1,068,485 allocs, 1,068,363 frees, 281,122,886 bytes allocated
Note that this commit also drop some redundant calls to strbuf_reset() right before calling decode_string(). The latter already knows to reset the buffer, so there is no need for these.
--- reftable/block.c | 4 ++- reftable/block.h | 2 ++ reftable/record.c | 52 ++++++++++++++++++-------------------- reftable/record.h | 5 ++-- reftable/record_test.c | 57 ++++++++++++++++++++++++++---------------- 5 files changed, 68 insertions(+), 52 deletions(-)
reftable/block.c +3 -1
@@ -332,7 +332,8 @@ int block_iter_next(struct block_iter *it, struct reftable_record *rec)return REFTABLE_FORMAT_ERROR ;string_view_consume (&in, n); - n =reftable_record_decode (rec, it->last_key, extra, in, it->br->hash_size); + n =reftable_record_decode (rec, it->last_key, extra, in, it->br->hash_size, + &it->scratch);if (n <0 )return -1 ;string_view_consume (&in, n);
@@ -369,6 +370,7 @@ int block_iter_seek(struct block_iter *it, struct strbuf *want)void block_iter_close (struct block_iter *it) {strbuf_release (&it->last_key); +strbuf_release (&it->scratch); } int block_reader_seek(struct block_reader *br, struct block_iter *it,
reftable/block.h +2 -0
@@ -84,10 +84,12 @@ struct block_iter {/* key for last entry we read. */ struct strbuf last_key; +struct strbuf scratch; };#define BLOCK_ITER_INIT { \ .last_key = STRBUF_INIT, \ + .scratch = STRBUF_INIT, \ }/* initializes a block reader. */
reftable/record.c +24 -28
136 lines not on this page · raw message
reftable/record.h +3 -2
@@ -55,7 +55,8 @@ struct reftable_record_vtable {/* decode data from `src` into the record. */ int (*decode)(void *rec,struct strbuf key,uint8_t extra, -struct string_view src,int hash_size); +struct string_view src,int hash_size, +struct strbuf *scratch);/* deallocate and null the record. */ void (*release)(void *rec);
@@ -138,7 +139,7 @@ int reftable_record_encode(struct reftable_record *rec, struct string_view dest, int hash_size);int reftable_record_decode (struct reftable_record *rec,struct strbuf key,uint8_t extra,struct string_view src, -int hash_size); +int hash_size,struct strbuf *scratch);int reftable_record_is_deletion (struct reftable_record *rec);static inline uint8_t reftable_record_type (struct reftable_record *rec)
reftable/record_test.c +36 -21
136 lines not on this page · raw message
Signature · 1 line
2.44.0
Re: [PATCH 6/7] reftable/record: use scratch buffer when decoding records
Quoted 31 lines · Patrick Steinhardt
When decoding log records we need a temporary buffer to decode the reflog entry's name, mail address and message. As this buffer is local to the function we thus have to reallocate it for every single log record which we're about to decode, which is inefficient.
Refactor the code such that callers need to pass in a scratch buffer, which allows us to reuse it for multiple decodes. This reduces the number of allocations when iterating through reflogs. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 2,068,487 allocs, 2,068,365 frees, 305,122,946 bytes allocatedAfter:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 1,068,485 allocs, 1,068,363 frees, 281,122,886 bytes allocatedNote that this commit also drop some redundant calls to
strbuf_reset()right before callingdecode_string(). The latter already knows to reset the buffer, so there is no need for these.Signed-off-by Patrick Steinhardt <[email protected]>--- reftable/block.c | 4 ++- reftable/block.h | 2 ++ reftable/record.c | 52 ++++++++++++++++++-------------------- reftable/record.h | 5 ++-- reftable/record_test.c | 57 ++++++++++++++++++++++++++---------------- 5 files changed, 68 insertions(+), 52 deletions(-)
[snip]
Quoted 17 lines
reftable/record.c +1
-1@@ -374,7 +374,7 @@ static int reftable_ref_record_encode(const void *rec, struct string_view s,static int reftable_ref_record_decode (void *rec,struct strbuf key,uint8_t val_type,struct string_view in, -int hash_size) +int hash_size,struct strbuf *scratch) {struct reftable_ref_record *r = rec;struct string_view start = in;@@ -425,13 +425,12 @@ static int reftable_ref_record_decode(void *rec, struct strbuf key,break ;case REFTABLE_REF_SYMREF : {
reftable/record.c static int reftable_ref_record_decode(void *rec, struct strbuf key, · line 434
Quoted 10 lines
reftable/record.c +2
-3-struct strbuf dest =STRBUF_INIT ; -int n =decode_string (&dest, in); +int n =decode_string (scratch, in);if (n <0 ) {return -1 ; }string_view_consume (&in, n); - r->value.symref = dest.buf; + r->value.symref =strbuf_detach (scratch,NULL ); }break ;
I had to dig into this to convince myself that we aren't leaking memory here, but IIUC this gets cleaned up eventually by reftable_ref_record_release(), right?
Re: [PATCH 6/7] reftable/record: use scratch buffer when decoding records
Quoted 1 line · Josh Steadmon
On 2024.03.05 13:11, Patrick Steinhardt wrote:
[snip]
Quoted 16 lines
Quoted 16 lines
reftable/record.c +1
-1@@ -374,7 +374,7 @@ static int reftable_ref_record_encode(const void *rec, struct string_view s,static int reftable_ref_record_decode (void *rec,struct strbuf key,uint8_t val_type,struct string_view in, -int hash_size) +int hash_size,struct strbuf *scratch) {struct reftable_ref_record *r = rec;struct string_view start = in;@@ -425,13 +425,12 @@ static int reftable_ref_record_decode(void *rec, struct strbuf key,break ;
Quoted 15 lines
reftable/record.c static int reftable_ref_record_decode(void *rec, struct strbuf key, · line 434
Quoted 11 lines
reftable/record.c +2
-3case REFTABLE_REF_SYMREF : { -struct strbuf dest =STRBUF_INIT ; -int n =decode_string (&dest, in); +int n =decode_string (scratch, in);if (n <0 ) {return -1 ; }string_view_consume (&in, n); - r->value.symref = dest.buf; + r->value.symref =strbuf_detach (scratch,NULL ); }break ;I had to dig into this to convince myself that we aren't leaking memory here, but IIUC this gets cleaned up eventually by
reftable_ref_record_release(), right?
Yes, exactly.
Patrick
Re: [PATCH 6/7] reftable/record: use scratch buffer when decoding records
In reply to Patrick SteinhardtQuoted 23 lines · Patrick Steinhardt
When decoding log records we need a temporary buffer to decode the reflog entry's name, mail address and message. As this buffer is local to the function we thus have to reallocate it for every single log record which we're about to decode, which is inefficient.
Refactor the code such that callers need to pass in a scratch buffer, which allows us to reuse it for multiple decodes. This reduces the number of allocations when iterating through reflogs. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 2,068,487 allocs, 2,068,365 frees, 305,122,946 bytes allocatedAfter:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 1,068,485 allocs, 1,068,363 frees, 281,122,886 bytes allocatedNote that this commit also drop some redundant calls to
strbuf_reset()right before callingdecode_string(). The latter already knows to reset the buffer, so there is no need for these.
Quoted 8 lines
Signed-off-by Patrick Steinhardt <[email protected]>--- reftable/block.c | 4 ++- reftable/block.h | 2 ++ reftable/record.c | 52 ++++++++++++++++++-------------------- reftable/record.h | 5 ++-- reftable/record_test.c | 57 ++++++++++++++++++++++++++---------------- 5 files changed, 68 insertions(+), 52 deletions(-)
At first glance I was feeling somewhat negatively about this change, as keeping persistent scratch space buffers means that either the owners or the users of the scratch space need to be more careful about making sure it's reset and that they're not accumulating cruft in between various calls.
However, it appears we already have similar scratch space usage in the sideband and cat-file code, so I guess we are OK with the pattern in general, and we can rely on tests to make sure things are good in practice.
PATCH 7/7 refs/reftable: track last log record name via strbuf
In reply to Patrick SteinhardtThe reflog iterator enumerates all reflogs known to a ref backend. In the "reftable" backend there is no way to list all existing reflogs directly. Instead, we have to iterate through all reflog entries and discard all those redundant entries for which we have already returned a reflog entry.
This logic is implemented by tracking the last reflog name that we have emitted to the iterator's user. If the next log record has the same name we simply skip it until we find another record with a different refname.
This last reflog name is stored in a simple C string, which requires us to free and reallocate it whenever we need to update the reflog name. Convert it to use a struct strbuf instead, which reduces the number of allocations. Before:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 1,068,485 allocs, 1,068,363 frees, 281,122,886 bytes allocated
After:
HEAP SUMMARY:
in use at exit: 13,473 bytes in 122 blocks
total heap usage: 68,485 allocs, 68,363 frees, 256,234,072 bytes allocated
Note that even after this change we still allocate quite a lot of data, even though the number of allocations does not scale with the number of log records anymore. This remainder comes mostly from decompressing the log blocks, where we decompress each block into newly allocated memory. This will be addressed at a later point in time.
--- refs/reftable-backend.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-)
refs/reftable-backend.c +6 -5
@@ -1575,7 +1575,7 @@ struct reftable_reflog_iterator {struct reftable_ref_store *refs;struct reftable_iterator iter;struct reftable_log_record log; -char *last_name; +struct strbuf last_name;int err; };
@@ -1594,15 +1594,15 @@ static int reftable_reflog_iterator_advance(struct ref_iterator *ref_iterator)* we've already produced this name. This could be faster by * seeking directly to reflog@update_index==0. */ -if (iter->last_name && !strcmp (iter->log.refname, iter->last_name)) +if (!strcmp (iter->log.refname, iter->last_name.buf))continue ;if (check_refname_format (iter->log.refname,REFNAME_ALLOW_ONELEVEL ))continue ; -free (iter->last_name); - iter->last_name =xstrdup (iter->log.refname); +strbuf_reset (&iter->last_name); +strbuf_addstr (&iter->last_name, iter->log.refname); iter->base.refname = iter->log.refname;break ;
@@ -1635,7 +1635,7 @@ static int reftable_reflog_iterator_abort(struct ref_iterator *ref_iterator) (struct reftable_reflog_iterator *)ref_iterator;reftable_log_record_release (&iter->log);reftable_iterator_destroy (&iter->iter); -free (iter->last_name); +strbuf_release (&iter->last_name);free (iter);return ITER_DONE ; }
@@ -1655,6 +1655,7 @@ static struct reftable_reflog_iterator *reflog_iterator_for_stack(struct reftabl iter =xcalloc (1 ,sizeof (*iter));base_ref_iterator_init (&iter->base, &reftable_reflog_iterator_vtable); +strbuf_init (&iter->last_name,0 ); iter->refs = refs; ret = refs->err;
Signature · 1 line
2.44.0
Quoted 30 lines · Patrick Steinhardt
Hi,
this patch series does the same as all the preceding patch series that optimized how the reftable library iterates through refs, but for reflogs instead.
The goal of this patch series is to arrive at a constant number of allocations when iterating refs. This is achieved in mostly the same way we did it for ref iteration, namely by reusing already-allocated memory. Overall, this brings us down from 8 allocations per reflog record to essentially 0 allocations per reflog. Iterating through 1 million reflogs with
git reflog listthus goes down from 8.068m allocations to only around 68.5k.This series is built on top of "master" at
b387623c12(The third batch, 2024-03-01) with Junio's "ps/reftable-iteration-perf-part2" at43f70eaea0(refs/reftable: precompute prefix length, 2024-03-04) merged into it.Patrick
Patrick Steinhardt (7):
refs/reftable: reload correct stack when creating reflog iter reftable/record: convert old and new object IDs to arrays reftable/record: avoid copying author info reftable/record: reuse refnames when decoding log records reftable/record: reuse message when decoding log records reftable/record: use scratch buffer when decoding records refs/reftable: track last log record name via strbuf
Quoted 15 lines
refs/reftable-backend.c | 52 +++++---------- reftable/block.c | 4 +- reftable/block.h | 2 + reftable/merged_test.c | 11 ++-- reftable/readwrite_test.c | 62 +++++++----------- reftable/record.c | 129 ++++++++++++++----------------------- reftable/record.h | 5 +- reftable/record_test.c | 68 ++++++++++--------- reftable/reftable-record.h | 6 +- reftable/stack_test.c | 26 ++++---- 10 files changed, 154 insertions(+), 211 deletions(-)Signature · 1 line
2.44.0
This series looks good to me (with one request that we add a test for patch #1 if possible).
Quoted 43 lines · Josh Steadmon
Quoted 42 lines · Patrick Steinhardt
Hi,
this patch series does the same as all the preceding patch series that optimized how the reftable library iterates through refs, but for reflogs instead.
The goal of this patch series is to arrive at a constant number of allocations when iterating refs. This is achieved in mostly the same way we did it for ref iteration, namely by reusing already-allocated memory. Overall, this brings us down from 8 allocations per reflog record to essentially 0 allocations per reflog. Iterating through 1 million reflogs with
git reflog listthus goes down from 8.068m allocations to only around 68.5k.This series is built on top of "master" at
b387623c12(The third batch, 2024-03-01) with Junio's "ps/reftable-iteration-perf-part2" at43f70eaea0(refs/reftable: precompute prefix length, 2024-03-04) merged into it.Patrick
Patrick Steinhardt (7):
refs/reftable: reload correct stack when creating reflog iter reftable/record: convert old and new object IDs to arrays reftable/record: avoid copying author info reftable/record: reuse refnames when decoding log records reftable/record: reuse message when decoding log records reftable/record: use scratch buffer when decoding records refs/reftable: track last log record name via strbufrefs/reftable-backend.c | 52 +++++---------- reftable/block.c | 4 +- reftable/block.h | 2 + reftable/merged_test.c | 11 ++-- reftable/readwrite_test.c | 62 +++++++----------- reftable/record.c | 129 ++++++++++++++----------------------- reftable/record.h | 5 +- reftable/record_test.c | 68 ++++++++++--------- reftable/reftable-record.h | 6 +- reftable/stack_test.c | 26 ++++---- 10 files changed, 154 insertions(+), 211 deletions(-)
Quoted 8 lines
Quoted 3 lines
Signature · 1 line
2.44.0This series looks good to me (with one request that we add a test for patch #1 if possible).
Reviewed-by Josh Steadmon <[email protected]>
Thanks for your review!
Patrick