Files
duplicati/Duplicati/Library/SharpRSync/DeltaFile.cs
T
kenneth.skovhede@gmail.com a02cdfc6cd Update issue #245
Status: Fixed

After a lot of work, I have now re-written the entire SharpRSync library.
My tests indicate that generating a diff file for an 8gb file took 10 minutes before, and now takes 8 minutes.
A delta for the same (unchanged) 8GB file took 58 minutes, and now takes 7 minutes.

The implementation seems to run faster under 64 bit, but I'm unsure if it is
related to the actual instructions issued, or the address simulation/conversion done by the OS.

I have also greatly optimized the amount of memory required by the program.
On windows, a delta for an 8GB file can be now be created with a total of 125 mb required memory.
The base app takes 65mb, so the used memory is around 60mb, and the raw diff file is approximately 44mb.

I compared it a bit to the rdiff program on linux.
For a 150mb file, rdiff takes about 1 second to generate a diff, and the same for a delta.
SharpRSync runs the same in about 7 seconds, although some of that is the mono startup and library loading.

For a 8gb file rdiff can generate a diff in about 500 seconds, where SharpRSync takes about 590 seconds.
When generating a delta, rdiff uses 530 seconds, and SharpRSync uses 770 seconds.
The last measurement is inaccurate as the memory requirement under mono is greater, and the test machine
had low memory, so there was a lot of swapping going on.

An interesting side effect is that the rdiff generated delta file is about 380kb, where the SharpRSync delta is 15 bytes.
This is because rdiff does not do the sequence matching optimization mentioned on the librsync page (last paragraph):
http://rsync.samba.org/tech_report/node4.html

Further optimization should probably focus on getting the MD4 algorithm to run faster.

git-svn-id: https://duplicati.googlecode.com/svn/trunk@480 59da171f-624f-0410-aa54-27559c288bec
2010-08-21 10:41:57 +00:00

360 lines
16 KiB
C#

#region Disclaimer / License
// Copyright (C) 2010, Kenneth Skovhede
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System;
using System.Collections.Generic;
using System.Text;
namespace Duplicati.Library.SharpRSync
{
/// <summary>
/// This class contains operations on a RDiff compatible delta file
/// </summary>
public class DeltaFile
{
/// <summary>
/// The size of the internal buffer used to read in data
/// </summary>
private const int BUFFER_SIZE = 100 * 1024;
/// <summary>
/// The ChecksumFileReader used to perform signature lookups
/// </summary>
ChecksumFileReader m_checksum;
/// <summary>
/// The possibly modified input data
/// </summary>
System.IO.Stream m_inputStream;
/// <summary>
/// Constructs a new DeltaFile based on a signature file.
/// This instance can be used to create a new DeltaFile
/// </summary>
/// <param name="checksum">The checksum to use</param>
public DeltaFile(ChecksumFileReader checksum)
{
m_checksum = checksum;
}
/// <summary>
/// Constructs a new DeltaFile by reading the stream.
/// This instance can be used to patch a file.
/// </summary>
/// <param name="inputStream">The stream containing the delta information</param>
public DeltaFile(System.IO.Stream inputStream)
{
m_inputStream = inputStream;
}
/// <summary>
/// Creates a new file based on the basefile and the delta information.
/// The basefile and output stream cannot point to the same resource (ea. file).
/// The base file MUST be seekable.
/// </summary>
/// <param name="basefile">A seekable stream with the baseinformation</param>
/// <param name="output">The stream to write the patched data to. Must not point to the same resource as the basefile.</param>
public void PatchFile(System.IO.Stream basefile, System.IO.Stream output)
{
//Validate the file header
byte[] sig = new byte[4];
if (Utility.ForceStreamRead(m_inputStream, sig, 4) != 4)
throw new Exception(Strings.DeltaFile.EndofstreamBeforeSignatureError);
for (int i = 0; i < sig.Length; i++)
if (RDiffBinary.DELTA_MAGIC[i] != sig[i])
throw new Exception(Strings.DeltaFile.InvalidSignatureError);
//Keep reading until we hit the end command
while (true)
{
int command = m_inputStream.ReadByte();
if (command == RDiffBinary.EndCommand)
break;
//It is an error to omit the end command
if (command < 0)
throw new Exception(Strings.DeltaFile.EndofstreamWithoutMarkerError);
if (Enum.IsDefined(typeof(RDiffBinary.LiteralDeltaCommand), (RDiffBinary.LiteralDeltaCommand)command))
{
//Find out how many bytes of literal data there is
int len = RDiffBinary.GetLiteralLength((RDiffBinary.LiteralDeltaCommand)command);
byte[] tmp = new byte[len];
if (Utility.ForceStreamRead(m_inputStream, tmp, tmp.Length) != tmp.Length)
throw new Exception(Strings.DeltaFile.UnexpectedEndofstreamError);
long size = RDiffBinary.DecodeLength(tmp);
if (size < 0)
throw new Exception(Strings.DeltaFile.InvalidLitteralSizeError);
//Copy the literal data from the patch to the output
Utility.StreamCopy(m_inputStream, output, size);
}
else if (Enum.IsDefined(typeof(RDiffBinary.CopyDeltaCommand), (RDiffBinary.CopyDeltaCommand)command))
{
//Find the offset of the data in the base file
int len = RDiffBinary.GetCopyOffsetSize((RDiffBinary.CopyDeltaCommand)command);
byte[] tmp = new byte[len];
if (Utility.ForceStreamRead(m_inputStream, tmp, tmp.Length) != tmp.Length)
throw new Exception(Strings.DeltaFile.UnexpectedEndofstreamError);
long offset = RDiffBinary.DecodeLength(tmp);
if (offset < 0)
throw new Exception(Strings.DeltaFile.InvalidCopyOffsetError);
//Find the length of the data to copy from the basefile
len = RDiffBinary.GetCopyLengthSize((RDiffBinary.CopyDeltaCommand)command);
tmp = new byte[len];
if (Utility.ForceStreamRead(m_inputStream, tmp, tmp.Length) != tmp.Length)
throw new Exception(Strings.DeltaFile.UnexpectedEndofstreamError);
long length = RDiffBinary.DecodeLength(tmp);
if (length < 0)
throw new Exception(Strings.DeltaFile.InvalidCopyLengthError);
//Seek to the begining, and copy
basefile.Position = offset;
Utility.StreamCopy(basefile, output, length);
}
else if (command <= RDiffBinary.LiteralLimit)
{
//Literal data less than 64 bytes are found, copy it
Utility.StreamCopy(m_inputStream, output, command);
}
else
throw new Exception(Strings.DeltaFile.UnknownCommandError);
}
output.Flush();
}
/// <summary>
/// Generates a delta file from input, and writes it to output
/// </summary>
/// <param name="input">The stream to generate the delta from</param>
/// <param name="output">The stream to write the delta to</param>
public void GenerateDeltaFile(System.IO.Stream input, System.IO.Stream output)
{
output.Write(RDiffBinary.DELTA_MAGIC, 0, RDiffBinary.DELTA_MAGIC.Length);
int blocklength = m_checksum.BlockLength;
int buffersize = BUFFER_SIZE;
//If we have some insanely large blocks, try to handle them nicely anyway
if (blocklength > BUFFER_SIZE / 2)
buffersize = blocklength * 4;
//The number of matched bytes
long matched = 0;
//The first matched byte
long matched_offset = 0;
//The index of the next expected block
long next_match_key = 0;
//The number of unmatched bytes
int unmatched = 0;
//The index of the first unmatched byte
int unmatched_offset = 0;
//Keep a local copy of the lookup table
bool[] weakLookup = m_checksum.WeakLookup;
//We use statically allocated buffers, and we need two buffers
// to prevent Array.Copy from allocating a temp buffer
byte[] working_data = new byte[BUFFER_SIZE];
byte[] temp_work = new byte[BUFFER_SIZE];
byte[] md4buf = new byte[blocklength];
//Read the initial buffer block
int buffer_len = Utility.ForceStreamRead(input, working_data);
int buffer_index = 0;
blocklength = Math.Min(blocklength, buffer_len);
//Setup the initial checksum
uint weakChecksum = Adler32Checksum.Calculate(working_data, 0, blocklength);
long indexMatched;
bool force_buffer_refill = false;
while (blocklength > 0)
{
//Check if the block matches somewhere
indexMatched = m_checksum.LookupChunck(weakChecksum, working_data, buffer_index, blocklength, next_match_key);
if (indexMatched >= 0)
{
//We have a match, flush unmatched
if (unmatched > 0)
{
WriteLiteral(working_data, unmatched_offset, unmatched, output);
unmatched = 0;
}
//First match
if (matched == 0)
{
matched_offset = indexMatched * blocklength;
}
else if (indexMatched != next_match_key)
{
//Subsequent match, but the sequence does not fit
WriteCopy(matched_offset, matched, output);
//Pretend this was the fist
matched = 0;
matched_offset = indexMatched * blocklength;
}
//If the next block matches this signature, we can write larger
// copy instructions and thus safe space
next_match_key = indexMatched + 1;
//Adjust the counters
matched += blocklength;
buffer_index += blocklength;
blocklength = Math.Min(blocklength, buffer_len - buffer_index);
//Reset the checksum to fit the new block
weakChecksum = Adler32Checksum.Calculate(working_data, buffer_index, blocklength);
}
else
{
//No match, flush accumulated matches, if any
if (matched > 0)
{
//Send the matching bytes as a copy
WriteCopy(matched_offset, matched, output);
matched = 0;
matched_offset = 0;
//We do not immediately start tapping the unmatched bytes,
// because the buffer may be nearly empty, and we
// want to gather as many unmatched bytes as possible
// to avoid the instruction overhead in the file
force_buffer_refill = true;
}
else
{
int lastPossible = buffer_len - blocklength;
if (unmatched == 0)
unmatched_offset = buffer_index;
//Local speedup for long non-matching regions
for (/* buffer_index = buffer_index */; buffer_index < lastPossible; buffer_index++)
{
weakChecksum = Adler32Checksum.Roll(working_data[buffer_index], working_data[buffer_index + blocklength], weakChecksum, blocklength);
if (weakLookup[weakChecksum >> 16])
break;
}
unmatched = buffer_index - unmatched_offset;
//If this is the last block, claim the remaining bytes as unmatched
if (temp_work == null)
{
unmatched += blocklength;
blocklength = 0;
}
}
}
//If we are out of buffer, try to load some more
if (force_buffer_refill || buffer_len - buffer_index <= m_checksum.BlockLength)
{
//Prevent continous refill
force_buffer_refill = false;
//If we have read the last bytes already, then just skip this
if (temp_work != null)
{
int remaining_bytes = buffer_len - buffer_index;
Array.Copy(working_data, buffer_index, temp_work, 0, remaining_bytes);
int tempread = Utility.ForceStreamRead(input, temp_work, remaining_bytes, temp_work.Length - remaining_bytes);
if (tempread > 0)
{
//We are about to discard some data, if it is unmatched, write it to stream
if (unmatched > 0)
{
WriteLiteral(working_data, unmatched_offset, unmatched, output);
unmatched = 0;
}
//Now swap the arrays
byte[] tmp = working_data;
working_data = temp_work;
temp_work = tmp;
buffer_index = 0;
buffer_len = remaining_bytes + tempread;
}
else
{
//Mark as done
temp_work = null;
//The last round has a smaller block length
blocklength = remaining_bytes;
}
}
}
}
//There cannot be both matched and unmatched bytes written
if (matched > 0 && unmatched > 0)
throw new Exception(Strings.DeltaFile.InternalBufferError);
if (matched > 0)
WriteCopy(matched_offset, matched, output);
if (unmatched > 0)
WriteLiteral(working_data, unmatched_offset, unmatched, output);
output.WriteByte((byte)RDiffBinary.EndCommand);
output.Flush();
}
/// <summary>
/// Writes a literal command to a delta stream
/// </summary>
/// <param name="data">The literal data to write</param>
/// <param name="output">The output delta stream</param>
private void WriteLiteral(byte[] data, int offset, int count, System.IO.Stream output)
{
output.WriteByte((byte)RDiffBinary.FindLiteralDeltaCommand(count));
byte[] len = RDiffBinary.EncodeLength(count);
output.Write(len, 0, len.Length);
output.Write(data, offset, count);
}
/// <summary>
/// Write a copy command to a delta stream
/// </summary>
/// <param name="offset">The offset in the basefile where the data is located</param>
/// <param name="length">The length of the data to copy</param>
/// <param name="output">The output delta stream</param>
private void WriteCopy(long offset, long length, System.IO.Stream output)
{
output.WriteByte((byte)RDiffBinary.FindCopyDeltaCommand(offset, length));
byte[] len = RDiffBinary.EncodeLength(offset);
output.Write(len, 0, len.Length);
len = RDiffBinary.EncodeLength(length);
output.Write(len, 0, len.Length);
}
}
}