IntelliJ IDEA에서 자가 프로파일링하기

다른 언어: English Español Português 中文

이전 글과 마찬가지로, 이 글은 약간 메타적입니다. 당연히, 당신은 IntelliJ IDEA를 다른 프로세스를 프로파일링하기 위해 사용할 수 있습니다, 하지만 IntelliJ IDEA 자체를 프로파일링할 수 있다는 사실을 아셨나요?

이것은 당신이 IntelliJ IDEA 플러그인을 작성하고 있고 해당 플러그인의 성능과 관련된 문제를 해결해야 할 경우 유용할 수 있습니다. 또한, 플러그인 작성자인지 여부와 상관없이 이 시나리오는 저의 커버하는 프로파일링 전략이 IntelliJ IDEA에만 국한되지 않기 때문에 당신에게 흥미로울 수 있습니다 – 다른 유형의 프로젝트에서 이와 유사한 병목현상을 해결하고 다른 도구를 사용할 수 있습니다.

문제

이 글에서는 수년 전에 발견한 상당히 흥미로운 성능 병목현상을 살펴보겠습니다. IntelliJ IDEA에서 사이드 프로젝트를 작업하면서 특정 짧은 이름, 예를 들어 A ,을 가진 클래스의 테스트를 찾는 데(이동 | 테스트Navigate | Test) 놀랍게도 시간이 많이 소요되는 것을 알게 되었습니다. 이는 종종 2분 이상 소요되었습니다.

'클래스에 대한 테스트 찾기...'라는 대화 상자 '클래스에 대한 테스트 찾기...'라는 대화 상자

병목 현상의 존재는 프로젝트의 크기에 따라 달라지지 않았습니다– 단일 클래스인 A 로 구성된 프로젝트에서도 탐색하는 데 여전히 많은 시간이 소요되었습니다. 거대한 IntelliJ IDEA 합체 저장소에서조차 이 기능과 관련하여 지연을 경험한 적이 없었기 때문에, 거의 비어 있는 프로젝트에서의 느린 속도는 특별히 궁금했습니다.

이런 일이 왜 발생했는지, 더 중요한 것은, 당신의 프로젝트에서 유사한 문제를 마주했을 때 어떻게 접근해야하는지 알아보겠습니다.

환경 재현하기

나는 원래 이 글을 JetBrains의 내부용으로 썼습니다, 그러나 이를 대중에게 공개하는 생각은 최근에야 들었습니다. 다행히도, 시간이 지나면서 글은 그리 잘 늙지 않았고, 최신 버전의 IntelliJ IDEA와 최신 하드웨어에서는 문제가 더 이상 재현되지 않는 것 같습니다.

나는 일을 수행하는 설정에서 느림 현상을 재현할 수 없었으므로, 나는 다시 밀봉했던 오래된 노트북을 더럽히는 작업을 수행하고 IntelliJ IDEA의 이전 버전을 설치했습니다. 당신이 당신의 IDE에서 수사를 따라가려면 IntelliJ IDEA Community 저장소를 복제하도록 하십시오, 이것은 당신의 탐색과 디버깅을 용이하게 할 것입니다.

또한, 아래 클래스가 있는 빈 프로젝트를 가지고 있는지 확인합시다:

public class A {
    public static void main(String[] args) {
        System.out.println("I like tests");
    }
}

IntelliJ Profiler

당신은 이미 알고 있겠지만, IntelliJ IDEA에는 통합된 JVM 프로파일러가 있습니다. 당신은 프로파일러가 첨부된 상태에서 애플리케이션을 실행할 수 있습니다. 또는, 우리가 할 것처럼 이미 실행 중인 프로세스에 프로파일러를 첨부할 수 있습니다.

이를 위해 프로파일러 (Profiler) 도구 창으로 이동하고 해당 프로세스를 찾습니다. 프로세스 목록에서 IDE를 보지 못한다면, 프로세스 (Process) 근처의 메뉴에서 개발 도구 표시 (Show Development Tools)를 확인하십시오. 프로세스를 클릭하면 IntelliJ IDEA는 통합 성능 분석 도구를 제안합니다, 이 도구를 사용하면 다음을 수행할 수 있습니다:

이 모든 도구들은 문서에 다루어져 있으며, 이 글에서는 특히 프로파일러에 초점을 맞추겠습니다.

'Profiler' 도구 창에서 프로세스를 클릭하면 'IntelliJ Profiler 첨부' 옵션을 가진 메뉴가 나타납니다. 'Profiler' 도구 창에서 프로세스를 클릭하면 'IntelliJ Profiler 첨부' 옵션을 가진 메뉴가 나타납니다.

문제가 발생하기 이전에 프로파일러를 첨부해야 합니다. 예를 들어, 문제가 API 호출 결과로 발생한다면, 먼저 프로세스에 프로파일러를 첨부한 후 문제를 발생시키는 이벤트를 재현해야 합니다.

Tip icon

이상적으로는, 문제를 재현하기 바로 전에 프로파일러를 첨부해야 합니다. 당신의 애플리케이션이 입력을 기다리는 것 외의 다른 일을 하는 경우, 이 접근법은 당신이 관련 없는 샘플 수를 최소화하는 데 도움이 됩니다.

문제가 발생하는 코드의 실행 시간에 따라, 프로파일러가 분석을 위해 더 많은 샘플을 수집할 수 있도록 문제를 여러 번 재현하는 것이 이치에 맞을 수 있습니다. 이렇게 하면 결과 보고서에 문제가 더 눈에 띄게 됩니다.

프로파일러를 분리하거나 프로세스를 종료하면, IntelliJ IDEA는 자동으로 결과 스냅샷을 엽니다.

보고서 분석하기

스냅샷을 분석하기 위해, 당신은 여러 를 사용할 수 있습니다. 호출 트리를 살펴보거나, 특정 메서드에 대한 통계를 검토하거나, 스레드당 CPU 부하, GC 활동 등을 살펴볼 수 있습니다.

현재 문제에 대해서는 타임라인 (Timeline) 뷰에서 시작하여 비정상적인 것을 발견할 수 있는지 확인하겠습니다:

아주 바빴던 스레드를 볼 수 있는 '타임라인' 탭 아주 바빴던 스레드를 볼 수 있는 '타임라인' 탭

실제로, 타임라인은 한 스레드가 이상하게 바빴음을 나타냅니다. 녹색 막대는 특정 스레드에 대해 수집된 샘플에 해당합니다. 이러한 막대 중 하나를 클릭하면 해당 샘플의 스택 추적을 볼 수 있습니다.

색깔 있는 막대를 클릭하면 도구 창의 오른쪽에 스택 추적이 표시됩니다. 색깔 있는 막대를 클릭하면 도구 창의 오른쪽에 스택 추적이 표시됩니다.

개별 샘플의 스택 추적은 스레드 활동이 테스트 찾기와 관련이 있다는 것을 시사합니다. 하지만 우리는 아직 전체 상황을 보지 못하였습니다. 불꽃 그래프에서 바빴던 스레드로 이동해 봅시다:

불꽃 그래프에 있는 두 개의 하이라이트된 메서드는 그래프의 전체 폭을 거의 차지합니다 불꽃 그래프에 있는 두 개의 하이라이트된 메서드는 그래프의 전체 폭을 거의 차지합니다

우리에게 관심이 있을 메서드들인, JavaTestFinder.findTestsForClass() KotlinTestFinder.findTestsForClass() 는 그래프의 하단에 있습니다. 우리는 그들 아래의 펼쳐진 메서드들을 고려하지 않는데, 그들은 상당한 자체 시간이나 분기를 가지고 있지 않습니다. 그들은 흐름을 제어하고 집중적인 계산을 수행하는 것이 아닙니다.

이들 메서드가 실제로 느린 현상과 관련이 있는지를 확인하기 위해, 비문제적인 경우를 프로파일링할 수 있습니다: 예를 들어, 더 현실적인 이름을 가진 클래스의 테스트를 검색해보겠습니다. 그런 다음 이들 메서드에 무슨 일이 일어나는지 diff 보기를 사용하여 확인합니다.

'findTestsForClass' 쿼리를 사용한 'Method list' 탭은 해당 메서드들을 93-95% 차이로 보여줍니다 'findTestsForClass' 쿼리를 사용한 'Method list' 탭은 해당 메서드들을 93-95% 차이로 보여줍니다

새로운 스냅샷에는 JavaTestFinder.findTestsForClass() KotlinTestFinder.findTestsForClass() 에 적은 샘플들이 93-95% 더 적습니다. 다른 메서드들의 런타임은 그렇게 많이 다르지 않습니다. 우리가 올바른 방향으로 가고 있음을 보입니다.

다음 질문은 왜 그런 일이 일어나는지입니다. 디버거를 사용하여 그 이유를 알아내봅시다.

왜 이렇게 큰 차이가 나는가?

findTestsForClass() 에서 브레이크 포인트를 설정하고 조금 코드를 스텝하는 것은 우리를 다음 지점으로 데려갑니다:

MinusculeMatcher matcher = NameUtil.buildMatcher("*" + klassName, NameUtil.MatchingCaseSensitivity.NONE);
    for (String eachName : ContainerUtil.newHashSet(cache.getAllClassNames())) {
        if (matcher.matches(eachName)) {
            for (PsiClass eachClass : cache.getClassesByName(eachName, scope)) {
                if (isTestClass(eachClass, klass) && !processor.process(Pair.create(eachClass, TestFinderHelper.calcTestNameProximity(klassName, eachName)))) {
                    return;
                }
            }
        }
    }
}

코드는 정규 표현식을 사용하여 현재 캐시에 있는 짧은 이름을 필터링합니다. 결과적으로 나오는 각각의 문자열에 대해 해당하는 클래스를 검색합니다.

로그를 통해 조건 후의 클래스 이름을 로깅하면, 그것을 통과하는 모든 클래스를 구할 수 있습니다.

"Searching for class:" + eachName 이라는 조건과 선택 취소된 Suspend 체크박스가 있는 브레이크 포인트 대화 상자 "Searching for class:" + eachName 이라는 조건과 선택 취소된 Suspend 체크박스가 있는 브레이크 포인트 대화 상자

프로그램을 실행했을 때 약 25000개의 클래스를 로그했습니다, 이는 빈 프로젝트에 대한 상당히 큰 수입니다!

콘솔에 클래스 이름 뒤에 '클래스를 찾는 중:'이라고 많은 줄이 디스플레이됩니다 콘솔에 클래스 이름 뒤에 '클래스를 찾는 중:'이라고 많은 줄이 디스플레이됩니다

로그에서 클래스 이름들은 확실히 다른 곳에서 왔습니다, 이는 내 ‘Hello World’ 프로젝트가 아닙니다. 수수께끼가 풀렸습니다: IntelliJ IDEA가 클래스 A 에 대한 테스트를 찾는 데 오랫동안 걸리는 이유는 의존성, JDK, 심지어 다른 프로젝트에서 온 모든 캐시된 클래스를 점검하기 때문입니다. 그 클래스들은 너무 많이 필터를 통과했습니다. 왜냐하면 그들은 모두 가지고 있기 때문입니다 그들의 이름에 ‘A’라는 문자를 가지고 있습니다. 긴 이름과 더 현실적인 클래스 이름으로 이 비효율성은 인지되지 않았을 것이고, 단지 대부분의 이러한 이름은 정규식에 의해 필터링되었을 것이기 때문입니다.

수정?

불행히도, 이 문제에 대한 간단하고 신뢰할 수 있는 수정법을 찾지 못했습니다. 하나의 잠재적인 전략은 검색 범위에서 의존성을 제외하는 것입니다. 이것은 첫눈에는 실행 가능해 보입니다, 하지만 의존성이 테스트를 포함할 가능성이 있습니다. 이것은 자주 일어나지는 않지만, 여전히 이 접근법은 그런 의존성에 대한 기능을 깨뜨릴 것입니다.

다른 접근법은 *.java 파일 마스크를 도입하는 것인데, 이것은 컴파일된 클래스를 필터링됩니다. Java에서는 잘 작동하지만, Kotlin 같은 다른 언어에서 작성된 테스트에 대해서는 문제가 됩니다. 모든 가능한 언어를 추가하더라도, 이 기능은 새롭게 지원되는 언어에 대해서는 단순히 실패하게 만들 것이므로, 유지관리와 디버깅에 추가 부하가 됩니다.

접근법에 관계없이, 수정은 그 자체로 글을 요구하므로, 지금 당장은 구현하지 않겠습니다. 하지만 우리가 한 것은, 느림 현상의 근본 원인을 발견하는 것이며, 이것이 바로 프로파일러를 사용하는 이유입니다.

스냅샷 공유하기

마무리 하기 전에, 논의해야 할 한 가지 사항이 더 있습니다. 다른 컴퓨터에서 촬영한 스냅샷을 사용한 것을 알았나요? 게다가, 스냅샷은 단지 다른 기계에서 온 것이 아니었습니다. 운영 체제와 IntelliJ IDEA 버전도 다르게 했습니다.

프로파일러에 대한 아름다움 중 종종 간과되는 것 중 하나는 데이터 공유의 용이성입니다. 스냅샷은 파일에 쓰여져, 당신이 다른 사람에게 보낼 수 있습니다(또는 누군가로부터 받을 수 있습니다). 다른 도구와 달리, 디버거를 사용할 때처럼 분석을 시작하기 위한 완전한 재현 프로그램이 필요하지 않습니다. 사실, 그럴 경우에조차 컴파일 가능한 프로젝트가 필요하지 않습니다.

나를 믿지 마시고, 직접 해보세요. 여기에 스냅샷을 첨부하겠습니다: idea64_exe_2024_07_22_113311.jfr.

all posts ->