вторник, 20 марта 2018 г.

[prog.c++] Как лучше добавить детализацию активности агентов в SObjectizer?

В SObjectizer уже довольно давно существует такая штука, как work thread activity tracking. Но этот механизм дает только самую общую информацию о том, сколько событий было обработано на конкретном диспетчере (точнее, на конкретной нити диспетчера), сколько времени это заняло, сколько раз приходилось ждать поступления событий и сколько времени это ожидание заняло. Механизм самый базовый. Но уже, при необходимости, помогающий посмотреть на то, куда и как тратится время в программе.

Возможно, пришло время расширить этот механизм и добавить в SObjectizer сбор более детальной информации о том, куда тратится время. На данный момент я вижу два разных варианта. Оба кратко описаны под катом. Если кому-то интересно, то прошу заглянуть под кат и высказать свое мнение о том, какой из вариантов был бы предпочтительнее для вас. Или, может быть, предложить свой вариант.

Важный момент. Интенсивность работы над этой задачей будет зависеть от того, какой интерес эта задача вызовет у публики. Если никто не заинтересуется, то мы ничего и не будем делать до тех пор, пока нам самим что-нибудь такое не потребуется. Ну а если, напротив, тема вызовет интерес, то мы выделим ресурсы на ее решение. Так что если вы ничего не скажете, то ничего и не получите, все просто ;)

Итак, есть два, как мне кажется, принципиально разных подхода.

Первый подход состоит в том, чтобы просто расширить существующий механизм work thread activity tracking дополнительной информацией. Предположительно, в сообщение work_thread_activity будет добавлено еще одно поле. Что-то вроде:

struct event_activity_key_t;
   {
      // Тип сообщения, которое обрабатывал агент.
      const std::type_index m_msg_type;
      // Некий идентификатор агента, который обрабатывал сообщение.
      // Актуальность этого идентификатора не зависит о того, жив ли
      // еще агент или же он уже прекратил свое существование.
      const so_5::stats::agent_identity_t m_agent;
      ...
   };

// Тип словаря, содержащего статистику работы агентов.
using activity_map_t =
      std::map<
            // В качестве ключа пара из msg_type/agent_id.
            event_activity_key_t,
            // В качестве значения -- статистика по этому агенту
            // и этому типу сообщения.
            so_5::stats::activity_stats_t>;

struct work_thread_activity : public message_t
   {
      ... // Все поля, которые уже есть.

      // Детальная статистика по событиям, которые были обработаны
      // агентами на этой рабочей нити.
      activity_map_t m_agents_activity;
   };

Этот вариант сразу же напрашивается когда речь заходит о расширении функциональности work thread activity tracking. Но здесь есть свои неудобства:

Во-первых, существующее поле work_thread_activity::m_stats дает статистику "с накапливающимся итогом". Т.е. в ней содержатся данные от момента начала работы рабочей нити. Соответственно, такие значения, как m_stats::m_working_stats::m_count или m_stats::m_waiting_stats::m_total_time будут монотонно возрастать. Тогда как содержимое m_agents_activity будет обновляться после каждого такта выдачи статистики. Например, если статистика выдается раз в минуту, то m_agents_activity будет содержать только информацию о событиях за прошедшую минуту. А информация о том, что было две или три минуты назад, будет потеряна.

Причем здесь все не так просто. Например, запускается обработчик события, который работает 15 минут подряд. Тогда как статистика будет выдаваться раз в минуту. В этом случае статистика за первую минуту будет содержать информацию о предыдущих обработчиках, а так же об этом обработчике (но длительность работы этого обработчика будет меньше минуты, что естественно, так как он только начал работать). В статистике за вторую минуту будет информация только об этом обработчике. Но будет указано, что он работает больше минуты. В статистике за третью минуту опять же будет информация только об этом обработчике, но время его работы будет уже больше 2-х минут. И т.д. И только в статистике за 16-ю минуту появится информация о других обработчиках, которые запустились после этого длительного обработчика. При этом для длительного обработчика будет указано, что он работал 15 минут.

В общем, получается, что интерпретировать информацию из work_thread_activity станет сложнее, т.к. нужно будет понимать, какая часть статистики как считается и какие в ней могут быть "всплески".

Во-вторых, этот подход дает возможность только получать статистику по событиям. Не более того. А вот, скажем, точный трейс работы, т.е. перечень событий и их длительностей, а так же их порядок, восстановить уже не получится. Хотя, в каких-то случаях именно эта информация и будет нужна.

Поэтому-то рассматривается еще и второй вариант.


Второй вариант предполагает наличие задаваемого пользователем монитора активности. Интерфейс которого может выглядеть следующим образом:

class activity_monitor_t
   {
   public:
      virtual ~activity_monitor_t() = default;

      // Этот метод будет вызван перед тем, как агенту будет передана
      // заявка для обработки.
      virtual void
      on_evt_processing_started(
         // Идентификатор рабочей нити, на которой происходит запуск обработчика.
         so_5::current_thread_id_t thread_id,
         // Описание заявки, которая будет обработана.
         const so_5::execution_demand_t & demand ) noexcept = 0;

      // Этот методо будет вызван после того, как агенту будет передана
      // заявка для обработки.
      virtual void
      on_evt_processing_finished(
         // Идентификатор рабочей нити, на которой запускался обработчик.
         so_5::current_thread_id_t thread_id,
         // Описание заявки, которая была обработана.
         const so_5::execution_demand_t & demand ) noexcept = 0;
   };

Пользователь реализует свой класс с интерфейсом activity_monitor_t, создает объект этого класса и передает этот объект в параметрах SObjectizer Environment:

class my_tracing_activity_monitor_t : public so_5::activity_monitor_t {
   ...
};

int main() {
   my_tracing_activity_monitor_t monitor;
   so_5::launch( [](so_5::environment_t & env) {...},
      [&](so_5::environment_params_t & params) {
         // Добавляем монитор в SOEnv для трассировки обработчиков событий.
         params.add_activity_monitor(monitor);
         ...
      });
}

В этом случае SObjectizer будет вызывать методы monitor.on_evt_processing_started() и monitor.on_evt_processing_finished() перед и после вызова любого события у любого агента.

Думаю, что можно будет сделать так, чтобы пользователь мог более гибко управлять тем, какие агенты попадают в монитор активности, а какие нет. Например, можно назначить монитор активности на весь SObjectizer Environment, но при этом запретить использование монитора для какого-то отдельного диспетчера. Или, напротив, не назначать монитор на весь SObjectizer, а назначить монитор только для определенных диспетчеров (причем для каждого диспетчера можно назначить свой собственный монитор).

В принципе, мониторы можно объединять в цепочки. Т.е. задать несколько мониторов. Тогда методы on_evt_processing_start/finish будут последовательно вызываться у каждого из них. Например, пользователь сможет создать два монитора: один собирает статистику, второй контролирует длительность работы обработчиков событий и генерирует тревоги, если какой-то обработчик работает слишком долго. Оба эти монитора можно будет задать для SObjectizer Environment:

class my_stats_collector_monitor_t : public so_5::activity_monitor_t {
   ...
};
class my_duration_checker_monitor_t : public so_5::activity_monitor_t {
   ...
};

int main() {
   my_stats_collector_monitor_t stats_collector;
   my_duration_checker_monitor_t duration_checker;
   so_5::launch( [](so_5::environment_t & env) {...},
      [&](so_5::environment_params_t & params) {
         // Добавляем мониторы в SOEnv.
         params.add_activity_monitor(stats_collector);
         params.add_activity_monitor(duration_checker);
         ...
      });
}

Этот вариант так же предполагает, что пользователь сам сможет создавать те мониторы, которые ему нужны. Ну и, плюс к тому, можно будет сделать какой-то готовый монитор, собирающий статистику. Этот монитор сможет удовлетворить пользователей в каких-то простых случаях.

Второй вариант гибче первого, т.к. позволяет делать разные вещи. Например, снимать трейс работы агентов. Или же собирать точные данные о длительностях работы обработчиков событий. Тут уж насколько у кого фантазии хватит.

Плюс к тому, он ортогонален существующему механизму work thread activity tracking. Т.е. можно не включать work thread activity tracking вообще, но добавить мониторы и получать информацию о работе агентов. Или можно включить work thread activity tracking одновременно с использованием мониторов активности (тогда в work_thread_activity начнут отражаться и накладные расходы мониторов, т.к. для рабочей нити это будет выглядеть как часть обработчика событий).

Но зато во втором варианте не будет информации о том, на каком диспетчере работает агент. В монитор попадает только Thread ID, но не информация о диспетчере. Можно ли передать в монитор еще и какое-то описание диспетчера -- это отдельный вопрос, требующий проработки.


Вот такие два варианта пока видны. Оба, в принципе, выглядят вполне себе реализуемыми. Хотя изрядное количество труда придется вложить и туда, и туда. И чтобы не тратить время и силы на то, что может никому не понадобится, прошу заинтересовашихся высказываться: нужна ли вам подобная функциональность в SO-5? Если нужна, то в каком виде? В том числе можно предложить и свой вариант.

PS. По поводу работы над параллельными состояниями. Там пока все довольно туго. Сложно вписать параллельные состояния в существующий в SO-5.5 механизм состояний, подписок и поиска обработчиков. Хорошей идеи о том, как это сделать не поломав совместимость и не увеличив накладные расходы на объекты so_5::state_t, пока не появилось. Поэтому приоритет этой задачи понизился.

Комментариев нет: