Skip to content

Commit 502ff9a

Browse files
committed
Enhancing C# invoker to avoid unecessary host restarts.
1 parent accb11e commit 502ff9a

File tree

10 files changed

+325
-8
lines changed

10 files changed

+325
-8
lines changed

src/WebJobs.Script/Description/CSharp/CSharpFunctionInvoker.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class CSharpFunctionInvoker : ScriptFunctionInvokerBase
3333
private readonly IFunctionEntryPointResolver _functionEntryPointResolver;
3434

3535
private MethodInfo _function;
36+
private CSharpFunctionSignature _functionSignature;
3637
private FunctionMetadataResolver _metadataResolver;
3738
private Action _reloadScript;
3839
private Action _restorePackages;
@@ -101,7 +102,8 @@ private void ReloadScript()
101102
stopwatch.Start();
102103

103104
Script<object> script = CreateScript();
104-
ImmutableArray<Diagnostic> compilationResult = script.GetCompilation().GetDiagnostics();
105+
Compilation compilation = script.GetCompilation();
106+
ImmutableArray<Diagnostic> compilationResult = compilation.GetDiagnostics();
105107

106108
stopwatch.Stop();
107109
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation completed ({0} milliseconds).", stopwatch.ElapsedMilliseconds));
@@ -112,10 +114,14 @@ private void ReloadScript()
112114
TraceWriter.Trace(traceEvent);
113115
}
114116

115-
// If the compilation succeeded, restart the host
116-
// TODO: Make this smarter so we only restart the host if the
117-
// method signature (or types used in the signature) change.
118-
if (!compilationResult.Any(d => d.Severity == DiagnosticSeverity.Error))
117+
// If the compilation succeeded, AND:
118+
// - We're referencing local function types (i.e. POCOs defined in the function)
119+
// OR
120+
// - Our our function signature has changed
121+
// Restart our host.
122+
if (!compilationResult.Any(d => d.Severity == DiagnosticSeverity.Error) &&
123+
(_functionSignature.HasLocalTypeReference ||
124+
!_functionSignature.Equals(CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver))))
119125
{
120126
_host.RestartEvent.Set();
121127
}
@@ -227,6 +233,7 @@ internal MethodInfo GetFunctionTarget()
227233
// Get our function entry point
228234
System.Reflection.TypeInfo scriptType = assembly.DefinedTypes.FirstOrDefault(t => string.Compare(t.Name, ScriptClassName, StringComparison.Ordinal) == 0);
229235
_function = _functionEntryPointResolver.GetFunctionEntryPoint(scriptType.DeclaredMethods.ToList());
236+
_functionSignature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
230237
}
231238
}
232239
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.Globalization;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Description
13+
{
14+
/// <summary>
15+
/// Provides function identity validation and identification.
16+
/// </summary>
17+
[CLSCompliant(false)]
18+
public sealed class CSharpFunctionSignature : IEquatable<CSharpFunctionSignature>
19+
{
20+
private readonly ImmutableArray<IParameterSymbol> _parameters;
21+
22+
private CSharpFunctionSignature(ImmutableArray<IParameterSymbol> parameters)
23+
{
24+
_parameters = parameters;
25+
}
26+
27+
/// <summary>
28+
/// Returns true if the function uses locally defined types (i.e. types defined in the function assembly) in its parameters;
29+
/// otherwise, false.
30+
/// </summary>
31+
public bool HasLocalTypeReference { get; set; }
32+
33+
public static CSharpFunctionSignature FromCompilation(Compilation compilation, IFunctionEntryPointResolver entryPointResolver)
34+
{
35+
if (compilation == null)
36+
{
37+
throw new ArgumentNullException("compilation");
38+
}
39+
if (entryPointResolver == null)
40+
{
41+
throw new ArgumentNullException("entryPointResolver");
42+
}
43+
if (!compilation.SyntaxTrees.Any())
44+
{
45+
throw new ArgumentException("The provided compilation does not have a syntax tree.", "compilation");
46+
}
47+
48+
SyntaxTree tree = compilation.SyntaxTrees.First();
49+
SemanticModel model = compilation.GetSemanticModel(tree);
50+
51+
var methods = ((INamedTypeSymbol)model.GetEnclosingSymbol(0))
52+
.GetMembers()
53+
.OfType<IMethodSymbol>()
54+
.Select(m => new MethodReference<IMethodSymbol>(m.Name, m.DeclaredAccessibility == Accessibility.Public, m));
55+
56+
IMethodSymbol entryPointReference = entryPointResolver.GetFunctionEntryPoint(methods).Value;
57+
58+
var signature = new CSharpFunctionSignature(entryPointReference.Parameters);
59+
signature.HasLocalTypeReference = entryPointReference.Parameters.Any(p => p.Type.ContainingAssembly == entryPointReference.ContainingAssembly);
60+
61+
return signature;
62+
}
63+
64+
private static bool AreParametersEquivalent(IParameterSymbol param1, IParameterSymbol param2)
65+
{
66+
if (ReferenceEquals(param1, param2))
67+
{
68+
return true;
69+
}
70+
71+
if (param1 == null || param2 == null)
72+
{
73+
return false;
74+
}
75+
76+
return param1.RefKind == param2.RefKind &&
77+
string.Compare(param1.Name, param2.Name, StringComparison.Ordinal) == 0 &&
78+
string.Compare(GetFullTypeName(param1.Type), GetFullTypeName(param2.Type), StringComparison.Ordinal) == 0 &&
79+
param1.IsOptional == param2.IsOptional;
80+
}
81+
82+
private static string GetFullTypeName(ITypeSymbol type)
83+
{
84+
if (type == null)
85+
{
86+
return string.Empty;
87+
}
88+
89+
return string.Format(CultureInfo.InvariantCulture, "{0}.{1}, {2}", type.ContainingNamespace.MetadataName, type.MetadataName, type.ContainingAssembly.ToDisplayString());
90+
}
91+
92+
public bool Equals(CSharpFunctionSignature other)
93+
{
94+
if (other == null)
95+
{
96+
return false;
97+
}
98+
99+
if (_parameters.Count() != other._parameters.Count())
100+
{
101+
return false;
102+
}
103+
104+
return _parameters.Zip(other._parameters, (a, b) => AreParametersEquivalent(a, b)).All(r => r);
105+
}
106+
107+
public override bool Equals(object obj)
108+
{
109+
return Equals(obj as CSharpFunctionSignature);
110+
}
111+
112+
public override int GetHashCode()
113+
{
114+
return string.Join("<>", _parameters.Select(p=>GetParameterIdentityString(p)))
115+
.GetHashCode();
116+
}
117+
118+
private static string GetParameterIdentityString(IParameterSymbol parameterSymbol)
119+
{
120+
return string.Join("::", parameterSymbol.RefKind, parameterSymbol.Name,
121+
GetFullTypeName(parameterSymbol.Type), parameterSymbol.IsOptional);
122+
}
123+
}
124+
}

src/WebJobs.Script/Description/CSharp/FunctionEntryPointResolver.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,16 @@ public MethodInfo GetFunctionEntryPoint(IList<MethodInfo> declaredMethods)
3232
{
3333
return declaredMethods[0];
3434
}
35-
36-
var runMethods = declaredMethods
35+
36+
var methods = declaredMethods.Select(m => new MethodReference<MethodInfo>(m.Name, m.IsPublic, m));
37+
MethodReference<MethodInfo> entryPoint = GetFunctionEntryPoint(methods);
38+
39+
return entryPoint.Value;
40+
}
41+
42+
public T GetFunctionEntryPoint<T>(IEnumerable<T> methods) where T : IMethodReference
43+
{
44+
var runMethods = methods
3745
.Where(m => m.IsPublic && string.Compare(m.Name, "run", StringComparison.OrdinalIgnoreCase) == 0)
3846
.ToList();
3947

src/WebJobs.Script/Description/CSharp/IFunctionEntryPointResolver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ namespace Microsoft.Azure.WebJobs.Script.Description
99
public interface IFunctionEntryPointResolver
1010
{
1111
MethodInfo GetFunctionEntryPoint(IList<MethodInfo> declaredMethods);
12+
13+
T GetFunctionEntryPoint<T>(IEnumerable<T> methods) where T : IMethodReference;
1214
}
1315
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Script.Description
5+
{
6+
public interface IMethodReference
7+
{
8+
string Name { get; }
9+
10+
bool IsPublic { get; }
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.WebJobs.Script.Description
5+
{
6+
public sealed class MethodReference<T> : IMethodReference
7+
{
8+
public MethodReference(string name, bool isPublic, T value)
9+
{
10+
Name = name;
11+
IsPublic = isPublic;
12+
Value = value;
13+
}
14+
15+
public string Name { get; set; }
16+
17+
public bool IsPublic { get; set; }
18+
19+
public T Value { get; set; }
20+
}
21+
}

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,16 @@
216216
<Compile Include="Description\BindingType.cs" />
217217
<Compile Include="Description\BlobBindingMetadata.cs" />
218218
<Compile Include="Description\CSharp\CSharpConstants.cs" />
219+
<Compile Include="Description\CSharp\CSharpFunctionSignature.cs" />
219220
<Compile Include="Description\CSharp\FunctionAssemblyLoadContext.cs" />
220221
<Compile Include="Description\CSharp\FunctionAssemblyLoader.cs" />
221222
<Compile Include="Description\CSharp\CSharpFunctionDescriptionProvider.cs" />
222223
<Compile Include="Description\CSharp\CSharpFunctionInvoker.cs" />
223224
<Compile Include="Description\CSharp\FunctionEntryPointResolver.cs" />
224225
<Compile Include="Description\CSharp\FunctionMetadataResolver.cs" />
225226
<Compile Include="Description\CSharp\IFunctionEntryPointResolver.cs" />
227+
<Compile Include="Description\CSharp\IMethodReference.cs" />
228+
<Compile Include="Description\CSharp\MethodReference.cs" />
226229
<Compile Include="Description\CSharp\PackageAssemblyResolver.cs" />
227230
<Compile Include="Description\CSharp\PackageManager.cs" />
228231
<Compile Include="Description\EventHubBindingMetadata.cs" />
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.Azure.WebJobs.Script.Description;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Xunit;
8+
9+
namespace WebJobs.Script.Tests
10+
{
11+
public class CSharpFunctionSignatureTests
12+
{
13+
[Fact]
14+
public void Matches_IsTrue_WhenParametersAreEqual()
15+
{
16+
var function1 = @"using System;
17+
public static void Run(string id, out string output)
18+
{
19+
output = string.Empty;
20+
}";
21+
22+
var function2 = @"using System;
23+
public static void Run(string id, out string output)
24+
{
25+
string result = string.Empty;
26+
output = result;
27+
}";
28+
29+
var tree1 = CSharpSyntaxTree.ParseText(function1, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
30+
var tree2 = CSharpSyntaxTree.ParseText(function2, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
31+
32+
var references = new MetadataReference[] { MetadataReference.CreateFromFile(typeof(string).Assembly.Location) };
33+
34+
var compilation1 = CSharpCompilation.Create("test1", references: references).AddSyntaxTrees(tree1);
35+
var compilation2 = CSharpCompilation.Create("test2", references: references).AddSyntaxTrees(tree1);
36+
37+
var signature1 = CSharpFunctionSignature.FromCompilation(compilation1, new FunctionEntryPointResolver());
38+
var signature2 = CSharpFunctionSignature.FromCompilation(compilation2, new FunctionEntryPointResolver());
39+
40+
Assert.True(signature1.Equals(signature2));
41+
Assert.Equal(signature1.GetHashCode(), signature2.GetHashCode());
42+
}
43+
44+
[Fact]
45+
public void Matches_IsTrue_WhenParametersAreEquivalent()
46+
{
47+
var function1 = @"using System;
48+
public static void Run(string id, out string output)
49+
{
50+
output = string.Empty;
51+
}";
52+
53+
// Diferent formatting, qualified name, not using alias
54+
var function2 = @"using System;
55+
public static void Run( System.String id ,
56+
out String output )
57+
{
58+
string result = string.Empty;
59+
output = result;
60+
}";
61+
62+
var tree1 = CSharpSyntaxTree.ParseText(function1, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
63+
var tree2 = CSharpSyntaxTree.ParseText(function2, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
64+
65+
var references = new MetadataReference[] { MetadataReference.CreateFromFile(typeof(string).Assembly.Location) };
66+
67+
var compilation1 = CSharpCompilation.Create("test1", references: references).AddSyntaxTrees(tree1);
68+
var compilation2 = CSharpCompilation.Create("test2", references: references).AddSyntaxTrees(tree1);
69+
70+
var signature1 = CSharpFunctionSignature.FromCompilation(compilation1, new FunctionEntryPointResolver());
71+
var signature2 = CSharpFunctionSignature.FromCompilation(compilation2, new FunctionEntryPointResolver());
72+
73+
Assert.True(signature1.Equals(signature2));
74+
Assert.Equal(signature1.GetHashCode(), signature2.GetHashCode());
75+
}
76+
77+
[Fact]
78+
public void Matches_IsTrue_WhenUsingLocalTypes()
79+
{
80+
var function1 = @"using System;
81+
public static void Run(Test id, out string output)
82+
{
83+
output = string.Empty;
84+
}
85+
86+
public class Test
87+
{
88+
public string Id { get; set; }
89+
}";
90+
91+
var tree = CSharpSyntaxTree.ParseText(function1, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
92+
var references = new MetadataReference[] { MetadataReference.CreateFromFile(typeof(string).Assembly.Location) };
93+
var compilation = CSharpCompilation.Create("test1", references: references).AddSyntaxTrees(tree);
94+
95+
var signature1 = CSharpFunctionSignature.FromCompilation(compilation, new FunctionEntryPointResolver());
96+
97+
Assert.True(signature1.HasLocalTypeReference);
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)