Skip to content

Commit 6aa4448

Browse files
authored
Order polyfills to reduce the number of comparisons (#155)
1 parent 5b9377f commit 6aa4448

5 files changed

Lines changed: 3770 additions & 20 deletions

File tree

Meziantou.Polyfill.Generator/Meziantou.Polyfill.Generator.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,13 @@
1616
</EmbeddedResource>
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<PackageDownload Include="NETStandard.Library" Version="[2.0.3]" />
21+
<PackageDownload Include="NETStandard.Library.Ref" Version="[2.1.0]" />
22+
<PackageDownload Include="Microsoft.NETFramework.ReferenceAssemblies.net462" Version="[1.0.3]" />
23+
<PackageDownload Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="[1.0.3]" />
24+
<PackageDownload Include="Microsoft.NETFramework.ReferenceAssemblies.net48" Version="[1.0.3]" />
25+
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[6.0.0];[7.0.0];[8.0.0];[9.0.0];[10.0.0]" />
26+
</ItemGroup>
27+
1928
</Project>

Meziantou.Polyfill.Generator/Program.cs

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#pragma warning disable MA0047 // Declare types in namespaces
33
#pragma warning disable MA0048 // File name must match type name
44
using System.Reflection;
5+
using System.Text.Json;
56
using Meziantou.Polyfill.Generator;
67
using Microsoft.CodeAnalysis.CSharp;
78
using Microsoft.CodeAnalysis;
@@ -105,6 +106,9 @@
105106
throw new InvalidOperationException("There are duplicated polyfills:\n" + sb.ToString());
106107
}
107108

109+
// Detect which TFMs support each polyfill, sort by earliest supported version, then reassign indices
110+
DetectAndAssignVersions(polyfills, compilation, GetRootPath());
111+
108112
polyfills = SortPolyfills(polyfills);
109113

110114
var requiredTypes = polyfills.SelectMany(p => p.PolyfillData.RequiredTypes)
@@ -271,10 +275,16 @@ string GenerateIncludePostCondition(PolyfillData data)
271275

272276
sb.AppendLine("public void AddSources(SourceProductionContext context)");
273277
sb.AppendLine("{");
274-
foreach (var polyfill in polyfills)
278+
foreach (var group in polyfills.GroupBy(p => p.CSharpFieldName, StringComparer.Ordinal))
275279
{
276-
sb.AppendLine($" if (({polyfill.CSharpFieldName} & {polyfill.CSharpFieldBitMask}ul) == {polyfill.CSharpFieldBitMask}ul)");
277-
sb.AppendLine($" AddSource(context, \"{polyfill.OutputPath}\", PolyfillContents.{polyfill.CSharpSourceTextPropertyName});");
280+
sb.AppendLine($" if ({group.Key} != 0)");
281+
sb.AppendLine(" {");
282+
foreach (var polyfill in group)
283+
{
284+
sb.AppendLine($" if (({polyfill.CSharpFieldName} & {polyfill.CSharpFieldBitMask}ul) == {polyfill.CSharpFieldBitMask}ul)");
285+
sb.AppendLine($" AddSource(context, \"{polyfill.OutputPath}\", PolyfillContents.{polyfill.CSharpSourceTextPropertyName});");
286+
}
287+
sb.AppendLine(" }");
278288
}
279289
sb.AppendLine("}");
280290

@@ -356,7 +366,7 @@ async Task GenerateReadme()
356366
.WithParameterOptions(SymbolDisplayParameterOptions.IncludeExtensionThis | SymbolDisplayParameterOptions.IncludeModifiers | SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeDefaultValue | SymbolDisplayParameterOptions.IncludeOptionalBrackets)
357367
.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.ExpandValueTuple)
358368
;
359-
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Type))
369+
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Type).OrderBy(p => p.TypeName, StringComparer.Ordinal))
360370
{
361371
sb.Append($"- `{polyfill.Symbol.ToDisplayString(typeDisplayFormat)}`\n");
362372
}
@@ -372,7 +382,7 @@ async Task GenerateReadme()
372382
.WithParameterOptions(SymbolDisplayParameterOptions.IncludeExtensionThis | SymbolDisplayParameterOptions.IncludeModifiers | SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeDefaultValue | SymbolDisplayParameterOptions.IncludeOptionalBrackets)
373383
.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.ExpandValueTuple)
374384
;
375-
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Method))
385+
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Method).OrderBy(p => p.TypeName, StringComparer.Ordinal))
376386
{
377387
sb.Append($"- `{polyfill.Symbol.ToDisplayString(methodDisplayFormat)}`\n");
378388
}
@@ -388,7 +398,7 @@ async Task GenerateReadme()
388398
.WithParameterOptions(SymbolDisplayParameterOptions.IncludeExtensionThis | SymbolDisplayParameterOptions.IncludeModifiers | SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeDefaultValue | SymbolDisplayParameterOptions.IncludeOptionalBrackets)
389399
.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.ExpandValueTuple)
390400
;
391-
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Property))
401+
foreach (var polyfill in polyfills.Where(p => p.Kind is PolyfillKind.Property).OrderBy(p => p.TypeName, StringComparer.Ordinal))
392402
{
393403
sb.Append($"- `{polyfill.Symbol.ToDisplayString(propertyDisplayFormat)}`\n");
394404
}
@@ -425,6 +435,150 @@ string ReadResourceAsString(string name)
425435
return sr.ReadToEnd();
426436
}
427437

438+
static void DetectAndAssignVersions(Polyfill[] polyfills, CSharpCompilation currentCompilation, string rootPath)
439+
{
440+
var nugetPackagesPath = GetNuGetPackagesPath();
441+
Console.WriteLine($"NuGet packages path: {nugetPackagesPath}");
442+
443+
// Define TFMs for netstandard and .NET Framework (stable, fixed versions)
444+
var tfmDefinitions = new List<(string Name, string RefPath)>
445+
{
446+
("netstandard2.0", Path.Combine(nugetPackagesPath, "netstandard.library", "2.0.3", "build", "netstandard2.0", "ref")),
447+
("netstandard2.1", Path.Combine(nugetPackagesPath, "netstandard.library.ref", "2.1.0", "ref", "netstandard2.1")),
448+
("net462", Path.Combine(nugetPackagesPath, "microsoft.netframework.referenceassemblies.net462", "1.0.3", "build", ".NETFramework", "v4.6.2")),
449+
("net472", Path.Combine(nugetPackagesPath, "microsoft.netframework.referenceassemblies.net472", "1.0.3", "build", ".NETFramework", "v4.7.2")),
450+
("net48", Path.Combine(nugetPackagesPath, "microsoft.netframework.referenceassemblies.net48", "1.0.3", "build", ".NETFramework", "v4.8")),
451+
};
452+
453+
// Dynamically discover Microsoft.NETCore.App.Ref versions from NuGet cache
454+
var netcoreAppRefPath = Path.Combine(nugetPackagesPath, "microsoft.netcore.app.ref");
455+
if (Directory.Exists(netcoreAppRefPath))
456+
{
457+
var discoveredVersions = new SortedDictionary<int, (string TfmName, string RefPath)>();
458+
foreach (var versionDir in Directory.GetDirectories(netcoreAppRefPath))
459+
{
460+
var versionName = Path.GetFileName(versionDir);
461+
var majorPart = versionName.Split('.')[0];
462+
if (int.TryParse(majorPart, out var major) && major >= 6 && !discoveredVersions.ContainsKey(major))
463+
{
464+
var tfmName = $"net{major}.0";
465+
var refPath = Path.Combine(versionDir, "ref", tfmName);
466+
if (Directory.Exists(refPath))
467+
{
468+
discoveredVersions[major] = (tfmName, refPath);
469+
}
470+
}
471+
}
472+
473+
foreach (var (_, (tfmName, refPath)) in discoveredVersions)
474+
{
475+
tfmDefinitions.Add((tfmName, refPath));
476+
}
477+
}
478+
479+
var compilations = new List<(string Name, int SortOrder, CSharpCompilation Compilation)>();
480+
481+
for (var i = 0; i < tfmDefinitions.Count; i++)
482+
{
483+
var (name, refPath) = tfmDefinitions[i];
484+
if (!Directory.Exists(refPath))
485+
{
486+
Console.WriteLine($"Warning: Reference assemblies not found for {name} at {refPath}");
487+
continue;
488+
}
489+
490+
var files = Directory.GetFiles(refPath, "*.dll").ToList();
491+
var facadesPath = Path.Combine(refPath, "Facades");
492+
if (Directory.Exists(facadesPath))
493+
{
494+
files.AddRange(Directory.GetFiles(facadesPath, "*.dll"));
495+
}
496+
497+
var comp = CSharpCompilation.Create(
498+
assemblyName: $"version-check-{name}",
499+
references: files.Select(f => MetadataReference.CreateFromFile(f)),
500+
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, metadataImportOptions: MetadataImportOptions.All));
501+
502+
compilations.Add((name, i, comp));
503+
Console.WriteLine($"Loaded {files.Count} assemblies for {name}");
504+
}
505+
506+
// Current runtime represents the latest .NET version
507+
var currentVersionName = $"net{Environment.Version.Major}.0";
508+
if (!compilations.Any(c => string.Equals(c.Name, currentVersionName, StringComparison.Ordinal)))
509+
{
510+
compilations.Add((currentVersionName, tfmDefinitions.Count, currentCompilation));
511+
Console.WriteLine($"Using current runtime as {currentVersionName}");
512+
}
513+
514+
var allVersionNames = compilations.Select(c => c.Name).ToArray();
515+
516+
// Check each polyfill against each compilation
517+
Console.WriteLine($"Checking {polyfills.Length} polyfills against {compilations.Count} TFMs...");
518+
foreach (var polyfill in polyfills)
519+
{
520+
var supportedVersions = new List<string>();
521+
foreach (var (name, _, comp) in compilations)
522+
{
523+
var symbols = DocumentationCommentId.GetSymbolsForDeclarationId(polyfill.TypeName, comp);
524+
if (symbols.Length > 0)
525+
{
526+
supportedVersions.Add(name);
527+
}
528+
}
529+
530+
polyfill.SupportedInVersions = [.. supportedVersions];
531+
}
532+
533+
// Write cache file
534+
var cacheEntries = new SortedDictionary<string, string[]>(StringComparer.Ordinal);
535+
foreach (var polyfill in polyfills)
536+
{
537+
cacheEntries[polyfill.TypeName] = polyfill.SupportedInVersions;
538+
}
539+
540+
var cacheFilePath = Path.Combine(rootPath, "Meziantou.Polyfill.Generator", "polyfill-supported-versions.json");
541+
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
542+
File.WriteAllText(cacheFilePath, JsonSerializer.Serialize(new { versions = allVersionNames, polyfills = cacheEntries }, jsonOptions));
543+
Console.WriteLine($"Wrote version cache to {cacheFilePath}");
544+
545+
// Sort by earliest supported version (ascending), then by name, and reassign Index
546+
var versionOrder = compilations.ToDictionary(c => c.Name, c => c.SortOrder, StringComparer.Ordinal);
547+
var sortedPolyfills = polyfills
548+
.OrderBy(p => GetEarliestVersionOrder(p.SupportedInVersions, versionOrder))
549+
.ThenBy(p => p.TypeName, StringComparer.Ordinal)
550+
.ToArray();
551+
552+
for (var i = 0; i < sortedPolyfills.Length; i++)
553+
{
554+
sortedPolyfills[i].Index = i;
555+
}
556+
557+
// Print version distribution summary
558+
foreach (var group in sortedPolyfills.GroupBy(p => GetEarliestVersionOrder(p.SupportedInVersions, versionOrder)).OrderBy(g => g.Key))
559+
{
560+
var versionName = group.Key == int.MaxValue ? "unknown" : compilations.First(c => c.SortOrder == group.Key).Name;
561+
Console.WriteLine($" {versionName}: {group.Count()} polyfills (bits {group.Min(p => p.Index)}\u2013{group.Max(p => p.Index)})");
562+
}
563+
564+
static int GetEarliestVersionOrder(string[] supportedVersions, Dictionary<string, int> versionOrder)
565+
{
566+
if (supportedVersions.Length == 0)
567+
return int.MaxValue;
568+
569+
return supportedVersions.Min(v => versionOrder.GetValueOrDefault(v, int.MaxValue));
570+
}
571+
572+
static string GetNuGetPackagesPath()
573+
{
574+
var path = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
575+
if (!string.IsNullOrEmpty(path))
576+
return path;
577+
578+
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
579+
}
580+
}
581+
428582
static Polyfill[] SortPolyfills(Polyfill[] items)
429583
{
430584
var result = new List<Polyfill>(items.Length);
@@ -502,6 +656,8 @@ internal sealed class Polyfill
502656
public required string OutputPath { get; set; }
503657
public required PolyfillKind Kind { get; set; }
504658

659+
public string[] SupportedInVersions { get; set; } = [];
660+
505661
public string CSharpFieldName => "_bits" + (Index / 64);
506662
public int CSharpFieldBitIndex => Index % 64;
507663
public ulong CSharpFieldBitMask => 1uL << (Index % 64);

0 commit comments

Comments
 (0)