git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 17:25 UTC

[PATCH v4 04/10] upload-pack: reduce lock contention when writing packfile data

From
Patrick Steinhardt <ps@pks.im>
Date
Mar 13, 2026, 06:45 UTC
Message-ID
<20260313-pks-upload-pack-write-contention-v4-4-7a9668061f7f@pks.im>
In-Reply-To
<20260313-pks-upload-pack-write-contention-v4-0-7a9668061f7f@pks.im>

In our production systems we have recently observed write contention in git-upload-pack(1). The system in question was consistently streaming packfiles at a rate of dozens of gigabits per second, but curiously the system was neither bottlenecked on CPU, memory or IOPS.

We eventually discovered that Git was spending 80% of its time in `pipe_write()`, out of which almost all of the time was spent in the `ep_poll_callback` function in the kernel. Quoting the reporter:

  This infrastructure is part of an event notification queue designed to
  allow for multiple producers to emit events, but that concurrency
  safety is guarded by 3 layers of locking. The layer we're hitting
  contention in uses a simple reader/writer lock mode (a.k.a. shared
  versus exclusive mode), where producers need shared-mode (read mode),
  and various other actions use exclusive (write) mode.

The system in question generates workloads where we have hundreds of git-upload-pack(1) processes active at the same point in time. These processes end up contending around those locks, and the consequence is that the Git processes stall.

Now git-upload-pack(1) already has the infrastructure in place to buffer some of the data it reads from git-pack-objects(1) before actually sending it out. We only use this infrastructure in very limited ways though, so we generally end up matching one read(3p) call with one write(3p) call. Even worse, when the sideband is enabled we end up matching one read with _two_ writes: one for the pkt-line length, and one for the packfile data.

Extend our use of the buffering infrastructure so that we soak up bytes until the buffer is filled up at least 2/3rds of its capacity. The change is relatively simple to implement as we already know to flush the buffer in `create_pack_file()` after git-pack-objects(1) has finished.

This significantly reduces the number of write(3p) syscalls we need to do. Before this change, cloning the Linux repository resulted in around 400,000 write(3p) syscalls. With the buffering in place we only do around 130,000 syscalls.

Now we could of course go even further and make sure that we always fill up the whole buffer. But this might cause an increase in read(3p) syscalls, and some tests show that this only reduces the number of write(3p) syscalls from 130,000 to 100,000. So overall this doesn't seem worth it.

Note that the issue could also be fixed by adapting the write buffer that we use in the downstream git-pack-objects(1) command, and such a change would have roughly the same result. But the command that generates the packfile data may not always be git-pack-objects(1) as it can be changed via "uploadpack.packObjectsHook", so such a fix would only help in _some_ cases. Regardless of that, we'll also adapt the write buffer size of git-pack-objects(1) in a subsequent commit.

Helped-by: Matt Smiley <msmiley@gitlab.com>
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 upload-pack.c | 7 +++++++
 1 file changed, 7 insertions(+)
diff --git a/upload-pack.c b/upload-pack.c
index 7a165d226d..9f6d6fe48c 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -276,6 +276,13 @@ static int relay_pack_data(int pack_objects_out, struct output_state *os,
 		}
 	}
 
+	/*
+	 * Make sure that we buffer some data before sending it to the client.
+	 * This significantly reduces the number of write(3p) syscalls.
+	 */
+	if (readsz && os->used < (sizeof(os->buffer) * 2 / 3))
+		return readsz;
+
 	if (os->used > 1) {
 		send_client_data(1, os->buffer, os->used - 1, use_sideband);
 		os->buffer[0] = os->buffer[os->used - 1];
-- 
2.53.0.904.g2727be2e99.dirty
Previous: Patrick SteinhardtNext: Patrick Steinhardt
Message 5 of 11 in “upload-pack: reduce lock contention when writing packfile data”
  1. 00/10 upload-pack: reduce lock contention when writing packfile dataPatrick Steinhardt, Mar 13, 2026
  2. 01/10 upload-pack: fix debug statement when flushing packfile dataPatrick Steinhardt, Mar 13, 2026
  3. 02/10 upload-pack: adapt keepalives based on bufferingPatrick Steinhardt, Mar 13, 2026
  4. 03/10 upload-pack: prefer flushing data over sending keepalivePatrick Steinhardt, Mar 13, 2026
  5. 04/10 upload-pack: reduce lock contention when writing packfile dataPatrick Steinhardt, Mar 13, 2026
  6. 05/10 compat/posix: introduce writev(3p) wrapperPatrick Steinhardt, Mar 13, 2026
  7. 06/10 wrapper: introduce writev(3p) wrappersPatrick Steinhardt, Mar 13, 2026
  8. 07/10 sideband: use writev(3p) to send pktlinesPatrick Steinhardt, Mar 13, 2026
  9. 08/10 csum-file: introduce `hashfd_ext()`Patrick Steinhardt, Mar 13, 2026
  10. 09/10 csum-file: drop `hashfd_throughput()`Patrick Steinhardt, Mar 13, 2026
  11. 10/10 builtin/pack-objects: reduce lock contention when writing packfile dataPatrick Steinhardt, Mar 13, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.