##// END OF EJS Templates
localrepo: experimental support for non-zlib revlog compression...
localrepo: experimental support for non-zlib revlog compression The final part of integrating the compression manager APIs into revlog storage is the plumbing for repositories to advertise they are using non-zlib storage and for revlogs to instantiate a non-zlib compression engine. The main intent of the compression manager work was to zstd all of the things. Adding zstd to revlogs has proved to be more involved than other places because revlogs are... special. Very small inputs and the use of delta chains (which are themselves a form of compression) are a completely different use case from streaming compression, which bundles and the wire protocol employ. I've conducted numerous experiments with zstd in revlogs and have yet to formalize compression settings and a storage architecture that I'm confident I won't regret later. In other words, I'm not yet ready to commit to a new mechanism for using zstd - or any other compression format - in revlogs. That being said, having some support for zstd (and other compression formats) in revlogs in core is beneficial. It can allow others to conduct experiments. This patch introduces *highly experimental* support for non-zlib compression formats in revlogs. Introduced is a config option to control which compression engine to use. Also introduced is a namespace of "exp-compression-*" requirements to denote support for non-zlib compression in revlogs. I've prefixed the namespace with "exp-" (short for "experimental") because I'm not confident of the requirements "schema" and in no way want to give the illusion of supporting these requirements in the future. I fully intend to drop support for these requirements once we figure out what we're doing with zstd in revlogs. A good portion of the patch is teaching the requirements system about registered compression engines and passing the requested compression engine as an opener option so revlogs can instantiate the proper compression engine for new operations. That's a verbose way of saying "we can now use zstd in revlogs!" On an `hg pull` conversion of the mozilla-unified repo with no extra redelta settings (like aggressivemergedeltas), we can see the impact of zstd vs zlib in revlogs: $ hg perfrevlogchunks -c ! chunk ! wall 2.032052 comb 2.040000 user 1.990000 sys 0.050000 (best of 5) ! wall 1.866360 comb 1.860000 user 1.820000 sys 0.040000 (best of 6) ! chunk batch ! wall 1.877261 comb 1.870000 user 1.860000 sys 0.010000 (best of 6) ! wall 1.705410 comb 1.710000 user 1.690000 sys 0.020000 (best of 6) $ hg perfrevlogchunks -m ! chunk ! wall 2.721427 comb 2.720000 user 2.640000 sys 0.080000 (best of 4) ! wall 2.035076 comb 2.030000 user 1.950000 sys 0.080000 (best of 5) ! chunk batch ! wall 2.614561 comb 2.620000 user 2.580000 sys 0.040000 (best of 4) ! wall 1.910252 comb 1.910000 user 1.880000 sys 0.030000 (best of 6) $ hg perfrevlog -c -d 1 ! wall 4.812885 comb 4.820000 user 4.800000 sys 0.020000 (best of 3) ! wall 4.699621 comb 4.710000 user 4.700000 sys 0.010000 (best of 3) $ hg perfrevlog -m -d 1000 ! wall 34.252800 comb 34.250000 user 33.730000 sys 0.520000 (best of 3) ! wall 24.094999 comb 24.090000 user 23.320000 sys 0.770000 (best of 3) Only modest wins for the changelog. But manifest reading is significantly faster. What's going on? One reason might be data volume. zstd decompresses faster. So given more bytes, it will put more distance between it and zlib. Another reason is size. In the current design, zstd revlogs are *larger*: debugcreatestreamclonebundle (size in bytes) zlib: 1,638,852,492 zstd: 1,680,601,332 I haven't investigated this fully, but I reckon a significant cause of larger revlogs is that the zstd frame/header has more bytes than zlib's. For very small inputs or data that doesn't compress well, we'll tend to store more uncompressed chunks than with zlib (because the compressed size isn't smaller than original). This will make revlog reading faster because it is doing less decompression. Moving on to bundle performance: $ hg bundle -a -t none-v2 (total CPU time) zlib: 102.79s zstd: 97.75s So, marginal CPU decrease for reading all chunks in all revlogs (this is somewhat disappointing). $ hg bundle -a -t <engine>-v2 (total CPU time) zlib: 191.59s zstd: 115.36s This last test effectively measures the difference between zlib->zlib and zstd->zstd for revlogs to bundle. This is a rough approximation of what a server does during `hg clone`. There are some promising results for zstd. But not enough for me to feel comfortable advertising it to users. We'll get there...

File last commit:

r30746:9cb0bb0f default
r30818:4c0a5a25 default
Show More
revlogs.txt
201 lines | 7.6 KiB | text/plain | TextLexer
Revision logs - or *revlogs* - are an append only data structure for
storing discrete entries, or *revisions*. They are the primary storage
mechanism of repository data.
Revlogs effectively model a directed acyclic graph (DAG). Each node
has edges to 1 or 2 *parent* nodes. Each node contains metadata and
the raw value for that node.
Revlogs consist of entries which have metadata and revision data.
Metadata includes the hash of the revision's content, sizes, and
links to its *parent* entries. The collective metadata is referred
to as the *index* and the revision data is the *data*.
Revision data is stored as a series of compressed deltas against previous
revisions.
Revlogs are written in an append-only fashion. We never need to rewrite
a file to insert nor do we need to remove data. Rolling back in-progress
writes can be performed by truncating files. Read locks can be avoided
using simple techniques. This means that references to other data in
the same revlog *always* refer to a previous entry.
Revlogs can be modeled as 0-indexed arrays. The first revision is
revision #0 and the second is revision #1. The revision -1 is typically
used to mean *does not exist* or *not defined*.
File Format
===========
A revlog begins with a 32-bit big endian integer holding version info
and feature flags. This integer is shared with the first revision
entry.
This integer is logically divided into 2 16-bit shorts. The least
significant half of the integer is the format/version short. The other
short holds feature flags that dictate behavior of the revlog.
Only 1 bit of the format/version short is currently used. Remaining
bits are reserved for future use.
The following values for the format/version short are defined:
0
The original revlog version.
1
RevlogNG (*next generation*). It replaced version 0 when it was
implemented in 2006.
The feature flags short consists of bit flags. Where 0 is the least
significant bit, the following bit offsets define flags:
0
Store revision data inline.
1
Generaldelta encoding.
2-15
Reserved for future use.
The following header values are common:
00 00 00 01
RevlogNG
00 01 00 01
RevlogNG + inline
00 02 00 01
RevlogNG + generaldelta
00 03 00 01
RevlogNG + inline + generaldelta
Following the 32-bit header is the remainder of the first index entry.
Following that are remaining *index* data. Inlined revision data is
possibly located between index entries. More on this layout is described
below.
RevlogNG Format
===============
RevlogNG (version 1) begins with an index describing the revisions in
the revlog. If the ``inline`` flag is set, revision data is stored inline,
or between index entries (as opposed to in a separate container).
Each index entry is 64 bytes. The byte layout of each entry is as
follows, with byte 0 being the first byte (all data stored as big endian):
0-3 (4 bytes) (rev 0 only)
Revlog header
0-5 (6 bytes)
Absolute offset of revision data from beginning of revlog.
6-7 (2 bytes)
Bit flags impacting revision behavior. The following bit offsets define:
0: REVIDX_ISCENSORED revision has censor metadata, must be verified.
1: REVIDX_EXTSTORED revision data is stored externally.
8-11 (4 bytes)
Compressed length of revision data / chunk as stored in revlog.
12-15 (4 bytes)
Uncompressed length of revision data. This is the size of the full
revision data, not the size of the chunk post decompression.
16-19 (4 bytes)
Base or previous revision this revision's delta was produced against.
-1 means this revision holds full text (as opposed to a delta).
For generaldelta repos, this is the previous revision in the delta
chain. For non-generaldelta repos, this is the base or first
revision in the delta chain.
20-23 (4 bytes)
A revision this revision is *linked* to. This allows a revision in
one revlog to be forever associated with a revision in another
revlog. For example, a file's revlog may point to the changelog
revision that introduced it.
24-27 (4 bytes)
Revision of 1st parent. -1 indicates no parent.
28-31 (4 bytes)
Revision of 2nd parent. -1 indicates no 2nd parent.
32-63 (32 bytes)
Hash of revision's full text. Currently, SHA-1 is used and only
the first 20 bytes of this field are used. The rest of the bytes
are ignored and should be stored as \0.
If inline revision data is being stored, the compressed revision data
(of length from bytes offset 8-11 from the index entry) immediately
follows the index entry. There is no header on the revision data. There
is no padding between it and the index entries before and after.
If revision data is not inline, then raw revision data is stored in a
separate byte container. The offsets from bytes 0-5 and the compressed
length from bytes 8-11 define how to access this data.
The first 4 bytes of the revlog are shared between the revlog header
and the 6 byte absolute offset field from the first revlog entry.
Delta Chains
============
Revision data is encoded as a chain of *chunks*. Each chain begins with
the compressed original full text for that revision. Each subsequent
*chunk* is a *delta* against the previous revision. We therefore call
these chains of chunks/deltas *delta chains*.
The full text for a revision is reconstructed by loading the original
full text for the base revision of a *delta chain* and then applying
*deltas* until the target revision is reconstructed.
*Delta chains* are limited in length so lookup time is bound. They are
limited to ~2x the length of the revision's data. The linear distance
between the base chunk and the final chunk is also limited so the
amount of read I/O to load all chunks in the delta chain is bound.
Deltas and delta chains are either computed against the previous
revision in the revlog or another revision (almost certainly one of
the parents of the revision). Historically, deltas were computed against
the previous revision. The *generaldelta* revlog feature flag (enabled
by default in Mercurial 3.7) activates the mode where deltas are
computed against an arbitrary revision (almost certainly a parent revision).
File Storage
============
Revlogs logically consist of an index (metadata of entries) and
revision data. This data may be stored together in a single file or in
separate files. The mechanism used is indicated by the ``inline`` feature
flag on the revlog.
Mercurial's behavior is to use inline storage until a revlog reaches a
certain size, at which point it will be converted to non-inline. The
reason there is a size limit on inline storage is to establish an upper
bound on how much data must be read to load the index. It would be a waste
to read tens or hundreds of extra megabytes of data just to access the
index data.
The actual layout of revlog files on disk is governed by the repository's
*store format*. Typically, a ``.i`` file represents the index revlog
(possibly containing inline data) and a ``.d`` file holds the revision data.
Revision Entries
================
Revision entries consist of an optional 1 byte header followed by an
encoding of the revision data. The headers are as follows:
\0 (0x00)
Revision data is the entirety of the entry, including this header.
u (0x75)
Raw revision data follows.
x (0x78)
zlib (RFC 1950) data.
The 0x78 value is actually the first byte of the zlib header (CMF byte).
Hash Computation
================
The hash of the revision is stored in the index and is used both as a primary
key and for data integrity verification.
Currently, SHA-1 is the only supported hashing algorithm. To obtain the SHA-1
hash of a revision:
1. Hash the parent nodes
2. Hash the fulltext of the revision
The 20 byte node ids of the parents are fed into the hasher in ascending order.