1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
6 */
7
8 #include "mwindow.h"
9
10 #include "vector.h"
11 #include "futils.h"
12 #include "map.h"
13 #include "runtime.h"
14 #include "strmap.h"
15 #include "pack.h"
16
17 #define DEFAULT_WINDOW_SIZE \
18 (sizeof(void*) >= 8 \
19 ? 1 * 1024 * 1024 * 1024 \
20 : 32 * 1024 * 1024)
21
22 #define DEFAULT_MAPPED_LIMIT \
23 ((1024 * 1024) * (sizeof(void*) >= 8 ? 8192ULL : 256UL))
24
25 /* default is unlimited */
26 #define DEFAULT_FILE_LIMIT 0
27
28 size_t git_mwindow__window_size = DEFAULT_WINDOW_SIZE;
29 size_t git_mwindow__mapped_limit = DEFAULT_MAPPED_LIMIT;
30 size_t git_mwindow__file_limit = DEFAULT_FILE_LIMIT;
31
32 /* Mutex to control access to `git_mwindow__mem_ctl` and `git__pack_cache`. */
33 git_mutex git__mwindow_mutex;
34
35 /* Whenever you want to read or modify this, grab `git__mwindow_mutex` */
36 git_mwindow_ctl git_mwindow__mem_ctl;
37
38 /* Global list of mwindow files, to open packs once across repos */
39 git_strmap *git__pack_cache = NULL;
40
git_mwindow_global_shutdown(void)41 static void git_mwindow_global_shutdown(void)
42 {
43 git_strmap *tmp = git__pack_cache;
44
45 git_mutex_free(&git__mwindow_mutex);
46
47 git__pack_cache = NULL;
48 git_strmap_free(tmp);
49 }
50
git_mwindow_global_init(void)51 int git_mwindow_global_init(void)
52 {
53 int error;
54
55 GIT_ASSERT(!git__pack_cache);
56
57 if ((error = git_mutex_init(&git__mwindow_mutex)) < 0 ||
58 (error = git_strmap_new(&git__pack_cache)) < 0)
59 return error;
60
61 return git_runtime_shutdown_register(git_mwindow_global_shutdown);
62 }
63
git_mwindow_get_pack(struct git_pack_file ** out,const char * path)64 int git_mwindow_get_pack(struct git_pack_file **out, const char *path)
65 {
66 struct git_pack_file *pack;
67 char *packname;
68 int error;
69
70 if ((error = git_packfile__name(&packname, path)) < 0)
71 return error;
72
73 if (git_mutex_lock(&git__mwindow_mutex) < 0) {
74 git_error_set(GIT_ERROR_OS, "failed to lock mwindow mutex");
75 return -1;
76 }
77
78 pack = git_strmap_get(git__pack_cache, packname);
79 git__free(packname);
80
81 if (pack != NULL) {
82 git_atomic32_inc(&pack->refcount);
83 git_mutex_unlock(&git__mwindow_mutex);
84 *out = pack;
85 return 0;
86 }
87
88 /* If we didn't find it, we need to create it */
89 if ((error = git_packfile_alloc(&pack, path)) < 0) {
90 git_mutex_unlock(&git__mwindow_mutex);
91 return error;
92 }
93
94 git_atomic32_inc(&pack->refcount);
95
96 error = git_strmap_set(git__pack_cache, pack->pack_name, pack);
97 git_mutex_unlock(&git__mwindow_mutex);
98 if (error < 0) {
99 git_packfile_free(pack, false);
100 return error;
101 }
102
103 *out = pack;
104 return 0;
105 }
106
git_mwindow_put_pack(struct git_pack_file * pack)107 int git_mwindow_put_pack(struct git_pack_file *pack)
108 {
109 int count, error;
110 struct git_pack_file *pack_to_delete = NULL;
111
112 if ((error = git_mutex_lock(&git__mwindow_mutex)) < 0)
113 return error;
114
115 /* put before get would be a corrupted state */
116 GIT_ASSERT(git__pack_cache);
117
118 /* if we cannot find it, the state is corrupted */
119 GIT_ASSERT(git_strmap_exists(git__pack_cache, pack->pack_name));
120
121 count = git_atomic32_dec(&pack->refcount);
122 if (count == 0) {
123 git_strmap_delete(git__pack_cache, pack->pack_name);
124 pack_to_delete = pack;
125 }
126 git_mutex_unlock(&git__mwindow_mutex);
127 git_packfile_free(pack_to_delete, false);
128
129 return 0;
130 }
131
132 /*
133 * Free all the windows in a sequence, typically because we're done
134 * with the file. Needs to hold the git__mwindow_mutex.
135 */
git_mwindow_free_all_locked(git_mwindow_file * mwf)136 static int git_mwindow_free_all_locked(git_mwindow_file *mwf)
137 {
138 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
139 size_t i;
140
141 /*
142 * Remove these windows from the global list
143 */
144 for (i = 0; i < ctl->windowfiles.length; ++i){
145 if (git_vector_get(&ctl->windowfiles, i) == mwf) {
146 git_vector_remove(&ctl->windowfiles, i);
147 break;
148 }
149 }
150
151 if (ctl->windowfiles.length == 0) {
152 git_vector_free(&ctl->windowfiles);
153 ctl->windowfiles.contents = NULL;
154 }
155
156 while (mwf->windows) {
157 git_mwindow *w = mwf->windows;
158 GIT_ASSERT(w->inuse_cnt == 0);
159
160 ctl->mapped -= w->window_map.len;
161 ctl->open_windows--;
162
163 git_futils_mmap_free(&w->window_map);
164
165 mwf->windows = w->next;
166 git__free(w);
167 }
168
169 return 0;
170 }
171
git_mwindow_free_all(git_mwindow_file * mwf)172 int git_mwindow_free_all(git_mwindow_file *mwf)
173 {
174 int error;
175
176 if (git_mutex_lock(&git__mwindow_mutex)) {
177 git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
178 return -1;
179 }
180
181 error = git_mwindow_free_all_locked(mwf);
182
183 git_mutex_unlock(&git__mwindow_mutex);
184
185 return error;
186 }
187
188 /*
189 * Check if a window 'win' contains the address 'offset'
190 */
git_mwindow_contains(git_mwindow * win,off64_t offset)191 int git_mwindow_contains(git_mwindow *win, off64_t offset)
192 {
193 off64_t win_off = win->offset;
194 return win_off <= offset
195 && offset <= (off64_t)(win_off + win->window_map.len);
196 }
197
198 #define GIT_MWINDOW__LRU -1
199 #define GIT_MWINDOW__MRU 1
200
201 /*
202 * Find the least- or most-recently-used window in a file that is not currently
203 * being used. The 'only_unused' flag controls whether the caller requires the
204 * file to only have unused windows. If '*out_window' is non-null, it is used as
205 * a starting point for the comparison.
206 *
207 * Returns whether such a window was found in the file.
208 */
git_mwindow_scan_recently_used(git_mwindow_file * mwf,git_mwindow ** out_window,git_mwindow ** out_last,bool only_unused,int comparison_sign)209 static bool git_mwindow_scan_recently_used(
210 git_mwindow_file *mwf,
211 git_mwindow **out_window,
212 git_mwindow **out_last,
213 bool only_unused,
214 int comparison_sign)
215 {
216 git_mwindow *w, *w_last;
217 git_mwindow *lru_window = NULL, *lru_last = NULL;
218 bool found = false;
219
220 GIT_ASSERT_ARG(mwf);
221 GIT_ASSERT_ARG(out_window);
222
223 lru_window = *out_window;
224 if (out_last)
225 lru_last = *out_last;
226
227 for (w_last = NULL, w = mwf->windows; w; w_last = w, w = w->next) {
228 if (w->inuse_cnt) {
229 if (only_unused)
230 return false;
231 /* This window is currently being used. Skip it. */
232 continue;
233 }
234
235 /*
236 * If the current one is more (or less) recent than the last one,
237 * store it in the output parameter. If lru_window is NULL,
238 * it's the first loop, so store it as well.
239 */
240 if (!lru_window ||
241 (comparison_sign == GIT_MWINDOW__LRU && lru_window->last_used > w->last_used) ||
242 (comparison_sign == GIT_MWINDOW__MRU && lru_window->last_used < w->last_used)) {
243 lru_window = w;
244 lru_last = w_last;
245 found = true;
246 }
247 }
248
249 if (!found)
250 return false;
251
252 *out_window = lru_window;
253 if (out_last)
254 *out_last = lru_last;
255 return true;
256 }
257
258 /*
259 * Close the least recently used window (that is currently not being used) out
260 * of all the files. Called under lock from new_window_locked.
261 */
git_mwindow_close_lru_window_locked(void)262 static int git_mwindow_close_lru_window_locked(void)
263 {
264 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
265 git_mwindow_file *cur;
266 size_t i;
267 git_mwindow *lru_window = NULL, *lru_last = NULL, **list = NULL;
268
269 git_vector_foreach(&ctl->windowfiles, i, cur) {
270 if (git_mwindow_scan_recently_used(
271 cur, &lru_window, &lru_last, false, GIT_MWINDOW__LRU)) {
272 list = &cur->windows;
273 }
274 }
275
276 if (!lru_window) {
277 git_error_set(GIT_ERROR_OS, "failed to close memory window; couldn't find LRU");
278 return -1;
279 }
280
281 ctl->mapped -= lru_window->window_map.len;
282 git_futils_mmap_free(&lru_window->window_map);
283
284 if (lru_last)
285 lru_last->next = lru_window->next;
286 else
287 *list = lru_window->next;
288
289 git__free(lru_window);
290 ctl->open_windows--;
291
292 return 0;
293 }
294
295 /*
296 * Finds the file that does not have any open windows AND whose
297 * most-recently-used window is the least-recently used one across all
298 * currently open files.
299 *
300 * Called under lock from new_window_locked.
301 */
git_mwindow_find_lru_file_locked(git_mwindow_file ** out)302 static int git_mwindow_find_lru_file_locked(git_mwindow_file **out)
303 {
304 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
305 git_mwindow_file *lru_file = NULL, *current_file = NULL;
306 git_mwindow *lru_window = NULL;
307 size_t i;
308
309 git_vector_foreach(&ctl->windowfiles, i, current_file) {
310 git_mwindow *mru_window = NULL;
311 if (!git_mwindow_scan_recently_used(
312 current_file, &mru_window, NULL, true, GIT_MWINDOW__MRU)) {
313 continue;
314 }
315 if (!lru_window || lru_window->last_used > mru_window->last_used) {
316 lru_window = mru_window;
317 lru_file = current_file;
318 }
319 }
320
321 if (!lru_file) {
322 git_error_set(GIT_ERROR_OS, "failed to close memory window file; couldn't find LRU");
323 return -1;
324 }
325
326 *out = lru_file;
327 return 0;
328 }
329
330 /* This gets called under lock from git_mwindow_open */
new_window_locked(git_file fd,off64_t size,off64_t offset)331 static git_mwindow *new_window_locked(
332 git_file fd,
333 off64_t size,
334 off64_t offset)
335 {
336 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
337 size_t walign = git_mwindow__window_size / 2;
338 off64_t len;
339 git_mwindow *w;
340
341 w = git__calloc(1, sizeof(*w));
342
343 if (w == NULL)
344 return NULL;
345
346 w->offset = (offset / walign) * walign;
347
348 len = size - w->offset;
349 if (len > (off64_t)git_mwindow__window_size)
350 len = (off64_t)git_mwindow__window_size;
351
352 ctl->mapped += (size_t)len;
353
354 while (git_mwindow__mapped_limit < ctl->mapped &&
355 git_mwindow_close_lru_window_locked() == 0) /* nop */;
356
357 /*
358 * We treat `mapped_limit` as a soft limit. If we can't find a
359 * window to close and are above the limit, we still mmap the new
360 * window.
361 */
362
363 if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) {
364 /*
365 * The first error might be down to memory fragmentation even if
366 * we're below our soft limits, so free up what we can and try again.
367 */
368
369 while (git_mwindow_close_lru_window_locked() == 0)
370 /* nop */;
371
372 if (git_futils_mmap_ro(&w->window_map, fd, w->offset, (size_t)len) < 0) {
373 git__free(w);
374 return NULL;
375 }
376 }
377
378 ctl->mmap_calls++;
379 ctl->open_windows++;
380
381 if (ctl->mapped > ctl->peak_mapped)
382 ctl->peak_mapped = ctl->mapped;
383
384 if (ctl->open_windows > ctl->peak_open_windows)
385 ctl->peak_open_windows = ctl->open_windows;
386
387 return w;
388 }
389
390 /*
391 * Open a new window, closing the least recenty used until we have
392 * enough space. Don't forget to add it to your list
393 */
git_mwindow_open(git_mwindow_file * mwf,git_mwindow ** cursor,off64_t offset,size_t extra,unsigned int * left)394 unsigned char *git_mwindow_open(
395 git_mwindow_file *mwf,
396 git_mwindow **cursor,
397 off64_t offset,
398 size_t extra,
399 unsigned int *left)
400 {
401 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
402 git_mwindow *w = *cursor;
403
404 if (git_mutex_lock(&git__mwindow_mutex)) {
405 git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
406 return NULL;
407 }
408
409 if (!w || !(git_mwindow_contains(w, offset) && git_mwindow_contains(w, offset + extra))) {
410 if (w) {
411 w->inuse_cnt--;
412 }
413
414 for (w = mwf->windows; w; w = w->next) {
415 if (git_mwindow_contains(w, offset) &&
416 git_mwindow_contains(w, offset + extra))
417 break;
418 }
419
420 /*
421 * If there isn't a suitable window, we need to create a new
422 * one.
423 */
424 if (!w) {
425 w = new_window_locked(mwf->fd, mwf->size, offset);
426 if (w == NULL) {
427 git_mutex_unlock(&git__mwindow_mutex);
428 return NULL;
429 }
430 w->next = mwf->windows;
431 mwf->windows = w;
432 }
433 }
434
435 /* If we changed w, store it in the cursor */
436 if (w != *cursor) {
437 w->last_used = ctl->used_ctr++;
438 w->inuse_cnt++;
439 *cursor = w;
440 }
441
442 offset -= w->offset;
443
444 if (left)
445 *left = (unsigned int)(w->window_map.len - offset);
446
447 git_mutex_unlock(&git__mwindow_mutex);
448 return (unsigned char *) w->window_map.data + offset;
449 }
450
git_mwindow_file_register(git_mwindow_file * mwf)451 int git_mwindow_file_register(git_mwindow_file *mwf)
452 {
453 git_vector closed_files = GIT_VECTOR_INIT;
454 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
455 int error;
456 size_t i;
457 git_mwindow_file *closed_file = NULL;
458
459 if (git_mutex_lock(&git__mwindow_mutex)) {
460 git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
461 return -1;
462 }
463
464 if (ctl->windowfiles.length == 0 &&
465 (error = git_vector_init(&ctl->windowfiles, 8, NULL)) < 0) {
466 git_mutex_unlock(&git__mwindow_mutex);
467 goto cleanup;
468 }
469
470 if (git_mwindow__file_limit) {
471 git_mwindow_file *lru_file;
472 while (git_mwindow__file_limit <= ctl->windowfiles.length &&
473 git_mwindow_find_lru_file_locked(&lru_file) == 0) {
474 if ((error = git_vector_insert(&closed_files, lru_file)) < 0) {
475 /*
476 * Exceeding the file limit seems preferrable to being open to
477 * data races that can end up corrupting the heap.
478 */
479 break;
480 }
481 git_mwindow_free_all_locked(lru_file);
482 }
483 }
484
485 error = git_vector_insert(&ctl->windowfiles, mwf);
486 git_mutex_unlock(&git__mwindow_mutex);
487 if (error < 0)
488 goto cleanup;
489
490 /*
491 * Once we have released the global windowfiles lock, we can close each
492 * individual file. Before doing so, acquire that file's lock to avoid
493 * closing a file that is currently being used.
494 */
495 git_vector_foreach(&closed_files, i, closed_file) {
496 error = git_mutex_lock(&closed_file->lock);
497 if (error < 0)
498 continue;
499 p_close(closed_file->fd);
500 closed_file->fd = -1;
501 git_mutex_unlock(&closed_file->lock);
502 }
503
504 cleanup:
505 git_vector_free(&closed_files);
506 return error;
507 }
508
git_mwindow_file_deregister(git_mwindow_file * mwf)509 void git_mwindow_file_deregister(git_mwindow_file *mwf)
510 {
511 git_mwindow_ctl *ctl = &git_mwindow__mem_ctl;
512 git_mwindow_file *cur;
513 size_t i;
514
515 if (git_mutex_lock(&git__mwindow_mutex))
516 return;
517
518 git_vector_foreach(&ctl->windowfiles, i, cur) {
519 if (cur == mwf) {
520 git_vector_remove(&ctl->windowfiles, i);
521 git_mutex_unlock(&git__mwindow_mutex);
522 return;
523 }
524 }
525 git_mutex_unlock(&git__mwindow_mutex);
526 }
527
git_mwindow_close(git_mwindow ** window)528 void git_mwindow_close(git_mwindow **window)
529 {
530 git_mwindow *w = *window;
531 if (w) {
532 if (git_mutex_lock(&git__mwindow_mutex)) {
533 git_error_set(GIT_ERROR_THREAD, "unable to lock mwindow mutex");
534 return;
535 }
536
537 w->inuse_cnt--;
538 git_mutex_unlock(&git__mwindow_mutex);
539 *window = NULL;
540 }
541 }
542