Skip to content

Commit 8a0e9d7

Browse files
committed
Create AssemblySizeTest.cs
1 parent f70dee2 commit 8a0e9d7

File tree

1 file changed

+339
-0
lines changed

1 file changed

+339
-0
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
public class AssemblySizeTest
2+
{
3+
static readonly string[] TargetFrameworks =
4+
[
5+
"netstandard2.0",
6+
"netstandard2.1",
7+
"net461",
8+
"net462",
9+
"net47",
10+
"net471",
11+
"net472",
12+
"net48",
13+
"net481",
14+
"netcoreapp2.0",
15+
"netcoreapp2.1",
16+
"netcoreapp2.2",
17+
"netcoreapp3.0",
18+
"netcoreapp3.1",
19+
"net5.0",
20+
"net6.0",
21+
"net7.0",
22+
"net8.0",
23+
"net9.0",
24+
"net10.0"
25+
];
26+
27+
[Test]
28+
[Explicit]
29+
public void MeasureAssemblySizes()
30+
{
31+
var tempDir = Path.Combine(Path.GetTempPath(), $"PolyfillSizeTest_{Guid.NewGuid():N}");
32+
Directory.CreateDirectory(tempDir);
33+
34+
try
35+
{
36+
Console.WriteLine("Building variants...");
37+
var (sizes, sourceSizes) = MeasureAllVariants(tempDir);
38+
var results = ConvertToSizeResults(sizes);
39+
40+
// Compute EmbedUntrackedSources results by adding source file sizes
41+
var resultsWithEmbed = ConvertToSizeResultsWithEmbed(sizes, sourceSizes);
42+
43+
// Generate markdown
44+
var mdPath = Path.Combine(ProjectFiles.SolutionDirectory, "..", "assemblySize.include.md");
45+
using var writer = File.CreateText(mdPath);
46+
47+
WriteTable(writer, results, "Assembly Sizes");
48+
writer.WriteLine();
49+
writer.WriteLine();
50+
WriteTable(writer, resultsWithEmbed, "Assembly Sizes with EmbedUntrackedSources");
51+
52+
Console.WriteLine($"Results written to {mdPath}");
53+
}
54+
finally
55+
{
56+
Directory.Delete(tempDir, recursive: true);
57+
}
58+
}
59+
60+
static (Dictionary<string, Dictionary<string, long>> assemblySizes, Dictionary<string, long> sourceSizes) MeasureAllVariants(string baseDir)
61+
{
62+
var projectDir = Path.Combine(baseDir, "build");
63+
Directory.CreateDirectory(projectDir);
64+
65+
// Build all variants and collect sizes
66+
var allSizes = new Dictionary<string, Dictionary<string, long>>();
67+
var sourceSizes = new Dictionary<string, long>();
68+
69+
Console.WriteLine(" Building without polyfill...");
70+
(allSizes["without"], sourceSizes["without"]) = BuildAllFrameworksAndMeasure(projectDir, "without", polyfillImport: false, polyOptions: "");
71+
72+
Console.WriteLine(" Building with polyfill...");
73+
(allSizes["with"], sourceSizes["with"]) = BuildAllFrameworksAndMeasure(projectDir, "with", polyfillImport: true, polyOptions: "<PolyEnsure>false</PolyEnsure><PolyArgumentExceptions>false</PolyArgumentExceptions><PolyStringInterpolation>false</PolyStringInterpolation><PolyNullability>false</PolyNullability>");
74+
75+
Console.WriteLine(" Building with PolyEnsure...");
76+
(allSizes["ensure"], sourceSizes["ensure"]) = BuildAllFrameworksAndMeasure(projectDir, "ensure", polyfillImport: true, polyOptions: "<PolyEnsure>true</PolyEnsure>");
77+
78+
Console.WriteLine(" Building with PolyArgumentExceptions...");
79+
(allSizes["argex"], sourceSizes["argex"]) = BuildAllFrameworksAndMeasure(projectDir, "argex", polyfillImport: true, polyOptions: "<PolyArgumentExceptions>true</PolyArgumentExceptions>");
80+
81+
Console.WriteLine(" Building with PolyStringInterpolation...");
82+
(allSizes["stringinterp"], sourceSizes["stringinterp"]) = BuildAllFrameworksAndMeasure(projectDir, "stringinterp", polyfillImport: true, polyOptions: "<PolyStringInterpolation>true</PolyStringInterpolation>");
83+
84+
Console.WriteLine(" Building with PolyNullability...");
85+
(allSizes["nullability"], sourceSizes["nullability"]) = BuildAllFrameworksAndMeasure(projectDir, "nullability", polyfillImport: true, polyOptions: "<PolyNullability>true</PolyNullability>");
86+
87+
return (allSizes, sourceSizes);
88+
}
89+
90+
static (Dictionary<string, long> assemblySizes, long sourceSize) BuildAllFrameworksAndMeasure(string projectDir, string variant, bool polyfillImport, string polyOptions)
91+
{
92+
var variantDir = Path.Combine(projectDir, variant);
93+
Directory.CreateDirectory(variantDir);
94+
95+
// Navigate from assembly location to find Polyfill directory
96+
var assemblyDir = Path.GetDirectoryName(typeof(AssemblySizeTest).Assembly.Location)!;
97+
// Go up from bin/Debug/net10.0 to src, then into Polyfill
98+
var polyfillDir = Path.GetFullPath(Path.Combine(assemblyDir, "..", "..", "..", "..", "Polyfill"));
99+
var polyfillTargetsPath = Path.Combine(polyfillDir, "Polyfill.targets");
100+
101+
// Calculate source file size based on what's included
102+
long sourceSize = 0;
103+
if (polyfillImport)
104+
{
105+
// Get all source files
106+
var allFiles = Directory.GetFiles(polyfillDir, "*.cs", SearchOption.AllDirectories)
107+
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") &&
108+
!f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}"))
109+
.ToList();
110+
111+
// Exclude directories based on polyOptions (matching Polyfill.targets logic)
112+
if (!polyOptions.Contains("<PolyEnsure>true</PolyEnsure>"))
113+
allFiles = allFiles.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}Ensure{Path.DirectorySeparatorChar}")).ToList();
114+
if (!polyOptions.Contains("<PolyArgumentExceptions>true</PolyArgumentExceptions>"))
115+
allFiles = allFiles.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}ArgumentExceptions{Path.DirectorySeparatorChar}")).ToList();
116+
if (!polyOptions.Contains("<PolyStringInterpolation>true</PolyStringInterpolation>"))
117+
allFiles = allFiles.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}StringInterpolation{Path.DirectorySeparatorChar}")).ToList();
118+
if (!polyOptions.Contains("<PolyNullability>true</PolyNullability>"))
119+
allFiles = allFiles.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}Nullability{Path.DirectorySeparatorChar}")).ToList();
120+
121+
// Calculate compressed size (EmbedUntrackedSources uses deflate compression)
122+
sourceSize = allFiles.Sum(f =>
123+
{
124+
var content = File.ReadAllBytes(f);
125+
using var output = new MemoryStream();
126+
using (var deflate = new DeflateStream(output, CompressionLevel.Optimal, leaveOpen: true))
127+
{
128+
deflate.Write(content);
129+
}
130+
return output.Length;
131+
});
132+
}
133+
134+
// Include Polyfill source files before the targets (which use Remove to exclude based on options)
135+
var polyfillSourceIncludes = polyfillImport
136+
? $"""
137+
<ItemGroup>
138+
<Compile Include="{polyfillDir}\**\*.cs" Exclude="{polyfillDir}\obj\**;{polyfillDir}\bin\**" />
139+
</ItemGroup>
140+
"""
141+
: "";
142+
var polyfillImportLines = polyfillImport
143+
? $"""
144+
<Import Project="{polyfillTargetsPath}" />
145+
"""
146+
: "";
147+
148+
var packageReferences = polyfillImport
149+
? """
150+
<ItemGroup>
151+
<PackageReference Include="System.Memory" Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard' or '$(TargetFrameworkIdentifier)' == '.NETFramework' or $(TargetFramework.StartsWith('netcoreapp'))" Version="4.5.5" />
152+
<PackageReference Include="System.ValueTuple" Condition="$(TargetFramework.StartsWith('net46'))" Version="4.5.0" />
153+
<PackageReference Include="System.Net.Http" Condition="$(TargetFramework.StartsWith('net4'))" Version="4.3.4" />
154+
<PackageReference Include="System.Threading.Tasks.Extensions" Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp2.0' or '$(TargetFrameworkIdentifier)' == '.NETFramework'" Version="4.5.4" />
155+
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="$(TargetFramework.StartsWith('net4'))" Version="4.3.0" />
156+
<PackageReference Include="System.IO.Compression" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" Version="4.3.0" />
157+
</ItemGroup>
158+
"""
159+
: "";
160+
161+
var allFrameworks = string.Join(";", TargetFrameworks);
162+
163+
var csproj = $"""
164+
<Project Sdk="Microsoft.NET.Sdk">
165+
<PropertyGroup>
166+
<TargetFrameworks>{allFrameworks}</TargetFrameworks>
167+
<OutputType>Library</OutputType>
168+
<EnableDefaultItems>false</EnableDefaultItems>
169+
<NoWarn>$(NoWarn);PolyfillTargetsForNuget</NoWarn>
170+
<LangVersion>preview</LangVersion>
171+
<DebugType>embedded</DebugType>
172+
<DebugSymbols>true</DebugSymbols>
173+
{polyOptions}
174+
</PropertyGroup>
175+
{packageReferences}
176+
{polyfillSourceIncludes}
177+
{polyfillImportLines}
178+
<ItemGroup>
179+
<Compile Include="Class1.cs" />
180+
</ItemGroup>
181+
</Project>
182+
""";
183+
184+
var csprojPath = Path.Combine(variantDir, "TestProject.csproj");
185+
File.WriteAllText(csprojPath, csproj);
186+
187+
var classFile = """
188+
public class Class1
189+
{
190+
public void Method1() { }
191+
}
192+
""";
193+
File.WriteAllText(Path.Combine(variantDir, "Class1.cs"), classFile);
194+
195+
// Build the project once for all frameworks
196+
var startInfo = new ProcessStartInfo
197+
{
198+
FileName = "dotnet",
199+
Arguments = "build -c Release",
200+
WorkingDirectory = variantDir,
201+
RedirectStandardOutput = true,
202+
RedirectStandardError = true,
203+
UseShellExecute = false,
204+
CreateNoWindow = true
205+
};
206+
207+
using var process = Process.Start(startInfo)!;
208+
var output = process.StandardOutput.ReadToEnd();
209+
var error = process.StandardError.ReadToEnd();
210+
process.WaitForExit(120000);
211+
212+
if (process.ExitCode != 0)
213+
{
214+
throw new(
215+
$"""
216+
Build failed for {variant}:
217+
{output}
218+
{error}
219+
""");
220+
}
221+
222+
// Collect sizes from all framework DLLs
223+
var sizes = new Dictionary<string, long>();
224+
var binPath = Path.Combine(variantDir, "bin", "Release");
225+
226+
foreach (var framework in TargetFrameworks)
227+
{
228+
var dllPath = Path.Combine(binPath, framework, "TestProject.dll");
229+
if (File.Exists(dllPath))
230+
{
231+
var fileInfo = new FileInfo(dllPath);
232+
sizes[framework] = fileInfo.Length;
233+
}
234+
else
235+
{
236+
Console.WriteLine($" Warning: DLL not found for {framework} at {dllPath}");
237+
sizes[framework] = -1; // Mark as unavailable
238+
}
239+
}
240+
241+
return (sizes, sourceSize);
242+
}
243+
244+
static List<SizeResult> ConvertToSizeResults(Dictionary<string, Dictionary<string, long>> allSizes)
245+
{
246+
var results = new List<SizeResult>();
247+
248+
foreach (var framework in TargetFrameworks)
249+
{
250+
results.Add(new SizeResult
251+
{
252+
TargetFramework = framework,
253+
SizeWithoutPolyfill = allSizes["without"].GetValueOrDefault(framework, -1),
254+
SizeWithPolyfill = allSizes["with"].GetValueOrDefault(framework, -1),
255+
SizeWithEnsure = allSizes["ensure"].GetValueOrDefault(framework, -1),
256+
SizeWithArgumentExceptions = allSizes["argex"].GetValueOrDefault(framework, -1),
257+
SizeWithStringInterpolation = allSizes["stringinterp"].GetValueOrDefault(framework, -1),
258+
SizeWithNullability = allSizes["nullability"].GetValueOrDefault(framework, -1)
259+
});
260+
}
261+
262+
return results;
263+
}
264+
265+
static List<SizeResult> ConvertToSizeResultsWithEmbed(Dictionary<string, Dictionary<string, long>> allSizes, Dictionary<string, long> sourceSizes)
266+
{
267+
var results = new List<SizeResult>();
268+
269+
foreach (var framework in TargetFrameworks)
270+
{
271+
results.Add(new SizeResult
272+
{
273+
TargetFramework = framework,
274+
SizeWithoutPolyfill = allSizes["without"].GetValueOrDefault(framework, -1) + sourceSizes["without"],
275+
SizeWithPolyfill = allSizes["with"].GetValueOrDefault(framework, -1) + sourceSizes["with"],
276+
SizeWithEnsure = allSizes["ensure"].GetValueOrDefault(framework, -1) + sourceSizes["ensure"],
277+
SizeWithArgumentExceptions = allSizes["argex"].GetValueOrDefault(framework, -1) + sourceSizes["argex"],
278+
SizeWithStringInterpolation = allSizes["stringinterp"].GetValueOrDefault(framework, -1) + sourceSizes["stringinterp"],
279+
SizeWithNullability = allSizes["nullability"].GetValueOrDefault(framework, -1) + sourceSizes["nullability"]
280+
});
281+
}
282+
283+
return results;
284+
}
285+
286+
static void WriteTable(StreamWriter writer, List<SizeResult> results, string title)
287+
{
288+
writer.WriteLine($"### {title}");
289+
writer.WriteLine();
290+
writer.WriteLine("| | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability |");
291+
writer.WriteLine("|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|");
292+
293+
foreach (var result in results)
294+
{
295+
var sizeDiff = result.SizeWithPolyfill - result.SizeWithoutPolyfill;
296+
var sizeDiffEnsure = result.SizeWithEnsure - result.SizeWithPolyfill;
297+
var sizeDiffArgEx = result.SizeWithArgumentExceptions - result.SizeWithPolyfill;
298+
var sizeDiffStringInterp = result.SizeWithStringInterpolation - result.SizeWithPolyfill;
299+
var sizeDiffNullability = result.SizeWithNullability - result.SizeWithPolyfill;
300+
301+
Debug.Assert(sizeDiffEnsure > 0, $"sizeDiffEnsure should be positive for {result.TargetFramework}, but was {sizeDiffEnsure}");
302+
Debug.Assert(sizeDiffNullability > 0, $"sizeDiffNullability should be positive for {result.TargetFramework}, but was {sizeDiffNullability}");
303+
304+
writer.WriteLine($"| {result.TargetFramework,-14} | {FormatSize(result.SizeWithoutPolyfill),14} | {FormatSize(result.SizeWithPolyfill),13} | {FormatSizeDiff(sizeDiff),9} | {FormatSizeDiff(sizeDiffEnsure),9} | {FormatSizeDiff(sizeDiffArgEx),18} | {FormatSizeDiff(sizeDiffStringInterp),19} | {FormatSizeDiff(sizeDiffNullability),11} |");
305+
}
306+
}
307+
308+
static string FormatSize(long bytes)
309+
{
310+
if (bytes < 1024)
311+
{
312+
return $"{bytes:N0} bytes";
313+
}
314+
315+
var kb = bytes / 1024.0;
316+
return $"{kb:N1} KB";
317+
}
318+
319+
static string FormatSizeDiff(long bytes)
320+
{
321+
if (bytes == 0)
322+
{
323+
return "";
324+
}
325+
326+
return $"+{FormatSize(bytes)}";
327+
}
328+
329+
class SizeResult
330+
{
331+
public string TargetFramework { get; init; } = "";
332+
public long SizeWithoutPolyfill { get; init; }
333+
public long SizeWithPolyfill { get; init; }
334+
public long SizeWithEnsure { get; init; }
335+
public long SizeWithArgumentExceptions { get; init; }
336+
public long SizeWithStringInterpolation { get; init; }
337+
public long SizeWithNullability { get; init; }
338+
}
339+
}

0 commit comments

Comments
 (0)