Русский
Русский
English
Статистика
Реклама

Блог компании auriga

EasyUI действительно easy?

31.05.2021 12:16:05 | Автор: admin

В этой статье я хочу поделиться опытом избавления от жуткой головной боли, возникшей при разработке веб-приложения для одного маленького, но весьма могучего устройства. Но сначала несколько слов об источнике этой боли о дизайне.

Дизайн сегодня один из необходимых компонентов любого продукта, а для сайтов и веб-приложений это самая важная часть. Всё, что находится под капотом, скрыто от глаз клиента. Пользователя не интересует гениальность исполнения движка или уникальность архитектуры: подразумевается само собой, что программа должна работать надежно и безопасно. Современному пользователю необходим стильный графический интерфейс.

Раньше создание такого интерфейса вызывало серьёзную головную боль у программистов, но теперь для избавления от неё выпущено большое количество различных фреймворков и библиотек. Казалось бы ура, проблема решена! Однако, теперь перед нами встаёт другой вопрос: какой препарат выбрать пенталгин или панадол?

Вопрос нелёгкий, и решать, в итоге, вам. Я же расскажу о своём лекарстве: библиотеке EasyIU, предназначенной для создания полноценных одностраничных веб-приложений (SPA) и основанной на jQuery, Angular, Vue и React.

Моё знакомство с EasyUI началось около двух лет назад, когда наша команда приступила к разработке софта для системы управления питанием и его интеллектуального распределения между потребителями. Управляющее устройство имело на борту Linux и кроме распределения питания должно было обмениваться данными с различными периферийными устройствами, уметь контролировать их, а также принимать показания от большого количества (до нескольких сотен) датчиков. Правила управления могли изменяться, позволяя пользователю настроить работу всего комплекса для выполнения необходимых задач.

Настройка и мониторинг устройства могли вестись через различные протоколы: ssh, snmp, redfish, BACnet, но основным способом общения с ним был http, то есть всем комплексом можно было управлять через обыкновенный веб-браузер. Это широко используемое решение, и оно не сулило никаких проблем. Однако, дьявол всё же основательно порылся в деталях.

Веб-страница должна была отражать состав комплекса, его внутренние и внешние компоненты и их содержимое в виде дерева разнотипных объектов. Показания датчиков должны были выводиться в виде таблицы (как в Excel'е, безмятежно улыбался заказчик), причём часть данных (показания, состояния датчиков и т.п.) требовалось непрерывно обновлять, а часть могла изменяться пользователем непосредственно в таблице. Для настройки комплекса было необходимо использовать многоуровневое меню и большое количество модальных диалоговых окон.

Всё это выглядело достаточно сложно, тем более что веб-сервис был лишь тем самым капотом, под которым прятался движок, и который мы также должны были написать. Проекту был необходим полноценный графический интерфейс. Bootstrap в нашем случае требовал значительной настройки и обвески кодом, и мы решили поискать другие фреймворки. Существует большое количество библиотек, реализующих отдельные элементы графического интерфейса или их небольшие совокупности. Их использование также не было целесообразно, поскольку не обеспечивало для элементов интерфейса ни общего стилевого оформления, ни единообразных методов управления ими.

В итоге наших поисков, фреймворк EasyUI показался нам наиболее подходящим для решения наших задач. Несколько забегая вперёд, я могу сказать, что это был хороший выбор, несмотря на обнаруженные в ходе реализации проекта недостатки этой библиотеки.

Итак, что же такое такое EasyUI?

Как я уже упоминал выше, EasyIU представляет собой набор компонентов пользовательского интерфейса, основанных на jQuery, Angular, Vue и React. Мы использовали библиотеку, базирующуюся на jQuery.

С самого начала мне понравилась возможность создания макета приложения без программирования на javascript. EasyUI для jQuery имеет встроенный парсер, который расширяет HTML-разметку и ассоциирует её с библиотечным кодом. Для этого в классе HTML-элемента достаточно указать наименование компонента, который необходимо применить.

Например, эта разметка создаст на странице приложения пять зон: шапку и подвал высотой 100 пикселей, и разделённую на три колонки среднюю часть. Левая и правая колонки имеют ширину 100 пикселей, а центральная часть имеет серый фон и занимает всё оставшееся место. Кроме этого, высоту шапки и подвала, ширину левой и правой колонок можно изменять мышью во время работы приложения. Для этого EasyUI создаст на странице приложения специальные разделители.

<body class="easyui-layout">  <div data-options="region:'north',title:'North Title',split:true"       style="height:100px;"></div>  <div data-options="region:'south',title:'South Title',split:true"       style="height:100px;"></div>  <div data-options="region:'east',title:'East',split:true"       style="width:100px;"></div>  <div data-options="region:'west',title:'West',split:true"       style="width:100px;"></div>  <div data-options="region:'center',title:'center title'"       style="padding:5px;background:#eee;"></div></body>

Конечно, EasyUI позволяет сделать то же самое при помощи javascript:

$('body').layout({fit: true}).layout('add', {  region: 'north', title: 'North Title', split: true, height: 100}).layout('add', {  region: 'south', title: 'South Title', split: true, height: 100}).layout('add', {  region: 'east', title: 'East Title', split: true, width: 100}).layout('add', {  region: 'west', title: 'West Title', split: true, width: 100}).layout('add', {  region: 'center', title: 'сenter Title', split: true, widht:100,  style: {padding: 5, background: '#eee'}});

В результате EasyUI создаст вот такую страницу:

Пока всё easy, не так ли? Мы можем создать предварительный макет без написания javascript кода, что очень удобно на начальных этапах проектирования приложения.

А что ещё она умеет?

Для беглой оценки возможностей EasyUI, достаточно посмотреть на её конструктор тем:

Здесь показаны не все возможности EasyUI, но для первого впечатления вполне достаточно и этого: разметка (layout), панели (panel), многоуровневое меню (menu, menubutton), вкладки (tab), аккордеоны (accordion), календарь (calendar), таблица (datagrid), набор конфигурационных параметров (propertygrid), список (datalist), дерево (tree), диалоги (dialog), формы (form) и их элементы (validatebox, textbox, passwordbox, maskedbox, combobox, tagbox, numberbox, datetimebox, spinner, slider, filebox, checkbox, radiobutton) и этот перечень далеко не полон. При более глубоком погружении в возможности библиотеки выясняется, что эти компоненты можно расширять и создавать на их основе новые. На сайте проекта есть раздел Extention, на котором представлены некоторые расширения, например, всем известная лента (Ribbon):

Для демонстрации всех компонентов, реализованных EasyUI, на странице проекта есть специальный раздел с демонстрацией их работы.

И снова о дизайне

EasyUI поддерживает изменение стиля оформления пользователем из набора готовых тем прямо во время выполнения приложения. На сайте проекта предлагается собственная коллекция стилей. К сожалению, решение из такого набора не всегда может удовлетворить заказчика. Это, скорее, набор стилей для начала работы, который впоследствии потребует корректировки. Для этого на сайте проекта имеется конструктор тем. Разумеется, всегда можно настроить тему приложения в соответствующей таблице стилей, которые находятся в каталоге themes проекта. Для нашего проекта поддержка возможности изменения темы приложения не требовалась, однако, таблицы стилей были существенно переработаны, чтобы UI соответствовал корпоративному стилю заказчика.

Создание диалога при помощи EasyUI

В качестве примера использования библиотеки в проекте я хочу привести код (под катом) для создания диалога настроек HTTP. Параметром функции открытия диалога является иконка, назначенная тому пункту меню, из которого вызывается диалог.

Код для создания диалога настроек HTTP
(function($) {   $.fn.httpConfDlg = function(icon) {     var title = _("HTTP Configuration"), me;     var succ = _(       "HTTP properties have been changed. " +       "You need to re-connect your browser " +       "according to the new properties."     );     var errcode = "System returned error code %1."     var errset = _(       "Can't set HTTP configuration. " + errcode     );     var errget = _(       "Can't get HTTP configuration. " + errcode     );     var allowed = $.SMR_PRIVILEGE.CHECK(       $.SMR_PRIVILEGE.CHANGE_NETWORK_CONFIGURATION     );     var buttons = [];     if (allowed) {       buttons.push({         okButton: true,         handler: function() {           var ho = $(this.parentElement).api({             fn: $.WAPI.FN_SET_HTTP_PROPERTIES,             param: {               httpPort: parseInt($('#httpPort').textbox('getValue')),               httpsPort: parseInt($('#httpsPort').textbox('getValue')),               forceHttps: $.HpiBool($('#forceHttp')[0].checked)             },             before: function() {               $('body').css('cursor', 'wait');             },             done: function() {               $('body').css('cursor', 'default');               me.dialog('close');             },             error: function(err) {               if (err.RC == $.WAPI.RC_BAD_RESPONSE) {                 $.messager.alert(                   title,                   $.fstr(errset, err.IC),                   'error'                 );                 return false;               } else if (err.RC == 1003) {                 ho.api('drop');                 $.messager.alert(title, succ, 'info', function() {                   $('#sinfo').session('logout');                 });                 return false;               }               return true;             }           });         }       });     }     buttons.push({cancelButton: true});     return this.each(function() {       document.body.appendChild(this);       me = $(this).append(         '<div id="httpSetting" style="padding: 10px 30px">' +         $.fitem('httpPort', _("HTTP port")) +         $.fitem('httpsPort', _("HTTPS port")) +         $.fcheck('forceHttp', _("Force HTTPS for Web Access")) +         '</div>'       );       $('#httpPort').textbox({         type: 'text', width: 60, disabled: !allowed       });       $('#httpsPort').textbox({         type: 'text', width: 60, disabled: !allowed       });       if (!allowed) $('#forceHttp').attr('disabled', 'disabled');         me.mdialog({           title: title,           iconCls: icon,           width: 320,           height: 180,           modal: true,           buttons: buttons,           onOpen: function() {             var ho = $(this).api({               fn: $.WAPI.FN_GET_HTTP_PROPERTIES,               receive: function(res) {                 $('#httpPort').textbox('setValue', res.httpPort);                 $('#httpsPort').textbox('setValue', res.httpsPort);                 if (res.forceHttps == 1) {                   $('#forceHttp').attr('checked', 'checked')                 } else {                   $('#forceHttp').removeAttr('checked')}               },               error: function(err) {                 if (err.RC == $.WAPI.RC_BAD_RESPONSE) {                   $.messager.alert(                     _("HTTP"),                     $.fstr(                       errget,                       err.IC                     ),                   'error'                 );                 me.dialog('close');                 return false;               }               me.dialog('close');               return true;             }           });         }       });     });   }; })(jQuery); 

Поскольку компоненты EasyUI реализованы в виде коллекций jQuery (в нашем случае это $('div').httpConfDlg(http_icon)), инициализация диалога производится через метод this.each().

В начале активируются кнопки диалога: OK и Cancel. Это можно сделать непосредственно при инициализации диалога, но кнопка OK создается только для обеспечения привилегированного доступа. Таким образом, для пользователя, не имеющего достаточных прав для изменения параметров HTTP протокола, диалог будет отображать только кнопку Cancel (Конечно, EasyUI допускает установку и снятие запрета нажатия на кнопки во время инициализации диалога, а также во время его работы кнопка при запрете использования изменяет стиль и не реагирует на нажатия. Однако, для сохранения общего стиля, неиспользуемые кнопки в диалогах нами не отображаются). Обработчик кнопки Cancel по умолчанию закрывает окно диалога без дополнительных действий. У кнопки OK есть обработчик, который выполняет AJAX-запрос. В качестве параметра запросу передаётся JSON структура, содержащая номер функции для бэкенда, набор параметров для самой функции и обработчики результатов выполнения (callback).

Затем родительский элемент, переданный через параметр this, заполняется контентом: двумя полями для указания номеров портов и одного флажка, устанавливающего принудительное использование защищённого протокола. Далее поля активируются как EasyUI textbox компоненты. Если пользователь не имеет привилегий для их изменения, текстовые поля и флажок будут недоступны для изменения.

И наконец, производится активация самого диалога: устанавливаются его размеры, заголовок, иконка, модальный режим, активируются кнопки диалога и их обработчики (указанные явно или по умолчанию). После открытия диалога запускается функция, выполняющая AJAX запрос для получения текущих параметров HTTP. Если при выполнении запроса происходит ошибка, генерируется сообщение об ошибке и диалог закрывается. Если запрос выполнен успешно, текстовые поля и флажок устанавливаются в соответствии с полученными результатами запроса.

При нажатии на кнопку OK вызывается функция-обработчик, которая считывает текущие значения текстовых полей и состояние флажка, формирует соответствующий AJAX-запрос и выполняет его. При любом результате выполнения этого запроса производится закрытие окна диалога (параметр done). В случае возникновения ошибки, в зависимости от её типа, выдаётся сообщение об ошибке. В нашем случае, даже при успешном выполнении запроса происходит ошибка, поскольку изменение настроек протокола приводит к разрыву соединения. Для его восстановления производится закрытие сеанса и вызов процедуры новой регистрации.

Несколько, пояснений к коду.

  • Вызов $.fitem('httpPort', _("HTTP port")) создаёт набор связанных HTML элементов, реализующих типовое для нашего приложения поле ввода с идентификатором httpPort и меткой (label) HTTP port. Функция _() обеспечивает использование языка, указанного пользователем в настройках. Последующий вызов компонента EasyUI $('#httpPort').textbox({type: 'text', width: 60, disabled: !allowed}); регистрирует поле ввода как EasyUI textbox. Вызов $('#httpPort').textbox('setValue', res.httpPort); устанавливает значение для текстового поля в соответствие результату AJAX запроса. И наконец, parseInt($('#httpPort').textbox('getValue')) в обработчике OK-кнопки возвращает текущее значение текстового поля.

  • Компонент mdialog() является нашим собственным расширением от базового компонента EasyUI dialog() для автоматического закрытия диалога при наступлении определённых событий, а также для создания типовых кнопок с обработкой нажатия по умолчанию. В данном случае это кнопка Cancel, которая создаётся короткой инструкцией buttons.push({cancelButton: true});

  • Функция $.messager вызывает окно предупреждения, которое также является компонентом EasyUI, производным от компонента Dialog.

В итоге диалог выглядит так:

EasyUI диалог для настройки HTTPEasyUI диалог для настройки HTTP

Ложка дёгтя

Как видно из приведенного выше примера, для создания диалога требуется довольно много кода. Большое количество диалогов и их компонентов (например, выпадающих списков с одинаковым содержимым) ощутимо раздуло наш проект однообразными велосипедными конструкциями.

Часть их удалось сократить с помощью создания собственных расширений, благо EasyUI позволяет сделать это, но всё-таки код во многих местах до сих пор кажется мне избыточным. И это, пожалуй, самый серьёзный недостаток EasyUI, который на мой взгляд, с лихвой компенсируется надёжностью библиотеки и простотой её использования.

И всё-таки, почему EasyUI?

Работа с большими массивами данных, представленными в виде таблиц и деревьев это та фишка, которая определила выбор EasyUI.

Для нас критически важно было отображать большой и постоянно обновляемый набор данных как в виде иерархической структуры, так и в виде таблицы. И EasyUI предоставила нам такую возможность. Для этого в составе библиотеки есть специальные компоненты: tree и datagrid.

EasyUI tree отображает иерархические данные в виде древовидной структуры. Узлы дерева можно наполнять различной информацией и извлекать её по событиям, разворачивать, сворачивать, перетаскивать, изменять стиль оформления по содержимому или другим условиям.

Компонент datagrid отображает данные в табличном формате и предлагает богатую поддержку для выбора, сортировки и группировки данных. Слияние ячеек, многоколоночные заголовки, замороженные столбцы и нижние колонтитулы вот лишь некоторые из его особенностей. Кроме этого, для datagrid есть специальные расширения: datagrid-scrollview, которое манипулирует полным набором данных, сохраняя в DOM-е лишь отображаемую их часть (а это существенно повышает скорость работы приложения), и datagrid-filter позволяющее накладывать фильтры на используемый набор данных.

И tree, и datagrid поддерживают асинхронную загрузку и редактирование данных непосредственно по месту положения элемента, имеют динамически настраиваемое оформление и свободно подключаемые иконки для элементов компонента, а также возможность отображения данных в виде чекбоксов.

Именно сочетание всех этих возможностей позволило нашей команде реализовать сложный интерфейс, полностью удовлетворяющий заказчика, не отвлекаясь на построение сложных DOM-конструкций и разработку CSS с нуля.

В завершение хочу ещё раз сказать, что библиотека EasyUI обеспечивает все потребности пользовательского интерфейса, оставаясь при этом лёгкой в использовании. Я думаю, эта библиотека может быть интересна разработчику веб-приложений, ориентированных на управление различными устройствами. В этой нише её использование, на мой взгляд, может быть предпочтительнее Bootstrap-а. По крайней мере, это хорошая альтернатива панадолу.

Подробнее..

Пример использования React Stockcharts для рисования графиков и графических элементов

16.06.2020 10:18:24 | Автор: admin
В статье изложен материал практического использования React для решения задачи построения графиков на основе информации с финансовых рынков. Функционал графиков расширен элементами рисования и индикаторами, что позволяет дополнительно производить анализ при выборе торговой стратегии. Статья может заинтересовать frontend разработчиков, решающих задачи по графическому отображению данных.

image


Представим, что вам поставили задачу создать график, аналогичный представленному на платформе tradingview.com. Такие графики используются для отображения информации, например, в торговых платформах. Решение задачи с нуля будет достаточно сложным, поэтому сначала стоит проанализировать уже имеющиеся наработки.

Если провести поиск готовых решений в Интернете, выяснится, что графических библиотек на React множество. Но единственный, уникальный в своем роде проект, позволяющий с минимальными временными затратами решить поставленную задачу, проект React Stockcharts. Эта библиотека написана на React и уже включает в себя поддержку некоторых индикаторов и элементов рисования, а также динамическую загрузку данных с отрисовкой баров. Другие графические библиотеки не имеют данного функционала и требуют значительной доработки. Нам же остается только расширить React Stockcharts добавлением новых индикаторов и элементов рисования. Цель этой статьи показать, как расширить функционал библиотеки и добавить новые элементы.

React Stockcharts использует d3js, поддерживает рисование на canvas и SVG, элементы библиотеки структурированы и разделены на отдельные компоненты. Это облегчает понимание логики работы библиотеки. При разработке использовалась версия React 16.8.6, для сборки проекта используется babel и webpack.

С чего начать?


Первое, что нужно сделать, скачать исходный код библиотеки с github. Проинсталлируйте зависимости, выполнив npm install --save react-stockcharts, и запустите проект командой npm run watch.
Структура папок подсказывает, как организован проект. Все элементы графика разделены на отдельные компоненты и называются соответственно.

image

Создание нового индикатора


Добавление индикатора рассмотрим на примере Money Flow Index. Для создания нового индикатора нужно выполнить следующие действия:

1. Создать файл mfi.js в папке indicator. В нем осуществляется привязка алгоритма, способа рисования и других свойств к индикатору.

indicator/mfi.js
import { rebind, merge } from "../utils";import { mfi } from "../calculator";import baseIndicator from "./baseIndicator";const ALGORITHM_TYPE = "MFI";export default function() {const base = baseIndicator().type(ALGORITHM_TYPE).accessor(d => d.mfi);const underlyingAlgorithm = mfi();const mergedAlgorithm = merge().algorithm(underlyingAlgorithm).merge((datum, indicator) => { datum.mfi = indicator; });const indicator = function(data, options = { merge: true }) {if (options.merge) {if (!base.accessor()) throw new Error(`Set an accessor to ${ALGORITHM_TYPE} before calculating`);return mergedAlgorithm(data);}return underlyingAlgorithm(data);};rebind(indicator, base, "id", "accessor", "stroke", "fill", "echo", "type");rebind(indicator, underlyingAlgorithm, "undefinedLength");rebind(indicator, underlyingAlgorithm, "options");rebind(indicator, mergedAlgorithm, "merge", "skipUndefined");return indicator;}


2. Создать файл mfi.js в папке calculator. Здесь реализован математический алгоритм для индикатора.

indicator/mfi.js
import { mean } from "d3-array";import { slidingWindow } from "../utils";import { MFI as defaultOptions } from "./defaultOptionsForComputation";export default function() {let options = defaultOptions;function calculator(data) {const { windowSize } = options;let typical_price, typical_price_privious, money_flow, flow_ratio, flow_index, val_positive_minus, val_negative_minus, money_flow_privious;let val_positive = 0, val_negative = 0, ind = 0;const arr_positive = [], arr_negative = [];return data.map(function(d,i){if(i === 0){typical_price_privious = (d.high + d.low + d.close) / 3;ind++;} else {typical_price = (d.high + d.low + d.close) / 3;money_flow = typical_price * d.volume;if(typical_price >= typical_price_privious){val_positive += money_flow;arr_positive.push(money_flow);arr_negative.push(0);} else {val_negative += money_flow;arr_negative.push(money_flow);arr_positive.push(0);}if(ind >= windowSize ){if(i !== windowSize){val_positive = val_positive - val_positive_minus;val_negative = val_negative - val_negative_minus;}val_positive_minus = arr_positive[0];val_negative_minus = arr_negative[0];arr_positive.shift();arr_negative.shift();}typical_price_privious = typical_price;money_flow_privious = money_flow;if(ind >= windowSize){flow_ratio = val_positive / val_negative;flow_index = 100 - (100 / (1 + flow_ratio));ind++;return flow_index;} else {ind++;return undefined;}}});}calculator.undefinedLength = function() {const { windowSize } = options;return windowSize - 1;};calculator.options = function(x) {if (!arguments.length) {return options;}options = { ...defaultOptions, ...x };return calculator;};return calculator;}


3. Файл calculator/defaultOptionsForComputation.js содержит значение параметров по умолчанию, для вычислений и для рисования графика.

calculator/defaultOptionsForComputation.js
...export const MFI = {source: d => ({volume: d.volume, high: d.high, low: d.low}), // "high", "low", "open", "close"sourcePath: "volume/high/low",windowSize: 10,};...


Для данного индикатора используется стандартный tooltip из tooltip/MovingAverageTooltip.js. Для рисования линии индикатора используется компонент LineSeries из series/LineSeries.js. Более сложные индикаторы состоят из комбинации отдельных элементов LineSeries, CircleMarker и т.д.

Результат работы индикатора MFI представлен на рисунке:



Добавления элемента рисования.


В примере ниже в библиотеку добавляется элемент рисования прямоугольник.

1. Создадим файл RectangleSimple.js в папке interactive/components. В данном файле реализован алгоритм рисования прямоугольника, определяется, когда курсор мыши находится над элементом, свойство isHovering.

interactive/components/RectangleSimple.js
import React, { Component } from "react";import PropTypes from "prop-types";import GenericChartComponent from "../../GenericChartComponent";import { getMouseCanvas } from "../../GenericComponent";import {isDefined,noop,hexToRGBA,getStrokeDasharray,strokeDashTypes,} from "../../utils";class RectangleSimple extends Component {constructor(props) {super(props);this.renderSVG = this.renderSVG.bind(this);this.drawOnCanvas = this.drawOnCanvas.bind(this);this.isHover = this.isHover.bind(this);}isHover(moreProps) {const { tolerance, onHover } = this.props;if (isDefined(onHover)) {const { x1Value, x2Value, y1Value, y2Value, type } = this.props;const { mouseXY, xScale } = moreProps;const { chartConfig: { yScale } } = moreProps;const hovering = isHovering({x1Value, y1Value,x2Value, y2Value,mouseXY,type,tolerance,xScale,yScale,});// console.log("hovering ->", hovering);return hovering;}return false;}drawOnCanvas(ctx, moreProps) {const { stroke, strokeWidth, strokeOpacity, strokeDasharray, type, fill, fillOpacity, isFill } = this.props;const { x1, y1, x2, y2 } = helper(this.props, moreProps);        const width = x2 - x1;        const height = y2 - y1;        ctx.beginPath();ctx.rect(x1, y1, width, height);ctx.stroke();          if(isFill){            ctx.fillStyle = hexToRGBA(fill, fillOpacity);            ctx.fill();        }}renderSVG(moreProps) {const { stroke, strokeWidth, strokeOpacity, strokeDasharray } = this.props;const lineWidth = strokeWidth;const { x1, y1, x2, y2 } = helper(this.props, moreProps);return ();}render() {const { selected, interactiveCursorClass } = this.props;const { onDragStart, onDrag, onDragComplete, onHover, onUnHover } = this.props;return ;}}export function isHovering2(start, end, [mouseX, mouseY], tolerance) {const m = getSlope(start, end);if (isDefined(m)) {const b = getYIntercept(m, end);const y = m * mouseX + b;return (mouseY < y + tolerance)&& mouseY > (y - tolerance)&& mouseX > Math.min(start[0], end[0]) - tolerance&& mouseX < Math.max(start[0], end[0]) + tolerance;} else {return mouseY >= Math.min(start[1], end[1])&& mouseY <= Math.max(start[1], end[1])&& mouseX < start[0] + tolerance&& mouseX > start[0] - tolerance;}}export function isHovering({x1Value, y1Value,x2Value, y2Value,mouseXY,type,tolerance,xScale,yScale,}) {const line = generateLine({type,start: [x1Value, y1Value],end: [x2Value, y2Value],xScale,yScale,});const start = [xScale(line.x1), yScale(line.y1)];const end = [xScale(line.x2), yScale(line.y2)];const m = getSlope(start, end);const [mouseX, mouseY] = mouseXY;if (isDefined(m)) {const b = getYIntercept(m, end);const y = m * mouseX + b;return mouseY < (y + tolerance)&& mouseY > (y - tolerance)&& mouseX > Math.min(start[0], end[0]) - tolerance&& mouseX < Math.max(start[0], end[0]) + tolerance;} else {return mouseY >= Math.min(start[1], end[1])&& mouseY <= Math.max(start[1], end[1])&& mouseX < start[0] + tolerance&& mouseX > start[0] - tolerance;}}function helper(props, moreProps) {const { x1Value, x2Value, y1Value, y2Value, type } = props;const { xScale, chartConfig: { yScale } } = moreProps;const modLine = generateLine({type,start: [x1Value, y1Value],end: [x2Value, y2Value],xScale,yScale,});const x1 = xScale(modLine.x1);const y1 = yScale(modLine.y1);const x2 = xScale(modLine.x2);const y2 = yScale(modLine.y2);return {x1, y1, x2, y2};}export function getSlope(start, end) {const m /* slope */ = end[0] === start[0]? undefined: (end[1] - start[1]) / (end[0] - start[0]);return m;}export function getYIntercept(m, end) {const b /* y intercept */ = -1 * m * end[0] + end[1];return b;}export function generateLine({type, start, end, xScale, yScale}) {const m /* slope */ = getSlope(start, end);// console.log(end[0] - start[0], m)const b /* y intercept */ = getYIntercept(m, start);switch (type) {case "XLINE":return getXLineCoordinates({type, start, end, xScale, yScale, m, b});case "RAY":return getRayCoordinates({type, start, end, xScale, yScale, m, b});case "LINE":return getLineCoordinates({type, start, end, xScale, yScale, m, b});}}function getXLineCoordinates({start, end, xScale, yScale, m, b}) {const [xBegin, xFinish] = xScale.domain();const [yBegin, yFinish] = yScale.domain();if (end[0] === start[0]) {return {x1: end[0], y1: yBegin,x2: end[0], y2: yFinish,};}const [x1, x2] = end[0] > start[0]? [xBegin, xFinish]: [xFinish, xBegin];return {x1, y1: m * x1 + b,x2, y2: m * x2 + b};}function getRayCoordinates({start, end, xScale, yScale, m, b}) {const [xBegin, xFinish] = xScale.domain();const [yBegin, yFinish] = yScale.domain();const x1 = start[0];if (end[0] === start[0]) {return {x1,y1: start[1],x2: x1,y2: end[1] > start[1] ? yFinish : yBegin,};}const x2 = end[0] > start[0]? xFinish: xBegin;return {x1, y1: m * x1 + b,x2, y2: m * x2 + b};}function getLineCoordinates({start, end}) {const [x1, y1] = start;const [x2, y2] = end;if (end[0] === start[0]) {return {x1,y1: start[1],x2: x1,y2: end[1],};}return {x1, y1,x2, y2,};}RectangleSimple.propTypes = {x1Value: PropTypes.any.isRequired,x2Value: PropTypes.any.isRequired,y1Value: PropTypes.any.isRequired,y2Value: PropTypes.any.isRequired,interactiveCursorClass: PropTypes.string,stroke: PropTypes.string.isRequired,strokeWidth: PropTypes.number.isRequired,strokeOpacity: PropTypes.number.isRequired,strokeDasharray: PropTypes.oneOf(strokeDashTypes),type: PropTypes.oneOf(["XLINE", // extends from -Infinity to +Infinity"RAY", // extends to +/-Infinity in one direction"LINE", // extends between the set bounds]).isRequired,onEdge1Drag: PropTypes.func.isRequired,onEdge2Drag: PropTypes.func.isRequired,onDragStart: PropTypes.func.isRequired,onDrag: PropTypes.func.isRequired,onDragComplete: PropTypes.func.isRequired,onHover: PropTypes.func,onUnHover: PropTypes.func,defaultClassName: PropTypes.string,r: PropTypes.number.isRequired,edgeFill: PropTypes.string.isRequired,edgeStroke: PropTypes.string.isRequired,edgeStrokeWidth: PropTypes.number.isRequired,withEdge: PropTypes.bool.isRequired,children: PropTypes.func.isRequired,tolerance: PropTypes.number.isRequired,selected: PropTypes.bool.isRequired,};RectangleSimple.defaultProps = {onEdge1Drag: noop,onEdge2Drag: noop,onDragStart: noop,onDrag: noop,onDragComplete: noop,edgeStrokeWidth: 3,edgeStroke: "#000000",edgeFill: "#FFFFFF",r: 10,withEdge: false,strokeWidth: 1,strokeDasharray: "Solid",children: noop,tolerance: 7,selected: false,};export default RectangleSimple;



2. Создадим файл EachRectangle.js в папке interactive/wrapper. Здесь определяются правила рисования множества прямоугольников.

interactive/wrapper/EachRectangle.js
import React, { Component } from "react";import PropTypes from "prop-types";import { ascending as d3Ascending } from "d3-array";import { noop, strokeDashTypes } from "../../utils";import { saveNodeType, isHover } from "../utils";import { getXValue } from "../../utils/ChartDataUtil";import Rectangle from "../components/Rectangle";import ClickableCircle from "../components/ClickableCircle";import HoverTextNearMouse from "../components/HoverTextNearMouse";class EachRectangle extends Component {constructor(props) {super(props);this.handleEdge1Drag = this.handleEdge1Drag.bind(this);this.handleEdge2Drag = this.handleEdge2Drag.bind(this);this.handleLineDragStart = this.handleLineDragStart.bind(this);this.handleLineDrag = this.handleLineDrag.bind(this);this.handleEdge1DragStart = this.handleEdge1DragStart.bind(this);this.handleEdge2DragStart = this.handleEdge2DragStart.bind(this);this.handleDragComplete = this.handleDragComplete.bind(this);this.handleHover = this.handleHover.bind(this);this.isHover = isHover.bind(this);this.saveNodeType = saveNodeType.bind(this);this.nodes = {};this.state = {hover: false,};}handleLineDragStart() {const {x1Value, y1Value,x2Value, y2Value,} = this.props;this.dragStart = {x1Value, y1Value,x2Value, y2Value,};}handleLineDrag(moreProps) {const { index, onDrag } = this.props;const {x1Value, y1Value,x2Value, y2Value,} = this.dragStart;const { xScale, chartConfig: { yScale }, xAccessor, fullData } = moreProps;const { startPos, mouseXY } = moreProps;const x1 = xScale(x1Value);const y1 = yScale(y1Value);const x2 = xScale(x2Value);const y2 = yScale(y2Value);const dx = startPos[0] - mouseXY[0];const dy = startPos[1] - mouseXY[1];const newX1Value = getXValue(xScale, xAccessor, [x1 - dx, y1 - dy], fullData);const newY1Value = yScale.invert(y1 - dy);const newX2Value = getXValue(xScale, xAccessor, [x2 - dx, y2 - dy], fullData);const newY2Value = yScale.invert(y2 - dy);onDrag(index, {x1Value: newX1Value,y1Value: newY1Value,x2Value: newX2Value,y2Value: newY2Value,});}handleEdge1DragStart() {this.setState({anchor: "edge2"});}handleEdge2DragStart() {this.setState({anchor: "edge1"});}handleDragComplete(...rest) {this.setState({anchor: undefined});this.props.onDragComplete(...rest);}handleEdge1Drag(moreProps) {const { index, onDrag } = this.props;const {x2Value, y2Value,} = this.props;const [x1Value, y1Value] = getNewXY(moreProps);onDrag(index, {x1Value,y1Value,x2Value,y2Value,});}handleEdge2Drag(moreProps) {const { index, onDrag } = this.props;const {x1Value, y1Value,} = this.props;const [x2Value, y2Value] = getNewXY(moreProps);onDrag(index, {x1Value,y1Value,x2Value,y2Value,});}handleHover(moreProps) {if (this.state.hover !== moreProps.hovering) {this.setState({hover: moreProps.hovering});}}render() {const {x1Value,y1Value,x2Value,y2Value,type,stroke,strokeWidth,strokeOpacity,strokeDasharray,r,edgeStrokeWidth,edgeFill,edgeStroke,edgeInteractiveCursor,lineInteractiveCursor,hoverText,selected,onDragComplete,} = this.props;const {enable: hoverTextEnabled,selectedText: hoverTextSelected,text: hoverTextUnselected,...restHoverTextProps} = hoverText;const { hover, anchor } = this.state;return ;}}export function getNewXY(moreProps) {const { xScale, chartConfig: { yScale }, xAccessor, plotData, mouseXY } = moreProps;const mouseY = mouseXY[1];const x = getXValue(xScale, xAccessor, mouseXY, plotData);const [small, big] = yScale.domain().slice().sort(d3Ascending);const y = yScale.invert(mouseY);const newY = Math.min(Math.max(y, small), big);return [x, newY];}EachRectangle.propTypes = {x1Value: PropTypes.any.isRequired,x2Value: PropTypes.any.isRequired,y1Value: PropTypes.any.isRequired,y2Value: PropTypes.any.isRequired,index: PropTypes.number,type: PropTypes.oneOf(["XLINE", // extends from -Infinity to +Infinity"RAY", // extends to +/-Infinity in one direction"LINE", // extends between the set bounds]).isRequired,onDrag: PropTypes.func.isRequired,onEdge1Drag: PropTypes.func.isRequired,onEdge2Drag: PropTypes.func.isRequired,onDragComplete: PropTypes.func.isRequired,onSelect: PropTypes.func.isRequired,onUnSelect: PropTypes.func.isRequired,r: PropTypes.number.isRequired,strokeOpacity: PropTypes.number.isRequired,defaultClassName: PropTypes.string,selected: PropTypes.bool,stroke: PropTypes.string.isRequired,strokeWidth: PropTypes.number.isRequired,strokeDasharray: PropTypes.oneOf(strokeDashTypes),edgeStrokeWidth: PropTypes.number.isRequired,edgeStroke: PropTypes.string.isRequired,edgeInteractiveCursor: PropTypes.string.isRequired,lineInteractiveCursor: PropTypes.string.isRequired,edgeFill: PropTypes.string.isRequired,hoverText: PropTypes.object.isRequired,};EachRectangle.defaultProps = {onDrag: noop,onEdge1Drag: noop,onEdge2Drag: noop,onDragComplete: noop,onSelect: noop,onUnSelect: noop,selected: false,edgeStroke: "#000000",edgeFill: "#FFFFFF",edgeStrokeWidth: 2,r: 5,strokeWidth: 1,strokeOpacity: 1,strokeDasharray: "Solid",hoverText: {enable: false,}};export default EachRectangle;



3. Создадим файл Rectangle.js в папке interactive. Это компонент rectangle верхнего уровня, который используется для рисования прямоугольника.
interactive/Rectangle.js
import React, { Component } from "react";import PropTypes from "prop-types";import { isDefined, isNotDefined, noop, strokeDashTypes } from "../utils";import {getValueFromOverride,terminate,saveNodeType,isHoverForInteractiveType,} from "./utils";import EachRectangle from "./wrapper/EachRectangle";import MouseLocationIndicator from "./components/MouseLocationIndicator";import HoverTextNearMouse from "./components/HoverTextNearMouse";class Rectangle extends Component {constructor(props) {super(props);this.handleStart = this.handleStart.bind(this);this.handleEnd = this.handleEnd.bind(this);this.handleDrawLine = this.handleDrawLine.bind(this);this.handleDragLine = this.handleDragLine.bind(this);this.handleDragLineComplete = this.handleDragLineComplete.bind(this);this.terminate = terminate.bind(this);this.saveNodeType = saveNodeType.bind(this);this.getSelectionState = isHoverForInteractiveType("trends").bind(this);this.state = {};this.nodes = [];}handleDragLine(index, newXYValue) {this.setState({override: {index,...newXYValue}});}handleDragLineComplete(moreProps) {const { override } = this.state;if (isDefined(override)) {const { trends } = this.props;const newTrends = trends.map((each, idx) => idx === override.index? {...each,start: [override.x1Value, override.y1Value],end: [override.x2Value, override.y2Value],selected: true,}: {...each,selected: false,});this.setState({override: null,}, () => {this.props.onComplete(newTrends, moreProps);});}}handleDrawLine(xyValue) {const { current } = this.state;if (isDefined(current) && isDefined(current.start)) {this.mouseMoved = true;this.setState({current: {start: current.start,end: xyValue,}});}}handleStart(xyValue, moreProps, e) {const { current } = this.state;if (isNotDefined(current) || isNotDefined(current.start)) {this.mouseMoved = false;this.setState({current: {start: xyValue,end: null,},}, () => {this.props.onStart(moreProps, e);});}}handleEnd(xyValue, moreProps, e) {const { current } = this.state;const { trends, appearance, type } = this.props;if (this.mouseMoved&& isDefined(current)&& isDefined(current.start)) {const newTrends = [...trends.map(d => ({ ...d, selected: false })),{start: current.start,end: xyValue,selected: true,appearance,type,}];this.setState({current: null,trends: newTrends}, () => {this.props.onComplete(newTrends, moreProps, e);});}}render() {const { appearance } = this.props;const { enabled, snap, shouldDisableSnap, snapTo, type } = this.props;const { currentPositionRadius, currentPositionStroke } = this.props;const { currentPositionstrokeOpacity, currentPositionStrokeWidth } = this.props;const { hoverText, trends } = this.props;const { current, override } = this.state;const tempLine = isDefined(current) && isDefined(current.end)? : null;return {trends.map((each, idx) => {const eachAppearance = isDefined(each.appearance)? { ...appearance, ...each.appearance }: appearance;const hoverTextWithDefault = {...Rectangle.defaultProps.hoverText,...hoverText};return ;})}{tempLine};}}Rectangle.propTypes = {snap: PropTypes.bool.isRequired,enabled: PropTypes.bool.isRequired,snapTo: PropTypes.func,shouldDisableSnap: PropTypes.func.isRequired,onStart: PropTypes.func.isRequired,onComplete: PropTypes.func.isRequired,onSelect: PropTypes.func,currentPositionStroke: PropTypes.string,currentPositionStrokeWidth: PropTypes.number,currentPositionstrokeOpacity: PropTypes.number,currentPositionRadius: PropTypes.number,type: PropTypes.oneOf(['RECTANGLE']),hoverText: PropTypes.object.isRequired,trends: PropTypes.array.isRequired,appearance: PropTypes.shape({        isFill: true,stroke: PropTypes.string.isRequired,strokeOpacity: PropTypes.number.isRequired,strokeWidth: PropTypes.number.isRequired,strokeDasharray: PropTypes.oneOf(strokeDashTypes),edgeStrokeWidth: PropTypes.number.isRequired,edgeFill: PropTypes.string.isRequired,edgeStroke: PropTypes.string.isRequired,}).isRequired};Rectangle.defaultProps = {type: "RECTANGLE",onStart: noop,onComplete: noop,onSelect: noop,currentPositionStroke: "#000000",currentPositionstrokeOpacity: 1,currentPositionStrokeWidth: 3,currentPositionRadius: 0,shouldDisableSnap: e => (e.button === 2 || e.shiftKey),hoverText: {...HoverTextNearMouse.defaultProps,enable: true,bgHeight: "auto",bgWidth: "auto",text: "Click to select object",selectedText: "",},trends: [],appearance: {stroke: "#000000",strokeOpacity: 1,strokeWidth: 1,strokeDasharray: "Solid",edgeStrokeWidth: 1,edgeFill: "#FFFFFF",edgeStroke: "#000000",r: 6,                fill: '#8AAFE2',                fillOpacity: 0.7,                text: '',        }};export default Rectangle;



Результат рисования прямоугольника представлен на рисунке:


В результате можно расширить библиотеку, добавляя различные элементы рисования и
индикаторы. Дополнительно можно создать диалоговые окна с настройками элементов
графика. Разработчики, которым интересен проект, могут поучаствовать в дальнейшем его
развитии.

Основные проблемы, с которыми пришлось столкнуться при разработке


В библиотеке используется ряд устаревших методов жизненного цикла компонента, например,
componentWillReceiveProps. Как известно, этот метод не будет поддерживаться в следующих
версиях React (начиная с 17). Переписывание логики, заложенной в данном методе, потребует
значительных трудозатрат и дополнительного тестирования.

Заключение


Библиотека React Stockcharts позволила быстро решить задачу, показала высокую
производительность и совместимость с различными версиями браузеров. Не было замечено
каких-либо значительных визуальных задержек при рисовании различных элементов на
графике, при получении данных в онлайн режиме. Благодаря широкому функционалу,
библиотека может быть использована как готовое решение для создания торговых терминалов,
а также сайтов, отображающих информацию с финансовых рынков.
Подробнее..

Clang-Tidy для автоматического рефакторинга кода

09.11.2020 10:07:58 | Автор: admin

Существует много инструментов для анализа кода: они умеют искать ошибки, узкие места, плохую архитектуру, предлагать оптимизацию. Но много ли среди них инструментов, которые могут не только найти, но и исправить код сами?


Представьте, что у вас есть большой проект на С или С++ (или даже С#), который разрабатывался много лет и многими людьми. В результате разные части проекта выглядят по-разному нет единого стиля имен переменных, функций, типов данных. То есть в разных частях проекта использовался разный coding style: где-то имена в верхнем регистре, где-то CamelCase, где-то с префиксами, в других местах без Некрасиво, в общем.


И вот вы собрались навести порядок, начав, например, с имен переменных. Решили, что все имена должны быть в верхнем регистре и с префиксом по типу данных (так называемая Венгерская нотация).


Вот такое, например, переименование:


Тут некоторые наверняка подумали: Ну, и в чем проблема? Автозамена же поможет. В крайнем случае скрипт на Python на коленке запилить


Хорошо, давайте разберем это: простая автозамена заменит всё и везде, не смотря на области видимости. Если у вас есть переменные с одинаковыми именами и в глобальной области, и в локальных, то все будет переименовано единообразно. Но мы-то можем захотеть давать разные имена, в зависимости от контекста.


Значит, автозамена не подходит. Пишем скрипт на коленке. С областями видимости разобраться не сложно. Но, представляете, переменным, функциям и даже типам данных иногда позволено иметь одинаковые имена. То есть реально, вот такая конструкция вполне себе законна (правда, только в GNU C):


typedef int Something;int main(){    int Something(Something Something)    {        return Something + Something;    }    printf("This is Something %d!\n", Something(10));    return 0;}

C:\HelloWorld.exeThis is Something 20!

Итак, наш скрипт должен уметь различать контекст, должен понимать, к какой сущности относится символ, должен находить те места, где на него ссылаются. Ничего не напоминает? Мы собираемся написать компилятор! А точнее, ту его часть, которая строит абстрактное синтаксическое дерево.


Абстрактное Синтаксическое Дерево (Abstract Syntax Tree, AST) это, по сути, ваш исходный код, разобранный на мельчайшие атомы, то есть переменные, константы, функции, типы данных и т.д., которые уложены в направленный граф.


typedef int SomeNumber;int SomeFunction(SomeNumber num){    return num + num;}int main(){       printf("This is some number = %d!\n", SomeFunction(10));    return 0;}



К счастью, строить дерево с нуля нам не нужно некоторые компиляторы настолько добрые, что готовы поделиться своим деревом с нами. Без регистрации и смс!


Где растут деревья?


Поскольку мы рассматриваем проект на С (или С++), то первым делом вспоминается великий и могучий GNU Compiler Collection, он же GCC.


Front-end в GCC для своих собственных нужд генерирует AST дерево и позволяет экспортировать его для дальнейшего использования. Это, в принципе, удобно для ручного анализа кода изнутри, но если мы задаемся целью делать автоматические исправления в самом коде, то в данном случае все остальное для этого нам придется написать самим.


Не менее могучий LLVM/clang также умеет экспортировать AST, но этот проект пошел еще дальше и предложил уже готовый инструмент для разбора и анализа дерева Clang-Tidy. Это инструмент 3-в-1 он генерирует дерево, анализирует его и выполняет проверки, а также автоматически вносит исправления в код там, где это нужно.


Что также важно для нас это Open Source проект с доступным исходным кодом, в который легко добавить свои новые функции, если вдруг там чего-то не хватает.


Лес рубят щепки летят


Для того, чтобы исследовать AST дерево своего проекта, нам понадобится Clang. Если у вас его еще нет, то готовую сборку можно скачать на странице проекта LLVM: https://clang.llvm.org/


Для начала нужно понять, что построение дерева это часть обычного процесса компиляции. Поэтому нельзя просто взять отдельный файл с исходниками и построить для него дерево. Дерево можно получить только для целой единицы трансляции, то есть набора из файла с исходным кодом и всех требуемых ему зависимостей: подключаемых заголовочных файлов и т.д.


Второй момент: чтобы получить правильное дерево, компилировать нужно с той же конфигурацией, что используется в вашем проекте. Если какая-либо единица трансляции в вашем проекте компилируется, например, с ключом -DDEBUG, то и при построении дерева нужно использовать этот же ключ. Иначе ваш код может или вообще не скомпилироваться (тогда вы сразу поймете, что что-то идет не так), или собрать вам неправильное дерево (и вы потратите неделю на то, чтобы понять это).


Первый шаг убедиться, что ваш проект успешно компилируется в Clang. Если вдруг нет, не стоит отчаиваться. Clang умеет отлично мимикрировать под многие другие инструменты, и, скорее всего, проблему можно решить использованием нужных ключей конфигурации. Правда, документацию все же придется почитать.


Эта команда выдаст все AST дерево сразу:


clang -c -Xclang -ast-dump <filename.c>

Вы наверняка удивитесь, насколько оно огромное. Для моего примера Hello World выше, из 7 строк кода, дерево получилось в 6259 строк. Это потому, что в нем будет также все, что было подключено из стандартных заголовочных файлов: типы данных, функции и т.д.


Поиск чего-то нужного в таком огромном массиве информации может вызвать уныние, поэтому удобнее использовать команду clang-query для извлечения только нужной информации. Запросы пишутся с использованием специального синтаксиса AST Matchers, который описывается вот тут


Например, следующий запрос выдаст нам весь внутренний мир функции с именем SomeFunction:


clang-query> set output dumpclang-query> match functionDecl(hasName("SomeFunction"))Match #1:Binding for "root":FunctionDecl 0x195581994f0 <C:\HelloWorld.c:5:1, line:8:1> line:5:5 used SomeFunction 'int (SomeNumber)'|-ParmVarDecl 0x19558199420 <col:18, col:29> col:29 used num 'SomeNumber':'int'`-CompoundStmt 0x19558199638 <line:6:1, line:8:1>  `-ReturnStmt 0x19558199628 <line:7:2, col:15>    `-BinaryOperator 0x19558199608 <col:9, col:15> 'int' '+'      |-ImplicitCastExpr 0x195581995d8 <col:9> 'SomeNumber':'int' <LValueToRValue>      | `-DeclRefExpr 0x19558199598 <col:9> 'SomeNumber':'int' lvalue ParmVar 0x19558199420 'num' 'SomeNumber':'int'      `-ImplicitCastExpr 0x195581995f0 <col:15> 'SomeNumber':'int' <LValueToRValue>        `-DeclRefExpr 0x195581995b8 <col:15> 'SomeNumber':'int' lvalue ParmVar 0x19558199420 'num' 'SomeNumber':'int'1 match.

Ну, и давайте попробуем запустить сам Clang-Tidy, из спортивного интереса:


C:\clang-tidy HelloWorld.c -checks=* --C:\HelloWorld.c:12:53: warning: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers]        printf("This is some number = %d!\n", SomeFunction(10));                                                           ^C:\HelloWorld.c:12:53: warning: 10 is a magic number; consider replacing it with a named constant [readability-magic-numbers]

Работает! И даже встроенные чекеры подают признаки жизни.


Пристегнитесь крепче начинаем кодировать!


Про дерево поговорили, пора бы уже переходить к нашей задаче с переименованием.


Первым делом нам нужны исходники LLVM. Да, прямо вот весь проект LLVM, огромный, отсюда: репозиторий


Еще понадобится cmake, вот здесь


Удобство проектов, написанных с использованием cmake, заключается в том, что можно автоматически сгенерировать проект для нескольких разных сред разработки. У меня, например, Visual Studio 2019 для Windows, поэтому мой алгоритм получения рабочего проекта выглядит так:


git clone https://github.com/llvm/llvm-project.git
cd llvm-project
mkdir build
cd build
cmake -DLLVM_ENABLE_PROJECTS='clang;clang-tools-extra' -G 'Visual Studio 16 2019' -A x64 -Thost=x64 ../llvm

После этих шагов будет сгенерирован LLVM.sln, который можно открывать в Visual Studio и собирать нужные компоненты. Минимальный набор: сам clang-tidy и clang-apply-replacements. Если времени не жалко совсем, то можно построить и весь LLVM, но в целом этого не требуется для нашей задачи.


Интересующие нас исходники находятся в llvm\clang-tools-extra\clang-tidy. Здесь можно посмотреть на примеры других чекеров, то есть модулей в Clang-Tidy для выполнения различных проверок. Они сгруппированы по нескольким категориям, таким как readability, portability, performance и т.д. Их назначение, в принципе, понятно из названия. Здесь же есть скрипт, который поможет нам сгенерировать заготовку для своего чекера:


python add_new_check.py misc ultra-cool-variable-renamer

Здесь misc это категория, в которую мы определили наш чекер, а ultra-cool-variable-renamer это имя нашего чекера.


Скрипт-генератор создаст несколько новых файлов, в том числе для документации и тестов. Но нам на данном шаге интересны только два, в папке misc: UltraCoolVariableRenamer.h и UltraCoolVariableRenamer.cpp


Важный момент: поскольку наш проект для Visual Studio был сгенерирован с помощью cmake, то новые файлы сами по себе в проект не попадут. Для этого нужно перезапустить cmake еще раз, и он обновит проект автоматически.


Собираем и запускаем Clang-Tidy. Видим, что наш чекер появился и показывает сообщения из
сгенерированного чекера-заготовки, радуемся этому:


C:\clang-tidy HelloWorld.c -header-filter=.* -checks=-*,misc-ultra-cool-variable-renamer 354 warnings generated.C:\HelloWorld.c:5:5: warning: function 'SomeFunction' is insufficiently awesome [misc-ultra-cool-variable-renamer]int SomeFunction(SomeNumber num)    ^C:\HelloWorld.c:5:5: note: insert 'awesome'int SomeFunction(SomeNumber num)    ^    awesome_C:\HelloWorld.c:10:5: warning: function 'main' is insufficiently awesome [misc-ultra-cool-variable-renamer]int main()    ^C:\HelloWorld.c:10:5: note: insert 'awesome'int main()    ^    awesome_

Генератор создал нам не только скелет нового чекера, но и положил в него пример кода, который предлагает немного улучшить имена функций. Эти сообщения мы видим здесь.


Если посмотреть на код самого чекера, то мы увидим что он состоит из всего двух методов: registerMatchers() и check().


void UltraCoolVariableRenamerCheck::registerMatchers(MatchFinder* Finder) {  // FIXME: Add matchers.  Finder->addMatcher(functionDecl().bind("x"), this);}void UltraCoolVariableRenamerCheck::check(const MatchFinder::MatchResult& Result) {  // FIXME: Add callback implementation.  const auto* MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");  if (MatchedDecl->getName().startswith("awesome_"))    return;  diag(MatchedDecl->getLocation(), "function %0 is insufficiently awesome")    << MatchedDecl;  diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note)    << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");}

Метод registerMatchers()вызывается один раз, при старте Clang-Tidy, и нужен для того, чтобы добавить правила для отлова нужных нам мест в AST дереве. При этом здесь применяется такой же синтаксис AST matchers, как мы использовали раньше в clang-query. Затем для каждого срабатывания зарегистрированного правила будет вызван метод check().


Сгенерированная заготовка чекера регистрирует только одно правило, для поиска объявлений функций. Для каждого такого объявления будет вызван метод check(), который и содержит всю логику для проверки кода. Нам же, для нашей задачи, нужно проверять переменные, поэтому поменяем этот код на:


  auto VariableDeclaration = varDecl();  Finder->addMatcher(VariableDeclaration.bind("variable_declaration"), this);  auto VariableReference = declRefExpr(to(varDecl()));  Finder->addMatcher(VariableReference.bind("variable_reference"), this);

Поскольку мы хотим переименовать переменные не только в месте их объявления, но и везде, где они используются, нам нужно зарегистрировать два правила. Обратите внимание на то, что для регистрации правила нужно использовать тот тип объекта из AST дерева, который мы хотим обработать. То есть varDecl для объявлений переменных и declRefExpr для ссылок на какой-либо объявленный объект. Поскольку объявленным объектом может быть не только переменная, то здесь мы применяем еще дополнительный критерий to(varDecl()), чтобы отфильтровать только ссылки на переменные.


Нам осталось написать содержимое метода check(), который будет проверять имя переменной и показывать предупреждение, если оно не соответствует используемому нами стандарту:


void UltraCoolVariableRenamerCheck::check(const MatchFinder::MatchResult& Result) {  const DeclRefExpr* VariableRef = Result.Nodes.getNodeAs<DeclRefExpr>("variable_reference");  const VarDecl* VariableDecl = Result.Nodes.getNodeAs<VarDecl>("variable_declaration");  SourceLocation location;  StringRef name;  StringRef type;  if (VariableDecl) {    location = VariableDecl->getLocation();    name = VariableDecl->getName();    type = StringRef(VariableDecl->getType().getAsString());      } else if (VariableRef) {    location = VariableRef->getLocation();    name = VariableRef->getDecl()->getName();    type = StringRef(VariableRef->getDecl()->getType().getAsString());  } else {    return;  }  if (!checkVarName(name, type)) {       diag(location, "variable '%0' does not comply with the coding style")        << name;  }}

Что здесь происходит: метод check() может быть вызван как для объявления переменной, так и для ее использования. Обрабатываем оба этих варианта. Дальше функция checkVarName() (оставим ее содержимое за кадром) проверяет соответствие имени переменной принятому нами стилю кодирования, и если соответствия нет, то показываем предупреждение.


Убеждаемся, что мы видим это предупреждение для всех трех мест, где встречается переменная в нашем коде:


C:\HelloWorld.c:5:29: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]int SomeFunction(SomeNumber num)                            ^C:\HelloWorld.c:7:9: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]        return num + num;               ^C:\HelloWorld.c:7:15: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]        return num + num;                     ^

Больше трюков добавляем исправление


На самом деле, мы уже почти все сделали. Чтобы Clang-Tidy не только показывал предупреждения, но и сам вносил исправления, нам осталось добавить пару строчек для того, чтобы описать, как именно и что нужно исправить.


Давайте напишем функцию generateVarName(StringRef oldVarName, StringRef varType), которая будет переводить прежнее имя переменной в верхний регистр и, в зависимости от типа данных, добавлять к ней префикс (примитивное содержимое этой функции также оставим за скобками).


Все, что нам осталось, это добавить в вывод diag правило замены, указав, где нужно заменить и на что:


  if (!checkVarName(name, type)) {       diag(location, "variable '%0' does not comply with the coding style")        << name       << type;      diag(location, "replace to '%0'", DiagnosticIDs::Note)        << generateVarName(name, type)        << FixItHint::CreateReplacement(location, generateVarName(name, type));  }

Теперь, если мы запустим Clang-Tidy еще раз, то увидим не только сообщения о неправильных именах, но и предложенные исправления. А если запустим его с ключом -fix, то эти исправления будут автоматически внесены в исходный код.


C:\HelloWorld.c:5:29: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]int SomeFunction(SomeNumber num)                            ^C:\HelloWorld.c:5:29: note: replace to 'snNUM'int SomeFunction(SomeNumber num)                            ^~~                            'snNUM'C:\HelloWorld.c:7:9: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]        return num + num;               ^C:\HelloWorld.c:7:9: note: replace to 'snNUM'        return num + num;               ^~~               'snNUM'C:\HelloWorld.c:7:15: warning: variable 'num' does not comply with the coding style [misc-ultra-cool-variable-renamer]        return num + num;                     ^C:\HelloWorld.c:7:15: note: replace to 'snNUM'        return num + num;                     ^~~                     'snNUM'

Итак, мы сделали автоматическое исправление всех переменных в нашем проекте, написав меньше 100 строчек кода. Да, этот пример примитивный, он не учитывает многих особенностей и на любом более-менее крупном проекте из коробки не заработает и все сломает Чтобы такое переименование заработало везде и хорошо, придется еще попотеть. И все же это вполне рабочий прототип, поверх которого можно добавлять ту логику, которую вы захотите.


Бочку мёда видим, где же дёготь?


Что ж, метод хорош, но как же без ограничений? Главное ограничение исходит из самой идеи использования синтаксического дерева: в поле зрения попадет и будет обработано только то, что есть в дереве. То, что в дерево не попало, останется без изменений.


Почему вдруг это может быть плохо?


  • То, что выключено условной компиляцией, в дерево не попадет. Помните про то, что мы строим дерево, используя реальные параметры вашего проекта? Поэтому, если в вашем проекте есть части кода, которые выглядят, например, вот так:


    #if ARCH==ARMdo_something();#elif ARCH==MIPSdo_something_else();#endif
    

    то у меня плохие новости в дерево попадет только то, что не будет вырезано предпроцессором на основе констант вашего проекта. А что тогда делать с неактивным кодом? Единого рецепта тут нет: можно запускать Tidy несколько раз, используя разный набор констант, или экспортировать список переименованных символов и дальше пытаться с помощью автозамены обновить оставшиеся части кода. В общем, придется проявить сообразительность и фантазию.


  • То, что не является исходным кодом, тоже не попадет в дерево. То есть комментарии в исходниках останутся нетронутыми. Если у вас в комментариях упоминаются ваши переменные или функции, то придется вам их обновлять вручную. Еще можно попробовать автоматизировать это внешними средствами, используя информацию о переименованиях, которую Clang-Tidy умеет экспортировать.


  • Не так все просто с макросами. Они обрабатываются предпроцессором еще до того, как запускается сама компиляция. Это означает, что для правильного переименования символов, которые взаимодействуют с макросами, нужно будет предпринимать дополнительные усилия. Также макросы не присутствуют в AST дереве, хотя возможность их обнаружения и переименования имеется для этого нужно использовать PPCallbacks.



Как глубока кроличья нора?


С простой задачей разобрались, а что насчет сложных? К этому моменту у вас наверняка возникло много вопросов: насколько умным может быть чекер, что вообще можно им сделать и где черпать идеи?


Прежде всего, с помощью Clang-Tidy можно сделать практически любой анализ и любые манипуляции с исходным кодом. В примере выше мы разобрали работу с переменными как самый простой случай. Однако функции, константы, классы, типы данных и все остальное, из чего состоит код, также разбирается на атомы в AST дереве и также доступно в Clang-Tidy.


В качестве отправной точки для изучения того, какие задачи можно решать с помощью Clang-Tidy и как это можно делать, я советую посмотреть на исходный код других чекеров, который находится там же, в папке с исходниками Clang-Tidy. Это отличное дополнение к официальной документации LLVM, которая, честно говоря, не блещет многословием. Особенно интересно подсмотреть, как сделаны чекеры от Google.


Напоследок хочется пожелать никогда не сдаваться, проявлять изобретательность и фантазию, и конечно удачи!

Подробнее..

А ваш фильтр Калмана правильно работает?

19.05.2021 10:20:52 | Автор: admin

Фильтр Калмана является одним из самых популярных алгоритмов фильтрации. Он широко распространен в машинном обучении, навигационных системах, автопилотируемых устройствах и пр.

В открытых источниках можно встретить множество работ, статей и книг по тому, как работает этот загадочный фильтр, будь то линейный, расширенный (extended), сигма-точечный (unscented) или любой другой фильтр Калмана. Однако, вопрос корректности работы фильтра освещается намного реже.

В это же время фильтр Калмана применяется в системах с особыми требованиями к функциональной безопасности, отказ или неисправность которых может привести, в том числе, к летальным исходам, как это имеет место в случае автопилотируемых устройств. Таким образом, валидация результатов работы фильтра Калмана это один из первостепенных вопросов, который должен стоять перед инженером при разработке ПО для подобных систем.

На поиск необходимой информации по валидации фильтра Калмана у меня ушло больше недели, но результат того стоил. Поэтому в данной статье я хочу поделиться алгоритмом, который позволяет находить неочевидные ошибки в коде фильтра Калмана и определять возможности применения фильтра к конкретной задаче.

Фильтр Калмана

Я предполагаю, что читатель знаком с базовыми принципами работы фильтра Калмана, поэтому лишь коротко объясню его суть. Для тех, кто не знаком, советую прочитать эту или эту статью.

Вкратце работу фильтра Калмана можно объяснить так:

Рис. 1. Измерения, предсказываемое и оптимальное состояние.Рис. 1. Измерения, предсказываемое и оптимальное состояние.

Для описания какого-либо процесса мы используем как состояние, полученное посредством измерений (Measurement), так и состояние, полученное по уравнениям, описывающим происходящий процесс (Predicted state estimate). Комбинируя эти два независимых состояния, мы получаем более точное оптимальное состояние (Optimal state estimate).

В качестве измерений могут служить, например, измерения GPS-датчика в автомобиле, а движение машины может быть описано с помощью кинематических или динамических уравнений, которые по текущей скорости, углу поворота колес, ускорению и прочим характеристикам, могут определить прогнозируемое положение автомобиля в следующий момент времени.

В зависимости от сложности процесса и измерений можно использовать линейный, расширенный или сигма-точечный фильтр Калмана.

Проблема верификации результатов

Фильтры Калмана используются в том случае, если мы имеем зашумленные измерения, а уравнения, описывающие динамику системы, известны лишь приблизительно. Алгоритмы, использующие фильтры Калмана, относятся к более общему классу оценочных алгоритмов, которые помимо состояния также оценивают возможную ошибку (с помощью ковариационной матрицы ошибок, например).

Есть множество материалов, статей, диссертаций, которые рассматривают существующие или разрабатывают новые разновидности фильтра Калмана, однако, вопрос валидации результатов встречается намного реже. Зачастую авторы ограничиваются сравнением зашумленного/оценочного/реального состояния, как это показано на рис. 2 (источник), аргументируя точность фильтра близким значением оценочного и реального состояний и забывая про ковариационную матрицу ошибок. Проблема такой оценки кроется в том, что без использования ковариационной матрицы ошибок мы не можем оценить, насколько точные данные мы имеем.

Рис. 2. Пример сравнения зашумленных, оценочных и реальных данных. Рис. 2. Пример сравнения зашумленных, оценочных и реальных данных.

Проиллюстрируем проблему на простом примере, когда состояние системы описывается одной переменной x. В таком случае, ковариационная матрица ошибок представляет из себя дисперсию x2 этой величины. Допустим, у нас x = 0 и x = 1, тогда в соответствии с правилом 3 сигма, мы можем с уверенностью > 99.5 % сказать, что наша величина лежит в интервале [-3, 3]. Если мы увеличим среднеквадратичное отклонение x до 10, то интервал увеличится до [-30, 30]. Если наш фильтр дает оценку x = 0 и x = 1, в то время как действительная величина среднеквадратичного отклонения x = 10, то наш объект может в реальности оказаться в местоположении, которое практически невозможно в соответствии с оценкой нашего фильтра. Чем это может грозить, думаю понятно без комментариев. Таким образом, дисперсия вносит существенный вклад в оценку состояния, и пренебрегать ей ни в коем случае нельзя.

С другой стороны, так как фильтр работает со случайными данными, то вполне возможно, что для какого-то набора данных даже некорректный фильтр выдаст правдоподобные результаты, поэтому делать выводы о том, как работает фильтр, по одному запуску было бы неверно. Кроме того, возникает вопрос: как убедиться, что значения дисперсии, рассчитанные по алгоритму, близки к реальным данным?

Терминология и обозначения

Прежде чем пойти дальше, введем основные обозначения и терминологию. Подстрочный индекс k будет обозначать номер временного слоя, xk вектор состояния системы, yk вектор измерений, Pk ковариационная матрица ошибок, x0, P0 начальные значения вектора состояния и ковариационной матрицы. Динамика системы описывается дискретными уравнениями:

\textbf{x}_k = f\left(\textbf{x}_{k-1}, \textbf{w}_k\right) \qquad \qquad \qquad (1)

где f функция перехода для рассматриваемого процесса, wk некоторая матрица шумов, ассоциированная с соответствующей функцией перехода. Функция f может иметь различный вид, например, в простейшем случае быть линейной как относительно xk-1, так и относительно wk.

Связь между измерениями системы и вектором состояния описываются уравнением:

\textbf{y}_k = h\left(\textbf{x}_k, \textbf{v}_k\right) \qquad \qquad \qquad (2)

где h функция, связывающая вектор состояния с вектором измерений, а vk некоторая матрица ошибок измерений.

Стоит отметить, что в общем случае размерности вектора измерений и вектора состояния различны. Например, состояние может в себя включать вектор положения и вектор скорости итого 6 компонент, а измерения только вектор положения, а значит, всего три компоненты.

Реальные значения вектора состояния и ковариационной матрицы мы будем обозначать xk* и Pk*, в то время как значения, полученные с помощью фильтра Калмана, просто xk и Pk.

Постановка численного эксперимента

Для валидации фильтра Калмана мы будем использовать метод Монте-Карло. Вначале мы вычислим реальные значения состояния xk* на каждом временном слое от 0 до n. Для практических целей это можно сделать с помощью уравнения (1), заменив wk на нулевую матрицу и задав определенные x0*, P0*.

Затем нам нужно смоделировать m независимых численных экспериментов. Всего у нас имеется 3 источника случайности, которые мы будем варьировать за каждый проход итерации Монте-Карло, это начальное состояние (его мы знаем только с точностью, определяемой ковариационной матрицей P0), и значения шумов, определяемые матрицами wk и vk. Таким образом, на каждом прогоне мы получаем различные значения начального состояния и значений шумов процесса и измерений на каждом временном слое. Номер итерации Монте-Карло мы будем обозначать буквой i. В результате моделирования мы будем иметь m произвольных наборов xki и Pki для 0 k n и 0 i m.

Стоит заметить, что обычно при постановке задачи значения Pk* не известны, однако их можно заменить на статистические значения, вычисленные по формуле:

\textbf{x}_k^{avg}=1/N \sum\textbf{x}_k^i \qquad \qquad \qquad \qquad \qquad \quad (3)\textbf{x}_k^{i, err}=\textbf{x}_k^i - \textbf{x}_k^{avg} \qquad \qquad \qquad \qquad \qquad \quad (4)\mathbf{P}_k^{est} = 1 / N \sum \textbf{x}_k^{i, err}\left(\textbf{x}_k^{i, err}\right)^T \qquad \qquad \qquad (5)

где надстрочный индекс T обозначает транспонирование. В дальнейшем везде под Pk* имеется в виду Pkest.

Верификация результатов

Стороннему наблюдателю может показаться, что мы ничего существенного не сделали, но, на самом деле, самое сложное уже позади. Осталось только понять, насколько близки значения xki и Pki к ожидаемым значениям xk* и Pk*. Для этого мы воспользуемся идеей из статьи [X. R. Li, Z. Zhao Measuring Estimator's Credibility: Noncredibility Index // In Proceedings of 2006 International Conference on Information Fusion, Florence, 2006.], которая заключается в следующем: для каждого временного слоя k и для каждой итерации Монте-Карло i мы вычисляем noncredibility index по формуле:

\gamma_k^i = 10\left|log\left( \left[\textbf{x}_k^{i, err} \mathbf{P}_k^i \textbf{x}_k^{i, err}\right] / \left[\textbf{x}_k^{i, err}\mathbf{P}_k^*\textbf{x}_k^{i, err}\right] \right)\right| \qquad \qquad \qquad (6)

Данная формула определяет некоторую ошибку как в случае несоответствия xki, так и в случае несоответствия Pki, и обладает следующими полезными свойствами:

  1. Значение ki близко к нулю, если xki и Pki близки к действительным значениям;

  2. Значение ki безразмерно и, следовательно, не зависит от единиц измерения xki;

  3. Значение ki одинаково серьезно штрафует как за оптимистичные (отношение [xki, err Pki xki, err] / [xki, err Pk* xki, err] > 1) значения, так и за пессимистичные значения Pki (когда это отношение меньше единицы).

Тогда для временного слоя k можно посчитать среднее значение noncredibility indexа:

\gamma_k^{avg} = 1 / N \sum \gamma_k^i \qquad \qquad \qquad (7)

Это и есть основная мера валидности результата, полученного с помощью фильтра Калмана. Таким образом, для каждого временного слоя мы можем посчитать kavg и по нему понять, насколько хорошо работает наш фильтр. В процитированной работе было показано, что значения kavg ~ 1 соответствуют достоверным результатам.

Собирая все в одну кучу

Весь процесс валидации фильтра Калмана сводится к следующим шагам:

  1. Для поставленной задачи задаем функцию перехода f, функцию, связывающую вектор состояния с вектором измерений, h, матрицы шумов wk и vk и начальные значения x0*, P0*;

  2. Проводим симуляцию без шумов для получения действительных значений xk*;

  3. Запускаем m итераций Монте-Карло и получаем xki и Pki для каждого временного слоя k и для каждой итерации i;

  4. Вычисляем Pkest в соответствии с формулой (5). Pkest будет служить в качестве Pk* ;

  5. Для каждого временного слоя вычисляем kavg, которая и будет показателем достоверности результатов;

  6. (Опционально) Строим график зависимости kavg от времени и смотрим, как изменялось качество работы фильтра.

Результаты

В качестве примера рассмотрим задачу отслеживания мотоцикла (рис. 3) из статьи.

Рис. 3. Кинематическая модель мотоциклаРис. 3. Кинематическая модель мотоцикла

Уравнения, описывающие кинематическую модель движения, запишутся в виде:

\dot{x} = v\cos{\left( \psi + \beta \right)}, \qquad \qquad \qquad \qquad \qquad \qquad (8)\dot{y} = v \sin{\left(\psi + \beta\right)}, \qquad \qquad \qquad \qquad \qquad \qquad (9)\dot{\psi} = \frac{v}{l_r}\sin{\left(\beta\right)}, \qquad \qquad \qquad \quad \qquad \qquad \qquad (10)\dot{v}=a, \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad (11)\beta = \tan^{-1}{\left(\frac{l_r}{l_r + l_f}\tan{\left(\delta_f\right)}\right)}, \qquad \qquad \qquad (12)

где x, y координаты центра тяжести, v скорость, угол между направлением мотоцикла и осью x, угол между направлением скорости и направлением мотоцикла, a ускорение, lr и lf длина от центра масс до задней и передней части соответственно, f угол между направлением переднего колеса и направлением мотоцикла.

Значения lr и lf являются входными параметрами конфигурации мотоцикла, f и a значения, которые определяют динамику системы, а x, y вектор состояния.

Из вида уравнений видно, что они имеют нелинейный вид, а следовательно, нужно применять либо расширенный, либо сигма-точечный фильтр Калмана.

Ради теста фильтров и для усложнения задачи с вычислительной точки зрения, будем считать, что измерения производятся в полярных координатах:

r = \sqrt{x^2 + y^2}+v^0, \qquad \qquad \qquad \quad (13)\phi = \tan^{-1}{\left( \frac{y}{x} \right)}+v^1. \qquad \qquad \qquad (14)

Измерения в полярных координатах вносят существенные нелинейности в расчеты и являются неплохой проверкой работы фильтра.

К сожалению, точные результаты работы для разрабатываемой библиотеки, которые можно было бы продемонстрировать, я опубликовать не могу. Однако, приведу здесь основные закономерности, которые были мной замечены при верификации фильтров Калмана.

Ключевое, что хотелось бы отметить: несмотря на близость реальных и полученных данных состояния, благодаря данной схеме валидации, были выявлены участки кода, в которых происходила потеря значимых разрядов для чисел одинарной точности как для расширенного, так и для сигма-точечного фильтра Калмана. Для чисел с одинарной точностью график зависимости вид, подобный изображенному на рис. 4.

Рис. 4. Зависимость noncredibility index от номера итерации для чисел одинарной точности.Рис. 4. Зависимость noncredibility index от номера итерации для чисел одинарной точности.

Как видно, значение noncredibility index растет с течением времени, что говорит о накапливаемой ошибке в результатах и, по-видимому, все большему удалению матрицы Pk от действительного значения.

Для чисел двойной точности значения kavg от номера итерации колебались в пределах от 0.8 до 1.6, что говорит о правдоподобности результатов (см. рис. 5). Как видно из рисунка, значения noncredibility index не растут с течением времени, а лишь колеблются в окрестности некоторого среднего значения.

Рис. 5. Зависимость noncredibility index от номера итерации для чисел двойной точности.Рис. 5. Зависимость noncredibility index от номера итерации для чисел двойной точности.

Вывод

В статье была разобрана достаточно простая задача, однако предложенный подход валидации легко обобщить как для более сложных постановок задач, так и для более комплексных подходов проведения численного эксперимента.

При применении рассмотренной выше схемы валидации для ряда более сложных задач мы установили, что расширенный фильтр дает неудовлетворительные результаты, которые было бы невозможно определить на глаз простым сравнением графиков векторов состояния. Таким образом, для дальнейшего моделирования мы использовали сигма-точечный фильтр.

Лично меня подкупила простота подхода какова бы ни была задача: со сложными дифференциальными уравнениями, описывающими процесс, и большим вектором состояния, или простыми алгебраическими уравнениями и скалярным состоянием, в итоге мы получаем лишь одно число, которое наглядно показывает, насколько хорошо фильтр описывает происходящий процесс. Другой плюс рассмотренного подхода это возможность валидации не только вектора состояния xki, но и ковариационной матрицы ошибок Pki.

Описанный подход также обладает достаточной общностью и может быть применен на более широкий круг фильтров, нежели фильтры Калмана.

Подробнее..

Выращивание Nested sets в условиях .Net

25.11.2020 12:06:49 | Автор: admin


Привет, меня зовут Антон, и я разработчик. Сына я родил, дом построил купил, осталось вырастить дерево. Так как агроном из меня не очень, пришлось дерево кодить. Наш проект состоит из нескольких микросервисов на .Net Core, в PostgreSQL базе хранятся сущности, которые образуют иерархические связи. Расскажу о том, какую структуру лучше выбрать для хранения таких данных, почему именно Nested sets, на какие грабли можно наступить, и как с этим потом жить.


Что такое Nested sets


Как растет дерево в саду все знают, а в случае Nested sets дерево растет так: для каждого узла хранится два поля Left и Right, они целочисленные. Логика здесь такая Left меньше чем Right, и, если у узла есть дочерние, то все значение Left и Right дочерних узлов должны быть между соответствующих значений родительского.



Как они растут у нас


У нас под хранение иерархий выделен отдельный микросервис. Фронтеду приходится часто рисовать полное дерево, а также поддеревья элементов в детализированном представлении, тогда как вставлять и переносить элементы надо сравнительно реже. Для такого случая Nested sets подходят как нельзя лучше. Хранится в такой таблице:



Id это идентификатор соответствующей сущности, по нему можно получить полную информацию из подходящего микросервиса. TreeId идентификатор корневого элемента. В проекте деревья в основном небольшие, но их много. Для чтения из базы используется EntityFramework, класс один к одному соответствует структуре таблицы.

Как прочитать


При таком способе хранения получить элементы поддерева просто запрашиваем все узлы, у которых Left больше Left родительского, а Right соответственно меньше. Также проверяем, что все узлы относятся к одному и тому же дереву по колонке TreeId. Это операция, которая необходима чаще других, и выполняется она быстро. К примеру, так:

dataContext.Nodes.Where(_ =>                         _.Left > node.Left &&                         _.Right < node.Right &&                         _.TreeId == node.TreeId); 


Еще одна часто выполняемая операция поиск всех родительских узлов объекта. Здесь тоже несложно запрашиваем узлы дерева, у которых Left меньше Left родительского элемента, а Right соответственно больше. Например, таким способом:

dataContext.Nodes.Where(_ =>                         _.Left < node.Left &&                         _.Right > node.Right &&                         _.TreeId == node.TreeId);


Как вырастить новые ветки


Перейдем к сложной части пересадка, т.е. добавление узлов или перенос из одного поддерева в другое. Разберем, как сделать перенос, т.к. по сути эта операция включает в себя все, что нужно для добавления дочернего элемента.

Первое, что нужно сделать для успешной вставки разрешить только одну операцию вставки или обновления. Для этого воспользуемся блокировкой. Блокировать всю таблицу не имеет смысла, так как узлы могут переходить только в рамках одного дерева, так что достаточно заблокировать только это дерево. Для этого выполним такой SQL запрос:

select * from "Nodes" where "TreeId" = <TreeId> for update; 


Это позволит нам читать элементы дерева сразу, но если потребуется добавить или изменить узел в дереве, где уже начата такая операция, придется подождать завершения предыдущей.

Следующим шагом подготовим место для пересадки создадим промежуток между Left и Right. Посчитаем, сколько место необходимо это разность между Right и Left корневого элемента перемещаемого поддерева. Добавим эту разность ко всем дочерним элементам узла, который станет новым родителем. Здесь можем поймать Exception, и вот почему. Для ускорения поиска и чтения в таблице заведены два B-Tree индекса, на поля Left и Right, и если одновременно менять значения этих полей, EntityFramework может выдать ошибку циклической зависимости, т.к. два индекса могут пересчитываться одновременно на одной строке. Ошибка будет типа InvalidOperationException с таким сообщением:

Unable to save changes because a circular dependency was detected in the data to be saved: 'Node[Modified]< Index { 'Right', 'TreeId' } Node[Modified]< Index { 'Left', 'TreeId' } Node[Modified]'.


Чтобы обойти, просто сделаем два отдельных цикла в одном изменим Left, в другом Right, и, конечно, сохранять изменения будем после каждого из них.

            var nodesToMove = await dataContext.Nodes                 .Where(n =>                     n.Right >= parentNodeRight &&                     n.TreeId == parentNode.TreeId)                 .OrderByDescending(n => n.Right)                 .ToListAsync();              foreach (var n in nodesToMove)             {                 n.Left += distance;             }              await dataContext.SaveChangesAsync();              foreach (var n in nodesToMove)             {                 n.Right += distance;             }              await dataContext.SaveChangesAsync(); 


Далее сама пересадка расстояние переноса будет равно разности между Left нового родителя и Left корня поддерева. Добавим это значение к Left и Right всех узлов перемещаемого поддерева.

            var nodes = await dataContext.Nodes                 .Where(n =>                     n.Left >= node.Left &&                     n.Right <= node.Right &&                     n.TreeId == node.TreeId)                 .ToListAsync();              foreach (var n in nodes)             {                 n.Left += distance;                 n.Right += distance; 


И последнее, что надо сделать закрыть промежуток так, где было перемещенное поддерево. Запросим все узлы правее этого поддерева это будут элементы, у которых Right больше либо равно Left корня поддерева, и передвинем их на освободившееся место. Для этого отнимем от Left и Right всех этих узлов разность между Right и Left корня. Здесь тоже придется сделать два отдельных цикла:

            var nodesToMove = await dataContext.Nodes               .Where(n => n.Right >= gap.Left && n.TreeId == gap.TreeId)               .ToListAsync();             nodesToMove = nodesToMove                 .Where(n => n.Right >= gap.Right)                 .OrderBy(n => n.Right)                 .ToList();              foreach (var n in nodesToMove)             {                 if (n.Left >= gap.Right)                 {                     n.Left -= distance;                 }             }              await dataContext.SaveChangesAsync();              foreach (var n in nodesToMove)             {                 n.Right -= distance;             }              await dataContext.SaveChangesAsync();


Заключение


Посмотрим, что у нас выросло. Мы получили дерево с возможностью быстрого чтения дочерних и родительских элементов. Если в вашем проекте нужно часто читать данные и получать поддеревья, Nested sets отличный выбор. Надо быть готовым к тому, что с операциями вставки и обновления могут возникнуть проблемы, но они вполне решаемые. Если же добавлять и переносить приходится часто, лучше подумать о применении какого-то другого алгоритма, либо рассмотреть некие гибридные варианты. Например скрестить Nested sets и Adjacency List. Для этого в каждый узел, помимо Left и Right, надо добавить прямую ссылку на идентификатор родительского элемента. Это позволит быстрее перемещаться по иерархии и находить родительские и дочерние узлы, и, кроме того, повысит надежность алгоритма.
Подробнее..

Симуляторы компьютерных систем всем знакомый полноплатформенный симулятор и никому неизвестные потактовый и трассы

02.07.2020 20:17:59 | Автор: admin
Во второй части статьи о симуляторах компьютерных систем продолжу рассказывать в простой ознакомительной форме о компьютерных симуляторах, а именно о полноплатформенной симуляции, с которой чаще всего сталкивается обычный пользователь, а также о потактовой модели и трассах, которые больше распространены в кругах разработчиков.

image

В первой части я рассказал, что такое симуляторы вообще, а также об уровнях моделирования. Теперь на основе тех знаний предлагаю нырнуть чуть поглубже и поговорить о полноплатформенной симуляции, о том, как собрать трассы, что с ними потом делать, а также о потактовой микроархитектурной эмуляции.

Полноплатформенный симулятор (full platform simulator), или Один в поле не воин


Если требуется исследовать работу одного конкретного устройства, например, сетевой карты, или написать для этого устройства прошивку или драйвер, то такое устройство можно смоделировать отдельно. Однако использовать его в отрыве от остальной инфраструктуры не очень удобно. Для запуска соответствующего драйвера потребуется центральный процессор, память, доступ к шине для передачи данных и прочее. Кроме того, для работы драйвера необходимы операционная система (ОС) и сетевой стек. В дополнение к этому может потребоваться отдельный генератор пакетов и сервер приема ответов.

Полноплатформенный симулятор создает окружение для запуска полного софтверного стека, который включает в себя все, начиная с BIOS и загрузчика и заканчивая самой ОС и различными ее подсистемами, такими как тот же сетевой стек, драйверами, приложениями пользовательского уровня. Для этого в нем реализованы программные модели большинства устройств компьютера: процессор и память, диск, устройства ввода-вывода (клавиатура, мышь, дисплей), а также та самая сетевая карта.

Ниже приведена блок-диаграмма чипсета x58 от компании Intel. В полноплатформенном симуляторе компьютера на этом чипсете необходима реализация большинства перечисленных устройств, в том числе и тех, что находятся внутри IOH (Input/Output Hub) и ICH (Input/Output Controller Hub), не нарисованных детально на блок-диаграмме. Хотя, как показывает практика, не так уж мало устройств, которые не используются тем ПО, которое мы собираемся запускать. Модели таких устройств можно не создавать.

image

Чаще всего полноплатформенные симуляторы реализуются на уровне инструкций процессора (ISA, см. предыдущую статью). Это позволяет относительно быстро и недорого создать сам симулятор. Уровень ISA также хорош тем, что остается более или менее постоянным, в отличие от, например, уровня API/ABI, который меняется чаще. К тому же, реализация на уровне инструкций позволяет запускать так называемое немодифицированное бинарное ПО, то есть запускать уже скомпилированный код без каких-либо изменений, ровно в том виде как он используется на реальном железе. Другими словами, можно сделать копию (дамп) жесткого диска, указать его в качестве образа для модели в полноплатформенном симуляторе и вуаля! ОС и остальные программы загружаются в симуляторе без всяких дополнительных действий.

Производительность симуляторов



image

Как было упомянуто чуть выше, сам процесс симуляции всей системы целиком, то есть всех ее устройств, довольно небыстрое мероприятие. Если еще и реализовать все это на совсем детальном уровне, например, микроархитектурном или логическом, то выполнение станет экстремально медленным. А вот уровень инструкций является подходящим выбором и позволяет ОС и программам выполняться на скоростях, достаточных пользователю для комфортного взаимодействия с ними.

Здесь как раз уместно будет коснуться темы производительности симуляторов. Обычно ее измеряют в IPS (instructions per second), точнее в MIPS (millions IPS), то есть количестве инструкций процессора, выполняемых симулятором за одну секунду. В то же время скорость симуляции зависит и от производительности системы, на которой работает сама симуляция. Поэтому, возможно, правильнее говорить о замедлении (slowdown) симулятора по сравнению с оригинальной системой.

Наиболее распространенные на рынке полноплатформенные симуляторы, те же QEMU, VirtualBox или VmWare Workstation, имеют неплохую производительность. Для пользователя может быть даже не заметно, что работа идет в симуляторе. Так происходит благодаря реализованной в процессорах специальной возможности виртуализации, алгоритмам бинарной трансляции и другим интересным вещам. Это все тема для отдельной статьи, но если совсем коротко, то виртуализация это аппаратная возможность современных процессоров, позволяющая симуляторам не симулировать инструкции, а отдавать на исполнение напрямую в реальный процессор, если, конечно, архитектуры симулятора и процессора похожи. Бинарная трансляция это перевод гостевого машинного кода в хостовый и последующее исполнение на реальном процессоре. В результате симуляция лишь ненамного медленнее, раз в 5-10, а часто вообще работает с той же скоростью, что и реальная система. Хотя на это влияет очень много факторов. Например, если мы хотим симулировать систему с несколькими десятками процессоров, то скорость тут же упадет в эти несколько десятков раз. С другой стороны, симуляторы типа Simics в последних версиях поддерживают многопроцессорное хостовое железо и эффективно распараллеливают симулируемые ядра на ядра реального процессора.

Если говорить про скорость микроархитектурной симуляции, то это обычно на несколько порядков, примерно в 1000-10000 раз, медленнее выполнения на обычном компьютере, без симуляции. А реализации на уровне логических элементов медленнее еще на несколько порядков. Поэтому в качестве эмулятора на этом уровне используют FPGA, что позволяет существенно увеличить производительность.

График ниже показывает примерную зависимость скорости симуляции от детализации модели.

image

Потактовая симуляция


Несмотря на невысокую скорость выполнения, микроархитектурные симуляторы довольно распространены. Моделирование внутренних блоков процессора необходимо для того, чтобы точно симулировать время выполнения каждой инструкции. Здесь может возникнуть непонимание ведь, казалось бы, почему просто не взять и запрограммировать время выполнения для каждой инструкции. Но такой симулятор будет работать очень неточно, поскольку время выполнения одной и той же инструкции может отличаться от вызова к вызову.

Простейший пример инструкция доступа в память. Если запрашиваемая ячейка памяти доступна в кэше, то время выполнения будет минимально. Если в кэше данной информации нет (промах кэша, cache miss), то это сильно увеличит время выполнения инструкции. Таким образом, для точной симуляции необходима модель кэша. Однако моделью кэша дело не ограничивается. Процессор не будет просто ждать получения данных из памяти при ее отсутствии в кэше. Вместо этого он начнет выполнять следующие инструкции, выбирая такие, которые не зависят от результата чтения из памяти. Это так называемое выполнение не по порядку (OOO, out of order execution), необходимое для минимизации времени простоя процессора. Учесть все это при расчете времени выполнения инструкций поможет моделирование соответствующих блоков процессора. Среди этих инструкций, выполняемых, пока ожидается результат чтения из памяти, может встретится операция условного перехода. Если результат выполнения условия неизвестен на данный момент, то опять-таки процессор не останавливает выполнение, а делает предположение, выполняет соответствующий переход и продолжает превентивно выполнять инструкции с места перехода. Такой блок, называемый branch predictor, также должен быть реализован в микроархитектурном симуляторе.

Картинка ниже показывает основные блоки процессора, ее знать необязательно, она приведена лишь для того, чтобы показать сложность микроархитектурной реализации.

image

Работа всех этих блоков в реальном процессоре синхронизуется специальными тактовыми сигналами, аналогично происходит и в модели. Такой микроархитектурный симулятор называют потактовым (cycle accurate). Основное его назначение точно спрогнозировать производительность разрабатываемого процессора и/или рассчитать время выполнения определенной программы, например, какого-либо бенчмарка. Если значения будут ниже необходимых, то потребуется дорабатывать алгоритмы и блоки процессора или оптимизировать программу.

Как было показано выше, потактовая симуляция очень медленная, поэтому ее используют только при исследовании определенных моментов работы программы, где необходимо узнать реальную скорость выполнения программ и оценить будущую производительность устройства, прототип которого моделируется.

При этом для симуляции остального времени работы программы используется функциональный симулятор. Как такое комбинированное использование происходит в реальности? Сначала запускается функциональный симулятор, на котором загружается ОС и все необходимое для запуска исследуемой программы. Ведь нас не интересует ни сама ОС, ни начальные стадии запуска программы, ее конфигурирование и прочее. Однако и пропустить эти части и сразу перейти к выполнению программы с середины мы тоже не можем. Поэтому все эти предварительные этапы прогоняются на функциональном симуляторе. После того, как программа исполнилась до интересующего нас момента, возможно два варианта. Можно заменить модель на потактовую и продолжить выполнение. Режим симуляции, при котором используется исполняемый код (т.е. обычные скомпилированные файлы программ), называют симуляцией по исполнению (execution driven simulation). Это самый распространенный вариант симуляции. Возможен также и другой подход симуляция на основе трасс (trace driven simulation).

Симуляция на основе трасс


Она состоит из двух шагов. С помощью функционального симулятора или на реальной системе собирается и записывается в файл лог действий программы. Такой лог называется трассой (trace). В зависимости от того, что исследуется, трасса может включать исполняемые инструкции, адреса памяти, номера портов, информацию по прерываниям.

Следующий шаг это проигрывание трассы, когда потактовый симулятор читает трассу и выполняет все инструкции, записанные в ней. В конце получаем время выполнения данного куска программы, а также различные характеристики этого процесса, например, процент попадания в кэш.

Важной особенностью работы с трассами является детерминированность, то есть, запуская симуляцию описанным выше образом, раз за разом мы воспроизводим одинаковую последовательность действий. Это дает возможность, изменяя параметры модели (размеры кэша, буферов и очередей) и используя разные внутренние алгоритмы или настраивая их, исследовать, как тот или иной параметр влияет на производительность системы и какой вариант дает наилучшие результаты. Все это можно проделать с моделью прототипа устройства до создания реального аппаратного прототипа.

Сложность данного подхода заключается в необходимости предварительного прогона приложения и сбора трассы, а также огромный размер файла с трассой. К плюсам можно отнести то, что достаточно смоделировать лишь интересующую часть устройства или платформы, в то время как симуляция по исполнению требует, как правило, полной модели.

Итак, в этой статье мы рассмотрели особенности полноплатформенной симуляции, поговорили про скорость реализаций на разных уровнях, потактовую симуляцию и трассы. В следующей статье я опишу основные сценарии использования симуляторов, как в личных целях, так и с точки зрения разработки в больших компаниях.
Подробнее..

Использование компьютерных симуляторов. Утром софт, вечером железо

16.11.2020 22:16:56 | Автор: admin
После того, как мы здесь и здесь разобрали, что же такое компьютерные симуляторы и какими они бывают, настало время поговорить о том, как они используются. И начну я, пожалуй, с наиболее интересной области применения расскажу о том, как профессиональные программисты используют симуляторы при разработке ПО, чтобы написать и отладить софт для железа, которого еще даже не существует.

image

Речь пойдет об использовании моделей не самых простых устройств, таких как, например, SoC (System on Chip) или печатных плат со множеством интегрированных устройств на борту. В разработке самих этих устройств можно выделить два этапа. Сначала разрабатывается аппаратная часть. Это процесс небыстрый может занять и год, и больше.

После того как появляется первая ревизия физического устройства и проводятся базовые проверки, устройство передается разработчикам ПО. С этого момента начинается вторая фаза разработка софта для устройства. Это могут быть прошивки (firmware), BIOS, BSP/CSP (Board and CPU Support Package) для различных операционных систем, а также драйвера.

image
Кстати, в разработке чипов, которые часто называют силикон (silicon), эти фазы именуются пре-силикон (pre-silicon) и пост-силикон (post-silicon) или просто силикон.

Теоретически, разработка ПО возможна и до появления физического устройства. Однако, на практике так мало кто делает. Разработка без возможности промежуточных запусков и отладки, а для этого как раз и требуется устройство, дело сильно неблагодарное, я встречал мало таких мазохистов. Все ждут железо, а оно, как я написал выше, появится только через год.

Кроме того, с железом есть еще и другая проблема. Первую ревизию выпускают лимитированной серией и раздают по одной штуке в руки. Проще говоря, плат и чипов на всех не хватает. Возникают очереди, конфликты, доходит до рукоприкладства. А если ты случайно спалил устройство, что очень даже несложно, то вся разработка останавливается, пока не будет найдено другое устройство. Часто оно не находится, необходимо производить новое, а потом ждать доставку. Это сильно тормозит процесс.

И тогда на помощь разработчикам ПО приходят симуляторы!

Создание виртуальной модели устройства начинается одновременно с проектированием физического устройства. Но так как создание и выпуск модели существенно проще, первые релизы таких моделей появляются условно не через год, а много раньше. Используя такие модели, разработчики ПО могут сразу приступить к своим задачам, не дожидаясь железа.

Да, там не будет всей функциональности, но программистам на первых этапах она вся и не нужна. Пока устройство не сделано, группы архитекторов железа, разработчиков моделей и разработчиков ПО должны иметь четкие и согласованные между собой планы, в которых отражается, какая конкретно функциональность на каком этапе должна быть сделана. На каждой итерации и с каждым этапом виртуальная модель прирастает дополнительным функционалом, который в свою очередь используют разработчики софта для написания драйвера или создания прошивки именно для этого функционала.

image
При таком подходе разработчики моделей и софта используют общие спецификации. Запуская ПО на модели, они, по сути, проверяют работу друг друга.

Дополнительным плюсом является валидация железа при том, что самого железа еще даже нет. Это звучит удивительно на первый взгляд, но речь здесь идет об архитектурных ошибках при проектировании устройства, часть из которых может выявляться при запуске софта на модели. И это очень круто, так как есть возможность исправить эти ошибки в железе до того, как оно будет выпущено. В противном случае эти ошибки привели бы к необходимости перевыпуска следующих ревизий, а это довольно дорогое удовольствие.

У нас был случай, когда в сложном устройстве было реализовано 10 блоков по обработке входящего потока данных, а регистры контроля и управления позволяли работать только с половиной из них. Так было реализовано в предыдущей версии устройства, и эту часть архитекторы просто забыли расширить. Когда мы создали модель, а другая команда написала и запустила драйвер, то довольно быстро выяснилось, что вся расширенная функциональность недоступна. Архитектура и спецификации были вовремя поправлены до момента создания физического прототипа устройства.

Большие компании-производители устройств могут поддерживать целую экосистему, выстроенную вокруг своих продуктов. В такую экосистему входят множество других компаний, в том числе и тех, которые производят ПО для данного оборудования. Например, это могут быть производители BIOS, так называемые IBV (Independent BIOS Vendors), наиболее известные из которых AMI, Insyde, Phoenix. Такие компании также получают модели от производителя оборудования и начинают разработку до появления физического устройства. Например, для платформ Intel используется симулятор Simics, о чем подробнее можно прочитать в статье Ecosystem Partners Shift Left with Intel for Faster Time-to-Market.

Очевидно, что создание моделей требует дополнительных затрат. Объем затрат, конечно, зависит от типа моделей (функциональные, потактовые и т.д.), но в целом можно принять стоимость создания модели примерно равной стоимости написания ПО для устройства. Не все компании готовы платить такую цену за более ранний выпуск, поэтому такой подход распространен в больших корпорациях. У них есть достаточные для этого бюджеты, а более ранний вывод продукта на рынок существенно увеличивает их доходы. Хотя в последнее время наблюдается тенденция более широкого использования симуляторов для разработки ПО и в небольших по размеру компаниях.

Часто для сокращения затрат в модели реализуют не всю возможную функциональность, а только минимально необходимую для определенных сценариев использования устройства. Например, если сетевое устройство не планируется использовать в VLAN-ах, а только чтобы обновлять прошивку и делать загрузку по tftp, то логику с VLAN tag-ами реализовывать необязательно, а вот функциональность для сценариев загрузки устройства по сети и обновления прошивки надо реализовать в полном объеме.

Что в итоге получается?

Если принять, что время разработки железа и софта примерно равны друг другу (см. картинку выше), то использование моделей позволяет существенно, практически в 2 раза, сократить время вывода продукта на рынок (т.н. TTM Time To Market), так как разработка железа и софта ведется параллельно, а не последовательно. Для этого часто используют термин Shift Left, пришедший из области тестирования, где он означал как можно более раннее тестирование. Этот же термин применим и к разработке ПО, которая как бы сдвигается влево по шкале времени, когда выполняется на симуляторе.

К моменту появления первой ревизии оборудования останется только проверить работоспособность ПО на реальном железе. В идеале, ошибки и исправления не должны быть значительными и данный этап не вносит больших задержек, так как код уже написан и отлажен на модели.

Подобный подход не бесплатный, он требует дополнительного бюджета и команды программистов, поэтому надо четко понимать, насколько окупятся эти затраты в том или ином случае.

Это не единственный вариант использования симуляторов. Об архитектурных исследованиях и создании сложного окружения я расскажу в следующих статьях. Не переключайтесь.
Подробнее..

Прототип на коленке cоздание приложения для мониторинга датчиков сердечного ритма в спортивном зале

03.11.2020 12:06:29 | Автор: admin


Однажды за утренним кофе обсуждали с приятелем современные технологии Интернета вещей и разговорились на предмет реализации системы мониторинга фитнес-оборудования в спортивном клубе. Приятель искал способ реализации своей идеи с нулевой стартовой стоимостью, а мне интересно было сделать что-то полезное и устроить себе очередную проверку знаний и творческих способностей.


В результате решили для начала создать, как говорится, на коленке, прототип устройства, собирающего данные с пульсометров датчиков сердечного ритма. По результатам работы я решил написать статью для обмена опытом с сообществом читателей, а еще для повышения собственного уровня в практике написания статей. В этой статье мы проследуем поэтапно от идеи до прототипа программы.


В интернете есть множество статей, в которых описываются готовые системы, но к сожалению часто в такого рода статьях опускаются подробности реализации на начальном этапе разработки. Я решил заполнить этот пробел.


Основным лейтмотивом реализации проекта служит идея совмещения низкоуровневой разработки программы управления устройством на языке C++ и быстрой высокоуровневой разработки сервиса на Python. Базовым программным обеспечением должна быть операционная система Linux. Будем использовать Linux way работа системы должна быть построена на небольших независимых сервисах, работающих под управлением ОС.


Итак, формулируем цель проекта


Контроль состояния здоровья посетителей спортивного зала преимущество как для клиентов (добавляет толику заботы и ощущение безопасности), так и для самой организации (повышает ее престиж и предупреждает возможные несчастные случаи). Главное условие на данный момент: стоимость стартапа не должна быть существенна; все необходимые компоненты должны находится в свободной продаже; программная часть должна быть построена на принципах свободного программного обеспечения.


Желаемое поведение системы


Посетитель спортивного зала в начале тренировки получает нагрудный датчик сердечного ритма HRM (Heart Rate Monitor) и регистрирует его у оператора в зале. Затем он перемещается по залу, и показания его датчика автоматически поступают на сервер сбора статистики для отслеживания состояния его здоровья. Такое предложение выгодно отличается от приобретения датчика самим посетителем: данные собираются централизовано и могут быть сопоставлены с данными с различных спортивных тренажеров, а также ретроспективно проанализированы.
В статье описан первый этап создания такого решенния программы, считывающую данные с датчика и с помощью которой можно будет в дальнейшем отправлять данные на сервер.


Технические аспекты


HRM представляет собой автономный датчик (монитор), прикрепленный на тело спортсмена, передающий данные по беспроводной сети. Большинство мониторов, предлагаемых сейчас на рынке, могут работать с использованием открытой сети с частотой 2.4ГГц по протоколам ANT+ и BLE. Показания датчика регистрируются на каком-либо программно-управляемом устройстве: мобильном телефоне или компьютере через USB приемопередатчик.


Для простоты решения задачи остановимся на протоколе ANT, так как такие датчики уже есть в наличии и протокол просто реализовать. Дальнейшее расширение системы будет производится с использованием других систем и технологий.


Основная проблема при использовании устройств ANT и BLE заключается в ограниченном радиусе действия сети (максимальный радиус в режиме минимальной мощности для ANT передатчика 1mW составляет всего 1 метр), поэтому решено создать распределенную сеть регистрирующих устройств. Для достижения этой цели выбраны бюджетные одноплатные компьютеры в качестве узлов проводной или беспроводной локальной сети. К такому маломощному компьютеру можно подсоединить одновременно несколько разнородных датчиков через USB разветвитель с дополнительным питанием и разнести на максимальную дальность действия USB кабеля (до 5 метров).


Железо и ПО


Для начала работы важно иметь все необходимые компоненты под рукой.


Перечислим то, что требуется:



Одноплатный компьютер Orange Pi Zero с ARM v7 с 2-х ядерным процессором,
256Мб ОЗУ и 2Gb Micro SD.



Приемопередатчик USB Ant+ Stick (далее USB стик)



Монитор (датчик) сердечного ритма HRM



USB TTL Serial преобразователь интерфейсов для связи с ПК


Итак, выбор железа состоялся. Для реализации программной части будем использовать C++ для взаимодействия с железом и Python версии 3 для сервиса. Выбор базового программного обеспечения остановим на операционной системе Linux. Вариант с использованием Android тоже вполне интересен, но несет больше риска в плане реализации. Что касается Linux для Orange Pi, то это будет Raspbian, наиболее полная и стабильная ОС для этого мини-компьютера. Все необходимые программные компоненты есть в репозитории Raspbian. Впрочем, результат работы можно будет в дальнейшем портировать на другие платформы.


Собираем все вместе и начинаем творить прототип.


Среда разработки


Для упрощения процесса разработки используем x86-64 машину с установленной Ubuntu Linux 18.04, а образ Orange Pi Zero загружаем с сайта https://www.armbian.com и в дальнейшем настраиваем для работы. Сборку проекта под целевую платформу будем производить непосредственно на одноплатнике.


Записываем полученный образ на SD карту, запускам плату, делаем первоначальную конфигурацию LAN / Wi-Fi. Устанавливаем Git, Python3 и GCC, остальное подгружаем по мере необходимости.


Структура приложения


Проведем декомпозицию программного кода, для этого разделим программную часть на уровни абстракции. На нижнем уровне расположим модуль для Python, реализованный на C++, который будет отвечать за взаимодействие ПО верхнего уровня с USB приемопередатчиком. На более высоких уровнях сетевое взаимодействие с сервером приложений. В самом простом случае это может быть WEB-сервер.


Первоначально хотел использовать готовое решение. Однако выяснилось, что большинство проектов использует библиотеку libusb, что требует изменения в образе Raspbian, в котором для данного оборудования уже есть готовый модуль ядра usb_serial_simple. Поэтому взаимодействие с железом осуществили через символьное устройство /dev/ttyUSB на скорости 115200 бод, что оказалось проще и удобнее.

Проект основан на переделке существующего открытого кода с GitHub (https://github.com/akokoshn/AntService). Код проекта был переработан и максимально упрощен для использования совместно с Python. Получившийся прототип можно найти по ссылке.


Сборка проекта будет с использованием CMake и Python Extension. На выходе получим исполняемый файл и динамическую библиотеку модуля Python.


Протокол работы ANT с HRM датчиком


Режим работы протокола ANT для HRM происходит в широковещательном режиме (Broadcast data) обмена данными по каналу между ведущим (master) HRM датчиком и ведомым (slave) USB стиком. Такой режим используется в случае, когда потеря данных не критична.


На аппаратном уровне ведомый осуществляет поиск ведущего в сети и пытается установить соединение, используя следующие характеристики: ключ сети, частоту, период опроса. Если ведущий найден, то устанавливается односторонний канал от ведущего к ведомому, приходят сообщения в виде байтовых посылок с полезной нагрузкой 8 байт на одно сообщение.


На диаграмме показан процесс установления соединения. Здесь Host управляющий компьютер, USB_stick приемопередатчик (ведомое устройство), HRM нагрудный датчик (ведущее устройство)



Последовательность действий:


  • Сброс устройства в первоначальное состояние
    • Настройка соединения
    • Активация канала
    • Периодическое чтение буфера для получения данных

Код приложения будем создавать в объектно-ориентированной парадигме, поэтому первым шагом определим список объектов:


  • Device обеспечивает соединение с драйвером операционной системы, работающим с USB приемо-передатчиком;
    • Stick реализует взаимодействие по протоколу ANT.

Список состояний, в которых могут находится объекты:


  • Device: подключен / не подключен;
  • Stick: подключен / не подключен / неопределенное состояние / инициализирован / не инициализирован.

Список методов объектов, изменяющих состояние объектов:


  • Device: подключить / отключить / отправить данные в устройство / получить данные из устройства;
  • Stick: инициализировать / установить соединение / отправить сообщение / обработать сообщение / выполнить команду.

По результатам анализа взаимодействия и выбора объектов для реализации построим диаграмму классов. Здесь Device будет абстрактным классом, реализующим интерфейс соединения с устройством.



Отправка сообщений происходит через метод do_comand, первым аргументом принимающий сообщение, а вторым обработчик результата (это может быть любой вызываемый объект).


Вот псевдокод, демонстрирующий, как использовать программу:


// Создаем объект класса Stick.Stick stick = Stick();// Создаем устройство TtyUsbDevice и передаем владение в объект класса Stick.stick.AttachDevice(std::unique_ptr<Device>(new TtyUsbDevice("/dev/ttyUSB0")));// Подключаем.stick.Connect();// Устанавливаем в исходное состояние.stick.Reset();// Инициализируем и устанавливаем соединение.stick.Init();// Получаем сообщение с датчика.ExtendedMessage msg;stick.ReadExtendedMsg(msg);

Пример использования Python модуля.


# Создаем объект класса с методом обратного вызова __call__import hrmclass Callable:    def __init__(self):        self.tries = 50    def __call__(self, json):        print(json)        self.tries -= 1        if self.tries <= 0:            return False # Stop        return True # Get next valuecall_back = Callable()# Подключаем файл устройстваhrm.attach('/dev/ttyUSB0')# Инициализируем устройствоstatus = hrm.init()print(f"Initialisation status {status}")if not status:    exit(1)# Передаем полученный объект для обработки модулемhrm.set_callback(call_back)

Здесь все просто и понятно, переходим к детальному описанию особенностей проекта.


Логирование


При разработке приложения не следует упрощать сбор логов и статистики, поэтому используются сторонние библиотеки: Glog, Boost.Log и другие. В нашем случае сборка проекта будет происходить непосредственно на устройстве, поэтому для уменьшения количества кода решено применить собственный логер.


Для отображения точки входа в область видимости и выхода используем простой макрос, который создает объект логгера на стеке. В конструкторе выводится в лог точка входа (имя С++ файла, имя метода, номер строки), в деструкторе точка выхода. В начало каждой интересуемой области видимости ставится макрос. Если логирование не требуется для всей программы, макрос определяется как пустой.


// Show debug info#define DEBUG#if defined(DEBUG)#include <string.h>class LogMessageObject{public:    LogMessageObject(std::string const &funcname, std::string const &path_to_file, unsigned line) {        auto found = path_to_file.rfind("/");        // Extra symbols make the output coloured        std::cout << "+ \x1b[31m" << funcname << " \x1b[33m["                  << (found == std::string::npos ? path_to_file : path_to_file.substr(found + 1))                  << ":" << std::dec << line << "]\x1b[0m" << std::endl;        this->funcname_ = funcname;    };    ~LogMessageObject() {        std::cout << "- \x1b[31m" << this->funcname_ << "\x1b[0m" << std::endl;    };private:    std::string funcname_;};#define LOG_MSG(msg) std::cout << msg << std::endl;#define LOG_ERR(msg) std::cerr << msg << std::endl;#define LOG_FUNC LogMessageObject lmsgo__(__func__, __FILE__, __LINE__);#else // DEBUG#define LOG_MSG(msg)#define LOG_ERR(msg)#define LOG_FUNC#endif // DEBUG

Пример работы логгера:


Attach Ant USB Stick: /dev/ttyUSB0+ AttachDevice [Stick.cpp:26]- AttachDevice+ Connect [Stick.cpp:34]+ Connect [TtyUsbDevice.cpp:46]- Connect- Connect+ reset [Stick.cpp:164]+ Message [Common.h:88]+ MessageChecksum [Common.h:77]- MessageChecksum- Message+ do_command [Stick.cpp:140]Write: 0xa4 0x1 0x4a 0x0 0xef+ ReadNextMessage [Stick.cpp:72]- ReadNextMessageRead: 0xa4 0x1 0x6f 0x20 0xea- do_command- reset+ Init [Stick.cpp:49]+ query_info [Stick.cpp:180]+ get_serial [Stick.cpp:199]+ Message [Common.h:88]+ MessageChecksum [Common.h:77]- MessageChecksum- Message+ do_command [Stick.cpp:140]Write: 0xa4 0x2 0x4d 0x0 0x61 0x8a+ ReadNextMessage [Stick.cpp:72]- ReadNextMessageRead: 0xa4 0x4 0x61 0x83 0x22 0x27 0x12 0x55- do_command- get_serial

Классы и структуры данных


Для уменьшения связности создадим абстрактный класс Device и конкретный класс TtyUsbDevice. Класс Device выступает в роли интерфейса для взаимодействия кода приложения с USB. Класс TtyUsbDevice работает с модулем ядра Linux через файл символьного устройства /dev/ttyUSB.


class Device {public:    virtual bool Read(std::vector<uint8_t> &) = 0;    virtual bool Write(std::vector<uint8_t> const &) = 0;    virtual bool Connect() = 0;    virtual bool IsConnected() = 0;    virtual bool Disconnect() = 0;    virtual ~Device() {}};

В качестве структуры данных для хранения сообщений используем std::vector<uint8_t>. Сообщение в формате ANT состоит из синхро-байта, однобайтного поля размер сообщения, однобайтного идентификатора сообщения, самих данных и контрольной суммы.


inline std::vector<uint8_t> Message(ant::MessageId id, std::vector<uint8_t> const &data){    LOG_FUNC;    std::vector<uint8_t> yield;    yield.push_back(static_cast<uint8_t>(ant::SYNC_BYTE));    yield.push_back(static_cast<uint8_t>(data.size()));    yield.push_back(static_cast<uint8_t>(id));    yield.insert(yield.end(), data.begin(), data.end());    yield.push_back(MessageChecksum(yield));    return yield;}

Класс Stick реализует протокол взаимодействия между хостом и USB стиком.


class Stick {public:    void AttachDevice(std::unique_ptr<Device> && device);    bool Connect();    bool Reset();    bool Init();    bool ReadNextMessage(std::vector<uint8_t> &);    bool ReadExtendedMsg(ExtendedMessage &);private:    ant::error do_command(const std::vector<uint8_t> &message,                          std::function<ant::error (const std::vector<uint8_t>&)> process,                          uint8_t wait_response_message_type);    ant::error reset();    ant::error query_info();    ant::error get_serial(unsigned &serial);    ant::error get_version(std::string &version);    ant::error get_capabilities(unsigned &max_channels, unsigned &max_networks);    ant::error check_channel_response(const std::vector<uint8_t> &response,                                      uint8_t channel, uint8_t cmd, uint8_t status);    ant::error set_network_key(std::vector<uint8_t> const &network_key);    ant::error set_extended_messages(bool enabled);    ant::error assign_channel(uint8_t channel_number, uint8_t network_key);    ant::error set_channel_id(uint8_t channel_number, uint32_t device_number, uint8_t device_type);    ant::error configure_channel(uint8_t channel_number, uint32_t period, uint8_t timeout, uint8_t frequency);    ant::error open_channel(uint8_t channel_number);private:    std::unique_ptr<Device> device_ {nullptr};    std::vector<uint8_t> stored_chunk_ {};    std::string version_ {};    unsigned serial_ = 0;    unsigned channels_ = 0;    unsigned networks_ = 0;};

Интерфейсная часть и реализация для удобства разделены семантически. Класс владеет единственным экземпляром типа Device, владение которым передается через метод AttachDevice.


Отправка и обработка команд происходит через вызов метода do_command, который в качестве первого аргумента принимает байты сообщения, вторым аргументом обработчик, затем тип ожидаемого сообщения. Главное требование для метода do_command заключается в том, что он должен быть точкой входа для всех сообщений и местом синхронизации. Для возможности расширения метода потребуется инкапсулировать его аргументы в новый объект сообщение. Код прототипа не является многопоточным, но подразумевает возможность переработки do_command на основе ворклетов и асинхронной обработки сообщений. Метод отбрасывает сообщения, не соответствующие ожидаемому типу. Это сделано для упрощения кода прототипа. В рабочей версии каждое сообщение будет обрабатываться асинхронно собственным обработчиком.


ant::error Stick::do_command(const std::vector<uint8_t> &message,                             std::function<ant::error (const std::vector<uint8_t>&)> check_func,                             uint8_t response_msg_type){    LOG_FUNC;    LOG_MSG("Write: " << MessageDump(message));    device_->Write(std::move(message));    std::vector<uint8_t> response_msg {};    do {        ReadNextMessage(response_msg);    } while (response_msg[2] != response_msg_type);    LOG_MSG("Read: " << MessageDump(response_msg));    ant::error status = check_func(response_msg);    if (status != ant::NO_ERROR) {        LOG_ERR("Returns with error status: " << status);        return status;    }    return ant::NO_ERROR;}

Структура ExtendedMessage, чтение расширенных сообщений.


Согласно алгоритму работы HRM датчика, данные передаются только в одну строну с использованием расширенного типа сообщения. Для прототипа используется простая схема: после открытия канала и установления соединения клиентское приложение использует метод ReadExtendedMsg для чтения расширенных сообщений.


struct ExtendedMessage {    uint8_t channel_number;    uint8_t payload[8];    uint16_t device_number;    uint8_t device_type;    uint8_t trans_type;};

bool Stick::ReadExtendedMsg(ExtendedMessage& ext_msg){/* Flagged Extended Data Message Format** | 1B   | 1B     | 1B  | 1B      | 8B      | 1B   | 2B     | 1B     | 1B    | 1B    |* |------|--------|-----|---------|---------|------|--------|--------|-------|-------|* | SYNC | Msg    | Msg | Channel | Payload | Flag | Device | Device | Trans | Check |* |      | Length | ID  | Number  |         | Byte | Number | Type   | Type  | sum   |* |      |        |     |         |         |      |        |        |       |       |* | 0    | 1      | 2   | 3       | 4-11    | 12   | 13,14  | 15     | 16    | 17    |*/    LOG_FUNC;    std::vector<uint8_t> buff {};    device_->Read(buff);    if (buff.size() != 18 or buff[2] != 0x4e or buff[12] != 0x80) {        LOG_ERR("This message is not extended data message");        return false;    }    ext_msg.channel_number = buff[3];    for (int j=0; j<8; j++) {        ext_msg.payload[j] = buff[j+4];    };    ext_msg.device_number = (uint16_t)buff[14] << 8 | (uint16_t)buff[13];    ext_msg.device_type = buff[15];    ext_msg.trans_type = buff[16];    return true;}

Модуль hrm


Для создания в Python модуля hrm, предназначенного для работы с ANT, воспользуемся distutils. Создадим два файла: setup.py (для сборки) и hrm.cpp, в котором находится исходный код модуля.


Сборку всего модуля опишем в файле setup.py через создание объект типа Extension. Для сборки вызовем функцию setup над этим объектом.


from distutils.core import setup, Extensionhrm = Extension('hrm',                language = "c++",                sources = ['hrm.cpp', '../src/TtyUsbDevice.cpp', '../src/Stick.cpp'],                extra_compile_args=["-std=c++17"],                include_dirs = ['../include'])setup(    name        = 'hrm',    version     = '1.0',    description = 'HRM python module',    ext_modules = [hrm])

Рассмотрим исходный код модуля.


Объект класса Stick храним в глобальной переменной


static std::shared_ptr<Stick> stick_shared

Далее создаем две структуры типа PyMethodDef и PyModuleDef и инициализируем модуль.


Для работы с USB стиком в Python создадим три функции:


  • attach для подключения файла символьного устройства;
    • init для инициализации соединения;
    • set_callback для установки функции обратного вызова обработки расширенных сообщения.

Теперь можно обобщить и сделать некоторые выводы


В результате разработки прототипа получилось простое приложение, с помощью которого можно производить проверку работы датчиков в тренажерном зале. Следующим шагом будет реализация технологии работы с сервером приложений.


Для проведения эксперимента по реализации бизнес-идеи не потребовалось использовать большое количество ресурсов и кода. Код приложения специально сделан упрощенным и линейным в первую очередь для уменьшения количества ошибок и демонстрации принципов работы с ANT.


Как результат работы приведу простой алгоритм моих действий для выполнения поставленной задачи:


  1. Понять суть задачи, сформулировать цели, подготовить техническое задание.
  2. Выполнить поиск готовых проектов, разобраться с лицензиями. Найти документацию о протоколах и стандартах. Понять алгоритм работы устройства.
  3. Найти необходимое оборудование, исходя из цены, доступности и технических возможностей.
  4. Продумать архитектуру приложения, выбрать среду разработки.
  5. Реализовать код приложения, заранее продумать критерии, например такие:
    код прототипа сделать однопоточным;
    использовать последний стандарт C++ 17 и стандартную библиотеку, использовать RAII;
    разделить интерфейс и реализацию семантически: методы, относящиеся к интерфейсу, называть в стиле CamelCase, а имена методов, отвечающих за реализацию, в стиле under_score, поля класса в стиле underscore;
    логирование.
  6. Протестировать проект.

Всем удачи во всех начинаниях!

Подробнее..

Разработка firmware на С словно игра в бисер. Как перестать динамически выделять память и начать жить

07.04.2021 10:06:45 | Автор: admin

C++ is a horrible language. It's made more horrible by the fact that a lotof substandard programmers use it, to the point where it's much mucheasier to generate total and utter crap with it.

Linus Benedict Torvalds

Собеседование шло уже второй час. Мы наконец-то закончили тягучее и вязкое обсуждение моей скромной персоны, и фокус внимания плавно переполз на предлагаемый мне проект. Самый бойкий из трех моих собеседников со знанием дела и без лишних деталей принялся за его описание. Говорил он быстро и уверенно явно повторяет весь этот рассказ уже не первый раз. По его словам, работа велась над неким чрезвычайно малым, но очень важным устройством на базе STM32L4. Потребление энергии должно быть сведено к минимуму... USART... SPI... ничего необычного, уже неоднократно слышал подобное. После нескольких убаюкивающих фраз собеседник внезапно подался чуть вперед и, перехватив мой сонный взгляд, не без гордости произнес:

А firmware мы пишем на C++! мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

Я попытался причудливо изогнуть бровь, иллюстрируя мое удивление, но, видимо, перестарался. Скромно сидевший слева болезненного вида молодой человек, очевидно, принял это за гримасу ужаса и отвращения.

У вас есть какие-то опасения? поспешил спросить он с искренней озабоченностью в голосе.

Опасения у меня всегда имеются в избытке. Я с ностальгическим умилением вспомнил, как писал первую прошивку на MbedOS для одной из демонстрационных плат. Память тогда закончилась быстрее, чем я успел моргнуть светодиодом второй раз. Эх, опыт, сын ошибок... Надо все же признать, что страшные темные времена, когда о прошивке на плюсах никто и не заикался, давно прошли. Все разумные возражения в настоящее время рассыпались в труху и превратились в мифы. Вроде бы...

Ну, вы знаете ничего такого, я вообще толерантный я замялся, не желая никого обижать. - Но на плюсах легко сделать что-нибудь этакое, и прошивка в один миг опухает, как мой мозг при прочтении произведений Германа Гессе.

Да, такая опасность есть, но каждый в нашей команде невероятно квалифицирован, пробасил молчавший до этого момента будущий коллега справа и высоко поднял указательный палец, акцентируя внимание на этом фантастическом факте.

И у нас есть код ревью! встрепенувшись, поспешили добавить хором двое других невероятно квалифицированных члена команды.

Да, я согласился участвовать в проекте. Но кто бы смел отказаться, придавленный столь весомыми аргументами?

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", наивно думал я, "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько не уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала Игры престолов.

Можно справедливо заметить, что всему виной сложные шаблонные конструкции. Да, но у GCC с пониманием аналогичных шаблонов никогда не было проблем.

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

Проще говоря, если от вашего устройства зависят жизни людей, то при его разработке нужно придерживаться определенных правил. Одно из них это отсутствие динамического распределения памяти, по крайней мере, после вызова функции main. Думаю, все знают, что в no-OS устройствах динамические аллокации чреваты проблемами, вроде фрагментации памяти и т.п.

Одно только это правило привело ко всем последующим значительным изменениям в коде, о которых речь пойдет ниже.

std::exception

Несмотря на утверждение Герба Саттера, что без исключений C++ уже перестает быть тем языком, который мы знаем и любим, они были беспощадно выпилены. Немудрено, ведь обычный механизм исключений использует динамическое выделение памяти, что недопустимо в нашем проекте.

Не будем вдаваться в подробности, как именно обрабатываются исключения в С++, полагаю, это всем известно и так. Если разложить процесс на элементарные функции, то можно найти среди них такую:

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе.Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

Те же самые требования безопасности ограничивает и использование векторов. Не секрет, что этот контейнер из стандартной библиотеки активно использует динамическое выделение памяти. Сам процесс может незначительно отличаться в зависимости от компилятора, но аллокация есть, факт.

Все вектора подлежали замене на std::array, но были особо сложные случаи, которые использовали возможности векторов по полной и были очень хитро вплетены в код. Для таких случаев можно написать простой аллокатор, который позволит изобразить вектор из стандартного массива.

template <class T, std::size_t Size>class StaticArray { using ssize_t = int;public: using value_type = T; template <class U> struct rebind {   using other = StaticArray<U, Size>; }; StaticArray() = default; ~StaticArray() = default; template <class U, std::size_t S> StaticArray(const StaticArray<U, S>&); auto allocate(std::size_t n) -> value_type*; auto deallocate(value_type* p, std::size_t n) -> void; auto max_size() const -> std::size_t;};

Ключевые функции, конечно, allocate и deallocate. Передаваемый им параметр n это не размер в байтах, а размер в попугаях, которые хранятся в векторе. Функция max_size используется при проверке вместимости аллокатора и возвращает максимально возможное теоретически число, которое можно передать в функцию allocate.

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;    v.push_back(1000);std::cout<<"check size "<<v.size()<<std::endl;    v.push_back(2000);std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

Конечно, контролировать управление памятью путем использования кастомных аллокаторов вполне возможно. В стандартной библиотеке есть замечательная функция std::allocate_shared, которая создаст разделяемый объект именно там, где мы укажем. Указать же можно самолепным аллокатором примерно такого вида:

template <class Element,           std::size_t Size,           class SharedWrapper = Element>class StaticSharedAllocator {  public:  static constexpr std::size_t kSize = Size;  using value_type = SharedWrapper;  using pool_type = StaticPool<Element, kSize>;  pool_type &pool_;  using ElementPlaceHolder = pool_type::value_type;  template <class U>  struct rebind {    using other = StaticSharedAllocator<Element, kSize, U>;  };  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}  ~StaticSharedAllocator() = default;  template <class Other, std::size_t OtherSize>  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other)     : pool_{other.pool_} {}  auto allocate(std::size_t n) -> value_type * {    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);      return reinterpret_cast<value_type *>(pool_.allocate(n));  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(reinterpret_cast<value_type *>(p), n);  }};

Очевидно, Element тип целевого объекта, который и должен храниться как разделяемый объект. Size максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

Лично я не нашел ничего лучше, чем просто резервировать немного больше места в хранилище или пулле объектов. Размер дополнительного пространства подобран эмпирически, ибо разным компиляторам нужен разный объем дополнительной памяти.

Само хранилище создается отдельно и передается в конструктор аллокатора именно потому, что allocate_shared работает не напрямую с переданным аллокатором, а с его копией. Поскольку этой функции нужен аллокатор, работающий с расширенным типом. Шаблонный конструктор копий тоже задействован, а пул объектов должен работать с любым из этих аллокаторов.

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>struct StaticPool {  static constexpr size_t kSize = Size;  static constexpr size_t kSizeOverhead = 48;  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead,                                             alignof(std::max_align_t)>;  StaticArray<value_type, Size> pool_;    auto allocate(std::size_t n) -> value_type * {    return pool_.allocate(n);  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(p, n);  }};

А теперь небольшой пример, как это все работает вместе:

struct Object {  int index;};constexpr size_t kMaxObjectNumber = 10u;StaticPool<Object, kMaxObjectNumber> object_pool {};StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};std::shared_ptr<Object> MakeObject() {  return std::allocate_shared<Object>(object_alloc_);}

Все эти изуверства позволили избежать динамического выделения памяти и здесь. Хотя, мне кажется, лучше реорганизовать код так, чтоб не было необходимости использовать shared_ptr вообще. К сожалению, не всегда рефакторинг можно сделать быстро и безболезненно.

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};    auto sum = [=] () -> int {      int sum = x[0];      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {        sum += x[i];      }      return sum;    };        std::function<int()> callback = sum; 

Когда элементов массива 5, то размер функции 20 байт. В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

Дело в том, что в классе нашей универсальной обертки содержится небольшой участок памяти (place holder), где может быть определена содержащаяся функция.

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

text

data

bss

67880

2496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

Внутри std::function явно вызываются исключения, которые внутри себя используют demangling для имен функций, и это дорогое удовольствие. Нужно ли это в небольших устройствах? Скорее всего, любоваться красивыми именами не придется.

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440cplus_demangle_operators0000049e__gxx_personality_v0000004c4 d_encoding000004fed_exprlist00000574_malloc_r0000060cd_print_mod000007f0d_type00000eec_dtoa_r00001b36_svfprintf_r0000306cd_print_comp

Много функций с префиксом d_* функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free функции для работы с динамическим выделением памяти.

Результат ожидаемый флаги -fno-exceptions и -fno-rtti не спасают.

Внедрим второй подобный функциональный объект в другой единице трансляции:

text

data

bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

Показательно также то, сколько объектных файлов и из каких библиотек мы используем для этих случаев.

Для случая без std::function список короткий
libc_nano.alibg_nano.alibg_nano.a(lib_a-exit.o)libg_nano.a(lib_a-exit.o) (_global_impure_ptr)libg_nano.a(lib_a-impure.o)libg_nano.a(lib_a-init.o)libg_nano.a(lib_a-memcpy-stub.o)libg_nano.a(lib_a-memset.o)libgcc.alibm.alibstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.alibg.alibg.a(lib_a-__atexit.o)libg.a(lib_a-__call_atexit.o)libg.a(lib_a-__call_atexit.o) (__libc_fini_array)libg.a(lib_a-__call_atexit.o) (atexit)libg.a(lib_a-abort.o)libg.a(lib_a-abort.o) (_exit)libg.a(lib_a-abort.o) (raise)libg.a(lib_a-atexit.o)libg.a(lib_a-callocr.o)libg.a(lib_a-closer.o)libg.a(lib_a-closer.o) (_close)libg.a(lib_a-ctype_.o)libg.a(lib_a-cxa_atexit.o)libg.a(lib_a-cxa_atexit.o) (__register_exitproc)libg.a(lib_a-dtoa.o)libg.a(lib_a-dtoa.o) (_Balloc)libg.a(lib_a-dtoa.o) (__aeabi_ddiv)libg.a(lib_a-exit.o)libg.a(lib_a-exit.o) (__call_exitprocs)libg.a(lib_a-exit.o) (_global_impure_ptr)libg.a(lib_a-fclose.o)libg.a(lib_a-fflush.o)libg.a(lib_a-findfp.o)libg.a(lib_a-findfp.o) (__sread)libg.a(lib_a-findfp.o) (_fclose_r)libg.a(lib_a-findfp.o) (_fwalk)libg.a(lib_a-fini.o)libg.a(lib_a-fputc.o)libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)libg.a(lib_a-fputc.o) (__sinit)libg.a(lib_a-fputc.o) (_putc_r)libg.a(lib_a-fputs.o)libg.a(lib_a-fputs.o) (__sfvwrite_r)libg.a(lib_a-freer.o)libg.a(lib_a-fstatr.o)libg.a(lib_a-fstatr.o) (_fstat)libg.a(lib_a-fvwrite.o)libg.a(lib_a-fvwrite.o) (__swsetup_r)libg.a(lib_a-fvwrite.o) (_fflush_r)libg.a(lib_a-fvwrite.o) (_free_r)libg.a(lib_a-fvwrite.o) (_malloc_r)libg.a(lib_a-fvwrite.o) (_realloc_r)libg.a(lib_a-fvwrite.o) (memchr)libg.a(lib_a-fvwrite.o) (memmove)libg.a(lib_a-fwalk.o)libg.a(lib_a-fwrite.o)libg.a(lib_a-impure.o)libg.a(lib_a-init.o)libg.a(lib_a-isattyr.o)libg.a(lib_a-isattyr.o) (_isatty)libg.a(lib_a-locale.o)libg.a(lib_a-locale.o) (__ascii_mbtowc)libg.a(lib_a-locale.o) (__ascii_wctomb)libg.a(lib_a-locale.o) (_ctype_)libg.a(lib_a-localeconv.o)libg.a(lib_a-localeconv.o) (__global_locale)libg.a(lib_a-lock.o)libg.a(lib_a-lseekr.o)libg.a(lib_a-lseekr.o) (_lseek)libg.a(lib_a-makebuf.o)libg.a(lib_a-makebuf.o) (_fstat_r)libg.a(lib_a-makebuf.o) (_isatty_r)libg.a(lib_a-malloc.o)libg.a(lib_a-mallocr.o)libg.a(lib_a-mallocr.o) (__malloc_lock)libg.a(lib_a-mallocr.o) (_sbrk_r)libg.a(lib_a-mbtowc_r.o)libg.a(lib_a-memchr.o)libg.a(lib_a-memcmp.o)libg.a(lib_a-memcpy.o)libg.a(lib_a-memmove.o)libg.a(lib_a-memset.o)libg.a(lib_a-mlock.o)libg.a(lib_a-mprec.o)libg.a(lib_a-mprec.o) (_calloc_r)libg.a(lib_a-putc.o)libg.a(lib_a-putc.o) (__swbuf_r)libg.a(lib_a-readr.o)libg.a(lib_a-readr.o) (_read)libg.a(lib_a-realloc.o)libg.a(lib_a-reallocr.o)libg.a(lib_a-reent.o)libg.a(lib_a-s_frexp.o)libg.a(lib_a-sbrkr.o)libg.a(lib_a-sbrkr.o) (_sbrk)libg.a(lib_a-sbrkr.o) (errno)libg.a(lib_a-signal.o)libg.a(lib_a-signal.o) (_kill_r)libg.a(lib_a-signalr.o)libg.a(lib_a-signalr.o) (_getpid)libg.a(lib_a-signalr.o) (_kill)libg.a(lib_a-sprintf.o)libg.a(lib_a-sprintf.o) (_svfprintf_r)libg.a(lib_a-stdio.o)libg.a(lib_a-stdio.o) (_close_r)libg.a(lib_a-stdio.o) (_lseek_r)libg.a(lib_a-stdio.o) (_read_r)libg.a(lib_a-strcmp.o)libg.a(lib_a-strlen.o)libg.a(lib_a-strncmp.o)libg.a(lib_a-strncpy.o)libg.a(lib_a-svfiprintf.o)libg.a(lib_a-svfprintf.o)libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)libg.a(lib_a-svfprintf.o) (__aeabi_dmul)libg.a(lib_a-svfprintf.o) (__aeabi_dsub)libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)libg.a(lib_a-svfprintf.o) (__ssprint_r)libg.a(lib_a-svfprintf.o) (_dtoa_r)libg.a(lib_a-svfprintf.o) (_localeconv_r)libg.a(lib_a-svfprintf.o) (frexp)libg.a(lib_a-svfprintf.o) (strncpy)libg.a(lib_a-syswrite.o)libg.a(lib_a-syswrite.o) (_write_r)libg.a(lib_a-wbuf.o)libg.a(lib_a-wctomb_r.o)libg.a(lib_a-writer.o)libg.a(lib_a-writer.o) (_write)libg.a(lib_a-wsetup.o)libg.a(lib_a-wsetup.o) (__smakebuf_r)libgcc.alibgcc.a(_aeabi_uldivmod.o)libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)libgcc.a(_arm_addsubdf3.o)libgcc.a(_arm_cmpdf2.o)libgcc.a(_arm_fixdfsi.o)libgcc.a(_arm_muldf3.o)libgcc.a(_arm_muldivdf3.o)libgcc.a(_arm_unorddf2.o)libgcc.a(_dvmd_tls.o)libgcc.a(_udivmoddi4.o)libgcc.a(libunwind.o)libgcc.a(pr-support.o)libgcc.a(unwind-arm.o)libgcc.a(unwind-arm.o) (__gnu_unwind_execute)libgcc.a(unwind-arm.o) (restore_core_regs)libm.alibnosys.alibnosys.a(_exit.o)libnosys.a(close.o)libnosys.a(fstat.o)libnosys.a(getpid.o)libnosys.a(isatty.o)libnosys.a(kill.o)libnosys.a(lseek.o)libnosys.a(read.o)libnosys.a(sbrk.o)libnosys.a(write.o)libstdc++.alibstdc++.a(atexit_arm.o)libstdc++.a(atexit_arm.o) (__cxa_atexit)libstdc++.a(class_type_info.o)libstdc++.a(cp-demangle.o)libstdc++.a(cp-demangle.o) (memcmp)libstdc++.a(cp-demangle.o) (realloc)libstdc++.a(cp-demangle.o) (sprintf)libstdc++.a(cp-demangle.o) (strlen)libstdc++.a(cp-demangle.o) (strncmp)libstdc++.a(del_op.o)libstdc++.a(del_ops.o)libstdc++.a(eh_alloc.o)libstdc++.a(eh_alloc.o) (std::terminate())libstdc++.a(eh_alloc.o) (malloc)libstdc++.a(eh_arm.o)libstdc++.a(eh_call.o)libstdc++.a(eh_call.o) (__cxa_get_globals_fast)libstdc++.a(eh_catch.o)libstdc++.a(eh_exception.o)libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))libstdc++.a(eh_exception.o) (__cxa_pure_virtual)libstdc++.a(eh_globals.o)libstdc++.a(eh_personality.o)libstdc++.a(eh_term_handler.o)libstdc++.a(eh_terminate.o)libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())libstdc++.a(eh_terminate.o) (__cxa_begin_catch)libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)libstdc++.a(eh_terminate.o) (__gxx_personality_v0)libstdc++.a(eh_terminate.o) (abort)libstdc++.a(eh_throw.o)libstdc++.a(eh_type.o)libstdc++.a(eh_unex_handler.o)libstdc++.a(functional.o)libstdc++.a(functional.o) (std::exception::~exception())libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)libstdc++.a(functional.o) (operator delete(void*))libstdc++.a(functional.o) (__cxa_allocate_exception)libstdc++.a(functional.o) (__cxa_throw)libstdc++.a(pure.o)libstdc++.a(pure.o) (write)libstdc++.a(si_class_type_info.o)libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)libstdc++.a(tinfo.o)libstdc++.a(tinfo.o) (strcmp)libstdc++.a(vterminate.o)libstdc++.a(vterminate.o) (__cxa_current_exception_type)libstdc++.a(vterminate.o) (__cxa_demangle)libstdc++.a(vterminate.o) (fputc)libstdc++.a(vterminate.o) (fputs)libstdc++.a(vterminate.o) (fwrite)

А что IAR?

Все устроено немного иначе. Он не требует явного указания спецификации nano или nosys, ему не нужны никакие заглушки. Этот компилятор все знает и сделает все в лучшем виде, не нужно ему мешать.

text

ro data

rw data

2958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o 1'404 496

heaptramp0.o 4

Конечно же, появились функции и данные для работы с динамической памятью, как в случае GCC.

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

Объектный файл того юнита, в котором была добавлена функция, тоже ощутимо располнел:

до

main.cpp.obj 3'218 412 36'924

после

main.cpp.obj 4'746 451 36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

text

ro data

rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

Убедившись, что моя примитивная реализация function работает и не требует много памяти, перенес ее в проект, заменив все std::function, до которых смог дотянуться. С чувством выполненного долга я отправился на набережную любоваться закатом.

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;static auto global_handlers = std::pair<Handler, Handler> {};

И это в заголовочном файле. Насколько я смог понять, эти функции использовались для создания бледного подобия shared_ptr, увеличивая и уменьшая счетчик ссылок на ресурс в конструкторе и деструкторе соответственно. В некотором роде такой подход даже работал, только для каждой единицы трансляции отдельно. Впрочем, это почти нигде уже не использовалось, но за каждое включение заголовочного файла приходилось платить примерно одним килобайтом памяти. А включений было предостаточно. Да, в релизной версии почти все вырезалось, но в отладочной оптимизация выключена и компилятор честно создавал все эти бесполезные объекты.

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

Давайте же наглядно продемонстрируем как можно получить несколько одинаковых символов в финальном файле. Ну, если кто-то еще сомневается, конечно.

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение cglob; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

С++ может быть прекрасным языком, если не подпускать к нему посредственных разработчиков. Поэтому я сейчас со статьей заканчиваю, а то пол в офисе не успею домыть.

Всем спасибо, всем удачи!

Подробнее..

Как пять лет просидеть в саппорте и за две недели стать Python-тестировщиком

03.12.2020 14:20:34 | Автор: admin
Да-да, это будет еще одна статья про Python. Тот самый язык, который считается одним из наиболее популярных для изучения и использования. Статья будет полезна тем, кто еще только задумывается об изучении Python или делает первые шаги. Я попытаюсь описать свой опыт по изучению языка, поделюсь личными приемами, подскажу полезные и наиболее эффективные ресурсы, а также обозначу, на что бесполезно тратить время.

image


Почему я решила изучать Python


Меня зовут Маша. Мне 28 годиков, 6 из которых я провела в обычной сфере обслуживания, а еще 5 в сфере обслуживания с техническим уклоном (простыми словами саппорт). Надо ли говорить, насколько за эти годы я устала от однообразия задач? И вот, в один прекрасный момент я загорелась идеей кардинально поменять свою жизнь, для чего была поставлена цель перейти в тестировщики с применением автоматизации на Python.

Можно было бы долго прокрастинировать и маленькими шажками заставлять себя двигаться к смене деятельности, но я выбрала жесткий вариант сразу попробовать найти работу.

Вы не обязаны оставаться тем же человеком, каким были год назад, месяц назад или даже день назад. Вы здесь для того, чтобы постоянно творить себя. (Ричард Фейнман, известный ученый-физик)


Помимо самого желания сменить сферу деятельности, у меня все же был некоторый полезный багаж, а именно: высшее образование по специальности Информатика и вычислительная техника и предыдущий опыт работы инженером технической поддержки, где я тоже не стояла на месте и пыталась развиваться. У меня был стандартный набор знаний по HTTP, SQL, XML, а также небольшой опыт работы с PHP, Kotlin в связке с Selenium Webdriver. Кроме этого, я изучала теоретические основы по тестированию и пыталась их применять в работе, выполняя небольшие дополнительные задания.

Итак, цель поставлена: за две недели с нуля максимально эффективно изучить Python.

С чего все обычно начинают


Будучи дитём девяностых, я начала с запроса в поисковой системе. Пролистав блок с рекламой (к нему я вернусь чуть позже), я принялась штудировать многочисленные статьи с подборками ресурсов по изучению Python. Каждая статья состоит минимум из 10-15 отборных и наилучших ресурсов, которые обязательно нужно использовать. Много статей просто перечисляют шедевры классики по Python, и читай их потом годами.

Я честно пыталась осилить несколько лучших книг для новичков по Python, однако через десяток страниц мне становилось откровенно скучно. Информация совершенно не хотела укладываться в моей голове. Признаюсь, ни одна из начатых мною книг до сих пор не прочитана до конца.

Мой совет на начальном этапе не тратить время на изучение книг. Огромный объем информации, представленный в них, без практики не усваивается. В качестве справочников удобно использовать такие онлайн-ресурсы, как python.org, pythonworld.ru и подобные, где в структурированном виде можно найти необходимую информацию с примерами использования.

Хорошие практики


Моим следующим шагом стало изучение интерактивных курсов. Из множества предложений по бесплатным интерактивным курсам и ресурсам я выбрала несколько, которыми хочу поделиться с вами. Их будет не десяток, как обычно, а всего лишь три. Это те ресурсы, которые я лично испробовала на себе и которые дали наиболее эффективный результат. Их можно освоить как раз за две недели, проходя один за одним или параллельно.

  • praktikum.yandex.ru/data-analyst и praktikum.yandex.ru/backend-developer два моих первых курса, будем считать их одним целым.

    Отличное начало, которое показало, что изучение языка может быть нескучным занятием. На этих курсах без занудства дается теоретическая часть, которую затем нужно сразу же применить практически при написании кода.
    Честно признаюсь, что я прошла только бесплатную часть курсов. Этого все равно оказалось достаточно, чтобы вдохновиться на последующее более углубленное изучение языка.
    Очень советую заглянуть на Практикум. Полезные курсы там могут найти не только начинающие. И это не реклама, а действительно мое честное мнение.
  • checkio.org англоязычный ресурс с выполнением задач в виде игры. Значительная часть задач имеет перевод на русский язык. Потрясающая возможность увидеть элегантные решения через код-ревью.

    Обычно при прохождении курсов я считала важным только выполнить полученную задачу, чтоб хоть как-то работало. Однако, chekio.org после выполнения задачи предложил мне оценить решения других учеников, где я неожиданно для себя сделала открытие, что одну и ту же простенькую задачу можно решить намного короче, элегантно используя конструкции языка.
  • hackerrank.com тоже англоязычный ресурс с преподнесением теории небольшими порциями и актуальными задачками. Запомнился четкой постановкой задач и очень строгими требованиями для зачета прохождения задачи схалтурить не получится!


При прохождении этих курсов я рекомендую не пропускать задания, даже если задача кажется элементарной или что-то подобное было изучено ранее. Повторение мать учения, а если у вас такая же плохая память, как у меня, это позволит запомнить моменты, которые не усвоились с первого раза. Также в разных курсах одна и та же тема может быть раскрыта по-разному. Например, один автор описал применение одной половины методов строк и подобрал соответствующие задания, а в другом курсе более подробно рассмотрены другие методы строк.

Самый лучший способ


Итак, потратив немало времени на разные курсы, я сделала для себя простой, но важный вывод. Хочешь изучить Python найди ему применение, пиши код каждый день. Если ваша работа хоть как-то связана с обработкой данных или IT-сферой, попробуйте найти рутинную задачу, которую можно решить с помощью Python. Даже если написание скрипта займет намного больше времени, чем само выполнение действия, это уже будет большая победа. Как говорят в шутку, если действие занимает больше 1.5 секунды вашего времени напишите для него скрипт.

image

Например, мне на работе периодически попадалась задача, в которой нужно в веб-форме заполнить пару полей. При небольшом количестве данных каждый раз казалось проще сделать это руками, чем приступать к автоматизации этого процесса. Но когда суммарное количество заполняемых полей переваливало за десятки, я решилась написать скрипт на Python, который бы забирал данные из csv файла и с помощью методов Selenium за меня заполнял веб-форму. Мне это успешно удалось, что было приятно отмечено начальством.

Второй пример приведу из личной жизни. Помимо языка программирования, мне требуется поддерживать уровень английского языка. Моя личная проблема я плохо запоминаю слова. Примерно как открыл-закрыл холодильник и сразу же забыл его содержимое, так же и у меня встретила слово, посмотрела его значение в словаре и забыла перевод через минуту.

Я решила совместить два моих минуса плохое запоминание английских слов и неидеальное владение Python и получить из них плюс. Был написан скрипт, который выводит рандомные слова из заранее подготовленного списка и проверяет введенный мной перевод.

Моя рекомендация прямо сейчас выбрать для себя задачу с работы или из личной жизни, которую можно автоматизировать через Python, и сразу же после завершения чтения статьи приступить к ее реализации. Кстати, наличие подобных собственных наработок в портфолио очень ценится работодателем при оценке кандидатов.

Второй отличный способ максимально эффективного изучения использовать Python на работе. Будь то проявление инициативы на текущей работе и взятие новых задач, смена должности или даже поиск новой.

Что показалось менее эффективным


Как бы не хотелось это признавать, но изучение Python по книгам не лучшая идея. Я люблю чтение, в нем много плюсов, но для начального этапа изучения языка это не эффективно. Обычно в книгах дается большой объем знаний без применения его на практике. Можно прочитать сотни страниц с описанием типов, примерами кода, лучшими практиками, но без самостоятельного написания кода это не имеет смысла. Например, может ли человек, только прочтя анатомический справочник, сразу же стать врачом и выполнить хотя бы простенькую хирургическую операцию?

После чтения книг у меня появилась идея в наш цифровой век воспользоваться визуальными способами потребления информации, для чего я полезла в YouTube для поиска видео или видеоканала, максимально полезного для изучения Python. К сожалению, идеальный для себя канал или серию видео я так и не нашла. Если искать конкретную тему или ответ на вопрос, то можно найти что-то полезное, но просмотр обучающего видео одного за другим в итоге снова привел к проблеме огромного количества информации, которое не применяется на практике. При просмотре видео становится лень повторять то, что ты и так уже видишь на экране. А после борьбы с ленью обязательно нужно расслабиться и включить какое-нибудь видео на отвлеченную тему. Всё, процесс обучения скатился в отдых. Тем не менее, если вы знаете отличные видео о Python на YouTube, просьба поделиться ссылками.

Следующий способ, который я попробовала и посчитала пустой тратой времени пробное занятие в виде вебинара от одной известной онлайн-школы. В анонсе обещалось, что всего лишь за три урока я смогу написать свой собственный мессенджер. За полтора часа первого занятия на меня вылилось столько воды, что я чуть не утонула. Я узнала обо всех плюсах онлайн-школы, удобстве рассрочки оплаты, послушала счастливые истории выпускников курса и стандартный текст-введение о том, почему Python так хорош. К программированию или разбору полезной информации мы так и не приступили. Было очень жалко потерять столько времени, поэтому я даже не стала далее рассматривать другие предложения от онлайн-школ.

Мое мнение о платных курсах


Когда начинаешь искать информацию по Python, в различных местах постоянно мелькают рекламные предложения о прохождении платных курсов. Их стоимость варьируется от нескольких тысяч до нескольких десятков тысяч рублей. Дается обещание, что по окончании курса ты сразу же станешь продвинутым программистом Python, найдешь работу мечты с высокой зарплатой, и вообще всё в жизни изменится к лучшему.

Почему я не включила платные курсы в списки хороших или плохих практик? Да потому что я ими не пользовалась. Кто ищет, тот всегда найдет, а на первое время достаточно только того, что можно найти бесплатно. Что касается платных онлайн-курсов, то в своем большинстве они предлагают изучение того, что и так доступно бесплатно.

Продвинутые онлайн-курсы я на начальном этапе не рассматриваю среди них вполне могут найтись достойные кандидаты, которые помогут не только приобрести знания, но и сертификаты, ценящиеся в IT-среде. Наличие ачивок в любом случае показывает заинтересованность темой и способность к самообучению. Предлагаю в комментариях поделиться ссылками на ваши лично рекомендуемые курсы для следующего уровня после начального.

Платные очные курсы я не рассматривала, хотя предполагаю, что они могут быть очень полезны. Спустя годы, вспоминая опыт общеобразовательной школы и института, я понимаю, что очные занятия пожалуй, самая эффективная форма обучения. Но во взрослой жизни на первое место выходят другие факторы. Моя личная причина я не могла выделить в своем расписании свободное время на регулярное посещение очных занятий.

Обучение должно быть в радость


Это главный совет, который мне хотелось бы дать. Только это поможет не растерять запал, благодаря которому мне удалось за две недели достичь максимального результата в изучении Python с нуля.

Сейчас, даже устроившись на новую работу, связанную с автоматизацией процесса тестирования, я не останавливаюсь на достигнутом и продолжаю погружение в Python, шаг за шагом, с каждой новой поставленной задачей.

Тем не менее, для будущих питонистов в заключении я поделюсь некоторыми практическими советами, ведь изученная выше теория почти бесполезна без применения ее на практике.

  • Совет 1
    Не ограничивайте себя поиском ответов на возникшие вопросы только в том материале, который был получен во время онлайн-уроков. Время от времени пытайтесь сформулировать свой вопрос (в этом может помочь отличное пособие sitengine.ru//smart-question-ru.html) и найти ответ самостоятельно в интернете. Поверьте, в дальнейшем такие ситуации будут возникать все чаще и чаще, а умение находить нужные данные в современном информационном потоке одно из главных умений будущего программиста и тестировщика.
  • Совет 2
    Разберитесь в типах данных и их методах. Запомните, какие есть типы данных в Python и какие у них особенности. При написании собственных скриптов это поможет вам лучше понять, какой тип данных подходит под конкретный случай. Также не стесняйтесь проверять, а не существует ли нужный метод у типа данных или уже готовая библиотека для выполнения какого-либо действия.
  • Совет 3
    Заранее перед написанием кода составьте примерный план, схему или описание работы скрипта. Это позволит сэкономить огромное количество времени и сил по сравнению с подходом, когда вы сразу начинаете писать код и уже в процессе написания приходится нагромождать дополнительные фичи.
  • Совет 4
    Проверяйте код малыми порциями. Ошибаются и делают опечатки абсолютно все, поэтому я советую при написании кода как можно чаще делать проверку, что код работает так, как нужно. У меня это происходит так: я запускаю код и делаю проверку после того, как доделаю очередной блок связанного кода. Например, блок получения или обработки данных, блок с условным оператором (if) или циклом (for, while).
  • Совет 5
    Не останавливайтесь в своем развитии и стремлении получить больше знаний, чем есть сейчас. Продолжайте изучать новую информацию и шлифовать свое мастерство. Посещайте тематические конференции и вебинары. Попробуйте сходить на собеседования, чтобы получить независимую оценку со стороны. Кстати, на этом этапе могут помочь книги, о которых я, возможно, продолжу речь в своей следующей статье.


Надеюсь, что эта статья поможет сэкономить время и другие ресурсы таким же новичкам, как я в прошлом.
Подробнее..

Robot Framework для автоматизации тестирования ограничения и плюшки

01.03.2021 14:22:08 | Автор: admin

В автоматизации тестирования я уже более 11 лет. Скажу сразу, что являюсь поклонником старомодного тестирования на Java и очень настороженно отношусь к различным готовым фреймворкам. Если вы придерживаетесь такого же мнения или только задумываетесь об использовании Robot Framework, в этой статье я постараюсь рассказать вам о его ограничениях и, конечно же, опишу все его достоинства.

Я столкнулся с Robot Framework около года назад. Перед нами стояла задача силами двух инженеров автоматизировать довольно большой объем тестов в сжатые сроки, т.к. ручная регрессия перестала влезать в разумные рамки. Сам проект связан с пожарной безопасностью. Тестировать предстояло Web-часть в трех браузерах и Mobile-часть на множестве iOS и Android телефонов и планшетов. Помимо этого, в наличии были тесты, которые взаимодействовали и с Web, и с Mobile. Конечно, это не ракету построить, но и не совсем тривиально. Честно скажу, я сопротивлялся, мы долго думали и в итоге, по совокупности внутренних и внешних факторов, выбрали Robot Framework.

Пара слов и картиночек для знакомства с Robot Framework

Прежде чем разбирать плюсы и минусы, давайте очень коротко поговорим о том, что же такое Robot Framework. Возможно, кто-то впервые видит это название.

Robot Framework это keyword-driven фреймворк, разработанный специально для автоматизации тестирования. Он написан на Python, но для написания тестов обычно достаточно использовать готовые ключевые слова (кейворды), заложенные в этом фреймворке, не прибегая к программированию на Python. Нужно лишь загрузить необходимые библиотеки, например, SeleniumLibrary, и можно писать тест. В этой статье я дам общее представление о Robot Framework, но если после прочтения вы захотите углубиться в тему, то советую обратиться к официальной документации. В конце статьи также приведены ссылки на популярные библиотеки.

Что ж, перейдем к картиночкам. Вот так может выглядеть простой проект в IDE (на примере всеми любимой Википедии):

  • Синий и зеленый папки с файлами для описания страниц и тестов соответственно. Так можно реализовать page object паттерн.

  • Коричневый драйвера для различных браузеров.

  • Красный тело теста.

  • Желтый консоль, из которой можно запускать тесты и видеть консольные сообщения (полноценные логи не тут, но об этом позже).

Как видно, в тесте сплошные обертки в стиле BDD (можно не применять такой синтаксис, но лично мне он тут кажется удобным). Имплементация находится в объектах страниц, например:

В стандартной секции Settings мы видим подгрузку библиотеки для работы с Selenium, а в другой стандартной секции Keywords находятся имплементации наших самописных ключевых слов.

Думаю, для получения общего представления этого достаточно. Детальное описание работы с Robot Framework лежит за рамками моего поста

Плюсы и минусы

В этой части я расскажу о плюсах и минусах Robot Framework, с которыми столкнулась наша команда. Учитывая специфику задач на проекте, наверняка у вас будут и свои подводные камни. Я буду перечислять, двигаясь, на мой взгляд, от более значимого достоинства/недостатка к менее значимому.

Плюшки

Низкий порог входа

Как я уже писал выше, Robot Framework является keyword-driven фреймворком, а не языком программирования. Хоть синтаксис и схож с Python, знаний программирования требуется несколько меньше или, скажем так, их применение не обязательно там, где это позволяет сложность самой задачи. Однако, при необходимости можно пользоваться переменными, циклами, функциями, возвращающими значения, и т.п. Ближайшими альтернативами могут показаться Pytest и Selenide, но они требуют большей подготовки пользователя, нежели Robot Framework. Например, одной из встроенных стандартных библиотек является BuiltIn. Там вы можете найти такие кейворды как Sleep, Log, Run Keyword If, Should Be Equal As Strings и т.п. и написать что-то вроде:

Run Keyword If '${status}' == 'PASS' SomeAction

Поддержка Web и Mobile

Robot Framework неплохо работает в связке Mobile+Web (как end-to-end, так и атомарные тесты).

Наши Web тесты работают с Chrome, FF и IE. Мобильная часть работает как с локальными реальными устройствами на Android и iOS, так и с устройствами с фермы SauceLabs. Ограничение реальное локальное iOS-устройство можно тестировать только с Mac. И вообще iOS требует гораздо больше внимания, ведь тот же веб-драйвер для него надо пересобирать самостоятельно. (Тестирование iOS это отдельная большая тема, и если интересно, дайте знать в комментариях, мне есть о чем рассказать)

Тэги

Есть возможность задавать тестам тэги. Тэгами может быть любая информация, которая пригодится нам для идентификации теста: ID теста, список компонент, к которым относится тест, и т.п. Этим мы обеспечиваем связь тестов с тестами или требованиями (traceability) и задаем необходимую информацию для конфигурирования запуска тестов. Указав в запускалке один тэг, мы можем запустить все тесты, которые относятся к определенному компоненту, или же можем при запуске явным образом перечислить тест-кейсы, которые надо запустить (удобно при регрессионном тестировании). Подробнее про тэги по ссылке.

Хорошие отчеты из коробки

Для предоставления стандартной отчетности ничего придумывать не надо. Отчеты создаются автоматически без единой дополнительной команды. Есть возможность объединения результатов разных тестовых прогонов. В результате прогона по умолчанию создаются три файла:

  • Output.xml результаты тестов в формате XML. Пригодятся для мерджа результатов командой rebot. Пример:

  • Log.html подробные результаты в HTML-формате. Полезны больше для разработчиков тестов. Пример:

  • Report.html высокоуровневые результаты без подробной детализации. Полезны для демонстрации людям со стороны и менеджменту. Пример:

BDD из коробки

Синтаксис Gherkin языка с его нотациями Given, When, Then и And включен по умолчанию, и любой шаг может быть записан как в этой нотации, так и без нее. Можно использовать нотации или нет тесты просто игнорируют их. К примеру, эти два кейворда с точки зрения фреймворка идентичны:

Welcome page should be open

And welcome page should be open

Подробнее по ссылке.

Page Object паттерн

Robot Framework позволяет реализовать Page Object паттерн не при помощи ООП, а при помощи синтаксиса ключевых слов. Смысл в том, чтобы последовательно в кейворде указывать, с какой страницей мы работаем -> с какой областью внутри нее мы работаем -> с каким контролом работаем и что мы с ним делаем. Пример:

On Main page on Users tab I click Create user icon

где кейворд On Main page on Users tab I click Create user icon хранится в отдельном робот файлике, скажем, с названием mainPage.robot. Этот файлик мы подгружаем в наш файл с тестами по необходимости.

См. также пример из секции Пара слов и картиночек для знакомства с Robot Framework.

Параллельный запуск

Параллельный запуск становится возможным благодаря альтернативной запускалке тестов под названием Pabot. Стандартным предустановленным способом является команда robot. Естественно, тесты должны быть на это рассчитаны и не должны влиять друг на друга

Грабли

Отсутствует возможность отладки встроенными средствами

Имеется ввиду классическая расстановка брейкпоинтов. Приходится либо выводить что-то дополнительное в лог, либо ставить временные слипы и так обходить эту проблему. В сети описаны некоторые способы прикрутить дебаг, но для уровня целевой аудитории Robot Framework это сложновато.

Не поддерживается AWS

AWS (Amazon Web Services коммерческое публичное облако, ферма мобильных устройств) не поддерживает тесты на Robot Framework. AWS работает таким образом, что код исполняется на стороне Amazon, и тесты в формате Robot Framework не допустимы. Зато другая ферма, SauceLabs, устроена по другому принципу и прекрасно работает с Robot Framework (есть проблемы с администрированием их сервиса из России, но они решаются общением со службой поддержки или работой под VPN).

IDE сложности

RIDE (Robot IDE), специальная IDE для Robot Framework, мягко говоря, сырая. Режим работы в табличном виде (как раз для воплощения идеи keyword-driven фреймворка) выглядит так:

Режим работы в редакторе текста:

Ни в одном из двух предлагаемых режимов работать невозможно. Приложение периодически падает (хотя на других проектах такого нет). В режиме текста нет элементарного Go to Definition. В режиме таблиц Go to Definition есть, но сам этот режим крайне неудобен для средних и больших проектов.

PyCharm работает лучше, но, к сожалению, существующие плагины не справляются с автокомплитом некоторых библиотек (например, SeleniumLibrary)

Плохая поддержка сторонних библиотек

Готовые, уже существующие в сети библиотеки зачастую не поддерживаются. Пользователей мало, и они переходят в разряд зомби. Например, работа с почтой, сравнение скриншотов и т.п. Можно, конечно, написать свои библиотеки на чистом Python (и Robot Framework это позволяет), но смысла в такой схеме остается мало.

Справедливости ради стоит сказать, что нам для наших целей не пришлось писать ничего на Python и хватило добытых не без труда готовых библиотек.

Выводы

Выбор инструмента Robot Framework для нашего проекта был абсолютно верным и позволил выполнить наши обязательства в срок и с надлежащим качеством. Однако, надо понимать, что это, конечно же, не серебряная пуля, есть много но, которые надо иметь в виду.

Инструмент это всего лишь средство для достижения поставленной цели, и не всегда надо микроскопом забивать гвозди, даже если это выглядит эффектно. Ругать или нахваливать Robot Framework я не берусь. Скажу лишь, что это хороший инструмент для своих целей

Полезные ссылки

  • Robot Framework User Guide: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html

  • SeleniumLibrary: https://robotframework.org/SeleniumLibrary/SeleniumLibrary.html

  • BuiltIn: http://robotframework.org/robotframework/latest/libraries/BuiltIn.html

  • AppiumLibrary: http://serhatbolsu.github.io/robotframework-appiumlibrary/AppiumLibrary.html

  • Collections: https://robotframework.org/robotframework/latest/libraries/Collections.html

Подробнее..

Xamarin.Forms. Личный опыт использования

25.06.2020 14:15:14 | Автор: admin
В статье речь пойдет о Xamarin.Forms на примере живого проекта. Кратко поговорим о том, что такое Xamarin.Forms, сравним с похожей технологией WPF, увидим, как достигается кроссплатформенность. Также разберём узкие места, с которыми мы столкнулись в процессе разработки, и добавим немного реактивного программирования с ReactiveUI.
image

Кроссплатформа что выбрать?


В современном мире, в век огромного количества различных устройств и операционных систем, более конкурентноспособными являются приложения, способные работать сразу на нескольких платформах. Для реализации кроссплатформенности существуют разные подходы: написание нативного кода для каждой целевой платформы (по сути, нескольких различных приложений) или использование специальных фреймворков, позволяющих разрабатывать единый код для всех случаев. Есть еще кроссплатформенные языки программирования или же среды исполнения, например, Java Virtual Machine, но здесь я их касаться не буду.

Нативный подход обеспечивает более высокое качество и скорость работы готовых приложений, позволяет более эффективно использовать ресурсы платформы. Однако, сроки разработки значительно больше, чем при написании единого кода. Увеличивается также и стоимость поддержки таких продуктов. Использование сторонних фреймворков для написания единой кодовой базы, напротив, позволяет писать приложения быстрее, используя более распространённые языки программирования, но может добавить накладных расходов при работе уже готового программного обеспечения, ведь любой инструмент, обеспечивающий кроссплатформенность, является своего рода обёрткой, транслирующей вызовы к системе и от неё. Никто не отменял и банальных погрешностей в работе самой этой обёртки.

Какое решение использовать на конкретном проекте выбор сугубо индивидуальный и зависит от многих параметров, таких как сроки, квалификация разработчиков, требования к приложению и т.д.

А теперь к сути


Не так давно нашей команде пришлось лицом к лицу столкнуться с кроссплатформенной разработкой. Задача от заказчика звучала так:
  • Создать iOS-приложение, работающее на iPad;
  • Разработать такое же приложение с расширенным функционалом под Windows 10;
  • Всё это при условии, что правки в дальнейшем могут вноситься как в оба приложения одновременно, так и по отдельности;
  • Сделать приложение максимально гибким, поддерживаемым и расширяемым, потому что техническое задание, как обычно, менялось со скоростью света.

Сроки и бюджет говорили о том, что создание нескольких приложений в данном случае неподходящее решение. Выгоднее будет разработать единую кодовую базу для двух платформ с возможностью добавления индивидуального функционала там, где это необходимо. В нашем случае стек технологий был продиктован заказчиком: языком программирования был выбран C#, а инструментом для кроссплатформенной разработки Xamarin.Forms.

Xamarin это инструмент для создания приложений на языках семейства .NET (C#, F#, Visual Basic), который позволяет создавать единый код, работающий на Android, iOS и Windows (UWP-приложения). Это xaml-подобная технология, то есть интерфейс описывается декларативно в формате xml, вы сразу видите, как элементы расположены на форме и какие свойства имеют. Такой подход очень удобен, в отличие, например, от Windows.Forms, в котором, если бы не графический редактор, разрабатывать и редактировать пользовательские интерфейсы было бы крайне сложно, так как все элементы и их свойства создаются динамически. У меня был опыт разработки подобных интерфейсов без декларативных описаний в среде, не имеющей удобного графического редактора, и я не хочу его повторять. В Xamarin.Forms сохранена возможность динамического создания элементов интерфейса в программном коде, но для чистоты кармы и благодарности от последователей вашего кода всё, что можно описать декларативно, лучше так и описывать.

Xamarin.Forms и WPF


Еще одной известной xaml-подобной технологией является WPF (Windows Presentation Foundation). Это потрясающий инструмент для создания красивых интерактивных пользовательских интерфейсов. На мой взгляд, это один из лучших инструментов, созданных Microsoft, имеющий, правда, серьезный недостаток такие приложения можно разрабатывать только под Windows.

Xamarin.Forms включает в себя урезанную версию WPF, агрегируя лишь те компоненты, которые имеют аналоги на других платформах. С одной стороны, это даёт разработчикам, имеющим опыт работы с WPF, неплохую фору для входа в Xamarin.Forms. С другой же, познав всю прелесть и возможности WPF, программисту будет трудно то тут, то там натыкаться на отсутствие всех этих преимуществ в Xamarin.Forms. Мне было крайне грустно осознать, что я не могу использовать шаблоны для элементов управления, которыми так привыкла жонглировать в WPF, в Xamarin.Forms их попросту нет. Или удобный и мощный ItemControl компонент WPF, позволяющий разместить на форме список элементов, задавая каким угодно образом их внешний вид и расположение. С помощью одного только этого компонента как основного можно написать, например, простенький прототип игры в бильярд. В Xamarin.Forms существуют аналоги ItemControl, но все они имеют конкретную реализацию внешнего вида списка, что лишает их необходимой гибкости и вынуждает разработчиков идти на GitHub за велосипедами.

Кроссплатформенность в Xamarin.Forms


Однако не бывает худа без добра. Своими ограничениями Xamarin.Forms платит за кроссплатформенность, которая действительно легко и удобно достигается средствами этого инструмента. Вы пишете общий код для iOS, Android и Windows, а Xamarin сам разбирается, как связать ваш код с родным для каждой платформы API. Кроме того, есть возможность писать не только общий, но и платформозависимый код, и Xamarin тоже поймет, что и где вызывать. Одним из главных механизмов достижения кроссплатформенности является наличие умных сервисов, способных осуществлять кросс-зависимые вызовы, то есть обращаться к той или иной реализации определённого функционала в зависимости от платформы. Платформозависимый код можно писать не только на C#, но и добавлять его в xaml-разметку. В нашем случае iOS версия была урезанной и часть графического интерфейса нужно было скрыть, размеры некоторых элементов также зависели от платформы.

Для большей наглядности приведу небольшой пример. В нашем проекте мы использовали библиотеку классов, предоставленную заказчиком, которая была написана на C++. Назовём её MedicalLib. MedicalLib собиралась в две разные сборки в зависимости от платформы (статическая MedicalLib.a для iOS и динамическая MedicalLib.dll для Windows). Кроме того, она имела различные wrapper-классы (классы-переходники) для вызова неуправляемого кода. Основной проект (общая кодовая база) содержал описание API MedicalLib (интерфейс), а платформозависимые проекты для iOS и Windows конкретные реализации этого интерфейса и ссылки на сборки MedicalLib.a и MedicalLib.dll соответственно. Из основного кода мы вызывали те или иные функции абстракции, не задумываясь, как именно они реализованы, а механизмы Xamarin.Forms, понимая, на какой платформе запущено приложение, вызывали необходимую реализацию и подгружали конкретную сборку.

Примерно так это можно изобразить схематично:
image

Среда разработки


Для написания программ с Xamarin.Forms используется Visual Studio. На MacOS Visual Studio for Mac. Многие разработчики считают это больше недостатком, ссылаясь на тяжеловесность этой IDE. Для меня Visual Studio является привычной средой разработки и на высокопроизводительных компьютерах не доставляет каких-либо неудобств. Хотя Mac-версия этой IDE пока еще далека от идеала и имеет на порядок больше недочетов, чем её Windows-собрат. Для мобильной разработки имеется целый ряд встроенных эмуляторов мобильных устройств, а также возможность подключить реальный девайс для отладки.

Reactive UI и реактивная модель


В основу любого проекта Xamarin.Forms отлично ложится паттерн проектирования MVVM (Model View View Model), главным принципом которого является отделение внешнего вида пользовательского интерфейса от бизнес-логики. В нашем случае мы использовали MVVM, что действительно оказалось удобно. Важным моментом реализации MVVM является механизм оповещений View о том, что какие-то данные изменились и это необходимо отобразить на интерфейсе. Думаю, многие разработчики на WPF слышали об интерфейсе INotifyPropertyChanged, реализуя который во вью-моделях, мы получаем возможность оповещать интерфейс об изменениях. Этот способ имеет свои плюсы и минусы, но главным недостатком является запутанность и громоздкость кода в случаях, когда во вью-моделях есть вычисляемые свойства (например, Name, Surname и вычисляемое FullName). Мы выбрали более удобный фреймворк ReactiveUI. Он уже содержит реализацию INotifyPropertyChanged, а также много других преимуществ например, IObservable.

IObservable это реактивные push-based провайдеры уведомлений о наличии обновлений для подписчиков. Очень похоже на события и подписки на них, но с рядом дополнительных встроенных фич. Например, мы можем реагировать не на все обновления, а с какими-нибудь фильтрами (допустим, наш IObservable поток целых чисел, и мы хотим принимать во внимание только четные). Или одним подписчиком можно подписаться на комбинацию из двух IObservable, первый из которых типа bool, и реагировать или не реагировать на обновления второго в зависимости от того, что пришло в первый. IObservable можно представить как поток данных, хотя по сути это коллекция.

Если коротко и не вдаваясь в подробности описывать предметную область нашего приложения, то это симулятор реального медицинского оборудования. На экране бегут графики якобы снимаемых с пациента данных, а пользователь, то есть врач, может менять настройки девайса и смотреть, как это влияет на медицинские характеристики пациента. Интерфейс в нашем случае это отображение в реальном времени данных, которые с большой частотой (раз в 40 миллисекунд) приходят из недр приложения.

Реализовано это было следующим образом: на бэкэнде постоянно крутился некий генератор данных, которые поступали в несколько IObservable. На каждый IObservable во вью-моделях были подписаны соответствующие свойства, которые с помощью механизмов ReactiveUI выводили данные на интерфейс в нужном виде.
image
Взаимодействие с интерфейсом в обратном направлении (т.е. обработка действий пользователя), было реализовано с помощью так называемых Interaction взаимодействий, которые инициировались при работе пользователя с UI (например, при нажатии на кнопку), а обрабатывались в любом месте приложения. Что-то наподобие интерфейса ICommand и команд в WPF, только интеракции в нашем случае назначались на интерфейсные элементы не декларативно (как с командами в WPF), а программно, что показалось не очень удобным.

Выше я уже проводила аналогию с WPF. Для наглядности покажу, как выглядит наша архитектура в сравнении со стандартным WPF-приложением:
image
Весь набор инструментов, описанных на этой схеме, мы получили, используя Reactive UI.

Сложности в процессе разработки


А теперь я перейду от красивой теории к практическим проблемам, с которыми пришлось столкнуться, ведь ни один живой проект без этого не обходится.

Наиболее сложные моменты были связаны с разработкой UWP приложения, Windows добавила немало головной боли из-за некоторых особенностей и ограничений в отношении этих программ.

При сворачивании окна приложение переходит в состояние Suspended, и Windows выделяет ему меньше ресурсов. Это было критично, так как в одной из вариаций наш Data Generator работал, интегрируясь с внешними источниками и получая данные через сокеты. При сворачивании окна сокеты продолжали получать данные, заполняя свой внутренний буфер пакетами. А при выходе из режима Suspended все эти пакеты тут же помещались в буфер приложения, начиная обрабатываться только в этот момент. Из-за этого происходила задержка в отображении данных после развертывания окна, которая была пропорциональна времени, проведенному в свёрнутом виде. Всё решилось грамотной обработкой событий Suspending и Resuming, при наступлении которых мы сохраняли состояние приложения и закрывали сокеты, открывая их снова при восстановлении штатного режима работы.

Ещё одной непростой задачей стало добавление второго окна приложения (это было необходимо в рамках технического задания). Сейчас я не могу сказать, были ли это ограничения UWP, Xamarin.Forms или же наша собственная ошибка краш происходил в конструкторе одного из представлений из-за UI-потоков. Позже выяснилось, что библиотека, предоставленная заказчиком и являющаяся ядром обработки данных, однопользовательская и хранит в себе состояние. Таким образом, два независимых окна требовали двух независимых экземпляров библиотеки, что добавило нам одну большую низкоуровневую задачу, решение которой не было бы тривиальным. Мы решили, что овчинка выделки не стоит и проще не открывать отдельное окно, а создавать новый экземпляр приложения со своими настройками и адресным пространством. В итоге со стороны пользователя это выглядело в точности как второе окно (даже при закрытии главного окна второе тоже завершало свою деятельность), и разницу можно было заметить, только заглянув в диспетчер задач и увидев второй процесс.

Ну, и главный недостаток UWP, с которым пришло немало повозиться, это политика его распространения. Чтобы установить приложение, его либо необходимо загрузить в Microsoft Store и скачивать оттуда, либо поставлять дистрибутив другим способом с одним ограничением тогда при его установке необходимо дать операционной системе соответствующие разрешения (Sideloaded Apps в настройках Windows). Техническое задание требовало реализации второго подхода, однако политика безопасности заказчика запрещала сотрудникам компании менять подобные настройки. В итоге нам пришлось написать инсталлер, который перед установкой включал Sideloaded Apps, устанавливал пакет и в конце выключал эту настройку (естественно, по согласованию с заказчиком).

Отдельно стоит отметить, что сама кроссплатформенная разработка обязует программиста быть более дисциплинированным и проверять свой код на всех целевых платформах, что, конечно, добавляет монотонности.

Что касается архитектуры и каких ошибок можно было бы избежать?

Выше я уже писала об использовании для взаимодействия с пользователем Interaction от ReactiveUI. Такие интеракции можно обработать в любом месте приложения, что является не только их достоинством, но и недостатком, так как может внести большую путаницу. Чтобы понять, почему при нажатии на кнопку она окрашивается в красный цвет, а не в зелёный, вам нужно найти все обработчики интеракции и проверить код в них. Договоренность внутри команды обрабатывать интеракции унифицированно может помочь снизить негативное влияние этого фактора, но избежать его совсем вряд ли удастся.

Не меньшим по значимости недостатком описанных выше интеракций является то, что управлять порядком вызовов обработчиков практически невозможно. Мы наступили еще на одни грабли и использовали их не только для взаимодействия с пользователем, но и для общения вью-моделей друг с другом. Это часто играло против нас, так как из-за неконтролируемости вызовов обработчиков мы получали много плавающих багов. Поэтому использовать Interaction я рекомендую с большой осторожностью, а при реализации MVVM прорабатывать иные подходы к взаимодействию вью-моделей.

Еще стоит отметить, что достаточно много времени наша команда потратила на адаптацию визуальной части пользовательского интерфейса под Windows-приложение. Сжатые сроки и приоритеты заказчика поставили разработку iOS-версии на первое место, поэтому к UWP мы приступили, имея уже наполовину готовый проект, работающий на iOS. Изначально не предполагалось изменение разрешения, и приложение оказалось не готовым к свободному масштабированию окна, не имея достаточной гибкости в расположении элементов, что привело нас к довольно объемному рефакторингу всех визуальных компонентов. Поэтому не могу не отметить, что разработка кроссплатформенного проекта всё-таки должна вестись параллельно на всех предполагаемых платформах.

Итог


Xamarin.Forms часто считают сырой и недоработанной технологией. Не могу сказать, что наш проект столкнулся с какими-то критичными проблемами именно со стороны Xamarin. Опыт нашей команды подтверждает, что адаптация приложения под вторую платформу процесс достаточно простой и быстрый. Не всё прошло гладко и без проблем, и задачи по адаптации всё-таки были например, из-за различия работы файловых систем или визуального отображения некоторых стилей. Но все эти проблемы либо устранялись достаточно быстро, либо не имели весомого значения для заказчика.

Благодаря архитектуре и выбранным решениям, со всеми их достоинствами и недостатками, проект уже полтора года наращивает функционал без какого-либо глобального рефакторинга и чувствует себя твёрдо стоящим на ногах. Инструмент работает и со своими основными задачами справляется. Кроме того, он не является новым для IT-сообщества, поэтому документации, которая помогает разобраться в тонкостях, достаточно. То, что теперь кроссплатформенную разработку можно вести на платформе .NET и удобном С#, которые предпочитают многие программисты, является неоспоримым преимуществом и может стать финальным аккордом в решении использовать Xamarin.Forms и на вашем проекте.
Подробнее..

Конвертируем doc в docx и xml на C

23.11.2020 10:05:21 | Автор: admin

Продолжаю свой цикл статей, посвященный конвертации различных текстовых файлов с помощью решений, реализованных на языке C#.


С момента моей последней публикации Конвертация xls в xlsx и xml на C# прошло более полугода, за которые я успел сменить как работодателя, так и пересмотреть свои взгляды на некоторые аспекты коммерческой разработки. Сейчас, работая в международной компании с совершенно иным подходом к разработке ПО (ревью кода, юнит-тестирование, команда автотестеров, строгое соблюдение СМК, заботливый менеджер, очаровательная HR и прочие корпоративные плюшки), я начинаю понимать, почему некоторые из комментаторов интересовались целесообразностью предлагаемых мной велокостылей, когда на рынке есть очень достойные готовые решения, например, от e-iceblue. Но давайте не забывать, что ситуации бывают разные, компании тем более, и если потребность в решении какой-то задачи с использованием определенного инструментария возникла у одного человека, то со значительной долей вероятности она возникнет и у другого.



Итак, дано:


  1. Неопределенное множество файлов в формате .doc, которые нужно конвертировать в xml (например, для парсинга и организации автоматизированной навигации внутри текста), желательно с сохранением форматирования.
  2. На сервере памяти чуть больше, чем у рыбки, а на процессоре уже можно жарить яичницу, да и у компании нет лишней лицензии на Word, поэтому конвертация должна происходить без запуска каких-либо офисных приложений.
  3. Сервис должен быть написан на языке C# и в последующем интегрирован в код другого продукта.
  4. На решение задачи два дня и две ночи, которые истекли вчера.

Поехали!


  • Во-первых, нужно сразу уяснить, что старые офисные форматы файлов, такие как .doc и .xls, являются бинарными, и достать что-нибудь человекочитаемое из них без использования текстовых редакторов/процессоров не получится. Прочитать об этом можно в официальной документации. Если есть желание поковыряться поглубже, посчитать нолики с единичками и узнать, что они означают, то лучше сразу перейти сюда.
  • Во-вторых, несмотря на наличие бесплатных решений для работы с .doc, большинство из них написаны на Python, Ruby и чем угодно еще, но не C#.
  • В-третьих, найденное мной решение, а именно библиотека b2xtranslator, является единственным доступным бесплатным инструментом такого рода, еще и написана при поддержке Microsoft, если верить вот этому источнику. Если вдруг вы встречали какие-нибудь аналоги данной библиотеки, пожалуйста, напишите об этом в комментариях. Даже это душеспасительное решение не превратит .doc в .xml, однако поможет нам превратить его в .docx, с которым мы уже умеем работать.

Довольно слов давайте к делу


Установка b2xtranslator


Для работы нам понадобиться библиотека b2xtranslator. Ее можно подключить через менеджера пакетов NuGet.

Однако я настоятельно рекомендую скачать ее из официального git-репозитория по следующим причинам:


  • a) Библиотека представляет собой комбайн, работающий с различными бинарными офисными документами (.doc, .xls, .ppt), что может быть избыточным
  • b) Проект достаточно долго не обновляется и вам, возможно, придется доработать его напильником
  • c) Задача, с которой я столкнулся, как раз потребовала внесения некоторых изменений в работу библиотеки, а также изучения ее алгоритмов и используемых структур для успешной интеграции в свое решение
    Для дальнейшей работы нам понадобиться подключить в свое решение два проекта из библиотеки: b2xtranslator\Common\b2xtranslator.csproj и b2xtranslator\Doc\b2xtranslator.doc.csproj

Конвертация .doc в .docx


Конвертация документов строится по следующему алгоритму:


  1. Инициализация дескриптора для конвертируемого файла.
    Для этого необходимо создать экземпляр класса StructuredStorageReader, конструктор которого в качестве аргумента может принимать или путь до файла, или последовательность байтов (Stream), что делает его крайне удобным при работе с файлами, загружаемыми по сети. Также обращаю внимание, что так как библиотека b2xtranslator является комбайном для конвертации бинарных офисных форматов в современный OpenXML, то независимо от того, какой формат мы хотим конвертировать (.ppt, .xls или .doc) инициализация дескриптора всегда будет происходить с помощью указанного класса (StructuredStorageReader).
    StructuredStorageReader reader = new StructuredStorageReader(docPath);
    
  2. Парсинг бинарного .doc файла с помощью объекта класса WordDocument, конструктор которого в качестве аргумента принимает объект типа StructuredStorageReader.
    WordDocument doc = new WordDocument(reader);
    
  3. Создание объекта, который будет хранить данные для файла в формате .docx.
    Для этого используется статический метод cs public static WordprocessingDocument Create(string fileName, OpenXmlPackage.DocumentType type) класса WordprocessingDocument. В первом аргументе указываем имя нового файла (вместе с путем), а вот во втором мы должны выбрать тип файла, который должен получиться на выходе:
    a. Document (обычный документ с расширением .docx);
    b. MacroEnabledDocument (файл, содержащий макросы, с расширением .docm);
    c. Template (файл шаблонов word с расширением .dotx);
    d. MacroEnabledTemplate (файл с шаблоном word, содержащий макросы. Имеет расширение .dotm).
    WordprocessingDocument docx = WordprocessingDocument.Create(docxPath, DocumentType.Document);
    
  4. Конвертация данных из бинарного формата в формат OpenXML и их запись в объект типа WordprocessingDocument.
    За выполнение указанной процедуры отвечает статический метод
    public static void Convert(WordDocument doc, WordprocessingDocument docx)
    

    класса Converter, который заодно и записывает получившийся результат в файл.

    Converter.Convert(doc, docx);
    

    В результате у вас должен получиться вот такой код:

    using b2xtranslator.StructuredStorage.Reader;using b2xtranslator.DocFileFormat;using b2xtranslator.OpenXmlLib.WordprocessingML;using b2xtranslator.WordprocessingMLMapping;using static b2xtranslator.OpenXmlLib.OpenXmlPackage;namespace ConverterToXml.Converters{    public class DocToDocx    {        public void ConvertToDocx(string docPath, string docxPath)        {            StructuredStorageReader reader = new StructuredStorageReader(docPath);            WordDocument doc = new WordDocument(reader);            WordprocessingDocument docx = WordprocessingDocument.Create(docxPath, DocumentType.Document);            Converter.Convert(doc, docx);        }    }}
    

    Внимание!
    Если вы используете платформу .Net Core 3 и выше в своем решении, обратите внимание на целевые среды для подключенных проектов b2xtranslator. Так как библиотека была написана довольно давно и не обновляется с 2018 года, по умолчанию она собирается под .Net Core 2.
    Чтобы сменить целевую среду, щелкните правой кнопкой мыши по проекту, выберите пункт Свойства и поменяйте целевую рабочую среду. В противном случае вы можете столкнуться с проблемой невозможности конвертации файлов .doc, содержащих в себе таблицы.
    Я не стал разбираться, почему так происходит, но энтузиастам могу подсказать, что причину стоит искать в 40 строчке файла ~\b2xtranslator\Doc\WordprocessingMLMapping\MainDocumentMapping.cs в момент обработки таблицы.
    Кроме того, рекомендую собирать все проекты и само решение под 64-битную платформу во избежание всяких непонятных ошибок.



    Сохранение результата в поток байтов


    Так как моей целью при использовании данного решения была конвертация .doc в .xml, а не в .docx, предлагаю вовсе не сохранять промежуточный OpenXML файл, а записать его в виде потока байтов. К сожалению, b2xtranslator не предоставляет нам подходящих методов, но это довольно легко исправить:
    В абстрактном классе OpenXmlPackage (см. ~\b2xtranslator\Common\OpenXmlLib\OpenXmlPackage.cs) давайте создадим виртуальный метод:


    public virtual byte[] CloseWithoutSavingFile(){    var writer = new OpenXmlWriter();    MemoryStream stream = new MemoryStream();    writer.Open(stream);    this.WritePackage(writer);    writer.Close();    byte[] docxStreamArray = stream.ToArray();    return docxStreamArray;}
    

    По большому счету, данный метод будет заменять собой метод Close(). Вот его исходный код:


    public virtual void Close(){     // serialize the package on closing    var writer = new OpenXmlWriter();    writer.Open(this.FileName);    this.WritePackage(writer);    writer.Close();}
    

    Скажем спасибо разработчикам библиотеки за то, что не забыли перегрузить метод Open(), который может принимать или имя файла, или поток байтов. Однако, библиотечный метод Close(), который как раз и отвечает за запись результата в файл, вызывается в методе Dispose() в классе OpenXmlPackage. Чтобы ничего лишнего не поломать и не заморачиваться с архитектурой фабрик (тем более в чужом проекте), я предлагаю просто закомментировать код внутри метода Dispose() и вызвать метод CloseWithoutSavingFile(), но уже внутри нашего метода после вызова Converter.Convert(doc, docx).
    Для сохранения результата конвертации вызываем вместо docx.Close() метод docx.CloseWithoutSavingFile():


    public MemoryStream ConvertToDocxMemoryStream(Stream stream){    StructuredStorageReader reader = new StructuredStorageReader(stream);    WordDocument doc = new WordDocument(reader);    var docx = WordprocessingDocument.Create("docx", DocumentType.Document);    Converter.Convert(doc, docx);    return new MemoryStream(docx.CloseWithoutSavingFile());}
    

    Теперь библиотека b2xtranslator будет возвращать сконвертированный из формата .doc в .docx файл в виде потока байтов. Даже если у вас нет цели получить на выходе .xml, такой метод может оказаться более подходящим для дальнейшей работы с файлами, тем более что стрим всегда можно сохранить в виде файла там, где вам надо.
    Для тех, кому все-таки очень хочется получить на выходе .xml документ, еще и с сохраненной структурой, предлагаю дойти до кухни, сварить кофе покрепче, добавить в него рюмку коньяка и приготовиться к приключению на 20 минут.


    Конвертация .doc в .xml



    Теперь, когда, казалось бы, можно воспользоваться классом-конвертором DocxToXml, работа которого была описана вот в этой статье, нас поджидает сюрприз, связанный с особенностями работы b2xtranslator.
    Давайте посмотрим на результат работы библиотеки повнимательнее и сравним с оригинальным .docx файлом, из которого был экспортирован .doc файл для конвертации. Для этого достаточно изменить расширение сравниваемых файлов с .docx на .zip. Вот отличия, которые мы увидим, заглянув внутрь архивов:


    1. В результате конвертации в новом .docx файле (справа) отсутствуют папки customXml и docProps.
    2. Внутри папки word, мы также найдем определенные отличия, перечислять которые я, конечно же, не буду:
    3. Естественно, что и метаданные, по которым осуществляется навигация внутри документа, также отличаются. Например, на представленном скрине и далее оригинальный .docx слева, сгенерированный b2xtranslator cправа.

      Налицо явное отличие в атрибутах тега w:document, но этим отличия не заканчиваются. Всю "мощь" библиотеки мы ощутим, когда захотим обработать списки и при этом:
      a. Сохранить их нумерацию
      b. Не потерять структуру вложенности
      c. Отделить один список от другого

    Давайте сравним файлы document.xml для вот этого списка:


    1.1 Первый.Первый1.2 Первый.Второй1.2.1   Первый.Второй.Первый1.2.2   Первый.Второй.ВторойКакая-то строчка 1.2.3   Первый.Второй.Третий2.  Второй2.1 Второй.Первый
    

    Вот так будет выглядеть .xml для первого элемента списка.


    -Во-первых, мы видим, что сама структура документов несколько отличается (например, точка внутри строк рассматривается как отдельный элемент, что, как оказалось, совсем не страшно).
    -Во-вторых, у тегов остался только один атрибут (w:rsidR), а вот w:rsidR, w14:textId, w:rsidRDefault, w:paraId и w:rsidP пропали. Все эти особенности приводят к тому, что наш класс-конвертер DocxToXml(про него подробно можно почитать здесь) подавится и поднимет лапки вверх с ошибкой NullReferenceException, что указывает на отсутствие индексирования параграфов внутри документа.

    Вместе с тем, если мы попытаемся такой файл отрыть в Word, то увидим, что все хорошо отображается, а таблицы и списки покоятся на своих местах! Магия!
    В общем, когда в поисках решения я потратил N часов на чтение документации, мои красные от дебагера глаза омылись горькими слезами, а один лишь запах кофе стремился показать коллегам мой дневной рацион, решение было найдено!
    Исходя из документации к формату doc и алгоритмов работы b2xtranslator, можно сделать вывод, что исторически в бинарных офисных текстовых документах отсутствовала индексация по параграфам*. Возникает задача расставить необходимые теги в нужных местах.
    За индекс параграфа отвечает атрибут тега paraId, о чем прямо написано здесь. Данный атрибут относится к пространству имен w14, о чем можно догадаться при изучении document.xml из архива .docx. В принципе, на скринах выше вы это тоже видите. Объявление пространства имен в .xml выглядит так:


    xmlns:wp14="http://personeltest.ru/away/schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
    

    Теперь давайте заставим b2xtranslator добавлять это пространство имен и идентификатор каждому параграфу. Для этого в файле ~\b2xtranslator\Common\OpenXmlLib\ContentTypes.cs после 113 строки добавим вот эту строчку:


    public const string WordprocessingML2010 = "http://schemas.microsoft.com/office/word/2010/wordml";
    

    Кстати, если посмотрите на комментарии в коде, то увидите, что в этом блоке как раз располагаются поддерживаемые пространства имен для вордовых документов:

    Далее наша задача заставить библиотеку вставлять в начало файла ссылку на данное пространство имен. Для этого в файле ~\b2xtranslator\Doc\WordprocessingMLMapping\MainDocumentMapping.cs в 24 строке вставим код:


    this._writer.WriteAttributeString("xmlns", "w14", null, OpenXmlNamespaces.WordprocessingML2010);
    

    Разработчики библиотеки также позаботились о документации:


    Теперь дело за малым заставить b2xtranslator индексировать параграфы. В качестве индексов предлагаю использовать рандомно сгенерированные GUID может быть, это несколько тяжеловато, но зато надежно!

    Переходим в файл ~\b2xtranslator\Doc\WordprocessingMLMapping\DocumentMapping.cs и в 504 и 505 строки вставляем вот этот код:


    this._writer.WriteAttributeString("w14", "paraId", OpenXmlNamespaces.WordprocessingML2010, Guid.NewGuid().ToString());            this._writer.WriteAttributeString("w14", "textId", OpenXmlNamespaces.WordprocessingML2010, "77777777");
    

    Что касается второй строчки, в которой мы добавляем каждому тегу параграфа атрибут w14:textId = "77777777", то тут можно лишь сказать, что без этого атрибута ничего работать не будет. Для пытливых умов вот ссылка на документацию.
    Если серьезно, то, как я понимаю, атрибут используется, когда текст разделен на разные блоки, внутри которых происходит индексация тегов, которые могут иметь одинаковый Id внутри одного документа. Видимо, для этих случаев используется дополнительная индексация текстовых блоков. Однако, так как мы используем GUID, который в несколько раз больше индексов, используемых в вордовских документах по умолчанию, то генерацией отдельных индексов для текстовых блоков можно и пренебречь.


    Вот теперь мы получили .docx-файл, пригодный для дальнейшего преобразования в .xml. Подробнее о том, как работать с ним дальше, вы можете прочитать в этой статье или воспользоваться уже выложенным на github решением.


    В заключение, если у вас на проекте есть возможность воспользоваться платным надежным софтом, то этот путь скорее всего не для вас. Однако же, если вы энтузиаст, пишете свой pet-проект и уважительно относитесь к авторским правам, а также если ваш проект находится в стадии прототипирования и пока не готов к покупке дорогостоящих лицензий, а разработку продолжать надо, то, мне кажется, этот вариант может вам очень даже подойти! Тем более, что у вас есть возможность воспользоваться готовым решением и не заливать свои краснющие зенки визином, изучая документацию и особенности работы некоторых, на первый взгляд, сомнительных решений.


    Наконец, бонус для тех, кто хочет разобраться, что значат все эти бесконечные теги и их атрибуты в документах .docx и как они мапаются на бинарный .doc: советую заглянуть в файл ~\b2xtranslator\Doc\DocFileFormat\CharacterProperties.cs, а также посмотреть спецификацию для docx и doc.

Подробнее..

Как подружить RxJava с VIPER в Android, подходы применения и о структуре планировщиков

23.07.2020 18:11:16 | Автор: admin
image

Привет, Хабровчане. Сегодня мы с вами поговорим о RxJava. Я знаю, что о ней написано материала вагон и маленькая тележка, но, как мне кажется, у меня есть пара интересных моментов, которыми стоит поделиться. Сначала расскажу, как мы используем RxJava вместе с архитектурой VIPER для Android приложений, заодно посмотрим на классический способ применения. После этого пробежимся по главным особенностям RxJava и остановимся подробнее на том, как устроены планировщики. Если вы уже запаслись вкусняшками, то добро пожаловать под кат.

Архитектура, которая подойдет всем


RxJava это реализация концепции ReactiveX, а создала эту реализацию компания Netflix. В их блоге есть цикл статей о том, зачем они это сделали и какие проблемы они решили. Ссылки (1, 2) вы найдете в конце статьи. Netflix использовали RxJava на стороне сервера (backend), чтобы распараллелить обработку одного большого запроса. Хотя они предложили способ применения RxJava на backend, такая архитектура подойдет для написания разных типов приложений (мобильные, desktop, backend и многих других). Разработчики Netflix использовали RxJava в сервисном слое таким образом, чтобы каждый метод сервисного слоя возвращал Observable.

Дело в том, что элементы в Observable могу доставляться синхронно и асинхронно. Это дает возможность методу самому решать, возвращать значение сразу синхронно, (например, если оно доступно в кэш) или сначала получить эти значения (например, из базы данных или удаленного сервиса) и вернуть их асинхронно. В любом случае, управление будет возвращаться сразу после вызова метода (либо с данными, либо без).

/** * Метод, который сразу возвращает значение, если оно * доступно, или использует  другой поток исполнения, * чтобы получить значение и передать его через callback `onNext()` */public Observable<T> getProduct(String name) {    if (productInCache(name)) {        // Если данные доступны, возвращаем их сразу        return Observable.create(observer -> {           observer.onNext(getProductFromCache(name));           observer.onComplete();        });    } else {        // Иначе задействуем другой поток исполнения        return Observable.<T>create(observer -> {            try {                // Выполняем работу в отдельном потоке                T product = getProductFromRemoteService(name);                // вовращаем значение                observer.onNext(product);                observer.onComplete();            } catch (Exception e) {                observer.onError(e);            }        })        // Говорим Observable использовать планировщик IO        // для создания/получения данных        .subscribeOn(Schedulers.io());    }}

При таком подходе мы получаем один неизменный API для клиента (в нашем случае контроллер) и разные реализации. Клиент всегда взаимодействует с Observable одинаково. Ему абсолютно не важно, получены ли значения синхронно или нет. При этом реализации API могут меняться от синхронных к асинхронным, никак не затрагивая взаимодействие с клиентом. С помощью такого подхода можно совершенно не задумываться о том, как организовать многопоточность, и сосредоточиться на реализации бизнес задач.

Подход применим не только в сервисном слое на backend, но и в архитектурах MVC, MVP, MVVM и др. Например, для MVP мы можем сделать класс Interactor, который будет ответственным за получение и сохранение данных в различные источники, и сделать так, чтобы все его методы возвращали Observable. Они будут являться контрактом взаимодействия с Model. Это также даст возможность использовать в Presenter всю мощь операторов, имеющихся в RxJava.

image

Мы можем пойти дальше и сделать реактивным API уже у Presenter, но для этого нам понадобится правильно реализовать механизм отписки, позволяющий всем View одновременно отписаться от Presenter.

Дальше посмотрим на пример применения такого подхода для архитектуры VIPER, которая является усовершенствованным MVP. Также стоит помнить, что нельзя делать Observable singleton объектами, потому что подписки к таким Observable будут порождать утечки памяти.

Опыт применения в Android и VIPER


В большинстве текущих и новых Android проектов мы используем архитектуру VIPER. Я познакомился с ней, когда присоединился к одному из проектов, в котором она уже использовалась. Помню, как удивился, когда у меня спросили, не смотрел ли я в сторону iOS. iOS в Android проекте?, подумал я. А между тем, VIPER пришел к нам из мира iOS и по сути является более структурированной и модульной версией MVP. О VIPER очень хорошо написано в этой статье (3).

Поначалу все казалось хорошо: правильно разделенные, не перегруженные слои, у каждого слоя своя область ответственности, понятная логика. Но через какое-то время стал проявляться один недостаток, а по мере роста и изменения проекта он стал очень даже мешать.

Дело в том, что мы использовали Interactor так же, как и коллеги в своей статье. Interactor реализует небольшой use case, например, скачать продукты из сети или взять продукт из БД по id, и выполняет действия в рабочем потоке. Внутри себя Interactor совершает операции, используя Observable. Чтобы запустить Interactor и получить результат, пользователь реализует интерфейс ObserverEntity вместе с его методами onNext, onError и onComplete и передает его вместе с параметрами в метод execute(params, ObserverEntity).

Вы, наверное, уже заметили проблему структура интерфейса. На практике нам редко нужны все три метода, часто используются один или два из них. Из-за этого в коде могут встречаться пустые методы. Конечно, мы можем пометить все методы интерфейса default, но такие методы скорее нужны для добавления новой функциональности в интерфейсы. К тому же, странно иметь интерфейс, все методы которого опциональны. Мы также можем, например, создать абстрактный класс, который наследует интерфейс, и переопределять нужные нам методы. Или, наконец, создать перегруженные версии метода execute(params, ObserverEntity), которые принимают от одного до трех функциональных интерфейсов. Эта проблема плохо сказывается на читаемости кода, но, к счастью, довольно просто решается. Однако, она не единственная.

saveProductInteractor.execute(product, new ObserverEntity<Void>() {    @Override    public void onNext(Void aVoid) {        // Сейчас этот метод нам не нужен,        // но мы обязана его реализовать    }    @Override    public void onError(Throwable throwable) {        // Сейчас этот метод используется        // Какой-то код    }    @Override    public void onComplete() {        // И этот метод тоже используется        // Какой-то код    }});

Кроме пустых методов, есть и более неприятная проблема. Мы используем Interactor, чтобы выполнить какое-то действие, но почти всегда это действие не является единственным. Например, мы можем взять продукт из базы данных, затем получить о нем отзывы и картинку, затем сохранить все это в другое место и в конце перейти на другой экран. Тут у нас каждое действие зависит от предыдущего и при использовании Interactor-ов мы получаем огромную цепочку из колбэков, прослеживать которую бывает очень утомительно.

private void checkProduct(int id, Locale locale) {    getProductByIdInteractor.execute(new TypesUtil.Pair<>(id, locale), new ObserverEntity<Product>() {        @Override        public void onNext(Product product) {            getProductInfo(product);        }        @Override        public void onError(Throwable throwable) {            // Какой-то код        }        @Override        public void onComplete() {        }    });}private void getProductInfo(Product product) {    getReviewsByProductIdInteractor.execute(product.getId(), new ObserverEntity<List<Review>>() {        @Override        public void onNext(List<Review> reviews) {            product.setReviews(reviews);            saveProduct(productInfo);        }        @Override        public void onError(Throwable throwable) {            // Какой-то код        }        @Override        public void onComplete() {            // Какой-то код        }    });    getImageForProductInteractor.execute(product.getId(), new ObserverEntity<Image>() {        @Override        public void onNext(Image image) {            product.setImage(image);            saveProduct(product);        }        @Override        public void onError(Throwable throwable) {            // Какой-то код        }        @Override        public void onComplete() {        }    });}private void saveProduct(Product product) {    saveProductInteractor.execute(product, new ObserverEntity<Void>() {        @Override        public void onNext(Void aVoid) {        }        @Override        public void onError(Throwable throwable) {            // Какой-то код        }        @Override        public void onComplete() {            goToSomeScreen();        }    });}

Ну, как вам такая макаронина? При этом у нас простая бизнес логика и одинарная вложенность, а представьте, что было бы с более сложным кодом. Это также затрудняет повторное использование метода и применение разных планировщиков для Interactor.

Решение на удивление простое. Вы чувствуете, что этот подход пытается повторить поведение Observable, но делает это неправильно и сам создает непонятные ограничения? Как я уже рассказывал раньше, этот код достался нам из уже существующего проекта. При исправлении этого legacy-кода будем использовать подход, который завещали нам ребята из Netflix. Вместо того, чтобы каждый раз реализовывать ObserverEntity, заставим Interactor просто возвращать Observable.

private Observable<Product> getProductById(int id, Locale locale) {    return getProductByIdInteractor.execute(new TypesUtil.Pair<>(id, locale));}private Observable<Product> getProductInfo(Product product) {    return getReviewsByProductIdInteractor.execute(product.getId())    .map(reviews -> {        product.set(reviews);        return product;    })    .flatMap(product -> {        getImageForProductInteractor.execute(product.getId())        .map(image -> {            product.set(image);            return product;        })    });}private Observable<Product> saveProduct(Product product) {    return saveProductInteractor.execute(product);}private doAll(int id, Locale locale) {    // Берем продукт из хранилища    getProductById (id, locale)    // Добавляем информацию    .flatMap(product -> getProductInfo(product))    // Сохраняем все в другое хранилище    .flatMap(product -> saveProduct(product))    // После сохранения продукты в потоке больше не нужны    .ignoreElements()    // Устанавливаем планировщики    .subscribeOn(Schedulers.io())    .observeOn(AndroidSchedulers.mainThread())    // Переходим на другой экран    .subscribe(() -> goToSomeScreen(), throwable -> handleError());}

Вуаля! Так мы не только избавились от того громоздкого и неповоротливого ужаса, но и привнесли мощь RxJava в Presenter.

Концепции в основе


Я довольно часто встречал, как с помощью функционального реактивного программирования (далее ФРП) пытались объяснить концепцию RxJava. На самом деле, оно никак не связано с этой библиотекой. ФРП больше о непрерывных динамически изменяемых значениях (поведениях), непрерывном времени и денотационной семантике. В конце статьи вы сможете найти пару интересных ссылок (4, 5, 6, 7).

В качестве основных концепций RxJava использует реактивное программирование и функциональное программирование. Реактивное программирование можно описать как последовательную передачу информации от наблюдаемого объекта объекту-наблюдателю таким образом, что объект-наблюдатель получает ее автоматически (асинхронно) по мере возникновения этой информации.

Функциональное программирование использует понятие чистых функций, то есть тех, которые не используют и не изменяют внешнее состояние; они полностью зависят от своих входных данных для получения своих выходных данных. Отсутствие у чистых функций побочных эффектов дает возможность использовать результаты одной функции в качестве входных параметров другой. Это дает возможность составлять неограниченную цепочку функций.

Соединение этих двух концепций вместе наряду с использованием GoF шаблонов Observer и Iterator позволяет создавать асинхронные потоки данных и обрабатывать их с помощью огромного арсенала очень удобных функций. Это также дает возможность очень просто, а главное безопасно, использовать многопоточность, не думая о таких ее проблемах как синхронизация, неконсистентность памяти, наложение потоков и т.д.

image

Три кита RxJava


Основные три компонента, на которых строится RxJava Observable, операторы и планировщики.
Observable в RxJava отвечает за реализацию реактивной парадигмы. Observable часто называют потоками, так как они реализуют как концепцию потоков данных, так и распространение изменений. Observable это тип, который достигает реализации реактивной парадигмы за счет объединения в себе двух шаблонов из книги Gang of Four: Observer и Iterator. Observable добавляет в Observer две отсутствующие семантики, которые есть в Iterable:

  • Возможность для производителя сигнализировать потребителю о том, что больше нет доступных данных (цикл foreach в Iterable завершается и просто возвращается; Observable в этом случае вызывает метод onCompleate).
  • Возможность для производителя сообщать потребителю, что произошла ошибка и Observable больше не может испускать элементы (Iterable бросает исключение, если во время итерации возникает ошибка; Observable вызывает метод onError у своего наблюдателя и завершается).

Если Iterable использует pull подход, то есть потребитель запрашивает значение у производителя, и поток исполнения блокируется до тех пор, пока это значение не прибудет, то Observable является его push эквивалентом. Это значит, что производитель отправляет значения потребителю, только когда они становятся доступны.

Observable это только начало RxJava. Он позволяет асинхронно получать значения, но настоящая мощь приходит с реактивными расширениями (отсюда ReactiveX) операторами, которые позволяют преобразовывать, комбинировать и создавать последовательности элементов, испускаемых Observable. Именно тут и выходит на передний план функциональная парадигма со своими чистыми функциями. Операторы используют эту концепцию в полной мере. Они позволяют безопасно работать с последовательностями элементов, которые испускает Observable, не боясь побочных эффектов, если, конечно, не создать их самостоятельно. Операторы позволяют применять многопоточность, не заботясь о таких проблемах как потокобезопасность, низкоуровневое управление потоками, синхронизация, ошибки некосистентности памяти, наложение потоков и т.д. Имея большой арсенал функций, можно легко оперировать различными данными. Это дает нам очень мощный инструмент. Главное помнить, что операторы модифицируют элементы, испускаемые Observable, а не сами Observable. Observable никогда не изменяются с момента их создания. Размышляя о потоках и операторах, лучше всего думать диаграммами. Если вы не знаете, как решить задачу, то подумайте, посмотрите на весь список доступных операторов и подумайте еще.

Хотя сама по себе концепция реактивного программирования является асинхронной (не путайте с многопоточностью), по умолчанию все элементы в Observable доставляются подписчику синхронно, в том же потоке, в котором был вызван метод subscribe(). Чтобы привнести ту самую асинхронность, нужно либо самостоятельно вызывать методы onNext(T), onError(Throwable), onComplete() в другом потоке исполнения, либо использовать планировщики. Обычно все разбирают их поведение, так что давайте посмотрим на их устройство.

Планировщики абстрагируют пользователя от источника параллелизма за собственным API. Они гарантируют, что будут предоставлять определенные свойства, независимо от лежащего в основе механизма параллельности (реализации), например, Threads, event loop или Executor. Планировщики используют daemon потоки. Это означает, что программа завершится вместе с завершением основного потока исполнения, даже если происходят какие-то вычисления внутри оператора Observable.

В RxJava есть несколько стандартных планировщиков, которые подходят для определенных целей. Все они расширяют абстрактный класс Scheduler и реализуют собственную логику управлением workers (рабочими). Например, планировщик ComputationScheduler во время своего создания формирует пул рабочих, количество которых равно количеству процессорных потоков. После этого ComputationScheduler использует рабочих для выполнения Runnable задач. Вы можете передать Runnable планировщику с помощью методов scheduleDirect() и schedulePeriodicallyDirect(). Для обоих методов планировщик берет очередного рабочего из пула и передает ему Runnable.

Рабочий находится внутри планировщика и представляет собой сущность, которая выполняет Runnable объекты (задачи), используя одну из нескольких схем параллельности. Другими словами, планировщик получает Runnable и передает ее рабочему для выполнения. Также можно самостоятельно получить рабочего у планировщика и передать ему один или несколько Runnable, независимо от других рабочих и самого планировщика. Когда рабочий получает задачу, он помещает ее в очередь. Рабочий гарантирует последовательное выполнение задач в том порядке, в котором они были переданы, однако порядок может нарушаться отложенными задачами.

Например, в планировщике ComputationScheduler рабочий реализован с помощью ScheduledExecutorService размером в один поток.

image

Таким образом, мы имеем абстрактных рабочих, которые могут реализовывать любую схему параллельности. Такой подход дает много плюсов: модульность, гибкость, один API, различные реализации. Похожий подход мы видели в ExecutorService. К тому же, мы можем использовать планировщики отдельно от Observable.

Заключение


RxJava очень мощная библиотека, которую можно использовать самыми разными способами во множестве архитектур. Способы ее применения не ограничиваются уже существующими, поэтому всегда пробуйте адаптировать ее под себя. Однако помните о SOLID, DRY и других принципах разработки, а также не забывайте делиться с коллегами своим опытом применения. Надеюсь, что вы смогли почерпнуть из статьи что-то новое и интересное, до встречи!

  1. Причины, почему Netflix начали использовать ReactiveX
  2. Презентация RxJava интернет-сообществу
  3. Объяснение архитектуры VIPER и пример применения
  4. Объяснение ФРП от его создателя
  5. Разница между ФРП и реактивным программированием
  6. Рассуждение о ФРП
  7. Блог Conal Elliot о ФРП
Подробнее..

ISTQB. Как проходит сдача экзамена онлайн

09.03.2021 10:13:46 | Автор: admin
Лучший тестировщикЛучший тестировщик

Когда я смотрела фильм Идиократия, момент с тестом на сообразительность показался мне
нереальным. Ни за что не хотелось, чтобы показанные в фильме события могли оказаться правдой, но спустя несколько лет это случилось. Я стала тестировщиком, и моя работа сейчас
выглядит примерно так, как показано на главной картинке. Наверно, именно так программисты
видят тестировщиков.

Как доказать, что ты хороший тестировщик? Есть много способов это сделать, и один из них
подтвердить свои знания и умения сертификатом ISTQB. В статье будет описан процесс
регистрации, предварительной технической подготовки и прохождения онлайн-экзамена,
который состоялся 5 декабря 2020.

На просторах русской части Интернета уже есть несколько статей, описывающих процесс сдачи
экзамена. Наиболее свежая статья от 20 мая 2020: Сертификация ISTQB стала доступна онлайн:
личный опыт
. Есть еще описание очного экзамена от 2016 года: ISTQB Сертификация. Опыт сдачи.
Дополню эти статьи своим актуальным опытом от декабря 2020.

Думаю, что не стоит надолго останавливаться на том, что такое ISTQB и зачем нужно его сдавать.
Кто попал сюда, скорее всего уже сам это знает. А кто не знает, буквально в нескольких строчках:
ISTQB (International Qualification Board for Software Testing) международная система
квалификации тестировщиков ПО, унифицирующая стандарты и подходы к тестированию.
Система имеет несколько уровней, от базового (foundation level, подтверждает наличие
минимальных теоретических знаний в области тестирования) до экспертного (expert, сочетает в
себе понимание общего процесса тестирования с глубоким пониманием определенной
предметной области). Для понимания крутости обладания сертификатом уровня Expert: кандидату
необходимо пройти все экзамены предыдущих уровней и иметь по крайней мере 7 лет
практического опыта в тестировании. Наличие сертификата ISTQB высоко ценится заказчиками (в
основном зарубежными).

Бронирование экзамена

Для начала необходимо забронировать место на экзамене. Это можно сделать на сайте GASQ или
на сайте российского представительства.

Нужно заполнить форму с контактными данными о себе и указать язык сдачи экзамена. Важное
замечание про способы оплаты:

Обратите внимание на способ оплаты! Выбрав счет-фактуру, Вы обязываетесь перевести
деньги на наш немецкий банковский счет. При выборе оплаты кредитной картой Вы можете произвести оплату с помощью кредитной карты онлайн.

Для подтверждения регистрации необходимо оплатить взнос (150 евро). Связываться с оплатой
банковского счета мне не хотелось, а при оплате картой могут возникнуть проблемы: платежная
система на сайте gasq.org пытается отобразить страницы для подтверждения 3ds в iframe, что
запрещено некоторыми банками. К примеру, моя дежурная карта меня подвела:

Зато спасла карта Сбера.
Отдельно хочу отметить, что у GASQ есть контактная почта и с нее даже отвечают живые люди :)
Я написала им о проблеме с оплатой, надеюсь, в будущем процесс оплаты будет изменен.

Регистрация на экзамен

После успешной оплаты на почту пришло письмо о подтверждении бронирования экзамена и
указание, что примерно за неделю до даты экзамена мне придет письмо с информацией о
времени и месте сдачи экзамена.

На самом деле письмо пришло за 5 дней до экзамена, когда я уже начала волноваться и писать им
с вопросами. В письме сообщалось, что в забронированную дату экзамен не может состояться по
причине Covid-19 и моя регистрация переносится на следующую дату, которую придется ждать
более трех месяцев. Однако, я могу выбрать вариант сдачи через онлайн-прокторинг в
изначально выбранную дату. Для этого необходимо в ответ написать подтверждение.

Прокторинг это процесс (процедура) контроля и наблюдения за каким-либо дистанционным испытанием или экзаменом (от англ. "proctor" человек, который следит за тем, чтобы экзамены в университете проходили без каких-либо нарушений).

Далее поступило письмо о том, что для завершения регистрации на экзамен необходимо пройти
техническую проверку System Check не менее чем за 48 часов до начала экзамена.

Техническая подготовка к сдаче экзамена онлайн

Подробное описание и инструкция по сдаче экзамена онлайн приходит на почту в виде pdf-файла
(прямая ссылка на этот документ).

Потребуются:

  • стационарный компьютер или ноутбук с веб-камерой, микрофоном и динамиками
    (именно микрофон и динамики компьютера, а не гарнитура; использование наушников на
    экзамене запрещено);

  • браузер Google Chrome с установленным расширением ProctorExam Screen Sharing;
    документ с фото, подтверждающий личность: паспорт, водительские права, студенческий
    билет (номер документа и иную приватную информацию на документе можно скрыть);

  • смартфон или планшет с камерой (будут записывать общий план рабочего места, с
    которого проводится экзамен);

  • стабильный Интернет.

Для начала проверки System Check проходим по ссылке из письма. Обязательно проводим
проверку точно на том оборудовании, которое планируется использовать на самом экзамене.

Этапы проверки:

  1. Микрофон
    На экзамене будет производиться запись окружающих звуков. На проверке
    потребуется просто создать шум, чтобы понять, что микрофон работает и записи звука
    не мешают системные настройки и разрешения.

  2. Динамики
    Стандартная проверка Если Вы слышите рингтон, нажмите да.

  3. Интернет
    Фактической проверки Интернет-соединения не будет, только предупредительное
    сообщение про необходимость стабильного Интернета.

  4. Веб-камера
    Стандартная проверка, видно ли видео с веб-камеры.

  5. Мобильное устройство
    Необходимо установить на устройство приложение ProctorExam. Запустить
    приложение и сканировать им выведенный на экран компьютера QR-код.

  6. Скриншеринг
    Проверка возможности показывать экран.

По завершении проверок вы увидите примерно такой экран:

Сразу после завершения System Check на почту должно прийти письмо со ссылкой для перехода к
экзамену.

В моем случае мне пришло два одинаковых письма для прохождения System Check. Я успешно провела проверку и стала ждать письма со ссылкой. Прошло двое суток, письма не было. Я догадалась пройти проверку по второму письму и о, чудо! сразу же пришло письмо со ссылкой на экзамен.

Процесс сдачи экзамена онлайн

Перед началом сдачи экзамена необходимо проверить, соответствует ли место проведения
экзамена некоторым условиям.

В комнате, в которой будет проходить экзамен, должен находиться только один экзаменуемый,
без посторонних людей. Нужно позаботиться о хорошем освещении и отсутствии громких шумов.
На рабочем столе и в пределах досягаемости сдающего не должно быть посторонних предметов.
Можно использовать офисную доску для ведения заметок.

За всем этим будут наблюдать через камеры экзаменаторы веб-камера компьютера следит за
лицом, а камера мобильного устройства должна быть расположена на расстоянии 3 метров и
снимать общий план.

Хочу отметить, что процесс экзамена очень хорошо продуман, и перед началом будут еще раз
даны все необходимые указания.

Мой экзамен начинался в 10 утра. Если пройти по ссылке раньше, то отобразится страница,
отсчитывающая минуты до начала экзамена. Там же указан временной интервал, в течение
которого можно стартовать, 45 минут (то есть с 10:00 до 10:45). До начала стартового окна не
забудьте еще раз убедиться, что все необходимые условия соблюдены, а также предварительно
сходить в туалет. Простите за такие детали, но во время экзамена нельзя выходить за кадр
ведущейся видеосъемки.

После старта выполняем все, что от нас потребуют. Сразу скажу, что каждое действие
сопровождалось инструкцией и gif-иллюстрацией, все было максимально понятно.

1. Через камеру компьютера сделать фото документа и своего лица.

2. На мобильном устройстве включить режим полета и подключиться к wi-fi. Через приложение ProctorExam отсканировать QR-код с экрана компьютера. С этого момента мобильное устройство начинает снимать видео, которое на время проверок будет транслироваться на экране компьютера.

3. Далее последовательно необходимо через камеру мобильного телефона показать:

  • компьютер, клавиатуру

  • рабочий стол и пространство вокруг компьютера

  • потолок и пространство под столом

  • углы комнаты

  • уши (чтобы показать отсутствие прослушивающих устройств)

4. Располагаем обе камеры. Мобильное устройство располагаем так, чтобы оно на
расстоянии 3 метров снимало общим планом экзаменуемого и захватывало экран
компьютера. Камера компьютера должна снимать лицо.
Если планируется использование доски для записей, то она должна быть в видимости
камер. Перед началом экзамена необходимо показать, что доска полностью чистая.

Рекомендую заранее подумать и порепетировать, как закрепить телефон или планшет в
удобном положении. Меня спас велосипедный держатель для телефона, который был
приделан к стулу. В домашних условиях придется подключить смекалку.

Мы прошли почти все технические мытарства. После требуемой подготовки, наконец-то, будет
получена ссылка для перехода непосредственно к экзамену. Она откроется в новой вкладке.

Обратите внимание, первая вкладка должна оставаться открытой до момента окончания
экзамена. Также на первой вкладке есть окно чата с проктором и чат технической поддержки.
Мне они не понадобились.

Внимательно читаем условия экзаменаВнимательно читаем условия экзамена

По инструкции далее необходимо внимательно заполнить форму, однако у меня она уже была
полностью заполнена данными с регистрации.

Еще раз нажимаем на кнопку старта экзамена:

Еще раз читаем условия экзамена:

Чтобы освоиться с окном экзамена, рекомендую заранее выполнить пробный экзамен на
официальном сайте
.

Мои результаты

Сразу после нажатия на кнопку завершения теста будет известен результат сдачи экзамена
PASSED или FAILED. На экран выводится таблица с общим результатом сдачи, а для дотошных
процентные результаты по темам. К сожалению, нет разбора правильных и неправильных
ответов.

Сам сертификат поступил на почту в виде pdf-файла через 3 дня после сдачи экзамена (экзамен
был в субботу, сертификат пришел во вторник).

Заключение

В качестве заключения попробую ответить на некоторые часто возникающие вопросы.

Нужно ли вообще сдавать ISTQB?

Вопрос, так ли нужно наличие сертификата ISTQB для работы тестировщиком, можно сравнить с
холиварным вопросом о необходимости корочки о высшем образовании. Много различных
мнений: большинство за то, что высшее образование необходимо, но бывают и исключения.
Меньшинство цепляется за истории успешных людей без высшего образования и считает
получение диплома ненужным архаизмом. Несомненно, для кандидата на должность
тестировщика наличие сертификата ISTQB будет плюсом.

Для себя я определила сертификат мне нужен. Не только как бумажка, но и как бесценный опыт.
Во время подготовки мне пришлось проштудировать много источников информации, что помогло разложить по полочкам в голове имеющиеся знания и изучить что-то новое.

Просто для интереса я зашла на hh.ru и задала запрос по ключевому слову ISTQB в описании
вакансий по всей России. Результат 64 вакансии (на 20 декабря 2020).

Для сравнения, всего по профобласти Тестирование на тот же момент было 5833 вакансии:

На каком языке сдавать экзамен на русском или на английском?

Сдавать экзамен на русском языке я советовала бы в двух случаях:

  1. Плохое знание или полное незнание английского языка;

  2. Не планируется дальнейшая сдача других уровней ISTQB, кроме начального.

Во всех остальных случаях я рекомендую сразу готовить себя к сдаче на английском языке. Во-первых, дальнейшие модули ISTQB доступны только на английском языке. Поскольку
сертификация ISTQB ценится в основном зарубежными заказчиками, то скорее всего сдающему
пригодится знание терминологии в английском варианте. Во-вторых, вопросы экзамена на русском языке это в любом случае перевод с английского. Возможно, я придираюсь, но многие
вопросы звучат более органично и понятнее в оригинале.
При этом нужно учитывать, что на экзамене понадобится широкий словарный запас. Скорее всего, неизвестным окажется главное слово вопроса, которое определяет смысл.

Небольшая викторина-разминка.

Попробуйте оценить свое знание английского языка по шкале от 0 до 10, вспомнив перевод следующих слов:
discrepancy
to undertake
entrepreneur
harness
extent
density
linkage
precise
complementary
prevail
(слова взяты из сборника вопросов по подготовке к ISTQB)

Небольшой бонус при сдаче экзамена на неродном языке ко времени добавляются
дополнительные 15 минут.

Где найти всю организационную информацию?

На сайте GASG. Здесь можно найти все официальные инструкции, FAQ, скачать силлабус и пройти
демо-экзамен.

Как лучше готовиться? Какие источники использовать?

Ответ на этот вопрос тянет на отдельную статью со списком источников. Попробую дать краткую
выдержку основных моментов.

Самый лучший источник собственный опыт. Проще всего мне давались вопросы, ответы на
которые я могу дать, исходя из опыта. Но не полагайтесь только на свои знания, используйте
разные виды подготовки.

Обязательно попробуйте сдать пробный экзамен на официальном сайте.

Скачайте и изучите силлабус.

Прочитайте хотя бы одну книгу. Советую Foundations of Software Testing: ISTQB Certification от
авторов Andreas Spillner, Tilo Linz, Hans Schaefer, которые непосредственно связаны с работой над
ISTQB. Содержание книги тесно переплетается с темами вопросов ISTQB и покрывает практически
весь необходимый теоретический минимум. Полный список книг по ссылке.

Обязательно потренируйтесь в решении вопросов теста на нескольких разных платформах.
Многие вопросы экзамена специально составлены так, чтобы сбить с толку, чтобы правильным
казался неправильный вариант. Лучше заранее потренироваться, чем столкнуться с этим уже на
самом экзамене. Попробую привести пример:

Q: Which of the following could be a disadvantage of independent testing?
A. Developer and independent testing will overlap and waste resources.
B. Communication is limited between independent testers and developers.
C. Independent testers are too slow and delay the project schedule.
D. Developers can lose a sense of responsibility for quality.

Если не знать правильный ответ, то акцент в вопросе на независимость тестирования (independent
testing) так и подсказывает выбрать ответ про коммуникацию между тестировщиками и
разработчиками. Вспомните свой опыт, как часто вы сами обращаетесь к разработчикам для
уточнения деталей?

Однако, правильный ответ D.

Рекомендую установить на телефон приложения для решения вопросов по ISTQB и ежедневно
ими пользоваться. Из всего разнообразия особенно оказалось полезным приложение Test Mentor
for ISTQB
. В нем есть survival mode, в котором необходимо набрать как можно больше очков,
правильно отвечая на вопросы. Если ответ выбран верно переход к следующему вопросу, если
неверно конец игры и показ правильного ответа. Очень удобно, что правильный ответ
отображается сразу, а не после завершения всего экзамена из 20-40 вопросов, как это
реализовано в других приложениях.

Вопросы на какие темы скорее всего будут на экзамене?

Темы вопросов экзамена всегда одни и те же:

  1. Fundamentals of Testing

  2. Testing Throughout the Software Development Lifecycle

  3. Static Testing

  4. Test Techniques

  5. Test Management

  6. Tool Support for Testing

Некоторые вопросы из подготовительных экзаменов могут встретиться в настоящем экзамене.
Сложилось впечатление, что некоторые вопросы даже кочуют из экзамена в экзамен с
небольшими переформулировками. Один из таких примеров:

Which of the following statements BEST describes the difference between testing and debugging?
A. Testing pinpoints (identifies the source of) the defects. Debugging analyzes the faults and proposes
prevention activities.
B. Dynamic testing shows failures caused by defects. Debugging finds, analyzes, and removes the causes
of failures in the software.
C. Testing removes faults. Debugging identifies the causes of failures.
D. Dynamic testing prevents causes of failures. Debugging removes the failures.

Which of the following statements correctly describes the difference between testing and debugging?
A. Testing shows failures caused by defects; debugging finds, analyses, and removes the failures in the
software.
B. Testing identifies the source of defects; debugging analyses the defects and proposes prevention
activities.
C. Testing prevents the causes of failures; debugging removes the failures.
D. Testing removes faults; debugging identifies the causes of failures.

Программу какого года (2011 или 2018) выбрать для изучения?

На текущий момент для сдачи экзамена можно выбрать только версию 2018 года. Здесь действует
простое правило чем свежее материал для подготовки, тем лучше.

Сколько времени нужно на подготовку?

Сложно дать единый ответ, который будет применим для каждого человека. Мне лично оказалось
достаточно 5 недель подготовки. Исходя из информации, что в моем городе экзамен проходит
примерно 3 раза в год, я решила сдавать его в ближайшую доступную дату.

Если есть дополнительные вопросы, готова ответить на них в комментариях.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru