Skip to content

Gallery 탭의 2초 설치 상태 폴링이 열려 있는 동안 메인 스레드 CPU를 계속 사용 #1

Description

@jiunbae

요약

허브 창의 Gallery 탭이 열려 있는 동안 GalleryView가 2초마다 설치 상태를 다시 읽습니다. 값이 그대로여도 @Published 속성을 다시 대입해서 Gallery 화면 전체를 다시 그립니다. 창이 최소화되거나 다른 창에 가려져도 계속 돌기 때문에, 배터리로 쓰는 동안 메인 스레드 CPU가 쌓입니다.

관찰 내용

환경: v0.1.4 (build 202608300114), macOS 26.6, Apple Silicon. 코드 기준은 main d51d36e입니다.

  • 누적 사용량: 앱을 실행한 14.6일 동안 barshelf-app 누적 CPU가 414분입니다. 코어 1개 기준 평균 약 2%입니다.

  • 스레드별: 스레드별로 나누면 메인 스레드가 410분(user 387분, sys 22분)으로 거의 전부입니다. 백그라운드 스레드는 몇 초 수준입니다.

  • 현재 측정값: 지금 직접 재 보면 가볍습니다. 팝업/스케줄러 쪽은 문제가 아닙니다.

    상태 CPU idle wakeups
    팝업 닫힘 0.10% 0/s
    팝업 열림 (20초) 0.55% 0.05/s
    팝업 닫은 뒤 0.10% 0/s
  • 폭주 흔적 없음: 커널의 CPU resource 경고(EXC_RESOURCE) 로그는 없습니다. 짧게 폭주한 것이 아니라 중간 강도가 오래 이어진 패턴으로 보입니다.

  • 수치 정황: 누적 idle wakeups 94,230회를 2초 타이머(초당 0.5회)로 환산하면 약 52시간이고, 414분 ÷ 52시간은 코어의 약 13%입니다. Gallery 탭이 이틀쯤 열려 있었다면 수치가 맞습니다. 다만 추정이고, Gallery 탭을 열어 둔 상태의 재현 측정은 아직 하지 않았습니다.

원인 코드

  • Sources/MenubucketApp/GalleryView.swift:313-327
    • Timer.publish(every: 2, on: .main, in: .common).autoconnect().onReceive가 매번 model.refreshInstalledStates()를 부릅니다.
  • Sources/MenubucketApp/GalleryView.swift:241-257 (refreshInstalledStates)는 tick마다 두 가지를 합니다.
    • 레지스트리 항목마다 fileExists를 확인하고 widget.json을 읽어 디코딩합니다. @MainActor라서 메인 스레드에서 돕니다.
    • installedIDs/installedVersions값 비교 없이 다시 대입합니다. 그러면 objectWillChange가 발생해 GalleryView body 전체가 다시 평가됩니다.
  • 동작 범위: Hub/HubView.swift:120-124에서 Gallery 탭일 때만 GalleryView가 존재하고, HubWindowController.windowWillClose에서 창과 모델을 해제합니다.
    • 따라서 허브 창이 Gallery 탭으로 열려 있는 동안(최소화·가려짐 포함) 계속 돌고, 창을 닫으면 멈춥니다.

확인 후 제외한 후보

  • RootView 피드백 루프: RootViewonReceive(runtime.objectWillChange)setVisibleWidgetIDs를 부르지만, visibleWidgetIDs@Published가 아니라서 루프가 생기지 않습니다.
  • 팝업이 닫힌 동안의 interval 타이머: SchedulePolicy.effectiveIntervalpauseWhenClosed이면 nil을 반환합니다. 실측으로도 확인했습니다.
  • 썸네일: QLThumbnailGenerator는 앱 밖 프로세스에서 돌고, 디스크 캐시가 있으며, 보일 때만 로드합니다.
  • 스크립트 위젯 deno 자식 프로세스: 누적 CPU가 1분 미만입니다.

수정 제안

  1. 폴링 제거: 설치가 끝났을 때만 갱신합니다.
    • WidgetInstaller.shared.onInstalled가 이미 있지만, StatusItemController가 쓰는 단일 클로저입니다. NotificationCenter나 Combine subject처럼 여러 곳이 구독할 수 있게 넓혀야 합니다.
    • 아니면 widgets 디렉토리를 기존 DirectoryWatcher로 감시합니다.
  2. 최소 수정: 새 값이 기존 값과 다를 때만 installedIDs/installedVersions를 대입합니다.
  3. 선택: widget.json 버전 읽기를 메인 스레드 밖으로 옮깁니다.
  4. 선택: 창이 보이지 않을 때(occlusionState, 최소화) 갱신을 멈춥니다.

검증 방법

  • 허브 창을 Gallery 탭으로 열어 둔 채 top -l 0 -s 5 -pid <pid> -stats pid,cpu,idlew,power로 재고, 수정 전후를 비교합니다. 최소화하거나 다른 창에 가려진 상태도 확인합니다.
  • 기대 결과: 열려 있어도 아무 조작이 없으면 CPU가 약 0%, idle wakeups가 약 0/s여야 합니다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions