From: Adrian Ratiu <hidden> Date: 2026-03-02 19:17:43
Exit early if the hooks do not exist, to avoid spinning up/down
sideband async threads which no-op.
It is important to call the hook_exists() API provided by hook.[ch]
because it covers both config-defined hooks and the "traditional"
hooks from the hookdir. find_hook() only covers the hookdir hooks.
The regression happened because the no-op async threads add some
additional overhead which can be measured with the receive-refs test
of the benchmarks suite [1].
Reproduced using:
cd benchmarks/receive-refs && \
./run --revisions /path/to/git \
fc148b146ad41be71a7852c4867f0773cbfe1ff9~,fc148b146ad41be71a7852c4867f0773cbfe1ff9 \
--parameter-list refformat reftable --parameter-list refcount 10000
1: https://gitlab.com/gitlab-org/data-access/git/benchmarks
Fixes: fc148b146ad4 ("receive-pack: convert update hooks to new API")
Reported-by: Patrick Steinhardt <redacted>
Helped-by: Jeff King [off-list ref]
Signed-off-by: Adrian Ratiu <redacted>
---
builtin/receive-pack.c | 9 +++++++++
1 file changed, 9 insertions(+)
@@ -934,6 +934,9 @@ static int run_receive_hook(struct command *commands,intsaved_stderr=-1;intret;+if(!hook_exists(the_repository,hook_name))+return0;+/* if there are no valid commands, don't invoke the hook at all. */while(iter&&skip_broken&&(iter->error_string||iter->did_not_exist))iter=iter->next;
@@ -980,6 +983,9 @@ static int run_update_hook(struct command *cmd)intsaved_stderr=-1;intcode;+if(!hook_exists(the_repository,"update"))+return0;+strvec_pushl(&opt.args,cmd->ref_name,oid_to_hex(&cmd->old_oid),
From: Patrick Steinhardt <hidden> Date: 2026-03-03 06:11:21
On Mon, Mar 02, 2026 at 09:17:04PM +0200, Adrian Ratiu wrote:
Exit early if the hooks do not exist, to avoid spinning up/down
sideband async threads which no-op.
It is important to call the hook_exists() API provided by hook.[ch]
because it covers both config-defined hooks and the "traditional"
hooks from the hookdir. find_hook() only covers the hookdir hooks.
Just out of curiosity: will `find_hook()` eventually be removed? I saw
that we still use it for the "proc-receive" hook in git-receive-pack(1)
for example, which feels a bit fishy to me.
In any case, if this is an oversight then this can be handled in a
subsequent patch series, if you ask me.
@@ -934,6 +934,9 @@ static int run_receive_hook(struct command *commands,intsaved_stderr=-1;intret;+if(!hook_exists(the_repository,hook_name))+return0;+/* if there are no valid commands, don't invoke the hook at all. */while(iter&&skip_broken&&(iter->error_string||iter->did_not_exist))iter=iter->next;
That fix is delightfully simple -- I was fearing for a deeper issue. I
can confirm that this restores original performance:
Benchmark 1: receive: many refs (refformat = reftable, refcount = 10000, revision = fc148b146ad41be71a7852c4867f0773cbfe1ff9~)
Time (mean ± σ): 177.4 ms ± 3.0 ms [User: 92.0 ms, System: 84.2 ms]
Range (min … max): 172.1 ms … 182.6 ms 15 runs
Benchmark 2: receive: many refs (refformat = reftable, refcount = 10000, revision = fc148b146ad41be71a7852c4867f0773cbfe1ff9)
Time (mean ± σ): 485.0 ms ± 7.1 ms [User: 180.0 ms, System: 375.0 ms]
Range (min … max): 466.9 ms … 491.0 ms 10 runs
Benchmark 3: receive: many refs (refformat = reftable, refcount = 10000, revision = 005f3fbe07a20dd5f7dea57f6f46cd797387e56a)
Time (mean ± σ): 178.1 ms ± 2.4 ms [User: 91.8 ms, System: 85.1 ms]
Range (min … max): 172.2 ms … 181.3 ms 15 runs
Summary
receive: many refs (refformat = reftable, refcount = 10000, revision = fc148b146ad41be71a7852c4867f0773cbfe1ff9~) ran
1.00 ± 0.02 times faster than receive: many refs (refformat = reftable, refcount = 10000, revision = 005f3fbe07a20dd5f7dea57f6f46cd797387e56a)
2.73 ± 0.06 times faster than receive: many refs (refformat = reftable, refcount = 10000, revision = fc148b146ad41be71a7852c4867f0773cbfe1ff9)
And Bencher has already picked up those changes, too, and graphs have
dropped back to previous levels. Awesome.
Thanks a lot for the quick turnaround!
Patrick
From: Adrian Ratiu <hidden> Date: 2026-03-03 12:45:53
On Tue, 03 Mar 2026, Patrick Steinhardt [off-list ref] wrote:
On Mon, Mar 02, 2026 at 09:17:04PM +0200, Adrian Ratiu wrote:
quoted
Exit early if the hooks do not exist, to avoid spinning up/down
sideband async threads which no-op.
It is important to call the hook_exists() API provided by hook.[ch]
because it covers both config-defined hooks and the "traditional"
hooks from the hookdir. find_hook() only covers the hookdir hooks.
Just out of curiosity: will `find_hook()` eventually be removed? I saw
that we still use it for the "proc-receive" hook in git-receive-pack(1)
for example, which feels a bit fishy to me.
The answer is a big YES and I actually thought about this while fixing
the regression yesterday (unrelated to proc-receive).
All hooks should use the new hook.[ch] APIs which provide clearer
functions like hook_exists() and all direct find_hook() / run-command
invocations should be removed.
In any case, if this is an oversight then this can be handled in a
subsequent patch series, if you ask me.
Yes, this can be done incrementally in a subsequent patch so that
proc-receive can also benefit from hook.[ch] features like being able to
specify it via configs.
It was out of scope for the initial patches, so I didn't pay too much
attention to it, but it should be rather simple to convert. I do plan to
convert it as well.
The end goal is to make find_hook() static (not exported outside hook.c)
once all its external uses have been converted.
From: Jeff King <hidden> Date: 2026-03-03 13:28:58
On Mon, Mar 02, 2026 at 09:17:04PM +0200, Adrian Ratiu wrote:
It is important to call the hook_exists() API provided by hook.[ch]
because it covers both config-defined hooks and the "traditional"
hooks from the hookdir. find_hook() only covers the hookdir hooks.
Ah, OK. Traditionally hook_exists() was just a thin wrapper over
find_hook(), but it looks like that changed in your series. But either
way, it much more clearly expresses the intent to use hook_exists().
So this obviously looks good, but just some random thoughts below.
quoted hunk
@@ -934,6 +934,9 @@ static int run_receive_hook(struct command *commands, int saved_stderr = -1; int ret;+ if (!hook_exists(the_repository, hook_name))+ return 0;
It is a little inelegant that we have to look up the hook data
separately ourselves here, and then it will be done again in
run_hooks_opt(). But I don't think there is an easy way to reorganize
it, short of something like:
struct hook myhook = HOOK_INIT;
load_hooks(&myhook, hook_name);
if (myhook.nr)
return 0; /* no hooks of this type */
...other prep work...
run_hooks_opt(repo, &myhook, &opt);
I doubt that is worth it, as the lookup process should not be too
expensive. It looks like we cache the config parts of the lookup
already. We call access() to find the traditional hooks on each lookup,
but that is also true of the code before your series.
It would not matter at all for pre-receive, for example, but for
something like update, I guess we are doing a bunch of pointless
access() calls that could be cached. But again, not new in your series,
and nobody has really noticed. So we can either treat it as an
optimization for later, or just leave it be forever.
-Peff