вторник, 16 апреля 2013 г.

Собственный плагин tsung

Давно хотел написать про tsung - замечательный инструмент для нагрузочного тестирования сервера. Замечательных сторон у tsung много, я назову три основные:
  1. Поддержка большого числа протоколов и типов серверов, среди них http, jabber, SOAP, LDAP, MySQL, Postgres, NFS и др.
  2. Очень гибкая настройка сценариев тестирования, основанная на XML
  3. Высокая скорость работы и возможность создания ботнетов из клиентов
Последнее качество обеспечивается тем, что tsung написан на Erlang.

Но что делать, если мы хотим протестировать наш собственный кастомный протокол, используя все вкусности, который предоставляет tsung? Ответ: написать плагин для tsung. Судя по всему раньше в сети была некая инструкция или статья о том, как это сделать, но сейчас она недоступна. Все, что я нашел, это небольшая статья с общими тезисами, расположенная здесь. Однако, ее оказалось достаточно для начала работы.

Итак, приступим к созданию тестового плагина tsung. Прежде всего определимся с протоколом. Пусть клиент посылает произвольную строку, а сервер вычисляет ее md5 хэш и возвращает исходную строку и хэш. Детали таковы - клиент и сервер сериализуют посылаемые данные во внутренние структуры с помощью boost::serialization. Для указания размера сериализованного архива перед его посылкой и тот и другой отправляют сначала 4 байта со значением размера. После ответа клиенту сервер тут же закрывает соединение.

Данные для сериализации поместим в файл data.h:
#ifndef DATA_H
#define DATA_H

#include <string>
#include <boost/serialization/string.hpp>

struct  Request
{
    std::string  value;

    template < typename  Archive >
    void  serialize( Archive &  ar, unsigned int  /* version */ )
    {
        ar & value;
    }
};

struct  Reply
{
    std::string  value;

    std::string  md5hex;

    template < typename  Archive >
    void  serialize( Archive &  ar, unsigned int  /* version */ )
    {
        ar & value;
        ar & md5hex;
    }
};

#endif
Тут все просто. Сервер построим на основе асинхронной модели boost::asio. Я опять-таки не буду вдаваться в подробности, поскольку это один из примеров, которые можно найти здесь, адаптированный под наш протокол. Для вычисления md5 хэша использована библиотека OpenSSL.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/cstdint.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <openssl/md5.h>
#include <arpa/inet.h>

#include "data.h"

using namespace  boost::asio;
using            boost::asio::ip::tcp;

namespace
{
    void  calc_md5hex( unsigned char  src[ MD5_DIGEST_LENGTH ],
                       char  dst[ MD5_DIGEST_LENGTH * 2 ] )
    {
        std::sprintf( dst, "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x"
                           "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
                      src[ 0 ],  src[ 1 ],  src[ 2 ],  src[ 3 ],
                      src[ 4 ],  src[ 5 ],  src[ 6 ],  src[ 7 ],
                      src[ 8 ],  src[ 9 ],  src[ 10 ], src[ 11 ],
                      src[ 12 ], src[ 13 ], src[ 14 ], src[ 15 ] );
    }
}

class  Session
{
    public:
        explicit Session( io_service &  io ) : socket_( io ), size_( 0 )
        {
        }

        tcp::socket &  socket( void )
        {
            return socket_;
        }

        void  start( void )
        {
            async_read( socket_, buffer( data_, sizeof( boost::uint32_t ) ),
                        boost::bind( &Session::handle_read_size, this,
                                     placeholders::error,
                                     placeholders::bytes_transferred ) );
        }

    private:
        void handle_read_size( const boost::system::error_code &  error,
                               size_t  bytes_transferred )
        {
            if ( ! error )
            {
                size_ =  *( ( boost::uint32_t * )data_ );

                async_read( socket_, buffer( data_, ntohl( size_ ) ),
                            boost::bind( &Session::handle_read, this,
                                         placeholders::error,
                                         placeholders::bytes_transferred ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_read( const boost::system::error_code &  error,
                          size_t  bytes_transferred )
        {
            if ( ! error )
            {
                std::istringstream  message_str;
                Request             request;

                message_str.str( std::string( data_, bytes_transferred ) );

                {
                    boost::archive::binary_iarchive  archive( message_str );
                    archive >> request;
                }

                std::ostringstream  reply_str;
                unsigned char       md5sum[ MD5_DIGEST_LENGTH ];
                char                md5hex[ MD5_DIGEST_LENGTH * 2 ];

                MD5( ( unsigned char * )request.value.c_str(),
                     request.value.size(), md5sum );

                calc_md5hex( md5sum, md5hex );

                Reply  reply = { request.value, md5hex };

                {
                    boost::archive::binary_oarchive  archive( reply_str );
                    archive << reply;
                }

                std::string  data( reply_str.str() );

                if ( data.size() > max_length )
                    throw std::runtime_error( "Too big message" );

                std::memcpy( data_, data.c_str(), data.size() );

                size_ = htonl( data.size() );

                async_write( socket_,
                             buffer( &size_, sizeof( boost::uint32_t ) ),
                             boost::bind( &Session::handle_write_size, this,
                                          boost::asio::placeholders::error,
                                          bytes_transferred ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_write_size( const boost::system::error_code &  error,
                                size_t  size )
        {
            if ( ! error )
            {
                async_write( socket_, buffer( data_, ntohl( size_ ) ),
                             boost::bind( &Session::handle_write, this,
                                          boost::asio::placeholders::error ) );
            }
            else
            {
                delete this;
            }
        }

        void handle_write( const boost::system::error_code &  error )
        {
            if ( ! error )
            {
                socket_.shutdown( tcp::socket::shutdown_both );
            }
            else
            {
                delete this;
            }
        }
    private:
        enum { max_length = 1024 };

    private:
        tcp::socket      socket_;

        char             data_[ max_length ];

        boost::uint32_t  size_;
};

class  Server
{
    public:
        Server( io_service &  io, short  port ) : io_( io ),
            acceptor_( io_, tcp::endpoint( tcp::v4(), port ) )
        {
            start_accept();
        }

    private:
        void  start_accept( void )
        {
            Session *  new_session = new Session( io_ );

            acceptor_.async_accept( new_session->socket(),
                                    boost::bind( &Server::handle_accept, this,
                                        new_session, placeholders::error ) );
        }

        void  handle_accept( Session *  new_session,
                                    const boost::system::error_code &  error )
        {
            if ( ! error )
            {
                new_session->start();
            }
            else
            {
                delete new_session;
            }

            start_accept();
        }

    private:
        io_service &   io_;

        tcp::acceptor  acceptor_;
};

int  main( int  argc, char *  argv[] )
{
    try
    {
        if ( argc != 2 )
        {
            std::cerr << "Usage: server <port>\n";

            return 1;
        }

        io_service  io_;

        Server  serv( io_, std::atoi( argv[ 1 ] ) );

        io_.run();
    }
    catch ( const std::exception &  e )
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}
Теперь напишем элементарный тестовый синхронный клиент (файл client.cc).
#include <cstring>
#include <sstream>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/cstdint.hpp>
#include <arpa/inet.h>

#include "data.h"

using namespace  boost::asio;
using            boost::asio::ip::tcp;

namespace
{
    enum { max_length = 1024 };
}

int  main( int  argc, char *  argv[] )
{
    try
    {
        if ( argc != 3 )
        {
            std::cerr << "Usage: client <host> <port>\n";

            return 1;
        }

        io_service  io_;

        tcp::resolver            resolver( io_ );
        tcp::resolver::query     query( tcp::v4(), argv[ 1 ], argv[ 2 ] );
        tcp::resolver::iterator  iterator( resolver.resolve( query ) );
        tcp::socket              sock( io_ );

        connect( sock, iterator );

        std::cout << "Enter message: ";

        char  message[ max_length ];

        std::cin.getline( message, max_length );

        std::ostringstream  message_str;
        Request             request = { message };

        {
            boost::archive::binary_oarchive  archive( message_str );
            archive << request;
        }

        std::string  data( message_str.str() );

        if ( data.size() > max_length )
            throw std::runtime_error( "Too big message" );

        boost::uint32_t  size( htonl( data.size() ) );

        write( sock, buffer( &size, sizeof( boost::uint32_t ) ) );
        write( sock, buffer( data.c_str(), data.size() ) );

        size_t    reply_length( read( sock, buffer( message,
                                                sizeof( boost::uint32_t ) ) ) );

        if ( reply_length != sizeof( boost::uint32_t ) )
            throw std::runtime_error( "Error while reading message size" );

        size = ntohl( *( ( boost::uint32_t * )message ) );

        reply_length = read( sock, buffer( message, size ) );

        if ( reply_length != size )
            throw std::runtime_error( "Error while reading message" );

        std::istringstream  reply_str;
        Reply               reply;

        reply_str.str( std::string( message, size ) );

        {
            boost::archive::binary_iarchive  archive( reply_str );
            archive >> reply;
        }

        std::cout << "Reply is: {'" << reply.value << "', '" << reply.md5hex <<
                "'}" << std::endl;
    }
    catch ( const std::exception &  e )
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}
Скомпилируем сервер и клиент и запустим их на выполнение.
g++ -Wall -g -o server server.cc -lboost_system-mt -lboost_serialization-mt \
        -lcrypto -lpthread
g++ -Wall -g -o client client.cc -lboost_system-mt -lboost_serialization-mt \
         -lpthread
./server 5555
(я разбил команды g++ на две строки, чтобы они уместились по ширине в колонку блога), в другом терминале запустим клиент:
./client localhost 5555
Enter message: Hello world!
Reply is: {'Hello world!', '86fb269d190d2c85f6e0468ceca42a20'}
Отлично, работает. Однако, для тестирования с помощью tsung тестовый клиент нам не нужен, поскольку tsung должен сам уметь отправлять клиентские запросы на сервер. Поскольку tsung написан на Erlang, а привязку к boost::serialization вряд ли получится просто реализовать на Erlang, то нам понадобится интерфейс erl_nif (NIF расшифровывается как Native Implemented Functions). Хороший пример его использования можно найти здесь. Поместим наш C++ интерфейс в файл erl_nif.cc:
#include <erl_nif.h>

#include <cstring>
#include <sstream>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/cstdint.hpp>
#include <arpa/inet.h>

#include "data.h"

namespace
{
    enum { max_length = 1024 };
}

extern "C"
{
    static ERL_NIF_TERM  request( ErlNifEnv *  env, int  argc,
                                  const ERL_NIF_TERM  argv[] )
    {
        if ( argc < 1 )
            return enif_make_badarg( env );

        char    message[ max_length + sizeof( boost::uint32_t ) ];
        size_t  len( 0 );

        if ( ( len = enif_get_string( env, argv[ 0 ],
                                      message + sizeof( boost::uint32_t ),
                                      max_length, ERL_NIF_LATIN1 ) ) <= 0 )
            return enif_make_badarg( env );

        std::ostringstream  message_str;
        Request             request = { message + sizeof( boost::uint32_t ) };

        {
            boost::archive::binary_oarchive  archive( message_str );
            archive << request;
        }

        std::string        data( message_str.str() );
        boost::uint32_t *  size( ( boost::uint32_t * )&message[ 0 ] );

        *size = htonl( data.size() );

        ErlNifBinary       result;

        enif_alloc_binary( sizeof( boost::uint32_t ) + ntohl( *size ), &result );

        std::memcpy( message + sizeof( boost::uint32_t ), data.c_str(),
                     data.size() );
        std::memcpy( result.data, message,
                     sizeof( boost::uint32_t ) + ntohl( *size ) );

        result.size = sizeof( boost::uint32_t ) + ntohl( *size );

        return enif_make_binary( env, &result );
    }

    static ERL_NIF_TERM  response( ErlNifEnv *  env, int  argc,
                                   const ERL_NIF_TERM  argv[] )
    {
        if ( argc < 1 )
            return enif_make_badarg( env );

        ERL_NIF_TERM  message( argv[ 0 ] );

        if ( ! enif_is_binary( env, message ) )
            return enif_make_badarg( env );

        ErlNifBinary    bin;

        enif_inspect_binary( env, message, &bin );

        if ( bin.size < sizeof( boost::uint32_t ) )
            return enif_make_badarg( env );

        //boost::uint32_t *  size( ( boost::uint32_t * )&bin.data );

        //if ( bin.size != sizeof( boost::uint32_t ) + ntohl( *size ) )
            //return enif_make_badarg( env );

        std::istringstream  reply_str;
        Reply               reply;

        reply_str.str( std::string(
                                ( char * )bin.data + sizeof( boost::uint32_t ),
                                bin.size - sizeof( boost::uint32_t ) ) );

        {
            boost::archive::binary_iarchive  archive( reply_str );
            archive >> reply;
        }

        ErlNifBinary  value;
        ErlNifBinary  md5hex;

        enif_alloc_binary( reply.value.size(), &value );
        std::memcpy( value.data, reply.value.c_str(), reply.value.size() );
        value.size = reply.value.size();

        enif_alloc_binary( reply.md5hex.size(), &md5hex );
        std::memcpy( md5hex.data, reply.md5hex.c_str(), reply.md5hex.size() );
        md5hex.size = reply.md5hex.size();

        return enif_make_tuple2( env, enif_make_binary( env, &value ),
                                 enif_make_binary( env, &md5hex ) );
    }

    static ErlNifFunc  ts_p_md5hex_funcs[] =
    {
        { "request", 1, request },
        { "response", 1, response }
    };
}

ERL_NIF_INIT( ts_p_md5hex_nif, ts_p_md5hex_funcs, NULL, NULL, NULL, NULL )
Здесь нужны небольшие пояснения, хотя сам код достаточно прозрачен. Во-первых, здесь мы определили две эрланговские функции request() и response() и задали имя будущего эрланговского модуля ts_p_md5hex_nif (в данном случае я использую p_, подразумевая слово plugin, хотя это увеличит в дальнейшем размеры имен в коде на Erlang). Эти определения реализуются в самой последней строке приведенного кода. Запрос в функции request() отличается от аналога из client.cc тем, что размер и сериализованный архив посылаются в одном запросе, но это не должно быть существенным различием. Функция request() возвращает буфер типа ErlNifBinary, готовый к отправке на сервер. Функция response() наоборот, парсит ответ сервера и возвращает кортеж (tuple), содержащий в себе исходную строку и вычисленный хэш. Вычисление размера архива (первые 4 байта в ответе сервера) в функции response() закомментировано, так как этот размер нам не нужен - сервер сам разрывает соединение после посылки ответа, и tsung это легко обнаружит самостоятельно.

Компилируем erl_nif.cc в разделяемую библиотеку ts_p_md5hex_nif.so (не забудьте предварительно установить Erlang, причем не старше версии R14B, поскольку erl_nif в более старых версиях не поддерживается).
g++ -Wall -fPIC -shared -o ts_p_md5hex_nif.so -I/usr/lib64/erlang/usr/include \
 erl_nif.cc -lboost_system-mt -lboost_serialization-mt -lpthread
Теперь напишем эрланговский интерфейс к erl_nif.cc, назовем его ts_p_md5hex_nif.erl.
-module(ts_p_md5hex_nif).
-author('garuda @ blogspot.com').

-export([request/1response/1]).

-on_load(init/0).

init() ->
    ok = erlang:load_nif("./ts_p_md5hex_nif"0).

request(_Value->
    exit(nif_library_not_loaded).

response(_Content->
    exit(nif_library_not_loaded).
Все, интерфейс erl_nif готов. Осталось написать собственно модуль для tsung. И здесь мы будем следовать шагам, упомянутым в статье, ссылку на которую я привел в самом начале. Прежде всего нужно скачать исходники tsung. Далее переходим в директорию с исходниками и добавляем определения для нашего модуля, который мы назовем p_md5hex в файл tsung-1.0.dtd. Я не хочу приводить здесь файл целиком, а diff получается очень широким, поэтому скажу лишь, что в список type из ATTLIST session и список new_type из ATTLIST change_type нужно добавить слово ts_p_md5hex, а в список ELEMENT request - слово p_md5hex. Кроме того нужно добавить описание элемента p_md5hex:
<!ELEMENT p_md5hex EMPTY >
<!ATTLIST p_md5hex
    value  CDATA   #REQUIRED
>
Этот элемент будет использоваться в XML сценариях в определении тэгов request, значение value - это строка, которую мы будем передавать в запросе. Элементы ts_p_md5hex будут использоваться в тэгах session и, если понадобится, change_type для определения протокола сессии.

Теперь нужно создать эрланговский хедер-файл ts_p_md5hex.hrl, в котором будет описан тип данных p_md5hex с единственным полем value. Файл должен находится в директории include/ относительно корня исходников tsung.
-vc('$Id$ ').
-author('garuda @ blogspot.com').

-recordp_md5hex, {
          value,
          bug %% see comment after member 'bug' in ts_raw.hrl
} ).
Как видим, элемент value оказался не единственным, второй элемент bug нужен из-за какого-то бага внутри tsung -  комментарий рядом с ним предлагает посмотреть описание бага в другом файле. Без этого поля наш плагин действительно не заработает.

Теперь создадим исходник для поддержки парсинга конфигурации ts_config_p_md5hex.erl в директории src/tsung_controller/.
-module(ts_config_p_md5hex).
-vc('$Id$ ').
-author('garuda @ blogspot.com').

-export([parse_config/2]).

-include("ts_profile.hrl").
-include("ts_config.hrl").
-include("ts_p_md5hex.hrl").

-include("xmerl.hrl").

%%----------------------------------------------------------------------
%% Function: parse_config/2
%% Purpose:  parse a request defined in the XML config file
%% Args:     Element, Config
%% Returns:  List
%%----------------------------------------------------------------------
%% Parsing other elements
parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) ->
    ts_config:parse(Element,Conf);

parse_config(Element = #xmlElement{name=p_md5hexattributes=Attrs},
             Config=#config{curid = Idsession_tab = Tab,
                            sessions = [CurS | _], dynvar=DynVar,
                            subst    = SubstFlagmatch=MatchRegExp}) ->
    Req = case ts_config:getAttr(string,Attrsdatasizeof
               [] ->
                   Value = ts_config:getAttr(stringAttrsvalue),
                   #p_md5hex{value=Value}
          end,
    ts_config:mark_prev_req(Id-1TabCurS),
    Msg=#ts_request{ack     = parse,
                    subst   = SubstFlag,
                    match   = MatchRegExp,
                    param   = Req},
    ets:insert(Tab,{{CurS#session.idId},Msg#ts_request{endpage=true,
                                                         dynvar_specs=DynVar}}),
    lists:foldlfun(A,B)->ts_config:parse(A,Bend,
                 Config#config{dynvar=[]},
                 Element#xmlElement.content);

%% Parsing other elements
parse_config(Element = #xmlElement{}, Conf = #config{}) ->
    ts_config:parse(Element,Conf);

%% Parsing non #xmlElement elements
parse_config(_Conf = #config{}) ->
    Conf.
Тут мне трудно что-либо прокомментировать за исключением того, что файл создан как комбинация исходников ts_config_raw.erl (как не очень большого по размеру) и ts_config_http.erl (как наиболее подходящего для нашего протокола). Единственное, что важно - элемент ack в конструкторе #ts_request должен быть равен parse.

Теперь собственно наш модуль ts_p_md5hex.erl, который следует разместить в директории src/tsung/.
-module(ts_p_md5hex).
-author('garuda @ blogspot.com').

-behavior(ts_plugin).

-include("ts_profile.hrl").
-include("ts_p_md5hex.hrl").

-export([init_dynparams/0,
         add_dynparams/4,
         get_message/2,
         session_defaults/0,
         dump/2,
         parse/2,
         parse_bidi/2,
         parse_config/2,
         decode_buffer/2,
         new_session/0,
         md5hex/1]).

%%----------------------------------------------------------------------
%% Function: session_defaults/0
%% Purpose:  default parameters for session (ack_type and persistent)
%% Returns:  {ok, true|false}
%%----------------------------------------------------------------------
session_defaults() ->
    {oktrue}.

%%----------------------------------------------------------------------
%% Function: decode_buffer/0
%% Purpose:  decode buffer for matching or dyn_variables
%% Returns:  decoded buffer
%%----------------------------------------------------------------------
decode_buffer(Buffer, #p_md5hex{}) ->
    Buffer.

%%----------------------------------------------------------------------
%% Function: new_session/0
%% Purpose:  initialize session information
%% Returns:  record or []
%%----------------------------------------------------------------------
new_session() ->
    #p_md5hex{}.

%%----------------------------------------------------------------------
%% Function: parse/2
%% Purpose:  Parse the given data and return a new state
%% Args:     Data (binary), State (record)
%% Returns:  NewState (record)
%%----------------------------------------------------------------------
parse(closedState->
    {State#state_rcv{ack_done = true}, [], true};

parse(DataState->
    {State#state_rcv{datasize = size(Data)}, [], false}.

parse_bidi(DataState->
    ts_plugin:parse_bidi(Data,State).

dump(A,B->
    ts_plugin:dump(A,B).

%%----------------------------------------------------------------------
%% Function: get_message/1
%% Purpose:  Build a message/request
%% Args:     #p_md5hex
%% Returns:  binary
%%----------------------------------------------------------------------
get_message(#p_md5hex{value=Value}, #state_rcv{session=S}) ->
    Packet=ts_p_md5hex_nif:request(Value),
    {PacketS}.

%%----------------------------------------------------------------------
%% Function: parse_config/2
%% Purpose:  parse tags in the XML config file related to the protocol
%% Returns:  List
%%----------------------------------------------------------------------
parse_config(ElementConf->
    ts_config_p_md5hex:parse_config(ElementConf).

%%----------------------------------------------------------------------
%% Function: add_dynparams/4
%% Purpose:  add dynamic parameters to build the message
%%----------------------------------------------------------------------
add_dynparams(_, [], Param_Host->
    Param;

add_dynparams(trueDynDataOldReq_Host->
    subst(OldReqDynData#dyndata.dynvars);

add_dynparams(_Subst_DynDataParam_Host->
    Param.

%%----------------------------------------------------------------------
%% Function: subst/2
%% Purpose:  Replace on the fly dynamic element of the request.
%%----------------------------------------------------------------------
subst(Req=#p_md5hex{value=Value}, DynData->
    Req#p_md5hex{value=ts_search:subst(ValueDynData)}.

init_dynparams() ->  #dyndata{}.

%%----------------------------------------------------------------------
%% Function: md5hex/1
%% Purpose:  Retrieve md5hex message from the request.
%%----------------------------------------------------------------------
md5hex(Data->
    element(2ts_p_md5hex_nif:response(Data)).
Здесь больше комментариев, чем кода. В модуле tsung должны быть определены все функции, перечисленные в секции export, за исключением последней md5hex(), которую мы добавили сами и планируем использовать для проверки правильности ответа сервера в XML сценарии. Реализация почти всех этих функций соответствует минимальному стандарту, который можно увидеть в файле ts_raw.erl в этой же директории. Исключение составляют две важные функции: get_message() и parse(). Функция get_message() создает запрос к серверу с помощью функции request() из нашего пакета ts_p_md5hex_nif, а функция parse() парсит ответ сервера. В нашем случае parse() просто добавляет размер полученных данных в поле datasize записи state_rcv (без этого размер принятых данных будет неверно рассчитан как нулевой), а при закрытии соединения сервером правильно завершает обработку  принятых данных на стороне tsung. Функция md5hex() парсит ответ сервера с помощью ts_p_md5hex_nif:response() и возвращает второй элемент кортежа (т.е. md5 хэш).

Перед сборкой tsung надо перенести файл ts_p_md5hex_nif.erl в директорию src/tsung/. После этого собираем и устанавливаем tsung стандартной цепочкой ./configure; make; make install.

После установки tsung настала пора протестировать наш плагин. Для этого создаем какую-нибудь директорию, например ~/tsung-runtime, копируем в нее библиотеку ts_p_md5hex_nif.so (в модуле ts_p_md5hex_nif.erl мы определили, что будем искать библиотеку в текущей рабочей директории) и переходим в нее. Создаем простой тестовый сценарий в файле scenario.xml.
<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/usr/local/share/tsung/tsung-1.0.dtd" [] >
<tsung loglevel="info">
  <clients>
    <client host="localhost" use_controller_vm="true"/>
  </clients>

  <servers>
    <server host="192.168.0.2" port="5555" type="tcp"></server>
  </servers>

  <load>
    <arrivalphase phase="1" duration="60" unit="second">
      <users arrivalrate="10" unit="second"></users>
    </arrivalphase>
    <arrivalphase phase="2" duration="60" unit="second">
      <users arrivalrate="20" unit="second"></users>
    </arrivalphase>
  </load>

  <sessions>
    <session name="md5hex_1" probability="50" type="ts_p_md5hex">
      <request>
        <match do='continue' when='match' apply_to_content='ts_p_md5hex:md5hex'>
          827ccb0eea8a706c4c34a16891f84e7b
        </match>
        <p_md5hex value="12345"/>
      </request>
    </session>
    <session name="md5hex_2" probability="50" type="ts_p_md5hex">
      <request>
        <match do='continue' when='match' apply_to_content='ts_p_md5hex:md5hex'>
          1e01ba3e07ac48cbdab2d3284d1dd0fa
        </match>
        <p_md5hex value="67890"/>
      </request>
    </session>
  </sessions>
</tsung>
На домашней странице tsung имеется прекрасный раздел с документацией, в котором можно изучить все тонкости написания сценариев. В данном сценарии используется единственный клиент, запускаемый на локальном хосте. Предполагается, что сервер запущен на хосте с адресом 192.168.0.2 (просто localhost здесь не сработает) и прослушивает порт 5555. Определены две стадии тестирования длительностью по 60 секунд, в первой стадии каждую секунду посылаются 10 новых запросов на сервер, во второй - 20. Запросы имеют тип ts_p_md5hex и равновероятно отправляют сериализованные значения 12345 или 67890. Хэш в ответе проверяется с помощью вызова функции ts_p_md5hex:md5hex() внутри тэгов match путем сопоставления с заранее посчитанным значением.

Запускаем tsung:
tsung -f scenario.xml -l ~/tmp/log/ start
и переходим в директорию с результатами (tsung выводит ее на экран). Внутри этой директории запускаем скрипт /usr/lib/tsung/bin/tsung_stats.pl, который генерирует статистический отчет, содержащий помимо сухих цифр некоторое количество замечательных картинок. Этот скрипт можно запускать неоднократно во время работы сценария или после того, как тестирование завершилось. Вот несколько картинок с результатами теста:

Длительность запроса и установки соединения:

Скорость генерации запросов:

Сетевой трафик:
Запросы, соответствующие условиям тэга match:

На втором, третьем и четвертом рисунках видно наличие двух фаз теста по 60 секунд каждая. На первом рисунке видно, что один запрос в среднем занимал чуть более одной миллисекунды, а скорость установки соединения равнялась половине миллисекунды, при этом увеличение нагрузки во второй фазе не отразилось на этих цифрах. На втором рисунке видно, что в первой фазе в среднем генерировалось 10 запросов в секунду, а во второй - 20, что соответствует сценарию теста. На третьем рисунке показан объем исходящего и входящего трафиков, соотношения кривых соответствуют нашему протоколу взаимодействия (ответ сервера примерно в два раза больше клиентского запроса). Четвертый рисунок показывает, что все ответы сервера были с правильно рассчитанным md5 хэшем.

Исходники здесь.

четверг, 21 марта 2013 г.

C++11: ссылки на rvalue и семантика переноса

Это небольшой пример для иллюстрации возможностей семантики переноса (move semantics) в C++11. Для компиляции программы я использовал компилятор gcc версии 4.7.2. В программе создается вектор, содержащий элементы некоторого класса A, который дальше заполняется большим количеством объектов этого класса. Внутри класс A содержит член data типа указателя на int, который инициализируется массивом в конструкторе класса с помощью оператора new[] и освобождается в деструкторе с помощью оператора delete[]. Вот исходный код программы:
#include <iostream>
#include <vector>

#ifdef __GXX_EXPERIMENTAL_CXX0X__
    #define NOEXCEPT      noexcept
    #define STDMOVE( x )  std::move( ( x ) )
#else
    #define NOEXCEPT
    #define STDMOVE( x )  ( x )
#endif

#ifdef DEBUG
    #define VSIZE 1
    #define DPRINT( x ) std::cout << ( x ) << std::endl;
#else
    #define VSIZE 10000
    #define DPRINT( x )
#endif

namespace
{
    const size_t  d_size( 1024 );
    const size_t  v_size( VSIZE );
}

class  A
{
    public:
        A() : data( new int[ d_size ] )
        {
            DPRINT( "Constructor" )
        }

        ~A() NOEXCEPT
        {
            DPRINT( "Destructor" )
            delete[] data;
        }

        A( const A &  a ) : data( new int[ d_size ] )
        {
            DPRINT( "Const reference" )

            /* May not assign 'a->data' to 'this->data' as 'a' may go out of
             * scope and be destructed, therefore 'a.data' will be deleted */
            /* this->data = a.data; */

            /* May not assign 'a.data' as it is const reference  */
            /* a.data = NULL; */

            /* May only copy data! */
            for ( int  i( 0 ); i < d_size; ++)
            {
                this->data[ i ] = a.data[ i ];
            }
        }

#ifdef __GXX_EXPERIMENTAL_CXX0X__
        A( A &&  a ) noexcept
        {
            DPRINT( "Rvalue reference" )

            /* May assign 'a.data' to 'this->data' because 'a' is
             * rvalue reference */
            this->data = a.data;

            /* 'a.data' must be uninitialized to avoid double deletion! */
            a.data = nullptr;
        }
#endif

    private:
        int *  data;
};


int  main( void )
{
    std::vector< A >  v( v_size );

    for ( int  i( 0 ); i < v_size; ++)
    {
        A  a;
        v.push_back( STDMOVE( a ) );
    }
}
В самом начале находятся определения макросов NOEXCEPT и STDMOVE: они нужны для того, чтобы программу можно было скомпилировать как с поддержкой расширений C++11, так и без нее. В случае компиляции без поддержки C++11 эти макросы не расширяются, а при поддержке - превращаются соответственно в noexcept и std::move. Далее идут определения макросов VSIZE и DPRINT: если собирать программу с флагом -DDEBUG, то DPRINT позволит выводить важные сообщения на экран терминала, а VSIZE, который мы будем использовать для определения размера вектора v, будет равен 1 - это позволит не захламлять экран большим числом сообщений, но при этом даст возможность убедиться, что вызываются правильные конструкторы.

Внутри класса A определены последовательно: конструктор A(), деструктор ~A(), копирующий конструктор, конструктор с семантикой переноса (завернутый в макрос __GXX_EXPERIMENTAL_CXX0X__) и член data. В функции main() создается вектор v, дальше в цикле локальный объект a типа A добавляется в конец вектора v.

Проверим, что все работает правильно. Для этого сначала соберем нашу программу без поддержки C++11 и семантики переноса, но с флагом -DDEBUG:
g++ -g -O2 -DDEBUG -o test main.cpp
Запустим собранную программу:
$ ./test 
Constructor
Const reference
Destructor
Constructor
Const reference
Const reference
Destructor
Destructor
Destructor
Destructor
Отлично, строки Const reference свидетельствуют о том, что были вызваны конструкторы копирования. Теперь соберем программу с поддержкой C++11:
g++ -g -O2 -std=c++11 -DDEBUG -o test main.cpp
Запускаем:
$ ./test 
Constructor
Constructor
Rvalue reference
Rvalue reference
Destructor
Destructor
Destructor
Destructor
Теперь были вызваны конструкторы с семантикой переноса! Кстати, обратим внимание на очевидные различия при инициализации вектора без поддержки и с поддержкой C++11. Теперь сравним производительность: компилируем оба варианта без флага -DDEBUG (теперь в вектор будет записано 10000 элементов типа A) и смотрим, насколько конструктор с семантикой переноса быстрее конструктора копирования.
  1. g++ -g -O2 -o test main.cpp
    $ time ./test 
    
    real 0m0.070s
    user 0m0.036s
    sys 0m0.030s
    
  2. g++ -g -O2 -std=c++11 -o test main.cpp
    $ time ./test 
    
    real 0m0.047s
    user 0m0.007s
    sys 0m0.038s
    
Можно запустить несколько раз для большей статистики - разница окажется более чем в полтора раза! Главное различие в семантике конструкторов: конструктор копирования обязан выделить память под data, а затем скопировать туда элементы data из копируемого объекта; в то же время конструктор с семантикой переноса ничего этого делать не обязан: вместо этого он может просто присвоить своему члену data значение data "копируемого" объекта, а это простой указатель! При этом, однако, он обязан присвоить data исходного объекта нулевое значение, иначе оператор delete[] в деструкторе класса A попытается дважды удалить выделенную память. Различия между конструкторами подчеркнуты в комментариях внутри исходного кода.

Теперь о некоторых второстепенных деталях. Во-первых, если в push_back() передавать не именованный объект класса A, а созданный на месте объект A(), то можно обойтись без std::move(): стандарт не позволяет рассматривать именованные объекты как ссылки на rvalue, в то время как неименованные таковыми являются. Задачей std::move() как раз и является превращение именованного объекта в ссылку на rvalue. Во-вторых, если вы уберете слово noexcept из деструктора или конструктора с семантикой переноса, то вы получите вызов конструктора копирования в дебрях алгоритма копирования элементов в std::vector. Попробуйте убрать это слово хотя бы из одной из этих функций, скомпилируйте программу с флагом -DDEBUG, и вы увидите всё сами.

Очень подробно вопросы, связанные с семантикой переноса, рассматриваются в этой статье. В частности, там показано, почему так важны std::move() и noexcept.

воскресенье, 10 февраля 2013 г.

vim: автоматическое переключение раскладки клавиатуры в режиме ввода

Это не автоматическое переключение раскладки клавиатуры при входе и выходе из режима ввода, о котором я писал здесь и здесь. Это гораздо более интересная, хотя и не настолько важная, как предыдущая, функция. Представьте, что вы редактируете некий файл, и вам нужно переключать раскладку клавиатуры в зависимости от положения курсора, не выходя из режима ввода. Я не взял эту задачу с потолка. Мне понадобилась такая функция в процессе изучения немецкого языка: просто мне намного проще запоминать слова, если я их самостоятельно выписываю в таблицу с оригинальным словом или фразой в одной колонке и его/ее переводом в другой колонке. Например:
| Wort         | Übersetzung |
|--------------|-------------|
| der Mond     | луна        |
| humpeln      | хромать     |
| stark        | сильный     |
| die Bewegung | движение    |
| bewegen      | двигать     |
Представьте, сколько нужно переключений раскладки, чтобы заполнить хотя бы десять рядов, это притом, что всегда присутствует английская раскладка, которую придется каждый раз старательно пропускать. В этой статье я покажу, как сделать, чтобы раскладка клавиатуры переключалась сама, в зависимости от положения курсора в первой или второй колонке таблицы. Разумеется, это всего лишь частный пример, и предложенный алгоритм можно использовать и в иных целях, когда синтаксический формат редактируемого файла заранее известен.

Определять положение курсора будем по синтаксическому идентификатору под курсором. Соответственно, нам понадобится определить синтаксис для словарной таблицы и написать синтаксический скрипт для этого синтаксиса. Я не стал определять собственный тип файла (filetype) для словаря, а просто решил, что его типом будет vimwiki. Vimwiki очень хорошо работает с таблицами, в частности имеет отличную поддержку для навигации внутри таблицы при нажатии на клавишу табуляции, автоматического добавления строк и форматирования столбцов. Однако vimwiki не дает того, что нам нужно: синтаксического различения первого и второго столбцов, а это значит, что мы не сможем детектировать положение курсора в таблице словаря и, в виде бонуса, не сможем подсвечивать столбцы разными цветами. Что же делать? Будем рассматривать наш словарь, как синтаксическую разновидность vimwiki. Это значит, что filetype словаря будет равен vimwiki, а синтаксические особенности мы опишем в файле $HOME/.vim/after/syntax/vimwiki.vim. Для формального различения словаря и других файлов vimwiki положим, что словарь будет иметь файловое расширение .mdict. Поэтому в .vimrc добавляем строку
autocmd BufNewFile,BufRead *.mdict setlocal filetype=vimwiki | EnableXkbSwitch
(она также присутствует ниже в листинге для .vimrc). Файл $HOME/.vim/after/syntax/vimwiki.vim выглядит так:
if match(bufname('%')'\.mdict$') == -1
    finish
endif

syntax match mdictOriginal '\%(^\s*|\)\@<=[^|]\+\ze|[^-]'
            \ containedin=VimwikiTableRow contained

syntax match mdictTranslated '\%([^-]|\)\@<=[^|]\+\ze|$'
            \ containedin=VimwikiTableRow contained

hi mdictOriginalHl term=standout ctermfg=63 guifg='#d7d7ff'
autocmd ColorScheme * hi mdictOriginalHl term=standout
            \ ctermfg=63 guifg='#d7d7ff'

hi mdictTranslatedHl term=standout ctermfg=28 guifg='#d7ffd7'
autocmd ColorScheme * hi mdictTranslatedHl term=standout
            \ ctermfg=28 guifg='#d7ffd7'

hi link mdictOriginal   mdictOriginalHl
hi link mdictTranslated mdictTranslatedHl
Содержимому первого столбца таблицы соответствует регулярное выражение mdictOriginal, второго столбца - mdictTranslated. В первых строках проверяется, что файл имеет расширение .mdict, и если это не так, то скрипт сразу заканчивает работу.

А теперь код, который следует поместить в .vimrc сразу за кодом для xkb_switch (см. здесь).
" automatic keyboard layout switching in a simple dictionary in insert mode
" (filetype is a subclass of vimwiki and must have extension '.mdict';
" there must exist syntax support in dedicated script
" $HOME/.vim/after/syntax/vimwiki.vim to define matches for original and
" translated colums 'mdictOriginal' and 'mdictTranslated')
" FIXME: currently layout will not switch correctly from within select modes
fun<SID>dict_check_lang(force)
    if !executable(g:XkbSwitchLib)
        return
    endif

    let cur_synid  = synIDattr(synID(line(".")col(".")1)"name")

    if !exists('b:saved_cur_synid')
        let b:saved_cur_synid = cur_synid
    endif

    if cur_synid != b:saved_cur_synid || a:force
        let cur_layout = libcall(g:XkbSwitchLib, 'Xkb_Switch_getXkbLayout',
                    \ '')
        if b:saved_cur_synid == 'mdictOriginal'
            let b:xkb_layout_dict_orig = cur_layout
        endif
        if b:saved_cur_synid == 'mdictTranslated'
            let b:xkb_layout_dict_trans = cur_layout
        endif
        if cur_synid == 'mdictOriginal'
            if exists('b:xkb_layout_dict_orig')
                call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout',
                        \ b:xkb_layout_dict_orig)
            else
                let b:xkb_layout_dict_orig = cur_layout
            endif
        endif
        if cur_synid == 'mdictTranslated'
            if exists('b:xkb_layout_dict_trans')
                call libcall(g:XkbSwitchLib, 'Xkb_Switch_setXkbLayout',
                        \ b:xkb_layout_dict_trans)
            else
                let b:xkb_layout_dict_trans = cur_layout
            endif
        endif
        let b:saved_cur_synid = cur_synid
    endif
endfun

autocmd BufNewFile,BufRead *.mdict setlocal filetype=vimwiki | EnableXkbSwitch
autocmd BufNewFile         *.mdict VimwikiTable 2 2
autocmd BufNewFile         *.mdict exe "normal dd" | startinsert
autocmd InsertEnter        *.mdict call <SID>dict_check_lang(1)
autocmd CursorMovedI       *.mdict call <SID>dict_check_lang(0)
Теперь при открытии нового файла с расширением .mdict будет автоматически создаваться таблица размерностью 2x2. В верхней сроке нужно поместить названия столбцов. При заполнении первой строки таблицы необходимо вручную переключать требуемые раскладки клавиатуры, в дальнейшем они будут переключаться автоматически. При выходе из режима ввода будет автоматически включена английская раскладка (так как мы включили EnableXkbSwitch), при входе в режим ввода будет включена раскладка в соответствии со столбцом, в котором находится курсор. При открытии уже существующего файла словаря vim не знает о соответствии столбцов и раскладок, поэтому его придется научить снова, дважды вручную переключив раскладку в разных столбцах.

Из недостатков/недоделок нужно упомянуть неправильную работу при переключении в режим ввода из режима выделения текста (Select mode), а также отсутствие немецких дубликатов маппингов режима ввода - их можно добавить по аналогии с русскими (см. здесь) - однако, поскольку немецкая раскладка мало отличается от английской, они, скорее всего, не понадобятся вообще.