Skip to contentSkip to conversation

Reduce pack-objects memory footprint?

PATCH Superseded Duy Nguyen 8 participants 273 messages Feb 28, 2018 – Apr 6, 2018 Documentation Page 3 of 3

lore
Share & tools
lore
#251 lore raw

PATCH v3 4/7 gc --auto: exclude base pack if not enough mem to "repack -ad"

In reply to Nguyễn Thái Ngọc Duy

pack-objects could be a big memory hog especially on large repos, everybody knows that. The suggestion to stick a .keep file on the giant base pack to avoid this problem is also known for a long time.

Recent patches add an option to do just this, but it has to be either configured or activated manually. This patch lets git gc --auto activate this mode automatically when it thinks repack -ad will use a lot of memory and start affecting the system due to swapping or flushing OS cache.

gc --auto decides to do this based on an estimation of pack-objects memory usage, which is quite accurate at least for the heap part, and whether that fits in half of system memory (the assumption here is for desktop environment where there are many other applications running).

This mechanism only kicks in if gc.bigBasePackThreshold is not configured. If it is, it is assumed that the user already knows what they want.

---
 Documentation/git-gc.txt |  9 +++-
 builtin/gc.c             | 99 +++++++++++++++++++++++++++++++++++++++-
 builtin/pack-objects.c   |  2 +-
 config.mak.uname         |  1 +
 git-compat-util.h        |  4 ++
 pack-objects.h           |  2 +
 t/t6500-gc.sh            |  7 +++
 7 files changed, 120 insertions(+), 4 deletions(-)
Documentation/git-gc.txt +7 -2
index 89f074f924..19b0d1741b 100644
@@ -58,8 +58,13 @@ If the number of packs exceeds the value of `gc.autoPackLimit`,
 then existing packs (except those marked with a `.keep` file
 or over `gc.bigPackThreshold` limit)
 are consolidated into a single pack by using the `-A` option of
-'git repack'. Setting `gc.autoPackLimit` to 0 disables
-automatic consolidation of packs.
+'git repack'.
+If the amount of memory is estimated not enough for `git repack` to
+run smoothly and `gc.bigPackThreshold` is not set, the largest
+pack will also be excluded (this is the equivalent of running `git gc`
+with `--keep-base-pack`).
+Setting `gc.autoPackLimit` to 0 disables automatic consolidation of
+packs.
 
 --prune=<date>::
 	Prune loose objects older than date (default is 2 weeks ago,
builtin/gc.c +98 -1

141 lines not on this page · raw message

builtin/pack-objects.c +1 -1
index 7b9fe6c89f..6abde6ec6d 100644
@@ -79,7 +79,7 @@ static uint16_t write_bitmap_options;
 static int exclude_promisor_objects;
 
 static unsigned long delta_cache_size = 0;
-static unsigned long max_delta_cache_size = 256 * 1024 * 1024;
+static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
 static unsigned long cache_max_small_delta_size = 1000;
 
 static unsigned long window_memory_limit = 0;
config.mak.uname +1 -0
index 6a1d0de0cc..ae9cbccec1 100644
@@ -37,6 +37,7 @@ ifeq ($(uname_S),Linux)
 	HAVE_GETDELIM = YesPlease
 	SANE_TEXT_GREP=-a
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
+	BASIC_CFLAGS += -DHAVE_SYSINFO
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
git-compat-util.h +4 -0
index 68b2ad531e..a84b21986d 100644
@@ -284,6 +284,10 @@ extern char *gitdirname(char *);
 #include <openssl/err.h>
 #endif
 
+#ifdef HAVE_SYSINFO
+# include <sys/sysinfo.h>
+#endif
+
 /* On most systems <netdb.h> would have given us this, but
  * not on some systems (e.g. z/OS).
  */
pack-objects.h +2 -0
index 03f1191659..af4f46c026 100644
@@ -1,6 +1,8 @@
 #ifndef PACK_OBJECTS_H
 #define PACK_OBJECTS_H
 
+#define DEFAULT_DELTA_CACHE_SIZE (256 * 1024 * 1024)
+
 struct object_entry {
 	struct pack_idx_entry idx;
 	unsigned long size;	/* uncompressed size */
t/t6500-gc.sh +7 -0
index 4136681b47..96ca70f9cc 100755
@@ -5,6 +5,13 @@ test_description='basic git gc tests
 
 . ./test-lib.sh
 
+test_expect_success 'setup' '
+	# do not let the amount of physical memory affects gc
+	# behavior, make sure we always pack everything to one pack by
+	# default
+	git config gc.bigPackThreshold 2g
+'
+
 test_expect_success 'gc empty repository' '
 	git gc
 '
Signature · 1 line
2.16.2.903.gd04caf5039
#252 lore raw

Re: [PATCH v3 4/7] gc --auto: exclude base pack if not enough mem to "repack -ad"

On Fri, Mar 16 2018, Nguyễn Thái Ngọc Duy jotted:

builtin/gc.c static int need_to_gc(void) · line 342

Quoted 1 line
builtin/gc.c +1 -0
+			struct packed_git * p = find_base_packs(&keep_pack, 0);

Style nit: space after "*".

#253 lore raw

PATCH v3 5/7 gc: handle a corner case in gc.bigPackThreshold

In reply to Nguyễn Thái Ngọc Duy

This config allows us to keep <N> packs back if their size is larger than a limit. But if this N >= gc.autoPackLimit, we may have a problem. We are supposed to reduce the number of packs after a threshold because it affects performance.

We could tell the user that they have incompatible gc.bigPackThreshold and gc.autoPackLimit, but it's kinda hard when 'git gc --auto' runs in background. Instead let's fall back to the next best stategy: try to reduce the number of packs anyway, but keep the base pack out. This reduces the number of packs to two and hopefully won't take up too much resources to repack (the assumption still is the base pack takes most resources to handle).

---
 Documentation/config.txt | 5 +++++
 builtin/gc.c             | 9 +++++++--
 2 files changed, 12 insertions(+), 2 deletions(-)
Documentation/config.txt +5 -0
index c12c58813c..ce40112e31 100644
@@ -1554,6 +1554,11 @@ gc.bigPackThreshold::
 	`git gc` is run. This is very similar to `--keep-base-pack`
 	except that all packs that meet the threshold are kept, not
 	just the base pack. Defaults to zero.
++
+Note that if the number of kept packs is more than gc.autoPackLimit,
+this configuration variable is ignored, all packs except the base pack
+will be repacked. After this the number of packs should go below
+gc.autoPackLimit and gc.bigPackThreshold should be respected again.
 
 gc.logExpiry::
 	If the file gc.log exists, then `git gc --auto` won't run
builtin/gc.c +7 -2
index c0f1922c24..140c1bb7dd 100644
@@ -336,9 +336,14 @@ static int need_to_gc(void)
 	if (too_many_packs()) {
 		struct string_list keep_pack = STRING_LIST_INIT_NODUP;
 
-		if (big_pack_threshold)
+		if (big_pack_threshold) {
 			find_base_packs(&keep_pack, big_pack_threshold);
-		else {
+			if (keep_pack.nr >= gc_auto_pack_limit) {
+				big_pack_threshold = 0;
+				string_list_clear(&keep_pack, 0);
+				find_base_packs(&keep_pack, 0);
+			}
+		} else {
 			struct packed_git * p = find_base_packs(&keep_pack, 0);
 			uint64_t mem_have, mem_want;
 
Signature · 1 line
2.16.2.903.gd04caf5039
#254 lore raw

Re: [PATCH v3 5/7] gc: handle a corner case in gc.bigPackThreshold

On Fri, Mar 16 2018, Nguyễn Thái Ngọc Duy jotted:

Quoted 5 lines

This config allows us to keep <N> packs back if their size is larger than a limit. But if this N >= gc.autoPackLimit, we may have a problem. We are supposed to reduce the number of packs after a threshold because it affects performance.

Quoted 7 lines

We could tell the user that they have incompatible gc.bigPackThreshold and gc.autoPackLimit, but it's kinda hard when 'git gc --auto' runs in background. Instead let's fall back to the next best stategy: try to reduce the number of packs anyway, but keep the base pack out. This reduces the number of packs to two and hopefully won't take up too much resources to repack (the assumption still is the base pack takes most resources to handle).

I think this strategy makes perfect sense.

Those with say a 1GB "base" pack might set this setting at to 500MB or something large like that, then it's realistically never going to happen that you're going to then have a collision between gc.bigPackThreshold and gc.autoPackLimit, even if your checkout is many years old *maybe* you've accumulated 5-10 of those 500MB packs for any sane repo.

But this also allows for setting this value really low, e.g. 50MB or something to place a very low upper bound on how much memory GC takes on a regular basis, but of course you'll need to repack that set of 50MB's eventually.

Great!

#255 lore raw

PATCH v3 6/7 pack-objects: show some progress when counting kept objects

In reply to Nguyễn Thái Ngọc Duy

We only show progress when there are new objects to be packed. But when --keep-pack is specified on the base pack, we will exclude most of objects. This makes 'pack-objects' stay silent for a long time while the counting phase is going.

Let's show some progress whenever we visit an object instead. The number of packed objects will be shown after if it's not the same as the number of visited objects.

Since the meaning of this number has changed, use another word instead of "Counting" to hint about the change.

---
 builtin/pack-objects.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
builtin/pack-objects.c +6 -6
index 6abde6ec6d..f74e9117f7 100644
@@ -44,7 +44,7 @@ static const char *pack_usage[] = {
 static struct packing_data to_pack;
 
 static struct pack_idx_entry **written_list;
-static uint32_t nr_result, nr_written;
+static uint32_t nr_result, nr_written, nr_seen;
 
 static int non_empty;
 static int reuse_delta = 1, reuse_object = 1;
@@ -1094,6 +1094,8 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 	off_t found_offset = 0;
 	uint32_t index_pos;
 
+	display_progress(progress_state, ++nr_seen);
+
 	if (have_duplicate_entry(oid, exclude, &index_pos))
 		return 0;
 
@@ -1109,8 +1111,6 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 	create_object_entry(oid, type, pack_name_hash(name),
 			    exclude, name && no_try_delta(name),
 			    index_pos, found_pack, found_offset);
-
-	display_progress(progress_state, nr_result);
 	return 1;
 }
 
@@ -1121,6 +1121,8 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
 {
 	uint32_t index_pos;
 
+	display_progress(progress_state, ++nr_seen);
+
 	if (have_duplicate_entry(oid, 0, &index_pos))
 		return 0;
 
@@ -1128,8 +1130,6 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
 		return 0;
 
 	create_object_entry(oid, type, name_hash, 0, 0, index_pos, pack, offset);
-
-	display_progress(progress_state, nr_result);
 	return 1;
 }
 
@@ -3210,7 +3210,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)
 	}
 
 	if (progress)
-		progress_state = start_progress(_("Counting objects"), 0);
+		progress_state = start_progress(_("Enumerating objects"), 0);
 	if (!use_internal_rev_list)
 		read_object_list_from_stdin();
 	else {
Signature · 1 line
2.16.2.903.gd04caf5039
#256 lore raw

PATCH v3 7/7 pack-objects: display progress in get_object_details()

In reply to Nguyễn Thái Ngọc Duy

This code is mostly about reading object headers, which is cheap. But when the number of objects is very large (e.g. 6.5M on linux-2.6.git) and the system is under memory pressure, this could take some time (86 seconds on my system).

Show something during this time to let the user know pack-objects is still going strong.

---
 builtin/pack-objects.c | 6 ++++++
 1 file changed, 6 insertions(+)
builtin/pack-objects.c +6 -0
index f74e9117f7..ac8f29dd52 100644
@@ -1715,6 +1715,10 @@ static void get_object_details(void)
 	uint32_t i;
 	struct object_entry **sorted_by_offset;
 
+	if (progress)
+		progress_state = start_progress(_("Getting object details"),
+						to_pack.nr_objects);
+
 	sorted_by_offset = xcalloc(to_pack.nr_objects, sizeof(struct object_entry *));
 	for (i = 0; i < to_pack.nr_objects; i++)
 		sorted_by_offset[i] = to_pack.objects + i;
@@ -1725,7 +1729,9 @@ static void get_object_details(void)
 		check_object(entry);
 		if (big_file_threshold < entry->size)
 			entry->no_try_delta = 1;
+		display_progress(progress_state, i + 1);
 	}
+	stop_progress(&progress_state);
 
 	/*
 	 * This must happen in a second pass, since we rely on the delta
Signature · 1 line
2.16.2.903.gd04caf5039
#257 lore raw

v4 cover nd/repack-keep-pack updates

In reply to Nguyễn Thái Ngọc Duy

v4 is mostly refining tests and other minor fixes on v3

- --keep-base-pack is renamed to --keep-largest-pack
- "Counting objects" progress line is back
- test and docs updates

Interdiff

Documentation/config.txt +2 -1
index 6b602f918f..cf862d3edf 100644
@@ -1562,7 +1562,8 @@ gc.bigPackThreshold::
 	If non-zero, all packs larger than this limit are kept when
 	`git gc` is run. This is very similar to `--keep-base-pack`
 	except that all packs that meet the threshold are kept, not
-	just the base pack. Defaults to zero.
+	just the base pack. Defaults to zero. Common unit suffixes of
+	'k', 'm', or 'g' are supported.
 +
 Note that if the number of kept packs is more than gc.autoPackLimit,
 this configuration variable is ignored, all packs except the base pack
Documentation/git-gc.txt +5 -6
index 19b0d1741b..7549094900 100644
@@ -9,7 +9,7 @@ git-gc - Cleanup unnecessary files and optimize the local repository
 SYNOPSIS
 --------
 [verse]
-'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force] [--keep-base-pack]
+'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force] [--keep-largest-pack]
 
 DESCRIPTION
 -----------
@@ -84,11 +84,10 @@ packs.
 	Force `git gc` to run even if there may be another `git gc`
 	instance running on this repository.
 
---keep-base-pack::
-	All packs except the base pack and those marked with a `.keep`
-	files are consolidated into a single pack. The largest pack is
-	considered the base pack. When this option is used,
-	`gc.bigPackThreshold` is ignored.
+--keep-largest-pack::
+	All packs except the largest pack and those marked with a
+	`.keep` files are consolidated into a single pack. When this
+	option is used, `gc.bigPackThreshold` is ignored.
 
 Configuration
 -------------
builtin/gc.c +3 -3
index 140c1bb7dd..d0b04e369e 100644
@@ -344,7 +344,7 @@ static int need_to_gc(void)
 				find_base_packs(&keep_pack, 0);
 			}
 		} else {
-			struct packed_git * p = find_base_packs(&keep_pack, 0);
+			struct packed_git *p = find_base_packs(&keep_pack, 0);
 			uint64_t mem_have, mem_want;
 
 			mem_have = total_ram();
@@ -504,8 +504,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "aggressive", &aggressive, N_("be more thorough (increased runtime)")),
 		OPT_BOOL(0, "auto", &auto_gc, N_("enable auto-gc mode")),
 		OPT_BOOL(0, "force", &force, N_("force running gc even if there may be another gc running")),
-		OPT_BOOL(0, "keep-base-pack", &keep_base_pack,
-			 N_("repack all other packs except the base pack")),
+		OPT_BOOL(0, "keep-largest-pack", &keep_base_pack,
+			 N_("repack all other packs except the largest pack")),
 		OPT_END()
 	};
 
builtin/pack-objects.c +13 -11
index 7d738627fc..1379b4cb92 100644
@@ -54,7 +54,8 @@ static int pack_loose_unreachable;
 static int local;
 static int have_non_local_packs;
 static int incremental;
-static int ignore_packed_keep, ignore_packed_keep_in_core;
+static int ignore_packed_keep_on_disk;
+static int ignore_packed_keep_in_core;
 static int allow_ofs_delta;
 static struct pack_idx_option pack_idx_opts;
 static const char *base_name;
@@ -983,14 +984,15 @@ static int want_found_object(int exclude, struct packed_git *p)
 	 * Otherwise, we signal "-1" at the end to tell the caller that we do
 	 * not know either way, and it needs to check more packs.
 	 */
-	if (!ignore_packed_keep && !ignore_packed_keep_in_core &&
+	if (!ignore_packed_keep_on_disk &&
+	    !ignore_packed_keep_in_core &&
 	    (!local || !have_non_local_packs))
 		return 1;
 
 	if (local && !p->pack_local)
 		return 0;
 	if (p->pack_local &&
-	    ((ignore_packed_keep && p->pack_keep) ||
+	    ((ignore_packed_keep_on_disk && p->pack_keep) ||
 	     (ignore_packed_keep_in_core && p->pack_keep_in_core)))
 		return 0;
 
@@ -1716,7 +1718,7 @@ static void get_object_details(void)
 	struct object_entry **sorted_by_offset;
 
 	if (progress)
-		progress_state = start_progress(_("Getting object details"),
+		progress_state = start_progress(_("Counting objects"),
 						to_pack.nr_objects);
 
 	sorted_by_offset = xcalloc(to_pack.nr_objects, sizeof(struct object_entry *));
@@ -2818,7 +2820,7 @@ static int pack_options_allow_reuse(void)
 {
 	return pack_to_stdout &&
 	       allow_ofs_delta &&
-	       !ignore_packed_keep &&
+	       !ignore_packed_keep_on_disk &&
 	       !ignore_packed_keep_in_core &&
 	       (!local || !have_non_local_packs) &&
 	       !incremental;
@@ -3059,7 +3061,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)
 			 N_("create thin packs")),
 		OPT_BOOL(0, "shallow", &shallow,
 			 N_("create packs suitable for shallow fetches")),
-		OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep,
+		OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep_on_disk,
 			 N_("ignore packs that have companion .keep file")),
 		OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
 				N_("ignore this pack")),
@@ -3192,19 +3194,19 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)
 
 	prepare_packed_git();
 	add_extra_kept_packs(&keep_pack_list);
-	if (ignore_packed_keep) {
+	if (ignore_packed_keep_on_disk) {
 		struct packed_git *p;
 		for (p = packed_git; p; p = p->next)
 			if (p->pack_local && p->pack_keep)
 				break;
 		if (!p) /* no keep-able packs found */
-			ignore_packed_keep = 0;
+			ignore_packed_keep_on_disk = 0;
 	}
 	if (local) {
 		/*
-		 * unlike ignore_packed_keep above, we do not want to
-		 * unset "local" based on looking at packs, as it
-		 * also covers non-local objects
+		 * unlike ignore_packed_keep_on_disk above, we do not
+		 * want to unset "local" based on looking at packs, as
+		 * it also covers non-local objects
 		 */
 		struct packed_git *p;
 		for (p = packed_git; p; p = p->next) {
t/t6500-gc.sh +25 -22

59 lines not on this page · raw message

t/t7700-repack.sh +15 -15
index 05ae0de3aa..6162e2a8e6 100755
@@ -4,6 +4,12 @@ test_description='git repack works correctly'
 
 . ./test-lib.sh
 
+commit_and_pack() {
+	test_commit "$@" >/dev/null &&
+	SHA1=$(git pack-objects --all --unpacked --incremental .git/objects/pack/pack </dev/null) &&
+	echo pack-${SHA1}.pack
+}
+
 test_expect_success 'objects in packs marked .keep are not repacked' '
 	echo content1 > file1 &&
 	echo content2 > file2 &&
@@ -200,22 +206,16 @@ test_expect_success 'repack --keep-pack' '
 	test_create_repo keep-pack &&
 	(
 		cd keep-pack &&
-		test_commit one &&
-		git repack -d &&
-		test_commit two &&
-		git repack -d &&
-		test_commit three &&
-		git repack -d &&
-		test_commit four &&
-		git repack -d &&
-		( cd .git/objects/pack && ls *.pack ) >pack-list &&
-		test_line_count = 4 pack-list &&
-		KEEP1=$(head -n1 pack-list) &&
-		KEEP4=$(tail -n1 pack-list) &&
-		git repack -a -d --keep-pack $KEEP1 --keep-pack $KEEP4 &&
+		P1=$(commit_and_pack 1) &&
+		P2=$(commit_and_pack 2) &&
+		P3=$(commit_and_pack 3) &&
+		P4=$(commit_and_pack 4) &&
+		ls .git/objects/pack/*.pack >old-counts &&
+		test_line_count = 4 old-counts &&
+		git repack -a -d --keep-pack $P1 --keep-pack $P4 &&
 		ls .git/objects/pack/*.pack >new-counts &&
-		grep -q $KEEP1 new-counts &&
-		grep -q $KEEP4 new-counts &&
+		grep -q $P1 new-counts &&
+		grep -q $P4 new-counts &&
 		test_line_count = 3 new-counts &&
 		git fsck
 	)

Nguyễn Thái Ngọc Duy (7):
t7700: have closing quote of a test at the beginning of line
repack: add --keep-pack option
gc: add --keep-largest-pack option
gc: add gc.bigPackThreshold config
gc: handle a corner case in gc.bigPackThreshold
gc --auto: exclude base pack if not enough mem to "repack -ad"
pack-objects: show some progress when counting kept objects

 Documentation/config.txt           |  12 +++
 Documentation/git-gc.txt           |  19 +++-
 Documentation/git-pack-objects.txt |   9 +-
 Documentation/git-repack.txt       |   9 +-
 builtin/gc.c                       | 167 +++++++++++++++++++++++++++--
 builtin/pack-objects.c             |  84 +++++++++++----
 builtin/repack.c                   |  21 +++-
 cache.h                            |   1 +
 config.mak.uname                   |   1 +
 git-compat-util.h                  |   4 +
 pack-objects.h                     |   2 +
 t/t6500-gc.sh                      |  32 ++++++
 t/t7700-repack.sh                  |  27 ++++-
 13 files changed, 352 insertions(+), 36 deletions(-)
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#258 lore raw

PATCH v4 1/7 t7700: have closing quote of a test at the beginning of line

The closing quote of a test body by convention is always at the start of line.

---
 t/t7700-repack.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
t/t7700-repack.sh +1 -1
index 6061a04147..38247afbec 100755
@@ -194,7 +194,7 @@ test_expect_success 'objects made unreachable by grafts only are kept' '
 	git reflog expire --expire=$test_tick --expire-unreachable=$test_tick --all &&
 	git repack -a -d &&
 	git cat-file -t $H1
-	'
+'
 
 test_done
 
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#259 lore raw

PATCH v4 2/7 repack: add --keep-pack option

In reply to Nguyễn Thái Ngọc Duy

We allow to keep existing packs by having companion .keep files. This is helpful when a pack is permanently kept. In the next patch, git-gc just wants to keep a pack temporarily, for one pack-objects run. git-gc can use --keep-pack for this use case.

A note about why the pack_keep field cannot be reused and pack_keep_in_core has to be added. This is about the case when --keep-pack is specified together with either --keep-unreachable or --unpack-unreachable, but --honor-pack-keep is NOT specified.

In this case, we want to exclude objects from the packs specified on command line, not from ones with .keep files. If only one bit flag is used, we have to clear pack_keep on pack files with the .keep file.

But we can't make any assumption about unreachable objects in .keep packs. If "pack_keep" field is false for .keep packs, we could potentially pull lots of unreachable objects into the new pack, or unpack them loose. The safer approach is ignore all packs with either .keep file or --keep-pack.

---
 Documentation/git-pack-objects.txt |  9 ++++-
 Documentation/git-repack.txt       |  9 ++++-
 builtin/pack-objects.c             | 64 ++++++++++++++++++++++++------
 builtin/repack.c                   | 21 ++++++++--
 cache.h                            |  1 +
 t/t7700-repack.sh                  | 25 ++++++++++++
 6 files changed, 111 insertions(+), 18 deletions(-)
Documentation/git-pack-objects.txt +8 -1
index 81bc490ac5..403524652a 100644
@@ -12,7 +12,7 @@ SYNOPSIS
 'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]
 	[--no-reuse-delta] [--delta-base-offset] [--non-empty]
 	[--local] [--incremental] [--window=<n>] [--depth=<n>]
-	[--revs [--unpacked | --all]]
+	[--revs [--unpacked | --all]] [--keep-pack=<pack-name>]
 	[--stdout [--filter=<filter-spec>] | base-name]
 	[--shallow] [--keep-true-parents] < object-list
 
@@ -126,6 +126,13 @@ base-name::
 	has a .keep file to be ignored, even if it would have
 	otherwise been packed.
 
+--keep-pack=<pack-name>::
+	This flag causes an object already in the given pack to be
+	ignored, even if it would have otherwise been
+	packed. `<pack-name>` is the the pack file name without
+	leading directory (e.g. `pack-123.pack`). The option could be
+	specified multiple times to keep multiple packs.
+
 --incremental::
 	This flag causes an object already in a pack to be ignored
 	even if it would have otherwise been packed.
Documentation/git-repack.txt +8 -1
index ae750e9e11..ce497d9d12 100644
@@ -9,7 +9,7 @@ git-repack - Pack unpacked objects in a repository
 SYNOPSIS
 --------
 [verse]
-'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [--window=<n>] [--depth=<n>] [--threads=<n>]
+'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
 
 DESCRIPTION
 -----------
@@ -133,6 +133,13 @@ other objects in that pack they already have locally.
 	with `-b` or `repack.writeBitmaps`, as it ensures that the
 	bitmapped packfile has the necessary objects.
 
+--keep-pack=<pack-name>::
+	Exclude the given pack from repacking. This is the equivalent
+	of having `.keep` file on the pack. `<pack-name>` is the the
+	pack file name without leading directory (e.g. `pack-123.pack`).
+	The option could be specified multiple times to keep multiple
+	packs.
+
 --unpack-unreachable=<when>::
 	When loosening unreachable objects, do not bother loosening any
 	objects older than `<when>`. This can be used to optimize out
builtin/pack-objects.c +51 -13

144 lines not on this page · raw message

builtin/repack.c +18 -3
index 7bdb40142f..6c636e159e 100644
@@ -86,7 +86,8 @@ static void remove_pack_on_signal(int signo)
  * have a corresponding .keep or .promisor file. These packs are not to
  * be kept if we are going to pack everything into one file.
  */
-static void get_non_kept_pack_filenames(struct string_list *fname_list)
+static void get_non_kept_pack_filenames(struct string_list *fname_list,
+					const struct string_list *extra_keep)
 {
 	DIR *dir;
 	struct dirent *e;
@@ -97,6 +98,14 @@ static void get_non_kept_pack_filenames(struct string_list *fname_list)
 
 	while ((e = readdir(dir)) != NULL) {
 		size_t len;
+		int i;
+
+		for (i = 0; i < extra_keep->nr; i++)
+			if (!fspathcmp(e->d_name, extra_keep->items[i].string))
+				break;
+		if (extra_keep->nr > 0 && i < extra_keep->nr)
+			continue;
+
 		if (!strip_suffix(e->d_name, ".pack", &len))
 			continue;
 
@@ -148,7 +157,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	struct string_list rollback = STRING_LIST_INIT_NODUP;
 	struct string_list existing_packs = STRING_LIST_INIT_DUP;
 	struct strbuf line = STRBUF_INIT;
-	int ext, ret, failed;
+	int i, ext, ret, failed;
 	FILE *out;
 
 	/* variables to be filled by option parsing */
@@ -160,6 +169,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	const char *depth = NULL;
 	const char *threads = NULL;
 	const char *max_pack_size = NULL;
+	struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
 	int no_reuse_delta = 0, no_reuse_object = 0;
 	int no_update_server_info = 0;
 	int quiet = 0;
@@ -200,6 +210,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 				N_("maximum size of each packfile")),
 		OPT_BOOL(0, "pack-kept-objects", &pack_kept_objects,
 				N_("repack objects in packs marked with .keep")),
+		OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
+				N_("do not repack this pack")),
 		OPT_END()
 	};
 
@@ -230,6 +242,9 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	argv_array_push(&cmd.args, "--keep-true-parents");
 	if (!pack_kept_objects)
 		argv_array_push(&cmd.args, "--honor-pack-keep");
+	for (i = 0; i < keep_pack_list.nr; i++)
+		argv_array_pushf(&cmd.args, "--keep-pack=%s",
+				 keep_pack_list.items[i].string);
 	argv_array_push(&cmd.args, "--non-empty");
 	argv_array_push(&cmd.args, "--all");
 	argv_array_push(&cmd.args, "--reflog");
@@ -254,7 +269,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 		argv_array_push(&cmd.args, "--write-bitmap-index");
 
 	if (pack_everything & ALL_INTO_ONE) {
-		get_non_kept_pack_filenames(&existing_packs);
+		get_non_kept_pack_filenames(&existing_packs, &keep_pack_list);
 
 		if (existing_packs.nr && delete_redundant) {
 			if (unpack_unreachable) {
cache.h +1 -0
index d06932ed0b..ba168e3571 100644
@@ -1635,6 +1635,7 @@ extern struct packed_git {
 	int pack_fd;
 	unsigned pack_local:1,
 		 pack_keep:1,
+		 pack_keep_in_core:1,
 		 freshened:1,
 		 do_not_close:1,
 		 pack_promisor:1;
t/t7700-repack.sh +25 -0
index 38247afbec..6162e2a8e6 100755
@@ -4,6 +4,12 @@ test_description='git repack works correctly'
 
 . ./test-lib.sh
 
+commit_and_pack() {
+	test_commit "$@" >/dev/null &&
+	SHA1=$(git pack-objects --all --unpacked --incremental .git/objects/pack/pack </dev/null) &&
+	echo pack-${SHA1}.pack
+}
+
 test_expect_success 'objects in packs marked .keep are not repacked' '
 	echo content1 > file1 &&
 	echo content2 > file2 &&
@@ -196,5 +202,24 @@ test_expect_success 'objects made unreachable by grafts only are kept' '
 	git cat-file -t $H1
 '
 
+test_expect_success 'repack --keep-pack' '
+	test_create_repo keep-pack &&
+	(
+		cd keep-pack &&
+		P1=$(commit_and_pack 1) &&
+		P2=$(commit_and_pack 2) &&
+		P3=$(commit_and_pack 3) &&
+		P4=$(commit_and_pack 4) &&
+		ls .git/objects/pack/*.pack >old-counts &&
+		test_line_count = 4 old-counts &&
+		git repack -a -d --keep-pack $P1 --keep-pack $P4 &&
+		ls .git/objects/pack/*.pack >new-counts &&
+		grep -q $P1 new-counts &&
+		grep -q $P4 new-counts &&
+		test_line_count = 3 new-counts &&
+		git fsck
+	)
+'
+
 test_done
 
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#260 lore raw

PATCH v4 3/7 gc: add --keep-largest-pack option

In reply to Nguyễn Thái Ngọc Duy

This adds a new repack mode that combines everything into a secondary pack, leaving the largest pack alone.

This could help reduce memory pressure. On linux-2.6.git, valgrind massif reports 1.6GB heap in "pack all" case, and 535MB in "pack all except the base pack" case. We save roughly 1GB memory by excluding the base pack.

This should also lower I/O because we don't have to rewrite a giant pack every time (e.g. for linux-2.6.git that's a 1.4GB pack file)..

PS. The use of string_list here seems overkill, but we'll need it in the next patch...

---
 Documentation/git-gc.txt |  6 ++++-
 builtin/gc.c             | 47 ++++++++++++++++++++++++++++++++++++----
 t/t6500-gc.sh            | 25 +++++++++++++++++++++
 3 files changed, 73 insertions(+), 5 deletions(-)
Documentation/git-gc.txt +5 -1
index 571b5a7e3c..bf81b8de30 100644
@@ -9,7 +9,7 @@ git-gc - Cleanup unnecessary files and optimize the local repository
 SYNOPSIS
 --------
 [verse]
-'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force]
+'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force] [--keep-largest-pack]
 
 DESCRIPTION
 -----------
@@ -78,6 +78,10 @@ automatic consolidation of packs.
 	Force `git gc` to run even if there may be another `git gc`
 	instance running on this repository.
 
+--keep-largest-pack::
+	All packs except the largest pack and those marked with a
+	`.keep` files are consolidated into a single pack.
+
 Configuration
 -------------
 
builtin/gc.c +43 -4

89 lines not on this page · raw message

t/t6500-gc.sh +25 -0
index 41b0be575d..a95969af2a 100755
@@ -43,6 +43,31 @@ test_expect_success 'gc is not aborted due to a stale symref' '
 	)
 '
 
+test_expect_success 'gc --keep-largest-pack' '
+	test_create_repo keep-pack &&
+	(
+		cd keep-pack &&
+		test_commit one &&
+		test_commit two &&
+		test_commit three &&
+		git gc &&
+		( cd .git/objects/pack && ls *.pack ) >pack-list &&
+		test_line_count = 1 pack-list &&
+		BASE_PACK=.git/objects/pack/pack-*.pack &&
+		test_commit four &&
+		git repack -d &&
+		test_commit five &&
+		git repack -d &&
+		( cd .git/objects/pack && ls *.pack ) >pack-list &&
+		test_line_count = 3 pack-list &&
+		git gc --keep-largest-pack &&
+		( cd .git/objects/pack && ls *.pack ) >pack-list &&
+		test_line_count = 2 pack-list &&
+		test_path_is_file $BASE_PACK &&
+		git fsck
+	)
+'
+
 test_expect_success 'auto gc with too many loose objects does not attempt to create bitmaps' '
 	test_config gc.auto 3 &&
 	test_config gc.autodetach false &&
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#261 lore raw

PATCH v4 4/7 gc: add gc.bigPackThreshold config

In reply to Nguyễn Thái Ngọc Duy

The --keep-largest-pack option is not very convenient to use because you need to tell gc to do this explicitly (and probably on just a few large repos).

Add a config key that enables this mode when packs larger than a limit are found. Note that there's a slight behavior difference compared to --keep-largest-pack: all packs larger than the threshold are kept, not just the largest one.

---
 Documentation/config.txt |  8 ++++++++
 Documentation/git-gc.txt |  6 ++++--
 builtin/gc.c             | 26 ++++++++++++++++++++------
 3 files changed, 32 insertions(+), 8 deletions(-)
Documentation/config.txt +8 -0
index ce9102cea8..d63db3f12c 100644
@@ -1558,6 +1558,14 @@ gc.autoDetach::
 	Make `git gc --auto` return immediately and run in background
 	if the system supports it. Default is true.
 
+gc.bigPackThreshold::
+	If non-zero, all packs larger than this limit are kept when
+	`git gc` is run. This is very similar to `--keep-base-pack`
+	except that all packs that meet the threshold are kept, not
+	just the base pack. Defaults to zero. Common unit suffixes of
+	'k', 'm', or 'g' are supported.
+
+
 gc.logExpiry::
 	If the file gc.log exists, then `git gc --auto` won't run
 	unless that file is more than 'gc.logExpiry' old.  Default is
Documentation/git-gc.txt +4 -2
index bf81b8de30..0adf381b52 100644
@@ -55,7 +55,8 @@ all loose objects are combined into a single pack using
 disables automatic packing of loose objects.
 +
 If the number of packs exceeds the value of `gc.autoPackLimit`,
-then existing packs (except those marked with a `.keep` file)
+then existing packs (except those marked with a `.keep` file
+or over `gc.bigPackThreshold` limit)
 are consolidated into a single pack by using the `-A` option of
 'git repack'. Setting `gc.autoPackLimit` to 0 disables
 automatic consolidation of packs.
@@ -80,7 +81,8 @@ automatic consolidation of packs.
 
 --keep-largest-pack::
 	All packs except the largest pack and those marked with a
-	`.keep` files are consolidated into a single pack.
+	`.keep` files are consolidated into a single pack. When this
+	option is used, `gc.bigPackThreshold` is ignored.
 
 Configuration
 -------------
builtin/gc.c +20 -6
index 9a09cf53b0..53a0500898 100644
@@ -39,6 +39,7 @@ static timestamp_t gc_log_expire_time;
 static const char *gc_log_expire = "1.day.ago";
 static const char *prune_expire = "2.weeks.ago";
 static const char *prune_worktrees_expire = "3.months.ago";
+static unsigned long big_pack_threshold;
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
@@ -126,6 +127,8 @@ static void gc_config(void)
 	git_config_get_expiry("gc.worktreepruneexpire", &prune_worktrees_expire);
 	git_config_get_expiry("gc.logexpiry", &gc_log_expire);
 
+	git_config_get_ulong("gc.bigpackthreshold", &big_pack_threshold);
+
 	git_config(git_default_config, NULL);
 }
 
@@ -164,7 +167,7 @@ static int too_many_loose_objects(void)
 	return needed;
 }
 
-static void find_base_packs(struct string_list *packs)
+static void find_base_packs(struct string_list *packs, unsigned long limit)
 {
 	struct packed_git *p, *base = NULL;
 
@@ -173,7 +176,10 @@ static void find_base_packs(struct string_list *packs)
 	for (p = packed_git; p; p = p->next) {
 		if (!p->pack_local)
 			continue;
-		if (!base || base->pack_size < p->pack_size) {
+		if (limit) {
+			if (p->pack_size >= limit)
+				string_list_append(packs, p->pack_name);
+		} else if (!base || base->pack_size < p->pack_size) {
 			base = p;
 		}
 	}
@@ -245,9 +251,15 @@ static int need_to_gc(void)
 	 * we run "repack -A -d -l".  Otherwise we tell the caller
 	 * there is no need.
 	 */
-	if (too_many_packs())
-		add_repack_all_option(NULL);
-	else if (too_many_loose_objects())
+	if (too_many_packs()) {
+		struct string_list keep_pack = STRING_LIST_INIT_NODUP;
+
+		if (big_pack_threshold)
+			find_base_packs(&keep_pack, big_pack_threshold);
+
+		add_repack_all_option(&keep_pack);
+		string_list_clear(&keep_pack, 0);
+	} else if (too_many_loose_objects())
 		add_repack_incremental_option();
 	else
 		return 0;
@@ -462,7 +474,9 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 
 		if (keep_base_pack != -1) {
 			if (keep_base_pack)
-				find_base_packs(&keep_pack);
+				find_base_packs(&keep_pack, 0);
+		} else if (big_pack_threshold) {
+			find_base_packs(&keep_pack, big_pack_threshold);
 		}
 
 		add_repack_all_option(&keep_pack);
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#262 lore raw

PATCH v4 5/7 gc: handle a corner case in gc.bigPackThreshold

In reply to Nguyễn Thái Ngọc Duy

This config allows us to keep <N> packs back if their size is larger than a limit. But if this N >= gc.autoPackLimit, we may have a problem. We are supposed to reduce the number of packs after a threshold because it affects performance.

We could tell the user that they have incompatible gc.bigPackThreshold and gc.autoPackLimit, but it's kinda hard when 'git gc --auto' runs in background. Instead let's fall back to the next best stategy: try to reduce the number of packs anyway, but keep the base pack out. This reduces the number of packs to two and hopefully won't take up too much resources to repack (the assumption still is the base pack takes most resources to handle).

---
 Documentation/config.txt | 6 +++++-
 builtin/gc.c             | 8 +++++++-
 2 files changed, 12 insertions(+), 2 deletions(-)
Documentation/config.txt +5 -1
index d63db3f12c..cf862d3edf 100644
@@ -1564,7 +1564,11 @@ gc.bigPackThreshold::
 	except that all packs that meet the threshold are kept, not
 	just the base pack. Defaults to zero. Common unit suffixes of
 	'k', 'm', or 'g' are supported.
-
++
+Note that if the number of kept packs is more than gc.autoPackLimit,
+this configuration variable is ignored, all packs except the base pack
+will be repacked. After this the number of packs should go below
+gc.autoPackLimit and gc.bigPackThreshold should be respected again.
 
 gc.logExpiry::
 	If the file gc.log exists, then `git gc --auto` won't run
builtin/gc.c +7 -1
index 53a0500898..74d3aaa270 100644
@@ -254,8 +254,14 @@ static int need_to_gc(void)
 	if (too_many_packs()) {
 		struct string_list keep_pack = STRING_LIST_INIT_NODUP;
 
-		if (big_pack_threshold)
+		if (big_pack_threshold) {
 			find_base_packs(&keep_pack, big_pack_threshold);
+			if (keep_pack.nr >= gc_auto_pack_limit) {
+				big_pack_threshold = 0;
+				string_list_clear(&keep_pack, 0);
+				find_base_packs(&keep_pack, 0);
+			}
+		}
 
 		add_repack_all_option(&keep_pack);
 		string_list_clear(&keep_pack, 0);
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#263 lore raw

PATCH v4 6/7 gc --auto: exclude base pack if not enough mem to "repack -ad"

In reply to Nguyễn Thái Ngọc Duy

pack-objects could be a big memory hog especially on large repos, everybody knows that. The suggestion to stick a .keep file on the giant base pack to avoid this problem is also known for a long time.

Recent patches add an option to do just this, but it has to be either configured or activated manually. This patch lets git gc --auto activate this mode automatically when it thinks repack -ad will use a lot of memory and start affecting the system due to swapping or flushing OS cache.

gc --auto decides to do this based on an estimation of pack-objects memory usage, which is quite accurate at least for the heap part, and whether that fits in half of system memory (the assumption here is for desktop environment where there are many other applications running).

This mechanism only kicks in if gc.bigBasePackThreshold is not configured. If it is, it is assumed that the user already knows what they want.

---
 Documentation/git-gc.txt |  9 +++-
 builtin/gc.c             | 98 +++++++++++++++++++++++++++++++++++++++-
 builtin/pack-objects.c   |  2 +-
 config.mak.uname         |  1 +
 git-compat-util.h        |  4 ++
 pack-objects.h           |  2 +
 t/t6500-gc.sh            |  7 +++
 7 files changed, 119 insertions(+), 4 deletions(-)
Documentation/git-gc.txt +7 -2
index 0adf381b52..7549094900 100644
@@ -58,8 +58,13 @@ If the number of packs exceeds the value of `gc.autoPackLimit`,
 then existing packs (except those marked with a `.keep` file
 or over `gc.bigPackThreshold` limit)
 are consolidated into a single pack by using the `-A` option of
-'git repack'. Setting `gc.autoPackLimit` to 0 disables
-automatic consolidation of packs.
+'git repack'.
+If the amount of memory is estimated not enough for `git repack` to
+run smoothly and `gc.bigPackThreshold` is not set, the largest
+pack will also be excluded (this is the equivalent of running `git gc`
+with `--keep-base-pack`).
+Setting `gc.autoPackLimit` to 0 disables automatic consolidation of
+packs.
 
 --prune=<date>::
 	Prune loose objects older than date (default is 2 weeks ago,
builtin/gc.c +97 -1

140 lines not on this page · raw message

builtin/pack-objects.c +1 -1
index 7ec70131a9..8b2f8b72bf 100644
@@ -80,7 +80,7 @@ static uint16_t write_bitmap_options;
 static int exclude_promisor_objects;
 
 static unsigned long delta_cache_size = 0;
-static unsigned long max_delta_cache_size = 256 * 1024 * 1024;
+static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
 static unsigned long cache_max_small_delta_size = 1000;
 
 static unsigned long window_memory_limit = 0;
config.mak.uname +1 -0
index 6a1d0de0cc..ae9cbccec1 100644
@@ -37,6 +37,7 @@ ifeq ($(uname_S),Linux)
 	HAVE_GETDELIM = YesPlease
 	SANE_TEXT_GREP=-a
 	FREAD_READS_DIRECTORIES = UnfortunatelyYes
+	BASIC_CFLAGS += -DHAVE_SYSINFO
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
git-compat-util.h +4 -0
index 07e383257b..e373af48b8 100644
@@ -284,6 +284,10 @@ extern char *gitdirname(char *);
 #include <openssl/err.h>
 #endif
 
+#ifdef HAVE_SYSINFO
+# include <sys/sysinfo.h>
+#endif
+
 /* On most systems <netdb.h> would have given us this, but
  * not on some systems (e.g. z/OS).
  */
pack-objects.h +2 -0
index 03f1191659..af4f46c026 100644
@@ -1,6 +1,8 @@
 #ifndef PACK_OBJECTS_H
 #define PACK_OBJECTS_H
 
+#define DEFAULT_DELTA_CACHE_SIZE (256 * 1024 * 1024)
+
 struct object_entry {
 	struct pack_idx_entry idx;
 	unsigned long size;	/* uncompressed size */
t/t6500-gc.sh +7 -0
index a95969af2a..100f287b97 100755
@@ -5,6 +5,13 @@ test_description='basic git gc tests
 
 . ./test-lib.sh
 
+test_expect_success 'setup' '
+	# do not let the amount of physical memory affects gc
+	# behavior, make sure we always pack everything to one pack by
+	# default
+	git config gc.bigPackThreshold 2g
+'
+
 test_expect_success 'gc empty repository' '
 	git gc
 '
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
#264 lore raw

PATCH v4 7/7 pack-objects: show some progress when counting kept objects

In reply to Nguyễn Thái Ngọc Duy

We only show progress when there are new objects to be packed. But when --keep-pack is specified on the base pack, we will exclude most of objects. This makes 'pack-objects' stay silent for a long time while the counting phase is going.

Let's show some progress whenever we visit an object instead. The old "Counting objects" is renamed to "Enumerating objects" and a new progress "Counting objects" line is added.

This new "Counting objects" line should progress pretty quick when the system is beefy. But when the system is under pressure, the reading object header done in this phase could be slow and showing progress is an improvement over staying silent in the current code.

---
 builtin/pack-objects.c | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)
builtin/pack-objects.c +12 -6
index 8b2f8b72bf..1379b4cb92 100644
@@ -44,7 +44,7 @@ static const char *pack_usage[] = {
 static struct packing_data to_pack;
 
 static struct pack_idx_entry **written_list;
-static uint32_t nr_result, nr_written;
+static uint32_t nr_result, nr_written, nr_seen;
 
 static int non_empty;
 static int reuse_delta = 1, reuse_object = 1;
@@ -1096,6 +1096,8 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 	off_t found_offset = 0;
 	uint32_t index_pos;
 
+	display_progress(progress_state, ++nr_seen);
+
 	if (have_duplicate_entry(oid, exclude, &index_pos))
 		return 0;
 
@@ -1111,8 +1113,6 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,
 	create_object_entry(oid, type, pack_name_hash(name),
 			    exclude, name && no_try_delta(name),
 			    index_pos, found_pack, found_offset);
-
-	display_progress(progress_state, nr_result);
 	return 1;
 }
 
@@ -1123,6 +1123,8 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
 {
 	uint32_t index_pos;
 
+	display_progress(progress_state, ++nr_seen);
+
 	if (have_duplicate_entry(oid, 0, &index_pos))
 		return 0;
 
@@ -1130,8 +1132,6 @@ static int add_object_entry_from_bitmap(const struct object_id *oid,
 		return 0;
 
 	create_object_entry(oid, type, name_hash, 0, 0, index_pos, pack, offset);
-
-	display_progress(progress_state, nr_result);
 	return 1;
 }
 
@@ -1717,6 +1717,10 @@ static void get_object_details(void)
 	uint32_t i;
 	struct object_entry **sorted_by_offset;
 
+	if (progress)
+		progress_state = start_progress(_("Counting objects"),
+						to_pack.nr_objects);
+
 	sorted_by_offset = xcalloc(to_pack.nr_objects, sizeof(struct object_entry *));
 	for (i = 0; i < to_pack.nr_objects; i++)
 		sorted_by_offset[i] = to_pack.objects + i;
@@ -1727,7 +1731,9 @@ static void get_object_details(void)
 		check_object(entry);
 		if (big_file_threshold < entry->size)
 			entry->no_try_delta = 1;
+		display_progress(progress_state, i + 1);
 	}
+	stop_progress(&progress_state);
 
 	/*
 	 * This must happen in a second pass, since we rely on the delta
@@ -3212,7 +3218,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)
 	}
 
 	if (progress)
-		progress_state = start_progress(_("Counting objects"), 0);
+		progress_state = start_progress(_("Enumerating objects"), 0);
 	if (!use_internal_rev_list)
 		read_object_list_from_stdin();
 	else {
Signature · 1 line
2.17.0.rc0.348.gd5a49e0b6f
author
#265 lore raw
In reply to Duy Nguyen
Quoted 5 lines · Duy Nguyen

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

Next low hanging fruit item:

struct revindex_entry {
        off_t offset;
        unsigned int nr;
};

We need on entry per object, so 6.5M objects * 16 bytes = 104 MB. If we break this struct apart and store two arrays of offset and nr in struct packed_git, we save 4 bytes per struct, 26 MB total.

It's getting low but every megabyte counts for me, and it does not look like breaking this struct will make horrible code (we recreate the struct at find_pack_revindex()) so I'm going to do this too unless someone objects. There will be slight performance regression due to cache effects, but hopefully it's ok.

Signature · 1 line
Duy
#266 lore raw
Quoted 8 lines · Duy Nguyen
struct revindex_entry {
        off_t offset;
        unsigned int nr;
};

We need on entry per object, so 6.5M objects * 16 bytes = 104 MB. If we break this struct apart and store two arrays of offset and nr in struct packed_git, we save 4 bytes per struct, 26 MB total.

Can the offset array can be a union which stores int32_t/uint32_t instead of off_t for projects which never exceed 2/4GB?

Likewise, places object_entry where "unsigned long" and off_t are 64-bit could benefit from being 32-bit. Testing/maintenance overhead could be bad, for those, though.

#267 lore raw
In reply to Duy Nguyen
Quoted 18 lines · Duy Nguyen
Quoted 5 lines · Duy Nguyen

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

Next low hanging fruit item:

struct revindex_entry {
        off_t offset;
        unsigned int nr;
};

We need on entry per object, so 6.5M objects * 16 bytes = 104 MB. If we break this struct apart and store two arrays of offset and nr in struct packed_git, we save 4 bytes per struct, 26 MB total.

Quoted 5 lines

It's getting low but every megabyte counts for me, and it does not look like breaking this struct will make horrible code (we recreate the struct at find_pack_revindex()) so I'm going to do this too unless someone objects. There will be slight performance regression due to cache effects, but hopefully it's ok.

Maybe you will prove me wrong, but I don't think splitting them is going to work. The point of the revindex_entry is that we sort the (offset,nr) tuple as a unit.

Or are you planning to sort it, and then copy the result into two separate arrays? I think that would work, but it sounds kind of nasty (arcane code, and extra CPU work for systems that don't care about the 26MB).

-Peff

author
#268 lore raw
Quoted 26 lines · Jeff King
Quoted 18 lines · Duy Nguyen
Quoted 5 lines · Duy Nguyen

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

Next low hanging fruit item:

struct revindex_entry {
        off_t offset;
        unsigned int nr;
};

We need on entry per object, so 6.5M objects * 16 bytes = 104 MB. If we break this struct apart and store two arrays of offset and nr in struct packed_git, we save 4 bytes per struct, 26 MB total.

Quoted 5 lines

It's getting low but every megabyte counts for me, and it does not look like breaking this struct will make horrible code (we recreate the struct at find_pack_revindex()) so I'm going to do this too unless someone objects. There will be slight performance regression due to cache effects, but hopefully it's ok.

Quoted 6 lines

Maybe you will prove me wrong, but I don't think splitting them is going to work. The point of the revindex_entry is that we sort the (offset,nr) tuple as a unit.

Or are you planning to sort it, and then copy the result into two separate arrays?

Yep.

Quoted 1 line

I think that would work, but it sounds kind of nasty

Yeah :(

Quoted 2 lines

(arcane code, and extra CPU work for systems that don't care about the 26MB).

Signature · 1 line
Duy
author
#269 lore raw
In reply to Jeff King
Quoted 26 lines · Jeff King
Quoted 18 lines · Duy Nguyen
Quoted 5 lines · Duy Nguyen

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

Next low hanging fruit item:

struct revindex_entry {
        off_t offset;
        unsigned int nr;
};

We need on entry per object, so 6.5M objects * 16 bytes = 104 MB. If we break this struct apart and store two arrays of offset and nr in struct packed_git, we save 4 bytes per struct, 26 MB total.

Quoted 5 lines

It's getting low but every megabyte counts for me, and it does not look like breaking this struct will make horrible code (we recreate the struct at find_pack_revindex()) so I'm going to do this too unless someone objects. There will be slight performance regression due to cache effects, but hopefully it's ok.

Quoted 8 lines

Maybe you will prove me wrong, but I don't think splitting them is going to work. The point of the revindex_entry is that we sort the (offset,nr) tuple as a unit.

Or are you planning to sort it, and then copy the result into two separate arrays? I think that would work, but it sounds kind of nasty (arcane code, and extra CPU work for systems that don't care about the 26MB).

How about two level lookups? We have

struct revindex_entry_l2 {
uint32_t offset; /* the lowest 32 bits */
uint32_t nr;
};

struct revindex {
        struct revindex_entry *level1[256]; /* 8 high bits */
};

This limits us to 1024GB pack files, which should give us some time before we have to worry about it again and most of the time we'll need just one or two items in level1[] so cache effects are not that bad. Preparing/Sorting this could be a problem though.

Signature · 1 line
Duy
#270 lore raw

Why does pack-objects use so much memory on incremental packing?

In reply to Duy Nguyen

On Wed, Feb 28 2018, Duy Nguyen jotted:

Quoted 10 lines

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

This demonstration patch (probably breaks some tests) would reduce the size of struct object_entry from from 136 down to 112 bytes on x86-64. There are 6483999 of these objects, so the saving is 17% or 148 MB.

Splitting this off into its own thread. Aside from the improvements in your repack memory reduction ([email protected]) and gc config ([email protected]) series's I'm wondering why repack takes so much memory to incrementally repack new stuff when you leave out the base pack.

Repacking git.git takes around 290MB of memory on my system, but I'd think that this would make it take a mere few megabytes, since all I'm asking it to do is pack up the few loose objects that got added and keep the base pack:

    (
        rm -rf /tmp/git &&
        git clone [email protected]:git/git.git /tmp/git &&
        cd /tmp/git &&
        touch $(ls .git/objects/pack/*pack | sed 's/\.pack$/.keep/') &&
        for i in {1..10}
        do
            touch $i &&
            git add $i &&
            git commit -m$i
        done &&
        /usr/bin/time -f %M git repack -A -d
    )

But no, it takes around 230MB. But thinking about it a bit further:

* This builds on top of existing history, so that needs to be read/consulted

* We might be reusing (if not directly, skipping re-comuting) deltas from the existing pack.

But I get the same result if after cloning I make an orphan branch, and pass all the "do this as cheaply as possible" branches I can find down to git-repack:

    (
        rm -rf /tmp/git &&
        git clone [email protected]:git/git.git /tmp/git &&
        cd /tmp/git &&
        touch $(ls .git/objects/pack/*pack | sed 's/\.pack$/.keep/') &&
        git checkout --orphan new &&
        git reset --hard &&
        for i in {1..10}
        do
            touch $i &&
            git add $i &&
            git commit -m$i
        done &&
        git tag -d $(git tag -l) &&
        /usr/bin/time -f %M git repack -A -d -f -F --window=1 --depth=1
    )

But the memory use barely changes, my first example used 227924 kb, but this one uses 226788.

Of course nobody's going to clone a huge repo and then right away create an --orphan branch, but is there an inherent reason for why this couldn't be taking as much memory as if the repo was cloned with --depth=1?

I.e. when I have a *.keep on an existing pack we would have some low-memory mode to copy the trees/blobs needed for the current commit over to the new pack, and use that as the basis for packing everything going forward.

Jeff: Is this something ref islands[1] could be (ab)used to do, or have I misunderstood that concept?

1. https://public-inbox.org/git/[email protected]/
https://public-inbox.org/git/[email protected]/
https://public-inbox.org/git/[email protected]/

author
#271 lore raw

Re: Why does pack-objects use so much memory on incremental packing?

Quoted 20 lines · Ævar Arnfjörð Bjarmason

On Wed, Feb 28 2018, Duy Nguyen jotted:

Quoted 10 lines

linux-2.6.git current has 6483999 objects. "git gc" on my poor laptop consumes 1.7G out of 4G RAM, pushing lots of data to swap and making all apps nearly unusuable (granted the problem is partly Linux I/O scheduler too). So I wonder if we can reduce pack-objects memory footprint a bit.

This demonstration patch (probably breaks some tests) would reduce the size of struct object_entry from from 136 down to 112 bytes on x86-64. There are 6483999 of these objects, so the saving is 17% or 148 MB.

Splitting this off into its own thread. Aside from the improvements in your repack memory reduction ([email protected]) and gc config ([email protected]) series's I'm wondering why repack takes so much memory to incrementally repack new stuff when you leave out the base pack.

Quoted 7 lines

Repacking git.git takes around 290MB of memory on my system, but I'd think that this would make it take a mere few megabytes, since all I'm asking it to do is pack up the few loose objects that got added and keep the base pack:

...

I left some clue in the new estimate_repack_memory() function in my gc series that could help you find this out. I haven't really tested this case but my guess is the two cache pools we have will likely be filled up close to full anyway and hit delta_base_cache_limit and max_delta_cache_size limits. When these are really full on default configuration, they'll take roughly ~300mb.

The second is, I think we still go through all objects to mark which one is included in the new pack, which one not (and probably which one can be delta base candidates). Try calling alloc_report() function at the end of repack to see exactly how much memory is locked in there. This we could perhaps improve for incremental repack by avoiding running rev-list.

Signature · 1 line
Duy
#272 lore raw

Re: Why does pack-objects use so much memory on incremental packing?

In reply to Ævar Arnfjörð Bjarmason
Quoted 5 lines · Ævar Arnfjörð Bjarmason

Splitting this off into its own thread. Aside from the improvements in your repack memory reduction ([email protected]) and gc config ([email protected]) series's I'm wondering why repack takes so much memory to incrementally repack new stuff when you leave out the base pack.

I think it's a combination of a few issues:

1. We do a complete history traversal, and then cull out objects which our filters reject (e.g., things in a .keep pack). So you pay for all of the "struct object", along with the obj_hash table to look them up.

In my measurements of just "git rev-list --objects --all", that's about 25MB for git.git. Plus a few misc things (pending object structs for the traversal, etc).

2. The delta-base cache used for the traversal is a fixed size. So that's going to be 96MB regardless of your repo size.

I measured a total heap usage of 130MB for "rev-list --objects --all". That's not 230, but I'm not sure what you're measuring. If it's RSS, keep in mind that includes the mmap'd packfiles, too.

Doing a separate "rev-list | pack-objects" should be minorly cheaper (although it will still have a similar peak cost, since that memory will just be moved to the rev-list process).

If you _just_ want to pack the loose objects, you could probably do something like:

find .git/objects/?? -type f |
tr -d / |
git pack-objects .git/objects/pack/pack
git prune-packed

But you'd get pretty crappy deltas out of that, since the heuristics rely on knowing the filenames of trees and blobs (which you can only get by walking the graph).

So you'd do better with something like:

  git rev-list --objects $new_tips --not $old_tips |
  git pack-objects .git/objects/pack/pack

but it's hard to know what "$old_tips" should be, unless you recorded it last time you did a full repack.

Quoted 4 lines

But no, it takes around 230MB. But thinking about it a bit further:

* This builds on top of existing history, so that needs to be read/consulted

Right, I think this is the main thing.

Quoted 2 lines

* We might be reusing (if not directly, skipping re-comuting) deltas from the existing pack.

I don't think that should matter. We'll reuse deltas if the base is going into our pack, but otherwise recompute. The delta computation itself takes some memory, but it should be fairly constant even for a large repo (it's really average_blob_size * window_size).

So I think most of your memory is just going to the traversal stuff. Running:

  valgrind --tool=massif git pack-objects --all foo </dev/null
  ms_print massif.out.*

shows 223MB at peak, with 43% of the memory to the delta cache, about 10% to traversal (object structs and hash), 6% to pack revindexes, and 34% for the big packlist array. So for anything focusing on the packlist, you can at best reclaim 75MB.

I suspect a bigger repository would be more interesting, though, since the delta cache would remain the same size.

Quoted 21 lines

But I get the same result if after cloning I make an orphan branch, and pass all the "do this as cheaply as possible" branches I can find down to git-repack:

    (
        rm -rf /tmp/git &&
        git clone [email protected]:git/git.git /tmp/git &&
        cd /tmp/git &&
        touch $(ls .git/objects/pack/*pack | sed 's/\.pack$/.keep/') &&
        git checkout --orphan new &&
        git reset --hard &&
        for i in {1..10}
        do
            touch $i &&
            git add $i &&
            git commit -m$i
        done &&
        git tag -d $(git tag -l) &&
        /usr/bin/time -f %M git repack -A -d -f -F --window=1 --depth=1
    )
Quoted 2 lines

But the memory use barely changes, my first example used 227924 kb, but this one uses 226788.

I think you still had to do the whole history traversal there, because you have existing refs (the "master" branch, along with refs/remotes) as well as reflogs.

Try:

git branch -d master
git remote rm origin
rm -rf .git/logs

After that, the repack uses about 5MB.

Quoted 6 lines

Jeff: Is this something ref islands[1] could be (ab)used to do, or have I misunderstood that concept?

1. https://public-inbox.org/git/[email protected]/
https://public-inbox.org/git/[email protected]/
https://public-inbox.org/git/[email protected]/

I think you misunderstood the concept. :)

They are about disallowing deltas between unrelated islands. They actually require _more_ memory, because you have to storage an island bitmap for each object (though with some copy-on-write magic, it's not too bad). But they can never save you memory, since reused deltas are always cheaper than re-finding new ones.

-Peff