Trawling

Zortax 블로그 작성자 Z, GPUI 3천 행 목록 라벨 변경에 8.4ms 측정

Zortax 블로그 작성자 Z는 GPUI의 `cx.notify()`가 변경된 필드 대신 소유 엔터티를 알리기 때문에 캐시 경계 밖의 뷰가 다시 실행될 수 있다고 주장합니다. 자체 하네스에서 활성 행 3,000개인 목록의 라벨 변경은 행마다 캐시 뷰를 둔 경우 8.4ms, 일반 요소로 구성한 경우 92ms였습니다. Zed 실험 브랜치의 측정에서는 보존 렌더링을 켠 쪽이 일반 작업의 `Window::draw` CPU 시간에서 1~3% 느렸습니다.

Discord 공동창업자 겸 CTO Stanislav Vishnevskiy는 데스크톱 클라이언트를 Electron에서 GPUI 기반 Rust 앱으로 재작성하고, 모바일은 네이티브 UI를 유지하면서 앱 코드를 공유할 계획이라고 밝혔습니다. 이 발표가 나온 뒤 Zortax 블로그 작성자 Z는 11월 8일 GPUI의 상태 모델을 비판하는 글을 올렸습니다. Z는 활성 행 3,000개인 목록에서 라벨 하나를 바꾸는 자체 측정을 근거로, 작은 데이터 변경에도 UI 갱신 비용이 커질 수 있다고 주장합니다.

Zed가 공개한 GPUI README에 따르면 공유 상태는 Entity에 저장하고, View는 Render를 구현해 화면 내용을 만드는 엔터티입니다. 각 프레임이 시작되면 창의 루트 뷰가 render를 실행해 Element 트리를 구성합니다. 뷰는 요소를 배치하고 스타일 API로 꾸민 뒤 GPUI에 넘겨 픽셀로 출력합니다. 더 낮은 수준의 명령형 인터페이스인 Element는 큰 목록의 효율적인 뷰나 코드 편집기의 사용자 정의 레이아웃에도 쓰입니다. Z는 Zed의 텍스트 편집기가 사용자 정의 요소로 구현돼 GPUI의 레이아웃·뷰 의미 체계를 우회한다고 설명합니다. 그의 비평은 고수준 뷰 모델에서 변경을 반영하는 경로에 초점을 둡니다.

GPUI 앱은 상태 필드를 바꾼 뒤 cx.notify()를 호출해 변경을 알립니다. 알림에는 변경된 필드가 아니라 상태를 소유한 엔터티의 식별자가 담깁니다. 일반적인 Rust의 &mut 접근은 필드 단위 쓰기 정보를 프레임워크에 전달하지 않으므로, GPUI는 어떤 필드가 바뀌었는지 알 수 없습니다. Z의 설명에 따르면 알림을 받은 엔터티 아래에서 유효한 cached() 경계 안에 놓이지 않은 뷰가 다시 실행됩니다. 따라서 값 하나가 바뀌는 작업도 데이터 변경량보다 뷰 트리의 크기에 따라 처리 비용이 커질 수 있습니다. Z는 cached()가 경계를 제공하지만 레이아웃 의미에 제약을 주고, 실제 애플리케이션에서도 드물게 쓰인다고 지적합니다.

Z가 만든 하네스에서는 행마다 캐시 뷰를 둔 3,000행 목록의 라벨 하나를 바꾸는 데 8.4ms가 걸렸습니다. 행을 일반 요소로 구성한 시험에서는 같은 라벨 변경에 92ms가 들었습니다. 이미 저장된 값과 같은 값을 다시 쓰는 작업은 7.9ms였고, 호출부에 동등성 검사를 직접 넣자 0.9µs로 줄었습니다. Z는 이 결과가 변경된 값의 양보다 트리 전체의 처리 비용이 중요해지는 사례라고 설명합니다. 이 수치는 그가 만든 하네스에서 특정 라벨 갱신 작업을 측정한 결과입니다.

프레임 사이에 뷰의 하위 트리를 보존하는 방식은 재구성 비용을 줄일 수 있습니다. Z가 Longbridge의 gpui-fast에서 다시 실행한 측정에서는 3,000행 목록의 라벨 변경 시간이 8.4ms에서 3.4ms로 줄었습니다. 다만 재사용하는 가장 작은 단위가 여전히 뷰여서 트리 순회 비용은 남는다고 Z는 설명합니다. 라벨 하나만 별도로 갱신하려면 앱 코드에서 해당 뷰의 엔터티와 Render 구현, 구독, 값 복사본, 동등성 검사와 소유자 관리를 마련해야 합니다. Z가 제기하는 문제는 단순한 렌더 함수의 편의성을 유지하면서 더 좁은 갱신 범위를 얻기 어렵다는 점입니다.

Z가 대안으로 제시한 한 번 실행하는 신호 모델에서는 구성 요소를 만들 때 본문을 한 차례 실행하고, 본문에서 신호를 읽는 클로저만 해당 값이 바뀔 때 다시 실행합니다. 프레임워크가 클로저와 값 사이의 읽기 관계를 추적하므로 앱 코드가 뷰마다 엔터티와 구독을 직접 엮는 작업을 줄일 수 있습니다. 이 모델도 상태를 거칠게 구성하면 갱신 범위가 커집니다. 예를 들어 모든 행이 하나의 전역 변경 신호를 읽으면 그 신호의 변경이 넓은 범위에 영향을 줍니다. 신호가 변경을 잘게 전달해도 레이아웃 엔진이 큰 트리를 다시 방문하면 전체 처리 시간은 줄지 않을 수 있습니다.

Zed의 GPUI 보존형 렌더링 계획은 측정 결과가 작업에 따라 달라지는 사례를 담고 있습니다. 계획은 암묵적 뷰 보존을 기능 플래그 뒤의 단계로 두고, Zed에서 플래그를 켜기 전에 버그 수정과 추적되지 않은 읽기 의존성 점검을 요구합니다. 계획 문서에 실린 실험 브랜치의 Zed 측정은 모드별 세 차례 실행에서 Window::draw의 CPU 시간을 비교했습니다. 일반 작업에서는 보존을 켠 쪽이 끈 쪽보다 1~3% 느렸습니다. 계획 작성자들은 실제 Zed에서 얻은 개선 대부분이 보존 자체보다 새 렌더 경로와 레이아웃 캐시 작업에서 나왔다고 설명합니다. 3,000행 목록의 라벨 갱신 벤치마크와 실제 Zed의 일반 작업 측정은 서로 다른 작업과 범위를 다룹니다.

Z는 GPUI의 현재 Linux 렌더러를 wgpu2로 이식했고, 비교표에 포함된 신호 기반 Rust 프레임워크 zgui에서 일하며, 측정 하네스도 직접 만들었다고 공개했습니다. 하네스는 공개하지 않았고 GPUI 유지관리자들은 어댑터를 검토하지 않았습니다. 측정값은 i9-13900K와 RTX 3080 Ti를 사용한 Linux·Vulkan 환경에서 얻은 apply와 settle 시간의 중앙값이며, 세 차례 패스 가운데 가장 좋은 결과를 사용했다고 Z는 밝혔습니다. 측정에는 Windows 운영체제, 컴포지터와 입력 처리가 포함되지 않습니다.

참고 자료

오류 제보