[Из песочницы] Как использовать .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(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 ( »Создать» »заявку », «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(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.

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

© Habrahabr.ru