#include "git-compat-util.h" #include "pseudo-merge.h" #include "date.h" #include "oid-array.h" #include "strbuf.h" #include "config.h" #include "string-list.h" #include "refs.h" #include "pack-bitmap.h" #include "commit.h" #include "alloc.h" #include "progress.h" #include "hex.h" #define DEFAULT_PSEUDO_MERGE_DECAY 1.0 #define DEFAULT_PSEUDO_MERGE_MAX_MERGES 64 #define DEFAULT_PSEUDO_MERGE_SAMPLE_RATE 1 #define DEFAULT_PSEUDO_MERGE_THRESHOLD approxidate("1.week.ago") #define DEFAULT_PSEUDO_MERGE_STABLE_THRESHOLD approxidate("1.month.ago") #define DEFAULT_PSEUDO_MERGE_STABLE_SIZE 512 static double gitexp(double base, int exp) { double result = 1; while (1) { if (exp % 2) result *= base; exp >>= 1; if (!exp) break; base *= base; } return result; } static uint32_t pseudo_merge_group_size(const struct pseudo_merge_group *group, const struct pseudo_merge_matches *matches, uint32_t i) { double C = 0.0f; uint32_t n; /* * The size of pseudo-merge groups decays according to a power series, * which looks like: * * f(n) = C * n^-k * * , where 'n' is the n-th pseudo-merge group, 'f(n)' is its size, 'k' * is the decay rate, and 'C' is a scaling value. * * The value of C depends on the number of groups, decay rate, and total * number of commits. It is computed such that if there are M and N * total groups and commits, respectively, that: * * N = f(0) + f(1) + ... f(M-1) * * Rearranging to isolate C, we get: * * N = \sum_{n=1}^M C / n^k * * N / C = \sum_{n=1}^M n^-k * * C = N / \sum_{n=1}^M n^-k * * For example, if we have a decay rate of 'k' being equal to 1.5, 'N' * total commits equal to 10,000, and 'M' being equal to 6 groups, then * the (rounded) group sizes are: * * { 5469, 1934, 1053, 684, 489, 372 } * * increasing the number of total groups, say to 10, scales the group * sizes appropriately: * * { 5012, 1772, 964, 626, 448, 341, 271, 221, 186, 158 } */ for (n = 0; n < group->max_merges; n++) C += 1.0 / gitexp(n + 1, group->decay); C = matches->unstable_nr / C; return (uint32_t)((C / gitexp(i + 1, group->decay)) + 0.5); } static void pseudo_merge_group_init(struct pseudo_merge_group *group) { memset(group, 0, sizeof(struct pseudo_merge_group)); strmap_init_with_options(&group->matches, NULL, 0); group->decay = DEFAULT_PSEUDO_MERGE_DECAY; group->max_merges = DEFAULT_PSEUDO_MERGE_MAX_MERGES; group->sample_rate = DEFAULT_PSEUDO_MERGE_SAMPLE_RATE; group->threshold = DEFAULT_PSEUDO_MERGE_THRESHOLD; group->stable_threshold = DEFAULT_PSEUDO_MERGE_STABLE_THRESHOLD; group->stable_size = DEFAULT_PSEUDO_MERGE_STABLE_SIZE; } static int pseudo_merge_config(const char *var, const char *value, const struct config_context *ctx, void *cb_data) { struct string_list *list = cb_data; struct string_list_item *item; struct pseudo_merge_group *group; struct strbuf buf = STRBUF_INIT; const char *sub, *key; size_t sub_len; int ret = 0; if (parse_config_key(var, "bitmappseudomerge", &sub, &sub_len, &key)) goto done; if (!sub_len) goto done; strbuf_add(&buf, sub, sub_len); item = string_list_lookup(list, buf.buf); if (!item) { item = string_list_insert(list, buf.buf); item->util = xmalloc(sizeof(struct pseudo_merge_group)); pseudo_merge_group_init(item->util); } group = item->util; if (!strcmp(key, "pattern")) { struct strbuf re = STRBUF_INIT; free(group->pattern); if (*value != '^') strbuf_addch(&re, '^'); strbuf_addstr(&re, value); group->pattern = xcalloc(1, sizeof(regex_t)); if (regcomp(group->pattern, re.buf, REG_EXTENDED)) die(_("failed to load pseudo-merge regex for %s: '%s'"), sub, re.buf); strbuf_release(&re); } else if (!strcmp(key, "decay")) { group->decay = git_config_double(var, value, ctx->kvi); if (group->decay < 0) { warning(_("%s must be non-negative, using default"), var); group->decay = DEFAULT_PSEUDO_MERGE_DECAY; } } else if (!strcmp(key, "samplerate")) { group->sample_rate = git_config_double(var, value, ctx->kvi); if (!(0 <= group->sample_rate && group->sample_rate <= 1)) { warning(_("%s must be between 0 and 1, using default"), var); group->sample_rate = DEFAULT_PSEUDO_MERGE_SAMPLE_RATE; } } else if (!strcmp(key, "threshold")) { if (git_config_expiry_date(&group->threshold, var, value)) { ret = -1; goto done; } } else if (!strcmp(key, "maxmerges")) { group->max_merges = git_config_int(var, value, ctx->kvi); if (group->max_merges < 0) { warning(_("%s must be non-negative, using default"), var); group->max_merges = DEFAULT_PSEUDO_MERGE_MAX_MERGES; } } else if (!strcmp(key, "stablethreshold")) { if (git_config_expiry_date(&group->stable_threshold, var, value)) { ret = -1; goto done; } } else if (!strcmp(key, "stablesize")) { group->stable_size = git_config_int(var, value, ctx->kvi); if (group->stable_size <= 0) { warning(_("%s must be positive, using default"), var); group->stable_size = DEFAULT_PSEUDO_MERGE_STABLE_SIZE; } } done: strbuf_release(&buf); return ret; } void load_pseudo_merges_from_config(struct string_list *list) { struct string_list_item *item; git_config(pseudo_merge_config, list); for_each_string_list_item(item, list) { struct pseudo_merge_group *group = item->util; if (!group->pattern) die(_("pseudo-merge group '%s' missing required pattern"), item->string); if (group->threshold < group->stable_threshold) die(_("pseudo-merge group '%s' has unstable threshold " "before stable one"), item->string); } } static int find_pseudo_merge_group_for_ref(const char *refname, const struct object_id *oid, int flags UNUSED, void *_data) { struct bitmap_writer *writer = _data; struct object_id peeled; struct commit *c; uint32_t i; int has_bitmap; if (!peel_iterated_oid(the_repository, oid, &peeled)) oid = &peeled; c = lookup_commit(the_repository, oid); if (!c) return 0; has_bitmap = bitmap_writer_has_bitmapped_object_id(writer, oid); for (i = 0; i < writer->pseudo_merge_groups.nr; i++) { struct pseudo_merge_group *group; struct pseudo_merge_matches *matches; struct strbuf group_name = STRBUF_INIT; regmatch_t captures[16]; size_t j; group = writer->pseudo_merge_groups.items[i].util; if (regexec(group->pattern, refname, ARRAY_SIZE(captures), captures, 0)) continue; if (captures[ARRAY_SIZE(captures) - 1].rm_so != -1) warning(_("pseudo-merge regex from config has too many capture " "groups (max=%"PRIuMAX")"), (uintmax_t)ARRAY_SIZE(captures) - 2); for (j = !!group->pattern->re_nsub; j < ARRAY_SIZE(captures); j++) { regmatch_t *match = &captures[j]; if (match->rm_so == -1) continue; if (group_name.len) strbuf_addch(&group_name, '-'); strbuf_add(&group_name, refname + match->rm_so, match->rm_eo - match->rm_so); } matches = strmap_get(&group->matches, group_name.buf); if (!matches) { matches = xcalloc(1, sizeof(*matches)); strmap_put(&group->matches, strbuf_detach(&group_name, NULL), matches); } if (c->date <= group->stable_threshold) { ALLOC_GROW(matches->stable, matches->stable_nr + 1, matches->stable_alloc); matches->stable[matches->stable_nr++] = c; } else if (c->date <= group->threshold && !has_bitmap) { ALLOC_GROW(matches->unstable, matches->unstable_nr + 1, matches->unstable_alloc); matches->unstable[matches->unstable_nr++] = c; } strbuf_release(&group_name); } return 0; } static struct commit *push_pseudo_merge(struct pseudo_merge_group *group) { struct commit *merge; ALLOC_GROW(group->merges, group->merges_nr + 1, group->merges_alloc); merge = alloc_commit_node(the_repository); merge->object.parsed = 1; merge->object.flags |= BITMAP_PSEUDO_MERGE; group->merges[group->merges_nr++] = merge; return merge; } static struct pseudo_merge_commit_idx *pseudo_merge_idx(kh_oid_map_t *pseudo_merge_commits, const struct object_id *oid) { struct pseudo_merge_commit_idx *pmc; int hash_ret; khiter_t hash_pos = kh_put_oid_map(pseudo_merge_commits, *oid, &hash_ret); if (hash_ret) { CALLOC_ARRAY(pmc, 1); kh_value(pseudo_merge_commits, hash_pos) = pmc; } else { pmc = kh_value(pseudo_merge_commits, hash_pos); } return pmc; } #define MIN_PSEUDO_MERGE_SIZE 8 static void select_pseudo_merges_1(struct bitmap_writer *writer, struct pseudo_merge_group *group, struct pseudo_merge_matches *matches) { uint32_t i, j; uint32_t stable_merges_nr; if (!matches->stable_nr && !matches->unstable_nr) return; /* all tips in this group already have bitmaps */ stable_merges_nr = matches->stable_nr / group->stable_size; if (matches->stable_nr % group->stable_size) stable_merges_nr++; /* make stable_merges_nr pseudo merges for stable commits */ for (i = 0, j = 0; i < stable_merges_nr; i++) { struct commit *merge; struct commit_list **p; merge = push_pseudo_merge(group); p = &merge->parents; /* * For each pseudo-merge created above, add parents to the * allocated commit node from the stable set of commits * (un-bitmapped, newer than the stable threshold). */ do { struct commit *c; struct pseudo_merge_commit_idx *pmc; if (j >= matches->stable_nr) break; c = matches->stable[j++]; /* * Here and below, make sure that we keep our mapping of * commits -> pseudo-merge(s) which include the key'd * commit up-to-date. */ pmc = pseudo_merge_idx(writer->pseudo_merge_commits, &c->object.oid); ALLOC_GROW(pmc->pseudo_merge, pmc->nr + 1, pmc->alloc); pmc->pseudo_merge[pmc->nr++] = writer->pseudo_merges_nr; p = commit_list_append(c, p); } while (j % group->stable_size); bitmap_writer_push_commit(writer, merge, 1); writer->pseudo_merges_nr++; } /* make up to group->max_merges pseudo merges for unstable commits */ for (i = 0, j = 0; i < group->max_merges; i++) { struct commit *merge; struct commit_list **p; uint32_t size, end; merge = push_pseudo_merge(group); p = &merge->parents; size = pseudo_merge_group_size(group, matches, i); end = size < MIN_PSEUDO_MERGE_SIZE ? matches->unstable_nr : j + size; /* * For each pseudo-merge commit created above, add parents to * the allocated commit node from the unstable set of commits * (newer than the stable threshold). * * Account for the sample rate, since not every candidate from * the set of stable commits will be included as a pseudo-merge * parent. */ for (; j < end && j < matches->unstable_nr; j++) { struct commit *c = matches->unstable[j]; struct pseudo_merge_commit_idx *pmc; if (j % (uint32_t)(1.0 / group->sample_rate)) continue; pmc = pseudo_merge_idx(writer->pseudo_merge_commits, &c->object.oid); ALLOC_GROW(pmc->pseudo_merge, pmc->nr + 1, pmc->alloc); pmc->pseudo_merge[pmc->nr++] = writer->pseudo_merges_nr; p = commit_list_append(c, p); } bitmap_writer_push_commit(writer, merge, 1); writer->pseudo_merges_nr++; if (end >= matches->unstable_nr) break; } } static int commit_date_cmp(const void *va, const void *vb) { timestamp_t a = (*(const struct commit **)va)->date; timestamp_t b = (*(const struct commit **)vb)->date; if (a < b) return -1; else if (a > b) return 1; return 0; } static void sort_pseudo_merge_matches(struct pseudo_merge_matches *matches) { QSORT(matches->stable, matches->stable_nr, commit_date_cmp); QSORT(matches->unstable, matches->unstable_nr, commit_date_cmp); } void select_pseudo_merges(struct bitmap_writer *writer, struct commit **commits, size_t commits_nr) { struct progress *progress = NULL; uint32_t i; if (!writer->pseudo_merge_groups.nr) return; if (writer->show_progress) progress = start_progress("Selecting pseudo-merge commits", writer->pseudo_merge_groups.nr); refs_for_each_ref(get_main_ref_store(the_repository), find_pseudo_merge_group_for_ref, writer); for (i = 0; i < writer->pseudo_merge_groups.nr; i++) { struct pseudo_merge_group *group; struct hashmap_iter iter; struct strmap_entry *e; group = writer->pseudo_merge_groups.items[i].util; strmap_for_each_entry(&group->matches, &iter, e) { struct pseudo_merge_matches *matches = e->value; sort_pseudo_merge_matches(matches); select_pseudo_merges_1(writer, group, matches); } display_progress(progress, i + 1); } stop_progress(&progress); } void free_pseudo_merge_map(struct pseudo_merge_map *pm) { uint32_t i; for (i = 0; i < pm->nr; i++) { ewah_pool_free(pm->v[i].commits); ewah_pool_free(pm->v[i].bitmap); } free(pm->v); } struct pseudo_merge_commit_ext { uint32_t nr; const unsigned char *ptr; }; static int pseudo_merge_ext_at(const struct pseudo_merge_map *pm, struct pseudo_merge_commit_ext *ext, size_t at) { if (at >= pm->map_size) return error(_("extended pseudo-merge read out-of-bounds " "(%"PRIuMAX" >= %"PRIuMAX")"), (uintmax_t)at, (uintmax_t)pm->map_size); if (at + 4 >= pm->map_size) return error(_("extended pseudo-merge entry is too short " "(%"PRIuMAX" >= %"PRIuMAX")"), (uintmax_t)(at + 4), (uintmax_t)pm->map_size); ext->nr = get_be32(pm->map + at); ext->ptr = pm->map + at + sizeof(uint32_t); return 0; } struct ewah_bitmap *pseudo_merge_bitmap(const struct pseudo_merge_map *pm, struct pseudo_merge *merge) { if (!merge->loaded_commits) BUG("cannot use unloaded pseudo-merge bitmap"); if (!merge->loaded_bitmap) { size_t at = merge->bitmap_at; merge->bitmap = read_bitmap(pm->map, pm->map_size, &at); merge->loaded_bitmap = 1; } return merge->bitmap; } struct pseudo_merge *use_pseudo_merge(const struct pseudo_merge_map *pm, struct pseudo_merge *merge) { if (!merge->loaded_commits) { size_t pos = merge->at; merge->commits = read_bitmap(pm->map, pm->map_size, &pos); merge->bitmap_at = pos; merge->loaded_commits = 1; } return merge; } static struct pseudo_merge *pseudo_merge_at(const struct pseudo_merge_map *pm, struct object_id *oid, size_t want) { size_t lo = 0; size_t hi = pm->nr; while (lo < hi) { size_t mi = lo + (hi - lo) / 2; size_t got = pm->v[mi].at; if (got == want) return use_pseudo_merge(pm, &pm->v[mi]); else if (got < want) hi = mi; else lo = mi + 1; } warning(_("could not find pseudo-merge for commit %s at offset %"PRIuMAX), oid_to_hex(oid), (uintmax_t)want); return NULL; } struct pseudo_merge_commit { uint32_t commit_pos; uint64_t pseudo_merge_ofs; }; #define PSEUDO_MERGE_COMMIT_RAWSZ (sizeof(uint32_t)+sizeof(uint64_t)) static void read_pseudo_merge_commit_at(struct pseudo_merge_commit *merge, const unsigned char *at) { merge->commit_pos = get_be32(at); merge->pseudo_merge_ofs = get_be64(at + sizeof(uint32_t)); } static int nth_pseudo_merge_ext(const struct pseudo_merge_map *pm, struct pseudo_merge_commit_ext *ext, struct pseudo_merge_commit *merge, uint32_t n) { size_t ofs; if (n >= ext->nr) return error(_("extended pseudo-merge lookup out-of-bounds " "(%"PRIu32" >= %"PRIu32")"), n, ext->nr); ofs = get_be64(ext->ptr + st_mult(n, sizeof(uint64_t))); if (ofs >= pm->map_size) return error(_("out-of-bounds read: (%"PRIuMAX" >= %"PRIuMAX")"), (uintmax_t)ofs, (uintmax_t)pm->map_size); read_pseudo_merge_commit_at(merge, pm->map + ofs); return 0; } static unsigned apply_pseudo_merge(const struct pseudo_merge_map *pm, struct pseudo_merge *merge, struct bitmap *result, struct bitmap *roots) { if (merge->satisfied) return 0; if (!ewah_bitmap_is_subset(merge->commits, roots ? roots : result)) return 0; bitmap_or_ewah(result, pseudo_merge_bitmap(pm, merge)); if (roots) bitmap_or_ewah(roots, pseudo_merge_bitmap(pm, merge)); merge->satisfied = 1; return 1; } static int pseudo_merge_commit_cmp(const void *va, const void *vb) { struct pseudo_merge_commit merge; uint32_t key = *(uint32_t*)va; read_pseudo_merge_commit_at(&merge, vb); if (key < merge.commit_pos) return -1; if (key > merge.commit_pos) return 1; return 0; } static struct pseudo_merge_commit *find_pseudo_merge(const struct pseudo_merge_map *pm, uint32_t pos) { if (!pm->commits_nr) return NULL; return bsearch(&pos, pm->commits, pm->commits_nr, PSEUDO_MERGE_COMMIT_RAWSZ, pseudo_merge_commit_cmp); } int apply_pseudo_merges_for_commit(const struct pseudo_merge_map *pm, struct bitmap *result, struct commit *commit, uint32_t commit_pos) { struct pseudo_merge *merge; struct pseudo_merge_commit *merge_commit; int ret = 0; merge_commit = find_pseudo_merge(pm, commit_pos); if (!merge_commit) return 0; if (merge_commit->pseudo_merge_ofs & ((uint64_t)1<<63)) { struct pseudo_merge_commit_ext ext = { 0 }; off_t ofs = merge_commit->pseudo_merge_ofs & ~((uint64_t)1<<63); uint32_t i; if (pseudo_merge_ext_at(pm, &ext, ofs) < -1) { warning(_("could not read extended pseudo-merge table " "for commit %s"), oid_to_hex(&commit->object.oid)); return ret; } for (i = 0; i < ext.nr; i++) { if (nth_pseudo_merge_ext(pm, &ext, merge_commit, i) < 0) return ret; merge = pseudo_merge_at(pm, &commit->object.oid, merge_commit->pseudo_merge_ofs); if (!merge) return ret; if (apply_pseudo_merge(pm, merge, result, NULL)) ret++; } } else { merge = pseudo_merge_at(pm, &commit->object.oid, merge_commit->pseudo_merge_ofs); if (!merge) return ret; if (apply_pseudo_merge(pm, merge, result, NULL)) ret++; } if (ret) cascade_pseudo_merges(pm, result, NULL); return ret; } int cascade_pseudo_merges(const struct pseudo_merge_map *pm, struct bitmap *result, struct bitmap *roots) { unsigned any_satisfied; int ret = 0; do { struct pseudo_merge *merge; uint32_t i; any_satisfied = 0; for (i = 0; i < pm->nr; i++) { merge = use_pseudo_merge(pm, &pm->v[i]); if (apply_pseudo_merge(pm, merge, result, roots)) { any_satisfied |= 1; ret++; } } } while (any_satisfied); return ret; } struct pseudo_merge *pseudo_merge_for_parents(const struct pseudo_merge_map *pm, struct bitmap *parents) { struct pseudo_merge *match = NULL; size_t i; if (!pm->nr) return NULL; /* * NOTE: this loop is quadratic in the worst-case (where no * matching pseudo-merge bitmaps are found), but in practice * this is OK for a few reasons: * * - Rejecting pseudo-merge bitmaps that do not match the * given commit is done quickly (i.e. `bitmap_equals_ewah()` * returns early when we know the two bitmaps aren't equal. * * - Already matched pseudo-merge bitmaps (which we track with * the `->satisfied` bit here) are skipped as potential * candidates. * * - The number of pseudo-merges should be small (in the * hundreds for most repositories). * * If in the future this semi-quadratic behavior does become a * problem, another approach would be to keep track of which * pseudo-merges are still "viable" after enumerating the * pseudo-merge commit's parents: * * - A pseudo-merge bitmap becomes non-viable when the bit(s) * corresponding to one or more parent(s) of the given * commit are not set in a candidate pseudo-merge's commits * bitmap. * * - After processing all bits, enumerate the remaining set of * viable pseudo-merge bitmaps, and check that their * popcount() matches the number of parents in the given * commit. */ for (i = 0; i < pm->nr; i++) { struct pseudo_merge *candidate = use_pseudo_merge(pm, &pm->v[i]); if (!candidate || candidate->satisfied) continue; if (!bitmap_equals_ewah(parents, candidate->commits)) continue; match = candidate; match->satisfied = 1; break; } return match; }