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