Как использовать .NET из LoadRunner

Хотя LoadRunner обладает неплохим API для различной текстовой обработки, иногда его всё же не хватает, и тогда приходится расширять его самописными функциями. Часто такие реализации становятся изобретением велосипеда, поскольку почти все задачи, как известно, уже когда-то кем-то решены. Кроме того, поскольку у меня неплохой бэкграунд в C#, при решении какой-либо задачи часто возникают мысли, что эта задача легко бы решилась, будь у меня под рукой библиотека классов .NET Framework. В принципе, если бы я был Java-программистом, у меня возникали бы аналогичный мысли и про Java (где тоже есть почти всё), но поскольку мне ближе .NET, то речь пойдёт именно о нём. В качестве побочного эффекта статья будет полезна тем, кто хочет узнать, как вызывать CLR-код из native-кода. Также приводится небольшое исследование производительности этого варианта и прилагается рабочий шаблон проекта Visual Studio и скрипт LoadRunner.

.NET, и Java в LoadRunner


Для начала рассмотрим привлекательный, но плохой вариант. В принципе и .NET, и Java работают в LoadRunner непосредственно. Для каждой из этих платформ есть классы, представляющие собой обёртки над стандартным API LoadRunner. Ими можно пользоваться прямо сразу же, выбрав, соответственно, режимы .NET и Java Vuser. Скажем сразу: эти режимы разработки скриптов созданы несколько для другого. Режим .NET позволяет записать активность .NET-приложения и создать скрипт, вызывающий непосредственно методы классов приложения. Java Vuser имеет полноценное и документированное API для Java, но зато и вовсе не имеет режима записи (его имеет Java Record Replay в смысле аналогичном .NET). По этой причине пользоваться ими для Web очень проблематично, и вообще, «пользоваться» в данном случае означает «написать как-то работающий код» и больше ничего. При разработке нагрузочных скриптов от инструмента важно получить возможность записи трафика и преобразования его в скрипт, хотя бы черновой, который впоследствии будет дорабатываться. Но вот незадача: запись веб-трафика (режим Web — HTTP/HTML) возможна только с конвертацией в код на Си. Сообщество уже давно этого ждёт, да и я, честно говоря, надеялся, что хотя бы в новой версии 12.00 появится возможность выбрать C# или Java, но этого так и не случилось. Кроме того, API для .NET практически не документировано, и действовать приходится, ступая по граблям в темноте (кстати, ещё и сигнатуры методов для .NET и Java-обёрток различаются). Если найти нужный метод-обёртку в классах .NET удаётся сравнительно быстро, то разобраться, как ему передать параметры, я не знаю.

К примеру:

namespace Script
{
    public partial class VuserClass
    {
        public int Action()
        {
        	web.url("ya.ru", "URL=http://ya.ru/", null, null);
            return 0;
        }
    }
}


Этот код работает и осуществляет запрос, эквивалентный Си-функции web_url(). Способ привлекателен тем, что можно подключать в Run-Time Settings любые .NET-библиотеки и сразу же их использовать, однако, тут сразу же возникают вопросы:

  • Как передать web.url() параметры аналогично web_url()?
  • Как заставить это всё работать через прокси?
  • Почему в этом режиме исчезает добрая часть Run-Time Settings? (В принципе понятно почему, но от этого не легче.)


В общем, этот режим сделан не для того, поэтому, несмотря на потенциальное удобство этого варианта, использовать его не по назначению мы не будем и останемся на Си. Всё же возможность быстро перезаписать кусок скрипта является более весомой. В то же время, выполнив немного протых действий, можно вызывать .NET-код из Си-скрипта LoadRunner.

LoadRunner штатно поддерживает вызов нативных библиотек, для этого есть функция lr_load_dll(). Для того, чтобы вызвать .NET, придётся написать нативную прослойку и, собственно, весь вопрос сводится к вызову CLR-кода из native кода. Кто знает, как это делается, можно следующий раздел пропустить, для остальных ниже я расскажу, как это можно сделать.

Native DLL с обращением к .NET


Однажды мне понадобилось раскодировать строки вида:

Создать заявку

Это разновидность кодировки, используемой внутри XML и HTML: каждый символ изображается в виде кода UTF-8. Сделать это на LoadRunner'е у меня не получилось (если кто знает как, прошу ткнуть меня носом). Зато в дотнетовском классе HttpUtility есть метод HtmlDecode(), который это прекрасно делает. Посмотрим, как можно его заюзать.

Требования

Естественно, потребуется установленный в системе .NET Framework. Конкретно данный метод есть в любой версии начиная, с 2.0 (а может и раньше, но это уже неважно), но помните, что в native-библиотеке указывается, к какой версии мы обращаемся, а разные версии не взаимозаменяемы. Также помните, что Win7/2008 уже установлен .NET FW 3.5, так что если у вас используются нагрузочные станции на этих ОС, то ничего ставить не нужно, нужно лишь указать внутри сиплюсплюсной библиотеки, что мы используем .NET Framework 3.5.

Пишем DLL

С некоторых пор в Visual Studio стало очень просто из C++ кода обращаться к классам .NET. Для этого нужно в настройках проекта установить режим Common Language Runtime Support. Далее нужно зайти в свойства проекта, Common Properties, Framework and References и добавить ссылки на сборки, которые вы хотите использовать. В данном случае нас интересует System.Web из Assemblies/Framework. После этого можно уже обращаться к классам .NET, правда, в C++-синтаксисе:

#include "..\LR include 11.50\lrun.h"
//...
int xml_http_decode(const char* inputStr, const char* outputParam)
{
	try
	{
		System::String^ temp = gcnew System::String(inputStr);
		System::String^ result = HttpUtility::HtmlDecode(temp);
		marshal_context^ context = gcnew marshal_context();
		lr_save_string(context->marshal_as<const char*>(result), outputParam);
	}
	catch(char* message)
	{
		lr_save_string(message, outputParam);
		return LR_FAIL;
	}
	catch(...)
	{
		lr_save_string("!!! Unknown exception raised !!!", outputParam);
		return LR_FAIL;
	}

	return LR_PASS;
}

LoadRunner умеет подключать только нативные dll-ки, поэтому мы объявляем функцию с Си-совместимыми сигнатурой и возвращаемым значением. Далее, для объявления типов .NET и отличия их от типов C++ используется знак "^". Мы должны создать из Си-строк строки CLR, чтобы передать их в методы .NET. Для создания объектов CLR используется оператор gcnew.

Чтобы функцию можно было вызывать извне dll, её нужно экспортировать. Для этого пишем:

extern "C"
{
	__declspec( dllexport ) int xml_http_decode(const char* inputStr, const char* outputParam);
}

Далее, подключаем библиотеку в LoadRunner и спокойно вызываем нашу функцию:

lr_load_dll("hplr.dll");	// Native DLL на C++.
xml_http_decode(
    "<span class=\"x11z\">&#1057;&#1086;&#1079;&#1076;&#1072;&#1090;&#1100; "
    "&#1079;&#1072;&#1103;&#1074;&#1082;&#1091;</span>",
    "p_decoded");
lr_output_message("%s", lr_eval_string("{p_decoded}")); // В параметре содержится раскодированное значение.

Если в LoadRunner'е функция lr_load_dll() падает с запутывающей ошибкой «не могу найти файл», то дело может быть не в самой подключаемой DLL, а в её зависимостях. Для успешного подключения библиотеки, собранной в режиме Debug, нужно добавить файлы msvcp110d.dll и msvcr110d.dll в System32 или в SysWOW64 для 32-битной и 64-битной ОС соответственно. Прочие зависимости можно исследовать при помощи тулзы Dependency Walker или подобных. Если библиотека собрана в режиме Release, то ничего дополнительного не нужно (уберите также дополнительные #include и зависимости в настройках компилятора).

Функции, написанные на C++, доступны в DLL сразу (не забыть сделать экспорт!). В LoadRunner'е можно писать
относительный путь к DLL, начиная с папки скрипта. Перезапускать VuGen или переоткрывать скрипт не надо.

Так мы можем обращаться к уже готовым классам .NET. Но писать свой код на C++ для работы с .NET, на мой взгляд, несколько неудобно, для этого есть более подходящие языки.
Примечание: на C++ тоже есть куча готовых библиотек (и работать они будут быстрее, кстати говоря), но во-первых, там нужны известные опыт и аккуратность, а во-вторых, это выходит за рамки данной статьи.

Обращение к кастомной библиотеке на C#


Предположим, мы написали более сложную логику на C# и упаковали в отдельную сборку (assembly). Как обратиться к ней из LR?
В принципе всё то же самое, что и в предыдущем случае, только ссылку нужно добавить на нашу сборку (через Solution или Browse) и классы нужно вызывать из нашей сборки.

DLL на C#

Почему-то LoadRunner может искать регулярные выражения только в ответах сервера, а как найти регулярное выражение в Си-строке или в значении параметра — непонятно (если кто знает, ткните меня носом). Для решения этой задачи можно написать такую функцию:

// C#-метод, ищущий в строке input регулярное выражение pattern и возвращающий
// группу номер nGroup вхождения с номером nMatch.
namespace HplrCs
{
    public static class HplrHelper
    {
        public static string GetRegexMatch(string input, string pattern, int nMatch, int nGroup)
        {
            try
            {
                var re = new Regex(pattern);
                var matches = re.Matches(input);
                if (matches.Count < nMatch + 1)
                    return String.Empty;
 
                var match = matches[nMatch];
                if (match.Success)
                {
                    if (match.Groups.Count < nGroup + 1)
                        return String.Empty;

                    return match.Groups[nGroup].Value;
                }
                else
                    return String.Empty;
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }
    }
}

Сборку с этим кодом мы должны положить в Global Assembly Cache (GAC). Для этого нужно воспользоваться утилитой gacutil.exe, которая входит в состав Windows SDK, а также ставится вместе с Visual Studio. Для .NET Framework 4.0/4.5 нужно пользоваться соответствующей версией gacutil.exe из 8-ой версии SDK, более ранние версии не смогут установить сборки 4.0/4.5.

gacutil.exe -i HplrCs.dll

Установка в GAC должна производиться под админскими правами. Убедиться, что сборка присутствует в кэше можно так:

gacutil.exe -l HplrCs

Чтобы заменить версию сборки в GAC нужно выполнить установку ещё раз, поверх предыдущей. Удалять старую версию не нужно, но важно убедиться, что установка прошла успешно.

Пишем native-обёртку

Опять же, аналогично первому варианту сделаем нативную библиотеку, которая будет связующим звеном между LoadRunner и сборкой .NET:

#include "..\LR include 11.50\lrun.h"
//...
extern "C"
{
	__declspec( dllexport ) int get_regex_match(
		const char* inputStr, const char* pattern,
		const char* outputParam, int nMatch, int nGroup
	);
}

int get_regex_match(const char* inputStr, const char* pattern, const char* outputParam, int nMatch, int nGroup)
{
	try
	{
		System::String^ _inputStr = gcnew System::String(inputStr);
		System::String^ _pattern = gcnew System::String(pattern);
		System::String^ result = HplrHelper::GetRegexMatch(_inputStr, _pattern, nMatch, nGroup);
		marshal_context^ context = gcnew marshal_context();
		lr_save_string(context->marshal_as<const char*>(result), outputParam);
	}
	catch(char* message)
	{
		lr_save_string(message, outputParam);
		return LR_FAIL;
	}
	catch(...)
	{
		lr_save_string("!!! Unknown exception raised !!!", outputParam);
		return LR_FAIL;
	}

	return LR_PASS;
}


Функции и константы LR


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

#include "..\LR include 11.50\lrun.h"


Чтобы это корректно собралось, нужно ещё добавить во вход линкера библиотеку lrun50.lib. Лежат они, соответственно, в

C:\Program Files (x86)\HP\LoadRunner\include
C:\Program Files (x86)\HP\LoadRunner\setup\dot_net\Vc9\VCWizards\LrCVuserDllLibrary\templates\1033


С их помощью вы можете вызывать функции API LoadRunner'а точно так же, как если бы вызывали их из скрипта.

Пример


Есть полностью готовый шаблон в виде проекта MS Visual Studio 2012. Его можно взять по ссылке.

Содержимое архива:

  • C++ — проект, демонстрирующий вызов кода как непосредственно в .NET Framework, так и в других сборках .NET.
  • C Sharp — пример библиотеки (сборки) на .NET.
  • LR include * и LR lib * — файлы из поставки LoadRunner, скопированы для возможности сборки проекта, если не установлен LoadRunner.
  • Output — собранные бинарники.
  • LR Ext lib usage example — пример скрипта LoadRunner, использующий вышеописанный подход.

При первом открытии проекта в Visual Studio вам предложат обновить версию используемой .NET Framework. Делать этого не следует, если вы не понимаете смысл происходящего. Обновление версии обяжет вас собирать .NET-сборку также с этой версией в качестве Target Framework (в свойствах проекта). Кстати, я не нашёл, где в интерфейсе можно для проекта на C++ изменить версию .NET Framework. Она показывается в свойствах C++-проекта но только на просмотр, изменить нельзя. Но это можно сделать, открыв файл .vcxproj в текстовом редакторе и найдя XML-элемент TargetFrameworkVersion. Поэтому, если вы всё-таки обновили проект, правьте TargetFrameworkVersion на нужную вам, или переходите на использование другой версии .NET Framework.

Производительность


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

Например, попробуем найти подстроку в строке длиной примерно 32KB (длиннее сделать затруднительно, т.к. это максимум, который позволяет LoadRunner выделять в стеке для локальных параметров). Я специально беру поиск подстроки, как одну из классических задач, потому как алгоритмы для неё, надо полагать, должны быть максимально оптимизированы.

	#define BUFF_SIZE 32700
	char buff[BUFF_SIZE];
	int i;
	
	lr_load_dll("hplr.dll");	// Native DLL.
	
	memset(buff, '-', BUFF_SIZE);
	buff[BUFF_SIZE - 1] = 0;
	strcpy(buff + BUFF_SIZE - 4, "+++");

	lr_start_transaction("Find substring, internal function");
	for (i = 0; i < 100000; i++)
		strstr(buff, "+++");
	lr_end_transaction("Find substring, internal function", LR_AUTO);
	
	lr_start_transaction("Find substrings, C# function");
	for (i = 0; i < 100000; i++)
		find_substr_net(buff, "+++");
	lr_end_transaction("Find substrings, C# function", LR_AUTO);


Код на .NET приводить не буду, за ним стоит обычный String.IndexOf(). Замеры нужно проводить только в режиме запуска в Controller, в VuGen выполнение происходит на два порядка медленнее.

Find substring, internal function:  1,505 
Find substring, C# function:  13,323


Ну что ж, ожидаемо. Основное время выполнения составляет собственно поиск подстроки, и тут нативный код даёт значительное преимущество. Также тормозит процесс преобразование const char* в System.String.
Но у нас есть, чем ответить. Заставим интерпретатор работать самостоятельно. Для этого напишем функцию, выполняющую простенькие действия в целочисленной арифметике:

int int_arithm_lr(int p)
{
	int i;
	int s = p;
	
	for (i = 0; i < 10000; i++)
	{
		s += i * (i % 2 * 2 - 1);
	}
	
	return s;
}


Код на C# точно такой же, только со словами public static.
Сравним скорость выполнения в обоих рантаймах:

	lr_start_transaction("Integer arithmetics, LR function");
	for (i = 0; i < 500; i++)
		int_arithm_lr(i);
	lr_end_transaction("Integer arithmetics, LR function", LR_AUTO);

	lr_start_transaction("Integer arithmetics, C# function");
	for (i = 0; i < 500; i++)
		int_arithm_net(i);
	lr_end_transaction("Integer arithmetics, C# function", LR_AUTO);

Integer arithmetics, LR function:  45,772
Integer arithmetics, C# function:   0,013


Разница в 3,5 тыс. раз.

Хотя использование подобных вычислений и не типично для сценариев нагрузочного тестирования, оно обнажает слабую сторону интерпретатора LR: низкую скорость работы собственного кода. Поэтому если вам нужно написать что-то замудрёное, то на языках .NET это не только будет удобнее реализовать, но и работать будет гораздо быстрее, чем Си-код в LR.

На этом, пожалуй, всё, спасибо за внимание!

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    0
    Спасибо, пригодится.
      0
      Для успешного подключения библиотеки, собранной в режиме Debug, нужно добавить файлы msvcp110d.dll и msvcr110d.dll в System32 или в SysWOW64 для 32-битной и 64-битной ОС соответственно.
      Оо. Это ж жесть какая… vcredist нужной версии ставить запрещают чтоли?)

      И вообще ИМХО лучше писать на чистом .net-языке, делать inproc-COM компонент из сборки и в чистом старом microsoft c++ использовать #import, чем городить c++ CLR длл-ку (mixed mode). Для того чтобы не регистрировать COM в системе, можно использовать registration-free com activation фичу в manifest приложения...
        0
        vcredist нужной версии ставить запрещают чтоли?)

        Бывает! Нагрузочные станции обычно под контролем заказчика, и однажды админ организации сказал, что ничего лишнего на них ставить не даст. Велел перечислить поимённо все файлы, которые потребуется добавить. Тогда я, не особо разбираясь, установил, что не хватает этих двух DLL. Позже уже понял, что и от них можно избавиться.

        ИМХО лучше писать на чистом .net-языке, делать inproc-COM компонент из сборки и в чистом старом microsoft c++ использовать #import

        Так сложилось, что мне был знаком мой способ, поэтому так и сделал. Если чуть подробнее напишите, как реализовать ваш способ, буду признателен. Кстати, а чем по-вашему это лучше?
          0
          Позже уже понял, что и от них можно избавиться.

          ну раз так, тогда вам надо всего лишь не линковать манифест в длл-ку, убрать из манифеста который появится publicKeyToken-ы CRT-шных либ (и если надо OpenMP), скопировать dll-ки CRT-шные рядом с вашей… ибо нефиг засирать систему конкретной версии длл-ок, т.к. это привести может к очень печальным последствиям. А «избавляться» от них не стоит, т.к. наличие двух CRT Runtime в одном процессе также может привести к очень странному поведению…

          Если чуть подробнее напишите, как реализовать ваш способ, буду признателен. Кстати, а чем по-вашему это лучше?

          Таки почитайте любую книжку по .net в части глав «работа с унаследованным кодом». Он лучше тем, что не надо изучать новый язык (C++\CLI).

      Only users with full accounts can post comments. Log in, please.