From 3e707b95089912478b1ba38e81eddd59c1da8b2c Mon Sep 17 00:00:00 2001 From: Angelo Wang Date: Tue, 23 May 2023 09:24:24 +0800 Subject: [PATCH] Improve first invoke performance The first `Invoke()` call is slow. When the xaml file contains a lot of VB scripts, JIT compiler will be used, but for **every** compilation, the compiler starts from the scratch and always tries to resolve missing references via `MetadataReference.CreateFromFile`, thus it will be very slow. With some test cache between compilations, one of our test xaml file is improved from 18s to 3.5s. --- .../Activities/ScriptingJitCompiler.cs | 133 +++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/src/UiPath.Workflow/Activities/ScriptingJitCompiler.cs b/src/UiPath.Workflow/Activities/ScriptingJitCompiler.cs index 4551673c..4a4c8342 100644 --- a/src/UiPath.Workflow/Activities/ScriptingJitCompiler.cs +++ b/src/UiPath.Workflow/Activities/ScriptingJitCompiler.cs @@ -4,11 +4,14 @@ using System.Activities.ExpressionParser; using System.Activities.Internals; using System.Activities.XamlIntegration; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Metadata; +using System.Runtime.Collections; using System.Runtime.Loader; using System.Text; using System.Threading; @@ -36,6 +39,133 @@ public record ExpressionToCompile(string Code, IReadOnlyCollection Impor : CompilerInput(Code, ImportedNamespaces) { } +public sealed class CachedMetadataReferenceResolver : MetadataReferenceResolver +{ + public static CachedMetadataReferenceResolver Default = new CachedMetadataReferenceResolver(ScriptMetadataResolver.Default); + + ScriptMetadataResolver _resolver; + + private class ResolveCacheKey + { + private string _reference; + private string _baseFilePath; + private MetadataReferenceProperties _properties; + + private readonly int _hashCode; + + public ResolveCacheKey(string reference, string baseFilePath, MetadataReferenceProperties properties) + { + _reference = reference; + _baseFilePath = baseFilePath; + _properties = properties; + + _hashCode = reference?.GetHashCode() ?? 0; + _hashCode = CombineHashCodes(_hashCode, _baseFilePath?.GetHashCode() ?? 0); + _hashCode = CombineHashCodes(_hashCode, properties.GetHashCode()); + } + + public override bool Equals(object obj) + { + if (obj is not ResolveCacheKey rtcKey || _hashCode != rtcKey._hashCode) + { + return false; + } + + return _reference == rtcKey._reference && + _baseFilePath == rtcKey._baseFilePath && + _properties.Equals(rtcKey._properties); + } + + public override int GetHashCode() => _hashCode; + } + ConcurrentDictionary> _resolveCache = new ConcurrentDictionary>(); + + private class ResolveMissingCacheKey + { + private MetadataReference _definition; + private AssemblyIdentity _referenceIdentity; + + private readonly int _hashCode; + + public ResolveMissingCacheKey(MetadataReference definition, AssemblyIdentity referenceIdentity) + { + _definition = definition; + this._referenceIdentity = referenceIdentity; + + _hashCode = definition.GetHashCode(); + _hashCode = CombineHashCodes(_hashCode, _referenceIdentity.GetHashCode()); + } + + public override bool Equals(object obj) + { + if (obj is not ResolveMissingCacheKey rtcKey || _hashCode != rtcKey._hashCode) + { + return false; + } + + return _definition.Equals(rtcKey._definition) && + _referenceIdentity.Equals(rtcKey._referenceIdentity); + } + + public override int GetHashCode() => _hashCode; + } + ConcurrentDictionary _resolveMissingCache = new ConcurrentDictionary(); + + public CachedMetadataReferenceResolver(ScriptMetadataResolver resolver) + { + _resolver = resolver; + } + + public override bool Equals(object other) + { + return ReferenceEquals(this, other) || + other != null && other is CachedMetadataReferenceResolver && + Equals(_resolver, ((CachedMetadataReferenceResolver)other)._resolver) && + Equals(_resolveCache, ((CachedMetadataReferenceResolver)other)._resolveCache) && + Equals(_resolveMissingCache, ((CachedMetadataReferenceResolver)other)._resolveMissingCache); + } + + public override int GetHashCode() + { + return CombineHashCodes(_resolver.GetHashCode(), + CombineHashCodes(_resolveCache.GetHashCode(), + _resolveMissingCache.GetHashCode())); + } + private static int CombineHashCodes(int h1, int h2) => ((h1 << 5) + h1) ^ h2; + + public override ImmutableArray ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties) + { + ImmutableArray ret; + + var cacheKey = new ResolveCacheKey(reference, baseFilePath, properties); + + if (!_resolveCache.TryGetValue(cacheKey, out ret)) + { + ret = _resolver.ResolveReference(reference, baseFilePath, properties); + _resolveCache.TryAdd(cacheKey, ret); + } + + return ret; + } + + public override bool ResolveMissingAssemblies => _resolver.ResolveMissingAssemblies; + + public override PortableExecutableReference ResolveMissingAssembly(MetadataReference definition, AssemblyIdentity referenceIdentity) + { + PortableExecutableReference ret; + + var cacheKey = new ResolveMissingCacheKey(definition, referenceIdentity); + + if (!_resolveMissingCache.TryGetValue(cacheKey, out ret)) + { + ret = _resolver.ResolveMissingAssembly(definition, referenceIdentity); + _resolveMissingCache.TryAdd(cacheKey, ret); + } + + return ret; + } +} + public abstract class ScriptingJitCompiler : JustInTimeCompiler { protected ScriptingJitCompiler(HashSet referencedAssemblies) @@ -55,7 +185,8 @@ public override LambdaExpression CompileExpression(ExpressionToCompile expressio var options = ScriptOptions.Default .WithReferences(MetadataReferences) .WithImports(expressionToCompile.ImportedNamespaces) - .WithOptimizationLevel(OptimizationLevel.Release); + .WithOptimizationLevel(OptimizationLevel.Release) + .WithMetadataResolver(CachedMetadataReferenceResolver.Default); var untypedExpressionScript = Create(expressionToCompile.Code, options); var compilation = untypedExpressionScript.GetCompilation(); var syntaxTree = compilation.SyntaxTrees.First();