Ранее, когда у нас не было своего корпоративного блога, я писал о том, как мы используем Microsoft TFS (Visual Studio Team Servives on Premises) для управления жизненным циклом разработки ПО и для автоматизации тестирования. В частности мы собрали большой набор автотестов по разным системам в один пакет, который запускаем каждый день. Подробнее об этом я рассказывал на конференции DevOpsDaysMoscow ( презентация, видео выступления )В ходе внедрения мы столкнулись с несколькими проблемами:
Последовательный запуск автотестов в один поток занимает слишком много времени
Часть падений автотестов не фиксируются
Сборки из дженкинса не публикуют результаты тестов в VSTS
все эти проблемы были успешно решены с помощью собственных расширений VSTS:
Параллельные Сборки
Автоматические дефекты
Доработка стандартной задачи запуска Jenkins job
Подробнее о проблемах и как мы искали решения — в моей прошлой статье. А сегодня я хочу рассказать о том как делать эти расширения и как мы можем помочь разработчикам расширений.
Как ?
Практически с самого начала мы поняли, что самый простой способ решить наши проблемы — это собственные задачи (tasks) или шаги сборки в TFS. Задачи можно писать либо на powershell, либо на typescript. Мы выбрали typescript, и вот почему — фактически это javascript, но с поддержкой типизации. О преимуществах typescript и его использовании существует множество статей, в том числе и на хабре. Для нас же основными преимуществами было следующее:
IntelliSense and static checks
Работает на linux агентах
возможность использования любых npm модулей. Это означает, что не нужно изобретать велосипеды — нужна генерация UUID — она есть, генерация результатов тестов в формате Visual Studio (trx файлов) — она есть, работа с XML — тоже есть.
Поддержка асинхронности на уровне языка — async/await. Позволяет избавиться от callback hell. Асинхронные методы и вызовы выглядят как синхронные.
Для работы c TFS Microsoft создала и опубликовала 2 npm модуля, что опять же избавило от необходимости изобретать велосипед.
Также большой помощью оказалось то, что многие задачи от Microsoft написаны и typescript и опубликованы под открытой лицензией. Кроме того, что это позволяет использовать их как примеры, это дало возможность сделать собственный форк этого репозитория и сделать свой "бутсрап" набор для быстрого создания задач и автоматизации сборки и упаковки расширений.
Что ?
Что представляет собой задача для VSTS? Это набор обязательных компонентов:
В этом блоке описывается уникальный id задачи, ее имя, категория и версия. При создании задачи необходимо указывать все эти поля. Поле instanceNameFormat определяет как будет выглядеть имя задачи в сборке VSTS по умолчанию. В нем могут быть указаны параметры из блока параметров в виде $(имя параметра)
блок параметров
В блоке параметров указываются входные параметры задачи, их имена, описания и типы. Параметры могут группироваться для удобства представления на странице настройки задачи. Ниже блок параметром задачи AutoDefects:
содержит набор локализованных строк для протоколирования работы задачи в лог файле сборки. Используется реже, чем блоки выше. Сообщения для текущих локальных настроек можно получить вызовом task.loc("messagename");
Главный исполняемый файл
Главный исполняемый файл — это скрипт, который выполняет VSTS при старте задачи. Как минимум должен содержать код для импорта необходимых модулей для работы задачи и обработку ошибок. Например:
import tl = require('vsts-task-lib/task');
import trm = require('vsts-task-lib/toolrunner');
import path = require('path');
import fs = require('fs');
import Q = require("q");
import * as vm from 'vso-node-api';
import * as bi from 'vso-node-api/interfaces/BuildInterfaces';
import * as ci from 'vso-node-api/interfaces/CoreInterfaces';
import * as ti from 'vso-node-api/interfaces/TestInterfaces';
import * as wi from 'vso-node-api/interfaces/WorkItemTrackingInterfaces';
async function run() {
tl.setResourcePath(path.join(__dirname, 'task.json'));
let projId = tl.getVariable("System.TeamProjectId");
try {
} catch(err) {
console.log(err);
console.log(err.stack);
throw err;
}
}
run()
.then(r => tl.setResult(tl.TaskResult.Succeeded,tl.loc("taskSucceeded")))
.catch(r => tl.setResult(tl.TaskResult.Failed,tl.loc("taskFailed")))
Как видно задача представляет собой набор стандартных компонентов, которые мало меняются от задачи к задаче. Поэтому, когда я создавал третью задачу, появилась идея автоматизировать создание задач. Так появился наш "бутстрап", который значительно облегчает жизнь разработику расширений для VSTS.
Как быстрее?
Что нужно сделать когда создаешь задачу для VSTS, кроме собственно написание кода самой задачи? Шаги обычно одни и те же:
Создать скелет задачи
Собрать задачу в изолированный компонент
Упаковать задачу в vsix для публикации в VSTS
Все эти шаги могут быть автоматизированы для ускорения разработки и устранения ненужного ручного труда. Для автоматизации этих шагов и можно применять наш "бутстрап". Схема работы нашего сборщика аналогична сборщику задач от Microsoft — задачи для сборки перечисляются в файле make-options.json в корне проекта:
Консольная утилита по работе с VSTS tfx-cli — npm install -g tfx-cli
Cоздание задачи
Задача TaskName создается командой: gulp generate –-name TaskName
В результае выполениея команды происходит следующее:
Задача добавляется в проектный список задач для сборки
Создание каталога задачи и «скелетных» файлов – taskname.ts, task.json, package.json, typings.json, icon.png
Скелетные файлы содержат минимально необходимый набор данных и кода.
Сборка задач проекта
Сборка задач проекта осуществляется комадной gulp
При этом для всех задач, перечисленных в make-options.json происходит следующее:
Трансляция .ts в .js
Установка node_modules в каталог задачи
Генерация языковых файлов
Упаковка задач
Упаковка задач осуществляется командой gulp mkext [--all] [--exts ext1,ext2]
По умолчанию каждая задача упаковывается в отдельный vsix файл, если указан параметр --all, то все задачи собираются в один большой vsix файл.
По умолчанию упаковываются все задачи, перечисленные в make-options.json, если указан параметр --exts, то упаковываются только перечисленные в параметре расширения.
У нас открыто
Бутстрап опубликован на GitHub — форки, feature requests, pull requests приветствуются.
Очень надеюсь, что эта статья вызовет интерес к Miscosoft VSTS, который на мой взгляд является отличным инструментом групповой работы не только для больших компаний, но и для небольших гибких команд.
Константин Нерадовский,
начальник отдела автоматизации тестирования,
банк "Открытие"
В данной статье приведено решение оптимизации на Transact SQL задачи расчета остатки на складах. Применено: партицирование таблиц и материализованных представлений.
Постановка задачи
Задачу необходимо решить на SQL Server 2014 Enterprise Edition (x64). В фирме есть много складов. В каждом складе ежедневно по нескольку тысяч отгрузок и приемок продуктов. Есть таблица движений товаров на складе приход/расход. Необходимо реализовать:
Расчет баланса на выбранную дату и время (с точностью до часа) по всем/любому складам по каждому продукту. Для аналитики необходимо создать объект (функцию, таблицу, представление) с помощью которого за выбранный диапазон дат вывести по всем складам и продуктам данные исходной таблицы и дополнительную расчетную колонку — остаток на складе позиции.
Указанные расчеты предполагаются выполняться по расписанию с разными диапазонами дат и должны работать в приемлемое время. Т.е. если необходимо вывести таблицу с остатками за последний час или день, то время выполнения должно быть максимально быстрым, равно как и если необходимо вывести за последние 3 года эти же данные, для последующей загрузки в аналитическую базу данных.
Технические подробности. Сама таблица:
create table dbo.Turnover
(
id int identity primary key,
dt datetime not null,
ProductID int not null,
StorehouseID int not null,
Operation smallint not null check (Operation in (-1,1)), -- +1 приход на склад, -1 расход со склада
Quantity numeric(20,2) not null,
Cost money not null
)
Dt — Дата время поступления/списания на/со склада.
ProductID — Продукт
StorehouseID — склад
Operation — 2 значения приход или расход
Quantity — количество продукта на складе. Может быть вещественным если продукт не в штуках, а, например, в килограммах.
Cost — стоимость партии продукта.
Исследование задачи
Создадим заполненную таблицу. Для того что бы ты мог вместе со мной тестировать и смотреть получившиеся результаты, предлагаю создать и заполнить таблицу dbo.Turnover скриптом:
if object_id('dbo.Turnover','U') is not null drop table dbo.Turnover;
go
with times as
(
select 1 id
union all
select id+1
from times
where id < 10*365*24*60 -- 10 лет * 365 дней * 24 часа * 60 минут = столько минут в 10 лет
)
, storehouse as
(
select 1 id
union all
select id+1
from storehouse
where id < 100 -- количество складов
)
select
identity(int,1,1) id,
dateadd(minute, t.id, convert(datetime,'20060101',120)) dt,
1+abs(convert(int,convert(binary(4),newid()))%1000) ProductID, -- 1000 - количество разных продуктов
s.id StorehouseID,
case when abs(convert(int,convert(binary(4),newid()))%3) in (0,1) then 1 else -1 end Operation, -- какой то приход и расход, из случайных сделаем из 3х вариантов 2 приход 1 расход
1+abs(convert(int,convert(binary(4),newid()))%100) Quantity
into dbo.Turnover
from times t cross join storehouse s
option(maxrecursion 0);
go
--- 15 min
alter table dbo.Turnover alter column id int not null
go
alter table dbo.Turnover add constraint pk_turnover primary key (id) with(data_compression=page)
go
-- 6 min
У меня этот скрипт на ПК с SSD диском выполнялся порядка 22 минуты, и размер таблицы занял около 8Гб на жестком диске. Ты можешь уменьшить количество лет, и количество складов, для того что бы время создания и заполнения таблицы сократить. Но какой-то неплохой объем для оценки планов запросов рекомендую оставить, хотя бы 1-2 гигабайта.
Сгруппируем данные до часа
Далее, нам нужно сгруппировать суммы по продуктам на складе за исследуемый период времени, в нашей постановке задачи это один час (можно до минуты, до 15 минут, дня. Но очевидно до миллисекунд вряд ли кому понадобится отчетность). Для сравнений в сессии (окне) где выполняем наши запросы выполним команду — set statistics time on;. Далее выполняем сами запросы и смотрим планы запросов:
select top(1000)
convert(datetime,convert(varchar(13),dt,120)+':00',120) as dt, -- округляем до часа
ProductID,
StorehouseID,
sum(Operation*Quantity) as Quantity
from dbo.Turnover
group by
convert(datetime,convert(varchar(13),dt,120)+':00',120),
ProductID,
StorehouseID
Стоимость запроса — 12406
(строк обработано: 1000)
Время работы SQL Server:
Время ЦП = 2096594 мс, затраченное время = 321797 мс.
Если мы сделаем результирующий запрос с балансом, который считается нарастающим итогом от нашего количества, то запрос и план запроса будут следующими:
select top(1000)
convert(datetime,convert(varchar(13),dt,120)+':00',120) as dt, -- округляем до часа
ProductID,
StorehouseID,
sum(Operation*Quantity) as Quantity,
sum(sum(Operation*Quantity)) over
(
partition by StorehouseID, ProductID
order by convert(datetime,convert(varchar(13),dt,120)+':00',120)
) as Balance
from dbo.Turnover
group by
convert(datetime,convert(varchar(13),dt,120)+':00',120),
ProductID,
StorehouseID
Стоимость запроса — 19329
(строк обработано: 1000)
Время работы SQL Server:
Время ЦП = 2413155 мс, затраченное время = 344631 мс.
Оптимизация группировки
Здесь достаточно все просто. Сам запрос без нарастающего итога можно оптимизировать материализованным представлением (index view). Для построения материализованного представления, то что суммируется не должно иметь значение NULL, у нас суммируются sum(Operation*Quantity), или каждое поле сделать NOT NULL или добавить isnull/coalesce в выражение. Предлагаю создать материализованное представление.
create view dbo.TurnoverHour
with schemabinding as
select
convert(datetime,convert(varchar(13),dt,120)+':00',120) as dt, -- округляем до часа
ProductID,
StorehouseID,
sum(isnull(Operation*Quantity,0)) as Quantity,
count_big(*) qty
from dbo.Turnover
group by
convert(datetime,convert(varchar(13),dt,120)+':00',120),
ProductID,
StorehouseID
go
И построить по нему кластерный индекс. В индексе порядок полей укажем так же как и в группировке (для группировки столько порядок не важен, важно что бы все поля группировки были в индексе) и нарастающем итоге (здесь важен порядок — сначала то, что в partition by, затем то, что в order by):
create unique clustered index uix_TurnoverHour on dbo.TurnoverHour (StorehouseID, ProductID, dt)
with (data_compression=page) — 19 min
Теперь после построения кластерного индекса мы можем заново выполнить запросы, изменив агрегацию суммы как в представлении:
select top(1000)
convert(datetime,convert(varchar(13),dt,120)+':00',120) as dt, -- округляем до часа
ProductID,
StorehouseID,
sum(isnull(Operation*Quantity,0)) as Quantity
from dbo.Turnover
group by
convert(datetime,convert(varchar(13),dt,120)+':00',120),
ProductID,
StorehouseID
select top(1000)
convert(datetime,convert(varchar(13),dt,120)+':00',120) as dt, -- округляем до часа
ProductID,
StorehouseID,
sum(isnull(Operation*Quantity,0)) as Quantity,
sum(sum(isnull(Operation*Quantity,0))) over
(
partition by StorehouseID, ProductID
order by convert(datetime,convert(varchar(13),dt,120)+':00',120)
) as Balance
from dbo.Turnover
group by
convert(datetime,convert(varchar(13),dt,120)+':00',120),
ProductID,
StorehouseID
Планы запросов стали:
Стоимость 0.008
Стоимость 0.01
Время работы SQL Server:
Время ЦП = 31 мс, затраченное время = 116 мс.
(строк обработано: 1000)
Время работы SQL Server:
Время ЦП = 0 мс, затраченное время = 151 мс.
Итого, мы видим, что с индексированной вьюхой запрос сканирует не таблицу группируя данные, а кластерный индекс, в котором уже все сгруппировано. И соответственно время выполнения сократилось с 321797 миллисекунд до 116 мс., т.е. в 2774 раза.
На этом бы можно было бы и закончить нашу оптимизацию, если бы не тот факт, что нам нужна зачастую не вся таблица (вьюха) а ее часть за выбранный диапазон.
Промежуточные балансы
В итоге нам нужно быстрое выполнение следующего запроса:
set dateformat ymd;
declare
@start datetime = '2015-01-02',
@finish datetime = '2015-01-03'
select *
from
(
select
dt,
StorehouseID,
ProductId,
Quantity,
sum(Quantity) over
(
partition by StorehouseID, ProductID
order by dt
) as Balance
from dbo.TurnoverHour with(noexpand)
where dt <= @finish
) as tmp
where dt >= @start
Стоимость плана = 3103. А представь что бы было, если бы не по материализованному представлению пошел а по самой таблице.
Вывод данных материализованного представления и баланса по каждому продукту на складе на дату со временем округленную до часа. Что бы посчитать баланс — необходимо с самого начала (с нулевого баланса) просуммировать все количества до указанной последней даты (@finish), а после уже в просуммированном резалтсете отсечь данные позже параметра start.
Здесь, очевидно, помогут промежуточные рассчитанные балансы. Например, на 1е число каждого месяца или на каждое воскресенье. Имея такие балансы, задача сводится к тому, что нужно будет суммировать ранее рассчитанные балансы и рассчитать баланс не от начала, а от последней рассчитанной даты. Для экспериментов и сравнений построим дополнительный не кластерный индекс по дате:
create index ix_dt on dbo.TurnoverHour (dt) include (Quantity) with(data_compression=page); --7 min
И наш запрос будет вида:
set dateformat ymd;
declare
@start datetime = '2015-01-02',
@finish datetime = '2015-01-03'
declare
@start_month datetime = convert(datetime,convert(varchar(9),@start,120)+'1',120)
select *
from
(
select
dt,
StorehouseID,
ProductId,
Quantity,
sum(Quantity) over
(
partition by StorehouseID, ProductID
order by dt
) as Balance
from dbo.TurnoverHour with(noexpand)
where dt between @start_month and @finish
) as tmp
where dt >= @start
order by StorehouseID, ProductID, dt
Вообще этот запрос имея даже индекс по дате полностью покрывающий все затрагиваемые в запросе поля, выберет кластерный наш индекс и сканирование. А не поиск по дате с последующей сортировкой. Предлагаю выполнить следующие 2 запроса и сравнить что у нас получилось, далее проанализируем что все-таки лучше:
set dateformat ymd;
declare
@start datetime = '2015-01-02',
@finish datetime = '2015-01-03'
declare
@start_month datetime = convert(datetime,convert(varchar(9),@start,120)+'1',120)
select *
from
(
select
dt,
StorehouseID,
ProductId,
Quantity,
sum(Quantity) over
(
partition by StorehouseID, ProductID
order by dt
) as Balance
from dbo.TurnoverHour with(noexpand)
where dt between @start_month and @finish
) as tmp
where dt >= @start
order by StorehouseID, ProductID, dt
select *
from
(
select
dt,
StorehouseID,
ProductId,
Quantity,
sum(Quantity) over
(
partition by StorehouseID, ProductID
order by dt
) as Balance
from dbo.TurnoverHour with(noexpand,index=ix_dt)
where dt between @start_month and @finish
) as tmp
where dt >= @start
order by StorehouseID, ProductID, dt
Время работы SQL Server:
Время ЦП = 33860 мс, затраченное время = 24247 мс.
(строк обработано: 145608)
(строк обработано: 1)
Время работы SQL Server:
Время ЦП = 6374 мс, затраченное время = 1718 мс.
Время синтаксического анализа и компиляции SQL Server:
время ЦП = 0 мс, истекшее время = 0 мс.
Из времени видно, что индекс по дате выполняется значительно быстрее. Но планы запросов в сравнении выглядят следующим образом:
Стоимость 1го запроса с автоматически выбранным кластерным индексом = 2752, а вот стоимость с индексом по дате запроса = 3119.
Как бы то не было, здесь нам требуется от индекса две задачи: сортировка и выборка диапазона. Одним индексом из имеющихся нам эту задачу не решить. В данном примере диапазон данных всего за 1 день, но если будет период больше, но далеко не весь, например, за 2 месяца, то однозначно поиск по индексу будет не эффективен из-за расходов на сортировку.
Здесь из видимых оптимальных решений я вижу:
Создать вычисляемое поле Год-Месяц и индекс создать (Год-Месяц, остальные поля кластерного индекса). В условии where dt between @start_month and finish заменить на Год-Месяц=@месяц, и после этого уже наложить фильтр на нужные даты.
Фильтрованные индексы — индекс сам как кластерный, но фильтр по дате, за нужный месяц. И таких индексов сделать столько, сколько у нас месяцев всего. Идея близка к решению, но здесь если диапазон условий будет из 2х фильтрованных индексов, потребуется соединение и в дальнейшем все равно сортировка неизбежна.
Секционируем кластерный индекс так, чтобы в каждой секции были данные только за один месяц.
В проекте в итоге я сделал 3-й вариант. Секционирование кластерного индекса материализованного представления. И если выборка идет за промежуток времени одного месяца, то по сути оптимизатор затрагивает только одну секцию, делая ее сканирование без сортировки. А отсечение неиспользуемых данных происходит на уровне отсечения неиспользуемых секций. Здесь если поиск с 10 по 20 число у нас не идет точный поиск этих дат, а поиск данных с 1го по последний день месяца, далее сканирование этого диапазона в отсортированном индексе с фильтрацией во время сканирования по выставленным датам.
Секционируем кластерный индекс вьюхи. Прежде всего удалим из вьюхи все индексы:
drop index ix_dt on dbo.TurnoverHour;
drop index uix_TurnoverHour on dbo.TurnoverHour;
И создадим функцию и схему секционирования:
set dateformat ymd;
create partition function pf_TurnoverHour(datetime) as range right for values (
'2006-01-01', '2006-02-01', '2006-03-01', '2006-04-01', '2006-05-01', '2006-06-01', '2006-07-01', '2006-08-01', '2006-09-01', '2006-10-01', '2006-11-01', '2006-12-01',
'2007-01-01', '2007-02-01', '2007-03-01', '2007-04-01', '2007-05-01', '2007-06-01', '2007-07-01', '2007-08-01', '2007-09-01', '2007-10-01', '2007-11-01', '2007-12-01',
'2008-01-01', '2008-02-01', '2008-03-01', '2008-04-01', '2008-05-01', '2008-06-01', '2008-07-01', '2008-08-01', '2008-09-01', '2008-10-01', '2008-11-01', '2008-12-01',
'2009-01-01', '2009-02-01', '2009-03-01', '2009-04-01', '2009-05-01', '2009-06-01', '2009-07-01', '2009-08-01', '2009-09-01', '2009-10-01', '2009-11-01', '2009-12-01',
'2010-01-01', '2010-02-01', '2010-03-01', '2010-04-01', '2010-05-01', '2010-06-01', '2010-07-01', '2010-08-01', '2010-09-01', '2010-10-01', '2010-11-01', '2010-12-01',
'2011-01-01', '2011-02-01', '2011-03-01', '2011-04-01', '2011-05-01', '2011-06-01', '2011-07-01', '2011-08-01', '2011-09-01', '2011-10-01', '2011-11-01', '2011-12-01',
'2012-01-01', '2012-02-01', '2012-03-01', '2012-04-01', '2012-05-01', '2012-06-01', '2012-07-01', '2012-08-01', '2012-09-01', '2012-10-01', '2012-11-01', '2012-12-01',
'2013-01-01', '2013-02-01', '2013-03-01', '2013-04-01', '2013-05-01', '2013-06-01', '2013-07-01', '2013-08-01', '2013-09-01', '2013-10-01', '2013-11-01', '2013-12-01',
'2014-01-01', '2014-02-01', '2014-03-01', '2014-04-01', '2014-05-01', '2014-06-01', '2014-07-01', '2014-08-01', '2014-09-01', '2014-10-01', '2014-11-01', '2014-12-01',
'2015-01-01', '2015-02-01', '2015-03-01', '2015-04-01', '2015-05-01', '2015-06-01', '2015-07-01', '2015-08-01', '2015-09-01', '2015-10-01', '2015-11-01', '2015-12-01',
'2016-01-01', '2016-02-01', '2016-03-01', '2016-04-01', '2016-05-01', '2016-06-01', '2016-07-01', '2016-08-01', '2016-09-01', '2016-10-01', '2016-11-01', '2016-12-01',
'2017-01-01', '2017-02-01', '2017-03-01', '2017-04-01', '2017-05-01', '2017-06-01', '2017-07-01', '2017-08-01', '2017-09-01', '2017-10-01', '2017-11-01', '2017-12-01',
'2018-01-01', '2018-02-01', '2018-03-01', '2018-04-01', '2018-05-01', '2018-06-01', '2018-07-01', '2018-08-01', '2018-09-01', '2018-10-01', '2018-11-01', '2018-12-01',
'2019-01-01', '2019-02-01', '2019-03-01', '2019-04-01', '2019-05-01', '2019-06-01', '2019-07-01', '2019-08-01', '2019-09-01', '2019-10-01', '2019-11-01', '2019-12-01');
go
create partition scheme ps_TurnoverHour as partition pf_TurnoverHour all to ([primary]);
go
Ну и уже известный нам кластерный индекс только в созданной схеме секционирования:
create unique clustered index uix_TurnoverHour on dbo.TurnoverHour (StorehouseID, ProductID, dt) with (data_compression=page) on ps_TurnoverHour(dt); --- 19 min
И теперь посмотрим, что у нас получилось. Сам запрос:
set dateformat ymd;
declare
@start datetime = '2015-01-02',
@finish datetime = '2015-01-03'
declare
@start_month datetime = convert(datetime,convert(varchar(9),@start,120)+'1',120)
select *
from
(
select
dt,
StorehouseID,
ProductId,
Quantity,
sum(Quantity) over
(
partition by StorehouseID, ProductID
order by dt
) as Balance
from dbo.TurnoverHour with(noexpand)
where dt between @start_month and @finish
) as tmp
where dt >= @start
order by StorehouseID, ProductID, dt
option(recompile);
Время работы SQL Server:
Время ЦП = 7860 мс, затраченное время = 1725 мс.
Время синтаксического анализа и компиляции SQL Server:
время ЦП = 0 мс, истекшее время = 0 мс.
Стоимость плана запроса = 9.4
По сути данные в одной секции выбираются и сканируются по кластерному индексу достаточно быстро. Здесь следует добавить то, что когда запрос параметризирован, возникает неприятный эффект parameter sniffing, лечится option(recompile).
Предлагаю подборку невероятно красивых фонов и тайлсетов в разных сеттингах: от джунглей до sci-fi. Вы найдете всё, чтобы создавать эффектные окружения и задавать играм настроение посредством дизайна.
Фоны
6 Game Backgrounds
Набор векторных фонов станет отличным дополнением для приключенческого сайд-скроллера. Каждый из шести фонов можно редактировать в Adobe Illustrator. А возможность настроить параллакс-эффект придется по душе разработчикам, желающим придать уровням дополнительную глубину.
5 in 1 Game Background
Пещеры, леса, космос и королевство леденцов – эти красочные и забавные фоны определенно порадуют игроков.
Jungle Run Game Background
Набор идеально подойдет для игры, действие которой происходит в джунглях. В нем есть всё необходимое: лианы, деревья и много другой зеленой растительности.
Retro City Building Pack
В вашем распоряжении набор легко редактируемых векторных ассетов – создайте неповторимый городской ландшафт.
Nature Game Background
Этот фон с минималистичным природным пейзажем станет отличным дополнением для бегающего или летающего персонажа.
Game Backgrounds #1
Набор включает десять простых и легко повторяемых фонов с параллакс-эффектом.
Game Backgrounds in One Set
Получите максимум за свои деньги с этим набором из 15 фонов, которые отлично подойдут для шутера, раннера или любого другого сайд-скроллера.
Game Background
Мне нравится, как хорошо эти фоны с их мечтательными горами и жуткими кладбищами передают разные настроения. Добавьте в игру десять захватывающих уровней!
9 Cartoon Game Backgrounds
В этом наборе из десяти мультипликационных фонов совершенно потрясающие цветовые палитры. Кто устоит перед игрой с такой красивой графикой?
8 Chinese Game Background
Если вы хотите развернуть действия игры в конкретном сеттинге, вам могут пригодиться эти восемь фонов в духе фэнтези с китайской архитектурой.
Тайлсеты
Стройте ваш игровой мир блок за блоком и создавайте невероятные сеттинги для разных жанров – от приключенческих игр до платформеров.
Scifi Shooter Sprite Sheet
Набор спрайтов и тайлов в лучших традициях sci-fi позволит вам построить неповторимый мир с видом сверху, где герои будут спасать человечество от нашествия инопланетян.
Platformer Game Tile Set 4
Тайлсет станет незаменимым при создании приключенческого платформера в духе хоррор. Пакет включает два стиля тайлов, фоны, а также целый ряд дополнительных предметов для построения полноценных уровней.
Pixel City Creation Kit
Стройте города с этим набором пиксельной графики. Отличный выбор для игр-симуляторов и не только.
Platformer Game Tile Set 5
Хотите создать у игроков зимнее настроение? Лучше всего подойдет набор со снеговиками, ледниками и тайлами в снегу.
Platformer Game Tile Set 6
Что мне нравится в таких тематических тайлсетах, так это возможность создавать множество уровней из разных наборов, предоставляя пользователю богатый игровой опыт. В данном случае мы имеем дело с интересным приключенческим тайлсетом, который включает лестницы, шипы, черешни и всё в таком духе.
Platformer Game Tile Set 9
А этот тайлсет стилизован под подземелье. Он идеально подойдет, если действие вашего платформера происходит в замке. Кроме того, подобные наборы могут послужить неплохим источником вдохновения для геймдизайнеров, так как они позволяют создавать дополнительные элементы к тем, что изначально входят в набор.
2D Tileset Platform Game
С этим набором ваш платформер будет выглядеть просто, но в то же время ярко и красочно. Он включает два стиля тайлов, фоны, а также предметы для наполнения уровней.
2D Tileset Platform Game 3
Создайте игру в сеттинге джунгли. Каждый предмет, тайл и фон детализированы настолько качественно, что невозможно было не включить этот сет в список.
Platform Game Tileset 4: Abandoned Castle
Если вы ищете что-то реалистичное, этот набор с графикой в духе заброшенного замка – то, что нужно. Он включает красиво прорисованные тайлы, предметы и фоны.
Top-Down Roguelike Dungeon Crawl RPG Tileset
Мне нравится этот тайлсет в стиле подземелья. Он отлично подойдет для RPG или приключенческой игры с видом сверху. А векторные ассеты легко редактируются, что позволит создавать множество уникальных уровней.
В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.
//Kotlin
fun rangeLoop() {
for (i in 1..10) {
println(i)
}
}
1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.
//Java
public static final void rangeLoop() {
int i = 1;
byte var1 = 10;
if(i <= var1) {
while(true) {
System.out.println(i);
if(i == var1) {
break;
}
++i;
}
}
}
Похожий цикл по массиву (который в Kotlin записывается в виде Array<*>), компилируется аналогичным образом в цикл for.
//Kotlin
fun arrayLoop(x: Array) {
for (s in x) {
println(s)
}
}
//Java
public static final void arrayLoop(@NotNull String[] x) {
Intrinsics.checkParameterIsNotNull(x, "x");
for(int var2 = 0; var2 < x.length; ++var2) {
String s = x[var2];
System.out.println(s);
}
}
Немного другая ситуация возникает, когда происходит перебор элементов из списка:
//Kotlin
fun listLoop(x: List) {
for (s in x) {
println(s)
}
}
В этом случае приходится использовать итератор:
//Java
public static final void listLoop(@NotNull List x) {
Intrinsics.checkParameterIsNotNull(x, "x");
Iterator var2 = x.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
System.out.println(s);
}
}
Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.
Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:
Циклы
Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.
When
When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:
Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:
//Java
public static final String tableWhen(int x) {
String var10000;
switch(x) {
case 0:
var10000 = "zero";
break;
case 1:
var10000 = "one";
break;
default:
var10000 = "many";
}
return var10000;
}
Если же немного изменить пример выше, и добавить константы:
//Kotlin
val ZERO = 1
val ONE = 1
fun constWhen(x: Int): String = when(x) {
ZERO -> "zero"
ONE -> "one"
else -> "many"
}
То код в этом случае уже компилируется уже в следующий вид:
//Java
public static final String constWhen(int x) {
return x == ZERO?"zero":(x == ONE?"one":"many");
}
Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.
Если же заменить константы на Enum:
//Kotlin (файл When3.kt)
enum class NumberValue {
ZERO, ONE, MANY
}
fun enumWhen(x: NumberValue): String = when(x) {
NumberValue.ZERO -> "zero"
NumberValue.ONE -> "one"
NumberValue.MANY -> "many"
}
То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).
//Java
public final class When3Kt$WhenMappings {
// $FF: synthetic field
public static final int[] $EnumSwitchMapping$0 = new int[NumberValue.values().length];
static {
$EnumSwitchMapping$0[NumberValue.ZERO.ordinal()] = 1;
$EnumSwitchMapping$0[NumberValue.ONE.ordinal()] = 2;
$EnumSwitchMapping$0[NumberValue.MANY.ordinal()] = 3;
}
}
public static final String enumWhen(@NotNull NumberValue x) {
Intrinsics.checkParameterIsNotNull(x, "x");
String var10000;
switch(When3Kt$WhenMappings.$EnumSwitchMapping$0[x.ordinal()]) {
case 1:
var10000 = "zero";
break;
case 2:
var10000 = "one";
break;
case 3:
var10000 = "many";
break;
default:
throw new NoWhenBranchMatchedException();
}
return var10000;
}
По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.
Посмотрим на сравнение производительности решений на Kotlin и Java:
When
Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordginal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.
Делегаты
Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:
//Kotlin
package examples
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b {
fun anotherMethod(): Unit {}
}
Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:
public final class Derived implements Base {
private final Base $$delegate_0;
public Derived(@NotNull Base b) {
Intrinsics.checkParameterIsNotNull(b, "b");
super();
this.$$delegate_0 = b;
}
public void print() {
this.$$delegate_0.print();
}
public final void anotherMethod() {
}
}
В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.
Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).
Код на Kotlin:
//Kotlin
class DeleteExample {
val name: String by Delegate()
}
Компилируется в код:
public final class DeleteExample {
@NotNull
private final Delegate name$delegate = new Delegate();
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(DeleteExample.class), "name", "getName()Ljava/lang/String;"))};
@NotNull
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
}
При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.
В Kotlin есть уже несколько стандартных делегатов:
— lazy, для ленивых вычислений значения поля.
— observable, который позволяет получать уведомления обо всех изменения значения поля
— map, используемый для инициализации значений поля из значений Map.
Object и companion object
В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:
Простое объявление object с одним методом выглядит следующим образом:
//Kotlin
object ObjectExample {
fun objectFun(): Int {
return 1
}
}
В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:
public final class ObjectExample {
public static final ObjectExample INSTANCE;
public final int objectFun() {
return 1;
}
private ObjectExample() {
INSTANCE = (ObjectExample)this;
}
static {
new ObjectExample();
}
}
И место вызова:
//Kotlin
val value = ObjectExample.objectFun()
Компилируется к вызову INSTANCE:
//Java
int value = ObjectExample.INSTANCE.objectFun();
companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.
//Kotlin
class ClassWithCompanion {
val name: String = "Kurt"
companion object {
fun companionFun(): Int = 5
}
}
//method call
ClassWithCompanion.companionFun()
Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:
//Java
public final class ClassWithCompanion {
@NotNull
private final String name = "Kurt";
public static final ClassWithCompanion.Companion Companion = new ClassWithCompanion.Companion((DefaultConstructorMarker)null);
@NotNull
public final String getName() {
return this.name;
}
public static final class Companion {
public final int companionFun() {
return 5;
}
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
//вызов функции
ClassWithCompanion.Companion.companionFun();
Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:
//Kotlin
object ObjectWithStatic {
@JvmStatic
fun staticFun(): Int {
return 5
}
}
В этом случае метод staticFun будет действительно объявлен статическим:
public final class ObjectWithStatic {
public static final ObjectWithStatic INSTANCE;
@JvmStatic
public static final int staticFun() {
return 5;
}
private ObjectWithStatic() {
INSTANCE = (ObjectWithStatic)this;
}
static {
new ObjectWithStatic();
}
}
Для методов из companion object тоже можно добавить аннотацию @JvmStatic:
class ClassWithCompanionStatic {
val name: String = "Kurt"
companion object {
@JvmStatic
fun companionFun(): Int = 5
}
}
Для такого кода будет также создан статичный метод companionFun. Но сам метод все равно будет вызывать метод из компаньона:
public final class ClassWithCompanionStatic {
@NotNull
private final String name = "Kurt";
public static final ClassWithCompanionStatic.Companion Companion = new ClassWithCompanionStatic.Companion((DefaultConstructorMarker)null);
@NotNull
public final String getName() {
return this.name;
}
@JvmStatic
public static final int companionFun() {
return Companion.companionFun();
}
public static final class Companion {
@JvmStatic
public final int companionFun() {
return 5;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)
lateinit свойства
Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.
//Kotlin
class LateinitExample {
lateinit var lateinitValue: String
}
Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:
//Java
public final class LateinitExample {
@NotNull
public String lateinitValue;
@NotNull
public final String getLateinitValue() {
String var10000 = this.lateinitValue;
if(this.lateinitValue == null) {
Intrinsics.throwUninitializedPropertyAccessException("lateinitValue");
}
return var10000;
}
public final void setLateinitValue(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "");
this.lateinitValue = var1;
}
}
В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.
coroutines
В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:
kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.
kotlinx-coroutines-reactive — утилиты для реактивных стримов
kotlinx-coroutines-reactor — утилиты для Reactor
kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
kotlinx-coroutines-rx2 — утилиты для RxJava 2.x
kotlinx-coroutines-android — UI контекст для Android.
kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.
Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.
Для того, чтобы обозначить, что функция может быть прервана и использована в контексте прерывания, используется модификатор suspend
//Kotlin
suspend fun asyncFun(x: Int): Int {
return x * 3
}
Декомпилированный код выглядит следующим образом:
//Java
public static final Object asyncFun(int x, @NotNull Continuation $continuation) {
Intrinsics.checkParameterIsNotNull($continuation, "$continuation");
return Integer.valueOf(x * 3);
}
Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.
interface Continuation {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
В нем хранится контекст выполнения, определена функция возвращения результата и функция возвращения исключения, в случае ошибки.
Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:
val a = a()
val y = foo(a).await() // точка прерывания #1
b()
val z = bar(a, y).await() // точка прерывания #2
c(z)
Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:
class extends CoroutineImpl<...> implements Continuation {
// текущее состояние машины состояний
int label = 0
// локальные переменные корутин
A a = null
Y y = null
void resume(Object data) {
if (label == 0) goto L0
if (label == 1) goto L1
if (label == 2) goto L2
else throw IllegalStateException()
L0:
a = a()
label = 1
data = foo(a).await(this) // 'this' передается как continuation
if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
L1:
// внешний код возвращает выполнение корутины, передавая результат как data
y = (Y) data
b()
label = 2
data = bar(a, y).await(this) // 'this' передается как continuation
if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
L2:
// внешний код возвращает выполнение корутины передавая результат как data
Z z = (Z) data
c(z)
label = -1 // Не допускается больше никаких шагов
return
}
}
Как видно, получаются 3 состояния: L0, L1, L2. Выполнение начинается в состоянии L0, далее из которого происходит переключение в состояние L1 и после в L2. В конце происходит переключение состояния в -1 как индикация того, что больше никаких шагов не допускается.
Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.
Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.
Выводы
Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.
Спасибо за внимание!
Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.
О Kotlin последнее время уже очень много сказано (особенно в совокупности с последними новостями c Google IO 17), но в то же время не очень много такой нужной информации, во что же компилируется Kotlin.
Давайте подробнее рассмотрим на примере компиляции в байткод JVM.
Это первая часть публикации. Вторую можно посмотреть тут
Процесс компиляции это довольно обширная тема и чтобы лучше раскрыть все ее нюансы я взял большую часть примеров компиляции из выступления Дмитрия Жемерова: Caught in the Act: Kotlin Bytecode Generation and Runtime Performance. Из этого же выступления взяты все бенчмарки. Помимо ознакомления с публикацией, настоятельно рекомендую вам еще и посмотреть его выступление. Некоторые вещи там рассказаны более подробно. Я же больше внимания акцентирую именно на компиляции языка.
Но прежде чем рассмотрим основные конструкции языка и то, в какой байткод они компилируются, нужно упомянуть о том, как непосредственно происходит сама компиляция языка:
На вход компилятора kotlinc поступают исходные файлы, причем не только файлы kotlin, но и файлы java. Это нужно чтобы можно было свободно ссылаться на Java из Kotlin, и наоборот. Сам компилятор прекрасно понимает исходники Java, но не занимается их компиляцией, на этом этапе происходит только компиляция файлов Kotlin. После полученные *.class файлы передаются компилятору javaс вместе с исходными файлами *.java. На этом этапе компилируются все java файлы, после чего становится возможным собрать вместе все файлы в jar (либо каким другим образом).
Для того чтобы посмотреть в какой байткод генерируется Kotlin, в Intellij IDEA можно открыть специальное окно из Tools -> Kotlin -> Show Kotlin Bytecode. И после, при открытие любого файла *.kt, в этом окне будет виден его байткод. Если в нем не будет ничего такого, что нельзя представить в Java, то также будет доступна возможность декомпилировать его в Java код кнопкой Decompile.
Если посмотреть на любой *.class файл kotlin, то там можно увидеть большую аннотацию @Metadata:
Она содержит всю ту информацию, которая существует в языке Kotlin, и которую невозможно представить на уровне Java байткода. Например информацию о свойствах, nullable типов и т.п. С этой информацией не нужно работать напрямую, но с ней работает компилятор, и к ней можно получить доступ используя Reflection API. Формат метадаты это на самом деле Protobuf cо своими декларациями.
Дмитрий Жемеров
Давайте теперь перейдем к примерам, в которых рассмотрим основные конструкции и то, в каком виде они представлены в байткоде. Но чтобы не разбираться в громоздких записях байткода в большинстве случаев рассмотрим декомпилированный вариант в Java:
Функции на уровне файла
Начнем с самого простого примера: функция на уровне файла.
//Kotlin, файл Example1.kt
fun foo() { }
В Java нет аналогичной конструкции. В байткоде она реализуется с помощью создания дополнительного класса.
//Java
public final class Example1Kt {
public static final void foo() {
}
}
В качестве названия для такого класса используется имя исходного файла с суффиксом *Kt (в данном случае Example1Kt). Существует также возможность поменять имя класса с помощью аннотации file:JvmName:
//Kotlin
@file:JvmName("Utils")
fun foo() { }
//Java
public final class Utils {
public static final void foo() {
}
}
Primary конструкторы
В Kotlin есть возможность прямо в заголовке конструктора объявить свойства (property).
//Kotlin
class A(val x: Int, val y: Long) {}
Они будут параметрами конструктора, для них будут сгенерированы поля и, соответственно, значения, переданные в конструктор, будут записаны в эти поля. Также будут созданы getter, позволяющие эти поля прочитать. Декомпилированный вариант примера выше будет выглядеть так:
//Java
public final class A {
private final int x;
private final long y;
public final int getX() {
return this.x;
}
public final long getY() {
return this.y;
}
public A(int x, long y) {
this.x = x;
this.y = y;
}
}
Если в объявлении класса A у переменной x изменить val на var, то тогда еще будет сгенерированы setter. Стоит также обратить внимание на то, что класс A будет объявлен с модификатором final и public. Это связано с тем что все классы в Kotlin по умолчанию final и имеют область видимости public.
data классы
В Kotlin есть специальный модификатор для класса data.
//Kotlin
data class B(val x: Int, val y: Long) { }
Это ключевое слово говорит компилятору о том, чтобы он сгенерировал для класса методы equals, hashCode, toString, copy и componentN функции. Последние нужны для того, чтобы класс можно было использовать в destructing объявлениях. Посмотрим на декомпилированный код:
//Java
public final class B {
// --- аналогично примеру 2
public final int component1() {
return this.x;
}
public final long component2() {
return this.y;
}
@NotNull
public final B copy(int x, long y) {
return new B(x, y);
}
public String toString() {
return "B(x=" + this.x + ", y=" + this.y + ")";
}
public int hashCode() {
return this.x * 31 + (int)(this.y ^ this.y >>> 32);
}
public boolean equals(Object var1) {
if(this != var1) {
if(var1 instanceof B) {
B var2 = (B)var1;
if(this.x == var2.x && this.y == var2.y) {
return true;
}
}
return false;
} else {
return true;
}
}
На практике модификатор data очень часто используется, особенно для классов, которые участвуют во взаимодействии между компонентами, либо хранятся в коллекциях. Также data классы позволяют быстро создать иммутабельный контейнер для данных.
Свойства в теле класса
Свойства также могут быть объявлены в теле класса.
//Kotlin
class C {
var x: String? = null
}
В данном примере в классе С мы объявили свойство x типа String, которое еще к тому же может быть null. В этом случае в коде появляются дополнительные аннотации @Nullable:
//Java
import org.jetbrains.annotations.Nullable;
public final class C {
@Nullable
private String x;
@Nullable
public final String getX() {
return this.x;
}
public final void setX(@Nullable String var1) {
this.x = var1;
}
}
В этом случае в декомпилированном варианте мы увидим getter, setter (так как переменная объявлена с модификатором var).Аннотация @Nullable необходима для того, чтобы те статические анализаторы, которые понимают данную аннотацию, могли проверять по ним код и сообщать о каких-либо возможных ошибках.
Если же нам не нужны getter и setter, а просто нужно публичное поле, то мы можем добавить аннотацию @JvmField:
//Kotlin
class C {
@JvmField var x: String? = null
}
Тогда результирующий Java код будет следующий:
//Java
public final class C {
@JvmField
@Nullable
public String x;
}
Not-null типы в публичных и приватных методах
В Kotlin существует небольшая разница между тем, какой байткод генерируется для public и private методов. Посмотрим на примере двух методов, в которые передаются not-null переменные.
//Kotlin
class E {
fun x(s: String) {
println(s)
}
private fun y(s: String) {
println(s)
}
}
В обоих методах передается параметр s типа String, и в обоих случаях этот параметр не может быть null.
//Java
import kotlin.jvm.internal.Intrinsics;
public final class E {
public final void x(@NotNull String s) {
Intrinsics.checkParameterIsNotNull(s, "s");
System.out.println(s);
}
private final void y(String s) {
System.out.println(s);
}
}
В таком случае для публичного метода генерируется дополнительная проверка типа (Intrinsics.checkParameterIsNotNull), которая проверяет что переданный параметр действительно не null. Это сделано для того, чтобы публичные методы можно было вызывать из Java. И если вдруг в них передается null, то этот метод должен падать в этом же месте, не передавая переменную дальше по коду. Это необходимо для раннего диагностирования ошибок. В приватных методах такой проверки нет. Из Java его просто так нельзя вызвать, только если через reflection. Но с помощью reflection можно вообще много чего сломать при желании. Из Kotlin же компилятор сам следит за вызовами и не даст передать null в такой метод.
Такие проверки, конечно, не могут совсем не влиять на быстродействие. Довольно интересно померить на сколько же они ее ухудшают, но простыми бенчмарками это сделать тяжело. Поэтому посмотрим на данные, которые удалось получить Дмитрию Жемерову:
Проверка параметров на null
Для одного параметра стоимость такой проверки на NotNull вообще пренебрежимо мала. Для метода с восемью параметрами, который больше ничего не делает, кроме как проверяет на null, уже получается что какая-то заметная стоимость есть. Но в любом случае в обычной жизни эту стоимость (приблизительно 3 наносекунды) можно не учитывать. Более вероятна ситуация, что это последнее, что придется оптимизировать в коде. Но если все же нужно убрать излишние проверки, то на данный момент это возможно с помощью дополнительный опций компилятора kotlinc: -Xno-param-assertions и -Xno-call-assertions (важно!: прежде чем отключать проверки, действительно подумайте, в этом ли причина ваших бед, и не будет ли такого, что это принесет больше вреда чем пользы)
Функции расширения (extension functions)
Kotlin позволяет расширять API существующих классов, написанных не только на Kotlin, но и на Java. Для любого класса можно написать объявление функции и дальше в коде ее можно использовать у этого класса так, как будто эта функция была при его объявлении.
//Kotlin (файл Example6.kt)
class T(val i: Int)
fun T.foo(): Int {
return i
}
fun useFoo() {
T(1).foo()
}
В Java генерируется класс, в котором будет просто статический метод с именем, как у функции расширения. В этот метод передается инстанс расширяемого класса. Таким образом, когда мы вызываем функцию расширения, мы на самом деле передаем в стическую функцию сам элемент, на котором вызываем метод.
//Java
public final class Example6Kt {
public static final int foo(@NotNull T $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
return $receiver.getI();
}
public static final void useFoo() {
foo(new T(1));
}
}
Почти вся стандартная библиотека Kotlin состоит из функций расширений для классов JDK. В Kotlin очень маленькая своя стандартная библиотека и нет объявления своих классов коллекций. Все коллекции, объявляемые через listOf, setOf, mapOf, которые в Kotlin выглядят на первый взгляд своими, на самом деле обычные Java коллекции ArrayList, HashSet, HashMap. И если нужно передать такую коллекцию в библиотеку (или из библиотеки), то нет никаких накладных расходов на конвертацию к своим внутренним классам (в отличие от Scala <-> Java) или копирование.
Тела методов в интерфейсах
В Kotlin есть возможность добавить реализацию для методов в интерфейсах.
//Kotlin
interface I {
fun foo(): Int {
return 42
}
}
class D : I { }
В Java 8 такая возможность также появилась, но по причине того, что Kotlin должен работать и на Java 6, результирующий код в Java выглядит следующим образом:
public interface I {
int foo();
public static final class DefaultImpls {
public static int foo(I $this) {
return 42;
}
}
}
public final class D implements I {
public int foo() {
return I.DefaultImpls.foo(this);
}
}
В Java создается обычный интерфейс, с декларацией метода, и появляется декларация класса DefaultImpls с реализацией по умолчанию для нужных методов. В местах же использования методов появляется вызов реализаций из объявленного в интерфейсе класса, в методы которого передается сам объект вызова.
У команды Kotlin есть планы для перехода на реализацию этой функциональности с помощью методов по умолчанию (default method) из Java 8, но на данный момент присутствуют трудности с сохранением бинарной совместимости с уже скомпилированными библиотеками. Можно посмотреть обсуждение этой проблемы на youtrack. Конечно большой проблемы это не создает, но если в проекте планируется создание api для Java, то нужно учитывать эту особенность.
Аргументы по умолчанию
В отличие от Java, в Kotlin есть аргументы по умолчанию. Но их реализация сделана достаточно интересно.
//Kotlin (файл Example8.kt)
fun first(x: Int = 11, y: Long = 22) {
println(x)
println(y)
}
fun second() {
first()
}
Для реализации аргументов по умолчанию в байткоде Java используется синтетический метод, в который передается битовая маска mask с информацией о том, какие аргументы отсутствуют в вызове.
//Java
public final class Example8Kt {
public static final void first(int x, long y) {
System.out.println(x);
System.out.println(y);
}
public static void first$default(int var0, long var1, int mask, Object var4) {
if((mask & 1) != 0) {
var0 = 11;
}
if((mask & 2) != 0) {
var1 = 22L;
}
first(var0, var1);
}
public static final void second() {
first$default(0, 0L, 3, (Object)null);
}
}
Единственный интересный момент, зачем генерируется аргумент var4? Сам он нигде не используется, а в местах использования передается null. Информацию по назначению этого аргумента я не нашел, может yole сможет прояснить ситуацию.
Ниже показаны оценки затрат на такие манипуляции:
Аргументы по умолчанию
Стоимость аргументов по умолчанию уже становится немного заметной. Но все равно потери измеряются в наносекундах и при обычной работе такими потерями можно пренебречь. Существует также способ заставить компилятор Kotlin по другому сгенерировать в байткоде аргументы по умолчанию. Для этого нужно добавить аннотацию @JvmOverloads:
//Kotlin
@JvmOverloads
fun first(x: Int = 11, y: Long = 22) {
println(x)
println(y)
}
В таком случае, помимо методов из предыдущего примера, еще будут сгенерированы перегрузки метода first под различные варианты передачи аргументов.
//Java
public final class Example8Kt {
//-- методы first, second, first$default из предыдущего примера
@JvmOverloads
public static final void first(int x) {
first$default(x, 0L, 2, (Object)null);
}
@JvmOverloads
public static final void first() {
first$default(0, 0L, 3, (Object)null);
}
}
Лямбды
Лямбды в Kotlin представляются практически также как и в Java (за исключением того что они являются объектами первого класса)
//Kotlin (файл Lambda1.kt)
fun runLambda(x: ()-> T): T = x()
В данном случае функция runLambda принимает инстанс интерфейса Function0 (объявление которого находится в стандартной библиотеке Kotlin), в котором есть функция invoke(). И соответственно это все совместимо с тем, как это работает в Java 8, и, конечно, работает SAM-конверсия из Java. Результирующий байткод будет выглядеть следующим образом:
//Java
public final class Lambda1Kt {
public static final Object runLambda(@NotNull Function0 x) {
Intrinsics.checkParameterIsNotNull(x, "x");
return x.invoke();
}
}
Компиляция в байткод сильно зависит от того, если ли захват значения из окружающего контекста или нет. Рассмотрим пример, когда есть глобальная переменная value и лямбда, которая просто возвращает ее значение.
//Kotlin (файл Lambda2.kt)
var value = 0
fun noncapLambda(): Int = runLambda { value }
В Java в данном случае, по сути, создается синглтон. Сама лямбда ничего из контекста не использует и соотвественно не нужно создавать разные инстансы под все вызовы. Поэтому просто компилируется класс, который реализует интерфейс Function0, и, как результат, вызов лямбды происходит без аллокации и весьма дешево.
//Java
final class Lambda2Kt$noncapLambda$1 extends Lambda implements Function0 {
public static final Lambda2Kt$noncapLambda$1 INSTANCE = new Lambda2Kt$noncapLambda$1()
public final int invoke() {
return Lambda2Kt.getValue();
}
}
public final class Lambda2Kt {
private static int value;
public static final int getValue() {
return value;
}
public static final void setValue(int var0) {
value = var0;
}
public static final int noncapLambda() {
return ((Number)Lambda1Kt.runLambda(Lambda2Kt$noncapLambda$1.INSTANCE)).intValue();
}
}
Рассмотрим другой пример с использованием локальных переменных с контекстами.
//Kotlin (файл Lambda3.kt)
fun capturingLambda(v: Int): Int = runLambda { v }
В данном случае синглтоном уже не обойтись, так как каждый конкретный инстанс лямбды должен иметь свое значение параметра.
//Java
public static final int capturingLambda(int v) {
return ((Number)Lambda1Kt.runLambda((Function0)(new Function0() {
public Object invoke() {
return Integer.valueOf(this.invoke());
}
public final int invoke() {
return v;
}
}))).intValue();
}
Лямбды в Kotlin также умеют менять значение не локальных переменных (в отличие от лямбд Java).
//Kotlin (файл Lambda4.kt)
fun mutatingLambda(): Int {
var x = 0
runLambda { x++ }
return x
}
В этом случае создается обертка для изменяемой переменной. Сама обертка, аналогично предыдущему примеру, передается в создаваемую лямбду, внутри которой и происходит изменение исходной переменной через обращение к обертке.
public final class Lambda4Kt {
public static final int mutatingLambda() {
final IntRef x = new IntRef();
x.element = 0;
Lambda1Kt.runLambda((Function0)(new Function0() {
public Object invoke() {
return Integer.valueOf(this.invoke());
}
public final int invoke() {
int var1 = x.element++;
return var1;
}
}));
return x.element;
}
}
Попробуем сравнить производительность решений на Kotlin, с аналогами на Java:
Лямбда
Как видно, возня с обертками (последний пример) занимает заметное время, но, с другой стороны, в Java такое не поддерживается из коробки, а если делать руками подобную реализацию, то и затраты будут аналогичные. В остальном разница не так заметна.
Также в Kotlin есть возможность передавать ссылки на методы (method reference) в лямбды, причем они, в отличие от лямбд, сохраняют информацию о том, на что же указывают методы. Ссылки на методы компилируется похожим образом на то, как выглядят лямбды без захвата контекста. Создается синглтон, который помимо значения еще знает на что же эта лямбда ссылается.
У лямбд в Kotlin есть еще одна интересная особенность: их можно объявить с модификатором inline. В этом случае компилятор сам найдет все места использования функции в коде и заменит их на тело функции. JIT тоже умеет инлайнить некоторые вещи и сам, но никогда нельзя быть уверенным в том, что он будет инлайнить, а что пропустит. Поэтому иметь свой управляемый механизм инлайна никогда не помешает.
//Kotin (файл Lambda5.kt)
fun inlineLambda(x: Int): Int = run { x }
//run это функция из стандартной библиотеки:
public inline fun run(block: () -> R): R = block()
//Java
public final class Lambda5Kt {
public static final int inlineLambda(int x) {
return x;
}
}
В примере выше не происходит никакой аллокации, никаких вызовов. По сути, код функции просто “схлопывается”. Это позволяет очень эффективно реализовывать всякие filter, map и т.п. Тот же оператор synchronized тоже инлайнится.
Спасибо за внимание!
Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточность, написать об этом мне в личном сообщении.
Эту неделю завершаем подборкой задач и вопросов, которые часто дают на собеседованиях в Facebook. Задачи выбрали разных уровней сложности от «Easy» до «Hard». Условие снова оставили на английском языке. Варианты решений прикрепим в комментарии через неделю. Good luck!
Вопросы:
1.Вы хотите запустить анимацию через полсекунды после нажатия пользователем кнопки. Какой способ сделать это будет лучшим?
2.Приложение для обмена фотографиями отображает системное уведомление, когда пользователь получает фотографию. Ваше приложение должно отображать фотографию, когда пользователь удаляет уведомление. Какое из следующих действий вам необходимо связать с объектом Notification, который вы передаете в Notification Manager? Задачи:
1.
Given an array nums, write a function to move all 0's to the end of it while maintaining the relative order of the non-zero elements.
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. Note:
You must do this in-place without making a copy of the array.
Minimize the total number of operations.
2.
Given an array nums and a target value k, find the maximum length of a subarray that sums to k. If there isn't one, return 0 instead.
Note:
The sum of the entire nums array is guaranteed to fit within the 32-bit signed integer range.
Example 1:
Given nums = [1, -1, 5, -2, 3], k = 3,
return 4. (because the subarray [1, -1, 5, -2] sums to 3 and is the longest)
Example 2:
Given nums = [-2, -1, 2, 1], k = 1,
return 2. (because the subarray [-1, 2] sums to 1 and is the longest)
Follow Up:
Can you do it in O(n) time?
3.
Remove the minimum number of invalid parentheses in order to make the input string valid. Return all possible results.
Note: The input string may contain letters other than the parentheses ( and ).
Возможно у вас наступал такой момент, что хотелось написать свой движок для игр, или просто вы хотели узнать, как такое реализовать, но по каким — то причинам вам это не удавалось.
Ну что ж, тема довольно обширная, поэтому я начинаю серию уроков по написанию своего 2д игрового движка, и поверьте он будет не хуже того же Love2d, именно такого стиля и будет наш движок.
Что нужно?
Средние знания С++(на нем и будем писать двигатель).
Базовые знания Lua (для описания игровой логики).
Как все устроено?
Вся игровая логика будет программироваться в файле, например — «main.lua».
Движок будет читать этот файл и исполнять действия описанные в этом файле.
Вывод графики будет с помощью SDL 2.0, физика — Box2D, аудио — OpenAl, скриптинг — Lua.
IDE — Microsoft Visual Studio любой версии.
Нарисовал схему
Начинаем!
Сначала надо скачать:
Lua
SDL 2.0
Box2D (надо будет скомпилировать самому)
OpenAL
Скачайте и переместите все файлы в отдельную папку, например — «Engine SDK».
Открываем MVS, создаем «пустое» консольное приложение, далее добавляем файл — «main.cpp».
Заполняем пока таким образом:
int main()
{
return 0;
}
Компилируем, если скомпилировалось, идем дальше.
Жмем «Project -> project properties»
Выбираем «C/C++ -> General» и добавляем дополнительные папки включения (указывайте путь где вы извлекли из архива Lua).
Указываем путь к «include» Lua.
После этого заходим «Linker ->General» и добавляем путь к либе.
Применяем и изменяем «main.cpp»
int main(int argc, char * argv[])
{
return 0;
}
Компилируем, идем дальше.
Далее нам нужно создать отдельный заголовочный файл, в котором будет основная часть движка.
Добавляем файл «Engiine.h»
И сразу заполняем его таким образом.
Компилируем, если нет ошибок, идем дальше, если же есть, значит вы где- то накосячили.
Изменяем «main.cpp»
include "Engiine.h"
int main(int argc, char * argv[])
{
lua.Init();
lua.Open("main.lua");
lua.Call_load();
lua.Close();
return 0;
}
Компилируем, если без ошибок, двигаемся дальше.
Создаем в папке проекта тестовый файл «main.lua».
Заполняем его так:
function Load()
print("Lua inited!")
end
function Update()
end
function Draw()
end
Компилируем, кидаем «lua5*.dll» в папку проекта, запускаем, и Оппа!
В консоле вывело «Lua inited!»
По сути мы написали простой Lua — интерпретатор.
В второй части приступим к выводу графики.
Доброго времени суток, Хабр! Сегодня я покажу каким образом сделать простенький real-time 2D шутер от третьего лица на node.js и phaser.js.
Сразу оговорюсь, что статья не претендует на лучшую статью хабра и рунета, а представленный код служит лишь наглядным примером, а не примером идеального кода. Просто, к сожалению, о использовании связки node.js и phaser.js есть только англоязычные статьи и хочется, по возможности, это исправить.
Итак, писать мы будем простой 2d шутер с видом от третьего лица. В дизайн примера никто особо силы не вкладывал, поэтому в конечно итоге получится, что-то в таком духе:
Вот список технологий, какие мы будем использовать:
— node.js;
— express;
— socket.IO;
— phaser.js
В начале серверная часть:
var express = require('express'); //подключаем экспресс
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http); //подключаем socket.IO
var port = process.env.PORT || 3000; //указываем порт на котором у нас будет работать игра
var players = {}; //переменная со всеми игроками
/* При открытии страницы, открываем новое подключение для обмена сокетами*/
io.on("connection", function(socket) {
console.log('an user connected ' + socket.id); //выводим в консоль node.js инфу о том, что подключился новый пользователь
players[socket.id] = {
"x": Math.floor(Math.random(1) * 2000),
"y": Math.floor(Math.random(1) * 2000),
"live": true,
}; //генерирует параметры нового игрока
io.sockets.emit('add_player', JSON.stringify({
"id": socket.id,
"player": players[socket.id]
})); //создаем нового игрока на клиенте
socket.emit('add_players', JSON.stringify(players));// создаем нового игрока на клиенте(если игроков более одного)
socket.on('player_rotation', function(data) {
io.sockets.emit('player_rotation_update', JSON.stringify({
"id": socket.id,
"value": data
}));
}); // пакет получающий данные о направлении игрока и отправляющий их на клиент для синхронного отображения для всех пользователей
socket.on('player_move', function(data) {
data = JSON.parse(data); //получаем данные какая кнопка нажата
data.x = 0; //создаем новое свойство объекта x и заодно сбрасываем его до нуля, при каждом обновлении сокета
data.y = 0; // аналогично предыдущему, только ось y
/*передаем различные параметры в зависимости от нажатой кнопки*/
switch (data.character) {
case "W":
data.y = -5;
players[data.id].y -= 5;
break;
case "S":
data.y = 5;
players[data.id].y += 5;
break;
case "A":
data.x = -5;
players[data.id].x -= 5;
break;
case "D":
data.x = 5;
players[data.id].x += 5;
break;
}
io.sockets.emit('player_position_update', JSON.stringify(data)); //отправляем данные на клиент
});
socket.on('shots_fired', function(id) { //получаем id стреляющего
io.sockets.emit("player_fire_add", id); //отправляем на клиент вызов функции выстрелов от конкретного пользователся
});
socket.on('player_killed', function (victimId) { //функция при попадания пули в игрока
io.sockets.emit('clean_dead_player', victimId); //чистим поле от проигравших
players[victimId].live = false; //заканчиваем выполнение ряда функций для проигравшего игрока
io.sockets.connected[victimId].emit('gameOver', 'Game Over'); //выводим пользователю в которого попали информацию о проиграше
});
socket.on('disconnect', function() { //убираем с поля отсоединившихся игроков
console.log("an user disconnected " + socket.id);
delete players[socket.id];
io.sockets.emit('player_disconnect', socket.id);
});
});
app.use("/", express.static(__dirname + "/public")); //пути к файлам клиента
app.get("/", function(req, res) {
res.sendFile(__dirname + "/public/index.html"); // главная страница
});
http.listen(port, function() {
console.log('listening on *:' + port); // запуск сервера
});
Теперь клиентская часть:
var width = window.innerWidth; //получаем ширину монитора
var height = window.innerHeight; //получаем высоту монитора
var game = new Phaser.Game(width, height, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create, update: update, render: render }); //создаем игровое поле с высотой и шириной экрана пользователя;
/*инициализируем все наши переменные */
var player;
var socket, players = {};
var map;
var map_size = 2000; //размер карты
var style = { font: "80px Arial", fill: "white" }; //стили для надписи "Game Over"
var text;
var bullets;
var fireRate = 100;
var nextFire = 0;
var balls;
var ball;
var player_speed = 400; //скорость движения игрока
var live;
var moveBullets;
/*предзагружаем графические элементы*/
function preload() {
game.load.image('unit', 'img/unit.png');
game.load.image('bullet', 'img/bullet.png');
game.load.image('killer', 'img/killers.png');
game.load.image('map', 'img/grid.png');
}
function create() {
socket = io.connect(window.location.host); //подключаем сокеты
game.physics.startSystem(Phaser.Physics.ARCADE);
game.time.advancedTiming = true;
game.time.desiredFps = 60;
game.time.slowMotion = 0;
bg = game.add.tileSprite(0, 0, map_size, map_size, 'map'); //спрайт карты
game.world.setBounds(0, 0, map_size, map_size); //размеры карты
game.stage.backgroundColor = "#242424"; //цвет фона
socket.on("add_players", function(data) {
data = JSON.parse(data);
for (let playerId in data) {
if (players[playerId] == null && data[playerId].live) {
addPlayer(playerId, data[playerId].x, data[playerId].y, data[playerId].name);
}
}
live = true;
}); //создаем игроков
socket.on("add_player", function(data) {
data = JSON.parse(data);
if (data.player.live) {
addPlayer(data.id, data.player.x, data.player.y, data.player.name);
}
}); //создаем игрока
socket.on("player_rotation_update", function(data) {
data = JSON.parse(data);
players[data.id].player.rotation = data.value;
}); //вращение вокруг своей оси, ориентируясь на курсор
socket.on("player_position_update", function(data) {
data = JSON.parse(data);
players[socket.id].player.body.velocity.x = 0;
players[socket.id].player.body.velocity.y = 0;
players[data.id].player.x += data.x;
players[data.id].player.y += data.y;
}); //обновляем положение игроков
socket.on('player_fire_add', function(id) {
players[id].weapon.fire();
}); //исполняем выстрелы
game.input.onDown.add(function() {
socket.emit("shots_fired", socket.id);
}); //вызываем выстрелы
socket.on('clean_dead_player', function(victimId) {
if (victimId == socket.id) {
live = false;
}
socket.on("gameOver", function(data){
text = game.add.text(width / 2, height / 2, data, { font: "32px Arial", fill: "#ffffff", align: "center" });
text.fixedToCamera = true;
text.anchor.setTo(.5, .5);
});
players[victimId].player.kill();
}); //чистим поле от игроков в которых попали и выводим им текст о проигрыш
socket.on('player_disconnect', function(id) {
players[id].player.kill();
}); //чистим поле от отключившихся игроков
keybord = game.input.keyboard.createCursorKeys(); //инициализируем клавиатуру
}
function update() {
if (live == true) {
players[socket.id].player.rotation = game.physics.arcade.angleToPointer(players[socket.id].player); //вращаем игрока в сторону курсора
socket.emit("player_rotation", players[socket.id].player.rotation); //отправляем на сервер данные о вращени
setCollisions(); //функция вызывающаяся при столкновении пули с игроком
characterController(); //управление игроком
}
}
function bulletHitHandler(player, bullet) {
socket.emit("player_killed", player.id);
bullet.destroy(); // убираем пулю с поля, после попадания
} //функция при столкновении пули с игроком
function setCollisions() {
for (let x in players) {
for (let y in players) {
if (x != y) {
game.physics.arcade.collide(players[x].weapon.bullets, players[y].player, bulletHitHandler, null, this);
}
}
}
} //Проверка столкновения игрока с пулей
function sendPosition(character) {
socket.emit("player_move", JSON.stringify({
"id": socket.id,
"character": character
}));
} //отправляем инфу о том, куда игрок двинулся на сервер
function characterController() {
if (game.input.keyboard.isDown(Phaser.Keyboard.A) || keybord.left.isDown) {
//players[socket.id].player.x -= 5;
sendPosition("A");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.D) || keybord.right.isDown) {
//players[socket.id].player.x += 5;
sendPosition("D");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.W) || keybord.up.isDown) {
//players[socket.id].player.y -= 5;
sendPosition("W");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.S) || keybord.down.isDown) {
//players[socket.id].player.y += 5;
sendPosition("S");
}
} //управление
function render() {
game.debug.cameraInfo(game.camera, 32, 32);
}
function addPlayer(playerId, x, y) {
player = game.add.sprite(x, y, "unit");
game.physics.arcade.enable(player);
player.smoothed = false;
player.anchor.setTo(0.5, 0.5);
player.scale.set(.8);
player.body.collideWorldBounds = true; //границы страницы
player.id = playerId;
let weapon = game.add.weapon(30, 'bullet'); //подключаем возможность выстрелов
weapon.bulletKillType = Phaser.Weapon.KILL_WORLD_BOUNDS;
weapon.bulletSpeed = 600; //скорость выстрелов
weapon.fireRate = 100;
weapon.trackSprite(player, 0, 0, true);
players[playerId] = { player, weapon };
game.camera.follow(players[socket.id].player, ); //слежение за игроком который открыл страницу
} //создаем игрока и даем ему ствол :)
В целом, все написано не очень оптимизировано и вероятнее всего игра не будет быстро работать при большом количестве игроков, но геймплей игры как бы сам просит возможность создавать отдельные комнаты для разных игроков.
Ещё раз повторюсь, что статья несет ознакомительный характер и не стремится претендовать в топ лучших статьей гейм-разработки.
До 2014 года на серверах FirstVDS мы использовали промышленные HDD-накопители с
SAS-интерфейсом и аппаратными контроллерами, собранные в RAID 10. Это решение полностью устраивало нас в плане надёжности и производительности. Проблемы с частичной потерей клиентских данных были 3 раза за 12 лет использования. Два раза выгорали аппаратные контроллеры. Один раз вышла из строя батарейка и при аварийном отключении питания встроенная кеш-память рейда очистилась.
Однако SAS HDD дорогие. Для одного сервера мы брали комплект из 4 дисков по 600 Гб, аппаратного RAID-контроллера с батарейкой. Всё решение обходилось в 44 806 руб. за 1 Тб. Повышать цены на VDS мы не хотели. Нужно было найти более дешёвое решение, при этом не потерять в скорости и надёжности. А в идеале и увеличить предоставляемое для VDS место.
Только SSD — ещё дороже. На тот момент диски по 240 Гб стоили от 8000 руб. Дешевле было остаться на Raid 10 SAS, чем использовать SSD суммарным объёмом в 1 Тб. А увеличить хранилище и того дороже. Поэтому мы рассмотрели несколько программных решений и включили SSD в тесты, чтобы сравнить скорость. Таблица с результатами ниже.
Альтернативные решения
zfs — файловая система и менеджер логических разделов с адаптивным замещающим кешем, разработанная компанией Sun Microsystems. Zfs нельзя включить в оригинальную версию ядра Linux из-за несовместимости лицензий (CDDL vs GPL). Систему можно прикрутить DKMS-модулями, но усилия не стоят того – судя по публичным тестам скорость записи/чтения была невысока. Тестировать сами не стали.
bcache — разработка Google, в 2013 году была ещё сырой — не использовалась в продакшене. Работала только с CentOS 7, а мы использовали CentOS 6. Bcache тоже не стали тестировать.
lvm cache — технология Linux сообщества. Тоже работала только с CentOS 7, но публичных тестов на тот момент не было — решили провести сами. Цифры не понравились.
flashcache — разработан Facebook: компания внушает доверие, и технология уже была проверена в продакшене.
Flashcache работает в 3 режимах:
Write through — данные сначала пишутся на диск, а потом сбрасываются в кеш. Кешируется только запись.
Write back — данные сначала пишутся в кеш, потом сбрасываются на диск. Кешируется запись и чтение.
Write around — данные пишутся на диск, а в кеш попадают после первого чтения. Кешируется только чтение.
Так как write back — самый быстрый режим, выбрали для тестов его.
MD — software raid. Flashcache работает в паре с MD и Raid 1. Мы включили в тестирование MD без Flashcache, чтобы проверить, как он работает отдельно.
Итоги тестирования
Чтобы максимально приблизить условия исследования к реальным, запустили рандомную запись и чтение в файл 32 Гб (примонтированную файловую систему).
Параметр
raid10 sas
SSD
MD
flashcache write back
глубина очереди
32
32
32
32
IOPSread
1 401
51 460
598
6 124
IOPSwrite
999
23 082
230
3 205
скорость чтения
5 607 Kb/s
205 842 Kb/s
2 393 Kb/s
24 496 Kb/s
скорость записи
3 998 Kb/s
92 329 Kb/s
922 Kb/s
12 823 Kb/s
Flashcache в режиме writeback обошёл lvmcache и обогнал software raid. Сильно проиграл дорогим SSD, но главное, flashcache превзошёл наше решение на SAS HDD.
Новое решение с flashcache
По результатам исследования в январе 2014 года мы внедрили flashcache на SSD + SATA HDD.
С тех пор на одном сервере стоит 1 SSD и 2 SATA HDD по 4ТБ в зеркале. Технология работает в режиме writeback: быстро записывает данные в кеш и медленно скидывает на основной носитель.
При внедрении и обслуживании flashcache мы столкнулись с некоторыми особенностями технологии.
Особенности flashcache
1) SSD изнашивается
Из-за превышенного количества записей/перезаписей SSD перестаёт записывать новые данные. Чтобы этого не произошло мы мониторим SMART-атрибуты:
Media_Wearout_Indicator – это время жизни или износ диска: значение для нового диска – 100, со временем оно уменьшается. Минимально допустимое – 10, при достижении этого значения диск становится пригодным только для чтения.
Reallocated_Sector_Count – количество переназначенных секторов – должно быть меньше 100.
Программа мониторинга следит за этими значениями в автоматическом режиме и уведомляет сотрудников о проблемных дисках. Нам остаётся только вовремя их менять.
Раньше мы использовали диски 240 Гб, они работали меньше года. Сейчас технология over-provisioning позволяет нам увеличить резервную область диска и за счёт этого продлить срок жизни SSD. Диск объёмом 1 Тб мы режем до 240 Гб, это рабочая область, остальные 760 Гб – резерв на износ. Сейчас SSD в среднем работает 1 год.
2) Сбои, когда сгорает SSD и теряются несинхронизированные (грязные) данные
В режиме writeback данные сначала попадают в кеш SSD и только потом в память SATA HDD. Данные, которые не успели скинуться на SATA HDD, называются грязными. При сбое они безвозвратно сгорают вместе с SSD. При экстренном отключении питания SSD тоже может выйти из строя с потерей данных.
К счастью, сбои происходят не так часто. За 2,5 года у нас произошло два случая с потерей клиентских данных, которые не успели записаться в хранилище.
Уменьшить количество сбоев можно двумя способами:
Использовать качественные серверные SSD. Что мы и делаем – покупаем диски Intel, Hitachi, Toshiba и др.
Настроить репликацию кеша (зеркальный рейд). Решение предусматривает установку второго SSD, но из-за редких сбоев деньги на него мы зажали.
3) Долго чистить кеш
Поменять SSD и настроить flashcache – 5 минут. Но перед этим нужно очистить кеш – скинуть все грязные данные на диски.
В среднем у нас 30% грязных данных на SSD, максимум – 70%. Очистка кеша занимает до 4 часов.
В это время система работает медленнее, потому что обращается к медленным носителям. Мы всегда предупреждаем клиентов о падении скорости, но форсировать процесс не можем. Скорость записи на SATA HDD зависит от того, насколько интенсивно клиенты используют диск. Чем интенсивнее используют, тем больше нагрузка и медленнее скорость записи.
4) Кеш может переполниться
Часто используемые данные находятся в кеше и называются горячими. На наших серверах их примерно 13%, максимум 62%. Такого объёма достаточно для быстрого чтения/записи всех VDS на сервере. Но переполнить кеш и снизить производительность может недоверие всего одного клиента.
Допустим, клиент захочет протестировать дисковую подсистему. Запустит программу рандомной записи файлов. Если диск клиента по объёму больше кеша, все плохо. Кеш переполнится и всё скатится в низкую производительность. Пострадают все VDS на сервере.
Если вздумаете провести такой тест, не ждите актуальных результатов. Мы программно ограничиваем нарушителю количество обращений на диск, это снижает скорость.
5) Flashcache не работает на Centos 7
После обновления ядра flashcache стал несовместим с Centos 7. Так как эта версия дистрибутива стоит на 50% наших серверов, проблема острая. Сейчас Centos 7 используется с sw raid1 с SSD. На трёх кластерах мы тестируем enhanceio — другую технологию кеширования — но пока не готовы озвучить результаты.
Результаты внедрения flashcache
Вычислим, насколько flashcache на SSD+SATA HDD выгоднее RAID 10 SAS. Для этого рассчитаем стоимость каждого решения.
RAID 10 SAS
примерные цены за март 2013 г.
SAS 600 Гб, 4 шт.
7714 руб. x 4
аппаратный контроллер + батарейка
8600 руб. + 4500 руб.
кабель
850 руб.
= 44806 руб. или 1493 $ ( при курсе 1$=30 руб.)
— стоимость 1 Тб места на родительском сервере
SATA HDD + SSD
цены на май 2017 г.
SATA HDD 4 Тб, 2 шт.
12 000 руб. x 2
SSD
17 100 руб.
= 41 100 руб. или 685 $ ( при курсе 1$=60 руб.)
С 2013 года доллар подорожал в 2 раза. Поэтому решение с flashcache в рублях стоит почти также, как RAID 10 SAS, а в долларах в 2 раза дешевле.
Увеличив объём хранилища в 4 раза, мы сократили цену 1 Тб. Теперь он дешевле в 4 раза в рублях и в 8 раз в долларах.
Вывод
В 2014 году мы внедрили flashcache — увеличили предоставляемое для VDS место в 4 раза, и повысили скорость взаимодействия с дисковой подсистемой. Это решение вышло дешевле предыдущего, позволило нам снизить затраты и не повышать цены на VDS.
Под вопросом осталась надёжность, всё-таки с HW RAID 10 SAS было меньше сбоев. В мае 2015 для людей, которым принципиально важна надёжность и скорость мы ввели тарифы с SSD в качестве основного носителя.
Мы продолжаем делиться открытыми трансляциями для желающих принять субботний поток силы! Ранее мы уже открывали трансляции с конференций DotNext 2017 Piter, Mobius 2017 Piter и JPoint 2017 (сейчас доступ к ней закрыт). В этот раз источник силы будет подпитывать JavaScript-разработчиков.
3 июня 2017 в 10 утра (по московскому времени) начнется бесплатная онлайн-трансляция из главного зала HolyJS 2017 Piter!
Перед разработчиками очень часто стоит непростая задача выбрать язык программирования для разработки клиентской части приложения. Как правило: выбор стоит между тремя китами: JavaScript, TypeScript и Dart. Легкий battle технологий без глубокого погружения. Алексей знает о чем говорит, за 10 лет JS-разработки он успел попробовать многое.
Этот (второй по счету) доклад Дугласа Крокфорда на конференции HolyJS 2017 Piter будет выдержан в стиле «два шага вперед, три шага назад, или почему надо знать путь развития технологии». Разбираться будем с противоречиями в языковом дизайне, начиная с письма Дийкстры к редактору.
И да, как искренне любящий JS и находящий в нём хорошие стороны, Дуглас не будет церемониться. Готовтесь к тому, что уроки силы и уроки танцев будут с Дугласом по полной программе.
Анджана посвятила свой доклад функциональному программированию на JS с использованием базовых JS-фич и некоторых популярных FP-библиотек, таких, как Mori и Ramdа. В свободное от выступлений время Anjana уже прошла путь от философии к преподаванию английского языка и от прикладной лингвистики к разработке ПО. Поэтому она сможет доходчиво дать ответы на вопросы о том, как выглядит функциональный код и чем он лучше других; как начать писать в стиле функционального программирования; откуда такой ажиотаж, а главное — зачем?
Webpack стал де-факто стандартом для сборки крупных приложений на JS. Его используют почти все, но, как правило, как черный ящик: если положить вот сюда файлы и написать такие-то строки в конфиг, то потом на выходе автоматически получится бандл. Опыт с проектами для eBay, Яндекса и Communigate позволил понять, как выглядит бандл изнутри, как разные настройки на него влияют, почему некоторые настройки могут привести к неожиданным сайдэффектам, а также как все это отладить и оптимизировать. Этим и будет делиться Алексей в своём докладе.
Модульные системы подразумевают, что все модули будут связываться между собой в единое целое, при этом модуль — это только один компонент, все компоненты находятся в зависимости друг от друга, и их надо выражать.
Зависимости между компонентами тянут за собой множество проблем: хардкодинг, сложность рефакторинга и прочие неприятности.
В своем докладе Владимир Гриненко, будучи руководителем группы общих компонентов интерфейсов в симферопольском офисе Яндекса, покажет способ, как избавиться если не от всех неприятностей, то от многих. В докладе будет рассказано, как применить новый подход на примере сборки на Gulp и Webpack. А также о пакете, который не только позволяет собирать таким образом проекты на React, но и обеспечивает множественное наследование для React-компонентов.
Lea Verou — автор книги «CSS Secrets» и один из экспертов CSS Working Group. Пока кто-то делит людей на «разработчиков-технарей» и «дизайнеров-гуманитариев», Лия известна своей любовью и к коду, и к дизайну, что она и реализовала на практике в нескольких open source проектах (Prism, Dabblet и -prefix-free)
Не удивительно, что кейноут «JS UX: Writing code for humans» как раз будет связан с этим пересечением: Лиа расскажет о том, как применение UX-подходов к программированию может сделать ваш код лучше. В конце концов, код, который кому-то надо читать (включая вас самих в будущем) — это тоже своего рода UI!
Трансляция в перерывах
Проблема многих онлайн-трансляций – пустые перерывы и кофе-брейки. Пока участники на конференции пьют кофе и общаются со спикерами, зрители трансляции вынуждены смотреть на заглушки и ждать начала следующего доклада.
Мы решили эту проблему по-своему – в перерывах будут транслироваться события, происходящие на конференции, а также интервью со спикерами. Вести интервью будут ARG89 вместе с phillennium — скучать вам не придется. Вопросы, если таковые вдруг возникнут, можно будет задать в Telegram-канале конференции: t.me/holyjsconf
Ограничения
Поскольку трансляция бесплатная, она предоставляется по принципу as is: мы уверены, что все будет хорошо, но если вдруг что – не обессудьте!
Видеозаписей не будет. То есть они, конечно, будут, но только для участников конференции, оставивших фидбек. А для всех остальных мы традиционно выложим их через 3-4 месяца.
Вы не сможете смотреть, что происходит в других залах. А там будет много интересного. В следующий раз регистрируйтесь и смотрите все без ограничений.
Всем привет! Не так давно мне поступила задача встроить визуальный редактор email в наш сервис внутренней рассылки, ибо людям надоело набирать html руками и компоновать валидные шаблоны для писем. Побродив по интернету, я нашёл 2 редактора, которые, как мне тогда казалось, прекрасно подойдут для этих целей. Ссылки на них приведу в конце топика. Изучив их более внимательно (EmailEditor написан с использованием jQuery, который я в своё время неплохо изучил, а Mosaico был на KnockoutJS, с ним я знаком лишь поверхностно), я остановился на EmailEditor, и снова окунулся в то дерьмо из которого год назад так успешно выбрался с помощью Angular и Ionic, а именно — файлы по 2-3к строк, повсеместное и рандомное изменение DOM различными способами из различных мест и т.д., ну вы меня понимаете).
Потратив больше месяца на попытки пофиксить все баги, запилить нужные нам для рассылки строительные блоки и прочее, я сдался… Решил попробовать Mosaico и даже начал активно изучать Knockout, но проблема в том, что этот монстр (я про Mosaico) был настолько сложно написан, что EmailEditor показался не таким уж и плохим. Плюс ко всему, а точнее минус, у Mosaico практически нет вменяемой документации и если в первом я интуитивно понимал как всё работает и как создать свои блоки, то тут никакая интуиция мне не помогла. Возможно, просто не хватило мозга, терпения и желания разбираться, не знаю, просто гляньте на досуге исходники этих редакторов… А сроки поджимали...
Что же делать?!
спросил я себя, и сам же себе ответил "Конечно же, изобретать велосипед! С золотой цепью и малиновыми колёсами!". Так получилось, что как раз в этот момент для одного из своих pet-projects мне нужно было приступить к изучению популярного на сегодняшний день React+Redux подхода к построению веб приложений. Прочитав про Redux, меня осенило! Вот же оно! Состояние приложения в одном месте — это ли не лучший вариант, чтобы строить архитектуру, в которой будет меняться JSON представление шаблона письма! И я начал писать… После пары недель бессонных ночей, начальству был презентован прототип и решено попробовать внедрить мой редактор. По репозиторию может быть заметно, что в самом начале мне трудно было определиться со структурой шаблона и принципами работы, но по мере изучения, пробуя разные подходы, решил не усложнять и таки пришёл к тому, что есть сейчас, а именно:
template — шаблон письма с блоками, где каждый блок содержит:
id — идентификатор блока (использую Date.now() и не парюсь);
block_type — тип блока (далее поясню для чего);
options — стили и свойства для контейнера и элементов блока, в котором:
container — объект который потом напрямую подставляется в style блока, т.е. это CSS стили;
elements — CSS стили для элементов блока и параметры типа source, text (я решил смешать их в кучу);
common — общие CSS настройки для блоков;
components — доступные для добавления блоками;
tabs — настройка видимости вкладок;
tinymce_config — общая настройка TinyMCE для использующих его блоков;
language — локализация приложения;
templateId — ID шаблона для сохранения\загрузки;
Вот и весь store.
Обзор работы с редактором
C чего бы начать… Здесь и далее я предполагаю что у вас установлены NodeJS, npm, и, желательно, MongoDB, а также, что у вас есть небольшой опыт работы как с ними, так и с React+Redux стеком. Запуск live development простой, поскольку проект пишется с использованием create-react-app. Так что, после того как скопируете репозиторий, просто выполните:
npm install
npm start
в папке проекта и в вашем браузере откроется адрес http://localhost:3000, где вы увидите примерно такую картину:
Смотреть
Из доступных локалей пока поддерживаются только en и ru, загрузка происходит напрямую из JSON файла в папке translations и, к сожалению, я пока не написал проверку того, доступна ли пользовательская локаль, чтобы подставить по дефолту, но это мелочи, это потом… Точка входа приложения — index.js в корне src/, там задаётся первоначальный store, и диспатчатся три action'а, чтобы загрузить локаль, список блоков и шаблон взятый по ID из вашего хранилища, либо, если ID не указан, — шаблон по умолчанию. Поскольку первоначально происходит запуск без каких-либо параметров, всё будет загружено из локальных файлов, настройка сервера на данном этапе не требуется (но понадобится для методов сохранения\загрузки шаблона, загрузки изображения и отправки тестового письма).
Интерфейс до жути простой — слева панель настроек и блоков, по центру — шаблон письма, по бокам от шаблона — кнопки. Блоки можно перетаскивать на шаблон (они добавляются как бы поверх целевого блока, смещая всё вниз), при наведении на целевой блок он меняет цвет. Тут я думаю о том, чтобы реализовать "фантомный блок", как в некоторых других редакторах, но это не приоритетная задача. При клике на блок активируется вкладка, в которой содержатся настройки для выделенного блока, и этот блок подсвечивается, а также появляется кнопка удаления блока, что видно на скриншоте:
Смотреть
Ну а если выбрать вкладку общих настроек, вы увидите набор настроек, которые будут применяться ко всем блокам, кроме тех, у которых стоит флаг Custom style. Также там есть возможность задать фон контейнера шаблона:
Смотреть
Клики на кнопках позволяют сохранить шаблон (вас попросят задать имя шаблона, но это легко выпиливается), отправить тестовое письмо и удалить блок (в планах также реализовать Undo\Redo функционал, сейчас читаю об этом)
Вы также можете запустить и поиграться с NodeJS сервером (он в папке server_nodejs), предварительно скопировав туда папку build которая появится если вы сделаете npm run build в основной папке проекта (не забудьте выполнить npm install в обеих папках!). Что умеет сервер: сохраняет\выдаёт шаблон(?id=ваш_id) и загружает изображения, а также говорит 'OK' при отправке тестового письма =). Думаю, разобраться не составит труда, структура проекта довольно простая, я вообще не люблю усложнять… Точка входа — app.js, в папке app есть Controller — там всё поведение, Router — прописаны пути и связаны с контроллером, и TemplateModel — ORM для шаблона.
Немного внутренностей
В папке src/components есть подпапки blocks и options в которых лежат шаблоны блоков и настроек этих блоков.
также в папке src/components есть файл Block.js, в котором подключены все блоки из blocks и switch...case, в котором по block_type (который я упоминал выше) определяется какой вариант блока будет возвращён.
Такой же принцип и в файле Options.js для настроек. И вот от этой архитектуры мне хотелось бы уйти как можно скорее (может у кого-то есть мысли в какую сторону осуществить переход?). В файле BlockList.js содержится шаблон письма, в котором видно, как всё устроено — в цикле строятся tr>td элементы, и td в данном случае является контейнером внутри которого уже размещается блок с элементами. Тут же подхватываются и настройки контейнера (стили из block.options.container), а также реализована DnD логика. В настройке тоже всё достаточно прозрачно, на инпуты навешаны обработчики onChange, внутри которых вызывается onPropChange(prop, value, container?, element_index) с параметрами ('свойство для изменения, например, color', новое значение свойства, элемент для изменения (контейнер — true, элемент — false), индекс элемента). В принципе это основная идея и больше рассказывать нечего =). На mindmap'е я постарался схематично изобразить работу этого конвейера:
Смотреть
P.S. В репозитории две ветки — master и react_email_editor_wordpress. В принципе особых отличий нет, различия в файлах sagas/api.js (у WP свой подход к AJAX), блоках типа feedback и social (там пути к картинкам другие… WP жеж). Редактор у нас интегрирован в WP и на данный момент тестируется.
Так как же сделать свой блок?
Очень просто! Ну мне так кажется, потому что я с этим работал плотно и каждодневно…
Начну с выбора типа блока. Бродя по интернету, я наткнулся на один симпатичный шаблон:
Смотреть
Мне понравился блок с тремя пиктограммами WEBSITES, SERVICES, SEO. Что-ж, попробую рассказать как же реализовать такой блок. Для начала давайте определимся с составом блока. Я вижу тут 6 элементов: 3 картинки и 3 текстовых элемента, ну а вы можете впоследствии запрограммировать своё видение этого блока. Поскольку я старался сделать как можно более гибкую настройку, вы вольны придумать практически любую компоновку (например 3 элемента картинка-текст), и это будет вполне реально осуществить. Довольно слов, go кодить!
Откройте файл public/components.json и добавьте следующий JSON:
Таким образом, мы определили блок типа 3_icons с превью images/3_icons.png, контейнером и шестью элементами. У них уже есть какая-то базовая настройка стилей, чтобы при добавлении смотрелось более-менее прилично. Ок, далее открываем GIMP (если установлен) и в нём открываем файл preview_template.xcf, который лежит в корне проекта. Эту заготовку я сделал для того, чтобы клепать превью блоков. Путём нехитрых манипуляций (Cut\Paste\Colorize) из исходного изображения шаблона получим превью для будущего блока:
Сохраните его в папку src/images (или public/images, а лучше в оба места) и обновите страницу с редактором. Вы увидите, что новый блок добавился на позицию, на которой вы его вставили в components.json
У меня он, например, воткнут после HEADER
Теперь создадим шаблон блока. Добавьте новый файл Block3Icons.js в папку src/components/blocks:
Как видно, блок простейший — 2 строки 3 столбца. Из настроек для элементов я пока сделал доступными только source для элементов изображений и text для текстовых элементов, стили контейнера применяются в файле BlockList.js, о котором я упоминал выше по тексту.
Пора создать настройку блока. Добавьте новый файл Options3Icons.js в папке src/components/options:
Options3Icons.js
import React from 'react';
const Options3Icons = ({ block, language, onFileChange, onPropChange }) => {
let textIndex = 3;
let imageIndex = 0;
return (
Отлично! Почти готово! Надеюсь, в том, что мы тут уже создали, вы хоть немного ориентируетесь? В блоке всё тупо (потому что он dumb component, т.е. рендерится только на основе своих props). В настройках каждому элементу ввода (checkbox, input, etc...) сопоставлен обработчик, в котором вызывается onPropChange для свойств (про это я тоже упоминал выше). На основе этих свойств блок динамически отрисовывается заново. Всё просто. Давайте теперь применим результаты трудов и посмотрим, наконец, работает ли это всё вообще =).
Для этого надо добавить в файл src/components/Block.js импорт нового блока и условие для его возвращения:
//...тут другие import'ы...
import Block3Icons from './blocks/Block3Icons';
//...тут тоже...
//...тут другие case'ы...
case '3_icons':
return ;
//...и тут тоже...
Почти то же самое проделайте в файле src/containers/Options.js
//...тут другие import'ы...
import Options3Icons from '../components/options/Options3Icons';
//...тут тоже...
//...тут другие case'ы...
case '3_icons':
return ;
//...и тут тоже...
Теперь сохраняем все файлы, и, если вы ранее запускали npm start в корне проекта, у вас должно всё скомпилироваться без ошибок. Перетащите ваш новый блок на шаблон, выделите его и поиграйтесь с его настройками. Вот пример, как это выглядит у меня:
Посмотреть
Итого
Я старался сделать редактор как можно более простым в использовании и достаточно удобным в плане интерфейса, а вышло ли у меня это или нет — решать конечно же вам. На мой взгляд получился редактор с низким порогом входа в плане внедрения и расширения компонентной базы в противовес Mosaico. Также у него гораздо более прозрачная (опять же по сравнению с Mosaico) и менее забагованная (по сравнению с EmailEditor'ом) реализация, которая легко настраивается, расширяется и переписывается под свои нужды буквально за часы (реже — дни).
В планах продолжить вести работу над следующими пунктами:
стили для responsive template;
внедрение Undo\Redo функционала;
сделать симпатичный дизайн (сам не могу, потребуется помощь...);
обособить TinyMCE чтобы не впиливать его в блоки с текст элементами;
использовать что-то типа styled-components для интерфейса редактора;
доработка NodeJS сервера. Сделаю перенос локалей и прочего из файлов в БД;
перенос блоков и настроек из проекта в хранилище, избавление от switch...case;
возможно сделаю что-то типа превью шаблона (но только после responsive);
возможно сделаю "Показать исходный код" для serverless выгрузки шаблона;
возможно сделаю упомянутый в статье "фантомный блок";
активная работа с issues и proposals конечно же =);
что-то ещё, если вспомню, допишу… да и вы пишите
Буду рад помощи, советам, критике, любому фидбеку. На основе этого решу продолжать ли заниматься проектом =).
На этом пока всё… Спасибо за внимание! В дальнейшем буду писать только об очень крупных изменениях, если, конечно, проект окажется кому-нибудь полезен.
TLDR.jpg (Заметьте: эта огромная простыня текста нужна всего лишь для выбора пола персонажа.)
«Слишком длинно. Вырежьте половину».
«Какую половину?»
«Ту, которая лишняя». Их звёздные полтора часа
Вся моя рабта связана с созданием сюжетно богатых игр со множеством текста. В нашей компании Spiderweb Software мало сотрудников. Мы не можем позволить себе потрясающую графику, поэтому полагаемся на слова. На интересные, качественно написанные слова.
Сейчас мы работаем над ремастерингом нашей серии с самым любимым сюжетом и наилучшайшими текстами. Также мы завершили новую серию, в которой тоже было много слов. Подозреваю, что она была не так хороша, потому что продажи оказались плохими. Сейчас мы планируем начать совершенно новую серию, и нам нужно понять, какие объём и содержание текстов в ней должны быть.
Нам нужно принять множество решений, поэтому я много думал о текстах в играх, и пришёл к некоторым выводам.
Для справки
Роман приличного размера содержит примерно 100 тысяч слов. В Библии около миллиона слов.
В самой разговорчивой и популярной игре Avernum 3, над ремастерингом которой я сейчас работаю, примерно 200 тысяч слов. После её выпуска люди говорили, что в ней очень-очень-очень много текста. Однако по современным стандартам она предельно лаконична.
Для сравнения, в одной из лучших с точки зрения подачи сюжета RPG последнего времени The Witcher 3 около 450 тысяч слов. Для The Witcher 3 «лучшая подача сюжета» означает, что в ней одна очень хорошая сюжетная линия и множество других, вполне неплохих сюжетных линий. (если честно, я считаю, что дополнение Heart of Stone написано действительно хорошо.)
Раздувание текстов продолжается. В Divinity: Original Sin было всего-то 350 тысяч слов. В Tyranny — уже 600 тысяч слов, рассказывающих о том, как вы становитесь самым злобным в мире менеджером среднего звена, пытаясь разобраться во всех 73 фракциях игры.
И это ещё относительно мало по сравнению с 1,2 миллиона слов Torment: Tides of Numenara. Признаюсь, мне любопытно, как может быть история настолько огромной и эпичной, что в три раза превзошла по объёму текста «Властелина Колец». Но я никогда этого не узнаю, потому что ничто не сможет убедить меня сыграть в игру, в которой 1,2 Библий текста.
Это я играю в RPG, лол.
Законы сторителлинга в видеоиграх по Фогелю
Игроки простят вашу игру за хорошую историю при условии, что вы позволите без неё обойтись.
Когда говорят, что в видеоигре «хорошая история», это значит, что история там есть.
История почти всех видеоигр звучит так: «Видишь этого парня? Он — плохой. Убей его». Такое никогда не приводит к хорошей истории.
К примеру, вы можете заставить читать текст в вашей RPG вот так.
Наблюдения о текстах в видеоиграх
Долгое время существовал большой спрос на такие игры, как Baldur's Gate и Planescape: Torment. То есть на олдскульные RPG с объёмной историей. Такие хиты, как Divinity: Original Sin и Pillars of Eternity, заработали кучу денег на этом спросе. Продажи новых игр в этом стиле, таких как Tyranny и Torment: Tides of Numenara показывают, что спрос ужеудовлетворён.
Писать слова очень легко. Очень-очень-очень легко. Любой писатель с крупицей навыка может как нечего делать написать 500 тысяч. Когда пальцы писателя устанут, жаждущий стать автором стажёр добавит ещё 100 тысяч. А когда и стажёр выдохнется, то можно позволить бэкерам на Kickstarter дописывать тексты к игре, и они будут платить за эту привилегию.
Нет, действительно, задумайтесь о последнем примере. Люди готовы платить, чтобы писать за вас игру! Добавление слов в игру имеет отрицательную стоимость! Задумайтесь об этом в следующий раз, когда кто-нибудь попробует продать вам игру, делая упор на огромное количество текстов.
Секрет хорошего писательства не в добавлении слов, а в их отбрасывании. Почти всегда можно улучшить сценарий, вырезав фрагменты и отполировав оставшееся. Однако из-за ограниченных бюджета и времени при разработке игры такой редактуры почти никогда не бывает.
Когда писатель становится знаменитым, его перестают редактировать. Именно поэтому в пятой книге о Гарри Поттере 900 страниц, на которых происходит всего два значимых события. И именно поэтому, когда сюжет для игры 2017 года пишет Известный Писатель, а в сценарии базиллион слов, то б'oльшая часть этих слов будет скучной.
Существуют хорошо прописанные игры. В Fallout: New Vegas и Witcher 3 сюжет очень крепкий. Помню, что Baldur's Gate II и Planescape: Torment тоже были хороши, но я играл в них 20 лет назад, и к воспоминаниям может примешиваться ностальгия. (И у меня, и у других игроков.) Planescape была крутой, но я помню, что просто прощёлкивал кучу текста, чтобы избавиться от него.
Здесь действует закон Старджона: «90 процентов чего угодно — ерунда». На каждую Planescape: Torment с хорошим антуражем, идеей сюжета и качественными текстами, вписанными в геймплей, есть ещё девять других игр, в которые просто запихнули толкиновскую историю в стиле «убей плохого парня», и понадеялись, что она «прокатит». Но не прокатило.
Большой объём лора в игре — это нормально. Некоторым игрокам очень нравится лор. Но в то же время, очень многим он не нравится. Думаю, стоит немного отделить лор от значимого в игре текста, например, как в Skyrim, где он излагается в книгах, которые можно с лёгкой душой пропустить. Окна квестов World of Warcraft идеально справляются с этой задачей. Весь лор содержится в одном куске текста («Так значит, гномы любят копать шахты? ПОТРЯСАЮЩЕ!»), а сам текст квеста («Убить 10 гоблинских младенцев») отделён от него, чтобы игрок мог быстро его переварить.
Хороший юмор писать очень сложно. Но именно его интереснее всего читать. Если вам удастся сделать игру по-настоящему весёлой, то люди будут любить её вечно. (Сам геймплей Psychonauts — на «четвёрку с минусом», но игроки обожают эту игру из-за её юмора.)
Самая важная цель сюжета игры: сделать его достаточно хорошим, чтобы игрок стремился проходить игру для изучения сюжета. Ваш сюжет должен быть НАГРАДОЙ. Если игроку приходится прорываться сквозь сюжет, чтобы добраться до геймплея, значит, текста слишком много.
Теперь я слушаю тебя СО ВСЕМ ВНИМАНИЕМ.
Врачу, исцелися сам!
Во всех играх, для которых я писал, было много текста. Мои фанаты очень полюбили тексты в некоторых из них. В других они не слишком понравились.
Моя цель для следующей серии игр — использовать меньше слов, но сделать их как можно более лёгкими, интересными и забавными. Я хочу, чтобы текст был наградой, тем, что заставляет людей пройти сквозь сюжет. И меня это страшит, потому что, повторюсь, писать что-то хорошее и короткое гораздо дольше, чем скучное и длинное.
Тем временем, я занимаюсь ремастерингом моей старой Avernum 3 с её скромными 200 тысячами слов. Это значит, что тексты подвергнутся редактированию. Много времени у меня уходит на избавление от лишних слов и на переделку оставшегося, чтобы его было легче, а по возможности и забавнее, читать. Если в новой версии будет больше слов, чем в старой, это будет значить, что я где-то ошибся.
Долгое время я продавал игры с большим количеством слов. Теперь в этой области гораздо больше конкуренции, а слова стали очень дешёвыми. Мне нужно попытаться продавать хорошие слова. Даже если у меня никогда не получится создать хорошего, популярного мема на этом конкурентном рынке, всё равно, любое, даже малейшее преимущество сослужит хорошую службу.
Джеф Фогель продаёт свои многословные олдскульные RPG в Spiderweb Software и заставляет себя быть лаконичным в Twitter.
В 2006-м, в 5-й java появился StringBuilder. Более легковесная и разумная альтернатива StringBuffer. Вот, что говорит официальная документация по StringBuffer:
Этот класс дополнен аналогичным классом предназначенным для использования в одном потоке — StringBuilder. В общем случае нужно отдавать предпочтение классу StringBuilder, так как он поддерживает все те же операции, что и этот (StringBuffer), но быстрее, так как не выполняет никаких синхронизаций.
Иметь synchronized в StringBuffer вообще никогда не было хорошей идеей. Основная проблема в том, что одной операции никогда не достаточно. Одиночная конкатенация .append(x) бесполезная без других операций, таких как .append(y) и .toString(). В то время, когда каждый конкретный метод потокобезопасный, вы не можете сделать несколько вызовов без конкуренции между потоками. Ваша единственная опция — внешняя синхронизация.
Так, что? Получается, 10 лет спустя уже никто не использует StringBuffer!? Ну, по крайней мере, точно не для нового функционала!?
Сколько объектов создает этот код?
Как я уже писал раньше, виртуальная машина создает много объектов на старте или при загрузке основных библиотек. Гораздо больше, чем Вы могли бы представить, задавая вопрос выше:
public class Main {
public static void main(String... args) {
System.out.println("Hello " + "world");
}
}
Oracle JVM 8-й версии создает приблизительно 10_000 объектов для выполнения этой программы.
Сколько же это создается StringBuffers?
Итак, чтобы запустить JVM нужно создать много объектов, но старые классы, у которых есть более быстрая альтернатива, которой уже 10 лет не должны быть использованы, так ведь?
public class Main {
public static void main(String[] args) throws IOException {
System.out.println("Waiting");
System.in.read();
}
}
Пока процесс запущен, мы можем выполнить следующую команду:
jmap -histo {pid} | grep StringBuffer
и получаем:
18: 129 3096 java.lang.StringBuffer
129 это количество объектов StringBuffer которые создала Java 8 Update 121. Это меньше, чем в прошлый раз, когда я проверял, но всё равно, немножко удивительно.
(Проверил у себя на Java 8 update 131, получил всего 14 объектов).
А как на счет новых фич — лямбд и стримов? Они были созданы явно в последние 10 лет и используют некоторые сторонние библиотеки, вроде Objectwebs ASM. И эти разработчики точно знают внутренности виртуальной машины и должны были спроектировать новые фичи максимально легковесными.
public class Main {
public static void main(String[] args) throws IOException {
IntStream.range(0, 4).forEach(System.out::println);
System.out.println("Waiting");
System.in.read();
}
}
Запускаем jmap опять и что мы видим?
17: 545 13080 java.lang.StringBuffer
Дополнительные 416 объектов для простейшей лямбды и стрима!
(Опять проверил у себя на Java 8 update 131 и получил 430 объектов, то есть разница в те же 416 объектов. Похоже, что стрими и лямбды не подчистили :)).
Кстати, программа выше в целом создает: Total 35486 4027224
или 35_486 объектов!
Выводы
Конечно же, использование StringBuffer на старте приложения не делает особой разницы, но зная, что есть годная альтернатива и StringBuffer до сих пор используется даже в новых фичах — показывает, как трудно бывает избавится от наследия старого кода или изменить мышление людей исользовать лучшие практики.
P. S.
В оригинальном посте оставили ссылку на тикет очистки от StringBuffer в 9-ке.
Так что вполне возможно через 3 месяца мы останемся без наследия :), правда речь лишь о StringBuffer. Не забываем про Vector, Hashtable и прочие приятности.
В нашей предыдущей статье о маршрутизаторе в виртуальном исполнении CSR 1000v от Cisco Systems мы постарались в максимально простой и краткой форме описать основные возможности этого замечательного продукта. Идеология подобных решений (и этого, в частности), на самом деле, очень проста и эффективна, т.к. виртуальное исполнение позволяет задействовать и максимально утилизировать имеющиеся вычислительные мощности. А, если учесть, что целевая область применения CSR – облачная инфраструктура, то ресурсов для его работы должно быть достаточно. При этом, повышается гибкость применения такого «устройства», т.к. его можно использовать на границах виртуальных сетей. Однако, в этой статье хотелось бы поговорить о том, как максимально повысить надёжность сети, минимизировать или даже свести к нулю время возможного простоя и не допустить появления единой точки отказа даже для небольшой группы виртуальных машин.
Средства обеспечения отказоустойчивости в нашем конкретном случае можно разделить на следующие три небольшие группы:
1. Механизмы обеспечения отказоустойчивости гипервизора.
2. Протоколы отказоустойчивости шлюза по умолчанию (FHRP – First Hop Redundancy Protocol).
3. Механизмы кластеризации, имеющиеся в ОС маршрутизатора.
О первой группе было кратко сказано в предыдущей статье. В нашем случае, при работе с виртуализацией VMware, есть 2 варианта: High Availability (HA) и Fault Tolerance (FT). Отличаются они друг от друга разными последствиями при выходе из строя хоста, на котором работала активная виртуальная машина (далее ВМ). В случае с HA ВМ перезапустится на другом хосте, при работе функционала FT – переходит в активное состояние теневая копия ВМ, работающая параллельно на другом хосте. Соответственно, для HA возможный простой составит от 5 минут в зависимости от других факторов (запустилась ли ВМ на другом хосте, хватило ли вычислительных ресурсов, была ли сохранена конфигурация CSR, имеются ли резервные копии). Если говорить о VMware FT, то здесь простой будет незаметен. Однако, FT во многих сценариях не применим, т.к. требует аппаратной совместимости серверов, где работают копии ВМ, в части наборов инструкций процессоров, а также, имеет ограничения в плане пропускной способности и увеличивает задержку сети. Ниже приведен ряд примеров того, как влияет использование FT при различных условиях.
На рисунке ниже показано, как влияет FT на пропускную способность виртуальной машины (ВМ) для сети со скоростью 1 Гбит/с.
На следующем рисунке аналогичный показатель, только, для 10 Гбит/с.
А вот так уже увеличивается задержка при включении FT для виртуальной машины.
Отсюда можно сделать вывод, что FT может быть использован не для всех сервисов, а только для тех, которые не требовательны к характеристикам сети (пропускная способность, задержка, джиттер и т.п.). Для CSR 1000v, как для сетевого устройства, любое негативное влияние на сетевые показатели нежелательно.
Переходим ко второй группе средств обеспечения отказоустойчивости. Далее у нас по плану семейство протоколов отказоустойчивости шлюза по умолчанию (FHRP – First Hop Redundancy Protocol). Как мы знаем, в контексте оборудования Cisco, основных представителей этой группы всего три: VRRP, HSRP и GLBP. Стоит сразу отметить, что данная группа протоколов обеспечивает лишь отказоустойчивый и относительно непрерывный «user experience», то есть обеспечивает резервирование и активное переключение нагрузки (либо балансировку), но не имеет ряда преимуществ других решений, как, например, единая точка управления в случае с методами из группы 1.
Подробно останавливаться на данной группе решений не будем, т.к. информация об указанных протоколах в избыточном объёме содержится в официальной документации Cisco. Скажу лишь кратко, что VRRP и HSRP подразумевают наличие одного активного «устройства» (не обязательно физического, т.к. для каждого интерфейса можно настроить отдельную группу), которое осуществляет передачу данных и одного или нескольких в режиме ожидания. GLBP отличается тем, что подразумевает балансировку между интерфейсами, входящими в одну группу.
Стоит отметить, что для корректной работы протоколов FHRP в виртуальной среде, то есть при использовании CSR 1000v, необходимо установить параметры «MAC address changes», «Forged transmissions» и «Promiscuous mode» в настройках безопасности для портовой группы виртуального коммутатора в значение «Accept». Это требуется, потому что при работе данных протоколов идёт работа с виртуальным MAC-адресом, что фактически приводит использованию нескольких MAC-адресов виртуальной машиной, а это по умолчанию не разрешается.
Также, скажу пару слов о производительности. Использование протоколов семейства FHRP значительно не влияет на сетевые показатели и загрузку ЦП маршрутизатора. Пропускная способность при этом также не пострадает.
Итак, наконец, переходим к последней группе – «кластеризация». Под этим словом в рамках данной статьи будем понимать механизм обеспечения отказоустойчивости, реализованный в IOS XE под названием High Availability, а если быть точным, то Firewall Box to Box High Availability.
Поддержка данного механизма появилась в ПО Cisco IOS XE Release 3.14S. Преимуществом данной технологии являются быстрая сходимость (обнаружение сбоя) и синхронизация состояний NAT и Firewall. Подробную информацию о настройках лучше подсмотреть в документации Cisco здесь. Для работы данного функционала требуется технологический пакет лицензий уровня Security или AX и не забываем про настройки безопасности портовой группы виртуального коммутатора аналогично FHRP.
Архитектура решения выглядит следующим образом:
Собственно, на этом закончим краткий обзор инструментов повышения отказоустойчивости относительно применения для Cisco CSR 1000v в виртуальной среде на базе VMware. Для кого-то информация окажется не новой, а кто-то, я надеюсь, найдёт в ней что-нибудь новое и полезное для себя. Не судите строго и используйте подходящие инструменты для защиты ваших сервисов. Спасибо!
В этой статье я собираюсь рассказать о плагине для IDA Pro, который написал прошлым летом еще находясь на стажировке в нашей кампании. В итоге, плагин был представлен на ZeroNights 2016 (Слайды) и, с тех пор, в нём было исправлено несколько багов и добавлены новые фичи. Хотя на GitHub я постарался описать его как можно подробнее, обычно коллеги и знакомые начинают пользоваться им только после проведения небольшого воркшопа. Кроме того там опущены некоторые детали внутренней работы, которые позволили бы лучше понять и использовать возможности плагина. Поэтому хотелось бы попытаться на примере объяснить как с ним работать, а также описать некоторые проблемы и тонкости.
HexRaysPyTools, как можно догадаться из названия, направлен на улучшение работы декомпилятора Hex-Rays Decompiler. Декомпилятор, создавая псевдо-С код, существенно облегчает работу ревёрсера. Основым его достоинством, выделяющим на фоне других подобных инструментов, является возможность трансформировать код, приводя его к удобному и понятному виду, в отличие от ассемблерного кода, который, даже при самом лучшем сопровождении, требует некоторой доли внимания и сосредоточенности для понимания его работы. У Hex-Rays Decompiler, как и самой IDA Pro, есть API, позволяющий писать расширения и выходить за рамки стандартного функционала. И хотя API очень широк и, в теории, позволяет удовлетворить самые изысканные потребности разработчика дополнений, он страдает несколькими существенными недостатками, а именно:
Слабая документированность. Для того, чтобы найти подходящие функции или классы самый эффективный способ — это производить поиск регулярными выражениями по файлам, угадывая ключевые слова.
Произвольные названия функций и структур — часто их название не говорит ни о чём.
Deprecated методы, для которых не предложено замены.
Общая запутанность работы. Например, для изменения типа аргумента у функции, нужно написать 8 строк кода и задействовать 3 класса. И это не самый странный пример.
API для языка Python не совсем совпадает с тем, что для C++. idaapi содержит новые методы наткнуться на которые получилось чисто случайно.
Есть неочевидные вещи, например, IDA Pro будет падать, если не отключать Garbage Collector для объектов, добавленных с помощью idaapi в абстрактное синтаксическое дерево (оно всегда строится, когда происходит декомпиляция и в нём можно изменять объекты или вставлять свои)
Чтобы разобраться во всем этом, помогали рабочие примеры, собранные в интернете (что-то интересное нашлось даже в китайском сегменте). Так что теперь, если кто-то захочет создать что-то своё для декомпилятора, то можно обратиться еще и к исходным кодам моего плагина.
Перейдем к описанию плагина. В HexRaysPyTools можно выделить две отдельные категории — это помощь по трансформации дефолтного вывода Hex-Rays Decompiler к удобному виду и реконструкция структур и классов.
Работа с кодом
Изначально, после запуска декомпилятора клавишей F5, IDA Pro выдаёт не очень понятый код, состоящий преимущественно из стандартных типов и имён переменных. И несмотря на то, что местами она пытается угадать типы, создать массивы или назвать эти переменные (которым повезло оказаться аргументами у стандартных функций) получается у неё это не очень. В целом это работа ревёрсера привести к адекватному виду декомпилированный код. К сожалению, есть вещи, которые невозможно сделать не прибегая к IDA SDK. Например, отрицательные обращения к полям структур, которые всегда выглядят безобразно (порой превращаясь в массивы с отрицательными индексами), а также длинные условные вложения, тянущиеся из левого верхнего угла в правый нижний. Кроме этого очень не хватает горячих клавиш и опций для более быстрой трансформации кода. По мере получения информации в процессе анализа программы приходится изменять сигнатуры функций, переименовывать переменные и изменять типы. Всё это требует большого количества манипуляций мышкой и копирований-вставок. Перейдем к описанию того, что предлагает плагин, для решения этих проблем.
Отрицательные смещения
Очень часто встречаются при реверсинге драйверов или ядра Windows или модулей ядра Linux. Например, несколько разных структур могут быть расположены в двусвязном списке с использованием структуры LIST_ENTRY. При этом у каждой структуры обращение к этому двусвязному списку может производиться из произвольного поля.
В результате, когда мы смотрим на то, что получается в IDA Pro, видим следующую картину:
Такой вывод будет всякий раз, когда в исходных кодах программ используются макросы CONTIAINING_RECORD (windows) и container_of (linux). Эти макросы возвращают указатель на начало структуры по её типу, адресу и названию поля. И именно их плагин позволяет вставлять в дизассемблер. Вот как выглядит пример после его применения:
Еще с отрицательными смещениями можно встретиться при множественном наследовании, но это довольно изысканный пример и надо еще постараться встретить его в своей практике.
Для того, чтобы вставить макрос в дизассемблер, нужно чтобы подходящая в данном контексте структуры существовала в Local Types или в одной из библиотек в Types Library (их может быть несколько). Мы кликаем по вложенной структуре правой кнопкой и выбираем Select Containing Structure. Далее выбираем где искать структуру — либо в Local Types, либо в Types Library и плагин составляет список подходящих структур. Для этого он анализирует как указанная переменная используется в коде и определяет минимальную и максимальную границы, по которым может находиться поле типа этой переменной. Затем используя эти сведения проходит по всем структурам отбирая те из них, которые содержат поле и у него все в порядке с границей. При поиске плагин смотрит вложенные структуры и объединения на любую глубину.
В примере выше у exe-файла есть символы, поэтому список подходящих структур получился довольно большой:
Помимо этого, существует ситуация, когда плагин автоматически может вставить макрос. Дело в том, что, когда есть явное присвоение указателя, IDA Pro догадывается (иногда неправильно) его вставить, но она не распространяет его дальше в коде.
Без плагина:
С плагином:
Сильная вложенность
Пожалуй, лучше всего показать искусственный пример. Без плагина:
С плагином:
Подобное изменение будет произведено автоматически, если установлен плагин. Хотелось бы, чтобы можно было накладывать и вручную, но увы то, что выдаёт декомпилятор очень нестабильно в плане сохранения вносимых изменений в синтаксическое дерево.
Переименования
Идея в том, чтобы называть переменную или аргумент приходилось не более одного раза, а дальше все переименования совершались горячими клавишами или двумя кликами.
Часто IDA Pro создаёт дублированные переменные. Можно было бы, используя стандартную опцию "map to another variable", избавиться от них. Но это не всегда удобно при отладке, может быть ошибочно и к тому же невозможно откатить не пересоздавая функцию заново.
Перебросить можно имя с одной переменной на другую, при этом добавляется символ "_":
До:
После:
Можно переименовать аргумент у функции, заставив её взять имя переменной (при этом лишние символы подчёркивания уберутся). Либо наоборот, переменной присвоить имя аргумента функции.
Recasts
Существует множество ситуаций, когда есть взаимодействие между двумя некоторыми сущностями с разными типами и нам нужно перенести тип одной сущности на другую. Под сущностями понимаются локальные, глобальные переменные, аргументы, функции, поля структур (с обращением по ссылке и без) и возвращаемые значения функции. Плагин позволяет это быстро произвести. Сложно показать это картинкой, рекомендую каждый раз, когда нужно перенести тип одной сущности на другую, кликнуть правой кнопкой мыши по ней и посмотреть на опции. В большинстве случаев там появится "Recast ..." (а если не появится, то можно написать мне, и я попробую её добавить).
Прочее
Помимо этого, добавляются следующие опции:
Поиск структуры по размеру и замена числа на sizeof(Structure)). Удобно для поиска структуры подходящего размера по числу байт, указанных оператору new или функции malloc.
Быстрое изменение сигнатуры функции. Кликнув правой кнопкой по её объявлению, можно добавить/удалить возвращаемое значение, удалить аргумент, сбросить соглашение о вызове к __stdcall.
Переход по двойному клику у виртуальных методов.
Восстановление структур
Одной из самых сложных и энергозатратных задач ревёрс-инжиниринга является понимание работы и реконструкция структур и классов. HexRaysPyTools выступает помощником в этом процессе. В чём собственно проблема? Средствами по умолчанию можно только залить уже готовое объявление структуры, поэтому приходится ползать по коду пытаясь насобирать сведения о полях, вручную высчитывать смещения и записывать куда-то всю информацию (например, в блокнот). Но, если у нас размеры классов исчисляются сотнями байт и, в придачу, имеют множество методов и несколько виртуальных таблиц, то всё становится гораздо сложнее.
Рассмотрим на примере, как помогает в данном случае плагин. Когда-то (исключительно ради самообразования :D) я делал бота для онлайн игрушки и наткнулся на защиту, шифрующую пакеты, не позволявшую модифицировать код в памяти и мешающую хукать вызов шифрующей функции (которая была жестко обфусцирована). Для того чтобы обойти её нужно было распарсить класс, отвечающий за обмен данными между клиентом и сервером и научиться, используя его, вызывать отправку пакетов и считывать полученные, расшифрованные пакеты за несколько вызовов от функций защиты. Тогда для меня это было непростой задачей, но с плагином всё делается довольно просто.
Вот так выглядит метод принимающий пакеты. this и v1 являются указателями на объект класса, gepard_1 — это функция, заменяющая recv
Если заглянуть внутрь функций sub_41AF50 и sub_41AFF0, то можно увидеть довольно много кода, обращающегося к разным полям. И даже — это только часть функционала ответственного за создание и отправку пакетов, поэтому разобраться в назначении полей может оказаться непросто. Плагин помогает автоматически проанализировать большое количество кода, и из собранной информации составить некий каркас структуры, которая в дальнейшем анализе может быть изменена исследователем и использована для автоматического сознания нового типа. Для начала надо открыть Structure Builder через Edit->Plugins->HexRaysPyTools. Это окошко будет содержать собранную информацию, предоставлять возможность для редактирования имен полей и разрешения конфликтов, а также просмотра виртуальных таблиц и сканирования виртуальных функций.
Есть 3 возможных способа собирать информацию о полях.
1) Можно кликнуть правой кнопкой по переменной и, нажав Scan Variable, запустить сканирование в пределах одной функции. При сканировании будет рассматриваться то, как происходит обращение к переменной и, если оно под падает под паттерн обращения к полю, то такая информацию будет запомнена. Если другой переменной присваивается значение первой (причем их типы не обязательно должны совпадать), то она так же подключается к сканированию (и отключается, если ей будет присвоено новое значение). Вот какой результат будет, если применить этот метод к переменной this в функции выше:
Хотя мы запускали сканирование для переменной this, информация о v1 также была собрана.
Жёлтым отмечены разного рода обращения к полям и нужно выбрать какой вариант более подходящий, отключив все остальные. До тех пор, пока есть конфликты, создать финальную структуру будет нельзя. Красный — это смещение начиная с которого производится сканирование. Его можно сдвинуть с помощью кнопки Origin для того чтобы просканировать потенциальную подструктуру. Например, можно зайти в функцию sub_41AF50 и сдвинув указатель собрать немного новой информации:
Если дважды кликнуть по столбцу Offset интересующего поля, то можно увидеть список всех обращений к этому полю и быстро переместиться в дизассемблере к месту обращения. Поэтому есть смысл как можно сильнее покрывать сканированиями все места использования восстанавливаемой структуры. Больше информации о полях — проще разобраться что зачем нужно. Сканировать каждую переменную может быть весьма утомительно, т.к. указатель на структуру может путешествовать по большому количеству функций, поэтому есть другой способ сбора информации.
2) Кликнув правой кнопкой по переменной, можно выбрать опции "Deep Scan Variable". Основной процесс сканирования будет такой же как и у первого способа, только теперь, если указатель на структуру будет передаваться как аргумент функции, то будет запущенно рекурсивное сканирование этого аргумента. Warning! Здесь есть одна проблема — декомпилятор не всегда распознает правильно аргументы функции, которую он еще не декомпилировал, поэтому приходится рекурсивно заходить и декомпилировать каждую функцию, которая потенциально может содержать указатель на нашу структуру как аргумент. Этот процесс запускается автоматически и случается только один раз за сессию для каждой функции. Поэтому первые процессы глубокого сканирования могут занять некоторое время (порядка пары минут)
Переместившись несколько раз вверх по цепочке вызов, можно найти место, где создается структура:
Запустив здесь сканирование получаем следующее:
3) Начинать сканировать лучше всего там, где структура впервые возникает, чтобы максимально покрыть её использование в коде. Плагин предоставляет возможность просканировать все переменные, которым присваивается результат, возвращаемый конструктором.
Если зайти в функции sub_419890, которая впервые возвращает указатель на структуру, то можно увидеть, что используется паттерн одиночка:
Количество вызовов этой функции очень велико:
Сканировать каждую переменную было бы утомительно, поэтому есть возможность запустить сканер для всех сразу, кликнув по заголовку функции и выбрав опцию "Deep Scan Returned Variables"
Вот результат применения на моём примере:
Можно заметить, что нашлась информация об обращении к полям 0x8 — 0x14. Так же нашлись виртуальные таблицы — они отображены жирным шрифтом и, дважды кликнув по ним, можно увидеть список виртуальных функций (а заодно и просканировать их как по одной, так и все сразу)
Теперь можно разбираться с устройством структуры. Напомню, что кликнув по офсету можно увидеть все обращения к полям.
Вот, что получилось у меня после непродолжительного анализа:
Подготовка создания структуры завершена. Теперь можно кликнуть "Finalize" и, внеся последние изменения, закончить её создание:
Далее, везде, куда протянул свои руки сканнер, переменным, которые являлись указателем на эту структуру будет применён её новосозданный тип. Вот как преобразится функция отправки пакетов:
Помимо представленного, в Structure Builder можно создать или попытаться угадать подструктуры, выделив необходимое количество полей и нажав Pack или Recognize Shape соответственно. При поиске подходящей структуры учитываются типы полей — они должны точно совпадать, за исключением базовых типов (char — BYTE, int — DWORD — int *) считающихся одинаковыми.
Для уже созданных классов (структур с виртуальными таблицами), в плагине есть возможность более удобной работы с ними. По пути View-> Open Subviews-> Classes можно открыть следующее окошко:
Здесь можно:
Переименовать методы, в результате чего изменения будут произведены сразу в коде и виртуальных таблицах (сделано, чтобы избежать рассинхронизации)
Изменить объявление методов
Быстро конвертировать первый аргумент в this.
Переместиться к функции в окне дизассемблера.
Фильтровать информацию регулярными выражениями.
И еще, хотелось бы лишний раз напомнить, что при работе с классами очень удобно использовать плагин ClassInformer. Если в файле есть RTTI информация, то это поможет восстановить иерархия классов, а мой плагин возьмёт имена виртуальных таблиц, что поможет получить близкие к оригиналу имена классов.
Надеюсь эта статья поможет разобраться как пользоваться плагином. Найти его и сообщить о багах можно по адресу — https://github.com/igogo-x86/HexRaysPyTools. Также жду feature requests.
Разговоры о снижении производительности ради продуктивности.
Я беру паузу в моём обсуждении asyncio в Python, чтобы поговорить о скорости Python. Позвольте представиться, я — ярый поклонник Python, и использую его везде, где только удаётся. Одна из причин, почему люди выступают против этого языка, — то, что он медленный. Некоторые отказываются даже попробовать на нём поработать лишь из-за того, что «X быстрее». Вот мои мысли на этот счёт.
Производительность более не важна
Раньше программы выполнялись очень долго. Ресурсы процессора и памяти были дорогими, и время работы было важным показателем. А уж сколько платили за электричество! Однако, те времена давно прошли, потому что главное правило гласит:
Оптимизируйте использование своих самых дорогих ресурсов.
Исторически самым дорогим было процессорное время. Именно к этому подводят при изучению информатики, фокусируясь на эффективности различных алгоритмов. Увы, это уже не так — железо сейчас дёшево как никогда, а вслед за ним и время выполнения становится всё дешевле. Самым дорогим же становится время сотрудника, то есть вас. Гораздо важнее решить задачу, нежели ускорить выполнение программы. Повторюсь для тех, кто просто пролистывает статью:
Гораздо важнее решить задачу, нежели ускорить выполнение программы.
Вы можете возразить: «В моей компании заботятся о скорости. Я создаю веб-приложение, в котором все ответы приходят меньше, чем за x миллисекунд» или «от нас уходили клиенты, потому что считали наше приложение слишком медленным». Я не говорю, что скорость работы не важна совсем, я хочу лишь сказать, что она перестала быть самой важной; это уже не самый дорогой ваш ресурс.
Скорость — единственная вещь, которая имеет значение.
Когда вы употребляете слово «скорость» в контексте программирования, скорей всего вы подразумеваете производительность CPU. Но когда ваш CEO употребляет его применительно к программированию, то он подразумевает скорость бизнеса. Самая главная метрика — время выхода на рынок. В конечном счёте неважно насколько быстро работают ваши продукты, не важно на каком языке программирования они написаны или сколько требуется ресурсов для их работы. Единственная вещь, которая заставит вашу компанию выжить или умереть, — это время выхода на рынок. Я говорю больше не о том, насколько быстро идея начнёт приносить доход, а о том, как быстро вы сможете выпустить первую версию продукта. Единственный способ выжить в бизнесе — развиваться быстрее, чем конкуренты. Неважно, сколько у вас идей, если конкуренты реализуют их быстрее. Вы должны быть первыми на рынке, ну или как минимум не отставать. Чуть затормозитесь — и погибните.
Единственный способ выжить в бизнесе — развиваться быстрее, чем конкуренты.
Поговорим о микросервисах
Крупные компании, такие как Amazon, Google или Netflix, понимают важность быстрого развития. Они специально строят свой бизнес так, чтобы быстро вводить инновации. Микросервисы помогают им в этом. Эта статья не имеет никакого отношения к тому, следует ли вам строить на них свою архитектуру, но, по крайней мере, согласитесь с тем, что Amazon и Google должны их использовать. Микросервисы медленны по своей сути. Сама их концепция заключается в том, чтобы использовать сетевые вызовы. Иными словами, вместо вызова функции вы отправляетесь гулять по сети. Что может быть хуже с точки зрения производительности?! Сетевые вызовы очень медленны по сравнению с тактами процессора. Однако, большие компании по-прежнему предпочитают строить архитектуру на основе микросервисов. Я действительно не знаю, что может быть медленнее. Самый большой минус такого подхода — производительность, в то время как плюсом будет время выхода на рынок. Создавая команды вокруг небольших проектов и кодовых баз, компания может проводить итерации и вводить инновации намного быстрее. Мы видим, что даже очень крупные компании заботятся о времени выхода на рынок, а не только стартапы.
Процессор не является вашим узким местом
Если вы пишете сетевое приложение, такое как веб-сервер, скорее всего, процессор не является узким местом вашего приложения. В процессе обработки запроса будет сделано несколько сетевых обращений, например, к базе данных или кешу. Эти сервера могут быть сколь угодно быстрыми, но всё упрётся в скорость передачи данных по сети. Вот действительно классная статья, в которой сравнивается время выполнения каждой операции. В ней автор считает за один такт процессора одну секунду. В таком случае запрос из Калифорнии в Нью-Йорк растянется на 4 года. Вот такая вот сеть медленная. Для примерных расчётов предположим, что передача данных по сети внутри датацентра занимает около 3 мс, что эквивалентно 3 месяцам по нашей шкале отсчёта. Теперь возьмём программу, которая нагружает CPU. Допустим, ей надо 100 000 циклов, чтобы обработать один вызов, это будет эквивалентно 1 дню. Предположим, что мы пишем на языке, который в 5 раз медленнее — это займёт 5 дней. Для тех, кто готов ждать 3 месяца, разница в 4 дня уже не так важна.
В конечном счёте то, что python медленный, уже не имеет значения. Скорость языка (или процессорного времени) почти никогда не является проблемой. Google описал это в своём исследовании. А там, между прочим, говорится о разработке высокопроизводительной системы. В заключении приходят к следующему выводу:
Решение использовать интерпретируемый язык программирования в высокопроизводительных приложениях может быть парадоксальным, но мы столкнулись с тем, что CPU редко когда является сдерживающим фактором; выразительность языка означает, что большинство программ невелики и большую часть времени тратят на ввод-вывод, а не на собственный код. Более того, гибкость интерпретируемой реализации была полезной, как в простоте экспериментов на лингвистическом уровне, так и в предоставлении нам возможности исследовать способы распределения вычислений на многих машинах.
Другими словами:
CPU редко когда является сдерживающим фактором.
Что, если мы всё-таки упираемся в CPU?
Что насчёт таких аргументов: «Всё это прекрасно, но что, если CPU становится узким местом и это начинает сказываться на производительности?» или «Язык x менее требователен к железу, нежели y»? Они тоже имеют место быть, однако вы можете масштабировать приложение горизонтально бесконечно. Просто добавьте серверов, и сервис будет работать быстрее :) Без сомнения, Python более требователен к железу, чем тот же C, но стоимость нового сервера гораздо меньше стоимости вашего времени. То есть дешевле будет докупить ресурсов, а не бросаться каждый раз что-то оптимизировать.
Так что, Python быстрый?
На протяжении всей статьи я твердил, что самое важное — время разработчика. Таким образом, остается открытым вопрос: быстрее ли Python языка X во время разработки? Смешно, но я, Googleикое-ктоещё, могут подтвердить насколько продуктивен Python. Он скрывает так много вещей от вас, помогая сосредоточиться на основной вашей задаче и не отвлекаясь на всякие мелочи. Давайте рассмотрим некоторые примеры из жизни.
По большей части все споры вокруг производительности Python скатываются к сравнению динамической и статической типизации. Думаю, все согласятся, что статически типизированные языки менее продуктивны, но на всякий случай вот хорошая статья, объясняющая почему. Что касается непосредственно Python, то есть хороший отчёт об исследовании, в котором рассматривается, сколько времени потребовалось, чтобы написать код для обработки строк на разных языках.
В представленном выше исследовании Python более чем в 2 раза продуктивнее Java. Многие другие также языки программирования дают похожий результат. Rosetta Code провёл довольно обширное исследование различий изучения языков программирования. В статье они сравнивают python с другими интерпретируемыми языками и заявляют:
Python, в целом, наиболее краток, даже в сравнении с функциональными языками (в среднем в 1,2-1,6 раза короче).
По-видимому, в реализации на Python будет как правило меньше строк кода, чем на каком-либо другом языке. Это может показаться ужасной метрикой, однако несколько исследований, в том числе и упомянутых выше, показывают, что время, затраченное на каждую строку кода, примерно одинаково на каждом языке. Таким образом, чем меньше строк кода, тем больше продуктивность. Даже сам codinghorror (программист на C#) написал статью о том, что Python более продуктивен.
Я думаю, будет справедливо сказать, что Python более продуктивен, чем многие другие языки программирования. В основном это связано с тем, что он поставляется «с батарейками», а также имеет множество сторонних библиотек на все случаи жизни. Вот небольшая статья, сравнивающая Python и X. Если вы мне не верите, можете сами убедиться насколько этот язык программирования прост и удобен. Вот ваша первая программа:
import __hello__
Но что, если скорость действительно важна?
Мои рассуждения выше могли сложить мнение, что оптимизация и скорость выполнения программы вообще не имеют значения. Во многих случаях это не так. Например, у вас есть веб-приложение и некоторый endpoint, который уж очень тормозит. У вас могут быть даже определённые требования на этот счёт. Наш пример строится на некоторых предположениях:
есть некоторый endpoint, который выполняется медленно
есть некоторые метрики, определяющие насколько медленными может быть обработка запросов
Мы не будем заниматься микро-оптимизацией всего приложения. Всё должно быть «достаточно быстро». Ваши пользователи могут заметить, если обработка запроса занимает секунды, но они никогда не заметят разницу между 35 мс и 25 мс. Вам нужно лишь сделать приложение «достаточно хорошим».
Дисклеймер
Должен заметить, что есть некоторые приложения, которые обрабатывают данные в реальном времени, которые нуждаются в микрооптимизации, и каждая миллисекунда имеет значение. Но это скорее исключение, чем правило.
Чтобы понять как оптимизировать, мы должны сначала понять что именно надо оптимизировать. В конце концов:
Любые улучшения, сделанные где-либо помимо узкого места, являются иллюзией. — Джин Ким.
Если оптимизация не устраняет узкого места приложения, то вы просто потратите впустую своё время и не решите реальную проблему. Вы не должны продолжать разработку, пока не исправите тормоза. Если вы пытаетесь оптимизировать нечто, не зная конкретно что, то вряд ли результат вас удовлетворит. Это и называется «преждевременной оптимизацией» — улучшение производительности без каких-либо метрик. Д. Кнуту часто приписывают следующую цитату, хотя он утверждает, что она не его:
Преждевременная оптимизация — корень всех зол.
Если быть точным, то более полная цитата:
Мы должны забыть об эффективности, скажем, на 97% времени: преждевременная оптимизация — корень всех зол. Однако мы не должны упускать наши возможности в этих критических 3%.
Другими словами, здесь говорится, что большую часть времени вам не нужно думать об оптимизации. Код и так хорош :) А в случае, когда это не так, нужно переписать не более 3% Вас никто не похвалит, если вы сделаете обработку запроса на несколько наносекунд быстрее. Оптимизируйте то, что поддаётся измерению.
Преждевременная оптимизация, как правило, заключается в вызове более быстрых методов <прим пер. видимо, подразумеваются ассемблерные вставки> или использовании специфичных структур данных из-за их внутренней реализации. В университете нас учили, что если два алгоритма имеют одну асимптотику Big-O, то они эквивалентны. Даже если один из них в 2 раза медленнее. Компьютеры сейчас настолько быстры, что вычислительную сложность пора измерять на большом количестве данных. То есть, если у вас есть две функции O(log n), но одна в два раза медленнее другой, то это не имеет большого значения. По мере увеличения размера данных они обе начинают показывать примерно одно и то же время выполнения. Вот почему преждевременная оптимизация — это корень всего зла; Это тратит наше время и практически никогда не помогает нашей общей производительности.
В терминах Big-O все языки программирования имеют сложность O(n), где n — кол-во строк кода или инструкций. Не имеет значения, насколько медленным будет язык или его виртуальная машина — все они имеют общую асимптоту. <прим. пер. автор имеет ввиду, что даже если сейчас язык X в два раза медленнее Y, то в будущем после оптимизаций они будут примерно равны по скорости> В соответствии с этим рассуждением можно сказать, что «быстрый» язык программирования всего лишь преждевременно оптимизированный, причём непонятно по каким метрикам.
Оптимизируя Python
Что мне нравится в Python, так это то, что он позволяет оптимизировать небольшой участок кода за раз. Допустим, у вас есть метод на Python, который вы считаете своим узким местом. Вы оптимизировали его несколько раз, возможно, следуя советам отсюда и отсюда, и теперь пришли к выводу, что уже сам Python является узким местом. Но он имеет возможность вызова кода на C, а это означает, что вы можете переписать этот метод на C, чтобы уменьшить проблему с производительностью. Вы без проблем можете использовать этот метод вместе с остальным кодом.
Это позволяет писать хорошо оптимизированные методы узких мест на любом языке, который компилируется в ассемблерный код, то есть вы продолжаете писать на Python большую часть времени и переходите к низкоуровневому программированию лишь вам это действительно нужно.
Существует также язык Cython, который является надмножеством Python. Он представляет из себя смесь с типизированным C. Любой код Python является также валидным кодом и на Cython, который транслируется в C. Вы можете смешивать типы C и утиную типизацию. Используя Cython, вы получаете прирост производительности только в узком месте, не переписывая весь остальной код. Так делает, например, EVE Online. Эта MMoRPG использует только Python и Cython для всего стека, проводя оптимизацию только там, где это требуется. Кроме того, есть и другие способы. Например, PyPy — реализация JIT Python, которая может дать вам значительное ускорение во время выполнения долгоживущих приложений (например, веб-сервера), просто путем замены CPython (реализация по умолчанию) на PyPy.
Итак, основные моменты:
оптимизируйте использование самого дорогого ресурса — то есть вас, а не компьютера;
выбирайте язык/фреймворк/архитектуру, которая позволяет вам разрабатывать продукты как можно быстрее. Не стоит выбирать язык программирования лишь потому, что программы на нём работают быстро;
если у вас проблемы с производительностью — определите, где именно;
и, скорее всего, это не ресурсы процессора или Python;
если это всё же Python (и вы уже оптимизировали алгоритм), реализуйте проблемное место на Cython/C;
и побыстрее возвращайтесь к основной работе.
Надеюсь, статья понравилась. Вы можете связаться со мной в twitter (@nhumrich) или в Slack.
GitLab спроектирован таким образом, что каждый участник проекта может вносить свой вклад, независимо от размера команды и местонахождения ее участников.
Зачастую в процессе разработки продукта несколько человек работают над одной задачей: например, разработчики фронтэнда и бэкэнда, дизайнер интерфейса, тестировщик и менеджер продукта. С выходом GitLab 9.2 все они могут быть назначены исполнителями одной задачи, что упрощает процесс совместной разработки и позволяет наглядно отображать их общую область ответственности.
Также в версии 9.2 положено начало процесса локализации GitLab: аналитика цикла разработки (Cycle Analytics) теперь доступна на испанском и немецком языках. Локализация GitLab будет продолжена в последующих релизах, чтобы каждый мог вносить свой вклад независимо от родного языка.
Кроме того, разработчики получили больше контроля над временем выполнения конвейеров CI/CD. Теперь можно составлять расписание выполнения конвейеров, что позволяет автоматизировать периодические задачи, такие как ночные сборки, профилактика или подтверждение внешних зависимостей.
При работе с GitLab нередко появляются задачи, которые требуют совместных усилий сразу нескольких человек. Ранее в таких ситуациях было сложно понять, на кого назначена задача; это было особенно актуально для организаций, в которых исполнители не контактируют друг с другом ежедневно. С выходом GitLab 9.2 становится возможным назначить исполнителями одной задачи столько пользователей, сколько необходимо. Все они будут обладать равным уровнем ответственности и получать те же оповещения, что и в предыдущих версиях. Все исполнители отображаются в списке задач и досках задач, каждый из них может отслеживать свою связь с задачами на своей панели управления.
Обратите внимание: в связи с этими изменениями параметр API задачassignee_id устарел, вместо него нужно использовать assignee_ids. Также, вместо объекта JSON assignee нужно использовать assignees.
Локализация аналитики цикла разработки (Cycle Analytics) (CE, EES, EEP)
Hola Mundo, Hallo Welt! Мы рады сообщить, что, начиная с данного релиза, мы приступаем к работе над локализацией GitLab. Будем рады вашей поддержке в этом начинании!
В версию 9.2 добавлены инструментарий и инфраструктура для перевода GitLab на любой язык мира. Пока что в тестовых целях мы перевели только одну страницу (Cycle Analytics) на испанский и немецкий. В следующих версиях мы будем переводить новые страницы, а также добавлять другие языки. Если вы хотите помочь нам в этом — обратитесь к инструкции.
Для смены языка GitLab нажмите на свою иконку в правом верхнем углу экрана и зайдите в настройки (Settings).
В большинстве проектов конвейеры CI/CD запускаются для каждого нового коммита — таким образом, все изменения в коде проходят сборку, тестирование и развертывание. Однако, существуют ситуации, в которых требуется запуск конвейера по определенному расписанию.
С выходом GitLab 9.2 становится возможным настроить запуск конвейеров тогда, когда нужно, и так часто, как нужно. Ежедневный и еженедельный запуск конвейеров, проверка внешних зависимостей или просто профилактические запуски — теперь все это можно легко настроить.
Больше информации о планировании запуска конвейеров в нашей документации
Установка GitLab на Kubernetes (CE, EES, EEP)
Мы хотим, чтобы GitLab был как можно лучшим инструментом для нативной облачной разработки. Для этого важно, чтобы вы могли легко начать работу с Kubernetes. Поэтому мы с радостью сообщаем, что с версией 9.2 выходят официальные Helm-чарты GitLab.
Helm — это инструмент управления пакетами, который позволяет проводить развертывание, апгрейд и настройку приложений в кластерах Kubernetes. GitLab и Kubernetes прекрасно взаимодействуют друг с другом: все идеи, представленные в нашем видео Idea to Production, такие как автоматизация масштабирования CI и развертывания в ваши кластеры, легко реализуются с минимумом дополнительной настройки.
Больше информации об установке GitLab при помощи Helm-чартов в нашей документации
Данные о воздействии мерж-реквестов на производительность (CE, EES, EEP)
В большинстве случаев нелегко отследить влияние определенного мержа на производительность системы. Зачастую, за данные о производительности отвечает отдельный инструмент, к которому у команды разработчиков даже нет доступа. Мы хотим, чтобы у разработчиков была возможность получать эти данные для каждого изменения, которое они вносят. Интеграция с Prometheus является большим шагом к достижению этой цели.
Начиная с GitLab 9.2 изменения в потреблении памяти после развертывания отображаются прямо в мерж-реквесте. Теперь разработчики могут отслеживать изменения в производительности, не отвлекаясь от привычного процесса работы. В последующих релизах мы планируем добавить другие метрики производительности.
Больше информации об отслеживании воздействия мерж-реквестов на потребление памяти в нашей документации
Улучшения альфа-версии аварийного восстановления (EEP)
Начиная с версии 9.0, в GitLab Geo входит альфа-версия аварийного восстановления. Мы стремимся улучшать ее с каждым новым релизом, и GitLab 9.2 не стал исключением. В новой версии мы сделали аварийное восстановление более удобным: смысл элементов управления стал более очевидным, а отчеты о процессе репликации — более подробными.
Также улучшился механизм репликации репозиториев: теперь повторная синхронизация для репозиториев, поврежденных из-за ошибок синхронизации, происходит автоматически. Кроме того, при обновлении основной базы данных происходит ее автоматическая синхронизация со вторичными базами.
Автоматическое обновление названий и описаний задач (CE, EES, EEP)
Поскольку страница задачи является основным местом совместной работы, ее содержимое постоянно меняется. Начиная с версии GitLab 9.2, название и описание задач обновляются автоматически, без обновления страницы. Так что теперь для того, чтобы постоянно видеть последнюю версию задачи, достаточно просто держать ее вкладку открытой. Даже название вкладки в браузере обновляется автоматически.
Кроме того, при редактировании описания задачи добавляется системная заметка, благодаря чему можно просмотреть полную историю изменений задачи просто пролистав ее комментарии. Эти изменения затронули и комментарии — они таким же образом автоматически обновляются при их редактировании.
Улучшенный поиск с помощью Elasticsearch (EES, EEP)
Мы добавляем больше возможностей расширенного поиска за счет интеграции с Elasticsearch. Если вы настроили Elasticsearch, вы можете искать точные фразы (в двойных кавычках) или неупорядоченный набор ключевых слов, по совпадению части слова или использовать другие возможности поискового синтаксиса. (Заметьте, что это относится к строке поиска в правом верхнем углу на любой странице GitLab, но не к поиску в списках задач и мерж-реквестов.)
Также благодаря Elasticsearch стал возможным глобальный поиск по всем вики в вашем инстансе.
В каждой итерации GitLab мы стараемся сделать путь от идеи к производству более быстрым и легким.
Эта новая маленькая фича позволит вам создать мерж-реквест прямо со страницы задачи, а необходимую для этого ветку GitLab создаст автоматически. Это особенно полезно, когда вам нужно сделать небольшой коммит прямо из интерфейса GitLab.
В сниппетах (даже в личных) часто происходит совместная работа. В этом обновлении появятся треды комментариев для каждого личного сниппета — так же, как и для сниппетов проекта.
При просмотре файлов GitLab многие файлы, например, SVG & Markdown, отображаются в отрендеренной форме. На странице просмотра файла появился удобный маленький переключатель между режимами просмотра исходного кода и отрендеренного файла.
Поддержка Terraform для GitLab Runner на Google Compute Engine (CE, EES, EEP)
В GitLab 9.1 мы запустили поддержку установки GitLab на GCE через Terraform компании Hashicorp. В версии 9.2 мы также добавляем возможность разворачивать GitLab Runner, дополнив этим принцип «от идеи к производству».
Предпросмотр артефактов заданий в пользовательском интерфейсе (CE, EES, EEP)
Артефактом может быть любой файл; вы можете добраться до этого файла с помощью поиска по архиву прямо в пользовательском интерфейсе. GitLab 9.2 расширил возможности этого поиска: теперь у файлов PDF, картинок, видео и других форматов будет предпросмотр прямо в поиске артефактов заданий — их больше не нужно скачивать.
Обработка неоднозначных маршрутов в динамических путях (CE, EES, EEP)
В GitLab есть несколько путей до конкретных фич. После введения вложенных групп эти фичи могли стать недоступными для тех проектов или групп, названия которых конфликтуют с этими путями. Например, для проекта под названием 'badges' бэйджи (badges) статусов сборки или покрытия стали недоступны.
Чтобы избежать таких недоразумений, мы убрали возможность создавать проекты или группы с названиями, которые могут конфликтовать с существующими маршрутами GitLab.
Если у вас были проекты или группы с названиями одного из таких путей, они автоматически переименуются при обновлении. Проект с названием badges будет называться badges0.
Учтите, что настройки git remote нужно будет обновить вручную, например, так:
Полный список зарезервированных слов вы можете посмотреть в файле dynamic_path_validator.rb. Список существующих проектов и групп, которых в этом релизе затронуло автоматическое переименование, можно найти в миграции, которая их переименовала.
После принятия мерж-реквеста ветка удаляется по умолчанию (CE, EES, EEP)
Начиная с GitLab 9.2 исходные ветки в мерж-реквестах по умолчанию удаляются. Если вы хотите сохранить ветку даже после мержа, вам нужно отключить опцию в виджете мерж-реквеста до его принятия.
Восстановление артефактов после кэширования файлов в заданиях CI (CE, EES, EEP)
Может так случиться, что кто-то (случайно или умышленно) использует в .gitlab-ci.yml один и тот же путь для ключевых слов cache (кэш) and artifacts (артефакты), что может вызвать ситуацию, когда залежавшийся кэш может случайно перезаписать артефакты, которые используются в конвейере.
С этого релиза артефакты можно восстанавливать после кэширования — чтобы убедиться, что они сохранятся и будут доступны даже в критической ситуации.
Реестр контейнеров GitLab (GitLab Container Registry) обновился с версии 2.4 до версии 2.6.1.
Назначение конкретных ревьюеров для мерж-реквестов (EES, EEP)
Интерфейс мерж-реквестов позволяет выбирать отдельных ревьюеров, которые смогут принять (подтвердить) этот реквест. На выбор предлагаются только участники проекта, а также группы, к которой принадлежит проект (parent group), и группы, которым предоставлен доступ к проекту (project share). Поэтому вы сможете легко найти нужных пользователей.
Комментирование в устаревших диффах (CE, EES, EEP)
В этом релизе появилась возможность ссылаться на устаревший дифф прямо из треда обсуждения мерж-реквеста. Это позволит быстро обращаться к содержимому старых коммитов в процессе разработки, совместной работы и ревью. Вы даже сможете оставлять комментарии в тех устаревших диффах.
Развертывания отображаются на доске производительности (CE, EES, EEP)
Если производительность продукта поменялась, то первый вопрос — менялось ли рабочее окружение? GitLab 9.2 теперь быстро отвечает на этот вопрос, отображая все развертывания в окружении прямо на доске мониторинга. Это позволяет сразу видеть зависимость между изменениями в производительности и новой версией приложения (и все это не покидая GitLab).
Ручные действия учитывают защищенные ветки (CE, EES, EEP)
Для ручных действий теперь требуются те же права доступа, что и на запись в репозиторий, что позволит контролировать, кто может их совершать. Например, теперь можно оставить право на ручной запуск задачи по развертыванию кода из стабильной ветки на production только для пользователей, имеющих право на коммит в эту ветку.
Это очень важная часть в списке улучшений системы безопасности, которые мы добавляем в GitLab для защиты развертывания в ваших проектах. Подробности можно прочитать в этой задаче.
Вкладка неудавшихся заданий, резюмирующая все отказы (CE, EES, EEP)
Когда вы совершаете коммит новой части кода в проект с continuous integration (CI), обычно вы ожидаете увидеть отметку, которая обозначает, что конвейер выполнен успешно, и все сработало так, как было задумано. В случае, если что-то пошло не так, нужно как можно быстрее узнать подробности. Но многочисленные переходы по каждому из этапов и заданий не очень вдохновляют, особенно если конвейер сложный.
В GitLab 9.2 на странице информации о конвейере (в разделе Pipelines выбрать конкретный конвейер) появилась вкладка Failed Jobs. На ней можно посмотреть логи всех проваленных заданий и оценить общую картину выполнения конвейера.
В стандартной библиотеке питона содержится специализированный тип "namedtuple", который, кажется, не получает того внимания, которое он заслуживает. Это одна из прекрасных фич в питоне, которая скрыта с первого взгляда.
Именованные кортежи могут быть отличной альтернативой определению классов и они имеют некоторые другие интересные особенности, которые я хочу показать вам в этой статье.
Так что же такое именованный кортеж и что делает его таким специализированным? Хороший способ поразмышлять над этим — рассмотреть именованные кортежи как расширение над обычными кортежами.
Кортежи в питоне представляют собой простую структуру данных для группировки произвольных объектов. Кортежи являются неизменяемыми — они не могут быть изменены после их создания.
>>> tup = ('hello', object(), 42)
>>> tup
('hello', , 42)
>>> tup[2]
42
>>> tup[2] = 23
TypeError: "'tuple' object does not support item assignment"
Обратная сторона кортежей — это то, что мы можем получать данные из них используя только числовые индексы. Вы не можете дать имена отдельным элементам сохранённым в кортеже. Это может повлиять на читаемость кода.
Также, кортеж всегда является узкоспециализированной структурой. Тяжело обеспечить, что бы два кортежа имели одни и те же номера полей и одни и те же свойства сохранённые в них. Это делает их лёгкими для знакомства со “slip-of-the-mind” багами, где легко перепутать порядок полей.
Именованные кортежи идут на выручку
Цель именованных кортежей — решить эти две проблемы.
Прежде всего, именованные кортежи являются неизменяемыми подобно обычным кортежам. Вы не можете изменять их после того, как вы что-то поместили в них.
Кроме этого, именованные кортежи это, эм..., именованные кортежи. Каждый объект сохраненный в них может быть доступен через уникальный, удобный для чтения человеком, идентификатор. Это освобождает вас от запоминания целочисленных индексов или выдумывания обходных путей типо определения целочисленных констант как мнемоник для ваших индексов.
Вот как выглядит именованный кортеж:
>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage')
Чтобы использовать именованный кортеж, вам нужно импортировать модуль collections. Именованные кортежи были добавлены в стандартную библиотеку в Python 2.6. В примере выше мы определили простой тип данных "Car" с двумя полями: "color" и "mileage".
Вы можете найти синтакс немного странным здесь. Почему мы передаём поля как строку закодированную с "color mileage"?
Ответ в том, что функция фабрики именованных кортежей вызывает метод split() на строки с именами полей. Таким образом, это, действительно, просто сокращение, что бы сказать следующее:
Конечно, вы также можете передать список со строками имён полей напрямую, если вы предпочитаете такой стиль. Преимущество использования списка в том, что в этом случае легко переформатировать этот код, если вам понадобится разделить его на несколько линий:
Как бы вы ни решили, сейчас вы можете создать новые объекты "car" через фабричную функцию Car. Поведение будет такое же, как если бы вы решили определить класс Car вручную и дать ему конструктор принимающий значения "color" и "mileage":
>>> color, mileage = my_car
>>> print(color, mileage)
red 3812.4
>>> print(*my_car)
red 3812.4
Несмотря на доступ к значениям сохранённым в именованном кортеже через их идентификатор, вы всё ещё можете обращаться к ним через их индекс. Это свойство именованных кортежей может быть использовано для их распаковки в обычный кортеж:
Вы даже можете получить красивое строковое отображение объектов бесплатно, что сэкономит вам время и спасёт от избыточности:
>>> my_car
Car(color='red' , mileage=3812.4)
Именованные кортежи, как и обычные кортежи, являются неизменяемыми. Когда вы попытаетесь перезаписать одно из их полей, вы получите исключение AttributeError:
>>> my_car.color = 'blue'
AttributeError: "can't set attribute"
Объекты именованных кортежей внутренне реализуются в питоне как обычные классы. Когда дело доходит до использованию памяти, то они так же "лучше", чем обычные классы и просто так же эффективны в использовании памяти как и обычные кортежи.
Хороший путь судить о них — считать, что именованные кортежи являются краткой формой для создания вручную эффективно работающего с памятью неизменяемого класса.
Наследование от именованных кортежей
Поскольку именованные кортежи построены на обычных классах, то вы можете добавить методы в класс, унаследованный от именованного кортежа.
>>> Car = namedtuple('Car', 'color mileage')
>>> class MyCarWithMethods(Car):
... def hexcolor(self):
... if self.color == 'red':
... return '#ff0000'
... else:
... return '#000000'
Сейчас мы можем создать объекты MyCarWithMethods и вызвать их метод hexcolor() так, как ожидалось:
>>> c = MyCarWithMethods('red', 1234)
>>> c.hexcolor()
'#ff0000'
Однако, это может быть немного неуклюже. Это может быть стоящим, если вы хотите получить класс с неизменяемыми свойствами. Но вы легко можете прострелить себе ногу в таком случае.
Например, добавление нового неизменяемого поля является каверзной операцией из-за того как именованные кортежи устроены внутри. Более простой путь создания иерархии именованных кортежей — использование свойства ._fields базового кортежа:
Встроенные вспомогательные методы именованного кортежа
Кроме свойства _fields каждый экземпляр именованного кортежа также предоставляет ещё несколько вспомогательных методов, которые вы можете найти полезными. Все их имена начинаются со знака подчёркивания, который говорит нам, что метод или свойство "приватное" и не является частью устоявшегося интерфейса класса или модуля.
Что касается именованных кортежей, то соглашение об именах начинающихся со знака подчёркивания здесь имеет другое значение: эти вспомогательные методы и свойства являются частью открытого интерфейса именованных кортежей. Символ подчёркивания в этих именах был использован для того, чтобы избежать коллизий имён с полями кортежа, определёнными пользователем. Так что, не стесняйтесь использовать их, если они вам понадобятся!
Я хочу показать вам несколько сценариев где вспомогательные методы именованного кортежа могут пригодиться. Давайте начнём с метода _asdict(). Он возвращает содержимое именованного кортежа в виде словаря:
Именованные кортежи могут быть простым способом для построения вашего кода, делая его более читаемым и обеспечивая лучшее структурирование ваших данных.
Я нахожу, например, что путь от узкоспециализированных структур данных с жёстко заданным форматом, таких как словари к именованным кортежам помогает мне выражать мои намерения более чисто. Часто когда я пытаюсь это рефакторить, то я магически достигаю лучших решений проблем, с которыми я сталкиваюсь.
Использование именованных кортежей вместо неструктурированных кортежей и словарей может также сделать жизнь моих коворкеров легче, потому что эти структуры данных помогают создавать данные понятным, самодокументирующимся(в достаточной степени) образом.
С другой стороны, я стараюсь не использовать именованные кортежи ради их пользы, если они не помогают мне писать чище, читабельнее и не делают код более лёгким в сопровождении. Слишком много хороших вещей может оказаться плохой вещью.
Однако, если вы используете их с осторожностью, то именованные кортежи, без сомнения, могут сделать ваш код на Python лучше и более выразительным.
Что нужно запомнить
collections.namedtuple — краткая форма для создания вручную эффективно работающего с памятью неизменяемого класса.
Именованные кортежи могут помочь сделать ваш код чище, обеспечивая вас более простыми в понимании структурами данных.
Именованные кортежи предоставляют несколько полезных вспомогательных методов которые начинаются с символа подчёркивания (_), но являются частью открытого интерфейса. Использовать их — это нормальная практика.