10 мая 2013 года я завёл репозиторий, чтобы перевести доклад Рича Хикки на русский. Доклад назывался Clojure Concurrency. План начинался с субтитров и кончался русской озвучкой. В README до сих пор лежит таблица готовности, которую я в него вписал: английские субтитры 5%, перевод на русский 0%, озвучка 0%. До 99% дошли 2 строки. Это те, где я восстанавливал его артефакты: слайды плюс исходник программы, которую он показывал. Последний раз я трогал этот файл 23 октября 2013 года.
Та неделя записана и в других репозиториях. 11 мая я добавил фоновый демон в java-сервер, над которым тогда работал. 15 мая в 21:09 закоммитил в тот же сервер правку с заголовком “Add removeClientHandler and add synchronized collections”.
То есть неделю я переводил доклад про конкурентность и писал ровно то, против чего он направлен. Доклад со списком это другой доклад, Simple Made Easy, Strange Loop, сентябрь того же года. Его словарём я пользуюсь с 2013 года и ни разу не прогнал его список по своему коду.
Что дал коммит 15 мая
В Simple Made Easy на 00:28:50 есть слайд с заголовком What’s in your Toolkit. В одну колонку он кладёт конструкции, которые сплетают, в другую замены попроще. Со стороны сложности: состояние, объекты, методы, наследование, switch, переменные, циклы, ORM и условия. На слайде Debugging, между 00:15:32 и 00:17:14, он даёт имя тому, что дают тесты. “All of our guardrails will have failed us.”
Тот сервер был командным репозиторием. Класс, о котором речь, не мой. Это был синглтон, державший 4 изменяемые коллекции. Каждый метод, менявший любую из них, делал это внутри synchronized. Геттеры отдавали живое представление вместо копии.
BenchDemon мой, добавлен 11 мая, 63 строки, под всеми стоит моё имя. Его работа обходит одно из таких представлений, bench.addAll(Beeper.getInstance().getBench().keySet()), в своём потоке и без замка. Через 4 дня мой коммит поменял другую сторону. Он вынул блоки synchronized и поставил на их место Collections.synchronizedSet и Hashtable. Каждая отдельная операция стала атомарной. Мой обход остался ровно таким же несинхронизированным.
Я собрал обе формы заново и померил на javac и java 21.0.12, на 4 ядрах. Пишущий поток добавляет и удаляет. Читающий поток обходит коллекцию 50000 раз. Считаю чтения, которые упали. Пауза это то, сколько пишущий ждёт между мутациями. В каждой ячейке 5 прогонов.
| пауза записи | до 15 мая | после 15 мая | копия под замком |
|---|---|---|---|
| нет | от 80.1 до 88.4% | от 79.7 до 90.7% | 0 |
| 10 мкс | от 0.60 до 0.82% | от 0.60 до 0.99% | 0 |
| 1 мс | от 23 до 25 падений | от 23 до 25 падений | 0 |
Каждое падение это ConcurrentModificationException. Верхняя строка говорит больше про планировщик, чем про мой код. Она расходится на 8-11 пунктов между прогонами одного и того же класс-файла. На 1 мс, при самой медленной записи, какую я мерил, обе формы падают около 24 раз на 50000 чтений и между собой не различаются. Эта запись всё равно куда занятее, чем тот сервер когда-либо был, потому что демон спит между проходами 1000 мс. Копия под замком не упала ни в одном из 15 замеров.
В сервере, каким он был, ничего этого всё равно случиться не могло. Main запускает сетевой сервер и больше ничего. new BenchDemon встречается только в тестах, а они зовут метод напрямую, без потока. Обе группы netty собраны с 1 потоком каждая, поэтому все обработчики шли по одному event loop. Защиту от конкурентности я написал для программы, обработчики которой никогда не шли параллельно. И так и не запустил ту часть, которой защита понадобилась бы.
Список, сегодня
В 2026 году набирает здесь агент, а правила, по которым он это делает, пишу я. Значит корпус записывает мои требования. Моих привычек в нём нет.
Строки его таблицы я считал через TypeScript compiler API, 5.9.3 на node 22.23.1. Регулярка не отличит for внутри строки от for в коде. Для 2013 года взял javalang 0.13.0 на python 3.12.3. Всё снято на коммите d287db2, git 2.43.0. Старые репозитории командные, поэтому сначала атрибуция: 42 java-файла и 2268 строк, из них blame отдаёт мне 51.4%. В счёт идут только файлы, где моего больше половины. Остаётся 24 файла и 1200 строк.
| java, 2013, 1200 строк | ts и js, 2026, 15632 строки | |
|---|---|---|
| объекты | 23 класса, 107 методов | 0 классов, 0 методов класса |
| наследование | 4 extends | 0 |
| switch | 1 | 1 |
| переменные | 102 локальные, из них final 0 | 3071 const против 621 let |
Последняя ячейка это 83.2% по всему корпусу. В тех 2303 строках, которые я держу и переписываю, счёт 373 против 34, то есть 91.6%. Рядом с 0 final в 2013 году это выглядит как 13 лет следования его совету. Было не так. Правила про const я не писал. Линтера, который бы его требовал, здесь нет, а tsc --noEmit на это вообще не смотрит. Он берётся оттуда, что так пишут на TypeScript в 2026 году. И оттуда, что пишу я мелкие скрипты на Node, где классу просто неоткуда взяться. Эти строки вышли чистыми, решения не принимал никто.
Строка про ORM показывает 0 аннотаций. Зато pom.xml тянет spring-data-jpa вместе с hibernate-entitymanager, а в XML спринга подняты фабрика менеджера сущностей и транзакционный менеджер JPA. Конфигурацию написали, а разметку так и не написал никто.
Единственная строка, которая всё ещё задаёт вопрос
Условия это строка, где решение встаёт на каждой строчке кода. Взамен он предлагает правила. Код 2026 года я разделил надвое. То, что держу и переписываю, это 20 файлов. Скрипты замеров, которые прогоняю один раз и бросаю, это 89.
В живом коде 16.7 ветвления на 100 строк. В выбрасываемых скриптах 14.6. Медианы по файлам говорят то же, 17.0 против 15.0, значит сумму не тащит один жирный файл. На единственной строке его таблицы, которая всё ещё чего-то мне стоит, плотнее оказался тот код, которому предстоит выжить, то есть наоборот. Ветвление здесь это if, тернарник, switch и короткое замыкание, взятые из синтаксического дерева. Пару дней назад я публиковал по этому же репозиторию 21.9, счётом по ключевым словам, по другому набору путей и вместе с циклами.
Мест, где я сделал по его совету, ровно 1. Попал я туда случайно. В src/lib/tells.ts лежат 19 механических проверок моего текста. 15 из них это записи в таблице данных, в каждой идентификатор и регулярка. В тех 53 строках 0 ветвлений. У файла 8.1 ветвления на 100 строк. Это меньше всех среди моих файлов от 100 строк, а следующий снизу 12.0.
Остальные 4 в таблицу не легли. Это 3 проверки тире плюс проверка сращивания слов дефисом. Каждой нужно то, чего в самой найденной строке нет, вроде длины предложения или языка, на котором она написана. Они живут в 2 функциях на 40 строк с 12 ветвлениями. Выходит 3.5 строки на правило внутри таблицы против 10 строк и 3 ветвлений на правило снаружи.
То, что они данные, не сделало их верными. Пару дней назад я опубликовал, как 6 из этих проверок сверили с тем, что обещает руководство. Разошлись 2, обе это строки таблицы. Сравнение слабое, потому что 5 из 6 сверенных были строками таблицы, но другого у меня нет. Проверки тире это 3 из тех 4, а руководство пересказывает их прозой в 5 документах помимо кода.
Чего я не проверял
Есть ли 1200 строк выборка хоть чего-нибудь. Считается ли курс по Clojure. Не считается: blame отдаёт мне все 850 строк, но репозиторий собран одним коммитом. Отделить мои решения от скелета, который выдал курс, он не может. Ещё 2 популяции кода 2026 года различаются не только сроком жизни, но и по существу. Скрипты замеров это длинные однократные молотилки данных, а живое это мелкие связующие модули. Плотность ветвлений я нарочно не поставил в колонку 2013 года, потому что java той поры тащит геттеры и сеттеры, которых в ts нет.
Эту строку я цитирую годами, а применил один раз, на 53 строках, не заметив. Перевод так и стоит на 0%. Машина, которая пишет эти эссе, доделала бы те субтитры за вечер. Отдельный вопрос: будет ли это применением его совета?