Skip to content

Commit f28b7c5

Browse files
Return generated typemap assemblies without a second buffer
Emitting a typemap assembly serialised the PE image into a BlobBuilder and then copied every byte again into a MemoryStream, so each generated assembly was held twice while it was produced. Return a read-only, seekable stream over the chunks the serialiser already produced instead. GeneratedAssembly.Content is now typed as Stream, which is all the build task needs — it hashes the stream and copies it to disk through Files.CopyIfStreamChanged, so atomic last-known-good replacement is unchanged, as are the generated bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 2870151 commit f28b7c5

8 files changed

Lines changed: 371 additions & 10 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection.Metadata;
5+
6+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
7+
8+
/// <summary>
9+
/// A read-only, seekable <see cref="Stream"/> over the chunks a <see cref="BlobBuilder"/> already
10+
/// holds, so a serialised PE image can be hashed and copied to disk without being duplicated into
11+
/// a second contiguous buffer.
12+
/// </summary>
13+
/// <remarks>
14+
/// Only the chunk arrays are retained — the <see cref="BlobBuilder"/> itself and the metadata
15+
/// graph that produced it stay collectible — so the live byte count matches what a
16+
/// <see cref="MemoryStream"/> copy would have held, without the transient second copy.
17+
/// </remarks>
18+
sealed class BlobBuilderStream : Stream
19+
{
20+
readonly ArraySegment<byte> [] segments;
21+
readonly long [] segmentStarts;
22+
readonly long length;
23+
long position;
24+
int cursor;
25+
26+
public BlobBuilderStream (BlobBuilder builder)
27+
{
28+
_ = builder ?? throw new ArgumentNullException (nameof (builder));
29+
30+
var collected = new List<ArraySegment<byte>> ();
31+
foreach (var blob in builder.GetBlobs ()) {
32+
var bytes = blob.GetBytes ();
33+
if (bytes.Count == 0 || bytes.Array is null) {
34+
continue;
35+
}
36+
collected.Add (bytes);
37+
}
38+
39+
segments = collected.ToArray ();
40+
segmentStarts = new long [segments.Length + 1];
41+
long total = 0;
42+
for (int i = 0; i < segments.Length; i++) {
43+
segmentStarts [i] = total;
44+
total += segments [i].Count;
45+
}
46+
segmentStarts [segments.Length] = total;
47+
length = total;
48+
}
49+
50+
public override bool CanRead => true;
51+
52+
public override bool CanSeek => true;
53+
54+
public override bool CanWrite => false;
55+
56+
public override long Length => length;
57+
58+
public override long Position {
59+
get => position;
60+
set {
61+
if (value < 0) {
62+
throw new ArgumentOutOfRangeException (nameof (value));
63+
}
64+
position = value;
65+
}
66+
}
67+
68+
public override void Flush ()
69+
{
70+
}
71+
72+
public override int Read (byte [] buffer, int offset, int count)
73+
{
74+
if (buffer is null) {
75+
throw new ArgumentNullException (nameof (buffer));
76+
}
77+
if (offset < 0 || count < 0 || buffer.Length - offset < count) {
78+
throw new ArgumentOutOfRangeException (nameof (count));
79+
}
80+
81+
int copied = 0;
82+
while (count > 0 && position < length) {
83+
int index = FindSegment (position);
84+
var segment = segments [index];
85+
int within = (int) (position - segmentStarts [index]);
86+
int available = segment.Count - within;
87+
int toCopy = Math.Min (available, count);
88+
if (segment.Array is null) {
89+
break;
90+
}
91+
Buffer.BlockCopy (segment.Array, segment.Offset + within, buffer, offset, toCopy);
92+
position += toCopy;
93+
offset += toCopy;
94+
count -= toCopy;
95+
copied += toCopy;
96+
}
97+
return copied;
98+
}
99+
100+
public override long Seek (long offset, SeekOrigin origin)
101+
{
102+
long target = origin switch {
103+
SeekOrigin.Begin => offset,
104+
SeekOrigin.Current => position + offset,
105+
SeekOrigin.End => length + offset,
106+
_ => throw new ArgumentOutOfRangeException (nameof (origin)),
107+
};
108+
if (target < 0) {
109+
throw new IOException ("Cannot seek before the beginning of the stream.");
110+
}
111+
position = target;
112+
return position;
113+
}
114+
115+
public override void SetLength (long value) => throw new NotSupportedException ();
116+
117+
public override void Write (byte [] buffer, int offset, int count) => throw new NotSupportedException ();
118+
119+
int FindSegment (long offset)
120+
{
121+
// Reads are overwhelmingly sequential, so try the last used chunk first.
122+
if (cursor < segments.Length && offset >= segmentStarts [cursor] && offset < segmentStarts [cursor + 1]) {
123+
return cursor;
124+
}
125+
126+
int low = 0;
127+
int high = segments.Length - 1;
128+
while (low <= high) {
129+
int middle = low + ((high - low) / 2);
130+
if (offset < segmentStarts [middle]) {
131+
high = middle - 1;
132+
} else if (offset >= segmentStarts [middle + 1]) {
133+
low = middle + 1;
134+
} else {
135+
cursor = middle;
136+
return middle;
137+
}
138+
}
139+
throw new ArgumentOutOfRangeException (nameof (offset));
140+
}
141+
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan<b
104104
/// Serialises the metadata + IL into a PE DLL and writes it to the given <paramref name="stream"/>.
105105
/// </summary>
106106
public void WritePE (Stream stream)
107+
{
108+
var peBlob = SerializePE ();
109+
if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) {
110+
memoryStream.Capacity = peBlob.Count;
111+
}
112+
peBlob.WriteContentTo (stream);
113+
}
114+
115+
/// <summary>
116+
/// Serialises the metadata + IL into a PE DLL and returns a read-only stream over the
117+
/// serialised bytes. Unlike <see cref="WritePE(Stream)"/> the image is not copied into a
118+
/// second contiguous buffer.
119+
/// </summary>
120+
public Stream CreatePEStream () => new BlobBuilderStream (SerializePE ());
121+
122+
BlobBuilder SerializePE ()
107123
{
108124
var peBuilder = new ManagedPEBuilder (
109125
new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
@@ -114,10 +130,7 @@ public void WritePE (Stream stream)
114130
deterministicIdProvider: DeterministicContentId);
115131
var peBlob = new BlobBuilder ();
116132
peBuilder.Serialize (peBlob);
117-
if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) {
118-
memoryStream.Capacity = peBlob.Count;
119-
}
120-
peBlob.WriteContentTo (stream);
133+
return peBlob;
121134
}
122135

123136
static BlobContentId DeterministicContentId (IEnumerable<Blob> content)

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,21 @@ internal void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedType
188188
_pe.WritePE (stream);
189189
}
190190

191+
/// <summary>
192+
/// Emits a PE assembly from the given model and returns a read-only stream over the serialised
193+
/// image, avoiding the copy into a second buffer that <see cref="Emit(TypeMapAssemblyData, Stream, bool, byte[])"/>
194+
/// performs.
195+
/// </summary>
196+
internal Stream EmitToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint)
197+
{
198+
if (model is null) {
199+
throw new ArgumentNullException (nameof (model));
200+
}
201+
202+
EmitCore (model, useSharedTypemapUniverse, contentFingerprint);
203+
return _pe.CreatePEStream ();
204+
}
205+
191206
void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint)
192207
{
193208
contentFingerprint ??= MetadataHelper

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ internal void Generate (TypeMapAssemblyData model, Stream stream, bool useShared
5555
emitter.Emit (model, stream, useSharedTypemapUniverse, contentFingerprint);
5656
}
5757

58+
/// <summary>
59+
/// Generates the PE assembly and returns a read-only stream over the serialised image without
60+
/// copying it into a second buffer.
61+
/// </summary>
62+
internal Stream GenerateToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint = null)
63+
{
64+
var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
65+
return emitter.EmitToStream (model, useSharedTypemapUniverse, contentFingerprint);
66+
}
67+
5868
/// <summary>
5969
/// Emits an empty typemap assembly (containing no type map entries) with the given
6070
/// <paramref name="assemblyName"/>, writing it to <paramref name="stream"/>. Used to satisfy

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,7 @@ internal List<GeneratedAssembly> GenerateTypeMapAssemblies (
326326
continue;
327327
}
328328
}
329-
var stream = new MemoryStream ();
330-
generator.Generate (model, stream, useSharedTypemapUniverse, fingerprints.Content);
331-
stream.Position = 0;
329+
var stream = generator.GenerateToStream (model, useSharedTypemapUniverse, fingerprints.Content);
332330
generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream));
333331
logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count);
334332
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ public record TrimmableTypeMapResult (
2020
ApplicationRegistrationTypes ?? [];
2121
}
2222

23-
public record GeneratedAssembly (string Name, MemoryStream Content);
23+
/// <summary>
24+
/// A generated typemap assembly. <paramref name="Content"/> is a read-only, seekable stream
25+
/// positioned at the start of the serialised PE image; callers own it and should dispose it.
26+
/// </summary>
27+
public record GeneratedAssembly (string Name, Stream Content);
2428

2529
public record GeneratedJavaSource (string RelativePath, string Content);
2630

0 commit comments

Comments
 (0)