3
3
// See the LICENSE file in the project root for more information.
4
4
5
5
using System . Collections . Immutable ;
6
+ using System . Security . Cryptography ;
7
+ using System . Text ;
6
8
using Microsoft . CodeAnalysis . LanguageServer . Logging ;
7
9
using Microsoft . CodeAnalysis . LanguageServer . Services ;
8
10
using Microsoft . CodeAnalysis . Shared . Collections ;
@@ -18,6 +20,7 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
18
20
ExtensionAssemblyManager extensionManager ,
19
21
IAssemblyLoader assemblyLoader ,
20
22
string ? devKitDependencyPath ,
23
+ string ? cacheDirectory ,
21
24
ILoggerFactory loggerFactory )
22
25
{
23
26
var logger = loggerFactory . CreateLogger < ExportProviderBuilder > ( ) ;
@@ -38,17 +41,60 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
38
41
// Add the extension assemblies to the MEF catalog.
39
42
assemblyPaths = assemblyPaths . Concat ( extensionManager . ExtensionAssemblyPaths ) ;
40
43
41
- logger . LogTrace ( $ "Composing MEF catalog using:{ Environment . NewLine } { string . Join ( $ " { Environment . NewLine } ", assemblyPaths ) } .") ;
44
+ // Get the cached MEF composition or create a new one.
45
+ var exportProviderFactory = await GetCompositionConfigurationAsync ( assemblyPaths . ToImmutableArray ( ) , assemblyLoader , cacheDirectory , logger ) ;
46
+
47
+ // Create an export provider, which represents a unique container of values.
48
+ // You can create as many of these as you want, but typically an app needs just one.
49
+ var exportProvider = exportProviderFactory . CreateExportProvider ( ) ;
50
+
51
+ // Immediately set the logger factory, so that way it'll be available for the rest of the composition
52
+ exportProvider . GetExportedValue < ServerLoggerFactory > ( ) . SetFactory ( loggerFactory ) ;
53
+
54
+ // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition.
55
+ exportProvider . GetExportedValue < ExtensionAssemblyManagerMefProvider > ( ) . SetMefExtensionAssemblyManager ( extensionManager ) ;
56
+
57
+ return exportProvider ;
58
+ }
42
59
60
+ private static async Task < IExportProviderFactory > GetCompositionConfigurationAsync (
61
+ ImmutableArray < string > assemblyPaths ,
62
+ IAssemblyLoader assemblyLoader ,
63
+ string ? cacheDirectory ,
64
+ ILogger logger )
65
+ {
43
66
// Create a MEF resolver that can resolve assemblies in the extension contexts.
44
67
var resolver = new Resolver ( assemblyLoader ) ;
45
68
69
+ string ? compositionCacheFile = cacheDirectory is not null
70
+ ? GetCompositionCacheFilePath ( cacheDirectory , assemblyPaths )
71
+ : null ;
72
+
73
+ // Try to load a cached composition.
74
+ try
75
+ {
76
+ if ( compositionCacheFile is not null && File . Exists ( compositionCacheFile ) )
77
+ {
78
+ logger . LogTrace ( $ "Loading cached MEF catalog: { compositionCacheFile } ") ;
79
+
80
+ CachedComposition cachedComposition = new ( ) ;
81
+ using FileStream cacheStream = new ( compositionCacheFile , FileMode . Open , FileAccess . Read , FileShare . Read , 4096 , useAsync : true ) ;
82
+ return await cachedComposition . LoadExportProviderFactoryAsync ( cacheStream , resolver ) ;
83
+ }
84
+ }
85
+ catch ( Exception ex )
86
+ {
87
+ // Log the error, and move on to recover by recreating the MEF composition.
88
+ logger . LogError ( $ "Loading cached MEF composition failed: { ex } ") ;
89
+ }
90
+
91
+ logger . LogTrace ( $ "Composing MEF catalog using:{ Environment . NewLine } { string . Join ( $ " { Environment . NewLine } ", assemblyPaths ) } .") ;
92
+
46
93
var discovery = PartDiscovery . Combine (
47
94
resolver ,
48
95
new AttributedPartDiscovery ( resolver , isNonPublicSupported : true ) , // "NuGet MEF" attributes (Microsoft.Composition)
49
96
new AttributedPartDiscoveryV1 ( resolver ) ) ;
50
97
51
- // TODO - we should likely cache the catalog so we don't have to rebuild it every time.
52
98
var catalog = ComposableCatalog . Create ( resolver )
53
99
. AddParts ( await discovery . CreatePartsAsync ( assemblyPaths ) )
54
100
. WithCompositionService ( ) ; // Makes an ICompositionService export available to MEF parts to import
@@ -59,20 +105,52 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
59
105
// Verify we only have expected errors.
60
106
ThrowOnUnexpectedErrors ( config , catalog , logger ) ;
61
107
62
- // Prepare an ExportProvider factory based on this graph.
63
- var exportProviderFactory = config . CreateExportProviderFactory ( ) ;
108
+ // Try to cache the composition.
109
+ if ( compositionCacheFile is not null )
110
+ {
111
+ if ( Path . GetDirectoryName ( compositionCacheFile ) is string directory )
112
+ {
113
+ Directory . CreateDirectory ( directory ) ;
114
+ }
64
115
65
- // Create an export provider, which represents a unique container of values.
66
- // You can create as many of these as you want, but typically an app needs just one.
67
- var exportProvider = exportProviderFactory . CreateExportProvider ( ) ;
116
+ CachedComposition cachedComposition = new ( ) ;
117
+ var tempFilePath = Path . Combine ( Path . GetTempPath ( ) , Path . GetTempFileName ( ) ) ;
118
+ using ( FileStream cacheStream = new ( tempFilePath , FileMode . Create , FileAccess . Write , FileShare . None , 4096 , useAsync : true ) )
119
+ {
120
+ await cachedComposition . SaveAsync ( config , cacheStream ) ;
121
+ }
68
122
69
- // Immediately set the logger factory, so that way it'll be available for the rest of the composition
70
- exportProvider . GetExportedValue < ServerLoggerFactory > ( ) . SetFactory ( loggerFactory ) ;
123
+ File . Move ( tempFilePath , compositionCacheFile , overwrite : true ) ;
124
+ }
71
125
72
- // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition.
73
- exportProvider . GetExportedValue < ExtensionAssemblyManagerMefProvider > ( ) . SetMefExtensionAssemblyManager ( extensionManager ) ;
126
+ // Prepare an ExportProvider factory based on this graph.
127
+ return config . CreateExportProviderFactory ( ) ;
128
+ }
74
129
75
- return exportProvider ;
130
+ private static string GetCompositionCacheFilePath ( string cacheDirectory , ImmutableArray < string > assemblyPaths )
131
+ {
132
+ // Include the .NET runtime version in the cache path so that running on a newer
133
+ // runtime causes the cache to be rebuilt.
134
+ var cacheSubdirectory = $ ".NET { Environment . Version . Major } ";
135
+ return Path . Combine ( cacheDirectory , cacheSubdirectory , $ "c#-languageserver.{ ComputeAssemblyHash ( assemblyPaths ) } .mef-composition") ;
136
+
137
+ static string ComputeAssemblyHash ( ImmutableArray < string > assemblyPaths )
138
+ {
139
+ var assemblies = new StringBuilder ( ) ;
140
+ foreach ( var assemblyPath in assemblyPaths )
141
+ {
142
+ // Include assembly path in the hash so that changes to the set of included
143
+ // assemblies cause the composition to be rebuilt.
144
+ assemblies . Append ( assemblyPath ) ;
145
+ // Include the last write time in the hash so that newer assemblies written
146
+ // to the same location cause the composition to be rebuilt.
147
+ assemblies . Append ( File . GetLastWriteTimeUtc ( assemblyPath ) . ToString ( "F" ) ) ;
148
+ }
149
+
150
+ var hash = SHA256 . HashData ( Encoding . UTF8 . GetBytes ( assemblies . ToString ( ) ) ) ;
151
+ // Convert to filename safe base64 string.
152
+ return Convert . ToBase64String ( hash ) . Replace ( '+' , '-' ) . Replace ( '/' , '_' ) . TrimEnd ( '=' ) ;
153
+ }
76
154
}
77
155
78
156
private static void ThrowOnUnexpectedErrors ( CompositionConfiguration configuration , ComposableCatalog catalog , ILogger logger )
0 commit comments