Path-walk API and applications
Subsystems (2)
cover Path-walk API and applications
This RFC is ultimately about introducing a new way to walk objects, called the "path-walk API" in the new path-walk.[ch] files. Before digging into the details of the API, let's discuss the applications which will hint at the API's design.
APPLICATIONS OF THE PATH-WALK API
=================================
The applications of this API were discovered in the following order, though I recommend reversing the order for the priority of their actual implementation in future patch series:
* git backfill: a builtin to download missing blobs in a blobless partial clone, done in batches and grouped by the path they appear in to maximize delta compression in each batch. Allows focusing on the paths of the sparse-checkout to only get the blobs necessary for history queries in the current focus.
* git survey: Jeff Hostetler built this feature [1] as a way to get functionality similar to git-sizer [2], but using the internals of Git to do it faster. It also displays information not available to git-sizer, like the on-disk size of objects. This RFC presents a simplified version of the builtin focused on listing the paths that contribute the most to the on-disk size of trees and blobs.
* git pack-objects --path-walk: In order to find a way to compute deltas among objects of the same path, I applied the path-walk API to 'git pack-objects' behind an optional flag. There are overlaps with the '--sparse' option [3], [4] that can be used here. This provides perfect partitioning by path name, without any possible collisions from the name-hash algorithm. It also allows using the name-hash values to find cross-path delta chains in a second pass.
* git repack --full-name-hash: If we are worried about name-hsah collisions, an easier thing to implement is a different name-hash algorithm that is less likely to have collisions. This feature was already sent to the mailing list as a fully-reviewable series [5]. It is included here because this series allows testing the --path-walk option against the --full-name-hash.
[1] https://github.com/microsoft/git/pull/667 [2]
https://github.com/github/git-sizer [3]
https://github.com/git/git/compare/5d826e972970a784bd7a7bdf587512510097b8c7...99dbbfa8ddbba2b620965d026d4ec199b8837a6f
[4]
https://devblogs.microsoft.com/devops/exploring-new-frontiers-for-git-push-performance/
[5]
https://lore.kernel.org/git/[email protected]
TIMELINE FOR CREATING THESE APPLICATIONS IN THIS ORDER
======================================================
Here's the story about how these applications came about: I was tasked with understanding why certain internal repositories were growing larger than expected. (Feel free to skip. Otherwise, thank you for indulging me.)
I first prototyped 'git backfill' as a way to download some of the largest repositories without being blocked on a full clone. This batched download mechanism allowed me to essentially have a retryable clone, since the client could restart the process from scratch and skip any objects that were already on disk. It was natural to batch based on the path of the blobs in order to theoretically save time and network bandwidth due to better delta calculations.
While investigating these repositories, I had some data hinting at the total size of the objects by type. But I was most interested in learning what exactly was causing this growth. I did have a hint that the "release" branches were taking up much more space than the default branch, since cloning with --single-branch resulted in ~55GB of data but then fetching the release branches led to an additional ~125GB of data. Using "git diff" it was clear that these branches stored some CHANGELOG.json and CHANGELOG.md files, but the diffs were relatively small (but multiple such changes were batched together into a single new commit). I needed to see why exactly these paths were taking up so much space.
To this, I turned to Jeff Hostetler's new "git survey" command. This told me information about the total size of trees and blobs and told me the path name for the individual blobs that were the largest in the repository. These paths were typically binary files that appeared only once or twice and did not account for the scale issues. I modified 'git survey' to use the same path-batching logic as in 'git backfill' to then consider each batch of objects and their size. Thus, the "path-walk API" was born. (This RFC contains a version that looks like it was created before 'git backfill'.)
The repository I was looking at had a clear pattern in its top 100 file paths by on-disk size: 99 of them were CHANGELOG.json and CHANGELOG.md files. The .md files surprised me, since they were always simple appends of the previous .md file at the same path. The .json files were capped in how many versions were being listed (at least in recent versions) but the data it stored was harder to compress. So I went looking into how 'git push' was calculating these delta bases. Adding some debug information to 'git pack-objects' demonstrated that the previous file versions were not being matched as delta bases. Instead, other new blobs in the push were being used as delta bases.
This meant that what should have been a trivial set of deltas bloated to 20-60 MB. (We will see later that it is possible for these to be 100-500 KB.)
Here is where I went on a little bit of a detour. (Come with me, it's important.) I knew that 'git pack-objects' used a name-hash to group objects by path, so I assumed that the reason these delta bases were not found was because the UNINTERESTING objects were not being added to the packing list. I have since discovered that this is incorrect, but I might have gotten stuck if I didn't think this.
This seemed like a natural reason to extend the path-walk API to allow walking commits and tags as part of 'git pack-objects' behind a new '--path-walk' option. The idea here is to compute deltas among the objects that share a common path and then later go through the (type, name-hash, size) sorting system to find other delta bases across path boundaries. After a lot of testing, failing, and testing again, the implementation in this RFC finally works to achieve the goal. It's not pretty (especially with how it handles tags) but it gets the job done.
In hindsight, I realized that the UNINTERESTING objects were being considered, but due to collisions in the name-hash algorithm these objects were being sorted outside of the delta computation window. For this reason, I thought to create a new name-hash algorithm. Thus, the --full-name-hash option for 'git pack-objects' and 'git repack' was born. This feature was split out and sent to the mailing list independently from this RFC.
RFC GOALS
=========
The goals of this RFC are:
1. To demonstrate potential applications of the path-walk API to motivate its generality as these features are sent in full-quality patch series, but in a different order.
2. To communicate the discoveries found during the --path-walk and --full-name-hash features in 'git pack-objects' and 'git repack'. This includes comparing and contrasting the effectiveness of these features.
3. To demonstrate the value of the path-based batching in the 'git survey' feature, and to inspire others to think about what other statistics would be valuable in that feature. (I anticipate that once a base is established, multiple contributors will help expand its functionality long into the future.)
RFC OUTLINE
===========
The patches are grouped roughly by the application, in order of discovery:
PART I: 'git backfill'
======================
These patches introduce the 'git backfill' builtin including its '--batch-size' and '--sparse' options. While this is the first and simplest application, it is also the lowest priority in terms of user need.
* path-walk: introduce an object walk by path
* backfill: add builtin boilerplate
* backfill: basic functionality and tests
* backfill: add --batch-size= option
* backfill: add --sparse option
* backfill: assume --sparse when sparse-checkout is enabled
PART II: 'git survey'
=====================
These patches reimplement a subset of the functionality of 'git survey' as well as generalize some of the data structures that Jeff's implementation made. The flexibility hopefully comes through to show the potential for future extensions. These patches are quite rough and will need more attention before they can be sent for full review.
* path-walk: allow consumer to specify object types
* path-walk: allow visiting tags
* survey: stub in new experimental git-survey command
* survey: add command line opts to select references
* survey: collect the set of requested refs
* survey: start pretty printing data in table form
* survey: add object count summary
* survey: summarize total sizes by object type
* survey: show progress during object walk
* survey: add ability to track prioritized lists
* survey: add report of "largest" paths
PART III: 'git pack-objects --path-walk'
========================================
Here is where I think the meat of the RFC really lies. There are still some rough edges, but the data will show that 'git pack-objects --path-walk' has the potential to be an extremely effective way to pack objects. (Caveats will come later in the analysis section.)
* revision: create mark_trees_uninteresting_dense()
* path-walk: add prune_all_uninteresting option
* pack-objects: add --path-walk option
* pack-objects: extract should_attempt_deltas()
* pack-objects: introduce GIT_TEST_PACK_PATH_WALK
* p5313: add size comparison test
* repack: add --path-walk option
* pack-objects: enable --path-walk via config
* scalar: enable path-walk during push via config
One obvious issue with this current implementation is that it inlines much of the delta calculation into the "Enumerate objects" phase, and thus makes it single-threaded. This should be fixed before being considered for full review, but even without threading this version can out-perform the standard packing strategy in terms of end-to-end packing time!
PART IV: 'git repack --full-name-hash'
======================================
This is a simplified version of the patch series that was split out by itself earlier for full review. This was split out on its own partly because it doesn't actually use the path-walk API. This has benefits and drawbacks, but it seems like a quick win for many scenarios.
* pack-objects: add --full-name-hash option
* test-name-hash: add helper to compute name-hash functions
* p5314: add a size test for name-hash collisions
* pack-objects: output debug info about deltas
This last patch is an add-on of the debugging information that I used to discover issues with delta bases during 'git push'.
DISCUSSION OF NAME HASH
=======================
One thing to talk about before digging into --path-walk and --full-name-hash features is the existing name-hash algorithm. This hash algorithm creates a uint32_t based on the final 16 characters of the path name, weighing the last characters more. There are multiple benefits to this:
1. Files of common types (.c, .txt, ...) may be grouped together.
2. Files that are renamed across directories may be grouped together.
(Thanks, Junio, for making this second benefit clear.)
The issue here is that some common patterns arise in repositories that use common path names across directories, and those files are creating name-hash collisions and making the sort less effective. One thing that can counteract these collisions is to increase the --window setting, but this significantly slows the delta computations.
Thus, the --path-walk and --full-name-hash features both attempt to combat these name-hash collisions in very different ways. The --path-walk mechanism uses the path-walk API to consider batches of objects that all share the same path. This avoids storing every possible path names in memory while doing the object walk, but still gives nice boundaries for delta compression possibilities. After the path-walk is complete, the full packing list is still sorted via name-hash and this allows for cross-path deltas. This is critical!
The --full-name-hash feature does the simpler choice of replacing the name-hash method with one that has fewer collisions, but loses the benefits of "nearby" paths having close hash values.
This naturally leads to these two main differences in the two approaches:
1. The --full-name-hash feature is not good for 'git push' or similarly small pack-files. Since it limits the delta chains to objects with the same full path and loses the benefit of "nearby" paths, this feature should be used for larger repacks. In my testing, 'git push' simulations almost always have poor packing but 'git repack -adf' simulations have packing rivaling the --path-walk option.
2. The --path-walk option changes the object ordering significantly, meaning it may not ever be appropriate to combine with advanced repacking features such as delta islands or even reachability bitmaps. While my testing has shown that the --path-walk repacks are the most efficient of all options, this limitation makes me hesitate to recommend it wider than client repositories.
One natural question to consider is to think about storing both the name-hash and the full-name-hash and doing two delta passes, each one sorting the objects by a different hash function. The first issue is that we don't want to store two hash values per object, as that will significantly increase the memory pressure during repacking. This could be side-stepped by storing the full-name-hash in the packing list and then a second mapping from full-name-hash to name-hash. However, that still leads to the two passes taking extra time. The --path-walk approach is faster than even a single pass. And in the right scenarios, the --full-name-hash option is very close to the --path-walk results.
ANALYSIS OF PACKING STRATEGIES
==============================
Since I was focused on an internal monorepo that stored a large collection of Javascript packages, it should be no surprise that I eventually found other Javascript repositories that used similar tooling and thus had similar issues with unexplained scale problems. I'll use these four repositories as examples repeatedly, but one is actually public: microsoft/fluentui [6]. I'll use Repo B, C, and D for the others, in increasing size.
[6] https://github.com/microsoft/fluentui
In each of these repositories, doing a full repack ('git repack -adf') right after cloning presents a sizeable reduction in space. This is expected for servers not being optimized for exactly the reachable set I'm cloning. So, I'll focus on how much the default repacking parameters compare to using the --full-name-hash or --path-walk options:
| Repo | Standard Repack | With --full-name-hash | With --path-walk | |----------|-----------------|-----------------------|------------------| | fluentui | 438 MB | 168 MB | 148 MB | | Repo B | 6,255 MB | 829 MB | 778 MB | | Repo C | 37,737 MB | 7,125 MB | 6,158 MB | | Repo D | 130,049 MB | 6,190 MB | 4,432 MB |
Hopefully these reductions show how much these name-hash collisions are causing issues in these repositories.
For the fluentui repo, I'm also able to share highlights from the 'git survey' output immediately after cloning and after repacking with the --path-walk option.
First, we can consider the total reachable objects in each scenario:
TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------
Commits | 20579 | 10443669 | 15092790
Trees | 276503 | 40212070 | 244429615
Blobs | 294500 | 635365661 | 10791187920TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------
Commits | 20579 | 10450605 | 15092790
Trees | 276503 | 31136263 | 244429615
Blobs | 294500 | 94442401 | 10791187920Now, we can consider the top 10 file paths before and after repacking with the --path-walk feature:
TOP FILES BY DISK SIZE
===================================================================
Path | Count | Disk Size | Inflated Size
--------------------------------+-------+-----------+--------------
yarn.lock | 802 | 58060531 | 889120488 ...-experiments/CHANGELOG.json | 505 | 28439452 | 252723999 ...fabric-react/CHANGELOG.json | 1270 | 25556510 | 902756623 ...t-components/CHANGELOG.json | 176 | 20366365 | 244936649 ...act-charting/CHANGELOG.json | 590 | 20106422 | 208224460 ...e-components/CHANGELOG.json | 559 | 15761271 | 189061764 ...act-examples/CHANGELOG.json | 577 | 13615569 | 234949961 ...react-charting/CHANGELOG.md | 564 | 11205840 | 104337986 .../experiments/CHANGELOG.json | 569 | 10596377 | 123662770 ...i-fabric-react/CHANGELOG.md | 1263 | 8154248 | 261494258
TOP FILES BY DISK SIZE
===================================================================
Path | Count | Disk Size | Inflated Size
--------------------------------+-------+-----------+--------------
yarn.lock | 802 | 9909326 | 889120488
...iceBrandGuide_16Sep2016.pdf | 1 | 2106334 | 2186005
...out/src/images/download.jpg | 1 | 1845249 | 1846117
...fluent-ui-logo-inverted.png | 3 | 1370372 | 1447493
.yarn/releases/cli.js | 1 | 1335657 | 6741614
...c/images/fluent-ui-logo.png | 3 | 1272902 | 1341139
...ages/fluent-ui-logo-dev.png | 3 | 1130989 | 1186897
...nents/public/SegoeUI-VF.ttf | 1 | 1074046 | 1844524
...ig/rush/npm-shrinkwrap.json | 138 | 1058531 | 89326567
...Accessibility_29Sep2016.pdf | 1 | 856621 | 927268As we can see from this example, before the repack we are mainly seeing the disk space be dominated by objects that appear at paths with CHANGELOG.json or CHANGELOG.md, which are frequently hitting collisions with the default name-hash. After the repack with the --path-walk feature, we see the largest paths are what we expect: checked in binaries, frequently-edited yarn files, and other hard-to- compress data.
The nice thing about this result is that we can point to files that are taking up space because there is no other way around it, not that the naming convention for the files is causing confusion during packing.
WHAT TO DO NOW?
===============
Thank you for reading this far. I hope that this has added context on the patch series for the --full-name-hash option, but also provided the right context for when I come back in a week or so with a review-ready version of the --path-walk option.
These patches are rough and I want to make sure everyone knows that. Reviewing them for style or even basic organization may lead to some wasted time. I know that at least one of the patches got mangled and its diff does not match its description (I'm thinking specifically about "pack-objects:
extract should_attempt_deltas()" but this could apply elsewhere).
But I think that interested parties could take my branch, build it, and give these features a try on their favorite repos. I'd love to hear feedback on the usability and effectiveness, especially if someone finds a case where the --path-walk option is less effective at packing data.
There are two things going on since I started working on this series that could cause issues with applying this series on top of current 'master' or with topics in 'seen':
* The most obvious one is the collision with the --full-name-hash feature under review. The in-review series takes precedent.
* The unused parameter warnings are now errors! I'm glad that got in, but I wasn't careful about it in these RFC patches.
* John Cai is proposing to change the builtin interface to have builtin methods take a repository pointer. I approve of this effort, but it collides with the two new builtins introduced here. As discussed above, the 'git survey' and 'git backfill' builtins are the lowest priority items for getting merged, in my opinion, so they can wait until that change has been made.
I look forward to any and all comments.
Thanks! -Stolee
Derrick Stolee (27):
path-walk: introduce an object walk by path
backfill: add builtin boilerplate
backfill: basic functionality and tests
backfill: add --batch-size=<n> option
backfill: add --sparse option
backfill: assume --sparse when sparse-checkout is enabled
path-walk: allow consumer to specify object types
path-walk: allow visiting tags
survey: start pretty printing data in table form
survey: add object count summary
survey: summarize total sizes by object type
survey: show progress during object walk
survey: add ability to track prioritized lists
survey: add report of "largest" paths
revision: create mark_trees_uninteresting_dense()
path-walk: add prune_all_uninteresting option
pack-objects: add --path-walk option
pack-objects: extract should_attempt_deltas()
pack-objects: introduce GIT_TEST_PACK_PATH_WALK
p5313: add size comparison test
repack: add --path-walk option
pack-objects: enable --path-walk via config
scalar: enable path-walk during push via config
pack-objects: add --full-name-hash option
test-name-hash: add helper to compute name-hash functions
p5314: add a size test for name-hash collisions
pack-objects: output debug info about deltas
Jeff Hostetler (3):
survey: stub in new experimental git-survey command
survey: add command line opts to select references
survey: collect the set of requested refs
.gitignore | 2 + Documentation/config/pack.txt | 8 + Documentation/git-backfill.txt | 60 ++ Documentation/git-survey.txt | 70 +++ Makefile | 4 + builtin.h | 2 + builtin/backfill.c | 141 +++++ builtin/pack-objects.c | 298 ++++++++-- builtin/repack.c | 10 + builtin/survey.c | 965 +++++++++++++++++++++++++++++++++ command-list.txt | 2 + git.c | 2 + pack-objects.h | 20 + path-walk.c | 401 ++++++++++++++ path-walk.h | 73 +++ repo-settings.c | 3 + repository.h | 1 + revision.c | 15 + revision.h | 1 + scalar.c | 1 + t/README | 4 + t/helper/test-name-hash.c | 23 + t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + t/perf/p5313-pack-objects.sh | 101 ++++ t/perf/p5314-name-hash.sh | 41 ++ t/t5620-backfill.sh | 181 +++++++ t/t8100-git-survey.sh | 76 +++ 28 files changed, 2469 insertions(+), 38 deletions(-) create mode 100644 Documentation/git-backfill.txt create mode 100644 Documentation/git-survey.txt create mode 100644 builtin/backfill.c create mode 100644 builtin/survey.c create mode 100644 path-walk.c create mode 100644 path-walk.h create mode 100644 t/helper/test-name-hash.c create mode 100755 t/perf/p5313-pack-objects.sh create mode 100755 t/perf/p5314-name-hash.sh create mode 100755 t/t5620-backfill.sh create mode 100755 t/t8100-git-survey.sh
base-commit: 17d4b10aea6bda2027047a0e3548a6f8ad667dde
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1786%2Fderrickstolee%2Fpath-walk-rfc-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1786/derrickstolee/path-walk-rfc-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1786
Signature · 1 line
gitgitgadget
PATCH 1/30 path-walk: introduce an object walk by path
From: Derrick Stolee <[email protected]>
In anticipation of a few planned applications, introduce the most basic form of a path-walk API. It currently assumes that there are no UNINTERESTING objects, and does not include any complicated filters. It calls a function pointer on groups of tree and blob objects as grouped by path. This only includes objects the first time they are discovered, so an object that appears at multiple paths will not be included in two batches.
There are many future adaptations that could be made, but they are left for future updates when consumers are ready to take advantage of those features.
RFC TODO: It would be helpful to create a test-tool that allows printing of each batch for strong testing.
--- Makefile | 1 + path-walk.c | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++ path-walk.h | 43 ++++++++++ 3 files changed, 279 insertions(+) create mode 100644 path-walk.c create mode 100644 path-walk.h
Makefile +1 -0
@@ -1090,6 +1090,7 @@ LIB_OBJS += parse-options.o LIB_OBJS += patch-delta.o LIB_OBJS += patch-ids.o LIB_OBJS += path.o +LIB_OBJS += path-walk.o LIB_OBJS += pathspec.o LIB_OBJS += pkt-line.o LIB_OBJS += preload-index.o
path-walk.c new file +235 -0
235 lines not on this page · raw message
path-walk.h new file +43 -0
43 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 2/30 backfill: add builtin boilerplate
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
In anticipation of implementing 'git backfill', populate the necessary files with the boilerplate of a new builtin.
RFC TODO: When preparing this for a full implementation, make sure it is based on the newest standards introduced by [1].
[1] https://lore.kernel.org/git/[email protected]/T/#m606036ea2e75a6d6819d6b5c90e729643b0ff7f7
[PATCH 1/3] builtin: add a repository parameter for builtin functions
--- .gitignore | 1 + Documentation/git-backfill.txt | 23 +++++++++++++++++++++++ Makefile | 1 + builtin.h | 1 + builtin/backfill.c | 29 +++++++++++++++++++++++++++++ command-list.txt | 1 + git.c | 1 + 7 files changed, 57 insertions(+) create mode 100644 Documentation/git-backfill.txt create mode 100644 builtin/backfill.c
.gitignore +1 -0
@@ -19,6 +19,7 @@ /git-apply /git-archimport /git-archive +/git-backfill /git-bisect /git-blame /git-branch
Documentation/git-backfill.txt new file +23 -0
@@ -0,0 +1,23 @@ +git-backfill(1) +=============== + +NAME +---- +git-backfill - Download missing objects in a partial clone + + +SYNOPSIS +-------- +[verse] +'git backfill' [<options>] + +DESCRIPTION +----------- + +SEE ALSO +-------- +linkgit:git-clone[1]. + +GIT +--- +Part of the linkgit:git[1] suite
Makefile +1 -0
@@ -1198,6 +1198,7 @@ BUILTIN_OBJS += builtin/am.o BUILTIN_OBJS += builtin/annotate.o BUILTIN_OBJS += builtin/apply.o BUILTIN_OBJS += builtin/archive.o +BUILTIN_OBJS += builtin/backfill.o BUILTIN_OBJS += builtin/bisect.o BUILTIN_OBJS += builtin/blame.o BUILTIN_OBJS += builtin/branch.o
builtin.h +1 -0
@@ -127,6 +127,7 @@ int cmd_am(int argc, const char **argv, const char *prefix);int cmd_annotate (int argc,const char **argv,const char *prefix);int cmd_apply (int argc,const char **argv,const char *prefix);int cmd_archive (int argc,const char **argv,const char *prefix); +int cmd_backfill (int argc,const char **argv,const char *prefix);int cmd_bisect (int argc,const char **argv,const char *prefix);int cmd_blame (int argc,const char **argv,const char *prefix);int cmd_branch (int argc,const char **argv,const char *prefix);
builtin/backfill.c new file +29 -0
@@ -0,0 +1,29 @@ +#include "builtin.h" +#include "config.h" +#include "parse-options.h" +#include "repository.h" +#include "object.h" + +static const char *const builtin_backfill_usage[] = { +N_ ("git backfill [<options>]" ), +NULL +}; + +int cmd_backfill (int argc,const char **argv,const char *prefix) +{ +struct option options[] = { +OPT_END (), + }; + +if (argc ==2 && !strcmp (argv[1 ],"-h" )) +usage_with_options (builtin_backfill_usage, options); + + argc =parse_options (argc, argv, prefix, options, builtin_backfill_usage, +0 ); + +git_config (git_default_config,NULL ); + +die (_ ("not implemented" )); + +return 0 ; +}
command-list.txt +1 -0
@@ -60,6 +60,7 @@ git-annotate ancillaryinterrogators git-apply plumbingmanipulators complete git-archimport foreignscminterface git-archive mainporcelain +git-backfill mainporcelain history git-bisect mainporcelain info git-blame ancillaryinterrogators complete git-branch mainporcelain history
git.c +1 -0
@@ -509,6 +509,7 @@ static struct cmd_struct commands[] = { { "annotate", cmd_annotate, RUN_SETUP }, { "apply", cmd_apply, RUN_SETUP_GENTLY }, { "archive", cmd_archive, RUN_SETUP_GENTLY }, + { "backfill", cmd_backfill, RUN_SETUP }, { "bisect", cmd_bisect, RUN_SETUP }, { "blame", cmd_blame, RUN_SETUP }, { "branch", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },
Signature · 1 line
gitgitgadget
PATCH 3/30 backfill: basic functionality and tests
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The default behavior of 'git backfill' is to fetch all missing blobs that are reachable from HEAD. Document and test this behavior.
The implementation is a very simple use of the path-walk API, initializing the revision walk at HEAD to start the path-walk from all commits reachable from HEAD. Ignore the object arrays that correspond to tree entries, assuming that they are all present already.
--- Documentation/git-backfill.txt | 24 ++++++++ builtin/backfill.c | 101 ++++++++++++++++++++++++++++++++- t/t5620-backfill.sh | 97 +++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100755 t/t5620-backfill.sh
Documentation/git-backfill.txt +24 -0
@@ -14,6 +14,30 @@ SYNOPSIS DESCRIPTION ----------- +Blobless partial clones are created using `git clone --filter=blob:none` +and then configure the local repository such that the Git client avoids +downloading blob objects unless they are required for a local operation. +This initially means that the clone and later fetches download reachable +commits and trees but no blobs. Later operations that change the `HEAD` +pointer, such as `git checkout` or `git merge`, may need to download +missing blobs in order to complete their operation. + +In the worst cases, commands that compute blob diffs, such as `git blame`, +become very slow as they download the missing blobs in single-blob +requests to satisfy the missing object as the Git command needs it. This +leads to multiple download requests and no ability for the Git server to +provide delta compression across those objects. + +The `git backfill` command provides a way for the user to request that +Git downloads the missing blobs (with optional filters) such that the +missing blobs representing historical versions of files can be downloaded +in batches. The `backfill` command attempts to optimize the request by +grouping blobs that appear at the same path, hopefully leading to good +delta compression in the packfile sent by the server. + +By default, `git backfill` downloads all blobs reachable from the `HEAD` +commit. This set can be restricted or expanded using various options. + SEE ALSO -------- linkgit:git-clone[1].
builtin/backfill.c +98 -3
121 lines not on this page · raw message
t/t5620-backfill.sh new file +97 -0
97 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 4/30 backfill: add --batch-size=<n> option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Users may want to specify a minimum batch size for their needs. This is only a minimum: the path-walk API provides a list of OIDs that correspond to the same path, and thus it is optimal to allow delta compression across those objects in a single server request.
We could consider limiting the request to have a maximum batch size in the future.
--- Documentation/git-backfill.txt | 10 +++++++++- builtin/backfill.c | 4 +++- t/t5620-backfill.sh | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-)
Documentation/git-backfill.txt +9 -1
@@ -9,7 +9,7 @@ git-backfill - Download missing objects in a partial clone SYNOPSIS -------- [verse] -'git backfill' [<options>] +'git backfill' [--batch-size=<n>] DESCRIPTION -----------
@@ -38,6 +38,14 @@ delta compression in the packfile sent by the server. By default, `git backfill` downloads all blobs reachable from the `HEAD` commit. This set can be restricted or expanded using various options. +OPTIONS +------- + +--batch-size=<n>:: + Specify a minimum size for a batch of missing objects to request + from the server. This size may be exceeded by the last set of + blobs seen at a given path. Default batch size is 16,000. + SEE ALSO -------- linkgit:git-clone[1].
builtin/backfill.c +3 -1
@@ -21,7 +21,7 @@#include "path-walk.h" static const char *const builtin_backfill_usage[] = { -N_ ("git backfill [<options>]" ), +N_ ("git backfill [--batch-size=<n>]" ),NULL };
@@ -109,6 +109,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix) .batch_size = 16000, };struct option options[] = { +OPT_INTEGER (0 ,"batch-size" , &ctx.batch_size, +N_ ("Minimun number of objects to request at a time" )),OPT_END (), };
t/t5620-backfill.sh +18 -0
@@ -62,6 +62,24 @@ test_expect_success 'do partial clone 1, backfill gets all objects' ' test_line_count = 0 revs2 ' +test_expect_success 'do partial clone 2, backfill batch size' ' + git clone --no-checkout --filter=blob:none \ + --single-branch --branch=main \ + "file://$(pwd)/srv.bare" backfill2 && + + GIT_TRACE2_EVENT="$(pwd)/batch-trace" git \ + -C backfill2 backfill --batch-size=20 && + + # Batches were used + test_trace2_data promisor fetch_count 20 <batch-trace >matches && + test_line_count = 2 matches && + test_trace2_data promisor fetch_count 8 <batch-trace && + + # No more missing objects! + git -C backfill2 rev-list --quiet --objects --missing=print HEAD >revs2 && + test_line_count = 0 revs2 +' + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd
Signature · 1 line
gitgitgadget
PATCH 5/30 backfill: add --sparse option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
One way to significantly reduce the cost of a Git clone and later fetches is to use a blobless partial clone and combine that with a sparse-checkout that reduces the paths that need to be populated in the working directory. Not only does this reduce the cost of clones and fetches, the sparse-checkout reduces the number of objects needed to download from a promisor remote.
However, history investigations can be expensie as computing blob diffs will trigger promisor remote requests for one object at a time. This can be avoided by downloading the blobs needed for the given sparse-checkout using 'git backfill' and its new '--sparse' mode, at a time that the user is willing to pay that extra cost.
Note that this is distinctly different from the '--filter=sparse:<oid>' option, as this assumes that the partial clone has all reachable trees and we are using client-side logic to avoid downloading blobs outside of the sparse-checkout cone. This avoids the server-side cost of walking trees while also achieving a similar goal. It also downloads in batches based on similar path names, presenting a resumable download if things are interrupted.
This augments the path-walk API to have a possibly-NULL 'pl' member that may point to a 'struct pattern_list'. This could be more general than the sparse-checkout definition at HEAD, but 'git backfill --sparse' is currently the only consumer.
Be sure to test this in both cone mode and not cone mode. Cone mode has the benefit that the path-walk can skip certain paths once they would expand beyond the sparse-checkout.
--- Documentation/git-backfill.txt | 6 +++- builtin/backfill.c | 13 +++++++- path-walk.c | 18 +++++++++++ path-walk.h | 11 +++++++ t/t5620-backfill.sh | 55 ++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-)
Documentation/git-backfill.txt +5 -1
@@ -9,7 +9,7 @@ git-backfill - Download missing objects in a partial clone SYNOPSIS -------- [verse] -'git backfill' [--batch-size=<n>] +'git backfill' [--batch-size=<n>] [--[no-]sparse] DESCRIPTION -----------
@@ -46,6 +46,10 @@ OPTIONS from the server. This size may be exceeded by the last set of blobs seen at a given path. Default batch size is 16,000. +--[no-]sparse:: + Only download objects if they appear at a path that matches the + current sparse-checkout. + SEE ALSO -------- linkgit:git-clone[1].
builtin/backfill.c +12 -1
@@ -4,6 +4,7 @@#include "parse-options.h" #include "repository.h" #include "commit.h" +#include "dir.h" #include "hex.h" #include "tree.h" #include "tree-walk.h"
@@ -21,7 +22,7 @@#include "path-walk.h" static const char *const builtin_backfill_usage[] = { -N_ ("git backfill [--batch-size=<n>]" ), +N_ ("git backfill [--batch-size=<n>] [--[no-]sparse]" ),NULL };
@@ -29,6 +30,7 @@ struct backfill_context {struct repository *repo;struct oid_array current_batch;size_t batch_size; +int sparse; };static void clear_backfill_context (struct backfill_context *ctx)
@@ -84,6 +86,12 @@ static int do_backfill(struct backfill_context *ctx)struct path_walk_info info =PATH_WALK_INFO_INIT ;int ret; +if (ctx->sparse) { +CALLOC_ARRAY (info.pl,1 ); +if (get_sparse_checkout_patterns (info.pl)) +return error (_ ("problem loading sparse-checkout" )); + } +repo_init_revisions (ctx->repo, &revs,"" );handle_revision_arg ("HEAD" , &revs,0 ,0 );
@@ -107,10 +115,13 @@ int cmd_backfill(int argc, const char **argv, const char *prefix) .repo = the_repository, .current_batch = OID_ARRAY_INIT, .batch_size = 16000, + .sparse = 0, };struct option options[] = {OPT_INTEGER (0 ,"batch-size" , &ctx.batch_size,N_ ("Minimun number of objects to request at a time" )), +OPT_BOOL (0 ,"sparse" , &ctx.sparse, +N_ ("Restrict the missing objects to the current sparse-checkout" )),OPT_END (), };
path-walk.c +18 -0
@@ -10,6 +10,7 @@#include "hex.h" #include "object.h" #include "oid-array.h" +#include "repository.h" #include "revision.h" #include "string-list.h" #include "strmap.h"
@@ -111,6 +112,23 @@ static int add_children(struct path_walk_context *ctx,if (type ==OBJ_TREE )strbuf_addch (&path,'/' ); +if (ctx->info->pl) { +int dtype; +enum pattern_match_result match; + match =path_matches_pattern_list (path.buf, path.len, + path.buf + base_len, &dtype, + ctx->info->pl, + ctx->repo->index); + +if (ctx->info->pl->use_cone_patterns && + match ==NOT_MATCHED ) +continue ; +else if (!ctx->info->pl->use_cone_patterns && + type ==OBJ_BLOB && + match !=MATCHED ) +continue ; + } +if (!(list =strmap_get (&ctx->paths_to_lists, path.buf))) {CALLOC_ARRAY (list,1 ); list->type = type;
path-walk.h +11 -0
@@ -6,6 +6,7 @@struct rev_info ;struct oid_array ; +struct pattern_list ;/** * The type of a function pointer for the method that is called on a list of
@@ -30,6 +31,16 @@ struct path_walk_info {*/ path_fn path_fn;void *path_fn_data; + +/** +* Specify a sparse-checkout definition to match our paths to. Do not +* walk outside of this sparse definition. If the patterns are in +* cone mode, then the search may prune directories that are outside +* of the cone. If not in cone mode, then all tree paths will be +* explored but the path_fn will only be called when the path matches +* the sparse-checkout patterns. +*/ +struct pattern_list *pl; };#define PATH_WALK_INFO_INIT { 0 }
t/t5620-backfill.sh +55 -0
61 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 6/30 backfill: assume --sparse when sparse-checkout is enabled
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The previous change introduced the '--[no-]sparse' option for the 'git backfill' command, but did not assume it as enabled by default. However, this is likely the behavior that users will most often want to happen. Without this default, users with a small sparse-checkout may be confused when 'git backfill' downloads every version of every object in the full history.
However, this is left as a separate change so this decision can be reviewed independently of the value of the '--[no-]sparse' option.
Add a test of adding the '--sparse' option to a repo without sparse-checkout to make it clear that supplying it without a sparse-checkout is an error.
--- Documentation/git-backfill.txt | 3 ++- builtin/backfill.c | 4 ++++ t/t5620-backfill.sh | 13 ++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-)
Documentation/git-backfill.txt +2 -1
@@ -48,7 +48,8 @@ OPTIONS --[no-]sparse:: Only download objects if they appear at a path that matches the - current sparse-checkout. + current sparse-checkout. If the sparse-checkout feature is enabled, + then `--sparse` is assumed and can be disabled with `--no-sparse`. SEE ALSO --------
builtin/backfill.c +4 -0
@@ -5,6 +5,7 @@#include "repository.h" #include "commit.h" #include "dir.h" +#include "environment.h" #include "hex.h" #include "tree.h" #include "tree-walk.h"
@@ -133,5 +134,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix)git_config (git_default_config,NULL ); +if (ctx.sparse <0 ) + ctx.sparse = core_apply_sparse_checkout; +return do_backfill (&ctx); }
t/t5620-backfill.sh +12 -1
@@ -80,6 +80,12 @@ test_expect_success 'do partial clone 2, backfill batch size' ' test_line_count = 0 revs2 ' +test_expect_success 'backfill --sparse without sparse-checkout fails' ' + git init not-sparse && + test_must_fail git -C not-sparse backfill --sparse 2>err && + grep "problem loading sparse-checkout" err +' + test_expect_success 'backfill --sparse' ' git clone --sparse --filter=blob:none \ --single-branch --branch=main \
@@ -108,7 +114,12 @@ test_expect_success 'backfill --sparse' ' test_trace2_data promisor fetch_count 8 <sparse-trace2 && test_trace2_data path-walk paths 15 <sparse-trace2 && git -C backfill3 rev-list --quiet --objects --missing=print HEAD >missing && - test_line_count = 24 missing + test_line_count = 24 missing && + + # Disabling the --sparse option (on by default) will download everything + git -C backfill3 backfill --no-sparse && + git -C backfill3 rev-list --quiet --objects --missing=print HEAD >missing && + test_line_count = 0 missing ' test_expect_success 'backfill --sparse without cone mode' '
Signature · 1 line
gitgitgadget
PATCH 7/30 path-walk: allow consumer to specify object types
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
This adds the ability to ask for the commits as a single list. This will also reduce the calls in 'git backfill' to be a BUG() statement if called with anything other than blobs.
--- builtin/backfill.c | 2 +- path-walk.c | 40 ++++++++++++++++++++++++++++++++++------ path-walk.h | 12 +++++++++++- 3 files changed, 46 insertions(+), 8 deletions(-)
builtin/backfill.c +1 -1
@@ -61,7 +61,7 @@ static int fill_missing_blobs(const char *path,struct backfill_context *ctx = data;if (type !=OBJ_BLOB ) -return 0 ; +BUG ("fill_missing_blobs only takes blob objects" );for (size_t i =0 ; i < list->nr; i++) {off_t size =0 ;
path-walk.c +34 -6
@@ -83,6 +83,10 @@ static int add_children(struct path_walk_context *ctx,if (S_ISGITLINK (entry.mode))continue ; +/* If the caller doesn't want blobs, then don't bother. */ +if (!ctx->info->blobs && type ==OBJ_BLOB ) +continue ; +if (type ==OBJ_TREE ) {struct tree *child =lookup_tree (ctx->repo, &entry.oid); o = child ? &child->object :NULL ;
@@ -156,9 +160,11 @@ static int walk_path(struct path_walk_context *ctx, list =strmap_get (&ctx->paths_to_lists, path); -/* Evaluate function pointer on this data. */ - ret = ctx->info->path_fn (path, &list->oids, list->type, - ctx->info->path_fn_data); +/* Evaluate function pointer on this data, if requested. */ +if ((list->type ==OBJ_TREE && ctx->info->trees) || + (list->type ==OBJ_BLOB && ctx->info->blobs)) + ret = ctx->info->path_fn (path, &list->oids, list->type, + ctx->info->path_fn_data);/* Expand data for children. */ if (list->type ==OBJ_TREE ) {
@@ -200,6 +206,7 @@ int walk_objects_by_path(struct path_walk_info *info)size_t commits_nr =0 , paths_nr =0 ;struct commit *c;struct type_and_oid_list *root_tree_list; +struct type_and_oid_list *commit_list;struct path_walk_context ctx = { .repo = info->revs->repo, .revs = info->revs,
@@ -210,28 +217,49 @@ int walk_objects_by_path(struct path_walk_info *info)trace2_region_enter ("path-walk" ,"commit-walk" , info->revs->repo); +CALLOC_ARRAY (commit_list,1 ); + commit_list->type =OBJ_COMMIT ; +/* Insert a single list for the root tree into the paths. */ CALLOC_ARRAY (root_tree_list,1 ); root_tree_list->type =OBJ_TREE ;strmap_put (&ctx.paths_to_lists, root_path, root_tree_list); -if (prepare_revision_walk (info->revs))die (_ ("failed to setup revision walk" ));while ((c =get_revision (info->revs))) { -struct object_id *oid =get_commit_tree_oid (c); -struct tree *t =lookup_tree (info->revs->repo, oid); +struct object_id *oid; +struct tree *t; commits_nr++; +if (info->commits) +oid_array_append (&commit_list->oids, + &c->object.oid); + +/* If we only care about commits, then skip trees. */ +if (!info->trees && !info->blobs) +continue ; + + oid =get_commit_tree_oid (c); + t =lookup_tree (info->revs->repo, oid); +if (t)oid_array_append (&root_tree_list->oids, oid);else warning ("could not find tree %s" ,oid_to_hex (oid)); + }trace2_data_intmax ("path-walk" , ctx.repo,"commits" , commits_nr);trace2_region_leave ("path-walk" ,"commit-walk" , info->revs->repo); +/* Track all commits. */ +if (info->commits) + ret = info->path_fn ("" , &commit_list->oids,OBJ_COMMIT , + info->path_fn_data); +oid_array_clear (&commit_list->oids); +free (commit_list); +string_list_append (&ctx.path_stack, root_path);trace2_region_enter ("path-walk" ,"path-walk" , info->revs->repo);
path-walk.h +11 -1
@@ -32,6 +32,14 @@ struct path_walk_info {path_fn path_fn;void *path_fn_data; +/** +* Initialize which object types the path_fn should be called on. This +* could also limit the walk to skip blobs if not set. +*/ +int commits; +int trees; +int blobs; +/** * Specify a sparse-checkout definition to match our paths to. Do not * walk outside of this sparse definition. If the patterns are in
@@ -43,7 +51,9 @@ struct path_walk_info {struct pattern_list *pl; }; -#define PATH_WALK_INFO_INIT { 0 } +#define PATH_WALK_INFO_INIT { \ + .blobs = 1, \ +}/** * Given the configuration of 'info', walk the commits based on 'info->revs' and
Signature · 1 line
gitgitgadget
PATCH 8/30 path-walk: allow visiting tags
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
In anticipation of using the path-walk API to analyze tags or include them in a pack-file, add the ability to walk the tags that were included in the revision walk.
--- path-walk.c | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ path-walk.h | 1 + 2 files changed, 59 insertions(+)
path-walk.c +58 -0
@@ -14,6 +14,7 @@#include "revision.h" #include "string-list.h" #include "strmap.h" +#include "tag.h" #include "trace2.h" #include "tree.h" #include "tree-walk.h"
@@ -215,6 +216,9 @@ int walk_objects_by_path(struct path_walk_info *info) .paths_to_lists = STRMAP_INIT }; +struct oid_array tagged_tree_list =OID_ARRAY_INIT ; +struct oid_array tagged_blob_list =OID_ARRAY_INIT ; +trace2_region_enter ("path-walk" ,"commit-walk" , info->revs->repo);CALLOC_ARRAY (commit_list,1 );
@@ -260,6 +264,60 @@ int walk_objects_by_path(struct path_walk_info *info)oid_array_clear (&commit_list->oids);free (commit_list); +if (info->tags) { +struct oid_array tags =OID_ARRAY_INIT ; + +trace2_region_enter ("path-walk" ,"tag-walk" , info->revs->repo); + +/* +* Walk any pending objects at this point, but they should only +* be tags. +*/ +for (size_t i =0 ; i < info->revs->pending.nr; i++) { +struct object_array_entry *pending = info->revs->pending.objects + i; +struct object *obj = pending->item; + +while (obj->type ==OBJ_TAG ) { +struct tag *tag =lookup_tag (info->revs->repo, + &obj->oid); +oid_array_append (&tags, &obj->oid); + obj = tag->tagged; + } + +switch (obj->type) { +case OBJ_TREE : +oid_array_append (&tagged_tree_list, &obj->oid); +break ; + +case OBJ_BLOB : +oid_array_append (&tagged_blob_list, &obj->oid); +break ; + +case OBJ_COMMIT : +/* skip */ +break ; + +default : +BUG ("should not see any other type here" ); + } + } + + info->path_fn ("initial" , &tags,OBJ_TAG , info->path_fn_data); + +if (tagged_tree_list.nr) + info->path_fn ("tagged-trees" , &tagged_tree_list,OBJ_TREE , + info->path_fn_data); +if (tagged_blob_list.nr) + info->path_fn ("tagged-blobs" , &tagged_blob_list,OBJ_BLOB , + info->path_fn_data); + +trace2_data_intmax ("path-walk" , ctx.repo,"tags" , tags.nr); +trace2_region_leave ("path-walk" ,"tag-walk" , info->revs->repo); +oid_array_clear (&tags); +oid_array_clear (&tagged_tree_list); +oid_array_clear (&tagged_blob_list); + } +string_list_append (&ctx.path_stack, root_path);trace2_region_enter ("path-walk" ,"path-walk" , info->revs->repo);
path-walk.h +1 -0
@@ -39,6 +39,7 @@ struct path_walk_info {int commits;int trees;int blobs; +int tags;/** * Specify a sparse-checkout definition to match our paths to. Do not
Signature · 1 line
gitgitgadget
PATCH 9/30 survey: stub in new experimental `git-survey` command
In reply to Derrick Stolee via GitGitGadgetFrom: Jeff Hostetler <[email protected]>
Start work on a new git survey command to scan the repository for monorepo performance and scaling problems. The goal is to measure the various known "dimensions of scale" and serve as a foundation for adding additional measurements as we learn more about Git monorepo scaling problems.
The initial goal is to complement the scanning and analysis performed by the GO-based git-sizer (https://github.com/github/git-sizer) tool. It is hoped that by creating a builtin command, we may be able to take advantage of internal Git data structures and code that is not accessible from GO to gain further insight into potential scaling problems.
RFC TODO: Adapt this boilerplat to match the upcoming changes to builtin methods that include a 'struct repository' pointer.
--- .gitignore | 1 + Documentation/git-survey.txt | 36 ++++++++++++++++++++++ Makefile | 1 + builtin.h | 1 + builtin/survey.c | 60 ++++++++++++++++++++++++++++++++++++ command-list.txt | 1 + git.c | 1 + t/t8100-git-survey.sh | 18 +++++++++++ 8 files changed, 119 insertions(+) create mode 100644 Documentation/git-survey.txt create mode 100644 builtin/survey.c create mode 100755 t/t8100-git-survey.sh
.gitignore +1 -0
@@ -165,6 +165,7 @@ /git-submodule /git-submodule--helper /git-subtree +/git-survey /git-svn /git-switch /git-symbolic-ref
Documentation/git-survey.txt new file +36 -0
@@ -0,0 +1,36 @@ +git-survey(1) +============= + +NAME +---- +git-survey - EXPERIMENTAL: Measure various repository dimensions of scale + +SYNOPSIS +-------- +[verse] +(EXPERIMENTAL!) `git survey` <options> + +DESCRIPTION +----------- + +Survey the repository and measure various dimensions of scale. + +As repositories grow to "monorepo" size, certain data shapes can cause +performance problems. `git-survey` attempts to measure and report on +known problem areas. + +OPTIONS +------- + +--progress:: + Show progress. This is automatically enabled when interactive. + +OUTPUT +------ + +By default, `git survey` will print information about the repository in a +human-readable format that includes overviews and tables. + +GIT +--- +Part of the linkgit:git[1] suite
Makefile +1 -0
@@ -1303,6 +1303,7 @@ BUILTIN_OBJS += builtin/sparse-checkout.o BUILTIN_OBJS += builtin/stash.o BUILTIN_OBJS += builtin/stripspace.o BUILTIN_OBJS += builtin/submodule--helper.o +BUILTIN_OBJS += builtin/survey.o BUILTIN_OBJS += builtin/symbolic-ref.o BUILTIN_OBJS += builtin/tag.o BUILTIN_OBJS += builtin/unpack-file.o
builtin.h +1 -0
@@ -239,6 +239,7 @@ int cmd_status(int argc, const char **argv, const char *prefix);int cmd_stash (int argc,const char **argv,const char *prefix);int cmd_stripspace (int argc,const char **argv,const char *prefix);int cmd_submodule__helper (int argc,const char **argv,const char *prefix); +int cmd_survey (int argc,const char **argv,const char *prefix);int cmd_switch (int argc,const char **argv,const char *prefix);int cmd_symbolic_ref (int argc,const char **argv,const char *prefix);int cmd_tag (int argc,const char **argv,const char *prefix);
builtin/survey.c new file +60 -0
60 lines not on this page · raw message
command-list.txt +1 -0
@@ -187,6 +187,7 @@ git-stash mainporcelain git-status mainporcelain info git-stripspace purehelpers git-submodule mainporcelain +git-survey mainporcelain git-svn foreignscminterface git-switch mainporcelain history git-symbolic-ref plumbingmanipulators
git.c +1 -0
@@ -630,6 +630,7 @@ static struct cmd_struct commands[] = { { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE }, { "stripspace", cmd_stripspace }, { "submodule--helper", cmd_submodule__helper, RUN_SETUP }, + { "survey", cmd_survey, RUN_SETUP }, { "switch", cmd_switch, RUN_SETUP | NEED_WORK_TREE }, { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, { "tag", cmd_tag, RUN_SETUP | DELAY_PAGER_CONFIG },
t/t8100-git-survey.sh new file +18 -0
@@ -0,0 +1,18 @@ +#!/bin/sh + +test_description='git survey' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +TEST_PASSES_SANITIZE_LEAK=0 +export TEST_PASSES_SANITIZE_LEAK + +. ./test-lib.sh + +test_expect_success 'git survey -h shows experimental warning' ' + test_expect_code 129 git survey -h 2>usage && + grep "EXPERIMENTAL!" usage +' + +test_done
Signature · 1 line
gitgitgadget
PATCH 10/30 survey: add command line opts to select references
In reply to Derrick Stolee via GitGitGadgetFrom: Jeff Hostetler <[email protected]>
By default we will scan all references in "refs/heads/", "refs/tags/" and "refs/remotes/".
Add command line opts let the use ask for all refs or a subset of them and to include a detached HEAD.
--- Documentation/git-survey.txt | 34 +++++++++++++ builtin/survey.c | 99 ++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+)
Documentation/git-survey.txt +34 -0
@@ -19,12 +19,46 @@ As repositories grow to "monorepo" size, certain data shapes can cause performance problems. `git-survey` attempts to measure and report on known problem areas. +Ref Selection and Reachable Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this first analysis phase, `git survey` will iterate over the set of +requested branches, tags, and other refs and treewalk over all of the +reachable commits, trees, and blobs and generate various statistics. + OPTIONS ------- --progress:: Show progress. This is automatically enabled when interactive. +Ref Selection +~~~~~~~~~~~~~ + +The following options control the set of refs that `git survey` will examine. +By default, `git survey` will look at tags, local branches, and remote refs. +If any of the following options are given, the default set is cleared and +only refs for the given options are added. + +--all-refs:: + Use all refs. This includes local branches, tags, remote refs, + notes, and stashes. This option overrides all of the following. + +--branches:: + Add local branches (`refs/heads/`) to the set. + +--tags:: + Add tags (`refs/tags/`) to the set. + +--remotes:: + Add remote branches (`refs/remote/`) to the set. + +--detached:: + Add HEAD to the set. + +--other:: + Add notes (`refs/notes/`) and stashes (`refs/stash/`) to the set. + OUTPUT ------
builtin/survey.c +99 -0
124 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 11/30 survey: collect the set of requested refs
In reply to Derrick Stolee via GitGitGadgetFrom: Jeff Hostetler <[email protected]>
Collect the set of requested branches, tags, and etc into a ref_array and collect the set of requested patterns into a strvec.
RFC TODO: This patch has some changes that should be in the previous patch, to make the diff look a lot better.
--- builtin/survey.c | 258 ++++++++++++++++++++++++++++++++++-------- t/t8100-git-survey.sh | 9 ++ 2 files changed, 217 insertions(+), 50 deletions(-)
builtin/survey.c +208 -50
@@ -1,6 +1,12 @@#include "builtin.h" #include "config.h" +#include "object.h" +#include "object-store-ll.h" #include "parse-options.h" +#include "progress.h" +#include "ref-filter.h" +#include "strvec.h" +#include "trace2.h" static const char *const survey_usage[] = {N_ ("(EXPERIMENTAL!) git survey <options>" ),
@@ -17,18 +23,8 @@ struct survey_refs_wanted {int want_other;/* see FILTER_REFS_OTHERS -- refs/notes/, refs/stash/ */ }; -/* -* The set of refs that we will search if the user doesn't select -* any on the command line. -*/ -static struct survey_refs_wanted refs_if_unspecified = { - .want_all_refs =0 , - - .want_branches =1 , - .want_tags =1 , - .want_remotes =1 , - .want_detached =0 , - .want_other =0 , +static struct survey_refs_wanted default_ref_options = { + .want_all_refs =1 , };struct survey_opts {
@@ -37,19 +33,51 @@ struct survey_opts {struct survey_refs_wanted refs; }; -static struct survey_opts survey_opts = { - .verbose =0 , - .show_progress =-1 ,/* defaults to isatty(2) */ +struct survey_report_ref_summary { +size_t refs_nr; +size_t branches_nr; +size_t remote_refs_nr; +size_t tags_nr; +size_t tags_annotated_nr; +size_t others_nr; +size_t unknown_nr; +}; + +/** +* This struct contains all of the information that needs to be printed +* at the end of the exploration of the repository and its references. +*/ +struct survey_report { +struct survey_report_ref_summary refs; +}; + +struct survey_context { +/* Options that control what is done. */ +struct survey_opts opts; + +/* Info for output only. */ +struct survey_report report; - .refs.want_all_refs =-1 , +/* +* The rest of the members are about enabling the activity +* of the 'git survey' command, including ref listings, object +* pointers, and progress. +*/ + +struct repository *repo; + +struct progress *progress; +size_t progress_nr; +size_t progress_total; - .refs.want_branches =-1 ,/* default these to undefined */ - .refs.want_tags =-1 , - .refs.want_remotes =-1 , - .refs.want_detached =-1 , - .refs.want_other =-1 , +struct strvec refs; }; +static void clear_survey_context (struct survey_context *ctx) +{ +strvec_clear (&ctx->refs); +} +/* * After parsing the command line arguments, figure out which refs we * should scan.
@@ -57,9 +85,9 @@ static struct survey_opts survey_opts = {* If ANY were given in positive sense, then we ONLY include them and * do not use the builtin values. */ -static void fixup_refs_wanted (void ) +static void fixup_refs_wanted (struct survey_context *ctx) { -struct survey_refs_wanted *rw = &survey_opts.refs; +struct survey_refs_wanted *rw = &ctx->opts.refs;/* * `--all-refs` overrides and enables everything.
@@ -82,7 +110,7 @@ static void fixup_refs_wanted(void) rw->want_remotes ==-1 && rw->want_detached ==-1 && rw->want_other == -1) { - *rw = refs_if_unspecified; + *rw = default_ref_options;return ; }
@@ -106,54 +134,184 @@ static void fixup_refs_wanted(void) rw->want_other =0 ; } -static struct option survey_options[] = { -OPT__VERBOSE (&survey_opts.verbose,N_ ("verbose output" )), -OPT_BOOL (0 ,"progress" , &survey_opts.show_progress,N_ ("show progress" )), - -OPT_BOOL_F (0 ,"all-refs" , &survey_opts.refs.want_all_refs,N_ ("include all refs" ),PARSE_OPT_NONEG ), - -OPT_BOOL_F (0 ,"branches" , &survey_opts.refs.want_branches,N_ ("include branches" ),PARSE_OPT_NONEG ), -OPT_BOOL_F (0 ,"tags" , &survey_opts.refs.want_tags,N_ ("include tags" ),PARSE_OPT_NONEG ), -OPT_BOOL_F (0 ,"remotes" , &survey_opts.refs.want_remotes,N_ ("include all remotes refs" ),PARSE_OPT_NONEG ), -OPT_BOOL_F (0 ,"detached" , &survey_opts.refs.want_detached,N_ ("include detached HEAD" ),PARSE_OPT_NONEG ), -OPT_BOOL_F (0 ,"other" , &survey_opts.refs.want_other,N_ ("include notes and stashes" ),PARSE_OPT_NONEG ), - -OPT_END (), -}; -static int survey_load_config_cb (const char *var,const char *value, -const struct config_context *ctx,void *pvoid) +const struct config_context *cctx,void *pvoid) { +struct survey_context *sctx = pvoid;if (!strcmp (var,"survey.verbose" )) { - survey_opts.verbose =git_config_bool (var, value); + sctx->opts.verbose =git_config_bool (var, value);return 0 ; }if (!strcmp (var,"survey.progress" )) { - survey_opts.show_progress =git_config_bool (var, value); + sctx->opts.show_progress =git_config_bool (var, value);return 0 ; } -return git_default_config (var, value, ctx, pvoid); +return git_default_config (var, value, cctx, pvoid); } -static void survey_load_config (void ) +static void survey_load_config (struct survey_context *ctx) { -git_config (survey_load_config_cb,NULL ); +git_config (survey_load_config_cb, ctx); +} + +static void do_load_refs (struct survey_context *ctx, +struct ref_array *ref_array) +{ +struct ref_filter filter =REF_FILTER_INIT ; +struct ref_sorting *sorting; +struct string_list sorting_options =STRING_LIST_INIT_DUP ; + +string_list_append (&sorting_options,"objectname" ); + sorting =ref_sorting_options (&sorting_options); + +if (ctx->opts.refs.want_detached) +strvec_push (&ctx->refs,"HEAD" ); + +if (ctx->opts.refs.want_all_refs) { +strvec_push (&ctx->refs,"refs/" ); + }else { +if (ctx->opts.refs.want_branches) +strvec_push (&ctx->refs,"refs/heads/" ); +if (ctx->opts.refs.want_tags) +strvec_push (&ctx->refs,"refs/tags/" ); +if (ctx->opts.refs.want_remotes) +strvec_push (&ctx->refs,"refs/remotes/" ); +if (ctx->opts.refs.want_other) { +strvec_push (&ctx->refs,"refs/notes/" ); +strvec_push (&ctx->refs,"refs/stash/" ); + } + } + + filter.name_patterns = ctx->refs.v; + filter.ignore_case =0 ; + filter.match_as_path =1 ; + +if (ctx->opts.show_progress) { + ctx->progress_total =0 ; + ctx->progress =start_progress (_ ("Scanning refs..." ),0 ); + } + +filter_refs (ref_array, &filter,FILTER_REFS_KIND_MASK ); + +if (ctx->opts.show_progress) { + ctx->progress_total = ref_array->nr; +display_progress (ctx->progress, ctx->progress_total); + } + +ref_array_sort (sorting, ref_array); + +stop_progress (&ctx->progress); +ref_filter_clear (&filter); +ref_sorting_release (sorting); +} + +/* +* The REFS phase: +* +* Load the set of requested refs and assess them for scalablity problems. +* Use that set to start a treewalk to all reachable objects and assess +* them. +* +* This data will give us insights into the repository itself (the number +* of refs, the size and shape of the DAG, the number and size of the +* objects). +* +* Theoretically, this data is independent of the on-disk representation +* (e.g. independent of packing concerns). +*/ +static void survey_phase_refs (struct survey_context *ctx) +{ +struct ref_array ref_array = {0 }; + +trace2_region_enter ("survey" ,"phase/refs" , ctx->repo); +do_load_refs (ctx, &ref_array); + + ctx->report.refs.refs_nr = ref_array.nr; +for (size_t i =0 ; i < ref_array.nr; i++) { +size_t size; +struct ref_array_item *item = ref_array.items[i]; + +switch (item->kind) { +case FILTER_REFS_TAGS : + ctx->report.refs.tags_nr++; +if (oid_object_info (ctx->repo, + &item->objectname, + &size) ==OBJ_TAG ) + ctx->report.refs.tags_annotated_nr++; +break ; + +case FILTER_REFS_BRANCHES : + ctx->report.refs.branches_nr++; +break ; + +case FILTER_REFS_REMOTES : + ctx->report.refs.remote_refs_nr++; +break ; + +case FILTER_REFS_OTHERS : + ctx->report.refs.others_nr++; +break ; + +default : + ctx->report.refs.unknown_nr++; +break ; + } + } + +trace2_region_leave ("survey" ,"phase/refs" , ctx->repo); + +ref_array_clear (&ref_array); }int cmd_survey (int argc,const char **argv,const char *prefix) { +static struct survey_context ctx = { + .opts = { + .verbose =0 , + .show_progress =-1 ,/* defaults to isatty(2) */ + + .refs.want_all_refs =-1 , + + .refs.want_branches =-1 ,/* default these to undefined */ + .refs.want_tags =-1 , + .refs.want_remotes =-1 , + .refs.want_detached =-1 , + .refs.want_other =-1 , + }, + .refs =STRVEC_INIT , + }; + +static struct option survey_options[] = { +OPT__VERBOSE (&ctx.opts.verbose,N_ ("verbose output" )), +OPT_BOOL (0 ,"progress" , &ctx.opts.show_progress,N_ ("show progress" )), + +OPT_BOOL_F (0 ,"all-refs" , &ctx.opts.refs.want_all_refs,N_ ("include all refs" ),PARSE_OPT_NONEG ), + +OPT_BOOL_F (0 ,"branches" , &ctx.opts.refs.want_branches,N_ ("include branches" ),PARSE_OPT_NONEG ), +OPT_BOOL_F (0 ,"tags" , &ctx.opts.refs.want_tags,N_ ("include tags" ),PARSE_OPT_NONEG ), +OPT_BOOL_F (0 ,"remotes" , &ctx.opts.refs.want_remotes,N_ ("include all remotes refs" ),PARSE_OPT_NONEG ), +OPT_BOOL_F (0 ,"detached" , &ctx.opts.refs.want_detached,N_ ("include detached HEAD" ),PARSE_OPT_NONEG ), +OPT_BOOL_F (0 ,"other" , &ctx.opts.refs.want_other,N_ ("include notes and stashes" ),PARSE_OPT_NONEG ), + +OPT_END (), + }; +if (argc ==2 && !strcmp (argv[1 ],"-h" ))usage_with_options (survey_usage, survey_options); -prepare_repo_settings (the_repository); -survey_load_config (); + ctx.repo = the_repository; +prepare_repo_settings (ctx.repo); +survey_load_config (&ctx); argc =parse_options (argc, argv, prefix, survey_options, survey_usage,0 ); -if (survey_opts.show_progress <0 ) - survey_opts.show_progress =isatty (2 ); -fixup_refs_wanted (); +if (ctx.opts.show_progress <0 ) + ctx.opts.show_progress =isatty (2 ); +fixup_refs_wanted (&ctx); + +survey_phase_refs (&ctx); +clear_survey_context (&ctx);return 0 ; }
t/t8100-git-survey.sh +9 -0
@@ -15,4 +15,13 @@ test_expect_success 'git survey -h shows experimental warning' ' grep "EXPERIMENTAL!" usage ' +test_expect_success 'creat a semi-interesting repo' ' + test_commit_bulk 10 +' + +test_expect_success 'git survey (default)' ' + git survey >out 2>err && + test_line_count = 0 err +' + test_done
Signature · 1 line
gitgitgadget
PATCH 12/30 survey: start pretty printing data in table form
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When 'git survey' provides information to the user, this will be presented in one of two formats: plaintext and JSON. The JSON implementation will be delayed until the functionality is complete for the plaintext format.
The most important parts of the plaintext format are headers specifying the different sections of the report and tables providing concreted data.
Create a custom table data structure that allows specifying a list of strings for the row values. When printing the table, check each column for the maximum width so we can create a table of the correct size from the start.
The table structure is designed to be flexible to the different kinds of output that will be implemented in future changes.
--- builtin/survey.c | 175 ++++++++++++++++++++++++++++++++++++++++++ t/t8100-git-survey.sh | 17 +++- 2 files changed, 191 insertions(+), 1 deletion(-)
builtin/survey.c +175 -0
@@ -5,6 +5,7 @@#include "parse-options.h" #include "progress.h" #include "ref-filter.h" +#include "strbuf.h" #include "strvec.h" #include "trace2.h"
@@ -27,10 +28,16 @@ static struct survey_refs_wanted default_ref_options = { .want_all_refs = 1, }; +enum survey_format { +SURVEY_PLAINTEXT =0 , +SURVEY_JSON =1 , +}; +struct survey_opts {int verbose;int show_progress;struct survey_refs_wanted refs; +enum survey_format format; };struct survey_report_ref_summary {
@@ -78,6 +85,161 @@ static void clear_survey_context(struct survey_context *ctx)strvec_clear (&ctx->refs); } +struct survey_table { +const char *table_name; +struct strvec header; +struct strvec *rows; +size_t rows_nr; +size_t rows_alloc; +}; + +#define SURVEY_TABLE_INIT { \ + .header = STRVEC_INIT, \ +} + +static void clear_table (struct survey_table *table) +{ +strvec_clear (&table->header); +for (size_t i =0 ; i < table->rows_nr; i++) +strvec_clear (&table->rows[i]); +free (table->rows); +} + +static void insert_table_rowv (struct survey_table *table, ...) +{ +va_list ap; +char *arg; +ALLOC_GROW (table->rows, table->rows_nr +1 , table->rows_alloc); + +memset (&table->rows[table->rows_nr],0 ,sizeof (struct strvec )); + +va_start (ap, table); + while ((arg = va_arg(ap, char *))) +strvec_push (&table->rows[table->rows_nr], arg); +va_end (ap); + + table->rows_nr++; +} + +static void print_table_title (const char *name,size_t *widths,size_t nr) +{ +static struct strbuf lines =STRBUF_INIT ; +size_t width =0 ; +strbuf_setlen (&lines,0 ); + +strbuf_addch (&lines,' ' ); +strbuf_addstr (&lines, name); +strbuf_addch (&lines,'\n' ); + +for (size_t i =0 ; i < nr; i++) { +if (i) + width +=3 ; + width += widths[i]; + } +strbuf_addchars (&lines,'=' , width); +printf ("%s\n" , lines.buf); +} + +static void print_row_plaintext (struct strvec *row,size_t *widths) +{ +static struct strbuf line =STRBUF_INIT ; +strbuf_setlen (&line,0 ); + +for (size_t i =0 ; i < row->nr; i++) { +const char *str = row->v[i]; +size_t len =strlen (str); +if (i) +strbuf_add (&line," | " ,3 ); +strbuf_addchars (&line,' ' , widths[i] - len); +strbuf_add (&line, str, len); + } +printf ("%s\n" , line.buf); +} + +static void print_divider_plaintext (size_t *widths,size_t nr) +{ +static struct strbuf line =STRBUF_INIT ; +strbuf_setlen (&line,0 ); + +for (size_t i =0 ; i < nr; i++) { +if (i) +strbuf_add (&line,"-+-" ,3 ); +strbuf_addchars (&line,'-' , widths[i]); + } +printf ("%s\n" , line.buf); +} + +static void print_table_plaintext (struct survey_table *table) +{ +size_t *column_widths; +size_t columns_nr = table->header.nr; +CALLOC_ARRAY (column_widths, columns_nr); + +for (size_t i =0 ; i < columns_nr; i++) { + column_widths[i] =strlen (table->header.v[i]); + +for (size_t j =0 ; j < table->rows_nr; j++) { +size_t rowlen =strlen (table->rows[j].v[i]); +if (column_widths[i] < rowlen) + column_widths[i] = rowlen; + } + } + +print_table_title (table->table_name, column_widths, columns_nr); +print_row_plaintext (&table->header, column_widths); +print_divider_plaintext (column_widths, columns_nr); + +for (size_t j =0 ; j < table->rows_nr; j++) +print_row_plaintext (&table->rows[j], column_widths); +} + +static void survey_report_plaintext_refs (struct survey_context *ctx) +{ +struct survey_report_ref_summary *refs = &ctx->report.refs; +struct survey_table table =SURVEY_TABLE_INIT ; + + table.table_name =_ ("REFERENCES SUMMARY" ); + +strvec_push (&table.header,_ ("Ref Type" )); +strvec_push (&table.header,_ ("Count" )); + +if (ctx->opts.refs.want_all_refs || ctx->opts.refs.want_branches) { +char *fmt =xstrfmt ("%" PRIuMAX"" , refs->branches_nr); +insert_table_rowv (&table,_ ("Branches" ), fmt,NULL ); +free (fmt); + } + +if (ctx->opts.refs.want_all_refs || ctx->opts.refs.want_remotes) { +char *fmt =xstrfmt ("%" PRIuMAX"" , refs->remote_refs_nr); +insert_table_rowv (&table,_ ("Remote refs" ), fmt,NULL ); +free (fmt); + } + +if (ctx->opts.refs.want_all_refs || ctx->opts.refs.want_tags) { +char *fmt =xstrfmt ("%" PRIuMAX"" , refs->tags_nr); +insert_table_rowv (&table,_ ("Tags (all)" ), fmt,NULL ); +free (fmt); + fmt =xstrfmt ("%" PRIuMAX"" , refs->tags_annotated_nr); +insert_table_rowv (&table,_ ("Tags (annotated)" ), fmt,NULL ); +free (fmt); + } + +print_table_plaintext (&table); +clear_table (&table); +} + +static void survey_report_plaintext (struct survey_context *ctx) +{ +printf ("GIT SURVEY for \"%s\"\n" , ctx->repo->worktree); +printf ("-----------------------------------------------------\n" ); +survey_report_plaintext_refs (ctx); +} + +static void survey_report_json (struct survey_context *ctx) +{ +/* TODO. */ +} +/* * After parsing the command line arguments, figure out which refs we * should scan.
@@ -312,6 +474,19 @@ int cmd_survey(int argc, const char **argv, const char *prefix)survey_phase_refs (&ctx); +switch (ctx.opts.format) { +case SURVEY_PLAINTEXT : +survey_report_plaintext (&ctx); +break ; + +case SURVEY_JSON : +survey_report_json (&ctx); +break ; + +default : +BUG ("Undefined format" ); + } +clear_survey_context (&ctx);return 0 ; }
t/t8100-git-survey.sh +16 -1
@@ -21,7 +21,22 @@ test_expect_success 'creat a semi-interesting repo' ' test_expect_success 'git survey (default)' ' git survey >out 2>err && - test_line_count = 0 err + test_line_count = 0 err && + + cat >expect <<-EOF && + GIT SURVEY for "$(pwd)" + ----------------------------------------------------- + REFERENCES SUMMARY + ======================== + Ref Type | Count + -----------------+------ + Branches | 1 + Remote refs | 0 + Tags (all) | 0 + Tags (annotated) | 0 + EOF + + test_cmp expect out ' test_done
Signature · 1 line
gitgitgadget
PATCH 13/30 survey: add object count summary
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
At the moment, nothing is obvious about the reason for the use of the path-walk API, but this will become more prevelant in future iterations. For now, use the path-walk API to sum up the counts of each kind of object.
For example, this is the reachable object summary output for my local repo:
REACHABLE OBJECT SUMMARY
========================
Object Type | Count
------------+-------
Tags | 0
Commits | 178573
Trees | 312745
Blobs | 183035(Note: the "Tags" are zero right now because the path-walk API has not been integrated to walk tags yet. This will be fixed in a later change.)
RFC TODO: make sure tags are walked before this change.
--- builtin/survey.c | 196 ++++++++++++++++++++++++++++++++++++++++-- t/t8100-git-survey.sh | 26 ++++-- 2 files changed, 209 insertions(+), 13 deletions(-)
builtin/survey.c +188 -8
@@ -1,12 +1,19 @@#include "builtin.h" #include "config.h" +#include "environment.h" +#include "hex.h" #include "object.h" +#include "object-name.h" #include "object-store-ll.h" #include "parse-options.h" +#include "path-walk.h" #include "progress.h" #include "ref-filter.h" +#include "refs.h" +#include "revision.h" #include "strbuf.h" #include "strvec.h" +#include "tag.h" #include "trace2.h" static const char *const survey_usage[] = {
@@ -50,12 +57,20 @@ struct survey_report_ref_summary {size_t unknown_nr; }; +struct survey_report_object_summary { +size_t commits_nr; +size_t tags_nr; +size_t trees_nr; +size_t blobs_nr; +}; +/** * This struct contains all of the information that needs to be printed * at the end of the exploration of the repository and its references. */ struct survey_report {struct survey_report_ref_summary refs; +struct survey_report_object_summary reachable_objects; };struct survey_context {
@@ -78,10 +93,12 @@ struct survey_context {size_t progress_total;struct strvec refs; +struct ref_array ref_array; };static void clear_survey_context (struct survey_context *ctx) { +ref_array_clear (&ctx->ref_array);strvec_clear (&ctx->refs); }
@@ -125,10 +142,12 @@ static void print_table_title(const char *name, size_t *widths, size_t nr) {static struct strbuf lines =STRBUF_INIT ;size_t width =0 ; +size_t min_width;strbuf_setlen (&lines,0 ); -strbuf_addch (&lines,' ' ); +strbuf_addch (&lines,'\n' );strbuf_addstr (&lines, name); + min_width = lines.len -1 ;strbuf_addch (&lines,'\n' );for (size_t i =0 ; i < nr; i++) {
@@ -136,6 +155,10 @@ static void print_table_title(const char *name, size_t *widths, size_t nr) width +=3 ; width += widths[i]; } + +if (width < min_width) + width = min_width; +strbuf_addchars (&lines,'=' , width);printf ("%s\n" , lines.buf); }
@@ -228,11 +251,43 @@ static void survey_report_plaintext_refs(struct survey_context *ctx)clear_table (&table); } +static void survey_report_plaintext_reachable_object_summary (struct survey_context *ctx) +{ +struct survey_report_object_summary *objs = &ctx->report.reachable_objects; +struct survey_table table =SURVEY_TABLE_INIT ; +char *fmt; + + table.table_name =_ ("REACHABLE OBJECT SUMMARY" ); + +strvec_push (&table.header,_ ("Object Type" )); +strvec_push (&table.header,_ ("Count" )); + + fmt =xstrfmt ("%" PRIuMAX"" , objs->tags_nr); +insert_table_rowv (&table,_ ("Tags" ), fmt,NULL ); +free (fmt); + + fmt =xstrfmt ("%" PRIuMAX"" , objs->commits_nr); +insert_table_rowv (&table,_ ("Commits" ), fmt,NULL ); +free (fmt); + + fmt =xstrfmt ("%" PRIuMAX"" , objs->trees_nr); +insert_table_rowv (&table,_ ("Trees" ), fmt,NULL ); +free (fmt); + + fmt =xstrfmt ("%" PRIuMAX"" , objs->blobs_nr); +insert_table_rowv (&table,_ ("Blobs" ), fmt,NULL ); +free (fmt); + +print_table_plaintext (&table); +clear_table (&table); +} +static void survey_report_plaintext (struct survey_context *ctx) {printf ("GIT SURVEY for \"%s\"\n" , ctx->repo->worktree);printf ("-----------------------------------------------------\n" );survey_report_plaintext_refs (ctx); +survey_report_plaintext_reachable_object_summary (ctx); }static void survey_report_json (struct survey_context *ctx)
@@ -384,15 +439,13 @@ static void do_load_refs(struct survey_context *ctx,*/ static void survey_phase_refs (struct survey_context *ctx) { -struct ref_array ref_array = {0 }; -trace2_region_enter ("survey" ,"phase/refs" , ctx->repo); -do_load_refs (ctx, &ref_array); +do_load_refs (ctx, &ctx->ref_array); - ctx->report.refs.refs_nr = ref_array.nr; -for (size_t i =0 ; i < ref_array.nr; i++) { + ctx->report.refs.refs_nr = ctx->ref_array.nr; +for (size_t i =0 ; i < ctx->ref_array.nr; i++) {size_t size; -struct ref_array_item *item = ref_array.items[i]; +struct ref_array_item *item = ctx->ref_array.items[i];switch (item->kind) {case FILTER_REFS_TAGS :
@@ -422,8 +475,133 @@ static void survey_phase_refs(struct survey_context *ctx) }trace2_region_leave ("survey" ,"phase/refs" , ctx->repo); +} + +static void increment_object_counts ( +struct survey_report_object_summary *summary, +enum object_type type, +size_t nr) +{ +switch (type) { +case OBJ_COMMIT : + summary->commits_nr += nr; +break ; + +case OBJ_TREE : + summary->trees_nr += nr; +break ; + +case OBJ_BLOB : + summary->blobs_nr += nr; +break ; + +default : +break ; + } +} + +static int survey_objects_path_walk_fn (const char *path, +struct oid_array *oids, +enum object_type type, +void *data) +{ +struct survey_context *ctx = data; + +increment_object_counts (&ctx->report.reachable_objects, + type, oids->nr); + +return 0 ; +} + +static int iterate_tag_chain (struct survey_context *ctx, +struct object_id *oid, +struct object_id *peeled) +{ +struct object *o =lookup_unknown_object (ctx->repo, oid); +struct tag *t; + +if (o->type !=OBJ_TAG ) { +oidcpy (peeled, &o->oid); +return o->type !=OBJ_COMMIT ; + } + + t =lookup_tag (ctx->repo, oid); +while (t) { +parse_tag (t); + ctx->report.reachable_objects.tags_nr++; + +if (!t->tagged) +break ; + + o =lookup_unknown_object (ctx->repo, &t->tagged->oid); +if (o && o->type ==OBJ_TAG ) + t =lookup_tag (ctx->repo, &t->tagged->oid); +else +break ; + } + +if (!t || !t->tagged) +return -1 ; -ref_array_clear (&ref_array); +oidcpy (peeled, &t->tagged->oid); + o =lookup_unknown_object (ctx->repo, peeled); +if (o && o->type ==OBJ_COMMIT ) +return 0 ; +return -1 ; +} + +static void survey_phase_objects (struct survey_context *ctx) +{ +struct rev_info revs =REV_INFO_INIT ; +struct path_walk_info info =PATH_WALK_INFO_INIT ; +unsigned int add_flags =0 ; + +trace2_region_enter ("survey" ,"phase/objects" , ctx->repo); + + info.revs = &revs; + info.path_fn = survey_objects_path_walk_fn; + info.path_fn_data = ctx; + + info.commits =1 ; + info.trees =1 ; + info.blobs =1 ; + info.tags =1 ; + +repo_init_revisions (ctx->repo, &revs,"" ); + +for (size_t i =0 ; i < ctx->ref_array.nr; i++) { +struct ref_array_item *item = ctx->ref_array.items[i]; +struct object_id peeled; + +switch (item->kind) { +case FILTER_REFS_TAGS : +if (!iterate_tag_chain (ctx, &item->objectname, &peeled)) +add_pending_oid (&revs,NULL , &peeled, add_flags); +break ; +case FILTER_REFS_BRANCHES : +add_pending_oid (&revs,NULL , &item->objectname, add_flags); +break ; +case FILTER_REFS_REMOTES : +add_pending_oid (&revs,NULL , &item->objectname, add_flags); +break ; +case FILTER_REFS_OTHERS : +/* +* This may be a note, stash, or custom namespace branch. +*/ +add_pending_oid (&revs,NULL , &item->objectname, add_flags); +break ; +case FILTER_REFS_DETACHED_HEAD : +add_pending_oid (&revs,NULL , &item->objectname, add_flags); +break ; +default : +break ; + } + } + +walk_objects_by_path (&info); + +release_revisions (&revs); +trace2_region_leave ("survey" ,"phase/objects" , ctx->repo); }int cmd_survey (int argc,const char **argv,const char *prefix)
@@ -474,6 +652,8 @@ int cmd_survey(int argc, const char **argv, const char *prefix)survey_phase_refs (&ctx); +survey_phase_objects (&ctx); +switch (ctx.opts.format) {case SURVEY_PLAINTEXT :survey_report_plaintext (&ctx);
t/t8100-git-survey.sh +21 -5
@@ -16,24 +16,40 @@ test_expect_success 'git survey -h shows experimental warning' ' ' test_expect_success 'creat a semi-interesting repo' ' - test_commit_bulk 10 + test_commit_bulk 10 && + git tag -a -m one one HEAD~5 && + git tag -a -m two two HEAD~3 && + git tag -a -m three three two && + git tag -a -m four four three && + git update-ref -d refs/tags/three && + git update-ref -d refs/tags/two ' test_expect_success 'git survey (default)' ' - git survey >out 2>err && + git survey --all-refs >out 2>err && test_line_count = 0 err && cat >expect <<-EOF && GIT SURVEY for "$(pwd)" ----------------------------------------------------- - REFERENCES SUMMARY + + REFERENCES SUMMARY ======================== Ref Type | Count -----------------+------ Branches | 1 Remote refs | 0 - Tags (all) | 0 - Tags (annotated) | 0 + Tags (all) | 2 + Tags (annotated) | 2 + + REACHABLE OBJECT SUMMARY + ======================== + Object Type | Count + ------------+------ + Tags | 0 + Commits | 10 + Trees | 10 + Blobs | 10 EOF test_cmp expect out
Signature · 1 line
gitgitgadget
PATCH 14/30 survey: summarize total sizes by object type
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Now that we have explored objects by count, we can expand that a bit more to summarize the data for the on-disk and inflated size of those objects. This information is helpful for diagnosing both why disk space (and perhaps clone or fetch times) is growing but also why certain operations are slow because the inflated size of the abstract objects that must be processed is so large.
--- builtin/survey.c | 113 ++++++++++++++++++++++++++++++++++++++++++ t/t8100-git-survey.sh | 8 +++ 2 files changed, 121 insertions(+)
builtin/survey.c +113 -0
@@ -64,6 +64,19 @@ struct survey_report_object_summary {size_t blobs_nr; }; +/** +* For some category given by 'label', count the number of objects +* that match that label along with the on-disk size and the size +* after decompressing (both with delta bases and zlib). +*/ +struct survey_report_object_size_summary { +char *label; +size_t nr; +size_t disk_size; +size_t inflated_size; +size_t num_missing; +}; +/** * This struct contains all of the information that needs to be printed * at the end of the exploration of the repository and its references.
@@ -71,8 +84,15 @@ struct survey_report_object_summary {struct survey_report {struct survey_report_ref_summary refs;struct survey_report_object_summary reachable_objects; + +struct survey_report_object_size_summary *by_type; }; +#define REPORT_TYPE_COMMIT 0 +#define REPORT_TYPE_TREE 1 +#define REPORT_TYPE_BLOB 2 +#define REPORT_TYPE_COUNT 3 +struct survey_context {/* Options that control what is done. */ struct survey_opts opts;
@@ -282,12 +302,41 @@ static void survey_report_plaintext_reachable_object_summary(struct survey_conteclear_table (&table); } +static void survey_report_object_sizes (const char *title, +const char *categories, +struct survey_report_object_size_summary *summary, +size_t summary_nr) +{ +struct survey_table table =SURVEY_TABLE_INIT ; + table.table_name = title; + +strvec_push (&table.header,xstrdup (categories)); +strvec_push (&table.header,xstrdup (_ ("Count" ))); +strvec_push (&table.header,xstrdup (_ ("Disk Size" ))); +strvec_push (&table.header,xstrdup (_ ("Inflated Size" ))); + +for (size_t i =0 ; i < summary_nr; i++) { +insert_table_rowv (&table,xstrdup (summary[i].label), +xstrfmt ("%" PRIuMAX, summary[i].nr), +xstrfmt ("%" PRIuMAX, summary[i].disk_size), +xstrfmt ("%" PRIuMAX, summary[i].inflated_size), +NULL ); + } + +print_table_plaintext (&table); +clear_table (&table); +} +static void survey_report_plaintext (struct survey_context *ctx) {printf ("GIT SURVEY for \"%s\"\n" , ctx->repo->worktree);printf ("-----------------------------------------------------\n" );survey_report_plaintext_refs (ctx);survey_report_plaintext_reachable_object_summary (ctx); +survey_report_object_sizes (_ ("TOTAL OBJECT SIZES BY TYPE" ), +_ ("Object Type" ), + ctx->report.by_type, +REPORT_TYPE_COUNT ); }static void survey_report_json (struct survey_context *ctx)
@@ -500,6 +549,64 @@ static void increment_object_counts( } } +static void increment_totals (struct survey_context *ctx, +struct oid_array *oids, +struct survey_report_object_size_summary *summary) +{ +for (size_t i =0 ; i < oids->nr; i++) { +struct object_info oi =OBJECT_INFO_INIT ; +unsigned oi_flags =OBJECT_INFO_FOR_PREFETCH ; +unsigned long object_length =0 ; +off_t disk_sizep =0 ; +enum object_type type; + + oi.typep = &type; + oi.sizep = &object_length; + oi.disk_sizep = &disk_sizep; + +if (oid_object_info_extended (ctx->repo, &oids->oid[i], + &oi, oi_flags) <0 ) { + summary->num_missing++; + }else { + summary->nr++; + summary->disk_size += disk_sizep; + summary->inflated_size += object_length; + } + } +} + +static void increment_object_totals (struct survey_context *ctx, +struct oid_array *oids, +enum object_type type) +{ +struct survey_report_object_size_summary *total; +struct survey_report_object_size_summary summary = {0 }; + +increment_totals (ctx, oids, &summary); + +switch (type) { +case OBJ_COMMIT : + total = &ctx->report.by_type[REPORT_TYPE_COMMIT ]; +break ; + +case OBJ_TREE : + total = &ctx->report.by_type[REPORT_TYPE_TREE ]; +break ; + +case OBJ_BLOB : + total = &ctx->report.by_type[REPORT_TYPE_BLOB ]; +break ; + +default : +BUG ("No other type allowed" ); + } + + total->nr += summary.nr; + total->disk_size += summary.disk_size; + total->inflated_size += summary.inflated_size; + total->num_missing += summary.num_missing; +} + static int survey_objects_path_walk_fn(const char *path, struct oid_array *oids, enum object_type type,
@@ -509,6 +616,7 @@ static int survey_objects_path_walk_fn(const char *path,increment_object_counts (&ctx->report.reachable_objects, type, oids->nr); +increment_object_totals (ctx, oids, type);return 0 ; }
@@ -567,6 +675,11 @@ static void survey_phase_objects(struct survey_context *ctx) info.blobs =1 ; info.tags =1 ; +CALLOC_ARRAY (ctx->report.by_type,REPORT_TYPE_COUNT ); + ctx->report.by_type[REPORT_TYPE_COMMIT ].label =xstrdup (_ ("Commits" )); + ctx->report.by_type[REPORT_TYPE_TREE ].label =xstrdup (_ ("Trees" )); + ctx->report.by_type[REPORT_TYPE_BLOB ].label =xstrdup (_ ("Blobs" )); +repo_init_revisions (ctx->repo, &revs,"" );for (size_t i =0 ; i < ctx->ref_array.nr; i++) {
t/t8100-git-survey.sh +8 -0
@@ -50,6 +50,14 @@ test_expect_success 'git survey (default)' ' Commits | 10 Trees | 10 Blobs | 10 + + TOTAL OBJECT SIZES BY TYPE + =============================================== + Object Type | Count | Disk Size | Inflated Size + ------------+-------+-----------+-------------- + Commits | 10 | 1523 | 2153 + Trees | 10 | 495 | 1706 + Blobs | 10 | 191 | 101 EOF test_cmp expect out
Signature · 1 line
gitgitgadget
PATCH 15/30 survey: show progress during object walk
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
--- builtin/survey.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+)
builtin/survey.c +16 -0
@@ -618,6 +618,9 @@ static int survey_objects_path_walk_fn(const char *path, type, oids->nr);increment_object_totals (ctx, oids, type); + ctx->progress_nr += oids->nr; +display_progress (ctx->progress, ctx->progress_nr); +return 0 ; }
@@ -682,6 +685,11 @@ static void survey_phase_objects(struct survey_context *ctx)repo_init_revisions (ctx->repo, &revs,"" ); + ctx->progress_nr =0 ; + ctx->progress_total = ctx->ref_array.nr; +if (ctx->opts.show_progress) + ctx->progress =start_progress (_ ("Preparing object walk" ), + ctx->progress_total);for (size_t i =0 ; i < ctx->ref_array.nr; i++) {struct ref_array_item *item = ctx->ref_array.items[i];struct object_id peeled;
@@ -709,9 +717,17 @@ static void survey_phase_objects(struct survey_context *ctx)default :break ; } + +display_progress (ctx->progress, ++(ctx->progress_nr)); } +stop_progress (&ctx->progress); + ctx->progress_nr =0 ; + ctx->progress_total =0 ; +if (ctx->opts.show_progress) + ctx->progress =start_progress (_ ("Walking objects" ),0 );walk_objects_by_path (&info); +stop_progress (&ctx->progress);release_revisions (&revs);trace2_region_leave ("survey" ,"phase/objects" , ctx->repo);
Signature · 1 line
gitgitgadget
PATCH 16/30 survey: add ability to track prioritized lists
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
In future changes, we will make use of these methods. The intention is to keep track of the top contributors according to some metric. We don't want to store all of the entries and do a sort at the end, so track a constant-size table and remove rows that get pushed out depending on the chosen sorting algorithm.
Signed-off-by; Jeff Hostetler <[email protected]>
--- builtin/survey.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+)
builtin/survey.c +96 -0
@@ -77,6 +77,102 @@ struct survey_report_object_size_summary {size_t num_missing; }; +typedef int (*survey_top_size_cmp )(struct survey_report_object_size_summary *s1, +struct survey_report_object_size_summary *s2); + +MAYBE_UNUSED +static int cmp_by_nr(struct survey_report_object_size_summary *s1, +struct survey_report_object_size_summary *s2) +{ +if (s1->nr < s2->nr) +return -1 ; +if (s1->nr > s2->nr) +return 1 ; +return 0 ; +} + +MAYBE_UNUSED +static int cmp_by_disk_size(struct survey_report_object_size_summary *s1, +struct survey_report_object_size_summary *s2) +{ +if (s1->disk_size < s2->disk_size) +return -1 ; +if (s1->disk_size > s2->disk_size) +return 1 ; +return 0 ; +} + +MAYBE_UNUSED +static int cmp_by_inflated_size(struct survey_report_object_size_summary *s1, +struct survey_report_object_size_summary *s2) +{ +if (s1->inflated_size < s2->inflated_size) +return -1 ; +if (s1->inflated_size > s2->inflated_size) +return 1 ; +return 0 ; +} + +/** +* Store a list of "top" categories by some sorting function. When +* inserting a new category, reorder the list and free the one that +* got ejected (if any). +*/ +struct survey_report_top_sizes { +const char *name; +survey_top_size_cmp cmp_fn; +struct survey_report_object_size_summary *data; +size_t nr; +size_t alloc; +}; + +MAYBE_UNUSED +static void init_top_sizes(struct survey_report_top_sizes *top, +size_t limit,const char *name, +survey_top_size_cmp cmp) +{ + top->name = name; + top->alloc = limit; + top->nr =0 ; +CALLOC_ARRAY (top->data, limit); + top->cmp_fn = cmp; +} + +MAYBE_UNUSED +static void clear_top_sizes(struct survey_report_top_sizes *top) +{ +for (size_t i =0 ; i < top->nr; i++) +free (top->data[i].label); +free (top->data); +} + +MAYBE_UNUSED +static void maybe_insert_into_top_size(struct survey_report_top_sizes *top, +struct survey_report_object_size_summary *summary) +{ +size_t pos = top->nr; + +/* Compare against list from the bottom. */ +while (pos >0 && top->cmp_fn (&top->data[pos -1 ], summary) <0 ) + pos--; + +/* Not big enough! */ +if (pos >= top->alloc) +return ; + +/* We need to shift the data. */ +if (top->nr == top->alloc) +free (top->data[top->nr -1 ].label); +else + top->nr++; + +for (size_t i = top->nr -1 ; i > pos; i--) +memcpy (&top->data[i], &top->data[i -1 ],sizeof (*top->data)); + +memcpy (&top->data[pos], summary,sizeof (*summary)); + top->data[pos].label =xstrdup (summary->label); +} +/** * This struct contains all of the information that needs to be printed * at the end of the exploration of the repository and its references.
Signature · 1 line
gitgitgadget
PATCH 17/30 survey: add report of "largest" paths
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since we are already walking our reachable objects using the path-walk API, let's now collect lists of the paths that contribute most to different metrics. Specifically, we care about
* Number of versions.
* Total size on disk.
* Total inflated size (no delta or zlib compression).
This information can be critical to discovering which parts of the repository are causing the most growth, especially on-disk size. Different packing strategies might help compress data more efficiently, but the toal inflated size is a representation of the raw size of all snapshots of those paths. Even when stored efficiently on disk, that size represents how much information must be processed to complete a command such as 'git blame'.
Since the on-disk size is likely to be fragile, stop testing the exact output of 'git survey' and check that the correct set of headers is output.
--- builtin/survey.c | 90 +++++++++++++++++++++++++++++++++++++------ t/t8100-git-survey.sh | 12 +++++- 2 files changed, 90 insertions(+), 12 deletions(-)
builtin/survey.c +79 -11
@@ -80,7 +80,6 @@ struct survey_report_object_size_summary {typedef int (*survey_top_size_cmp )(struct survey_report_object_size_summary *s1,struct survey_report_object_size_summary *s2); -MAYBE_UNUSED static int cmp_by_nr (struct survey_report_object_size_summary *s1,struct survey_report_object_size_summary *s2) {
@@ -91,7 +90,6 @@ static int cmp_by_nr(struct survey_report_object_size_summary *s1,return 0 ; } -MAYBE_UNUSED static int cmp_by_disk_size (struct survey_report_object_size_summary *s1,struct survey_report_object_size_summary *s2) {
@@ -102,7 +100,6 @@ static int cmp_by_disk_size(struct survey_report_object_size_summary *s1,return 0 ; } -MAYBE_UNUSED static int cmp_by_inflated_size (struct survey_report_object_size_summary *s1,struct survey_report_object_size_summary *s2) {
@@ -126,7 +123,6 @@ struct survey_report_top_sizes {size_t alloc; }; -MAYBE_UNUSED static void init_top_sizes (struct survey_report_top_sizes *top,size_t limit,const char *name,survey_top_size_cmp cmp)
@@ -146,7 +142,6 @@ static void clear_top_sizes(struct survey_report_top_sizes *top)free (top->data); } -MAYBE_UNUSED static void maybe_insert_into_top_size (struct survey_report_top_sizes *top,struct survey_report_object_size_summary *summary) {
@@ -182,6 +177,10 @@ struct survey_report {struct survey_report_object_summary reachable_objects;struct survey_report_object_size_summary *by_type; + +struct survey_report_top_sizes *top_paths_by_count; +struct survey_report_top_sizes *top_paths_by_disk; +struct survey_report_top_sizes *top_paths_by_inflate; };#define REPORT_TYPE_COMMIT 0
@@ -423,6 +422,13 @@ static void survey_report_object_sizes(const char *title,clear_table (&table); } +static void survey_report_plaintext_sorted_size ( +struct survey_report_top_sizes *top) +{ +survey_report_object_sizes (top->name,_ ("Path" ), + top->data, top->nr); +} +static void survey_report_plaintext (struct survey_context *ctx) {printf ("GIT SURVEY for \"%s\"\n" , ctx->repo->worktree);
@@ -433,6 +439,21 @@ static void survey_report_plaintext(struct survey_context *ctx)_ ("Object Type" ), ctx->report.by_type, REPORT_TYPE_COUNT); + +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_count[REPORT_TYPE_TREE ]); +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_count[REPORT_TYPE_BLOB ]); + +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_disk[REPORT_TYPE_TREE ]); +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_disk[REPORT_TYPE_BLOB ]); + +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_inflate[REPORT_TYPE_TREE ]); +survey_report_plaintext_sorted_size ( + &ctx->report.top_paths_by_inflate[REPORT_TYPE_BLOB ]); }static void survey_report_json (struct survey_context *ctx)
@@ -673,7 +694,8 @@ static void increment_totals(struct survey_context *ctx,static void increment_object_totals (struct survey_context *ctx,struct oid_array *oids, -enum object_type type) +enum object_type type, +const char *path) {struct survey_report_object_size_summary *total;struct survey_report_object_size_summary summary = {0 };
@@ -701,6 +723,27 @@ static void increment_object_totals(struct survey_context *ctx, total->disk_size += summary.disk_size; total->inflated_size += summary.inflated_size; total->num_missing += summary.num_missing; + +if (type ==OBJ_TREE || type ==OBJ_BLOB ) { +int index = type ==OBJ_TREE ? +REPORT_TYPE_TREE :REPORT_TYPE_BLOB ; +struct survey_report_top_sizes *top; + +/* +* Temporarily store (const char *) here, but it will +* be duped if inserted and will not be freed. +*/ + summary.label = (char *)path; + + top = ctx->report.top_paths_by_count; +maybe_insert_into_top_size (&top[index], &summary); + + top = ctx->report.top_paths_by_disk; +maybe_insert_into_top_size (&top[index], &summary); + + top = ctx->report.top_paths_by_inflate; +maybe_insert_into_top_size (&top[index], &summary); + } } static int survey_objects_path_walk_fn(const char *path,
@@ -712,7 +755,7 @@ static int survey_objects_path_walk_fn(const char *path,increment_object_counts (&ctx->report.reachable_objects, type, oids->nr); -increment_object_totals (ctx, oids, type); +increment_object_totals (ctx, oids, type, path); ctx->progress_nr += oids->nr;display_progress (ctx->progress, ctx->progress_nr);
@@ -757,6 +800,34 @@ static int iterate_tag_chain(struct survey_context *ctx,return -1 ; } +static void initialize_report (struct survey_context *ctx) +{ +const int top_limit =100 ; + +CALLOC_ARRAY (ctx->report.by_type,REPORT_TYPE_COUNT ); + ctx->report.by_type[REPORT_TYPE_COMMIT ].label =xstrdup (_ ("Commits" )); + ctx->report.by_type[REPORT_TYPE_TREE ].label =xstrdup (_ ("Trees" )); + ctx->report.by_type[REPORT_TYPE_BLOB ].label =xstrdup (_ ("Blobs" )); + +CALLOC_ARRAY (ctx->report.top_paths_by_count,REPORT_TYPE_COUNT ); +init_top_sizes (&ctx->report.top_paths_by_count[REPORT_TYPE_TREE ], + top_limit,_ ("TOP DIRECTORIES BY COUNT" ), cmp_by_nr); +init_top_sizes (&ctx->report.top_paths_by_count[REPORT_TYPE_BLOB ], + top_limit,_ ("TOP FILES BY COUNT" ), cmp_by_nr); + +CALLOC_ARRAY (ctx->report.top_paths_by_disk,REPORT_TYPE_COUNT ); +init_top_sizes (&ctx->report.top_paths_by_disk[REPORT_TYPE_TREE ], + top_limit,_ ("TOP DIRECTORIES BY DISK SIZE" ), cmp_by_disk_size); +init_top_sizes (&ctx->report.top_paths_by_disk[REPORT_TYPE_BLOB ], + top_limit,_ ("TOP FILES BY DISK SIZE" ), cmp_by_disk_size); + +CALLOC_ARRAY (ctx->report.top_paths_by_inflate,REPORT_TYPE_COUNT ); +init_top_sizes (&ctx->report.top_paths_by_inflate[REPORT_TYPE_TREE ], + top_limit,_ ("TOP DIRECTORIES BY INFLATED SIZE" ), cmp_by_inflated_size); +init_top_sizes (&ctx->report.top_paths_by_inflate[REPORT_TYPE_BLOB ], + top_limit,_ ("TOP FILES BY INFLATED SIZE" ), cmp_by_inflated_size); +} +static void survey_phase_objects (struct survey_context *ctx) {struct rev_info revs =REV_INFO_INIT ;
@@ -774,10 +845,7 @@ static void survey_phase_objects(struct survey_context *ctx) info.blobs =1 ; info.tags =1 ; -CALLOC_ARRAY (ctx->report.by_type,REPORT_TYPE_COUNT ); - ctx->report.by_type[REPORT_TYPE_COMMIT ].label =xstrdup (_ ("Commits" )); - ctx->report.by_type[REPORT_TYPE_TREE ].label =xstrdup (_ ("Trees" )); - ctx->report.by_type[REPORT_TYPE_BLOB ].label =xstrdup (_ ("Blobs" )); +initialize_report (ctx);repo_init_revisions (ctx->repo, &revs,"" );
t/t8100-git-survey.sh +11 -1
@@ -60,7 +60,17 @@ test_expect_success 'git survey (default)' ' Blobs | 10 | 191 | 101 EOF - test_cmp expect out + lines=$(wc -l <expect) && + head -n $lines out >out-trimmed && + test_cmp expect out-trimmed && + + for type in "DIRECTORIES" "FILES" + do + for metric in "COUNT" "DISK SIZE" "INFLATED SIZE" + do + grep "TOP $type BY $metric" out || return 1 + done || return 1 + done ' test_done
Signature · 1 line
gitgitgadget
PATCH 18/30 revision: create mark_trees_uninteresting_dense()
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The sparse tree walk algorithm was created in d5d2e93577e (revision:
implement sparse algorithm, 2019-01-16) and involves using the mark_trees_uninteresting_sparse() method. This method takes a repository and an oidset of tree IDs, some of which have the UNINTERESTING flag and some of which do not.
Create a method that has an equivalent set of preconditions but uses a "dense" walk (recursively visits all reachable trees, as long as they have not previously been marked UNINTERESTING). This is an important difference from mark_tree_uninteresting(), which short-circuits if the given tree has the UNINTERESTING flag.
A use of this method will be added in a later change, with a condition set whether the sparse or dense approach should be used.
--- revision.c | 15 +++++++++++++++ revision.h | 1 + 2 files changed, 16 insertions(+)
revision.c +15 -0
@@ -219,6 +219,21 @@ static void add_children_by_path(struct repository *r,free_tree_buffer (tree); } +void mark_trees_uninteresting_dense (struct repository *r, +struct oidset *trees) +{ +struct object_id *oid; +struct oidset_iter iter; + +oidset_iter_init (trees, &iter); +while ((oid =oidset_iter_next (&iter))) { +struct tree *tree =lookup_tree (r, oid); + +if (tree->object.flags &UNINTERESTING ) +mark_tree_contents_uninteresting (r, tree); + } +} +void mark_trees_uninteresting_sparse (struct repository *r,struct oidset *trees) {
revision.h +1 -0
@@ -487,6 +487,7 @@ void put_revision_mark(const struct rev_info *revs,void mark_parents_uninteresting (struct rev_info *revs,struct commit *commit);void mark_tree_uninteresting (struct repository *r,struct tree *tree); +void mark_trees_uninteresting_dense (struct repository *r,struct oidset *trees);void mark_trees_uninteresting_sparse (struct repository *r,struct oidset *trees);void show_object_with_name (FILE *,struct object *,const char *);
Signature · 1 line
gitgitgadget
PATCH 19/30 path-walk: add prune_all_uninteresting option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
This option causes the path-walk API to act like the sparse tree-walk algorithm implemented by mark_trees_uninteresting_sparse() in list-objects.c.
Starting from the commits marked as UNINTERESTING, their root trees and all objects reachable from those trees are UNINTERSTING, at least as we walk path-by-path. When we reach a path where all objects associated with that path are marked UNINTERESTING, then do no continue walking the children of that path.
We need to be careful to pass the UNINTERESTING flag in a deep way on the UNINTERESTING objects before we start the path-walk, or else the depth-first search for the path-walk API may accidentally report some objects as interesting.
--- path-walk.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++--- path-walk.h | 8 +++++++ 2 files changed, 73 insertions(+), 3 deletions(-)
path-walk.c +65 -3
@@ -23,6 +23,7 @@ struct type_and_oid_list {enum object_type type;struct oid_array oids; +int maybe_interesting; };#define TYPE_AND_OID_LIST_INIT { \
@@ -139,6 +140,9 @@ static int add_children(struct path_walk_context *ctx, list->type = type;strmap_put (&ctx->paths_to_lists, path.buf, list);string_list_append (&ctx->path_stack, path.buf); + +if (!(o->flags &UNINTERESTING )) + list->maybe_interesting =1 ; }oid_array_append (&list->oids, &entry.oid); }
@@ -161,6 +165,40 @@ static int walk_path(struct path_walk_context *ctx, list =strmap_get (&ctx->paths_to_lists, path); +if (ctx->info->prune_all_uninteresting) { +/* +* This is true if all objects were UNINTERESTING +* when added to the list. +*/ +if (!list->maybe_interesting) +return 0 ; + +/* +* But it's still possible that the objects were set +* as UNINTERESTING after being added. Do a quick check. +*/ + list->maybe_interesting =0 ; +for (size_t i =0 ; + !list->maybe_interesting && i < list->oids.nr; + i++) { +if (list->type ==OBJ_TREE ) { +struct tree *t =lookup_tree (ctx->repo, + &list->oids.oid[i]); +if (t && !(t->object.flags &UNINTERESTING )) + list->maybe_interesting =1 ; + }else { +struct blob *b =lookup_blob (ctx->repo, + &list->oids.oid[i]); +if (b && !(b->object.flags &UNINTERESTING )) + list->maybe_interesting =1 ; + } + } + +/* We have confirmed that all objects are UNINTERESTING. */ +if (!list->maybe_interesting) +return 0 ; + } +/* Evaluate function pointer on this data, if requested. */ if ((list->type ==OBJ_TREE && ctx->info->trees) || (list->type ==OBJ_BLOB && ctx->info->blobs))
@@ -203,7 +241,7 @@ static void clear_strmap(struct strmap *map)int walk_objects_by_path (struct path_walk_info *info) {const char *root_path ="" ; -int ret =0 ; +int ret =0 , has_uninteresting =0 ;size_t commits_nr =0 , paths_nr =0 ;struct commit *c;struct type_and_oid_list *root_tree_list;
@@ -215,6 +253,7 @@ int walk_objects_by_path(struct path_walk_info *info) .path_stack = STRING_LIST_INIT_DUP, .paths_to_lists = STRMAP_INIT }; +struct oidset root_tree_set =OIDSET_INIT ;struct oid_array tagged_tree_list =OID_ARRAY_INIT ;struct oid_array tagged_blob_list =OID_ARRAY_INIT ;
@@ -227,7 +266,9 @@ int walk_objects_by_path(struct path_walk_info *info)/* Insert a single list for the root tree into the paths. */ CALLOC_ARRAY (root_tree_list,1 ); root_tree_list->type =OBJ_TREE ; + root_tree_list->maybe_interesting =1 ;strmap_put (&ctx.paths_to_lists, root_path, root_tree_list); +if (prepare_revision_walk (info->revs))die (_ ("failed to setup revision walk" ));
@@ -247,11 +288,17 @@ int walk_objects_by_path(struct path_walk_info *info) oid =get_commit_tree_oid (c); t =lookup_tree (info->revs->repo, oid); -if (t) +if (t) { +oidset_insert (&root_tree_set, oid);oid_array_append (&root_tree_list->oids, oid); -else + }else {warning ("could not find tree %s" ,oid_to_hex (oid)); + } +if (t && (c->object.flags &UNINTERESTING )) { + t->object.flags |=UNINTERESTING ; + has_uninteresting =1 ; + } }trace2_data_intmax ("path-walk" , ctx.repo,"commits" , commits_nr);
@@ -318,6 +365,21 @@ int walk_objects_by_path(struct path_walk_info *info)oid_array_clear (&tagged_blob_list); } +/* +* Before performing a DFS of our paths and emitting them as interesting, +* do a full walk of the trees to distribute the UNINTERESTING bit. Use +* the sparse algorithm if prune_all_uninteresting was set. +*/ +if (has_uninteresting) { +trace2_region_enter ("path-walk" ,"uninteresting-walk" , info->revs->repo); +if (info->prune_all_uninteresting) +mark_trees_uninteresting_sparse (ctx.repo, &root_tree_set); +else +mark_trees_uninteresting_dense (ctx.repo, &root_tree_set); +trace2_region_leave ("path-walk" ,"uninteresting-walk" , info->revs->repo); + } +oidset_clear (&root_tree_set); +string_list_append (&ctx.path_stack, root_path);trace2_region_enter ("path-walk" ,"path-walk" , info->revs->repo);
path-walk.h +8 -0
@@ -50,6 +50,14 @@ struct path_walk_info {* the sparse-checkout patterns. */ struct pattern_list *pl; + +/** +* When 'prune_all_uninteresting' is set and a path has all objects +* marked as UNINTERESTING, then the path-walk will not visit those +* objects. It will not call path_fn on those objects and will not +* walk the children of such trees. +*/ +int prune_all_uninteresting; };#define PATH_WALK_INFO_INIT { \
Signature · 1 line
gitgitgadget
PATCH 20/30 pack-objects: add --path-walk option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
In order to more easily compute delta bases among objects that appear at the exact same path, add a --path-walk option to 'git pack-objects'.
This option will use the path-walk API instead of the object walk given by the revision machinery. Since objects will be provided in batches representing a common path, those objects can be tested for delta bases immediately instead of waiting for a sort of the full object list by name-hash. This has multiple benefits, including avoiding collisions by name-hash.
The objects marked as UNINTERESTING are included in these batches, so we are guaranteeing some locality to find good delta bases.
After the individual passes are done on a per-path basis, the default name-hash is used to find other opportunistic delta bases that did not match exactly by the full path name.
RFC TODO: It is important to note that this option is inherently incompatible with using a bitmap index. This walk probably also does not work with other advanced features, such as delta islands.
Getting ahead of myself, this option compares well with --full-name-hash when the packfile is large enough, but also performs at least as well as the default in all cases that I've seen.
RFC TODO: this should probably be recording the batch locations to another list so they could be processed in a second phase using threads.
RFC TODO: list some examples of how this outperforms previous pack-objects strategies. (This is coming in later commits that include performance test changes.)
--- builtin/pack-objects.c | 209 ++++++++++++++++++++++++++++++++++------- path-walk.c | 2 +- 2 files changed, 177 insertions(+), 34 deletions(-)
builtin/pack-objects.c +176 -33
@@ -39,6 +39,9 @@#include "promisor-remote.h" #include "pack-mtimes.h" #include "parse-options.h" +#include "blob.h" +#include "tree.h" +#include "path-walk.h" /* * Objects we are going to pack are collected in the `to_pack` structure.
@@ -215,6 +218,7 @@ static int delta_search_threads;static int pack_to_stdout;static int sparse;static int thin; +static int path_walk;static int num_preferred_base;static struct progress *progress_state;
@@ -3139,6 +3143,38 @@ static int add_ref_tag(const char *tag UNUSED, const char *referent UNUSED, consreturn 0 ; } +static int should_attempt_deltas (struct object_entry *entry) +{ +if (DELTA (entry)) +/* This happens if we decided to reuse existing +* delta from a pack. "reuse_delta &&" is implied. +*/ +return 0 ; + +if (!entry->type_valid || +oe_size_less_than (&to_pack, entry,50 )) +return 0 ; + +if (entry->no_try_delta) +return 0 ; + +if (!entry->preferred_base) { +if (oe_type (entry) <0 ) +die (_ ("unable to get type of object %s" ), +oid_to_hex (&entry->idx.oid)); + }else { +if (oe_type (entry) <0 ) { +/* +* This object is not found, but we +* don't have to include it anyway. +*/ +return 0 ; + } + } + +return 1 ; +} +static void prepare_pack (int window,int depth) {struct object_entry **delta_list;
@@ -3169,33 +3205,11 @@ static void prepare_pack(int window, int depth)for (i =0 ; i < to_pack.nr_objects; i++) {struct object_entry *entry = to_pack.objects + i; -if (DELTA (entry)) -/* This happens if we decided to reuse existing -* delta from a pack. "reuse_delta &&" is implied. -*/ +if (!should_attempt_deltas (entry))continue ; -if (!entry->type_valid || -oe_size_less_than (&to_pack, entry,50 )) -continue ; - -if (entry->no_try_delta) -continue ; - -if (!entry->preferred_base) { +if (!entry->preferred_base) nr_deltas++; -if (oe_type (entry) <0 ) -die (_ ("unable to get type of object %s" ), -oid_to_hex (&entry->idx.oid)); - }else { -if (oe_type (entry) <0 ) { -/* -* This object is not found, but we -* don't have to include it anyway. -*/ -continue ; - } - } delta_list[n++] = entry; }
@@ -4110,6 +4124,117 @@ static void mark_bitmap_preferred_tips(void) } } +static inline int is_oid_interesting (struct repository *repo, +struct object_id *oid, +enum object_type type) +{ +if (type ==OBJ_TAG ) { +struct tag *t =lookup_tag (repo, oid); +return t && !(t->object.flags &UNINTERESTING ); + } + +if (type ==OBJ_COMMIT ) { +struct commit *c =lookup_commit (repo, oid); +return c && !(c->object.flags &UNINTERESTING ); + } + +if (type ==OBJ_TREE ) { +struct tree *t =lookup_tree (repo, oid); +return t && !(t->object.flags &UNINTERESTING ); + } + +if (type ==OBJ_BLOB ) { +struct blob *b =lookup_blob (repo, oid); +return b && !(b->object.flags &UNINTERESTING ); + } + +return 0 ; +} + +static int add_objects_by_path (const char *path, +struct oid_array *oids, +enum object_type type, +void *data) +{ +struct object_entry **delta_list; +size_t oe_start = to_pack.nr_objects; +size_t oe_end; +unsigned int sub_list_size; +unsigned int *processed = data; + +/* +* First, add all objects to the packing data, including the ones +* marked UNINTERESTING (translated to 'exclude') as they can be +* used as delta bases. +*/ +for (size_t i =0 ; i < oids->nr; i++) { +struct object_id *oid = &oids->oid[i]; +int exclude = !is_oid_interesting (the_repository, oid, type); +add_object_entry (oid, type, path, exclude); + } + + oe_end = to_pack.nr_objects; + +/* We can skip delta calculations if it is a no-op. */ +if (oe_end == oe_start || !window) +return 0 ; + + sub_list_size =0 ; +ALLOC_ARRAY (delta_list, oe_end - oe_start); + +for (size_t i =0 ; i < oe_end - oe_start; i++) { +struct object_entry *entry = to_pack.objects + oe_start + i; + +if (!should_attempt_deltas (entry)) +continue ; + + delta_list[sub_list_size++] = entry; + } + +/* +* Find delta bases among this list of objects that all match the same +* path. This causes the delta compression to be interleaved in the +* object walk, which can lead to confusing progress indicators. This is +* also incompatible with threaded delta calculations. In the future, +* consider creating a list of regions in the full to_pack.objects array +* that could be picked up by the threaded delta computation. +*/ +if (sub_list_size && window) { +QSORT (delta_list, sub_list_size, type_size_sort); +find_deltas (delta_list, &sub_list_size, window, depth, processed); + } + +free (delta_list); +return 0 ; +} + +static void get_object_list_path_walk (struct rev_info *revs) +{ +struct path_walk_info info =PATH_WALK_INFO_INIT ; +unsigned int processed =0 ; + + info.revs = revs; + + info.revs->tag_objects =1 ; + info.tags =1 ; + info.commits =1 ; + info.trees =1 ; + info.blobs =1 ; + info.path_fn = add_objects_by_path; + info.path_fn_data = &processed; + +/* +* Allow the --[no-]sparse option to be interesting here, if only +* for testing purposes. Paths with no interesting objects will not +* contribute to the resulting pack, but only create noisy preferred +* base objects. +*/ + info.prune_all_uninteresting = sparse; + +if (walk_objects_by_path (&info)) +die (_ ("failed to pack objects via path-walk" )); +} +static void get_object_list (struct rev_info *revs,int ac,const char **av) {struct setup_revision_opt s_r_opt = {
@@ -4156,7 +4281,7 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av) warn_on_object_refname_ambiguity = save_warning; -if (use_bitmap_index && !get_object_list_from_bitmap (revs)) +if (use_bitmap_index && !path_walk && !get_object_list_from_bitmap (revs))return ;if (use_delta_islands)
@@ -4165,15 +4290,19 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)if (write_bitmap_index)mark_bitmap_preferred_tips (); -if (prepare_revision_walk (revs)) -die (_ ("revision walk setup failed" )); -mark_edges_uninteresting (revs, show_edge, sparse); -if (!fn_show_object) fn_show_object = show_object; -traverse_commit_list (revs, - show_commit, fn_show_object, -NULL ); + +if (path_walk) { +get_object_list_path_walk (revs); + }else { +if (prepare_revision_walk (revs)) +die (_ ("revision walk setup failed" )); +mark_edges_uninteresting (revs, show_edge, sparse); +traverse_commit_list (revs, + show_commit, fn_show_object, +NULL ); + }if (unpack_unreachable_expiration) { revs->ignore_missing_links =1 ;
@@ -4368,6 +4497,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) N_("use the sparse reachability algorithm")),OPT_BOOL (0 ,"thin" , &thin,N_ ("create thin packs" )), +OPT_BOOL (0 ,"path-walk" , &path_walk, +N_ ("use the path-walk API to walk objects when possible" )),OPT_BOOL (0 ,"shallow" , &shallow,N_ ("create packs suitable for shallow fetches" )), OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep_on_disk,
@@ -4448,7 +4579,19 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) window =0 ;strvec_push (&rp,"pack-objects" ); -if (thin) { + +if (path_walk && filter_options.choice) { +warning (_ ("cannot use --filter with --path-walk" )); + path_walk =0 ; + } +if (path_walk) { +strvec_push (&rp,"--boundary" ); +/* +* We must disable the bitmaps because we are removing +* the --objects / --objects-edge[-aggressive] options. +*/ + use_bitmap_index =0 ; + }else if (thin) { use_internal_rev_list =1 ; strvec_push(&rp, shallow ? "--objects-edge-aggressive"
path-walk.c +1 -1
@@ -306,7 +306,7 @@ int walk_objects_by_path(struct path_walk_info *info)/* Track all commits. */ if (info->commits) - ret = info->path_fn ("" , &commit_list->oids,OBJ_COMMIT , + ret = info->path_fn ("initial" , &commit_list->oids,OBJ_COMMIT , info->path_fn_data);oid_array_clear (&commit_list->oids);free (commit_list);
Signature · 1 line
gitgitgadget
PATCH 21/30 pack-objects: extract should_attempt_deltas()
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
--- builtin/pack-objects.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-)
builtin/pack-objects.c +7 -10
@@ -3151,8 +3151,7 @@ static int should_attempt_deltas(struct object_entry *entry)*/ return 0 ; -if (!entry->type_valid || -oe_size_less_than (&to_pack, entry,50 )) +if (!entry->type_valid ||oe_size_less_than (&to_pack, entry,50 ))return 0 ;if (entry->no_try_delta)
@@ -3162,14 +3161,12 @@ static int should_attempt_deltas(struct object_entry *entry)if (oe_type (entry) <0 )die (_ ("unable to get type of object %s" ),oid_to_hex (&entry->idx.oid)); - }else { -if (oe_type (entry) <0 ) { -/* -* This object is not found, but we -* don't have to include it anyway. -*/ -return 0 ; - } + }else if (oe_type (entry) <0 ) { +/* +* This object is not found, but we +* don't have to include it anyway. +*/ +return 0 ; }return 1 ;
Signature · 1 line
gitgitgadget
PATCH 22/30 pack-objects: introduce GIT_TEST_PACK_PATH_WALK
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
There are many tests that validate whether 'git pack-objects' works as expected. Instead of duplicating these tests, add a new test environment variable, GIT_TEST_PACK_PATH_WALK, that implies --path-walk by default when specified.
This was useful in testing the implementation of the --path-walk implementation, especially in conjunction with test such as:
- t5322-pack-objects-sparse.sh : This demonstrates the effectiveness of the --sparse option and how it combines with --path-walk.
RFC TODO: list other helpful test cases, as well as the ones where the behavior breaks if this is enabled...
--- builtin/pack-objects.c | 1 + t/README | 4 ++++ 2 files changed, 5 insertions(+)
builtin/pack-objects.c +1 -0
@@ -4534,6 +4534,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)disable_replace_refs (); + path_walk =git_env_bool ("GIT_TEST_PACK_PATH_WALK" ,0 ); sparse =git_env_bool ("GIT_TEST_PACK_SPARSE" ,-1 );if (the_repository->gitdir) {prepare_repo_settings (the_repository);
t/README +4 -0
@@ -433,6 +433,10 @@ GIT_TEST_PACK_SPARSE=<boolean> if disabled will default the pack-objects builtin to use the non-sparse object walk. This can still be overridden by the --sparse command-line argument. +GIT_TEST_PACK_PATH_WALK=<boolean> if enabled will default the pack-objects +builtin to use the path-walk API for the object walk. This can still be +overridden by the --no-path-walk command-line argument. + GIT_TEST_PRELOAD_INDEX=<boolean> exercises the preload-index code path by overriding the minimum number of cache entries required per thread.
Signature · 1 line
gitgitgadget
PATCH 23/30 p5313: add size comparison test
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
To test the benefits of the new --path-walk option in 'git pack-objects', create a performance test that times the process but also compares the size of the output.
Against the microsoft/fluentui repo [1] against a particular commit [2], this has reproducible results of a similar scale:
Test this tree
---------------------------------------------------------------
5313.2: thin pack 0.39(0.48+0.03)
5313.3: thin pack size 1.2M
5313.4: thin pack with --path-walk 0.09(0.07+0.01)
5313.5: thin pack size with --path-walk 20.8K
5313.6: big recent pack 2.13(8.29+0.26)
5313.7: big recent pack size 17.7M
5313.8: big recent pack with --path-walk 3.18(4.21+0.22)
5313.9: big recent pack size with --path-walk 15.0M
[1] https://github.com/microsoft/reactui
[2] e70848ebac1cd720875bccaa3026f4a9ed700e08
RFC TODO: Note that the path-walk version is slower for the big case, but the delta calculation is single-threaded with the current implementation! It's still faster for the small case that mimics a typical push.
--- t/perf/p5313-pack-objects.sh | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 t/perf/p5313-pack-objects.sh
t/perf/p5313-pack-objects.sh new file +55 -0
@@ -0,0 +1,55 @@ +#!/bin/sh + +test_description='Tests pack performance using bitmaps' +. ./perf-lib.sh + +GIT_TEST_PASSING_SANITIZE_LEAK=0 +export GIT_TEST_PASSING_SANITIZE_LEAK + +test_perf_large_repo + +test_expect_success 'create rev input' ' + cat >in-thin <<-EOF && + $(git rev-parse HEAD) + ^$(git rev-parse HEAD~1) + EOF + + cat >in-big-recent <<-EOF + $(git rev-parse HEAD) + ^$(git rev-parse HEAD~1000) + EOF +' + +test_perf 'thin pack' ' + git pack-objects --thin --stdout --revs --sparse <in-thin >out +' + +test_size 'thin pack size' ' + wc -c <out +' + +test_perf 'thin pack with --path-walk' ' + git pack-objects --thin --stdout --revs --sparse --path-walk <in-thin >out +' + +test_size 'thin pack size with --path-walk' ' + wc -c <out +' + +test_perf 'big recent pack' ' + git pack-objects --stdout --revs <in-big-recent >out +' + +test_size 'big recent pack size' ' + wc -c <out +' + +test_perf 'big recent pack with --path-walk' ' + git pack-objects --stdout --revs --path-walk <in-big-recent >out +' + +test_size 'big recent pack size with --path-walk' ' + wc -c <out +' + +test_done
Signature · 1 line
gitgitgadget
PATCH 24/30 repack: add --path-walk option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Since 'git pack-objects' supports a --path-walk option, allow passing it through in 'git repack'. This presents interesting testing opportunities for comparing the different repacking strategies against each other.
For the microsoft/fluentui repo [1], the results are very interesting:
Test this tree
-------------------------------------------------------------------
5313.10: full repack 97.91(663.47+2.83)
5313.11: full repack size 449.1K
5313.12: full repack with --path-walk 105.42(120.49+0.95)
5313.13: full repack size with --path-walk 159.1K
[1] https://github.com/microsoft/fluentui
This repo suffers from having a lot of paths that collide in the name hash, so examining them in groups by path leads to better deltas. Also, in this case, the single-threaded implementation is competitive with the full repack. This is saving time diffing files that have significant differences from each other.
--- builtin/repack.c | 5 +++++ t/perf/p5313-pack-objects.sh | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+)
builtin/repack.c +5 -0
@@ -57,6 +57,7 @@ struct pack_objects_args {int no_reuse_object;int quiet;int local; +int path_walk;struct list_objects_filter_options filter_options; };
@@ -288,6 +289,8 @@ static void prepare_pack_objects(struct child_process *cmd,strvec_pushf (&cmd->args,"--no-reuse-delta" );if (args->no_reuse_object)strvec_pushf (&cmd->args,"--no-reuse-object" ); +if (args->path_walk) +strvec_pushf (&cmd->args,"--path-walk" );if (args->local)strvec_push (&cmd->args,"--local" );if (args->quiet)
@@ -1158,6 +1161,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix) N_("pass --no-reuse-delta to git-pack-objects")),OPT_BOOL ('F' ,NULL , &po_args.no_reuse_object,N_ ("pass --no-reuse-object to git-pack-objects" )), +OPT_BOOL (0 ,"path-walk" , &po_args.path_walk, +N_ ("pass --path-walk to git-pack-objects" )),OPT_NEGBIT ('n' ,NULL , &run_update_server_info,N_ ("do not run git-update-server-info" ),1 ), OPT__QUIET(&po_args.quiet, N_("be quiet")),
t/perf/p5313-pack-objects.sh +20 -0
@@ -52,4 +52,24 @@ test_size 'big recent pack size with --path-walk' ' wc -c <out ' +test_perf 'full repack' ' + git repack -adf --no-write-bitmap-index +' + +test_size 'full repack size' ' + du -a .git/objects/pack | \ + awk "{ print \$1; }" | \ + sort -nr | head -n 1 +' + +test_perf 'full repack with --path-walk' ' + git repack -adf --no-write-bitmap-index --path-walk +' + +test_size 'full repack size with --path-walk' ' + du -a .git/objects/pack | \ + awk "{ print \$1; }" | \ + sort -nr | head -n 1 +' + test_done
Signature · 1 line
gitgitgadget
PATCH 25/30 pack-objects: enable --path-walk via config
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Users may want to enable the --path-walk option for 'git pack-objects' by default, especially underneath commands like 'git push' or 'git repack'.
This should be limited to client repositories, since the --path-walk option disables bitmap walks, so would be bad to include in Git servers when serving fetches and clones. There is potential that it may be helpful to consider when repacking the repository, to take advantage of improved deltas across historical versions of the same files.
Much like how "pack.useSparse" was introduced and included in "feature.experimental" before being enabled by default, use the repository settings infrastructure to make the new "pack.usePathWalk" config enabled by "feature.experimental" and "feature.manyFiles".
--- Documentation/config/pack.txt | 8 ++++++++ builtin/pack-objects.c | 4 +++- repo-settings.c | 3 +++ repository.h | 1 + 4 files changed, 15 insertions(+), 1 deletion(-)
Documentation/config/pack.txt +8 -0
@@ -155,6 +155,14 @@ pack.useSparse:: commits contain certain types of direct renames. Default is `true`. +pack.usePathWalk:: + When true, git will default to using the '--path-walk' option in + 'git pack-objects' when the '--revs' option is present. This + algorithm groups objects by path to maximize the ability to + compute delta chains across historical versions of the same + object. This may disable other options, such as using bitmaps to + enumerate objects. + pack.preferBitmapTips:: When selecting which commits will receive bitmaps, prefer a commit at the tip of any reference that is a suffix of any value
builtin/pack-objects.c +3 -1
@@ -4534,12 +4534,14 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)disable_replace_refs (); - path_walk =git_env_bool ("GIT_TEST_PACK_PATH_WALK" ,0 ); + path_walk =git_env_bool ("GIT_TEST_PACK_PATH_WALK" ,-1 ); sparse =git_env_bool ("GIT_TEST_PACK_SPARSE" ,-1 );if (the_repository->gitdir) {prepare_repo_settings (the_repository);if (sparse <0 ) sparse = the_repository->settings.pack_use_sparse; +if (path_walk <0 ) + path_walk = the_repository->settings.pack_use_path_walk;if (the_repository->settings.pack_use_multi_pack_reuse) allow_pack_reuse =MULTI_PACK_REUSE ; }
repo-settings.c +3 -0
@@ -45,11 +45,13 @@ void prepare_repo_settings(struct repository *r) r->settings.fetch_negotiation_algorithm =FETCH_NEGOTIATION_SKIPPING ; r->settings.pack_use_bitmap_boundary_traversal =1 ; r->settings.pack_use_multi_pack_reuse =1 ; + r->settings.pack_use_path_walk =1 ; }if (manyfiles) { r->settings.index_version =4 ; r->settings.index_skip_hash =1 ; r->settings.core_untracked_cache =UNTRACKED_CACHE_WRITE ; + r->settings.pack_use_path_walk =1 ; }/* Commit graph config or default, does not cascade (simple) */
@@ -64,6 +66,7 @@ void prepare_repo_settings(struct repository *r)/* Boolean config or default, does not cascade (simple) */ repo_cfg_bool (r,"pack.usesparse" , &r->settings.pack_use_sparse,1 ); +repo_cfg_bool (r,"pack.usepathwalk" , &r->settings.pack_use_path_walk,0 );repo_cfg_bool (r,"core.multipackindex" , &r->settings.core_multi_pack_index,1 );repo_cfg_bool (r,"index.sparse" , &r->settings.sparse_index,0 );repo_cfg_bool (r,"index.skiphash" , &r->settings.index_skip_hash, r->settings.index_skip_hash);
repository.h +1 -0
@@ -62,6 +62,7 @@ struct repo_settings {enum untracked_cache_setting core_untracked_cache;int pack_use_sparse; +int pack_use_path_walk;enum fetch_negotiation_setting fetch_negotiation_algorithm;int core_multi_pack_index;
Signature · 1 line
gitgitgadget
PATCH 26/30 scalar: enable path-walk during push via config
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Repositories registered with Scalar are expected to be client-only repositories that are rather large. This means that they are more likely to be good candidates for using the --path-walk option when running 'git pack-objects', especially under the hood of 'git push'. Enable this config in Scalar repositories.
--- scalar.c | 1 + 1 file changed, 1 insertion(+)
scalar.c +1 -0
@@ -170,6 +170,7 @@ static int set_recommended_config(int reconfigure) { "core.autoCRLF", "false" }, { "core.safeCRLF", "false" }, { "fetch.showForcedUpdates", "false" }, + { "push.usePathWalk", "true" }, { NULL, NULL }, };int i;
Signature · 1 line
gitgitgadget
PATCH 27/30 pack-objects: add --full-name-hash option
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
RFC NOTE: this is essentially the same as the patch introduced independently of the RFC, but now is on top of the --path-walk option instead. This is included in the RFC for comparison purposes.
RFC NOTE: As you can see from the details below, the --full-name-hash option essentially attempts to do similar things as the --path-walk option, but sometimes misses the mark. Collisions still happen with the --full-name-hash option, leading to some misses. However, in cases where the default name-hash algorithm has low collision rates and deltas are actually desired across objects with similar names but different full names, the --path-walk option can still take advantage of the default name hash approach.
Here are the new performance details simulating a single push in an internal monorepo using a lot of paths that collide in the default name hash. We can see that --full-name-hash gets close to the --path-walk option's size.
Test this tree
--------------------------------------------------------------
5313.2: thin pack 2.43(2.92+0.14)
5313.3: thin pack size 4.5M
5313.4: thin pack with --full-name-hash 0.31(0.49+0.12)
5313.5: thin pack size with --full-name-hash 15.5K
5313.6: thin pack with --path-walk 0.35(0.31+0.04)
5313.7: thin pack size with --path-walk 14.2K
However, when simulating pushes on repositories that do not have issues with name-hash collisions, the --full-name-hash option presents a potential of worse delta calculations, such as this example using my local Git repository:
Test this tree
--------------------------------------------------------------
5313.2: thin pack 0.03(0.01+0.01)
5313.3: thin pack size 475
5313.4: thin pack with --full-name-hash 0.02(0.01+0.01)
5313.5: thin pack size with --full-name-hash 14.8K
5313.6: thin pack with --path-walk 0.02(0.01+0.01)
5313.7: thin pack size with --path-walk 475
Note that the path-walk option found the same delta bases as the default options in this case.
In the full repack case, the --full-name-hash option may be preferable because it interacts well with other advanced features, such as using bitmap indexes and tracking delta islands.
--- builtin/pack-objects.c | 20 +++++++++++++++----- builtin/repack.c | 5 +++++ pack-objects.h | 20 ++++++++++++++++++++ t/perf/p5313-pack-objects.sh | 26 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-)
builtin/pack-objects.c +15 -5
@@ -270,6 +270,14 @@ struct configured_exclusion {static struct oidmap configured_exclusions;static struct oidset excluded_by_config; +static int use_full_name_hash; + +static inline uint32_t pack_name_hash_fn (const char *name) +{ +if (use_full_name_hash) +return pack_full_name_hash (name); +return pack_name_hash (name); +}/* * stats
@@ -1674,7 +1682,7 @@ static int add_object_entry(const struct object_id *oid, enum object_type type,return 0 ; } -create_object_entry (oid, type,pack_name_hash (name), +create_object_entry (oid, type,pack_name_hash_fn (name), exclude, name &&no_try_delta (name), found_pack, found_offset);return 1 ;
@@ -1888,7 +1896,7 @@ static void add_preferred_base_object(const char *name) {struct pbase_tree *it;size_t cmplen; -unsigned hash =pack_name_hash (name); +unsigned hash =pack_name_hash_fn (name);if (!num_preferred_base ||check_pbase_path (hash))return ;
@@ -3405,7 +3413,7 @@ static void show_object_pack_hint(struct object *object, const char *name,* here using a now in order to perhaps improve the delta selection * process. */ - oe->hash =pack_name_hash (name); + oe->hash =pack_name_hash_fn (name); oe->no_try_delta = name &&no_try_delta (name); stdin_packs_hints_nr++;
@@ -3555,7 +3563,7 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type entry =packlist_find (&to_pack, oid);if (entry) {if (name) { - entry->hash =pack_name_hash (name); + entry->hash =pack_name_hash_fn (name); entry->no_try_delta =no_try_delta (name); } }else {
@@ -3578,7 +3586,7 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_typereturn ; } - entry =create_object_entry (oid, type,pack_name_hash (name), + entry =create_object_entry (oid, type,pack_name_hash_fn (name),0 , name &&no_try_delta (name), pack, offset); }
@@ -4526,6 +4534,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_STRING_LIST (0 ,"uri-protocol" , &uri_protocols,N_ ("protocol" ),N_ ("exclude any configured uploadpack.blobpackfileuri with this protocol" )), +OPT_BOOL (0 ,"full-name-hash" , &use_full_name_hash, +N_ ("optimize delta compression across identical path names over time" )), OPT_END(), };
builtin/repack.c +5 -0
@@ -58,6 +58,7 @@ struct pack_objects_args {int quiet;int local;int path_walk; +int full_name_hash;struct list_objects_filter_options filter_options; };
@@ -291,6 +292,8 @@ static void prepare_pack_objects(struct child_process *cmd,strvec_pushf (&cmd->args,"--no-reuse-object" );if (args->path_walk)strvec_pushf (&cmd->args,"--path-walk" ); +if (args->full_name_hash) +strvec_pushf (&cmd->args,"--full-name-hash" );if (args->local)strvec_push (&cmd->args,"--local" );if (args->quiet)
@@ -1163,6 +1166,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix) N_("pass --no-reuse-object to git-pack-objects")),OPT_BOOL (0 ,"path-walk" , &po_args.path_walk,N_ ("pass --path-walk to git-pack-objects" )), +OPT_BOOL (0 ,"full-name-hash" , &po_args.full_name_hash, +N_ ("pass --full-name-hash to git-pack-objects" )),OPT_NEGBIT ('n' ,NULL , &run_update_server_info,N_ ("do not run git-update-server-info" ),1 ), OPT__QUIET(&po_args.quiet, N_("be quiet")),
pack-objects.h +20 -0
@@ -207,6 +207,26 @@ static inline uint32_t pack_name_hash(const char *name)return hash; } +static inline uint32_t pack_full_name_hash (const char *name) +{ +const uint32_t bigp =1234572167U ; +uint32_t c, hash = bigp; + +if (!name) +return 0 ; + +/* +* Just do the dumbest thing possible: add random multiples of a +* large prime number with a binary shift. Goal is not cryptographic, +* but generally uniformly distributed. +*/ +while ((c = *name++) !=0 ) { + hash += c * bigp; + hash = (hash >>5 ) | (hash <<27 ); + } +return hash; +} +static inline enum object_type oe_type (const struct object_entry *e) {return e->type_valid ? e->type_ :OBJ_BAD ;
t/perf/p5313-pack-objects.sh +26 -0
@@ -28,6 +28,14 @@ test_size 'thin pack size' ' wc -c <out ' +test_perf 'thin pack with --full-name-hash' ' + git pack-objects --thin --stdout --revs --sparse --full-name-hash <in-thin >out +' + +test_size 'thin pack size with --full-name-hash' ' + wc -c <out +' + test_perf 'thin pack with --path-walk' ' git pack-objects --thin --stdout --revs --sparse --path-walk <in-thin >out '
@@ -44,6 +52,14 @@ test_size 'big recent pack size' ' wc -c <out ' +test_perf 'big recent pack with --full-name-hash' ' + git pack-objects --stdout --revs --full-name-hash <in-big-recent >out +' + +test_size 'big recent pack size with --full-name-hash' ' + wc -c <out +' + test_perf 'big recent pack with --path-walk' ' git pack-objects --stdout --revs --path-walk <in-big-recent >out '
@@ -62,6 +78,16 @@ test_size 'full repack size' ' sort -nr | head -n 1 ' +test_perf 'full repack with --full-name-hash' ' + git repack -adf --no-write-bitmap-index --full-name-hash +' + +test_size 'full repack size with --full-name-hash' ' + du -a .git/objects/pack | \ + awk "{ print \$1; }" | \ + sort -nr | head -n 1 +' + test_perf 'full repack with --path-walk' ' git repack -adf --no-write-bitmap-index --path-walk '
Signature · 1 line
gitgitgadget
PATCH 28/30 test-name-hash: add helper to compute name-hash functions
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
Using this tool, we can count how many distinct name-hash values exist within a list of paths. Examples include
git ls-tree -r --name-only HEAD | \ test-tool name-hash | \
awk "{print \$1;}" | \
sort -ns | uniq | wc -l
which outputs the number of distinct name-hash values that appear at HEAD. Or, the following which presents the resulting name-hash values of maximum multiplicity:
git ls-tree -r --name-only HEAD | \
test-tool name-hash | \
awk "{print \$1;}" | \
sort -n | uniq -c | sort -nr | head -n 25For an internal monorepo with around a quarter million paths at HEAD, the highest multiplicity for the standard name-hash function was 14,424 while the full name-hash algorithm had only seven hash values with any collision, with a maximum multiplicity of two.
--- Makefile | 1 + t/helper/test-name-hash.c | 23 +++++++++++++++++++++++ t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + 4 files changed, 26 insertions(+) create mode 100644 t/helper/test-name-hash.c
Makefile +1 -0
@@ -808,6 +808,7 @@ TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o TEST_BUILTINS_OBJS += test-match-trees.o TEST_BUILTINS_OBJS += test-mergesort.o TEST_BUILTINS_OBJS += test-mktemp.o +TEST_BUILTINS_OBJS += test-name-hash.o TEST_BUILTINS_OBJS += test-oid-array.o TEST_BUILTINS_OBJS += test-online-cpus.o TEST_BUILTINS_OBJS += test-pack-mtimes.o
t/helper/test-name-hash.c new file +23 -0
@@ -0,0 +1,23 @@ +/* +* test-name-hash.c: Read a list of paths over stdin and report on their +* name-hash and full name-hash. +*/ + +#include "test-tool.h" +#include "git-compat-util.h" +#include "pack-objects.h" +#include "strbuf.h" + +int cmd__name_hash (int argc,const char **argv) +{ +struct strbuf line =STRBUF_INIT ; + +while (!strbuf_getline (&line, stdin)) { +uint32_t name_hash =pack_name_hash (line.buf); +uint32_t full_hash =pack_full_name_hash (line.buf); + +printf ("%10" PRIu32"\t%10" PRIu32"\t%s\n" , name_hash, full_hash, line.buf); + } + +return 0 ; +}
t/helper/test-tool.c +1 -0
@@ -43,6 +43,7 @@ static struct test_cmd cmds[] = { { "match-trees", cmd__match_trees }, { "mergesort", cmd__mergesort }, { "mktemp", cmd__mktemp }, + { "name-hash", cmd__name_hash }, { "oid-array", cmd__oid_array }, { "online-cpus", cmd__online_cpus }, { "pack-mtimes", cmd__pack_mtimes },
t/helper/test-tool.h +1 -0
@@ -37,6 +37,7 @@ int cmd__lazy_init_name_hash(int argc, const char **argv);int cmd__match_trees (int argc,const char **argv);int cmd__mergesort (int argc,const char **argv);int cmd__mktemp (int argc,const char **argv); +int cmd__name_hash (int argc,const char **argv);int cmd__online_cpus (int argc,const char **argv);int cmd__pack_mtimes (int argc,const char **argv);int cmd__parse_options (int argc,const char **argv);
Signature · 1 line
gitgitgadget
PATCH 29/30 p5314: add a size test for name-hash collisions
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
This test helps inform someone as to the behavior of the name-hash algorithms for their repo based on the paths at HEAD.
For example, the microsoft/fluentui repo had these statistics at time of committing:
Test this tree
-----------------------------------------------------------------
5314.1: paths at head 19.6K
5314.2: number of distinct name-hashes 8.2K
5314.3: number of distinct full-name-hashes 19.6K
5314.4: maximum multiplicity of name-hashes 279
5314.5: maximum multiplicity of fullname-hashes 1
That demonstrates that of the nearly twenty thousand path names, they are assigned around eight thousand distinct values. 279 paths are assigned to a single value, leading the packing algorithm to sort objects from those paths together, by size.
In this repository, no collisions occur for the full-name-hash algorithm.
In a more extreme example, an internal monorepo had a much worse collision rate:
Test this tree
-----------------------------------------------------------------
5314.1: paths at head 221.6K
5314.2: number of distinct name-hashes 72.0K
5314.3: number of distinct full-name-hashes 221.6K
5314.4: maximum multiplicity of name-hashes 14.4K
5314.5: maximum multiplicity of fullname-hashes 2
Even in this repository with many more paths at HEAD, the collision rate was low and the maximum number of paths being grouped into a single bucket by the full-path-name algorithm was two.
--- t/perf/p5314-name-hash.sh | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 t/perf/p5314-name-hash.sh
t/perf/p5314-name-hash.sh new file +41 -0
@@ -0,0 +1,41 @@ +#!/bin/sh + +test_description='Tests pack performance using bitmaps' +. ./perf-lib.sh + +GIT_TEST_PASSING_SANITIZE_LEAK=0 +export GIT_TEST_PASSING_SANITIZE_LEAK + +test_perf_large_repo + +test_size 'paths at head' ' + git ls-tree -r --name-only HEAD >path-list && + wc -l <path-list +' + +test_size 'number of distinct name-hashes' ' + cat path-list | test-tool name-hash >name-hashes && + cat name-hashes | awk "{ print \$1; }" | sort -n | uniq -c >name-hash-count && + wc -l <name-hash-count +' + +test_size 'number of distinct full-name-hashes' ' + cat name-hashes | awk "{ print \$2; }" | sort -n | uniq -c >full-name-hash-count && + wc -l <full-name-hash-count +' + +test_size 'maximum multiplicity of name-hashes' ' + cat name-hash-count | \ + sort -nr | \ + head -n 1 | \ + awk "{ print \$1; }" +' + +test_size 'maximum multiplicity of fullname-hashes' ' + cat full-name-hash-count | \ + sort -nr | \ + head -n 1 | \ + awk "{ print \$1; }" +' + +test_done
Signature · 1 line
gitgitgadget
PATCH 30/30 pack-objects: output debug info about deltas
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
In order to debug what is going on during delta calculations, add a --debug-file=<file> option to 'git pack-objects'. This leads to sending a JSON-formatted description of the delta information to that file.
--- builtin/pack-objects.c | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+)
builtin/pack-objects.c +69 -0
@@ -50,6 +50,9 @@*/ static struct packing_data to_pack; +static FILE *delta_file; +static int delta_file_nr; +static inline struct object_entry *oe_delta (const struct packing_data *pack,const struct object_entry *e)
@@ -516,6 +519,14 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent hdrlen =encode_in_pack_object_header (header,sizeof (header), type, size); +if (delta_file) { +if (delta_file_nr++) +fprintf (delta_file,",\n" ); +fprintf (delta_file,"\t\t{\n" ); +fprintf (delta_file,"\t\t\t\"oid\" : \"%s\",\n" ,oid_to_hex (&entry->idx.oid)); +fprintf (delta_file,"\t\t\t\"size\" : %" PRIuMAX",\n" , datalen); + } +if (type ==OBJ_OFS_DELTA ) {/* * Deltas with relative base contain an additional
@@ -536,6 +547,11 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_enthashwrite (f, header, hdrlen);hashwrite (f, dheader + pos,sizeof (dheader) - pos); hdrlen +=sizeof (dheader) - pos; +if (delta_file) { +fprintf (delta_file,"\t\t\t\"delta_type\" : \"OFS\",\n" ); +fprintf (delta_file,"\t\t\t\"offset\" : %" PRIuMAX",\n" , ofs); +fprintf (delta_file,"\t\t\t\"delta_base\" : \"%s\",\n" ,oid_to_hex (&DELTA (entry)->idx.oid)); + } }else if (type ==OBJ_REF_DELTA ) {/* * Deltas with a base reference contain
@@ -550,6 +566,10 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_enthashwrite (f, header, hdrlen);hashwrite (f,DELTA (entry)->idx.oid.hash, hashsz); hdrlen += hashsz; +if (delta_file) { +fprintf (delta_file,"\t\t\t\"delta_type\" : \"REF\",\n" ); +fprintf (delta_file,"\t\t\t\"delta_base\" : \"%s\",\n" ,oid_to_hex (&DELTA (entry)->idx.oid)); + } }else {if (limit && hdrlen + datalen + hashsz >= limit) {if (st)
@@ -559,6 +579,10 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent }hashwrite (f, header, hdrlen); } + +if (delta_file) +fprintf (delta_file,"\t\t\t\"reused\" : false\n\t\t}" ); +if (st) { datalen =write_large_blob_data (st, f, &entry->idx.oid);close_istream (st);
@@ -619,6 +643,14 @@ static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,return write_no_reuse_object (f, entry, limit, usable_delta); } +if (delta_file) { +if (delta_file_nr++) +fprintf (delta_file,",\n" ); +fprintf (delta_file,"\t\t{\n" ); +fprintf (delta_file,"\t\t\t\"oid\" : \"%s\",\n" ,oid_to_hex (&entry->idx.oid)); +fprintf (delta_file,"\t\t\t\"size\" : %" PRIuMAX",\n" , entry_size); + } +if (type ==OBJ_OFS_DELTA ) {off_t ofs = entry->idx.offset -DELTA (entry)->idx.offset;unsigned pos =sizeof (dheader) -1 ;
@@ -633,6 +665,12 @@ static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,hashwrite (f, dheader + pos,sizeof (dheader) - pos); hdrlen +=sizeof (dheader) - pos; reused_delta++; + +if (delta_file) { +fprintf (delta_file,"\t\t\t\"delta_type\" : \"OFS\",\n" ); +fprintf (delta_file,"\t\t\t\"offset\" : %" PRIuMAX",\n" , ofs); +fprintf (delta_file,"\t\t\t\"delta_base\" : \"%s\",\n" ,oid_to_hex (&DELTA (entry)->idx.oid)); + } }else if (type ==OBJ_REF_DELTA ) {if (limit && hdrlen + hashsz + datalen + hashsz >= limit) {unuse_pack (&w_curs);
@@ -642,6 +680,10 @@ static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,hashwrite (f,DELTA (entry)->idx.oid.hash, hashsz); hdrlen += hashsz; reused_delta++; +if (delta_file) { +fprintf (delta_file,"\t\t\t\"delta_type\" : \"REF\",\n" ); +fprintf (delta_file,"\t\t\t\"delta_base\" : \"%s\",\n" ,oid_to_hex (&DELTA (entry)->idx.oid)); + } }else {if (limit && hdrlen + datalen + hashsz >= limit) {unuse_pack (&w_curs);
@@ -652,6 +694,10 @@ static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,copy_pack_data (f, p, &w_curs, offset, datalen);unuse_pack (&w_curs); reused++; + +if (delta_file) +fprintf (delta_file,"\t\t\t\"reused\" : true\n\t\t}" ); +return hdrlen + datalen; }
@@ -1264,6 +1310,11 @@ static void write_pack_file(void)ALLOC_ARRAY (written_list, to_pack.nr_objects); write_order =compute_write_order (); +if (delta_file) { +fprintf (delta_file,"{\n\t\"num_objects\" : %" PRIu32",\n" , to_pack.nr_objects); +fprintf (delta_file,"\t\"objects\" : [\n" ); + } + do { unsigned char hash[GIT_MAX_RAWSZ]; char *pack_tmp_name = NULL;
@@ -1412,6 +1463,9 @@ static void write_pack_file(void) written, nr_result);trace2_data_intmax ("pack-objects" , the_repository,"write_pack_file/wrote" , nr_result); + +if (delta_file) +fprintf (delta_file,"\n\t]\n}" ); }static int no_try_delta (const char *path)
@@ -4430,6 +4484,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)struct string_list keep_pack_list =STRING_LIST_INIT_NODUP ;struct list_objects_filter_options filter_options =LIST_OBJECTS_FILTER_INIT ; +const char *delta_file_name =NULL ;struct option pack_objects_options[] = { OPT_CALLBACK_F('q', "quiet", &progress, NULL,
@@ -4536,6 +4591,9 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) N_("exclude any configured uploadpack.blobpackfileuri with this protocol")),OPT_BOOL (0 ,"full-name-hash" , &use_full_name_hash,N_ ("optimize delta compression across identical path names over time" )), +OPT_STRING (0 ,"delta-file" , &delta_file_name, +N_ ("filename" ), +N_ ("output delta compression details to the given file" )), OPT_END(), };
@@ -4573,6 +4631,12 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if (pack_to_stdout != !base_name || argc)usage_with_options (pack_usage, pack_objects_options); +if (delta_file_name) { + delta_file =fopen (delta_file_name,"w" ); +if (!delta_file) +die_errno ("failed to open '%s'" , delta_file_name); +trace2_printf ("opened '%s' for writing deltas" , delta_file_name); + }if (depth <0 ) depth =0 ;if (depth >= (1 <<OE_DEPTH_BITS )) {
@@ -4796,5 +4860,10 @@ cleanup:list_objects_filter_release (&filter_options);strvec_clear (&rp); +if (delta_file) { +fflush (delta_file); +fclose (delta_file); + } +return 0 ; }
Signature · 1 line
gitgitgadget
Quoted 3 lines · Derrick Stolee via GitGitGadget
One obvious issue with this current implementation is that it inlines much of the delta calculation into the "Enumerate objects" phase, and thus makes it single-threaded.
Naïvely, traversal of history partitioned by paths smells embarrassingly parallelizable; it may need some post processing to make sure that the same object only appears once, though, and the devil probably is in the details ;-).
Thanks for an enjoyable cover letter that pulls readers in.
Quoted 14 lines · Derrick Stolee via GitGitGadget
This RFC is ultimately about introducing a new way to walk objects, called the "path-walk API" in the new path-walk.[ch] files. Before digging into the details of the API, let's discuss the applications which will hint at the API's design.
APPLICATIONS OF THE PATH-WALK API
=================================The applications of this API were discovered in the following order, though I recommend reversing the order for the priority of their actual implementation in future patch series:
Quoted 5 lines
* git backfill: a builtin to download missing blobs in a blobless partial clone, done in batches and grouped by the path they appear in to maximize delta compression in each batch. Allows focusing on the paths of the sparse-checkout to only get the blobs necessary for history queries in the current focus.
It's not very clear if this would be useful when doing a git sparse-checkout add, or a git blame on a file path not covered by the current sparse-checkout, or both. I think it would be clearer if there were a few examples.
Quoted 6 lines
* git survey: Jeff Hostetler built this feature [1] as a way to get functionality similar to git-sizer [2], but using the internals of Git to do it faster. It also displays information not available to git-sizer, like the on-disk size of objects. This RFC presents a simplified version of the builtin focused on listing the paths that contribute the most to the on-disk size of trees and blobs.
Not sure how git survey works, but git sizer works on a whole repo, so, if they work in the same way, I am not sure I see what a new path oriented way to walk repos would bring to the tool.
Quoted 7 lines
* git pack-objects --path-walk: In order to find a way to compute deltas among objects of the same path, I applied the path-walk API to 'git pack-objects' behind an optional flag. There are overlaps with the '--sparse' option [3], [4] that can be used here. This provides perfect partitioning by path name, without any possible collisions from the name-hash algorithm. It also allows using the name-hash values to find cross-path delta chains in a second pass.
Do you mean that the new way to walk repos allows pack-object to perform better in the general case or maybe only in the case where partial clone without blobs and sparse-checkout are used as described in the git backfill related point above?
Quoted 1 line
* git repack --full-name-hash: If we are worried about name-hsah
s/name-hsah/name-hash/
Quoted 2 lines
collisions, an easier thing to implement is a different name-hash algorithm that is less likely to have collisions.
Maybe it could help to remind people a bit (perhaps in a note or using a link) what name-hash collisions are and why we could be worried about them.
Actually there is a "DISCUSSION OF NAME HASH" section below with explanations, so maybe a note could just tell about that section.
Quoted 4 lines
This feature was
already sent to the mailing list as a fully-reviewable series [5]. It is included here because this series allows testing the --path-walk option against the --full-name-hash.
Do you mean that the new way to walk repos can be used along with git repack --full-name-hash (maybe with git repack --full-name-hash --path-walk or a config option or otherwise?) and that it brings some performance or other kind (better packs or which ones?) of improvements?
Quoted 7 lines
[1] https://github.com/microsoft/git/pull/667 [2]
https://github.com/github/git-sizer [3]
https://github.com/git/git/compare/5d826e972970a784bd7a7bdf587512510097b8c7...99dbbfa8ddbba2b620965d026d4ec199b8837a6f
[4]
https://devblogs.microsoft.com/devops/exploring-new-frontiers-for-git-push-performance/
[5]
https://lore.kernel.org/git/[email protected]
It looks like adding line breaks could help make the above link list more readable.
Quoted 15 lines
TIMELINE FOR CREATING THESE APPLICATIONS IN THIS ORDER
======================================================Here's the story about how these applications came about: I was tasked with understanding why certain internal repositories were growing larger than expected. (Feel free to skip. Otherwise, thank you for indulging me.)
I first prototyped 'git backfill' as a way to download some of the largest repositories without being blocked on a full clone. This batched download mechanism allowed me to essentially have a retryable clone, since the client could restart the process from scratch and skip any objects that were already on disk. It was natural to batch based on the path of the blobs in order to theoretically save time and network bandwidth due to better delta calculations.
Quoted 2 lines
While investigating these repositories, I had some data hinting at the total size of the objects by type.
By type (blob, tree, commit, tag?) or by path? Why would the size of the objects by type be interesting? Could trees delta poorly?
Quoted 10 lines
But I was most interested in learning what
exactly was causing this growth. I did have a hint that the "release" branches were taking up much more space than the default branch, since cloning with --single-branch resulted in ~55GB of data but then fetching the release branches led to an additional ~125GB of data. Using "git diff" it was clear that these branches stored some CHANGELOG.json and CHANGELOG.md files, but the diffs were relatively small (but multiple such changes were batched together into a single new commit). I needed to see why exactly these paths were taking up so much space.
Quoted 3 lines
To this, I turned to Jeff Hostetler's new "git survey" command. This told me information about the total size of trees and blobs and told me the path name for the individual blobs that were the largest in the repository.
Nice.
Quoted 3 lines
These
paths were typically binary files that appeared only once or twice and did not account for the scale issues.
You mean they couldn't account for a 55GB to 125GB change in data size?
Quoted 5 lines
I modified 'git survey' to use the same
path-batching logic as in 'git backfill' to then consider each batch of objects and their size. Thus, the "path-walk API" was born. (This RFC contains a version that looks like it was created before 'git backfill'.)
Quoted 10 lines
The repository I was looking at had a clear pattern in its top 100 file paths by on-disk size: 99 of them were CHANGELOG.json and CHANGELOG.md files. The .md files surprised me, since they were always simple appends of the previous .md file at the same path. The .json files were capped in how many versions were being listed (at least in recent versions) but the data it stored was harder to compress. So I went looking into how 'git push' was calculating these delta bases. Adding some debug information to 'git pack-objects' demonstrated that the previous file versions were not being matched as delta bases. Instead, other new blobs in the push were being used as delta bases.
Interesting.
Quoted 11 lines
This meant that what should have been a trivial set of deltas bloated to 20-60 MB. (We will see later that it is possible for these to be 100-500 KB.)
Here is where I went on a little bit of a detour. (Come with me, it's important.) I knew that 'git pack-objects' used a name-hash to group objects by path, so I assumed that the reason these delta bases were not found was because the UNINTERESTING objects were not being added to the packing list. I have since discovered that this is incorrect, but I might have gotten stuck if I didn't think this.
Quoted 8 lines
This seemed like a natural reason to extend the path-walk API to allow walking commits and tags as part of 'git pack-objects' behind a new '--path-walk' option. The idea here is to compute deltas among the objects that share a common path and then later go through the (type, name-hash, size) sorting system to find other delta bases across path boundaries. After a lot of testing, failing, and testing again, the implementation in this RFC finally works to achieve the goal. It's not pretty (especially with how it handles tags) but it gets the job done.
Yeah, if it can significantly improve the generated packfiles (without drawbacks), it looks like a great improvement.
Quoted 6 lines
In hindsight, I realized that the UNINTERESTING objects were being considered, but due to collisions in the name-hash algorithm these objects were being sorted outside of the delta computation window. For this reason, I thought to create a new name-hash algorithm. Thus, the --full-name-hash option for 'git pack-objects' and 'git repack' was born. This feature was split out and sent to the mailing list independently from this RFC.
So the question is "Is the problem fully solved by the new name-hash algorithm or are there still benefits to using a new way to walk repos?"
Quoted 7 lines
RFC GOALS
=========The goals of this RFC are:
1. To demonstrate potential applications of the path-walk API to motivate its generality
Yeah, the "Path-walk API and applications" title summarizes this well.
Quoted 2 lines
as these features are sent in full-quality patch series,
but in a different order.
I wonder if the patch series could have been separated and not all sent in a 30 patch long series.
Quoted 3 lines
2. To communicate the discoveries found during the --path-walk and --full-name-hash features in 'git pack-objects' and 'git repack'. This includes comparing and contrasting the effectiveness of these features.
Thanks for communicating that.
Quoted 5 lines
3. To demonstrate the value of the path-based batching in the 'git survey' feature, and to inspire others to think about what other statistics would be valuable in that feature. (I anticipate that once a base is established, multiple contributors will help expand its functionality long into the future.)
Yeah, a better git sizer would be valuable for GitLab too and probably everyone hosting a significant number of repos.
Quoted 13 lines
RFC OUTLINE
===========The patches are grouped roughly by the application, in order of discovery:
PART I: 'git backfill'
======================These patches introduce the 'git backfill' builtin including its '--batch-size' and '--sparse' options. While this is the first and simplest application, it is also the lowest priority in terms of user need.
Quoted 6 lines
* path-walk: introduce an object walk by path
* backfill: add builtin boilerplate
* backfill: basic functionality and tests
* backfill: add --batch-size= option
* backfill: add --sparse option
* backfill: assume --sparse when sparse-checkout is enabled
I would prefer a first patch series with all the above and the first patch creating a technical doc called maybe Documentation/technical/path-walk.txt which could contain a lot of information from this RFC and perhaps technical details of how the path-walk works and how it is different from a regular walk.
Quoted 29 lines
DISCUSSION OF NAME HASH
=======================One thing to talk about before digging into --path-walk and --full-name-hash features is the existing name-hash algorithm. This hash algorithm creates a
uint32_tbased on the final 16 characters of the path name, weighing the last characters more. There are multiple benefits to this:1. Files of common types (.c, .txt, ...) may be grouped together.
2. Files that are renamed across directories may be grouped together.
(Thanks, Junio, for making this second benefit clear.)
The issue here is that some common patterns arise in repositories that use common path names across directories, and those files are creating name-hash collisions and making the sort less effective. One thing that can counteract these collisions is to increase the --window setting, but this significantly slows the delta computations.
Thus, the --path-walk and --full-name-hash features both attempt to combat these name-hash collisions in very different ways. The --path-walk mechanism uses the path-walk API to consider batches of objects that all share the same path. This avoids storing every possible path names in memory while doing the object walk, but still gives nice boundaries for delta compression possibilities. After the path-walk is complete, the full packing list is still sorted via name-hash and this allows for cross-path deltas. This is critical!
Quoted 3 lines
The --full-name-hash feature does the simpler choice of replacing the name-hash method with one that has fewer collisions, but loses the benefits of "nearby" paths having close hash values.
The benefits of "nearby" paths are only available with --path-walk, not in the current way object packing works.
Quoted 8 lines
This naturally leads to these two main differences in the two approaches:
1. The --full-name-hash feature is not good for 'git push' or similarly small pack-files. Since it limits the delta chains to objects with the same full path and loses the benefit of "nearby" paths, this feature should be used for larger repacks. In my testing, 'git push' simulations almost always have poor packing but 'git repack -adf' simulations have packing rivaling the --path-walk option.
Interesting.
Quoted 6 lines
2. The --path-walk option changes the object ordering significantly, meaning it may not ever be appropriate to combine with advanced repacking features such as delta islands or even reachability bitmaps. While my testing has shown that the --path-walk repacks are the most efficient of all options, this limitation makes me hesitate to recommend it wider than client repositories.
Might still be interesting to have it for client repos.
Quoted 5 lines
One natural question to consider is to think about storing both the name-hash and the full-name-hash and doing two delta passes, each one sorting the objects by a different hash function. The first issue is that we don't want to store two hash values per object, as that will significantly increase the memory pressure during repacking.
Is one more uint32_t per object really increasing the memory pressure so much? Isn't there a way to pack bits together to avoid that?
Quoted 4 lines
This could be side-stepped by
storing the full-name-hash in the packing list and then a second mapping from full-name-hash to name-hash. However, that still leads to the two passes taking extra time.
Isn't there a way to sort using both hash functions in a single pass? (That is to perform a single pass and when considering an object sort it using both hash functions before considering a different object.)
Quoted 2 lines
The --path-walk approach is faster than even a
single pass.
Is there a reason for that?
Quoted 30 lines
And in the right scenarios, the --full-name-hash option is very close to the --path-walk results.
ANALYSIS OF PACKING STRATEGIES
==============================Since I was focused on an internal monorepo that stored a large collection of Javascript packages, it should be no surprise that I eventually found other Javascript repositories that used similar tooling and thus had similar issues with unexplained scale problems. I'll use these four repositories as examples repeatedly, but one is actually public: microsoft/fluentui [6]. I'll use Repo B, C, and D for the others, in increasing size.
[6] https://github.com/microsoft/fluentui
In each of these repositories, doing a full repack ('git repack -adf') right after cloning presents a sizeable reduction in space. This is expected for servers not being optimized for exactly the reachable set I'm cloning. So, I'll focus on how much the default repacking parameters compare to using the --full-name-hash or --path-walk options:
| Repo | Standard Repack | With --full-name-hash | With --path-walk | |----------|-----------------|-----------------------|------------------| | fluentui | 438 MB | 168 MB | 148 MB | | Repo B | 6,255 MB | 829 MB | 778 MB | | Repo C | 37,737 MB | 7,125 MB | 6,158 MB | | Repo D | 130,049 MB | 6,190 MB | 4,432 MB |
Quoted 2 lines
Hopefully these reductions show how much these name-hash collisions are causing issues in these repositories.
Yeah, right. It might be interesting to see the size reduction in % too.
Quoted 14 lines
For the fluentui repo, I'm also able to share highlights from the 'git survey' output immediately after cloning and after repacking with the --path-walk option.
First, we can consider the total reachable objects in each scenario:
TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------Commits | 20579 | 10443669 | 15092790 Trees | 276503 | 40212070 | 244429615 Blobs | 294500 | 635365661 | 10791187920
Quoted 7 lines
TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------Commits | 20579 | 10450605 | 15092790 Trees | 276503 | 31136263 | 244429615 Blobs | 294500 | 94442401 | 10791187920
Using % of size reduction or increase might help make it a bit clearer here too.
Quoted 42 lines
Now, we can consider the top 10 file paths before and after repacking with the --path-walk feature:
TOP FILES BY DISK SIZE
===================================================================
Path | Count | Disk Size | Inflated Size
--------------------------------+-------+-----------+--------------yarn.lock | 802 | 58060531 | 889120488 ...-experiments/CHANGELOG.json | 505 | 28439452 | 252723999 ...fabric-react/CHANGELOG.json | 1270 | 25556510 | 902756623 ...t-components/CHANGELOG.json | 176 | 20366365 | 244936649 ...act-charting/CHANGELOG.json | 590 | 20106422 | 208224460 ...e-components/CHANGELOG.json | 559 | 15761271 | 189061764 ...act-examples/CHANGELOG.json | 577 | 13615569 | 234949961 ...react-charting/CHANGELOG.md | 564 | 11205840 | 104337986 .../experiments/CHANGELOG.json | 569 | 10596377 | 123662770 ...i-fabric-react/CHANGELOG.md | 1263 | 8154248 | 261494258TOP FILES BY DISK SIZE
===================================================================
Path | Count | Disk Size | Inflated Size
--------------------------------+-------+-----------+--------------yarn.lock | 802 | 9909326 | 889120488 ...iceBrandGuide_16Sep2016.pdf | 1 | 2106334 | 2186005 ...out/src/images/download.jpg | 1 | 1845249 | 1846117 ...fluent-ui-logo-inverted.png | 3 | 1370372 | 1447493 .yarn/releases/cli.js | 1 | 1335657 | 6741614 ...c/images/fluent-ui-logo.png | 3 | 1272902 | 1341139 ...ages/fluent-ui-logo-dev.png | 3 | 1130989 | 1186897 ...nents/public/SegoeUI-VF.ttf | 1 | 1074046 | 1844524 ...ig/rush/npm-shrinkwrap.json | 138 | 1058531 | 89326567 ...Accessibility_29Sep2016.pdf | 1 | 856621 | 927268As we can see from this example, before the repack we are mainly seeing the disk space be dominated by objects that appear at paths with CHANGELOG.json or CHANGELOG.md, which are frequently hitting collisions with the default name-hash. After the repack with the --path-walk feature, we see the largest paths are what we expect: checked in binaries, frequently-edited yarn files, and other hard-to- compress data.
Quoted 3 lines
The nice thing about this result is that we can point to files that are taking up space because there is no other way around it, not that the naming convention for the files is causing confusion during packing.
Yeah, nice result. I guess the result is the same or very similar when the improved name-hash algorithm is used.
Quoted 8 lines
WHAT TO DO NOW?
===============Thank you for reading this far. I hope that this has added context on the patch series for the --full-name-hash option, but also provided the right context for when I come back in a week or so with a review-ready version of the --path-walk option.
Quoted 5 lines
These patches are rough and I want to make sure everyone knows that. Reviewing them for style or even basic organization may lead to some wasted time. I know that at least one of the patches got mangled and its diff does not match its description (I'm thinking specifically about "pack-objects:
extractshould_attempt_deltas()" but this could apply elsewhere).
Ok, I will wait for the next iteration before reviewing the patches then.
Quoted 4 lines
But I think that interested parties could take my branch, build it, and give these features a try on their favorite repos. I'd love to hear feedback on the usability and effectiveness, especially if someone finds a case where the --path-walk option is less effective at packing data.
I might do that when reviewing the patch series later. I think it can still be valuable for you to get some first feedback from just reading this RFC.
Thanks.
Quoted 9 lines · Christian Couder
Quoted 5 lines · Derrick Stolee via GitGitGadget
* git backfill: a builtin to download missing blobs in a blobless partial clone, done in batches and grouped by the path they appear in to maximize delta compression in each batch. Allows focusing on the paths of the sparse-checkout to only get the blobs necessary for history queries in the current focus.
Quoted 4 lines
It's not very clear if this would be useful when doing a
git sparse-checkout add, or agit blameon a file path not covered by the current sparse-checkout, or both. I think it would be clearer if there were a few examples.
You are correct, this doesn't help with either of those examples, as they both require blobs outside of the current sparse-checkout. The idea is for users who are most often in a given sparse-checkout to have the experience as if they were in a non-partial Git clone, as long as they restrict their activity within their sparse-checkout.
If they increase their sparse-checkout, then they can re-run 'git backfill --sparse' to get the still-missing objects in the new paths.
Quoted 10 lines
Quoted 6 lines
* git survey: Jeff Hostetler built this feature [1] as a way to get functionality similar to git-sizer [2], but using the internals of Git to do it faster. It also displays information not available to git-sizer, like the on-disk size of objects. This RFC presents a simplified version of the builtin focused on listing the paths that contribute the most to the on-disk size of trees and blobs.
Not sure how
git surveyworks, butgit sizerworks on a whole repo, so, if they work in the same way, I am not sure I see what a new path oriented way to walk repos would bring to the tool.
git survey also works on a whole repo, but with the path-walk API implementation can talk about a group of objects that appear at a common path instead of only talking about single extreme objects, such as one large binary (that never changes) being singled out over a small file that chnages frequently and across all versions contributes more to the repo's disk size.
The main benefit of using git survey over git-sizer is that it can report on things internal to Git's storage that are not accessible to git-sizer through the git cat-file output it uses.
Quoted 8 lines
Quoted 7 lines
* git pack-objects --path-walk: In order to find a way to compute deltas among objects of the same path, I applied the path-walk API to 'git pack-objects' behind an optional flag. There are overlaps with the '--sparse' option [3], [4] that can be used here. This provides perfect partitioning by path name, without any possible collisions from the name-hash algorithm. It also allows using the name-hash values to find cross-path delta chains in a second pass.
Quoted 4 lines
Do you mean that the new way to walk repos allows pack-object to perform better in the general case or maybe only in the case where partial clone without blobs and sparse-checkout are used as described in the git backfill related point above?
From what I can see, git pack-objects --path-walk outperforms all other repacking strategies that I have found, but it does have some weaknesses in how it interacts with things like delta islands. There may be other concerns, and I _was_ able to find a repo that compressed slightly better with git pack-objects --full-name-hash, but it was also storing computer-generated, single-line JSON files representing configuration of production systems. I would like to have a more generic thing to say about this, but it will vary on data shape.
Quoted 10 lines
Quoted 4 lines
This feature was
already sent to the mailing list as a fully-reviewable series [5]. It is included here because this series allows testing the --path-walk option against the --full-name-hash.Do you mean that the new way to walk repos can be used along with
git repack --full-name-hash(maybe withgit repack --full-name-hash --path-walkor a config option or otherwise?) and that it brings some performance or other kind (better packs or which ones?) of improvements?
Combining the two features actually ends up with very similar performance to what --full-name-hash already does. It's actually important that the --path-walk option does a full pass of the objects via the standard name-hash after its first pass in groups based on the path.
It's more that I include performance tests that compare how effective the two features are relative to the existing repacking strategy and in a few different situations. This helps with both time and space performance measurements.
Quoted 15 lines
Quoted 15 lines
TIMELINE FOR CREATING THESE APPLICATIONS IN THIS ORDER
======================================================Here's the story about how these applications came about: I was tasked with understanding why certain internal repositories were growing larger than expected. (Feel free to skip. Otherwise, thank you for indulging me.)
I first prototyped 'git backfill' as a way to download some of the largest repositories without being blocked on a full clone. This batched download mechanism allowed me to essentially have a retryable clone, since the client could restart the process from scratch and skip any objects that were already on disk. It was natural to batch based on the path of the blobs in order to theoretically save time and network bandwidth due to better delta calculations.
Quoted 5 lines
Quoted 2 lines
While investigating these repositories, I had some data hinting at the total size of the objects by type.
By type (blob, tree, commit, tag?) or by path? Why would the size of the objects by type be interesting? Could trees delta poorly?
The previously-existing data could only group objects by type (commit, tree, blob) and report on the size of the reachable objects in those categories. The per-path data was not available as no tool that I knew about had the capability to expose that information.
Quoted 5 lines
Quoted 3 lines
These
paths were typically binary files that appeared only once or twice and did not account for the scale issues.You mean they couldn't account for a 55GB to 125GB change in data size?
The numbers were so small they didn't seem like they could have accounted for any issues at all, let alone a hundred gigabytes of data in the repo.
Quoted 8 lines
Quoted 7 lines
RFC GOALS
=========The goals of this RFC are:
1. To demonstrate potential applications of the path-walk API to motivate its generality
Quoted 7 lines
Yeah, the "Path-walk API and applications" title summarizes this well.
Quoted 2 lines
as these features are sent in full-quality patch series,
but in a different order.I wonder if the patch series could have been separated and not all sent in a 30 patch long series.
I was not clear about this, but the RFC is 30 patches so it's possible to see the big picture, but I will be breaking it into at least four series in sequence for actual review. They match the four sections described above, but will be in the opposite order:
A. git repack --full-name-hash
B. git pack-objects --path-walk
C. git survey
D. git backfill
(It's possible that git survey and git backfill may be orthogonal enough that they could be under review at the same time. Alternatively, git backfill may jump the line because it's so simple to implement once the path-walk API is established.)
Quoted 8 lines
Quoted 5 lines
3. To demonstrate the value of the path-based batching in the 'git survey' feature, and to inspire others to think about what other statistics would be valuable in that feature. (I anticipate that once a base is established, multiple contributors will help expand its functionality long into the future.)
Yeah, a better git sizer would be valuable for GitLab too and probably everyone hosting a significant number of repos.
This was definitely Jeff's intention, and I agree. Hopefully we can establish a baseline quickly and then make room for extensions as people discover new ways to investigate repo size or performance issues.
Quoted 14 lines
Quoted 7 lines
PART I: 'git backfill'
======================These patches introduce the 'git backfill' builtin including its '--batch-size' and '--sparse' options. While this is the first and simplest application, it is also the lowest priority in terms of user need.
Quoted 6 lines
* path-walk: introduce an object walk by path
* backfill: add builtin boilerplate
* backfill: basic functionality and tests
* backfill: add --batch-size= option
* backfill: add --sparse option
* backfill: assume --sparse when sparse-checkout is enabled
Quoted 5 lines
I would prefer a first patch series with all the above and the first patch creating a technical doc called maybe
Documentation/technical/path-walk.txtwhich could contain a lot of information from this RFC and perhaps technical details of how the path-walk works and how it is different from a regular walk.
My rework of the git pack-objects --path-walk series has a draft of a technical document as you suggest [1]. I regret not having time to get it done before this RFC.
Quoted 2 lines
Quoted 2 lines
DISCUSSION OF NAME HASH
=======================
...
Quoted 6 lines
Quoted 3 lines
The --full-name-hash feature does the simpler choice of replacing the name-hash method with one that has fewer collisions, but loses the benefits of "nearby" paths having close hash values.
The benefits of "nearby" paths are only available with --path-walk, not in the current way object packing works.
I suppose this depends on your definition of "nearby" and I think we are using it differently.
The --full-name-hash mechanism groups objects by their full path name, with a small probability of a collision with another path, and thus we can think about it as grouping objects by their full path. However, this lack of collision comes at a cost: having unequal but "near" hash value does not imply any similarity in the full path.
The standard name-hash algorithm has a form of "locality" that provides a nice property: paths with unequal by "near" hash values will have some similarities in the end of their path names.
Between these two mechanisms, the collisions of the standard name-hash can cause objects that should delta together to be separated in the object order due to too many objects with the same hash value, even though they have very different full path names. While the full-name-hash mechanism helps keep objects from the same path close together in the order, it fails to compute good deltas across objects from different paths, even if they may end similarly (implying similar file types or even renames across directories).
The --path-walk feature has the nice benefit that it first attempts to compute delta bases are grouped to exactly the set of objects that appear at a given path (no more, no less) but then it _also_ does the standard name-hash sort to help look for delta bases using the name-hash locality heuristic.
Quoted 8 lines
Quoted 5 lines
One natural question to consider is to think about storing both the name-hash and the full-name-hash and doing two delta passes, each one sorting the objects by a different hash function. The first issue is that we don't want to store two hash values per object, as that will significantly increase the memory pressure during repacking.
Is one more
uint32_tper object really increasing the memory pressure so much? Isn't there a way to pack bits together to avoid that?
I hesitate to conclude that four bytes per object will not have a meaningful impact. One thing that I'll be reporting on in my v2 of the --full-name-hash feature is that I've gone and tried to prototype what would happen with this kind of approach.
The short version is that I failed to make the addition of more data to this struct helpful in outperforming the --full-name-hash option. I thought that a two-pass approach would work, but something about how the two sorts worked caused problems that I could not overcome. (Maybe someone with a stronger handle on this could make it work.) My WIP branch is here [2] for anyone willing to try to succeed where I failed.
[2] https://github.com/derrickstolee/git/compare/full-name...derrickstolee:git:full-name-wip
Quoted 8 lines
Quoted 4 lines
This could be side-stepped by
storing the full-name-hash in the packing list and then a second mapping from full-name-hash to name-hash. However, that still leads to the two passes taking extra time.Isn't there a way to sort using both hash functions in a single pass? (That is to perform a single pass and when considering an object sort it using both hash functions before considering a different object.)
We could sort the objects using both hash functions (say, name-hash then full-name-hash to break ties) but then the full-name-hash values keep the similar-sized objects with the same name-hash from being sorted near each other (at least, within the short default window, which I use for all of my testing). So this ends up being ineffective. In [2] above, this is something I tested along the way and am pretty confident that it does not work as well as we'd like it to.
Quoted 4 lines
Quoted 2 lines
The --path-walk approach is faster than even a
single pass.Is there a reason for that?
My guess here is that we are finding the "best" deltas quickly, allowing us to short-circuit the possibility of other deltas due to file size differences. (If my object has size X, with a current delta of size D, then any object smaller than X - D will not be a good base.)
Quoted 18 lines
Quoted 10 lines
First, we can consider the total reachable objects in each scenario:
TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------Commits | 20579 | 10443669 | 15092790 Trees | 276503 | 40212070 | 244429615 Blobs | 294500 | 635365661 | 10791187920Quoted 7 lines
TOTAL OBJECT SIZES BY TYPE
================================================
Object Type | Count | Disk Size | Inflated Size
------------+--------+-----------+--------------Commits | 20579 | 10450605 | 15092790 Trees | 276503 | 31136263 | 244429615 Blobs | 294500 | 94442401 | 10791187920
Quoted 1 line
Using % of size reduction or increase might help make it a bit clearer here too.
True. I could compute that manually, as this data is not available in the same process. I'll try to do that in the future.
Quoted 2 lines
Quoted 2 lines
Now, we can consider the top 10 file paths before and after repacking with the --path-walk feature:
...
Quoted 6 lines
Quoted 3 lines
The nice thing about this result is that we can point to files that are taking up space because there is no other way around it, not that the naming convention for the files is causing confusion during packing.
Yeah, nice result. I guess the result is the same or very similar when the improved name-hash algorithm is used.
Yes, similar results happen there. I haven't dug into them very much to see if it helps point out anything about inefficiencies in --full-name-hash, but I don't expect any of the big differences between --full-name-hash and --path-walk to appear in these tables.
Quoted 8 lines
Quoted 8 lines
WHAT TO DO NOW?
===============Thank you for reading this far. I hope that this has added context on the patch series for the --full-name-hash option, but also provided the right context for when I come back in a week or so with a review-ready version of the --path-walk option.
Quoted 7 lines
Quoted 5 lines
These patches are rough and I want to make sure everyone knows that. Reviewing them for style or even basic organization may lead to some wasted time. I know that at least one of the patches got mangled and its diff does not match its description (I'm thinking specifically about "pack-objects:
extractshould_attempt_deltas()" but this could apply elsewhere).Ok, I will wait for the next iteration before reviewing the patches then.
Yes, I only meant for them to be available for those who were curious to explore. Please do review v2 of the --full-name-hash series [3].
[3] https://lore.kernel.org/git/[email protected]/
Quoted 8 lines
Quoted 4 lines
But I think that interested parties could take my branch, build it, and give these features a try on their favorite repos. I'd love to hear feedback on the usability and effectiveness, especially if someone finds a case where the --path-walk option is less effective at packing data.
I might do that when reviewing the patch series later. I think it can still be valuable for you to get some first feedback from just reading this RFC.
Thank you! And thanks for the thoughts on this very long cover letter.
-Stolee
Quoted 15 lines · Derrick Stolee
Combining the two features actually ends up with very similar performance to what
--full-name-hashalready does. It's actually important that the--path-walkoption does a full pass of the objects via the standard name-hash after its first pass in groups based on the path.
...
I was not clear about this, but the RFC is 30 patches so it's possible to see the big picture, but I will be breaking it into at least four series in sequence for actual review. They match the four sections described above, but will be in the opposite order:A.
git repack --full-name-hash
B.git pack-objects --path-walk
C.git survey
D.git backfill
Quoted 4 lines
(It's possible that
git surveyandgit backfillmay be orthogonal enough that they could be under review at the same time. Alternatively,git backfillmay jump the line because it's so simple to implement once the path-walk API is established.)
I actually was hoping to hear something like "since it turns out that --path-walk gives a better performance and it does not regress small incremental transfer like --full-name-hash does, the real series drops --full-name hash", i.e. without part (A). That reduces things we need to worry about (like having to either keep track of two "hashes" per object, or making small incremental transfer more costly) greatly.
Thanks.
Quoted 4 lines · Derrick Stolee via GitGitGadget
This RFC is ultimately about introducing a new way to walk objects, called the "path-walk API" in the new path-walk.[ch] files. Before digging into the details of the API, let's discuss the applications which will hint at the API's design.
This series is superbly well-presented. I’m in
awe here from the peanut gallery.
Signature · 1 line
Kristoffer Haugsbakk
Quoted 22 lines · Junio C Hamano
Quoted 15 lines · Derrick Stolee
Combining the two features actually ends up with very similar performance to what
--full-name-hashalready does. It's actually important that the--path-walkoption does a full pass of the objects via the standard name-hash after its first pass in groups based on the path.
...
I was not clear about this, but the RFC is 30 patches so it's possible to see the big picture, but I will be breaking it into at least four series in sequence for actual review. They match the four sections described above, but will be in the opposite order:A. `git repack --full-name-hash` B. `git pack-objects --path-walk` C. `git survey` D. `git backfill`Quoted 4 lines
(It's possible that
git surveyandgit backfillmay be orthogonal enough that they could be under review at the same time. Alternatively,git backfillmay jump the line because it's so simple to implement once the path-walk API is established.)
Quoted 7 lines
I actually was hoping to hear something like "since it turns out that --path-walk gives a better performance and it does not regress small incremental transfer like --full-name-hash does, the real series drops --full-name hash", i.e. without part (A). That reduces things we need to worry about (like having to either keep track of two "hashes" per object, or making small incremental transfer more costly) greatly.
I believe that the --full-name-hash version still has some benefits, in that it could better integrate with reachability bitmaps and delta islands:
1. The .bitmap file format would need a modification in order to signal which hash function is being used for compatibility reasons, but this does seem within reach without too much work.
2. The delta islands feature integrates seamlessly with --full-name-hash and seems difficult to integrate with the --path-walk feature. Either we would need to have a second object walk to get the delta island markers, or somehow put the passing of the object markers into the path-walk API itself (similar to how it needs to push the UNINTERESTING bit around during the walk).
I'm not recommending any version that requires tracking two hash values per object, as I have not been able to demonstrate any improvement when doing so.
But, it would be helpful to know if the --full-name-hash feature should not be pursued due to the --path-walk feature being prepared shortly after it. I can see an argument for either direction: having a new hash algorithm provides a smaller change to get most of the results for the full repack case, but gets worse performance in many push scenarios. This is the point of an RFC, to get questions like this worked out based on the "big picture" view of everything.
Perhaps I should pause the --full-name-hash topic and focus on getting the --path-walk topic up and running. I am curious to hear from folks who are currently running Git servers about their thoughts on these trade-offs and potential uses in their environment. My needs on the client side are solved by the --path-walk approach.
Thanks,
-Stolee
Quoted 5 lines · Derrick Stolee
... I can see an argument for either direction: having a new hash algorithm provides a smaller change to get most of the results for the full repack case, but gets worse performance in many push scenarios. This is the point of an RFC, to get questions like this worked out based on the "big picture" view of everything.
Exactly. We might want to use the series as an example in our developer docs on how to propose a large-ish effort.
Quoted 5 lines
Perhaps I should pause the --full-name-hash topic and focus on getting the --path-walk topic up and running. I am curious to hear from folks who are currently running Git servers about their thoughts on these trade-offs and potential uses in their environment. My needs on the client side are solved by the --path-walk approach.
Yeah, such third-party inputs would be very useful.
Thanks.