воскресенье, 26 мая 2013 г.

Accelerate the entity framework application 20 times

 

Или некоторые соображения по скорости работы EF в реальных приложениях. Довольно часто мы не задумываемся сколько «стоит» удобство использования ORM и всякого «сахара». На днях сел писать программу для импорта данных из sqlite в MS SQL. Структура приведена ниже на рисунках.

Следующие поля в исходной базе строковые:

· Bssid

· Ssid

· Capabilities

· Type

Импортируемые данные

clip_image001

Схема таблиц для вставки данных

clip_image002

Хранить фичи в виде строки мне показалось плохой идеей поэтому распарсил строки и завел новую табличку для возможностей. Получившаяся схема приведена на рисунке «Схема таблиц для вставки данных»

Дальше использовал EF code first для чтения данных и для записи. Результаты не утешали, на обработку одной записи уходило 170- 210 ms. На 50 000 записях это 142 минуты. Ого!!!!!!! При том что чтение этих данных в редакторе заняло 2 – 3 секунды. Немножко про архитектуру - все в принципе стандартно:

public class KvnSiteContext : DbContext{

public DbSet<Node> Nodes { get; set; }

public DbSet<Feature> Features { get; set; }

public DbSet<UserProfile> UserProfiles { get; set; }

public DbSet<Location> Locations { get; set; }

}

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

sealed internal class Duration{

public Duration()

{

_start = DateTime.Now;

}

private readonly DateTime _start;

public TimeSpan GetDuration()

{

return DateTime.Now - _start;

}

}

Это позволило точно определить виновника по времени.

Итак первоначальный алгоритм:

· Считать записи из таблицы network

· Распарсить фичи

· Каждую фичу вставить в коллекцию и сохранить если новая

· Вставить элемент Node

· Сохранить контекст

· Считать записи из таблицы location

· Найти по полю bssid сущность Node

· Вставить сущность Location

· Сохранить контекст

Результаты были неутешительны, как я уже писал 170 – 210 ms.

Будем что то менять. Переносим сохранение коллекции фич на момент сохранения ноды.

Время уменьшается, до 140-160ms. Дальнейшее расследование показало что слишком много запросов даже при простом поиске, хотя данные уже у нас и зачем искать в БД?

Было применено локальное кэширование всего чего возможно, результаты неутешительные 110-120ms. И тогда я понял что время тяжёлой артиллерии. TSQL!!!!!!!!!!!!

Были созданы 3 хранимые процедуры и одна функция найдена в интернете. В результате время выполнения импорта снизилось с 142 мин до 7. Вот вам один из примеров того как влияют фреймворки на скорость.

Код хранимок:

USE [kvnwifi]

GO

/****** Object: StoredProcedure [dbo].[insertNode] Script Date: 05/26/2013 20:46:50 ******/

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

ALTER PROCEDURE [dbo].[insertNode]

@Id uniqueidentifier OUT,

@bssid nvarchar(MAX),

@ssid nvarchar (MAX),

@frequency int,

@capabilities nvarchar(MAX),

@lasttime bigint,

@lastlat float,

@lastlon float ,

@type int,

@user_id int

AS

BEGIN

-- SET NOCOUNT ON added to prevent extra result sets from

-- interfering with SELECT statements.

SET NOCOUNT ON;

set @id = NEWID();

-- Insert statements for procedure here

INSERT into [dbo].[Nodes]

(

[Id],

[BsSid],

[SSid],

[Frequency],

[LastTime],

[Lat],

[Lon],

[Type],

[User_UserId]

)

VALUES(

@id,

@bssid,

@ssid,

@frequency,

@lasttime,

@lastlat,

@lastlon,

@type,

@user_id

);

exec dbo.InsertCapabilities @Nodeid = @id, @Capabilities = @capabilities

END

USE [kvnwifi]

GO

/****** Object: StoredProcedure [dbo].[InsertCapabilities] Script Date: 05/26/2013 20:47:15 ******/

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

ALTER PROCEDURE [dbo].[InsertCapabilities]

@NodeId uniqueidentifier,

@Capabilities nvarchar(MAX)

AS

BEGIN

SET NOCOUNT ON

DECLARE @i int

DECLARE @numrows int

DECLARE @feature_name nvarchar(100)

DECLARE @numfeature int

DECLARE @fid int

DECLARE @fn_table TABLE(

idx smallint Primary Key IDENTITY(1,1),

[Name] nvarchar(100)

)

INSERT @fn_table

select [Name] from [dbo].[splitstring](@Capabilities)

SET @i = 1

SET @numrows = (SELECT COUNT(*) FROM @fn_table)

IF @numrows > 0

WHILE (@i <= (SELECT MAX(idx) FROM @fn_table))

BEGIN

-- get the next employee primary key

SET @feature_name = (SELECT [Name] FROM @fn_table WHERE idx = @i)

SET @numfeature = (SELECT COUNT(*) from [dbo].[Features] WHERE [FeatureName] = @feature_name)

IF @numfeature != 0

BEGIN

SET @fid = (SELECT [Id] FROM Features WHERE [FeatureName] = @feature_name)

END

ELSE

BEGIN

INSERT INTO dbo.Features ([FeatureName]) VALUES (@feature_name)

SET @fid = @@IDENTITY

END

INSERT INTO dbo.FeatureNodes ([Feature_Id],[Node_Id]) VALUES (@fid,@NodeId)

-- increment counter for next employee

SET @i = @i + 1

END

END

USE [kvnwifi]

GO

/****** Object: StoredProcedure [dbo].[insertLocation] Script Date: 05/26/2013 20:47:33 ******/

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

-- =============================================

-- Author: <Author,,Name>

-- Create date: <Create Date,,>

-- Description: <Description,,>

-- =============================================

ALTER PROCEDURE [dbo].[insertLocation]

@Id uniqueidentifier OUT,

@Bssid nvarchar(MAX),

@Level bigint,

@Lat float,

@Lon float,

@Altitude float,

@Accuracy float,

@LastDate bigint

AS

BEGIN

DECLARE @bsguid uniqueidentifier

DECLARE @return uniqueidentifier

-- SET NOCOUNT ON added to prevent extra result sets from

-- interfering with SELECT statements.

SET NOCOUNT ON;

SET @bsguid = (SELECT [Id] FROM [dbo].[Nodes] WHERE [BsSid] = @Bssid)

SET @Id = NEWID()

INSERT INTO [dbo].[Locations]

(

[Id],

[Bssid],

[Level],

[Lat],

[Lon],

[Altitude],

[Accuracy],

[LastDate]

)

VALUES

(

@Id,

@bsguid,

@Level,

@Lat,

@Lon,

@Altitude,

@Accuracy,

@LastDate

)

END

start: 26.05.2013 18:38:24 end: 26.05.2013 18:45:01

start: 26.05.2013 18:38:24 end: 26.05.2013 18:38:24 item processed: 43301 last time ms: 10,0006

вторник, 7 мая 2013 г.

Automaticaly detect user ticket based on email headers

Введение

Часто мне приходят письма из службы технической поддержки и они меня сильно раздражают такими фразами“Это письмо сгенерировано автоматически, при последующих обращениях указывайте в теме письма номер тикета xxx-1977”. По моему это бред. Сегодня мой коллега столкнулся с этой проблемой, как передать информацию в письме? После работы я посидел покурил RFC И решение пришло

Немного стандартов

http://tools.ietf.org/html/rfc822 собственно это наше все, уникальный заголовок письма описывается в 4.6.1.  MESSAGE-ID / RESENT-MESSAGE-ID это нам и нужно. В вольном переводе:

Данный заголовок должен представлять из себя уникальную строку формируемую сервером при ее отсутствии либо клиентом (Бинго!!!). Смотри дальше  видим заголовки

4.6.2. IN-REPLY-TO
 
4.6.3. REFERENCES

 

Конечно они опциональные, но таки нормальные клиенты их ставят (Бинго!!!)

 

Сам код

этот код стандартный C#, но я думаю на любом другом ЯП можно с легкостью его повторить

using (var client = new SmtpClient())
using(var message = new MailMessage("test@ya.ru",model.To,model.Subject,model.Body))
{
  int requestId = 1977;
  var messageid = string.Format("<{0}_{1}>", Guid.NewGuid().ToString("D"), requestId);
  message.Headers.Add("Message-ID", messageid);
                    client.Send(message);
}

суббота, 27 апреля 2013 г.

Кэширование часто запрашиваемых данных

Класс возник по результату трасировки приложения, оказалось что есть куча дублирующих запросов, трассировка выполнялась с помощью http://miniprofiler.com/, получил целую кучу дублированных запросов. Написал этот кэш менеджер, вместо 42 сек. получил время загрузки 3 секунды.

Примеры вызова:
// время кэширования 3 сек. количество элементов 100, фоновая очистка включена
OneTimeCash cash = new OneTimeCash(3,100,true); 
// время кэширования 400 сек. количество элементов 100, фоновая очистка выключена
var cashe1 = new OneTimeCash(400, 100, false);



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace OneTimeCach
{
    ///

    ///  Кэширование предмета 1 раз. Используется для тяжелых операций типа обращения к БД
    ///
    /// тип кэшируемого значения
    public class OneTimeCash
    {
        ///

        ///Конструктор класса с инициализацией параметров по умолчанию.
        ///
        /// интервал очистки старых элементов в кэше по умолчанию 5 секунд
        /// Максимальное значение элементов в кэше
        /// Запускать очистку в фоновом потоке по умолчанию false, в этом случае очистка происходит при вставке элемента
        public OneTimeCash(int duration = 5, int maxcount = 100, bool backgroundCleanup = false)
        {
            this.duration = duration;
            this.isBackgroundCleanup = backgroundCleanup;
            this.maxCount = maxcount;
            this.items = new List(maxCount);
            if (isBackgroundCleanup)
            {
                isALive = true;
                Thread thread = new Thread(new ThreadStart(CleanapThreadWorker));
                thread.Start();
            }
        }

        ///

        /// Закрываем поток очистки кэша
        ///
        ~OneTimeCash()
        {
            this.isALive = false;
        }

        ///

        ///  Процедура фоновой очистки кэша
        ///
        void CleanapThreadWorker()
        {
            while (isALive)
            {
                Thread.Sleep(new TimeSpan(0, 0, duration));
                Cleanup();
            }
        }

        ///

        /// Контейнер для кэшируемых данных
        ///
        private class Container
        {
            ///

            /// Времф создания или обновления обьекта в кэше
            ///
            public DateTime Updated;

            ///

            /// Ключ для кэшированного обьекта
            ///
            public string Key;

            ///

            /// Кэшированный обьект
            ///
            public T Value;
        }

        ///

        /// Флаг фонового очищения данных кэша
        ///
        bool isBackgroundCleanup;

        ///

        /// Флаг указывающий на необходимость работы фоновой очистки данных
        ///
        bool isALive;


        public bool IsAlive { get { return isALive; } set { isALive = value; } }
        ///

        /// Промежуток времени жизни обьектов в кэше
        ///
        int duration;

        ///

        /// Максимальное количество обьектов в кэше
        ///
        int maxCount;

        ///

        /// Контейнер для хранения кэшированных обьектов
        ///
        List items;


        ///

        /// Обьект блокировки
        ///
        object locker = new object();

        ///

        /// Помещает элемент в кэш, и производит очистку устаревших элементов если не установлена автоочистка
        ///
        /// Ключ для сохранения кэшированного обьекта
        /// Кэшируемый обьект
        public void PutItem(string key, T cashedObject)
        {
            var item = items.FirstOrDefault(x => x.Key == key);
            if (item != null)
            {
                item.Updated = DateTime.Now;
                item.Value = cashedObject;
            }
            else
            {
                lock (locker)
                {
                    items.Add(new Container() { Key = key, Value = cashedObject, Updated = DateTime.Now });
                }
            }
            if (items.Count > maxCount )
            {
                Cleanup();
            }
        }

        ///

        /// Очистка от устаревших элементов кэша
        ///
        private void Cleanup()
        {
            var result = (from sel in items
                          where sel.Updated > DateTime.Now.AddSeconds(-duration)
                          orderby sel.Updated descending
                          select sel).Take(maxCount);
         
            List container = new List(maxCount);
            container.AddRange(result);
            lock (locker)
            {
                items = container;
            }

        }

        ///

        ///  Проверяет наличие обьекта в кэше
        ///
        /// Ключ для поиска
        ///
        public bool IsItemInCache(string key)
        {
            return items.Any(x => x.Key == key);
        }

        ///

        ///  Получаем закешированный обьект
        ///
        /// Ключ для получения кэшированного обьекта
        /// Возникает когда обьекта нет в кэше
        ///
        public T GetItem(string key)
        {
            var item = items.FirstOrDefault(x => x.Key == key);
            if (item != null)
            {
                return item.Value;
            }
            else
            {
                throw new ArgumentException(string.Format("No cashed object: {0}", key));
            }
        }
    }
}