about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/libstore/build.cc10
-rw-r--r--src/libstore/gc.cc54
-rw-r--r--src/libstore/local-store.cc5
-rw-r--r--src/libstore/local-store.hh18
-rw-r--r--src/libstore/optimise-store.cc1
-rw-r--r--src/libstore/remote-store.cc22
-rw-r--r--src/libstore/remote-store.hh3
-rw-r--r--src/libstore/store-api.hh113
-rw-r--r--src/libstore/worker-protocol.hh34
-rw-r--r--src/libutil/serialise.cc31
-rw-r--r--src/libutil/serialise.hh2
-rw-r--r--src/libutil/util.cc35
-rw-r--r--src/libutil/util.hh8
-rw-r--r--src/nix-store/nix-store.cc57
-rw-r--r--src/nix-worker/nix-worker.cc25
15 files changed, 252 insertions, 166 deletions
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 0664a118a00f..1d624723f92b 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -578,17 +578,17 @@ void getOwnership(const Path & path)
 
 
 void deletePathWrapped(const Path & path,
-    unsigned long long & bytesFreed)
+    unsigned long long & bytesFreed, unsigned long long & blocksFreed)
 {
     try {
         /* First try to delete it ourselves. */
-        deletePath(path, bytesFreed);
+        deletePath(path, bytesFreed, blocksFreed);
     } catch (SysError & e) {
         /* If this failed due to a permission error, then try it with
            the setuid helper. */
         if (haveBuildUsers() && !amPrivileged()) {
             getOwnership(path);
-            deletePath(path, bytesFreed);
+            deletePath(path, bytesFreed, blocksFreed);
         } else
             throw;
     }
@@ -597,8 +597,8 @@ void deletePathWrapped(const Path & path,
 
 void deletePathWrapped(const Path & path)
 {
-    unsigned long long dummy;
-    deletePathWrapped(path, dummy);
+    unsigned long long dummy1, dummy2;
+    deletePathWrapped(path, dummy1, dummy2);
 }
 
 
diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc
index c46cc817bc9d..0caf1ce4e627 100644
--- a/src/libstore/gc.cc
+++ b/src/libstore/gc.cc
@@ -439,9 +439,9 @@ Paths topoSortPaths(const PathSet & paths)
 }
 
 
-void LocalStore::tryToDelete(GCAction action, const PathSet & livePaths,
-    const PathSet & tempRootsClosed, PathSet & done, PathSet & deleted,
-    const Path & path, unsigned long long & bytesFreed)
+void LocalStore::tryToDelete(const GCOptions & options, GCResults & results, 
+    const PathSet & livePaths, const PathSet & tempRootsClosed, PathSet & done, 
+    const Path & path)
 {
     if (done.find(path) != done.end()) return;
     done.insert(path);
@@ -449,7 +449,7 @@ void LocalStore::tryToDelete(GCAction action, const PathSet & livePaths,
     debug(format("considering deletion of `%1%'") % path);
         
     if (livePaths.find(path) != livePaths.end()) {
-        if (action == gcDeleteSpecific)
+        if (options.action == GCOptions::gcDeleteSpecific)
             throw Error(format("cannot delete path `%1%' since it is still alive") % path);
         debug(format("live path `%1%'") % path);
         return;
@@ -470,15 +470,18 @@ void LocalStore::tryToDelete(GCAction action, const PathSet & livePaths,
         queryReferrers(path, referrers);
     foreach (PathSet::iterator, i, referrers)
         if (*i != path)
-            tryToDelete(action, livePaths, tempRootsClosed, done, deleted, *i, bytesFreed);
+            tryToDelete(options, results, livePaths, tempRootsClosed, done, *i);
 
     debug(format("dead path `%1%'") % path);
-    deleted.insert(path);
+    results.paths.insert(path);
 
     /* If just returning the set of dead paths, we also return the
        space that would be freed if we deleted them. */
-    if (action == gcReturnDead) {
-        bytesFreed += computePathSize(path);
+    if (options.action == GCOptions::gcReturnDead) {
+        unsigned long long bytesFreed, blocksFreed;
+        computePathSize(path, bytesFreed, blocksFreed);
+        results.bytesFreed += bytesFreed;
+        results.blocksFreed += blocksFreed;
         return;
     }
 
@@ -504,9 +507,10 @@ void LocalStore::tryToDelete(GCAction action, const PathSet & livePaths,
     printMsg(lvlInfo, format("deleting `%1%'") % path);
             
     /* Okay, it's safe to delete. */
-    unsigned long long freed;
-    deleteFromStore(path, freed);
-    bytesFreed += freed;
+    unsigned long long bytesFreed, blocksFreed;
+    deleteFromStore(path, bytesFreed, blocksFreed);
+    results.bytesFreed += bytesFreed;
+    results.blocksFreed += blocksFreed;
 
 #ifndef __CYGWIN__
     if (fdLock != -1)
@@ -516,12 +520,8 @@ void LocalStore::tryToDelete(GCAction action, const PathSet & livePaths,
 }
 
 
-void LocalStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
-    bool ignoreLiveness, PathSet & result, unsigned long long & bytesFreed)
+void LocalStore::collectGarbage(const GCOptions & options, GCResults & results)
 {
-    result.clear();
-    bytesFreed = 0;
-
     bool gcKeepOutputs =
         queryBoolSetting("gc-keep-outputs", false);
     bool gcKeepDerivations =
@@ -537,7 +537,7 @@ void LocalStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
     /* Find the roots.  Since we've grabbed the GC lock, the set of
        permanent roots cannot increase now. */
     printMsg(lvlError, format("finding garbage collector roots..."));
-    Roots rootMap = ignoreLiveness ? Roots() : nix::findRoots(true);
+    Roots rootMap = options.ignoreLiveness ? Roots() : nix::findRoots(true);
 
     PathSet roots;
     for (Roots::iterator i = rootMap.begin(); i != rootMap.end(); ++i)
@@ -547,11 +547,11 @@ void LocalStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
        NIX_ROOT_FINDER environment variable.  This is typically used
        to add running programs to the set of roots (to prevent them
        from being garbage collected). */
-    if (!ignoreLiveness)
+    if (!options.ignoreLiveness)
         addAdditionalRoots(roots);
 
-    if (action == gcReturnRoots) {
-        result = roots;
+    if (options.action == GCOptions::gcReturnRoots) {
+        results.paths = roots;
         return;
     }
 
@@ -595,8 +595,8 @@ void LocalStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
             }
     }
 
-    if (action == gcReturnLive) {
-        result = livePaths;
+    if (options.action == GCOptions::gcReturnLive) {
+        results.paths = livePaths;
         return;
     }
 
@@ -633,27 +633,25 @@ void LocalStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
        paths. */
     printMsg(lvlError, format("reading the Nix store..."));
     PathSet storePaths;
-    if (action != gcDeleteSpecific) {
+    if (options.action != GCOptions::gcDeleteSpecific) {
         Paths entries = readDirectory(nixStore);
         for (Paths::iterator i = entries.begin(); i != entries.end(); ++i)
             storePaths.insert(canonPath(nixStore + "/" + *i));
     } else {
-        for (PathSet::iterator i = pathsToDelete.begin();
-             i != pathsToDelete.end(); ++i)
-        {
+        foreach (PathSet::iterator, i, options.pathsToDelete) {
             assertStorePath(*i);
             storePaths.insert(*i);
         }
     }
 
     /* Try to delete store paths in the topologically sorted order. */
-    printMsg(lvlError, action == gcReturnDead
+    printMsg(lvlError, options.action == GCOptions::gcReturnDead
         ? format("looking for garbage...")
         : format("deleting garbage..."));
 
     PathSet done;
     foreach (PathSet::iterator, i, storePaths)
-        tryToDelete(action, livePaths, tempRootsClosed, done, result, *i, bytesFreed);
+        tryToDelete(options, results, livePaths, tempRootsClosed, done, *i);
 }
 
  
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index 2e53d0dc6da4..105f711228a8 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -851,7 +851,8 @@ Path LocalStore::importPath(bool requireSignature, Source & source)
 }
 
 
-void LocalStore::deleteFromStore(const Path & path, unsigned long long & bytesFreed)
+void LocalStore::deleteFromStore(const Path & path, unsigned long long & bytesFreed,
+    unsigned long long & blocksFreed)
 {
     bytesFreed = 0;
 
@@ -871,7 +872,7 @@ void LocalStore::deleteFromStore(const Path & path, unsigned long long & bytesFr
         invalidatePath(path);
     }
 
-    deletePathWrapped(path, bytesFreed);
+    deletePathWrapped(path, bytesFreed, blocksFreed);
 }
 
 
diff --git a/src/libstore/local-store.hh b/src/libstore/local-store.hh
index 444a5f3402df..1957b9f57948 100644
--- a/src/libstore/local-store.hh
+++ b/src/libstore/local-store.hh
@@ -25,10 +25,11 @@ struct OptimiseStats
     unsigned long sameContents;
     unsigned long filesLinked;
     unsigned long long bytesFreed;
+    unsigned long long blocksFreed;
     OptimiseStats()
     {
         totalFiles = sameContents = filesLinked = 0;
-        bytesFreed = 0;
+        bytesFreed = blocksFreed = 0;
     }
 };
 
@@ -89,11 +90,11 @@ public:
 
     Roots findRoots();
 
-    void collectGarbage(GCAction action, const PathSet & pathsToDelete,
-        bool ignoreLiveness, PathSet & result, unsigned long long & bytesFreed);
+    void collectGarbage(const GCOptions & options, GCResults & results);
 
     /* Delete a path from the Nix store. */
-    void deleteFromStore(const Path & path, unsigned long long & bytesFreed);
+    void deleteFromStore(const Path & path, unsigned long long & bytesFreed,
+        unsigned long long & blocksFreed);
     
     /* Optimise the disk space usage of the Nix store by hard-linking
        files with the same contents. */
@@ -143,10 +144,9 @@ private:
     
     void upgradeStore12();
 
-    void tryToDelete(GCAction action, const PathSet & livePaths,
-        const PathSet & tempRootsClosed, PathSet & done, PathSet & deleted,
-        const Path & path, unsigned long long & bytesFreed);
-    
+    void tryToDelete(const GCOptions & options, GCResults & results,
+        const PathSet & livePaths, const PathSet & tempRootsClosed, PathSet & done, 
+        const Path & path);
 };
 
 
@@ -179,7 +179,7 @@ void getOwnership(const Path & path);
 /* Like deletePath(), but changes the ownership of `path' using the
    setuid wrapper if necessary (and possible). */
 void deletePathWrapped(const Path & path,
-    unsigned long long & bytesFreed);
+    unsigned long long & bytesFreed, unsigned long long & blocksFreed);
 
 void deletePathWrapped(const Path & path);
  
diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc
index c260f253e3c7..bd76a1aeccef 100644
--- a/src/libstore/optimise-store.cc
+++ b/src/libstore/optimise-store.cc
@@ -101,6 +101,7 @@ static void hashAndLink(bool dryRun, HashToPath & hashToPath,
         
         stats.filesLinked++;
         stats.bytesFreed += st.st_size;
+        stats.blocksFreed += st.st_blocks;
     }
 
     if (S_ISDIR(st.st_mode)) {
diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc
index 14412b8c8b60..274196a2b6a9 100644
--- a/src/libstore/remote-store.cc
+++ b/src/libstore/remote-store.cc
@@ -372,24 +372,20 @@ Roots RemoteStore::findRoots()
 }
 
 
-void RemoteStore::collectGarbage(GCAction action, const PathSet & pathsToDelete,
-    bool ignoreLiveness, PathSet & result, unsigned long long & bytesFreed)
+void RemoteStore::collectGarbage(const GCOptions & options, GCResults & results)
 {
-    result.clear();
-    bytesFreed = 0;
     writeInt(wopCollectGarbage, to);
-    writeInt(action, to);
-    writeStringSet(pathsToDelete, to);
-    writeInt(ignoreLiveness, to);
+    writeInt(options.action, to);
+    writeStringSet(options.pathsToDelete, to);
+    writeInt(options.ignoreLiveness, to);
+    writeLongLong(options.maxFreed, to);
+    writeInt(options.maxLinks, to);
     
     processStderr();
     
-    result = readStringSet(from);
-
-    /* Ugh - NAR integers are 64 bits, but read/writeInt() aren't. */
-    unsigned int lo = readInt(from);
-    unsigned int hi = readInt(from);
-    bytesFreed = (((unsigned long long) hi) << 32) | lo;
+    results.paths = readStringSet(from);
+    results.bytesFreed = readLongLong(from);
+    results.blocksFreed = readLongLong(from);
 }
 
 
diff --git a/src/libstore/remote-store.hh b/src/libstore/remote-store.hh
index 1501591e2a57..c40feeaa4743 100644
--- a/src/libstore/remote-store.hh
+++ b/src/libstore/remote-store.hh
@@ -65,8 +65,7 @@ public:
     
     Roots findRoots();
 
-    void collectGarbage(GCAction action, const PathSet & pathsToDelete,
-        bool ignoreLiveness, PathSet & result, unsigned long long & bytesFreed);
+    void collectGarbage(const GCOptions & options, GCResults & results);
     
 private:
     AutoCloseFD fdSocket;
diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh
index a50dcf645193..760e71adc70b 100644
--- a/src/libstore/store-api.hh
+++ b/src/libstore/store-api.hh
@@ -16,14 +16,82 @@ namespace nix {
 typedef std::map<Path, Path> Roots;
 
 
-/* Garbage collector operation. */
-typedef enum {
-    gcReturnRoots,
-    gcReturnLive,
-    gcReturnDead,
-    gcDeleteDead,
-    gcDeleteSpecific,
-} GCAction;
+
+
+struct GCOptions
+{
+    /* Garbage collector operation:
+
+       - `gcReturnRoots': find and return the set of roots for the
+         garbage collector.  These are the store paths symlinked to in
+         the `gcroots' directory.
+
+       - `gcReturnLive': return the set of paths reachable from
+         (i.e. in the closure of) the roots.
+
+       - `gcReturnDead': return the set of paths not reachable from
+         the roots.
+
+       - `gcDeleteDead': actually delete the latter set.
+
+       - `gcDeleteSpecific': delete the paths listed in
+          `pathsToDelete', insofar as they are not reachable.
+    */
+    typedef enum {
+        gcReturnRoots,
+        gcReturnLive,
+        gcReturnDead,
+        gcDeleteDead,
+        gcDeleteSpecific,
+    } GCAction;
+
+    GCAction action;
+
+    /* If `ignoreLiveness' is set, then reachability from the roots is
+       ignored (dangerous!).  However, the paths must still be
+       unreferenced *within* the store (i.e., there can be no other
+       store paths that depend on them). */
+    bool ignoreLiveness;
+
+    /* For `gcDeleteSpecific', the paths to delete. */
+    PathSet pathsToDelete;
+
+    /* Stop after at least `maxFreed' bytes have been freed. */
+    unsigned long long maxFreed;
+
+    /* Stop after the number of hard links to the Nix store directory
+       has dropped to at least `maxLinks'. */
+    unsigned int maxLinks;
+
+    GCOptions() 
+    {
+        action = gcDeleteDead;
+        ignoreLiveness = false;
+        maxFreed = ULLONG_MAX;
+        maxLinks = UINT_MAX;
+    }
+};
+
+
+struct GCResults 
+{
+    /* Depending on the action, the GC roots, or the paths that would
+       be or have been deleted. */
+    PathSet paths;
+
+    /* For `gcReturnDead', `gcDeleteDead' and `gcDeleteSpecific', the
+       number of bytes that would be or was freed. */
+    unsigned long long bytesFreed;
+
+    /* The number of file system blocks that would be or was freed. */
+    unsigned long long blocksFreed;
+
+    GCResults()
+    {
+        bytesFreed = 0;
+        blocksFreed = 0;
+    }
+};
 
 
 class StoreAPI 
@@ -137,33 +205,8 @@ public:
        outside of the Nix store that point to `storePath'.  */
     virtual Roots findRoots() = 0;
 
-    /* Depending on `action', this function does the following:
-
-       - `gcReturnRoots': find and return the set of roots for the
-         garbage collector.  These are the store paths symlinked to in
-         the `gcroots' directory.
-
-       - `gcReturnLive': return the set of paths reachable from
-         (i.e. in the closure of) the roots.
-
-       - `gcReturnDead': return the set of paths not reachable from
-         the roots.
-
-       - `gcDeleteDead': actually delete the latter set.
-
-       - `gcDeleteSpecific': delete the paths listed in
-         `pathsToDelete', insofar as they are not reachable.
-
-       If `ignoreLiveness' is set, then reachability from the roots is
-       ignored (dangerous!).  However, the paths must still be
-       unreferenced *within* the store (i.e., there can be no other
-       store paths that depend on them).
-
-       For `gcReturnDead', `gcDeleteDead' and `gcDeleteSpecific', the
-       number of bytes that would be or was freed is returned in
-       `bytesFreed'. */
-    virtual void collectGarbage(GCAction action, const PathSet & pathsToDelete,
-        bool ignoreLiveness, PathSet & result, unsigned long long & bytesFreed) = 0;
+    /* Perform a garbage collection. */
+    virtual void collectGarbage(const GCOptions & options, GCResults & results) = 0;
 };
 
 
diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh
index 44db4ad9b141..d887ee59bd95 100644
--- a/src/libstore/worker-protocol.hh
+++ b/src/libstore/worker-protocol.hh
@@ -15,24 +15,24 @@ namespace nix {
 
 typedef enum {
     wopQuit = 0,
-    wopIsValidPath,
+    wopIsValidPath = 1,
     wopHasSubstitutes = 3,
-    wopQueryPathHash,
-    wopQueryReferences,
-    wopQueryReferrers,
-    wopAddToStore,
-    wopAddTextToStore,
-    wopBuildDerivations,
-    wopEnsurePath,
-    wopAddTempRoot,
-    wopAddIndirectRoot,
-    wopSyncWithGC,
-    wopFindRoots,
-    wopCollectGarbage,
-    wopExportPath,
-    wopImportPath,
-    wopQueryDeriver,
-    wopSetOptions,
+    wopQueryPathHash = 4,
+    wopQueryReferences = 5,
+    wopQueryReferrers = 6,
+    wopAddToStore = 7,
+    wopAddTextToStore = 8,
+    wopBuildDerivations = 9,
+    wopEnsurePath = 10,
+    wopAddTempRoot = 11,
+    wopAddIndirectRoot = 12,
+    wopSyncWithGC = 13,
+    wopFindRoots = 14,
+    wopExportPath = 16,
+    wopImportPath = 17,
+    wopQueryDeriver = 18,
+    wopSetOptions = 19,
+    wopCollectGarbage = 20,
 } WorkerOp;
 
 
diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc
index c3fd4dd10cc8..c13e8c7e38e5 100644
--- a/src/libutil/serialise.cc
+++ b/src/libutil/serialise.cc
@@ -41,6 +41,21 @@ void writeInt(unsigned int n, Sink & sink)
 }
 
 
+void writeLongLong(unsigned long long n, Sink & sink)
+{
+    unsigned char buf[8];
+    buf[0] = n & 0xff;
+    buf[1] = (n >> 8) & 0xff;
+    buf[2] = (n >> 16) & 0xff;
+    buf[3] = (n >> 24) & 0xff;
+    buf[4] = (n >> 32) & 0xff;
+    buf[5] = (n >> 40) & 0xff;
+    buf[6] = (n >> 48) & 0xff;
+    buf[7] = (n >> 56) & 0xff;
+    sink(buf, sizeof(buf));
+}
+
+
 void writeString(const string & s, Sink & sink)
 {
     unsigned int len = s.length();
@@ -84,6 +99,22 @@ unsigned int readInt(Source & source)
 }
 
 
+unsigned long long readLongLong(Source & source)
+{
+    unsigned char buf[8];
+    source(buf, sizeof(buf));
+    return
+        ((unsigned long long) buf[0]) |
+        ((unsigned long long) buf[1] << 8) |
+        ((unsigned long long) buf[2] << 16) |
+        ((unsigned long long) buf[3] << 24) |
+        ((unsigned long long) buf[4] << 32) |
+        ((unsigned long long) buf[5] << 40) |
+        ((unsigned long long) buf[6] << 48) |
+        ((unsigned long long) buf[7] << 56);
+}
+
+
 string readString(Source & source)
 {
     unsigned int len = readInt(source);
diff --git a/src/libutil/serialise.hh b/src/libutil/serialise.hh
index c18e82463b5a..75d17bc60502 100644
--- a/src/libutil/serialise.hh
+++ b/src/libutil/serialise.hh
@@ -95,11 +95,13 @@ struct StringSource : Source
 
 void writePadding(unsigned int len, Sink & sink);
 void writeInt(unsigned int n, Sink & sink);
+void writeLongLong(unsigned long long n, Sink & sink);
 void writeString(const string & s, Sink & sink);
 void writeStringSet(const StringSet & ss, Sink & sink);
 
 void readPadding(unsigned int len, Source & source);
 unsigned int readInt(Source & source);
+unsigned long long readLongLong(Source & source);
 string readString(Source & source);
 StringSet readStringSet(Source & source);
 
diff --git a/src/libutil/util.cc b/src/libutil/util.cc
index e18f9841fd4d..1873ccfe536c 100644
--- a/src/libutil/util.cc
+++ b/src/libutil/util.cc
@@ -229,30 +229,38 @@ void writeFile(const Path & path, const string & s)
 }
 
 
-unsigned long long computePathSize(const Path & path)
+static void _computePathSize(const Path & path,
+    unsigned long long & bytes, unsigned long long & blocks)
 {
-    unsigned long long size = 0;
-    
     checkInterrupt();
 
     struct stat st;
     if (lstat(path.c_str(), &st))
 	throw SysError(format("getting attributes of path `%1%'") % path);
 
-    size += st.st_size;
+    bytes += st.st_size;
+    blocks += st.st_blocks;
 
     if (S_ISDIR(st.st_mode)) {
 	Strings names = readDirectory(path);
 
 	for (Strings::iterator i = names.begin(); i != names.end(); ++i)
-            size += computePathSize(path + "/" + *i);
+            _computePathSize(path + "/" + *i, bytes, blocks);
     }
+}
 
-    return size;
+
+void computePathSize(const Path & path,
+    unsigned long long & bytes, unsigned long long & blocks)
+{
+    bytes = 0;
+    blocks = 0;
+    _computePathSize(path, bytes, blocks);
 }
 
 
-static void _deletePath(const Path & path, unsigned long long & bytesFreed)
+static void _deletePath(const Path & path, unsigned long long & bytesFreed,
+    unsigned long long & blocksFreed)
 {
     checkInterrupt();
 
@@ -263,6 +271,7 @@ static void _deletePath(const Path & path, unsigned long long & bytesFreed)
 	throw SysError(format("getting attributes of path `%1%'") % path);
 
     bytesFreed += st.st_size;
+    blocksFreed += st.st_blocks;
 
     if (S_ISDIR(st.st_mode)) {
 	Strings names = readDirectory(path);
@@ -274,7 +283,7 @@ static void _deletePath(const Path & path, unsigned long long & bytesFreed)
 	}
 
 	for (Strings::iterator i = names.begin(); i != names.end(); ++i)
-            _deletePath(path + "/" + *i, bytesFreed);
+            _deletePath(path + "/" + *i, bytesFreed, blocksFreed);
     }
 
     if (remove(path.c_str()) == -1)
@@ -284,17 +293,19 @@ static void _deletePath(const Path & path, unsigned long long & bytesFreed)
 
 void deletePath(const Path & path)
 {
-    unsigned long long dummy;
-    deletePath(path, dummy);
+    unsigned long long dummy1, dummy2;
+    deletePath(path, dummy1, dummy2);
 }
 
 
-void deletePath(const Path & path, unsigned long long & bytesFreed)
+void deletePath(const Path & path, unsigned long long & bytesFreed,
+    unsigned long long & blocksFreed)
 {
     startNest(nest, lvlDebug,
         format("recursively deleting path `%1%'") % path);
     bytesFreed = 0;
-    _deletePath(path, bytesFreed);
+    blocksFreed = 0;
+    _deletePath(path, bytesFreed, blocksFreed);
 }
 
 
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
index d52ab3e4d920..d1e30fa6b9e3 100644
--- a/src/libutil/util.hh
+++ b/src/libutil/util.hh
@@ -61,14 +61,16 @@ string readFile(const Path & path);
 void writeFile(const Path & path, const string & s);
 
 /* Compute the sum of the sizes of all files in `path'. */
-unsigned long long computePathSize(const Path & path);
+void computePathSize(const Path & path,
+    unsigned long long & bytes, unsigned long long & blocks);
 
 /* Delete a path; i.e., in the case of a directory, it is deleted
    recursively.  Don't use this at home, kids.  The second variant
-   returns the number of bytes freed. */
+   returns the number of bytes and blocks freed. */
 void deletePath(const Path & path);
 
-void deletePath(const Path & path, unsigned long long & bytesFreed);
+void deletePath(const Path & path, unsigned long long & bytesFreed,
+    unsigned long long & blocksFreed);
 
 /* Make a path read-only recursively. */
 void makePathReadOnly(const Path & path);
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index df027fcc7646..9618823ea681 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -489,19 +489,19 @@ static void opCheckValidity(Strings opFlags, Strings opArgs)
 }
 
 
-static string showBytes(unsigned long long bytes)
+static string showBytes(unsigned long long bytes, unsigned long long blocks)
 {
-    return (format("%d bytes (%.2f MiB)")
-        % bytes % (bytes / (1024.0 * 1024.0))).str();
+    return (format("%d bytes (%.2f MiB, %d blocks)")
+        % bytes % (bytes / (1024.0 * 1024.0)) % blocks).str();
 }
 
 
 struct PrintFreed 
 {
     bool show, dryRun;
-    unsigned long long bytesFreed;
-    PrintFreed(bool show, bool dryRun)
-        : show(show), dryRun(dryRun), bytesFreed(0) { }
+    const GCResults & results;
+    PrintFreed(bool show, bool dryRun, const GCResults & results)
+        : show(show), dryRun(dryRun), results(results) { }
     ~PrintFreed() 
     {
         if (show)
@@ -509,33 +509,35 @@ struct PrintFreed
                 (dryRun
                     ? "%1% would be freed\n"
                     : "%1% freed\n"))
-                % showBytes(bytesFreed);
+                % showBytes(results.bytesFreed, results.blocksFreed);
     }
 };
 
 
 static void opGC(Strings opFlags, Strings opArgs)
 {
-    GCAction action = gcDeleteDead;
+    GCOptions options;
+    options.action = GCOptions::gcDeleteDead;
+    
+    GCResults results;
     
     /* Do what? */
     for (Strings::iterator i = opFlags.begin();
          i != opFlags.end(); ++i)
-        if (*i == "--print-roots") action = gcReturnRoots;
-        else if (*i == "--print-live") action = gcReturnLive;
-        else if (*i == "--print-dead") action = gcReturnDead;
-        else if (*i == "--delete") action = gcDeleteDead;
+        if (*i == "--print-roots") options.action = GCOptions::gcReturnRoots;
+        else if (*i == "--print-live") options.action = GCOptions::gcReturnLive;
+        else if (*i == "--print-dead") options.action = GCOptions::gcReturnDead;
+        else if (*i == "--delete") options.action = GCOptions::gcDeleteDead;
         else throw UsageError(format("bad sub-operation `%1%' in GC") % *i);
 
-    PathSet result;
-    PrintFreed freed(action == gcDeleteDead || action == gcReturnDead,
-        action == gcReturnDead);
-    store->collectGarbage(action, PathSet(), false, result, freed.bytesFreed);
+    PrintFreed freed(
+        options.action == GCOptions::gcDeleteDead || options.action == GCOptions::gcReturnDead,
+        options.action == GCOptions::gcReturnDead, results);
+    store->collectGarbage(options, results);
 
-    if (action != gcDeleteDead) {
-        for (PathSet::iterator i = result.begin(); i != result.end(); ++i)
+    if (options.action != GCOptions::gcDeleteDead)
+        foreach (PathSet::iterator, i, results.paths)
             cout << *i << std::endl;
-    }
 }
 
 
@@ -544,22 +546,21 @@ static void opGC(Strings opFlags, Strings opArgs)
    roots). */
 static void opDelete(Strings opFlags, Strings opArgs)
 {
-    bool ignoreLiveness = false;
+    GCOptions options;
+    options.action = GCOptions::gcDeleteSpecific;
     
     for (Strings::iterator i = opFlags.begin();
          i != opFlags.end(); ++i)
-        if (*i == "--ignore-liveness") ignoreLiveness = true;
+        if (*i == "--ignore-liveness") options.ignoreLiveness = true;
         else throw UsageError(format("unknown flag `%1%'") % *i);
 
-    PathSet pathsToDelete;
     for (Strings::iterator i = opArgs.begin();
          i != opArgs.end(); ++i)
-        pathsToDelete.insert(followLinksToStorePath(*i));
+        options.pathsToDelete.insert(followLinksToStorePath(*i));
     
-    PathSet dummy;
-    PrintFreed freed(true, false);
-    store->collectGarbage(gcDeleteSpecific, pathsToDelete, ignoreLiveness,
-        dummy, freed.bytesFreed);
+    GCResults results;
+    PrintFreed freed(true, false, results);
+    store->collectGarbage(options, results);
 }
 
 
@@ -653,7 +654,7 @@ static void showOptimiseStats(OptimiseStats & stats)
 {
     printMsg(lvlError,
         format("%1% freed by hard-linking %2% files; there are %3% files with equal contents out of %4% files in total")
-        % showBytes(stats.bytesFreed)
+        % showBytes(stats.bytesFreed, stats.blocksFreed)
         % stats.filesLinked
         % stats.sameContents
         % stats.totalFiles);
diff --git a/src/nix-worker/nix-worker.cc b/src/nix-worker/nix-worker.cc
index 0d0964f3a668..d8d86434ea95 100644
--- a/src/nix-worker/nix-worker.cc
+++ b/src/nix-worker/nix-worker.cc
@@ -395,23 +395,24 @@ static void performOp(unsigned int clientVersion,
     }
 
     case wopCollectGarbage: {
-        GCAction action = (GCAction) readInt(from);
-        PathSet pathsToDelete = readStorePaths(from);
-        bool ignoreLiveness = readInt(from);
-
-        PathSet result;
-        unsigned long long bytesFreed;
+        GCOptions options;
+        options.action = (GCOptions::GCAction) readInt(from);
+        options.pathsToDelete = readStorePaths(from);
+        options.ignoreLiveness = readInt(from);
+        options.maxFreed = readLongLong(from);
+        options.maxLinks = readInt(from);
+
+        GCResults results;
         
         startWork();
-        if (ignoreLiveness)
+        if (options.ignoreLiveness)
             throw Error("you are not allowed to ignore liveness");
-        store->collectGarbage(action, pathsToDelete, ignoreLiveness,
-            result, bytesFreed);
+        store->collectGarbage(options, results);
         stopWork();
         
-        writeStringSet(result, to);
-        writeInt(bytesFreed & 0xffffffff, to);
-        writeInt(bytesFreed >> 32, to);
+        writeStringSet(results.paths, to);
+        writeLongLong(results.bytesFreed, to);
+        writeLongLong(results.blocksFreed, to);
         
         break;
     }