lundi 3 août 2020

Why is memory utilization continuiously increasing when using dependency injection in a C# console application?

I may know the answer to my posted question: I'm using constructor dependency injection throughout the entire application which is a looped C# console application that does not exit after each request.

I suspect the life time of all of the included objects is essentially infinite due to this. When attempting to adjust the life time while registering, it warns that a transient object cannot be implemented on a singleton object due to dependencies (which inspired looking at memory utilization and this question).

This is my first ground up console application, a bot, that logs into a service provider and waits for messages. I come from .NET Core Web API which again has dependencies all over, but I think the key difference here is below all of my code is the platform itself which handles each request individually then kills the thread that ran.

How close am I? Would I have to separate the bot itself from the base console application listening to the service provider and attempt to replicate the platform that IIS/kestrel/MVC routing provides to separate the individual requests?

Edit: Originally I intended this question as more of a design principal, best practice, or asking for direction direction. Folks requested reproducible code so here we go:

namespace BotLesson
{
    internal class Program
    {
        private static readonly Container Container;

        static Program()
        {
            Container = new Container();
        }

        private static void Main(string[] args)
        {
            var config = new Configuration(args);

            Container.AddConfiguration(args);
            Container.AddLogging(config);

            Container.Register<ITelegramBotClient>(() => new TelegramBotClient(config["TelegramToken"])
            {
                Timeout = TimeSpan.FromSeconds(30)
            });
            Container.Register<IBot, Bot>();
            Container.Register<ISignalHandler, SignalHandler>();

            Container.Register<IEventHandler, EventHandler>();
            Container.Register<IEvent, MessageEvent>();

            Container.Verify();

            Container.GetInstance<IBot>().Process();

            Container?.Dispose();
        }
    }
}

Bot.cs

namespace BotLesson
{
    internal class Bot : IBot
    {
        private readonly ITelegramBotClient _client;
        private readonly ISignalHandler _signalHandler;
        private bool _disposed;

        public Bot(ITelegramBotClient client, IEventHandler handler, ISignalHandler signalHandler)
        {
            _signalHandler = signalHandler;

            _client = client;
            _client.OnCallbackQuery += handler.OnCallbackQuery;
            _client.OnInlineQuery += handler.OnInlineQuery;
            _client.OnInlineResultChosen += handler.OnInlineResultChosen;
            _client.OnMessage += handler.OnMessage;
            _client.OnMessageEdited += handler.OnMessageEdited;
            _client.OnReceiveError += (sender, args) => Log.Error(args.ApiRequestException.Message, args.ApiRequestException);
            _client.OnReceiveGeneralError += (sender, args) => Log.Error(args.Exception.Message, args.Exception);
            _client.OnUpdate += handler.OnUpdate;
        }

        public void Process()
        {
            _signalHandler.Set();
            _client.StartReceiving();

            Log.Information("Application running");

            _signalHandler.Wait();

            Log.Information("Application shutting down");
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;
            if (disposing) _client.StopReceiving();
            _disposed = true;
        }
    }
}

EventHandler.cs

namespace BotLesson
{
    internal class EventHandler : IEventHandler
    {
        public void OnCallbackQuery(object? sender, CallbackQueryEventArgs e)
        {
            Log.Debug("CallbackQueryEventArgs: {e}", e);
        }

        public void OnInlineQuery(object? sender, InlineQueryEventArgs e)
        {
            Log.Debug("InlineQueryEventArgs: {e}", e);
        }

        public void OnInlineResultChosen(object? sender, ChosenInlineResultEventArgs e)
        {
            Log.Debug("ChosenInlineResultEventArgs: {e}", e);
        }

        public void OnMessage(object? sender, MessageEventArgs e)
        {
            Log.Debug("MessageEventArgs: {e}", e);
        }

        public void OnMessageEdited(object? sender, MessageEventArgs e)
        {
            Log.Debug("MessageEventArgs: {e}", e);
        }

        public void OnReceiveError(object? sender, ReceiveErrorEventArgs e)
        {
            Log.Error(e.ApiRequestException, e.ApiRequestException.Message);
        }

        public void OnReceiveGeneralError(object? sender, ReceiveGeneralErrorEventArgs e)
        {
            Log.Error(e.Exception, e.Exception.Message);
        }

        public void OnUpdate(object? sender, UpdateEventArgs e)
        {
            Log.Debug("UpdateEventArgs: {e}", e);
        }
    }
}

SignalHandler.cs

This isn't directly related to my problem, but it is holding the application in a waiting pattern while the third party library listens for messages.

namespace BotLesson
{
    internal class SignalHandler : ISignalHandler
    {
        private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);
        private readonly SetConsoleCtrlHandler? _setConsoleCtrlHandler;

        public SignalHandler()
        {
            if (!NativeLibrary.TryLoad("Kernel32", typeof(Library).Assembly, null, out var kernel)) return;
            if (NativeLibrary.TryGetExport(kernel, "SetConsoleCtrlHandler", out var intPtr))
                _setConsoleCtrlHandler = (SetConsoleCtrlHandler) Marshal.GetDelegateForFunctionPointer(intPtr,
                    typeof(SetConsoleCtrlHandler));
        }

        public void Set()
        {
            if (_setConsoleCtrlHandler == null) Task.Factory.StartNew(UnixSignalHandler);
            else _setConsoleCtrlHandler(WindowsSignalHandler, true);
        }

        public void Wait()
        {
            _resetEvent.WaitOne();
        }

        public void Exit()
        {
            _resetEvent.Set();
        }

        private void UnixSignalHandler()
        {
            UnixSignal[] signals =
            {
                new UnixSignal(Signum.SIGHUP),
                new UnixSignal(Signum.SIGINT),
                new UnixSignal(Signum.SIGQUIT),
                new UnixSignal(Signum.SIGABRT),
                new UnixSignal(Signum.SIGTERM)
            };

            UnixSignal.WaitAny(signals);
            Exit();
        }

        private bool WindowsSignalHandler(WindowsCtrlType signal)
        {
            switch (signal)
            {
                case WindowsCtrlType.CtrlCEvent:
                case WindowsCtrlType.CtrlBreakEvent:
                case WindowsCtrlType.CtrlCloseEvent:
                case WindowsCtrlType.CtrlLogoffEvent:
                case WindowsCtrlType.CtrlShutdownEvent:
                    Exit();
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(signal), signal, null);
            }

            return true;
        }

        private delegate bool SetConsoleCtrlHandler(SetConsoleCtrlEventHandler handlerRoutine, bool add);

        private delegate bool SetConsoleCtrlEventHandler(WindowsCtrlType sig);

        private enum WindowsCtrlType
        {
            CtrlCEvent = 0,
            CtrlBreakEvent = 1,
            CtrlCloseEvent = 2,
            CtrlLogoffEvent = 5,
            CtrlShutdownEvent = 6
        }
    }
}

My original point is based off of some assumptions I am making on SimpleInject--or more specifically the way I am using SimpleInject.

The application stays running, waiting on SignalHandler._resetEvent. Meanwhile messages come in via any of the handlers on Bot.cs constructor.

So my thought/theory is Main launches Bot.Process which has a direct dependency on ITelegramClient and IEventHandler. In my code there isn't a mechanism to let these resources go and I suspect I was assuming the IoC was going to perform magic and release resources.

However, sending messages to the bot continuously increases the number of objects, according to Visual Studio memory usage. This is reflected in actual process memory as well.

Though, while editing this post for approval, I think I may have ultimately been misinterpreting Visual Studio's diagnostic tools. The application's memory utilization seems to hang out at around 36 MB after 15 minutes of run time. Or it's simply increasing so little at a time that it's difficult to see.

Comparing Memory Usage snapshots I took at 1 minute versus 17 minutes, there appears to have been 1 of each of the objects above created. If I am reading this properly, I imagine that proves the IoC is not creating new objects (or they are being disposed before I have a chance to create a snapshot.

Aucun commentaire:

Enregistrer un commentaire