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

в 7:17, , рубрики: load testing, нагрузочное тестирование, тестирование

Хотя 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.50lrun.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">Создать "
    "заявку</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.50lrun.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.50lrun.h"

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

C:Program Files (x86)HPLoadRunnerinclude
C:Program Files (x86)HPLoadRunnersetupdot_netVc9VCWizardsLrCVuserDllLibrarytemplates1033

С их помощью вы можете вызывать функции 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.

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

Автор: greatvovan

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js