티스토리 뷰

지난 글에서 UICollectionView Cell에 더티 플래그를 사용해 Constraints 업데이트 하는 횟수를 줄여주었다.

 

[iOS] UICollectionViewCell에 더티 플래그 적용하기 (1/2) - 구현

부스트캠프 7기 - burstcamp 개발 과정을 공유하는 포스트입니다. GitHub - boostcampwm-2022/iOS09-burstcamp: iOS 얼죽아 burstcamp 입니다 ^^ iOS 얼죽아 burstcamp 입니다 ^^. Contribute to boostcampwm-2022/iOS09-burstcamp developm

malchafrappuccino.tistory.com

 

막상 적용하기는 했는데 성능이 얼마나 좋아졌는지 정량적으로 확인해보고 싶어 테스트를 진행했다.

 

  • 피드를 1000개 생성한다.
  • 기존에 Constraints를 업데이트 안 할 때랑 CPU 사용량을 비교한다.
  • 피드 리스트에서 이미지가 있는 피드 : 이미지가 없는 피드의 비율을 조정해가며 확인해본다.

 

테스트를 위한 목업 데이터 만들기

테스트를 위한 목업 피드가 필요했다.

 

목업 피드를 생성하는 구조체를 만들어주었다.

 

struct MockUpFeedData {

// 썸네일 이미지가 있는 피드
    func createMockUpFeed() -> Feed {
        return Feed(
            feedUUID: UUID().uuidString,
            writer: createMockUpFeedWriter(),
            title: "이미지가 있는 피드입니다. 이미지가 있는 피드입니다. 이미지가 있는 피드입니다. 이미지가 있는 피드입니다.",
            pubDate: Date(),
            url: "",
            thumbnailURL: "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fodt1V%2FbtrUt8DP6bH%2FBIyRcfXepkYEkKPYcUwv8k%2Fimg.png",
            content: content(),
            scrapCount: Int.random(in: 0...50),
            isScraped: false
        )
    }
  
  // 썸네일 이미지가 없는 피드
      func createMockUpFeedWithoutThumbnailImage() -> Feed {
        return Feed(
            feedUUID: UUID().uuidString,
            writer: createMockUpFeedWriter(),
            title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
            pubDate: Date(),
            url: "",
            thumbnailURL: "",
            content: content(),
            scrapCount: Int.random(in: 0...50),
            isScraped: false
        )
    }
  	
	private func createMockUpFeedWriter() -> FeedWriter {
	// 피드 작성자 생성
	// 길어서 생략
	}
  
	private func content() -> String {
	// 피드 컨텐츠 생성
	// 길어서 생략
	}
 }

 

Diffable DataSource를 사용해서 모든 Feed가 Hashable 해야하므로, FeedUUID에 UUID 값을 넣어주었다.

 

혹시 constraints 설정이 잘못됐으면 확인하려고 이미지 유무에 따라 제목을 다르게 해주었다.

 

 

 

현재 앱에서는 클린 아키텍처를 사용하고 있다.

 

FirebaseService -> FeedRepository를 거쳐 UseCase로 피드 데이터를 전달한다.

간략히 보는 burstcamp 아키텍처

 

MockUpFeed를 만들기 위해 FeedRepository를 채택하는 새로운 MockUpFeedRepository 클래스를 만들어주었다.

 

final class MockUpFeedRepository: FeedRepository {

    let mockUpFeedData = MockUpFeedData()

    func fetchRecentHomeFeedList() async throws -> HomeFeedList {
        let mockUpNormalFeedList = createMockUpNormalFeedList(imageFeedCount: 500, noImageFeedCount: 500)
        return HomeFeedList(recommendFeed: [], normalFeed: mockUpNormalFeedList)
    }

    private func createMockUpNormalFeedList(imageFeedCount: Int, noImageFeedCount: Int) -> [Feed] {
        var result: [Feed] = []
        for _ in 1...imageFeedCount {
            result.append(mockUpFeedData.createMockUpFeed())
        }
        for _ in 1...noImageFeedCount {
            result.append(mockUpFeedData.createMockUpFeedWithoutThumbnailImage())
        }
        return result.shuffled()
    }
    
    // 프로토콜에 대한 나머지 구현은 현재 사용하지 않기 때문에 에러를 throw해주었다.
        func fetchMoreNormalFeed() async throws -> [Feed] {
        throw MockUpFeedRepositoryError.noImplementation
    }
    // ...
}

썸네일 이미지가 있는 피드썸네일 이미지가 없는 피드를 500개씩 만들고 섞어주었다.

 

함수 인자를 바꿔가며 피드 비율을 조절해주었다.

 

 

피드 케이스 설정하기

테스트 케이스를 4가지 정도 설정해주었다.

 

1. 100 : 0

전부 다 이미지가 있는 피드라 업데이트가 발생하지 않음

다른 테스트 케이스와 비교를 위한 기본 테스트

 

2. 50 : 50

가장 기본적인 반반 테스트

 

3. 80 : 20

운영체제의 교체 정책을 공부하다 보면  80 대 20 워크로드가 있다.

 

20%의 페이지(인기 있는 페이지)들에서  80%의 참조가 발생하고 나머지 80%의 페이지(비인기 페이지) 들에 대해서 20%의 참조만 발생한다는 것이다.

 

비단 운영체제뿐 만아니라 경제학에서도 사용되는 개념으로 더 자세한 건 파레토 법칙을 검색해보면 된다.

 

썸네일 이미지가 있는 블로그가 대부분이므로 80%라 가정하고 테스트를 했다.

 

 

4. 95 : 5

썸네일 이미지가 있는 블로그가 극단적으로 많다고 생각하고 테스트를 했다.

 

 

Instruments 사용하기

Instruments - Time Profiler를 사용해 CPU 사용량을 확인할 수 있다.

 

 

테스트 환경

  • 14인 맥북 프로 M1 Pro 8코어 & 14코어 GPU, 512GB, 16GB (14인치 깡통 맥북 프로 M1)
  • Ventura 13.0
  • 시뮬레이터 iphone 14 pro max - iOS 16.2
  • 맥북 충전하면서 실행
  • Instreuments 버전 14.1

 

테스트 내용

30초 동안 스크롤하면서 Constraints 업데이트에 따른 CPU 사용량 확인하기

 

 

결과

결과를 확인하기 앞서 Instruments 사용이 처음이라 테스트 케이스에 대해 비교하는 방법을 못 찾았다 🫠

 

피그마로 옮겨 직접 비교했다.

결과는 그림과 같았다.

차이가 보이는가???

 

50 : 50 과 80: 20을 비교해보면 확실히 80: 20이 CPU 사용량이 적어보인다.

But, 가장 업데이트가 적은 95 : 5가 CPU 사용량이 많아 보인다.

 

그렇다. 정확한 비교가 불가능하다.

 

 

시뮬레이터에서 스크롤을 내가 직접했기 때문

각 테스트 마다 스크롤 속도가 달라서 요청하는 연산량이 달랐고 정확한 비교가 불가능했다.

 

Instrument 어렵다....

 

 

 

 

 

+ Hang

Instruments에 Hangs이라는 것 있다.

Hang은 아무런 반응을 하지 않은 상태로써 시스템 운영이 불가능한 상태를 의미한다. 

목업 데이터 1000개를 한 번에 생성하다 보니 시작시 순간적으로 시뮬레이터가 멈췄다.

 

 

 

Update 수 측정하기

성능 측정을 위한 다른 방법을 고민했다.

 

가장 쉽고 좋은 방법이 있었다. 바로 contraints가 업데이트 되는 수를 측정하는 것이다!

 

더티 플래그가 없었을 때, Cell이 1000개 있다면 1000번의 업데이트가 이루어졌다.

더티 플래그가 있다면 호출 수가 얼마나 줄지 확인해보았다.

 

앞선 테스트 케이스와 마찬가지로 1000개의 셀에 대해 50 : 50, 80 : 20, 95 : 5 비율로 비교를 해봤다.

 

 

Counter 만들기

가볍게 counter를 하나 만들어주었다.

struct TestCounter {
    static var count = 0

    static func up() {
        count += 1
        print("Constraints 업데이트 수 : ", count)
    }
}

 

업데이트 될 때마다 count를 up하고 출력해주었다.

    private func setThumbnailImage(urlString: String) {
        if hasThumbnailImage && !newFeedHaveImage(urlString) {
            updateEmptyImageView()
            updateTitleLabelWithEmptyImageView()
            hasThumbnailImage = false
            TestCounter.up()
        } else if !hasThumbnailImage && newFeedHaveImage(urlString) {
            updateThumbnailImageView()
            updateTitleLabel()
            self.thumbnailImageView.setImage(urlString: urlString)
            hasThumbnailImage = true
            TestCounter.up()
        }
    }

 

 

개발자답게 확인하기

개발자는 어떤 사람인가....

사람이 할 일을 컴퓨터가 하도록 만드는 사람이다.....

1000개의 Cell을 그냥 스크롤 하는 것이 아닌 자동화하는게 진짜 개발자 아니겠는가...

라는 생각을 가지고 머리를 굴려봤다.

 

현재 앱에서 네비게이션바를 누르면 스크롤 최상단으로 온다.

 

이걸 조금 수정해서 네비게이션바를 누르면 collectionView 최하단으로 이동시켜 주었다.

private func configureNavigationBar() {
	navigationController?.navigationBar.topItem?.title = "홈"
	let tapGesture = UITapGestureRecognizer(target: self, action: #selector(scrollToBottom))
	navigationController?.navigationBar.addGestureRecognizer(tapGesture)
}
    
@objc private func scrollToBottom() {
	homeView.collectionView.scrollToItem(at: IndexPath(item: 999, section: 1), at: .bottom, animated: true)
}

현재 앱에 2개의 섹션(추천 피드 섹션, 일반 피드 섹션)과 1000개의 일반 피드가 있다.

1000 - 1 = 999 번째로 이동해주었다.

 

이동 영상

근데 뭔가 이상했다. 

 

업데이트 된 수가 계속 50 언저리로만 나왔다.

 

최악의 경우를 생각하면 O(2n)이라 1000이 나올 수도 있는데 50이 나오는 건 말이 안됐다.

 

scrollToItem() 을 정확히 알기 위해 공식 문서로 갔다.

 

 

https://developer.apple.com/documentation/uikit/uicollectionview/1618046-scrolltoitem

설명이 한 줄이 끝이다. 스크롤 한다고만 돼있다. 

 

아무튼 중간을 거치지 않는다. 그렇다면 뭐다???

 

 

 

직접 스크롤 하기

훌륭한 개발자가 되었다는 자아도취에서 빠져나와 원래로 돌아왔다.

 

머리가 나쁘면 뭐다?

 
직접 스크롤한 영상

3개의 테스트 케이스에 대해 10번씩, 30 * 1000 = 30000개의 Cell을 직접 스크롤 했다.

 

현재 내 손

 

 

결과

Constraints 업데이트 횟수가 50% ~ 90% 줄어든 것을 확인했다. 하지만 이건 피드 비율에 따라서 달라질 수 있는 수치이다.

 

조금 더 알고리즘적으로 생각해보자.

  • m : 썸네일 이미지가 있는 피드 개수
  • n : 썸네일 이미지가 없는 피드 개수

최악의 경우는 더티 플래그 히트 없이 모든 셀이 업데이트 되는 경우이다.

 

O(2n) 일 것이다.

 

하지만 통계적으로 대부분의 블로그에는 썸네일 이미지가 있다. 그렇다면 m >> n 일 것이고 0 에 수렴할 것이다.

 

 

그래프로 보면 다음과 같다.

 

블로그의 경우 대부분 썸네일 이미지가 있으므로 100:0에 가까워질 것이다.

전체 피드 N이 커짐에 따라 Update 되는 횟수가 거의 0에 근접할 것이다.

 

버스트 캠프 DB

현재 테스트 DB에는 팀원 3명의 피드 30개가 있다.

썸네일 이미지가 있는 피드가 24개, 없는 피드가 6개이다.

 

 

현재 기준 Constraints를 업데이트하는 횟수를 최소 60% 줄였다!

 

나중에 앱 출시하고 DB에 데이터가 쌓이면 얼마나 줄었는지 더 확인해봐야겠다.

 

 

 

UICollectionView에 더티플래그 사용하기

 

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함