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

Для получения получения стека вызовов была написана вспомогательная утилита:


webstackwalker
#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.