-
Пруф
У меня простое правило: кому нужны пруфы, тот сам их ищет.
Если раньше на вброс “ну и кто пишет на твоей Кложе” я начинал кидаться ссылками, то теперь отвечаю: найди ответ самостоятельно. Дил уыз ыт.
Я не Гугл и не Википедия. Если ты действительно хочешь знать, все ответы найдешь сам. Если троллишь, то мы обоюдно сэкономим время.
Предположим, я не знаю, кто пишет на Кложе. Значит ли это, что не пишет никто? Это как утверждать, что человек, с которым ты не знаком, не существует. Реальность-то никак не меняется.
Конечно, я делаю исключение если интерес собеседника искренний. Но случается это крайне редко.
-
Database optimization
Sometimes, especially on interviews, developers are asked about what would be their steps to fix a database that works slowly. That’s a really extensive subject to discuss. Here, I decided to summarize my experience. I hope it help to not only increase the performance but do that in acceptable time and with less misery.
First, of all, fixing the database queries is the last thing you should do. Fixing code first is premature optimization that is a root of all evil as we know. It could take huge amount of time whereas the real problem is kept in a wrong configuration file.
So in my prospective, the right steps to make your database more peppy are something like follows below. Keep in mind that it is not a technical tutorial with code snippets that you could paste and make the job done.
Ensure you use SSD drives. It sounds silly but still you should check it. SSD gives you up to 10 times performance boost immediately. Ensure that SSD drive is a highlighted feature in you hoster’s plan.
Check you database config before tweaking something else. Usually, the default configurations are set up poorly to make the DB work on cheap computers. Some distributions carry config templates which names include memory or CPU values. Read them, they are well-documented. Google for best practices when configuring your database. Read books.
Consider a case when the entire database is kept in memory. That also involves DBA and Ops teams so probably you won’t be allowed to perform such a move by your own. But since memory is cheat nowadays, it is possible. For backups, replicate such a database using streaming replications on servers that are run on hard drives. PostgreSQL supports various replication scenarios as well. You may also dump memory images into files and restore them on demand.
I used to work in a project where they ran 400Gb database into RAM and it worked quite fast.
Check you application uses connection pooling when communicating to the DB. Suddenly, such wildly spreaded frameworks as Django do not use them. On each HTTP request, they open a fresh connection and close it once the response has been sent. Now imagine your application serves 20 requests per second. Network communication will be quite expensive in that case!
Consider using either PGBouncer in front your Postgres installation or any library for your language that deals with reusable connections by its own. Say, for Clojure, HikariCP would be a good choice.
Everything that might be read from replica should be read from it, not the prod DB instance. Don’t run reports against the production. All the analytics and statistics should not touch it. Modern frameworks allow you to declare multiple database backends and switch between them on demand. For example, in Django, calling
.using('replica')
method on a Queryset instance dispatches a query into another database, not the default one.For quite complicated queries, use materialized views that may be turned into physical tables on demand. Querying them would be much faster than processing a query full of joins and aggregates every time. Setup a cron job to rebuild such views every night for example when the load is low.
Select only those fields that you really need to process a request. Avoid using
select * from table
since it takes extra time for the database to find out what fields to fetch exactly. With Django ORM, either specify needed fields with.only()
method or bypass some of them calling.defer()
. But keep in mind that touching a non-fetched field hits the database again.When writing unit tests, add a case that counts a number of database queries made during request. Sometimes, it leads to terrifying findings. Touching fields of foreign entities in a cycle without joining them initially may end up with up to 100 queries or more.
Be carefull when passing a collection of IDs as a parameter. On the database level, it turns into something like
STATEMENT: SELECT * FROM users WHERE id IN (?, ?, ?, <100 more of them>) PARAMETERS: $1=10035, $2=10036, $3=10037, $4=10038, $5=10039,...
When not limiting the number of such IDs, one may end up with 1000 of them what is quite harmful for the database. Either process them by chunks or try to compose a single query with raw SQL.
For fast data processing, don’t use ORM at all. Building a model instance from a low-lever tuple that the database driver returns is really costly. Even SQLAlchemy (Python’s the most powerful ORM) spends double time on building instances than reading data from psycopg2 (Python PostgreSQL driver) directly.
Denormalize your tables. Sometimes, it really worth moving any columns into foreign tables and joining them on demand. A good example might be ranking system. You’ve got a users table full of fields. Now you need to add rank field and recalculate it every 6 hours. Adding yet another fields would be a bad choice since our users table is already under load.
But having the
users_ranks
table that has a unique foreign key to a user and a rank value would be a good choice. Now that, we may rewrite it as we wish without touching actual user’s data. When needing to sort users by their rank, we join that table and order the result by rank as well.The same relates to a search document. When implementing full search on some entity, we need to build a special document known as a search vector. Storing that vector inside an entity is a bad choice because it is not the data but just technical information.
Join tables only by foreign and primary keys but not any custom conditions like strings equality, regex matching and so on. When you need such a join, probably it is better to fetch both parts of it and compose the result in your app’s code.
When a query really works badly, examine it with EXPLAIN operator. If you don’t understand its output completely, use online services that turn it into more human-friendly format. Ensure the query hits indexes. Don’t add multiple indexes since having them a lot may slow down writing procedure.
Finally, never be afraid of raw SQL. No matter how powerful your ORM is, check what queries it produces exactly. On my dev machine, I always start Postgres not as a service but as a process passing
-E
flag to print all the queries:postgres -E -D /usr/local/var/postgres
That gives me the whole vision of what’s going on with the database.
Summarizing all together:
- use modern hardware;
- configure your database properly;
- know all the shadowed parts of your ORM;
- rise your SQL skills.
-
Clojure in Highload Cup
1st September was the day when the Highload Cup competition has finished. I’m proud of I took participation in it. The Cup was a quite interesting event full of drive, enthusiasm and night coding. Although I haven’t taken any prize, I’ve got lots of fun and new knowledge that I’ve partially shared with you in my previous publications.
In that post, I’m going to highlight some technical details unmentioned before.
A minute of vanity: I’m a single developer who used Clojure/Datomic stack to implement a solution. And by the way a single member from my hometown Voronezh (my colleagues who live here argued on Cup passionately but still without a result).
The task was easy only at first glance. For non-Russian speakers, let me retell it quickly. You’ve got three entities: a user, a location and a visit. They represent, respectively, a physical person, a place in the world and a fact that somebody visited a place and put mark for it. Say, the last year John Doe visited Siberia and was so excited that he put 5 stars.
Your task is to ship a Docker container that carries a REST server. The server should implement basic CRUD operations on users, locations and visits. All the data pass in and out using JSON.
In addition to CRUD, there are two aggregate APIs. The first one is to return all the places visited by specific user. The second one is to return an average mark assigned to specific location by all the users who have ever visited it. Both APIs accept optional query string arguments to filter the results by foreign entities. Say,
fromAge
parameter stands for we need to consider only those people who are alder than that number,distanceTo
limits those locations with distances less than the passed value and so on.Once you’ve built a Docker container, you submit it to the central server where it is shot with a special software. It considers lots of such facts as proper response codes, incorrect data filtering, response time and so on. Than it calculates your rank. The less is the rank, the better your position is.
Sounds simple, but I spent a week trying to implement fast Clojure solution. TL/DR: finally, the C++ guys have come and taken the top of the rank table. Some of them wrote their own HTTP server. But still, it was quite fun to compete with them.
As the Cup has finished, you are welcome to review my code (it was private before due to Cup rules). The final version uses Clojure 1.9, Datomic free edition and clojure.spec to validate incoming data. There were some experiments with SQLite database kept in memory but at the end I finished with Datomic (more on that below).
So here are some technical details that I wanted to discuss.
Reading ZIP on the fly
According to the Cup’s rules, when your application starts, it finds the input data in
/tmp/data
directory. There is a single zip archive with JSON files inside. The Docker container is mount with read-only file system, so you cannot unzip it using standard Unix tools. Instead, you should read the data directly using streams.Thanks to Java, it ships
java.util.zip
package with all we need inside. Surprisingly, I ended up with quite short code to read the file:(defn read-zip [path] (let [zip (java.util.zip.ZipFile. path) entries (-> zip .entries enumeration-seq)] (for [e entries] (.getInputStream zip e))))
It accepts path to a zip file and returns a lazy sequence of input streams. Each stream might be read into a data structure with a function:
(defn read-stream [stream] (json/parse-stream (io/reader stream) true))
, where
json
is an alias to the Cheshire library included as[cheshire.core :as json]
at the top of the namespace.The data backend
Since the beginning it was obvious to keep the data in memory but not on the disk. I was thinking on whether I should use in-memory SQLite backend or use Datomic within in-memory storage. After all, I’ve tried both options and ended up with Datomic finally.
With SQLite, I’ve got only one trouble when connecting to the database. I described the problem in details in my previous post “In-Memory SQLite Database In Clojure”. For the rest, it worked fine. I used HugSQL to compose queries like this:
-- :name get-location-avg :? :1 select avg(v.mark) as avg from visits v /*~ (when (or (:fromAge params) (:toAge params) (:gender params)) */ join users u on v.user = u.id /*~ ) ~*/ where v.location = :location_id /*~ (when (:fromDate params) */ and v.visited_at > :fromDate /*~ ) ~*/ /*~ (when (:toDate params) */ and v.visited_at < :toDate /*~ ) ~*/ /*~ (when (:fromAge params) */ and u.birth_date < :fromAge /*~ ) ~*/ /*~ (when (:toAge params) */ and u.birth_date > :toAge /*~ ) ~*/ /*~ (when (:gender params) */ and u.gender = :gender /*~ ) ~*/
Then I switched to Datomic backend. I was wondering whether it would be slower than good old SQLite. The results were in favor of Datomic: it was about 1.5 times faster when returning responses.
For in-memory backend, you do not need a registered version or a license key. Just add
[com.datomic/datomic-free "0.9.5561.54"]
into dependencies list and I’ve done. Then pass something like"datomic:mem://highloadcup"
when connecting to the database.It was a good decision to create common functions for CRUD operations (create-user, update-user, etc). In fact, I had only three general functions to create, update and read for something, and the entity-specific functions became just partials on them.
Having that, I could quickly switch from SQLite-powered backed to Datomic.
The only think I’ve got stuck on was applying optional filters to a query. That became a reason to write “Conditional Queries in Datomic” article.
You may examine Datomic database backend in master branch whereas SQLite version lives in a self-titled branch.
JSON validation
A system that tests you server tends to send incorrect data. If you accept it without returning
400 Bad Request
status you will get penalty score. So the validation is a major part of our application.Before, I used Schema module for that purpose. I know it well including some of its shadowed parts. But having Clojure 1.9 on board was a great chance to try clojure.spec that is still in alpha but works great.
After some REPL experiments, I ended up with my own
highloadcup.spec
namespace that carried wrappers around the original spec. One of them isvalidate
function that does the following:- validates the data against a spec;
- coerces string numbers into integers when needed;
- returns
nil
when the data is invalid.
Its code is
(ns highloadcup.spec (:require [clojure.spec.alpha :as s])) (def invalid :clojure.spec.alpha/invalid) (defn validate [spec value] (let [result (s/conform spec value)] (when-not (= result invalid) result)))
Pay attention it’s a good practice to declare
invalid
constant at the top. Once the library becomes stable, its namespace will get rid of “alpha”.Another point, spec was designed to be used with full-qualified keys. But in my case, all the keys were without namespaces. That’s normal for non-Clojure applications. Declare your specs as usual, but once you compose a map of them, pass
:opt-un
parameter (stands for “unqualified”):(def enum-gender #{"m" "f"}) (s/def :user/id int?) (s/def :user/email string?) (s/def :user/first_name string?) (s/def :user/last_name string?) (s/def :user/gender enum-gender) (s/def :user/birth_date int?) (s/def :user/create (s/keys :req-un [:user/id :user/email :user/first_name :user/last_name :user/gender :user/birth_date]))
This is a spec for creating a user where every field is required. For updating a user, there is a similar spec with all the fields optional:
(s/def :user/update (s/keys :opt-un [:user/email :user/first_name :user/last_name :user/gender :user/birth_date]))
When applying query string filters, they are all plain strings even when represent numbers. Turning them to the proper type is also knowing as coercion. To coerce a string value during validation, use
conformer
:(defn x-integer? [x] (if (integer? x) x (if (string? x) (try (Integer/parseInt x) (catch Exception e invalid)) invalid))) (def ->int (s/conformer x-integer?)) (s/def :opt.visits/fromDate ->int) (s/def :opt.visits/toDate ->int) (s/def :opt.visits/country string?) (s/def :opt.visits/toDistance ->int) (s/def :opt.visits/params (s/keys :opt-un [:opt.visits/fromDate :opt.visits/toDate :opt.visits/country :opt.visits/toDistance]))
Now, then you validate parameters taken from the Ring request against
:opt.visits/params
spec, all the numbers represented with strings will be turned into integers as well.Docker
Let’s talk a bit about building a Docker container. I don’t see any reason to compile uberjar inside Docker. It’s Java so that it is “compiled once, works everywhere” (usually I’m sceptical on that point, but not now). All you need is to copy an uberjar file into container and setup CMD properly.
Do not use the official Java template. Under the hood, it ships OpenJDK that is 1.5 times slower than OracleJDK, unfortunately. So I had to inherit my image from Vlad Frolov’s one. I know that’s illegal to distribute Java runtime as a part of your application. But the difference in score was more important these days.
JVM flags also could help to tweak performance. The web pages I’ve found googling for “java docker” said that Java has troubles detecting heap size when running in Docker. So at least
"-Xmx"
and"-Xms"
should be specified. Next two,"-da"
and"-dsa"
reduce all the assert statements. See the rest of flags in my Dockerfile.All the project duties (lein, docker, files) should be automated with good old Make utility. Ideally, the default target should build the entire project from scratch.
Acknowledgments
I want to thank Mail.ru team who were responsible for handling that Cup. They’ve done really huge amount of work. I thought they didn’t sleep at all. During the three weeks term, they’ve been answering all the questions in chat, fixing the infrastructure, solving bugs, writing wiki and adapting the official website.
Thank you guys, your are real professionals!
PS to my Russian readers: you are welcome to share that post with your foreign colleagues. Next time, let them join Highload Cup too!
-
Мобильное приложение
Ох, что я в интернете прочел.
Значит, проблема: люди тупят в телефон на ходу. Спотыкаются, врезаются в столбы, падают в фонтаны.
Знаете, что предлагает автор? Готовы? Написать мобильное приложение, которое будет читать камеру, распознавать препятствия и сигналить пользователю.
Похоже, автор (дизайнер) не читал параграф Лебедева об идее на минус миллион. Это когда дизайнер думает, что идея офигенна только потому, что он ее придумал, а проблемы бизнеса не его дело.
Давайте представим такое приложение. Батарея будет садиться в один момент. Чтение камеры это дорогая операция, а распознавание изображений еще дороже. При этом нужно, чтобы не подвисало основное приложение, с которым пользователь сейчас работает.
Это технические трудности, предположим, мы их решили.
При ходьбе камера смотрит почти отвесно вниз, поля обзора хватит на два метра вперед. Это расстояние человек пройдет за две секунды. Даже если приложение справится за 0.1 секунды, бедняга не успеет остановиться.
Пользователь же не просто смотрит в экран, он мысленно погружается в контекст. Вот идет Вася, читает Вконтактик. Маша пишет: приходи ко мне ночевать. Все, у Васи мысли о сексе, кровь отлила от мозга в пах. А через два метра открытый люк.
Поможет ли Васе приложение? Сомневаюсь.
Как различать опасности? Это может быть и бордюр, и сливная решетка, и кусок брусчатки, приподнятый на сантиметр и потому невидимый приложению.
В толпе люди разделены метром. Человек впереди это препятствие?
Даже если представить, что приложение использует новейшие технологии (нейросети, глубокое обучение), это делает его хуже. Оно даст ложное чувство защиты. Человек будет выходить на середину дороги и попадать под автобус.
В странах с развитой судебной системой производителя завалят исками. Люди будут так же падать, разбивать носы, но вдобавок отсуживать суммы за ущерб здоровью.
В защиту этой ахинеи можно сказать, что для слепых делают что-то отдаленно похожее. Такие перчатки-локаторы чтобы надевать на запястье. Внутри Ардуино, эхо-локатор и пуговица-вибратор. Ультразвук отражается от препятствия. Если дистанция меньше метра, вибратор дрожит.
Даже с этим девайсом масса проблем, при том что слепой каждую секунду слушает сигнал и все мысли его о том, как бы не упасть. И дальность локатора 15 метров против двух у камеры.
Вывод: если мобильный телефон приносит проблемы, нужно убрать телефон. Делать мобильное приложение – что-то за гранью мыслимого. Хуже, чем лечить рак легких щадящими сигаретами.
-
Центрирование курсора
В редакторах или IDE крайне редко встречается следующая функция.
Предположим, я дописываю код в конец файла. Если кода больше, чем один экран, текст можно набирать только внизу. Промотать редактор на середину нельзя – файл закончился, скролл вниз не работает.
Это страшно раздражает. Я привык, что глаза смотрят в центр экрана. Гораздо чаще мы редактируем старый код нежели пишем новый, поэтому курсор в центре. Долго смотреть вниз неудобно, устают глаза и шея.
Слева на картинке Саблайм. Файл закончился, набирать текст приходится внизу, смотаться нельзя. Емакс проматывается в середину сам. Сочетание Ctrl+L центрирует буфер по требованию, повторное нажатие перематывает вверх, оставляя одну строчку. Обратите внимание, что буфер кончился, слева нет нумерации строк, но для перемотки это не проблема.
В VS Code смотаться можно, но при редактировании внизу он проматывает на одну строчку. Приходится мотать вручную, выполняя работу машины. Написал-промотал. Емакс же сразу прыгает в центр экрана.
IDEA по умолчанию оставляет задел в пять пустых строчек в конце файла, но ниже нельзя. Нормальная промотка включается где-то в недрах настроек. Опять же, нет авто-центрирования.
Раньше я боролся с напастью так: зажимал Энтер и вставлял 50 переносов. Так я мог проматывать файл ниже его логического конца, а с точки зрения редактора от все еще продолжался. Время от времени эта дичь попадала в коммиты. Был и другой вариант – сплющить окно редактора, чтобы оно доходило только до середины экрана.
Почему об этом думали в 1970-бородатом году, а сейчас никого не волнует?
-
Вконтакт
Не могу передать, как меня бесит ВК. В нем все плохо. Дурацкое название с предлогом. Убогий дизайн, когда нажимаешь на выпадашку, а там одна опция. Но главная беда как раз в том, ради чего это это создавалось – в общении.
Предположим, хочу повесить объявление в группе жильцов ЖК. В шапке группы жирная кнопка “написать сообщение”. Открывается диалог:
Минуточку, мне нужно, чтобы отвечали все, а не только администраторы. Или остальные тоже могут? Как понять?
Что за ссылка “перейти к диалогу с сообществом”? А сейчас что, не диалог? На всякий случай нажал, там пустой экран. Ясно, этим не пользуются. Зачем ссылка?
В группе есть “обсуждения”: жильцы второго дома, любители котят и тд. Этакий форум из нулевых. Объявление в одном обсуждении не увидят в другом.
Сбоку ссылка на чат. Что-то вроде Телеграма, только внутри ВК. Слева личные сообщения.
Черт, как разместить текст, чтобы его прочли все жильцы?
Все засунуто под выпадашки, даже когда места полно. Всюду “еще”, “подробней” и прочая импотенция.
Что за дизайнеры верстали все это? Кто это проектировал?
Я слышу: сперва добейся, потом предъявляй. Но совершенно верно сказал Лебедев: если известная субстанция приносит много денег, быть ею от этого она не перестает.
-
Дети
Чтобы ребенку было интересно, нужно, чтобы родителю было интересно. Чтобы ребенку нравилось, нужно, чтобы родителю тоже нравилось.
Удивительно, как много взрослых не понимают этой банальности. Они считают, что ребенку можно на весь день включить мультики и забыть о его существовании. Что можно подарить книгу, и типа читай сам. Что можно подарить китайское говно взамен качественной вещи.
Дети все прекрасно понимают. В силу физиологии они не могут развернуто сформировать претензию или обиду. Но халтуру чуют за версту. Это особенно заметно в период с 2 до 3 лет, когда речь еще толком не поставлена, но уже есть устойчивые вкусы и понимание всего, что происходит вокруг.
Чтобы ребенок с упоением читал, нужно читать ему каждый день несколько лет. Чтобы с интересом смотрел мультик и понимал сюжет, нужно смотреть вместе с ним.
Качественная детская продукция мало чем отличается от аналогов для взрослых. Если это игрушка, она должна иметь приятные пропорции, быть устойчивой к неаккуратному обращению, не звучать слишком громко. Если техника, то быть правильно спроектированной, с понятным дизайном, качественной сборкой.
Если это мультфильм, то повествование должно быть последовательным, проблемы персонажей понятны, хорошо прослеживается главная мысль: обманывать нехорошо или если накосячил, то исправь.
Никогда не делайте скидку на то, что это ребенок. Он такой же человек, как и вы. Критерии качества у детей такие же высокие.
Остается печальный случай, когда ребенок потребляет халтуру (вещи, информацию) из-за отсутствия вкуса у родителей. Увы, эта проблема неразрешима и будет передана на поколение вперед.
Может, и ваш ребенок одупляет телевизор третий час, пока вы это читаете?
-
Ворчание ягнят
Никита и Рахим запустили сайт, где можно посетовать на дебильный интерфейс. Встречайте, Ворчание ягнят. Написан на Кложе.
Дизайн не моя стихия, и писать в блог про каждую кривую кнопку мне кажется лишним. Это скорее для Твиттера, но я с ним не дружен. Так что все наболевшее шлю в приват Никите, а он обещал запостить.
Подписывайтесь по РСС, там будет изредка мелькать и от моего имени.
UPD стрим в процессе создания:
-
Фаерфокс
Мне кажется, в недалеком будущем Фаерфокс умрет. Не полностью и не буквально, конечно. Просто сократит долю рынка настолько, что станет неликвидным. Ему присвоят звание “жемчужины опен-сорса”, гики будут стенать, причитать и сидеть на неподдерживаемой версии трехлетней давности, как это случилось с Оперой после перехода на Веб-кит. На поддержке останутся FF-only легаси-системы.
Кто-то сказал, что браузеры – продукт массового потребления. В целом, они все работают хорошо и покрывают нужды рядового пользователя. Уже нет проблем с отрисовкой элементов. Основные усилия уходят на то, чтобы еще теснее интегрировать браузер с операционной системой и сервисами компаний.
В этом плане все в порядке у Хрома, Сафари и Эджа (который Эксплорер). Гугл вливает в браузер колоссальные деньги, поскольку Хром – это окно во все сервисы: рекламу, почту, карты. Сафари связан с Маком и системой Эпла. Эдж, понятно, по самые уши интегрирован с Виндой, Бингом, Икс-боксом, офисным пакетом.
Одного браузера недостаточно, нужны сервисы. Свои некоторые программы Гугл подкрепляет расширениями в Хроме. Так, при открытии Гугло-дока Хром молча ставит расширения Google Docs, Google Sheets и другие. Так они работают быстрее. А с чем интегрироваться Фаерфоксу?
На рынке могут появиться разработки для снобов вроде Вивальди. Они обязательно снискают своего пользователя, но никогда не выйдут на массовый рынок. Потенциально новый браузер могут предложить только гиганты вроде Амазона или Фейсбука.
Лично мои претензии к Фаерфоксу в том, что во-первых, он работает медленней Хрома. Пусть меньше загружены процессор и память, мое время дороже. Второе, Фаерфокс весь из себя для гиков. Это слегка умиляет, но в долгосрочной перспективе раздражает, потому что браузер нужен не только для работы, но и развлечения.
-
Реальность
В плане профессии я знаю очень мало.
На Си и Ассемблере писал только в институте, уже ничего не помню.
Плаваю в указателях, памяти, куче. Код на C++ не понимаю.
О нейросетях и машин-лернинге имею самые смутные представления.
Про Биткоин читал только пару бульварных статей.
Знаю три команды из Гита. Путаю, что куда мерджить.
Накупил книжек, поставил пылиться на полку.
Не напишу и пяти строчек на баше без интернетов.
Не дочитал SICP. Хотел сделать все практические задания, слился.
Кнут только в самых влажных мечтах.
Компилятор или язык в жизни не осилю.
Пилил свой проект, бросил.
Пишу на Кложе, но в Джаве ни бум-бум. На Кложе потому, что язык редкий.
Всю жизнь клепаю формочки к базе.
Ненавижу JS, потому что не понимаю его, или наоборот.
Емакс и Лисп чтобы быть не таким как все.
Говорю по-английски с проблемами. Ссу провести вебинар на английском.
Гуглю каждый HTML-тег. Копипащу со Стека.
Умничаю в чате. Высмеиваю мессаджеры, сам же ими пользуюсь.
Не знаю нужных хоткеев, не ставлю клевых программ.
Не трекаю время, непродуктивен. Прокрастинирую.
Разбираюсь только в Лиспе, Мейк-файлах и немного в Постгресе. Тем и живу.