요약
허브 창의 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 피드백 루프:
RootView의 onReceive(runtime.objectWillChange)가 setVisibleWidgetIDs를 부르지만, visibleWidgetIDs가 @Published가 아니라서 루프가 생기지 않습니다.
- 팝업이 닫힌 동안의 interval 타이머:
SchedulePolicy.effectiveInterval은 pauseWhenClosed이면 nil을 반환합니다. 실측으로도 확인했습니다.
- 썸네일:
QLThumbnailGenerator는 앱 밖 프로세스에서 돌고, 디스크 캐시가 있으며, 보일 때만 로드합니다.
- 스크립트 위젯 deno 자식 프로세스: 누적 CPU가 1분 미만입니다.
수정 제안
- 폴링 제거: 설치가 끝났을 때만 갱신합니다.
WidgetInstaller.shared.onInstalled가 이미 있지만, StatusItemController가 쓰는 단일 클로저입니다. NotificationCenter나 Combine subject처럼 여러 곳이 구독할 수 있게 넓혀야 합니다.
- 아니면 widgets 디렉토리를 기존
DirectoryWatcher로 감시합니다.
- 최소 수정: 새 값이 기존 값과 다를 때만
installedIDs/installedVersions를 대입합니다.
- 선택:
widget.json 버전 읽기를 메인 스레드 밖으로 옮깁니다.
- 선택: 창이 보이지 않을 때(
occlusionState, 최소화) 갱신을 멈춥니다.
검증 방법
- 허브 창을 Gallery 탭으로 열어 둔 채
top -l 0 -s 5 -pid <pid> -stats pid,cpu,idlew,power로 재고, 수정 전후를 비교합니다. 최소화하거나 다른 창에 가려진 상태도 확인합니다.
- 기대 결과: 열려 있어도 아무 조작이 없으면 CPU가 약 0%, idle wakeups가 약 0/s여야 합니다.
요약
허브 창의 Gallery 탭이 열려 있는 동안
GalleryView가 2초마다 설치 상태를 다시 읽습니다. 값이 그대로여도@Published속성을 다시 대입해서 Gallery 화면 전체를 다시 그립니다. 창이 최소화되거나 다른 창에 가려져도 계속 돌기 때문에, 배터리로 쓰는 동안 메인 스레드 CPU가 쌓입니다.관찰 내용
환경: v0.1.4 (build 202608300114), macOS 26.6, Apple Silicon. 코드 기준은
maind51d36e입니다.누적 사용량: 앱을 실행한 14.6일 동안
barshelf-app누적 CPU가 414분입니다. 코어 1개 기준 평균 약 2%입니다.스레드별: 스레드별로 나누면 메인 스레드가 410분(user 387분, sys 22분)으로 거의 전부입니다. 백그라운드 스레드는 몇 초 수준입니다.
현재 측정값: 지금 직접 재 보면 가볍습니다. 팝업/스케줄러 쪽은 문제가 아닙니다.
폭주 흔적 없음: 커널의 CPU resource 경고(EXC_RESOURCE) 로그는 없습니다. 짧게 폭주한 것이 아니라 중간 강도가 오래 이어진 패턴으로 보입니다.
수치 정황: 누적 idle wakeups 94,230회를 2초 타이머(초당 0.5회)로 환산하면 약 52시간이고, 414분 ÷ 52시간은 코어의 약 13%입니다. Gallery 탭이 이틀쯤 열려 있었다면 수치가 맞습니다. 다만 추정이고, Gallery 탭을 열어 둔 상태의 재현 측정은 아직 하지 않았습니다.
원인 코드
Sources/MenubucketApp/GalleryView.swift:313-327Timer.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가 발생해GalleryViewbody 전체가 다시 평가됩니다.Hub/HubView.swift:120-124에서 Gallery 탭일 때만GalleryView가 존재하고,HubWindowController.windowWillClose에서 창과 모델을 해제합니다.확인 후 제외한 후보
RootView의onReceive(runtime.objectWillChange)가setVisibleWidgetIDs를 부르지만,visibleWidgetIDs가@Published가 아니라서 루프가 생기지 않습니다.SchedulePolicy.effectiveInterval은pauseWhenClosed이면nil을 반환합니다. 실측으로도 확인했습니다.QLThumbnailGenerator는 앱 밖 프로세스에서 돌고, 디스크 캐시가 있으며, 보일 때만 로드합니다.수정 제안
WidgetInstaller.shared.onInstalled가 이미 있지만,StatusItemController가 쓰는 단일 클로저입니다. NotificationCenter나 Combine subject처럼 여러 곳이 구독할 수 있게 넓혀야 합니다.DirectoryWatcher로 감시합니다.installedIDs/installedVersions를 대입합니다.widget.json버전 읽기를 메인 스레드 밖으로 옮깁니다.occlusionState, 최소화) 갱신을 멈춥니다.검증 방법
top -l 0 -s 5 -pid <pid> -stats pid,cpu,idlew,power로 재고, 수정 전후를 비교합니다. 최소화하거나 다른 창에 가려진 상태도 확인합니다.