В прошлом квартале делали 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.


