Программы статического анализа кода — это необычный класс программ-верификаторов, и в течение некоторого времени я не был убежден в необходимости их использования при разработке для фейсбука. Я не терплю стилистические правила на своей шее, и ложные предупреждения об ошибках могут испортить всю задачу. Впрочем, в них есть и хорошее: если проверяющий механически ищет проблемы, которые традиционно не контролируются компилятором, то это должно почти всегда улучшать качество кода, как только проблема будет исправлена.
Флинт, программа Фейсбука для статического анализа, выдает ошибки анализа, которые автоматически появляются в нашей системе ревью (phabricator) рядом с каждым предложенным изменением кода, уведомляя программиста, что что-то может пойти не так. Flint стал важной частью работы, которую мы делаем в Фейсбуке, и я очень рад открыть его исходники, чтобы каждый мог проверить, что же мы делаем, и попробовать это для себя.
Но почему бы не использовать существующие анализаторы кода?
Написание анализатора кода для С++ — задача не для слабонервных, ведь С++ весьма сложен в разборе. Но тем не менее, в настоящее время есть целая куча анализаторов со множеством фич, некоторые даже с открытым кодом. Так что вопрос, почему мы решили написать свой собственный, а не использовать существующие, вполне логичен.
Этот Прямой Эфир Хочется Пересматривать Вечно! Топ 10
Когда мы начинали это проект, все опробованные нами программы были слишком медленными и не поддерживали большинства нововведений С++11, которые уже использовались нами в разработке. Clang, который сегодня будет логичной отправной точкой для анализа кода на С++, предлагал слишком мало поддержки в то время. И даже сейчас он не может компилировать часть нашего кода на C++.
И самое главное, правила анализаторов кода очень сильно зависят от характера организаций, которые их используют. Мы представляли, что мы ищем, и выяснили, что какой бы анализатор мы не выбрали, нам придется долго дорабатывать его. Так что мы решил разработать собственный анализатор кода.
Токены, комментарии и язык D, ох!
Основываясь на принципе «простейшее решение, которое будет работать», флинт является токен-ориентированным, что противопоставляется построению дерева разбора кода. Анализатор загружает входной файл, конвертирует в массив токенов и по-разному анализирует это массив. Каждый токен сохраняет предшествующий комментарий (если он есть), так что комментирующая информация сохранятся. Некоторые из наших правил требуют использования комментариев в специальном стиле, вы увидите это ниже.
Целью такого дизайна было реализовать быстрый токенайзер, добавить пару простых правил и выпустить его на свободу от фейсбука с надеждой, что люди будут добавлять анализатору интересные правила. Решение добавить парсинг выкинули, но наши инженеры добавили около двух дюжин правил, которые мы проверим за минуту.
Флинт написан на D, и это первый опенсорсный проект на этом языке от Фейсбука. На самом деле, наша первая версия была написана на С++; перевод на D начинался как эксперимент. По результатам измерений и историй, в которых мы участвовали, перевод на D был победой по всем направлениям: версия на D оказалась значительно меньше, значительно быстрее собиралась, значительно быстрее запускалась и была более легкой для добавления изменений.
Партнерская программа casino Flint
Перевод флинта с С++ на D
Для переезда с С++ на D я решил сделать полумеханический перевод, т.е использовать ближайший код на D c той же семантикой, что и С++ код.
При реализации на С++ я выяснил, что использование генератора лексем приносило больше проблем, чем пользы, так что я просто написал быстрый, выделенный лексер с использованием макросов. В D нет макросов, что делает перевод один в один невозможным. Но D имеет кое-что получше — полную интерпретацию в течение компиляции, которая сочетается с возможностью самоанализа, генерации и компиляции сгенерированного кода налету.
Я написал функцию на 58 строк, которая генерирует развернутое дерево совпадений. Для С++ код разворачивается в 965 строк, которые затем подаются обратно в компилятор с примесью выражений.
В таком матчинге есть как минимум один плюс — он независим от языка, который мы разбиваем на лексемы; такой подход делает возможным засунуть его в библиотеку и оставить только язык-специфичные части (такие как парсинг чисел или комментариев). При этом подходе становится легче создавать и использовать оптимальные лексеры для любого языка без необходимости использования стороннего генератора. Реализация такого решения заняла часть первого дня, после чего у меня был работающий лексер, последующие дни были потрачены на портирование анализатора и его проверку на самом себе.
После перевода цикл редактирования/сборки/тестирования flint’а стал гораздо приятнее. Сборка flint на D примерно в пять раз быстрее сборки на С++, что оказалось очень важным в итерационной разработке. Скорость запуска стала немного лучше, между 5 и 25% — в зависимости от файла, выигрыш был больше для больших файлов.
Быстрая сборка дала интересный побочный эффект для меня. Разница между обычным временем сборки в C++ и D не удивительна, но это был первый раз, когда я работал над похожими проектами параллельно, переключаясь между ними, что дало мне возможность почувствовать разницу.
С++ может производить быстрый код, но сборка С++ сопровождается целой кучей лишних телодвижений и множеством шума. Инициация сборки неизбежно сопровождается некоторой церемониальностью и пышностью («назад, парни, возможна отдача!») и в течение дня я бы аккуратно следил за сборкой, чтобы максимизировать пользу от потраченного времени. Цикл сборки проектов на D происходит обескураживающе быстро — даже как-то обидно иногда. Бойкость, с которой компилятор D проходит сквозь код, сначала удивила меня, я даже заподозрил ошибку — что мой код вообще не был скомпилирован. На самом-то деле, новый язык просто способен лучше пробегать через код на значительно больших скоростях.
Проверки, совершаемые флинтом
- Черный список последовательностей токенов (checkBlacklistedSequences). Некоторые последовательности могут быть просто запрещены в организации. В Фейсбуке мы считаем «volatile» запрещенным, но не всегда: мы разрешаем последовательность «asm volatile», которая означает немного другое.
- Черный список идентификаторов (checkBlacklistedIdentifiers). Некоторые идентификаторы могут быть запрещены в организации. В фейсбуке мы исключаем печально известную функцию языка C — strtok. Для нее есть безопасные альтернативы, так что нет ни одной причины для использования strtok.
- Резервированные идентификаторы (checkDefinedNames). И в С, и в С++ есть часто забываемое правило именования, согласно которому все идентификаторы, начинающиеся с подчеркивания и последующей заглавной буквы, а также все идентификаторы, содержащиеся два последовательных подчеркивания, зарезервированы для служебных нужд. (Конечно в нашем коде есть исключения от этого правила, такие как _GNU_SOURCE или _XOPEN_SOURCE, поэтому флинт использует и whitelist при проверке зарезервированных идентификаторов).
- Идиома включения гардов (checkIncludeGuard). Большинство заголовочных файлов должно быть защищено явным образом (путем использования директивы #pragma once или #ifndef макроса), так что мы добавили правило для проверки этой защиты.
- Порядок аргументов в memset (checkMemset). Многие из нас иногда писали memset( и рассказывали об этом жуткие истории, пугая племянниц на Хэллоуин. Соответствующее диагностическое правило флинта позволит легко избежать этой пагубной ошибки.
- Сомнительные инклуды (#include) — checkQuestionableIncludes. Многие организации используют несколько библиотек, которые хранятся только для обеспечения обратной совместимости и никогда не используются в новом коде. Значит, такие хедеры не должны включаться — хорошая работенка для анализатора. Одной из проблем, которую мы нашли в Фейсбуке, было то, что некоторые хедеры после препроцессора становились слишком большими, чтобы их можно было включить в другие — это приводило к слишком большому времени компиляции. Нам пришлось научить флинт решать и эту задачу.
- Выделение «. -inl.h» файлов (checkInlHeaderInclusions). Популярным способом организации встраиваемого и тяжелого шаблонного кода является соответствующее разделение инлайновых и шаблонных артифактов — например, «Widget.h» превращается в «Widget-inl.h». Последний файл не должен быть включен где-то, кроме «Widget.h», и специальное правило следит за этим.
- Инициализация переменной самой себя (checkInitializeFromItself). Мы выяснили, что люди пишут конструкторы в стиле
class X < .. int a_; X(const X>X(int a) : a_(a_) <> >;
class C < . // Плохие паттерны, флинт выдаст диагностическое сообщение C(int a) C(const C); // Хорошие паттерны, флинт такие любит explicit C(int a); /* implicit */ C(char* a); C(int a, double b); >;
Throw спецификации (checkThrowSpecification). Throw спецификации устарели и должны удаляться из кода. Флинт позволяет сделать еще одно улучшение: классы, наследующиеся от std::exception, должны добавлять throw() в деструкторе и реализовывать метод what().
Проверка против бросания исключения у указателей, инициализированных с помощью new (checkThrowsHeapException). Это предотвращает антипаттерн throw new T.
Конфликтующие директивы using namespace (checkUsingNamespaceDirectives). Некоторые пространства имен используют одинаковые идентификаторы — например, boost и STL: оба они определяют shared_ptr. Мы испытали проблему несовместимости, так что мы добавили правило анализатора, которое предотвращает использование конфликтующих пространств имен одновременно.
Пространства имен в заголовочных файлах (checkUsingDirectives). «using namespace» никогда не должно встречаться внутри хедера. Но это может происходить внутри inline-функции в заголовочном файле. Фейсбук считает, что пространство имен «facebook» может быть введено только на верхнем уровне. Вам надо заменить название нэймспейса на какое-нибудь свое.
Неправильное использование библиотек (checkFollyDetail). Это распространенная практика разместить библиотекозависимый код в нэймспейс типа «detail». Иногда такой код не может быть инкапсулирован от клиентской части из-за прозрачности шаблонов в C++. Флинт выдает предупреждения против использования таких нэймспейсов, названных в стиле «folly::detail».
Передача «дешевых» типов по ссылке (checkFollyStringPieceByValue). Некоторые типы, такие как итераторы или пара итераторов, являются маленькими и дешевыми в копировании. Поэтому их легче передавать по значению вместо передачи по константной ссылке. Пример — StringPiece в Folly, который занимает два слова и имеет семантику простого копирования.
Нет protected-наследованию (checkProtectedInheritance). Защищенное наследование — странная вещь, и оставлена лишь для полноты картины, но никак не для практического применения.
Нет неявному преобразования операторов (checkImplicitCast). Неявное преобразование операторов опасно так же, как неявное преобразование конструкторов:
class C < // Плохие паттерны, флинт выдаст диагностическое сообщение operator string(); operator bool(); // Хорошие паттерны, флинт такие любит /* implicit */ operator string(); explicit operator bool(); >;
Плохой и устаревший NULL должен быть везде заменен на nullptr (checkUpcaseNull).
Проверка, что std::exception всегда отнаследован публично (checkExceptionInheritance). Обратите внимание:
class MyException : std::exception < . >;
Автор хотел определить собственный класс исключения, но забыл указать наследование публичным. В соответствии с правилами языка, наследование по умолчанию будет приватным. В конечном счете, интересный эффект проявится в том, что код, который должен будет перехватывать все стандартные и пользовательские исключения — catch (const std::exceptionмимолетные” rvalue-конструкции, которые иногда бесполезны (checkMutexHolderHasName). Рассмотрим: mutex m_lock; . lock_guard(m_lock); Вне зависимости от цели, этот код не делает ничего полезного: это просто вызов конструктора lock_guard. Это создаст rvalue, которое будет жить в промежутке между закрывающей скобкой и точкой с запятой.
В подтверждение Закона Кармака («Все, что синтаксически правильно и принимается компилятором, рано или поздно попадет в ваш код») вы получаете жизненную необходимость в правилах синтаксического анализа. В идеале, оно должно перехватить все неиспользуемые переменные. На текущий момент мы определяем только несколько распространенных подозрительных мест.
Раскрытие исходников flint
Под капотом: сборка и открытие исходников flint
2014-05-28 в 8:42, admin , рубрики: c++
Программы статического анализа кода — это необычный класс программ-верификаторов, и в течение некоторого времени я не был убежден в необходимости их использования при разработке для фейсбука. Я не терплю стилистические правила на своей шее, и ложные предупреждения об ошибках могут испортить всю задачу. Впрочем, в них есть и хорошее: если проверяющий механически ищет проблемы, которые традиционно не контролируются компилятором, то это должно почти всегда улучшать качество кода, как только проблема будет исправлена.
Флинт, программа Фейсбука для статического анализа, выдает ошибки анализа, которые автоматически появляются в нашей системе ревью (phabricator) рядом с каждым предложенным изменением кода, уведомляя программиста, что что-то может пойти не так. Flint стал важной частью работы, которую мы делаем в Фейсбуке, и я очень рад открыть его исходники, чтобы каждый мог проверить, что же мы делаем, и попробовать это для себя.
Но почему бы не использовать существующие анализаторы кода?
Написание анализатора кода для С++ — задача не для слабонервных, ведь С++ весьма сложен в разборе. Но тем не менее, в настоящее время есть целая куча анализаторов со множеством фич, некоторые даже с открытым кодом. Так что вопрос, почему мы решили написать свой собственный, а не использовать существующие, вполне логичен.
Когда мы начинали это проект, все опробованные нами программы были слишком медленными и не поддерживали большинства нововведений С++11, которые уже использовались нами в разработке. Clang, который сегодня будет логичной отправной точкой для анализа кода на С++, предлагал слишком мало поддержки в то время. И даже сейчас он не может компилировать часть нашего кода на C++.
И самое главное, правила анализаторов кода очень сильно зависят от характера организаций, которые их используют. Мы представляли, что мы ищем, и выяснили, что какой бы анализатор мы не выбрали, нам придется долго дорабатывать его. Так что мы решил разработать собственный анализатор кода.
Токены, комментарии и язык D, ох!
Основываясь на принципе «простейшее решение, которое будет работать», флинт является токен-ориентированным, что противопоставляется построению дерева разбора кода. Анализатор загружает входной файл, конвертирует в массив токенов и по-разному анализирует это массив. Каждый токен сохраняет предшествующий комментарий (если он есть), так что комментирующая информация сохранятся. Некоторые из наших правил требуют использования комментариев в специальном стиле, вы увидите это ниже.
Целью такого дизайна было реализовать быстрый токенайзер, добавить пару простых правил и выпустить его на свободу от фейсбука с надеждой, что люди будут добавлять анализатору интересные правила. Решение добавить парсинг выкинули, но наши инженеры добавили около двух дюжин правил, которые мы проверим за минуту.
Флинт написан на D, и это первый опенсорсный проект на этом языке от Фейсбука. На самом деле, наша первая версия была написана на С++; перевод на D начинался как эксперимент. По результатам измерений и историй, в которых мы участвовали, перевод на D был победой по всем направлениям: версия на D оказалась значительно меньше, значительно быстрее собиралась, значительно быстрее запускалась и была более легкой для добавления изменений.
Перевод флинта с С++ на D
Для переезда с С++ на D я решил сделать полумеханический перевод, т.е использовать ближайший код на D c той же семантикой, что и С++ код.
При реализации на С++ я выяснил, что использование генератора лексем приносило больше проблем, чем пользы, так что я просто написал быстрый, выделенный лексер с использованием макросов. В D нет макросов, что делает перевод один в один невозможным. Но D имеет кое-что получше — полную интерпретацию в течение компиляции, которая сочетается с возможностью самоанализа, генерации и компиляции сгенерированного кода налету.
Я написал функцию на 58 строк, которая генерирует развернутое дерево совпадений. Для С++ код разворачивается в 965 строк, которые затем подаются обратно в компилятор с примесью выражений.
В таком матчинге есть как минимум один плюс — он независим от языка, который мы разбиваем на лексемы; такой подход делает возможным засунуть его в библиотеку и оставить только язык-специфичные части (такие как парсинг чисел или комментариев). При этом подходе становится легче создавать и использовать оптимальные лексеры для любого языка без необходимости использования стороннего генератора. Реализация такого решения заняла часть первого дня, после чего у меня был работающий лексер, последующие дни были потрачены на портирование анализатора и его проверку на самом себе.
После перевода цикл редактирования/сборки/тестирования flint’а стал гораздо приятнее. Сборка flint на D примерно в пять раз быстрее сборки на С++, что оказалось очень важным в итерационной разработке. Скорость запуска стала немного лучше, между 5 и 25% — в зависимости от файла, выигрыш был больше для больших файлов.
Быстрая сборка дала интересный побочный эффект для меня. Разница между обычным временем сборки в C++ и D не удивительна, но это был первый раз, когда я работал над похожими проектами параллельно, переключаясь между ними, что дало мне возможность почувствовать разницу.
С++ может производить быстрый код, но сборка С++ сопровождается целой кучей лишних телодвижений и множеством шума. Инициация сборки неизбежно сопровождается некоторой церемониальностью и пышностью («назад, парни, возможна отдача!») и в течение дня я бы аккуратно следил за сборкой, чтобы максимизировать пользу от потраченного времени. Цикл сборки проектов на D происходит обескураживающе быстро — даже как-то обидно иногда. Бойкость, с которой компилятор D проходит сквозь код, сначала удивила меня, я даже заподозрил ошибку — что мой код вообще не был скомпилирован. На самом-то деле, новый язык просто способен лучше пробегать через код на значительно больших скоростях.
Проверки, совершаемые флинтом
Каждая проверка соответствует настоящему названию функции в кодовой базе, так что вы можете просто найти её, чтобы увидеть, как эта проверка работает.
- Черный список последовательностей токенов (checkBlacklistedSequences). Некоторые последовательности могут быть просто запрещены в организации. В Фейсбуке мы считаем «volatile» запрещенным, но не всегда: мы разрешаем последовательность «asm volatile», которая означает немного другое.
- Черный список идентификаторов (checkBlacklistedIdentifiers). Некоторые идентификаторы могут быть запрещены в организации. В фейсбуке мы исключаем печально известную функцию языка C — strtok. Для нее есть безопасные альтернативы, так что нет ни одной причины для использования strtok.
- Резервированные идентификаторы (checkDefinedNames). И в С, и в С++ есть часто забываемое правило именования, согласно которому все идентификаторы, начинающиеся с подчеркивания и последующей заглавной буквы, а также все идентификаторы, содержащиеся два последовательных подчеркивания, зарезервированы для служебных нужд. (Конечно в нашем коде есть исключения от этого правила, такие как _GNU_SOURCE или _XOPEN_SOURCE, поэтому флинт использует и whitelist при проверке зарезервированных идентификаторов).
- Идиома включения гардов (checkIncludeGuard). Большинство заголовочных файлов должно быть защищено явным образом (путем использования директивы #pragma once или #ifndef макроса), так что мы добавили правило для проверки этой защиты.
- Порядок аргументов в memset (checkMemset). Многие из нас иногда писали memset( и рассказывали об этом жуткие истории, пугая племянниц на Хэллоуин. Соответствующее диагностическое правило флинта позволит легко избежать этой пагубной ошибки.
- Сомнительные инклуды (#include) — checkQuestionableIncludes. Многие организации используют несколько библиотек, которые хранятся только для обеспечения обратной совместимости и никогда не используются в новом коде. Значит, такие хидеры не должны включаться — хорошая работенка для анализатора. Одной из проблем, которую мы нашли в Фейсбуке, было то, что некоторые хидеры после препроцессора становились слишком большими, чтобы их можно было включить в другие — это приводило к слишком большому времени компиляции. Нам пришлось научить флинт решать и эту задачу.
- Выделение «. -inl.h» файлов (checkInlHeaderInclusions). Популярным способом организации встраиваемого и тяжелого шаблонного кода является соответствующее разделение инлайновых и шаблонных артифактов — например, «Widget.h» превращается в «Widget-inl.h». Последний файл не должен быть включен где-то, кроме «Widget.h», и специальное правило следит за этим.
- Инициализация переменной самой себя (checkInitializeFromItself). Мы выяснили, что люди пишут конструкторы в стиле
class X < .. int a_; X(const X>X(int a) : a_(a_) <> >;
class C < . // Плохие паттерны, флинт выдаст диагностическое сообщение C(int a) C(const C); // Хорошие паттерны, флинт такие любит explicit C(int a); /* implicit */ C(char* a); C(int a, double b); >;
class C < // Плохие паттерны, флинт выдаст диагностическое сообщение operator string(); operator bool(); // Хорошие паттерны, флинт такие любит /* неявно! */ operator string(); explicit operator bool(); >;
class MyException : std::exception < . >;
Раскрытие исходников flint
Анализ данных временных рядов с Apache Spark: пара примеров c Flint и Pandas
В этой статье для дата-инженеров и аналитиков рассмотрим пример мониторинга состояния электрогенераторов с помощью анализа данных временных рядов и ранжирования в pandas для предупреждения выхода оборудования из строя. А также разберем основы анализа временных рядов на больших данных с открытой библиотекой Flint для Apache Spark.
Постановка задачи: температура и производительность электрогенераторов
Показания датчиков, измеряющих температуру и давление, во многих технических системах нужны для мониторинга их состояния в текущих условиях эксплуатации. Непрерывный мониторинг за этими параметрами поможет определить, как выглядит нормальное поведение актива и сообщить о возможных повреждениях или износе при отклонениях от нормы. Так можно предупредить отказы дорогостоящего оборудования, своевременно предприняв меры по профилактическому обслуживанию или осмотру.
Однако, что температура связана с рядом факторов: она зависит как от окружающей среды (летом теплее, чем зимой), загруженности производства и эффективность средств охлаждения. Поэтому имеет смысл отслеживать не только состояние каждого отдельного прибора, а учитывать его параметры вместе с окружением. В частности, для электрических генераторов имеет смысл ранжировать поведение каждого генератора относительно других в течение времени.
Предположим, собраны исторические показания электрогенерации и температуры для набора генераторов на станции. Один из генераторов показывал самую высокую производительность на протяжении 4-х лет, показатели других также были стабильны, т.е. держались на своем уровне «нормы».
Но спустя 5 лет картина поменялась: среди генераторов поменялся лидер по производительности и показания их температуры также изменились. Отслеживая только показания температуры или только производительность выработки электроэнергии, можно было бы пропустить аномальные изменения.
Поэтому целесообразно провести исследование данных по этим двум измерениям, чтобы понять, что влияет на систему в данный момент? Например, был ли актив в автономном режиме в течение длительного периода, случались ли проблемы со связью или точностью датчика? Что именно могло вызвать такое поведение? Чтобы ответить на эти вопросы, далее проанализируем исходные данных временных рядов, поступающие с генераторов. Подробнее про особенности данных временных рядов читайте в нашей новой статье.
Практический пример на PySpark
Датасет с исходными данными содержит отметку времени (Timestamp), идентификатор актива (Asset), показатели производительности генератора (Generation) и его температуру (Temperature). Данные можно группировать по желаемому временному интервалу, чтобы получить среднегодовые или месячные значения и понять отклонения. Далее сгруппированные данные ранжируются и анализируются.
Для подготовки данных к ранжированию можно использовать следующий код на PySpark:
# import pandas library import pandas as pd# read in timeseries data, may be via csv or queried direct # from a database df = pd.read_csv(r’filepathfilename.csv’)# create a variable for your desired time interval # i.e. year or month df[‘year’] = pd.to_datetime(df[‘TimeStamp’]).dt.to_period(‘Y’)# group the data by generator and year and find the aggregate values #e.g. average temperature over the year df2 = df.groupby([‘GeneratorId’,’year’]).agg()# write to excel df2.to_csv(r’filepathfilename.csv’, index = False)
Хотя на этом этапе уже можно применить ранжирование, лучше сперва сверить полученное подмножество данных с фактическими значениями, чтобы проверить корректность скрипта и избежать ошибок в дальнейшем. Функция ранжирования данных по температуре очень проста :
df2[‘average_temp_rank’] = df_year.groupby(‘year’)[‘average_temp_mean’].rank(pct=True)
Аналогичный прием можно использовать для ранжирования других значений, а затем провести дальнейший анализ, чтобы отследить, как изменились эти рейтинги. Например, чтобы определить тенденцию ранжирования поколения: соотношение ранжирования температуры, остается ли оно стабильным или становится несоответствующим? Также имеет смысл посмотреть ранжирование стандартного отклонения с течением времени. В частности, в генераторах частые колебания температуры могут привести к повреждению из-за расширения и сжатия материалов в ответ на эти изменения.
Рассмотренный пример с использованием Python-библиотеки pandas отлично иллюстрирует саму идею анализа данных о технологических параметрах реальных устройств, но неприменим для работы с огромными объемами данных. В этом случае для анализа данных временных рядов можно использовать Flint – открытую библиотеку Apache Spark, которую мы рассмотрим далее.
Источник: bigdataschool.ru