https://gradle.com/blog/determine-the-root-cause-of-github-actions-failures-faster-with-gradle-enterprise/ https://medium.com/@liutikas/hello-my-gradle-builds-are-slow-483427e6eb4
깃허브 액션 CI/CD 워크플로우에서 빌드 스캔 리포트 분석으로 CI 시간을 30분에서 3분으로, 27분 단축시킨 썰 디펜던시 다운로드 시간에서 오래 걸리고 있었음. 그 이유는 자체 디펜던시인 androidx.* 에 해당하는 모든 아티팩트를 일일히 mavenCentral로 질의해서 404를 받고 있었기 때문 이를 확인하고 특정 url에서만 원하는 아티팩트를 탐색하도록 지정하여 30m+ -> 3m으로 빌드 시간을 줄임
“CI는 블랙박스입니다. 로그 외부에서는 CI에서 실제로 무슨 일이 일어나고 있는지 알 수 없습니다. 로그를 읽는 것조차 쉬운 일이 아닙니다. 수천 줄의 로그가 있으면 구문 분석하기가 정말 어렵습니다.”
gradle build scan을 쓰면 CI 블랙박스를 분석하고, 해당 프로세스를 최적화할 수 있음!
https://github.com/gradle/gradle-build-action#optimizing-cache-effectiveness
Github Actions에서 gradle-build-action
을 사용하여 캐시를 효율적으로 최적화하기 위해서는 다음 내용을 알아야 한다.
이 액션은 job이 끝날 때마다 읽고 쓴 캐시에 대한 리포트를 작성하고, Gradle Build에 대한 Job Summary를 제공한다. buil scan 리포트를 통해 어떤 부분을 최적화할지 결정할 수 있다. 아래에서 다루는 내용은 그저 방법일 뿐이고 이를 어떻게 사용할지는 각자의 워크플로우마다 달라진다.
먼저 어떤 Job에 대해 캐시를 쓸 것인지 결정해야 한다. 만약 "컴파일 + 단위 테스트" 작업을 실행해서 코드를 컴파일하고, 여러 개의 단위 테스트를 수행할 수 있다. 그리고 그 다음으로는 통합 테스트를 병렬 실행하는 "통합 테스트 작업"을 수행한다고 하자. 각 통합 테스트 Job은 "컴파일 + 단위 테스트" Job에 필요한 디펜던시 그리고 추가적으로 필요한 하나 혹은 둘 정도의 추가적인 디펜던시를 필요로 한다.
각 통합 테스트 Job이 끝나면 새로운 캐시 엔트리가 쓰여질 것이다. 만약 다른 추가적인 디펜던시가 다운로드되지 않았다면 가정 하에, 이 캐시 엔트리는 이전에 수행했던 "컴파일 + 단위 테스트" Job과 디펜던시 항목을 완전히 공유하지만, 만약 하나의 디펜던시만 추가되더라도 완전히 새로운 디펜던시 엔트리가 쓰여진다.
만약 디펜던시의 캐시 엔트리가 레이어 구조라서 추가된 디펜던시만 쌓아주면 좋겠지만, gradle-build-action
은 아직 레이어링된 캐시를 지원하지 않는다. 아무튼 서로 다른 "디펜던시" 엔트리를 가진 각 "통합 테스트" 엔트리가 너무 커지면, 깃허브 액션 캐시에서 이전에 있던 캐시가 지워질 수 있다.
그래서 캐시 규모를 적절하게 유지하기 위해서는 아래와 같은 방법을 적용할 수 있다.
- "통합 테스트" 잡을
cache-read-only: true
로 설정하는 것이다. 이렇게 하면 각 잡은 "컴파일 + 단위 테스트" 잡에서 썼던 엔트리를 재사용한다. 이렇게 하면 아주 사소한 디펜던시 추가 때문에 모든 디펜던시를 다시 다운로드해야 하는 오버헤드를 줄일 수 있다. - "컴파일 + 유닛 테스트" 잡에서 통합 테스트 잡에 필요한 모든 디펜던시를 미리 받아놓는 추가적인 스텝을 추가한다. 이렇게 하면 "컴파일 + 유닛 테스트" 잡에 대한 디펜던시와 "통합 테스트" 잡에 대한 디펜던시가 서로 공유될 수 있다. 결국 통합 테스트 엔트리는 훨씬 작아질 것이고, cache eviction도 피할 수 있다.
- 이 두 가지 방법을 같이 적용한다. 그러면 어떤 캐시 엔트리도 "통합 테스트" 잡에 의해서 쓰여지지 않겠지만, "통합 테스트" 잡에 필요한 모든 디펜던시는 "컴파일 + 유닛 테스트" 엔트리에 존재하기 때문에 괜찮다.
깃허브 캐시 엔트리는 기본적으로 서로 다른 브랜치에서 공유되지 않는다. 이는 각 PR 브랜치가 각각의 Gradle User Home Cache를 갖는다는 것을 의미하며, 다른 PR 브랜치의 캐시 엔트리를 히트할 수 없다는 것을 의미한다. 단, 예외가 있다. 바로 부모 브랜치인 경우와 업스트림 브랜치인 경우, 그리고 디폴트 브랜치(master
, main
)인 경우에는 다른 브랜치에서도 캐시를 읽을 수 있다.
디폴트값으로, gradle-build-action
은 디폴트(master
, main
) 브랜치에서 실행되는 빌드에 대해 오직 쓰기 작업만 수행한다. 다른 브랜치에서 실행되는 잡들은 캐시로부터 읽기 작업만 수행한다. 대부분의 경우 의도한 방향이 이 방침과 일치할 것이다. 왜냐면 다른 브랜치에 대해 실행되는 작업은 굳이 자신의 브랜치에 대한 캐시 엔트리를 작성하지 않고도, main 브랜치로부터 Gradle User Home의 캐시를 히트할 수 있기 때문이다. 이렇게 하면 공유하고 있는 캐시 엔트리를 evict할 가능성도 낮아진다.
만약 캐시에 대한 쓰기 작업이 도움이 되는, 오랫동안 유지되는 브랜치가 존재한다면, cache-read-only
옵션을 오버라이딩하여 이를 재구성할 수 있다. 마찬가지로 워크플로우의 특정 잡에 대해서 read-only를 사용하고, 대신 바로 이전(upstream)에 수행된 잡의 캐시를 재사용할 수도 있다.