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


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


Является продолжением предыдущих публикаций.


Обычно, поверхностный подход «в лоб» не является самым эффективным. 99% задач, связанных с анализом и обработкой данных начинаются с их импорта. В этом кратком очерке рассмотрим проблемы, возникающие на базовом этапе импорта данных с данными в фо��мате json, на примере типовой задачи «глубокого» анализа данных инсталляции Jira. json поддерживает сложную объектную модель, в отличии от csv, поэтому его парсинг в случае сложных структур может стать весьма тяжелым и долгим.


Постановка задачи


Дано:


  • jira внедрена и используется в процессе разработки ПО как система управления задачами и багтрекер.
  • Прямого доступа к БД jira нет, взаимодействие осуществляется через REST API (гальваническая развязка).
  • Забираемые json файлы имеют весьма сложную древовидную структуру с вложенными кортежами, требуемые для выгрузки всей истории действий. Для расчета же метрик требуется относительно небольшое количество параметров, разбросанных по разным уровням иерархии.

Пример штатного jira json на рисунке.



Требуется:


  • На основании данных jira необходимо найти узкие места и точки возможного роста эффективности процессов разработки и повышения качества получаемого продукта на основе анализа всех зарегистрированных действий.

Решение


Теоретически в R есть несколько различных пакетов по загрузке json и преобразованию их в data.frame. Наиболее удобным выглядит пакет jsonlite. Однако, прямое преобразование иерархии json в data.frame затруднительно в силу многоуровневого вложения и сильной параметризированности структуры записей. Выцепление конкретных параметров, связанных, например, с историей действий, может потребовать различных доп. проверок и циклов. Т.е. задачу можно решить, но для json файла размером в 32 задачи (включает все артефакты и всю историю по задачам) такой нелинейный разбор средствами jsonlite и tidyverse занимает ~10 секунд на ноутбуке средней производительности.


Сами по себе 10 секунд — это немного. Но ровно до момента, пока этих файлов не становится слишком много. Оценка на сэмпле разбора и загрузки подобным «прямым» методом ~4000 файлов (~4 Гб) дала 8-9 часов работы.


Такое большое количество файлов появилось неспроста. Во-первых, jira имеет временные ограничения на REST сессию, вытащить все балком невозможно. Во-вторых, будучи встроенным в продуктивный контур, ожидается ежедневная выгрузка данных по обновленным задачам. В-третих, и это будет упомянуто дальше, задача очень хороша для линейного масштабирования и думать о параллелизации надо с самого первого шага.


Даже 10-15 итераций на этапе анализа данных, выявления необходимого минимального набора параметров, обнаружения исключительных или ошибочных ситуаций и выработки алгоритмов постпроцессинга дают затраты в размере 2-3 недели (только счетное время).
Естественно, что подобная «производительность» не подходит для операционной аналитики, встроенной в продуктивный контур, и очень неффективно на этапе первичного анализа данных и разработки прототипа.


Пропуская все промежуточные детали, сразу перехожу к ответу. Вспоминаем Дональда Кнута, засучиваем рукава и начинаем заниматься микробенчмаркингом всех ключевых операций безжалостно срезая все, что только можно.


Результирующее решение сводится к следующим 10 стро��ками (это сутевой скелет, без последующего нефункционального обвеса):


library(tidyverse)
library(jsonlite)
library(readtext)

fnames <- fs::dir_ls(here::here("input_data"), glob = "*.txt")

ff <- function(fname){
  json_vec <- readtext(fname, text_field = "texts", encoding = "UTF-8") %>%  
    .$text %>%
    jqr::jq('[. | {issues: .issues}[] | .[]', 
            '{id: .id, key: .key, created: .fields.created, 
             type: .fields.issuetype.name, summary: .fields.summary, 
             descr: .fields.description}]')
  jsonlite::fromJSON(json_vec, flatten = TRUE)
}
tictoc::tic("Loading with jqr-jsonlite single-threaded technique")
issues_df <- fnames %>% 
  purrr::map(ff) %>%
  data.table::rbindlist(use.names = FALSE)
tictoc::toc()

system.time({fst::write_fst(issues_df, here::here("data", "issues.fst"))})

Что здесь интересного?


  1. Для ускорения процесса загрузки хорошо использовать специализированные профилированные пакеты, такие как readtext.
  2. Применение потокового парсера jq позволяет пе��евести все выцепление нужных атрибутов на функциональный язык, опустить его на CPP уровень и минимизировать ручные манипуляции над вложенными списками или списками в data.frame.
  3. Появился очень перспективный пакет bench для микробенчамарков. Он позволяет изучать не только время исполнения операций, но и манипуляции с памятью. Не секрет, что на копировании данных в памяти можно терять очень много.
  4. Для больших объемов данных и простой обработки часто приходится в финальном решении отказываться от tidyverse и переводить трудоемкие части на data.table, в частности здесь идет слияние таблиц средствами именно data.table. А также все преобразования на этапе постпроцессинга (которые включены в цикл посредством функции ff также сделаны средствами data.table с подходом изменения данных по ссылке, либо пакетами, построенными с применением Rcpp, например, пакет anytime для работы с датами и временем.
  5. Для сброса данных в файл и последующего чтения очень хорош пакет fst. В частности, всего доли секунды уходят на сохранение всей аналитики jira истории за 4 года, а данные сохраняются именно как типы данных R, что хорошо для последующего их переиспользования.

В ходе решения был рассмотрен подход с применением пакета rjson. Вариант jsonlite::fromJSON примерно в 2 раза медленнее, чем rjson = rjson::fromJSON(json_vec), но пришлось оставить именно его, потому как в даных бывают NULL значения, а на этапе преобразования NULL в NA в списках, выдаваемых rjson мы теряем преимущество, а код утяжеляется.


Заключение


  1. Подобный рефакторинг привел к изменению времени процессинга всех json файлов в однопоточном режиме на этом же ноутбуке с 8-9 часов до 10 минут.
  2. Добавление параллелизации задачи средствами foreach практически не утяжелило код (+ 5 строчек) но снизило время исполнения до 5 минут.
  3. Перевод решения на слабенький linux сервер (всего 4 ядра), но работающего на SSD в многопоточном режиме свело время исполнения до 40 секунд.
  4. Публикация на продуктивный контур (20 ядер, 3 ГГц, SSD) дало снижение времени исполнения до 6-8 секунд, что является более чем приемлемым для задач операционной аналитики.

Итого, оставаясь в рамках платформы R, простым рефакторингом кода удалось добитьcя уменьшения времени исполнения с ~9 часов до ~9 секунд.


Решения на R могут быть вполне быстрыми. Если у вас что-то не получается, попробуйте взглянуть на это под другим углом и с применением свежих методик.


Предыдущая публикация — «Аналитический паRашют для менеджера».