-
Notifications
You must be signed in to change notification settings - Fork 578
Stream typemap fingerprints and PE output #12602
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
Open
simonrozsival
wants to merge
5
commits into
simonrozsival-incremental-typemap-emission
Choose a base branch
from
simonrozsival-typemap-optimization-retry
base: simonrozsival-incremental-typemap-emission
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3ebb677
Reduce typemap signature encoder allocations
simonrozsival e8ac902
Reuse metadata signature encoder
simonrozsival e055229
Compute typemap fingerprints in a single streaming pass
simonrozsival d5ee47f
Return generated typemap assemblies without a second buffer
simonrozsival 3eb5249
Address code review feedback
simonrozsival File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
147 changes: 147 additions & 0 deletions
147
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Reflection.Metadata; | ||
|
|
||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap; | ||
|
|
||
| /// <summary> | ||
| /// A read-only, seekable <see cref="Stream"/> over the chunks a <see cref="BlobBuilder"/> already | ||
| /// holds, so a serialised PE image can be hashed and copied to disk without being duplicated into | ||
| /// a second contiguous buffer. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Only the chunk arrays are retained — the <see cref="BlobBuilder"/> itself and the metadata | ||
| /// graph that produced it stay collectible — so the live byte count matches what a | ||
| /// <see cref="MemoryStream"/> copy would have held, without the transient second copy. | ||
| /// </remarks> | ||
| sealed class BlobBuilderStream : Stream | ||
| { | ||
| readonly ArraySegment<byte> [] segments; | ||
| readonly long [] segmentStarts; | ||
| readonly long length; | ||
| long position; | ||
| int cursor; | ||
|
|
||
| public BlobBuilderStream (BlobBuilder builder) | ||
| { | ||
| _ = builder ?? throw new ArgumentNullException (nameof (builder)); | ||
|
|
||
| var collected = new List<ArraySegment<byte>> (); | ||
| foreach (var blob in builder.GetBlobs ()) { | ||
| var bytes = blob.GetBytes (); | ||
| if (bytes.Count == 0 || bytes.Array is null) { | ||
| continue; | ||
| } | ||
| collected.Add (bytes); | ||
| } | ||
|
|
||
| segments = collected.ToArray (); | ||
| segmentStarts = new long [segments.Length + 1]; | ||
| long total = 0; | ||
| for (int i = 0; i < segments.Length; i++) { | ||
| segmentStarts [i] = total; | ||
| total += segments [i].Count; | ||
| } | ||
| segmentStarts [segments.Length] = total; | ||
| length = total; | ||
| } | ||
|
|
||
| public override bool CanRead => true; | ||
|
|
||
| public override bool CanSeek => true; | ||
|
|
||
| public override bool CanWrite => false; | ||
|
|
||
| public override long Length => length; | ||
|
|
||
| public override long Position { | ||
| get => position; | ||
| set { | ||
| if (value < 0) { | ||
| throw new ArgumentOutOfRangeException (nameof (value)); | ||
| } | ||
| position = value; | ||
| } | ||
| } | ||
|
|
||
| public override void Flush () | ||
| { | ||
| } | ||
|
|
||
| public override int Read (byte [] buffer, int offset, int count) | ||
| { | ||
| if (buffer is null) { | ||
| throw new ArgumentNullException (nameof (buffer)); | ||
| } | ||
| if (offset < 0) { | ||
| throw new ArgumentOutOfRangeException (nameof (offset)); | ||
| } | ||
| if (count < 0) { | ||
| throw new ArgumentOutOfRangeException (nameof (count)); | ||
| } | ||
| if (buffer.Length - offset < count) { | ||
| throw new ArgumentException ("The buffer is too small for the requested range."); | ||
| } | ||
|
|
||
| int copied = 0; | ||
| while (count > 0 && position < length) { | ||
| int index = FindSegment (position); | ||
| var segment = segments [index]; | ||
| int within = (int) (position - segmentStarts [index]); | ||
| int available = segment.Count - within; | ||
| int toCopy = Math.Min (available, count); | ||
| if (segment.Array is null) { | ||
| break; | ||
| } | ||
| Buffer.BlockCopy (segment.Array, segment.Offset + within, buffer, offset, toCopy); | ||
| position += toCopy; | ||
| offset += toCopy; | ||
| count -= toCopy; | ||
| copied += toCopy; | ||
| } | ||
| return copied; | ||
| } | ||
|
|
||
| public override long Seek (long offset, SeekOrigin origin) | ||
| { | ||
| long target = origin switch { | ||
| SeekOrigin.Begin => offset, | ||
| SeekOrigin.Current => position + offset, | ||
| SeekOrigin.End => length + offset, | ||
| _ => throw new ArgumentOutOfRangeException (nameof (origin)), | ||
| }; | ||
| if (target < 0) { | ||
| throw new IOException ("Cannot seek before the beginning of the stream."); | ||
| } | ||
| position = target; | ||
| return position; | ||
| } | ||
|
|
||
| public override void SetLength (long value) => throw new NotSupportedException (); | ||
|
|
||
| public override void Write (byte [] buffer, int offset, int count) => throw new NotSupportedException (); | ||
|
|
||
| int FindSegment (long offset) | ||
| { | ||
| // Reads are overwhelmingly sequential, so try the last used chunk first. | ||
| if (cursor < segments.Length && offset >= segmentStarts [cursor] && offset < segmentStarts [cursor + 1]) { | ||
| return cursor; | ||
| } | ||
|
|
||
| int low = 0; | ||
| int high = segments.Length - 1; | ||
| while (low <= high) { | ||
| int middle = low + ((high - low) / 2); | ||
| if (offset < segmentStarts [middle]) { | ||
| high = middle - 1; | ||
| } else if (offset >= segmentStarts [middle + 1]) { | ||
| low = middle + 1; | ||
| } else { | ||
| cursor = middle; | ||
| return middle; | ||
| } | ||
| } | ||
| throw new ArgumentOutOfRangeException (nameof (offset)); | ||
| } | ||
| } |
183 changes: 183 additions & 0 deletions
183
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| using System; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
|
|
||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap; | ||
|
|
||
| /// <summary> | ||
| /// Streams model fields into one or two SHA-256 hashes without materialising the whole | ||
| /// serialised model in memory. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// The byte stream produced for each sink is identical to what | ||
| /// <see cref="System.IO.BinaryWriter"/> would have written into a <see cref="System.IO.MemoryStream"/>: | ||
| /// strings are UTF-8 encoded with a 7-bit encoded byte-length prefix, <see cref="bool"/> is one | ||
| /// byte, and <see cref="int"/> is four bytes little-endian. Keeping the encoding identical means | ||
| /// the resulting fingerprints — and therefore the deterministic MVIDs derived from them — are | ||
| /// unchanged. | ||
| /// </para> | ||
| /// <para> | ||
| /// Two sinks are supported so the content fingerprint (which seeds the MVID) and the | ||
| /// incremental-build fingerprint can be produced from a single walk over the model. Fields shared | ||
| /// by both fingerprints are UTF-8 encoded once and appended to both sinks; fields belonging to | ||
| /// only one fingerprint are appended to that sink alone. | ||
| /// </para> | ||
| /// </remarks> | ||
| sealed class FingerprintWriter : IDisposable | ||
| { | ||
| /// <summary>Selects which fingerprint(s) a write applies to.</summary> | ||
| [Flags] | ||
| public enum Sink | ||
| { | ||
| Content = 1, | ||
| Incremental = 2, | ||
| Both = Content | Incremental, | ||
| } | ||
|
|
||
| // Large enough to absorb the small field writes that dominate the model walk without | ||
| // paying per-field hash update costs, small enough to stay off the large object heap. | ||
| const int BufferSize = 8 * 1024; | ||
|
|
||
| readonly IncrementalHash contentHash; | ||
| readonly IncrementalHash? incrementalHash; | ||
| readonly byte [] contentBuffer; | ||
| readonly byte []? incrementalBuffer; | ||
| byte [] scratch = new byte [512]; | ||
| int contentPosition; | ||
| int incrementalPosition; | ||
|
|
||
| public FingerprintWriter (bool includeIncremental) | ||
| { | ||
| contentHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); | ||
| contentBuffer = new byte [BufferSize]; | ||
| if (includeIncremental) { | ||
| incrementalHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); | ||
| incrementalBuffer = new byte [BufferSize]; | ||
| } | ||
| } | ||
|
|
||
| public void WriteString (Sink sink, string value) | ||
| { | ||
| int byteCount = Encoding.UTF8.GetByteCount (value); | ||
| EnsureScratch (byteCount + 5); | ||
| int offset = Write7BitEncodedInt (scratch, 0, byteCount); | ||
| Encoding.UTF8.GetBytes (value, 0, value.Length, scratch, offset); | ||
| Write (sink, scratch, 0, offset + byteCount); | ||
| } | ||
|
|
||
| public void WriteOptionalString (Sink sink, string? value) | ||
| { | ||
| WriteBoolean (sink, value is not null); | ||
| if (value is not null) { | ||
| WriteString (sink, value); | ||
| } | ||
| } | ||
|
|
||
| public void WriteBoolean (Sink sink, bool value) => WriteByte (sink, value ? (byte) 1 : (byte) 0); | ||
|
|
||
| public void WriteByte (Sink sink, byte value) | ||
| { | ||
| scratch [0] = value; | ||
| Write (sink, scratch, 0, 1); | ||
| } | ||
|
|
||
| public void WriteInt32 (Sink sink, int value) | ||
| { | ||
| scratch [0] = (byte) value; | ||
| scratch [1] = (byte) (value >> 8); | ||
| scratch [2] = (byte) (value >> 16); | ||
| scratch [3] = (byte) (value >> 24); | ||
| Write (sink, scratch, 0, 4); | ||
| } | ||
|
|
||
| public void WriteRaw (Sink sink, byte [] value) => Write (sink, value, 0, value.Length); | ||
|
|
||
| public byte [] GetContentFingerprint () | ||
| { | ||
| FlushContent (); | ||
| return contentHash.GetHashAndReset (); | ||
| } | ||
|
|
||
| public byte [] GetIncrementalFingerprint () | ||
| { | ||
| if (incrementalHash is null) { | ||
| throw new InvalidOperationException ("The incremental fingerprint was not requested."); | ||
| } | ||
| FlushIncremental (); | ||
| return incrementalHash.GetHashAndReset (); | ||
| } | ||
|
|
||
| public void Dispose () | ||
| { | ||
| contentHash.Dispose (); | ||
| incrementalHash?.Dispose (); | ||
| } | ||
|
|
||
| void EnsureScratch (int required) | ||
| { | ||
| if (scratch.Length < required) { | ||
| scratch = new byte [Math.Max (required, scratch.Length * 2)]; | ||
| } | ||
| } | ||
|
|
||
| void Write (Sink sink, byte [] data, int offset, int count) | ||
| { | ||
| if ((sink & Sink.Content) != 0) { | ||
| if (count > contentBuffer.Length - contentPosition) { | ||
| FlushContent (); | ||
| } | ||
| if (count > contentBuffer.Length) { | ||
| contentHash.AppendData (data, offset, count); | ||
| } else { | ||
| Buffer.BlockCopy (data, offset, contentBuffer, contentPosition, count); | ||
| contentPosition += count; | ||
| } | ||
| } | ||
| if ((sink & Sink.Incremental) == 0) { | ||
| return; | ||
| } | ||
| if (incrementalHash is null || incrementalBuffer is null) { | ||
| // Silently dropping the write would produce a fingerprint that looks valid but | ||
| // covers less than it claims to, so fail fast on the caller's mistake instead. | ||
| throw new InvalidOperationException ( | ||
| $"Cannot write to {nameof (Sink.Incremental)} because the incremental fingerprint was not requested."); | ||
| } | ||
| if (count > incrementalBuffer.Length - incrementalPosition) { | ||
| FlushIncremental (); | ||
| } | ||
| if (count > incrementalBuffer.Length) { | ||
| incrementalHash.AppendData (data, offset, count); | ||
| } else { | ||
| Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); | ||
| incrementalPosition += count; | ||
| } | ||
| } | ||
|
|
||
| void FlushContent () | ||
| { | ||
| if (contentPosition > 0) { | ||
| contentHash.AppendData (contentBuffer, 0, contentPosition); | ||
| contentPosition = 0; | ||
| } | ||
| } | ||
|
|
||
| void FlushIncremental () | ||
| { | ||
| if (incrementalPosition > 0 && incrementalHash is not null && incrementalBuffer is not null) { | ||
| incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); | ||
| incrementalPosition = 0; | ||
| } | ||
| } | ||
|
|
||
| static int Write7BitEncodedInt (byte [] destination, int offset, int value) | ||
| { | ||
| uint remaining = (uint) value; | ||
| while (remaining > 0x7Fu) { | ||
| destination [offset++] = (byte) (remaining | ~0x7Fu); | ||
| remaining >>= 7; | ||
| } | ||
| destination [offset++] = (byte) remaining; | ||
| return offset; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.