Применение интегрирования Монте-Карло в рендеринге

Все мы изучали в курсе математики численные методы. Это такие методы, как интегрирование, интерполяция, ряды и так далее. Существует два вида числовых методов: детерминированные и рандомизированные.

Типичный детерминированный метод интегрирования функции $f$ в интервале $[a, b]$ выглядит так: мы берём $n + 1$ равномерно расположенных в интервале точек $t_0 = a, t_1 = a + \frac{b - a }{n}, \ldots, t_n - b$, вычисляем $f$ в средней точке $\frac{t_i + t_{i + 1}}{2}$ каждого из интервалов, определяемых этими точками, суммируем результаты и умножаем на ширину каждого интервала $\frac{b -a}{b}$. Для достаточно непрерывных функций $f$ при увеличении $n$ результат будет сходиться к верному значению.


Вероятностный метод, или метод Монте-Карло для вычисления, или, если точнее, приблизительной оценки интеграла $f$ в интервале $[a, b]$, выглядит так: пусть $X_1, \ldots, X_n$ — случайно выбранные точки в интервале $[a, b]$. Тогда $Y = (b - a) \frac{1}{n}\sum_{i = 1}^{n}f(X_i)$ — это случайное значение, среднее которого является интегралом $\int_{[a,b]}f$. Для реализации метода мы используем генератор случайных чисел, генерирующий $n$ точек в интервале $[a, b]$, вычисляем в каждой $f$, усредняем результаты и умножаем на $b-a$. Это даёт нам приблизительное значение интеграла, как показано на рисунке ниже. $\int_{-1}^{1}\sqrt{1 - x^2} dx$ с 20 сэмплами аппроксимирует верный результат, равный $\frac{\pi}{2}$.

Разумеется, каждый раз, когда мы будем вычислять такое приблизительное значение, то будем получать разный результат. Дисперсия этих значений зависит от формы функции $f$. Если мы генерируем случайные точки $x_i$ неравномерно, то нам необходимо слегка изменить формулу. Но благодаря использованию неравномерного распределения точек мы получаем огромное преимущество: заставив неравномерное распределение отдавать предпочтение точкам $x_i$, где $f(x)$ велика, мы можем значительно снизить дисперсию приблизительных значений. Такое принцип неравномерной дискретизации называется выборкой по значимости.

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

  1. Для каждого $s \in S$ существует $p(s) \geq 0$.
  2. $\sum_{s \in S}p(s) = 1$

Первое свойство называется «неотрицательностью». Второе называется «нормальностью». Интуитивно понятно, что $S$ представляет собой множество результатов некоторого эксперимента, а $p(s)$ — это результат вероятности $s$, член $S$. Исход — это подмножество пространства вероятностей. Вероятность исхода является суммой PMF элементов этого исхода, поскольку

$ Pr\{E\} = \sum_{s \in S} p(s) $

Случайная переменная — это функция, обычно обозначаемая заглавной буквой, ставящая в соответствие пространству вероятностей вещественные числа:

$ X: S \rightarrow \boldsymbol{R}. $

Учтите, что функция $X$ — это не переменная, а функция с вещественными значениями. Она также не является случайной, $X(s)$ — это отдельное вещественное число для любого результата $s\in S$.

Случайная переменная используется для определения исходов. Например, множество результата $s$, для которого $X(s) = 1$, то есть если ht и th — это множество строк, обозначающих «орлы» или «решки», то

$ E = {s \in S : X(s) = 1} $

и

$ = {ht, th} $

это исход с вероятностью $\frac{1}{2}$. Запишем это как $Pr\{X=1\} = \frac{1}{2}$. Мы используем предикат $X=1$ как укороченную запись для исхода, определяемого предикатом.

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

headcount = 0 if (randb()): // first coin flip     headcount++ if (randb()): // second coin flip     headcount++ return headcount

Здесь мы обозначаем как ranb() булеву функцию, которая возвращает true в половине случаев. Как она связана с нашей абстракцией? Представьте множество $S$ всех возможных выполнений программы, объявив два выполнения одинаковыми значениями, возвращаемыми ranb, попарно идентичными. Это значит, что существует четыре возможных выполнений программы, в которых два вызова ranb() возвращают TT, TF, FT и FF. По своему опыту мы можем сказать, что эти четыре выполнения равновероятны, то есть каждое встречается примерно в четверти случаев.

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

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

$ E[X] = \sum_{s\in S} p(s)X(s) $

Представьте, что h — это «орлы», а t — «решки». Мы уже рассмотрели ht и th. Также существуют hh и tt. Поэтому ожидаемое значение будет следующим:

$ E[X] = p(hh)X(hh) + p(ht)X(ht) + p(th)X(th) + p(tt)X(tt) $

$ = \frac{1}{4}. 2 +\frac{1}{4} . 1 + \frac{1}{4} . 1 + \frac{1}{4} .0 $

$ = 1 \text{QED} $

Вы можете задаться вопросом, откуда взялся $X$. Здесь я имел в виду, что мы должны назначать значение $X$ самостоятельно. В данном случае мы присвоили h значение 1, а t значение 0. $X(hh)$ равно 2, потому что в ней содержится 2 $h$.

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

Когда мы говорим, что случайная переменная $X$ имеет распределение $f$, то должны обозначить $X \sim f$.

Рассеяние значений, скопившихся вокруг $X$, называется её дисперсией и определяется следующим образом:

$ \boldsymbol{Var}[X] = E[(X - \bar{X})^2] $

Где $\bar{X}$ — это среднее $X$.

$\sqrt{\boldsymbol{Var}}$ называется стандартным отклонением. Случайные переменные $X$ и $Y$ называются независимыми, если:

$ Pr\{X = x \text{ and } Y = y\} = Pr\{X = x\}.Pr\{Y = y\} $

Важные свойства независимых случайных переменных:

  1. $ E[XY] = E[X]E[Y] $
  2. $ \boldsymbol{Var}[X + Y] = \boldsymbol{Var}[X] + \boldsymbol{Var}[Y] $

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

  1. Значения непрерывны. То есть числа бесконечны.
  2. Некоторые аспекты анализа требуют таких математических тонкостей, как измеряемость.
  3. Наше пространство вероятностей будет бесконечным. Вместо PMF мы должны использовать функцию плотности вероятностей (PDF).

Свойства PDF:

  1. Для каждого $s \in S$ у нас есть $p(s) \geq 0$
  2. $\int_{s\in S}p(s) = 1$

Но если распределение $S$ равномерно, то PDF определяется так:

image

При непрерывной вероятности $E[X]$ определяется следующим образом:

$ E[X] := \int_{s\in S} p(s)X(s) $

Теперь сравним определения PMF и PDF:

$ \mathbb{PMF} \rightarrow p_y(t) = Pr\{Y = t\} \text{ for } t \in T $

$ \mathbb{PDF} \rightarrow Pr\{a\leq X \leq b\} = \int_a^bp(r)dr $

В случае непрерывной вероятности случайные величины лучше называть случайными точками. Потому что если $S$ — пространство вероятностей, а $Y : S \rightarrow T$ отображается в другое пространство, отличающееся от $\mathbb{R}$, тогда мы должны назвать $Y$ случайной точкой, а не случайной величиной. Понятие плотности вероятностей применимо здесь, потому что можно сказать, что для любого $U \subset T$ мы имеем:

image

Теперь давайте применим то, что мы узнали, к сфере. Сфера имеет три координаты: широту, долготу и дополнение широты. Долготу и дополнение широты мы используем только в $\mathbb{R}^2$, двухмерные декартовы координаты, применённые к случайной величине $S$, превращают её в $S^2$. Получаем следующую детализацию:

$ Y : [0, 1] \times [0, 1] \rightarrow S^2 : (u, v) \rightarrow (\cos(2\pi u)\sin(\pi v), \cos(\pi v) \sin( 2\pi u) sin(\pi v)) $

Мы начинаем с равномерной плотности вероятностей $p$ при $[0, 1] \times [0, 1]$, или $p(u, v) = 1$. Посмотрите выше формулу плотности равномерной вероятности. Для удобства мы запишем $(x, y, z) = Y(u, v)$.

У нас есть интуитивное понимание, что если выбирать точки равномерно и случайно в единичном квадрате и использовать $f$ для преобразования их в точки на единичной сфере, то они будут скапливаться рядом с полюсом. Это означает, что полученная плотность вероятностей в $T$ не будет равномерной. Это показано на рисунке ниже.

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

$ L^{ref}(P, \omega_o) = \int_{\omega_i \in S_{+}^{2}}L(P, - \omega_i)f_s(P,\omega_i,\omega_0)\omega_i . \boldsymbol{n}d\omega_i, $

для различных значений $P$ и $\omega_0$. Значение $\omega$ — это направление падающего света. Код, генерирующий случайное число, равномерно распределённое в интервале $[0, 1]$ и берущий квадратный корень, создаёт значение в интервале от 0 до 1. Если мы используем для него PDF, поскольку это равномерное значение, то ожидаемое значение будет равно $\frac{2}{3}$. Также это значение является средним значением $f(x) = \sqrt{x}$ в этом интервале. Что это означает?

Рассмотрим теорему 3.48 из книги «Computer Graphics: Principles and Practice». Она гласит, что если $f : [a, b] \rightarrow \mathbb{R}$ является функцией с вещественными значениями, а $X \sim \boldsymbol{U}(a, b)$ является равномерной случайной величиной в интервале $[a, b]$, то $(b-a)f(x)$ — это случайная величина, ожидаемое значение которой имеет вид:

$ E[(b-a)f(x)] = \int_a^b f(x)dx . $

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

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

Мы уже обсудили дискретные и непрерывные вероятности. Но существует и третий тип, который называется смешанными вероятностями и используется в рендеринге. Такие вероятности возникают вследствие импульсов в функциях распределения двунаправленного рассеяния, или импульсов, вызванных точечными источниками освещения. Такие вероятности определены в непрерывном множестве, например, в интервале $[0, 1]$, но не определены строго функцией PDF. Рассмотрим такую программу:

if uniform(0, 1) > 0.6 :     return 0.3 else :     return uniform(0, 1)

В шестидесяти процентах случаев программа будет возвращать 0.3, а в оставшихся 40% она будет возвращать значение, равномерно распределённое в $[0, 1]$. Возвращаемое значение — это случайная переменная, имеющая при 0.3 массу вероятности 0.6, а его PDF во всех других точках задаётся как $d(x) = 0.4$. Мы должны определить PDF как:

image

В целом, случайная переменная со смешанной вероятностью — это такая переменная, для которой существует конечное множество точек в области определения PDF, и наоборот, равномерно распределённые точки, где PMF не определена.


ссылка на оригинал статьи https://habr.com/ru/post/461805/

Разработка надёжных Python-скриптов

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

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

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

Автор материала, перевод которого мы сегодня публикуем, собирается продемонстрировать подобное «превращение» на примере классической задачи «Fizz Buzz Test». Эта задача заключается в том, чтобы вывести список чисел от 1 до 100, заменив некоторые из них особыми строками. Так, если число кратно 3 — вместо него нужно вывести строку Fizz, если число кратно 5 — строку Buzz, а если соблюдаются оба этих условия — FizzBuzz.

Исходный код

Вот исходный код Python-скрипта, который позволяет решить задачу:

import sys for n in range(int(sys.argv[1]), int(sys.argv[2])):     if n % 3 == 0 and n % 5 == 0:         print("fizzbuzz")     elif n % 3 == 0:         print("fizz")     elif n % 5 == 0:         print("buzz")     else:         print(n)

Поговорим о том, как его улучшить.

Документация

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

#!/usr/bin/env python3  """Simple fizzbuzz generator.  This script prints out a sequence of numbers from a provided range with the following restrictions:   - if the number is divisible by 3, then print out "fizz",  - if the number is divisible by 5, then print out "buzz",  - if the number is divisible by 3 and 5, then print out "fizzbuzz". """

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

Аргументы командной строки

Следующей задачей по улучшению скрипта станет замена значений, жёстко заданных в коде, на документированные значения, передаваемые скрипту через аргументы командной строки. Реализовать это можно с использованием модуля argparse. В нашем примере мы предлагаем пользователю указать диапазон чисел и указать значения для «fizz» и «buzz», используемые при проверке чисел из указанного диапазона.

import argparse import sys   class CustomFormatter(argparse.RawDescriptionHelpFormatter,                       argparse.ArgumentDefaultsHelpFormatter):     pass   def parse_args(args=sys.argv[1:]):     """Parse arguments."""     parser = argparse.ArgumentParser(         description=sys.modules[__name__].__doc__,         formatter_class=CustomFormatter)      g = parser.add_argument_group("fizzbuzz settings")     g.add_argument("--fizz", metavar="N",                    default=3,                    type=int,                    help="Modulo value for fizz")     g.add_argument("--buzz", metavar="N",                    default=5,                    type=int,                    help="Modulo value for buzz")      parser.add_argument("start", type=int, help="Start value")     parser.add_argument("end", type=int, help="End value")      return parser.parse_args(args)   options = parse_args() for n in range(options.start, options.end + 1):     # ...

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

$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end  Simple fizzbuzz generator.  This script prints out a sequence of numbers from a provided range with the following restrictions:   - if the number is divisible by 3, then print out "fizz",  - if the number is divisible by 5, then print out "buzz",  - if the number is divisible by 3 and 5, then print out "fizzbuzz".  positional arguments:   start         Start value   end           End value  optional arguments:   -h, --help    show this help message and exit  fizzbuzz settings:   --fizz N      Modulo value for fizz (default: 3)   --buzz N      Modulo value for buzz (default: 5)

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

Логирование

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

import logging import logging.handlers import os import sys  logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])

Затем сделаем так, чтобы подробностью сведений, выводимых при логировании, можно было бы управлять. Так, команда logger.debug() должна выводить что-то только в том случае, если скрипт запускают с ключом --debug. Если же скрипт запускают с ключом --silent — скрипт не должен выводить ничего кроме сообщений об исключениях. Для реализации этих возможностей добавим в parse_args() следующий код:

# В parse_args() g = parser.add_mutually_exclusive_group() g.add_argument("--debug", "-d", action="store_true",                default=False,                help="enable debugging") g.add_argument("--silent", "-s", action="store_true",                default=False,                help="don't log to console")

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

def setup_logging(options):     """Configure logging."""     root = logging.getLogger("")     root.setLevel(logging.WARNING)     logger.setLevel(options.debug and logging.DEBUG or logging.INFO)     if not options.silent:         ch = logging.StreamHandler()         ch.setFormatter(logging.Formatter(             "%(levelname)s[%(name)s] %(message)s"))         root.addHandler(ch)

Основной код скрипта при этом изменится так:

if __name__ == "__main__":     options = parse_args()     setup_logging(options)      try:         logger.debug("compute fizzbuzz from {} to {}".format(options.start,                                                              options.end))         for n in range(options.start, options.end + 1):             # ..     except Exception as e:         logger.exception("%s", e)         sys.exit(1)     sys.exit(0)

Если скрипт планируется запускать без прямого участия пользователя, например, с помощью crontab, можно сделать так, чтобы его вывод поступал бы в syslog:

def setup_logging(options):     """Configure logging."""     root = logging.getLogger("")     root.setLevel(logging.WARNING)     logger.setLevel(options.debug and logging.DEBUG or logging.INFO)     if not options.silent:         if not sys.stderr.isatty():             facility = logging.handlers.SysLogHandler.LOG_DAEMON             sh = logging.handlers.SysLogHandler(address='/dev/log',                                                 facility=facility)             sh.setFormatter(logging.Formatter(                 "{0}[{1}]: %(message)s".format(                     logger.name,                     os.getpid())))             root.addHandler(sh)         else:             ch = logging.StreamHandler()             ch.setFormatter(logging.Formatter(                 "%(levelname)s[%(name)s] %(message)s"))             root.addHandler(ch)

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

$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz

Тесты

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

def fizzbuzz(n, fizz, buzz):     """Compute fizzbuzz nth item given modulo values for fizz and buzz.      >>> fizzbuzz(5, fizz=3, buzz=5)     'buzz'     >>> fizzbuzz(3, fizz=3, buzz=5)     'fizz'     >>> fizzbuzz(15, fizz=3, buzz=5)     'fizzbuzz'     >>> fizzbuzz(4, fizz=3, buzz=5)     4     >>> fizzbuzz(4, fizz=4, buzz=6)     'fizz'      """     if n % fizz == 0 and n % buzz == 0:         return "fizzbuzz"     if n % fizz == 0:         return "fizz"     if n % buzz == 0:         return "buzz"     return n

Проверить правильность работы функции можно с помощью pytest:

$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item  fizzbuzz.py::fizzbuzz.fizzbuzz PASSED                                  [100%]  ========================== 1 passed in 0.05 seconds ==========================

Для того чтобы всё это заработало, нужно, чтобы после имени скрипта шло бы расширение .py. Мне не нравится добавлять расширения к именам скриптов: язык — это лишь техническая деталь, которую не нужно демонстрировать пользователю. Однако возникает такое ощущение, что оснащение имени скрипта расширением — это самый простой способ позволить системам для запуска тестов, вроде pytest, находить тесты, включённые в код.

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

$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item  fizzbuzz.py::fizzbuzz.fizzbuzz FAILED                                  [100%]  ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101     >>> fizzbuzz(5, fizz=3, buzz=5) 102     'buzz' 103     >>> fizzbuzz(3, fizz=3, buzz=5) 104     'fizz' 105     >>> fizzbuzz(15, fizz=3, buzz=5) 106     'fizzbuzz' 107     >>> fizzbuzz(4, fizz=3, buzz=5) 108     4 109     >>> fizzbuzz(4, fizz=4, buzz=6) Expected:     fizz Got:     4  /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================

Модульные тесты можно писать и в виде обычного кода. Представим, что нам нужно протестировать следующую функцию:

def main(options):     """Compute a fizzbuzz set of strings and return them as an array."""     logger.debug("compute fizzbuzz from {} to {}".format(options.start,                                                          options.end))     return [str(fizzbuzz(i, options.fizz, options.buzz))             for i in range(options.start, options.end+1)]

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

# Модульные тесты import pytest                   # noqa: E402 import shlex                    # noqa: E402   @pytest.mark.parametrize("args, expected", [     ("0 0", ["fizzbuzz"]),     ("3 5", ["fizz", "4", "buzz"]),     ("9 12", ["fizz", "buzz", "11", "fizz"]),     ("14 17", ["14", "fizzbuzz", "16", "17"]),     ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]),     ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected):     options = parse_args(shlex.split(args))     options.debug = True     options.silent = True     setup_logging(options)     assert main(options) == expected

Обратите внимание на то, что, так как код скрипта завершается вызовом sys.exit(), при его обычном вызове тесты выполняться не будут. Благодаря этому pytest для запуска скрипта не нужен.

Тестовая функция будет вызвана по одному разу для каждой группы параметров. Сущность args используется в качестве входных данных для функции parse_args(). Благодаря этому механизму мы получаем то, что нужно передать функции main(). Сущность expected сравнивается с тем, что выдаёт main(). Вот что сообщит нам pytest в том случае, если всё работает так, как ожидается:

$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items  fizzbuzz.py::fizzbuzz.fizzbuzz PASSED                                  [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED                           [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED                           [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED                          [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED                         [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED                [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED               [100%]  ========================== 7 passed in 0.03 seconds ==========================

Если произойдёт ошибка — pytest даст полезные сведения о том, что случилось:

$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [...] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________  args = '0 0', expected = ['0']      @pytest.mark.parametrize("args, expected", [         ("0 0", ["0"]),         ("3 5", ["fizz", "4", "buzz"]),         ("9 12", ["fizz", "buzz", "11", "fizz"]),         ("14 17", ["14", "fizzbuzz", "16", "17"]),         ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]),         ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]),     ])     def test_main(args, expected):         options = parse_args(shlex.split(args))         options.debug = True         options.silent = True         setup_logging(options)        assert main(options) == expected E       AssertionError: assert ['fizzbuzz'] == ['0'] E         At index 0 diff: 'fizzbuzz' != '0' E         Full diff: E         - ['fizzbuzz'] E         + ['0']  fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py                125 DEBUG    compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================

В эти выходные данные включён и вывод команды logger.debug(). Это — ещё одна веская причина для использования в скриптах механизмов логирования. Если вы хотите узнать подробности о замечательных возможностях pytest — взгляните на этот материал.

Итоги

Сделать Python-скрипты надёжнее можно, выполнив следующие четыре шага:

  • Оснастить скрипт документацией, размещаемой в верхней части файла.
  • Использовать модуль argparse для документирования параметров, с которыми можно вызывать скрипт.
  • Использовать модуль logging для вывода сведений о процессе работы скрипта.
  • Написать модульные тесты.

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

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

Уважаемые читатели! Планируете ли вы применять рекомендации по написанию Python-скриптов, данные в этой публикации?


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/462007/

Красота в глазах смотрящего

Я давно уже занимаюсь разработкой web-приложений. Очень давно. Свои первые web-приложения в среде Lotus Domino я создавал в те времена, когда слово «google» ещё не было глаголом, а для поиска информации в интернете люди использовали Yahoo! и Rambler. Я же пользовался Infoseek‘ом — у них был сужающийся поиск и не такой безобразно перегруженный интерфейс, как у Yahoo!

Разработка приложений, любых приложений, не только для web’а — работа творческая. Вряд ли кто-то будет спорить с этим утверждением. А красота в творчестве, это как практика в научном познании — критерий истины. Но если научная практика объективна и базируется на измерениях, то красота — предмет субъективный, зависит от того, кто смотрит. Вот я и задался вопросом, а что для меня лично является красивым web-приложением?

(на КДПВ глаз не мой, глаз женский, но, IMHO, женский глаз на КДПВ более уместен, чем мужской, ведь это же — КДПВ!)

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

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

Среда Обитания

Протоколы

Не знаю даже, стоит ли отдельно выносить этот критерий. Web-приложения живут в Сети и вынуждены соответствовать Законам Сети (протоколам). Главные протоколы в Сети — TCP и IP. На них основывается множество других протоколов, но для web-приложений я считаю самым важным HTTP (вернее, его расширение HTTPS на базе TLS). Т.е., красивое web-приложение доступно по HTTPS/TLS (как вариант — по HTTP), а остальные протоколы (LDAP, RPC, IMAP4, POP3, SMTP, FTP, NNTP, …) делают его менее красивым с каждым дополнительно поддерживаемым протоколом. Само же приложение может при помощи этих дополнительных протоколов использовать внешние ресурсы.

Что касается WebSocket, то у меня нет достаточного опыта использования этого протокола с web-приложениями. Выглядит красиво и перспективно, но насколько это стабильно и практично — сказать не могу.

Браузеры

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

ЯП

С языками программирования, которые используются для создания web-приложений, всё очень запутано. Для клиентской части web-приложений есть масса технологий, позволяющих разработчику облегчить создание триады HTML/CSS/JS (то, что понимают все современные браузеры). Но я в своё время плотно соприкоснулся с GWT и считаю красивым, когда разработчик видит в браузере оригинальный код, а не результат компиляции или транспиляции. Поэтому использование webpack‘а и ему подобных продуктов для генерации клиентского кода, IMHO, некрасиво. Чем больше исполняемый в браузере код похож на исходный код, созданный разработчиком, тем лучше. Не верите? Попробуйте отдебажить в production’е код, созданный GWT.

На серверной стороне свободы больше (Java, PHP, perl, python, C#, Ruby, …), но мне кажется, что красиво, когда и на серверной стороне, и в браузере используется один язык программирования — JavaScript. Всё-таки язык определяет мышление, а команды единомышленников более продуктивны.

Человечность

Красивое web-приложение должно быть полезным. Полезным, в первую очередь, для человека, как конечного потребителя. Поэтому я не могу назвать красивым web-приложением web-сервисы. Обычному человеку (не web-девелоперу) с ними сложно. Web-сервисы красивы по-своему,

Красивое web-приложение должно иметь интуитивно понятный интерфейс. Можно спорить о UI‘е — это довольно субъективная вещь. Но с UX всё гораздо проще, если пользователь не может использовать приложение без заветного RTFM — плохой UX, некрасивое web-приложение. Самые красивые относительно этого критерия web-приложения могут с лёгкостью использоваться детьми, которые ещё не умеют читать.

Обратная Масштабируемость

Давным-давно программы можно было переносить на дискетах, сейчас — на флешках или сразу скачать из Сети. Скопировать обычное приложение и запустить его на другой машине — задача тривиальная. С web-приложениями ситуация несколько особая. Сеть представляет собой глобальную среду, в которой нет нужды иметь клоны одного и того же web-приложения. В Сети хватает одного Facebook’а, Twitter’а, Instagram’а, Mail.ru или Yandex’а. Можно иметь различные web-приложения в одной и той же тематической нише, но с разными аудиториями (как Facebook и Вконтакте, Mail.ru и Gmail, Google Maps и Azure Maps). Аппаратные ресурсы для обеспечения глобальной доступности таких web-приложений нужны, скажем так, нетривиальные.

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

Красивое web-приложение масштабируется не только вверх и вширь (для пользователей), но и вниз и вовнутрь (для разработчиков).

«Земноводность»

Для доступа к современным web-приложениям используются устройства двух типов:

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

Где-то на горизонте маячит ещё «интернет вещей«, но пока так.

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

Я считаю некрасивым «земноводность«, это как попытка усидеть на двух (с SEO — трёх) стульях. Лучше уж как Фиона из Шрека — днём одна, а ночью другая. Да, дороже. Но лучше.

Cross-sharing

Я уже отмечал в пункте «Обратная Масштабируемость», что глобальность Сети даёт возможность иметь одно web-приложение на Планету. Поэтому каждое web-приложение должно хоть чем-то отличаться от других, чтобы обеспечить себе выживание. Тем не менее, мой многолетний опыт с Magento (каркас для построения магазинов e-commerce) говорит, что между отдельными web-приложениями общего может быть больше, чем различий. Красивое web-приложение не только должно быть модульным, оно также должно разделять свои модули с другими web-приложениями. В какой-то мере эта идея отражена в спецификациях JSR 168 и JSR 286 и таких framework’ах, как WordPress, Django и та же Magento. Чем большее количество модулей web-приложения используется другими web-приложениями, тем оно красивее с моей точки зрения. Cross-sharing позволяет создавать более качественные модули и, как следствие, более стабильные web-приложения.

Под модулем я не подразумеваю библиотеки типа jQuery или RequireJS — скорее более крупные образования, типа плагинов в WordPress и Django. Но для библиотек также справедлив тезис, что широкое распространение библиотеки позволяет сделать её более качественной и устойчивой.

Гарвардская архитектура

Гарвардская архитектура, в отличие от ныне правящей бал принстонской, подразумевает разделение кода и данных. Архитектура не взлетела, но сама идея лично мне кажется красивой. Особенно для web-приложений. Любая статика (HTML/CSS/JS/Images/…) — это код. Его можно и нужно кэшировать хоть на серверной стороне, хоть на клиентской. А данные — это REST/JSON (красиво) или SOAP/XML (чуть менее красиво). Или WebSockets/JSON (может быть наилучшим вариантом, но я не пробовал).

Локализация

Есть две вещи, которые меня особенно волнуют при разработке web-приложений — это мультиязычный интерфейс и часовые пояса. Я сам из Латвии, у нас в ходу три языка: LV, RU, EN. Красивое web-приложение должно давать возможность не только использовать несколько языков в самом приложении, но и позволять расширять количество используемых языков при помощи внешних ресурсов, типа Crowdin. Это же справедливо и для модулей, из которых собирается web-приложение.

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

Кузницы вместо «Звезд Смерти»

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

А теперь посмотрите на такой сервис, как DNS. Когда ложатся корневые сервера, лихорадит весь мир.

На мой взгляд, красивое web-приложение не может быть такого размера, как Facebook или Mail.ru. Это уже ближе к «Звезде Смерти» и по ресурсам, необходимым для постройки, и по ресурсам, необходимым для поддержания работоспособности. Да, в случае уничтожения Facebook’а человечество не исчезнет, его функции достаточно быстро переймут другие приложения (тот же ВК на территории РФ и прилегающих, Instagram, Twitter, …). Тем не менее, замыкание значимой части населения Планеты на одно приложение — это некрасиво. Тем более, при наличии гораздо более устойчивых альтернатив (например, torrents).

Резюме

Если вы дочитали до конца и испытываете недоумение — «что это было?«, то выражаю вам своё искреннее сочувствие. Я не заставлял вас читать это. Я просто попытался облечь свои мысли в слова, чтобы найти тех, кто думает так же. Возможно, я смогу обсудить с ними некоторые аспекты создания красивых web-приложений и узнать ответы на свои вопросы. А их у меня много.

Спасибо за прочтение.


ссылка на оригинал статьи https://habr.com/ru/post/461749/

SVG в реальной жизни. Доклад Яндекса

Привет, меня зовут Артём, я руководитель одной из групп разработки интерфейсов в Яндексе. Неделю назад на Я.Субботнике я рассказал, как мы использовали SVG для создания внутреннего календаря. Это расшифровка моего доклада, несколько историй из реализации виджета календаря: масштабирование, заливка паттерном, маски, символы и особенности формата.

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

Начали мы, конечно, с макета. Он выглядел так:

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

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

Был прикольный прототип, когда мы генерировали всю эту картинку линейными градиентами, но она при масштабировании и при переходе на ретину съезжала. Поэтому в итоге мы пришли к SVG. Почему? Во-первых, там полностью независимая от документа система координат, поэтому можно внутри расположить всё абсолютно, и это никак не сломается независимо ни от чего. Также там есть нормальная работа с масштабированием. Даже если в браузере сделать зум, если открыть на ретине или как угодно растягивать календарик, он будет ресайзиться как картинка и в любом случае выглядеть нормально. У нас на макете была заливка клеточками, и очень хорошо, что в SVG есть заливка паттернами.

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

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

Начнем с основы. SVG — гигантская координатная плоскость, на которой можно произвольно размещать векторную графику. При этом часть области, которую мы видим, определяет viewBox, а что за ее границами — это такой overflow hidden на стероидах. Что бы там ни было, его не будет видно. Мы решили, что для простоты расчетов сделаем в календаре один пиксель, равный одной минуте. Поэтому один час будет занимать ровно 60 пикселей. Чтобы было еще проще, мы решили, что день по ширине тоже будет 60 пикселей — чтобы все было квадратным, как в армии. И начали верстать.

Viewbox задается четырьмя параметрами. Первые два — верхняя левая точка в системе координат, от которой считается viewBox, для нас это 0,0. При этом ширина — это 60 * на количество дней, а высота — 60 * на количество часов.

Внутрь SVG валидно вставлять другие документы SVG, в которых внутри будет своя система координат. И чтобы события в дне можно было позиционировать только по вертикальной оси, мы решили, что на каждый день заведем отдельный SVG, и их просто сместим по горизонтали на 60 * на позицию дня в календаре. Тогда все события можно будет просто по вертикали по Y ставить, будет очень удобно. А внутрь каждой SVG, которая представляет собой день, мы положили прямоугольник, который будет отображать заливку дня.

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

Заготовка есть. Теперь надо добавить сетку. Так как мы хотели ресайзить календарь, а линии сетки должны быть всегда однопиксельными, мы использовали атрибут vector-effect=non-scaling-stroke. Это приводит к тому, что как бы мы ни ресайзили, ни зумили, всегда будет однопиксельная сетка. Достаточно просто горизонтальных и вертикальных линий нужное количество добавить, и будет такая сетка.

С основой разобрались, перейдем к событиям на весь день. Это такая хитрая штука. Вы замечали, что в календарях есть события и есть галочка «на весь день». Эти события отличаются тем, что они идут весь день, независимо от того, в каком часовом поясе вы на них смотрите. Поэтому если где-нибудь в самом начале часовых поясов на Аляске событие начинается рано утром, то где-то через 48 часов в противоположном конце земного шара оно все еще будет идти. Звучит сложно, но для реализации это проще всего: просто сравниваешь дату с датой отображаемого дня. Если попадает — значит событие в этот день. Если два события на весь день попадают на день, то показывается то, которое позже началось. Так заливкой отображаются события на весь день.

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

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

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

Начнем по порядку. В SVG есть тег <defs>, он позволяет объявлять внутри него элементы, которые не отображаются, но их можно по ссылке использовать, ссылаясь на них. Первое, что мы сделаем — объявим <defs>, и в нем заведем паттерн. <pattern> — это тег, который позволяет объявить паттерн, который можно использовать для заливки того или иного элемента тем или иным узором.

Нам надо сделать в этом паттерне клеточки. У нас 60 на 60 пикселей, клеточки должны быть 6 на 6, поэтому мы объявили паттерн 12 на 12, и внутри него нарисуем <path>, как на схеме слева. У него есть атрибут d, который обозначает, как именно двигается линия. Он начинается из точки 0,0, и потом по координатам стрелками показано, как именно он рисуется. Если мы зальем его белым, получится такой узор: что не залито белым, залито черным.

Переходим к следующему шагу, теперь объявим маску. <mask> — это такой элемент в SVG, который позволяет добавлять другим элементам альфа-канал. То, что в маске нарисовано черным, в том элементе, к которому маска применена, невидимо, прозрачно. То, что нарисовано белым, непрозрачно. То, что серым, то полупрозрачно. У нас черно-белый паттерн, и мы внутрь маски добавим прямоугольник, и его этим паттерном зальем. Теперь у нас есть маска.

Следующий шаг — <symbol>. Это такой тег в SVG, который позволяет объявлять переиспользуемую графику. Чаще всего символы используются, например, для иконок. И здесь мы объявим символ, внутрь которого положим два прямоугольника. Один ничем не зальем, чтобы он наследовал свойство fill от родительского SVG, а другой зальем currentСolor и применим к нему маску. Теперь у нас будет два прямоугольника: один с дырками и залит currentColor, а другой без дырок и залит fill. И они друг на друге лежат. Если мы зададим эти цвета одинаковыми, у нас будет сплошная заливка. А если разными — клеточки. К этому всё и шло. Теперь можно просто использовать CSS и через классы задавать произвольную заливку двух цветов для всех событий.

Теперь надо определить, какие именно события должны попасть в календарь в тот или иной день. У нас есть часовой пояс +3, в котором мы все сидим, в нем есть шкала от 9 до 20 часов. Также есть человек, который сидит в условном Оренбурге, у него часовой пояс +5, его шкала смещена относительно нас на два часа. Мы сделаем проекцию на UTC, и видим, что по UTC этот промежуток от верха до низа надо отобразить в дне, чтобы пользователь мог, переключаясь между часовыми поясами, видеть и события, которые попадают в его календарь, и календарь того, на кого он смотрит.

Запомним эти числа, которые лежат в offset, потому что проще всего события, которые приезжают в UTC, позиционировать в этом же самом UTC. Для этого мы возьмем тег <g>, который обозначает в SVG группу, и все события там спозиционируем абсолютно по UTC, а сам <g> будем смещать на нужное нам количество пикселей, чтобы отображался тот или иной часовой пояс.

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

Текущее время тоже делается просто, это будет линия с тем же эффектом non-scaling-stroke, чтобы она всегда была однопиксельной. Вот как она отображается.

Время на месте не стоит, и надо, чтобы стрелочка двигалась. Самый прикольный способ, который мы придумали, — анимация. Мы решили, что сделаем анимацию, которая будет смещать стрелочку на количество минут в сутках, и делать это за сутки. А чтобы она не постоянно медленно двигалась, а именно тикала раз в минуту, мы использовали steps(). И как только мы это добавили, время стало двигаться. При этом на самом деле, так как анимация не гарантирует, что будет постоянно двигаться, она то отстает, то еще что-то. Но у нас события в календаре время от времени обновляются, и где-то раз в две-три минуты или когда пользователь ушел со вкладки и вернулся, весь календарь перерисовывается и время обновляется. Поэтому анимация видна, только когда ты сидишь и пристально смотришь, тикает она или нет.

Есть одна проблема. Здесь я сделал календарик пошире, чтобы он больше был похож на тот, что в продакшене. Стало видно, что клеточки уже не квадратные. Это потому что пропорции не сохраняются, и если мы растягиваем или изменяем соотношения сторон физически, то изменяется оно как в картинке. Чтобы этого избежать, надо написать немного JS. Есть соотношение сторон viewBox, которое было в нашем изначальном SVG, и фактическое соотношение сторон, которое используется у нас в верстке. Если найти отношение этих соотношений и потом его засунуть в трасформ паттерна, то клеточки станут квадратными. А еще этот коэффициент, который мы тут получили, можно использовать, если мы хотим понять, куда кликнул пользователь. Так как у нас одна минута в исходном SVG равна одному пикселю, то по координатам клика, умноженного на этот коэффициент, можно понять, в какое время попал пользователь.

Осталось добавить HTML, чтобы были буквы и цифры сверху. Получится календарик.

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

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

Пользуйтесь CSS, пользуйтесь SVG. Спасибо!


ссылка на оригинал статьи https://habr.com/ru/company/yandex/blog/461571/

Тюнинг производительности запросов в PostgreSQL

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

Поиск медленных запросов

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

pg_stats_statements

Модуль pg_stats_statements — отличное место для начала. Он просто отслеживает статистику выполнения операторов SQL и может быть простым способом поиска неэффективных запросов.

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

SELECT * FROM   pg_stat_statements ORDER BY   total_time DESC;

user_id dbid queryid query calls total_time
16384 16385 2948 SELECT address_1 FROM addresses a INNER JOIN people p ON a.person_id = p.id WHERE a.state = @state_abbrev; 39483 15224.670
16384 16385 924 SELECT person_id FROM people WHERE name = name; 26483 12225.670
16384 16385 395 SELECT _ FROM orders WHERE EXISTS (select _ from products where is_featured = true) 18583 224.67

auto_explain

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

Опция log_min_duration контролирует, какие планы выполнения запросов регистрируются, основываясь на том, как долго они выполняются. Например, если вы установите значение 1000, все записи, которые выполняются дольше 1 секунды, будут зарегистрированы.

Тюнинг индексов

Другой важной стратегией настройки является обеспечение правильного использования индексов. В качестве предварительного условия нам нужно включить Cборщик Cтатистики (Statistics Collector).

Postgres Statistics Collector — это подсистема первого класса, которая собирает все виды полезной статистики производительности.

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

Отсутствующие индексы

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

SELECT   relname,   seq_scan - idx_scan AS too_much_seq,   CASE     WHEN       seq_scan - coalesce(idx_scan, 0) > 0     THEN       'Missing Index?'     ELSE       'OK'   END,   pg_relation_size(relname::regclass) AS rel_size, seq_scan, idx_scan FROM   pg_stat_all_tables WHERE   schemaname = 'public'   AND pg_relation_size(relname::regclass) > 80000 ORDER BY   too_much_seq DESC;

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

Неиспользуемые индексы

Индексируйте все сущности, правильно? Знаете ли вы, что неиспользуемые индексы могут негативно повлиять на производительность записи? Причина в том, что при создании индекса Postgres обременен задачей обновления этого индекса после операций записи (INSERT / UPDATE / DELETE). Таким образом, добавление индекса является уравновешивающим действием, поскольку оно может ускорить чтение данных (если оно создано правильно), но замедлит операции записи. Чтобы найти неиспользуемые индексы, вы можете выполнить следующий запрос.

SELECT   indexrelid::regclass as index,   relid::regclass as table,   'DROP INDEX ' || indexrelid::regclass || ';' as drop_statement FROM   pg_stat_user_indexes   JOIN     pg_index USING (indexrelid) WHERE   idx_scan = 0   AND indisunique is false;

Примечание о статистике сред разработки

Полагаться на статистику, полученную из локальной базы данных разработки, может быть проблематично. В идеале вы можете получить приведенную выше статистику с вашей рабочей машины или сгенерировать ее из восстановленной рабочей резервной копии. Зачем? Факторы окружения могут изменить работу оптимизатора запросов Postgres. Два примера:

  • когда у машины меньше памяти, PostgreSQL может быть не в состоянии выполнить Hash Join, в противном случае он сможет и сделает это быстрее.
  • если в таблице не так много строк (как в базе данных разработки), PostgresSQL может предпочесть выполнять последовательное сканирование таблицы, а не использовать доступный индекс. Когда размеры таблиц невелики, Seq Scan может быть быстрее. (Примечание: вы можете запустить
    SET enable_seqscan = OFF

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

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

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

EXPLAIN

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

При использовании EXPLAIN для настройки, я рекомендую всегда использовать опцию ANALYZE (EXPLAIN ANALYZE), поскольку она дает вам более точные результаты. Опция ANALYZE фактически выполняет оператор (а не просто оценивает его), а затем объясняет его.

Давайте окунемся и начнем понимать вывод EXPLAIN. Вот пример:

Узлы

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

Стоимость

cost=146.63..148.65

Первое число — это начальные затраты (затраты на получение первой записи), а второе число — это затраты на обработку всего узла (общие затраты от начала до конца).

Фактически, это стоимость, которую, по оценкам PostgreSQL, придется выполнить для выполнения оператора. Это число не означает сколько времени потребуется для выполения запроса, хотя обычно существует прямая зависимость, необходимого для выполнения. Стоимость — это комбинация из 5 рабочих компонентов, используемых для оценки требуемой работы: последовательная выборка, непоследовательная (случайная) выборка, обработка строки, оператор (функция) обработки и запись индекса обработки. Стоимость представляет собой операции ввода-вывода и загрузки процессора, и здесь важно знать, что относительно высокая стоимость означает, что PostgresSQL считает, что ему придется выполнять больше работы. Оптимизатор принимает решение о том, какой план выполнения использовать, исходя из стоимости. Оптимизатор предпочитает более низкие затраты.

Фактическое время

actual time=55.009..55.012

В миллисекундах первое число — это время запуска (время для извлечения первой записи), а второе число — это время, необходимое для обработки всего узла (общее время от начала до конца). Легко понять, верно?

В приведенном выше примере потребовалось 55,009 мс для получения первой записи и 55,012 мс для завершения всего узла.

Узнать больше о планах выполнения

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

Настройка запросов

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

Заметка о кеше данных и издержках

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

Вещи, которые я узнал, могут помочь улучшить планы выполнения:

  • Индексы
    • Исключите последовательное сканирование (Seq Scan), добавив индексы (если размер таблицы не мал)
    • При использовании многоколоночного индекса убедитесь, что вы обращаете внимание на порядок, в котором вы определяете включенные столбцы — Дополнительная информация
    • Попробуйте использовать индексы, которые очень избирательны к часто используемым данным. Это сделает их использование более эффективным.
  • Условие ГДЕ
    • Избегайте LIKE
    • Избегайте вызовов функций в условии WHERE
    • Избегайте больших условий IN()
  • JOINы
    • При объединении таблиц попробуйте использовать простое выражение равенства в предложении ON (т.е. a.id = b.person_id). Это позволяет использовать более эффективные методы объединения (т. Е. Hash Join, а не Nested Loop Join)
    • Преобразуйте подзапросы в операторы JOIN, когда это возможно, поскольку это обычно позволяет оптимизатору понять цель и, возможно, выбрать лучший план.
    • Правильно используйте СОЕДИНЕНИЯ: используете ли вы GROUP BY или DISTINCT только потому, что получаете дублирующиеся результаты? Это обычно указывает на неправильное использование JOIN и может привести к более высоким затратам
    • Если план выполнения использует Hash Join, он может быть очень медленным, если оценки размера таблицы неверны. Поэтому убедитесь, что статистика вашей таблицы точна, пересмотрев стратегию очистки (vacuuming strategy )
    • По возможности избегайте коррелированных подзапросов; они могут значительно увеличить стоимость запроса
    • Используйте EXISTS при проверке существования строк на основе критерия, поскольку он подобен короткому замыканию (останавливает обработку, когда находит хотя бы одно совпадение)
  • Общие рекомендации
    • Делайте больше с меньшими затратами; Процессор быстрее чем операции ввода/вывода (I/O)
    • Используйте Common Table Expressions и временные таблицы, когда вам нужно выполнить цепочечные запросы.
    • Избегайте операторов LOOP и предпочитайте операции SET
    • Избегайте COUNT (*), поскольку PostgresSQL для этого выполняет сканирование таблиц (только для версий <= 9.1)
    • По возможности избегайте ORDER BY, DISTINCT, GROUP BY, UNION, поскольку это приводит к высоким начальным затратам
    • Ищите большую разницу между оценочными и фактическими строками в выражении EXPLAIN. Если счетчик сильно отличается, статистика таблицы может быть устаревшей, а PostgreSQL оценивает стоимость с использованием неточной статистики. Например:
      Limit (cost=282.37..302.01 rows=93 width=22) (actual time=34.35..49.59 rows=2203 loops=1)

      Расчетное количество строк составило 93, а фактическое — 2203. Поэтому, скорее всего, это плохое решение плана. Вы должны пересмотреть свою стратегию очистки (vacuuming strategy) и убедиться, что ANALYZE выполняется достаточно часто.


ссылка на оригинал статьи https://habr.com/ru/post/461997/