-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve performance of Tar library #74281
Conversation
Not a perf thing, just readability.
…sync variants, and overhaul GenerateExtendedAttributesDataStream
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsLow-hanging fruit.
private MemoryStream _archive = new MemoryStream();
private string[] _names;
private MemoryStream[] _streams;
[GlobalSetup]
public void Setup()
{
_names = Enumerable.Range(0, 10).Select(i => $"HelloWorld{i}.txt").ToArray();
_streams = _names.Select(s => new MemoryStream(Encoding.UTF8.GetBytes(s))).ToArray();
}
[Benchmark]
[Arguments(TarEntryFormat.Pax)]
[Arguments(TarEntryFormat.Gnu)]
[Arguments(TarEntryFormat.Ustar)]
public void Roundtrip(TarEntryFormat format)
{
using (TarWriter writer = new TarWriter(_archive, leaveOpen: true))
{
for (int i = 0; i < _names.Length; i++)
{
_streams[i].Position = 0;
TarEntry entry = format switch
{
TarEntryFormat.Pax => new PaxTarEntry(TarEntryType.RegularFile, _names[i]) { DataStream = _streams[i] },
TarEntryFormat.Gnu => new GnuTarEntry(TarEntryType.RegularFile, _names[i]) { DataStream = _streams[i] },
_ => new UstarTarEntry(TarEntryType.RegularFile, _names[i]) { DataStream = _streams[i] },
};
writer.WriteEntry(entry);
}
}
_archive.Position = 0;
using (TarReader reader = new TarReader(_archive, leaveOpen: true))
{
TarEntry entry;
while ((entry = reader.GetNextEntry()) != null)
{
entry.DataStream?.CopyTo(Stream.Null);
}
}
}
|
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs
Show resolved
Hide resolved
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs
Show resolved
Hide resolved
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs
Outdated
Show resolved
Hide resolved
// The checksum accumulator first adds up the byte values of eight space chars, then the final number | ||
// is written on top of those spaces on the specified span as ascii. | ||
// At the end, it's saved in the header field and the final value returned. | ||
internal int WriteChecksum(int checksum, Span<byte> buffer) | ||
internal static int WriteChecksum(int checksum, Span<byte> buffer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@carlossanlop, even before my changes, is this method functionally correct? The input and output spans are the same length, but the output span has two characters at the end reserved, so are we frequently losing digits from the checksum of the checksum is large enough?
(I didn't want to mess with the logic if it was already buggy, but this should be changed to just span.CopyTo rather than an open-coded loop.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have to investigate this with a header whose fields have ascii characters with the highest possible values, so that when they get added up for the checksum, its final value would go beyond the checksum field length, excluding the two reserved characters at the end.
return string.Format(PaxHeadersFormat, dirName, processId, fileName, trailingSeparator); | ||
return _typeFlag is TarEntryType.Directory or TarEntryType.DirectoryList ? | ||
$"{dirName}/PaxHeaders.{Environment.ProcessId}/{fileName}" : | ||
$"{dirName}/PaxHeaders.{Environment.ProcessId}/{fileName}{Path.DirectorySeparatorChar}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. this condition is reversed... but no tests are failing as a result. I'll fix the inversion, but it seems like a test gap that should be addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can add a test for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for your help, @stephentoub. No blocking comments from me. I'll address your question to me separately.
The CI passed so I'll merge it so I can submit this as an RC1 backport.
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs
Show resolved
Hide resolved
// The checksum accumulator first adds up the byte values of eight space chars, then the final number | ||
// is written on top of those spaces on the specified span as ascii. | ||
// At the end, it's saved in the header field and the final value returned. | ||
internal int WriteChecksum(int checksum, Span<byte> buffer) | ||
internal static int WriteChecksum(int checksum, Span<byte> buffer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have to investigate this with a header whose fields have ascii characters with the highest possible values, so that when they get added up for the checksum, its final value would go beyond the checksum field length, excluding the two reserved characters at the end.
/backport to release/7.0-rc1 |
Started backporting to release/7.0-rc1: https://github.com/dotnet/runtime/actions/runs/2897330001 |
// "XX attribute=value\n" | ||
// where "XX" is the number of characters in the entry, including those required for the count itself. | ||
int length = 3 + Encoding.UTF8.GetByteCount(attribute) + Encoding.UTF8.GetByteCount(value); | ||
length += CountDigits(length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was meant to model the logic previously there, but I think this logic needs to be tweaked, and it should be more like:
int digitCount = CountDigits(length) ;
length += digitCount;
length += CountDigits(length) - digitCount; // account for possible digit length increase
Do we have tests for this stuff? Do we validate that the archives at produce are readable by other tools? Our own code appears to ignore this length when reading archives (maybe it shouldn't?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we even sure the length is supposed to include itself? Have you seen that being done in archives produced by other tools? That would be a very strange format design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From man tar 5:
The extended attributes themselves are stored as a series of text-format lines encoded in the portable UTF-8 encoding. Each line consists of a decimal number, a space, a key string, an equals sign, a value string, and a new line. The decimal number indicates the length of the entire line, including the initial length field and the trailing newline. An example of such a field is:
25 ctime=1084839148.1212\n
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's strange design.
I did verify that the tar
tool was able to read our archives containing extended attributes entries.
Since we know the length of the data section, I didn't feel it was too important to verify that the length of each extended attribute entry was correct, considering that we need to look for a mandatory newline char suffix. Maybe if we checked the length number and advanced the position instead of searching for the newline, we could improve perf a bit. I can investigate if this is true and determine if we need to submit a PR to verify the length number.
for (int i = 0; i < dest.Length; i++) | ||
{ | ||
char ch = dest[i]; | ||
if (ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If on Unix both these constants are /
runtime/src/libraries/Common/src/System/IO/PathInternal.Unix.cs
Lines 13 to 14 in e71a958
internal const char DirectorySeparatorChar = '/'; | |
internal const char AltDirectorySeparatorChar = '/'; |
we could skip the cycle on Unix. On Windows we could do only one check
\
in the cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is the same on both platforms.
@EgorBo just out of curiosity, would the JIT in theory be able to legally collapse such a thing? ie, if (ch == '/' || ch == '/')
..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is the same on both platforms.
It makes no sense to replace /
with /
on Unix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that, but what are you proposing -- duplicate this method for Unix and Windows so they can be different? Is this code path that hot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not super hot, but it can be improved. I need to push up a fix anyway, so I'll do so.
while (true) | ||
{ | ||
digits[i] = (byte)('0' + (remaining % 8)); | ||
remaining /= 8; | ||
if (remaining == 0) break; | ||
i--; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SharpLab show that there are no optimizations with shifts as I'd expect. So why not unsafe convert the long to span[8]?
* Avoid unnecessary byte[] allocations * Remove unnecessary use of FileStreamOptions * Clean up Dispose{Async} implementations * Clean up unnecessary consts Not a perf thing, just readability. * Remove MemoryStream/Encoding.UTF8.GetBytes allocations, unnecessary async variants, and overhaul GenerateExtendedAttributesDataStream * Avoid string allocations in ReadMagicAttribute * Avoid allocation in WriteAsOctal * Improve handling of octal * Avoid allocation for version string * Removing boxing and char string allocation in GenerateExtendedAttributeName * Fix a couple unnecessary dictionary lookups * Replace Enum.HasFlag usage * Remove allocations from Write{Posix}Name * Replace ArrayPool use with string.Create * Replace more superfluous ArrayPool usage * Remove ArrayPool use from System.IO.Compression.ZipFile * Fix inverted condition * Use generic math to parse octal * Remove allocations from StringReader and string.Split * Remove magic string allocation for Ustar when not V7 * Remove file name and directory name allocation in GenerateExtendedAttributeName
* Avoid unnecessary byte[] allocations * Remove unnecessary use of FileStreamOptions * Clean up Dispose{Async} implementations * Clean up unnecessary consts Not a perf thing, just readability. * Remove MemoryStream/Encoding.UTF8.GetBytes allocations, unnecessary async variants, and overhaul GenerateExtendedAttributesDataStream * Avoid string allocations in ReadMagicAttribute * Avoid allocation in WriteAsOctal * Improve handling of octal * Avoid allocation for version string * Removing boxing and char string allocation in GenerateExtendedAttributeName * Fix a couple unnecessary dictionary lookups * Replace Enum.HasFlag usage * Remove allocations from Write{Posix}Name * Replace ArrayPool use with string.Create * Replace more superfluous ArrayPool usage * Remove ArrayPool use from System.IO.Compression.ZipFile * Fix inverted condition * Use generic math to parse octal * Remove allocations from StringReader and string.Split * Remove magic string allocation for Ustar when not V7 * Remove file name and directory name allocation in GenerateExtendedAttributeName
* Improve performance of Tar library (#74281) * Avoid unnecessary byte[] allocations * Remove unnecessary use of FileStreamOptions * Clean up Dispose{Async} implementations * Clean up unnecessary consts Not a perf thing, just readability. * Remove MemoryStream/Encoding.UTF8.GetBytes allocations, unnecessary async variants, and overhaul GenerateExtendedAttributesDataStream * Avoid string allocations in ReadMagicAttribute * Avoid allocation in WriteAsOctal * Improve handling of octal * Avoid allocation for version string * Removing boxing and char string allocation in GenerateExtendedAttributeName * Fix a couple unnecessary dictionary lookups * Replace Enum.HasFlag usage * Remove allocations from Write{Posix}Name * Replace ArrayPool use with string.Create * Replace more superfluous ArrayPool usage * Remove ArrayPool use from System.IO.Compression.ZipFile * Fix inverted condition * Use generic math to parse octal * Remove allocations from StringReader and string.Split * Remove magic string allocation for Ustar when not V7 * Remove file name and directory name allocation in GenerateExtendedAttributeName * fix tar strings (#74321) * Fix some corner cases in TarReader (#74329) * Fix a few Tar issues post perf improvements (#74338) * Fix a few Tar issues post perf improvements * Update src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs * Skip directory symlink recursion on TarFile archive creation (#74376) * Skip directory symlink recursion on TarFile archive creation * Add symlink verification * Address suggestions by danmoseley Co-authored-by: carlossanlop <[email protected]> * SkipBlockAlignmentPadding must be executed for all entries (#74396) * Set modified timestamps on files being extracted from tar archives (#74400) * Add tests for exotic external tar asset archives, fix some more corner case bugs (#74412) * Remove unused _readFirstEntry. Remnant from before we created PaxGlobalExtendedAttributesEntry. * Set the position of the freshly copied data stream to 0, so the first user access of the DataStream property gives them a stream ready to read from the beginning. * Process a PAX actual entry's data block only after the extended attributes are analyzed, in case the size is found as an extended attribute and needs to be overriden. * Add tests to verify the entries of the new external tar assets can be read. Verify their DataStream if available. * Add copyData argument to recent alignment padding tests. * Throw an exception sooner and with a clearer message when a data section is unexpected for the entry type. * Allow trailing nulls and spaces in octal fields. Co-authored-by: @am11 Adeel Mujahid <[email protected]> * Throw a clearer exception if the unsupported sparse file entry type is encountered. These entries have additional data that indicates the locations of sparse bytes, which cannot be read with just the size field. So to avoid accidentally offseting the reader, we throw. * Tests. * Rename to TrimLeadingNullsAndSpaces Co-authored-by: carlossanlop <[email protected]> * Remove Compression changes, keep changes confined to Tar. * Fix build failure due to missing using in TarHelpers.Windows Co-authored-by: Stephen Toub <[email protected]> Co-authored-by: Dan Moseley <[email protected]> Co-authored-by: Adeel Mujahid <[email protected]> Co-authored-by: carlossanlop <[email protected]> Co-authored-by: David Cantú <[email protected]>
Low-hanging fruit.