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

Поиск сообщений в 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 ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

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

Проекции? Hет, спасибо

Понедельник, 18 Сентября 2017 г. 07:02 + в цитатник
zzeng сегодня в 07:02 Разработка

Проекции? Hет, спасибо


    Под катом будет небольшая заметка о применении пространственного индекса
    на основе zcurve для индексации точечных данных, расположенных на сфере.
    А так же bencmark-и для PostgreSQL и сравнение с таким же (но совсем другим)
    индексом на R-дереве.

    В развитие темы (1, 2, 3, 4, 5, 6).
    Собственно, возвращаемся к самому началу — идее индексировать географические координаты, размещая их на поверхности сферы. Обычная индексация пары широта/долгота приводит к искажениям вдали от экватора, работа с проекциями не универсальна. Поэтому мысль переводить географические координаты в трехмерное пространство выглядит довольно изящно.

    Сама по себе идея эта не нова, аналогично работает, например, расширение PostgreSQL — PGSphere, которое использует для индексации 3-мерное R-дерево. С ним и будем сравнивать.

    Подготовка данных.


    PGSphere


    • Для начала придётся выкачать, собрать и инсталлировать расширение (автор использовал текущую версию 1.1.5)
      gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config
      sudo gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config install
      
    • Далее загрузить его (psql)
      CREATE EXTENSION pg_sphere;
    • Создадим таблицу для тестовых данных
      CREATE TABLE spoint_data (sp spoint);
    • Нам потребуется источник случайных данных.
      Первый параметр программы — радиус, второй — число результатов.
      Единственная тонкость — данные равномерно распределены внутри шара с заданным радиусом, иначе не получится равномерного распределения на сфере
    • Случайные данные пропустим через скрипт awk чтобы превратить в геокоординаты
      # --- gendata.awk ------
      BEGIN{
      	pi=3.1415926535897932;
      	degra=pi/180.0;
      	rad=180.0/pi;
      	Grad = 1000000.;
      }
      {
      	x = $1;	y = $2;	z = $3;
      	r3 = sqrt(x*x + y*y + z*z);
      	x *= Grad / r3;
      	y *= Grad / r3;
      	z *= Grad / r3;
      
      	r2 = sqrt(x*x + y*y);
      	lat = atan2(z, r2) * rad;
      	lon = 180. + atan2(y, x) * rad;
      
      	printf ("(%14.10fd, %14.10fd)\n", lon, lat);
      }
    • Собственно создание данных, здесь радиус не важен, важно чтобы и pgsphere и zcurve получили одни и те же данные. Сортировка весьма желательна для ускорения индексации.
      ./random 1000000 100000000 | gawk -f gendata.awk | sort > pgsphere.txt
    • Заливаем данные в нашу таблицу
      COPY spoint_data (sp) FROM  '/home/.../pgsphere.txt';
    • Индексируем
      CREATE INDEX sp_idx ON spoint_data USING gist (sp);

    ZORDER


    • Для начала придётся выкачать, собрать и инсталлировать расширение
      gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config
      sudo gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config install
      
    • Создадим таблицу для тестовых данных
      create table test_points_3d (x integer,y integer, z integer);
    • Нам потребуется тот же источник случайных данных.
    • Случайные данные пропустим через скрипт awk чтобы разместить их внутри куба со стороной в 2 000 000
      #--- gendata2.awk ------
      BEGIN{
      	pi=3.1415926535897932;
      	degra=pi/180.0;
      	rad=180.0/pi;
      	Grad = 1000000.;
      }
      {
      	x = $1;	y = $2;	z = $3;
      	r3 = sqrt(x*x + y*y + z*z);
      	x *= Grad / r3;
      	y *= Grad / r3;
      	z *= Grad / r3;
      
      	ix = int(x+0.5+Grad);
      	iy = int(y+0.5+Grad);
      	iz = int(z+0.5+Grad);
      	print ix"\t"iy"\t"iz;
      }
    • Собственно создание данных, здесь радиус важен. Сортировка не обязательна.
      ./random 1000000 100000000 | gawk -f gendata2.awk > zcurve.txt
    • Заливаем данные в нашу таблицу
      COPY test_points_3d FROM '/home/.../zcurve.txt';
    • Индексируем
      create index zcurve_test_points_3d on test_points_3d(zcurve_num_from_xyz(x,y,z));

    Подготовка тестов


    PGSphere


    Для тестирования потребуется вот такой awk скрипт
    #--- gentest.awk -------
    BEGIN{
    	pi=3.1415926535897932;
    	degra=pi/180.0;
    	rad=180.0/pi;
    	Grad = 1000000.;
    }
    {
    	x = $1;	y = $2;	z = $3;
    	r3 = sqrt(x*x + y*y + z*z);
    	x *= Grad / r3;
    	y *= Grad / r3;
    	z *= Grad / r3;
    
    	r2 = sqrt(x*x + y*y);
    
    	lat = atan2(z, r2) * rad;
    	lon = 180. + atan2(y, x) * rad;
    
    #	EXPLAIN (ANALYZE,BUFFERS) 
      printf ("select count(1) from spoint_data where sp        @'<(%14.10fd,%14.10fd),.316d>'::scircle;\n", lon, lat);
    }

    Этот скрипт вполне симметричен тому, с помощью которого мы подготавливали данные. Стоит обратить внимание на число .316, это радиус сферы с центром в вычисленной случайной точке, в которой мы ищем данные
    Подготовка серии запросов делается так:
    ./random 1000000 100 1023 | gawk -f gentest.awk >tests1.sql

    Здесь 100 — размер тестовой серии, 1023 — seed рандомизатора.

    ZCURVE


    Для тестирования тоже потребуется awk скрипт
    #--- gentest2.awk -------
    BEGIN{
    	pi=3.1415926535897932;
    	degra=pi/180.0;
    	rad=180.0/pi;
    	Grad = 1000000.;
    }
    {
    	x = $1;	y = $2;	z = $3;
    	r3 = sqrt(x*x + y*y + z*z);
    	x *= Grad / r3;
    	y *= Grad / r3;
    	z *= Grad / r3;
    
    	ix = int(x+0.5+Grad);
    	iy = int(y+0.5+Grad);
    	iz = int(z+0.5+Grad);
    #	EXPLAIN (ANALYZE,BUFFERS) 
    	lrad = int(0.5 + Grad * sin(.316 * degra));
    	print "select count(1) from zcurve_3d_lookup_tidonly('zcurve_test_points_3d',   "ix-lrad","iy-lrad","iz-lrad","ix+lrad","iy+lrad","iz+lrad");";
    }

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

    Подготовка серии запросов делается так:
    ./random 1000000 100 1023 | gawk -f gentest2.awk >tests2.sql

    Здесь 100 — размер тестовой серии, 1023 — seed рандомизатора, лучше, если он совпадает с оным от pgsphere.

    Benchmark


    Как и раньше, замеры проводились на скромной виртуальной машине с двумя ядрами и 4 Гб ОЗУ, поэтому времена не имеют абсолютной ценности, а вот числам прочитанных страниц по прежнему можно доверять.
    Времена показаны на вторых прогонах, на разогретом сервере и виртуальной машине. Количества прочитанных буферов — на свеже-поднятом сервере.
    Radius AVG NPoints Nreq Type Time(ms) Reads Hits
    .01° 1.17
    0.7631
    (0.7615)
    10 000 zcurve
    rtree
    .37
    .46
    1.4397
    2.1165
    9.5647
    3.087
    .0316° 11.6
    7.6392
    (7.6045)
    10 000 zcurve
    rtree
    .39
    .67
    2.0466
    3.0944
    20.9707
    2.7769
    .1° 115.22
    76.193
    (76.15)
    1 000 zcurve
    rtree
    .44
    2.75 *
    4.4184
    6.073
    82.8572
    2.469
    .316° 1145.3
    758.37
    (760.45)
    1 000 zcurve
    rtree
    .59
    18.3 *
    15.2719
    21.706
    401.791
    1.62
    1.° 11310
    7602
    (7615)
    100 zcurve
    rtree
    7.2
    94.5 *
    74.9544
    132.15
    1651.45
    1.12
    где
        Radius — размер поисковой области в градусах
        Npoints — среднее число точек в выдаче, в скобках — теоретически ожидаемое число
         (в сфере 41252.96 кв. градусов, 100 000 000 точек, ~2424 точки на кв. градус)

        Nreq — число запросов в серии
        Type
          ‘zcurve’ — оно и есть
          ’rtree’- PGSphere
        Time(ms) — среднее время выполнения запроса
        Reads — среднее число чтений на запрос
        Hits — число обращений к буферам

        * в какой-то момент производительность R-tree начинает резко
        проседать, связано это с тем, это дерево читает заметно больше
        страниц и его рабочий набор перестаёт помещаться в кэше (по-видимому).

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

    Попробуем оценить разницу. Вообще, задача найти площадь куска сферы, попавшей внутрь куба нетривиальна. Попробуем сделать оценку.
    • Предположим, что наш куб в среднем вырезает из сферы ту же площадь, что и сфера равного объема
    • Объем единичной сферы 1.33*3.14=4.19
    • Объем куба со стороной 2 = 8.
    • Тогда корень третьей степени из 8/4.19 = 1.24 — это отношение радиусов мнимой сферы к настоящей
    • соотношение площадей мнимой сферы к настоящей 1.24*1.24=1.54
    • имеем из экспериментальных данных 1.17/0.7631= 1.5332
      Bingo!
    Original source: habrahabr.ru (comments, light).

    https://habrahabr.ru/post/338088/


    Метки:  

    Тянем ролик с YouTube и раздаем по WebRTC в реалтайме

    Понедельник, 18 Сентября 2017 г. 06:54 + в цитатник
    flashphoner сегодня в 06:54 Разработка

    Тянем ролик с YouTube и раздаем по WebRTC в реалтайме



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

      Ролик как стрим


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

      Для того, чтобы разброса не было, нужно раздавать этот ролик всем одновременно. Это можно реализовать, если обернуть ролик в Live-stream. Покажем как это сделать с помощью связки этой библиотеки с ffmpeg.


      Нам нужно реализовать схему, описанную выше. А именно, ydl подключается к YouTube и начинает скачивать ролик. FFmpeg подхватывает скачивающийся ролик, оборачивает его в RTMP поток и отправляет на сервер. Сервер раздает полученный поток как WebRTC в реальном времени.

      Установка youtube-dl


      Начинаем с установки youtube-dl. Процесс установки на Linux предельно простой и подробно описан в Readme под Lin и под Win.

      1. Скачиваем.

      curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl

      2. Даем права на запуск

      chmod a+rx /usr/local/bin/youtube-dl

      На этом все. YouTube скачивалка готова к работе.

      Возьмем ролик с YouTube и посмотрим на его мета-данные:

      youtube-dl --list-formats https://www.youtube.com/watch?v=9cQT4urTlXM

      Результат будет таким:

      [youtube] 9cQT4urTlXM: Downloading webpage
      [youtube] 9cQT4urTlXM: Downloading video info webpage
      [youtube] 9cQT4urTlXM: Extracting video information
      [youtube] 9cQT4urTlXM: Downloading MPD manifest
      [info] Available formats for 9cQT4urTlXM:
      format code  extension  resolution note
      171          webm       audio only DASH audio    8k , vorbis@128k, 540.24KiB
      249          webm       audio only DASH audio   10k , opus @ 50k, 797.30KiB
      250          webm       audio only DASH audio   10k , opus @ 70k, 797.30KiB
      251          webm       audio only DASH audio   10k , opus @160k, 797.30KiB
      139          m4a        audio only DASH audio   53k , m4a_dash container, mp4a.40.5@ 48k (22050Hz), 10.36MiB
      140          m4a        audio only DASH audio  137k , m4a_dash container, mp4a.40.2@128k (44100Hz), 27.56MiB
      278          webm       256x144    144p   41k , webm container, vp9, 30fps, video only, 6.54MiB
      242          webm       426x240    240p   70k , vp9, 30fps, video only, 13.42MiB
      243          webm       640x360    360p  101k , vp9, 30fps, video only, 20.55MiB
      160          mp4        256x144    DASH video  123k , avc1.4d400c, 15fps, video only, 24.83MiB
      134          mp4        640x360    DASH video  138k , avc1.4d401e, 30fps, video only, 28.07MiB
      244          webm       854x480    480p  149k , vp9, 30fps, video only, 30.55MiB
      135          mp4        854x480    DASH video  209k , avc1.4d401f, 30fps, video only, 42.42MiB
      133          mp4        426x240    DASH video  274k , avc1.4d4015, 30fps, video only, 57.63MiB
      247          webm       1280x720   720p  298k , vp9, 30fps, video only, 59.25MiB
      136          mp4        1280x720   DASH video  307k , avc1.4d401f, 30fps, video only, 62.58MiB
      17           3gp        176x144    small , mp4v.20.3, mp4a.40.2@ 24k
      36           3gp        320x180    small , mp4v.20.3, mp4a.40.2
      43           webm       640x360    medium , vp8.0, vorbis@128k
      18           mp4        640x360    medium , avc1.42001E, mp4a.40.2@ 96k
      22           mp4        1280x720   hd720 , avc1.64001F, mp4a.40.2@192k (best)


      Установка ffmpeg


      Далее устанавливаем ffmpeg стандартными заклинаниями:

      wget http://ffmpeg.org/releases/ffmpeg-3.3.4.tar.bz2
      tar -xvjf ffmpeg-3.3.4.tar.bz2
      cd  ffmpeg-3.3.4
      ./configure --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp
      make
      make install

      Проверяем что получилось

      ffmpeg -v

      Теперь самое интересное. Библиотека youtube-dl предназначена для скачивания. Она так и называется YouTube Download. Т.е. Можно скачать youtube ролик полностью и уже после этого застримить его через ffmpeg как файл.

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

      1. Маркетолог говорит — «А теперь, коллеги, давайте посмотрим этот ролик с котиками, он полностью отвечает нашей стратегии выхода на рынок», и жмет кнопку «показать всем ролик».
      2. На экране появляется прелоадер: «Подождите, ролик с котиками скачивается. Это займет не более 10 минут».
      3. Менеджер идет пить кофе, а программист — читать хабр.

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

      Передача данных из youtube-dl в ffmpeg


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

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

      #!/usr/bin/python
      
      import subprocess
      import sys
      
      def show_help():
          print 'Usage: '
          print './streamer.py url streamName destination'
          print './streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM streamName rtmp://192.168.88.59:1935/live'
          return
      
      def streamer() :
          url = sys.argv[1]
          if not url :
              print 'Error: url is empty'
              return
          stream_id = sys.argv[2]
          if not stream_id:
              print 'Error: stream name is empty'
              return
          destination = sys.argv[3]
          if not destination:
              print 'Error: destination is empty'
              return
      
          _youtube_process = subprocess.Popen(('youtube-dl','-f','','--prefer-ffmpeg', '--no-color', '--no-cache-dir', '--no-progress','-o', '-', '-f', '22/18', url, '--reject-title', stream_id),stdout=subprocess.PIPE)
          _ffmpeg_process = subprocess.Popen(('ffmpeg','-re','-i', '-','-preset', 'ultrafast','-vcodec', 'copy', '-acodec', 'copy','-threads','1', '-f', 'flv',destination + "/" + stream_id), stdin=_youtube_process.stdout)
          return
      
      if len(sys.argv) < 4:
          show_help()
      else:
          streamer()

      Этот питон-скрипт делает следующее:

      1. Создает подпроцесс _youtube_process зачитки ролика библиотекой youtube-dl
      2. Создает второй подпроцесс _ffmpeg_process, которому передаются данные из первого через pipe. Этот процесс уже создает RTMP поток и отправляет его на сервер по указанному адресу.


      Тестирование скрипта


      Для запуска скрипта нужно установить python. Скачать можно здесь.

      Мы при тестировании использовали версию 2.6.6. Скорее всего подойдет любая версия, т.к. скрипт достаточно простой и его задача — передать из одного процесса в другой.

      Запуск скрипта:
      python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://192.168.88.59:1935/live

      Как видите, передается три аргумента:

      1. Адрес youtube ролика.
        www.youtube.com/watch?v=9cQT4urTlXM
      2. Имя потока, с которым будет проходить RTMP-трансляция.
        stream1
      3. Адрес RTMP-сервера.
        rtmp://192.168.88.59:1935/live

      Для тестирования мы будем использовать Web Call Server. Он умеет принимать RTMP потоки и раздавать их по WebRTC. Здесь можно скачать и установить WCS5 на свой VPS или локальный тестовый сервер под управлением Linux.

      Схема тестирования с Web Call Server:


      Ниже мы задействуем для теста один из демо-серверов:

      rtmp://wcs5-eu.flashphoner.com:1935/live

      Это RTMP адрес, который нужно передать скрипту streamer.py чтобы быстро протестировать трансляцию с нашим демо-сервером.

      Запуск должен выглядеть так:

      python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live

      В консоли stdout увидим следующий вывод:

      # python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live
      ffmpeg version 3.2.3 Copyright (c) 2000-2017 the FFmpeg developers
        built with gcc 4.4.7 (GCC) 20120313 (Red Hat 4.4.7-11)
        configuration: --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp --disable-yasm
        libavutil      55. 34.101 / 55. 34.101
        libavcodec     57. 64.101 / 57. 64.101
        libavformat    57. 56.101 / 57. 56.101
        libavdevice    57.  1.100 / 57.  1.100
        libavfilter     6. 65.100 /  6. 65.100
        libswscale      4.  2.100 /  4.  2.100
        libswresample   2.  3.100 /  2.  3.100
        libpostproc    54.  1.100 / 54.  1.100
      ]# [youtube] 9cQT4urTlXM: Downloading webpage
      [youtube] 9cQT4urTlXM: Downloading video info webpage
      [youtube] 9cQT4urTlXM: Extracting video information
      [youtube] 9cQT4urTlXM: Downloading MPD manifest
      [download] Destination: -
      Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pipe:':
        Metadata:
          major_brand     : mp42
          minor_version   : 0
          compatible_brands: isommp42
          creation_time   : 2016-08-23T12:21:06.000000Z
        Duration: 00:29:59.99, start: 0.000000, bitrate: N/A
          Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 288 kb/s, 30 fps, 30 tbr, 90k tbn, 60 tbc (default)
          Metadata:
            creation_time   : 2016-08-23T12:21:06.000000Z
            handler_name    : ISO Media file produced by Google Inc.
          Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
          Metadata:
            creation_time   : 2016-08-23T12:21:06.000000Z
            handler_name    : ISO Media file produced by Google Inc.
      Output #0, flv, to 'rtmp://192.168.88.59:1935/live/stream1':
        Metadata:
          major_brand     : mp42
          minor_version   : 0
          compatible_brands: isommp42
          encoder         : Lavf57.56.101
          Stream #0:0(und): Video: h264 (Main) ([7][0][0][0] / 0x0007), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 288 kb/s, 30 fps, 30 tbr, 1k tbn, 90k tbc (default)
          Metadata:
            creation_time   : 2016-08-23T12:21:06.000000Z
            handler_name    : ISO Media file produced by Google Inc.
          Stream #0:1(und): Audio: aac (LC) ([10][0][0][0] / 0x000A), 44100 Hz, stereo, 125 kb/s (default)
          Metadata:
            creation_time   : 2016-08-23T12:21:06.000000Z
            handler_name    : ISO Media file produced by Google Inc.
      Stream mapping:
        Stream #0:0 -> #0:0 (copy)
        Stream #0:1 -> #0:1 (copy)
      frame=  383 fps= 30 q=-1.0 size=     654kB time=00:00:12.70 bitrate= 421.8kbits/s speed=   1x

      Если бегло пробежать по этому логу, то можно понять, что происходит следующее:

      1. Открывается страница с видеороликом.
      2. Извлекаются данные о видео форматах.
      3. Скачивается mp4 ролик 1280x720, H.264+AAC
      4. Запускается ffmpeg, подхватывает скачиваемые данные и стримит по RTMP с битрейтом 421 kbps. Такой скудный битрейт объясняется выбранным роликом  с таймером. Нормальный видеоролик даст на порядок большее значение битрейта.

      После того, как процесс стриминга запустился, пытаемся проиграть поток в WebRTC плеере. Имя потока задается в поле Stream, а адрес сервера в поле Server. Подключение к серверу происходит по протоколу Websocket (wss), а поток приходит на плеер по WebRTC (UDP).


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

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

      Получаем следующие результаты:

      Test 1


      Test 2


      Test 3


      Как видите, каждый из зрителей видит одно и то же видео, с разбросом не более 130 миллисекунд.

      Таким образом задача реалтаймовой трансляции ролика с YouTube на WebRTC решена. Зрители получили поток практически одновременно. Менеджер не ушел пить кофе, программист — читать хабр, а маркетолог успешно показал всем ролик с  котиками.

      Хорошего стриминга!

      Ссылки


      youtube-dl  — библиотека для скачивания видео с YouTube
      ffmpeg — RTMP encoder
      Web Call Server — сервер, умеющий раздвать RTMP поток по WebRTC
      streamer.py — скрипт для интеграции youtube-dl и ffmpeg с отправкой RTMP потока
      Original source: habrahabr.ru (comments, light).

      https://habrahabr.ru/post/338098/


      Тянем ролик с YouTube и раздаем по WebRTC в реалтайме

      Понедельник, 18 Сентября 2017 г. 06:54 + в цитатник
      flashphoner сегодня в 06:54 Разработка

      Тянем ролик с YouTube и раздаем по WebRTC в реалтайме



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

        Ролик как стрим


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

        Для того, чтобы разброса не было, нужно раздавать этот ролик всем одновременно. Это можно реализовать, если обернуть ролик в Live-stream. Покажем как это сделать с помощью связки этой библиотеки с ffmpeg.


        Нам нужно реализовать схему, описанную выше. А именно, ydl подключается к YouTube и начинает скачивать ролик. FFmpeg подхватывает скачивающийся ролик, оборачивает его в RTMP поток и отправляет на сервер. Сервер раздает полученный поток как WebRTC в реальном времени.

        Установка youtube-dl


        Начинаем с установки youtube-dl. Процесс установки на Linux предельно простой и подробно описан в Readme под Lin и под Win.

        1. Скачиваем.

        curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl

        2. Даем права на запуск

        chmod a+rx /usr/local/bin/youtube-dl

        На этом все. YouTube скачивалка готова к работе.

        Возьмем ролик с YouTube и посмотрим на его мета-данные:

        youtube-dl --list-formats https://www.youtube.com/watch?v=9cQT4urTlXM

        Результат будет таким:

        [youtube] 9cQT4urTlXM: Downloading webpage
        [youtube] 9cQT4urTlXM: Downloading video info webpage
        [youtube] 9cQT4urTlXM: Extracting video information
        [youtube] 9cQT4urTlXM: Downloading MPD manifest
        [info] Available formats for 9cQT4urTlXM:
        format code  extension  resolution note
        171          webm       audio only DASH audio    8k , vorbis@128k, 540.24KiB
        249          webm       audio only DASH audio   10k , opus @ 50k, 797.30KiB
        250          webm       audio only DASH audio   10k , opus @ 70k, 797.30KiB
        251          webm       audio only DASH audio   10k , opus @160k, 797.30KiB
        139          m4a        audio only DASH audio   53k , m4a_dash container, mp4a.40.5@ 48k (22050Hz), 10.36MiB
        140          m4a        audio only DASH audio  137k , m4a_dash container, mp4a.40.2@128k (44100Hz), 27.56MiB
        278          webm       256x144    144p   41k , webm container, vp9, 30fps, video only, 6.54MiB
        242          webm       426x240    240p   70k , vp9, 30fps, video only, 13.42MiB
        243          webm       640x360    360p  101k , vp9, 30fps, video only, 20.55MiB
        160          mp4        256x144    DASH video  123k , avc1.4d400c, 15fps, video only, 24.83MiB
        134          mp4        640x360    DASH video  138k , avc1.4d401e, 30fps, video only, 28.07MiB
        244          webm       854x480    480p  149k , vp9, 30fps, video only, 30.55MiB
        135          mp4        854x480    DASH video  209k , avc1.4d401f, 30fps, video only, 42.42MiB
        133          mp4        426x240    DASH video  274k , avc1.4d4015, 30fps, video only, 57.63MiB
        247          webm       1280x720   720p  298k , vp9, 30fps, video only, 59.25MiB
        136          mp4        1280x720   DASH video  307k , avc1.4d401f, 30fps, video only, 62.58MiB
        17           3gp        176x144    small , mp4v.20.3, mp4a.40.2@ 24k
        36           3gp        320x180    small , mp4v.20.3, mp4a.40.2
        43           webm       640x360    medium , vp8.0, vorbis@128k
        18           mp4        640x360    medium , avc1.42001E, mp4a.40.2@ 96k
        22           mp4        1280x720   hd720 , avc1.64001F, mp4a.40.2@192k (best)


        Установка ffmpeg


        Далее устанавливаем ffmpeg стандартными заклинаниями:

        wget http://ffmpeg.org/releases/ffmpeg-3.3.4.tar.bz2
        tar -xvjf ffmpeg-3.3.4.tar.bz2
        cd  ffmpeg-3.3.4
        ./configure --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp
        make
        make install

        Проверяем что получилось

        ffmpeg -v

        Теперь самое интересное. Библиотека youtube-dl предназначена для скачивания. Она так и называется YouTube Download. Т.е. Можно скачать youtube ролик полностью и уже после этого застримить его через ffmpeg как файл.

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

        1. Маркетолог говорит — «А теперь, коллеги, давайте посмотрим этот ролик с котиками, он полностью отвечает нашей стратегии выхода на рынок», и жмет кнопку «показать всем ролик».
        2. На экране появляется прелоадер: «Подождите, ролик с котиками скачивается. Это займет не более 10 минут».
        3. Менеджер идет пить кофе, а программист — читать хабр.

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

        Передача данных из youtube-dl в ffmpeg


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

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

        #!/usr/bin/python
        
        import subprocess
        import sys
        
        def show_help():
            print 'Usage: '
            print './streamer.py url streamName destination'
            print './streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM streamName rtmp://192.168.88.59:1935/live'
            return
        
        def streamer() :
            url = sys.argv[1]
            if not url :
                print 'Error: url is empty'
                return
            stream_id = sys.argv[2]
            if not stream_id:
                print 'Error: stream name is empty'
                return
            destination = sys.argv[3]
            if not destination:
                print 'Error: destination is empty'
                return
        
            _youtube_process = subprocess.Popen(('youtube-dl','-f','','--prefer-ffmpeg', '--no-color', '--no-cache-dir', '--no-progress','-o', '-', '-f', '22/18', url, '--reject-title', stream_id),stdout=subprocess.PIPE)
            _ffmpeg_process = subprocess.Popen(('ffmpeg','-re','-i', '-','-preset', 'ultrafast','-vcodec', 'copy', '-acodec', 'copy','-threads','1', '-f', 'flv',destination + "/" + stream_id), stdin=_youtube_process.stdout)
            return
        
        if len(sys.argv) < 4:
            show_help()
        else:
            streamer()

        Этот питон-скрипт делает следующее:

        1. Создает подпроцесс _youtube_process зачитки ролика библиотекой youtube-dl
        2. Создает второй подпроцесс _ffmpeg_process, которому передаются данные из первого через pipe. Этот процесс уже создает RTMP поток и отправляет его на сервер по указанному адресу.


        Тестирование скрипта


        Для запуска скрипта нужно установить python. Скачать можно здесь.

        Мы при тестировании использовали версию 2.6.6. Скорее всего подойдет любая версия, т.к. скрипт достаточно простой и его задача — передать из одного процесса в другой.

        Запуск скрипта:
        python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://192.168.88.59:1935/live

        Как видите, передается три аргумента:

        1. Адрес youtube ролика.
          www.youtube.com/watch?v=9cQT4urTlXM
        2. Имя потока, с которым будет проходить RTMP-трансляция.
          stream1
        3. Адрес RTMP-сервера.
          rtmp://192.168.88.59:1935/live

        Для тестирования мы будем использовать Web Call Server. Он умеет принимать RTMP потоки и раздавать их по WebRTC. Здесь можно скачать и установить WCS5 на свой VPS или локальный тестовый сервер под управлением Linux.

        Схема тестирования с Web Call Server:


        Ниже мы задействуем для теста один из демо-серверов:

        rtmp://wcs5-eu.flashphoner.com:1935/live

        Это RTMP адрес, который нужно передать скрипту streamer.py чтобы быстро протестировать трансляцию с нашим демо-сервером.

        Запуск должен выглядеть так:

        python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live

        В консоли stdout увидим следующий вывод:

        # python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live
        ffmpeg version 3.2.3 Copyright (c) 2000-2017 the FFmpeg developers
          built with gcc 4.4.7 (GCC) 20120313 (Red Hat 4.4.7-11)
          configuration: --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp --disable-yasm
          libavutil      55. 34.101 / 55. 34.101
          libavcodec     57. 64.101 / 57. 64.101
          libavformat    57. 56.101 / 57. 56.101
          libavdevice    57.  1.100 / 57.  1.100
          libavfilter     6. 65.100 /  6. 65.100
          libswscale      4.  2.100 /  4.  2.100
          libswresample   2.  3.100 /  2.  3.100
          libpostproc    54.  1.100 / 54.  1.100
        ]# [youtube] 9cQT4urTlXM: Downloading webpage
        [youtube] 9cQT4urTlXM: Downloading video info webpage
        [youtube] 9cQT4urTlXM: Extracting video information
        [youtube] 9cQT4urTlXM: Downloading MPD manifest
        [download] Destination: -
        Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pipe:':
          Metadata:
            major_brand     : mp42
            minor_version   : 0
            compatible_brands: isommp42
            creation_time   : 2016-08-23T12:21:06.000000Z
          Duration: 00:29:59.99, start: 0.000000, bitrate: N/A
            Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 288 kb/s, 30 fps, 30 tbr, 90k tbn, 60 tbc (default)
            Metadata:
              creation_time   : 2016-08-23T12:21:06.000000Z
              handler_name    : ISO Media file produced by Google Inc.
            Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
            Metadata:
              creation_time   : 2016-08-23T12:21:06.000000Z
              handler_name    : ISO Media file produced by Google Inc.
        Output #0, flv, to 'rtmp://192.168.88.59:1935/live/stream1':
          Metadata:
            major_brand     : mp42
            minor_version   : 0
            compatible_brands: isommp42
            encoder         : Lavf57.56.101
            Stream #0:0(und): Video: h264 (Main) ([7][0][0][0] / 0x0007), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 288 kb/s, 30 fps, 30 tbr, 1k tbn, 90k tbc (default)
            Metadata:
              creation_time   : 2016-08-23T12:21:06.000000Z
              handler_name    : ISO Media file produced by Google Inc.
            Stream #0:1(und): Audio: aac (LC) ([10][0][0][0] / 0x000A), 44100 Hz, stereo, 125 kb/s (default)
            Metadata:
              creation_time   : 2016-08-23T12:21:06.000000Z
              handler_name    : ISO Media file produced by Google Inc.
        Stream mapping:
          Stream #0:0 -> #0:0 (copy)
          Stream #0:1 -> #0:1 (copy)
        frame=  383 fps= 30 q=-1.0 size=     654kB time=00:00:12.70 bitrate= 421.8kbits/s speed=   1x

        Если бегло пробежать по этому логу, то можно понять, что происходит следующее:

        1. Открывается страница с видеороликом.
        2. Извлекаются данные о видео форматах.
        3. Скачивается mp4 ролик 1280x720, H.264+AAC
        4. Запускается ffmpeg, подхватывает скачиваемые данные и стримит по RTMP с битрейтом 421 kbps. Такой скудный битрейт объясняется выбранным роликом  с таймером. Нормальный видеоролик даст на порядок большее значение битрейта.

        После того, как процесс стриминга запустился, пытаемся проиграть поток в WebRTC плеере. Имя потока задается в поле Stream, а адрес сервера в поле Server. Подключение к серверу происходит по протоколу Websocket (wss), а поток приходит на плеер по WebRTC (UDP).


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

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

        Получаем следующие результаты:

        Test 1


        Test 2


        Test 3


        Как видите, каждый из зрителей видит одно и то же видео, с разбросом не более 130 миллисекунд.

        Таким образом задача реалтаймовой трансляции ролика с YouTube на WebRTC решена. Зрители получили поток практически одновременно. Менеджер не ушел пить кофе, программист — читать хабр, а маркетолог успешно показал всем ролик с  котиками.

        Хорошего стриминга!

        Ссылки


        youtube-dl  — библиотека для скачивания видео с YouTube
        ffmpeg — RTMP encoder
        Web Call Server — сервер, умеющий раздвать RTMP поток по WebRTC
        streamer.py — скрипт для интеграции youtube-dl и ffmpeg с отправкой RTMP потока
        Original source: habrahabr.ru (comments, light).

        https://habrahabr.ru/post/338098/


        Тянем ролик с YouTube и раздаем по WebRTC в реалтайме

        Понедельник, 18 Сентября 2017 г. 06:54 + в цитатник
        flashphoner сегодня в 06:54 Разработка

        Тянем ролик с YouTube и раздаем по WebRTC в реалтайме



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

          Ролик как стрим


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

          Для того, чтобы разброса не было, нужно раздавать этот ролик всем одновременно. Это можно реализовать, если обернуть ролик в Live-stream. Покажем как это сделать с помощью связки этой библиотеки с ffmpeg.


          Нам нужно реализовать схему, описанную выше. А именно, ydl подключается к YouTube и начинает скачивать ролик. FFmpeg подхватывает скачивающийся ролик, оборачивает его в RTMP поток и отправляет на сервер. Сервер раздает полученный поток как WebRTC в реальном времени.

          Установка youtube-dl


          Начинаем с установки youtube-dl. Процесс установки на Linux предельно простой и подробно описан в Readme под Lin и под Win.

          1. Скачиваем.

          curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl

          2. Даем права на запуск

          chmod a+rx /usr/local/bin/youtube-dl

          На этом все. YouTube скачивалка готова к работе.

          Возьмем ролик с YouTube и посмотрим на его мета-данные:

          youtube-dl --list-formats https://www.youtube.com/watch?v=9cQT4urTlXM

          Результат будет таким:

          [youtube] 9cQT4urTlXM: Downloading webpage
          [youtube] 9cQT4urTlXM: Downloading video info webpage
          [youtube] 9cQT4urTlXM: Extracting video information
          [youtube] 9cQT4urTlXM: Downloading MPD manifest
          [info] Available formats for 9cQT4urTlXM:
          format code  extension  resolution note
          171          webm       audio only DASH audio    8k , vorbis@128k, 540.24KiB
          249          webm       audio only DASH audio   10k , opus @ 50k, 797.30KiB
          250          webm       audio only DASH audio   10k , opus @ 70k, 797.30KiB
          251          webm       audio only DASH audio   10k , opus @160k, 797.30KiB
          139          m4a        audio only DASH audio   53k , m4a_dash container, mp4a.40.5@ 48k (22050Hz), 10.36MiB
          140          m4a        audio only DASH audio  137k , m4a_dash container, mp4a.40.2@128k (44100Hz), 27.56MiB
          278          webm       256x144    144p   41k , webm container, vp9, 30fps, video only, 6.54MiB
          242          webm       426x240    240p   70k , vp9, 30fps, video only, 13.42MiB
          243          webm       640x360    360p  101k , vp9, 30fps, video only, 20.55MiB
          160          mp4        256x144    DASH video  123k , avc1.4d400c, 15fps, video only, 24.83MiB
          134          mp4        640x360    DASH video  138k , avc1.4d401e, 30fps, video only, 28.07MiB
          244          webm       854x480    480p  149k , vp9, 30fps, video only, 30.55MiB
          135          mp4        854x480    DASH video  209k , avc1.4d401f, 30fps, video only, 42.42MiB
          133          mp4        426x240    DASH video  274k , avc1.4d4015, 30fps, video only, 57.63MiB
          247          webm       1280x720   720p  298k , vp9, 30fps, video only, 59.25MiB
          136          mp4        1280x720   DASH video  307k , avc1.4d401f, 30fps, video only, 62.58MiB
          17           3gp        176x144    small , mp4v.20.3, mp4a.40.2@ 24k
          36           3gp        320x180    small , mp4v.20.3, mp4a.40.2
          43           webm       640x360    medium , vp8.0, vorbis@128k
          18           mp4        640x360    medium , avc1.42001E, mp4a.40.2@ 96k
          22           mp4        1280x720   hd720 , avc1.64001F, mp4a.40.2@192k (best)


          Установка ffmpeg


          Далее устанавливаем ffmpeg стандартными заклинаниями:

          wget http://ffmpeg.org/releases/ffmpeg-3.3.4.tar.bz2
          tar -xvjf ffmpeg-3.3.4.tar.bz2
          cd  ffmpeg-3.3.4
          ./configure --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp
          make
          make install

          Проверяем что получилось

          ffmpeg -v

          Теперь самое интересное. Библиотека youtube-dl предназначена для скачивания. Она так и называется YouTube Download. Т.е. Можно скачать youtube ролик полностью и уже после этого застримить его через ffmpeg как файл.

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

          1. Маркетолог говорит — «А теперь, коллеги, давайте посмотрим этот ролик с котиками, он полностью отвечает нашей стратегии выхода на рынок», и жмет кнопку «показать всем ролик».
          2. На экране появляется прелоадер: «Подождите, ролик с котиками скачивается. Это займет не более 10 минут».
          3. Менеджер идет пить кофе, а программист — читать хабр.

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

          Передача данных из youtube-dl в ffmpeg


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

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

          #!/usr/bin/python
          
          import subprocess
          import sys
          
          def show_help():
              print 'Usage: '
              print './streamer.py url streamName destination'
              print './streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM streamName rtmp://192.168.88.59:1935/live'
              return
          
          def streamer() :
              url = sys.argv[1]
              if not url :
                  print 'Error: url is empty'
                  return
              stream_id = sys.argv[2]
              if not stream_id:
                  print 'Error: stream name is empty'
                  return
              destination = sys.argv[3]
              if not destination:
                  print 'Error: destination is empty'
                  return
          
              _youtube_process = subprocess.Popen(('youtube-dl','-f','','--prefer-ffmpeg', '--no-color', '--no-cache-dir', '--no-progress','-o', '-', '-f', '22/18', url, '--reject-title', stream_id),stdout=subprocess.PIPE)
              _ffmpeg_process = subprocess.Popen(('ffmpeg','-re','-i', '-','-preset', 'ultrafast','-vcodec', 'copy', '-acodec', 'copy','-threads','1', '-f', 'flv',destination + "/" + stream_id), stdin=_youtube_process.stdout)
              return
          
          if len(sys.argv) < 4:
              show_help()
          else:
              streamer()

          Этот питон-скрипт делает следующее:

          1. Создает подпроцесс _youtube_process зачитки ролика библиотекой youtube-dl
          2. Создает второй подпроцесс _ffmpeg_process, которому передаются данные из первого через pipe. Этот процесс уже создает RTMP поток и отправляет его на сервер по указанному адресу.


          Тестирование скрипта


          Для запуска скрипта нужно установить python. Скачать можно здесь.

          Мы при тестировании использовали версию 2.6.6. Скорее всего подойдет любая версия, т.к. скрипт достаточно простой и его задача — передать из одного процесса в другой.

          Запуск скрипта:
          python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://192.168.88.59:1935/live

          Как видите, передается три аргумента:

          1. Адрес youtube ролика.
            www.youtube.com/watch?v=9cQT4urTlXM
          2. Имя потока, с которым будет проходить RTMP-трансляция.
            stream1
          3. Адрес RTMP-сервера.
            rtmp://192.168.88.59:1935/live

          Для тестирования мы будем использовать Web Call Server. Он умеет принимать RTMP потоки и раздавать их по WebRTC. Здесь можно скачать и установить WCS5 на свой VPS или локальный тестовый сервер под управлением Linux.

          Схема тестирования с Web Call Server:


          Ниже мы задействуем для теста один из демо-серверов:

          rtmp://wcs5-eu.flashphoner.com:1935/live

          Это RTMP адрес, который нужно передать скрипту streamer.py чтобы быстро протестировать трансляцию с нашим демо-сервером.

          Запуск должен выглядеть так:

          python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live

          В консоли stdout увидим следующий вывод:

          # python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live
          ffmpeg version 3.2.3 Copyright (c) 2000-2017 the FFmpeg developers
            built with gcc 4.4.7 (GCC) 20120313 (Red Hat 4.4.7-11)
            configuration: --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp --disable-yasm
            libavutil      55. 34.101 / 55. 34.101
            libavcodec     57. 64.101 / 57. 64.101
            libavformat    57. 56.101 / 57. 56.101
            libavdevice    57.  1.100 / 57.  1.100
            libavfilter     6. 65.100 /  6. 65.100
            libswscale      4.  2.100 /  4.  2.100
            libswresample   2.  3.100 /  2.  3.100
            libpostproc    54.  1.100 / 54.  1.100
          ]# [youtube] 9cQT4urTlXM: Downloading webpage
          [youtube] 9cQT4urTlXM: Downloading video info webpage
          [youtube] 9cQT4urTlXM: Extracting video information
          [youtube] 9cQT4urTlXM: Downloading MPD manifest
          [download] Destination: -
          Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pipe:':
            Metadata:
              major_brand     : mp42
              minor_version   : 0
              compatible_brands: isommp42
              creation_time   : 2016-08-23T12:21:06.000000Z
            Duration: 00:29:59.99, start: 0.000000, bitrate: N/A
              Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 288 kb/s, 30 fps, 30 tbr, 90k tbn, 60 tbc (default)
              Metadata:
                creation_time   : 2016-08-23T12:21:06.000000Z
                handler_name    : ISO Media file produced by Google Inc.
              Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
              Metadata:
                creation_time   : 2016-08-23T12:21:06.000000Z
                handler_name    : ISO Media file produced by Google Inc.
          Output #0, flv, to 'rtmp://192.168.88.59:1935/live/stream1':
            Metadata:
              major_brand     : mp42
              minor_version   : 0
              compatible_brands: isommp42
              encoder         : Lavf57.56.101
              Stream #0:0(und): Video: h264 (Main) ([7][0][0][0] / 0x0007), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 288 kb/s, 30 fps, 30 tbr, 1k tbn, 90k tbc (default)
              Metadata:
                creation_time   : 2016-08-23T12:21:06.000000Z
                handler_name    : ISO Media file produced by Google Inc.
              Stream #0:1(und): Audio: aac (LC) ([10][0][0][0] / 0x000A), 44100 Hz, stereo, 125 kb/s (default)
              Metadata:
                creation_time   : 2016-08-23T12:21:06.000000Z
                handler_name    : ISO Media file produced by Google Inc.
          Stream mapping:
            Stream #0:0 -> #0:0 (copy)
            Stream #0:1 -> #0:1 (copy)
          frame=  383 fps= 30 q=-1.0 size=     654kB time=00:00:12.70 bitrate= 421.8kbits/s speed=   1x

          Если бегло пробежать по этому логу, то можно понять, что происходит следующее:

          1. Открывается страница с видеороликом.
          2. Извлекаются данные о видео форматах.
          3. Скачивается mp4 ролик 1280x720, H.264+AAC
          4. Запускается ffmpeg, подхватывает скачиваемые данные и стримит по RTMP с битрейтом 421 kbps. Такой скудный битрейт объясняется выбранным роликом  с таймером. Нормальный видеоролик даст на порядок большее значение битрейта.

          После того, как процесс стриминга запустился, пытаемся проиграть поток в WebRTC плеере. Имя потока задается в поле Stream, а адрес сервера в поле Server. Подключение к серверу происходит по протоколу Websocket (wss), а поток приходит на плеер по WebRTC (UDP).


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

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

          Получаем следующие результаты:

          Test 1


          Test 2


          Test 3


          Как видите, каждый из зрителей видит одно и то же видео, с разбросом не более 130 миллисекунд.

          Таким образом задача реалтаймовой трансляции ролика с YouTube на WebRTC решена. Зрители получили поток практически одновременно. Менеджер не ушел пить кофе, программист — читать хабр, а маркетолог успешно показал всем ролик с  котиками.

          Хорошего стриминга!

          Ссылки


          youtube-dl  — библиотека для скачивания видео с YouTube
          ffmpeg — RTMP encoder
          Web Call Server — сервер, умеющий раздвать RTMP поток по WebRTC
          streamer.py — скрипт для интеграции youtube-dl и ffmpeg с отправкой RTMP потока
          Original source: habrahabr.ru (comments, light).

          https://habrahabr.ru/post/338098/


          Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

          Понедельник, 18 Сентября 2017 г. 06:28 + в цитатник
          vivchar сегодня в 06:28 Разработка

          Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

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

            Сегодня мы разберем:

            • как можно упростить поддержку DiffUtil в этой реализации;
            • как добавить поддержку вложенных RecyclerView.


            Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.

            DiffUtil


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

            В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
                    ...
                    final MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());
                    final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
            
                    mRecyclerViewAdapter.setItems(getNewItems());
                    diffResult.dispatchUpdatesTo(mRecyclerViewAdapter);
                    ...
            

            Классическая реализация DiffUtil.Callback
            public class MyDiffCallback extends DiffUtil.Callback {
            
                private final List mOldList;
                private final List mNewList;
            
                public MyDiffCallback(List oldList, List newList) {
                    mOldList = oldList;
                    mNewList = newList;
                }
            
                @Override
                public int getOldListSize() {
                    return mOldList.size();
                }
            
                @Override
                public int getNewListSize() {
                    return mNewList.size();
                }
            
                @Override
                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                    return mOldList.get(oldItemPosition).getID() == mNewList.get(
                            newItemPosition).getID();
                }
            
                @Override
                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                    BaseItemModel oldItem = mOldList.get(oldItemPosition);
                    BaseItemModel newItem = mNewList.get(newItemPosition);
            
                    return oldItem.equals(newItem);
                }
            
                @Nullable
                @Override
                public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                    return super.getChangePayload(oldItemPosition, newItemPosition);
                }
            }
            


            И расширенный интерфейс ItemModel:
            public interface BaseItemModel extends ItemModel {
            	int getID();
            }
            


            В общем-то реализуемо и не сложно, но если это делать в нескольких местах, то стоит задуматься зачем столько много одинакового кода. Попробуем вынести общие моменты в свою реализацию DiffUtil.Callback:
            public abstract static class DiffCallback  extends DiffUtil.Callback {
            
            	private final List mOldItems = new ArrayList<>();
            	private final List mNewItems = new ArrayList<>();
            
            	void setItems(List oldItems, List newItems) {
            		mOldItems.clear();
            		mOldItems.addAll(oldItems);
            
            		mNewItems.clear();
            		mNewItems.addAll(newItems);
            	}
            
            	@Override
            	public int getOldListSize() {
            		return mOldItems.size();
            	}
            
            	@Override
            	public int getNewListSize() {
            		return mNewItems.size();
            	}
            
            	@Override
            	public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            		return areItemsTheSame(
            				mOldItems.get(oldItemPosition),
            				mNewItems.get(newItemPosition)
            		);
            	}
            
            	public abstract boolean areItemsTheSame(BM oldItem, BM newItem);
            
            	@Override
            	public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            		return areContentsTheSame(
            				mOldItems.get(oldItemPosition),
            				mNewItems.get(newItemPosition)
            		);
            	}
            
            	public abstract boolean areContentsTheSame(BM oldItem, BM newItem);
            
                    ...
            }
            


            В общем получилось достаточно универсально, мы избавились от рутинны и сосредоточились на главных методах — areItemsTheSame() и areContentsTheSame(), которые обязательны к реализации и могут отличаться.
            Реализация метода getChangePayload() намеренно пропущена, её реализацию можно посмотреть в исходниках.

            Теперь мы можем добавить еще один метод с поддержкой DiffUtil в наш адаптер:
            public void setItems(List items, DiffCallback diffCallback) {
            	diffCallback.setItems(mItems, items);
            
            	final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
            
            	mItems.clear();
            	mItems.addAll(items);
            
            	diffResult.dispatchUpdatesTo(this);
            }
            


            В общем то с DiffUtil это все, теперь при необходимости мы используем наш абстрактный класс — DiffCallback, и реализуем всего два метода.

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

            Вложенные RecyclerView


            Часто по воле заказчика или веянию дизайнеров в нашем приложении появляются вложенные списки. До недавних пор я недолюбливал их, я сталкивался с такими проблемами:

            • сложность реализации ячейки, которая содержит RecyclerView;
            • сложность обновление данных во вложенных ячейках;
            • непереиспользуемость вложенных ячеек;
            • дублирование кода;
            • запутанность проброса кликов от вложенных ячеек в корневое место — Fragment/Activity;


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

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


            Важно заметить, что здесь я разделил понятие ячейка и элемент списка:
            элемент списка — сущность используемая в RecyclerView.
            ячейка — набор классов, позволяющих отобразить один тип элемента списка, в нашем случае это реализация ранее известных классов и интерфейсов: ViewRenderer, ItemModel, ViewHolder.

            И так, что мы имеем. Ключевым интерфесом у нас является ItemModel, очевидно что нам удобно будет далее с ним и работать. Наша композитная модель должна содержать в себе дочерние модели, добавляем новый интерфейс:
            public interface CompositeItemModel extends ItemModel {
            	List getItems();
            }
            


            Выглядит неплохо, соответсвенно, композитный ViewRenderer должен знать о дочерних рендерерах — добавляем:
            public abstract class CompositeViewRenderer  extends ViewRenderer {
            
            	private final ArrayList mRenderers = new ArrayList<>();
            
            	public CompositeViewRenderer(int viewType, Context context) {
            		super(viewType, context);
            	}
            
            	public CompositeViewRenderer(int viewType, Context context, ViewRenderer... renderers) {
            		super(viewType, context);
            		Collections.addAll(mRenderers, renderers);
            	}
            
            	public CompositeViewRenderer registerRenderer(ViewRenderer renderer) {
            		mRenderers.add(renderer);
            		return this;
            	}
            
            	public void bindView(M model, VH holder) {}
            
                    public VH createViewHolder(ViewGroup parent) { return ...; }
                    ...
            }
            


            Здесь я добавил два способа добавления дочерних рендереров, уверен, они нам пригодятся.
            Так же обратите внимание на генерик CompositeViewHolder — это будет тоже отдельный класс для композитного ViewHolder, что там будет пока не знаю. А сейчас продолжим работу с CompositeViewRenderer, у нас осталось два обязательных метода — bindView(), createViewHolder(). В createViewHolder() нужно инициализировать адаптер и познакомить его с рендерами, а в bindView() сделаем простое, дефолтное обновление элементов:
            public abstract class CompositeViewRenderer  extends ViewRenderer {
            
            	private final ArrayList mRenderers = new ArrayList<>();
                    private RendererRecyclerViewAdapter mAdapter;
            
                    ...        
            
            	public void bindView(M model, VH holder) {
            		mAdapter.setItems(model.getItems());
            		mAdapter.notifyDataSetChanged();
            	}
            
            	public VH createViewHolder(ViewGroup parent) {
            		mAdapter = new RendererRecyclerViewAdapter();
            
            		for (final ViewRenderer renderer : mRenderers) {
            			mAdapter.registerRenderer(renderer);
            		}
            
            		return ???;
            	}
            
                    ...
            }
            


            Почти получилось, как оказалось, для такой реализации в методе createViewHolder() нам нужен сам viewHolder, инициализировать мы его тут не можем — создаем отдельный абстрактный метод, заодно хотелось бы тут познакомить наш адаптер с RecyclerView, который мы можем взять у нереализованного CompositeViewHolder — реализуем:
            public abstract class CompositeViewHolder extends RecyclerView.ViewHolder {
            
            	public RecyclerView mRecyclerView;
            
            	public CompositeViewHolder(View itemView) {
            		super(itemView);
            	}
            }
            

            public abstract class CompositeViewRenderer  extends ViewRenderer {
            
            	public VH createViewHolder(ViewGroup parent) {
            		mAdapter = new RendererRecyclerViewAdapter();
            
            		for (final ViewRenderer renderer : mRenderers) {
            			mAdapter.registerRenderer(renderer);
            		}
            
            	        VH viewHolder = createCompositeViewHolder(parent);
            		viewHolder.mRecyclerView.setLayoutManager(createLayoutManager());
            		viewHolder.mRecyclerView.setAdapter(mAdapter);
            
            		return viewHolder;
            	}
            
                    public abstract VH createCompositeViewHolder(ViewGroup parent);
            
            	protected RecyclerView.LayoutManager createLayoutManager() {
            		return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
            	}
                    ...
            }
            


            Да, верно! Я добавил дефолтную реализацию с LinearLayoutManager :( посчитал что это принесет больше пользы, а при необходимости можно метод перегрузить и выставить другой LayoutManager.

            Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:

            SomeCompositeItemModel
            public class SomeCompositeItemModel implements CompositeItemModel {
            
            	public static final int TYPE = 999;
            	private int mID;
            	private final List mItems;
            
            	public SomeCompositeItemModel(final int ID, List items) {
            		mID = ID;
            		mItems = items;
            	}
            
            	public int getID() {
            		return mID;
            	}
            
            	public int getType() {
            		return TYPE;
            	}
            
            	public List getItems() {
            		return mItems;
            	}
            }
            


            SomeCompositeViewHolder
            public class SomeCompositeViewHolder extends CompositeViewHolder {
            
            	public SomeCompositeViewHolder(View view) {
            		super(view);
            		mRecyclerView = (RecyclerView) view.findViewById(R.id.composite_recycler_view);
            	}
            }
            


            SomeCompositeViewRenderer
            public class SomeCompositeViewRenderer extends CompositeViewRenderer {
            
            	public SomeCompositeViewRenderer(int viewType, Context context) {
            		super(viewType, context);
             	}
            
            	public SomeCompositeViewHolder createCompositeViewHolder(ViewGroup parent) {
                            return new SomeCompositeViewHolder(inflate(R.layout.item_composite, parent));
            	}
            }
            



            Регистрируем наш композитный рендерер:
            public class SomeActivity extends AppCompatActivity {
            
                    protected void onCreate(final Bundle savedInstanceState) {
                            super.onCreate(savedInstanceState);
            
                            ...
                            SomeCompositeViewRenderer composite = new SomeCompositeViewRenderer(
                                    SomeCompositeItemModel.TYPE, 
                                    this,
                                    new SomeViewRenderer(SomeModel.TYPE, this, mListener)
                            );
            
                            mRecyclerViewAdapter.registerRenderer(composite);
            
                            ...
                    }
                    ...
            }
            


            Как видно из последнего семпла, для подписки на клики мы просто передаем необходимый интерфейс в конструктор рендерера, таким образом наше корневое место реализует этот интерфейс и знает о всех необходимых кликах
            Пример проброса кликов
            public class SomeViewRenderer extends ViewRenderer {
            
            	private final Listener mListener;
            
            	public SomeViewRenderer(int type, Context context, Listener listener) {
            		super(type, context);
            		mListener = listener;
            	}
            
            	public void bindView(SomeModel model, SomeViewHolder holder) {
            		...
            		holder.itemView.setOnClickListener(new View.OnClickListener() {
            			public void onClick(final View view) {
            				mListener.onSomeItemClicked(model);
            			}
            		});
            	}
            
                    ...
            
                    public interface Listener {
            		void onSomeItemClicked(SomeModel model);
            	}
            }
            


            Заключение

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

            Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
            Original source: habrahabr.ru (comments, light).

            https://habrahabr.ru/post/337774/


            Метки:  

            Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

            Понедельник, 18 Сентября 2017 г. 06:28 + в цитатник
            vivchar сегодня в 06:28 Разработка

            Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

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

              Сегодня мы разберем:

              • как можно упростить поддержку DiffUtil в этой реализации;
              • как добавить поддержку вложенных RecyclerView.


              Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.

              DiffUtil


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

              В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
                      ...
                      final MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());
                      final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
              
                      mRecyclerViewAdapter.setItems(getNewItems());
                      diffResult.dispatchUpdatesTo(mRecyclerViewAdapter);
                      ...
              

              Классическая реализация DiffUtil.Callback
              public class MyDiffCallback extends DiffUtil.Callback {
              
                  private final List mOldList;
                  private final List mNewList;
              
                  public MyDiffCallback(List oldList, List newList) {
                      mOldList = oldList;
                      mNewList = newList;
                  }
              
                  @Override
                  public int getOldListSize() {
                      return mOldList.size();
                  }
              
                  @Override
                  public int getNewListSize() {
                      return mNewList.size();
                  }
              
                  @Override
                  public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                      return mOldList.get(oldItemPosition).getID() == mNewList.get(
                              newItemPosition).getID();
                  }
              
                  @Override
                  public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                      BaseItemModel oldItem = mOldList.get(oldItemPosition);
                      BaseItemModel newItem = mNewList.get(newItemPosition);
              
                      return oldItem.equals(newItem);
                  }
              
                  @Nullable
                  @Override
                  public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                      return super.getChangePayload(oldItemPosition, newItemPosition);
                  }
              }
              


              И расширенный интерфейс ItemModel:
              public interface BaseItemModel extends ItemModel {
              	int getID();
              }
              


              В общем-то реализуемо и не сложно, но если это делать в нескольких местах, то стоит задуматься зачем столько много одинакового кода. Попробуем вынести общие моменты в свою реализацию DiffUtil.Callback:
              public abstract static class DiffCallback  extends DiffUtil.Callback {
              
              	private final List mOldItems = new ArrayList<>();
              	private final List mNewItems = new ArrayList<>();
              
              	void setItems(List oldItems, List newItems) {
              		mOldItems.clear();
              		mOldItems.addAll(oldItems);
              
              		mNewItems.clear();
              		mNewItems.addAll(newItems);
              	}
              
              	@Override
              	public int getOldListSize() {
              		return mOldItems.size();
              	}
              
              	@Override
              	public int getNewListSize() {
              		return mNewItems.size();
              	}
              
              	@Override
              	public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
              		return areItemsTheSame(
              				mOldItems.get(oldItemPosition),
              				mNewItems.get(newItemPosition)
              		);
              	}
              
              	public abstract boolean areItemsTheSame(BM oldItem, BM newItem);
              
              	@Override
              	public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
              		return areContentsTheSame(
              				mOldItems.get(oldItemPosition),
              				mNewItems.get(newItemPosition)
              		);
              	}
              
              	public abstract boolean areContentsTheSame(BM oldItem, BM newItem);
              
                      ...
              }
              


              В общем получилось достаточно универсально, мы избавились от рутинны и сосредоточились на главных методах — areItemsTheSame() и areContentsTheSame(), которые обязательны к реализации и могут отличаться.
              Реализация метода getChangePayload() намеренно пропущена, её реализацию можно посмотреть в исходниках.

              Теперь мы можем добавить еще один метод с поддержкой DiffUtil в наш адаптер:
              public void setItems(List items, DiffCallback diffCallback) {
              	diffCallback.setItems(mItems, items);
              
              	final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
              
              	mItems.clear();
              	mItems.addAll(items);
              
              	diffResult.dispatchUpdatesTo(this);
              }
              


              В общем то с DiffUtil это все, теперь при необходимости мы используем наш абстрактный класс — DiffCallback, и реализуем всего два метода.

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

              Вложенные RecyclerView


              Часто по воле заказчика или веянию дизайнеров в нашем приложении появляются вложенные списки. До недавних пор я недолюбливал их, я сталкивался с такими проблемами:

              • сложность реализации ячейки, которая содержит RecyclerView;
              • сложность обновление данных во вложенных ячейках;
              • непереиспользуемость вложенных ячеек;
              • дублирование кода;
              • запутанность проброса кликов от вложенных ячеек в корневое место — Fragment/Activity;


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

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


              Важно заметить, что здесь я разделил понятие ячейка и элемент списка:
              элемент списка — сущность используемая в RecyclerView.
              ячейка — набор классов, позволяющих отобразить один тип элемента списка, в нашем случае это реализация ранее известных классов и интерфейсов: ViewRenderer, ItemModel, ViewHolder.

              И так, что мы имеем. Ключевым интерфесом у нас является ItemModel, очевидно что нам удобно будет далее с ним и работать. Наша композитная модель должна содержать в себе дочерние модели, добавляем новый интерфейс:
              public interface CompositeItemModel extends ItemModel {
              	List getItems();
              }
              


              Выглядит неплохо, соответсвенно, композитный ViewRenderer должен знать о дочерних рендерерах — добавляем:
              public abstract class CompositeViewRenderer  extends ViewRenderer {
              
              	private final ArrayList mRenderers = new ArrayList<>();
              
              	public CompositeViewRenderer(int viewType, Context context) {
              		super(viewType, context);
              	}
              
              	public CompositeViewRenderer(int viewType, Context context, ViewRenderer... renderers) {
              		super(viewType, context);
              		Collections.addAll(mRenderers, renderers);
              	}
              
              	public CompositeViewRenderer registerRenderer(ViewRenderer renderer) {
              		mRenderers.add(renderer);
              		return this;
              	}
              
              	public void bindView(M model, VH holder) {}
              
                      public VH createViewHolder(ViewGroup parent) { return ...; }
                      ...
              }
              


              Здесь я добавил два способа добавления дочерних рендереров, уверен, они нам пригодятся.
              Так же обратите внимание на генерик CompositeViewHolder — это будет тоже отдельный класс для композитного ViewHolder, что там будет пока не знаю. А сейчас продолжим работу с CompositeViewRenderer, у нас осталось два обязательных метода — bindView(), createViewHolder(). В createViewHolder() нужно инициализировать адаптер и познакомить его с рендерами, а в bindView() сделаем простое, дефолтное обновление элементов:
              public abstract class CompositeViewRenderer  extends ViewRenderer {
              
              	private final ArrayList mRenderers = new ArrayList<>();
                      private RendererRecyclerViewAdapter mAdapter;
              
                      ...        
              
              	public void bindView(M model, VH holder) {
              		mAdapter.setItems(model.getItems());
              		mAdapter.notifyDataSetChanged();
              	}
              
              	public VH createViewHolder(ViewGroup parent) {
              		mAdapter = new RendererRecyclerViewAdapter();
              
              		for (final ViewRenderer renderer : mRenderers) {
              			mAdapter.registerRenderer(renderer);
              		}
              
              		return ???;
              	}
              
                      ...
              }
              


              Почти получилось, как оказалось, для такой реализации в методе createViewHolder() нам нужен сам viewHolder, инициализировать мы его тут не можем — создаем отдельный абстрактный метод, заодно хотелось бы тут познакомить наш адаптер с RecyclerView, который мы можем взять у нереализованного CompositeViewHolder — реализуем:
              public abstract class CompositeViewHolder extends RecyclerView.ViewHolder {
              
              	public RecyclerView mRecyclerView;
              
              	public CompositeViewHolder(View itemView) {
              		super(itemView);
              	}
              }
              

              public abstract class CompositeViewRenderer  extends ViewRenderer {
              
              	public VH createViewHolder(ViewGroup parent) {
              		mAdapter = new RendererRecyclerViewAdapter();
              
              		for (final ViewRenderer renderer : mRenderers) {
              			mAdapter.registerRenderer(renderer);
              		}
              
              	        VH viewHolder = createCompositeViewHolder(parent);
              		viewHolder.mRecyclerView.setLayoutManager(createLayoutManager());
              		viewHolder.mRecyclerView.setAdapter(mAdapter);
              
              		return viewHolder;
              	}
              
                      public abstract VH createCompositeViewHolder(ViewGroup parent);
              
              	protected RecyclerView.LayoutManager createLayoutManager() {
              		return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
              	}
                      ...
              }
              


              Да, верно! Я добавил дефолтную реализацию с LinearLayoutManager :( посчитал что это принесет больше пользы, а при необходимости можно метод перегрузить и выставить другой LayoutManager.

              Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:

              SomeCompositeItemModel
              public class SomeCompositeItemModel implements CompositeItemModel {
              
              	public static final int TYPE = 999;
              	private int mID;
              	private final List mItems;
              
              	public SomeCompositeItemModel(final int ID, List items) {
              		mID = ID;
              		mItems = items;
              	}
              
              	public int getID() {
              		return mID;
              	}
              
              	public int getType() {
              		return TYPE;
              	}
              
              	public List getItems() {
              		return mItems;
              	}
              }
              


              SomeCompositeViewHolder
              public class SomeCompositeViewHolder extends CompositeViewHolder {
              
              	public SomeCompositeViewHolder(View view) {
              		super(view);
              		mRecyclerView = (RecyclerView) view.findViewById(R.id.composite_recycler_view);
              	}
              }
              


              SomeCompositeViewRenderer
              public class SomeCompositeViewRenderer extends CompositeViewRenderer {
              
              	public SomeCompositeViewRenderer(int viewType, Context context) {
              		super(viewType, context);
               	}
              
              	public SomeCompositeViewHolder createCompositeViewHolder(ViewGroup parent) {
                              return new SomeCompositeViewHolder(inflate(R.layout.item_composite, parent));
              	}
              }
              



              Регистрируем наш композитный рендерер:
              public class SomeActivity extends AppCompatActivity {
              
                      protected void onCreate(final Bundle savedInstanceState) {
                              super.onCreate(savedInstanceState);
              
                              ...
                              SomeCompositeViewRenderer composite = new SomeCompositeViewRenderer(
                                      SomeCompositeItemModel.TYPE, 
                                      this,
                                      new SomeViewRenderer(SomeModel.TYPE, this, mListener)
                              );
              
                              mRecyclerViewAdapter.registerRenderer(composite);
              
                              ...
                      }
                      ...
              }
              


              Как видно из последнего семпла, для подписки на клики мы просто передаем необходимый интерфейс в конструктор рендерера, таким образом наше корневое место реализует этот интерфейс и знает о всех необходимых кликах
              Пример проброса кликов
              public class SomeViewRenderer extends ViewRenderer {
              
              	private final Listener mListener;
              
              	public SomeViewRenderer(int type, Context context, Listener listener) {
              		super(type, context);
              		mListener = listener;
              	}
              
              	public void bindView(SomeModel model, SomeViewHolder holder) {
              		...
              		holder.itemView.setOnClickListener(new View.OnClickListener() {
              			public void onClick(final View view) {
              				mListener.onSomeItemClicked(model);
              			}
              		});
              	}
              
                      ...
              
                      public interface Listener {
              		void onSomeItemClicked(SomeModel model);
              	}
              }
              


              Заключение

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

              Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
              Original source: habrahabr.ru (comments, light).

              https://habrahabr.ru/post/337774/


              Метки:  

              Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

              Понедельник, 18 Сентября 2017 г. 06:28 + в цитатник
              vivchar сегодня в 06:28 Разработка

              Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

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

                Сегодня мы разберем:

                • как можно упростить поддержку DiffUtil в этой реализации;
                • как добавить поддержку вложенных RecyclerView.


                Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.

                DiffUtil


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

                В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
                        ...
                        final MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());
                        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
                
                        mRecyclerViewAdapter.setItems(getNewItems());
                        diffResult.dispatchUpdatesTo(mRecyclerViewAdapter);
                        ...
                

                Классическая реализация DiffUtil.Callback
                public class MyDiffCallback extends DiffUtil.Callback {
                
                    private final List mOldList;
                    private final List mNewList;
                
                    public MyDiffCallback(List oldList, List newList) {
                        mOldList = oldList;
                        mNewList = newList;
                    }
                
                    @Override
                    public int getOldListSize() {
                        return mOldList.size();
                    }
                
                    @Override
                    public int getNewListSize() {
                        return mNewList.size();
                    }
                
                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                        return mOldList.get(oldItemPosition).getID() == mNewList.get(
                                newItemPosition).getID();
                    }
                
                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        BaseItemModel oldItem = mOldList.get(oldItemPosition);
                        BaseItemModel newItem = mNewList.get(newItemPosition);
                
                        return oldItem.equals(newItem);
                    }
                
                    @Nullable
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                        return super.getChangePayload(oldItemPosition, newItemPosition);
                    }
                }
                


                И расширенный интерфейс ItemModel:
                public interface BaseItemModel extends ItemModel {
                	int getID();
                }
                


                В общем-то реализуемо и не сложно, но если это делать в нескольких местах, то стоит задуматься зачем столько много одинакового кода. Попробуем вынести общие моменты в свою реализацию DiffUtil.Callback:
                public abstract static class DiffCallback  extends DiffUtil.Callback {
                
                	private final List mOldItems = new ArrayList<>();
                	private final List mNewItems = new ArrayList<>();
                
                	void setItems(List oldItems, List newItems) {
                		mOldItems.clear();
                		mOldItems.addAll(oldItems);
                
                		mNewItems.clear();
                		mNewItems.addAll(newItems);
                	}
                
                	@Override
                	public int getOldListSize() {
                		return mOldItems.size();
                	}
                
                	@Override
                	public int getNewListSize() {
                		return mNewItems.size();
                	}
                
                	@Override
                	public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                		return areItemsTheSame(
                				mOldItems.get(oldItemPosition),
                				mNewItems.get(newItemPosition)
                		);
                	}
                
                	public abstract boolean areItemsTheSame(BM oldItem, BM newItem);
                
                	@Override
                	public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                		return areContentsTheSame(
                				mOldItems.get(oldItemPosition),
                				mNewItems.get(newItemPosition)
                		);
                	}
                
                	public abstract boolean areContentsTheSame(BM oldItem, BM newItem);
                
                        ...
                }
                


                В общем получилось достаточно универсально, мы избавились от рутинны и сосредоточились на главных методах — areItemsTheSame() и areContentsTheSame(), которые обязательны к реализации и могут отличаться.
                Реализация метода getChangePayload() намеренно пропущена, её реализацию можно посмотреть в исходниках.

                Теперь мы можем добавить еще один метод с поддержкой DiffUtil в наш адаптер:
                public void setItems(List items, DiffCallback diffCallback) {
                	diffCallback.setItems(mItems, items);
                
                	final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
                
                	mItems.clear();
                	mItems.addAll(items);
                
                	diffResult.dispatchUpdatesTo(this);
                }
                


                В общем то с DiffUtil это все, теперь при необходимости мы используем наш абстрактный класс — DiffCallback, и реализуем всего два метода.

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

                Вложенные RecyclerView


                Часто по воле заказчика или веянию дизайнеров в нашем приложении появляются вложенные списки. До недавних пор я недолюбливал их, я сталкивался с такими проблемами:

                • сложность реализации ячейки, которая содержит RecyclerView;
                • сложность обновление данных во вложенных ячейках;
                • непереиспользуемость вложенных ячеек;
                • дублирование кода;
                • запутанность проброса кликов от вложенных ячеек в корневое место — Fragment/Activity;


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

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


                Важно заметить, что здесь я разделил понятие ячейка и элемент списка:
                элемент списка — сущность используемая в RecyclerView.
                ячейка — набор классов, позволяющих отобразить один тип элемента списка, в нашем случае это реализация ранее известных классов и интерфейсов: ViewRenderer, ItemModel, ViewHolder.

                И так, что мы имеем. Ключевым интерфесом у нас является ItemModel, очевидно что нам удобно будет далее с ним и работать. Наша композитная модель должна содержать в себе дочерние модели, добавляем новый интерфейс:
                public interface CompositeItemModel extends ItemModel {
                	List getItems();
                }
                


                Выглядит неплохо, соответсвенно, композитный ViewRenderer должен знать о дочерних рендерерах — добавляем:
                public abstract class CompositeViewRenderer  extends ViewRenderer {
                
                	private final ArrayList mRenderers = new ArrayList<>();
                
                	public CompositeViewRenderer(int viewType, Context context) {
                		super(viewType, context);
                	}
                
                	public CompositeViewRenderer(int viewType, Context context, ViewRenderer... renderers) {
                		super(viewType, context);
                		Collections.addAll(mRenderers, renderers);
                	}
                
                	public CompositeViewRenderer registerRenderer(ViewRenderer renderer) {
                		mRenderers.add(renderer);
                		return this;
                	}
                
                	public void bindView(M model, VH holder) {}
                
                        public VH createViewHolder(ViewGroup parent) { return ...; }
                        ...
                }
                


                Здесь я добавил два способа добавления дочерних рендереров, уверен, они нам пригодятся.
                Так же обратите внимание на генерик CompositeViewHolder — это будет тоже отдельный класс для композитного ViewHolder, что там будет пока не знаю. А сейчас продолжим работу с CompositeViewRenderer, у нас осталось два обязательных метода — bindView(), createViewHolder(). В createViewHolder() нужно инициализировать адаптер и познакомить его с рендерами, а в bindView() сделаем простое, дефолтное обновление элементов:
                public abstract class CompositeViewRenderer  extends ViewRenderer {
                
                	private final ArrayList mRenderers = new ArrayList<>();
                        private RendererRecyclerViewAdapter mAdapter;
                
                        ...        
                
                	public void bindView(M model, VH holder) {
                		mAdapter.setItems(model.getItems());
                		mAdapter.notifyDataSetChanged();
                	}
                
                	public VH createViewHolder(ViewGroup parent) {
                		mAdapter = new RendererRecyclerViewAdapter();
                
                		for (final ViewRenderer renderer : mRenderers) {
                			mAdapter.registerRenderer(renderer);
                		}
                
                		return ???;
                	}
                
                        ...
                }
                


                Почти получилось, как оказалось, для такой реализации в методе createViewHolder() нам нужен сам viewHolder, инициализировать мы его тут не можем — создаем отдельный абстрактный метод, заодно хотелось бы тут познакомить наш адаптер с RecyclerView, который мы можем взять у нереализованного CompositeViewHolder — реализуем:
                public abstract class CompositeViewHolder extends RecyclerView.ViewHolder {
                
                	public RecyclerView mRecyclerView;
                
                	public CompositeViewHolder(View itemView) {
                		super(itemView);
                	}
                }
                

                public abstract class CompositeViewRenderer  extends ViewRenderer {
                
                	public VH createViewHolder(ViewGroup parent) {
                		mAdapter = new RendererRecyclerViewAdapter();
                
                		for (final ViewRenderer renderer : mRenderers) {
                			mAdapter.registerRenderer(renderer);
                		}
                
                	        VH viewHolder = createCompositeViewHolder(parent);
                		viewHolder.mRecyclerView.setLayoutManager(createLayoutManager());
                		viewHolder.mRecyclerView.setAdapter(mAdapter);
                
                		return viewHolder;
                	}
                
                        public abstract VH createCompositeViewHolder(ViewGroup parent);
                
                	protected RecyclerView.LayoutManager createLayoutManager() {
                		return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
                	}
                        ...
                }
                


                Да, верно! Я добавил дефолтную реализацию с LinearLayoutManager :( посчитал что это принесет больше пользы, а при необходимости можно метод перегрузить и выставить другой LayoutManager.

                Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:

                SomeCompositeItemModel
                public class SomeCompositeItemModel implements CompositeItemModel {
                
                	public static final int TYPE = 999;
                	private int mID;
                	private final List mItems;
                
                	public SomeCompositeItemModel(final int ID, List items) {
                		mID = ID;
                		mItems = items;
                	}
                
                	public int getID() {
                		return mID;
                	}
                
                	public int getType() {
                		return TYPE;
                	}
                
                	public List getItems() {
                		return mItems;
                	}
                }
                


                SomeCompositeViewHolder
                public class SomeCompositeViewHolder extends CompositeViewHolder {
                
                	public SomeCompositeViewHolder(View view) {
                		super(view);
                		mRecyclerView = (RecyclerView) view.findViewById(R.id.composite_recycler_view);
                	}
                }
                


                SomeCompositeViewRenderer
                public class SomeCompositeViewRenderer extends CompositeViewRenderer {
                
                	public SomeCompositeViewRenderer(int viewType, Context context) {
                		super(viewType, context);
                 	}
                
                	public SomeCompositeViewHolder createCompositeViewHolder(ViewGroup parent) {
                                return new SomeCompositeViewHolder(inflate(R.layout.item_composite, parent));
                	}
                }
                



                Регистрируем наш композитный рендерер:
                public class SomeActivity extends AppCompatActivity {
                
                        protected void onCreate(final Bundle savedInstanceState) {
                                super.onCreate(savedInstanceState);
                
                                ...
                                SomeCompositeViewRenderer composite = new SomeCompositeViewRenderer(
                                        SomeCompositeItemModel.TYPE, 
                                        this,
                                        new SomeViewRenderer(SomeModel.TYPE, this, mListener)
                                );
                
                                mRecyclerViewAdapter.registerRenderer(composite);
                
                                ...
                        }
                        ...
                }
                


                Как видно из последнего семпла, для подписки на клики мы просто передаем необходимый интерфейс в конструктор рендерера, таким образом наше корневое место реализует этот интерфейс и знает о всех необходимых кликах
                Пример проброса кликов
                public class SomeViewRenderer extends ViewRenderer {
                
                	private final Listener mListener;
                
                	public SomeViewRenderer(int type, Context context, Listener listener) {
                		super(type, context);
                		mListener = listener;
                	}
                
                	public void bindView(SomeModel model, SomeViewHolder holder) {
                		...
                		holder.itemView.setOnClickListener(new View.OnClickListener() {
                			public void onClick(final View view) {
                				mListener.onSomeItemClicked(model);
                			}
                		});
                	}
                
                        ...
                
                        public interface Listener {
                		void onSomeItemClicked(SomeModel model);
                	}
                }
                


                Заключение

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

                Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/337774/


                Метки:  

                5 правил работы с суммами

                Понедельник, 18 Сентября 2017 г. 03:30 + в цитатник

                Метки:  

                [Перевод] «Паттерны» функционального программирования

                Понедельник, 18 Сентября 2017 г. 00:31 + в цитатник
                marshinov сегодня в 00:31 Разработка

                «Паттерны» функционального программирования

                • Перевод
                • Tutorial

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

                До недавнего времени такой взгляд на вещи действительно был недалек от истины: говорим ФП, подразумеваем Хаскель и теорию категорий. В последнее время ситуация изменилась и функциональная парадигма набирает обороты в web-разработке, не без помощи F#, Scala и React. Попробуем взглянуть на «паттерны» функционального программирования, полезные для решения повседневных задач с точки зрения ООП – парадигмы.

                ООП широко распространено в разработке прикладного ПО не одно десятилетие. Все мы знакомы с SOLID и GOF. Что будет их функциональным эквивалентом?.. Функции! Функциональное программирование просто «другое» и предлагает другие решения.



                Основные принципы функционального проектирования (дизайна)



                Функции как объекты первого класса


                В отличие от «классического» ООП (первые версии C++, C#, Java) функции в ФП представляют собой самостоятельные объекты и не должны принадлежать какому-либо классу. Удобно представлять функцию как волшебный железнодорожный тоннель: подаете на вход яблоки, а на выходе получаете бананы (apple -> banana).

                Синтаксис F# подчеркивает, что функции и значения равны в правах:

                let z = 1
                let add = x + y // int -> int ->int


                Композиция как основной «строительный материал»




                Если у нас есть две функции, одна преобразующая яблоки в бананы (apple -> banana), а другая бананы в вишни (banana -> cherry), объединив их мы получим функции преобразования яблок в вишни (apple -> cherry). С точки зрения программиста нет разницы получена эта функция с помощью композиции или написана вручную, главное – ее сигнатура.

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



                Можно представлять себе композицию функций как фрактал. Определение фрактала в строгом смысле не совпадает с определением композиции. Представляя фрактал вы можете визуализировать как ваш control flow состоит из скомпонованных функций, состоящих из скомпонованных функций, состоящих из…
                Шаблон компоновщик (Composite) в ООП тоже можно представлять себе «фракталом», но компоновщик работает со структурами данных, а не преобразованиями.

                Типы != классы


                У системы типов в ФП больше общего с теорией множеств, чем с классами из ООП. int – это тип. Но тип не обязательно должен быть примитивом. Customer – это тоже тип. Функции могут принимать на вход и возвращать функции. int -> int – тоже тип. Так что «тип» — это название для некоторого множества.

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

                Перемножение (логическое «и», record type в F#)

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

                type Birthday = Person * Date

                Сложение (логическое «или», discriminated union type в F#)


                type PaymentMethod =  
                | Cash
                | Cheque of ChequeNumber
                | Card of CardType * CardNumber

                Discriminated union – сложное название. Проще представлять себе этот тип как выбор. Например, вы можете на выбор оплатить товар наличными, банковским переводом или с помощью кредитной карты. Между этими вариантами нет ничего общего, кроме того, все они являются способом оплаты.
                Однажды нам пригодились «объединения» для моделирования предметной модели.
                Entity Framework умеет работать с такими типами из коробки, нужно лишь добавить id.

                Стремление к «полноте»




                Давайте рассмотрим функцию «разделить 12 на». Ее сигнатура int -> int и это ложь! Если мы подадим на вход 0, функция выбросит исключение. Вместо этого мы можем заменить сигнатуру на NonZeroInteger -> int или на int -> int option.



                ФП подталкивает вас к более строгому и полному описанию сигнатур функций. Если функции не выбрасывают исключений вы можете использовать сигнатуру и систему типов в качестве документации. Вы также можете использовать систему типов для создания предметной модели (Domain Model) и описания бизнес-правил (Business Rules). Таким образом можно гарантировать, что операции не допустимые в реальном мире не будут компилироваться в приложении, что дает более надежную защиту, чем модульные тесты. Подробнее об этом подходе вы можете прочитать в отдельной статье.

                Функции в качестве аргументов




                Хардкодить данные считается дурным тоном в программирование, вместо этого мы передаем их в качестве параметров (аргументов методов). В ФП мы идем дальше. Почему бы не параметризировать и поведение?



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

                let printList anAction aList =
                    for i in aList do
                        anAction i

                Пойдем дальше. Рассмотрим императивный пример на C#. Очевидно, что в данном коде присутствует дублирование (одинаковые циклы). Для того чтобы устранить дублирование нужно выделить общее и выделить общее в функцию:

                public static int Product(int n)
                {     
                    int product = 1; // инициализация
                    for (int i = 1; i <= n; i++) // цикл
                    {
                        product *= i; // действие
                    }
                
                    return product; // возвращаемое значение
                } 
                 
                public static int Sum(int n) 
                {
                    int sum = 0; // инициализация
                    for (int i = 1; i <= n; i++) // цикл
                    {
                        sum += i;
                    }
                
                    return sum; // возвращаемое значение
                } 

                В F# для работы с последовательностями уже есть функция fold:

                let product n =
                    let initialValue = 1
                    let action productSoFar x = productSoFar * x
                
                [1..n] |> List.fold action initialValue 
                 
                let sum n =
                    let initialValue = 0
                    let action sumSoFar x = sumSoFar+x
                
                [1..n] |> List.fold action initialValue

                Но, позвольте, в C# есть Aggregate, который делает тоже самое! Поздравляю, LINQ написан в функциональном стиле :)
                Рекомендую цикл статей Эрика Липперта о монадах в C#. С десятой части начинается объяснение «монадической» природы SelectMany

                Функции в качестве интерфейсов


                Допустим у нас есть интерфейс.

                interface IBunchOfStuff
                {
                    int DoSomething(int x);
                    string DoSomethingElse(int x); // один интерфейс - одно дело
                    void DoAThirdThing(string x); // нужно разделить
                } 
                

                Если взять SRP и ISP и возвести их в абсолют все интерфейсы будут содержать только одну функцию.

                interface IBunchOfStuff
                {
                    int DoSomething(int x);
                } 
                

                Тогда это просто функция int -> int. В F# не нужно объявлять интерфейс, чтобы сделать функции взаимозаменяемыми, они взаимозаменяемы «из коробки» просто по своей сигнатуре. Таким образом паттерн «стратегия» реализуется простой передачей функции в качестве аргумента другой функции:

                let DoSomethingWithStuff strategy x =
                    strategy x

                Паттерн «декоратор» реализуется с помощью композиции функций

                let isEvenWithLogging = log >> isEven >> log  // int -> bool

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

                Каррирование и частичное применение


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



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

                Такие преобразования возможны не только для компилируемых функций в программировании, но и для математических функций. Возможность такого преобразования впервые отмечена в трудах Готтлоба Фреге, систематически изучена Моисеем Шейнфинкелем в 1920-е годы, а наименование получило по имени Хаскелла Карри — разработчика комбинаторной логики, в которой сведение к функциям одного аргумента носит основополагающий характер.

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

                let three = 1 + 2 
                let three = (+) 1 2 
                let three = ((+) 1) 2 
                let add1 = (+) 1  
                let three = add1 2 

                Это называется частичным применением. В функциональных ЯП частичное применение заменяет принцип инъекции зависимостей (Dependency Injection)

                // эта функция требует зависимость
                let getCustomerFromDatabase connection (customerId:CustomerId) =
                    from connection
                    select customer
                    where customerId = customerId
                 
                // а эта уже нет
                let getCustomer1 = getCustomerFromDatabase myConnection 

                Продолжения (continuations)


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

                int Divide(int top, int bottom) 
                {
                    if (bottom == 0)
                    {
                        // кто решил, что нужно выбросить исключение?
                        throw new InvalidOperationException("div by 0");
                    }
                    else 
                    {
                        return top/bottom;
                    }
                }
                

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

                void Divide(int top, int bottom, Action ifZero, Action ifSuccess) 
                {
                    if (bottom == 0)
                    {
                        ifZero();
                    }
                    else
                    {
                        ifSuccess( top/bottom );
                     }
                }
                 

                Если вы когда-нибудь писали асинхронный код, то наверняка знакомы с «пирамидой погибели» (Pyramid Of Doom)



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

                let ifSomeDo f opt =
                    if opt.IsSome then
                        f opt.Value
                    else
                        None

                И переписать код, используя продолжения

                let example input =
                    doSomething input
                    |> ifSomeDo doSomethingElse
                    |> ifSomeDo doAThirdThing
                    |> ifSomeDo (fun z -> Some z)
                

                Монады


                Монады – это одно из «страшных» слов ФП. В первую очередь, из-за того, что обычно объяснения начинаются с теории категорий. Во вторую — из-за того что «монада» — это очень абстрактное понятие, не имеющее прямой аналогии с объектами реального мира. Я большой сторонник подхода «от частного к общему». Поняв практическую пользу на конкретном примере проще двигаться дальше к более полному и абстрактному определению.



                Зная о «продолжениях», вернемся к аналогии с рельсами и тоннелем. Функцию, в которую передаются аргумент и два «продолжения» можно представить как развилку.

                Но такие функции не компонуются :(




                На помощь приходит функция bind



                let bind nextFunction optionInput =
                    match optionInput with
                    // передаем результат выполнения предыдущей функции в случае успеха
                    | Some s -> nextFunction s
                    // или просто пробрасываем значение None дальше
                    | None -> None
                

                Код пирамиды погибели может быть переписан с помощью bind

                // было
                let example input =
                    let x = doSomething input
                    if x.IsSome then
                        let y = doSomethingElse (x.Value)
                        if y.IsSome then
                            let z = doAThirdThing (y.Value)
                            if z.IsSome then
                                let result = z.Value
                                Some result
                            else
                               None
                        else 
                            None 
                    else
                        None 
                
                // стало
                let bind f opt =
                    match opt with
                        | Some v -> f v
                        | None -> None
                
                let example input =
                    doSomething input
                        |> bind doSomethingElse
                        |> bind doAThirdThing
                        |> bind (fun z -> Some z)

                Кстати, это называется «monadic bind». Скажите своим друзьям, любителям хаскеля, что вы знаете, что такое «monadic bind» и вас примут в тайное общество:)

                Bind можно использовать для сцепления асинхронных операций (промисы в JS устроены именно так)



                Bind для обработки ошибок


                Если у вас появилось смутное ощущение, что дальше идет описание монады Either, так оно и есть

                Рассмотрим код на C#. Он выглядит достаточно хорошо: все кратко и понятно. Однако в нем отсутствует обработка ошибок. Действительно, что может пойти не так?

                string UpdateCustomerWithErrorHandling() 
                {
                    var request = receiveRequest();
                    validateRequest(request);
                    canonicalizeEmail(request);
                    db.updateDbFromRequest(request);
                    smtpServer.sendEmail(request.Email) 
                    return "OK";
                } 

                Мы все знаем, что обрабатывать ошибки нужно. Добавим обработку.

                string UpdateCustomerWithErrorHandling() 
                {
                    var request = receiveRequest();
                    var isValidated = validateRequest(request);
                    if (!isValidated) 
                    {
                        return "Request is not valid"
                    }
                    
                    canonicalizeEmail(request);
                    try 
                    {
                         var result = db.updateDbFromRequest(request);
                         if (!result) 
                        {
                           return "Customer record not found"
                        }
                    }
                    catch
                    {
                        return "DB error: Customer record not updated"
                    } 
                 
                    if (!smtpServer.sendEmail(request.Email))
                    {
                        log.Error "Customer email not sent"
                    } 
                 
                    return "OK";
                } 

                Вместо шести понятных теперь 18 не понятных строчек. Это 200% дополнительных строчек кода. Кроме того, линейная логика метода теперь зашумлена ветвлениями и ранними выходами.

                С помощью bind можно абстрагировать логику обработки ошибок. Вот так будет выглядеть метод без обработки ошибок, если его переписать на F#:

                А вот этот код но уже с обработкой ошибок:


                Более подробно эта тема раскрыта в отдельном докладе.

                Функторы


                Мне не очень понравилось описание функторов у Скотта. Прочитайте лучше статью «Функторы, аппликативные функторы и монады в картинках»

                Моноиды


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

                Я предупредил, итак, математика


                • 1 + 2 = 3
                • 1 + (2 + 3) = (1 + 2) + 3
                • 1 + 0 = 1
                  0 + 1 = 1

                И еще немного


                • 2 * 3 = 6
                • 2 * (3 * 4) = (2 * 3) * 4
                • 1 * 2 = 2
                  2 * 1 = 2

                Что общего между этими примерами?


                1. Есть некоторые объекты, в данном случае числа, и способ их взаимодействия. Причем результат взаимодействия — это тоже число (замкнутость).
                2. Порядок взаимодействия не важен (ассоциативность).
                3. Кроме того, есть некоторый специальный элемент, взаимодействие с которым не меняет исходный объект (нейтральный элемент).

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

                Замкнутость


                Дает возможность перейти от попарных операций к операциям на списках

                1 * 2 * 3 * 4
                [ 1; 2; 3; 4 ] |> List.reduce (*)

                Ассоциативность


                Применение принципа «разделяй и властвуй», «халявная» параллелизация. Если у нашего процессора 2 ядра и нам нужно рассчитать значение 1 + 2 + 3 + 4. Мы можем вычислить 1 + 2 на первом ядре, а 3 + 4 — на втором, а результат сложить. Больше последовательных вычислений — больше ядер.

                Нейтральный элемент


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

                Map / Reduce


                Если ваши объекты — не моноиды, попробуйте преобразовать их. Знаменитая модель распределенных вычислений Google — не более чем эксплуатация моноидов.



                Эндоморфизмы


                Функции с одинаковым типом входного и выходного значения являются моноидами и имеют специальное название — «эндоморфизмы» (название заимствовано из теории категорий). Что более важно, функции, содержащие эндоморфизмы могут быть преобразованы к эндоморфизмам с помощью частичного применения.
                Грег Янг открыто заявляет, что Event Sourcing — это просто функциональный код. Flux и unidirectional data flow, кстати тоже.


                Монады VS моноиды


                Монады являются моноидами, ведь как известно, монада — это всего лишь моноид в категории эндофункторов, а монадические законы — не более чем определение моноида в контексте продолжений.
                Кстати, бастион ООП — GOF тоже содержит монады. Паттерн «интерпретатор» — это так называемая свободная монада.
                Original source: habrahabr.ru (comments, light).

                https://habrahabr.ru/post/337880/


                Метки:  

                Дайджест свежих материалов из мира фронтенда за последнюю неделю №280 (11 — 17 сентября 2017)

                Воскресенье, 17 Сентября 2017 г. 22:56 + в цитатник

                Что в черном ящике: из чего складываются затраты на виртуальную и физическую инфраструктуру

                Воскресенье, 17 Сентября 2017 г. 18:56 + в цитатник
                1cloud сегодня в 18:56 Управление

                Что в черном ящике: из чего складываются затраты на виртуальную и физическую инфраструктуру

                  Аналитики Gartner в апреле сообщили, что мировые траты на дата-центры по сравнению с прошлым годом возрастут и перевалят за $170 млрд. Во столько Forbes оценивает бренд Apple, и именно такая сумма проходит через все магазины крупнейшего ретейлера в США Walmart. В этой статье мы рассмотрим, куда идут такие деньги в контексте ЦОД, и поговорим о том, из чего складываются затраты на виртуальную и физическую инфраструктуры.


                  / кадр из видео о нашем дата-центре SDN + (подробнее об инфраструктуре 1cloud)

                  Затраты на корпоративные серверы


                  Неважно, говорим ли мы об облачном решении или о «железе», расчет должен учитывать прямые, косвенные и скрытые затраты. Все эти группы расходов укладываются в показатель совокупной стоимости владения (TCO). Существует мнение, что большую часть затрат на физическую инфраструктуру составляет оборудование. Однако, по данным Intel, стоимость «железа» представляет собой 15% от общих затрат на IT-активы.

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

                  Что касается оборудования, компании потребуется приобрести сам сервер и продумать будущую архитектуру ЦОДа — будет ли это масштабируемая, модульная система, обладающая несколькими блоками, или традиционные стойки. Необходимо продумать стратегию дальнейшего расширения.

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

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

                  Следующая остановка — программное обеспечение. Серверу нужна операционная система, например Windows Server 2016, которую придется приобрести. Предприятию могут понадобиться и другие инструменты для решения корпоративных задач: Microsoft SQL Server, Citrix Presentation Server и др. Также нужно организовать сеть. С рекомендациями по программному обеспечению для создания локальной сети можно познакомиться на профильных площадках.

                  Наконец, мы добрались до электроэнергии, стоимость которой достаточно велика. Весьма показательна история Марко Пончиелли (Marco Ponchielli) из Rackspace. Он приводит данные, согласно которым раньше стоимость электроэнергии в общих затратах была столь незначительна, что требовалось от 30 до 50 лет, чтобы она приблизилась к стоимости вычислений.

                  Теперь же, в районах с высокими тарифами, этот период сократился до двух лет, а в среднем — до четырех. Марко также рассказал об IT-отделе, который потратил $22 млн на блейд-серверы. Затраты на их обслуживание за три года вылились в $30 млн, а модернизация инфраструктуры для их установки — в $54 млн.

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

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

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

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

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

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

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


                  / 1сloud

                  Затраты на виртуальные серверы


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

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

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

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

                  Как уже было отмечено, человеческий ресурс является еще одной графой затрат в работе дата-центра. В зависимости от навыков персонала и географического положения годовые зарплаты всего штата, обслуживающего дата-центр, варьируются от $75 тыс. до более чем $125 тыс., по словам главы исследовательской компании Clabby Analytics Джо Клэбби (Joe Clabby). Не стоит забывать, что, кроме администраторов, ЦОД обслуживают и десятки других сотрудников. Например, дата-центры уровня Tier III требуют физической охраны серверов.

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

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

                  Экосистема виртуальных провайдеров позволяет использовать меньшее количество серверов, что снижает потребление электроэнергии. К тому же с учетом масштабов и резервных систем появляется возможность для перераспределения и отключения серверов без ущерба для системы. По данным Uptime Institute, вывод из эксплуатации одной серверной стойки 1U может ежегодно экономить более $2 тыс.

                  По результатам исследования Uptime Institute, в 2015 году около 30% всех серверов не использовались, то есть они лежали «мертвым грузом» на плечах провайдеров. Во всем мире насчитывалось около 10 млн «коматозных» серверов. В денежном выражении это не менее $30 млрд. Вот почему облачные провайдеры «избавляются от балласта», экономя тем самым для себя и клиента приличные деньги.

                  Поэтому компании все чаще обращают внимание на облачные сервисы. Недавний отчет показал, что инвестирование в частные ЦОДы теряет смысл для IT-компаний. Вместо этого, руководители стараются нарастить вложения в разработку бизнес-приложений. Представители 70% опрошенных организаций заявили, что планируют увеличить расходы на безопасность в 2017 году, 67% отдали приоритет облачным приложениям, а еще 52% — облачной инфраструктуре.

                  P.S. В нашем корпоративном блоге есть и другие статьи на тему виртуальной инфраструктуры:

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

                  https://habrahabr.ru/post/338082/


                  Метки:  

                  Дайджест интересных материалов для мобильного разработчика #221 (11-17 сентября)

                  Воскресенье, 17 Сентября 2017 г. 17:39 + в цитатник
                  EverydayTools сегодня в 17:39 Разработка

                  Дайджест интересных материалов для мобильного разработчика #221 (11-17 сентября)

                    Apple на этой неделе представила iPhone 8 и главную новинку — iPhone X. Многие уже называют его лучшим смартфоном в истории компании (кстати, Pixel 2 ждем 4 октября – очень интересно, чем ответит Google), ну а мы пока разбираемся как создавать приложения для него, как рендерить океаны на мобильных устройствах, кто лучший в материальном дизайне, в тестировании, атрибуции и росте.



                    Быстрый рендеринг океанских волн на мобильных устройствах

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

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

                    iOS


                    Android


                    Разработка


                    Аналитика, маркетинг и монетизация


                    Устройства, IoT, AI



                    < Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.
                    Original source: habrahabr.ru (comments, light).

                    https://habrahabr.ru/post/338114/


                    Мечтают ли WAF’ы о статанализаторах

                    Воскресенье, 17 Сентября 2017 г. 17:29 + в цитатник

                    Мечтают ли WAF’ы о статанализаторах

                    Воскресенье, 17 Сентября 2017 г. 17:29 + в цитатник

                    Анализ работы MS SQL Server, для тех кто видит его впервые (часть 2)

                    Воскресенье, 17 Сентября 2017 г. 16:01 + в цитатник
                    71rmn сегодня в 16:01 Администрирование

                    Анализ работы MS SQL Server, для тех кто видит его впервые (часть 2)

                      часть 1
                      Продолжаем анализировать что происходит на нашем MS SQL сервере.
                      В этой части посмотрим как получить информацию о работе пользователей: кто и что делает, сколько ресурсов на это расходуется.
                      Думаю, вторая часть будет интересна не только админам БД, но и разработчикам (возможно даже разработчикам больше), которым необходимо разбираться, что не так с запросами на рабочем сервере, которые до этого отлично работали в тестовом.
                      Задачи анализа действий пользователей условно поделим на группы и рассмотрим каждую отдельно:
                      1. проанализировать конкретный запрос
                      2. проанализировать нагрузку от приложения в конкретных условиях (например, при нажатии пользователем какой-то кнопки в стороннем приложении работающим с БД)
                      3. анализ ситуации происходящей в данный момент

                      Предупреждение
                      Анализ производительности требует глубокого понимания устройства и принципов работы сервера БД и ОС. Поэтому, чтение только этих статей не сделает из вас эксперта.
                      Рассматриваемые критерии и счетчики в реальных системах находятся в сложной зависимости друг от друга. Например высокая нагрузка HDD, часто связана с проблемой не самого HDD, а с нехваткой оперативной памяти. Даже если вы проведете некоторые из замеров — этого недостаточно для взвешенной оценки проблем.
                      Цель статей — познакомить с базовыми вещами на простых примерах. Рекомендации не стоит рассматривать как «руководство к действию», рассматривайте их как учебные задачи (упрощенно отражающие действительность) и как варианты «на подумать», призванные пояснить ход мысли.
                      Надеюсь, по итогу статей вы научитесь аргументировать цифрами свои заключения о работе сервера. И вместо слов «сервер тормозит» будете приводить конкретные величины конкретных показателей.

                      Анализируем конкретный запрос


                      Первый пункт довольно прост, остановимся на нем кратко. Рассмотрим только некоторые малоочевидные вещи.
                      SSMS, кроме результатов запроса, позволяет получать дополнительную информацию о выполнении запроса:
                      • практически все знают, что план запроса получается кнопками «Display Estimated Execution Plan» (оценочный план) и «Include Actual Execution Plan» (фактический план). Отличаются они тем, что оценочный план строится без выполнения запроса. Соответственно, информация о количестве обработанных строк в нем будет только оценочная. В фактическом плане будут как оценочные данные, так и фактические. Сильные расхождения этих величин говорят о неактуальности статистики. Впрочем, анализ плана — тема для отдельной большой статьи — пока не будем углубляться.
                      • менее известный факт — можно получать замеры затрат процессора и дисковых операций сервера. Для этого необходимо включить SET опции либо в диалоге через меню «Query» / «Query options...»
                        скрин

                        либо напрямую командами SET в запросе, например
                        SET STATISTICS IO ON
                        SET STATISTICS TIME ON
                        SELECT * FROM Production.Product p
                        JOIN Production.ProductDocument pd ON p.ProductID = pd.ProductID
                        JOIN Production.ProductProductPhoto ppp ON p.ProductID = ppp.ProductID
                        

                        В результате выполнения, получим данные по затратам времени на компиляцию и выполнение, а также, количество дисковых операций.
                        Пример вывода
                        Время синтаксического анализа и компиляции SQL Server:
                        время ЦП = 16 мс, истекшее время = 89 мс.

                        Время работы SQL Server:
                        Время ЦП = 0 мс, затраченное время = 0 мс.

                        Время работы SQL Server:
                        Время ЦП = 0 мс, затраченное время = 0 мс.

                        (32 row(s) affected)
                        Таблица «ProductProductPhoto». Число просмотров 32, логических чтений 96, физических чтений 5, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.
                        Таблица «Product». Число просмотров 0, логических чтений 64, физических чтений 0, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.
                        Таблица «ProductDocument». Число просмотров 1, логических чтений 3, физических чтений 1, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.

                        Время работы SQL Server:
                        Время ЦП = 15 мс, затраченное время = 35 мс.


                        Здесь стоит обратить внимание на время компиляции и текст «логических чтений 96, физических чтений 5». При втором и последующих выполнениях одного и того же запроса — физические чтения могут уменьшаться, а повторная компиляция может не потребоваться. Из-за этого часто возникает ситуация, что второй и последующие разы запрос выполняется быстрее чем первый. Причина, как вы поняли, в кэшировании данных и скомпилированных планов запросов.
                      • еще полезная кнопочка рядом с кнопками планов — «Include Client Statistics» — выводит информацию по сетевому обмену, количестве выполненных операций и суммарном времени выполнения, с учетом затрат на сетевой обмен и обработку клиентом.
                        пример на котором видно что первое выполнение занимает больше времени
                      • в SSMS 2016 версии появилась кнопка «Include Live Query Statistics». Отображает картинку как и в случае с планом запроса, только на ней цифры обработанных строк не статические, а меняются на экране прямо в процессе выполнения запроса. Картинка получается очень наглядная — по мигающим стрелкам и бегущим цифрам сразу видно где тратится время. Кнопка есть в 2016 студии, но работает с серверами начиная с 2014 версии.

                      Подытожим первую часть:
                      Затраты процессора смотрим используя SET STATISTICS TIME ON.
                      Дисковые операции: SET STATISTICS IO ON. Не забываем, что «логическое чтение» — это операция чтения, завершившаяся в кэше диска без физического обращения к дисковой системе. «Физическое чтение» требует значительно больше времени.
                      Объем сетевого трафика оцениваем с помощью «Include Client Statistics».
                      Детально алгоритм выполнения запроса анализируем по «плану выполнения» с помощью «Include Actual Execution Plan» и «Include Live Query Statistics».

                      Анализируем нагрузку от приложения


                      Для второго раздела вооружаемся profiler-ом. После запуска и подключения к серверу, необходимо выбрать журналируемые события. Можно пойти простым путем — запустить профилирование со стандартным темплэйтом трассировки. На закладке «General» в поле «Use the template» выбрать «Standard (default)» и нажать «Run».
                      картинка

                      Чуть более сложный путь — к выбранному шаблону добавить (или убавить) фильтров или событий. Данные опции на второй закладке диалога. Чтобы увидеть полный набор возможных событий и колонок для выбора — отметьте пункты «Show All Events» и «Show All Columns».
                      картинка

                      Из событий нам потребуются (лишние лучше не включать — чтобы создавать меньше трафика):
                      • Stored Procedures \ RPC:Completed
                      • TSQL \ SQL:BatchCompleted

                      Эти события фиксируют все внешние sql-вызовы к серверу. Они возникают, как видно из названия (Completed), после окончания обработки запроса. Имеются аналогичные события фиксирующие старт sql-вызова:
                      • Stored Procedures \ RPC:Starting
                      • TSQL \ SQL:BatchStarting

                      Но они нам подходят меньше, так как не содержат информации о затраченных на выполнение запроса ресурсах сервера. Очевидно, что такая информация доступна только по окончании выполнения. Соответственно, столбцы с данными по CPU, Reads, Writes в событиях *Starting будут пустыми.
                      Еще полезные события, которые мы пока не будем включать:
                      • Stored Procedures \ SP:Starting (*Completed) — фиксирует внутренний вызов хранимой процедуры (не с клиента, а внутри текущего запроса или другой процедуры).
                      • Stored Procedures \ SP:StmtStarting (*Completed) — фиксирует старт каждого выражения внутри хранимой процедуры. Если в процедуре цикл — будет столько событий для команд внутри цикла, сколько итераций было в цикле.
                      • TSQL \ SQL:StmtStarting (*Completed) — фиксирует старт каждого выражения внутри SQL-batch. Если ваш запрос содержит несколько команд — будет по событию на каждую. Т.е. аналогично предыдущему, только действует не для команд внутри процедур, а для команд внутри запроса.

                      Эти события удобны для отслеживания шагов выполнения. Например, когда использование отладчика невозможно.
                      По колонкам
                      Какие выбирать, как правило, понятно из названия колонки. Нам будут нужны:
                      • TextData, BinaryData — для описанных выше событий содержат сам текст запроса.
                      • CPU, Reads, Writes, Duration — данные о затратах ресурсов.
                      • StartTime, EndTime — время начала/окончания выполнения. Удобны для сортировки.

                      Прочие колонки добавляйте на свой вкус.
                      По кнопке «Column Filters...» можно вызвать диалог установки фильтров событий. Если интересует активность конкретного пользователя — задать фильтр по номеру сессии или имени пользователя. К сожалению, в случае подключения приложения через app-server c пулом коннектов — отследить конкретного пользователя сложнее.
                      Фильтры можно использовать, например, для отбора только «тяжелых» запросов (Duration>X). Или запросов которые вызывают интенсивную запись (Writes>Y). Да хоть просто по содержимому запроса.

                      Что же еще нам нужно от профайлера? Конечно же план выполнения!
                      Такая возможность имеется. Необходимо добавить в трассировку событие «Performance \ Showplan XML Statistics Profile». Выполняя наш запрос, мы получим примерно следующую картинку.
                      Текст запроса

                      План выполнения

                      И это еще не всё
                      Трассу можно сохранять в файл или таблицу БД (а не только выводить на экран).
                      Настройки трассировки можно сохранить в виде личного template для быстрого запуска.
                      Запуск трассировки можно выполнять и без профайлера — с помощью t-sql кода, используя процедуры: sp_trace_create, sp_trace_setevent, sp_trace_setstatus, sp_trace_getdata. Пример как это сделать. Данный подход может пригодиться, например, для автоматического старта записи трассы в файл по расписанию. Как именно использовать эти команды, можно подсмотреть у самого профайлера. Достаточно запустить две трассировки и в одной отследить что происходит при старте второй. Обратите внимание на фильтр по колонке «ApplicationName» — проконтролируйте, что там отсутствует фильтр на сам профайлер.
                      Список событий фиксируемых профайлером очень обширен и не ограничивается только получением текстов запросов. Имеются события фиксирующие fullscan, рекомпиляции, autogrow, deadlock и многое другое.

                      Анализируем активность пользователей в целом по серверу


                      Жизненные ситуации бывают и такими, когда информация из разделов выше не помогает:
                      Какой-то запрос висит на «выполнении» очень долго и непонятно, закончится он когда-нибудь или нет. Проанализировать проблемный запрос отдельно — хотелось бы — но надо сначала определить что за запрос. Профайлером ловить бесполезно — starting событие мы уже пропустили, а completed неясно сколько ждать.
                      А может висит и не пользовательский запрос совсем, а может это сам сервер что-то активно делает…
                      Давайте разбираться
                      Все вы наверно видели «Activity Monitor». В старших студиях его функционал стал богаче. Чем он может нам помочь? В «Activity Monitor» много полезного и интересного, но третий раздел не о нем. Всё что нужно будем доставать напрямую из системных представлений и функций (а сам Монитор полезен тем, что на него можно натравить профайлер и посмотреть какие запросы он выполняет). Нам понадобятся:
                      • sys.dm_exec_sessions — информация о сессиях. Отображает информацию по подключенным пользователям. Полезные поля (в рамках этой статьи) — идентифицирующие пользователя (login_name, login_time, host_name, program_name, ...) и поля с информацией о затраченных ресурсах (cpu_time, reads, writes, memory_usage, ...)
                      • sys.dm_exec_requests — информация о запросах выполняющихся в данный момент. Полей тут тоже довольно много, рассмотрим только некоторые:
                        • session_id — код сессии для связи с предыдущим представлением
                        • start_time — время старта запроса
                        • command — это поле, вопреки названию, содержит не запрос, а тип выполняемой команды. Для пользовательских запросов — обычно это что-то вроде select/update/delete/и т.п. (также, важные примечания ниже)
                        • sql_handle, statement_start_offset, statement_end_offset — информация для получения текста запроса: хэндл, а также начальная и конечная позиция в тексте запроса — обозначающая часть выполняемую в данный момент (для случая когда ваш запрос содержит несколько команд).
                        • plan_handle — хэндл сгенерированного плана.
                        • blocking_session_id — при возникновении блокировок препятствующих выполнению запроса — указывает на номер сессии которая стала причиной блокировки
                        • wait_type, wait_time, wait_resource — поля с информацией о причине и длительности ожидания. Для некоторых видов ожидания, например, блокировка данных — дополнительно указывается код заблокированного ресурса.
                        • percent_complete — по названию понятно, что это процент выполнения. К сожалению, доступен только для команд у которых четко прогнозируемый прогресс выполнения (например, backup или restore).
                        • cpu_time, reads, writes, logical_reads, granted_query_memory — затраты ресурсов.
                      • sys.dm_exec_sql_text(sql_handle | plan_handle), sys.dm_exec_query_plan(plan_handle) — функции получения текста и плана запроса. Ниже рассмотрим пример использования.
                      • sys.dm_exec_query_stats — сводная статистика выполнения в разрезе запросов. Показывает какой запрос сколько раз выполнялся и сколько ресурсов на это потрачено.

                      Важные примечания
                      Приведенный перечень — лишь малая часть. Полный список всех системных представлений и функций описан в документации. Также, имеется схема связей основных объектов в виде красивой картинки — можно распечатать на А1 и повесить на стену.
                      Текст запроса, его план и статистика исполнения — данные хранящиеся в процедурном кэше. Во время выполнения они доступны. После выполнения доступность не гарантируется и зависит от давления на кэш. Да, кэш можно очищать вручную. Иногда это советуют делать когда «поплыли» планы выполнения, но тут очень много нюансов… В общем, «Имеются противопоказания, рекомендовано проконсультироваться со специалистом».
                      Поле «command» — для пользовательских запросов оно практически бессмысленно — ведь мы можем получить полный текст… Но не всё так просто. Это поле очень важно для получения информации о системных процессах. Как правило, они выполняют какие-то внутренние задачи и не имеют текста sql. Для таких процессов, информация о команде единственный намек на тип активности. В комментариях к предыдущей статье был вопрос про то, чем занят сервер, когда он, вроде бы, ничем не должен быть занят — возможно ответ будет в значении этого поля. На моей практике, поле «command» для активных системных процессов всегда выдавало что-то вполне понятное: autoshrink/autogrow/checkpoint/logwriter/и т.п.

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

                      Пример 1: Какой процесс расходует cpu/reads/writes/memory
                      Для начала, посмотрим какие сессии больше всего потребляют, например, CPU. Информация в sys.dm_exec_sessions. Но данные по CPU (а также reads, writes) — накопительные. Т.е цифра в поле содержит «итого» за все время подключения. Понятно, что больше всего будет у того кто подключился месяц назад, да так и не отключался ни разу. Это вовсе не означает, что он прямо сейчас грузит систему.
                      Немного кода решает проблему, алгоритм примерно такой:
                      1. сначала сделаем выборку и сохраним во временную таблицу
                      2. затем подождем немного
                      3. делаем выборку второй раз
                      4. сравниваем результаты первой и второй выборки — разница, как раз и будет затратами возникшими на п.2
                      5. для удобства, разницу можем поделить на длительность п.2, чтобы получить усредненные «затраты в секунду».

                      Пример скрипта
                      if object_id('tempdb..#tmp') is NULL
                      BEGIN
                      	SELECT * into #tmp from sys.dm_exec_sessions s
                      	PRINT 'ждем секунду для накопления статистики при первом запуске'
                      	-- при последующих запусках не ждем, т.к. сравниваем с результатом предыдущего запуска
                      	WAITFOR DELAY '00:00:01';
                      END
                      if object_id('tempdb..#tmp1') is not null drop table #tmp1
                      
                      declare @d datetime
                      declare @dd float
                      select @d = crdate from tempdb.dbo.sysobjects where id=object_id('tempdb..#tmp')
                      
                      select * into #tmp1 from sys.dm_exec_sessions s
                      select @dd=datediff(ms,@d,getdate())
                      select @dd AS [интервал времени, мс]
                      
                      SELECT TOP 30 s.session_id, s.host_name, db_name(s.database_id) as db, s.login_name,s.login_time,s.program_name,
                             s.cpu_time-isnull(t.cpu_time,0) as cpu_Diff, convert(numeric(16,2),(s.cpu_time-isnull(t.cpu_time,0))/@dd*1000) as cpu_sec,
                             s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0) as totIO_Diff, convert(numeric(16,2),(s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0))/@dd*1000) as totIO_sec,
                             s.reads-isnull(t.reads,0) as reads_Diff, convert(numeric(16,2),(s.reads-isnull(t.reads,0))/@dd*1000) as reads_sec,
                             s.writes-isnull(t.writes,0) as writes_Diff, convert(numeric(16,2),(s.writes-isnull(t.writes,0))/@dd*1000) as writes_sec,
                             s.logical_reads-isnull(t.logical_reads,0) as logical_reads_Diff, convert(numeric(16,2),(s.logical_reads-isnull(t.logical_reads,0))/@dd*1000) as logical_reads_sec,
                             s.memory_usage, s.memory_usage-isnull(t.memory_usage,0) as [mem_D],
                            s.nt_user_name,s.nt_domain
                      from #tmp1 s
                      LEFT join #tmp t on s.session_id=t.session_id
                      order BY
                      cpu_Diff desc
                      --totIO_Diff desc
                      --logical_reads_Diff desc
                      
                      drop table #tmp
                      GO
                      select * into #tmp from #tmp1
                      drop table #tmp1
                      

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

                      Те, кто не любят выполнять запрос в студии — могут его завернуть в приложение написанное на своём любимом языке программирования. Я покажу как это сделать в MS Excel без единой строки кода.
                      В меню «Данные» подключаемся к серверу. Если будет требовать выбрать таблицу — выбираем произвольную — потом поменяем это. Как всегда, жмем «Next» и «Finish» пока не увидим диалог «Импорт данных» — в нем нужно нажать «Свойства...». В свойствах необходимо сменить «тип команды» на значение «SQL» и в поле «текст команды» вставить немного измененный наш запрос.
                      Запрос придется немного поменять:
                      • добавим «SET NOCOUNT ON» — т.к. Excel не любит отсечки количества строк;
                      • «временные таблицы» заменим на «таблицы переменные»;
                      • задержка всегда будет 1сек — поля с усредненными значениями не нужны

                      Измененный запрос для Excel
                      SET NOCOUNT ON;
                      declare @tmp table(session_id	smallint primary key,login_time	datetime,host_name nvarchar(256),program_name nvarchar(256),login_name nvarchar(256),nt_user_name	nvarchar(256),cpu_time	int,memory_usage	int,reads	bigint,writes	bigint,logical_reads	bigint,database_id	smallint)
                      
                      declare @d datetime;
                      select @d=GETDATE()
                      
                      INSERT INTO @tmp(session_id,login_time,host_name,program_name,login_name,nt_user_name,cpu_time,memory_usage,reads,writes,logical_reads,database_id)
                      SELECT session_id,login_time,host_name,program_name,login_name,nt_user_name,cpu_time,memory_usage,reads,writes,logical_reads,database_id
                      from sys.dm_exec_sessions s;
                      
                      WAITFOR DELAY '00:00:01';
                      
                      declare @dd float;
                      select @dd=datediff(ms,@d,getdate());
                      
                      SELECT 
                      	s.session_id, s.host_name, db_name(s.database_id) as db, s.login_name,s.login_time,s.program_name,
                      	s.cpu_time-isnull(t.cpu_time,0) as cpu_Diff,
                      	s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0) as totIO_Diff,
                      	s.reads-isnull(t.reads,0) as reads_Diff,
                      	s.writes-isnull(t.writes,0) as writes_Diff,
                      	s.logical_reads-isnull(t.logical_reads,0) as logical_reads_Diff,
                      	s.memory_usage, s.memory_usage-isnull(t.memory_usage,0) as [mem_Diff],
                      	s.nt_user_name,s.nt_domain
                      from sys.dm_exec_sessions s
                      left join @tmp t on s.session_id=t.session_id

                      Картинки процесса




                      Результат


                      Когда данные будут в Excel-е, можете их сортировать как вам нужно. Для актуализации информации — жмите «Обновить». В настройках книги, для удобства, можете поставить «автообновление» через заданный период времени и «обновление при открытии». Файл можете сохранить и передать коллегам. Таким образом, мы из навоза и веточек подручных средств собрали ЫнтерпрайзМониторингТул удобный и простой инструмент.

                      Пример 2: На что сессия расходует ресурсы
                      Итак, в предыдущем примере мы определили проблемные сессии. Теперь определим, что именно они делают. Используем sys.dm_exec_requests, а также функции получения текста и плана запроса.
                      текст запроса и план по номеру сессии
                      DECLARE @sql_handle varbinary(64)
                      DECLARE @plan_handle varbinary(64)
                      DECLARE @sid INT
                      Declare @statement_start_offset int, @statement_end_offset INT, @session_id SMALLINT
                      
                      -- для инфы по конкретному юзеру - указываем номер сессии
                      SELECT @sid=182
                      
                      -- получаем переменные состояния для дальнейшей обработки
                      IF @sid IS NOT NULL
                      SELECT @sql_handle=der.sql_handle, @plan_handle=der.plan_handle, @statement_start_offset=der.statement_start_offset, @statement_end_offset=der.statement_end_offset, @session_id = der.session_id
                      FROM sys.dm_exec_requests der WHERE der.session_id=@sid
                      
                      --печатаем текст выполняемого запроса
                      DECLARE @txt VARCHAR(max)
                      IF @sql_handle IS NOT NULL
                      SELECT @txt=[text] FROM sys.dm_exec_sql_text(@sql_handle)
                      PRINT @txt
                      -- выводим план выполняемого батча/процы
                      IF @plan_handle IS NOT NULL
                      select * from sys.dm_exec_query_plan(@plan_handle)
                      -- и план выполняемого запроса в рамках батча/процы
                      IF @plan_handle IS NOT NULL
                      SELECT dbid, objectid, number, encrypted, CAST(query_plan AS XML) AS planxml
                      from sys.dm_exec_text_query_plan(@plan_handle, @statement_start_offset, @statement_end_offset)

                      Подставляйте в запрос номер сессии и выполняйте. После выполнения, на закладке «Results» будут планы (два: первый для всего запроса, второй для текущего шага — если шагов в запросе несколько), на закладке «Messages» — текст запроса. Для просмотра плана — необходимо кликнуть в строке на текст оформленный в виде url. План откроется в отдельной закладке. Иногда бывает что план открывается не в графическом виде, а в виде xml-текста. Это, скорее всего, связано с тем что версия студии ниже чем сервера. Попробуйте пересохранить полученный xml в файл с расширением sqlplan, предварительно удалив из первой строки упоминания «Version» и «Build», а затем отдельно открыть его. Если и это не помогает — напоминаю, что 2016 студия официально доступна бесплатно на сайте MS.
                      Картинки






                      Очевидно, полученный план будет «оценочным», т.к. запрос еще выполняется. Но некоторую статистику по выполнению получить всё равно можно. Используем представление sys.dm_exec_query_stats с фильтром по нашим хэндлам.
                      Допишем в конец предыдущего запроса
                      -- статистика по плану
                      IF @sql_handle IS NOT NULL
                      SELECT * FROM sys.dm_exec_query_stats QS WHERE QS.sql_handle=@sql_handle
                      

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

                      Пример 3: А можно всех посмотреть
                      Давайте объединим рассмотренные системные представления и функции в одном запросе. Это может быть удобно для оценки ситуации в целом.
                      -- получаем список всех текущих запросов
                      SELECT LEFT((SELECT [text] FROM sys.dm_exec_sql_text(der.sql_handle)),500) AS txt
                      --,(select top 1 1 from sys.dm_exec_query_profiles where session_id=der.session_id) as HasLiveStat
                      ,der.blocking_session_id as blocker, DB_NAME(der.database_id) AS База, s.login_name, *
                      from sys.dm_exec_requests der
                      left join sys.dm_exec_sessions s ON s.session_id = der.session_id
                      WHERE der.session_id<>@@SPID
                      -- AND der.session_id>50
                      

                      Запрос выводит список активных сессий и тексты их запросов. Для системных процессов, напоминаю, обычно запрос отсутствует, но заполнено поле «command». Видна информация о блокировках и ожиданиях. Можете попробовать скрестить этот запрос с примером 1, чтобы еще и отсортировать по нагрузке. Но будьте аккуратны — тексты запросов могут оказаться очень большими. Выбирать их массово может оказаться ресурсоемко. Да и трафик будет большим. В примере я ограничил получаемый запрос первыми 500 символами, а получение плана не стал делать.

                      Примеры запросов выложил на гитхаб.

                      Заключение


                      Было бы ещё неплохо, получить Live Query Statistics для произвольной сессии. По заявлению производителя, на текущий момент, постоянный сбор статистики требует значительных ресурсов и поэтому, по умолчанию, отключен. Включить не проблема, но дополнительные манипуляции усложняют процесс и уменьшают практическую пользу. Возможно, попробуем это сделать в отдельной статье.

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

                      Впереди у нас еще анализ нагрузки на память и сеть, а также прочие нюансы. Дойдем и до них. Материала еще на несколько статей.
                      Original source: habrahabr.ru (comments, light).

                      https://habrahabr.ru/post/336586/


                      Метки:  

                      Анализ работы MS SQL Server, для тех кто видит его впервые (часть 2)

                      Воскресенье, 17 Сентября 2017 г. 16:01 + в цитатник
                      71rmn сегодня в 16:01 Администрирование

                      Анализ работы MS SQL Server, для тех кто видит его впервые (часть 2)

                        часть 1
                        Продолжаем анализировать что происходит на нашем MS SQL сервере.
                        В этой части посмотрим как получить информацию о работе пользователей: кто и что делает, сколько ресурсов на это расходуется.
                        Думаю, вторая часть будет интересна не только админам БД, но и разработчикам (возможно даже разработчикам больше), которым необходимо разбираться, что не так с запросами на рабочем сервере, которые до этого отлично работали в тестовом.
                        Задачи анализа действий пользователей условно поделим на группы и рассмотрим каждую отдельно:
                        1. проанализировать конкретный запрос
                        2. проанализировать нагрузку от приложения в конкретных условиях (например, при нажатии пользователем какой-то кнопки в стороннем приложении работающим с БД)
                        3. анализ ситуации происходящей в данный момент

                        Предупреждение
                        Анализ производительности требует глубокого понимания устройства и принципов работы сервера БД и ОС. Поэтому, чтение только этих статей не сделает из вас эксперта.
                        Рассматриваемые критерии и счетчики в реальных системах находятся в сложной зависимости друг от друга. Например высокая нагрузка HDD, часто связана с проблемой не самого HDD, а с нехваткой оперативной памяти. Даже если вы проведете некоторые из замеров — этого недостаточно для взвешенной оценки проблем.
                        Цель статей — познакомить с базовыми вещами на простых примерах. Рекомендации не стоит рассматривать как «руководство к действию», рассматривайте их как учебные задачи (упрощенно отражающие действительность) и как варианты «на подумать», призванные пояснить ход мысли.
                        Надеюсь, по итогу статей вы научитесь аргументировать цифрами свои заключения о работе сервера. И вместо слов «сервер тормозит» будете приводить конкретные величины конкретных показателей.

                        Анализируем конкретный запрос


                        Первый пункт довольно прост, остановимся на нем кратко. Рассмотрим только некоторые малоочевидные вещи.
                        SSMS, кроме результатов запроса, позволяет получать дополнительную информацию о выполнении запроса:
                        • практически все знают, что план запроса получается кнопками «Display Estimated Execution Plan» (оценочный план) и «Include Actual Execution Plan» (фактический план). Отличаются они тем, что оценочный план строится без выполнения запроса. Соответственно, информация о количестве обработанных строк в нем будет только оценочная. В фактическом плане будут как оценочные данные, так и фактические. Сильные расхождения этих величин говорят о неактуальности статистики. Впрочем, анализ плана — тема для отдельной большой статьи — пока не будем углубляться.
                        • менее известный факт — можно получать замеры затрат процессора и дисковых операций сервера. Для этого необходимо включить SET опции либо в диалоге через меню «Query» / «Query options...»
                          скрин

                          либо напрямую командами SET в запросе, например
                          SET STATISTICS IO ON
                          SET STATISTICS TIME ON
                          SELECT * FROM Production.Product p
                          JOIN Production.ProductDocument pd ON p.ProductID = pd.ProductID
                          JOIN Production.ProductProductPhoto ppp ON p.ProductID = ppp.ProductID
                          

                          В результате выполнения, получим данные по затратам времени на компиляцию и выполнение, а также, количество дисковых операций.
                          Пример вывода
                          Время синтаксического анализа и компиляции SQL Server:
                          время ЦП = 16 мс, истекшее время = 89 мс.

                          Время работы SQL Server:
                          Время ЦП = 0 мс, затраченное время = 0 мс.

                          Время работы SQL Server:
                          Время ЦП = 0 мс, затраченное время = 0 мс.

                          (32 row(s) affected)
                          Таблица «ProductProductPhoto». Число просмотров 32, логических чтений 96, физических чтений 5, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.
                          Таблица «Product». Число просмотров 0, логических чтений 64, физических чтений 0, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.
                          Таблица «ProductDocument». Число просмотров 1, логических чтений 3, физических чтений 1, упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.

                          Время работы SQL Server:
                          Время ЦП = 15 мс, затраченное время = 35 мс.


                          Здесь стоит обратить внимание на время компиляции и текст «логических чтений 96, физических чтений 5». При втором и последующих выполнениях одного и того же запроса — физические чтения могут уменьшаться, а повторная компиляция может не потребоваться. Из-за этого часто возникает ситуация, что второй и последующие разы запрос выполняется быстрее чем первый. Причина, как вы поняли, в кэшировании данных и скомпилированных планов запросов.
                        • еще полезная кнопочка рядом с кнопками планов — «Include Client Statistics» — выводит информацию по сетевому обмену, количестве выполненных операций и суммарном времени выполнения, с учетом затрат на сетевой обмен и обработку клиентом.
                          пример на котором видно что первое выполнение занимает больше времени
                        • в SSMS 2016 версии появилась кнопка «Include Live Query Statistics». Отображает картинку как и в случае с планом запроса, только на ней цифры обработанных строк не статические, а меняются на экране прямо в процессе выполнения запроса. Картинка получается очень наглядная — по мигающим стрелкам и бегущим цифрам сразу видно где тратится время. Кнопка есть в 2016 студии, но работает с серверами начиная с 2014 версии.

                        Подытожим первую часть:
                        Затраты процессора смотрим используя SET STATISTICS TIME ON.
                        Дисковые операции: SET STATISTICS IO ON. Не забываем, что «логическое чтение» — это операция чтения, завершившаяся в кэше диска без физического обращения к дисковой системе. «Физическое чтение» требует значительно больше времени.
                        Объем сетевого трафика оцениваем с помощью «Include Client Statistics».
                        Детально алгоритм выполнения запроса анализируем по «плану выполнения» с помощью «Include Actual Execution Plan» и «Include Live Query Statistics».

                        Анализируем нагрузку от приложения


                        Для второго раздела вооружаемся profiler-ом. После запуска и подключения к серверу, необходимо выбрать журналируемые события. Можно пойти простым путем — запустить профилирование со стандартным темплэйтом трассировки. На закладке «General» в поле «Use the template» выбрать «Standard (default)» и нажать «Run».
                        картинка

                        Чуть более сложный путь — к выбранному шаблону добавить (или убавить) фильтров или событий. Данные опции на второй закладке диалога. Чтобы увидеть полный набор возможных событий и колонок для выбора — отметьте пункты «Show All Events» и «Show All Columns».
                        картинка

                        Из событий нам потребуются (лишние лучше не включать — чтобы создавать меньше трафика):
                        • Stored Procedures \ RPC:Completed
                        • TSQL \ SQL:BatchCompleted

                        Эти события фиксируют все внешние sql-вызовы к серверу. Они возникают, как видно из названия (Completed), после окончания обработки запроса. Имеются аналогичные события фиксирующие старт sql-вызова:
                        • Stored Procedures \ RPC:Starting
                        • TSQL \ SQL:BatchStarting

                        Но они нам подходят меньше, так как не содержат информации о затраченных на выполнение запроса ресурсах сервера. Очевидно, что такая информация доступна только по окончании выполнения. Соответственно, столбцы с данными по CPU, Reads, Writes в событиях *Starting будут пустыми.
                        Еще полезные события, которые мы пока не будем включать:
                        • Stored Procedures \ SP:Starting (*Completed) — фиксирует внутренний вызов хранимой процедуры (не с клиента, а внутри текущего запроса или другой процедуры).
                        • Stored Procedures \ SP:StmtStarting (*Completed) — фиксирует старт каждого выражения внутри хранимой процедуры. Если в процедуре цикл — будет столько событий для команд внутри цикла, сколько итераций было в цикле.
                        • TSQL \ SQL:StmtStarting (*Completed) — фиксирует старт каждого выражения внутри SQL-batch. Если ваш запрос содержит несколько команд — будет по событию на каждую. Т.е. аналогично предыдущему, только действует не для команд внутри процедур, а для команд внутри запроса.

                        Эти события удобны для отслеживания шагов выполнения. Например, когда использование отладчика невозможно.
                        По колонкам
                        Какие выбирать, как правило, понятно из названия колонки. Нам будут нужны:
                        • TextData, BinaryData — для описанных выше событий содержат сам текст запроса.
                        • CPU, Reads, Writes, Duration — данные о затратах ресурсов.
                        • StartTime, EndTime — время начала/окончания выполнения. Удобны для сортировки.

                        Прочие колонки добавляйте на свой вкус.
                        По кнопке «Column Filters...» можно вызвать диалог установки фильтров событий. Если интересует активность конкретного пользователя — задать фильтр по номеру сессии или имени пользователя. К сожалению, в случае подключения приложения через app-server c пулом коннектов — отследить конкретного пользователя сложнее.
                        Фильтры можно использовать, например, для отбора только «тяжелых» запросов (Duration>X). Или запросов которые вызывают интенсивную запись (Writes>Y). Да хоть просто по содержимому запроса.

                        Что же еще нам нужно от профайлера? Конечно же план выполнения!
                        Такая возможность имеется. Необходимо добавить в трассировку событие «Performance \ Showplan XML Statistics Profile». Выполняя наш запрос, мы получим примерно следующую картинку.
                        Текст запроса

                        План выполнения

                        И это еще не всё
                        Трассу можно сохранять в файл или таблицу БД (а не только выводить на экран).
                        Настройки трассировки можно сохранить в виде личного template для быстрого запуска.
                        Запуск трассировки можно выполнять и без профайлера — с помощью t-sql кода, используя процедуры: sp_trace_create, sp_trace_setevent, sp_trace_setstatus, sp_trace_getdata. Пример как это сделать. Данный подход может пригодиться, например, для автоматического старта записи трассы в файл по расписанию. Как именно использовать эти команды, можно подсмотреть у самого профайлера. Достаточно запустить две трассировки и в одной отследить что происходит при старте второй. Обратите внимание на фильтр по колонке «ApplicationName» — проконтролируйте, что там отсутствует фильтр на сам профайлер.
                        Список событий фиксируемых профайлером очень обширен и не ограничивается только получением текстов запросов. Имеются события фиксирующие fullscan, рекомпиляции, autogrow, deadlock и многое другое.

                        Анализируем активность пользователей в целом по серверу


                        Жизненные ситуации бывают и такими, когда информация из разделов выше не помогает:
                        Какой-то запрос висит на «выполнении» очень долго и непонятно, закончится он когда-нибудь или нет. Проанализировать проблемный запрос отдельно — хотелось бы — но надо сначала определить что за запрос. Профайлером ловить бесполезно — starting событие мы уже пропустили, а completed неясно сколько ждать.
                        А может висит и не пользовательский запрос совсем, а может это сам сервер что-то активно делает…
                        Давайте разбираться
                        Все вы наверно видели «Activity Monitor». В старших студиях его функционал стал богаче. Чем он может нам помочь? В «Activity Monitor» много полезного и интересного, но третий раздел не о нем. Всё что нужно будем доставать напрямую из системных представлений и функций (а сам Монитор полезен тем, что на него можно натравить профайлер и посмотреть какие запросы он выполняет). Нам понадобятся:
                        • sys.dm_exec_sessions — информация о сессиях. Отображает информацию по подключенным пользователям. Полезные поля (в рамках этой статьи) — идентифицирующие пользователя (login_name, login_time, host_name, program_name, ...) и поля с информацией о затраченных ресурсах (cpu_time, reads, writes, memory_usage, ...)
                        • sys.dm_exec_requests — информация о запросах выполняющихся в данный момент. Полей тут тоже довольно много, рассмотрим только некоторые:
                          • session_id — код сессии для связи с предыдущим представлением
                          • start_time — время старта запроса
                          • command — это поле, вопреки названию, содержит не запрос, а тип выполняемой команды. Для пользовательских запросов — обычно это что-то вроде select/update/delete/и т.п. (также, важные примечания ниже)
                          • sql_handle, statement_start_offset, statement_end_offset — информация для получения текста запроса: хэндл, а также начальная и конечная позиция в тексте запроса — обозначающая часть выполняемую в данный момент (для случая когда ваш запрос содержит несколько команд).
                          • plan_handle — хэндл сгенерированного плана.
                          • blocking_session_id — при возникновении блокировок препятствующих выполнению запроса — указывает на номер сессии которая стала причиной блокировки
                          • wait_type, wait_time, wait_resource — поля с информацией о причине и длительности ожидания. Для некоторых видов ожидания, например, блокировка данных — дополнительно указывается код заблокированного ресурса.
                          • percent_complete — по названию понятно, что это процент выполнения. К сожалению, доступен только для команд у которых четко прогнозируемый прогресс выполнения (например, backup или restore).
                          • cpu_time, reads, writes, logical_reads, granted_query_memory — затраты ресурсов.
                        • sys.dm_exec_sql_text(sql_handle | plan_handle), sys.dm_exec_query_plan(plan_handle) — функции получения текста и плана запроса. Ниже рассмотрим пример использования.
                        • sys.dm_exec_query_stats — сводная статистика выполнения в разрезе запросов. Показывает какой запрос сколько раз выполнялся и сколько ресурсов на это потрачено.

                        Важные примечания
                        Приведенный перечень — лишь малая часть. Полный список всех системных представлений и функций описан в документации. Также, имеется схема связей основных объектов в виде красивой картинки — можно распечатать на А1 и повесить на стену.
                        Текст запроса, его план и статистика исполнения — данные хранящиеся в процедурном кэше. Во время выполнения они доступны. После выполнения доступность не гарантируется и зависит от давления на кэш. Да, кэш можно очищать вручную. Иногда это советуют делать когда «поплыли» планы выполнения, но тут очень много нюансов… В общем, «Имеются противопоказания, рекомендовано проконсультироваться со специалистом».
                        Поле «command» — для пользовательских запросов оно практически бессмысленно — ведь мы можем получить полный текст… Но не всё так просто. Это поле очень важно для получения информации о системных процессах. Как правило, они выполняют какие-то внутренние задачи и не имеют текста sql. Для таких процессов, информация о команде единственный намек на тип активности. В комментариях к предыдущей статье был вопрос про то, чем занят сервер, когда он, вроде бы, ничем не должен быть занят — возможно ответ будет в значении этого поля. На моей практике, поле «command» для активных системных процессов всегда выдавало что-то вполне понятное: autoshrink/autogrow/checkpoint/logwriter/и т.п.

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

                        Пример 1: Какой процесс расходует cpu/reads/writes/memory
                        Для начала, посмотрим какие сессии больше всего потребляют, например, CPU. Информация в sys.dm_exec_sessions. Но данные по CPU (а также reads, writes) — накопительные. Т.е цифра в поле содержит «итого» за все время подключения. Понятно, что больше всего будет у того кто подключился месяц назад, да так и не отключался ни разу. Это вовсе не означает, что он прямо сейчас грузит систему.
                        Немного кода решает проблему, алгоритм примерно такой:
                        1. сначала сделаем выборку и сохраним во временную таблицу
                        2. затем подождем немного
                        3. делаем выборку второй раз
                        4. сравниваем результаты первой и второй выборки — разница, как раз и будет затратами возникшими на п.2
                        5. для удобства, разницу можем поделить на длительность п.2, чтобы получить усредненные «затраты в секунду».

                        Пример скрипта
                        if object_id('tempdb..#tmp') is NULL
                        BEGIN
                        	SELECT * into #tmp from sys.dm_exec_sessions s
                        	PRINT 'ждем секунду для накопления статистики при первом запуске'
                        	-- при последующих запусках не ждем, т.к. сравниваем с результатом предыдущего запуска
                        	WAITFOR DELAY '00:00:01';
                        END
                        if object_id('tempdb..#tmp1') is not null drop table #tmp1
                        
                        declare @d datetime
                        declare @dd float
                        select @d = crdate from tempdb.dbo.sysobjects where id=object_id('tempdb..#tmp')
                        
                        select * into #tmp1 from sys.dm_exec_sessions s
                        select @dd=datediff(ms,@d,getdate())
                        select @dd AS [интервал времени, мс]
                        
                        SELECT TOP 30 s.session_id, s.host_name, db_name(s.database_id) as db, s.login_name,s.login_time,s.program_name,
                               s.cpu_time-isnull(t.cpu_time,0) as cpu_Diff, convert(numeric(16,2),(s.cpu_time-isnull(t.cpu_time,0))/@dd*1000) as cpu_sec,
                               s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0) as totIO_Diff, convert(numeric(16,2),(s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0))/@dd*1000) as totIO_sec,
                               s.reads-isnull(t.reads,0) as reads_Diff, convert(numeric(16,2),(s.reads-isnull(t.reads,0))/@dd*1000) as reads_sec,
                               s.writes-isnull(t.writes,0) as writes_Diff, convert(numeric(16,2),(s.writes-isnull(t.writes,0))/@dd*1000) as writes_sec,
                               s.logical_reads-isnull(t.logical_reads,0) as logical_reads_Diff, convert(numeric(16,2),(s.logical_reads-isnull(t.logical_reads,0))/@dd*1000) as logical_reads_sec,
                               s.memory_usage, s.memory_usage-isnull(t.memory_usage,0) as [mem_D],
                              s.nt_user_name,s.nt_domain
                        from #tmp1 s
                        LEFT join #tmp t on s.session_id=t.session_id
                        order BY
                        cpu_Diff desc
                        --totIO_Diff desc
                        --logical_reads_Diff desc
                        
                        drop table #tmp
                        GO
                        select * into #tmp from #tmp1
                        drop table #tmp1
                        

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

                        Те, кто не любят выполнять запрос в студии — могут его завернуть в приложение написанное на своём любимом языке программирования. Я покажу как это сделать в MS Excel без единой строки кода.
                        В меню «Данные» подключаемся к серверу. Если будет требовать выбрать таблицу — выбираем произвольную — потом поменяем это. Как всегда, жмем «Next» и «Finish» пока не увидим диалог «Импорт данных» — в нем нужно нажать «Свойства...». В свойствах необходимо сменить «тип команды» на значение «SQL» и в поле «текст команды» вставить немного измененный наш запрос.
                        Запрос придется немного поменять:
                        • добавим «SET NOCOUNT ON» — т.к. Excel не любит отсечки количества строк;
                        • «временные таблицы» заменим на «таблицы переменные»;
                        • задержка всегда будет 1сек — поля с усредненными значениями не нужны

                        Измененный запрос для Excel
                        SET NOCOUNT ON;
                        declare @tmp table(session_id	smallint primary key,login_time	datetime,host_name nvarchar(256),program_name nvarchar(256),login_name nvarchar(256),nt_user_name	nvarchar(256),cpu_time	int,memory_usage	int,reads	bigint,writes	bigint,logical_reads	bigint,database_id	smallint)
                        
                        declare @d datetime;
                        select @d=GETDATE()
                        
                        INSERT INTO @tmp(session_id,login_time,host_name,program_name,login_name,nt_user_name,cpu_time,memory_usage,reads,writes,logical_reads,database_id)
                        SELECT session_id,login_time,host_name,program_name,login_name,nt_user_name,cpu_time,memory_usage,reads,writes,logical_reads,database_id
                        from sys.dm_exec_sessions s;
                        
                        WAITFOR DELAY '00:00:01';
                        
                        declare @dd float;
                        select @dd=datediff(ms,@d,getdate());
                        
                        SELECT 
                        	s.session_id, s.host_name, db_name(s.database_id) as db, s.login_name,s.login_time,s.program_name,
                        	s.cpu_time-isnull(t.cpu_time,0) as cpu_Diff,
                        	s.reads+s.writes-isnull(t.reads,0)-isnull(t.writes,0) as totIO_Diff,
                        	s.reads-isnull(t.reads,0) as reads_Diff,
                        	s.writes-isnull(t.writes,0) as writes_Diff,
                        	s.logical_reads-isnull(t.logical_reads,0) as logical_reads_Diff,
                        	s.memory_usage, s.memory_usage-isnull(t.memory_usage,0) as [mem_Diff],
                        	s.nt_user_name,s.nt_domain
                        from sys.dm_exec_sessions s
                        left join @tmp t on s.session_id=t.session_id

                        Картинки процесса




                        Результат


                        Когда данные будут в Excel-е, можете их сортировать как вам нужно. Для актуализации информации — жмите «Обновить». В настройках книги, для удобства, можете поставить «автообновление» через заданный период времени и «обновление при открытии». Файл можете сохранить и передать коллегам. Таким образом, мы из навоза и веточек подручных средств собрали ЫнтерпрайзМониторингТул удобный и простой инструмент.

                        Пример 2: На что сессия расходует ресурсы
                        Итак, в предыдущем примере мы определили проблемные сессии. Теперь определим, что именно они делают. Используем sys.dm_exec_requests, а также функции получения текста и плана запроса.
                        текст запроса и план по номеру сессии
                        DECLARE @sql_handle varbinary(64)
                        DECLARE @plan_handle varbinary(64)
                        DECLARE @sid INT
                        Declare @statement_start_offset int, @statement_end_offset INT, @session_id SMALLINT
                        
                        -- для инфы по конкретному юзеру - указываем номер сессии
                        SELECT @sid=182
                        
                        -- получаем переменные состояния для дальнейшей обработки
                        IF @sid IS NOT NULL
                        SELECT @sql_handle=der.sql_handle, @plan_handle=der.plan_handle, @statement_start_offset=der.statement_start_offset, @statement_end_offset=der.statement_end_offset, @session_id = der.session_id
                        FROM sys.dm_exec_requests der WHERE der.session_id=@sid
                        
                        --печатаем текст выполняемого запроса
                        DECLARE @txt VARCHAR(max)
                        IF @sql_handle IS NOT NULL
                        SELECT @txt=[text] FROM sys.dm_exec_sql_text(@sql_handle)
                        PRINT @txt
                        -- выводим план выполняемого батча/процы
                        IF @plan_handle IS NOT NULL
                        select * from sys.dm_exec_query_plan(@plan_handle)
                        -- и план выполняемого запроса в рамках батча/процы
                        IF @plan_handle IS NOT NULL
                        SELECT dbid, objectid, number, encrypted, CAST(query_plan AS XML) AS planxml
                        from sys.dm_exec_text_query_plan(@plan_handle, @statement_start_offset, @statement_end_offset)

                        Подставляйте в запрос номер сессии и выполняйте. После выполнения, на закладке «Results» будут планы (два: первый для всего запроса, второй для текущего шага — если шагов в запросе несколько), на закладке «Messages» — текст запроса. Для просмотра плана — необходимо кликнуть в строке на текст оформленный в виде url. План откроется в отдельной закладке. Иногда бывает что план открывается не в графическом виде, а в виде xml-текста. Это, скорее всего, связано с тем что версия студии ниже чем сервера. Попробуйте пересохранить полученный xml в файл с расширением sqlplan, предварительно удалив из первой строки упоминания «Version» и «Build», а затем отдельно открыть его. Если и это не помогает — напоминаю, что 2016 студия официально доступна бесплатно на сайте MS.
                        Картинки






                        Очевидно, полученный план будет «оценочным», т.к. запрос еще выполняется. Но некоторую статистику по выполнению получить всё равно можно. Используем представление sys.dm_exec_query_stats с фильтром по нашим хэндлам.
                        Допишем в конец предыдущего запроса
                        -- статистика по плану
                        IF @sql_handle IS NOT NULL
                        SELECT * FROM sys.dm_exec_query_stats QS WHERE QS.sql_handle=@sql_handle
                        

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

                        Пример 3: А можно всех посмотреть
                        Давайте объединим рассмотренные системные представления и функции в одном запросе. Это может быть удобно для оценки ситуации в целом.
                        -- получаем список всех текущих запросов
                        SELECT LEFT((SELECT [text] FROM sys.dm_exec_sql_text(der.sql_handle)),500) AS txt
                        --,(select top 1 1 from sys.dm_exec_query_profiles where session_id=der.session_id) as HasLiveStat
                        ,der.blocking_session_id as blocker, DB_NAME(der.database_id) AS База, s.login_name, *
                        from sys.dm_exec_requests der
                        left join sys.dm_exec_sessions s ON s.session_id = der.session_id
                        WHERE der.session_id<>@@SPID
                        -- AND der.session_id>50
                        

                        Запрос выводит список активных сессий и тексты их запросов. Для системных процессов, напоминаю, обычно запрос отсутствует, но заполнено поле «command». Видна информация о блокировках и ожиданиях. Можете попробовать скрестить этот запрос с примером 1, чтобы еще и отсортировать по нагрузке. Но будьте аккуратны — тексты запросов могут оказаться очень большими. Выбирать их массово может оказаться ресурсоемко. Да и трафик будет большим. В примере я ограничил получаемый запрос первыми 500 символами, а получение плана не стал делать.

                        Примеры запросов выложил на гитхаб.

                        Заключение


                        Было бы ещё неплохо, получить Live Query Statistics для произвольной сессии. По заявлению производителя, на текущий момент, постоянный сбор статистики требует значительных ресурсов и поэтому, по умолчанию, отключен. Включить не проблема, но дополнительные манипуляции усложняют процесс и уменьшают практическую пользу. Возможно, попробуем это сделать в отдельной статье.

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

                        Впереди у нас еще анализ нагрузки на память и сеть, а также прочие нюансы. Дойдем и до них. Материала еще на несколько статей.
                        Original source: habrahabr.ru (comments, light).

                        https://habrahabr.ru/post/336586/


                        Метки:  

                        Как я проходил собеседования на позицию Junior .Net Developer

                        Воскресенье, 17 Сентября 2017 г. 14:21 + в цитатник
                        JosefDzeranov сегодня в 14:21 Разработка

                        Как я проходил собеседования на позицию Junior .Net Developer

                          Приветствую всех. Сегодня расскажу вам как я проходил собеседования в Москве на позицию .Net Developer. Усиленно готовился к собеседованиям месяц, целыми днями сидел и смотрел вопросы и пытался отвечать на них, а также читал книжки по С#. В статье привожу интересные задачки и вопросы, которые мне задавали в разных компаниях в Москве. Заранее скажу, что я попал в ту компанию, в которой хотел. Я прошел 4 собеседования в этой компании и меня наконец то взяли! Много статей было прочитано в частности здесь и надеюсь, что эту статью тоже будут читать начинающие Net разработчики и спрашивать все в комментариях.Кому интересна тема прохождения собеседований, прошу под кат! При поиске работы я пользовался Head Hunter. Там очень много вакансий с подробным изложением того, что хотят фирмы от кандидата. Решил откликнутся практически на все вакансии «C# разработчик», потому что для меня главное было пройти как можно больше собеседований, набраться опыта, а также во время таких собеседованиях найти интересную работу в интересной компании (забегая вперед скажу, что я нашел очень интересную работу). Некоторые компании (всего 2), для того, чтобы откликнуться на предложенную ими вакансию, хотели, чтобы кандидат решил предложенные ими задачи (напоминаю это при отклике). На мой взгляд это даже правильно, ведь это показывает, что компания очень ценит свое время и хочет, чтобы кандидат на собеседовании умел решать простые задачи. Обычно на таких собеседованиях спрашивают более «узкие» вопросы. Практически во всех компаниях есть несколько этапов собеседований. На первом обычно собеседует hr, на второй — старший разработчик, на третьем — технический директор. Бывали компании, в которых на первом же приходили и старший разработчик, и Team Lead и технический директор, что конечно чуть заставляет нервничать. Пройдя 5 собеседований, я выяснил такую вещь: чем позитивнее и свободнее ведешь себя на собеседовании, тем легче и быстрее она проходит.А теперь хочу поделиться с вами тем, какие вопросы мне задавали на очных собеседованиях. Поехали.Я не ожидал так много вопросов по базам данных, мне пришлось быстро осваивать (точнее вспоминать) sql язык. Вот какие вопросы и задания мне задавали:
                          1. Что такое кластеризованный и некластеризованный индекс? Когда какое надо использовать?
                          2. Что такое Join? Чем он отличается от Left Join, Right Join? Inner Join? Outer Join?
                          3. Есть три таблицы: CUSTOMERS (ID, NAME, MANAGER_ID); MANAGERS (ID, NAME); ORDERS (ID, DATE, AMOUNT, CUSTOMER_ID). Написать запрос, который выведет имена Customers и их SalesManagers, которые сделали покупок на общую сумму больше 10000 с 01.01.2013.
                          4. Делаем электронный справочник по книгам. Ищем:А) В каком магазине купить данную книгу.Б) В каких магазинах купить книги этого автора (авторов)В) Кто автор книгиГ) Какие книги написал авторНарисовать БД. Написать запрос Б. (Не забыть учесть, что у одной книжки — может быть несколько авторов)
                          5. Что такое агрегирующие функции? Операторы Group By, Having? Приведите примеры их использования.
                          6. Table «PC» (id, cpu(MHz), memory(Mb), hdd(Gb))1) Тактовые частоты CPU тех компьютеров, у которых объем памяти 3000 Мб. Вывод: id, cpu, memory2) Минимальный объём жесткого диска, установленного в компьютере на складе. Вывод: hdd3) Количество компьютеров с минимальным объемом жесткого диска, доступного на складе. Вывод: count, hdd
                          7. Дана следующая структура базы данных в MS SQL: Departments (Id, Name), Employees(Id, DepartmentId, Name, Salary)Необходимо:• Написать запрос получения имени одного сотрудника, имеющего максимальную зарплату в компании, и название его отдела• Получить список отделов, средняя зарплата в которых больше 1000$
                          8. Ado Net – что за технология? и как и когда она используется?
                          9. Что такое Entity Framework? Какие подходы проектирования БД знаете? Расскажите про Code First.
                          Конечно вопросы про ООП
                          1. Назовите и объясните основные парадигмы ООП.
                          2. Назовите преимущества объектно-ориентированного подхода к программированию перед структурным программированием
                          3. Перечислите недостатки ООП парадигмы.
                          4. Что такое раннее и позднее связывание?
                          5. Перечислите модификаторы доступа и когда они используются?
                          Вопросы про паттерны проектирования
                          1. Расскажите про SOLID и примеры его использования
                          2. В чем отличие паттерна «Стратегия» от паттерна «Шаблонный метод»?
                          3. Паттерн Адаптер и его применение
                          4. Расскажите про паттерн «Фасад»
                          Ну и конечно огромное количество вопросов по С#
                          1. using (SomeClass sc = new SomeClass()){} Что делает данная конструкция?
                          2. int i = 1; Console.WriteLine(«i = {0}», ++i); Что выведет данный код?
                          3. Различие класса и структуры? И что будет если их передать в метод в виде параметров?
                          4. Задача: есть нули и единицы в массиве. Надо для каждого нуля посчитать сколько единиц правее него и вывести сумму таких чисел. Сделать за один проход.
                          5. Различие абстрактного класса и интерфейса? Можно ли отказаться от интерфейсов и использовать только абстрактный класс, ведь мы можем в абстрактном классе просто указать сигнатуры методов?
                          6. Что такое интернирование строк ?
                          7. Расскажите про интерфейс IEnumerable? Зачем он используется?
                          8. Когда мы можем пройтись по собственной коллекции foreach- ом? Что для этого надо сделать и почему? (Рассказать про утиную типизацию)
                          9. Различие между IEnumerable and IQueryable ?
                          10. Как устроен Dictionary внутри? Как борются с коллизиями?
                          11. Есть обычный пользовательский класс. Нужно его использовать как ключ в Dictionary. Что для этого надо поменять (добавить) в классе ?
                          12. Какова алгоритмическая сложность для операций чтения и записи для коллекции Dictionary?
                          13. В чем различие между ключевыми словами «ref» и «out»?
                          14. Расскажите как работает try, catch, finally? Когда вызывается каждый
                          15. Чем отличаются друг от друга классы String и StringBuilder? Зачем нужно такое разделение?
                          16. Какие отличие между значимыми и ссылочными типами? Зачем придумали такое разделение? Нельзя было придумать только либо значимые либо ссылочные?
                          17. В чем отличие использования Finalize и Dispose?
                          18. Что такое управляемый код и CLR? Основные требования к управляемому коду?
                          19. Что такое assembly manifest (манифест сборки)?
                          20. Что такое Boxing и Unboxing?
                          21. В чем суть полиморфизма?
                          22. Чем отличается event от delegate?
                          23. Может ли класс реализовать два интерфейса, у которых объявлены одинаковые методы? Если да, то каким образом?
                          24. Что такое абстрактный класс? В каком случае вы обязаны объявить класс абстрактным?
                          25. В чем разница инкапсуляции и сокрытия?
                          26. Что такое частные и общие сборки?
                          27. Что такое .Net Framework?
                          28. LINQ lazy loading, eager loading в чем разница?
                          29. Можно ли запретить наследование от своего собственного класса?
                          30. Определение паттерна синглтон
                          31. Что такое интеграционные тесты и unit-тесты?
                          32. Что такое MVC, MVVM, WEB API?
                          33. Каким образом можно присвоить значения полям, которые помечены ключевым словом readonly?
                          34. Когда вызывается статический конструктор класса?
                          35. Чем отличаются константы и поля, доступные только для чтения?
                          36. Чем отличаются константы и поля, доступные только для чтения?
                          37. Разница между асинхронностью и параллельностью?
                          38. У вас есть сайт, вы заметили что он долго отвечает, как вы будете искать причину?
                          Ну и как же без логических задач
                          1. Вы находитесь в пустом поезде. Это даже не поезд, а просто вагоны, они сцеплены друг с другом. Все вагоны внутри одинаковы, двери на выход из вагона закрыты, через окна ничего не видно. Вы можете включать и выключать свет в вагоне, в котором находитесь, можете сходить в соседний вагон, там тоже можно включать или выключать свет. Вам известно, что вагоны стоят на кольце и сами сцеплены в кольцо, первый вагон сцеплен с последним, ходить по кругу можно сколько угодно. В момент начала решения задачи в каких-то вагонах свет уже горит, в каких-то — не горит.Ваша задача при помощи управления светом в вагонах и перемещения по ним узнать сколько в этом кольце вагонов.
                          2. У вас есть 100 монет по одному рублю. Они все лежат на столе. У вас также есть два носка. Вам нужно распределить все монеты по 2 носкам, так чтобы в одной из них было в два раза больше монет, чем во второй, при этом
                            • не разрешается ломать их (они должны остаться целыми)
                            • надо использовать все 100 монет (прятать нельзя)
                            • они все одинаковые (по весу, по виду)
                          Также в одной компании дали готовый пользовательский класс. Мне нужно было найти логические и синтаксические ошибки. К сожалению, это задание мне не дали с собой взять после собеседования и из-за этого не могу опубликовать, но я помню, что там можно было докопаться практически к каждой строчке!В другой компании, попросили спроектировать базу данных для приложения вроде «Инстаграмм». Потом попросили написать сигнатуру метода «Upload», который отправляет фото на сервер. Прозвучал также вопрос что, если бы руководство изменило вот «это», то как бы вы «переспроектировали» эту базу. Ну так как у меня нет опыта в проектировании, мне пришлось на ходу думать и генерировать идеи, как оказалось, я прошел это собеседование).Другая компания решила дать интересную задачу. Я думаю, что это часть задачи, которую решают внутри компании. Решили проверить, как решают другие. Вот как она звучит: Реализовать программу анализа текста. Входной текст произвольный и может быть большим по объему. Количество и содержание метрик определяется самостоятельно. Требования к алгоритму: Программа должна быть расширяема к изменению списку метрик. Масштабируемость. На что обращаем внимание и что анализируем:
                          1. Какие метрики предложены кандидатом и их смысл — аналитическое мышление
                          2. SOLID анализ предложенного решения — навыки проектирования
                          3. Какие шаблоны применены для масштабируемости и расширяемости — архитектурные скилы кандидата. повод при общении затронуть более глубоко вопрос проектирования
                          4. Какие синтаксические конструкции языка применены и какая технология используется — использование сахара и средств упрощения кода
                          5. Написаны ли unit тесты — что и как тестирует, какие фреймворки и стили тестов использует
                          6. Сравнение с когнитивными сервисами — знание трендов применения технологий в реальных проектах
                          7. Время, сложность решения, активность дополнительных вопросов выполнения — заинтересованность к решаемым задача/получению оффера
                          Так же хочется отметить тот факт, что в своем резюме я указал, что занимаюсь олимпиадным программированием и это очень положительно повлияло на ход многих собеседований. Меня просили рассказать, что это такое. Меня удивило, что большинство интервьюверов не знали про такое движение в программировании. Те, кто знали, просили меня реализовать какие- нибудь сортировки (пузырьковая, вставками, qsort), задачи с олимпиадного программирования. Я считаю, что алгоритмы и структуры данных дают огромный плюс в жизни программиста и теперь как оказалось еще и при трудоустройстве.Также положительным фактом оказалось преподавание информатики в ВЦНМО. Расспрашивали каково преподавать и трудно ли объяснять сложные вещи на пальцах.Для меня было неожиданностью, что меня пригласили работать практически все компании (80%), в которых я проходил собеседование. Может это чувство из-за моей низкой самооценки?! После прохождения собеседований, с уверенностью могу сказать, что это несложно, а даже легко и интересно. Так что друзья, не бойтесь собеседований и крупных компаний, будьте самоуверенными, верьте в свои силы и все будет на 5!Для тех, кто будет готовится или уже готовится к собеседованиям, ниже перечислю ссылки, которые помогут вам (по моему мнению) подготовится к собеседованию на должность Junior C# Developer, .Net Developer.
                          1. metanit.com/sharp. Здесь собран большой материал по C#, А также есть специальный раздел «Вопросы к собеседованию». Рекомендую пока пройти теоретический материал, а потом попробовать тесты. Тесты находятся здесь (https://metanit.com/sharp/interview/).
                          2. www.quizful.net/test. Сайт направлен именно на собеседования по разным направлениям разработки, где вы также сможете найти и C#. Там есть очень много «острых» и хитрых вопросов.
                          3. CLR via C#. Программирование на платформе Microsoft.NET Framework 4.5 на языке C#. Джефри Рихтер. Эта классическая книга по освоению .Net. Там очень хорошо написано про CLR, про сборки и манифесты. Книга написано на довольно простом языке.
                          4. metanit.com/sql. Здесь довольно хорошо и просто описывается язык запросов SQL.
                          5. www.w3schools.com/sql/default.asp Здесь также можете пройти туториал по SQL. В конце будет тест, который очень легко сдать(опять- таки, это лично субъективное мнение).
                          6. tproger.ru/articles/problems Здесь собраны наиболее интересные логические задачи с ответами.
                          7. ivinsky.livejournal.com/3266.html здесь собраны задачи по C#, которые часто бывают на собеседованиях
                          8. oignatov.blogspot.ru/2015/10/net-developer.html здесь собраны задачи по C#, которые часто бывают на собеседованиях
                          9. jopr.org/blog/detail/voprosy-na-sobesedovanii-po-c здесь собраны задачи по C#, которые часто бывают на собеседованиях
                          Original source: habrahabr.ru (comments, light).

                          https://habrahabr.ru/post/338102/


                          Метки:  

                          Как я проходил собеседования на позицию Junior .Net Developer

                          Воскресенье, 17 Сентября 2017 г. 14:21 + в цитатник
                          JosefDzeranov сегодня в 14:21 Разработка

                          Как я проходил собеседования на позицию Junior .Net Developer

                            Приветствую всех. Сегодня расскажу вам как я проходил собеседования в Москве на позицию .Net Developer. Усиленно готовился к собеседованиям месяц, целыми днями сидел и смотрел вопросы и пытался отвечать на них, а также читал книжки по С#. В статье привожу интересные задачки и вопросы, которые мне задавали в разных компаниях в Москве. Заранее скажу, что я попал в ту компанию, в которой хотел. Я прошел 4 собеседования в этой компании и меня наконец то взяли! Много статей было прочитано в частности здесь и надеюсь, что эту статью тоже будут читать начинающие Net разработчики и спрашивать все в комментариях.Кому интересна тема прохождения собеседований, прошу под кат! При поиске работы я пользовался Head Hunter. Там очень много вакансий с подробным изложением того, что хотят фирмы от кандидата. Решил откликнутся практически на все вакансии «C# разработчик», потому что для меня главное было пройти как можно больше собеседований, набраться опыта, а также во время таких собеседованиях найти интересную работу в интересной компании (забегая вперед скажу, что я нашел очень интересную работу). Некоторые компании (всего 2), для того, чтобы откликнуться на предложенную ими вакансию, хотели, чтобы кандидат решил предложенные ими задачи (напоминаю это при отклике). На мой взгляд это даже правильно, ведь это показывает, что компания очень ценит свое время и хочет, чтобы кандидат на собеседовании умел решать простые задачи. Обычно на таких собеседованиях спрашивают более «узкие» вопросы. Практически во всех компаниях есть несколько этапов собеседований. На первом обычно собеседует hr, на второй — старший разработчик, на третьем — технический директор. Бывали компании, в которых на первом же приходили и старший разработчик, и Team Lead и технический директор, что конечно чуть заставляет нервничать. Пройдя 5 собеседований, я выяснил такую вещь: чем позитивнее и свободнее ведешь себя на собеседовании, тем легче и быстрее она проходит.А теперь хочу поделиться с вами тем, какие вопросы мне задавали на очных собеседованиях. Поехали.Я не ожидал так много вопросов по базам данных, мне пришлось быстро осваивать (точнее вспоминать) sql язык. Вот какие вопросы и задания мне задавали:
                            1. Что такое кластеризованный и некластеризованный индекс? Когда какое надо использовать?
                            2. Что такое Join? Чем он отличается от Left Join, Right Join? Inner Join? Outer Join?
                            3. Есть три таблицы: CUSTOMERS (ID, NAME, MANAGER_ID); MANAGERS (ID, NAME); ORDERS (ID, DATE, AMOUNT, CUSTOMER_ID). Написать запрос, который выведет имена Customers и их SalesManagers, которые сделали покупок на общую сумму больше 10000 с 01.01.2013.
                            4. Делаем электронный справочник по книгам. Ищем:А) В каком магазине купить данную книгу.Б) В каких магазинах купить книги этого автора (авторов)В) Кто автор книгиГ) Какие книги написал авторНарисовать БД. Написать запрос Б. (Не забыть учесть, что у одной книжки — может быть несколько авторов)
                            5. Что такое агрегирующие функции? Операторы Group By, Having? Приведите примеры их использования.
                            6. Table «PC» (id, cpu(MHz), memory(Mb), hdd(Gb))1) Тактовые частоты CPU тех компьютеров, у которых объем памяти 3000 Мб. Вывод: id, cpu, memory2) Минимальный объём жесткого диска, установленного в компьютере на складе. Вывод: hdd3) Количество компьютеров с минимальным объемом жесткого диска, доступного на складе. Вывод: count, hdd
                            7. Дана следующая структура базы данных в MS SQL: Departments (Id, Name), Employees(Id, DepartmentId, Name, Salary)Необходимо:• Написать запрос получения имени одного сотрудника, имеющего максимальную зарплату в компании, и название его отдела• Получить список отделов, средняя зарплата в которых больше 1000$
                            8. Ado Net – что за технология? и как и когда она используется?
                            9. Что такое Entity Framework? Какие подходы проектирования БД знаете? Расскажите про Code First.
                            Конечно вопросы про ООП
                            1. Назовите и объясните основные парадигмы ООП.
                            2. Назовите преимущества объектно-ориентированного подхода к программированию перед структурным программированием
                            3. Перечислите недостатки ООП парадигмы.
                            4. Что такое раннее и позднее связывание?
                            5. Перечислите модификаторы доступа и когда они используются?
                            Вопросы про паттерны проектирования
                            1. Расскажите про SOLID и примеры его использования
                            2. В чем отличие паттерна «Стратегия» от паттерна «Шаблонный метод»?
                            3. Паттерн Адаптер и его применение
                            4. Расскажите про паттерн «Фасад»
                            Ну и конечно огромное количество вопросов по С#
                            1. using (SomeClass sc = new SomeClass()){} Что делает данная конструкция?
                            2. int i = 1; Console.WriteLine(«i = {0}», ++i); Что выведет данный код?
                            3. Различие класса и структуры? И что будет если их передать в метод в виде параметров?
                            4. Задача: есть нули и единицы в массиве. Надо для каждого нуля посчитать сколько единиц правее него и вывести сумму таких чисел. Сделать за один проход.
                            5. Различие абстрактного класса и интерфейса? Можно ли отказаться от интерфейсов и использовать только абстрактный класс, ведь мы можем в абстрактном классе просто указать сигнатуры методов?
                            6. Что такое интернирование строк ?
                            7. Расскажите про интерфейс IEnumerable? Зачем он используется?
                            8. Когда мы можем пройтись по собственной коллекции foreach- ом? Что для этого надо сделать и почему? (Рассказать про утиную типизацию)
                            9. Различие между IEnumerable and IQueryable ?
                            10. Как устроен Dictionary внутри? Как борются с коллизиями?
                            11. Есть обычный пользовательский класс. Нужно его использовать как ключ в Dictionary. Что для этого надо поменять (добавить) в классе ?
                            12. Какова алгоритмическая сложность для операций чтения и записи для коллекции Dictionary?
                            13. В чем различие между ключевыми словами «ref» и «out»?
                            14. Расскажите как работает try, catch, finally? Когда вызывается каждый
                            15. Чем отличаются друг от друга классы String и StringBuilder? Зачем нужно такое разделение?
                            16. Какие отличие между значимыми и ссылочными типами? Зачем придумали такое разделение? Нельзя было придумать только либо значимые либо ссылочные?
                            17. В чем отличие использования Finalize и Dispose?
                            18. Что такое управляемый код и CLR? Основные требования к управляемому коду?
                            19. Что такое assembly manifest (манифест сборки)?
                            20. Что такое Boxing и Unboxing?
                            21. В чем суть полиморфизма?
                            22. Чем отличается event от delegate?
                            23. Может ли класс реализовать два интерфейса, у которых объявлены одинаковые методы? Если да, то каким образом?
                            24. Что такое абстрактный класс? В каком случае вы обязаны объявить класс абстрактным?
                            25. В чем разница инкапсуляции и сокрытия?
                            26. Что такое частные и общие сборки?
                            27. Что такое .Net Framework?
                            28. LINQ lazy loading, eager loading в чем разница?
                            29. Можно ли запретить наследование от своего собственного класса?
                            30. Определение паттерна синглтон
                            31. Что такое интеграционные тесты и unit-тесты?
                            32. Что такое MVC, MVVM, WEB API?
                            33. Каким образом можно присвоить значения полям, которые помечены ключевым словом readonly?
                            34. Когда вызывается статический конструктор класса?
                            35. Чем отличаются константы и поля, доступные только для чтения?
                            36. Чем отличаются константы и поля, доступные только для чтения?
                            37. Разница между асинхронностью и параллельностью?
                            38. У вас есть сайт, вы заметили что он долго отвечает, как вы будете искать причину?
                            Ну и как же без логических задач
                            1. Вы находитесь в пустом поезде. Это даже не поезд, а просто вагоны, они сцеплены друг с другом. Все вагоны внутри одинаковы, двери на выход из вагона закрыты, через окна ничего не видно. Вы можете включать и выключать свет в вагоне, в котором находитесь, можете сходить в соседний вагон, там тоже можно включать или выключать свет. Вам известно, что вагоны стоят на кольце и сами сцеплены в кольцо, первый вагон сцеплен с последним, ходить по кругу можно сколько угодно. В момент начала решения задачи в каких-то вагонах свет уже горит, в каких-то — не горит.Ваша задача при помощи управления светом в вагонах и перемещения по ним узнать сколько в этом кольце вагонов.
                            2. У вас есть 100 монет по одному рублю. Они все лежат на столе. У вас также есть два носка. Вам нужно распределить все монеты по 2 носкам, так чтобы в одной из них было в два раза больше монет, чем во второй, при этом
                              • не разрешается ломать их (они должны остаться целыми)
                              • надо использовать все 100 монет (прятать нельзя)
                              • они все одинаковые (по весу, по виду)
                            Также в одной компании дали готовый пользовательский класс. Мне нужно было найти логические и синтаксические ошибки. К сожалению, это задание мне не дали с собой взять после собеседования и из-за этого не могу опубликовать, но я помню, что там можно было докопаться практически к каждой строчке!В другой компании, попросили спроектировать базу данных для приложения вроде «Инстаграмм». Потом попросили написать сигнатуру метода «Upload», который отправляет фото на сервер. Прозвучал также вопрос что, если бы руководство изменило вот «это», то как бы вы «переспроектировали» эту базу. Ну так как у меня нет опыта в проектировании, мне пришлось на ходу думать и генерировать идеи, как оказалось, я прошел это собеседование).Другая компания решила дать интересную задачу. Я думаю, что это часть задачи, которую решают внутри компании. Решили проверить, как решают другие. Вот как она звучит: Реализовать программу анализа текста. Входной текст произвольный и может быть большим по объему. Количество и содержание метрик определяется самостоятельно. Требования к алгоритму: Программа должна быть расширяема к изменению списку метрик. Масштабируемость. На что обращаем внимание и что анализируем:
                            1. Какие метрики предложены кандидатом и их смысл — аналитическое мышление
                            2. SOLID анализ предложенного решения — навыки проектирования
                            3. Какие шаблоны применены для масштабируемости и расширяемости — архитектурные скилы кандидата. повод при общении затронуть более глубоко вопрос проектирования
                            4. Какие синтаксические конструкции языка применены и какая технология используется — использование сахара и средств упрощения кода
                            5. Написаны ли unit тесты — что и как тестирует, какие фреймворки и стили тестов использует
                            6. Сравнение с когнитивными сервисами — знание трендов применения технологий в реальных проектах
                            7. Время, сложность решения, активность дополнительных вопросов выполнения — заинтересованность к решаемым задача/получению оффера
                            Так же хочется отметить тот факт, что в своем резюме я указал, что занимаюсь олимпиадным программированием и это очень положительно повлияло на ход многих собеседований. Меня просили рассказать, что это такое. Меня удивило, что большинство интервьюверов не знали про такое движение в программировании. Те, кто знали, просили меня реализовать какие- нибудь сортировки (пузырьковая, вставками, qsort), задачи с олимпиадного программирования. Я считаю, что алгоритмы и структуры данных дают огромный плюс в жизни программиста и теперь как оказалось еще и при трудоустройстве.Также положительным фактом оказалось преподавание информатики в ВЦНМО. Расспрашивали каково преподавать и трудно ли объяснять сложные вещи на пальцах.Для меня было неожиданностью, что меня пригласили работать практически все компании (80%), в которых я проходил собеседование. Может это чувство из-за моей низкой самооценки?! После прохождения собеседований, с уверенностью могу сказать, что это несложно, а даже легко и интересно. Так что друзья, не бойтесь собеседований и крупных компаний, будьте самоуверенными, верьте в свои силы и все будет на 5!Для тех, кто будет готовится или уже готовится к собеседованиям, ниже перечислю ссылки, которые помогут вам (по моему мнению) подготовится к собеседованию на должность Junior C# Developer, .Net Developer.
                            1. metanit.com/sharp. Здесь собран большой материал по C#, А также есть специальный раздел «Вопросы к собеседованию». Рекомендую пока пройти теоретический материал, а потом попробовать тесты. Тесты находятся здесь (https://metanit.com/sharp/interview/).
                            2. www.quizful.net/test. Сайт направлен именно на собеседования по разным направлениям разработки, где вы также сможете найти и C#. Там есть очень много «острых» и хитрых вопросов.
                            3. CLR via C#. Программирование на платформе Microsoft.NET Framework 4.5 на языке C#. Джефри Рихтер. Эта классическая книга по освоению .Net. Там очень хорошо написано про CLR, про сборки и манифесты. Книга написано на довольно простом языке.
                            4. metanit.com/sql. Здесь довольно хорошо и просто описывается язык запросов SQL.
                            5. www.w3schools.com/sql/default.asp Здесь также можете пройти туториал по SQL. В конце будет тест, который очень легко сдать(опять- таки, это лично субъективное мнение).
                            6. tproger.ru/articles/problems Здесь собраны наиболее интересные логические задачи с ответами.
                            7. ivinsky.livejournal.com/3266.html здесь собраны задачи по C#, которые часто бывают на собеседованиях
                            8. oignatov.blogspot.ru/2015/10/net-developer.html здесь собраны задачи по C#, которые часто бывают на собеседованиях
                            9. jopr.org/blog/detail/voprosy-na-sobesedovanii-po-c здесь собраны задачи по C#, которые часто бывают на собеседованиях
                            Original source: habrahabr.ru (comments, light).

                            https://habrahabr.ru/post/338102/


                            Метки:  

                            Управление фермой Android-устройств. Лекция в Яндексе

                            Воскресенье, 17 Сентября 2017 г. 13:43 + в цитатник
                            Leono сегодня в 13:43 Разработка

                            Управление фермой Android-устройств. Лекция в Яндексе

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

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




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

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

                              В центре приложения «Мой Офис Документы» стоит нативное ядро, CORE, которое делается внутри нашей компании, но делается целой командой. Оно написано на чистом С++, чтобы его потом можно было переиспользовать. Переиспользуется оно всеми командами, которые работают над линейкой наших продуктов.

                              Это десктоп, веб, Android, iOS. Даже Tizen есть, его полноценно и весьма успешно делают.

                              Самое важное — это что ребята, которые делают CORE, очень тесно завязаны на всех, кто использует этот CORE. Часто происходят ситуации, когда мы просим функционал, который был бы полезен, например, на мобильных клиентах, на iOS, на Android, но на десктопе он не так полезен. Есть такие вещи.

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



                              Есть три компонента: нативный CORE, Android-библиотека, в которую этот CORE обернут, и наше Java-приложение. Нативный CORE — чистый С++. Дальше в виде source-кодов он идет в Android-модуль, где сделаны все биндинги для натива, добавлены небольшие прослойки, какая-то логика. Плюс вся отрисовка, которую использует SKI, тоже сделана в этом модуле. Затем модуль как стандартная AR-библиотека вставляется в наше приложение, мы ее используем.

                              Всякое происходит — могут появляться баги в нативе, в CORE, в биндингах. Тот же unsatisfying link exception, когда неправильно забиндили Java-класс на нативный класс и получили ситуацию, при которой приложение падает во время обращения к нативному методу. И баги у нас в Java-коде.

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

                              Чтобы это делать, нужны тесты — самое лучшее, что может быть в данной ситуации. Наша задача — делать код, работающее приложение. Самый простой способ увидеть, что оно работает, — написать для него тесты. Тесты бывают абсолютно разные. Мы пишем все: unit, UI, интеграционные тесты.

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

                              Тесты должны регулярно запускаться на CI. Иначе они бессмысленны. Если у вас есть тесты, которые лежат, то у вас нет тестов.



                              С unit-тестами все довольно просто. Запускаем grlu-тест, таску. Она запускается на CI — все хорошо, прогоняются unit-тесты в вашем приложении, вы видите репорты, отчеты.

                              Android-тесты запускаются как тесты connect Android, там нужны те же unit-тесты, только они гоняются на девайсах. И тут возникает проблема: UI и интеграционные тесты должны запускаться на реальных устройствах. А CI — не реальное устройство. Можно эту проблему решить несколькими способами.

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

                              Можно запускать эмулятор на CI. Это довольно рабочий вариант, тот же Jenkins поддерживает плагин, который позволяет запускать эмулятор, но проблема в том, что эмулятор — это, скорее всего, 86-й эмулятор. А если мы говорим про интеграционные тесты, под интеграцией я в нашем случае подразумеваю внешние зависимости, в частности — именно нативный код, потому что у нас очень много нативного кода. И под интеграционными тестами я понимаю тесты, которые проверяют логику «плюсов».

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

                              Тут на сцену выходит проект Open STF.

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

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

                              Проект Open STF — опенсорсный, и у него есть несколько преимуществ. В первую очередь — работа с реальными устройствами. Как большинство Android-разработчиков, вы понимаете, что ваш код должен проверяться на устройствах. Эмулятор — это хорошо, но есть много вещей, которые нужно проверять на реальных устройствах: тот же натив, работа с SSL. Там много вещей, поведение которых может отличаться. Ферма эту проблему решает.



                              Что приятно, для работы с этой фермой вам не нужен root на девайсах. Вы просто подключаете девайс к ферме, и он доступен для работы.

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

                              Устройством могут пользоваться несколько команд. В нашем случае это интересный кейс. У нас обширный парк девайсов в компании и несколько команд, которые с ними работают. Мы — разработчики, тестировщики, все сидим рядом, и девайсы нам нужны для работы. Вторая команда — автоматизаторы, они сидят на другом этаже, но пишут автоматизированные тесты со своим стеком технологий для всех, в том числе для Android. Они тоже имеют доступ ко всем устройствам в компании. Третья — служба поддержки. Им тоже нужно как можно больше устройств. Когда пишут про проблемы в продукте, им нужно их воспроизводить. Проблемы могут быть разные. Преимущество в том, что они имеют доступ ко всем устройствам компании. Это плюс, на устройствах можно запускать приложения и быстрее оказывать поддержку.

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

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

                              Есть Rest API, можно подумать про автоматизацию.

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



                              Как и у любого проекта, есть недостатки. Не все устройства поддерживаются. Не смогу назвать точных правил. Бывает, подключаете устройство к ферме и оно не определяется. Такое бывает очень редко, у нас был буквально один или два девайса. 95% всего парка работает отлично. Бывает исключение с какими-то китайцами — не определяется и все. Один девайс на 86-м процессоре, фиг знает почему.

                              Не очень удобно обновлять. К вопросу об обновлении самого продукта STF: поскольку это open source, обновлением в нашей компании занимается команда девопсов. Это не просто нажать кнопку и обновить. Но нет ничего невозможного. Поскольку речь идет про open source, можно облегчить процесс, проблема не критическая.

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



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

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



                              Ко всем устройствам написано, что это за продукт, какая версия ОС, SDK level, какие архитектуры у этого устройства. И его location — провайдер, о котором я говорил. Тут два провайдера. Это наши устройства и устройства саппорта. Последние мы стараемся не трогать, это их устройства, доступные через единый интерфейс.

                              Сама ферма расположена на GitHub. Первая ссылка — больше рекламная штука.



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

                              Проблема была в том, что нужно было как-то подружить эти две вещи. Есть тесты и ферма.



                              CI-сервер — это любой сервис, который вам больше нравится. Мы используем Jenkins, у меня примеры с интерфейсом про Jenkins, но вы ни к чему не привязаны.

                              У вас есть STF сервер — сам сервер, провайдер, устройства.

                              Как их объединять? Очевидно, самый простой способ — Gradle-плагин, который позволяет настроить подключение к ферме при запуске тестов.

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

                              Что такое нужное устройство? Через плагин вы можете гибко настроить то, какие именно устройства вам нужны. Вы можете отфильтровать их по названию, взять одни Nexus или Samsung, выбрать количество, которое вы хотите отфильтровать. Это может быть один небольшой набор тестов — вы говорите, что хочу на двух девайсах прогнать и убедиться, что ничего не отломалось. Или nightly-прогон сделать, который все девайсы возьмет, проверит, все запустит, все будет отображаться.

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

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

                              Еще полезно сортировать по API-уровню. Если вы хотите зачем-нибудь запустить тест на API 21 и выше — это можно.



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



                              Сейчас сделан следующий шаг. При запуске нужно привязаться к таске запуска тестов, которая будет запускаться на CI, чтобы плагин работал. Сейчас сделано таким образом. Может, и неудобно, но как есть. Улучшить — не проблема. Главное, что можно привязаться к таске connectToSmartphoneTestFarm. Это основная таска, отвечающая за то, чтобы подключиться к девайсам и отпустить девайсы.



                              Ну и третье — настройка параметров фермы. baseUrl — путь, где ферма расположена. apiKey — ключ, чтобы подключаться по REST, это настраивается в консоли фермы. adbPath — чтобы выполнялась операция adbConnect ко всем устройствам, которые будут найдены. Timeout — системная настройка, по дефолту стоит минута. Она нужна, чтобы ферма сама отпускала девайсы, если они по каким-то причинам не используются.



                              Так выглядит запуск тестов с использованием фермы. Мы говорим, что connectedDebugAndroidTest запустит все ваши тесты, и сюда передан параметр о том, чтобы не использовать саппорт. Тильда — в данном случае отрицание. Дальше сказать, что я хочу пять устройств, и чтобы они все были –DK21, то есть Lollypop и выше.



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



                              Как итог — после прогона всех тестов вы видите самый стандартный HTML-репорт запуска GUnit, только с одним аспектом: вы будете видеть, что они запускались на разных устройствах. Вы будете видеть названия всех тестов, что вы пробежали, и поймете, что они запустились на каждом устройстве. Вы даже увидите, сколько они запускаются по времени, чтобы из этого в дальнейшем строить анализ, чтобы искать какую-то регрессию. Тут полет для фантазии — можно продумать тест, который один и тот же код запустит сто раз и померяет это. И вы увидите, как код на 86-м или на ARM работает: быстрее или медленнее. Ферма в этом поможет, чтобы можно было не руками подключать, а в автоматическом режиме.



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

                              Стоит упомянуть, что Gradle есть не везде по объективным причинам. Например, это может быть Appium. Я упоминал ранее, что у нас есть команда автоматизаторов, которые пишут свои тесты на технологии Appium. Там и не пахло Gradle, но им тоже надо использовать ферму.

                              Это может быть терминал. Есть ферма, на девайсе произошел какой-то краш, и хорошо бы получить log cut с него, скачать файл, что угодно. Что делать? Либо взять девайс и подключить к себе — но тогда теряется вся магия фермы, — либо использовать какой-то дополнительный клиент.



                              Разработали тулзу простую, которая делает все то же самое, но работает через терминал. Вы так же можете подключаться к устройствам, отключаться, выводить их в список, подключаться, чтобы они были доступны в adb, и эта команда говорит: нужны пять Nexus, когда их найдешь — подключись ко всем. После выполнения команды у вас будут в adb доступны пять устройств. Можете что хотите делать из терминала, тоже удобно. Главное преимущество — это быстрее, нежели делать руками. И тоже доступно на GitHub.

                              Чисто технически Gradle-плагин и клиент используют нашу библиотеку STL client вместе. Весь сервер написан на Java, есть дальнейшие планы дописать плагин для студии, чтобы девайсы можно было выбирать прямо из UI студии, когда вы работаете. По собственным ощущениям, последние полгода я устройства руками не трогал. Устройства лежат на ферме, я к ним подключаюсь через веб-интерфейс, подключаюсь к adb, копирую путь на ферме и руками девайс не трогаю — лениво. Просто подключился к другому устройству — работаешь с другим.

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



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

                              Задача — разделять их. Это то, с чем мы столкнулись, когда начали интегрировать весь процесс работы с фермой и всю описанную автоматизацию. У нас есть как интеграционный тест, так и UI-тесты.

                              Есть несколько способов. Самые простые известны. И все рабочие, кому как удобнее.

                              Можно написать свой test runner, который будет анализировать, например, названия классов. Рабочий вариант вполне. Договаривайтесь, что вы именуете классы, которые заканчиваются на TestIntegration или TestUI. Вполне рабочий вариант — test runner это разруливает.

                              Можно немного пошаманить с Gradle. Складывать тесты в отдельные папки, настроить в Gradle, чтобы он эти папки видел как папки с кодом. На Stack Overflow есть хорошее описание, но я не пробовал.

                              Можно использовать вариант с JUnit Suit — классом, который позволяет компоновать тесты. Мы остановились на этом варианте, только потому что он самый простой. У него есть недостатки, но с ним проще всего стартануть — не нужно с Gradle шаманить и писать test runner и переименовывать классы, которые у нас есть.

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



                              Когда вы запускаете таску для теста, надо указать отдельным параметром, что я хочу запускать именно данный suite, чтобы запустился только он. Технически так любой тест можно передать, это удобно. Обычно все тесты запустили, они все прошли. В данном случае их можно фильтровать, если вы не используете синтаксис adb instrumentation frame. Все то, что вы видите в студии, когда нажимаете «запустить тесты». Еще это можно шаблонами делать. В данном случае это нужно указать. Тогда будет запускаться только один набор тестов, и он будет выполнять ту изначальную задачу, которую вы перед ним поставили.

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

                              Раньше то, что отломалось, падало вообще сходу, но если какой-то метод плохо забиндился или значение какого-то параметра поменялось, то мы узнавали только в самом конце цикла, когда проводилось ручное тестирование — например, в restore. И тут приходят тестировщики и говорят, что раньше работало одним образом, а сейчас немного по-другому — почему? А мы не знаем. Начинаем разбираться. Сначала виним одних ребят, потом других.

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

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

                              Если у вас такого нет, может, оно вам и не надо. Вы живете, все с вами хорошо.

                              Другое дело, если у вас появляются внешние зависимости — например, нативный код, причем тот, который постоянно меняется, развивается и дописывается, или у вас идет работа с какими-нибудь секьюрными вещами типа SSL, свои сертификаты, все такое, UI не проверить. Технически можно запустить тесты GUnity на CI или на машине, но в идеале хотелось бы, чтобы такие вещи гонялись на устройствах.

                              Тесты полезны. Лютое капитанство: чтобы тесты приносили вам пользу, они должны запускаться и работать с наименьшей болью, чтобы не нужно было руками после какого-то действия брать устройство и подключать руками. Желательно, чтобы оно работало, в идеале само, чтобы можно по job запускать. Либо пусть оно хотя бы работает по нажатию одной кнопки «Проверить». Цель в этом.

                              Все. Это доступно на GitHub, сама ферма с описаниями — как поднять, как настроить, что поддерживается. Плагины доступны, клиенты-плагины.

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

                              Интересная фича, которая легко реализуема. Есть тулза, которая позволяет записывать видео с экрана. Поскольку весь экран гоняется по веб-сокетам, вам ничто не мешает по REST получить веб-сокет, узнать, куда подключиться, и получать все ивенты экрана, работать с ними. Мы для себя это не сделали, в отличие от ребят.
                              Original source: habrahabr.ru (comments, light).

                              https://habrahabr.ru/post/338038/


                              Управление фермой Android-устройств. Лекция в Яндексе

                              Воскресенье, 17 Сентября 2017 г. 13:43 + в цитатник
                              Leono сегодня в 13:43 Разработка

                              Управление фермой Android-устройств. Лекция в Яндексе

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

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




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

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

                                В центре приложения «Мой Офис Документы» стоит нативное ядро, CORE, которое делается внутри нашей компании, но делается целой командой. Оно написано на чистом С++, чтобы его потом можно было переиспользовать. Переиспользуется оно всеми командами, которые работают над линейкой наших продуктов.

                                Это десктоп, веб, Android, iOS. Даже Tizen есть, его полноценно и весьма успешно делают.

                                Самое важное — это что ребята, которые делают CORE, очень тесно завязаны на всех, кто использует этот CORE. Часто происходят ситуации, когда мы просим функционал, который был бы полезен, например, на мобильных клиентах, на iOS, на Android, но на десктопе он не так полезен. Есть такие вещи.

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



                                Есть три компонента: нативный CORE, Android-библиотека, в которую этот CORE обернут, и наше Java-приложение. Нативный CORE — чистый С++. Дальше в виде source-кодов он идет в Android-модуль, где сделаны все биндинги для натива, добавлены небольшие прослойки, какая-то логика. Плюс вся отрисовка, которую использует SKI, тоже сделана в этом модуле. Затем модуль как стандартная AR-библиотека вставляется в наше приложение, мы ее используем.

                                Всякое происходит — могут появляться баги в нативе, в CORE, в биндингах. Тот же unsatisfying link exception, когда неправильно забиндили Java-класс на нативный класс и получили ситуацию, при которой приложение падает во время обращения к нативному методу. И баги у нас в Java-коде.

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

                                Чтобы это делать, нужны тесты — самое лучшее, что может быть в данной ситуации. Наша задача — делать код, работающее приложение. Самый простой способ увидеть, что оно работает, — написать для него тесты. Тесты бывают абсолютно разные. Мы пишем все: unit, UI, интеграционные тесты.

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

                                Тесты должны регулярно запускаться на CI. Иначе они бессмысленны. Если у вас есть тесты, которые лежат, то у вас нет тестов.



                                С unit-тестами все довольно просто. Запускаем grlu-тест, таску. Она запускается на CI — все хорошо, прогоняются unit-тесты в вашем приложении, вы видите репорты, отчеты.

                                Android-тесты запускаются как тесты connect Android, там нужны те же unit-тесты, только они гоняются на девайсах. И тут возникает проблема: UI и интеграционные тесты должны запускаться на реальных устройствах. А CI — не реальное устройство. Можно эту проблему решить несколькими способами.

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

                                Можно запускать эмулятор на CI. Это довольно рабочий вариант, тот же Jenkins поддерживает плагин, который позволяет запускать эмулятор, но проблема в том, что эмулятор — это, скорее всего, 86-й эмулятор. А если мы говорим про интеграционные тесты, под интеграцией я в нашем случае подразумеваю внешние зависимости, в частности — именно нативный код, потому что у нас очень много нативного кода. И под интеграционными тестами я понимаю тесты, которые проверяют логику «плюсов».

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

                                Тут на сцену выходит проект Open STF.

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

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

                                Проект Open STF — опенсорсный, и у него есть несколько преимуществ. В первую очередь — работа с реальными устройствами. Как большинство Android-разработчиков, вы понимаете, что ваш код должен проверяться на устройствах. Эмулятор — это хорошо, но есть много вещей, которые нужно проверять на реальных устройствах: тот же натив, работа с SSL. Там много вещей, поведение которых может отличаться. Ферма эту проблему решает.



                                Что приятно, для работы с этой фермой вам не нужен root на девайсах. Вы просто подключаете девайс к ферме, и он доступен для работы.

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

                                Устройством могут пользоваться несколько команд. В нашем случае это интересный кейс. У нас обширный парк девайсов в компании и несколько команд, которые с ними работают. Мы — разработчики, тестировщики, все сидим рядом, и девайсы нам нужны для работы. Вторая команда — автоматизаторы, они сидят на другом этаже, но пишут автоматизированные тесты со своим стеком технологий для всех, в том числе для Android. Они тоже имеют доступ ко всем устройствам в компании. Третья — служба поддержки. Им тоже нужно как можно больше устройств. Когда пишут про проблемы в продукте, им нужно их воспроизводить. Проблемы могут быть разные. Преимущество в том, что они имеют доступ ко всем устройствам компании. Это плюс, на устройствах можно запускать приложения и быстрее оказывать поддержку.

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

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

                                Есть Rest API, можно подумать про автоматизацию.

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



                                Как и у любого проекта, есть недостатки. Не все устройства поддерживаются. Не смогу назвать точных правил. Бывает, подключаете устройство к ферме и оно не определяется. Такое бывает очень редко, у нас был буквально один или два девайса. 95% всего парка работает отлично. Бывает исключение с какими-то китайцами — не определяется и все. Один девайс на 86-м процессоре, фиг знает почему.

                                Не очень удобно обновлять. К вопросу об обновлении самого продукта STF: поскольку это open source, обновлением в нашей компании занимается команда девопсов. Это не просто нажать кнопку и обновить. Но нет ничего невозможного. Поскольку речь идет про open source, можно облегчить процесс, проблема не критическая.

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



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

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



                                Ко всем устройствам написано, что это за продукт, какая версия ОС, SDK level, какие архитектуры у этого устройства. И его location — провайдер, о котором я говорил. Тут два провайдера. Это наши устройства и устройства саппорта. Последние мы стараемся не трогать, это их устройства, доступные через единый интерфейс.

                                Сама ферма расположена на GitHub. Первая ссылка — больше рекламная штука.



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

                                Проблема была в том, что нужно было как-то подружить эти две вещи. Есть тесты и ферма.



                                CI-сервер — это любой сервис, который вам больше нравится. Мы используем Jenkins, у меня примеры с интерфейсом про Jenkins, но вы ни к чему не привязаны.

                                У вас есть STF сервер — сам сервер, провайдер, устройства.

                                Как их объединять? Очевидно, самый простой способ — Gradle-плагин, который позволяет настроить подключение к ферме при запуске тестов.

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

                                Что такое нужное устройство? Через плагин вы можете гибко настроить то, какие именно устройства вам нужны. Вы можете отфильтровать их по названию, взять одни Nexus или Samsung, выбрать количество, которое вы хотите отфильтровать. Это может быть один небольшой набор тестов — вы говорите, что хочу на двух девайсах прогнать и убедиться, что ничего не отломалось. Или nightly-прогон сделать, который все девайсы возьмет, проверит, все запустит, все будет отображаться.

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

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

                                Еще полезно сортировать по API-уровню. Если вы хотите зачем-нибудь запустить тест на API 21 и выше — это можно.



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



                                Сейчас сделан следующий шаг. При запуске нужно привязаться к таске запуска тестов, которая будет запускаться на CI, чтобы плагин работал. Сейчас сделано таким образом. Может, и неудобно, но как есть. Улучшить — не проблема. Главное, что можно привязаться к таске connectToSmartphoneTestFarm. Это основная таска, отвечающая за то, чтобы подключиться к девайсам и отпустить девайсы.



                                Ну и третье — настройка параметров фермы. baseUrl — путь, где ферма расположена. apiKey — ключ, чтобы подключаться по REST, это настраивается в консоли фермы. adbPath — чтобы выполнялась операция adbConnect ко всем устройствам, которые будут найдены. Timeout — системная настройка, по дефолту стоит минута. Она нужна, чтобы ферма сама отпускала девайсы, если они по каким-то причинам не используются.



                                Так выглядит запуск тестов с использованием фермы. Мы говорим, что connectedDebugAndroidTest запустит все ваши тесты, и сюда передан параметр о том, чтобы не использовать саппорт. Тильда — в данном случае отрицание. Дальше сказать, что я хочу пять устройств, и чтобы они все были –DK21, то есть Lollypop и выше.



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



                                Как итог — после прогона всех тестов вы видите самый стандартный HTML-репорт запуска GUnit, только с одним аспектом: вы будете видеть, что они запускались на разных устройствах. Вы будете видеть названия всех тестов, что вы пробежали, и поймете, что они запустились на каждом устройстве. Вы даже увидите, сколько они запускаются по времени, чтобы из этого в дальнейшем строить анализ, чтобы искать какую-то регрессию. Тут полет для фантазии — можно продумать тест, который один и тот же код запустит сто раз и померяет это. И вы увидите, как код на 86-м или на ARM работает: быстрее или медленнее. Ферма в этом поможет, чтобы можно было не руками подключать, а в автоматическом режиме.



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

                                Стоит упомянуть, что Gradle есть не везде по объективным причинам. Например, это может быть Appium. Я упоминал ранее, что у нас есть команда автоматизаторов, которые пишут свои тесты на технологии Appium. Там и не пахло Gradle, но им тоже надо использовать ферму.

                                Это может быть терминал. Есть ферма, на девайсе произошел какой-то краш, и хорошо бы получить log cut с него, скачать файл, что угодно. Что делать? Либо взять девайс и подключить к себе — но тогда теряется вся магия фермы, — либо использовать какой-то дополнительный клиент.



                                Разработали тулзу простую, которая делает все то же самое, но работает через терминал. Вы так же можете подключаться к устройствам, отключаться, выводить их в список, подключаться, чтобы они были доступны в adb, и эта команда говорит: нужны пять Nexus, когда их найдешь — подключись ко всем. После выполнения команды у вас будут в adb доступны пять устройств. Можете что хотите делать из терминала, тоже удобно. Главное преимущество — это быстрее, нежели делать руками. И тоже доступно на GitHub.

                                Чисто технически Gradle-плагин и клиент используют нашу библиотеку STL client вместе. Весь сервер написан на Java, есть дальнейшие планы дописать плагин для студии, чтобы девайсы можно было выбирать прямо из UI студии, когда вы работаете. По собственным ощущениям, последние полгода я устройства руками не трогал. Устройства лежат на ферме, я к ним подключаюсь через веб-интерфейс, подключаюсь к adb, копирую путь на ферме и руками девайс не трогаю — лениво. Просто подключился к другому устройству — работаешь с другим.

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



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

                                Задача — разделять их. Это то, с чем мы столкнулись, когда начали интегрировать весь процесс работы с фермой и всю описанную автоматизацию. У нас есть как интеграционный тест, так и UI-тесты.

                                Есть несколько способов. Самые простые известны. И все рабочие, кому как удобнее.

                                Можно написать свой test runner, который будет анализировать, например, названия классов. Рабочий вариант вполне. Договаривайтесь, что вы именуете классы, которые заканчиваются на TestIntegration или TestUI. Вполне рабочий вариант — test runner это разруливает.

                                Можно немного пошаманить с Gradle. Складывать тесты в отдельные папки, настроить в Gradle, чтобы он эти папки видел как папки с кодом. На Stack Overflow есть хорошее описание, но я не пробовал.

                                Можно использовать вариант с JUnit Suit — классом, который позволяет компоновать тесты. Мы остановились на этом варианте, только потому что он самый простой. У него есть недостатки, но с ним проще всего стартануть — не нужно с Gradle шаманить и писать test runner и переименовывать классы, которые у нас есть.

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



                                Когда вы запускаете таску для теста, надо указать отдельным параметром, что я хочу запускать именно данный suite, чтобы запустился только он. Технически так любой тест можно передать, это удобно. Обычно все тесты запустили, они все прошли. В данном случае их можно фильтровать, если вы не используете синтаксис adb instrumentation frame. Все то, что вы видите в студии, когда нажимаете «запустить тесты». Еще это можно шаблонами делать. В данном случае это нужно указать. Тогда будет запускаться только один набор тестов, и он будет выполнять ту изначальную задачу, которую вы перед ним поставили.

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

                                Раньше то, что отломалось, падало вообще сходу, но если какой-то метод плохо забиндился или значение какого-то параметра поменялось, то мы узнавали только в самом конце цикла, когда проводилось ручное тестирование — например, в restore. И тут приходят тестировщики и говорят, что раньше работало одним образом, а сейчас немного по-другому — почему? А мы не знаем. Начинаем разбираться. Сначала виним одних ребят, потом других.

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

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

                                Если у вас такого нет, может, оно вам и не надо. Вы живете, все с вами хорошо.

                                Другое дело, если у вас появляются внешние зависимости — например, нативный код, причем тот, который постоянно меняется, развивается и дописывается, или у вас идет работа с какими-нибудь секьюрными вещами типа SSL, свои сертификаты, все такое, UI не проверить. Технически можно запустить тесты GUnity на CI или на машине, но в идеале хотелось бы, чтобы такие вещи гонялись на устройствах.

                                Тесты полезны. Лютое капитанство: чтобы тесты приносили вам пользу, они должны запускаться и работать с наименьшей болью, чтобы не нужно было руками после какого-то действия брать устройство и подключать руками. Желательно, чтобы оно работало, в идеале само, чтобы можно по job запускать. Либо пусть оно хотя бы работает по нажатию одной кнопки «Проверить». Цель в этом.

                                Все. Это доступно на GitHub, сама ферма с описаниями — как поднять, как настроить, что поддерживается. Плагины доступны, клиенты-плагины.

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

                                Интересная фича, которая легко реализуема. Есть тулза, которая позволяет записывать видео с экрана. Поскольку весь экран гоняется по веб-сокетам, вам ничто не мешает по REST получить веб-сокет, узнать, куда подключиться, и получать все ивенты экрана, работать с ними. Мы для себя это не сделали, в отличие от ребят.
                                Original source: habrahabr.ru (comments, light).

                                https://habrahabr.ru/post/338038/



                                Поиск сообщений в rss_rss_hh_full
                                Страницы: 1824 ... 1535 1534 [1533] 1532 1531 ..
                                .. 1 Календарь