Skip to content

Commit 4bb2a85

Browse files
committed
index-pack: hash full blobs in a bounded worker pool
The first pass through a pack inflates and hashes each object on the main thread. `pack.threads` applies later while resolving deltas, so SHA1DC work for full blobs remains serial even when CPUs are idle. In a 99 Hz profile of an 844,020,252-byte pack dominated by full blobs, SHA1DC accounted for 72.14% of sampled user CPU. Add an opt-in worker pool for complete heap-backed blobs. The producer continues parsing, inflating, computing CRCs, and writing the pack. The workers use the normal object hashing backend, including SHA1DC, while the main thread performs the existing ODB, collision, and content checks as deferred results are retired in queue order. Bound retained data by both bytes and job count. Strict, fsck, promisor, non-threaded, streamed, and otherwise ineligible objects retain the serial path. The default is disabled. Across three runs of the original prototype on that pack, two workers reduced median wall time from 36.076 to 19.227 seconds. CPU changed from 50.121 to 50.955 seconds and peak RSS from 413.7 to 409.8 MiB. The object/delta-heavy control did not show a wall-time improvement. The fixed-size queue used here has not been rebenchmarked. t5352 covers output equivalence, queue limits, serial fallbacks, collision and duplicate handling, REF/OFS ordering, corrupt input, and configuration validation. p5352 compares serial, one-worker, and two-worker indexing on a reproducible full-blob pack. Existing SHA-1 and SHA-256 index-pack tests also pass.
1 parent c4ab2e0 commit 4bb2a85

6 files changed

Lines changed: 488 additions & 4 deletions

File tree

Documentation/config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@ include::config/includeif.adoc[]
518518

519519
include::config/index.adoc[]
520520

521+
include::config/indexpack.adoc[]
522+
521523
include::config/init.adoc[]
522524

523525
include::config/instaweb.adoc[]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
indexPack.hashThreads::
2+
Experimental number of workers used to hash full blobs during the
3+
first pass of linkgit:git-index-pack[1]. The default, zero, disables
4+
the workers. Values from 1 through 32 are accepted. Parsing and
5+
inflation remain serial, and normal collision/content validation runs
6+
on the main thread. This is independent of `pack.threads` and of
7+
concurrent packfile-URI downloads. Strict, fsck, and promisor modes,
8+
and builds without thread support, use the existing serial path.
9+
10+
indexPack.hashBufferSize::
11+
Maximum retained blob-buffer bytes for `indexPack.hashThreads`,
12+
including the terminating byte, the producer's reserved buffer, and
13+
completed results awaiting validation. Defaults to 64 MiB. At most
14+
twice the number of hash workers can be outstanding. Objects that do
15+
not fit use the existing serial path; the limit is not a bound on
16+
Git's total memory use. Usual `k`, `m`, and `g` suffixes are accepted.
17+
18+
indexPack.hashMinSize::
19+
Minimum full-blob size eligible for `indexPack.hashThreads`.
20+
Defaults to 64 KiB. Blobs above `core.bigFileThreshold` retain their
21+
existing streaming path. Usual `k`, `m`, and `g` suffixes are accepted.

Documentation/git-index-pack.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ accessible through promisor objects.
148148
+
149149
Requires <pack-file> to not be specified.
150150

151+
CONFIGURATION
152+
-------------
153+
154+
include::config/indexpack.adoc[]
155+
151156
NOTES
152157
-----
153158

builtin/index-pack.c

Lines changed: 234 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "run-command.h"
3232
#include "setup.h"
3333
#include "strvec.h"
34+
#include "trace2.h"
3435

3536
static const char index_pack_usage[] =
3637
"git index-pack [-v] [-o <index-file>] [--keep | --keep=<msg>] [--[no-]rev-index] [--verify] [--strict[=<msg-id>=<severity>...]] [--fsck-objects[=<msg-id>=<severity>...]] (<pack-file> | --stdin [--fix-thin] [<pack-file>])";
@@ -43,6 +44,43 @@ struct object_entry {
4344
signed char real_type;
4445
};
4546

47+
/*
48+
* Hash workers own no repository state. They only hash immutable full-blob
49+
* buffers; the main thread retires their results and performs the usual
50+
* collision/content checks before releasing those buffers.
51+
*/
52+
struct first_pass_hash_job {
53+
struct object_entry *obj;
54+
struct object_id oid;
55+
void *data;
56+
int done;
57+
};
58+
59+
struct first_pass_hash_pool {
60+
pthread_t *threads;
61+
pthread_mutex_t mutex;
62+
pthread_cond_t work_ready;
63+
pthread_cond_t result_ready;
64+
struct first_pass_hash_job *queue;
65+
size_t nr_threads, queue_size;
66+
size_t first, next_work, nr, pending;
67+
size_t buffered, jobs;
68+
int stop;
69+
};
70+
71+
#define FIRST_PASS_HASH_MAX_THREADS 32
72+
73+
static int first_pass_hash_threads;
74+
static size_t first_pass_hash_buffer_size = 64 * 1024 * 1024;
75+
static size_t first_pass_hash_min_size = 64 * 1024;
76+
static struct first_pass_hash_pool first_pass_hash_pool;
77+
78+
static struct first_pass_hash_job *reserve_first_pass_hash(struct object_entry *obj);
79+
static void submit_first_pass_hash(struct first_pass_hash_job *job,
80+
struct object_entry *obj, void *data);
81+
static void start_first_pass_hash(void);
82+
static void finish_first_pass_hash(void);
83+
4684
struct object_stat {
4785
unsigned delta_depth;
4886
int base_object_no;
@@ -479,7 +517,7 @@ static void *unpack_entry_data(off_t offset, size_t size,
479517
char hdr[32];
480518
int hdrlen;
481519

482-
if (!is_delta_type(type)) {
520+
if (!is_delta_type(type) && oid) {
483521
hdrlen = format_object_header(hdr, sizeof(hdr), type, size);
484522
git_hash_init(&c, the_hash_algo);
485523
git_hash_update(&c, hdr, hdrlen);
@@ -520,7 +558,8 @@ static void *unpack_entry_data(off_t offset, size_t size,
520558
static void *unpack_raw_entry(struct object_entry *obj,
521559
off_t *ofs_offset,
522560
struct object_id *ref_oid,
523-
struct object_id *oid)
561+
struct object_id *oid,
562+
struct first_pass_hash_job **hash_job)
524563
{
525564
unsigned char *p;
526565
size_t size, c;
@@ -582,7 +621,9 @@ static void *unpack_raw_entry(struct object_entry *obj,
582621
}
583622
obj->hdr_size = consumed_bytes - obj->idx.offset;
584623

585-
data = unpack_entry_data(obj->idx.offset, obj->size, obj->type, oid);
624+
*hash_job = reserve_first_pass_hash(obj);
625+
data = unpack_entry_data(obj->idx.offset, obj->size, obj->type,
626+
*hash_job ? NULL : oid);
586627
obj->idx.crc32 = input_crc32;
587628
return data;
588629
}
@@ -978,6 +1019,171 @@ static void sha1_object(const void *data, struct object_entry *obj_entry,
9781019
free(new_data);
9791020
}
9801021

1022+
static void *first_pass_hash_worker(void *data UNUSED)
1023+
{
1024+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1025+
1026+
trace2_thread_start("index-pack-hash");
1027+
for (;;) {
1028+
struct first_pass_hash_job *job;
1029+
1030+
pthread_mutex_lock(&pool->mutex);
1031+
while (!pool->pending && !pool->stop)
1032+
pthread_cond_wait(&pool->work_ready, &pool->mutex);
1033+
if (!pool->pending) {
1034+
pthread_mutex_unlock(&pool->mutex);
1035+
break;
1036+
}
1037+
job = &pool->queue[pool->next_work];
1038+
pool->next_work = (pool->next_work + 1) % pool->queue_size;
1039+
pool->pending--;
1040+
pthread_mutex_unlock(&pool->mutex);
1041+
1042+
/* This uses the same collision-detecting hash as the serial path. */
1043+
hash_object_file(the_hash_algo, job->data, job->obj->size,
1044+
OBJ_BLOB, &job->oid);
1045+
1046+
pthread_mutex_lock(&pool->mutex);
1047+
job->done = 1;
1048+
pthread_cond_signal(&pool->result_ready);
1049+
pthread_mutex_unlock(&pool->mutex);
1050+
}
1051+
trace2_thread_exit();
1052+
return NULL;
1053+
}
1054+
1055+
static void retire_first_pass_hash(void)
1056+
{
1057+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1058+
struct first_pass_hash_job *job;
1059+
size_t allocation;
1060+
1061+
pthread_mutex_lock(&pool->mutex);
1062+
if (!pool->nr)
1063+
BUG("no queued first-pass hash to retire");
1064+
job = &pool->queue[pool->first];
1065+
while (!job->done)
1066+
pthread_cond_wait(&pool->result_ready, &pool->mutex);
1067+
pool->first = (pool->first + 1) % pool->queue_size;
1068+
pool->nr--;
1069+
pthread_mutex_unlock(&pool->mutex);
1070+
1071+
/* All ODB and object-cache access stays on the main thread. */
1072+
oidcpy(&job->obj->idx.oid, &job->oid);
1073+
sha1_object(job->data, NULL, job->obj->size, OBJ_BLOB,
1074+
&job->obj->idx.oid);
1075+
allocation = st_add(job->obj->size, 1);
1076+
free(job->data);
1077+
pool->buffered -= allocation;
1078+
}
1079+
1080+
static struct first_pass_hash_job *reserve_first_pass_hash(struct object_entry *obj)
1081+
{
1082+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1083+
size_t allocation;
1084+
1085+
if (!pool->nr_threads || obj->type != OBJ_BLOB ||
1086+
obj->size < first_pass_hash_min_size ||
1087+
obj->size >= first_pass_hash_buffer_size ||
1088+
obj->size > repo_settings_get_big_file_threshold(the_repository))
1089+
return NULL;
1090+
1091+
allocation = st_add(obj->size, 1);
1092+
while (pool->nr == pool->queue_size ||
1093+
allocation > first_pass_hash_buffer_size - pool->buffered)
1094+
retire_first_pass_hash();
1095+
1096+
/* Reserve before the producer allocates the inflated blob. */
1097+
pool->buffered += allocation;
1098+
return &pool->queue[(pool->first + pool->nr) % pool->queue_size];
1099+
}
1100+
1101+
static void submit_first_pass_hash(struct first_pass_hash_job *job,
1102+
struct object_entry *obj, void *data)
1103+
{
1104+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1105+
1106+
assert(pool->nr_threads && data && obj->type == OBJ_BLOB);
1107+
job->obj = obj;
1108+
job->data = data;
1109+
job->done = 0;
1110+
1111+
pthread_mutex_lock(&pool->mutex);
1112+
pool->nr++;
1113+
pool->pending++;
1114+
pool->jobs++;
1115+
pthread_cond_signal(&pool->work_ready);
1116+
pthread_mutex_unlock(&pool->mutex);
1117+
}
1118+
1119+
static void stop_first_pass_hash(size_t nr_threads)
1120+
{
1121+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1122+
size_t i;
1123+
1124+
pthread_mutex_lock(&pool->mutex);
1125+
pool->stop = 1;
1126+
pthread_cond_broadcast(&pool->work_ready);
1127+
pthread_mutex_unlock(&pool->mutex);
1128+
for (i = 0; i < nr_threads; i++)
1129+
pthread_join(pool->threads[i], NULL);
1130+
pthread_cond_destroy(&pool->result_ready);
1131+
pthread_cond_destroy(&pool->work_ready);
1132+
pthread_mutex_destroy(&pool->mutex);
1133+
free(pool->queue);
1134+
free(pool->threads);
1135+
}
1136+
1137+
static void start_first_pass_hash(void)
1138+
{
1139+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1140+
size_t i;
1141+
int ret;
1142+
1143+
/* These modes share fsck/object-cache state; retain their serial path. */
1144+
if (!HAVE_THREADS || !first_pass_hash_threads || strict ||
1145+
do_fsck_object || record_outgoing_links ||
1146+
first_pass_hash_min_size >= first_pass_hash_buffer_size) {
1147+
trace2_data_intmax("index-pack", the_repository,
1148+
"first_pass_hash/threads", 0);
1149+
return;
1150+
}
1151+
1152+
pool->nr_threads = first_pass_hash_threads;
1153+
pool->queue_size = st_mult(pool->nr_threads, 2);
1154+
CALLOC_ARRAY(pool->threads, pool->nr_threads);
1155+
CALLOC_ARRAY(pool->queue, pool->queue_size);
1156+
pthread_mutex_init(&pool->mutex, NULL);
1157+
pthread_cond_init(&pool->work_ready, NULL);
1158+
pthread_cond_init(&pool->result_ready, NULL);
1159+
for (i = 0; i < pool->nr_threads; i++) {
1160+
ret = pthread_create(&pool->threads[i], NULL,
1161+
first_pass_hash_worker, NULL);
1162+
if (ret) {
1163+
stop_first_pass_hash(i);
1164+
die(_("unable to create index-pack hash thread: %s"),
1165+
strerror(ret));
1166+
}
1167+
}
1168+
trace2_data_intmax("index-pack", the_repository,
1169+
"first_pass_hash/threads", pool->nr_threads);
1170+
}
1171+
1172+
static void finish_first_pass_hash(void)
1173+
{
1174+
struct first_pass_hash_pool *pool = &first_pass_hash_pool;
1175+
1176+
if (!pool->nr_threads)
1177+
return;
1178+
while (pool->nr)
1179+
retire_first_pass_hash();
1180+
1181+
stop_first_pass_hash(pool->nr_threads);
1182+
trace2_data_intmax("index-pack", the_repository,
1183+
"first_pass_hash/jobs", pool->jobs);
1184+
pool->nr_threads = 0;
1185+
}
1186+
9811187
/*
9821188
* Ensure that this node has been reconstructed and return its contents.
9831189
*
@@ -1255,6 +1461,8 @@ static void parse_pack_objects(unsigned char *hash)
12551461
struct stat st;
12561462
struct git_hash_ctx tmp_ctx;
12571463

1464+
start_first_pass_hash();
1465+
12581466
if (verbose)
12591467
progress = start_progress(
12601468
the_repository,
@@ -1263,9 +1471,10 @@ static void parse_pack_objects(unsigned char *hash)
12631471
nr_objects);
12641472
for (i = 0; i < nr_objects; i++) {
12651473
struct object_entry *obj = &objects[i];
1474+
struct first_pass_hash_job *hash_job;
12661475
void *data = unpack_raw_entry(obj, &ofs_delta->offset,
12671476
&ref_delta_oid,
1268-
&obj->idx.oid);
1477+
&obj->idx.oid, &hash_job);
12691478
obj->real_type = obj->type;
12701479
if (obj->type == OBJ_OFS_DELTA) {
12711480
nr_ofs_deltas++;
@@ -1276,6 +1485,9 @@ static void parse_pack_objects(unsigned char *hash)
12761485
oidcpy(&ref_deltas[nr_ref_deltas].oid, &ref_delta_oid);
12771486
ref_deltas[nr_ref_deltas].obj_no = i;
12781487
nr_ref_deltas++;
1488+
} else if (hash_job) {
1489+
submit_first_pass_hash(hash_job, obj, data);
1490+
data = NULL;
12791491
} else if (!data) {
12801492
/* large blobs, check later */
12811493
obj->real_type = OBJ_BAD;
@@ -1287,6 +1499,7 @@ static void parse_pack_objects(unsigned char *hash)
12871499
display_progress(progress, i+1);
12881500
}
12891501
objects[i].idx.offset = consumed_bytes;
1502+
finish_first_pass_hash();
12901503
stop_progress(&progress);
12911504

12921505
/* Check pack integrity */
@@ -1667,6 +1880,23 @@ static int git_index_pack_config(const char *k, const char *v,
16671880
{
16681881
struct pack_idx_option *opts = cb;
16691882

1883+
if (!strcmp(k, "indexpack.hashthreads")) {
1884+
first_pass_hash_threads = git_config_int(k, v, ctx->kvi);
1885+
if (first_pass_hash_threads < 0 ||
1886+
first_pass_hash_threads > FIRST_PASS_HASH_MAX_THREADS)
1887+
die(_("%s must be between 0 and %d"),
1888+
k, FIRST_PASS_HASH_MAX_THREADS);
1889+
return 0;
1890+
}
1891+
if (!strcmp(k, "indexpack.hashbuffersize")) {
1892+
first_pass_hash_buffer_size = git_config_ulong(k, v, ctx->kvi);
1893+
return 0;
1894+
}
1895+
if (!strcmp(k, "indexpack.hashminsize")) {
1896+
first_pass_hash_min_size = git_config_ulong(k, v, ctx->kvi);
1897+
return 0;
1898+
}
1899+
16701900
if (!strcmp(k, "pack.indexversion")) {
16711901
opts->version = git_config_int(k, v, ctx->kvi);
16721902
if (opts->version > 2)

0 commit comments

Comments
 (0)