On Thu, Jul 19, 2018 at 07:31:35PM +0200, Duy Nguyen wrote:
> On Thu, Jul 19, 2018 at 01:23:58PM -0400, Jeff King wrote:
> > On Thu, Jul 19, 2018 at 09:42:00AM -0700, Elijah Newren wrote:
> >
> > > Thanks for the quick turnaround. Unfortunately, I have some bad news.
> > > With this patch, I get the following:
> > >
> > > $ /usr/bin/time -f 'MaxRSS:%M Time:%e' git gc --aggressive
> > > Enumerating objects: 4460703, done.
> > > Counting objects: 100% (4460703/4460703), done.
> > > Delta compression using up to 40 threads.
> > > Compressing objects: 100% (3807140/3807140), done.
> > > Writing objects: 100% (4460703/4460703), done.
> > > Total 4460703 (delta 2831383), reused 1587071 (delta 0)
> > > error: failed to unpack compressed delta at offset 183854150 from
> > > .git/objects/pack/pack-30d4f0b0e5a03dc91a658a0586f4e74cdf4a94d6.pack
> > > fatal: packed object 20ce811e53dabbb8ef9368c108cbbdfa65639c03 (stored
> > > in .git/objects/pack/pack-30d4f0b0e5a03dc91a658a0586f4e74cdf4a94d6.pack)
> > > is corrupt
> > > error: failed to run prune
> > > MaxRSS:40025196 Time:2531.52
> >
> > Looking at that output, my _guess_ is that we somehow end up with a
> > bogus delta_size value and write out a truncated entry. But I couldn't
> > reproduce the issue with smaller test cases.
>
> Could it be a race condition?
I'm convinced my code is racy (between two writes). I created a broken
pack once with 32 threads. Elijah please try again with this new
patch. It should fix this (I only tried repack a few times so far but
will continue)
The race is this
1. Thread one sees a large delta size and NULL delta_size[] array,
allocates the new array and in the middle of copying old delta
sizes over.
2. Thread two wants to write a new (large) delta size. It sees that
delta_size[] is already allocated, it writes the correct size there
(and truncated one in object_entry->delta_size_)
3. Back to thread one, it now copies the truncated value in
delta_size_ from step 2 to delta_size[] array, overwriting the good
value that thread two wrote.
There is also a potential read/write race where a read from
pack_size[] happens when the array is not ready. But I don't think it
can happen with current try_delta() code. I protect it anyway to be
safe.
-- 8< --
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index ebc8cefb53..d67997f11c 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -32,6 +32,12 @@
#include "object-store.h"
#include "dir.h"
+static unsigned long oe_delta_size(struct packing_data *pack,
+ const struct object_entry *e);
+static void oe_set_delta_size(struct packing_data *pack,
+ struct object_entry *e,
+ unsigned long size);
+
#define IN_PACK(obj) oe_in_pack(&to_pack, obj)
#define SIZE(obj) oe_size(&to_pack, obj)
#define SET_SIZE(obj,size) oe_set_size(&to_pack, obj, size)
@@ -1915,6 +1921,51 @@ unsigned long oe_get_size_slow(struct packing_data *pack,
return size;
}
+static unsigned long oe_delta_size(struct packing_data *pack,
+ const struct object_entry *e)
+{
+ unsigned long size;
+
+ read_lock(); /* to protect access to pack->delta_size[] */
+ if (pack->delta_size)
+ size = pack->delta_size[e - pack->objects];
+ else
+ size = e->delta_size_;
+ read_unlock();
+ return size;
+}
+
+static void oe_set_delta_size(struct packing_data *pack,
+ struct object_entry *e,
+ unsigned long size)
+{
+ read_lock(); /* to protect access to pack->delta_size[] */
+ if (!pack->delta_size && size < pack->oe_delta_size_limit) {
+ e->delta_size_ = size;
+ read_unlock();
+ return;
+ }
+ /*
+ * We have had at least one delta size exceeding OE_DELTA_SIZE_BITS
+ * limit. delta_size_ will not be used anymore. All delta sizes are now
+ * from the delta_size[] array.
+ */
+ if (!pack->delta_size) {
+ uint32_t i;
+
+ /*
+ * nr_alloc, not nr_objects to align with realloc() strategy in
+ * packlist_alloc()
+ */
+ ALLOC_ARRAY(pack->delta_size, pack->nr_alloc);
+
+ for (i = 0; i < pack->nr_objects; i++)
+ pack->delta_size[i] = pack->objects[i].delta_size_;
+ }
+ pack->delta_size[e - pack->objects] = size;
+ read_unlock();
+}
+
static int try_delta(struct unpacked *trg, struct unpacked *src,
unsigned max_depth, unsigned long *mem_usage)
{
@@ -2023,10 +2074,6 @@ static int try_delta(struct unpacked *trg, struct
unpacked *src,
delta_buf = create_delta(src->index, trg->data, trg_size, &delta_size,
max_size);
if (!delta_buf)
return 0;
- if (delta_size >= (1U << OE_DELTA_SIZE_BITS)) {
- free(delta_buf);
- return 0;
- }
if (DELTA(trg_entry)) {
/* Prefer only shallower same-sized deltas. */
diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh
index 4b04c75b7f..2a5bff4a1c 100755
--- a/ci/run-build-and-tests.sh
+++ b/ci/run-build-and-tests.sh
@@ -14,6 +14,7 @@ then
export GIT_TEST_SPLIT_INDEX=yes
export GIT_TEST_FULL_IN_PACK_ARRAY=true
export GIT_TEST_OE_SIZE=10
+ export GIT_TEST_OE_DELTA_SIZE=5
make --quiet test
fi
diff --git a/pack-objects.c b/pack-objects.c
index 92708522e7..e3c32bbfc2 100644
--- a/pack-objects.c
+++ b/pack-objects.c
@@ -146,6 +146,8 @@ void prepare_packing_data(struct packing_data *pdata)
pdata->oe_size_limit = git_env_ulong("GIT_TEST_OE_SIZE",
1U << OE_SIZE_BITS);
+ pdata->oe_delta_size_limit = git_env_ulong("GIT_TEST_OE_DELTA_SIZE",
+ 1U << OE_DELTA_SIZE_BITS);
}
struct object_entry *packlist_alloc(struct packing_data *pdata,
@@ -160,6 +162,8 @@ struct object_entry *packlist_alloc(struct packing_data
*pdata,
if (!pdata->in_pack_by_idx)
REALLOC_ARRAY(pdata->in_pack, pdata->nr_alloc);
+ if (pdata->delta_size)
+ REALLOC_ARRAY(pdata->delta_size, pdata->nr_alloc);
}
new_entry = pdata->objects + pdata->nr_objects++;
diff --git a/pack-objects.h b/pack-objects.h
index edf74dabdd..7477c7b919 100644
--- a/pack-objects.h
+++ b/pack-objects.h
@@ -14,7 +14,7 @@
* above this limit. Don't lower it too much.
*/
#define OE_SIZE_BITS 31
-#define OE_DELTA_SIZE_BITS 20
+#define OE_DELTA_SIZE_BITS 20
/*
* State flags for depth-first search used for analyzing delta cycles.
@@ -93,12 +93,12 @@ struct object_entry {
* uses the same base as me
*/
unsigned delta_size_:OE_DELTA_SIZE_BITS; /* delta data size
(uncompressed) */
- unsigned delta_size_valid:1;
+ unsigned char in_pack_header_size;
unsigned in_pack_idx:OE_IN_PACK_BITS; /* already in pack */
unsigned z_delta_size:OE_Z_DELTA_BITS;
unsigned type_valid:1;
- unsigned type_:TYPE_BITS;
unsigned no_try_delta:1;
+ unsigned type_:TYPE_BITS;
unsigned in_pack_type:TYPE_BITS; /* could be delta */
unsigned preferred_base:1; /*
* we do not pack this, but is available
@@ -108,17 +108,16 @@ struct object_entry {
unsigned tagged:1; /* near the very tip of refs */
unsigned filled:1; /* assigned write-order */
unsigned dfs_state:OE_DFS_STATE_BITS;
- unsigned char in_pack_header_size;
unsigned depth:OE_DEPTH_BITS;
/*
* pahole results on 64-bit linux (gcc and clang)
*
- * size: 80, bit_padding: 20 bits, holes: 8 bits
+ * size: 80, bit_padding: 9 bits
*
* and on 32-bit (gcc)
*
- * size: 76, bit_padding: 20 bits, holes: 8 bits
+ * size: 76, bit_padding: 9 bits
*/
};
@@ -130,6 +129,7 @@ struct packing_data {
uint32_t index_size;
unsigned int *in_pack_pos;
+ uint32_t *delta_size;
/*
* Only one of these can be non-NULL and they have different
@@ -141,6 +141,7 @@ struct packing_data {
struct packed_git **in_pack;
uintmax_t oe_size_limit;
+ uintmax_t oe_delta_size_limit;
};
void prepare_packing_data(struct packing_data *pdata);
@@ -327,23 +328,4 @@ static inline void oe_set_size(struct packing_data *pack,
}
}
-static inline unsigned long oe_delta_size(struct packing_data *pack,
- const struct object_entry *e)
-{
- if (e->delta_size_valid)
- return e->delta_size_;
- return oe_size(pack, e);
-}
-
-static inline void oe_set_delta_size(struct packing_data *pack,
- struct object_entry *e,
- unsigned long size)
-{
- e->delta_size_ = size;
- e->delta_size_valid = e->delta_size_ == size;
- if (!e->delta_size_valid && size != oe_size(pack, e))
- BUG("this can only happen in check_object() "
- "where delta size is the same as entry size");
-}
-
#endif
diff --git a/t/README b/t/README
index 8373a27fea..9028b47d92 100644
--- a/t/README
+++ b/t/README
@@ -315,6 +315,10 @@ packs on demand. This normally only happens when the
object size is
over 2GB. This variable forces the code path on any object larger than
<n> bytes.
+GIT_TEST_OE_DELTA_SIZE=<n> exercises the uncomon pack-objects code
+path where deltas larger than this limit require extra memory
+allocation for bookkeeping.
+
Naming Tests
------------
-- 8< --
--
Duy