summaryrefslogtreecommitdiff
path: root/src/libgit2/mwindow.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libgit2/mwindow.c')
-rw-r--r--src/libgit2/mwindow.c541
1 files changed, 541 insertions, 0 deletions
diff --git a/src/libgit2/mwindow.c b/src/libgit2/mwindow.c
new file mode 100644
index 000000000..ad649490a
--- /dev/null
+++ b/src/libgit2/mwindow.c
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "mwindow.h"
+
+#include "vector.h"
+#include "futils.h"
+#include "map.h"
+#include "runtime.h"
+#include "strmap.h"
+#include "pack.h"
+
+#define DEFAULT_WINDOW_SIZE \
+ (sizeof(void*) >= 8 \
+ ? 1 * 1024 * 1024 * 1024 \
+ : 32 * 1024 * 1024)
+
+#define DEFAULT_MAPPED_LIMIT \
+ ((1024 * 1024) * (sizeof(void*) >= 8 ? UINT64_C(8192) : UINT64_C(256)))
+
+/* default is unlimited */
+#define DEFAULT_FILE_LIMIT 0
+
+size_t git_mwindow__window_size = DEFAULT_WINDOW_SIZE;
+size_t git_mwindow__mapped_limit = DEFAULT_MAPPED_LIMIT;
+size_t git_mwindow__file_limit = DEFAULT_FILE_LIMIT;
+
+/* Mutex to control access to `git_mwindow__mem_ctl` and `git__pack_cache`. */
+git_mutex git__mwindow_mutex;
+
+/* Whenever you want to read or modify this, grab `git__mwindow_mutex` */
+git_mwindow_ctl git_mwindow__mem_ctl;
+
+/* Global list of mwindow files, to open packs once across repos */
+git_strmap *git__pack_cache = NULL;
+
+static void git_mwindow_global_shutdown(void)
+{
+ git_strmap *tmp = git__pack_cache;
+
+ git_mutex_free(&git__mwindow_mutex);
+
+ git__pack_cache = NULL;
+ git_strmap_free(tmp);
+}
+
+int git_mwindow_global_init(void)
+{
+ int error;
+
+ GIT_ASSERT(!git__pack_cache);
+
+ if ((error = git_mutex_init(&git__mwindow_mutex)) < 0 ||
+ (error = git_strmap_new(&git__pack_cache)) < 0)
+ return error;
+
+ return git_runtime_shutdown_register(git_mwindow_global_shutdown);
+}
+
+int git_mwindow_get_pack(struct git_pack_file **out, const char *path)
+{
+ struct git_pack_file *pack;
+ char *packname;
+ int error;
+
+ if ((error = git_packfile__name(&packname, path)) < 0)
+ return error;
+
+ if (git_mutex_lock(&git__mwindow_mutex) < 0) {
+ git_error_set(GIT_ERROR_OS, "failed to lock mwindow mutex");
+ return -1;
+ }
+
+ pack = git_strmap_get(git__pack_cache, packname);
+ git__free(packname);
+
+ if (pack != NULL) {
+ git_atomic32_inc(&pack->refcount);
+ git_mutex_unlock(&git__mwindow_mutex);
+ *out = pack;
+ return 0;
+ }
+
+ /* If we didn't find it, we need to create it */
+ if ((error = git_packfile_alloc(&pack, path)) < 0) {
+ git_mutex_unlock(&git__mwindow_mutex);
+ return error;
+ }
+
+ git_atomic32_inc(&pack->refcount);
+
+ error = git_strmap_set(git__pack_cache, pack->pack_name, pack);
+ git_mutex_unlock(&git__mwindow_mutex);
+ if (error < 0) {
+ git_packfile_free(pack, false);
+ return error;
+ }
+
+ *out = pack;
+ return 0;
+}
+
+int git_mwindow_put_pack(struct git_pack_file *pack)
+{
+ int count, error;
+ struct git_pack_file *pack_to_delete = NULL;
+
+ if ((error = git_mutex_lock(&git__mwindow_mutex)) < 0)
+ return error;
+
+ /* put before get would be a corrupted state */
+ GIT_ASSERT(git__pack_cache);
+
+ /* if we cannot find it, the state is corrupted */
+ GIT_ASSERT(git_strmap_exists(git__pack_cache, pack->pack_name));
+
+ count = git_atomic32_dec(&pack->refcount);
+ if (count == 0) {
+ git_strmap_delete(git__pack_cache, pack->pack_name);
+ pack_to_delete = pack;
+ }
+ git_mutex_unlock(&git__mwindow_mutex);
+ git_packfile_free(pack_to_delete, false);
+
+ return 0;
+}
+
+/*
+ * Free all the windows in a sequence, typically because we're done
+ * with the file. Needs to hold the git__mwindow_mutex.
+ */
+static int git_mwindow_free_all_locked(git_mwindow_file *mwf)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ size_t i;
+
+ /*
+ * Remove these windows from the global list
+ */
+ for (i = 0; i < ctl->windowfiles.length; ++i){
+ if (git_vector_get(&ctl->windowfiles, i) == mwf) {
+ git_vector_remove(&ctl->windowfiles, i);
+ break;
+ }
+ }
+
+ if (ctl->windowfiles.length == 0) {
+ git_vector_free(&ctl->windowfiles);
+ ctl->windowfiles.contents = NULL;
+ }
+
+ while (mwf->windows) {
+ git_mwindow *w = mwf->windows;
+ GIT_ASSERT(w->inuse_cnt == 0);
+
+ ctl->mapped -= w->window_map.len;
+ ctl->open_windows--;
+
+ git_futils_mmap_free(&w->window_map);
+
+ mwf->windows = w->next;
+ git__free(w);
+ }
+
+ return 0;
+}
+
+int git_mwindow_free_all(git_mwindow_file *mwf)
+{
+ int error;
+
+ if (git_mutex_lock(&git__mwindow_mutex)) {
+ git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
+ return -1;
+ }
+
+ error = git_mwindow_free_all_locked(mwf);
+
+ git_mutex_unlock(&git__mwindow_mutex);
+
+ return error;
+}
+
+/*
+ * Check if a window 'win' contains both the address 'offset' and 'extra'.
+ *
+ * 'extra' is the size of the hash we're using as we always want to make sure
+ * that it's contained.
+ */
+int git_mwindow_contains(git_mwindow *win, off64_t offset, off64_t extra)
+{
+ off64_t win_off = win->offset;
+ return win_off <= offset
+ && (offset + extra) <= (off64_t)(win_off + win->window_map.len);
+}
+
+#define GIT_MWINDOW__LRU -1
+#define GIT_MWINDOW__MRU 1
+
+/*
+ * Find the least- or most-recently-used window in a file that is not currently
+ * being used. The 'only_unused' flag controls whether the caller requires the
+ * file to only have unused windows. If '*out_window' is non-null, it is used as
+ * a starting point for the comparison.
+ *
+ * Returns whether such a window was found in the file.
+ */
+static bool git_mwindow_scan_recently_used(
+ git_mwindow_file *mwf,
+ git_mwindow **out_window,
+ git_mwindow **out_last,
+ bool only_unused,
+ int comparison_sign)
+{
+ git_mwindow *w, *w_last;
+ git_mwindow *lru_window = NULL, *lru_last = NULL;
+ bool found = false;
+
+ GIT_ASSERT_ARG(mwf);
+ GIT_ASSERT_ARG(out_window);
+
+ lru_window = *out_window;
+ if (out_last)
+ lru_last = *out_last;
+
+ for (w_last = NULL, w = mwf->windows; w; w_last = w, w = w->next) {
+ if (w->inuse_cnt) {
+ if (only_unused)
+ return false;
+ /* This window is currently being used. Skip it. */
+ continue;
+ }
+
+ /*
+ * If the current one is more (or less) recent than the last one,
+ * store it in the output parameter. If lru_window is NULL,
+ * it's the first loop, so store it as well.
+ */
+ if (!lru_window || (comparison_sign * w->last_used) > lru_window->last_used) {
+ lru_window = w;
+ lru_last = w_last;
+ found = true;
+ }
+ }
+
+ if (!found)
+ return false;
+
+ *out_window = lru_window;
+ if (out_last)
+ *out_last = lru_last;
+ return true;
+}
+
+/*
+ * Close the least recently used window (that is currently not being used) out
+ * of all the files. Called under lock from new_window_locked.
+ */
+static int git_mwindow_close_lru_window_locked(void)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ git_mwindow_file *cur;
+ size_t i;
+ git_mwindow *lru_window = NULL, *lru_last = NULL, **list = NULL;
+
+ git_vector_foreach(&ctl->windowfiles, i, cur) {
+ if (git_mwindow_scan_recently_used(
+ cur, &lru_window, &lru_last, false, GIT_MWINDOW__LRU)) {
+ list = &cur->windows;
+ }
+ }
+
+ if (!lru_window) {
+ git_error_set(GIT_ERROR_OS, "failed to close memory window; couldn't find LRU");
+ return -1;
+ }
+
+ ctl->mapped -= lru_window->window_map.len;
+ git_futils_mmap_free(&lru_window->window_map);
+
+ if (lru_last)
+ lru_last->next = lru_window->next;
+ else
+ *list = lru_window->next;
+
+ git__free(lru_window);
+ ctl->open_windows--;
+
+ return 0;
+}
+
+/*
+ * Finds the file that does not have any open windows AND whose
+ * most-recently-used window is the least-recently used one across all
+ * currently open files.
+ *
+ * Called under lock from new_window_locked.
+ */
+static int git_mwindow_find_lru_file_locked(git_mwindow_file **out)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ git_mwindow_file *lru_file = NULL, *current_file = NULL;
+ git_mwindow *lru_window = NULL;
+ size_t i;
+
+ git_vector_foreach(&ctl->windowfiles, i, current_file) {
+ git_mwindow *mru_window = NULL;
+ if (!git_mwindow_scan_recently_used(
+ current_file, &mru_window, NULL, true, GIT_MWINDOW__MRU)) {
+ continue;
+ }
+ if (!lru_window || lru_window->last_used > mru_window->last_used) {
+ lru_window = mru_window;
+ lru_file = current_file;
+ }
+ }
+
+ if (!lru_file) {
+ git_error_set(GIT_ERROR_OS, "failed to close memory window file; couldn't find LRU");
+ return -1;
+ }
+
+ *out = lru_file;
+ return 0;
+}
+
+/* This gets called under lock from git_mwindow_open */
+static git_mwindow *new_window_locked(
+ git_file fd,
+ off64_t size,
+ off64_t offset)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ size_t walign = git_mwindow__window_size / 2;
+ off64_t len;
+ git_mwindow *w;
+
+ w = git__calloc(1, sizeof(*w));
+
+ if (w == NULL)
+ return NULL;
+
+ w->offset = (offset / walign) * walign;
+
+ len = size - w->offset;
+ if (len > (off64_t)git_mwindow__window_size)
+ len = (off64_t)git_mwindow__window_size;
+
+ ctl->mapped += (size_t)len;
+
+ while (git_mwindow__mapped_limit < ctl->mapped &&
+ git_mwindow_close_lru_window_locked() == 0) /* nop */;
+
+ /*
+ * We treat `mapped_limit` as a soft limit. If we can't find a
+ * window to close and are above the limit, we still mmap the new
+ * window.
+ */
+
+ if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) {
+ /*
+ * The first error might be down to memory fragmentation even if
+ * we're below our soft limits, so free up what we can and try again.
+ */
+
+ while (git_mwindow_close_lru_window_locked() == 0)
+ /* nop */;
+
+ if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) {
+ git__free(w);
+ return NULL;
+ }
+ }
+
+ ctl->mmap_calls++;
+ ctl->open_windows++;
+
+ if (ctl->mapped > ctl->peak_mapped)
+ ctl->peak_mapped = ctl->mapped;
+
+ if (ctl->open_windows > ctl->peak_open_windows)
+ ctl->peak_open_windows = ctl->open_windows;
+
+ return w;
+}
+
+/*
+ * Open a new window, closing the least recenty used until we have
+ * enough space. Don't forget to add it to your list
+ */
+unsigned char *git_mwindow_open(
+ git_mwindow_file *mwf,
+ git_mwindow **cursor,
+ off64_t offset,
+ size_t extra,
+ unsigned int *left)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ git_mwindow *w = *cursor;
+
+ if (git_mutex_lock(&git__mwindow_mutex)) {
+ git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
+ return NULL;
+ }
+
+ if (!w || !(git_mwindow_contains(w, offset, extra))) {
+ if (w) {
+ w->inuse_cnt--;
+ }
+
+ for (w = mwf->windows; w; w = w->next) {
+ if (git_mwindow_contains(w, offset, extra))
+ break;
+ }
+
+ /*
+ * If there isn't a suitable window, we need to create a new
+ * one.
+ */
+ if (!w) {
+ w = new_window_locked(mwf->fd, mwf->size, offset);
+ if (w == NULL) {
+ git_mutex_unlock(&git__mwindow_mutex);
+ return NULL;
+ }
+ w->next = mwf->windows;
+ mwf->windows = w;
+ }
+ }
+
+ /* If we changed w, store it in the cursor */
+ if (w != *cursor) {
+ w->last_used = ctl->used_ctr++;
+ w->inuse_cnt++;
+ *cursor = w;
+ }
+
+ offset -= w->offset;
+
+ if (left)
+ *left = (unsigned int)(w->window_map.len - offset);
+
+ git_mutex_unlock(&git__mwindow_mutex);
+ return (unsigned char *) w->window_map.data + offset;
+}
+
+int git_mwindow_file_register(git_mwindow_file *mwf)
+{
+ git_vector closed_files = GIT_VECTOR_INIT;
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ int error;
+ size_t i;
+ git_mwindow_file *closed_file = NULL;
+
+ if (git_mutex_lock(&git__mwindow_mutex)) {
+ git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
+ return -1;
+ }
+
+ if (ctl->windowfiles.length == 0 &&
+ (error = git_vector_init(&ctl->windowfiles, 8, NULL)) < 0) {
+ git_mutex_unlock(&git__mwindow_mutex);
+ goto cleanup;
+ }
+
+ if (git_mwindow__file_limit) {
+ git_mwindow_file *lru_file;
+ while (git_mwindow__file_limit <= ctl->windowfiles.length &&
+ git_mwindow_find_lru_file_locked(&lru_file) == 0) {
+ if ((error = git_vector_insert(&closed_files, lru_file)) < 0) {
+ /*
+ * Exceeding the file limit seems preferable to being open to
+ * data races that can end up corrupting the heap.
+ */
+ break;
+ }
+ git_mwindow_free_all_locked(lru_file);
+ }
+ }
+
+ error = git_vector_insert(&ctl->windowfiles, mwf);
+ git_mutex_unlock(&git__mwindow_mutex);
+ if (error < 0)
+ goto cleanup;
+
+ /*
+ * Once we have released the global windowfiles lock, we can close each
+ * individual file. Before doing so, acquire that file's lock to avoid
+ * closing a file that is currently being used.
+ */
+ git_vector_foreach(&closed_files, i, closed_file) {
+ error = git_mutex_lock(&closed_file->lock);
+ if (error < 0)
+ continue;
+ p_close(closed_file->fd);
+ closed_file->fd = -1;
+ git_mutex_unlock(&closed_file->lock);
+ }
+
+cleanup:
+ git_vector_free(&closed_files);
+ return error;
+}
+
+void git_mwindow_file_deregister(git_mwindow_file *mwf)
+{
+ git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
+ git_mwindow_file *cur;
+ size_t i;
+
+ if (git_mutex_lock(&git__mwindow_mutex))
+ return;
+
+ git_vector_foreach(&ctl->windowfiles, i, cur) {
+ if (cur == mwf) {
+ git_vector_remove(&ctl->windowfiles, i);
+ git_mutex_unlock(&git__mwindow_mutex);
+ return;
+ }
+ }
+ git_mutex_unlock(&git__mwindow_mutex);
+}
+
+void git_mwindow_close(git_mwindow **window)
+{
+ git_mwindow *w = *window;
+ if (w) {
+ if (git_mutex_lock(&git__mwindow_mutex)) {
+ git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
+ return;
+ }
+
+ w->inuse_cnt--;
+ git_mutex_unlock(&git__mwindow_mutex);
+ *window = NULL;
+ }
+}