Деревья переходов, deprecation и миграция
Добавление свойства следует по ребру перехода или создаёт его к новому Map; дерево ветвится по порядку добавления, по смене elements kind и по обобщению representation поля. Когда representation должен расшириться по всем экземплярам, старый Map помечается deprecated
Вы уже знаете из обзора, что «порядок свойств важен». Но вот что удивляет даже сеньоров: Map может стать deprecated — помеченным мёртвым, — пока объекты всё ещё указывают на него, и V8 тихо переписывает эти объекты на более новую раскладку при следующем обращении к ним. Присваивание 2.5 полю, которое раньше держало целые, может незаметно перестроить форму каждого объекта, разделяющего её. Этот урок — о том, как граф форм растёт, мутирует и заживает.
Переходы — это рёбра в общем дереве
Вспомним однострочную версию из browser/03-v8-internals/03-hidden-classes: добавление x затем y приходит в другой лист, чем добавление y затем x, так что порядок важен. Теперь механизм. Каждый Map хранит forward-TransitionArray с ключом {имя свойства, attributes}. Добавление свойства:
- Ищет ребро
{имя, attributes}в TransitionArray текущего Map. - Если ребро есть, следует по нему — никакой аллокации, вы переиспользуете существующий дочерний Map и его (общий) DescriptorArray.
- Если ребра нет, создаёт новый дочерний Map (расширяя DescriptorArray родителя на один дескриптор), записывает ребро и направляет объект на новый Map.
Поскольку дерево разделяется всем изолятом, второй объект, идущий по пути, бесплатен — он следует по рёбрам, созданным первым объектом. Поэтому строить объекты одинаково дёшево, а строить их в перемешанных порядках — нет: перемешанные порядки никогда не находят ребро заново, так что каждый объект вынуждает свежий Map.
const a = {}; a.x = 1; a.y = 2; // empty -> +x -> +x,y (создаёт 2 ребра)
const b = {}; b.x = 3; b.y = 4; // empty -> +x -> +x,y (следует по ним; 0 новых Map)
const c = {}; c.y = 5; c.x = 6; // empty -> +y -> +y,x (новая ветка; 2 новых Map)Ещё три способа перейти (не только добавление свойств)
Обзор намекал, что переходы приходят лишь от добавления свойств. Это не так. Есть три других триггера, и они кусаются в проде:
- Смена elements kind. Запись float’а в массив
PACKED_SMI_ELEMENTSили создание дырки переводит Map массива в более широкий elements kind (PACKED_SMI → PACKED_DOUBLE → PACKED_ELEMENTSи HOLEY-варианты). Односторонне, как и дерево свойств. - Обобщение representation поля. Это главное. Поле стартует узким — скажем,
Smi. Если позже какой-то экземпляр сохранит значение, которое не помещается (2.5или строку), representation поля должен расшириться доDoubleилиTagged. Но representation — это свойство Map, общее для всех экземпляров. Так что V8 не может изменить один объект; он должен обобщить Map. - Потеря constness. Поле
const, пока каждый экземпляр присваивал ему одно и то же значение. Первый экземпляр, присвоивший другое значение, переводит дескриптор вmutable, что само по себе переход (скомпилированный код, заинлайнивший константу, должен быть инвалидирован).
Все три «не-добавляющих» триггера объединяет одно: изменение того, что поле может держать, вынуждает менять общий Map, а не отдельные объекты. Без этого понимания можно часами грешить на GC или сеть, когда настоящая причина — одно присваивание float’а двумя фреймами выше.
Deprecation и миграция: как V8 расширяет общее поле
Вот тонкая часть. Пусть 10000 объектов разделяют Map M1, где поле temp имеет representation Smi. Теперь один объект делает obj.temp = 98.6. Поле должно стать Double — но для всей формы, потому что Map общий. V8 не может переписать 10000 объектов в этот миг (у него даже нет их списка). Вместо этого:
- Он создаёт новый Map
M2, идентичныйM1, кроме того, чтоtemp—Double(более общий representation). Разделение происходит в нужном месте дерева, потомки перенаправляются. - Он помечает
M1как deprecated — мёртвый Map, который больше не следует использовать. Существующие объекты пока всё ещё указывают наM1. - Миграция ленива. В следующий раз, когда V8 трогает один из этих объектов по медленному пути (доступ к свойству с промахом inline cache,
%DebugPrintи т. п.), он замечает, что Map объекта deprecated, идёт к соответствующему не-deprecated Map (M2) по back-pointer’ам из урока 01, переписывает раскладку объекта на месте (упаковывая целое в double-слот) и обновляет указатель map объекта. Это путь «MigrationMarker» / миграции.
Так что единственное obj.temp = 98.6 может начать deprecation формы, используемой тысячами объектов, каждый из которых платит небольшую стоимость миграции при следующем доступе. Если это поле колеблется между целым и float’ом, можно повторно churn’ить deprecation — настоящий, трудно замечаемый источник деоптов.
- Ключ поиска перехода
- {имя свойства, attributes}
- Повторный проход существующего пути
- 0 новых Map — рёбра следуются
- Порядок расширения representation
- Smi → Double → Tagged (односторонне)
- Старый Map после обобщения
- помечен deprecated
- Миграция экземпляра
- лениво, при следующем медленном доступе
- Поиск живого Map
- идти по back-pointer'ам к не-deprecated потомку
- Трассировка переходов
- --trace-maps в d8 / node
Почему перемешанные порядки ключей патологичны
Сложите кусочки. Функция, строящая объекты итерацией ключей входа в произвольном порядке (ре-сериализатор JSON, обобщённый mapper, генерированный код), создаёт новую ветку дерева переходов почти на каждый различный порядок, который видит. Формы никогда не повторяются, поэтому:
- Каждый объект аллоцирует свежие Map и DescriptorArray (раздувание памяти из урока 01).
- Нижестоящий inline cache, читающий эти объекты, видит свежий Map почти каждый раз → он становится polymorphic, затем megamorphic, затем сдаётся (урок 05).
- TurboFan не может специализироваться на стабильной форме, так что горячая функция либо не достигает верхнего яруса, либо повторно деоптится.
Фикс всегда один: строй по фиксированной схеме в постоянном порядке (отсутствующие поля по умолчанию null) или храни действительно динамические наборы ключей в коллекции Map, созданной для произвольных ключей.
10000 объектов разделяют Map с полем `score`, представленным как `Smi`. Один объект делает `o.score = 1.5`. Что происходит с общим Map?
Объект `a` создан `{}; a.p=1; a.q=2`. Объект `b` затем создаётся так же. Сколько новых объектов Map аллоцирует создание `b`?
Расставьте по порядку, что делает V8, когда один экземпляр присваивает float поле, которое общий Map записывает как Smi.
- 1 Обнаружить, что новое значение не помещается в текущий representation поля (Smi)
- 2 Создать новый Map, идентичный, кроме того, что поле обобщено до Double
- 3 Пометить старый Map deprecated, чтобы он перестал использоваться для новых объектов
- 4 Оставить существующие экземпляры пока указывающими на deprecated Map
- 5 При следующем медленном доступе каждого экземпляра идти по back-pointer'ам к живому Map и мигрировать его на месте
▸Граничные случаи
Числа — не единственное обобщение. Присваивание объекта кучи туда, где поле раньше держало лишь Smi, обобщает representation до Tagged (самого общего), что навсегда отключает часть оптимизаций распаковки TurboFan для этого поля. Поле, держащее null для «отсутствует» и объект для «присутствует», уже Tagged с первой не-Smi-записи — обычно это нормально, но знайте, что это закрывает быстрый путь распакованного целого. Если поле горячее и числовое, держите его числовым.
- 01Назовите все виды событий, создающих переход к новому Map, помимо добавления свойства.
- 02Объясните deprecation и ленивую миграцию шаг за шагом.
- 03Почему построение объектов итерацией ключей входа в произвольном порядке разрушает производительность, в терминах дерева переходов?
Переход — это ребро в дереве форм, разделяемом всем изолятом. Добавление свойства ищет ребро {имя, attributes} в TransitionArray текущего Map и следует по нему (бесплатно) или создаёт его (один новый дочерний Map, расширяющий DescriptorArray родителя). Но три других события тоже дают переход: расширение elements kind массива, обобщение representation поля (Smi → Double → Tagged) и потеря полем constness. Обобщение representation особое, потому что representation принадлежит общему Map: V8 создаёт обобщённый Map, помечает старый deprecated и мигрирует каждый экземпляр лениво — при следующем медленном доступе он идёт по back-pointer’ам к живому Map и переписывает хранилище на месте. Построение объектов в перемешанных порядках ключей бесконечно ветвит дерево, раздувает память Map, гонит нижестоящий inline cache в megamorphic и лишает TurboFan стабильной формы; фикс — порядок создания по фиксированной схеме или коллекция Map для действительно динамических ключей. Трассировать всё это можно через --trace-maps. Теперь, когда увидишь внезапный провал пропускной способности после безобидного с виду присваивания существующему полю, загляни в --trace-maps на события deprecation — возможно, ты только что расширил representation, разделяемый тысячами объектов.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в184
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.