воскресенье, 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);
}