[Blink] Add WTF hash traits for DOMNodeIdType

MERGED2026domhash
2026. 9. 15.Yelihi 프로필 이미지Yelihi

Blink에서 DOM 노드 ID를 타입 안전한 DOMNodeIdType으로 점진적으로 전환하기 위해 WTF HashMap과 HashSet에서 사용할 해시 특성을 추가했습니다. 기
존 정수 키의 유효 범위와 GC 정책을 유지하는 데 초점을 맞춘 첫 번째 CL이며, 리뷰와 CQ 검증을 거쳐 병합되었습니다.

문제 설명

기존 Blink 내부의 DOMNodeId는 int의 타입 별칭입니다. 이름은 다르지만 C++에서는 같은 타입이므로, 다른 의미의 정수와 실수로 비교하거나 전달해
도 컴파일러가 구분하지 못합니다.

이를 개선하기 위해 base::IdType32 기반의 DOMNodeIdType이 도입되었지만, Blink 내부로 확대하려면 다음 문제를 먼저 해결해야 했습니다.

  • 해시 컨테이너 지원: 새 타입을 WTF HashMap과 HashSet의 키로 사용하기 위한 HashTraits가 필요했습니다.
  • 기존 정책 유지: 타입을 감싸는 것만으로도 기본 traits의 GC 관련 판단이 달라질 수 있었습니다.
  • 변경 범위 관리: Node와 DOMNodeIds 같은 공통 API를 먼저 바꾸면 여러 모듈과 OWNERS에 걸친 수정이 필요했습니다.

따라서 첫 CL은 해시 컨테이너 지원에 한정하고, 실제 사용처 전환은 후속 CL로 분리했습니다.

해결 내용

1. Blink의 컨테이너 계층에 traits 추가

renderer/platform/graphics/dom_node_id_traits.h에 HashTraits 특수화를 추가했습니다.

특수화는 특정 타입에 적용할 템플릿 규칙을 별도로 정의하는 C++ 기능입니다. 이를 통해 컨테이너가 DOMNodeIdType의 해시 계산 방법과 빈 슬롯·삭제
된 슬롯의 표현을 알 수 있습니다.

공개 타입 정의는 public/common에 유지하고, WTF 컨테이너 지원은 Blink 내부의 renderer/platform에 두었습니다. 공개 타입이 Blink 내부 컨테이너
에 의존하지 않도록 하기 위한 배치입니다.

2. 기존 정수 키의 값 범위 보존

WTF 해시 테이블은 슬롯 상태를 구분하는 특별한 값인 센티널을 사용합니다. 기존 정수 traits에 맞춰 다음 값을 선택했습니다.

  • 0: 빈 슬롯
  • -1: 삭제된 슬롯
  • 1부터 INT32_MAX: 실제 DOM 노드 ID로 사용 가능

DOMNodeIds의 ID 생성 코드가 최댓값까지 사용할 수 있으므로, INT32_MAX를 내부 예약값으로 소비하지 않는 것이 중요했습니다.

아래는 구현의 핵심 부분입니다.

 template <> struct HashTraits<DOMNodeIdType> : GenericHashTraits<DOMNodeIdType> { static uint32_t GetHash(const DOMNodeIdType& id) {
     return HashInt(id.value());
   }

   // Match integer-key sentinels to preserve the full positive ID range
   // generated by `DOMNodeIds`, including INT32_MAX.
   static constexpr bool kEmptyValueIsZero = true;
   static constexpr DOMNodeIdType EmptyValue() { return DOMNodeIdType(); }
   static constexpr DOMNodeIdType DeletedValue() { return DOMNodeIdType(-1); }

   // Preserve integer-key GC policies during migration. `GenericHashTraits`
   // would disable compaction and forbid GC during moves, even though this
   // wrapper is trivially copyable and movable.
   static_assert(std::is_trivially_copyable_v<DOMNodeIdType>);
   static_assert(std::is_trivially_move_constructible_v<DOMNodeIdType>);
   static constexpr bool kSupportsCompaction = true;

   template <typename U = void>
   struct NeedsToForbidGCOnMove {
     static constexpr bool value = false;
   };
 };

3. 정수 키와 동일한 GC 정책 유지

GenericHashTraits의 기본 설정을 그대로 사용하면 정수 키일 때와 달리 compaction 지원이 비활성화되고, 이동 시 GC 금지 구간이 필요하다고 판단됩
니다.

DOMNodeIdType은 단순한 정수 래퍼이므로 기존 정책을 명시적으로 유지했습니다. 그 판단의 전제인 단순 복사·이동 가능성은 static_assert로 검사하
여, 향후 타입이 바뀌면 컴파일 단계에서 확인할 수 있도록 했습니다.

주석도 코드의 동작을 반복하기보다 왜 정수 키의 센티널과 GC 정책을 유지해야 하는지 설명하도록 작성했습니다.

최종 변경은 traits 헤더, 테스트 파일, BUILD.gn 등록의 세 파일로 제한했습니다.

테스트 방법

1. 단위 테스트

DOMNodeIdTraitsTest에 세 가지 테스트를 추가했고, 로컬 실행에서 모두 통과했습니다.

컨테이너 테스트에는 일반 ID인 1과 경계값인 INT32_MAX를 함께 사용했습니다. 최댓값이 내부 예약값으로 잘못 처리되지 않는지 확인하기 위해서입니
다.

정책과 타입 크기처럼 컴파일 시 확인할 수 있는 조건은 static_assert로 검증했습니다.

테스트 실행 명령은 다음과 같습니다. out/Default는 실제 빌드 디렉터리로 대체합니다.

out/Default/blink_platform_unittests \
   --gtest_filter='DOMNodeIdTraitsTest.*'

2. CQ 검증

업로드한 CL은 CQ dry run을 통과했으며, David Bokan과 Michael Lippautz의 리뷰를 거쳐 병합되었습니다.

이 CL에서는 실제 Blink 사용처를 전환하지 않았으므로 별도의 기능 통합 테스트를 추가하지 않았습니다.

3. 성능 테스트

별도 성능 벤치마크는 수행하지 않았습니다. 기존 정수와 동일한 해시 계산 및 GC 정책을 유지했지만, 이를 성능 측정 결과로 해석하지는 않았습니다.

배운 점

  • 타입 별칭과 독립적인 타입의 차이: using DOMNodeId = int는 의미를 표현하지만 타입 혼용을 막지는 못합니다. 독립적인 래퍼 타입은 잘못된 사용
    을 컴파일 단계에서 발견하게 해 줍니다.

  • 해시 지원은 해시 함수만의 문제가 아님: 빈 슬롯과 삭제된 슬롯의 표현, 유효한 키 범위, GC 정책까지 함께 살펴봐야 합니다.

  • 기본 traits가 항상 기존 동작을 보존하지는 않음: 내부 값이 정수여도 래퍼 타입에 적용되는 기본 정책은 달라질 수 있습니다.

  • 작은 CL에도 명확한 근거가 필요함: 공통 API 전환과 준비 작업을 분리하여 리뷰 범위를 줄이고, 센티널과 GC 정책을 독립적으로 검증할 수 있었습
    니다.

  • 주석은 선택의 이유를 남겨야 함: 코드에서 바로 읽히는 동작보다 유효 ID 범위와 기존 정책을 보존하려는 의도를 기록했습니다.

  • 후속 전환은 사용처별 검토가 필요함: 이번 traits는 기본 정수 키 정책을 따릅니다. 별도 센티널 정책을 사용하는 컨테이너는 그대로 치환하지 않
    고 검토해야 합니다.

다음 단계는 작은 Blink 영역부터 DOMNodeIdType으로 전환하는 것입니다. 기존 정수 API와 만나는 지점에서는 명시적 변환을 사용하고, 공통 API가 전
환되면 임시 변환을 제거할 계획입니다.