Skip to content

Commit 92f6643

Browse files
authored
Merge pull request #760 from karpinsn/dev/nikarpin/mef14
Reduce ComposableCatalog.GetExports allocations (-35% composition) + add BenchmarkDotNet harness
2 parents 707ae4b + a20b668 commit 92f6643

7 files changed

Lines changed: 552 additions & 7 deletions

File tree

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
<CodeAnalysisVersion>5.6.0</CodeAnalysisVersion>
1111
<CodefixTestingVersion>1.1.4</CodefixTestingVersion>
1212
</PropertyGroup>
13+
1314
<ItemGroup>
1415
<PackageVersion Include="DiffPlex" Version="1.9.0" />
16+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
17+
<PackageVersion Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.7.37220.1" />
1518
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
1619
<PackageVersion Include="Microsoft.CodeAnalysis.Common" version="$(CodeAnalysisVersion)" />
1720
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="$(CodefixTestingVersion)" />

Microsoft.VisualStudio.Composition.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Project Path="test/Microsoft.VisualStudio.Composition.AppDomainTests2/Microsoft.VisualStudio.Composition.AppDomainTests2.csproj" />
3636
<Project Path="test/Microsoft.VisualStudio.Composition.AssemblyDiscoveryTests/Microsoft.VisualStudio.Composition.AssemblyDiscoveryTests.csproj" />
3737
<Project Path="test/Microsoft.VisualStudio.Composition.AssemblyDiscoveryTests2/Microsoft.VisualStudio.Composition.AssemblyDiscoveryTests2.csproj" />
38+
<Project Path="test/Microsoft.VisualStudio.Composition.Benchmarks/Microsoft.VisualStudio.Composition.Benchmarks.csproj" />
3839
<Project Path="test/Microsoft.VisualStudio.Composition.BrokenAssemblyTests/Microsoft.VisualStudio.Composition.BrokenAssemblyTests.csproj" />
3940
<Project Path="test/Microsoft.VisualStudio.Composition.EmbeddedTypeReceiver/Microsoft.VisualStudio.Composition.EmbeddedTypeReceiver.csproj" />
4041
<Project Path="test/Microsoft.VisualStudio.Composition.MissingAssemblyTests/Microsoft.VisualStudio.Composition.MissingAssemblyTests.csproj" />

src/Microsoft.VisualStudio.Composition/ComposableCatalog.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ namespace Microsoft.VisualStudio.Composition
66
using System;
77
using System.Collections.Generic;
88
using System.Collections.Immutable;
9-
using System.ComponentModel;
109
using System.Diagnostics.CodeAnalysis;
1110
using System.IO;
1211
using System.Linq;
1312
using System.Reflection;
14-
using System.Text;
15-
using System.Threading.Tasks;
1613
using Microsoft.VisualStudio.Composition.Reflection;
1714

1815
public class ComposableCatalog : IEquatable<ComposableCatalog>
@@ -239,11 +236,57 @@ from export in openGenericExports
239236
select export.CloseGenericExport(genericTypeArguments));
240237
}
241238

242-
var filteredExports = from export in exports
243-
where importDefinition.ExportConstraints.All(c => c.IsSatisfiedBy(export.ExportDefinition))
244-
select export;
239+
IReadOnlyCollection<IImportSatisfiabilityConstraint> constraints = importDefinition.ExportConstraints;
240+
if (constraints.Count == 0)
241+
{
242+
// No constraints: every matching export qualifies.
243+
return exports;
244+
}
245+
246+
int exportIndex = 0;
247+
foreach (ExportDefinitionBinding export in exports)
248+
{
249+
if (!IsExportSatisfiedByAllConstraints(constraints, export.ExportDefinition))
250+
{
251+
// Remove the non-qualifying export.
252+
// We're enumerating an ImmutableList<T> and mutating it is a copy-on-write operation,
253+
// so it's safe to create a new collection with the export removed and continue enumerating the old collection.
254+
// In doing so, we do NOT increment exportIndex, since exportIndex will point at the next one implicitly after removal.
255+
exports = exports.RemoveAt(exportIndex);
256+
}
257+
else
258+
{
259+
exportIndex++;
260+
}
261+
}
262+
263+
return exports;
264+
}
265+
266+
private static bool IsExportSatisfiedByAllConstraints(IReadOnlyCollection<IImportSatisfiabilityConstraint> constraints, ExportDefinition exportDefinition)
267+
{
268+
if (constraints is ImmutableList<IImportSatisfiabilityConstraint> list)
269+
{
270+
foreach (IImportSatisfiabilityConstraint constraint in list)
271+
{
272+
if (!constraint.IsSatisfiedBy(exportDefinition))
273+
{
274+
return false;
275+
}
276+
}
277+
278+
return true;
279+
}
280+
281+
foreach (IImportSatisfiabilityConstraint constraint in constraints)
282+
{
283+
if (!constraint.IsSatisfiedBy(exportDefinition))
284+
{
285+
return false;
286+
}
287+
}
245288

246-
return ImmutableList.CreateRange(filteredExports);
289+
return true;
247290
}
248291

249292
internal static bool TryGetOpenGenericExport(ImportDefinition importDefinition, [NotNullWhen(true)] out string? contractName, [NotNullWhen(true)] out Type[]? typeArguments)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.VisualStudio.Composition.Benchmarks;
5+
6+
using System.Collections.Immutable;
7+
using System.Reflection;
8+
using BenchmarkDotNet.Attributes;
9+
using Microsoft.VSDiagnostics;
10+
11+
[MemoryDiagnoser]
12+
[CPUUsageDiagnoser]
13+
public class CompositionBenchmarks
14+
{
15+
private Resolver resolver = null!;
16+
private Assembly[] assemblies = null!;
17+
private ComposableCatalog catalog = null!;
18+
private RuntimeComposition runtime = null!;
19+
private byte[] serialized = null!;
20+
private IExportProviderFactory exportProviderFactory = null!;
21+
private ImportDefinition unconstrainedImport = null!;
22+
private ImportDefinition constrainedImport = null!;
23+
24+
[GlobalSetup]
25+
public void Setup()
26+
{
27+
this.resolver = Resolver.DefaultInstance;
28+
this.assemblies = new[] { typeof(CompositionBenchmarks).Assembly };
29+
30+
var discovered = this.NewDiscovery().CreatePartsAsync(this.assemblies).GetAwaiter().GetResult();
31+
this.catalog = ComposableCatalog.Create(this.resolver).AddParts(discovered);
32+
var config = CompositionConfiguration.Create(this.catalog);
33+
this.runtime = RuntimeComposition.CreateRuntimeComposition(config);
34+
35+
using var ms = new MemoryStream();
36+
new CachedComposition().SaveAsync(this.runtime, ms).GetAwaiter().GetResult();
37+
this.serialized = ms.ToArray();
38+
39+
this.exportProviderFactory = this.runtime.CreateExportProviderFactory();
40+
41+
// Two imports for the catalog's most-exported contract: one with no export constraints and
42+
// one with a type-identity constraint. Both resolve the same exports, so the delta between
43+
// the GetExports* benchmarks isolates (and guards) the no-constraint fast path.
44+
string contractName = this.catalog.Parts
45+
.SelectMany(p => p.ExportedTypes)
46+
.GroupBy(e => e.ContractName)
47+
.OrderByDescending(g => g.Count())
48+
.First().Key;
49+
var emptyMetadata = ImmutableDictionary<string, object?>.Empty;
50+
this.unconstrainedImport = new ImportDefinition(
51+
contractName,
52+
ImportCardinality.ZeroOrMore,
53+
emptyMetadata,
54+
ImmutableList<IImportSatisfiabilityConstraint>.Empty);
55+
this.constrainedImport = new ImportDefinition(
56+
contractName,
57+
ImportCardinality.ZeroOrMore,
58+
emptyMetadata,
59+
new IImportSatisfiabilityConstraint[] { new ExportTypeIdentityConstraint(typeof(IService)) });
60+
}
61+
62+
[Benchmark]
63+
public int Discovery()
64+
{
65+
var discovered = this.NewDiscovery().CreatePartsAsync(this.assemblies).GetAwaiter().GetResult();
66+
return discovered.Parts.Count;
67+
}
68+
69+
[Benchmark]
70+
public int Composition()
71+
{
72+
var configuration = CompositionConfiguration.Create(this.catalog);
73+
return configuration.Parts.Count;
74+
}
75+
76+
[Benchmark]
77+
public long Serialize()
78+
{
79+
using var ms = new MemoryStream(this.serialized.Length);
80+
new CachedComposition().SaveAsync(this.runtime, ms).GetAwaiter().GetResult();
81+
return ms.Length;
82+
}
83+
84+
[Benchmark]
85+
public int Deserialize()
86+
{
87+
using var ms = new MemoryStream(this.serialized, writable: false);
88+
var loaded = new CachedComposition().LoadRuntimeCompositionAsync(ms, this.resolver).GetAwaiter().GetResult();
89+
return loaded.Parts.Count;
90+
}
91+
92+
[Benchmark]
93+
public int Runtime()
94+
{
95+
int count = 0;
96+
using var exportProvider = this.exportProviderFactory.CreateExportProvider();
97+
foreach (var processor in exportProvider.GetExportedValues<IProcessor>())
98+
{
99+
count++;
100+
}
101+
102+
foreach (var service in exportProvider.GetExportedValues<IService>())
103+
{
104+
count++;
105+
}
106+
107+
return count;
108+
}
109+
110+
[Benchmark]
111+
public int GetExportsUnconstrained() => this.catalog.GetExports(this.unconstrainedImport).Count;
112+
113+
[Benchmark]
114+
public int GetExportsConstrained() => this.catalog.GetExports(this.constrainedImport).Count;
115+
116+
private PartDiscovery NewDiscovery() => PartDiscovery.Combine(
117+
this.resolver,
118+
new AttributedPartDiscovery(this.resolver, isNonPublicSupported: true),
119+
new AttributedPartDiscoveryV1(this.resolver));
120+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net10.0;net472</TargetFrameworks>
6+
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\Microsoft.VisualStudio.Composition\Microsoft.VisualStudio.Composition.csproj" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="BenchmarkDotNet" />
15+
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" />
16+
<PackageReference Include="System.ComponentModel.Composition" />
17+
<PackageReference Include="System.Composition.AttributedModel" />
18+
<PackageReference Include="System.Memory" />
19+
</ItemGroup>
20+
21+
</Project>

0 commit comments

Comments
 (0)