git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 6/7] merge-ort: avoid recursing into directories when we don't need to

From
Elijah Newren via GitGitGadget <gitgitgadget@gmail.com>
Date
Jul 1, 2021, 03:46 UTC
Message-ID
<3409a6cd631deb361d3ecb94491c0c297c52fb53.1625111177.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.988.git.1625111177.gitgitgadget@gmail.com>
From: Elijah Newren <newren@gmail.com>

This combines the work of the last several patches, and implements the conditions when we don't need to recurse into directories. It's perhaps easiest to see the logic by separating the fact that a directory might have both rename sources and rename destinations:

  * rename sources: only files present in the merge base can serve as
    rename sources, and only when one side deletes that file.  When the
    tree on one side matches the merge base, that means every file
    within the subtree matches the merge base.  This means that the
    skip-irrelevant-rename-detection optimization from before kicks in
    and we don't need any of these files as rename sources.
  * rename destinations: the tree that does not match the merge base
    might have newly added and hence unmatched destination files.
    This is what usually prevents us from doing trivial directory
    resolutions in the merge machinery.  However, the fact that we have
    deferred recursing into this directory until the end means we know
    whether there are any unmatched relevant potential rename sources
    elsewhere in this merge.  If there are no unmatched such relevant
    sources anywhere, then there is no need to look for unmatched
    potential rename destinations to match them with.
This informs our algorithm:
  * Search through relevant_sources; if we have entries, they better all
    be reflected in cached_pairs or cached_irrelevant, otherwise they
    represent an unmatched potential rename source (causing the
    optimization to be disallowed).
  * For any relevant_source represented in cached_pairs, we do need to
    to make sure to get the destination for each source, meaning we need
    to recurse into any ancestor directories of those destinations.
  * Once we've recursed into all the rename destinations for any
    relevant_sources in cached_pairs, we can then do the trivial
    directory resolution for the remaining directories.

For the testcases mentioned in commit 557ac0350d ("merge-ort: begin performance work; instrument with trace2_region_* calls", 2020-10-28), this change improves the performance as follows:

                            Before                  After
    no-renames:        5.235 s ±  0.042 s   205.1  ms ±  3.8  ms
    mega-renames:      9.419 s ±  0.107 s     1.564 s ±  0.010 s
    just-one-mega:   480.1  ms ±  3.9  ms   479.5  ms ±  3.9  ms
Signed-off-by: Elijah Newren <newren@gmail.com>
---
 merge-ort.c | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 98 insertions(+), 3 deletions(-)
diff --git a/merge-ort.c b/merge-ort.c
index bf0712d18a0..c9cf7a158c8 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -1223,6 +1223,18 @@ static int collect_merge_info_callback(int n,
 	return mask;
 }
 
+static void resolve_trivial_directory_merge(struct conflict_info *ci, int side)
+{
+	VERIFY_CI(ci);
+	assert((side == 1 && ci->match_mask == 5) ||
+	       (side == 2 && ci->match_mask == 3));
+	oidcpy(&ci->merged.result.oid, &ci->stages[side].oid);
+	ci->merged.result.mode = ci->stages[side].mode;
+	ci->merged.is_null = is_null_oid(&ci->stages[side].oid);
+	ci->match_mask = 0;
+	ci->merged.clean = 1; /* (ci->filemask == 0); */
+}
+
 static int handle_deferred_entries(struct merge_options *opt,
 				   struct traverse_info *info)
 {
@@ -1232,9 +1244,71 @@ static int handle_deferred_entries(struct merge_options *opt,
 	int side, ret = 0;
 
 	for (side = MERGE_SIDE1; side <= MERGE_SIDE2; side++) {
-		renames->trivial_merges_okay[side] = 0;
-		strintmap_for_each_entry(&renames->possible_trivial_merges[side],
-					 &iter, entry) {
+		unsigned optimization_okay = 1;
+		struct strintmap copy;
+
+		/* Loop over the set of paths we need to know rename info for */
+		strset_for_each_entry(&renames->relevant_sources[side],
+				      &iter, entry) {
+			char *rename_target, *dir, *dir_marker;
+			struct strmap_entry *e;
+
+			/*
+			 * if we don't know delete/rename info for this path,
+			 * then we need to recurse into all trees to get all
+			 * adds to make sure we have it.
+			 */
+			if (strset_contains(&renames->cached_irrelevant[side],
+					    entry->key))
+				continue;
+			e = strmap_get_entry(&renames->cached_pairs[side],
+					     entry->key);
+			if (!e) {
+				optimization_okay = 0;
+				break;
+			}
+
+			/* If this is a delete, we have enough info already */
+			rename_target = e->value;
+			if (!rename_target)
+				continue;
+
+			/* If we already walked the rename target, we're good */
+			if (strmap_contains(&opt->priv->paths, rename_target))
+				continue;
+
+			/*
+			 * Otherwise, we need to get a list of directories that
+			 * will need to be recursed into to get this
+			 * rename_target.
+			 */
+			dir = xstrdup(rename_target);
+			while ((dir_marker = strrchr(dir, '/'))) {
+				*dir_marker = '\0';
+				if (strset_contains(&renames->target_dirs[side],
+						    dir))
+					break;
+				strset_add(&renames->target_dirs[side], dir);
+			}
+			free(dir);
+		}
+		renames->trivial_merges_okay[side] = optimization_okay;
+		/*
+		 * We need to recurse into any directories in
+		 * possible_trivial_merges[side] found in target_dirs[side].
+		 * But when we recurse, we may need to queue up some of the
+		 * subdirectories for possible_trivial_merges[side].  Since
+		 * we can't safely iterate through a hashmap while also adding
+		 * entries, move the entries into 'copy', iterate over 'copy',
+		 * and then we'll also iterate anything added into
+		 * possible_trivial_merges[side] once this loop is done.
+		 */
+		copy = renames->possible_trivial_merges[side];
+		strintmap_init_with_options(&renames->possible_trivial_merges[side],
+					    0,
+					    NULL,
+					    0);
+		strintmap_for_each_entry(&copy, &iter, entry) {
 			const char *path = entry->key;
 			unsigned dir_rename_mask = (intptr_t)entry->value;
 			struct conflict_info *ci;
@@ -1247,6 +1321,13 @@ static int handle_deferred_entries(struct merge_options *opt,
 			VERIFY_CI(ci);
 			dirmask = ci->dirmask;
 
+			if (optimization_okay &&
+			    !strset_contains(&renames->target_dirs[side],
+					     path)) {
+				resolve_trivial_directory_merge(ci, side);
+				continue;
+			}
+
 			info->name = path;
 			info->namelen = strlen(path);
 			info->pathlen = info->namelen + 1;
@@ -1282,6 +1363,20 @@ static int handle_deferred_entries(struct merge_options *opt,
 			if (ret < 0)
 				return ret;
 		}
+		strintmap_clear(&copy);
+		strintmap_for_each_entry(&renames->possible_trivial_merges[side],
+					 &iter, entry) {
+			const char *path = entry->key;
+			struct conflict_info *ci;
+
+			ci = strmap_get(&opt->priv->paths, path);
+			VERIFY_CI(ci);
+
+			assert(renames->trivial_merges_okay[side] &&
+			       !strset_contains(&renames->target_dirs[side],
+						path));
+			resolve_trivial_directory_merge(ci, side);
+		}
 	}
 	return ret;
 }
-- 
gitgitgadget
Previous: Elijah Newren via GitGitGadgetNext: Elijah Newren via GitGitGadget
Message 7 of 52 in “Optimization batch 14: trivial directory resolution”
  1. 0/7 Optimization batch 14: trivial directory resolutionElijah Newren via GitGitGadget, Jul 1, 2021
  2. 2/7 merge-ort: add some more explanations in collect_merge_info_callback()Elijah Newren via GitGitGadget, Jul 1, 2021
  3. 1/7 merge-ort: resolve paths early when we have sufficient informationElijah Newren via GitGitGadget, Jul 1, 2021
  4. 5/7 merge-ort: defer recursing into directories when merge base is matchedElijah Newren via GitGitGadget, Jul 1, 2021
  5. 4/7 merge-ort: add a handle_deferred_entries() helper functionElijah Newren via GitGitGadget, Jul 1, 2021
  6. 3/7 merge-ort: add data structures for allowable trivial directory resolvesElijah Newren via GitGitGadget, Jul 1, 2021
  7. 6/7 merge-ort: avoid recursing into directories when we don't need toElijah Newren via GitGitGadget, Jul 1, 2021
  8. 7/7 merge-ort: restart merge with cached renames to reduce process entry costElijah Newren via GitGitGadget, Jul 1, 2021
  9. Ævar Arnfjörð BjarmasonJul 1, 2021
  10. Elijah NewrenJul 1, 2021
  11. Elijah NewrenJul 1, 2021
  12. 0/7 Optimization batch 14: trivial directory resolutionElijah Newren via GitGitGadget, Jul 13, 2021
  13. 1/7 merge-ort: resolve paths early when we have sufficient informationElijah Newren via GitGitGadget, Jul 13, 2021
  14. 2/7 merge-ort: add some more explanations in collect_merge_info_callback()Elijah Newren via GitGitGadget, Jul 13, 2021
  15. Bagas SanjayaJul 13, 2021
  16. Elijah NewrenJul 14, 2021
  17. 3/7 merge-ort: add data structures for allowable trivial directory resolvesElijah Newren via GitGitGadget, Jul 13, 2021
  18. Derrick StoleeJul 15, 2021
  19. Elijah NewrenJul 15, 2021
  20. 7/7 merge-ort: restart merge with cached renames to reduce process entry costElijah Newren via GitGitGadget, Jul 13, 2021
  21. Derrick StoleeJul 15, 2021
  22. Elijah NewrenJul 15, 2021
  23. Derrick StoleeJul 15, 2021
  24. Elijah NewrenJul 15, 2021
  25. 4/7 merge-ort: add a handle_deferred_entries() helper functionElijah Newren via GitGitGadget, Jul 13, 2021
  26. Derrick StoleeJul 15, 2021
  27. Elijah NewrenJul 15, 2021
  28. 5/7 merge-ort: defer recursing into directories when merge base is matchedElijah Newren via GitGitGadget, Jul 13, 2021
  29. Derrick StoleeJul 15, 2021
  30. Elijah NewrenJul 15, 2021
  31. Derrick StoleeJul 15, 2021
  32. 6/7 merge-ort: avoid recursing into directories when we don't need toElijah Newren via GitGitGadget, Jul 13, 2021
  33. Derrick StoleeJul 15, 2021
  34. Elijah NewrenJul 15, 2021
  35. 0/7 Optimization batch 14: trivial directory resolutionElijah Newren via GitGitGadget, Jul 16, 2021
  36. 1/7 merge-ort: resolve paths early when we have sufficient informationElijah Newren via GitGitGadget, Jul 16, 2021
  37. 2/7 merge-ort: add some more explanations in collect_merge_info_callback()Elijah Newren via GitGitGadget, Jul 16, 2021
  38. 4/7 merge-ort: add a handle_deferred_entries() helper functionElijah Newren via GitGitGadget, Jul 16, 2021
  39. 3/7 merge-ort: add data structures for allowable trivial directory resolvesElijah Newren via GitGitGadget, Jul 16, 2021
  40. 5/7 merge-ort: defer recursing into directories when merge base is matchedElijah Newren via GitGitGadget, Jul 16, 2021
  41. 6/7 merge-ort: avoid recursing into directories when we don't need toElijah Newren via GitGitGadget, Jul 16, 2021
  42. 7/7 merge-ort: restart merge with cached renames to reduce process entry costElijah Newren via GitGitGadget, Jul 16, 2021
  43. Derrick StoleeJul 20, 2021
  44. Junio C HamanoJul 20, 2021
  45. 0/7 Optimization batch 14: trivial directory resolutionElijah Newren via GitGitGadget, Jul 21, 2021
  46. 1/7 merge-ort: resolve paths early when we have sufficient informationElijah Newren via GitGitGadget, Jul 21, 2021
  47. 2/7 merge-ort: add some more explanations in collect_merge_info_callback()Elijah Newren via GitGitGadget, Jul 21, 2021
  48. 3/7 merge-ort: add data structures for allowable trivial directory resolvesElijah Newren via GitGitGadget, Jul 21, 2021
  49. 4/7 merge-ort: add a handle_deferred_entries() helper functionElijah Newren via GitGitGadget, Jul 21, 2021
  50. 5/7 merge-ort: defer recursing into directories when merge base is matchedElijah Newren via GitGitGadget, Jul 21, 2021
  51. 6/7 merge-ort: avoid recursing into directories when we don't need toElijah Newren via GitGitGadget, Jul 21, 2021
  52. 7/7 merge-ort: restart merge with cached renames to reduce process entry costElijah Newren via GitGitGadget, Jul 21, 2021

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

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