dimhold.by
← Артыкулы

15 гадоў пасля Simple Made Easy: у 2013 годзе ніводная з маіх 102 лакальных зменных не была final, цяпер у жывым кодзе 91.6% гэта const. Я не выбіраў ні таго ні другога

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 замераў.

beeper-server, май 2013 bench = HashMap synchronized (bench) addUserToBench() removeUserFromBench() getBench() аддае жывое прадстаўленне BenchDemon, свой паток processBench() addAll(.keySet()) без замка чытанняў, што ўпалі, на 50000, пісьменнік раз на 1 мс да 15 мая ад 23 да 25 пасля 15 мая ад 23 да 25 копія пад замком 0
Дзе стаіць замок і куды ідзе чытанне. Абодва метады, якія мяняюць карту, працуюць унутры synchronized. Дэман, які яе абыходзіць, не, бо гетэр аддае жывое прадстаўленне. Падзенні на 50000 чытанняў пры паўзе пісьменніка 1 мс, 5 прагонаў.

У тым выглядзе, у якім сервер існаваў, нічога гэтага ўсё роўна здарыцца не магло. 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 extends0
switch11
зменныя102 лакальныя, з іх final 03071 const супраць 621 let

Апошняя клетка гэта 83.2% па ўсім корпусе. У тых 2303 радках, якія я трымаю і перапісваю, лік 373 супраць 34, то бок 91.6%. Побач з 0 final у 2013 годзе гэта выглядае так, быццам я 13 гадоў трымаўся яго парады. Было не так. Правіла пра const я не пісаў. Лінтара, які б яго патрабаваў, тут няма, а tsc --noEmit на гэта ўвогуле не глядзіць. const бярэцца адтуль, што так пішуць на TypeScript у 2026 годзе. І адтуль, што пішу я дробныя скрыпты на Node, дзе класу проста няма адкуль узяцца. Гэтыя радкі выйшлі чыстымі і ніхто пры гэтым нічога не вырашаў.

Радок пра ORM паказвае 0 анатацый. Затое pom.xml цягне spring-data-jpa разам з hibernate-entitymanager, а ў XML Spring паднятыя фабрыка менеджара сутнасцей і транзакцыйны менеджар 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 разгалінаванняў на правіла звонку.

src/lib/tells.ts, 19 праверак данымі 15 правіл, 53 радкоў, 0 разгалінаванняў рукамі 4 правілы, 40 радкоў, 12 разгалінаванняў радкоў на правіла 3.5 10.0 тыя 4, што не ўлезлі: 3 правілы працяжніка і правіла зрошчвання
19 механічных праверак майго тэксту, падзеленыя па спосабе запісу. 15 гэта радкі даных і кожная каштуе 3.5 радка без ніводнага разгалінавання. Тыя 4, што не ўлезлі, каштуюць 10 радкоў і 3 галінаванні кожнае.

Тое, што яны даныя, не зрабіла іх правільнымі. Пару дзён таму я апублікаваў, як 6 з гэтых праверак зверылі з тым, што абяцае інструкцыя. Разышліся 2, абедзве гэта радкі табліцы. Параўнанне слабое, бо 5 з 6 звераных былі радкамі табліцы, але іншага ў мяне няма. Праверкі працяжніка гэта 3 з тых 4, а інструкцыя пераказвае іх прозай у 5 дакументах апроч кода.

Чаго я не правяраў

Ці ёсць 1200 радкоў выбаркай хоць чаго-небудзь. Ці лічыцца курс па Clojure. Не лічыцца: blame аддае мне ўсе 850 радкоў, але рэпазіторый сабраны адным камітам. Аддзяліць мае рашэнні ад шкілета, які выдаў курс, ён не можа. Яшчэ 2 папуляцыі кода 2026 года адрозніваюцца не толькі тэрмінам жыцця, але і сутнасцю. Скрыпты замераў гэта доўгія аднаразовыя малатарні даных, а жывое гэта дробныя злучальныя модулі. Шчыльнасць разгалінаванняў я наўмысна не паставіў у калонку 2013 года, бо java той пары цягне гетэры і сетэры, якіх у ts няма.

Гэты радок я цытую гадамі, а ўжыў адзін раз, на 53 радках, не заўважыўшы. Пераклад так і стаіць на 0%. Машына, якая піша гэтыя эсэ, дарабіла б тыя субтытры за вечар. Асобнае пытанне: ці будзе гэта ўжываннем яго парады?