dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.95k stars 4.02k forks source link

CSharpScript seemingly excessive memory usage #22219

Open tomlaigna opened 7 years ago

tomlaigna commented 7 years ago

Version Used: 2.3.0.0

I have an application that creates and pre-compiles a number of scripts. For me though, it seems that ~39 scripts is the limit where compilation will fail with an OOM exception.

var script = CSharpScript.Create(text, scriptOptions);
script.Compile();

My scriptOptions contains 1 assembly reference and 2 imports. Each script compilation consumes roughly over 50mb.

Releasing the references to these scripts allows the gc to free up memory and not get an OOM ex meaning each script reference seems to consume roughly the same amount of memory as the entire main application without scripts. This seems slightly excessive.

I will help with any details needed. All help is welcome.

pseabury commented 6 years ago

I see the same issue with roughly 20 CSharpScript objects. Currently we allow certain users the ability to write C# expressions that are intended to filter data for them and keep the cached scripts around for execution against a domain object. Unfortunately, being limited to 20-30 scripts before an OOM condition is not scalable (or testable).

tomlaigna commented 6 years ago

As an update, I tried setting ScriptOptions.Default.WithEmitDebugInformation(false);, hoping that it's some kind of debug information taking the space, with no change.

Taking a snapshot of managed memory I can see that 5 Script objects consume only 10 234 720 bytes.

Microsoft.CodeAnalysis.Scripting.Script<Object> 5   220 10 234 720

From this it looks like the memory is consumed somewhere outside managed memory and i don't know how to provide better information without digging into the compilation process myself.

tomlaigna commented 6 years ago

As an update I found the probable cause for this, which unfortunately i did not add to the issue description:

The globalsType i was passing to the script is defined in the main assembly. Debugging the script compilation process I could make out that the reference manager seems to load all assemblies which are referenced in the assembly in which the globalsType is located in.

Moving the globalsType to a separate assembly brought memory usage down to sane levels.

I will leave this issue open for now.

pseabury commented 6 years ago

That did not work for me. My globalsType was in the assembly that referenced all of the numerous roslyn-related assemblies, and I moved it into a simple models class library, but memory consumption did not change at all for me.

CyberSinh commented 6 years ago

You will find in issue #16897 a sample repro showing how to get OutOfMemoryException with C# scripting API.

molinch commented 6 years ago

Thanks @CyberSinh for referencing it Btw I confirm what @tomthoros said: exporting the Globals type into a brand new assembly which has nothing except it, doesn't create this memory peak.

CyberSinh commented 6 years ago

Thanks @molinch, but this workaround doesn't work for me because I have to manipulate with scripts several instances running from my main (and big) assembly. These memory peaks are really a big issue for me.

wilmerbarriosvilla commented 6 years ago

had the same problem, the memory consumption was very high. the solution storing in a dynamic object the compiled and (MVVM) I store compiled code in Sql server and execute it in runtime and there is no need to be compiling at all times.

my app store XAML,c# into sql server and créate screen on runtime

SaveScreenAssembly = store to sql server loadlibraryy= load from sql server

i can send simple, my email Wilmer.barrios@siasoftsas.com

if (this.isMVVM == true) { if (_code == string.Empty) { _code = ((Inicio)Application.Current.MainWindow).DicScreen[idpnt].Code.ToString().Trim(); } if (_loadScreen == string.Empty) { _loadScreen = ((Inicio)Application.Current.MainWindow).DicScreen[idpnt].Load_Screen.ToString().Trim(); } //var scriptOptions = ScriptOptions.Default; ScriptOptions scriptOptions = ScriptOptions.Default; // adiciona referencias scriptOptions = scriptOptions.AddImports("System.Xml", "System.Data.SqlClient", "System", "System.Collections.Generic", "System.ComponentModel", "System.Data", "System.Data.DataSetExtensions", "Microsoft.CSharp", "System.Xml.Linq", "System.Windows", "System.Windows.Controls", "SiasoftApp", "System.Windows.Markup").AddReferences("PresentationCore", "PresentationFramework", "WindowsBase", "System.Core"); scriptOptions = scriptOptions.AddReferences(this.GetType().Assembly.Location).AddReferences("System", "System.Data", "System.Data.DataSetExtensions", "Microsoft.CSharp", "System.Xml.Linq", "System.Windows", "System.Windows.Controls", "SiasoftApp"); _loadScreen = _loadScreen.Replace("_DirLibrary", ((Inicio)Application.Current.MainWindow)._DirLibrary); var engine = CSharpScript.Create(_loadScreen + Environment.NewLine + _code + Environment.NewLine, scriptOptions, typeof(SiasoftApp.HostObjectTab)); //engine = engine.ContinueWith(" UIElement myElement = (UIElement)XamlReader.Parse(usercontrol.__xaml);GridControl.Content = myElement;" + Environment.NewLine); //engine.RunAsync(globals: hostObj).Wait(); //AdjuntarEventos(0, this.GridControl); //engine = engine.ContinueWith(_RegistraObjetos + Environment.NewLine); if (tabitem.Maestra.Trim() == string.Empty) { BarraMaestra.Visibility = Visibility.Visible; ActivaDesactivaMaestra(_EstadoAdEdMae); } else { //if (_EstadoAdEdMae == 0) ActivaDesactivaMaestra(_EstadoAdEdMae); if (IniActivo == false) { _EstadoAdEdMae = 0; ActivaDesactivaMaestra(_EstadoAdEdMae); } } engine.RunAsync(globals: hostObj); //////////// save dll screen ////// var compilacion = engine.GetCompilation(); //compilacion.WithOptions(scriptCompilationOptions); var ms = new MemoryStream(); var diagnostics = compilacion.GetDiagnostics(); System.Text.StringBuilder sb = new System.Text.StringBuilder(); if (diagnostics.Length > 0) { foreach (var diagnostic in diagnostics) { sb.Append(diagnostic.Id + " - " + diagnostic.ToString() + "\n"); } MessageBox.Show(sb.ToString()); } else { Byte[] b = new Byte[ms.Length]; b = ms.ToArray(); SaveScreenAssembly(idpnt, b); ((Inicio)Application.Current.MainWindow).DicScreen[idpnt].CodeMvVm = b; loadlibraryy(b,idpnt); //File.WriteAllBytes(Libraryz, b); } ms.Close(); sb = null; }

SaveScreenAssembly = store to sql server loadlibraryy= load from sql server

i can send simple, my email Wilmer.barrios@siasoftsas.com

UdayaBhandaru commented 6 years ago

Hi Team is there any update on the Issue. We need this to be fixed ASAP.

wilmerbarriosvilla commented 6 years ago

hi, my friend, i can send app sample and your check… all code c# and XAML is store in sql server and compiler in run time.  is very good  3 year init this project Wilmer Barrios Villa

El ‎jueves‎, ‎julio‎ ‎05‎, ‎2018‎ ‎02‎:‎20‎:‎15‎ ‎AM‎ ‎-05, UdayaBhandaru <notifications@github.com> escribió:  

Hi Team is there any update on the Issue. We need this to be fixed ASAP.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

RenishVNair commented 6 years ago

Hi, We are using Microsoft.CodeAnalysis.CSharp.Scripting > CSharpScript class to compile scripts When the script to compile is less then 50 then there is no issue but if it is near to 65-70 memory consumption is beyond excess level and project crashes. As per checking found on each record compile around 50mb of ram is used so it is the problem.

Is there any update going to be implemented in near future, Can you please suggest some alternate method of using it so memory consumption is not too much.

@tomthoros or @molinch Can you please provide any sample code or project that you have used to overcome the memory issue by removing Microsoft.CodeAnalysis.CSharp.Scripting into seperate project.

Thanks

tomlaigna commented 6 years ago

@RenishVNair are you passing in globalsType? Try moving it's definition to a separate clean assembly.

RenishVNair commented 6 years ago

@tomthoros We are using global type in the same project as per below image, will try to move it to a separate project and use that project's dll image

fabiogusmao commented 6 years ago

The solutions that puts the Globals object in a separate assemby nailed it for me. However it is important to highlight that caching the Script objects is important to prevent memory from rising sky high.

My code looks like this:

public class FormulaEvaluator
    {
        private static Regex normalizeRegex = new Regex(@"(\[(\w+)\])", RegexOptions.Compiled);
        private Dictionary<string, Script<double>> compiled = new Dictionary<string, Script<double>>();

        public async Task<double> Evaluate(string formula, CalculatorGlobals globals)
        {

            Script<double> script;
            if (!compiled.TryGetValue(formula, out script))
            {
                string normalized = Normalize(formula);
                script = CSharpScript.Create<double>(normalized, globalsType: typeof(CalculatorGlobals));
                script.Compile();
                compiled.Add(formula, script);
            }

            var result = await script.RunAsync(globals);
            return result.ReturnValue;
        }

        private string Normalize(string formula)
        {
            var matches = normalizeRegex.Matches(formula);

            if (matches.Count == 0)
                return formula;

            StringBuilder sb = new StringBuilder(formula);
            foreach (Match match in matches)
            {
                sb.Replace(match.Groups[1].Value, match.Groups[2].Value);
            }
            return sb.ToString();
        }
    }

The normalization step is just because the system that inputs the formulas I have to execute on mine formats variables as [A]+[B] which is not valid C# code.

The memory footprint will rise every time your program goes across the script.Compile(); line. You can see it happening by putting a breakpoint there and looking at the Memory chat in Diagnostics Tools, and that is why we have to cache already compiled scripts.

samuelGrahame commented 5 years ago

also one option would be to implement Microsoft.CodeAnalysis.MetadataReferenceResolver

public class MissingResolver : Microsoft.CodeAnalysis.MetadataReferenceResolver
        {
            public override bool Equals(object other)
            {
                throw new NotImplementedException();
            }

            public override int GetHashCode()
            {
                throw new NotImplementedException();
            }

            public override bool ResolveMissingAssemblies => false;

            public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties)
            {
                throw new NotImplementedException();
            }
        }

ScriptOptions.Default.WithMetadataResolver(new MissingResolver())

When you don't Resolve Missing Assemblies - for instance i had a StringBuilder in my globals.

.Net Framework didn't crash and .Net Core 2 Crashed - Haven't tested with .Net Core 3

If I remove I do ResolveMissingAssemblies for .Net Core 2 - it will load around 31 Missing References just for StringBuilder. Memory was around 300MB

doing above for .Net Framework it was around 70MB

using MissingResolver class with the .Net Framework memory usage was 30MB

I am also include Mysql.Data for my scripts.

just what I have found so far.

abadyl commented 5 years ago

To have less memory consumption you should use script.CreateDelegate()

var script = CSharpScript.Create<int>("X*Y", globalsType: typeof(Globals)); ScriptRunner<int> runner = script.CreateDelegate();

The delegate doesn’t hold compilation resources (syntax trees, etc.) alive. https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples#-create-a-delegate-to-a-script

Workaround: if you will use script.CreateDelegate(); and then call GC.Collect then your memory will be cleaned up.

To summarize - script.CreateDelegate() allocates a lot of memory but this memory will be collected during next GC.

wilmerbarriosvilla commented 5 years ago

Hi, my name is Wilmer Barrios From Bogota colombia, i download my first Roslyn in year 2014 I have been developing software with Rosly for 3 years, this software has all the source code of c # and XAML of Wpf in Sql server and then it is armed in Runtime,

I would like to be able to show and publish this great development for the community of Visula FoxPro and WPF

Send images

El viernes, 19 de abril de 2019 10:11:08 a. m. GMT-5, Alexey Badyl <notifications@github.com> escribió:  

To have less memory consumption you should use script.CreateDelegate()

var script = CSharpScript.Create("X*Y", globalsType: typeof(Globals)); ScriptRunner runner = script.CreateDelegate();

The delegate doesn’t hold compilation resources (syntax trees, etc.) alive. https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples#-create-a-delegate-to-a-script

Workaround: if you will use script.CreateDelegate(); and then call GC.Collect then your memory will be cleaned up.

To summarize - script.CreateDelegate() allocates a lot of memory but this memory will be collected during next GC.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

mscottreed commented 3 years ago

@abadyl FYI - I have a unit test which does an expression compilation in a loop with explicit GC.Collects, and using CreateDelegate did not actually help matters. My particular case was still leaking on average 18K per compile. I was curious if anyone was been able to successfully Compile SCRIPT using the .NET Core 3.0's AssemblyLoadContext? I am using compilation in a long running server process, so I am very motivated to solve the memory leak. Has there been any news on this front?

pseabury commented 3 years ago

I run scripts - very often and on a long-running server process (in azure Functions). I had problems before I changed to using delegates, but this seems to work fine. You can ignore the custom models I'm passing to the scripts although do note that I made custom DTOs with no dependencies (ScriptingModel.Incident is a simple no-dependecy POCO) or else memory would also balloon.

var script = CSharpScript.Create<ScriptingModel.Incident>(agreement.RoutingExpression, opts, globalsType: typeof(CompiledExpressionGlobals));
var runner = script.CreateDelegate();
var result = await runner(globals);
WesleyBuck commented 3 years ago

I did a similar implementation to @fabiogusmao with a few alterations. The pre/post conditions contain the C# scripts to execute, the extension method is predominantly used to simplify the use of Roslyn as it forms part of a much bigger project. I build all the scripts from a base script essentially having a singular base assembly to work from and load these scripts into memory for future use.

/// </summary>
///Simplify pre and post condition evaluation
/// </summary>
public static class DeciderExtension
{
    private static ScriptOptions scriptOptions =
                ScriptOptions.Default
                    // This adds a reference, to the underlying class defined within the imported library.
                    .WithReferences(new Assembly[] {
                        typeof(Extension).Assembly,
                        typeof(ExceptionHelper).Assembly,
                        typeof(Messages).Assembly,
                        typeof(Data.Processes).Assembly,
                        typeof(Model.Constants.SYS_Type).Assembly,
                        typeof(Data.Utility.LinqExtensions).Assembly
                    })
                    // This adds a using statement
                    .WithImports(new string[] {
                        "Core.Utility.Extensions",
                        "Core.Utility.Helper",
                        "Onboarding.Resources",
                        "Data.Processes",
                        "System.Linq",
                        "System.Collections.Generic",
                        "Model.Constants.SYS_Type",
                        "Data.Utility.LinqExtensions"
                    });

    //Base scripts to start from increases performance with future executions
    private static Script baseScript = CSharpScript.Create("", options: scriptOptions, globalsType: typeof(Evaluate));
    private static Script<string> basePostconditionScript = baseScript.ContinueWith<string>("");
    private static Script<List<Model.UI.ComponentValidation>> basePreconditionScript = baseScript.ContinueWith<List<Model.UI.ComponentValidation>>("");

    //Loaded scripts persisted in memory
    private static Dictionary<string, Script<string>> loadedPostconditionSripts = new Dictionary<string, Script<string>>();
    private static Dictionary<string, Script<List<Model.UI.ComponentValidation>>> loadedPreconditionScripts = new Dictionary<string, Script<List<Model.UI.ComponentValidation>>>();

    /// <summary>
    /// Executes post condition validation on a performance to determine next step. Post condition contains the C# code
    /// </summary>
    /// <param name="_performance"></param>
    /// <param name="_value"></param>
    /// <returns></returns>
    public static async Task<string> EvaluateAsync(this Data.Performance _performance, Evaluate _value)
    {
        if (string.IsNullOrWhiteSpace(_performance.Postcondition))
            return default(string);

        if (!loadedPostconditionSripts.ContainsKey(_performance.Code))
            loadedPostconditionSripts.Add(_performance.Code, basePostconditionScript.ContinueWith<string>(_performance.Postcondition));

        ScriptState<string> scriptState = await loadedPostconditionSripts[_performance.Code].RunAsync(_value);
        return scriptState.ReturnValue;
    }

    /// <summary>
    /// Executes post condition validation on a performance. Pre condition contains the C# code
    /// </summary>
    /// <param name="_performance"></param>
    /// <param name="_value"></param>
    /// <returns></returns>
    public static async Task<List<Model.UI.ComponentValidation>> EvaluatePerformanceAsync(this Data.Evaluation _performance, Evaluate _value)
    {
        if (string.IsNullOrWhiteSpace(_performance.Precondition))
            return new List<Model.UI.ComponentValidation> {
                    new Model.UI.ComponentValidation(_value._decisionTask.SubperformanceCode, true)
                };

        return await evaluate(_performance, _value);
    }

    /// <summary>
    /// Executes pre condition validation on a evidence. Pre condition contains the C# code
    /// </summary>
    /// <param name="_evidence"></param>
    /// <param name="_value"></param>
    /// <returns></returns>
    public static async Task<List<Model.UI.ComponentValidation>> EvaluateEvidenceAsync(this Data.Evaluation _evidence, Evaluate _value)
    {
        if (string.IsNullOrWhiteSpace(_evidence.Precondition))
            return new List<Model.UI.ComponentValidation> {
                    new Model.UI.ComponentValidation(_value._decisionTask.Evidence.First().Code, true)
                };

        return await evaluate(_evidence, _value);
    }

    private static async Task<List<ComponentValidation>> evaluate(Evaluation _eval, Evaluate _value)
    {
        if (!loadedPreconditionScripts.ContainsKey(_eval.Code))
            loadedPreconditionScripts.Add(_eval.Code, basePreconditionScript.ContinueWith<List<Model.UI.ComponentValidation>>(_eval.Precondition));

        ScriptState<List<Model.UI.ComponentValidation>> scriptState = await loadedPreconditionScripts[_eval.Code].RunAsync(_value);
        return scriptState.ReturnValue;
    }
}