summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config-daemon.ac11
-rw-r--r--guix/store/deduplication.scm69
-rw-r--r--nix/libstore/gc.cc4
-rw-r--r--nix/libstore/local-store.hh3
-rw-r--r--nix/libstore/optimise-store.cc15
-rw-r--r--tests/derivations.scm14
-rw-r--r--tests/nar.scm7
-rw-r--r--tests/store-deduplication.scm41
-rw-r--r--tests/store.scm4
9 files changed, 126 insertions, 42 deletions
diff --git a/config-daemon.ac b/config-daemon.ac
index 5ddc740600c..86306effe1f 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -94,17 +94,6 @@ if test "x$guix_build_daemon" = "xyes"; then
94 AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \ 94 AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
95 statvfs nanosleep strsignal statx]) 95 statvfs nanosleep strsignal statx])
96 96
97 dnl Check whether the store optimiser can optimise symlinks.
98 AC_MSG_CHECKING([whether it is possible to create a link to a symlink])
99 ln -s bla tmp_link
100 if ln tmp_link tmp_link2 2> /dev/null; then
101 AC_MSG_RESULT(yes)
102 AC_DEFINE(CAN_LINK_SYMLINK, 1, [Whether link() works on symlinks.])
103 else
104 AC_MSG_RESULT(no)
105 fi
106 rm -f tmp_link tmp_link2
107
108 dnl Check for <locale>. 97 dnl Check for <locale>.
109 AC_LANG_PUSH(C++) 98 AC_LANG_PUSH(C++)
110 AC_CHECK_HEADERS([locale]) 99 AC_CHECK_HEADERS([locale])
diff --git a/guix/store/deduplication.scm b/guix/store/deduplication.scm
index cd9660174cf..370df4a74cf 100644
--- a/guix/store/deduplication.scm
+++ b/guix/store/deduplication.scm
@@ -1,6 +1,6 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2017 Caleb Ristvedt <caleb.ristvedt@cune.org> 2;;; Copyright © 2017 Caleb Ristvedt <caleb.ristvedt@cune.org>
3;;; Copyright © 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org> 3;;; Copyright © 2018-2021 Ludovic Courtès <ludo@gnu.org>
4;;; 4;;;
5;;; This file is part of GNU Guix. 5;;; This file is part of GNU Guix.
6;;; 6;;;
@@ -22,12 +22,13 @@
22 22
23(define-module (guix store deduplication) 23(define-module (guix store deduplication)
24 #:use-module (gcrypt hash) 24 #:use-module (gcrypt hash)
25 #:use-module (guix build utils) 25 #:use-module ((guix build utils) #:hide (dump-port))
26 #:use-module (guix build syscalls) 26 #:use-module (guix build syscalls)
27 #:use-module (guix base32) 27 #:use-module (guix base32)
28 #:use-module (srfi srfi-11) 28 #:use-module (srfi srfi-11)
29 #:use-module (srfi srfi-34) 29 #:use-module (srfi srfi-34)
30 #:use-module (srfi srfi-35) 30 #:use-module (srfi srfi-35)
31 #:use-module (rnrs bytevectors)
31 #:use-module (rnrs io ports) 32 #:use-module (rnrs io ports)
32 #:use-module (ice-9 ftw) 33 #:use-module (ice-9 ftw)
33 #:use-module (ice-9 match) 34 #:use-module (ice-9 match)
@@ -37,6 +38,31 @@
37 dump-file/deduplicate 38 dump-file/deduplicate
38 copy-file/deduplicate)) 39 copy-file/deduplicate))
39 40
41;; TODO: Remove once 'dump-port' in (guix build utils) has an optional 'len'
42;; parameter.
43(define* (dump-port in out
44 #:optional len
45 #:key (buffer-size 16384))
46 "Read LEN bytes from IN (or as much as possible if LEN is #f) and write it
47to OUT, using chunks of BUFFER-SIZE bytes."
48 (define buffer
49 (make-bytevector buffer-size))
50
51 (let loop ((total 0)
52 (bytes (get-bytevector-n! in buffer 0
53 (if len
54 (min len buffer-size)
55 buffer-size))))
56 (or (eof-object? bytes)
57 (and len (= total len))
58 (let ((total (+ total bytes)))
59 (put-bytevector out buffer 0 bytes)
60 (loop total
61 (get-bytevector-n! in buffer 0
62 (if len
63 (min (- len total) buffer-size)
64 buffer-size)))))))
65
40(define (nar-sha256 file) 66(define (nar-sha256 file)
41 "Gives the sha256 hash of a file and the size of the file in nar form." 67 "Gives the sha256 hash of a file and the size of the file in nar form."
42 (let-values (((port get-hash) (open-sha256-port))) 68 (let-values (((port get-hash) (open-sha256-port)))
@@ -127,11 +153,27 @@ Note: TARGET, TO-REPLACE, and SWAP-DIRECTORY must be on the same file system."
127 (unless (= EMLINK (system-error-errno args)) 153 (unless (= EMLINK (system-error-errno args))
128 (apply throw args))))))) 154 (apply throw args)))))))
129 155
156(define %deduplication-minimum-size
157 ;; Size below which files are not deduplicated. This avoids adding too many
158 ;; entries to '.links', which would slow down 'removeUnusedLinks' while
159 ;; saving little space. Keep in sync with optimize-store.cc.
160 8192)
161
130(define* (deduplicate path hash #:key (store (%store-directory))) 162(define* (deduplicate path hash #:key (store (%store-directory)))
131 "Check if a store item with sha256 hash HASH already exists. If so, 163 "Check if a store item with sha256 hash HASH already exists. If so,
132replace PATH with a hardlink to the already-existing one. If not, register 164replace PATH with a hardlink to the already-existing one. If not, register
133PATH so that future duplicates can hardlink to it. PATH is assumed to be 165PATH so that future duplicates can hardlink to it. PATH is assumed to be
134under STORE." 166under STORE."
167 ;; Lightweight promises.
168 (define-syntax-rule (delay exp)
169 (let ((value #f))
170 (lambda ()
171 (unless value
172 (set! value exp))
173 value)))
174 (define-syntax-rule (force promise)
175 (promise))
176
135 (define links-directory 177 (define links-directory
136 (string-append store "/.links")) 178 (string-append store "/.links"))
137 179
@@ -144,13 +186,18 @@ under STORE."
144 ((file . properties) 186 ((file . properties)
145 (unless (member file '("." "..")) 187 (unless (member file '("." ".."))
146 (let* ((file (string-append path "/" file)) 188 (let* ((file (string-append path "/" file))
189 (st (delay (lstat file)))
147 (type (match (assoc-ref properties 'type) 190 (type (match (assoc-ref properties 'type)
148 ((or 'unknown #f) 191 ((or 'unknown #f)
149 (stat:type (lstat file))) 192 (stat:type (force st)))
150 (type type)))) 193 (type type))))
151 (loop file type 194 (when (or (eq? 'directory type)
152 (and (not (eq? 'directory type)) 195 (and (eq? 'regular type)
153 (nar-sha256 file))))))) 196 (>= (stat:size (force st))
197 %deduplication-minimum-size)))
198 (loop file type
199 (and (not (eq? 'directory type))
200 (nar-sha256 file))))))))
154 (scandir* path)) 201 (scandir* path))
155 (let ((link-file (string-append links-directory "/" 202 (let ((link-file (string-append links-directory "/"
156 (bytevector->nix-base32-string hash)))) 203 (bytevector->nix-base32-string hash))))
@@ -222,9 +269,9 @@ OUTPUT as it goes."
222 269
223This procedure is suitable as a #:dump-file argument to 'restore-file'. When 270This procedure is suitable as a #:dump-file argument to 'restore-file'. When
224used that way, it deduplicates files on the fly as they are restored, thereby 271used that way, it deduplicates files on the fly as they are restored, thereby
225removing the need to a deduplication pass that would re-read all the files 272removing the need for a deduplication pass that would re-read all the files
226down the road." 273down the road."
227 (define hash 274 (define (dump-and-compute-hash)
228 (call-with-output-file file 275 (call-with-output-file file
229 (lambda (output) 276 (lambda (output)
230 (let-values (((hash-port get-hash) 277 (let-values (((hash-port get-hash)
@@ -236,7 +283,11 @@ down the road."
236 (close-port hash-port) 283 (close-port hash-port)
237 (get-hash))))) 284 (get-hash)))))
238 285
239 (deduplicate file hash #:store store)) 286 (if (>= size %deduplication-minimum-size)
287 (deduplicate file (dump-and-compute-hash) #:store store)
288 (call-with-output-file file
289 (lambda (output)
290 (dump-port input output size)))))
240 291
241(define* (copy-file/deduplicate source target 292(define* (copy-file/deduplicate source target
242 #:key (store (%store-directory))) 293 #:key (store (%store-directory)))
diff --git a/nix/libstore/gc.cc b/nix/libstore/gc.cc
index e1d07651543..16519116e4f 100644
--- a/nix/libstore/gc.cc
+++ b/nix/libstore/gc.cc
@@ -606,7 +606,9 @@ void LocalStore::removeUnusedLinks(const GCState & state)
606 throw SysError(format("statting `%1%'") % path); 606 throw SysError(format("statting `%1%'") % path);
607#endif 607#endif
608 608
609 if (st.st_nlink != 1) { 609 /* Drop links for files smaller than 'deduplicationMinSize', even if
610 they have more than one hard link. */
611 if (st.st_nlink != 1 && st.st_size >= deduplicationMinSize) {
610 actualSize += st.st_size; 612 actualSize += st.st_size;
611 unsharedSize += (st.st_nlink - 1) * st.st_size; 613 unsharedSize += (st.st_nlink - 1) * st.st_size;
612 continue; 614 continue;
diff --git a/nix/libstore/local-store.hh b/nix/libstore/local-store.hh
index 9ba37219da0..20d3c3c8930 100644
--- a/nix/libstore/local-store.hh
+++ b/nix/libstore/local-store.hh
@@ -292,4 +292,7 @@ void canonicaliseTimestampAndPermissions(const Path & path);
292 292
293MakeError(PathInUse, Error); 293MakeError(PathInUse, Error);
294 294
295/* Size below which a file is not considered for deduplication. */
296extern const size_t deduplicationMinSize;
297
295} 298}
diff --git a/nix/libstore/optimise-store.cc b/nix/libstore/optimise-store.cc
index eb303ab4c35..9fd6f3cb356 100644
--- a/nix/libstore/optimise-store.cc
+++ b/nix/libstore/optimise-store.cc
@@ -15,6 +15,9 @@
15 15
16namespace nix { 16namespace nix {
17 17
18/* Any file smaller than this is not considered for deduplication.
19 Keep in sync with (guix store deduplication). */
20const size_t deduplicationMinSize = 8192;
18 21
19static void makeWritable(const Path & path) 22static void makeWritable(const Path & path)
20{ 23{
@@ -105,12 +108,12 @@ void LocalStore::optimisePath_(OptimiseStats & stats, const Path & path, InodeHa
105 return; 108 return;
106 } 109 }
107 110
108 /* We can hard link regular files and maybe symlinks. */ 111 /* We can hard link regular files (and maybe symlinks), but do that only
109 if (!S_ISREG(st.st_mode) 112 for files larger than some threshold. This avoids adding too many
110#if CAN_LINK_SYMLINK 113 entries to '.links', which would slow down 'removeUnusedLinks' while
111 && !S_ISLNK(st.st_mode) 114 saving little space. */
112#endif 115 if (!S_ISREG(st.st_mode) || ((size_t) st.st_size) < deduplicationMinSize)
113 ) return; 116 return;
114 117
115 /* Sometimes SNAFUs can cause files in the store to be 118 /* Sometimes SNAFUs can cause files in the store to be
116 modified, in particular when running programs as root under 119 modified, in particular when running programs as root under
diff --git a/tests/derivations.scm b/tests/derivations.scm
index cd165d1be6a..0775719ea35 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2012-2021 Ludovic Courtès <ludo@gnu.org>
3;;; 3;;;
4;;; This file is part of GNU Guix. 4;;; This file is part of GNU Guix.
5;;; 5;;;
@@ -170,11 +170,15 @@
170 #f)))) 170 #f))))
171 171
172(test-assert "identical files are deduplicated" 172(test-assert "identical files are deduplicated"
173 (let* ((build1 (add-text-to-store %store "one.sh" 173 ;; Note: DATA must be longer than %DEDUPLICATION-MINIMUM-SIZE.
174 "echo hello, world > \"$out\"\n" 174 (let* ((data (make-string 9000 #\a))
175 (build1 (add-text-to-store %store "one.sh"
176 (string-append "echo -n " data
177 " > \"$out\"\n")
175 '())) 178 '()))
176 (build2 (add-text-to-store %store "two.sh" 179 (build2 (add-text-to-store %store "two.sh"
177 "# Hey!\necho hello, world > \"$out\"\n" 180 (string-append "# Hey!\necho -n "
181 data " > \"$out\"\n")
178 '())) 182 '()))
179 (drv1 (derivation %store "foo" 183 (drv1 (derivation %store "foo"
180 %bash `(,build1) 184 %bash `(,build1)
@@ -187,7 +191,7 @@
187 (file2 (derivation->output-path drv2))) 191 (file2 (derivation->output-path drv2)))
188 (and (valid-path? %store file1) (valid-path? %store file2) 192 (and (valid-path? %store file1) (valid-path? %store file2)
189 (string=? (call-with-input-file file1 get-string-all) 193 (string=? (call-with-input-file file1 get-string-all)
190 "hello, world\n") 194 data)
191 (= (stat:ino (lstat file1)) 195 (= (stat:ino (lstat file1))
192 (stat:ino (lstat file2)))))))) 196 (stat:ino (lstat file2))))))))
193 197
diff --git a/tests/nar.scm b/tests/nar.scm
index ba4881caaa1..98752f20884 100644
--- a/tests/nar.scm
+++ b/tests/nar.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2012-2021 Ludovic Courtès <ludo@gnu.org>
3;;; 3;;;
4;;; This file is part of GNU Guix. 4;;; This file is part of GNU Guix.
5;;; 5;;;
@@ -486,8 +486,9 @@
486 ;; their mtime and permissions were not reset. Ensure that this bug is 486 ;; their mtime and permissions were not reset. Ensure that this bug is
487 ;; gone. 487 ;; gone.
488 (with-store store 488 (with-store store
489 (let* ((text1 (random-text)) 489 ;; Note: TEXT1 and TEXT2 must be longer than %DEDUPLICATION-MINIMUM-SIZE.
490 (text2 (random-text)) 490 (let* ((text1 (string-concatenate (make-list 200 (random-text))))
491 (text2 (string-concatenate (make-list 200 (random-text))))
491 (tree `("tree" directory 492 (tree `("tree" directory
492 ("a" regular (data ,text1)) 493 ("a" regular (data ,text1))
493 ("b" directory 494 ("b" directory
diff --git a/tests/store-deduplication.scm b/tests/store-deduplication.scm
index b1c2d93bbd3..2950fbc1a3e 100644
--- a/tests/store-deduplication.scm
+++ b/tests/store-deduplication.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2018, 2020 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2018, 2020-2021 Ludovic Courtès <ludo@gnu.org>
3;;; 3;;;
4;;; This file is part of GNU Guix. 4;;; This file is part of GNU Guix.
5;;; 5;;;
@@ -30,13 +30,40 @@
30 30
31(test-begin "store-deduplication") 31(test-begin "store-deduplication")
32 32
33(test-equal "deduplicate, below %deduplication-minimum-size"
34 (list #t (make-list 5 1))
35
36 (call-with-temporary-directory
37 (lambda (store)
38 ;; Note: DATA must be longer than %DEDUPLICATION-MINIMUM-SIZE.
39 (let ((data "Hello, world!")
40 (identical (map (lambda (n)
41 (string-append store "/" (number->string n)
42 "/a/b/c"))
43 (iota 5))))
44 (for-each (lambda (file)
45 (mkdir-p (dirname file))
46 (call-with-output-file file
47 (lambda (port)
48 (put-bytevector port (string->utf8 data)))))
49 identical)
50
51 (deduplicate store (nar-sha256 store) #:store store)
52
53 ;; (system (string-append "ls -lRia " store))
54 (list (= (length (delete-duplicates
55 (map (compose stat:ino stat) identical)))
56 (length identical))
57 (map (compose stat:nlink stat) identical))))))
58
33(test-equal "deduplicate" 59(test-equal "deduplicate"
34 (cons* #t #f ;inode comparisons 60 (cons* #t #f ;inode comparisons
35 2 (make-list 5 6)) ;'nlink' values 61 2 (make-list 5 6)) ;'nlink' values
36 62
37 (call-with-temporary-directory 63 (call-with-temporary-directory
38 (lambda (store) 64 (lambda (store)
39 (let ((data (string->utf8 "Hello, world!")) 65 ;; Note: DATA must be longer than %DEDUPLICATION-MINIMUM-SIZE.
66 (let ((data (string-concatenate (make-list 1000 "Hello, world!")))
40 (identical (map (lambda (n) 67 (identical (map (lambda (n)
41 (string-append store "/" (number->string n) 68 (string-append store "/" (number->string n)
42 "/a/b/c")) 69 "/a/b/c"))
@@ -46,7 +73,7 @@
46 (mkdir-p (dirname file)) 73 (mkdir-p (dirname file))
47 (call-with-output-file file 74 (call-with-output-file file
48 (lambda (port) 75 (lambda (port)
49 (put-bytevector port data)))) 76 (put-bytevector port (string->utf8 data)))))
50 identical) 77 identical)
51 ;; Make the parent of IDENTICAL read-only. This should not prevent 78 ;; Make the parent of IDENTICAL read-only. This should not prevent
52 ;; deduplication from inserting its hard link. 79 ;; deduplication from inserting its hard link.
@@ -54,7 +81,7 @@
54 81
55 (call-with-output-file unique 82 (call-with-output-file unique
56 (lambda (port) 83 (lambda (port)
57 (put-bytevector port (string->utf8 "This is unique.")))) 84 (put-bytevector port (string->utf8 (string-reverse data)))))
58 85
59 (deduplicate store (nar-sha256 store) #:store store) 86 (deduplicate store (nar-sha256 store) #:store store)
60 87
@@ -77,8 +104,10 @@
77 (lambda (store) 104 (lambda (store)
78 (let ((true-link link) 105 (let ((true-link link)
79 (links 0) 106 (links 0)
80 (data1 (string->utf8 "Hello, world!")) 107 (data1 (string->utf8
81 (data2 (string->utf8 "Hi, world!")) 108 (string-concatenate (make-list 1000 "Hello, world!"))))
109 (data2 (string->utf8
110 (string-concatenate (make-list 1000 "Hi, world!"))))
82 (identical (map (lambda (n) 111 (identical (map (lambda (n)
83 (string-append store "/" (number->string n) 112 (string-append store "/" (number->string n)
84 "/a/b/c")) 113 "/a/b/c"))
diff --git a/tests/store.scm b/tests/store.scm
index 2150a0048cb..5c9f651d6c0 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -759,7 +759,9 @@
759 759
760(test-assert "substitute, deduplication" 760(test-assert "substitute, deduplication"
761 (with-store s 761 (with-store s
762 (let* ((c (random-text)) ; contents of the output 762 ;; Note: C must be longer than %DEDUPLICATION-MINIMUM-SIZE.
763 (let* ((c (string-concatenate
764 (make-list 200 (random-text)))) ; contents of the output
763 (g (package-derivation s %bootstrap-guile)) 765 (g (package-derivation s %bootstrap-guile))
764 (d1 (build-expression->derivation s "substitute-me" 766 (d1 (build-expression->derivation s "substitute-me"
765 `(begin ,c (exit 1)) 767 `(begin ,c (exit 1))