comparison mercurial/util.py @ 30442:41a8106789ca

util: implement zstd compression engine Now that zstd is vendored and being built (in some configurations), we can implement a compression engine for zstd! The zstd engine is a little different from existing engines. Because it may not always be present, we have to defer load the module in case importing it fails. We facilitate this via a cached property that holds a reference to the module or None. The "available" method is implemented to reflect reality. The zstd engine declares its ability to handle bundles using the "zstd" human name and the "ZS" internal name. The latter was chosen because internal names are 2 characters (by only convention I think) and "ZS" seems reasonable. The engine, like others, supports specifying the compression level. However, there are no consumers of this API that yet pass in that argument. I have plans to change that, so stay tuned. Since all we need to do to support bundle generation with a new compression engine is implement and register the compression engine, bundle generation with zstd "just works!" Tests demonstrating this have been added. How does performance of zstd for bundle generation compare? On the mozilla-unified repo, `hg bundle --all -t <engine>-v2` yields the following on my i7-6700K on Linux: engine CPU time bundle size vs orig size throughput none 97.0s 4,054,405,584 100.0% 41.8 MB/s bzip2 (l=9) 393.6s 975,343,098 24.0% 10.3 MB/s gzip (l=6) 184.0s 1,140,533,074 28.1% 22.0 MB/s zstd (l=1) 108.2s 1,119,434,718 27.6% 37.5 MB/s zstd (l=2) 111.3s 1,078,328,002 26.6% 36.4 MB/s zstd (l=3) 113.7s 1,011,823,727 25.0% 35.7 MB/s zstd (l=4) 116.0s 1,008,965,888 24.9% 35.0 MB/s zstd (l=5) 121.0s 977,203,148 24.1% 33.5 MB/s zstd (l=6) 131.7s 927,360,198 22.9% 30.8 MB/s zstd (l=7) 139.0s 912,808,505 22.5% 29.2 MB/s zstd (l=12) 198.1s 854,527,714 21.1% 20.5 MB/s zstd (l=18) 681.6s 789,750,690 19.5% 5.9 MB/s On compression, zstd for bundle generation delivers: * better compression than gzip with significantly less CPU utilization * better than bzip2 compression ratios while still being significantly faster than gzip * ability to aggressively tune compression level to achieve significantly smaller bundles That last point is important. With clone bundles, a server can pre-generate a bundle file, upload it to a static file server, and redirect clients to transparently download it during clone. The server could choose to produce a zstd bundle with the highest compression settings possible. This would take a very long time - a magnitude longer than a typical zstd bundle generation - but the result would be hundreds of megabytes smaller! For the clone volume we do at Mozilla, this could translate to petabytes of bandwidth savings per year and faster clones (due to smaller transfer size). I don't have detailed numbers to report on decompression. However, zstd decompression is fast: >1 GB/s output throughput on this machine, even through the Python bindings. And it can do that regardless of the compression level of the input. By the time you have enough data to worry about overhead of decompression, you have plenty of other things to worry about performance wise. zstd is wins all around. I can't wait to implement support for it on the wire protocol and in revlogs.
author Gregory Szorc <gregory.szorc@gmail.com>
date Fri, 11 Nov 2016 01:10:07 -0800
parents 90933e4e44fd
children 00c9ac4ce816
comparison
equal deleted inserted replaced
30441:de48d3a0573a 30442:41a8106789ca
3195 def decompressorreader(self, fh): 3195 def decompressorreader(self, fh):
3196 return fh 3196 return fh
3197 3197
3198 compengines.register(_noopengine()) 3198 compengines.register(_noopengine())
3199 3199
3200 class _zstdengine(compressionengine):
3201 def name(self):
3202 return 'zstd'
3203
3204 @propertycache
3205 def _module(self):
3206 # Not all installs have the zstd module available. So defer importing
3207 # until first access.
3208 try:
3209 from . import zstd
3210 # Force delayed import.
3211 zstd.__version__
3212 return zstd
3213 except ImportError:
3214 return None
3215
3216 def available(self):
3217 return bool(self._module)
3218
3219 def bundletype(self):
3220 return 'zstd', 'ZS'
3221
3222 def compressstream(self, it, opts=None):
3223 opts = opts or {}
3224 # zstd level 3 is almost always significantly faster than zlib
3225 # while providing no worse compression. It strikes a good balance
3226 # between speed and compression.
3227 level = opts.get('level', 3)
3228
3229 zstd = self._module
3230 z = zstd.ZstdCompressor(level=level).compressobj()
3231 for chunk in it:
3232 data = z.compress(chunk)
3233 if data:
3234 yield data
3235
3236 yield z.flush()
3237
3238 def decompressorreader(self, fh):
3239 zstd = self._module
3240 dctx = zstd.ZstdDecompressor()
3241 return chunkbuffer(dctx.read_from(fh))
3242
3243 compengines.register(_zstdengine())
3244
3200 # convenient shortcut 3245 # convenient shortcut
3201 dst = debugstacktrace 3246 dst = debugstacktrace