-Поиск по дневнику

Поиск сообщений в rss_rss_hh_full

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 1

Habrahabr








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

It's a (focus) Trap

Вторник, 19 Сентября 2017 г. 15:33 + в цитатник

Метки:  

Sberbank In-Memory Computing Day

Вторник, 19 Сентября 2017 г. 15:14 + в цитатник
8 октября в Москве Сбербанк проведет мероприятие Sberbank InMemory Computing Day. Это уникальный форум для популяризации IMDG технологий (In-Memory Data Grid), в котором примут участие лидеры компаний мирового уровня: IBM, Boeing, Intel, SAP, Mail.Ru Group, EPAM и другие. В программе запланированы выступления спикеров ИТ-индустрии и мастер классы по четырем направлениям: IMDG, DevOps, Artificial Intelligence (AI), Internet of Things (IoT). Также компании-партнеры банка представят на выставочной площадке форума инновационные решения по разработке в области IoT и AI. читать далее

https://habrahabr.ru/post/338234/


Метки:  

Sberbank In-Memory Computing Day

Вторник, 19 Сентября 2017 г. 15:14 + в цитатник
8 октября в Москве Сбербанк проведет мероприятие Sberbank InMemory Computing Day. Это уникальный форум для популяризации IMDG технологий (In-Memory Data Grid), в котором примут участие лидеры компаний мирового уровня: IBM, Boeing, Intel, SAP, Mail.Ru Group, EPAM и другие. В программе запланированы выступления спикеров ИТ-индустрии и мастер классы по четырем направлениям: IMDG, DevOps, Artificial Intelligence (AI), Internet of Things (IoT). Также компании-партнеры банка представят на выставочной площадке форума инновационные решения по разработке в области IoT и AI. читать далее

https://habrahabr.ru/post/338234/


Метки:  

[Перевод] Создаём GTK-видеоплеер с использованием Haskell

Вторник, 19 Сентября 2017 г. 15:04 + в цитатник
AloneCoder сегодня в 15:04 Разработка

Создаём GTK-видеоплеер с использованием Haskell

  • Перевод


Когда мы в последний раз остановились на Movie Monad, мы создали десктопный видео-плеер, использующий все веб-технологии (HTML, CSS, JavaScript и Electron). Фокус был в том, что весь исходный код проекта был написан на Haskell.


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


Мы могли бы продолжить развивать наш подход с вебом, настроив бэкенд на стриминг видеофайла в HTML5-сервер, запустив параллельно сервер и Electron-приложение. Вместо этого мы откажемся от веб-технологий и обратимся к GTK+, Gstreamer и системе управления окнами X11.


image


Если вы используете другую систему управления окнами, например, Wayland, Quartz или WinAPI, то этот подход может быть адаптирован для работы с вашим GDK-бэкендом. Адаптация заключается во встраивании выходного видеосигнала GStreamer playbin в окно Movie Monad.


GDK — важный аспект портируемости GTK+. Поскольку Glib уже предоставляет низкоуровневую кроссплатформенную функциональность, то чтобы заставить GTK+ работать на других платформах вам нужно только портировать GDK на базовый графический уровень операционной системы. То есть именно GDK-порты на Windows API и Quartz позволяют приложениям GTK+ исполняться на Windows и macOS (источник).


Для кого эта статья


  • Для программистов на Haskell, которые хотят реализовать пользовательский интерфейс на GTK+.
  • Для программистов, интересующихся функциональным программированием.
  • Для создателей GUI.
  • Для тех, кто ищет альтернативы GitHub Electron.
  • Для фанатов видео-плееров.

Что мы рассмотрим


  • Stack.
  • Привязки (bindings) haskell-gi
  • Директорию различных данных и файлы с ними.
  • Glade.
  • GTK+.
  • GStreamer.
  • Как создать Movie Monad.

Настройка проекта


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


Платформа Haskell


Если ваша машина ещё не готова к разработке Haskell-программ, то всё необходимое вы можете получить, скачав и установив платформу Haskell.


Stack


Если у вас ещё нет Stack, то обязательно установите его, прежде чем приступать к разработке. Но если вы уже пользовались платформой Haskell, то Stack у вас уже есть.


ExifTool


Прежде чем проигрывать видео в Movie Monad, нам нужно собрать кое-какую информацию о выбранном пользователем файле. Для этого воспользуемся ExifTool. Если вы работаете под Linux, то велик шанс, что у вас уже есть этот инструмент (which exiftool). ExifTool доступен для Windows, Mac и Linux.


Файлы проекта


Есть три способа получения файлов проекта.


wget https://github.com/lettier/movie-monad/archive/master.zip
unzip master.zip
mv movie-monad-master movie-monad
cd movie-monad/

Можете скачать ZIP-архив и извлечь их.


git clone git@github.com:lettier/movie-monad.git
cd movie-monad/

Можете сделать Git-клон с помощью SSH.


git clone https://github.com/lettier/movie-monad.git
cd movie-monad/

Можете склонировать git через HTTPS.


haskell-gi


haskell-gi умеет генерировать Haskell-привязки (bindings) к библиотекам, использующим связующее ПО для самодиагностики (introspection middleware) GObject. На момент написания статьи все необходимые привязки доступны на Hackage.


Зависимости


Теперь устанавливаем зависимости проекта.


cd movie-monad/
stack install --dependencies-only

Код


Теперь настраиваем внедрение Movie Monad. Вы можете удалить исходные файлы и создать их заново, или следовать указаниям.


Paths_movie_monad.hs


Paths_movie_monad.hs используется для поиска файла Glade XML GUI во время runtime. Поскольку мы занимаемся разработкой, то будем использовать фиктивный модуль (dummy module) (movie-monad/src/dev/Paths_movie_monad.hs) для поиска файла movie-monad/src/data/gui.glade. После сборки/установки проекта реальный модуль Paths_movie_monad будет сгенерирован автоматически. Он предоставит нам функцию getDataFileName. Она присваивает своим выходным данным префикс в виде абсолютного пути, куда скопированы или установлены data-dir (movie-monad/src/) data-files.


{-# LANGUAGE OverloadedStrings #-}

module Paths_movie_monad where

dataDir :: String
dataDir = "./src/"

getDataFileName :: FilePath -> IO FilePath
getDataFileName a = do
  putStrLn "You are using a fake Paths_movie_monad."
  return (dataDir ++ "/" ++ a)

Фиктивный модуль Paths_movie_monad.


{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-missing-import-lists #-}
{-# OPTIONS_GHC -fno-warn-implicit-prelude #-}
module Paths_movie_monad (
    version,
    getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir,
    getDataFileName, getSysconfDir
  ) where

import qualified Control.Exception as Exception
import Data.Version (Version(..))
import System.Environment (getEnv)
import Prelude

#if defined(VERSION_base)

#if MIN_VERSION_base(4,0,0)
catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a
#else
catchIO :: IO a -> (Exception.Exception -> IO a) -> IO a
#endif

#else
catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a
#endif
catchIO = Exception.catch

version :: Version
version = Version [0,0,0,0] []
bindir, libdir, dynlibdir, datadir, libexecdir, sysconfdir :: FilePath

bindir     = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/bin"
libdir     = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0"
dynlibdir  = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2"
datadir    = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/share/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0"
libexecdir = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/libexec"
sysconfdir = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/etc"

getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir, getSysconfDir :: IO FilePath
getBinDir = catchIO (getEnv "movie_monad_bindir") (\_ -> return bindir)
getLibDir = catchIO (getEnv "movie_monad_libdir") (\_ -> return libdir)
getDynLibDir = catchIO (getEnv "movie_monad_dynlibdir") (\_ -> return dynlibdir)
getDataDir = catchIO (getEnv "movie_monad_datadir") (\_ -> return datadir)
getLibexecDir = catchIO (getEnv "movie_monad_libexecdir") (\_ -> return libexecdir)
getSysconfDir = catchIO (getEnv "movie_monad_sysconfdir") (\_ -> return sysconfdir)

getDataFileName :: FilePath -> IO FilePath
getDataFileName name = do
  dir <- getDataDir
  return (dir ++ "/" ++ name)

Автоматически сгенерированный модуль Paths_movie_monad.


Main.hs


Main.hs — это входная точка для Movie Monad. В этом файле мы настраиваем наше окно с разными виджетами, подключаем GStreamer, а когда пользователь выходит, мы сносим окно.


Прагмы (Pragmas)


Нам нужно сказать компилятору (GHC), что нам нужны перегруженные (overloaded) строковые и лексически входящие в область видимости (lexically scoped) переменные типов.


OverloadedStrings позволяет нам использовать строковые литералы ("Literal") там, где требуются String/[Char] или Text. ScopedTypeVariables позволяет нам использовать сигнатуру типа в паттерне параметра лямбда-функции, передаваемую для перехвата при вызове ExifTool.


{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

Импорты


module Main where

import Prelude
import Foreign.C.Types
import System.Process
import System.Exit
import Control.Monad
import Control.Exception
import Text.Read
import Data.IORef
import Data.Maybe
import Data.Int
import Data.Text
import Data.GI.Base
import Data.GI.Base.Signals
import Data.GI.Base.Properties
import GI.GLib
import GI.GObject
import qualified GI.Gtk
import GI.Gst
import GI.GstVideo
import GI.Gdk
import GI.GdkX11
import Paths_movie_monad

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


IsVideoOverlay


GStreamer-видеопривязки (gi-gstvideo) содержат класс типа (интерфейс) IsVideoOverlay. GStreamer-привязки (gi-gst) содержат тип элемента. Чтобы использовать элемент playbin с функцией GI.GstVideo.videoOverlaySetWindowHandle, нам нужно объявить GI.Gst.Element — экземпляр типа (type instance) IsVideoOverlay. А на стороне Cи playbin реализует интерфейс VideoOverlay.


newtype GstElement = GstElement GI.Gst.Element
instance GI.GstVideo.IsVideoOverlay GstElement

Обратите внимание, что мы обёртываем GI.Gst.Element в новый тип (newtype), чтобы избежать появления потерянного (orphaned) экземпляра, поскольку мы объявляем экземпляр вне привязок haskell-gi.


main


Main — наша самая большая функция. В ней мы инициализируем все GUI-виджеты и определяем коллбэк-процедуры на основе определённых событий.


main :: IO ()
main = do

GI-инициализация


  _ <- GI.Gst.init Nothing
  _ <- GI.Gtk.init Nothing

Здесь мы инициализировали GStreamer и GTK+.


Сборка GUI-виджетов


  gladeFile <- getDataFileName "data/gui.glade"
  builder <- GI.Gtk.builderNewFromFile (pack gladeFile)

  window <- builderGetObject GI.Gtk.Window builder "window"
  fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button"
  drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area"
  seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"
  onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"
  volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"
  desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"
  fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button"
  errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog"
  aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"
  aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"

Как уже было сказано, мы получаем абсолютный путь к XML-файлу data/gui.glade, который описывает все наши GUI-виджеты. Дальше создаём из этого файла конструктор и получаем свои виджеты. Если бы мы не использовали Glade, то их пришлось бы создавать вручную, что довольно утомительно.


Playbin


  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer")

Здесь мы создаём GStreamer-конвейер playbin. Он предназначен для решения самых разных нужд и экономит нам время на создании собственного конвейера. Назовём этот элемент MultimediaPlayer.


Встраиванние выходных данных GStreamer


Чтобы GTK+ и GStreamer заработали вместе, нам нужно сказать GStreamer, куда именно нужно выводить видео. Если этого не сделать, то GStreamer создаст собственное окно, поскольку мы используем playbin.


  _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton

-- ...

onDrawingAreaRealize ::
  GI.Gtk.Widget ->
  GI.Gst.Element ->
  GI.Gtk.Button ->
  GI.Gtk.WidgetRealizeCallback
onDrawingAreaRealize drawingArea playbin fullscreenButton = do
  gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea
  x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow

  xid <- GI.GdkX11.x11WindowGetXid x11Window
  let xid' = fromIntegral xid :: CUIntPtr

  GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid'

  GI.Gtk.widgetHide fullscreenButton

Здесь вы видите настройку коллбэка по мере готовности виджета drawingArea. Именно в этом виджете GStreamer должен показывать видео. Мы получаем родительское GDK-окно для виджета области отрисовки. Затем получаем обработчик окна, или XID системы X11 нашего окна GTK+. Строка CUIntPtr преобразует ID из CULong в CUIntPtr, необходимый для videoOverlaySetWindowHandle. Получив правильный тип, мы уведомляем GStreamer, что с помощью обработчика xid' он может отрисовывать в нашем окне выходные данные playbin.


Из-за бага в Glade мы программно скрываем полноэкранный виджет, поскольку если в Glade снять галочку visible box, то виджет всё-равно не будет спрятан.


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


Выбор файла


  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $
    onFileChooserButtonFileSet
      playbin
      fileChooserButton
      volumeButton
      isWindowFullScreenRef
      desiredVideoWidthComboBox
      onOffSwitch
      fullscreenButton
      drawingArea
      window
      errorMessageDialog

-- ...

onFileChooserButtonFileSet ::
  GI.Gst.Element ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.VolumeButton ->
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Switch ->
  GI.Gtk.Button ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  GI.Gtk.MessageDialog ->
  GI.Gtk.FileChooserButtonFileSetCallback
onFileChooserButtonFileSet
  playbin
  fileChooserButton
  volumeButton
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  onOffSwitch
  fullscreenButton
  drawingArea
  window
  errorMessageDialog
  = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull

  filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton

  setPlaybinUriAndVolume playbin filename volumeButton

  isWindowFullScreen <- readIORef isWindowFullScreenRef

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused
      GI.Gtk.windowUnfullscreen window
      GI.Gtk.switchSetActive onOffSwitch False
      GI.Gtk.widgetHide fullscreenButton
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
      _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)
      void $ GI.Gtk.dialogRun errorMessageDialog
    Just (width, height) -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying
      GI.Gtk.switchSetActive onOffSwitch True
      GI.Gtk.widgetShow fullscreenButton
      unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window

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


  • Получаем имя файла из виджета выбора файла.
  • Говорим playbin, какой файл он должен воспроизвести.
  • Делаем уровень громкомсти в виджете таким же, как в playbin.
  • На основе желаемой ширины изображения и размера видео определяем подходящие ширину и высоту окна.
  • Если размеры окна успешно получены:
    • Начинаем воспроизведение файла.
    • Переводим кнопку пауза/воспроизведение в состояние ”on”.
    • Показываем полноэкранный виджет.
    • Если видео не в полноэкранном режиме:
    • Меняем размер окна, чтобы оно совпало с относительным размером видео.
  • Если не удалось получить размеры окна:
    • Ставим playbin на паузу.
    • Переводим переключатель в положение ”off”.
    • Если это возможно, выводим окно из полноэкранного режима.
    • Сбрасываем размер окна.
    • Выводим маленькое диалоговое сообщение об ошибке.

Пауза и воспроизведение


  _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin)

-- ...

onSwitchStateSet ::
  GI.Gst.Element ->
  Bool ->
  IO Bool
onSwitchStateSet playbin switchOn = do
  if switchOn
    then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying
    else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused
  return switchOn

Всё просто. Если переключатель в положении ”on”, то задаём элементу playbin состояние воспроизведения. В противном случае задаём ему состояние паузы.


Настройка громкости


  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin)

-- ...

onScaleButtonValueChanged ::
  GI.Gst.Element ->
  Double ->
  IO ()
onScaleButtonValueChanged playbin volume =
    void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume

При изменении уровня громкости в виджете мы передаём его значение в GStreamer, чтобы тот мог подстроить громкость воспроизведение.


Перемещение по видео


  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale)

-- ...

onRangeValueChanged ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  IO ()
onRangeValueChanged playbin seekScale = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime

  when couldQueryDuration $ do
    percentage' <- GI.Gtk.rangeGetValue seekScale
    let percentage = percentage' / 100.0
    let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64
    void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position

В Movie Monad есть шкала воспроизведения, в которой вы можете перемещать ползунок вперёд/назад, тем самым переходя по видеофреймам.


Шкала от 0 до 100% представляет общую длительность видео-файла. Если переместить ползунок, например, на 50, то мы перейдём к временной отметке, находящийся посередине между началом и окончанием. Можно было бы настроить шкалу от нуля до значения длительности видео, но описанный метод более универсален.


Обратите внимание, что для этого коллбэка мы используем сигнальный ID (seekScaleHandlerId), поскольку он понадобится нам позднее.


Обновление шкалы воспроизведения


  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId)

-- ...

updateSeekScale ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  Data.GI.Base.Signals.SignalHandlerId ->
  IO Bool
updateSeekScale playbin seekScale seekScaleHandlerId = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime
  (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime

  let percentage =
        if couldQueryDuration && couldQueryPosition && duration > 0
          then 100.0 * (fromIntegral position / fromIntegral duration :: Double)
          else 0.0

  GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId
  GI.Gtk.rangeSetValue seekScale percentage
  GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId

  return True

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


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


Изменение размеров видео


  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $
      onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window

-- ...

onComboBoxChanged ::
  GI.Gtk.FileChooserButton ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
onComboBoxChanged
  fileChooserButton
  desiredVideoWidthComboBox
  drawingArea
  window
  = do
  filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton
  let filename = fromMaybe "" filename'

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
    Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window

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


Полноэкранный режим


  _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton
      (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window)

-- ...

onFullscreenButtonRelease ::
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Window ->
  GI.Gdk.EventButton ->
  IO Bool
onFullscreenButtonRelease
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  fileChooserButton
  window
  _
  = do
  isWindowFullScreen <- readIORef isWindowFullScreenRef
  if isWindowFullScreen
    then do
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      GI.Gtk.widgetShow fileChooserButton
      void $ GI.Gtk.windowUnfullscreen window
    else do
      GI.Gtk.widgetHide desiredVideoWidthComboBox
      GI.Gtk.widgetHide fileChooserButton
      void $ GI.Gtk.windowFullscreen window
  return True

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


Обратите внимание, что мы не показываем виджет полноэкранного режима, если у нас нет видео.


  _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef)

-- ...

onWidgetWindowStateEvent ::
  IORef Bool ->
  GI.Gdk.EventWindowState ->
  IO Bool
onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do
  windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState
  let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates
  writeIORef isWindowFullScreenRef isWindowFullScreen
  return True

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


Благодаря использованию в главном потоке выполнения единственного пишущего и кучи сигнальных коллбэков, мы избегаем возможных ловушек общего изменяемого состояния. Если бы нас заботила безопасность потока выполнения, то вместо этого мы могли бы использовать MVar, TVar или atomicModifyIORef.


О программе


  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog)

-- ...

onAboutButtonRelease ::
  GI.Gtk.AboutDialog ->
  GI.Gdk.EventButton ->
  IO Bool
onAboutButtonRelease aboutDialog _ = do
  _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)
  _ <- GI.Gtk.dialogRun aboutDialog
  return True

Последний рассматриваемый виджет — диалоговое окно «О программе». Здесь мы связываем диалоговое окно с кнопкой «О программе», отображающейся в основном окне.


Закрытие окна


  _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin)

-- ...

onWindowDestroy ::
  GI.Gst.Element ->
  IO ()
onWindowDestroy playbin = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull
  _ <- GI.Gst.objectUnref playbin
  GI.Gtk.mainQuit

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


Запуск


  GI.Gtk.widgetShowAll window
  GI.Gtk.main

Наконец, мы показываем или отрисовываем главное окно и запускаем основной цикл GTK+. Он блокируется до вызова mainQuit.


Полный файл Main.hs


Ниже приведён файл movie-monad/src/Main.hs. Не показаны разные вспомогательные функции, относящиеся к main.


{-
  Movie Monad
  (C) 2017 David lettier
  lettier.com
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import Prelude
import Foreign.C.Types
import System.Process
import System.Exit
import Control.Monad
import Control.Exception
import Text.Read
import Data.IORef
import Data.Maybe
import Data.Int
import Data.Text
import Data.GI.Base
import Data.GI.Base.Signals
import Data.GI.Base.Properties
import GI.GLib
import GI.GObject
import qualified GI.Gtk
import GI.Gst
import GI.GstVideo
import GI.Gdk
import GI.GdkX11
import Paths_movie_monad

-- Declare Element a type instance of IsVideoOverlay via a newtype wrapper
-- Our GStreamer element is playbin
-- Playbin implements the GStreamer VideoOverlay interface
newtype GstElement = GstElement GI.Gst.Element
instance GI.GstVideo.IsVideoOverlay GstElement

main :: IO ()
main = do
  _ <- GI.Gst.init Nothing
  _ <- GI.Gtk.init Nothing

  gladeFile <- getDataFileName "data/gui.glade"
  builder <- GI.Gtk.builderNewFromFile (pack gladeFile)

  window <- builderGetObject GI.Gtk.Window builder "window"
  fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button"
  drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area"
  seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"
  onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"
  volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"
  desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"
  fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button"
  errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog"
  aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"
  aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"

  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer")

  isWindowFullScreenRef <- newIORef False

  _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton

  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $
    onFileChooserButtonFileSet
      playbin
      fileChooserButton
      volumeButton
      isWindowFullScreenRef
      desiredVideoWidthComboBox
      onOffSwitch
      fullscreenButton
      drawingArea
      window
      errorMessageDialog

  _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin)

  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin)

  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale)

  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId)

  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $
      onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window

  _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton
      (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window)

  _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef)

  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog)

  _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin)

  GI.Gtk.widgetShowAll window
  GI.Gtk.main

builderGetObject ::
  (GI.GObject.GObject b, GI.Gtk.IsBuilder a) =>
  (Data.GI.Base.ManagedPtr b -> b) ->
  a ->
  Prelude.String ->
  IO b
builderGetObject objectTypeClass builder objectId =
  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=
    GI.Gtk.unsafeCastTo objectTypeClass

onDrawingAreaRealize ::
  GI.Gtk.Widget ->
  GI.Gst.Element ->
  GI.Gtk.Button ->
  GI.Gtk.WidgetRealizeCallback
onDrawingAreaRealize drawingArea playbin fullscreenButton = do
  gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea
  x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow

  xid <- GI.GdkX11.x11WindowGetXid x11Window
  let xid' = fromIntegral xid :: CUIntPtr

  GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid'

  GI.Gtk.widgetHide fullscreenButton

onFileChooserButtonFileSet ::
  GI.Gst.Element ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.VolumeButton ->
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Switch ->
  GI.Gtk.Button ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  GI.Gtk.MessageDialog ->
  GI.Gtk.FileChooserButtonFileSetCallback
onFileChooserButtonFileSet
  playbin
  fileChooserButton
  volumeButton
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  onOffSwitch
  fullscreenButton
  drawingArea
  window
  errorMessageDialog
  = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull

  filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton

  setPlaybinUriAndVolume playbin filename volumeButton

  isWindowFullScreen <- readIORef isWindowFullScreenRef

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused
      GI.Gtk.windowUnfullscreen window
      GI.Gtk.switchSetActive onOffSwitch False
      GI.Gtk.widgetHide fullscreenButton
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
      _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)
      void $ GI.Gtk.dialogRun errorMessageDialog
    Just (width, height) -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying
      GI.Gtk.switchSetActive onOffSwitch True
      GI.Gtk.widgetShow fullscreenButton
      unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window

onSwitchStateSet ::
  GI.Gst.Element ->
  Bool ->
  IO Bool
onSwitchStateSet playbin switchOn = do
  if switchOn
    then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying
    else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused
  return switchOn

onScaleButtonValueChanged ::
  GI.Gst.Element ->
  Double ->
  IO ()
onScaleButtonValueChanged playbin volume =
    void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume

onRangeValueChanged ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  IO ()
onRangeValueChanged playbin seekScale = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime

  when couldQueryDuration $ do
    percentage' <- GI.Gtk.rangeGetValue seekScale
    let percentage = percentage' / 100.0
    let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64
    void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position

updateSeekScale ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  Data.GI.Base.Signals.SignalHandlerId ->
  IO Bool
updateSeekScale playbin seekScale seekScaleHandlerId = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime
  (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime

  let percentage =
        if couldQueryDuration && couldQueryPosition && duration > 0
          then 100.0 * (fromIntegral position / fromIntegral duration :: Double)
          else 0.0

  GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId
  GI.Gtk.rangeSetValue seekScale percentage
  GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId

  return True

onComboBoxChanged ::
  GI.Gtk.FileChooserButton ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
onComboBoxChanged
  fileChooserButton
  desiredVideoWidthComboBox
  drawingArea
  window
  = do
  filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton
  let filename = fromMaybe "" filename'

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
    Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window

onFullscreenButtonRelease ::
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Window ->
  GI.Gdk.EventButton ->
  IO Bool
onFullscreenButtonRelease
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  fileChooserButton
  window
  _
  = do
  isWindowFullScreen <- readIORef isWindowFullScreenRef
  if isWindowFullScreen
    then do
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      GI.Gtk.widgetShow fileChooserButton
      void $ GI.Gtk.windowUnfullscreen window
    else do
      GI.Gtk.widgetHide desiredVideoWidthComboBox
      GI.Gtk.widgetHide fileChooserButton
      void $ GI.Gtk.windowFullscreen window
  return True

onWidgetWindowStateEvent ::
  IORef Bool ->
  GI.Gdk.EventWindowState ->
  IO Bool
onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do
  windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState
  let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates
  writeIORef isWindowFullScreenRef isWindowFullScreen
  return True

onAboutButtonRelease ::
  GI.Gtk.AboutDialog ->
  GI.Gdk.EventButton ->
  IO Bool
onAboutButtonRelease aboutDialog _ = do
  _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)
  _ <- GI.Gtk.dialogRun aboutDialog
  return True

onWindowDestroy ::
  GI.Gst.Element ->
  IO ()
onWindowDestroy playbin = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull
  _ <- GI.Gst.objectUnref playbin
  GI.Gtk.mainQuit

setPlaybinUriAndVolume ::
  GI.Gst.Element ->
  Prelude.String ->
  GI.Gtk.VolumeButton ->
  IO ()
setPlaybinUriAndVolume playbin filename volumeButton = do
  let uri = "file://" ++ filename
  volume <- GI.Gtk.scaleButtonGetValue volumeButton
  Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume
  Data.GI.Base.Properties.setObjectPropertyString playbin "uri" (Just $ pack uri)

getVideoInfo :: Prelude.String -> Prelude.String -> IO (Maybe Prelude.String)
getVideoInfo flag filename = do
  (code, out, _) <- catch (
      readProcessWithExitCode
        "exiftool"
        [flag, "-s", "-S", filename]
        ""
    ) (\ (_ :: Control.Exception.IOException) -> return (ExitFailure 1, "", ""))
  if code == System.Exit.ExitSuccess
    then return (Just out)
    else return Nothing

isVideo :: Prelude.String -> IO Bool
isVideo filename = do
  maybeOut <- getVideoInfo "-MIMEType" filename
  case maybeOut of
    Nothing -> return False
    Just out -> return ("video" `isInfixOf` pack out)

getWindowSize :: Int -> Prelude.String -> IO (Maybe (Int32, Int32))
getWindowSize desiredVideoWidth filename =
  isVideo filename >>=
  getWidthHeightString >>=
  splitWidthHeightString >>=
  widthHeightToDouble >>=
  ratio >>=
  windowSize
  where
    getWidthHeightString :: Bool -> IO (Maybe Prelude.String)
    getWidthHeightString False = return Nothing
    getWidthHeightString True = getVideoInfo "-ImageSize" filename
    splitWidthHeightString :: Maybe Prelude.String -> IO (Maybe [Text])
    splitWidthHeightString Nothing = return Nothing
    splitWidthHeightString (Just string) = return (Just (Data.Text.splitOn "x" (pack string)))
    widthHeightToDouble :: Maybe [Text] -> IO (Maybe Double, Maybe Double)
    widthHeightToDouble (Just (x:y:_)) = return (readMaybe (unpack x) :: Maybe Double, readMaybe (unpack y) :: Maybe Double)
    widthHeightToDouble _ = return (Nothing, Nothing)
    ratio :: (Maybe Double, Maybe Double) -> IO (Maybe Double)
    ratio (Just width, Just height) =
      if width <= 0.0 then return Nothing else return (Just (height / width))
    ratio _ = return Nothing
    windowSize :: Maybe Double -> IO (Maybe (Int32, Int32))
    windowSize Nothing = return Nothing
    windowSize (Just ratio') =
      return (Just (fromIntegral desiredVideoWidth :: Int32, round ((fromIntegral desiredVideoWidth :: Double) *  ratio') :: Int32))

getDesiredVideoWidth :: GI.Gtk.ComboBoxText -> IO Int
getDesiredVideoWidth = fmap (\ x -> read (Data.Text.unpack x) :: Int) . GI.Gtk.comboBoxTextGetActiveText

setWindowSize ::
  Int32 ->
  Int32 ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
setWindowSize width height fileChooserButton drawingArea window = do
  GI.Gtk.setWidgetWidthRequest fileChooserButton width

  GI.Gtk.setWidgetWidthRequest drawingArea width
  GI.Gtk.setWidgetHeightRequest drawingArea height

  GI.Gtk.setWidgetWidthRequest window width
  GI.Gtk.setWidgetHeightRequest window height
  GI.Gtk.windowResize window width (if height <= 0 then 1 else height)

resetWindowSize ::
  (Integral a) =>
  a ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
resetWindowSize width' fileChooserButton drawingArea window = do
  let width = fromIntegral width' :: Int32
  GI.Gtk.widgetQueueDraw drawingArea
  setWindowSize width 0 fileChooserButton drawingArea window

Собираем Movie Monad


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


cd movie-monad/
stack clean
stack install
stack exec -- movie-monad
# Or just `movie-monad` if `stack path | grep local-bin-path` is in your `echo $PATH`

Если всё в порядке, то Movie Monad должен запуститься.


Заключение


Пересмотрев проект Movie Monad, мы заново сделали приложение с помощью программных библиотек GTK+ и GStreamer. Благодаря им приложение осталось таким же портируемым, как и Electron-версия. Movie Monad теперь может обрабатывать большие видеофайлы и имеет все стандартные элементы управления.


Другим преимуществом использования GTK+ стало уменьшение потребления памяти. Если сравнивать резидентный размер в памяти при старте, то версия GTK+ занимает ~50 Мб, а версия Electron — ~300 Мб (500%-ное увеличение).


Наконец, вариант с GTK+ имеет меньше ограничений и требует меньше программирования. Для обеспечения такой же функциональности, вариант с Electron требует использования громоздкой клиент-серверной архитектуры. Но благодаря прекрасным сборкам haskell-gi мы смогли избежать решения на базе веба.


Если хотите посмотреть другие приложения, построенные с помощью GTK+ и Haskell, то обратите внимание на Gifcurry. Оно умеет брать видеофайлы и на их основе создавать гифки с наложенным текстом.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/338176/


Метки:  

[Перевод] Создаём GTK-видеоплеер с использованием Haskell

Вторник, 19 Сентября 2017 г. 15:04 + в цитатник
AloneCoder сегодня в 15:04 Разработка

Создаём GTK-видеоплеер с использованием Haskell

  • Перевод


Когда мы в последний раз остановились на Movie Monad, мы создали десктопный видео-плеер, использующий все веб-технологии (HTML, CSS, JavaScript и Electron). Фокус был в том, что весь исходный код проекта был написан на Haskell.


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


Мы могли бы продолжить развивать наш подход с вебом, настроив бэкенд на стриминг видеофайла в HTML5-сервер, запустив параллельно сервер и Electron-приложение. Вместо этого мы откажемся от веб-технологий и обратимся к GTK+, Gstreamer и системе управления окнами X11.


image


Если вы используете другую систему управления окнами, например, Wayland, Quartz или WinAPI, то этот подход может быть адаптирован для работы с вашим GDK-бэкендом. Адаптация заключается во встраивании выходного видеосигнала GStreamer playbin в окно Movie Monad.


GDK — важный аспект портируемости GTK+. Поскольку Glib уже предоставляет низкоуровневую кроссплатформенную функциональность, то чтобы заставить GTK+ работать на других платформах вам нужно только портировать GDK на базовый графический уровень операционной системы. То есть именно GDK-порты на Windows API и Quartz позволяют приложениям GTK+ исполняться на Windows и macOS (источник).


Для кого эта статья


  • Для программистов на Haskell, которые хотят реализовать пользовательский интерфейс на GTK+.
  • Для программистов, интересующихся функциональным программированием.
  • Для создателей GUI.
  • Для тех, кто ищет альтернативы GitHub Electron.
  • Для фанатов видео-плееров.

Что мы рассмотрим


  • Stack.
  • Привязки (bindings) haskell-gi
  • Директорию различных данных и файлы с ними.
  • Glade.
  • GTK+.
  • GStreamer.
  • Как создать Movie Monad.

Настройка проекта


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


Платформа Haskell


Если ваша машина ещё не готова к разработке Haskell-программ, то всё необходимое вы можете получить, скачав и установив платформу Haskell.


Stack


Если у вас ещё нет Stack, то обязательно установите его, прежде чем приступать к разработке. Но если вы уже пользовались платформой Haskell, то Stack у вас уже есть.


ExifTool


Прежде чем проигрывать видео в Movie Monad, нам нужно собрать кое-какую информацию о выбранном пользователем файле. Для этого воспользуемся ExifTool. Если вы работаете под Linux, то велик шанс, что у вас уже есть этот инструмент (which exiftool). ExifTool доступен для Windows, Mac и Linux.


Файлы проекта


Есть три способа получения файлов проекта.


wget https://github.com/lettier/movie-monad/archive/master.zip
unzip master.zip
mv movie-monad-master movie-monad
cd movie-monad/

Можете скачать ZIP-архив и извлечь их.


git clone git@github.com:lettier/movie-monad.git
cd movie-monad/

Можете сделать Git-клон с помощью SSH.


git clone https://github.com/lettier/movie-monad.git
cd movie-monad/

Можете склонировать git через HTTPS.


haskell-gi


haskell-gi умеет генерировать Haskell-привязки (bindings) к библиотекам, использующим связующее ПО для самодиагностики (introspection middleware) GObject. На момент написания статьи все необходимые привязки доступны на Hackage.


Зависимости


Теперь устанавливаем зависимости проекта.


cd movie-monad/
stack install --dependencies-only

Код


Теперь настраиваем внедрение Movie Monad. Вы можете удалить исходные файлы и создать их заново, или следовать указаниям.


Paths_movie_monad.hs


Paths_movie_monad.hs используется для поиска файла Glade XML GUI во время runtime. Поскольку мы занимаемся разработкой, то будем использовать фиктивный модуль (dummy module) (movie-monad/src/dev/Paths_movie_monad.hs) для поиска файла movie-monad/src/data/gui.glade. После сборки/установки проекта реальный модуль Paths_movie_monad будет сгенерирован автоматически. Он предоставит нам функцию getDataFileName. Она присваивает своим выходным данным префикс в виде абсолютного пути, куда скопированы или установлены data-dir (movie-monad/src/) data-files.


{-# LANGUAGE OverloadedStrings #-}

module Paths_movie_monad where

dataDir :: String
dataDir = "./src/"

getDataFileName :: FilePath -> IO FilePath
getDataFileName a = do
  putStrLn "You are using a fake Paths_movie_monad."
  return (dataDir ++ "/" ++ a)

Фиктивный модуль Paths_movie_monad.


{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-missing-import-lists #-}
{-# OPTIONS_GHC -fno-warn-implicit-prelude #-}
module Paths_movie_monad (
    version,
    getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir,
    getDataFileName, getSysconfDir
  ) where

import qualified Control.Exception as Exception
import Data.Version (Version(..))
import System.Environment (getEnv)
import Prelude

#if defined(VERSION_base)

#if MIN_VERSION_base(4,0,0)
catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a
#else
catchIO :: IO a -> (Exception.Exception -> IO a) -> IO a
#endif

#else
catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a
#endif
catchIO = Exception.catch

version :: Version
version = Version [0,0,0,0] []
bindir, libdir, dynlibdir, datadir, libexecdir, sysconfdir :: FilePath

bindir     = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/bin"
libdir     = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0"
dynlibdir  = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2"
datadir    = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/share/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0"
libexecdir = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/libexec"
sysconfdir = "/home//.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/etc"

getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir, getSysconfDir :: IO FilePath
getBinDir = catchIO (getEnv "movie_monad_bindir") (\_ -> return bindir)
getLibDir = catchIO (getEnv "movie_monad_libdir") (\_ -> return libdir)
getDynLibDir = catchIO (getEnv "movie_monad_dynlibdir") (\_ -> return dynlibdir)
getDataDir = catchIO (getEnv "movie_monad_datadir") (\_ -> return datadir)
getLibexecDir = catchIO (getEnv "movie_monad_libexecdir") (\_ -> return libexecdir)
getSysconfDir = catchIO (getEnv "movie_monad_sysconfdir") (\_ -> return sysconfdir)

getDataFileName :: FilePath -> IO FilePath
getDataFileName name = do
  dir <- getDataDir
  return (dir ++ "/" ++ name)

Автоматически сгенерированный модуль Paths_movie_monad.


Main.hs


Main.hs — это входная точка для Movie Monad. В этом файле мы настраиваем наше окно с разными виджетами, подключаем GStreamer, а когда пользователь выходит, мы сносим окно.


Прагмы (Pragmas)


Нам нужно сказать компилятору (GHC), что нам нужны перегруженные (overloaded) строковые и лексически входящие в область видимости (lexically scoped) переменные типов.


OverloadedStrings позволяет нам использовать строковые литералы ("Literal") там, где требуются String/[Char] или Text. ScopedTypeVariables позволяет нам использовать сигнатуру типа в паттерне параметра лямбда-функции, передаваемую для перехвата при вызове ExifTool.


{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

Импорты


module Main where

import Prelude
import Foreign.C.Types
import System.Process
import System.Exit
import Control.Monad
import Control.Exception
import Text.Read
import Data.IORef
import Data.Maybe
import Data.Int
import Data.Text
import Data.GI.Base
import Data.GI.Base.Signals
import Data.GI.Base.Properties
import GI.GLib
import GI.GObject
import qualified GI.Gtk
import GI.Gst
import GI.GstVideo
import GI.Gdk
import GI.GdkX11
import Paths_movie_monad

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


IsVideoOverlay


GStreamer-видеопривязки (gi-gstvideo) содержат класс типа (интерфейс) IsVideoOverlay. GStreamer-привязки (gi-gst) содержат тип элемента. Чтобы использовать элемент playbin с функцией GI.GstVideo.videoOverlaySetWindowHandle, нам нужно объявить GI.Gst.Element — экземпляр типа (type instance) IsVideoOverlay. А на стороне Cи playbin реализует интерфейс VideoOverlay.


newtype GstElement = GstElement GI.Gst.Element
instance GI.GstVideo.IsVideoOverlay GstElement

Обратите внимание, что мы обёртываем GI.Gst.Element в новый тип (newtype), чтобы избежать появления потерянного (orphaned) экземпляра, поскольку мы объявляем экземпляр вне привязок haskell-gi.


main


Main — наша самая большая функция. В ней мы инициализируем все GUI-виджеты и определяем коллбэк-процедуры на основе определённых событий.


main :: IO ()
main = do

GI-инициализация


  _ <- GI.Gst.init Nothing
  _ <- GI.Gtk.init Nothing

Здесь мы инициализировали GStreamer и GTK+.


Сборка GUI-виджетов


  gladeFile <- getDataFileName "data/gui.glade"
  builder <- GI.Gtk.builderNewFromFile (pack gladeFile)

  window <- builderGetObject GI.Gtk.Window builder "window"
  fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button"
  drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area"
  seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"
  onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"
  volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"
  desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"
  fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button"
  errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog"
  aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"
  aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"

Как уже было сказано, мы получаем абсолютный путь к XML-файлу data/gui.glade, который описывает все наши GUI-виджеты. Дальше создаём из этого файла конструктор и получаем свои виджеты. Если бы мы не использовали Glade, то их пришлось бы создавать вручную, что довольно утомительно.


Playbin


  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer")

Здесь мы создаём GStreamer-конвейер playbin. Он предназначен для решения самых разных нужд и экономит нам время на создании собственного конвейера. Назовём этот элемент MultimediaPlayer.


Встраиванние выходных данных GStreamer


Чтобы GTK+ и GStreamer заработали вместе, нам нужно сказать GStreamer, куда именно нужно выводить видео. Если этого не сделать, то GStreamer создаст собственное окно, поскольку мы используем playbin.


  _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton

-- ...

onDrawingAreaRealize ::
  GI.Gtk.Widget ->
  GI.Gst.Element ->
  GI.Gtk.Button ->
  GI.Gtk.WidgetRealizeCallback
onDrawingAreaRealize drawingArea playbin fullscreenButton = do
  gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea
  x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow

  xid <- GI.GdkX11.x11WindowGetXid x11Window
  let xid' = fromIntegral xid :: CUIntPtr

  GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid'

  GI.Gtk.widgetHide fullscreenButton

Здесь вы видите настройку коллбэка по мере готовности виджета drawingArea. Именно в этом виджете GStreamer должен показывать видео. Мы получаем родительское GDK-окно для виджета области отрисовки. Затем получаем обработчик окна, или XID системы X11 нашего окна GTK+. Строка CUIntPtr преобразует ID из CULong в CUIntPtr, необходимый для videoOverlaySetWindowHandle. Получив правильный тип, мы уведомляем GStreamer, что с помощью обработчика xid' он может отрисовывать в нашем окне выходные данные playbin.


Из-за бага в Glade мы программно скрываем полноэкранный виджет, поскольку если в Glade снять галочку visible box, то виджет всё-равно не будет спрятан.


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


Выбор файла


  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $
    onFileChooserButtonFileSet
      playbin
      fileChooserButton
      volumeButton
      isWindowFullScreenRef
      desiredVideoWidthComboBox
      onOffSwitch
      fullscreenButton
      drawingArea
      window
      errorMessageDialog

-- ...

onFileChooserButtonFileSet ::
  GI.Gst.Element ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.VolumeButton ->
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Switch ->
  GI.Gtk.Button ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  GI.Gtk.MessageDialog ->
  GI.Gtk.FileChooserButtonFileSetCallback
onFileChooserButtonFileSet
  playbin
  fileChooserButton
  volumeButton
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  onOffSwitch
  fullscreenButton
  drawingArea
  window
  errorMessageDialog
  = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull

  filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton

  setPlaybinUriAndVolume playbin filename volumeButton

  isWindowFullScreen <- readIORef isWindowFullScreenRef

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused
      GI.Gtk.windowUnfullscreen window
      GI.Gtk.switchSetActive onOffSwitch False
      GI.Gtk.widgetHide fullscreenButton
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
      _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)
      void $ GI.Gtk.dialogRun errorMessageDialog
    Just (width, height) -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying
      GI.Gtk.switchSetActive onOffSwitch True
      GI.Gtk.widgetShow fullscreenButton
      unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window

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


  • Получаем имя файла из виджета выбора файла.
  • Говорим playbin, какой файл он должен воспроизвести.
  • Делаем уровень громкомсти в виджете таким же, как в playbin.
  • На основе желаемой ширины изображения и размера видео определяем подходящие ширину и высоту окна.
  • Если размеры окна успешно получены:
    • Начинаем воспроизведение файла.
    • Переводим кнопку пауза/воспроизведение в состояние ”on”.
    • Показываем полноэкранный виджет.
    • Если видео не в полноэкранном режиме:
    • Меняем размер окна, чтобы оно совпало с относительным размером видео.
  • Если не удалось получить размеры окна:
    • Ставим playbin на паузу.
    • Переводим переключатель в положение ”off”.
    • Если это возможно, выводим окно из полноэкранного режима.
    • Сбрасываем размер окна.
    • Выводим маленькое диалоговое сообщение об ошибке.

Пауза и воспроизведение


  _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin)

-- ...

onSwitchStateSet ::
  GI.Gst.Element ->
  Bool ->
  IO Bool
onSwitchStateSet playbin switchOn = do
  if switchOn
    then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying
    else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused
  return switchOn

Всё просто. Если переключатель в положении ”on”, то задаём элементу playbin состояние воспроизведения. В противном случае задаём ему состояние паузы.


Настройка громкости


  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin)

-- ...

onScaleButtonValueChanged ::
  GI.Gst.Element ->
  Double ->
  IO ()
onScaleButtonValueChanged playbin volume =
    void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume

При изменении уровня громкости в виджете мы передаём его значение в GStreamer, чтобы тот мог подстроить громкость воспроизведение.


Перемещение по видео


  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale)

-- ...

onRangeValueChanged ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  IO ()
onRangeValueChanged playbin seekScale = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime

  when couldQueryDuration $ do
    percentage' <- GI.Gtk.rangeGetValue seekScale
    let percentage = percentage' / 100.0
    let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64
    void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position

В Movie Monad есть шкала воспроизведения, в которой вы можете перемещать ползунок вперёд/назад, тем самым переходя по видеофреймам.


Шкала от 0 до 100% представляет общую длительность видео-файла. Если переместить ползунок, например, на 50, то мы перейдём к временной отметке, находящийся посередине между началом и окончанием. Можно было бы настроить шкалу от нуля до значения длительности видео, но описанный метод более универсален.


Обратите внимание, что для этого коллбэка мы используем сигнальный ID (seekScaleHandlerId), поскольку он понадобится нам позднее.


Обновление шкалы воспроизведения


  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId)

-- ...

updateSeekScale ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  Data.GI.Base.Signals.SignalHandlerId ->
  IO Bool
updateSeekScale playbin seekScale seekScaleHandlerId = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime
  (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime

  let percentage =
        if couldQueryDuration && couldQueryPosition && duration > 0
          then 100.0 * (fromIntegral position / fromIntegral duration :: Double)
          else 0.0

  GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId
  GI.Gtk.rangeSetValue seekScale percentage
  GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId

  return True

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


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


Изменение размеров видео


  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $
      onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window

-- ...

onComboBoxChanged ::
  GI.Gtk.FileChooserButton ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
onComboBoxChanged
  fileChooserButton
  desiredVideoWidthComboBox
  drawingArea
  window
  = do
  filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton
  let filename = fromMaybe "" filename'

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
    Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window

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


Полноэкранный режим


  _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton
      (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window)

-- ...

onFullscreenButtonRelease ::
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Window ->
  GI.Gdk.EventButton ->
  IO Bool
onFullscreenButtonRelease
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  fileChooserButton
  window
  _
  = do
  isWindowFullScreen <- readIORef isWindowFullScreenRef
  if isWindowFullScreen
    then do
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      GI.Gtk.widgetShow fileChooserButton
      void $ GI.Gtk.windowUnfullscreen window
    else do
      GI.Gtk.widgetHide desiredVideoWidthComboBox
      GI.Gtk.widgetHide fileChooserButton
      void $ GI.Gtk.windowFullscreen window
  return True

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


Обратите внимание, что мы не показываем виджет полноэкранного режима, если у нас нет видео.


  _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef)

-- ...

onWidgetWindowStateEvent ::
  IORef Bool ->
  GI.Gdk.EventWindowState ->
  IO Bool
onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do
  windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState
  let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates
  writeIORef isWindowFullScreenRef isWindowFullScreen
  return True

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


Благодаря использованию в главном потоке выполнения единственного пишущего и кучи сигнальных коллбэков, мы избегаем возможных ловушек общего изменяемого состояния. Если бы нас заботила безопасность потока выполнения, то вместо этого мы могли бы использовать MVar, TVar или atomicModifyIORef.


О программе


  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog)

-- ...

onAboutButtonRelease ::
  GI.Gtk.AboutDialog ->
  GI.Gdk.EventButton ->
  IO Bool
onAboutButtonRelease aboutDialog _ = do
  _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)
  _ <- GI.Gtk.dialogRun aboutDialog
  return True

Последний рассматриваемый виджет — диалоговое окно «О программе». Здесь мы связываем диалоговое окно с кнопкой «О программе», отображающейся в основном окне.


Закрытие окна


  _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin)

-- ...

onWindowDestroy ::
  GI.Gst.Element ->
  IO ()
onWindowDestroy playbin = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull
  _ <- GI.Gst.objectUnref playbin
  GI.Gtk.mainQuit

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


Запуск


  GI.Gtk.widgetShowAll window
  GI.Gtk.main

Наконец, мы показываем или отрисовываем главное окно и запускаем основной цикл GTK+. Он блокируется до вызова mainQuit.


Полный файл Main.hs


Ниже приведён файл movie-monad/src/Main.hs. Не показаны разные вспомогательные функции, относящиеся к main.


{-
  Movie Monad
  (C) 2017 David lettier
  lettier.com
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import Prelude
import Foreign.C.Types
import System.Process
import System.Exit
import Control.Monad
import Control.Exception
import Text.Read
import Data.IORef
import Data.Maybe
import Data.Int
import Data.Text
import Data.GI.Base
import Data.GI.Base.Signals
import Data.GI.Base.Properties
import GI.GLib
import GI.GObject
import qualified GI.Gtk
import GI.Gst
import GI.GstVideo
import GI.Gdk
import GI.GdkX11
import Paths_movie_monad

-- Declare Element a type instance of IsVideoOverlay via a newtype wrapper
-- Our GStreamer element is playbin
-- Playbin implements the GStreamer VideoOverlay interface
newtype GstElement = GstElement GI.Gst.Element
instance GI.GstVideo.IsVideoOverlay GstElement

main :: IO ()
main = do
  _ <- GI.Gst.init Nothing
  _ <- GI.Gtk.init Nothing

  gladeFile <- getDataFileName "data/gui.glade"
  builder <- GI.Gtk.builderNewFromFile (pack gladeFile)

  window <- builderGetObject GI.Gtk.Window builder "window"
  fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button"
  drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area"
  seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale"
  onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch"
  volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button"
  desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box"
  fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button"
  errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog"
  aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"
  aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"

  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer")

  isWindowFullScreenRef <- newIORef False

  _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton

  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $
    onFileChooserButtonFileSet
      playbin
      fileChooserButton
      volumeButton
      isWindowFullScreenRef
      desiredVideoWidthComboBox
      onOffSwitch
      fullscreenButton
      drawingArea
      window
      errorMessageDialog

  _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin)

  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin)

  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale)

  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId)

  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $
      onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window

  _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton
      (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window)

  _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef)

  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog)

  _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin)

  GI.Gtk.widgetShowAll window
  GI.Gtk.main

builderGetObject ::
  (GI.GObject.GObject b, GI.Gtk.IsBuilder a) =>
  (Data.GI.Base.ManagedPtr b -> b) ->
  a ->
  Prelude.String ->
  IO b
builderGetObject objectTypeClass builder objectId =
  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=
    GI.Gtk.unsafeCastTo objectTypeClass

onDrawingAreaRealize ::
  GI.Gtk.Widget ->
  GI.Gst.Element ->
  GI.Gtk.Button ->
  GI.Gtk.WidgetRealizeCallback
onDrawingAreaRealize drawingArea playbin fullscreenButton = do
  gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea
  x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow

  xid <- GI.GdkX11.x11WindowGetXid x11Window
  let xid' = fromIntegral xid :: CUIntPtr

  GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid'

  GI.Gtk.widgetHide fullscreenButton

onFileChooserButtonFileSet ::
  GI.Gst.Element ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.VolumeButton ->
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Switch ->
  GI.Gtk.Button ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  GI.Gtk.MessageDialog ->
  GI.Gtk.FileChooserButtonFileSetCallback
onFileChooserButtonFileSet
  playbin
  fileChooserButton
  volumeButton
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  onOffSwitch
  fullscreenButton
  drawingArea
  window
  errorMessageDialog
  = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull

  filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton

  setPlaybinUriAndVolume playbin filename volumeButton

  isWindowFullScreen <- readIORef isWindowFullScreenRef

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused
      GI.Gtk.windowUnfullscreen window
      GI.Gtk.switchSetActive onOffSwitch False
      GI.Gtk.widgetHide fullscreenButton
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
      _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog)
      void $ GI.Gtk.dialogRun errorMessageDialog
    Just (width, height) -> do
      _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying
      GI.Gtk.switchSetActive onOffSwitch True
      GI.Gtk.widgetShow fullscreenButton
      unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window

onSwitchStateSet ::
  GI.Gst.Element ->
  Bool ->
  IO Bool
onSwitchStateSet playbin switchOn = do
  if switchOn
    then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying
    else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused
  return switchOn

onScaleButtonValueChanged ::
  GI.Gst.Element ->
  Double ->
  IO ()
onScaleButtonValueChanged playbin volume =
    void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume

onRangeValueChanged ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  IO ()
onRangeValueChanged playbin seekScale = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime

  when couldQueryDuration $ do
    percentage' <- GI.Gtk.rangeGetValue seekScale
    let percentage = percentage' / 100.0
    let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64
    void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position

updateSeekScale ::
  GI.Gst.Element ->
  GI.Gtk.Scale ->
  Data.GI.Base.Signals.SignalHandlerId ->
  IO Bool
updateSeekScale playbin seekScale seekScaleHandlerId = do
  (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime
  (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime

  let percentage =
        if couldQueryDuration && couldQueryPosition && duration > 0
          then 100.0 * (fromIntegral position / fromIntegral duration :: Double)
          else 0.0

  GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId
  GI.Gtk.rangeSetValue seekScale percentage
  GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId

  return True

onComboBoxChanged ::
  GI.Gtk.FileChooserButton ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
onComboBoxChanged
  fileChooserButton
  desiredVideoWidthComboBox
  drawingArea
  window
  = do
  filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton
  let filename = fromMaybe "" filename'

  desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox
  maybeWindowSize <- getWindowSize desiredVideoWidth filename

  case maybeWindowSize of
    Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window
    Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window

onFullscreenButtonRelease ::
  IORef Bool ->
  GI.Gtk.ComboBoxText ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Window ->
  GI.Gdk.EventButton ->
  IO Bool
onFullscreenButtonRelease
  isWindowFullScreenRef
  desiredVideoWidthComboBox
  fileChooserButton
  window
  _
  = do
  isWindowFullScreen <- readIORef isWindowFullScreenRef
  if isWindowFullScreen
    then do
      GI.Gtk.widgetShow desiredVideoWidthComboBox
      GI.Gtk.widgetShow fileChooserButton
      void $ GI.Gtk.windowUnfullscreen window
    else do
      GI.Gtk.widgetHide desiredVideoWidthComboBox
      GI.Gtk.widgetHide fileChooserButton
      void $ GI.Gtk.windowFullscreen window
  return True

onWidgetWindowStateEvent ::
  IORef Bool ->
  GI.Gdk.EventWindowState ->
  IO Bool
onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do
  windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState
  let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates
  writeIORef isWindowFullScreenRef isWindowFullScreen
  return True

onAboutButtonRelease ::
  GI.Gtk.AboutDialog ->
  GI.Gdk.EventButton ->
  IO Bool
onAboutButtonRelease aboutDialog _ = do
  _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog)
  _ <- GI.Gtk.dialogRun aboutDialog
  return True

onWindowDestroy ::
  GI.Gst.Element ->
  IO ()
onWindowDestroy playbin = do
  _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull
  _ <- GI.Gst.objectUnref playbin
  GI.Gtk.mainQuit

setPlaybinUriAndVolume ::
  GI.Gst.Element ->
  Prelude.String ->
  GI.Gtk.VolumeButton ->
  IO ()
setPlaybinUriAndVolume playbin filename volumeButton = do
  let uri = "file://" ++ filename
  volume <- GI.Gtk.scaleButtonGetValue volumeButton
  Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume
  Data.GI.Base.Properties.setObjectPropertyString playbin "uri" (Just $ pack uri)

getVideoInfo :: Prelude.String -> Prelude.String -> IO (Maybe Prelude.String)
getVideoInfo flag filename = do
  (code, out, _) <- catch (
      readProcessWithExitCode
        "exiftool"
        [flag, "-s", "-S", filename]
        ""
    ) (\ (_ :: Control.Exception.IOException) -> return (ExitFailure 1, "", ""))
  if code == System.Exit.ExitSuccess
    then return (Just out)
    else return Nothing

isVideo :: Prelude.String -> IO Bool
isVideo filename = do
  maybeOut <- getVideoInfo "-MIMEType" filename
  case maybeOut of
    Nothing -> return False
    Just out -> return ("video" `isInfixOf` pack out)

getWindowSize :: Int -> Prelude.String -> IO (Maybe (Int32, Int32))
getWindowSize desiredVideoWidth filename =
  isVideo filename >>=
  getWidthHeightString >>=
  splitWidthHeightString >>=
  widthHeightToDouble >>=
  ratio >>=
  windowSize
  where
    getWidthHeightString :: Bool -> IO (Maybe Prelude.String)
    getWidthHeightString False = return Nothing
    getWidthHeightString True = getVideoInfo "-ImageSize" filename
    splitWidthHeightString :: Maybe Prelude.String -> IO (Maybe [Text])
    splitWidthHeightString Nothing = return Nothing
    splitWidthHeightString (Just string) = return (Just (Data.Text.splitOn "x" (pack string)))
    widthHeightToDouble :: Maybe [Text] -> IO (Maybe Double, Maybe Double)
    widthHeightToDouble (Just (x:y:_)) = return (readMaybe (unpack x) :: Maybe Double, readMaybe (unpack y) :: Maybe Double)
    widthHeightToDouble _ = return (Nothing, Nothing)
    ratio :: (Maybe Double, Maybe Double) -> IO (Maybe Double)
    ratio (Just width, Just height) =
      if width <= 0.0 then return Nothing else return (Just (height / width))
    ratio _ = return Nothing
    windowSize :: Maybe Double -> IO (Maybe (Int32, Int32))
    windowSize Nothing = return Nothing
    windowSize (Just ratio') =
      return (Just (fromIntegral desiredVideoWidth :: Int32, round ((fromIntegral desiredVideoWidth :: Double) *  ratio') :: Int32))

getDesiredVideoWidth :: GI.Gtk.ComboBoxText -> IO Int
getDesiredVideoWidth = fmap (\ x -> read (Data.Text.unpack x) :: Int) . GI.Gtk.comboBoxTextGetActiveText

setWindowSize ::
  Int32 ->
  Int32 ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
setWindowSize width height fileChooserButton drawingArea window = do
  GI.Gtk.setWidgetWidthRequest fileChooserButton width

  GI.Gtk.setWidgetWidthRequest drawingArea width
  GI.Gtk.setWidgetHeightRequest drawingArea height

  GI.Gtk.setWidgetWidthRequest window width
  GI.Gtk.setWidgetHeightRequest window height
  GI.Gtk.windowResize window width (if height <= 0 then 1 else height)

resetWindowSize ::
  (Integral a) =>
  a ->
  GI.Gtk.FileChooserButton ->
  GI.Gtk.Widget ->
  GI.Gtk.Window ->
  IO ()
resetWindowSize width' fileChooserButton drawingArea window = do
  let width = fromIntegral width' :: Int32
  GI.Gtk.widgetQueueDraw drawingArea
  setWindowSize width 0 fileChooserButton drawingArea window

Собираем Movie Monad


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


cd movie-monad/
stack clean
stack install
stack exec -- movie-monad
# Or just `movie-monad` if `stack path | grep local-bin-path` is in your `echo $PATH`

Если всё в порядке, то Movie Monad должен запуститься.


Заключение


Пересмотрев проект Movie Monad, мы заново сделали приложение с помощью программных библиотек GTK+ и GStreamer. Благодаря им приложение осталось таким же портируемым, как и Electron-версия. Movie Monad теперь может обрабатывать большие видеофайлы и имеет все стандартные элементы управления.


Другим преимуществом использования GTK+ стало уменьшение потребления памяти. Если сравнивать резидентный размер в памяти при старте, то версия GTK+ занимает ~50 Мб, а версия Electron — ~300 Мб (500%-ное увеличение).


Наконец, вариант с GTK+ имеет меньше ограничений и требует меньше программирования. Для обеспечения такой же функциональности, вариант с Electron требует использования громоздкой клиент-серверной архитектуры. Но благодаря прекрасным сборкам haskell-gi мы смогли избежать решения на базе веба.


Если хотите посмотреть другие приложения, построенные с помощью GTK+ и Haskell, то обратите внимание на Gifcurry. Оно умеет брать видеофайлы и на их основе создавать гифки с наложенным текстом.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/338176/


Метки:  

Как увеличить показатели сервиса в 7 раз за три месяца с помощью HADI-циклов и приоритизации гипотез

Вторник, 19 Сентября 2017 г. 14:44 + в цитатник
friifond сегодня в 14:44 Управление

Как увеличить показатели сервиса в 7 раз за три месяца с помощью HADI-циклов и приоритизации гипотез

    image

    Меня зовут Илья Китанин, я более 7 лет руковожу разработкой в различных компаниях, сейчас — в Преакселераторе ФРИИ. В этой статье я расскажу, как с помощью HADI-циклов и Теории ограничений Голдратта (TOC) мы смогли вырастить ключевые показатели сервиса Cofoundit в 7 раз за 3 месяца и продолжаем активно расти сейчас. В этом материале — кейсы применения методологии ФРИИ, грабли, которые мы прошли, и необходимый минимум теории.

    Cofoundit — это внутренний стартап ФРИИ для подбора сотрудников и сооснователей в команду, где стартапы и кандидаты могут быстро найти друг друга и вместе побежать к международной компании.

    Я с моей коллегой Юлей Копыловой начали заниматься этим сервисом 15 марта этого года. Сервис существовал уже полгода, но значимых результатов не показывал. И перед нами стояла задача до 1 июля доказать состоятельность сервиса. Для этого необходимо было кратно увеличить количество закрытых вакансий в месяц, а также ускорить этот процесс.

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

    1. Сроки: как я писал выше, надо было успеть показать результат к 1 июля.
    2. Время: я и Юля продолжили выполнять наши основные задачи, а сервисом занимались только в свободное время. Я руководил разработкой, а Юля выполняла обязанности аккаунт-менеджера.
    3. Бюджет на разработку был знатно ограничен.



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


    Механика была следующая:

    Стартап заполнял анкету Преакселератора и в блоке «Команда» создавал вакансию.

    image
    Примеры вопросов анкеты для стартапа

    Потом шла автоматическая проверка на минимальные требования (что у стартапа есть прототип, и спрос на продукт проверен) — и заявка отправлялась на ручную модерацию Юле. Только когда модерация проходила успешно, вакансия создавалась и становилась доступной для подбора кандидатов.



    Со стороны кандидата путь был чуть проще: он заполнял небольшую анкету, где отвечал на несколько вопросов и прикреплял своё резюме. После этого он видел не более 5 вакансий, каждой из которых он мог отказать (нажать «не интересно») или заинтересоваться (лайкнуть), после этого выбрать предполагаемое время встречи. Стартапу при этом уходило письмо, что им заинтересовался кандидат.

    Стартап назначал встречу и уже после встречи решал, продолжается ли их с кандидатом общение, и в каком формате — начали работать вместе (произошел match, вакансия закрылась), кандидат получил тестовое задание, или стартап назначил вторую встречу.



    При такой механике ключевые показатели сервиса были недостаточно хороши:

    • 11 закрытых вакансий в квартал;
    • Среднее время до первой заинтересованности кандидатом (лайка) — 48 дней.

    Решение: инструмент HADI-циклов и методология ТОС для быстрого тестирования и приоритизации гипотез


    Посмотрев на стоящие перед нами цели и ограничения, мы решили использовать инструмент HADI-циклов, по которому работают стартапы Акселератора ФРИИ.

    HADI-циклы — это очень простая методика проверки гипотез, которая состоит из 4 этапов:

    1. Постановка Гипотезы,
    2. Действия по её реализации,
    3. Сбор данных о качестве работы гипотезы,
    4. Выводы о результате работы гипотезы, могут включать некоторые следующие гипотезы.



    Самое главное тут — тестировать как можно больше различных гипотез как можно быстрее. Если даже гипотеза не сработала — ничего страшного, важно быстро от неё отказаться и начать тестировать новые.

    Сложность здесь — определить, какие гипотезы стоит тестировать первыми. Для приоритизации гипотез мы решили использовать часть Теории Ограничений Голдратта (ТОС), связанную с определением узких мест. Для понимания этой методологии в Акселераторе стартапам рекомендуют к прочтению книгу Голдратта «Цель».

    ТОС — также достаточно простая методология, для повышения пропускной способности системы (=повышения результативности какого-либо процесса) по ней необходимо:

    1. Найти «узкое место» — ограничение системы, на котором «застревает» процесс производства (или какой-то иной), т.е. накапливаются задачи,
    2. Решить, как максимально использовать «мощность» этого ограничения,
    3. Подчинить систему этому решению (=сделать так, чтобы «мощность» ограничения не простаивала),
    4. Расширить ограничение,
    5. Если ограничение устранено, вернуться к шагу 1. Если необходимо еще расширить — к шагу 4.



    Мы использовали именно эти 2 методологии, так как HADI-циклы идеально подходят для быстрого роста показателей, а TOC помогает определиться с тем, какую именно гипотезу тестировать в первую очередь.

    Для начала мы решили определить ограничения или «узкие места» системы, для этого подняли и проанализировали всю статистику, которая на тот момент собиралась (оказалось, что и тут у нас были ограничения).




    И сразу увидели 2 «узких места», где срезается конверсия.

    1. Стартапы хорошо отвечают на лайки кандидатов только первые 5 дней после публикации вакансии, а дальше отвечают в 2,5 раза хуже.
    2. Кандидаты хорошо открывают письма с переподбором (=похожими вакансиями), но очень редко переходят в профили стартапов после просмотра этих писем.

    Гипотеза 1. По 1-ой проблеме мы решили выкидывать из подбора те стартапы, которые не отвечают кандидатам более двух недель. Нам сразу хотелось сделать красивое решение, где мы сможем часть стартапов выделить в отдельный сегмент и автоматически отправлять им письма, мотивирующие их вернуться в систему. Но наша цель была БЫСТРО протестировать гипотезу — и в лучших традициях MVP мы просто убрали часть проектов из подборки. Реализацию дополнительного функционала отложили до момента, когда уже проверим, сработала гипотеза или нет — увеличилась ли конверсия в ответы кандидатам после наших действий.

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

    Было




    Стало




    Таким образом получили первые две гипотезы на проверку.

    Гипотеза 3. Также добавили 3-ю гипотезу — убрать механизм премодерации, это должно было высвободить время Юли, которая занималась ручным просмотром проектов, и ускорить попадание вакансий в подбор. В результате стартапам не нужно будет ожидать проверки, а время от момента создания вакансии до начала подбора сократится от 2-3 дней до 4 часов.

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

    Гипотеза 4. Мы решили сделать «безопасную задачу», которая никак не может повлиять на метрики предыдущих гипотез, — расширить статистику с расчетом на то, что она позволит нам в будущем быстрее и лучше проверять гипотезы. Мы добавили в аналитику информацию о том, смотрел ли кандидат вакансию и когда именно. Тем самым мы смогли разбить конверсию из подобранных вакансий в «лайки» на две:

    1) из подобранных — в просмотренные,
    2) из просмотренных — в «лайки».

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

    Гипотеза 5. Добавить мотивацию стартапам выставлять статус, что они работают с кандидатом. Мы решили добавить в письмо по итогам собеседования «бонус»: стартапы, которые выставили в профиле статус, что они работают с кандидатом, получат бесплатную 3-х недельную программу по развитию бизнеса. Это должно было увеличить количество закрытых вакансий в системе — до этого приходилось дополнительно обзванивать стартапы и выставлять статусы вручную.

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

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

    Приоритизация гипотез с помощью «плеча» метрик


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

    Мы вспомнили об основном инструменте Акселератора для приоритизации гипотез — это расчет unit-экономики и «плеча» метрик. Чтобы определить, на какой показатель эффективнее всего влиять при тестировании гипотез, на основе текущих данных прогнозируется изменение показателей системы при влиянии на каждую метрику.

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

    Мы посчитали, что главным для нас параметром является количество новых кандидатов, необходимых для одного закрытия вакансии, посчитали показатели сервиса на тот момент, построили формулы и получили следующую таблицу:



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

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

    1. Описываем гипотезу, которую хотим проверить.
    2. Действие — как именно мы хотим её проверить.
    3. Метрика, на которую хотим повлиять.
    4. Ожидаемый эффект по росту данной метрики.
    5. Планируемая дата релиза (чтобы не тестировать гипотезы, влияющие на одну и ту же метрику, параллельно).
    6. Фактическая дата реализации — старта тестирования гипотезы.
    7. Дата, когда посчитаем эффект (устанавливается исходя из необходимого количества данных и времени на их сбор, также нужна, чтобы не тестировать гипотезы, влияющие на одну и ту же метрику, параллельно).
    8. Полученный эффект (как изменилась метрика в результате).
    9. Следующие шаги — здесь пишем о том, какие выводы сделали по итогу проверки гипотезы.



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


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

    4 из них были настоящим прорывом, только 1 ухудшила показатели системы, а остальные дали относительно небольшой рост. Это позволило улучшить показатели до:



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

    Что мы делали не так — выводы


    В процессе работы над проектом мы совершили 3 ошибки, от которых хотим предостеречь другие стартапы:

    1. Не хотели отказываться от неподтвержденных гипотез


    У нас была гипотеза, которую мы очень любили.

    Раньше кандидат при заполнении анкеты прикреплял своё резюме — и только потом мог посмотреть стартапы с открытыми вакансиями.

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

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

    Всё это был самообман — в сентябре мы наконец вернули резюме обратно. На это решение мы потратили 3 месяца. 3 месяца, Карл! То есть наш сервис хуже работал целый квартал.

    2. Не декомпозировали гипотезы


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

    Мы на самом деле тестировали несколько гипотез в рамках одной. Каждое отдельное письмо — это гипотеза.

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

    Если бы мы тестировали отдельно каждое письмо, то смогли бы получить первый результат уже через 2 дня после постановки гипотезы. И точно узнать, какое письмо сработало, какое нет, и почему. Поэтому несмотря на то, что гипотеза сработала и показала неплохой прирост показателей, считать её однозначно успешной нельзя. Если бы мы чуть аккуратнее подошли к процессу — вероятно, получили бы больший рост и быстрее.

    3. Тестировали связанные гипотезы, которые влияют на один и тот же показатель


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

    Мы аккуратно разобрали статистику и выяснили, что гипотеза «Убрать неактивные стартапы из подбора» повлияла и на конверсию в закрытие вакансии (match) — остались более мотивированные команды, и они не только чаще отвечали лайкнувшим их кандидатам, но и выставляли статусы после встречи.

    Резюмируя:

    1. HADI-циклы — эффективный инструмент для развития бизнеса!
    2. Тестируйте гипотезы как можно быстрее, для приоритизации можно использовать инструмент измерения «плеча» метрики.
    3. Не допускайте наших ошибок:
      a. Будьте решительны с неподтвержденными гипотезами,
      b. Декомпозируйте гипотезы,
      c. Не тестируйте связанные гипотезы, влияющие на одну метрику.
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/338240/


    Как увеличить показатели сервиса в 7 раз за три месяца с помощью HADI-циклов и приоритизации гипотез

    Вторник, 19 Сентября 2017 г. 14:44 + в цитатник
    friifond сегодня в 14:44 Управление

    Как увеличить показатели сервиса в 7 раз за три месяца с помощью HADI-циклов и приоритизации гипотез

      image

      Меня зовут Илья Китанин, я более 7 лет руковожу разработкой в различных компаниях, сейчас — в Преакселераторе ФРИИ. В этой статье я расскажу, как с помощью HADI-циклов и Теории ограничений Голдратта (TOC) мы смогли вырастить ключевые показатели сервиса Cofoundit в 7 раз за 3 месяца и продолжаем активно расти сейчас. В этом материале — кейсы применения методологии ФРИИ, грабли, которые мы прошли, и необходимый минимум теории.

      Cofoundit — это внутренний стартап ФРИИ для подбора сотрудников и сооснователей в команду, где стартапы и кандидаты могут быстро найти друг друга и вместе побежать к международной компании.

      Я с моей коллегой Юлей Копыловой начали заниматься этим сервисом 15 марта этого года. Сервис существовал уже полгода, но значимых результатов не показывал. И перед нами стояла задача до 1 июля доказать состоятельность сервиса. Для этого необходимо было кратно увеличить количество закрытых вакансий в месяц, а также ускорить этот процесс.

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

      1. Сроки: как я писал выше, надо было успеть показать результат к 1 июля.
      2. Время: я и Юля продолжили выполнять наши основные задачи, а сервисом занимались только в свободное время. Я руководил разработкой, а Юля выполняла обязанности аккаунт-менеджера.
      3. Бюджет на разработку был знатно ограничен.



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


      Механика была следующая:

      Стартап заполнял анкету Преакселератора и в блоке «Команда» создавал вакансию.

      image
      Примеры вопросов анкеты для стартапа

      Потом шла автоматическая проверка на минимальные требования (что у стартапа есть прототип, и спрос на продукт проверен) — и заявка отправлялась на ручную модерацию Юле. Только когда модерация проходила успешно, вакансия создавалась и становилась доступной для подбора кандидатов.



      Со стороны кандидата путь был чуть проще: он заполнял небольшую анкету, где отвечал на несколько вопросов и прикреплял своё резюме. После этого он видел не более 5 вакансий, каждой из которых он мог отказать (нажать «не интересно») или заинтересоваться (лайкнуть), после этого выбрать предполагаемое время встречи. Стартапу при этом уходило письмо, что им заинтересовался кандидат.

      Стартап назначал встречу и уже после встречи решал, продолжается ли их с кандидатом общение, и в каком формате — начали работать вместе (произошел match, вакансия закрылась), кандидат получил тестовое задание, или стартап назначил вторую встречу.



      При такой механике ключевые показатели сервиса были недостаточно хороши:

      • 11 закрытых вакансий в квартал;
      • Среднее время до первой заинтересованности кандидатом (лайка) — 48 дней.

      Решение: инструмент HADI-циклов и методология ТОС для быстрого тестирования и приоритизации гипотез


      Посмотрев на стоящие перед нами цели и ограничения, мы решили использовать инструмент HADI-циклов, по которому работают стартапы Акселератора ФРИИ.

      HADI-циклы — это очень простая методика проверки гипотез, которая состоит из 4 этапов:

      1. Постановка Гипотезы,
      2. Действия по её реализации,
      3. Сбор данных о качестве работы гипотезы,
      4. Выводы о результате работы гипотезы, могут включать некоторые следующие гипотезы.



      Самое главное тут — тестировать как можно больше различных гипотез как можно быстрее. Если даже гипотеза не сработала — ничего страшного, важно быстро от неё отказаться и начать тестировать новые.

      Сложность здесь — определить, какие гипотезы стоит тестировать первыми. Для приоритизации гипотез мы решили использовать часть Теории Ограничений Голдратта (ТОС), связанную с определением узких мест. Для понимания этой методологии в Акселераторе стартапам рекомендуют к прочтению книгу Голдратта «Цель».

      ТОС — также достаточно простая методология, для повышения пропускной способности системы (=повышения результативности какого-либо процесса) по ней необходимо:

      1. Найти «узкое место» — ограничение системы, на котором «застревает» процесс производства (или какой-то иной), т.е. накапливаются задачи,
      2. Решить, как максимально использовать «мощность» этого ограничения,
      3. Подчинить систему этому решению (=сделать так, чтобы «мощность» ограничения не простаивала),
      4. Расширить ограничение,
      5. Если ограничение устранено, вернуться к шагу 1. Если необходимо еще расширить — к шагу 4.



      Мы использовали именно эти 2 методологии, так как HADI-циклы идеально подходят для быстрого роста показателей, а TOC помогает определиться с тем, какую именно гипотезу тестировать в первую очередь.

      Для начала мы решили определить ограничения или «узкие места» системы, для этого подняли и проанализировали всю статистику, которая на тот момент собиралась (оказалось, что и тут у нас были ограничения).




      И сразу увидели 2 «узких места», где срезается конверсия.

      1. Стартапы хорошо отвечают на лайки кандидатов только первые 5 дней после публикации вакансии, а дальше отвечают в 2,5 раза хуже.
      2. Кандидаты хорошо открывают письма с переподбором (=похожими вакансиями), но очень редко переходят в профили стартапов после просмотра этих писем.

      Гипотеза 1. По 1-ой проблеме мы решили выкидывать из подбора те стартапы, которые не отвечают кандидатам более двух недель. Нам сразу хотелось сделать красивое решение, где мы сможем часть стартапов выделить в отдельный сегмент и автоматически отправлять им письма, мотивирующие их вернуться в систему. Но наша цель была БЫСТРО протестировать гипотезу — и в лучших традициях MVP мы просто убрали часть проектов из подборки. Реализацию дополнительного функционала отложили до момента, когда уже проверим, сработала гипотеза или нет — увеличилась ли конверсия в ответы кандидатам после наших действий.

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

      Было




      Стало




      Таким образом получили первые две гипотезы на проверку.

      Гипотеза 3. Также добавили 3-ю гипотезу — убрать механизм премодерации, это должно было высвободить время Юли, которая занималась ручным просмотром проектов, и ускорить попадание вакансий в подбор. В результате стартапам не нужно будет ожидать проверки, а время от момента создания вакансии до начала подбора сократится от 2-3 дней до 4 часов.

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

      Гипотеза 4. Мы решили сделать «безопасную задачу», которая никак не может повлиять на метрики предыдущих гипотез, — расширить статистику с расчетом на то, что она позволит нам в будущем быстрее и лучше проверять гипотезы. Мы добавили в аналитику информацию о том, смотрел ли кандидат вакансию и когда именно. Тем самым мы смогли разбить конверсию из подобранных вакансий в «лайки» на две:

      1) из подобранных — в просмотренные,
      2) из просмотренных — в «лайки».

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

      Гипотеза 5. Добавить мотивацию стартапам выставлять статус, что они работают с кандидатом. Мы решили добавить в письмо по итогам собеседования «бонус»: стартапы, которые выставили в профиле статус, что они работают с кандидатом, получат бесплатную 3-х недельную программу по развитию бизнеса. Это должно было увеличить количество закрытых вакансий в системе — до этого приходилось дополнительно обзванивать стартапы и выставлять статусы вручную.

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

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

      Приоритизация гипотез с помощью «плеча» метрик


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

      Мы вспомнили об основном инструменте Акселератора для приоритизации гипотез — это расчет unit-экономики и «плеча» метрик. Чтобы определить, на какой показатель эффективнее всего влиять при тестировании гипотез, на основе текущих данных прогнозируется изменение показателей системы при влиянии на каждую метрику.

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

      Мы посчитали, что главным для нас параметром является количество новых кандидатов, необходимых для одного закрытия вакансии, посчитали показатели сервиса на тот момент, построили формулы и получили следующую таблицу:



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

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

      1. Описываем гипотезу, которую хотим проверить.
      2. Действие — как именно мы хотим её проверить.
      3. Метрика, на которую хотим повлиять.
      4. Ожидаемый эффект по росту данной метрики.
      5. Планируемая дата релиза (чтобы не тестировать гипотезы, влияющие на одну и ту же метрику, параллельно).
      6. Фактическая дата реализации — старта тестирования гипотезы.
      7. Дата, когда посчитаем эффект (устанавливается исходя из необходимого количества данных и времени на их сбор, также нужна, чтобы не тестировать гипотезы, влияющие на одну и ту же метрику, параллельно).
      8. Полученный эффект (как изменилась метрика в результате).
      9. Следующие шаги — здесь пишем о том, какие выводы сделали по итогу проверки гипотезы.



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


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

      4 из них были настоящим прорывом, только 1 ухудшила показатели системы, а остальные дали относительно небольшой рост. Это позволило улучшить показатели до:



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

      Что мы делали не так — выводы


      В процессе работы над проектом мы совершили 3 ошибки, от которых хотим предостеречь другие стартапы:

      1. Не хотели отказываться от неподтвержденных гипотез


      У нас была гипотеза, которую мы очень любили.

      Раньше кандидат при заполнении анкеты прикреплял своё резюме — и только потом мог посмотреть стартапы с открытыми вакансиями.

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

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

      Всё это был самообман — в сентябре мы наконец вернули резюме обратно. На это решение мы потратили 3 месяца. 3 месяца, Карл! То есть наш сервис хуже работал целый квартал.

      2. Не декомпозировали гипотезы


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

      Мы на самом деле тестировали несколько гипотез в рамках одной. Каждое отдельное письмо — это гипотеза.

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

      Если бы мы тестировали отдельно каждое письмо, то смогли бы получить первый результат уже через 2 дня после постановки гипотезы. И точно узнать, какое письмо сработало, какое нет, и почему. Поэтому несмотря на то, что гипотеза сработала и показала неплохой прирост показателей, считать её однозначно успешной нельзя. Если бы мы чуть аккуратнее подошли к процессу — вероятно, получили бы больший рост и быстрее.

      3. Тестировали связанные гипотезы, которые влияют на один и тот же показатель


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

      Мы аккуратно разобрали статистику и выяснили, что гипотеза «Убрать неактивные стартапы из подбора» повлияла и на конверсию в закрытие вакансии (match) — остались более мотивированные команды, и они не только чаще отвечали лайкнувшим их кандидатам, но и выставляли статусы после встречи.

      Резюмируя:

      1. HADI-циклы — эффективный инструмент для развития бизнеса!
      2. Тестируйте гипотезы как можно быстрее, для приоритизации можно использовать инструмент измерения «плеча» метрики.
      3. Не допускайте наших ошибок:
        a. Будьте решительны с неподтвержденными гипотезами,
        b. Декомпозируйте гипотезы,
        c. Не тестируйте связанные гипотезы, влияющие на одну метрику.
      Original source: habrahabr.ru (comments, light).

      https://habrahabr.ru/post/338240/


      Parrot Security OS — альтернатива Kali Linux

      Вторник, 19 Сентября 2017 г. 13:37 + в цитатник
      LukaSafonov сегодня в 13:37 Разработка

      Parrot Security OS — альтернатива Kali Linux

        image
         
        Parrot Security OS — набирающий популярность security-дистрибутив, основанный на Debian-linux. Простой в освоении, подходит и для новичков и для профессионалов. В этой статье я расскажу об этом дистрибутиве и о развитии проекта от одного из контрибьюторов M. Emrah "UNS"UR с которым мне удалось пообщаться.

        Parrot Secuirty OS


        Набирающий популярность security-дистрибутив, основанный на Debian-linux. Довольно простой в освоении, подходит и для новичков и для профессионалов. Этот дистрибутив нацелен как на проведение тестирования на проникновение, так и на анонимную работу в сети Интернет.

        Довольно легкий и эффективный инструмент, многие security специалисты нашли в нем замену все более «прожорливому» Kali, тем более что Parrot использует репозитории Kali для обновления. Использует оболочку MATE и дисплей-менеджер LightDM.



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

        Из отличительных особенностей можно назвать больший, нежелив Kali уклона в анонимность: интеграция I2P (invisible internet project) и предустановленные сервисы TOR.

        Существует несколько версий (включающих 32 и 64 битные платформы) Parrot Security OS (3.8 — JollyRoger):

        • Parrot Security 3.8 Full Edition — полная версия.
        • Parrot Lite 3.8 Home Edition — без утилит, как основа для вашей сборки.
        • Parrot AIR 3.8 — для тестирования беспроводных сетей.
        • Parrot Cloud Edition — версия для деплоя в облачных сервсиах.
        • Embedded Devices and IoT — Raspberry Pi, Orange Pi, Pine64.

        Развитие проекта


        Данный дистрибутив выглядит довольно перспективным, поэтому мне захотелось узнать о нем немного больше и я пообщался с одним из контрибьюторов и активным участником проекта — M. Emrah "UNS"UR.

        M. Emrah "UNS"UR (meu @ Parrot Project) исследователь по безопасности в частной компании. Является контрибьютором и модератором форума проекта Parrot.

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


        Зачем создавать свой дистрибутив? Есть несколько причин сделать что-то подобное. Основная причина заключается в том, чтобы по рукой всегда было большинство используемых программных продуктов в самой удобной упаковке. В случае Parrot Security OS разработчиков по той или иной причини не устраивали текущие дистрибутивы, в виде простой и готовой к использованию сборки, с постоянными обновлениями, современными технологиями и актуальным списком ПО, которое реально используется при проведении пентеста. (Самое время вспонимть BlackArch c > 1500 утилит). Также этот дистрибутив должен подходить для повседневной работы.

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

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

        Итог


        Если вы занимаетесь практической информационной безопасностью — рекомендую ознакомиться с этим дистрибутивом, возможно он поможет сделать вашу работу по тестированию на проникновение более продуктивной и комфортной. Если у вас есть время и желание участвовать в развитии проекта Parrot Security OS — пишите в официальное сообщество, участники будут рады любой помощи проекту.
        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/337712/


        Метки:  

        Parrot Security OS — альтернатива Kali Linux

        Вторник, 19 Сентября 2017 г. 13:37 + в цитатник
        LukaSafonov сегодня в 13:37 Разработка

        Parrot Security OS — альтернатива Kali Linux

          image
           
          Parrot Security OS — набирающий популярность security-дистрибутив, основанный на Debian-linux. Простой в освоении, подходит и для новичков и для профессионалов. В этой статье я расскажу об этом дистрибутиве и о развитии проекта от одного из контрибьюторов M. Emrah "UNS"UR с которым мне удалось пообщаться.

          Parrot Secuirty OS


          Набирающий популярность security-дистрибутив, основанный на Debian-linux. Довольно простой в освоении, подходит и для новичков и для профессионалов. Этот дистрибутив нацелен как на проведение тестирования на проникновение, так и на анонимную работу в сети Интернет.

          Довольно легкий и эффективный инструмент, многие security специалисты нашли в нем замену все более «прожорливому» Kali, тем более что Parrot использует репозитории Kali для обновления. Использует оболочку MATE и дисплей-менеджер LightDM.



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

          Из отличительных особенностей можно назвать больший, нежелив Kali уклона в анонимность: интеграция I2P (invisible internet project) и предустановленные сервисы TOR.

          Существует несколько версий (включающих 32 и 64 битные платформы) Parrot Security OS (3.8 — JollyRoger):

          • Parrot Security 3.8 Full Edition — полная версия.
          • Parrot Lite 3.8 Home Edition — без утилит, как основа для вашей сборки.
          • Parrot AIR 3.8 — для тестирования беспроводных сетей.
          • Parrot Cloud Edition — версия для деплоя в облачных сервсиах.
          • Embedded Devices and IoT — Raspberry Pi, Orange Pi, Pine64.

          Развитие проекта


          Данный дистрибутив выглядит довольно перспективным, поэтому мне захотелось узнать о нем немного больше и я пообщался с одним из контрибьюторов и активным участником проекта — M. Emrah "UNS"UR.

          M. Emrah "UNS"UR (meu @ Parrot Project) исследователь по безопасности в частной компании. Является контрибьютором и модератором форума проекта Parrot.

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


          Зачем создавать свой дистрибутив? Есть несколько причин сделать что-то подобное. Основная причина заключается в том, чтобы по рукой всегда было большинство используемых программных продуктов в самой удобной упаковке. В случае Parrot Security OS разработчиков по той или иной причини не устраивали текущие дистрибутивы, в виде простой и готовой к использованию сборки, с постоянными обновлениями, современными технологиями и актуальным списком ПО, которое реально используется при проведении пентеста. (Самое время вспонимть BlackArch c > 1500 утилит). Также этот дистрибутив должен подходить для повседневной работы.

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

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

          Итог


          Если вы занимаетесь практической информационной безопасностью — рекомендую ознакомиться с этим дистрибутивом, возможно он поможет сделать вашу работу по тестированию на проникновение более продуктивной и комфортной. Если у вас есть время и желание участвовать в развитии проекта Parrot Security OS — пишите в официальное сообщество, участники будут рады любой помощи проекту.
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/337712/


          Метки:  

          [Из песочницы] Из хирурга в разработчики: как в 40 лет сменить профессию?

          Вторник, 19 Сентября 2017 г. 13:00 + в цитатник
          alxpotapov сегодня в 13:00 Разное

          Из хирурга в разработчики: как в 40 лет сменить профессию?

          image


          Привет! Меня зовут Алексей, я тимлид в крупной IT-компании. Сейчас мне 43, только в 40 лет я стал разработчиком, а до этого 15 лет был практикующим врачом-хирургом. Делюсь с вами, как в середине жизни я поменял профессию, о страхах, рисках и планах с этим связанных.


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


          В 1998 году я закончил Самарский государственный медицинский университет, в 2000 – ординатуру по специальности «Хирургия» и одновременно защитил кандидатскую диссертацию. Переехал в г. Усинск (Республика Коми), где 8 лет проработал хирургом, потом был г. Ханты-Мансийск (Югра), где я продолжил трудиться по специальности.


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


          За время работы повышал профессиональный уровень с помощью дополнительных специализаций, в том числе в больницах и госпиталях Франции, Чехии и США.


          В целом моя карьера складывалась удачно, были профессиональные перспективы, но были и сложности. В России врач – это призвание. Не в том смысле, чтобы любить свое дело и посвящать ему себя полностью. Этого хватало. Несмотря на то, что ты ежедневно отвечаешь за жизнь и здоровье людей, тебе и твоей семье при этом приходится практически выживать. На севере (Республика Коми, ХМАО) еще можно получать хорошую зарплату врача, но в средней полосе ситуация крайне сложная. Туда мне предстояло вернуться: на малой родине (г. Пенза) остались родители, которым нужно помогать и поддерживать.


          А в этом регионе с зарплатами совсем туго. Чтобы не оказаться без денег после очередного переезда, нужно было позаботиться о будущем заранее. Помогло хобби. В свободное время я выручал знакомых – настраивал программное обеспечение. Даже одно время подрабатывал программистом в пожарной части в Усинске. Начальник пожарной части был у меня пациентом, а потом предложил дополнительный заработок. В основном делал внешние отчеты и дорабатывал конфигурацию 1С Предприятия под их организацию. В общем пришлось освоить нехитрый язык 1С. Помимо этого написал и поддерживал систему учета в пожарной части на FireBird & Delphi.


          Я был самоучкой, специальных знаний не имел, мне просто нравилось программирование само по себе. Решил, что дополнительная профессия не помешает, а станет моей подстраховкой. Потому в 2011 году поступил на заочное отделение в Томский государственный университет систем управления и радиоэлектроники по специальности «Программное обеспечение вычислительных систем и автоматизированных комплексов». Закончил его экстерном в 2014 году.


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


          В том же 2014 году я с семьей переехал в Поволжье. Во всех городах средней полосы ситуация с зарплатами плачевная. На 20 тысяч докторской з/п, что мне предложили в Пензе, невозможно обеспечить достойную жизни для себя и семьи. Предстояло решить, что делать дальше. С одной стороны, привычная жизнь, профессиональные успехи, но критично низкая зарплата и грустная перспектива – в финансовом отношении ждать изменений не приходилось. С другой стороны, стартовая позиция в новой профессии и не факт, что «выстрелит» и я в возрасте «далеко за 30» чего-то достигну. Однако надежда поднять уровень жизни семьи и хорошо зарабатывать в будущем перевесила страхи.


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


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


          В нынешней компании я оказался случайно. Жена увидела открытую вакансию, мы обсудили и решили: а почему бы не попробовать? Тогда я сомневался: крупная компания, серьезный продукт, у меня мало опыта, совсем не был уверен, что из этого что-то выйдет. Но на вакансию откликнулся, решил, что попытаться стоит. Выполнил тестовое, меня пригласили на собеседование в головной офис. Волновался, но все прошло гладко и меня взяли на испытательный срок на 2 месяца.


          Плюсы, которые, я сразу оценил: хорошая зарплата, крутая команда, возможность работать удаленно и перспективы роста. Начал с позиции рядового разработчика, за 1,5 года дорос до тимлида. Сейчас практически все мое рабочее время занимает SIEM: подготовка кандидат-релизов, написание коннекторов, разработка дальнейшей функциональности. Иногда исправляю старые «баги», доставшиеся мне по наследству от коллег по работе, участвую в разработке общего для всех продуктов SDK взаимодействия между компонентами (REST). Задачи интересные, команда сильная.


          Сейчас я пишу на Delphi, Go, немного поработал с C#. В качестве БД на хорошем уровне изучил MSSQL и MongoDB. Теперь я могу жить и работать в том регионе, где мне удобно, заниматься делом, которое мне по душе, и при этом не быть ущемленным финансово.


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


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


          1. Оценить насколько нынешняя профессия схожа с новой. Конечно, проще уходить в смежную область или просто в коммерческую сферу, к примеру, были школьным учителем – пошли в репетиторы или педагогом в престижный частный лицей. В моем случае может показаться, что профессии совершенно разные, но сейчас могу сказать, что в хирургии и в программировании есть общее – в обоих случаях работа строится на определенных алгоритмах. Сходства помогают легче адаптироваться, не менять кардинально мышление.
          2. Не верить в миф, что 30-40-летним сложнее усваивать знания. Проверил на себе: если поддерживать себя в хорошей физической форме, заниматься спортом и саморазвитием, то хватит концентрации и сил на освоение даже целой области знаний.
          3. Оценить риски. В юности это редко кто делает, в 30-40 ты уже выбираешь осознанно, потому что несешь ответственность за близких. Кардинальные перемены требуют тщательной проработки и оценка ситуации и рисков нужна обязательно.
          4. Подготовить подушку безопасности. Все просто: рассчитайте, чтобы вам было на что жить 5-6 месяцев.
          5. Приготовьтесь к тому, что возможно первое время ваш доход будет ниже привычного. Вы пойдете на стартовую позицию, а значит, что полгода-год должны будете жить скромнее, а работать больше. Мой первый год в программировании уходил на работу, обучение и сон. Доход при этом был чуть выше зарплаты оперирующего хирурга на 1.5 ставки с высшей категорией и кандидатской ученой степенью. Это больше важно принять психологически, потому что будут мучить мысли о том, что все происходящее – большая ошибка, и возникнет желание вернуться в привычную колею.

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


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

          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/338232/


          Метки:  

          [Из песочницы] Из хирурга в разработчики: как в 40 лет сменить профессию?

          Вторник, 19 Сентября 2017 г. 13:00 + в цитатник
          alxpotapov сегодня в 13:00 Разное

          Из хирурга в разработчики: как в 40 лет сменить профессию?

          image


          Привет! Меня зовут Алексей, я тимлид в крупной IT-компании. Сейчас мне 43, только в 40 лет я стал разработчиком, а до этого 15 лет был практикующим врачом-хирургом. Делюсь с вами, как в середине жизни я поменял профессию, о страхах, рисках и планах с этим связанных.


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


          В 1998 году я закончил Самарский государственный медицинский университет, в 2000 – ординатуру по специальности «Хирургия» и одновременно защитил кандидатскую диссертацию. Переехал в г. Усинск (Республика Коми), где 8 лет проработал хирургом, потом был г. Ханты-Мансийск (Югра), где я продолжил трудиться по специальности.


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


          За время работы повышал профессиональный уровень с помощью дополнительных специализаций, в том числе в больницах и госпиталях Франции, Чехии и США.


          В целом моя карьера складывалась удачно, были профессиональные перспективы, но были и сложности. В России врач – это призвание. Не в том смысле, чтобы любить свое дело и посвящать ему себя полностью. Этого хватало. Несмотря на то, что ты ежедневно отвечаешь за жизнь и здоровье людей, тебе и твоей семье при этом приходится практически выживать. На севере (Республика Коми, ХМАО) еще можно получать хорошую зарплату врача, но в средней полосе ситуация крайне сложная. Туда мне предстояло вернуться: на малой родине (г. Пенза) остались родители, которым нужно помогать и поддерживать.


          А в этом регионе с зарплатами совсем туго. Чтобы не оказаться без денег после очередного переезда, нужно было позаботиться о будущем заранее. Помогло хобби. В свободное время я выручал знакомых – настраивал программное обеспечение. Даже одно время подрабатывал программистом в пожарной части в Усинске. Начальник пожарной части был у меня пациентом, а потом предложил дополнительный заработок. В основном делал внешние отчеты и дорабатывал конфигурацию 1С Предприятия под их организацию. В общем пришлось освоить нехитрый язык 1С. Помимо этого написал и поддерживал систему учета в пожарной части на FireBird & Delphi.


          Я был самоучкой, специальных знаний не имел, мне просто нравилось программирование само по себе. Решил, что дополнительная профессия не помешает, а станет моей подстраховкой. Потому в 2011 году поступил на заочное отделение в Томский государственный университет систем управления и радиоэлектроники по специальности «Программное обеспечение вычислительных систем и автоматизированных комплексов». Закончил его экстерном в 2014 году.


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


          В том же 2014 году я с семьей переехал в Поволжье. Во всех городах средней полосы ситуация с зарплатами плачевная. На 20 тысяч докторской з/п, что мне предложили в Пензе, невозможно обеспечить достойную жизни для себя и семьи. Предстояло решить, что делать дальше. С одной стороны, привычная жизнь, профессиональные успехи, но критично низкая зарплата и грустная перспектива – в финансовом отношении ждать изменений не приходилось. С другой стороны, стартовая позиция в новой профессии и не факт, что «выстрелит» и я в возрасте «далеко за 30» чего-то достигну. Однако надежда поднять уровень жизни семьи и хорошо зарабатывать в будущем перевесила страхи.


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


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


          В нынешней компании я оказался случайно. Жена увидела открытую вакансию, мы обсудили и решили: а почему бы не попробовать? Тогда я сомневался: крупная компания, серьезный продукт, у меня мало опыта, совсем не был уверен, что из этого что-то выйдет. Но на вакансию откликнулся, решил, что попытаться стоит. Выполнил тестовое, меня пригласили на собеседование в головной офис. Волновался, но все прошло гладко и меня взяли на испытательный срок на 2 месяца.


          Плюсы, которые, я сразу оценил: хорошая зарплата, крутая команда, возможность работать удаленно и перспективы роста. Начал с позиции рядового разработчика, за 1,5 года дорос до тимлида. Сейчас практически все мое рабочее время занимает SIEM: подготовка кандидат-релизов, написание коннекторов, разработка дальнейшей функциональности. Иногда исправляю старые «баги», доставшиеся мне по наследству от коллег по работе, участвую в разработке общего для всех продуктов SDK взаимодействия между компонентами (REST). Задачи интересные, команда сильная.


          Сейчас я пишу на Delphi, Go, немного поработал с C#. В качестве БД на хорошем уровне изучил MSSQL и MongoDB. Теперь я могу жить и работать в том регионе, где мне удобно, заниматься делом, которое мне по душе, и при этом не быть ущемленным финансово.


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


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


          1. Оценить насколько нынешняя профессия схожа с новой. Конечно, проще уходить в смежную область или просто в коммерческую сферу, к примеру, были школьным учителем – пошли в репетиторы или педагогом в престижный частный лицей. В моем случае может показаться, что профессии совершенно разные, но сейчас могу сказать, что в хирургии и в программировании есть общее – в обоих случаях работа строится на определенных алгоритмах. Сходства помогают легче адаптироваться, не менять кардинально мышление.
          2. Не верить в миф, что 30-40-летним сложнее усваивать знания. Проверил на себе: если поддерживать себя в хорошей физической форме, заниматься спортом и саморазвитием, то хватит концентрации и сил на освоение даже целой области знаний.
          3. Оценить риски. В юности это редко кто делает, в 30-40 ты уже выбираешь осознанно, потому что несешь ответственность за близких. Кардинальные перемены требуют тщательной проработки и оценка ситуации и рисков нужна обязательно.
          4. Подготовить подушку безопасности. Все просто: рассчитайте, чтобы вам было на что жить 5-6 месяцев.
          5. Приготовьтесь к тому, что возможно первое время ваш доход будет ниже привычного. Вы пойдете на стартовую позицию, а значит, что полгода-год должны будете жить скромнее, а работать больше. Мой первый год в программировании уходил на работу, обучение и сон. Доход при этом был чуть выше зарплаты оперирующего хирурга на 1.5 ставки с высшей категорией и кандидатской ученой степенью. Это больше важно принять психологически, потому что будут мучить мысли о том, что все происходящее – большая ошибка, и возникнет желание вернуться в привычную колею.

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


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

          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/338232/


          Метки:  

          [Перевод] Оптимизация веб-серверов для повышения пропускной способности и уменьшения задержки

          Вторник, 19 Сентября 2017 г. 12:52 + в цитатник
          max_m сегодня в 12:52 Администрирование

          Оптимизация веб-серверов для повышения пропускной способности и уменьшения задержки

          • Перевод


          Привет! Меня зовут Макс Матюхин, я работаю в SRV-команде Badoo. Мы в Badoo не только активно пишем посты в свой блог, но и внимательно читаем блоги наших коллег из других компаний. Недавно ребята из Dropbox опубликовали шикарный пост о различных способах оптимизации серверных приложений: начиная с железа и заканчивая уровнем приложения. Его автор – Алексей Иванов – дал огромное количество советов и ссылок на дополнительные источники информации. К сожалению, у Dropbox нет блога на Хабре, поэтому я решил перевести этот пост для наших читателей.


          Это расширенная версия моего выступления на nginx.conf 2017 в сентябре этого года. В качестве старшего инженера по контролю качестве (SRE) в команде Dropbox Traffic я отвечаю за нашу сеть Edge: её надёжность, производительность и эффективность. Это proxy-tier-сеть, построенная на базе nginx и предназначенная как для обработки чувствительных к задержке метаданных, так и для передачи данных с высокой пропускной способностью. В системе, обрабатывающей десятки гигабитов в секунду и одновременно – десятки тысяч транзакций, чувствительных к задержкам, используются различные оптимизации эффективности и производительности: начиная с драйверов и прерываний, сквозь ядро и TCP/ IP-стек, и заканчивая библиотеками и настройками уровня приложения.


          Пояснения


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


          Это не пост о производительности Linux (хотя я и буду часто ссылаться на bcc, eBPF и perf) и не исчерпывающее руководство по использованию инструментов профилирования производительности (если вы хотите узнать о них больше, почитайте блог Брендана Грегга).


          Это также не пост о производительности браузеров. Я буду упоминать о клиентской производительности применительно к оптимизациям задержек, но очень коротко. Хотите узнать больше – прочитайте статью High Performance Browser Networking Ильи Григорика.


          И это не компиляция на тему лучших методик TLS. Хотя я и буду упоминать TLS-библиотеки и их настройки, вы и ваша команда обеспечения безопасности должны самостоятельно оценивать их производительность и влияние на безопасность. Чтобы узнать, насколько ваши серверы отвечают набору лучших методик, можете воспользоваться Qualys SSL Test. Если хотите узнать больше о TLS в целом, подпишитесь на рассылку Feisty Duck Bulletproof TLS Newsletter.


          Структура поста


          Мы рассмотрим оптимизации эффективности/ производительности на разных уровнях системы. Начнём с самого нижнего, аппаратно-драйверного, уровня: эти настройки можно применить практически к любому высоконагруженному серверу. Затем я перейду к ядру Linux и его TCP/IP-стеку: можете покрутить эти ручки на своих ящиках, активно использующих TCP. Наконец, мы обсудим настройки на уровне библиотек и приложений, которые по большей части применимы ко многим веб-серверам и в частности к nginx.


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


          Оборудование


          Процессор


          Для хорошей производительности асимметричного RSA/EC выбирайте процессоры как минимум с поддержкой AVX2 (avx2 в /proc/cpuinfo) и желательно подходящие для вычислений с большими целыми числами (bmi и adx). Для симметричного шифрования выбирайте AES-NI для AES-шифров и AVX-512 – для ChaCha+Poly. У Intel есть сравнение производительности разных поколений процессоров с OpenSSL 1.0.2, где рассматривается влияние этих аппаратных оптимизаций.


          Для задач, где важен уровень задержки, вроде роутинга рекомендуется уменьшить количество NUMA-узлов и отключить Hyper-Threading. Задачи, требующие высокой пропускной способности, эффективнее выполняются при большем количестве ядер с использованием Hyper-Threading (если только нет привязки к кэшу), и в целом NUMA не играет для них особой роли.


          Если выбираете среди продукции Intel, то смотрите на процессоры с архитектурой Haswell/ Broadwell, а лучше Skylake. У AMD впечатляющую производительность демонстрируют EPYC-модели.


          Сетевая карта


          Вам нужно как минимум 10 Гбит, а лучше – 25 Гбит. Если хотите передавать через один сервер с TLS ещё больше, то описанных здесь настроек может быть недостаточно – возможно, придётся сдвинуть TLS-фрейминг на уровень ядра (FreeBSD, Linux).


          Что касается программного уровня, поищите open-source-драйверы с активными списками рассылки и сообществами. Это будет очень важным фактором, если (скорее «когда») вы будете заниматься решением проблем, связанных с драйверами.


          Память


          Эмпирическое правило: задачи, чувствительные к задержке, требуют более быстрой памяти; задачи, чувствительные к пропускной способности, требуют больше памяти.


          Диски


          Всё зависит от ваших требований к буферизации/кэшированию. Если вам нужно много буферизировать или кэшировать, то лучше выбрать SSD-диски. Некоторые даже устанавливают заточенные под флеш файловые системы (обычно log-structured), но они не всегда показывают более высокую производительность по сравнению с обычными ext4/ xfs.
          В любом случае не сгубите свои флеш-накопители, забыв включить TRIM или обновить прошивку.


          Операционные системы: низкий уровень


          Прошивка


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


          Драйверы


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


          Процессор


          Ваш лучший друг — репозиторий ядра (и инструменты, поставляемые с ним). В Ubuntu/ Debian вы можете установить пакет linux-tools с набором утилит, но в этом посте мы будем использовать только cpupower, turbostat и x86_energy_perf_policy. Для проверки связанных с процессором оптимизаций вы можете провести стресс-тестирование своего ПО с помощью любимого генератора нагрузки (например, Yandex.Tank). Вот презентация о лучших методиках нагрузочного тестирования от разработчиков nginx: NGINX Performance testing.


          cpupower


          $ cpupower frequency-info
          ...
            driver: intel_pstate
            ...
            available cpufreq governors: performance powersave
            ...            
            The governor "performance" may decide which speed to use
            ...
            boost state support:
              Supported: yes
              Active: yes

          Проверьте, включён ли Turbo Boost, а если у вас процессор Intel, удостоверьтесь, что система работает с intel_pstate, а не с acpi-cpufreq или pcc-cpufreq. Если вы всё ещё используете acpi-cpufreq, обновите ядро. Если это невозможно, используйте режим performance. При работе с intel_pstate даже режим powersave должен выполняться с хорошей производительностью, но вам придётся проверить это самостоятельно.


          Что касается простоя, чтобы посмотреть, что реально происходит с вашим процессором, вы можете с помощью turbostat напрямую заглянуть в процессорные MSR и извлечь информацию о питании, частоте и так называемых Idle States:


          # turbostat --debug -P
          ... Avg_MHz Busy% ... CPU%c1 CPU%c3 CPU%c6 ... Pkg%pc2 Pkg%pc3 Pkg%pc6 ...

          Здесь вы видите реальную частоту процессора (да, /proc/cpuinfo вам врёт), а также текущее состояние ядра/набора ядер.


          Если даже с драйвером intel_pstate процессор тратит на простой больше времени, чем вы думали, вы можете:


          • переключить регулятор на performance;
          • для повышения производительности настроить x86_energy_perf_policy.

          А для очень чувствительных к задержке задач можно:


          • использовать интерфейс /dev/cpu_dma_latency;
          • для UDP-трафика использовать busy-polling.

          Узнать больше об управлении питанием процессора в целом и P-состояниями в частности можно из презентации Balancing Power and Performance in the Linux Kernel с LinuxCon Europe 2015.


          Привязка к процессору


          Можно ещё больше уменьшить задержку, привязав поток или процесс к CPU. Например, в nginx есть директива worker_cpu_affinity, которая автоматически привязывает каждый процесс веб-сервера к конкретному ядру. Это позволяет исключить миграцию процесса / потока на другое ядро, уменьшить количество промахов кэша и ошибок страниц памяти, а также слегка увеличить количество инструкций в цикле. Всё это можно проверить через perf stat.


          Но процессорная привязка негативно влияет на производительность, поскольку процессам дольше приходится ждать освобождения процессора. Это можно отслеживать с помощь запуска runqlat на одном из ваших PID nginx-воркера:


          usecs               : count     distribution
              0 -> 1          : 819      |                                        |
              2 -> 3          : 58888    |******************************          |
              4 -> 7          : 77984    |****************************************|
              8 -> 15         : 10529    |*****                                   |
             16 -> 31         : 4853     |**                                      |
             ...
           4096 -> 8191       : 34       |                                        |
           8192 -> 16383      : 39       |                                        |
          16384 -> 32767      : 17       |                                        |

          Если заметите длинные хвосты на много миллисекунд, то, вероятно, на серверах выполняется слишком много всего, помимо nginx, и привязка увеличит задержку, а не уменьшит её.


          Память


          Все настройки Memory Management обычно сильно зависят от рабочего процесса, так что могу дать лишь такие рекомендации:



          Современные процессоры представляют собой несколько отдельных процессоров, связанных очень быстрой шиной и совместно использующих различные ресурсы, начиная с кэша L1 на HT-ядрах и заканчивая кэшем L3 применительно к пакетам, памятью и PCIe-соединениями в рамках сокетов. Это и есть NUMA: многочисленные модули исполнения и хранения с быстрой шиной обмена данными.


          Исчерпывающее описание NUMA и её применения содержится в статье Фрэнка Деннемана NUMA Deep Dive Series.


          Короче, вы можете:


          • игнорировать её, отключив в BIOS или выполняя своё ПО под numactl --interleave=all (так вы получите посредственную, но достаточно стабильную производительность);
          • отказаться от неё, используя одноузловые серверы, как это делает Facebook с платформой OCP Yosemite;
          • принять её, оптимизируя размещение процессора/ памяти в пространствах ядра и пользователя.

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


          Для правильного использования NUMA вам нужно рассматривать каждый её узел в качестве отдельного сервера. Проверьте топологию с помощью numactl --hardware:


          $ numactl --hardware
          available: 4 nodes (0-3)
          node 0 cpus: 0 1 2 3 16 17 18 19
          node 0 size: 32149 MB
          node 1 cpus: 4 5 6 7 20 21 22 23
          node 1 size: 32213 MB
          node 2 cpus: 8 9 10 11 24 25 26 27
          node 2 size: 0 MB
          node 3 cpus: 12 13 14 15 28 29 30 31
          node 3 size: 0 MB
          node distances:
          node   0   1   2   3
            0:  10  16  16  16
            1:  16  10  16  16
            2:  16  16  10  16
            3:  16  16  16  10

          Что нужно проверять:


          • количество узлов;
          • объём памяти для каждого узла;
          • количество процессоров для каждого узла;
          • расстояние между узлами.

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


          Это можно проверить с помощью numastat:


          $ numastat -n -c
                            Node 0   Node 1 Node 2 Node 3    Total
                          -------- -------- ------ ------ --------
          Numa_Hit        26833500 11885723      0      0 38719223
          Numa_Miss          18672  8561876      0      0  8580548
          Numa_Foreign     8561876    18672      0      0  8580548
          Interleave_Hit    392066   553771      0      0   945836
          Local_Node       8222745 11507968      0      0 19730712
          Other_Node      18629427  8939632      0      0 27569060

          Также с помощью numastat можно получить статистику использования памяти по каждому узлу в формате /proc/meminfo:


          $ numastat -m -c
                           Node 0 Node 1 Node 2 Node 3 Total
                           ------ ------ ------ ------ -----
          MemTotal          32150  32214      0      0 64363
          MemFree             462   5793      0      0  6255
          MemUsed           31688  26421      0      0 58109
          Active            16021   8588      0      0 24608
          Inactive          13436  16121      0      0 29557
          Active(anon)       1193    970      0      0  2163
          Inactive(anon)      121    108      0      0   229
          Active(file)      14828   7618      0      0 22446
          Inactive(file)    13315  16013      0      0 29327
          ...
          FilePages         28498  23957      0      0 52454
          Mapped              131    130      0      0   261
          AnonPages           962    757      0      0  1718
          Shmem               355    323      0      0   678
          KernelStack          10      5      0      0    16

          Теперь рассмотрим пример более простой топологии.


          $ numactl --hardware
          available: 2 nodes (0-1)
          node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
          node 0 size: 46967 MB
          node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
          node 1 size: 48355 MB

          Поскольку узлы по большей части симметричны, мы можем привязать экземпляр нашего приложения к каждому NUMA-узлу с помощью numactl --cpunodebind=X --membind=X, а затем открыть его на другом порте. Пропускная способность увеличится благодаря использованию обоих узлов и уменьшению задержки за счёт сохранения локальности памяти.


          Проверить эффективность размещения NUMA можно по задержке операций в памяти. Например, с помощью funclatency в BCC измерьте задержку операции, активно использующей память, допустим, memmove.
          Наблюдать за эффективностью на стороне ядра можно с помощью perf stat, отслеживая соответствующие события памяти и планировщика:


          # perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID
          ...
                           1      sched:sched_stick_numa
                           3      sched:sched_move_numa
                          41      sched:sched_swap_numa
                       5,239      migrate:mm_migrate_pages
                      50,161      minor-faults

          Последняя порция связанных с NUMA оптимизаций для сетевых нагрузок с активным использованием сети продиктована тем фактом, что сетевая карта — это PCIe-устройство, а каждое устройство привязано к своему NUMA-узлу; следовательно, у каких-то процессоров задержка при обращении к сети будет меньше. Возможные оптимизации мы обсудим в главе, где будет рассматриваться привязка сетевая карта -> процессор, а пока перейдём к PCI Express.


          PCIe


          Обычно нет нужды углубляться в решение проблем с PCIe, если только не возникает какой-то аппаратный сбой. Однако стоит хотя бы просто создать для своих PCIe-устройств «ширину шины», «скорость шины» и предупреждения RxErr/BadTLP. Это должно сэкономить вам часы на отладку из повреждённого железа или сбойного PCIe-согласования. Для этого можете воспользоваться lspci:


          # lspci -s 0a:00.0 -vvv
          ...
          LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM L1, Exit Latency L0s <2us, L1 <16us
          LnkSta: Speed 8GT/s, Width x8, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
          ...
          Capabilities: [100 v2] Advanced Error Reporting
          UESta:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
          UEMsk:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
          UESvrt: DLP+ SDES+ TLP- FCP+ CmpltTO- CmpltAbrt- ...
          CESta:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr-
          CEMsk:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr+

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



          Также советую прочитть статью Understanding PCIe Configuration for Maximum Performance, в ней подробнее рассматривается конфигурация PCIe, что может быть полезно при высоких скоростях, когда происходит потеря пакетов между картой и ОС.


          Intel предполагает, что иногда управление питанием PCIe (ASPM) может приводить к большим задержкам, а значит, и к потере большего количества пакетов. Эту функцию можно отключить, введя pcie_aspm=off в командной строке ядра.


          Сетевая карта


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


          Также можете поискать руководства для вашей ОС. Например, в руководстве по настройке сетевой производительности в Linux от Red Hat Enterprise объясняются многие из упомянутых выше оптимизаций. У Cloudflare тоже есть хорошая статья о настройке этой части сетевого стека, хотя по большей части она посвящена ситуациям, когда нужна низкая задержка.


          В ходе оптимизации вашим лучшим другом будет ethtool.
          Примечание: если вы используете достаточно свежее ядро (а вам следует это сделать!), то вы также столкнётесь с некоторыми аспектами вашего пользовательского пространства. Например, для сетевых операций вы, вероятно, захотите использовать более свежие версии пакетов ethtool, iproute2 и, быть может, iptables/nftables.


          Получить ценные сведения о том, что происходит с вашей сетевой картой, можно с помощью ethtool -S:


          $ ethtool -S eth0 | egrep 'miss|over|drop|lost|fifo'
               rx_dropped: 0
               tx_dropped: 0
               port.rx_dropped: 0
               port.tx_dropped_link_down: 0
               port.rx_oversize: 0
               port.arq_overflows: 0

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


          Что касается ядра, то нужно смотреть /proc/interrupts, /proc/softirqs и /proc/net/softnet_stat. Здесь есть два полезных BCC-инструмента: hardirqs и softirqs. Цель вашей оптимизации сети заключается в такой настройке системы, чтобы процессор использовался минимально, а пакеты не терялись.


          Привязка прерываний


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


          • для максимальной пропускной способности можно распределить прерывания по всем NUMA-узлам;
          • для минимизации задержки можно ограничить прерывания одним NUMA-узлом (для этого вам может понадобиться уменьшить количество очередей, чтобы не превысить возможности одного узла (обычно приходится уменьшать вдвое с помощью ethtool -L).

          Как правило, для этого вендоры предоставляют скрипты. Например, у Intel это set_irq_affinity.


          Размеры кольцевого буфера


          Сетевым картам нужно обмениваться информацией с ядром. Обычно это делается через структуру данных, называющуюся «кольцо». Текущий/ максимальный размер этого кольца можно посмотреть с помощью ethtool -g:


          $ ethtool -g eth0
          Ring parameters for eth0:
          Pre-set maximums:
          RX:                4096
          TX:                4096
          Current hardware settings:
          RX:                4096
          TX:                4096

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


          • в более старых ядрах или драйверах без поддержки BQL высокие значения могут относиться к более высокому bufferbloat на TX-стороне;


          • буферы большего размера увеличивают давление кэша, так что, если вы с этим столкнётесь, постарайтесь уменьшить буферы.

          Объединение прерываний


          Этот механизм обеспечивает задержку уведомления ядра о новых событиях за счёт объединения нескольких сообщений в одно прерывание. Текущие настройки можно посмотреть с помощью ethtool -c:


          $ ethtool -c eth0
          Coalesce parameters for eth0:
          ...
          rx-usecs: 50
          tx-usecs: 50

          Также вы можете придерживаться статичных пределов (static limits), жёстко ограничив максимальное количество прерываний в секунду на одно ядро, или положиться на автоматическую аппаратную подстройку частоты прерываний в зависимости от пропускной способности.


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


          Разгрузки


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


          Все возможные разгрузки можно просмотреть с помощью ethtool -k:


          $ ethtool -k eth0
          Features for eth0:
          ...
          tcp-segmentation-offload: on
          generic-segmentation-offload: on
          generic-receive-offload: on
          large-receive-offload: off [fixed]

          Все ненастраиваемые разгрузки помечены суффиксом [fixed]. О них можно долго рассказывать, но я только приведу несколько эмпирических правил:


          • не включайте LRO – вместо этого используйте GRO;
          • будьте осторожны с TSO, поскольку это сильно зависит от качества ваших драйверов/ прошивки;
          • не включайте TSO/ GSO на старых ядрах, потому что это может привести к чрезмерному bufferbloat.

          Регулирование пакетов


          Все современные сетевые карты оптимизированы под многопроцессорные системы, поэтому они распределяют пакеты по виртуальным очередям (обычно по одной на процессор). Когда это выполняется аппаратно, то называется RSS; когда за балансировку пакетов между процессорами отвечает ОС, это называется RPS (TX-эквивалент называется XPS). Если ОС пытается регулировать потоки к процессорам, которые в данный момент обрабатывают этот сокет, это называется RFS. А когда этим занимается железо, это называется «ускоренный RFS» или aRFS.


          Вот несколько хороших методик:


          • если вы используете новое оборудование 25 Гбит+, то в нём, вероятно, есть достаточно очередей и огромная таблица косвенной переадресации (indirection table), чтобы можно было применять RSS между всеми ядрами (некоторые более старые карты могут использовать только первые 16 процессоров);
          • можете попробовать включить RPS, если:

          1) у вас больше процессоров, чем аппаратных очередей, и вы хотите пожертвовать задержкой в пользу пропускной способности;
          2) вы используете внутреннее туннелирование (например, GRE/ IPinIP), при котором сетевая карта не может применять RSS;


          • не включайте RPS, если у вас достаточно старый процессор, не имеющий x2APIC;
          • привязка каждого процессора к собственной TX-очереди посредством XPS – в целом хорошая идея;
          • эффективность RFS во многом зависит от вашей рабочей нагрузки, а также от того, примените ли вы процессорную привязку.

          Flow Director и ATR


          Включённый Flow Director (или fdir в терминологии Intel) по умолчанию оперирует в режиме Application Targeting Routing, при котором реализуется aRFS посредством семплирования пакетов и регулирования потоков в процессорное ядро, где они, по-видимому, обрабатываются. Статистику можно посмотреть с помощью ethtool -S:$ ethtool -S eth0 | egrep ‘fdir’ port.fdir_flush_cnt: 0 …


          Хотя Intel заявляет, что fdir в некоторых случаях увеличивает производительность, результаты одного исследования говорят о том, что это может также привести к переупорядочиванию 1% пакетов, что может довольно негативно сказаться на производительности TCP. Поэтому протестируйте самостоятельно и посмотрите, будет ли Flow Director полезен при вашей рабочей нагрузке, проверяя счётчик TCPOFOQueue.


          Операционные системы: сетевой стек


          Существует огромное количество книг, видео и руководств по настройке сетевого стека Linux, в которых растиражирован «карго-культ sysctl.conf». И хотя свежие версии ядра уже не требуют такого объёма настройки, как десять лет назад, а большинство новых TCP/ IP-свойств по умолчанию включены и хорошо настроены, люди продолжают копипастить свои старые sysctls.conf, которые они использовали для настройки ядер версий 2.6.18/ 2.6.32.


          Для проверки эффективности сетевых оптимизаций сделайте следующее:


          • с помощью /proc/net/snmp and /proc/net/netstat соберите TCP-метрики в рамках системы;
          • добавьте метрики подключения, собранные с помощью ss -n --extended --info или при вызове внутри сервера getsockopt(``[TCP_INFO]``)/getsockopt(``[TCP_CC_INFO]``);
          • соберите tcptrace(1)`ы образцов TCP-потоков;
          • проанализируйте RUM-метрики приложения/ браузера.

          В качестве источников информации о сетевых оптимизациях я обычно использую выступления специалистов по CDN, потому что, как правило, они знают, что делают. Например, Fastly on LinuxCon Australia. Полезно также послушать, что говорят разработчики ядра Linux, к примеру, на NetDevConf и Netconf.


          Также стоит упомянуть про подробные материалы от PackageCloud по сетевому стеку Linux, особенно в свете того, что они сделали акцент на мониторинг, а не на «слепую» настройку:



          И позвольте дать совет напоследок: обновите ядро ОС! Существует множество новых сетевых улучшений, и я говорю даже не об IW10 (который 2010) – я говорю о таких новинках, как автоматический выбор размера TSO, FQ, pacing, TLP и RACK. В качестве бонуса от апгрейда вы получите ряд улучшений масштабируемости, например, убранный кэш рутинга, неблокирующие сокеты прослушивания, SO_REUSEPORT и многое другое.


          Обзор


          Из недавних документов по работе с сетью в Linux особенно выделяется Making Linux TCP Fast. В нём на четырёх страницах собраны улучшения в ядре ОС за много лет. TCP-стек на стороне отправителя разбит на функциональные части:



          Fair queueing и pacing


          Fair queueing отвечает за соблюдение «справедливости» и уменьшает блокировку очереди между TCP-потоками, что положительно сказывается на частоте отбрасывания пакетов. Pacing, в свою очередь, равномерно распределяет пакеты во времени с частотой, определяемой Congestion Control, что ещё больше уменьшает долю потерянных пакетов, тем самым увеличивая пропускную способность.


          Попутно хочу заметить, что fair queueing и pacing доступны в Linux посредством fq qdisc. Обе фичи требуются для BBR (впрочем, уже нет), но их можно использовать и с CUBIC, добиваясь 15–20%-ного снижения потери пакетов, а значит, и повышения пропускной способности в алгоритмах управления перегрузками (loss-based CCs). Только не используйте их на старых ядрах (<3.19), поскольку вы станете регулировать обычные ACKs и сломаете аплоад/ RPCs.


          Автоматический выбор размера TSO и TSQ


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


          Управление перегрузками


          CC-алгоритмы сами по себе – объёмная тема, и в последние годы о них было много разговоров. Что-то из этого вылилось в код: tcp_cdg (CAIA), tcp_nv (Facebook) и tcp_bbr (Google). Мы не будем углубляться в их устройство, скажу лишь, что индикация о перегрузке во всех них основана больше на увеличении отсрочки (delay), чем на отбрасывании пакетов.


          BBR – один из наиболее задокументированных, протестированных и практичных из всех новых алгоритмов управления перегрузками. На основании доли доставленных пакетов создаётся модель сетевого пути, а затем для увеличения ширины пропускания и минимизации RTT выполняются управляющие циклы. Это именно то, что мы ищем в нашем прокси-стеке.


          Предварительные результаты экспериментов с BBR на наших Edge PoP показали увеличение скорости скачивания файлов:



          Шестичасовой эксперимент с TCP BBR в Tokyo PoP: ось x — время, ось y — скорость скачивания на клиенте


          Увеличение скорости наблюдалось по всем перцентилям. При изменениях бэкенда такого не происходит — обычно положительный результат наблюдается только p90+ пользователей (у которых самое быстрое интернет-подключение), поскольку мы считаем, что у всех остальных уже ограничена полоса пропускания. Настройки на сетевом уровне вроде изменения управления перегрузками или включения FQ/ pacing демонстрируют, что у пользователей ограничена не полоса пропускания, а, я бы сказал, присутствует «ограниченность TCP».


          Если вы хотите больше узнать о BBR, то у APNIC есть хороший обзор для новичков (и сравнение с loss-based-управлением перегрузками). Более глубокую информацию можно извлечь из архивов почтовой рассылки bbr-dev (там сверху закреплено множество полезных ссылок). Если вас в целом интересует тема управления перегрузками, то можете понаблюдать за активностью Internet Congestion Control Research Group.


          ACK-обработка и обнаружение пропадания пакетов


          Теперь поговорим об обнаружении пропадания пакетов (loss detection). Снова упомяну про важность использования свежей версии ядра ОС. В TCP постоянно добавляются новые эвристики вроде TLP и RACK, а старые (наподобие FACK и ER) убираются. Нововведения работают по умолчанию, так что вам не придётся настраивать систему после апгрейда.


          Приоритизация пользовательского пространства и HOL


          API сокета пользовательского пространства (userspace socket API) предоставляют механизм явной буферизации, и после отправки чанков их уже невозможно перегруппировать. Поэтому при использовании мультиплексирования (например, в HTTP/2) это может привести к Head-of-Line блокировке и инверсии h2-приоритетов. Для решения этой проблемы были разработаны опция сокета и соответствующая опция sysctl net.ipv4.tcp_notsent_lowat. Они позволяют настраивать границы, в пределах которых сокет считает себя доступным для записи (то есть epoll в вашем приложении будет врать). Это может решить проблемы с HTTP/2-приоритизацией, но при этом плохо повлиять на пропускную способность, так что рекомендую проверить самостоятельно.


          Sysctls


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



          Лучше сделайте вот что:


          • net.ipv4.tcp_slow_start_after_idle=0: главная проблема с медленным стартом (slow start) после простоя заключается в том, что «простой» определяется как один RTO, а этого слишком мало;
          • net.ipv4.tcp_mtu_probing=1: полезно при наличии ICMP-«чёрных дыр» между вами и клиентами (наверняка они есть );
          • net.ipv4.tcp_rmem, net.ipv4.tcp_wmem: нужно настроить так, чтобы подходило к BDP; только не забудьте, что больше – не значит лучше;
          • echo 2 > /sys/module/tcp_cubic/parameters/hystart_detect: если вы используете FQ+CUBIC, то это может помочь решить проблему слишком раннего выхода tcp_cubic из медленного старта.

          Стоит упомянуть, что существует RFC-черновик (хотя и подзаброшенный) от Дэниела Штенберга, автора curl, под названием TCP Tuning for HTTP, в котором сделана попытка собрать все системные настройки, которые могут быть полезны для HTTP.


          Уровень приложения: средний уровень


          Инструментарий


          Как и в случае с ядром ОС, пользовательское пространство крайне важно актуализировать. Начните с обновления своего инструментария, например, можете упаковать более свежие версии perf, bcc и так далее.


          После этого можно приступать к настройке и отслеживанию поведения системы. В этой части поста мы будем по большей части опираться на профилирование процессора с помощью perf top, on-CPU flame-графики и ad hoc-гистрограммы из funclatency в bcc.



          Инструментарий для компилирования


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


          Помимо производительности, новые компиляторы могут похвастаться и новыми свойствами обеспечения безопасности (например, -fstack-protector-strong или SafeStack). Также современный инструментарий будет полезен, если вы хотите прогонять тесты через бинарные файлы, скомпилированные с использованием санитайзеров (например, AddressSanitizer и других).


          Системные библиотеки


          Рекомендую обновить системные библиотеки вроде glibc, иначе вы можете не получить свежих оптимизаций низкоуровневых функций из -lc, -lm, -lrt и так далее. Стандартное предупреждение: тестируйте самостоятельно, поскольку могут встречаться неожиданные регрессии.


          zlib


          Обычно за компрессию отвечает веб-сервер. В зависимости от объёма данных, проходящих через прокси, вы можете встретить упоминание zlib в perf top, например:


          # perf top
          ...
             8.88%  nginx        [.] longest_match
             8.29%  nginx        [.] deflate_slow
             1.90%  nginx        [.] compress_block

          Это можно оптимизировать на самом низком уровне: Intel и Cloudflare, как и отдельный проект zlib-ng, имеют собственные zlib-форки, обеспечивающие более высокую производительность за счёт использования новых наборов инструкций.


          malloc


          При обсуждении оптимизаций до этого момента мы по большей части ориентировались на процессор. Теперь же поговорим о памяти. Если вы активно используете Lua с FFI или тяжёлые сторонние модули, которые самостоятельно управляют памятью, то могли заметить рост потребления памяти из-за фрагментации. Эту проблему можно попытаться решить переключением на jemalloc или tcmalloc.


          Использование кастомного malloc даёт следующие преимущества:



          Если в конфигурации nginx вы используете многочисленные сложные регулярные выражения или активно применяете Lua, то могли встретить в perf top упоминание PCRE. Это можно оптимизировать, скомпилировав PCRE с JIT, а также включив её в nginx посредством pcre_jit on;.


          Результат оптимизации можно проверить на flame-графиках или с помощью funclatency:


          # funclatency /srv/nginx-bazel/sbin/nginx:ngx_http_regex_exec -u
          ...
               usecs               : count     distribution
                   0 -> 1          : 1159     |**********                              |
                   2 -> 3          : 4468     |****************************************|
                   4 -> 7          : 622      |*****                                   |
                   8 -> 15         : 610      |*****                                   |
                  16 -> 31         : 209      |*                                       |
                  32 -> 63         : 91       |                                        |

          TLS


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


          Сегодня первое, что вам нужно решить, — какую TLS-библиотеку вы будете использовать: Vanilla OpenSSL, OpenBSD’s LibreSSL или BoringSSL от Google. Определившись, вам нужно правильно её собрать: к примеру, у OpenSSL есть куча сборочных эвристик, позволяющих использовать оптимизации на базе сборочного окружения; у BoringSSL есть детерминистские сборки, но они более консервативны и по умолчанию просто отключают некоторые оптимизации. В любом случае, здесь вы наконец-то ощутите выгоду от выбора современного процессора: большинство TLS-библиотек могут использовать всё, от AES-NI и SSE до ADX и AVX-512. Можете воспользоваться встроенными тестами производительности. Например, в случае с BoringSSL это bssl speed.


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


          Асимметричное шифрование


          Если у вас “front”-сервис (сервис к которому пользователи подключаются напрямую), то вы могли столкнуться со значительным количеством TLS-«рукопожатий», а значит, немалая доля ресурсов вашего процессора тратится на асимметричное шифрование, которое необходимо оптимизировать.


          Для оптимизации использования серверного процессора можете переключиться на сертификаты ECDSA, которые в десять раз быстрее, чем RSA. К тому же они значительно меньше, что может ускорить «рукопожатия» при наличии потерь пакетов. Но ECDSA сильно зависят от качества генератора случайных чисел в вашей системе, так что если вы используете OpenSSL, то удостоверьтесь, что у вас достаточно энтропии (в случае с BoringSSL об этом можно не волноваться).


          И ещё раз напоминаю, что больше – не значит лучше, то есть использование сертификатов 4096 RSA ухудшит производительность в десять раз:


          $ bssl speed
          Did 1517 RSA 2048 signing ... (1507.3 ops/sec)
          Did 160 RSA 4096 signing ...  (153.4 ops/sec)

          Но меньше тоже не значит лучше: при использовании малораспространённого поля p-224 для ECDSA вы получите 60%-ное снижение производительности по сравнению с обычным p-256:


          $ bssl speed
          Did 7056 ECDSA P-224 signing ...  (6831.1 ops/sec)
          Did 17000 ECDSA P-256 signing ... (16885.3 ops/sec)

          Эмпирическое правило: самое распространённое шифрование обычно самое оптимизированное.


          При запуске правильно оптимизированной библиотеки на основе OpenTLS, использующей сертификаты RSA, в своём perf top вы должны увидеть следующие трейсы: процессоры, использующие AVX2, а не ADX (например, с архитектурой Haswell), должны использовать кодовый путь AVX2:


            6.42%  nginx                [.] rsaz_1024_sqr_avx2
            1.61%  nginx                [.] rsaz_1024_mul_avx2

          Более новые модели должны использовать обычный алгоритм Монтгомери с кодовым путём ADX:


            7.08%  nginx                [.] sqrx8x_internal
            2.30%  nginx                [.] mulx4x_internal

          Симметричное шифрование


          Если у вас много массовых передач данных вроде видео, фото и прочих файлов, то можете начать отслеживать в данных профилировщика упоминания о симметричном шифровании. Тогда просто удостоверьтесь, что ваш процессор поддерживает AES-NI и что вы настроили на сервере применение шифров AES-GCM. При правильно настроенном оборудовании в perf top должно выдаваться:


            8.47%  nginx                [.] aesni_ctr32_ghash_6x

          Но заниматься шифрованием/ дешифрованием будут не только ваши серверы, но и клиенты, причём у них априори гораздо более слабые процессоры. Без аппаратного ускорения это может быть достаточно сложной операцией, поэтому позаботьтесь о выборе алгоритма, который работает быстро без аппаратных технологий ускорения работы с шифрованием, например, ChaCha20-Poly1305. Это снизит TTLB для части мобильных клиентов.


          В BoringSSL из коробки поддерживается ChaCha20-Poly1305, а в OpenSSL 1.0.2 можете использовать патчи Cloudflare. BoringSSL также поддерживает «шифрогруппы равного предпочтения», так что можете использовать следующую конфигурацию, которая позволит клиентам решать, какие шифры использовать, отталкиваясь от своих аппаратных возможностей (бесстыдно украдено из cloudflare/sslconfig):


          ssl_ciphers '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES';
          ssl_prefer_server_ciphers on;

          Уровень приложения: высокоуровневые оптимизации


          Для анализа эффективности ваших оптимизаций на этом уровне вам нужно собирать RUM-данные. В браузерах можно применять API Navigation Timing и Resource Timing. Ваши главные метрики — TTFB и TTV/ TTI. Вам сильно упростит итерирование, если эти данные будут представлены в форматах, удобных для составления запросов и графиков.


          Компрессия


          В nginx компрессия начинается с файла mime.types, определяющего соответствие между расширением файла и MIME-типом. Затем вам нужно определить, какой тип вы хотите передавать компрессору, с, например, gzip_types. Если хотите завершить этот список, то для автоматического генерирования mime.types с добавлением compressible == true to gzip_types можете воспользоваться mime-db.


          Включая gzip, имейте в виду:


          • это увеличивает потребление памяти (проблема решается путём ограничения gzip_buffers);
          • вследствие буферизации это увеличивает TTFB (проблема решается с помощью gzip_no_buffer).

          Отмечу, что HTTP-компрессия не ограничивается одним gzip: в nginx есть сторонний модуль ngx_brotli, который способен сжимать на 30% лучше, чем gzip.


          Что касается настроек сжатия, давайте рассмотрим два отдельных случая: статичные и динамические данные.


          В случае со статичными данными можно архивировать коэффициенты сжатия с помощью предварительной компрессии статичных ресурсов, сделав эту процедуру частью процесса сборки. Для gzip и brotli это подробно рассмотрено в посте Deploying Brotli for static content.


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


          Буферизация внутри прокси может сильно влиять на производительность веб-сервера, особенно с учётом задержки. В прокси-модуле nginx есть разные настройки буферизации, которые можно регулировать в зависимости от местонахождения буферов и каждая из которых полезна в определённых случаях. С помощью proxy_request_buffering и proxy_buffering можно отдельно управлять буферизацией в обоих направлениях. Если включена буферизация, то верхняя граница потребления памяти определяется с помощью client_body_buffer_size и proxy_buffers, и по достижении этой границы запрос/ ответ будут буферизоваться на диске. Для ответов это можно отключить, присвоив proxy_max_temp_file_size значение 0.


          Наиболее распространённые примеры использования:


          • буферизация запроса/ ответа до определённого предела — в памяти, а затем сбрасывание на диск. Если включена буферизация запросов, вы можете отправить запрос на бэкенд только после его полного получения. А если включена буферизация ответов, вы можете мгновенно освободить поток выполнения бэкенда, как только тот будет готов ответить. При таком подходе улучшаются пропускная способность и защита бэкенда, но при этом растут задержка, потребление памяти и количество операций ввода/ вывода (хотя, если вы используете SSD, это не будет особой проблемой);
          • буферизация отключена. Это делается при наличии маршрутов, чувствительных к уровню задержки, особенно в случае со стримингом. Но если вы отключите буферизацию, то вашему бэкенду придётся как-то работать с медленными клиентами (включая обработку атак Slow POST/ Slow Read);
          • также посредством заголовка X-Accel-Buffering можно реализовать управляемую приложением буферизацию ответов.

          Что бы вы ни выбрали, не забудьте протестировать это на TTFB и TTLB. Как уже упоминалось, буферизация может повлиять на количество операций ввода/ вывода и даже использование бэкенда, так что отслеживайте и эти моменты.


          TLS


          Теперь поговорим о высокоуровневых аспектах TLS и уменьшения задержки, которые можно реализовать с помощью правильной конфигурации nginx. Большинство оптимизаций, которые я буду упоминать, описаны в разделе Optimizing for TLS High Performance Browser Networking и в выступлении Making HTTPS Fast(er) на nginx.conf 2014. Настройки, описываемые в этой части поста, повлияют на производительность и безопасность вашего веб-сервера, так что, если вы в них не уверены, обратитесь к руководству Mozilla’s Server Side TLS Guide и/ или проконсультируйтесь со своими коллегами, отвечающими за безопасность.


          Для проверки результатов оптимизаций можно использовать:



          Возобновление сессии


          Как любят говорить DBA, «самый быстрый запрос – тот, который вы не делали». Это касается и TLS: можно уменьшить задержку с помощью одного RTT, если вы кэшируете результаты «рукопожатия». Это можно сделать двумя способами:


          • попросить клиент хранить все параметры сессии (в подписанном и зашифрованном виде) и отправлять их вам во время следующего «рукопожатия» (как и куки). На стороне nginx это конфигурируется посредством директивы ssl_session_tickets. Это не приводит к потреблению памяти на сервере, но имеет ряд недостатков:

          1) понадобится инфраструктура для создания, ротации и распределения случайных ключей шифрования/ подписи для TLS-сессий. Помните, что не следует: 1) использовать управление ресурсами для хранения тикет-ключей; 2) генерировать эти ключи на основе каких-то неэфемерных вещей вроде даты или сертификата;
          2) PFS будет зависеть не от конкретной сессии, а от TLS-тикет-ключа, так что если злоумышленник завладеет тикет-ключом, то сможет расшифровать любой перехваченный трафик в течение всего действия тикета;
          3) ваше шифрование будет ограничено размером тикет-ключа. Не имеет смысла использовать AES-256, если вы применяете 128-битный тикет-ключ. Nginx поддерживает 128-битные и 256-битные ключи;
          4) не все клиенты поддерживают тикет-ключи (хотя они поддерживаются всеми современными браузерами);


          • хранить параметры TLS-сессии на сервере и отдавать клиенту только ссылку (ID). Это делается посредством директивы ssl_session_cache. Преимущество подхода в том, что PFS сохраняется между сессиями, а видов возможных атак становится гораздо меньше. Хотя у тикет-ключей тоже есть недостатки:

          1) они потребляют на сервере ~256 байтов памяти на каждую сессию, так что вы не сможете хранить слишком много ключей слишком долго;
          2) нет простого способа использовать их одновременно несколькими серверами. Так что вам понадобится ещё и балансировщик нагрузки, который будет отправлять тот же клиент на тот же сервер, чтобы сохранить локальн


          Метки:  

          [Перевод] Оптимизация веб-серверов для повышения пропускной способности и уменьшения задержки

          Вторник, 19 Сентября 2017 г. 12:52 + в цитатник
          max_m сегодня в 12:52 Администрирование

          Оптимизация веб-серверов для повышения пропускной способности и уменьшения задержки

          • Перевод


          Привет! Меня зовут Макс Матюхин, я работаю в SRV-команде Badoo. Мы в Badoo не только активно пишем посты в свой блог, но и внимательно читаем блоги наших коллег из других компаний. Недавно ребята из Dropbox опубликовали шикарный пост о различных способах оптимизации серверных приложений: начиная с железа и заканчивая уровнем приложения. Его автор – Алексей Иванов – дал огромное количество советов и ссылок на дополнительные источники информации. К сожалению, у Dropbox нет блога на Хабре, поэтому я решил перевести этот пост для наших читателей.


          Это расширенная версия моего выступления на nginx.conf 2017 в сентябре этого года. В качестве старшего инженера по контролю качестве (SRE) в команде Dropbox Traffic я отвечаю за нашу сеть Edge: её надёжность, производительность и эффективность. Это proxy-tier-сеть, построенная на базе nginx и предназначенная как для обработки чувствительных к задержке метаданных, так и для передачи данных с высокой пропускной способностью. В системе, обрабатывающей десятки гигабитов в секунду и одновременно – десятки тысяч транзакций, чувствительных к задержкам, используются различные оптимизации эффективности и производительности: начиная с драйверов и прерываний, сквозь ядро и TCP/ IP-стек, и заканчивая библиотеками и настройками уровня приложения.


          Пояснения


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


          Это не пост о производительности Linux (хотя я и буду часто ссылаться на bcc, eBPF и perf) и не исчерпывающее руководство по использованию инструментов профилирования производительности (если вы хотите узнать о них больше, почитайте блог Брендана Грегга).


          Это также не пост о производительности браузеров. Я буду упоминать о клиентской производительности применительно к оптимизациям задержек, но очень коротко. Хотите узнать больше – прочитайте статью High Performance Browser Networking Ильи Григорика.


          И это не компиляция на тему лучших методик TLS. Хотя я и буду упоминать TLS-библиотеки и их настройки, вы и ваша команда обеспечения безопасности должны самостоятельно оценивать их производительность и влияние на безопасность. Чтобы узнать, насколько ваши серверы отвечают набору лучших методик, можете воспользоваться Qualys SSL Test. Если хотите узнать больше о TLS в целом, подпишитесь на рассылку Feisty Duck Bulletproof TLS Newsletter.


          Структура поста


          Мы рассмотрим оптимизации эффективности/ производительности на разных уровнях системы. Начнём с самого нижнего, аппаратно-драйверного, уровня: эти настройки можно применить практически к любому высоконагруженному серверу. Затем я перейду к ядру Linux и его TCP/IP-стеку: можете покрутить эти ручки на своих ящиках, активно использующих TCP. Наконец, мы обсудим настройки на уровне библиотек и приложений, которые по большей части применимы ко многим веб-серверам и в частности к nginx.


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


          Оборудование


          Процессор


          Для хорошей производительности асимметричного RSA/EC выбирайте процессоры как минимум с поддержкой AVX2 (avx2 в /proc/cpuinfo) и желательно подходящие для вычислений с большими целыми числами (bmi и adx). Для симметричного шифрования выбирайте AES-NI для AES-шифров и AVX-512 – для ChaCha+Poly. У Intel есть сравнение производительности разных поколений процессоров с OpenSSL 1.0.2, где рассматривается влияние этих аппаратных оптимизаций.


          Для задач, где важен уровень задержки, вроде роутинга рекомендуется уменьшить количество NUMA-узлов и отключить Hyper-Threading. Задачи, требующие высокой пропускной способности, эффективнее выполняются при большем количестве ядер с использованием Hyper-Threading (если только нет привязки к кэшу), и в целом NUMA не играет для них особой роли.


          Если выбираете среди продукции Intel, то смотрите на процессоры с архитектурой Haswell/ Broadwell, а лучше Skylake. У AMD впечатляющую производительность демонстрируют EPYC-модели.


          Сетевая карта


          Вам нужно как минимум 10 Гбит, а лучше – 25 Гбит. Если хотите передавать через один сервер с TLS ещё больше, то описанных здесь настроек может быть недостаточно – возможно, придётся сдвинуть TLS-фрейминг на уровень ядра (FreeBSD, Linux).


          Что касается программного уровня, поищите open-source-драйверы с активными списками рассылки и сообществами. Это будет очень важным фактором, если (скорее «когда») вы будете заниматься решением проблем, связанных с драйверами.


          Память


          Эмпирическое правило: задачи, чувствительные к задержке, требуют более быстрой памяти; задачи, чувствительные к пропускной способности, требуют больше памяти.


          Диски


          Всё зависит от ваших требований к буферизации/кэшированию. Если вам нужно много буферизировать или кэшировать, то лучше выбрать SSD-диски. Некоторые даже устанавливают заточенные под флеш файловые системы (обычно log-structured), но они не всегда показывают более высокую производительность по сравнению с обычными ext4/ xfs.
          В любом случае не сгубите свои флеш-накопители, забыв включить TRIM или обновить прошивку.


          Операционные системы: низкий уровень


          Прошивка


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


          Драйверы


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


          Процессор


          Ваш лучший друг — репозиторий ядра (и инструменты, поставляемые с ним). В Ubuntu/ Debian вы можете установить пакет linux-tools с набором утилит, но в этом посте мы будем использовать только cpupower, turbostat и x86_energy_perf_policy. Для проверки связанных с процессором оптимизаций вы можете провести стресс-тестирование своего ПО с помощью любимого генератора нагрузки (например, Yandex.Tank). Вот презентация о лучших методиках нагрузочного тестирования от разработчиков nginx: NGINX Performance testing.


          cpupower


          $ cpupower frequency-info
          ...
            driver: intel_pstate
            ...
            available cpufreq governors: performance powersave
            ...            
            The governor "performance" may decide which speed to use
            ...
            boost state support:
              Supported: yes
              Active: yes

          Проверьте, включён ли Turbo Boost, а если у вас процессор Intel, удостоверьтесь, что система работает с intel_pstate, а не с acpi-cpufreq или pcc-cpufreq. Если вы всё ещё используете acpi-cpufreq, обновите ядро. Если это невозможно, используйте режим performance. При работе с intel_pstate даже режим powersave должен выполняться с хорошей производительностью, но вам придётся проверить это самостоятельно.


          Что касается простоя, чтобы посмотреть, что реально происходит с вашим процессором, вы можете с помощью turbostat напрямую заглянуть в процессорные MSR и извлечь информацию о питании, частоте и так называемых Idle States:


          # turbostat --debug -P
          ... Avg_MHz Busy% ... CPU%c1 CPU%c3 CPU%c6 ... Pkg%pc2 Pkg%pc3 Pkg%pc6 ...

          Здесь вы видите реальную частоту процессора (да, /proc/cpuinfo вам врёт), а также текущее состояние ядра/набора ядер.


          Если даже с драйвером intel_pstate процессор тратит на простой больше времени, чем вы думали, вы можете:


          • переключить регулятор на performance;
          • для повышения производительности настроить x86_energy_perf_policy.

          А для очень чувствительных к задержке задач можно:


          • использовать интерфейс /dev/cpu_dma_latency;
          • для UDP-трафика использовать busy-polling.

          Узнать больше об управлении питанием процессора в целом и P-состояниями в частности можно из презентации Balancing Power and Performance in the Linux Kernel с LinuxCon Europe 2015.


          Привязка к процессору


          Можно ещё больше уменьшить задержку, привязав поток или процесс к CPU. Например, в nginx есть директива worker_cpu_affinity, которая автоматически привязывает каждый процесс веб-сервера к конкретному ядру. Это позволяет исключить миграцию процесса / потока на другое ядро, уменьшить количество промахов кэша и ошибок страниц памяти, а также слегка увеличить количество инструкций в цикле. Всё это можно проверить через perf stat.


          Но процессорная привязка негативно влияет на производительность, поскольку процессам дольше приходится ждать освобождения процессора. Это можно отслеживать с помощь запуска runqlat на одном из ваших PID nginx-воркера:


          usecs               : count     distribution
              0 -> 1          : 819      |                                        |
              2 -> 3          : 58888    |******************************          |
              4 -> 7          : 77984    |****************************************|
              8 -> 15         : 10529    |*****                                   |
             16 -> 31         : 4853     |**                                      |
             ...
           4096 -> 8191       : 34       |                                        |
           8192 -> 16383      : 39       |                                        |
          16384 -> 32767      : 17       |                                        |

          Если заметите длинные хвосты на много миллисекунд, то, вероятно, на серверах выполняется слишком много всего, помимо nginx, и привязка увеличит задержку, а не уменьшит её.


          Память


          Все настройки Memory Management обычно сильно зависят от рабочего процесса, так что могу дать лишь такие рекомендации:



          Современные процессоры представляют собой несколько отдельных процессоров, связанных очень быстрой шиной и совместно использующих различные ресурсы, начиная с кэша L1 на HT-ядрах и заканчивая кэшем L3 применительно к пакетам, памятью и PCIe-соединениями в рамках сокетов. Это и есть NUMA: многочисленные модули исполнения и хранения с быстрой шиной обмена данными.


          Исчерпывающее описание NUMA и её применения содержится в статье Фрэнка Деннемана NUMA Deep Dive Series.


          Короче, вы можете:


          • игнорировать её, отключив в BIOS или выполняя своё ПО под numactl --interleave=all (так вы получите посредственную, но достаточно стабильную производительность);
          • отказаться от неё, используя одноузловые серверы, как это делает Facebook с платформой OCP Yosemite;
          • принять её, оптимизируя размещение процессора/ памяти в пространствах ядра и пользователя.

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


          Для правильного использования NUMA вам нужно рассматривать каждый её узел в качестве отдельного сервера. Проверьте топологию с помощью numactl --hardware:


          $ numactl --hardware
          available: 4 nodes (0-3)
          node 0 cpus: 0 1 2 3 16 17 18 19
          node 0 size: 32149 MB
          node 1 cpus: 4 5 6 7 20 21 22 23
          node 1 size: 32213 MB
          node 2 cpus: 8 9 10 11 24 25 26 27
          node 2 size: 0 MB
          node 3 cpus: 12 13 14 15 28 29 30 31
          node 3 size: 0 MB
          node distances:
          node   0   1   2   3
            0:  10  16  16  16
            1:  16  10  16  16
            2:  16  16  10  16
            3:  16  16  16  10

          Что нужно проверять:


          • количество узлов;
          • объём памяти для каждого узла;
          • количество процессоров для каждого узла;
          • расстояние между узлами.

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


          Это можно проверить с помощью numastat:


          $ numastat -n -c
                            Node 0   Node 1 Node 2 Node 3    Total
                          -------- -------- ------ ------ --------
          Numa_Hit        26833500 11885723      0      0 38719223
          Numa_Miss          18672  8561876      0      0  8580548
          Numa_Foreign     8561876    18672      0      0  8580548
          Interleave_Hit    392066   553771      0      0   945836
          Local_Node       8222745 11507968      0      0 19730712
          Other_Node      18629427  8939632      0      0 27569060

          Также с помощью numastat можно получить статистику использования памяти по каждому узлу в формате /proc/meminfo:


          $ numastat -m -c
                           Node 0 Node 1 Node 2 Node 3 Total
                           ------ ------ ------ ------ -----
          MemTotal          32150  32214      0      0 64363
          MemFree             462   5793      0      0  6255
          MemUsed           31688  26421      0      0 58109
          Active            16021   8588      0      0 24608
          Inactive          13436  16121      0      0 29557
          Active(anon)       1193    970      0      0  2163
          Inactive(anon)      121    108      0      0   229
          Active(file)      14828   7618      0      0 22446
          Inactive(file)    13315  16013      0      0 29327
          ...
          FilePages         28498  23957      0      0 52454
          Mapped              131    130      0      0   261
          AnonPages           962    757      0      0  1718
          Shmem               355    323      0      0   678
          KernelStack          10      5      0      0    16

          Теперь рассмотрим пример более простой топологии.


          $ numactl --hardware
          available: 2 nodes (0-1)
          node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
          node 0 size: 46967 MB
          node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
          node 1 size: 48355 MB

          Поскольку узлы по большей части симметричны, мы можем привязать экземпляр нашего приложения к каждому NUMA-узлу с помощью numactl --cpunodebind=X --membind=X, а затем открыть его на другом порте. Пропускная способность увеличится благодаря использованию обоих узлов и уменьшению задержки за счёт сохранения локальности памяти.


          Проверить эффективность размещения NUMA можно по задержке операций в памяти. Например, с помощью funclatency в BCC измерьте задержку операции, активно использующей память, допустим, memmove.
          Наблюдать за эффективностью на стороне ядра можно с помощью perf stat, отслеживая соответствующие события памяти и планировщика:


          # perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID
          ...
                           1      sched:sched_stick_numa
                           3      sched:sched_move_numa
                          41      sched:sched_swap_numa
                       5,239      migrate:mm_migrate_pages
                      50,161      minor-faults

          Последняя порция связанных с NUMA оптимизаций для сетевых нагрузок с активным использованием сети продиктована тем фактом, что сетевая карта — это PCIe-устройство, а каждое устройство привязано к своему NUMA-узлу; следовательно, у каких-то процессоров задержка при обращении к сети будет меньше. Возможные оптимизации мы обсудим в главе, где будет рассматриваться привязка сетевая карта -> процессор, а пока перейдём к PCI Express.


          PCIe


          Обычно нет нужды углубляться в решение проблем с PCIe, если только не возникает какой-то аппаратный сбой. Однако стоит хотя бы просто создать для своих PCIe-устройств «ширину шины», «скорость шины» и предупреждения RxErr/BadTLP. Это должно сэкономить вам часы на отладку из повреждённого железа или сбойного PCIe-согласования. Для этого можете воспользоваться lspci:


          # lspci -s 0a:00.0 -vvv
          ...
          LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM L1, Exit Latency L0s <2us, L1 <16us
          LnkSta: Speed 8GT/s, Width x8, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
          ...
          Capabilities: [100 v2] Advanced Error Reporting
          UESta:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
          UEMsk:  DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
          UESvrt: DLP+ SDES+ TLP- FCP+ CmpltTO- CmpltAbrt- ...
          CESta:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr-
          CEMsk:  RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr+

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



          Также советую прочитть статью Understanding PCIe Configuration for Maximum Performance, в ней подробнее рассматривается конфигурация PCIe, что может быть полезно при высоких скоростях, когда происходит потеря пакетов между картой и ОС.


          Intel предполагает, что иногда управление питанием PCIe (ASPM) может приводить к большим задержкам, а значит, и к потере большего количества пакетов. Эту функцию можно отключить, введя pcie_aspm=off в командной строке ядра.


          Сетевая карта


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


          Также можете поискать руководства для вашей ОС. Например, в руководстве по настройке сетевой производительности в Linux от Red Hat Enterprise объясняются многие из упомянутых выше оптимизаций. У Cloudflare тоже есть хорошая статья о настройке этой части сетевого стека, хотя по большей части она посвящена ситуациям, когда нужна низкая задержка.


          В ходе оптимизации вашим лучшим другом будет ethtool.
          Примечание: если вы используете достаточно свежее ядро (а вам следует это сделать!), то вы также столкнётесь с некоторыми аспектами вашего пользовательского пространства. Например, для сетевых операций вы, вероятно, захотите использовать более свежие версии пакетов ethtool, iproute2 и, быть может, iptables/nftables.


          Получить ценные сведения о том, что происходит с вашей сетевой картой, можно с помощью ethtool -S:


          $ ethtool -S eth0 | egrep 'miss|over|drop|lost|fifo'
               rx_dropped: 0
               tx_dropped: 0
               port.rx_dropped: 0
               port.tx_dropped_link_down: 0
               port.rx_oversize: 0
               port.arq_overflows: 0

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


          Что касается ядра, то нужно смотреть /proc/interrupts, /proc/softirqs и /proc/net/softnet_stat. Здесь есть два полезных BCC-инструмента: hardirqs и softirqs. Цель вашей оптимизации сети заключается в такой настройке системы, чтобы процессор использовался минимально, а пакеты не терялись.


          Привязка прерываний


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


          • для максимальной пропускной способности можно распределить прерывания по всем NUMA-узлам;
          • для минимизации задержки можно ограничить прерывания одним NUMA-узлом (для этого вам может понадобиться уменьшить количество очередей, чтобы не превысить возможности одного узла (обычно приходится уменьшать вдвое с помощью ethtool -L).

          Как правило, для этого вендоры предоставляют скрипты. Например, у Intel это set_irq_affinity.


          Размеры кольцевого буфера


          Сетевым картам нужно обмениваться информацией с ядром. Обычно это делается через структуру данных, называющуюся «кольцо». Текущий/ максимальный размер этого кольца можно посмотреть с помощью ethtool -g:


          $ ethtool -g eth0
          Ring parameters for eth0:
          Pre-set maximums:
          RX:                4096
          TX:                4096
          Current hardware settings:
          RX:                4096
          TX:                4096

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


          • в более старых ядрах или драйверах без поддержки BQL высокие значения могут относиться к более высокому bufferbloat на TX-стороне;


          • буферы большего размера увеличивают давление кэша, так что, если вы с этим столкнётесь, постарайтесь уменьшить буферы.

          Объединение прерываний


          Этот механизм обеспечивает задержку уведомления ядра о новых событиях за счёт объединения нескольких сообщений в одно прерывание. Текущие настройки можно посмотреть с помощью ethtool -c:


          $ ethtool -c eth0
          Coalesce parameters for eth0:
          ...
          rx-usecs: 50
          tx-usecs: 50

          Также вы можете придерживаться статичных пределов (static limits), жёстко ограничив максимальное количество прерываний в секунду на одно ядро, или положиться на автоматическую аппаратную подстройку частоты прерываний в зависимости от пропускной способности.


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


          Разгрузки


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


          Все возможные разгрузки можно просмотреть с помощью ethtool -k:


          $ ethtool -k eth0
          Features for eth0:
          ...
          tcp-segmentation-offload: on
          generic-segmentation-offload: on
          generic-receive-offload: on
          large-receive-offload: off [fixed]

          Все ненастраиваемые разгрузки помечены суффиксом [fixed]. О них можно долго рассказывать, но я только приведу несколько эмпирических правил:


          • не включайте LRO – вместо этого используйте GRO;
          • будьте осторожны с TSO, поскольку это сильно зависит от качества ваших драйверов/ прошивки;
          • не включайте TSO/ GSO на старых ядрах, потому что это может привести к чрезмерному bufferbloat.

          Регулирование пакетов


          Все современные сетевые карты оптимизированы под многопроцессорные системы, поэтому они распределяют пакеты по виртуальным очередям (обычно по одной на процессор). Когда это выполняется аппаратно, то называется RSS; когда за балансировку пакетов между процессорами отвечает ОС, это называется RPS (TX-эквивалент называется XPS). Если ОС пытается регулировать потоки к процессорам, которые в данный момент обрабатывают этот сокет, это называется RFS. А когда этим занимается железо, это называется «ускоренный RFS» или aRFS.


          Вот несколько хороших методик:


          • если вы используете новое оборудование 25 Гбит+, то в нём, вероятно, есть достаточно очередей и огромная таблица косвенной переадресации (indirection table), чтобы можно было применять RSS между всеми ядрами (некоторые более старые карты могут использовать только первые 16 процессоров);
          • можете попробовать включить RPS, если:

          1) у вас больше процессоров, чем аппаратных очередей, и вы хотите пожертвовать задержкой в пользу пропускной способности;
          2) вы используете внутреннее туннелирование (например, GRE/ IPinIP), при котором сетевая карта не может применять RSS;


          • не включайте RPS, если у вас достаточно старый процессор, не имеющий x2APIC;
          • привязка каждого процессора к собственной TX-очереди посредством XPS – в целом хорошая идея;
          • эффективность RFS во многом зависит от вашей рабочей нагрузки, а также от того, примените ли вы процессорную привязку.

          Flow Director и ATR


          Включённый Flow Director (или fdir в терминологии Intel) по умолчанию оперирует в режиме Application Targeting Routing, при котором реализуется aRFS посредством семплирования пакетов и регулирования потоков в процессорное ядро, где они, по-видимому, обрабатываются. Статистику можно посмотреть с помощью ethtool -S:$ ethtool -S eth0 | egrep ‘fdir’ port.fdir_flush_cnt: 0 …


          Хотя Intel заявляет, что fdir в некоторых случаях увеличивает производительность, результаты одного исследования говорят о том, что это может также привести к переупорядочиванию 1% пакетов, что может довольно негативно сказаться на производительности TCP. Поэтому протестируйте самостоятельно и посмотрите, будет ли Flow Director полезен при вашей рабочей нагрузке, проверяя счётчик TCPOFOQueue.


          Операционные системы: сетевой стек


          Существует огромное количество книг, видео и руководств по настройке сетевого стека Linux, в которых растиражирован «карго-культ sysctl.conf». И хотя свежие версии ядра уже не требуют такого объёма настройки, как десять лет назад, а большинство новых TCP/ IP-свойств по умолчанию включены и хорошо настроены, люди продолжают копипастить свои старые sysctls.conf, которые они использовали для настройки ядер версий 2.6.18/ 2.6.32.


          Для проверки эффективности сетевых оптимизаций сделайте следующее:


          • с помощью /proc/net/snmp and /proc/net/netstat соберите TCP-метрики в рамках системы;
          • добавьте метрики подключения, собранные с помощью ss -n --extended --info или при вызове внутри сервера getsockopt(``[TCP_INFO]``)/getsockopt(``[TCP_CC_INFO]``);
          • соберите tcptrace(1)`ы образцов TCP-потоков;
          • проанализируйте RUM-метрики приложения/ браузера.

          В качестве источников информации о сетевых оптимизациях я обычно использую выступления специалистов по CDN, потому что, как правило, они знают, что делают. Например, Fastly on LinuxCon Australia. Полезно также послушать, что говорят разработчики ядра Linux, к примеру, на NetDevConf и Netconf.


          Также стоит упомянуть про подробные материалы от PackageCloud по сетевому стеку Linux, особенно в свете того, что они сделали акцент на мониторинг, а не на «слепую» настройку:



          И позвольте дать совет напоследок: обновите ядро ОС! Существует множество новых сетевых улучшений, и я говорю даже не об IW10 (который 2010) – я говорю о таких новинках, как автоматический выбор размера TSO, FQ, pacing, TLP и RACK. В качестве бонуса от апгрейда вы получите ряд улучшений масштабируемости, например, убранный кэш рутинга, неблокирующие сокеты прослушивания, SO_REUSEPORT и многое другое.


          Обзор


          Из недавних документов по работе с сетью в Linux особенно выделяется Making Linux TCP Fast. В нём на четырёх страницах собраны улучшения в ядре ОС за много лет. TCP-стек на стороне отправителя разбит на функциональные части:



          Fair queueing и pacing


          Fair queueing отвечает за соблюдение «справедливости» и уменьшает блокировку очереди между TCP-потоками, что положительно сказывается на частоте отбрасывания пакетов. Pacing, в свою очередь, равномерно распределяет пакеты во времени с частотой, определяемой Congestion Control, что ещё больше уменьшает долю потерянных пакетов, тем самым увеличивая пропускную способность.


          Попутно хочу заметить, что fair queueing и pacing доступны в Linux посредством fq qdisc. Обе фичи требуются для BBR (впрочем, уже нет), но их можно использовать и с CUBIC, добиваясь 15–20%-ного снижения потери пакетов, а значит, и повышения пропускной способности в алгоритмах управления перегрузками (loss-based CCs). Только не используйте их на старых ядрах (<3.19), поскольку вы станете регулировать обычные ACKs и сломаете аплоад/ RPCs.


          Автоматический выбор размера TSO и TSQ


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


          Управление перегрузками


          CC-алгоритмы сами по себе – объёмная тема, и в последние годы о них было много разговоров. Что-то из этого вылилось в код: tcp_cdg (CAIA), tcp_nv (Facebook) и tcp_bbr (Google). Мы не будем углубляться в их устройство, скажу лишь, что индикация о перегрузке во всех них основана больше на увеличении отсрочки (delay), чем на отбрасывании пакетов.


          BBR – один из наиболее задокументированных, протестированных и практичных из всех новых алгоритмов управления перегрузками. На основании доли доставленных пакетов создаётся модель сетевого пути, а затем для увеличения ширины пропускания и минимизации RTT выполняются управляющие циклы. Это именно то, что мы ищем в нашем прокси-стеке.


          Предварительные результаты экспериментов с BBR на наших Edge PoP показали увеличение скорости скачивания файлов:



          Шестичасовой эксперимент с TCP BBR в Tokyo PoP: ось x — время, ось y — скорость скачивания на клиенте


          Увеличение скорости наблюдалось по всем перцентилям. При изменениях бэкенда такого не происходит — обычно положительный результат наблюдается только p90+ пользователей (у которых самое быстрое интернет-подключение), поскольку мы считаем, что у всех остальных уже ограничена полоса пропускания. Настройки на сетевом уровне вроде изменения управления перегрузками или включения FQ/ pacing демонстрируют, что у пользователей ограничена не полоса пропускания, а, я бы сказал, присутствует «ограниченность TCP».


          Если вы хотите больше узнать о BBR, то у APNIC есть хороший обзор для новичков (и сравнение с loss-based-управлением перегрузками). Более глубокую информацию можно извлечь из архивов почтовой рассылки bbr-dev (там сверху закреплено множество полезных ссылок). Если вас в целом интересует тема управления перегрузками, то можете понаблюдать за активностью Internet Congestion Control Research Group.


          ACK-обработка и обнаружение пропадания пакетов


          Теперь поговорим об обнаружении пропадания пакетов (loss detection). Снова упомяну про важность использования свежей версии ядра ОС. В TCP постоянно добавляются новые эвристики вроде TLP и RACK, а старые (наподобие FACK и ER) убираются. Нововведения работают по умолчанию, так что вам не придётся настраивать систему после апгрейда.


          Приоритизация пользовательского пространства и HOL


          API сокета пользовательского пространства (userspace socket API) предоставляют механизм явной буферизации, и после отправки чанков их уже невозможно перегруппировать. Поэтому при использовании мультиплексирования (например, в HTTP/2) это может привести к Head-of-Line блокировке и инверсии h2-приоритетов. Для решения этой проблемы были разработаны опция сокета и соответствующая опция sysctl net.ipv4.tcp_notsent_lowat. Они позволяют настраивать границы, в пределах которых сокет считает себя доступным для записи (то есть epoll в вашем приложении будет врать). Это может решить проблемы с HTTP/2-приоритизацией, но при этом плохо повлиять на пропускную способность, так что рекомендую проверить самостоятельно.


          Sysctls


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



          Лучше сделайте вот что:


          • net.ipv4.tcp_slow_start_after_idle=0: главная проблема с медленным стартом (slow start) после простоя заключается в том, что «простой» определяется как один RTO, а этого слишком мало;
          • net.ipv4.tcp_mtu_probing=1: полезно при наличии ICMP-«чёрных дыр» между вами и клиентами (наверняка они есть );
          • net.ipv4.tcp_rmem, net.ipv4.tcp_wmem: нужно настроить так, чтобы подходило к BDP; только не забудьте, что больше – не значит лучше;
          • echo 2 > /sys/module/tcp_cubic/parameters/hystart_detect: если вы используете FQ+CUBIC, то это может помочь решить проблему слишком раннего выхода tcp_cubic из медленного старта.

          Стоит упомянуть, что существует RFC-черновик (хотя и подзаброшенный) от Дэниела Штенберга, автора curl, под названием TCP Tuning for HTTP, в котором сделана попытка собрать все системные настройки, которые могут быть полезны для HTTP.


          Уровень приложения: средний уровень


          Инструментарий


          Как и в случае с ядром ОС, пользовательское пространство крайне важно актуализировать. Начните с обновления своего инструментария, например, можете упаковать более свежие версии perf, bcc и так далее.


          После этого можно приступать к настройке и отслеживанию поведения системы. В этой части поста мы будем по большей части опираться на профилирование процессора с помощью perf top, on-CPU flame-графики и ad hoc-гистрограммы из funclatency в bcc.



          Инструментарий для компилирования


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


          Помимо производительности, новые компиляторы могут похвастаться и новыми свойствами обеспечения безопасности (например, -fstack-protector-strong или SafeStack). Также современный инструментарий будет полезен, если вы хотите прогонять тесты через бинарные файлы, скомпилированные с использованием санитайзеров (например, AddressSanitizer и других).


          Системные библиотеки


          Рекомендую обновить системные библиотеки вроде glibc, иначе вы можете не получить свежих оптимизаций низкоуровневых функций из -lc, -lm, -lrt и так далее. Стандартное предупреждение: тестируйте самостоятельно, поскольку могут встречаться неожиданные регрессии.


          zlib


          Обычно за компрессию отвечает веб-сервер. В зависимости от объёма данных, проходящих через прокси, вы можете встретить упоминание zlib в perf top, например:


          # perf top
          ...
             8.88%  nginx        [.] longest_match
             8.29%  nginx        [.] deflate_slow
             1.90%  nginx        [.] compress_block

          Это можно оптимизировать на самом низком уровне: Intel и Cloudflare, как и отдельный проект zlib-ng, имеют собственные zlib-форки, обеспечивающие более высокую производительность за счёт использования новых наборов инструкций.


          malloc


          При обсуждении оптимизаций до этого момента мы по большей части ориентировались на процессор. Теперь же поговорим о памяти. Если вы активно используете Lua с FFI или тяжёлые сторонние модули, которые самостоятельно управляют памятью, то могли заметить рост потребления памяти из-за фрагментации. Эту проблему можно попытаться решить переключением на jemalloc или tcmalloc.


          Использование кастомного malloc даёт следующие преимущества:



          Если в конфигурации nginx вы используете многочисленные сложные регулярные выражения или активно применяете Lua, то могли встретить в perf top упоминание PCRE. Это можно оптимизировать, скомпилировав PCRE с JIT, а также включив её в nginx посредством pcre_jit on;.


          Результат оптимизации можно проверить на flame-графиках или с помощью funclatency:


          # funclatency /srv/nginx-bazel/sbin/nginx:ngx_http_regex_exec -u
          ...
               usecs               : count     distribution
                   0 -> 1          : 1159     |**********                              |
                   2 -> 3          : 4468     |****************************************|
                   4 -> 7          : 622      |*****                                   |
                   8 -> 15         : 610      |*****                                   |
                  16 -> 31         : 209      |*                                       |
                  32 -> 63         : 91       |                                        |

          TLS


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


          Сегодня первое, что вам нужно решить, — какую TLS-библиотеку вы будете использовать: Vanilla OpenSSL, OpenBSD’s LibreSSL или BoringSSL от Google. Определившись, вам нужно правильно её собрать: к примеру, у OpenSSL есть куча сборочных эвристик, позволяющих использовать оптимизации на базе сборочного окружения; у BoringSSL есть детерминистские сборки, но они более консервативны и по умолчанию просто отключают некоторые оптимизации. В любом случае, здесь вы наконец-то ощутите выгоду от выбора современного процессора: большинство TLS-библиотек могут использовать всё, от AES-NI и SSE до ADX и AVX-512. Можете воспользоваться встроенными тестами производительности. Например, в случае с BoringSSL это bssl speed.


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


          Асимметричное шифрование


          Если у вас “front”-сервис (сервис к которому пользователи подключаются напрямую), то вы могли столкнуться со значительным количеством TLS-«рукопожатий», а значит, немалая доля ресурсов вашего процессора тратится на асимметричное шифрование, которое необходимо оптимизировать.


          Для оптимизации использования серверного процессора можете переключиться на сертификаты ECDSA, которые в десять раз быстрее, чем RSA. К тому же они значительно меньше, что может ускорить «рукопожатия» при наличии потерь пакетов. Но ECDSA сильно зависят от качества генератора случайных чисел в вашей системе, так что если вы используете OpenSSL, то удостоверьтесь, что у вас достаточно энтропии (в случае с BoringSSL об этом можно не волноваться).


          И ещё раз напоминаю, что больше – не значит лучше, то есть использование сертификатов 4096 RSA ухудшит производительность в десять раз:


          $ bssl speed
          Did 1517 RSA 2048 signing ... (1507.3 ops/sec)
          Did 160 RSA 4096 signing ...  (153.4 ops/sec)

          Но меньше тоже не значит лучше: при использовании малораспространённого поля p-224 для ECDSA вы получите 60%-ное снижение производительности по сравнению с обычным p-256:


          $ bssl speed
          Did 7056 ECDSA P-224 signing ...  (6831.1 ops/sec)
          Did 17000 ECDSA P-256 signing ... (16885.3 ops/sec)

          Эмпирическое правило: самое распространённое шифрование обычно самое оптимизированное.


          При запуске правильно оптимизированной библиотеки на основе OpenTLS, использующей сертификаты RSA, в своём perf top вы должны увидеть следующие трейсы: процессоры, использующие AVX2, а не ADX (например, с архитектурой Haswell), должны использовать кодовый путь AVX2:


            6.42%  nginx                [.] rsaz_1024_sqr_avx2
            1.61%  nginx                [.] rsaz_1024_mul_avx2

          Более новые модели должны использовать обычный алгоритм Монтгомери с кодовым путём ADX:


            7.08%  nginx                [.] sqrx8x_internal
            2.30%  nginx                [.] mulx4x_internal

          Симметричное шифрование


          Если у вас много массовых передач данных вроде видео, фото и прочих файлов, то можете начать отслеживать в данных профилировщика упоминания о симметричном шифровании. Тогда просто удостоверьтесь, что ваш процессор поддерживает AES-NI и что вы настроили на сервере применение шифров AES-GCM. При правильно настроенном оборудовании в perf top должно выдаваться:


            8.47%  nginx                [.] aesni_ctr32_ghash_6x

          Но заниматься шифрованием/ дешифрованием будут не только ваши серверы, но и клиенты, причём у них априори гораздо более слабые процессоры. Без аппаратного ускорения это может быть достаточно сложной операцией, поэтому позаботьтесь о выборе алгоритма, который работает быстро без аппаратных технологий ускорения работы с шифрованием, например, ChaCha20-Poly1305. Это снизит TTLB для части мобильных клиентов.


          В BoringSSL из коробки поддерживается ChaCha20-Poly1305, а в OpenSSL 1.0.2 можете использовать патчи Cloudflare. BoringSSL также поддерживает «шифрогруппы равного предпочтения», так что можете использовать следующую конфигурацию, которая позволит клиентам решать, какие шифры использовать, отталкиваясь от своих аппаратных возможностей (бесстыдно украдено из cloudflare/sslconfig):


          ssl_ciphers '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES';
          ssl_prefer_server_ciphers on;

          Уровень приложения: высокоуровневые оптимизации


          Для анализа эффективности ваших оптимизаций на этом уровне вам нужно собирать RUM-данные. В браузерах можно применять API Navigation Timing и Resource Timing. Ваши главные метрики — TTFB и TTV/ TTI. Вам сильно упростит итерирование, если эти данные будут представлены в форматах, удобных для составления запросов и графиков.


          Компрессия


          В nginx компрессия начинается с файла mime.types, определяющего соответствие между расширением файла и MIME-типом. Затем вам нужно определить, какой тип вы хотите передавать компрессору, с, например, gzip_types. Если хотите завершить этот список, то для автоматического генерирования mime.types с добавлением compressible == true to gzip_types можете воспользоваться mime-db.


          Включая gzip, имейте в виду:


          • это увеличивает потребление памяти (проблема решается путём ограничения gzip_buffers);
          • вследствие буферизации это увеличивает TTFB (проблема решается с помощью gzip_no_buffer).

          Отмечу, что HTTP-компрессия не ограничивается одним gzip: в nginx есть сторонний модуль ngx_brotli, который способен сжимать на 30% лучше, чем gzip.


          Что касается настроек сжатия, давайте рассмотрим два отдельных случая: статичные и динамические данные.


          В случае со статичными данными можно архивировать коэффициенты сжатия с помощью предварительной компрессии статичных ресурсов, сделав эту процедуру частью процесса сборки. Для gzip и brotli это подробно рассмотрено в посте Deploying Brotli for static content.


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


          Буферизация внутри прокси может сильно влиять на производительность веб-сервера, особенно с учётом задержки. В прокси-модуле nginx есть разные настройки буферизации, которые можно регулировать в зависимости от местонахождения буферов и каждая из которых полезна в определённых случаях. С помощью proxy_request_buffering и proxy_buffering можно отдельно управлять буферизацией в обоих направлениях. Если включена буферизация, то верхняя граница потребления памяти определяется с помощью client_body_buffer_size и proxy_buffers, и по достижении этой границы запрос/ ответ будут буферизоваться на диске. Для ответов это можно отключить, присвоив proxy_max_temp_file_size значение 0.


          Наиболее распространённые примеры использования:


          • буферизация запроса/ ответа до определённого предела — в памяти, а затем сбрасывание на диск. Если включена буферизация запросов, вы можете отправить запрос на бэкенд только после его полного получения. А если включена буферизация ответов, вы можете мгновенно освободить поток выполнения бэкенда, как только тот будет готов ответить. При таком подходе улучшаются пропускная способность и защита бэкенда, но при этом растут задержка, потребление памяти и количество операций ввода/ вывода (хотя, если вы используете SSD, это не будет особой проблемой);
          • буферизация отключена. Это делается при наличии маршрутов, чувствительных к уровню задержки, особенно в случае со стримингом. Но если вы отключите буферизацию, то вашему бэкенду придётся как-то работать с медленными клиентами (включая обработку атак Slow POST/ Slow Read);
          • также посредством заголовка X-Accel-Buffering можно реализовать управляемую приложением буферизацию ответов.

          Что бы вы ни выбрали, не забудьте протестировать это на TTFB и TTLB. Как уже упоминалось, буферизация может повлиять на количество операций ввода/ вывода и даже использование бэкенда, так что отслеживайте и эти моменты.


          TLS


          Теперь поговорим о высокоуровневых аспектах TLS и уменьшения задержки, которые можно реализовать с помощью правильной конфигурации nginx. Большинство оптимизаций, которые я буду упоминать, описаны в разделе Optimizing for TLS High Performance Browser Networking и в выступлении Making HTTPS Fast(er) на nginx.conf 2014. Настройки, описываемые в этой части поста, повлияют на производительность и безопасность вашего веб-сервера, так что, если вы в них не уверены, обратитесь к руководству Mozilla’s Server Side TLS Guide и/ или проконсультируйтесь со своими коллегами, отвечающими за безопасность.


          Для проверки результатов оптимизаций можно использовать:



          Возобновление сессии


          Как любят говорить DBA, «самый быстрый запрос – тот, который вы не делали». Это касается и TLS: можно уменьшить задержку с помощью одного RTT, если вы кэшируете результаты «рукопожатия». Это можно сделать двумя способами:


          • попросить клиент хранить все параметры сессии (в подписанном и зашифрованном виде) и отправлять их вам во время следующего «рукопожатия» (как и куки). На стороне nginx это конфигурируется посредством директивы ssl_session_tickets. Это не приводит к потреблению памяти на сервере, но имеет ряд недостатков:

          1) понадобится инфраструктура для создания, ротации и распределения случайных ключей шифрования/ подписи для TLS-сессий. Помните, что не следует: 1) использовать управление ресурсами для хранения тикет-ключей; 2) генерировать эти ключи на основе каких-то неэфемерных вещей вроде даты или сертификата;
          2) PFS будет зависеть не от конкретной сессии, а от TLS-тикет-ключа, так что если злоумышленник завладеет тикет-ключом, то сможет расшифровать любой перехваченный трафик в течение всего действия тикета;
          3) ваше шифрование будет ограничено размером тикет-ключа. Не имеет смысла использовать AES-256, если вы применяете 128-битный тикет-ключ. Nginx поддерживает 128-битные и 256-битные ключи;
          4) не все клиенты поддерживают тикет-ключи (хотя они поддерживаются всеми современными браузерами);


          • хранить параметры TLS-сессии на сервере и отдавать клиенту только ссылку (ID). Это делается посредством директивы ssl_session_cache. Преимущество подхода в том, что PFS сохраняется между сессиями, а видов возможных атак становится гораздо меньше. Хотя у тикет-ключей тоже есть недостатки:

          1) они потребляют на сервере ~256 байтов памяти на каждую сессию, так что вы не сможете хранить слишком много ключей слишком долго;
          2) нет простого способа использовать их одновременно несколькими серверами. Так что вам понадобится ещё и балансировщик нагрузки, который будет отправлять тот же клиент на тот же сервер, чтобы сохранить локальн


          Метки:  

          Тайминговая атака на Node.js — когда время работает против вас

          Вторник, 19 Сентября 2017 г. 11:17 + в цитатник

          Метки:  

          Тайминговая атака на Node.js — когда время работает против вас

          Вторник, 19 Сентября 2017 г. 11:17 + в цитатник

          Метки:  

          Трюки в Chrome DevTools

          Вторник, 19 Сентября 2017 г. 10:03 + в цитатник
          SSul сегодня в 10:03 Разработка

          Трюки в Chrome DevTools

          • Tutorial
          Перейдем к самому интересному — подборке самых полезных и интересных, на наш взгляд, трюков и скрытых возможностей Chrome DevTools.

          Верстка.


          1. Инспектируем анимацию.


          Меню анимаций в DevTools позволит вам замедлить все анимации на странице или подвигать “руками” конкретную анимацию.
          image

          2. Экспериментируем с цветами


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

          3. Редактируем любой текст на странице


          Допустим у вас есть текстовый блок на страницы и вы хотите узнать, как он будет выглядеть, если текст в нем изменится. Переключите документ в режим дизайна! Наберите в консоле document.designMode = 'on', аналогично работает document.body.contentEditable = true. После этого вы сможете редактировать все элементы имеющие текстовый контент.
          image

          4. Просмотр отрендеренных шрифтов


          Иногда очень сложно сказать, какой шрифт на самом деле был отрендерен. В нижней части Computed вкладки панели инструментов, вы можете увидеть какой шрифт используется, даже если его названия нет в списке font-family.
          image

          5. Принудительные псевдоклассы для элементов


          DevTools имеет функцию, которая имитирует применение псевдоклассов CSS, например такие как :hover и :focus на элементах, что упрощает их стилизацию. Она доступна из редактора CSS.
          image

          6. Изменение формата цвета


          Используйте Shift + Клик на предварительном просмотре цвета, чтобы изменить формат: rgb, hsl и hex.
          image

          7. Редактор кривых безье для анимации.


          Вы можете легко изменить временные функции используя DevTools (например, значение свойства для animation-timing-function CSS свойств).
          image

          Возможности консоли.


          1. Вывести HTML элемент в представлении JS объекта


          При чтении HTML, браузер генерирует DOM-модель. Если необходимо вывести элемент именно в виде JS объекта в консоль, проще всего для этого воспользоваться методом console.dir().
          image

          2. Группировка сообщений


          Иногда бывает полезно сгруппировать логи для упрощения работы с ними и меньшего засорения консоли. Для этого существуют методы console.group(), console.groupCollapsed() и console.groupEnd().
          function name(obj) {
            console.group('name');
            console.log('first: ', obj.first);
            console.log('middle: ', obj.middle);
            console.log('last: ', obj.last);
            console.groupEnd();
          }
          name({"first":"Wile","middle":"E","last":"Coyote"});
          

          image

          3.1. Вывод значений переменных в виде таблиц


          Иногда намного удобнее и нагляднее работать с массивами или объектами в виде таблицы, а не в виде стандартного иерархического представления. Для вывода данных в виде таблице существует метод console.table().
          function Person(firstName, lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
          }
          var family = {};
          family.mother = new Person("Jane", "Smith");
          family.father = new Person("John", "Smith");
          family.daughter = new Person("Emily", "Smith");
          console.table(family);
          

          image

          3.2 keys(object) и values(object)


          Keys() — возвращает массив имён свойств объекта.
          Values() — возвращает массив, содержащий значения всех свойств указанного объекта.
          image

          4. Логирование времени выполнения кода


          Представьте, что у вас есть две функции которые делают одно и тоже но их реализация различна. Как понять какая из них работает быстрее? Можно воспользоваться методами console.time() и console.timeEnd().
          image

          5. Профилирование


          Помимо замера времени можно профилировать Ваш код и вывести стек профилирования, который подробно показывает, где и сколько времени потратил браузер.
          console.profile();
          // Some code to execute
          console.profileEnd();

          image

          6.1. $(selector)


          Если вы знакомы с jQuery, то знаете о возможности конструкций вроде $(‘.class’) и $(‘#id’). Консоль разработчика обладает похожей функциональностью. Здесь «$» отношения к jQuery не имеет, хотя делает, то же самое. Это – сокращение для функции document.querySelector(), возвращает ссылку на первый элемент DOM с указанным CSS-селектором. При этом, если в документе доступна jQuery, её «$» перекроет функционал консоли.

          6.2. $$(selector)


          Возвращает массив элементов, содержащих указанный селектор. Эта команда эквивалентна вызову document.querySelectorAll().
          image

          7. clear(), copy(object) и inspect(object/function)


          Clear() — очистка всех записей в консоли.
          Copy() — копирование в буфер обмена строкового представления указанного объекта.
          Inspect() — открывает и выбирает указанный элемент или объект в соответствующей панели: Elements или Profiles.

          Прочее.


          1. Продвинутая кнопка перезагрузки


          (Работает только при открытом DevTools и только в браузере Google Chrome!)
          По долгому нажатию на кнопку «Обновить страницу» (либо по правому клику) мы открываем специальное меню, которое предоставляет нам выбор:
          • Обычная перезагрузка (обновляются просроченные ресурсы).
          • Аппаратная перезагрузка (принудительная загрузка всех ресурсов сайта).
          • Очистка кэша и аппаратная перезагрузка.

          image

          2. Форматирование минифицированых исходников


          Инструменты разработчика Chrome имеют встроенный «прихорашиватель» минимизированных исходных кодов к удобочитаемому виду. Кнопка находится в левом нижнем углу открытого в данный момент файла во вкладке Sources.
          image

          3. Отображение shadow DOM


          Такие элементы как поля ввода и кнопки, браузеры, создают из других базовых элементов которые обычно скрыты. Тем не менее, вы можете перейти Settings -> General и включить Show user agent shadow DOM, для отображения этих базовых элементов во вкладке Elements. Это даст вам возможность оформлять их по отдельности.
          image

          4. Фильтрация и поиск


          При работе с DOM, CSS, списком запросов на вкладке Network и т.д. зачастую мы видим перед собой большой список элементов, селекторов, свойств и так далее, в котором бывает сложно быстро найти интересующее нас значение. В таких случаях не стоит забывать про использование фильтрации и поиска которое присутствует на всех вкладках. Благодаря фильтрации мы будем отсеивать все варианты кроме подходящих под условия, а поиск позволит Вам быстро найти то что нужно, если Вы знаете ключевые «слова» для поиска. Как правило поле поиска скрыто и вызывается комбинацией Ctrl + F.
          image

          5. Come to the Dark Side


          Просто потому что темная сторона круче (:
          А если серьезно, темная тема убережет ваши глаза от лишнего напряжения, если основной цвет разрабатываемой или отлаживаемой страницы темный, как в моем случае. Включить можно вначале страницы настроек DevTools.
          image

          Используемые ресурсы:
          developers.google.com/web/tools/chrome-devtools
          habrahabr.ru
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/336710/


          Метки:  

          Трюки в Chrome DevTools

          Вторник, 19 Сентября 2017 г. 10:03 + в цитатник
          SSul сегодня в 10:03 Разработка

          Трюки в Chrome DevTools

          • Tutorial
          Перейдем к самому интересному — подборке самых полезных и интересных, на наш взгляд, трюков и скрытых возможностей Chrome DevTools.

          Верстка.


          1. Инспектируем анимацию.


          Меню анимаций в DevTools позволит вам замедлить все анимации на странице или подвигать “руками” конкретную анимацию.
          image

          2. Экспериментируем с цветами


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

          3. Редактируем любой текст на странице


          Допустим у вас есть текстовый блок на страницы и вы хотите узнать, как он будет выглядеть, если текст в нем изменится. Переключите документ в режим дизайна! Наберите в консоле document.designMode = 'on', аналогично работает document.body.contentEditable = true. После этого вы сможете редактировать все элементы имеющие текстовый контент.
          image

          4. Просмотр отрендеренных шрифтов


          Иногда очень сложно сказать, какой шрифт на самом деле был отрендерен. В нижней части Computed вкладки панели инструментов, вы можете увидеть какой шрифт используется, даже если его названия нет в списке font-family.
          image

          5. Принудительные псевдоклассы для элементов


          DevTools имеет функцию, которая имитирует применение псевдоклассов CSS, например такие как :hover и :focus на элементах, что упрощает их стилизацию. Она доступна из редактора CSS.
          image

          6. Изменение формата цвета


          Используйте Shift + Клик на предварительном просмотре цвета, чтобы изменить формат: rgb, hsl и hex.
          image

          7. Редактор кривых безье для анимации.


          Вы можете легко изменить временные функции используя DevTools (например, значение свойства для animation-timing-function CSS свойств).
          image

          Возможности консоли.


          1. Вывести HTML элемент в представлении JS объекта


          При чтении HTML, браузер генерирует DOM-модель. Если необходимо вывести элемент именно в виде JS объекта в консоль, проще всего для этого воспользоваться методом console.dir().
          image

          2. Группировка сообщений


          Иногда бывает полезно сгруппировать логи для упрощения работы с ними и меньшего засорения консоли. Для этого существуют методы console.group(), console.groupCollapsed() и console.groupEnd().
          function name(obj) {
            console.group('name');
            console.log('first: ', obj.first);
            console.log('middle: ', obj.middle);
            console.log('last: ', obj.last);
            console.groupEnd();
          }
          name({"first":"Wile","middle":"E","last":"Coyote"});
          

          image

          3.1. Вывод значений переменных в виде таблиц


          Иногда намного удобнее и нагляднее работать с массивами или объектами в виде таблицы, а не в виде стандартного иерархического представления. Для вывода данных в виде таблице существует метод console.table().
          function Person(firstName, lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
          }
          var family = {};
          family.mother = new Person("Jane", "Smith");
          family.father = new Person("John", "Smith");
          family.daughter = new Person("Emily", "Smith");
          console.table(family);
          

          image

          3.2 keys(object) и values(object)


          Keys() — возвращает массив имён свойств объекта.
          Values() — возвращает массив, содержащий значения всех свойств указанного объекта.
          image

          4. Логирование времени выполнения кода


          Представьте, что у вас есть две функции которые делают одно и тоже но их реализация различна. Как понять какая из них работает быстрее? Можно воспользоваться методами console.time() и console.timeEnd().
          image

          5. Профилирование


          Помимо замера времени можно профилировать Ваш код и вывести стек профилирования, который подробно показывает, где и сколько времени потратил браузер.
          console.profile();
          // Some code to execute
          console.profileEnd();

          image

          6.1. $(selector)


          Если вы знакомы с jQuery, то знаете о возможности конструкций вроде $(‘.class’) и $(‘#id’). Консоль разработчика обладает похожей функциональностью. Здесь «$» отношения к jQuery не имеет, хотя делает, то же самое. Это – сокращение для функции document.querySelector(), возвращает ссылку на первый элемент DOM с указанным CSS-селектором. При этом, если в документе доступна jQuery, её «$» перекроет функционал консоли.

          6.2. $$(selector)


          Возвращает массив элементов, содержащих указанный селектор. Эта команда эквивалентна вызову document.querySelectorAll().
          image

          7. clear(), copy(object) и inspect(object/function)


          Clear() — очистка всех записей в консоли.
          Copy() — копирование в буфер обмена строкового представления указанного объекта.
          Inspect() — открывает и выбирает указанный элемент или объект в соответствующей панели: Elements или Profiles.

          Прочее.


          1. Продвинутая кнопка перезагрузки


          (Работает только при открытом DevTools и только в браузере Google Chrome!)
          По долгому нажатию на кнопку «Обновить страницу» (либо по правому клику) мы открываем специальное меню, которое предоставляет нам выбор:
          • Обычная перезагрузка (обновляются просроченные ресурсы).
          • Аппаратная перезагрузка (принудительная загрузка всех ресурсов сайта).
          • Очистка кэша и аппаратная перезагрузка.

          image

          2. Форматирование минифицированых исходников


          Инструменты разработчика Chrome имеют встроенный «прихорашиватель» минимизированных исходных кодов к удобочитаемому виду. Кнопка находится в левом нижнем углу открытого в данный момент файла во вкладке Sources.
          image

          3. Отображение shadow DOM


          Такие элементы как поля ввода и кнопки, браузеры, создают из других базовых элементов которые обычно скрыты. Тем не менее, вы можете перейти Settings -> General и включить Show user agent shadow DOM, для отображения этих базовых элементов во вкладке Elements. Это даст вам возможность оформлять их по отдельности.
          image

          4. Фильтрация и поиск


          При работе с DOM, CSS, списком запросов на вкладке Network и т.д. зачастую мы видим перед собой большой список элементов, селекторов, свойств и так далее, в котором бывает сложно быстро найти интересующее нас значение. В таких случаях не стоит забывать про использование фильтрации и поиска которое присутствует на всех вкладках. Благодаря фильтрации мы будем отсеивать все варианты кроме подходящих под условия, а поиск позволит Вам быстро найти то что нужно, если Вы знаете ключевые «слова» для поиска. Как правило поле поиска скрыто и вызывается комбинацией Ctrl + F.
          image

          5. Come to the Dark Side


          Просто потому что темная сторона круче (:
          А если серьезно, темная тема убережет ваши глаза от лишнего напряжения, если основной цвет разрабатываемой или отлаживаемой страницы темный, как в моем случае. Включить можно вначале страницы настроек DevTools.
          image

          Используемые ресурсы:
          developers.google.com/web/tools/chrome-devtools
          habrahabr.ru
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/336710/


          Метки:  

          Точка Б: как мы обучали приложение Яндекс.Такси предсказывать пункт назначения

          Вторник, 19 Сентября 2017 г. 10:00 + в цитатник
          vkantor сегодня в 10:00 Разработка

          Точка Б: как мы обучали приложение Яндекс.Такси предсказывать пункт назначения

            Представьте: вы открываете приложение, чтобы в очередной раз заказать такси в часто посещаемое вами место, и, конечно, в 2017 году вы ожидаете, что все, что нужно сделать – это сказать приложению «Вызывай», и такси за вами тут же выедет. А куда вы хотели ехать, через сколько минут и на какой машине — все это приложение узнает благодаря истории заказов и машинному обучению. В общем-то все, как в шутках про идеальный интерфейс с единственной кнопкой «сделать хорошо», лучше которого только экран с надписью «все уже хорошо». Звучит здорово, но как же приблизить эту реальность? Об этом мы и хотим рассказать.



            На днях мы выпустили новое приложение Яндекс.Такси для iOS. В обновленном интерфейсе один из акцентов, как нетрудно заметить, сделан на выборе конечной точки маршрута («точки Б»). Но новая версия – это не просто новый UI. К запуску обновления мы существенно переработали технологию прогнозирования пункта назначения, заменив старые эвристики на обученный на исторических данных классификатор.

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

            Задача


            При выборе пункта назначения, в поле «куда» у нас есть возможность предложить пользователю буквально 2-3 варианта на главном экране и еще несколько – если он перейдет в меню выбора точки Б:



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

            До построения модели на основе машинного обучения была реализована такая стратегия: берутся все точки из истории, «одинаковые» точки (с одинаковым адресом или находящиеся близко) сливаются, за разные параметры получившимся локациям начисляются различные баллы (если пользователь ездил в эту точку, ездил из этой точки, ездил в эту точку в какое-то временное окно и так далее). Затем выбираются локации с наибольшим числом баллов и рекомендуются пользователю. Как я уже говорил, исторически эта стратегия неплохо работала, но точность прогнозирования можно было повысить. А главное — мы знали как.

            «Старые» эвристики и обученный классификатор могут давать разные прогнозы: на этом рисунке размер круга показывает, насколько высоко данная точка находится в списке рекомендаций в конкретный день и конкретное время. Обратите внимание, что обученный алгоритм в данном примере предложил несколько дополнительных локаций (зеленые круги).

            Наша задача состояла в том, чтобы поверх «старой» стратегии использовать переранжирование наиболее вероятных «точек Б» с помощью машинного обучения. Общий подход такой: сначала начисляем баллы, как и раньше, затем берём топ n по этим баллам (где n заведомо больше, чем нужно в итоге порекомендовать), а далее переранжируем его и пользователю в итоге показываем только три наиболее вероятные точки Б. Точное число кандидатов в списке, конечно, определяется в ходе оптимизации, чтобы и качество кандидатов и качество переранжирования были как можно выше. В нашем случае оптимальное число кандидатов оказалось равным 20.

            Об оценке качества и результатах


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

            Начнём с первой метрики, то есть качества кандидатов. Стоит отметить, что только примерно в 72 процентах заказов выбранная точка Б (или очень близкая к ней географически) присутствовала ранее в истории. Это означает, что максимальное качество кандидатов ограничено этим числом, так как мы пока не можем рекомендовать точки, куда человек еще ни разу не ездил. Мы подобрали эвристики с назначением баллов так, что при отборе топ-20 кандидатов по этим баллам правильный ответ в них содержался в 71% случаев. При максимуме в 72% это весьма неплохой результат. Это качество кандидатов нас полностью устраивало, поэтому эту часть модели мы дальше не трогали, хотя, в принципе, могли бы. Например, вместо эвристик можно было обучить отдельную модель. Но так как механизм поиска кандидатов, основанный на эвристиках, уже был технически реализован, мы смогли сэкономить на разработке, лишь подобрав нужные коэффициенты.

            Снова вернемся к качеству переранжирования. Так как на главном экране заказа поездки показывать мы можем одну, две, ну максимум три точки, то нас интересует доля случаев, когда правильный ответ оказался в топ-1, топ-2 и топ-3 списка ранжирования соответственно. В машинном обучении долю правильных ответов в топ k списка ранжирования (от всех правильных ответов) называют recall@k. В нашем случае нас интересуют recall@1, recall@2 и recall@3. При этом «правильный ответ» в нашей задаче только один (с точностью до близко расположенных точек), а значит, эта метрика как раз будет показывать долю случаев, когда настоящая «точка Б» попадает в топ 1, топ 2 и топ 3 нашего алгоритма.

            В качестве базового алгоритма ранжирования мы взяли сортировку по количеству баллов и получили следующие результаты (в процентах): recall@1 = 63,7; recall@2 = 78,5 и recall@3 = 84,6. Заметим, что проценты здесь – это доля от тех случаев, где правильный ответ в кандидатах вообще присутствовал. В какой-то момент возник логичный вопрос: возможно, простая сортировка по популярности уже даёт хорошее качество, а все идеи сортировки по баллам и использование машинного обучения излишни? Но нет, такой алгоритм показал самое плохое качество: recall@1 = 41,2; recall@2 = 64,6; recall@3 = 74,7.

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


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

            Какая модель строилась


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

            Теперь перейдём к модели для переранжирования. Так как мы хотим максимизировать recall@k, в первом приближении мы хотим прогнозировать вероятность поездки пользователя в определенное место и ранжировать места по вероятности. «В первом приближении» потому, что эти рассуждения верны только при очень точной оценке вероятностей, а когда в них появляются погрешности, recall@k может лучше оптимизироваться и другим решением. Простая аналогия: в машинном обучении, если мы точно знаем все распределения вероятностей, самый лучший классификатор — байесовский, но поскольку на практике распределения восстанавливаются по данным приближенно, часто другие классификаторы работают лучше.

            Задача классификации ставилась следующим образом: объектами были пары (история предыдущих поездок пользователя; кандидат в рекомендации), где положительные примеры (класс 1) — пары, в которых пользователь все-таки поехал в эту точку Б, отрицательные примеры (класс 0) — пары, в которых пользователь в итоге поехал в другое место.

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

            Классификатор мы обучали на минимизацию log loss, а в качестве модели использовали активно применяемый в Яндексе Матрикснет (градиентный бустинг над решающими деревьями). И если с Матрикснетом всё понятно, то почему всё-таки оптимизировался log loss?

            Ну, во-первых, оптимизация этой метрики приводит к правильным оценкам вероятности. Причина, по которой так происходит, своими корнями уходит в математическую статистику, в метод максимального правдоподобия. Пусть вероятность единичного класса у i-го объекта равна $p_i$, а истинный класс равен $y_i$. Тогда, если считать исходы независимыми, правдоподобие выборки (грубо говоря, вероятность получить именно такой исход, который был получен) равно следующему произведению:

            $\prod_{i}p_i^{y_i}(1-p_i)^{(1-y_i)}$

            В идеале нам хотелось бы иметь такие $p_i$, чтобы получить максимум этого выражения (ведь мы хотим, чтобы полученные нами результаты были максимально правдоподобны, правда?). Но если прологарифмировать это выражение (так как логарифм — монотонная функция, то можно его максимизировать вместо исходного выражения), то мы получим $\sum_i y_i ln (p_i) + (1-y_i)ln(1-p_i)$. А это уже известный нам log loss, если заменить $p_i$ на их предсказания и умножить все выражение на минус-единицу.

            Во-вторых, мы, конечно, доверяем теории, но на всякий случай попробовали оптимизировать разные функции потерь, и log loss показал самый большой recall@k, так что все в итоге сошлось.

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

            И, наконец, о результатах: после переранжирования с помощью построенной нами модели мы получили следующие данные (опять же в процентах): recall@1 = 72,1 (+8,4); recall@2 = 82,6 (+4,1); recall@3 = 88 (+3,4). Совсем неплохо, но в будущем возможны улучшения за счет включения в признаки дополнительной информации.

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

            Дальнейшие планы


            Разумеется, впереди много экспериментов по улучшению модели. Будет и добавление новых данных для построения рекомендаций, в том числе интересов пользователя, а также предполагаемых «дома» и «работы» из Яндекс Крипты и связанных с ними признаков, и эксперимент по сравнению различных методов классификации в этой задаче. Например, у Яндекса уже есть не только используемый сугубо внутри команды Матрикснет, но и набирающий популярность и в Яндексе, и за его пределами CatBoost. При этом, конечно же, нам интересно сравнивать не только собственные разработки, но и другие реализации популярных алгоритмов.

            Кроме того, как уже было сказано в начале поста – было бы здорово, если бы приложение само понимало, когда, куда и на какой машине нам нужно поехать. Сейчас сложно сказать, насколько по историческим данным возможно точно угадывать параметры заказа и настройки приложения, подходящие пользователю, но мы работаем в этом направлении, будем делать наше приложение все более «умным» и способным подстраиваться под вас.
            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337328/


            Метки:  

            Философия статического анализа кода: три простых шага

            Вторник, 19 Сентября 2017 г. 09:42 + в цитатник
            EvgeniyRyzhkov сегодня в 09:42 Управление

            Философия статического анализа кода: три простых шага

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

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



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

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

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

              Стоит ли использовать статический анализ вместо других методологий? Статический анализ — это не панацея от всех бед! Его нельзя начать использовать вместо юнит-тестов или code review. Статический анализ — это ответ на вопрос: «А что ещё мы можем сделать, чтобы наш код был лучше?». Что значит лучше? Легче поддерживать, проще развивать, быстрее устранять проблемы. Если ваша компания зарабатывает деньги используя программный код вы просто не можете не использовать статический анализ кода.
              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/338220/


              Метки:  

              Философия статического анализа кода: три простых шага

              Вторник, 19 Сентября 2017 г. 09:42 + в цитатник
              EvgeniyRyzhkov сегодня в 09:42 Управление

              Философия статического анализа кода: три простых шага

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

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



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

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

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

                Стоит ли использовать статический анализ вместо других методологий? Статический анализ — это не панацея от всех бед! Его нельзя начать использовать вместо юнит-тестов или code review. Статический анализ — это ответ на вопрос: «А что ещё мы можем сделать, чтобы наш код был лучше?». Что значит лучше? Легче поддерживать, проще развивать, быстрее устранять проблемы. Если ваша компания зарабатывает деньги используя программный код вы просто не можете не использовать статический анализ кода.
                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/338220/


                Метки:  

                Поиск сообщений в rss_rss_hh_full
                Страницы: 1824 ... 1538 1537 [1536] 1535 1534 ..
                .. 1 Календарь