Repository: borgbackup/borg

Issue:
implement borg benchmark cpu
## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes

## Is this a BUG / ISSUE report or a QUESTION?

ISSUE

# Describe the problem you're observing

https://borgbackup.readthedocs.io/en/master/usage/init.html says that `repokey-blake2` my be faster than `repokey` on some clients, but slower on others.

Instead of trying to give users hints on which mode might be faster we should show users how to compare the two modes on **their** hardware. Trusting advice given in the docs instead of doing your own experiments is a cargo cult (and we should discourage that!).

It looks like we already have [`borg benchmark crud`](https://borgbackup.readthedocs.io/en/master/usage/benchmark.html) that could be used for this purpose - someone just needs to figure out how and document that.
<!--
## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).

#### Operating system (distribution) and version.

#### Hardware / network configuration, and filesystems used.

#### How much data is handled by borg?

#### Full borg commandline that lead to the problem (leave away excludes and passwords)


## Describe the problem you're observing.

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

#### Include any warning/errors/backtraces from the system logs

If this complaint relates to borg performance, please include CRUD benchmark
results and any steps you took to troubleshoot.
How to run benchmark: http://borgbackup.readthedocs.io/en/stable/usage/benchmark.html

*IMPORTANT* - Please mark logs and text output from terminal commands 
or else Github will not display them correctly. 
An example is provided below.

Example:
```
this is an example how log text should be marked (wrap it with ```)
```
-->



Discussion:
Rather than using a "full stack" benchmark like the "crud" one (which depends on lots of stuff), we could also add a more specific one that shows hashing / MACing / encryption speeds.

Both results can be useful, depending on what you want to know. But the effects of slow / not slow sha256 will be much more visible in the more specific benchmark.
Man, if there was a "benchmark all" feature that generated report for all function at all levels would be amazing!
While we add some scope creep: msgpack and chunker speeds might be interesting also.

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -537,6 +537,102 @@ def test_files(path, count, size, random):
 
         return 0
 
+    def do_benchmark_cpu(self, args):
+        """Benchmark CPU bound operations."""
+        from timeit import timeit
+        random_10M = os.urandom(10*1000*1000)
+        key_256 = os.urandom(32)
+        key_128 = os.urandom(16)
+        key_96 = os.urandom(12)
+
+        import io
+        from borg.chunker import get_chunker
+        print("Chunkers =======================================================")
+        size = "1GB"
+
+        def chunkit(chunker_name, *args, **kwargs):
+            with io.BytesIO(random_10M) as data_file:
+                ch = get_chunker(chunker_name, *args, **kwargs)
+                for _ in ch.chunkify(fd=data_file):
+                    pass
+
+        for spec, func in [
+            ("buzhash,19,23,21,4095", lambda: chunkit("buzhash", 19, 23, 21, 4095, seed=0)),
+            ("fixed,1048576", lambda: chunkit("fixed", 1048576, sparse=False)),
+        ]:
+            print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")
+
+        import zlib
+        from borg.algorithms.checksums import crc32, deflate_crc32, xxh64
+        print("Non-cryptographic checksums / hashes ===========================")
+        size = "1GB"
+        tests = [
+            ("xxh64", lambda: xxh64(random_10M)),
+        ]
+        if crc32 is zlib.crc32:
+            tests.insert(0, ("crc32 (zlib, used)", lambda: crc32(random_10M)))
+            tests.insert(1, ("crc32 (libdeflate)", lambda: deflate_crc32(random_10M)))
+        elif crc32 is deflate_crc32:
+            tests.insert(0, ("crc32 (libdeflate, used)", lambda: crc32(random_10M)))
+            tests.insert(1, ("crc32 (zlib)", lambda: zlib.crc32(random_10M)))
+        else:
+            raise Error("crc32 benchmarking code missing")
+        for spec, func in tests:
+            print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")
+
+        from borg.crypto.low_level import hmac_sha256, blake2b_256
+        print("Cryptographic hashes / MACs ====================================")
+        size = "1GB"
+        for spec, func in [
+            ("hmac-sha256", lambda: hmac_sha256(key_256, random_10M)),
+            ("blake2b-256", lambda: blake2b_256(key_256, random_10M)),
+        ]:
+            print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")
+
+        from borg.crypto.low_level import AES256_CTR_BLAKE2b, AES256_CTR_HMAC_SHA256
+        from borg.crypto.low_level import AES256_OCB, CHACHA20_POLY1305
+        print("Encryption =====================================================")
+        size = "1GB"
+
+        for spec, func in [
+            ("aes-256-ctr-hmac-sha256", lambda: AES256_CTR_HMAC_SHA256(
+                key_256, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(random_10M, header=b'X')),
+            ("aes-256-ctr-blake2b", lambda: AES256_CTR_BLAKE2b(
+                key_256*4, key_256, iv=key_128, header_len=1, aad_offset=1).encrypt(random_10M, header=b'X')),
+            ("aes-256-ocb", lambda: AES256_OCB(
+                None, key_256, iv=key_96, header_len=1, aad_offset=1).encrypt(random_10M, header=b'X')),
+            ("chacha20-poly1305", lambda: CHACHA20_POLY1305(
+                None, key_256, iv=key_96, header_len=1, aad_offset=1).encrypt(random_10M, header=b'X')),
+        ]:
+            print(f"{spec:<24} {size:<10} {timeit(func, number=100):.3f}s")
+
+        from borg.compress import CompressionSpec
+        print("Compression ====================================================")
+        for spec in [
+            'lz4',
+            'zstd,1',
+            'zstd,3',
+            'zstd,22',
+            'zlib,0',
+            'zlib,6',
+            'zlib,9',
+            'lzma,0',
+            'lzma,6',
+            'lzma,9',
+        ]:
+            compressor = CompressionSpec(spec).compressor
+            size = "0.1GB"
+            print(f"{spec:<12} {size:<10} {timeit(lambda: compressor.compress(random_10M), number=10):.3f}s")
+
+        print("msgpack ========================================================")
+        item = Item(path="/foo/bar/baz", mode=660, mtime=1234567)
+        items = [item.as_dict(), ] * 1000
+        size = "100k Items"
+        spec = "msgpack"
+        print(f"{spec:<12} {size:<10} {timeit(lambda: msgpack.packb(items), number=100):.3f}s")
+
+        return 0
+
     @with_repository(fake='dry_run', exclusive=True, compatibility=(Manifest.Operation.WRITE,))
     def do_create(self, args, repository, manifest=None, key=None):
         """Create new archive"""
@@ -3119,6 +3215,21 @@ def define_borg_mount(parser):
 
         subparser.add_argument('path', metavar='PATH', help='path were to create benchmark input data')
 
+        bench_cpu_epilog = process_epilog("""
+        This command benchmarks misc. CPU bound borg operations.
+
+        It creates input data in memory, runs the operation and then displays throughput.
+        To reduce outside influence on the timings, please make sure to run this with:
+        - an otherwise as idle as possible machine
+        - enough free memory so there will be no slow down due to paging activity
+        """)
+        subparser = benchmark_parsers.add_parser('cpu', parents=[common_parser], add_help=False,
+                                                 description=self.do_benchmark_cpu.__doc__,
+                                                 epilog=bench_cpu_epilog,
+                                                 formatter_class=argparse.RawDescriptionHelpFormatter,
+                                                 help='benchmarks borg CPU bound operations.')
+        subparser.set_defaults(func=self.do_benchmark_cpu)
+
         # borg break-lock
         break_lock_epilog = process_epilog("""
         This command breaks the repository and cache locks.


Test patch: