В прошлом квартале делали MVP сервиса по обработке крешей. Аналог Socorro от Mozilla, но с учетом своих требований. Код сервиса будет выкладываться на GitHub по мере рефакторинга. Утилиты, о которых пойдет речь в этой статье, доступны тут.
У нас были следующие требования:
- получение отчета с Windows, Mac OS X, GNU/Linux;
- получение отчета о падения с веба(собираем через emscripten);
- сбор данных об оборудовании(CPU, GPU, Memory);
- группировка падений по версии, платформе, пользователю, причине;
- приложение ведет логи, нужно вместе с отчетом хранить и лог.
Содержание:
- Breakpad: файлы символов и отчеты о падениях;
- Emscripten: параметры компиляции, файлы символов, обработка ошибок;
- UI.
Breakpad
В составе breakpad есть утилита извлекающая файлы символов из elf/pdb. Вот описание формата файла. Это текстовый файл, но нас интересует первая строка имеет формат MODULE operatingsystem architecture id name, у нас она выглядит так:
MODULE windows x86 9E8FC13F1B3F448B89FF7C940AC054A21 IQ Option.pdb MODULE Linux x86_64 4FC3EB040E16C7C75481BC5AA03EC8F50 IQOption MODULE mac x86_64 B25BF49C9270383E8DE34560730689030 IQOption
Далее эти файлы следует расположить в особом порядке: base_dir/name/id/name.sym, выглядит это так:
base_dir/IQ Option/9E8FC13F1B3F448B89FF7C940AC054A21/IQ Option.sym base_dir/IQOption/4FC3EB040E16C7C75481BC5AA03EC8F50/IQOption.sym base_dir/IQOption/B25BF49C9270383E8DE34560730689030/IQOption.sym
Для получения отчета о падения можно воспользоваться утилитой minidump_stackwalk из поставки breakpad:
$ minidump_stackwalk path_to_crash base_dir
Данная утилита может выводить как в человеко читаемым виде так и в machine-readable формате.
Но это не очень удобно. В Mozilla Socorro входит утилита stackwalker которая выдает json(пример на crash-stats.mozilla.com)
Emscripten
Ловить падения можно через глобальный обработчик window.onerror. В зависимости от браузера, сообщения будут отличаться:
Uncaught abort() at Error at jsStackTrace (http://...) at Object.abort (http://...) at _abort (http://...) at _free (...) at __ZN2F28ViewMain13setFullscreenEb (...) at Array.__ZNSt3__210__function6__funcIZN2F28ViewMainC1EvE4__13NS_9allocatorIS4_EEFbPNS2_9UIElementEEEclEOS8_ (http://...) at __ZNKSt3__28functionIFllEEclEl (http://...) at __ZNK2F26detail23multicast_function_baseIFbPNS_9UIElementEENS_24multicast_result_reducerIFbRKNSt3__26vectorIbNS6_9allocatorIbEEEEEXadL_ZNS_10atLeastOneESC_EEEEiLin1EEclERKS3_ (http://...) at __ZN2F29UIElement14processTouchUpERKNS_6vec2_tIfEE (http://...) If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.
uncaught exception: abort() at jsStackTrace@http://localhost/traderoom/glengine.js?v=1485951440.84:1258:13 stackTrace@http://... abort@http://... _abort@http://... _free@http://... __ZN2F28ViewMain13setFullscreenEb@http://... __ZNSt3__210__function6__funcIZN2F28ViewMainC1EvE4__13NS_9allocatorIS4_EEFbPNS2_9UIElementEEEclEOS8_@http://... __ZNKSt3__28functionIFllEEclEl@http://... __ZNK2F26detail23multicast_function_baseIFbPNS_9UIElementEENS_24multicast_result_reducerIFbRKNSt3__26vectorIbNS6_9allocatorIbEEEEEXadL_ZNS_10atLeastOneESC_EEEEiLin1EEclERKS3_@http://... __ZN2F29UIElement14processTouchUpERKNS_6vec2_tIfEE@http://... __ZN2F29UIElement17processTouchEventERNSt3__26vectorINS1_4pairIPS0_NS_6vec2_tIfEEEENS1_9allocatorIS7_EEEEjNS_15UI_TOUCH_ACTIONE@http://... __ZN2F29UIElement5touchEffNS_15UI_TOUCH_ACTIONEj@http://... __ZN2F213MVApplication5touchEffNS_15UI_TOUCH_ACTIONEj@http://... at stackTrace (http://... glengine.js?v=1485951440.84:360629:11 __ZN2F27UIInput7processEj@http://... __Z14on_mouse_eventiPK20EmscriptenMouseEventPv@http://... dynCall_iiii@http://... dynCall@http://... handlerFunc@http://... jsEventHandler@http://... If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.
Такое сообщение создаются если при компиляции использовать ключ -g. На нашем проекте размер выходного asm.js кода раза в 3 больше. Поэтому у нас используется --emit-symbol-map.
На выходе получаем файл с символами в простом формате key:value:
$cc:__ZNSt3__210__function6__funcIZN2F28ViewMain17animateLeftPannelEbE4__36NS_9allocatorIS4_EEFvvEEclEv f8d:__ZNKSt3__210__function6__funcIZN2F218MVMessageQueueImpl4sendINS2_26EventSocialProfileReceivedEJiEEEvDpRKT0_EUlvE_NS_9allocatorISA_EEFvvEE7__cloneEPNS0_6__baseISD_EE Z1:__ZN2F211recognizers24UIPinchGestureRecognizer6updateEPNS_9UIElementERKNS_6vec2_tIfEEj
а сообщения теперь имеют вид:
Uncaught abort() at Error at jsStackTrace (http://...) at stackTrace (http://...) at Object.abort (http://...) at _abort (http://...) at Eb (http://...) at Xc (http://...) at rc (http://...) at Array.$c (http://...) at Pc (http://...) at Array.Wb (http://...) If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.
Для получения получения стека вызовов была написана вспомогательная утилита:
#include <map> #include <set> #include <list> #include <regex> #include <fstream> #include <iostream> #include <cxxabi.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> namespace { struct Deleter { void operator()(char *data) const { free((void *) data); } }; using CharPtr = std::unique_ptr<char, Deleter>; } std::string demangle(const std::string &mangledName) { int status = 0; int shift = 0; if (mangledName[1] == '_') { shift = 1; } CharPtr realname(abi::__cxa_demangle(mangledName.data() + shift, 0, 0, &status)); if (status == 0) { return std::string(realname.get()); } else { if (mangledName[0] == '_') { const auto str = mangledName.substr(1, mangledName.size() - 1); int status = 0; CharPtr realname(abi::__cxa_demangle(str.data(), 0, 0, &status)); if (status == 0) { return std::string(realname.get()); } return mangledName; } return mangledName; } } void printUsage() { std::cout << "webstackwalker crash_dump symbol_file" << std::endl; } std::map<std::string, std::string> SYMBOL_MAP; void readSymbols(const std::string &path); void flushUnParseLine(const std::string &line); int main(int argc, char **argv) { if (argc < 2) { printUsage(); return 1; } readSymbols(std::string(argv[2])); const std::string inputFile(argv[1]); std::ifstream input(inputFile); const std::regex re("^(?:\\s{4}at\\s){0,1}(?:Array\\.){0,1}([\\w\\d\\$]+)(?: \\(|@).+\\){0,1}"); rapidjson::StringBuffer buffer; rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); writer.StartArray(); const std::set<std::string> skip = {"jsStackTrace", "stackTrace", "abort"}; while (!input.eof()) { std::smatch match; std::string line; std::getline(input, line); if (std::regex_search(line, match, re)) { if (skip.count(match[1])) { continue; } auto iter = SYMBOL_MAP.find(match[1]); std::string function; if (iter != SYMBOL_MAP.cend()) { function = demangle(iter->second); } else { function = demangle(match[1]); } writer.String(function.c_str()); } } writer.EndArray(); std::cout << buffer.GetString() << std::endl; return 0; } void readSymbols(const std::string &path) { std::ifstream input(path); if (!input.is_open()) { std::cerr << "Can't open symbols file: " << path << std::endl; exit(2); } const std::regex re("^([\\d\\w$]+):([\\d\\w]+)$"); while (!input.eof()) { std::smatch match; std::string line; std::getline(input, line); if (std::regex_search(line, match, re)) { SYMBOL_MAP[match[1]] = match[2]; } } }
Утилита использует demangle, для преобразования:
_ZN2F211recognizers24UIPinchGestureRecognizer6updateEPNS_9UIElementERKNS_6vec2_tIfEEj
в
F2::recognizers::UIPinchGestureRecognizer::update(F2::UIElement*, F2::vec2_t<float> const&, unsigned int)
UI
Отчеты о падении складываем в Elasticsearch, поэтому на первое время используем Kibana, как средство визуализации и анализа содержимого эластика.
При использовании кибаны получаем из коробки:
- дашборды с автоматическим обновлением;
- визуализации:
- грифики и диаграммы;
- таблицы.
Дашборды группируют креши по платформе, билду, сигнатуре. Система фильтров позволяет узнать:
- в каких билдах встречается данный баг;
- на каких платформах воспроизводится.
Примененные фильтры можно перенести на вкладку discover, где можно посмотреть под��обности падения. Как оказалось кибана имеет модульную структуру, что позволяет расширять её возможности. Был написан простой плагин, добавляющий рендер отчета, что намного удобней стандартного Table и JSon.


