Где у меня утечка памяти и как это исправить? Почему увеличивается потребление памяти?

Я уже несколько дней борюсь с проблемой роста потребления памяти консольным приложением в .Net Core 2.2, и только сейчас у меня закончились идеи, что еще можно улучшить.

В моем приложении у меня есть метод, который запускает метод StartUpdatingAsync:

public MenuViewModel()
        {
            if (File.Exists(_logFile))
                File.Delete(_logFile);

            try
        {
            StartUpdatingAsync("basic").GetAwaiter().GetResult();
        }
        catch (ArgumentException aex)
        {
            Console.WriteLine($"Caught ArgumentException: {aex.Message}");
        }

            Console.ReadKey();
        }

StartUpdatingAsync создает «репо», и экземпляр получает из БД список объектов для обновления (около 200 тыс.):

private async Task StartUpdatingAsync(string dataType)
        {
            _repo = new DataRepository();
            List<SomeModel> some_list = new List<SomeModel>();
            some_list = _repo.GetAllToBeUpdated();

            await IterateStepsAsync(some_list, _step, dataType);
        }

И теперь, в течение IterateStepsAsync, мы получаем обновления, анализируя их с существующими данными и обновляя БД. Внутри каждого while я создавал новые экземпляры всех новых классов и списков, чтобы быть уверенным, что старые освобождают память, но это не помогло. Также мне было GC.Collect() в конце метода, что тоже не помогает. Обратите внимание, что приведенный ниже метод запускает множество параллельных задач, но они должны быть размещены внутри него, я прав?:

private async Task IterateStepsAsync(List<SomeModel> some_list, int step, string dataType)
        {
            List<Area> areas = _repo.GetAreas();
            int counter = 0;

            while (counter < some_list.Count)
            {
                _repo = new DataRepository();
                _updates = new HttpUpdates();
                List<Task> tasks = new List<Task>();
                List<VesselModel> vessels = new List<VesselModel>();
                SemaphoreSlim throttler = new SemaphoreSlim(_degreeOfParallelism);

                for (int i = counter; i < step; i++)
                {
                    int iteration = i;
                    bool skip = false;

                    if (dataType == "basic" && (some_list[iteration].Mmsi == 0 || !some_list[iteration].Speed.HasValue)) //if could not be parsed with "full"
                        skip = true;

                    tasks.Add(Task.Run(async () =>
                    {
                        string updated= "";
                        await throttler.WaitAsync();
                        try
                        {
                            if (!skip)
                            {
                                Model model= await _updates.ScrapeSingleModelAsync(some_list[iteration].Mmsi);
                                while (Updating)
                                {
                                    await Task.Delay(1000);
                                }
                                if (model != null)
                                {
                                    lock (((ICollection)vessels).SyncRoot)
                                    {
                                        vessels.Add(model);

                                        scraped = BuildData(model);
                                    }
                                }
                            }
                            else
                            {
                                //do nothing
                            }
                        }
                        catch (Exception ex)
                        {
                            Log("Scrape error: " + ex.Message);
                        }
                        finally
                        {
                            while (Updating)
                            {
                                await Task.Delay(1000);
                            }
                            Console.WriteLine("Updates for " + counter++ + " of " + some_list.Count + scraped);

                            throttler.Release();
                        }

                    }));
                }

                try
                {
                    await Task.WhenAll(tasks);
                }
                catch (Exception ex)
                {
                    Log("Critical error: " + ex.Message);
                }
                finally
                {
                    _repo.UpdateModels(vessels, dataType, counter, some_list.Count, _step);

                    step = step + _step;

                    GC.Collect();
                }
            }
        }

Внутри метода выше мы вызываем _repo.UpdateModels, где обновляется БД. Я попробовал два подхода: с использованием EC Core и SqlConnection. Оба с похожими результатами. Ниже вы можете найти их обоих.

EF Core

internal List<VesselModel> UpdateModels(List<Model> vessels, string dataType, int counter, int total, int _step)
        {

            for (int i = 0; i < vessels.Count; i++)
            {
                Console.WriteLine("Parsing " + i + " of " + vessels.Count);

                Model existing = _context.Vessels.Where(v => v.id == vessels[i].Id).FirstOrDefault();
                if (vessels[i].LatestActivity.HasValue)
                {
                    existing.LatestActivity = vessels[i].LatestActivity;
                }
                //and similar parsing several times, as above
            }

            Console.WriteLine("Saving ...");
            _context.SaveChanges();
            return new List<Model>(_step);
        }

SqlConnection

internal List<VesselModel> UpdateModels(List<Model> vessels, string dataType, int counter, int total, int _step)
        {
            if (vessels.Count > 0)
            {
                using (SqlConnection connection = GetConnection(_connectionString))
                using (SqlCommand command = connection.CreateCommand())
                {
                    connection.Open();
                    StringBuilder querySb = new StringBuilder();

                    for (int i = 0; i < vessels.Count; i++)
                    {
                        Console.WriteLine("Updating " + i + " of " + vessels.Count);
                        //PARSE

                        VesselAisUpdateModel existing = new VesselAisUpdateModel();

                        if (vessels[i].Id > 0)
                        {
                            //find existing
                        }

                        if (existing != null)
                        {
                            //update for basic data
                            querySb.Append("UPDATE dbo." + _vesselsTableName + " SET Id = '" + vessels[i].Id+ "'");
                            if (existing.Mmsi == 0)
                            {
                                if (vessels[i].MMSI.HasValue)
                                {
                                    querySb.Append(" , MMSI = '" + vessels[i].MMSI + "'");
                                }
                            }
                            //and similar parsing several times, as above

                            querySb.Append(" WHERE Id= " + existing.Id+ "; ");

                            querySb.AppendLine();
                        }
                    }

                    try
                    {
                        Console.WriteLine("Sending SQL query to " + counter);
                        command.CommandTimeout = 3000;
                        command.CommandType = CommandType.Text;
                        command.CommandText = querySb.ToString();
                        command.ExecuteNonQuery();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                    finally
                    {
                        connection.Close();
                    }
                }
            }
            return new List<Model>(_step);
        }

Основная проблема в том, что после десятков/сотен тысяч обновленных моделей потребление памяти моим консольным приложением постоянно увеличивается. И я понятия не имею, почему.

РЕШЕНИЕ моя проблема заключалась в методе ScrapeSingleModelAsync, где я неправильно использовал HtmlAgilityPack, что я смог отладить благодаря cassandrad.


person bakunet    schedule 24.04.2020    source источник


Ответы (1)


Ваш код беспорядочный, с огромным количеством разных объектов с неизвестным временем жизни. Вряд ли можно понять проблему, просто глядя на нее.

Рассмотрите возможность использования инструментов профилирования, например Visual Studio Diagnostic Tools, они помогут вам определить, какие объекты слишком долго живут в куче. здесь представлен обзор его функций. связанные с профилированием памяти. Настоятельно рекомендуется к прочтению.

Короче говоря, вам нужно сделать два снимка и посмотреть, какие объекты занимают больше всего памяти. Давайте рассмотрим простой пример.

int[] first = new int[10000];
Console.WriteLine(first.Length);
int[] secod = new int[9999];
Console.WriteLine(secod.Length);
Console.ReadKey();

Сделайте первый снимок, когда ваша функция сработает хотя бы один раз. В моем случае я сделал снимок, когда было выделено первое огромное пространство. введите здесь описание изображения

После этого дайте вашему приложению поработать какое-то время, чтобы разница в использовании памяти стала заметной, сделайте второй снимок памяти.

введите здесь описание изображения Вы заметите, что добавлен еще один снимок с информацией о том, насколько велика разница. Чтобы получить более конкретную информацию, нажмите на ту или иную синюю метку последнего снимка, чтобы открыть сравнение снимков.

введите здесь описание изображения

Следуя моему примеру, мы видим изменение количества массивов int. По умолчанию int[] не был виден в таблице, поэтому мне пришлось снять флажок Just My Code в параметрах фильтрации.
Итак, это то, что нужно сделать. После того, как вы выясните, какие объекты увеличиваются в количестве или размере с течением времени, вы можете определить, где создаются эти объекты, и оптимизировать эту операцию.

person cassandrad    schedule 24.04.2020
comment
На самом деле это выглядит как отличный инструмент для диагностики моей проблемы. Я никогда не использовал Сделать снимок. Я посмотрю на это и вернусь с результатами. Спасибо. - person bakunet; 24.04.2020
comment
Хорошо, благодаря Take Snapshot я смог выяснить, что HttpClient объекты, созданные HtmlAgilityPack, утекали в память. Спасибо. - person bakunet; 25.04.2020