Repository: borgbackup/borg

Issue:
borg mount / slow / 100% cpu usage / mac
I created a local backup with borg 1.0.6 on Mac El Capitan of a 6GB Raw File.
After mounting the repo locally (borg mount using OSXFuse 3.3.3)I tried to copy the file.
Perfomance is really bad:

```
rsync  stdin --size-only --progress /Users/myuser/
Password:
stdin
    31227904   0%   74.86kB/s   29:17:01
```

"borg mount" process consumes 100%cpu (Core i7)



Discussion:
There is a known performance issue in the FUSE implementation in the 1.0 series (#966).

If borg-extract shows adequate performance then this is a duplicate of #966

What also helps is to do something like `dd if=<large file on FUSE mount> bs=16M | some other program(s) [or just use of=/where/you/want/to/save/it`. Recent `GNU dd` versions have status=progress to show how fast it's going, otherwise you can use the usual `killall -SIGUSR1 dd` thing.

Also, in general, the fuse mount is rather for convenience than performance.
If you know you will deal with huge files or with lots of files, borg extract is usually quicker.

I'll try it with extract, but i have **no** performance issues with my other productive backup(borg 1.0.3/linux debian jessie64).
Using borg mount is very convenient if you want to startup a backup VM with `qemu -hda /my/rawfile.img -snapshot`

Currently rsync calculates **24h for 6GB file**
(Thanks for this fast feedback)

EDIT: borg extract runs fast(as expected)

When I `strace` an `rsync` then I see that it often does a 32k read followed by one or more 4 byte (!) or 8 byte reads, e.g.

```
pid   653] read(0, "..."..., 32768) = 32768
[pid   653] read(0, "\366k3\24if\260#", 8) = 8
[pid   653] read(0, "..."..., 32768) = 32768
```

Let's suppose that 2 out of 3 reads are not 32k but 4 or 8 bytes (which seems to be approx the case here) then the average read size is only 10k. Less if these short reads happen more often. Thus we arrive at an average slowdown of 2 MB / 10 kB (or less) = 200 or more. If borg-extract would manage, say, 50 MB/s, then this will lead to only 250 kB/s through FUSE on 1.0.x

I am not sure how qemu handles IO to disk files, but if it pretty much reads them on a page-by-page basis (4 kB) then it's probably bad performance too.

@ThomasWaldmann : Backport #965 to 1.0-maint for 1.0.7?

Yes, seems simple enough.

Just checked current master.
Much better:

```
bash-3.2# rsync  -r stdin --size-only --progress /Users/myuser/
building file list ... 
1 file to consider
stdin
   604798976   7%    4.28MB/s    0:27:48
```

`qemu-system-x86_64 -hda stdin  -snapshot -m 1024`

Works also as expected.
Fantastic support, waiting for 1.0.7 

I'm running tests on a cherry pick right now, PR should be up soon.


Resolving patch:
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -1275,6 +1275,11 @@ def build_parser(self, args=None, prog=None):
         option is given the command will run in the background until the filesystem
         is ``umounted``.
 
+        The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users
+        to tweak the performance. It sets the number of cached data chunks; additional
+        memory usage can be up to ~8 MiB times this number. The default is the number
+        of CPU cores.
+
         For mount options, see the fuse(8) manual page. Additional mount options
         supported by borg:
 
diff --git a/borg/fuse.py b/borg/fuse.py
--- a/borg/fuse.py
+++ b/borg/fuse.py
@@ -13,6 +13,7 @@
 from .archive import Archive
 from .helpers import daemonize, bigint_to_int
 from .logger import create_logger
+from .lrucache import LRUCache
 logger = create_logger()
 
 
@@ -62,6 +63,9 @@ def __init__(self, key, repository, manifest, archive, cached_repo):
         self.pending_archives = {}
         self.accounted_chunks = {}
         self.cache = ItemCache()
+        data_cache_capacity = int(os.environ.get('BORG_MOUNT_DATA_CACHE_ENTRIES', os.cpu_count() or 1))
+        logger.debug('mount data cache capacity: %d chunks', data_cache_capacity)
+        self.data_cache = LRUCache(capacity=data_cache_capacity, dispose=lambda _: None)
         if archive:
             self.process_archive(archive)
         else:
@@ -282,8 +286,17 @@ def read(self, fh, offset, size):
                 offset -= s
                 continue
             n = min(size, s - offset)
-            chunk = self.key.decrypt(id, self.repository.get(id))
-            parts.append(chunk[offset:offset + n])
+            if id in self.data_cache:
+                data = self.data_cache[id]
+                if offset + n == len(data):
+                    # evict fully read chunk from cache
+                    del self.data_cache[id]
+            else:
+                data = self.key.decrypt(id, self.repository.get(id))
+                if offset + n < len(data):
+                    # chunk was only partially read, cache it
+                    self.data_cache[id] = data
+            parts.append(data[offset:offset + n])
             offset = 0
             size -= n
             if not size:


Test patch: