В прошлом квартале делали 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://localhost/traderoom/glengine.js?v=1485951440.84:1258:13)
at Object.abort (http://localhost/traderoom/glengine.js?v=1485951440.84:776417:44)
at _abort (http://localhost/traderoom/glengine.js?v=1485951440.84:9914:22)
at _free (http://localhost/traderoom/glengine.js?v=1485951440.84:232487:38)
at __ZN2F28ViewMain13setFullscreenEb (http://localhost/traderoom/glengine.js?v=1485951440.84:533436:2)
at Array.__ZNSt3__210__function6__funcIZN2F28ViewMainC1EvE4__13NS_9allocatorIS4_EEFbPNS2_9UIElementEEEclEOS8_ (http://localhost/traderoom/glengine.js?v=1485951440.84:658644:2)
at __ZNKSt3__28functionIFllEEclEl (http://localhost/traderoom/glengine.js?v=1485951440.84:673406:75)
at __ZNK2F26detail23multicast_function_baseIFbPNS_9UIElementEENS_24multicast_result_reducerIFbRKNSt3__26vectorIbNS6_9allocatorIbEEEEEXadL_ZNS_10atLeastOneESC_EEEEiLin1EEclERKS3_ (http://localhost/traderoom/glengine.js?v=1485951440.84:476310:12)
at __ZN2F29UIElement14processTouchUpERKNS_6vec2_tIfEE (http://localhost/traderoom/glengine.js?v=1485951440.84:471241:9)
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://localhost/traderoom/glengine.js?v=1485951440.84:1275:12
abort@http://localhost/traderoom/glengine.js?v=1485951440.84:776417:44
_abort@http://localhost/traderoom/glengine.js?v=1485951440.84:9914:7
_free@http://localhost/traderoom/glengine.js?v=1485951440.84:232487:38
__ZN2F28ViewMain13setFullscreenEb@http://localhost/traderoom/glengine.js?v=1485951440.84:533436:2
__ZNSt3__210__function6__funcIZN2F28ViewMainC1EvE4__13NS_9allocatorIS4_EEFbPNS2_9UIElementEEEclEOS8_@http://localhost/traderoom/glengine.js?v=1485951440.84:658644:2
__ZNKSt3__28functionIFllEEclEl@http://localhost/traderoom/glengine.js?v=1485951440.84:673406:9
__ZNK2F26detail23multicast_function_baseIFbPNS_9UIElementEENS_24multicast_result_reducerIFbRKNSt3__26vectorIbNS6_9allocatorIbEEEEEXadL_ZNS_10atLeastOneESC_EEEEiLin1EEclERKS3_@http://localhost/traderoom/glengine.js?v=1485951440.84:476310:12
__ZN2F29UIElement14processTouchUpERKNS_6vec2_tIfEE@http://localhost/traderoom/glengine.js?v=1485951440.84:471241:9
__ZN2F29UIElement17processTouchEventERNSt3__26vectorINS1_4pairIPS0_NS_6vec2_tIfEEEENS1_9allocatorIS7_EEEEjNS_15UI_TOUCH_ACTIONE@http://localhost/traderoom/glengine.js?v=1485951440.84:468018:35
__ZN2F29UIElement5touchEffNS_15UI_TOUCH_ACTIONEj@http://localhost/traderoom/glengine.js?v=1485951440.84:598797:8
__ZN2F213MVApplication5touchEffNS_15UI_TOUCH_ACTIONEj@http://localhost/traderoom/ at stackTrace (http://localhost/traderoom/glengine.js?v=1485951440.84:1275:12)
glengine.js?v=1485951440.84:360629:11
__ZN2F27UIInput7processEj@http://localhost/traderoom/glengine.js?v=1485951440.84:273450:6
__Z14on_mouse_eventiPK20EmscriptenMouseEventPv@http://localhost/traderoom/glengine.js?v=1485951440.84:446769:5
dynCall_iiii@http://localhost/traderoom/glengine.js?v=1485951440.84:767912:9
dynCall@http://localhost/traderoom/glengine.js?v=1485951440.84:501:14
handlerFunc@http://localhost/traderoom/glengine.js?v=1485951440.84:2526:30
jsEventHandler@http://localhost/traderoom/glengine.js?v=1485951440.84:2429:11
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://10.10.1.247:8080/main.js:1:17947)
at stackTrace (http://10.10.1.247:8080/main.js:1:18118)
at Object.abort (http://10.10.1.247:8080/main.js:12:6480)
at _abort (http://10.10.1.247:8080/main.js:1:37453)
at Eb (http://10.10.1.247:8080/main.js:5:22979)
at Xc (http://10.10.1.247:8080/main.js:5:53767)
at rc (http://10.10.1.247:8080/main.js:5:47782)
at Array.$c (http://10.10.1.247:8080/main.js:5:54228)
at Pc (http://10.10.1.247:8080/main.js:5:52663)
at Array.Wb (http://10.10.1.247:8080/main.js:5:40899)
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.
Автор: RPG18