JProfiler 도움말

인젝티드 프로브

스크립트 프로브와 마찬가지로 인젝티드 프로브는 선택된 메서드에 대한 인터셉션 핸들러를 정의합니다. 하지만 인젝티드 프로브는 JProfiler GUI 외부의 IDE에서 개발되며 JProfiler가 제공하는 인젝티드 프로브 API를 기반으로 합니다. 이 API는 허용적인 Apache License 2.0 버전 하에 라이선스가 부여되므로 관련 아티팩트를 쉽게 배포할 수 있습니다.

인젝티드 프로브를 시작하는 가장 좋은 방법은 JProfiler 설치 디렉터리의 api/samples/simple-injected-probe 디렉터리에 있는 예제를 살펴보는 것입니다. 해당 디렉터리에서 ../gradlew를 실행하여 컴파일하고 실행할 수 있습니다. Gradle 빌드 파일인 build.gradle에 예제에 대한 추가 정보가 포함되어 있습니다. api/samples/advanced-injected-probe의 예제는 제어 객체를 포함하여 프로브 시스템의 더 많은 기능을 보여줍니다.

개발 환경

인젝티드 프로브 개발에 필요한 프로브 API는 다음 Maven 좌표를 갖는 단일 아티팩트에 포함되어 있습니다.

group: com.jprofiler
artifact: jprofiler-probe-injected
version: <JProfiler version>

여기서 이 매뉴얼에 해당하는 JProfiler 버전은 16.3입니다.

JAR, 소스 및 javadoc 아티팩트는 Maven Central 리포지토리에 게시되어 있습니다. Gradle이나 Maven과 같은 빌드 도구를 사용하여 개발 클래스패스에 프로브 API를 추가하거나, JProfiler 설치 디렉터리의

api/jprofiler-probe-injected.jar

JAR 파일을 사용할 수 있습니다.

com.jprofiler.api.probe.injected 패키지의 개요는 API를 살펴보는 데 좋은 출발점입니다.

프로브 구조

인젝티드 프로브는 com.jprofiler.api.probe.injected.Probe로 어노테이션이 지정된 클래스입니다. 해당 어노테이션의 속성은 전체 프로브에 대한 설정 옵션을 노출합니다. 예를 들어, 개별적으로 검사할 필요가 없는 프로브 이벤트를 많이 생성하는 경우, events 속성을 사용하여 프로브 이벤트 뷰를 비활성화하고 오버헤드를 줄일 수 있습니다.

@Probe(name = "Foo", description = "Shows foo server requests", events = false)
public class FooProbe {
    ...
}

인터셉션 핸들러를 정의하기 위해 프로브 클래스에 특수한 어노테이션이 지정된 정적 메서드를 추가합니다. PayloadInterception 어노테이션은 페이로드를 생성하고, SplitInterception 어노테이션은 호출 트리를 분할합니다. 핸들러의 반환값은 어노테이션에 따라 페이로드 또는 분할 문자열로 사용됩니다. 스크립트 프로브와 마찬가지로 null을 반환하면 인터셉션이 적용되지 않습니다. 인터셉트된 메서드에 대한 타이밍 정보는 자동으로 계산됩니다.

@Probe(name = "FooBar")
public class FooProbe {
    @PayloadInterception(
        invokeOn = InvocationType.ENTER,
        method = @MethodSpec(className = "com.bar.Database",
                             methodName = "processQuery",
                             parameterTypes = {"com.bar.Query"},
                             returnType = "void"))
    public static String fooRequest(@Parameter(0) Query query) {
        return query.getVerbose();
    }

    @SplitInterception(
        method = @MethodSpec(className = "com.foo.Server",
                             methodName = "handleRequest",
                             parameterTypes = {"com.foo.Request"},
                             returnType = "void"))
    public static String barQuery(@Parameter(0) Request request) {
        return request.getPath();
    }
}

위의 예제에서 볼 수 있듯이, 두 어노테이션 모두 MethodSpec으로 인터셉트할 메서드를 정의하기 위한 method 속성을 가지고 있습니다. 스크립트 프로브와 달리 MethodSpec에는 빈 클래스 이름을 지정할 수 있으므로, 클래스 이름에 관계없이 특정 시그니처를 가진 모든 메서드가 인터셉트됩니다. 또는 MethodSpec의 subtypes 속성을 사용하여 전체 클래스 계층 구조를 인터셉트할 수도 있습니다.

모든 파라미터를 자동으로 사용할 수 있는 스크립트 프로브와 달리, 핸들러 메서드는 관심 있는 값을 요청하기 위해 파라미터를 선언합니다. 각 파라미터는 com.jprofiler.api.probe.injected.parameter 패키지의 어노테이션으로 지정되므로, 프로파일링 에이전트는 어떤 객체나 원시(primitive) 타입 값이 메서드에 전달되어야 하는지 알 수 있습니다. 예를 들어, 핸들러 메서드의 파라미터에 @Parameter(0)을 지정하면 인터셉트된 메서드의 첫 번째 파라미터가 주입됩니다.

인터셉트된 메서드의 파라미터는 모든 인터셉션 유형에서 사용할 수 있습니다. 프로파일링 에이전트가 메서드 진입(entry) 대신 종료(exit) 시점을 인터셉트하도록 지시한 경우, 페이로드 인터셉션은 @ReturnValue로 반환값에 접근하거나 @ExceptionValue로 발생한 예외에 접근할 수 있습니다. 이는 PayloadInterception 어노테이션의 invokeOn 속성으로 설정합니다.

스크립트 프로브와 달리, 인터셉션 어노테이션의 reentrant 속성을 true로 설정하면 인터셉트된 메서드의 재귀 호출에 대해서도 인젝티드 프로브 핸들러가 호출될 수 있습니다. 핸들러 메서드에 ProbeContext 타입의 파라미터를 선언하면 ProbeContext.getOuterPayload() 또는 ProbeContext.restartTiming()을 호출하여 중첩 호출에 대한 프로브의 동작을 제어할 수 있습니다.

고급 인터셉션

때로는 단일 인터셉션만으로는 프로브 문자열을 구성하는 데 필요한 모든 정보를 수집하기에 충분하지 않습니다. 이를 위해 프로브에는 페이로드나 분할을 생성하지 않는 Interception 어노테이션이 지정된 인터셉션 핸들러를 원하는 수만큼 포함할 수 있습니다. 정보는 프로브 클래스의 정적 필드에 저장할 수 있습니다. 멀티스레드 환경에서의 스레드 안전성을 위해 참조 타입을 저장할 때는 ThreadLocal 인스턴스를 사용하고, 카운터를 유지 관리할 때는 java.util.concurrent.atomic 패키지의 원자적 숫자 타입을 사용해야 합니다.

경우에 따라 메서드 진입과 메서드 종료 모두에 대해 인터셉션이 필요할 수 있습니다. 일반적인 사례로는 다른 인터셉션의 동작을 수정하는 inMethodCall과 같은 상태 변수를 유지 관리하는 경우입니다. 기본 인터셉션 유형인 진입 인터셉션에서 inMethodCall을 true로 설정할 수 있습니다. 그런 다음 해당 인터셉션 바로 아래에 다른 정적 메서드를 정의하고 @AdditionalInterception(invokeOn = InvocationType.EXIT)으로 어노테이션을 지정합니다. 인터셉트할 메서드는 위의 인터셉션 핸들러에서 가져오므로 다시 지정할 필요가 없습니다. 메서드 본문에서는 inMethodCall 변수를 false로 설정할 수 있습니다.

...

private static final ThreadLocal<Boolean> inMethodCall =
    ThreadLocal.withInitial(() -> Boolean.FALSE);

@Interception(
    invokeOn = InvocationType.ENTER,
    method = @MethodSpec(className = "com.foo.Server",
                         methodName = "internalCall",
                         parameterTypes = {"com.foo.Request"},
                         returnType = "void"))
public static void guardEnter() {
    inMethodCall.set(Boolean.TRUE);
}

@AdditionalInterception(InvocationType.EXIT)
public static void guardExit() {
    inMethodCall.set(Boolean.FALSE);
}

@SplitInterception(
      method = @MethodSpec(className = "com.foo.Server",
                           methodName = "handleRequest",
                           parameterTypes = {"com.foo.Request"},
                           returnType = "void"),
      reentrant = true)
public static String splitRequest(@Parameter(0) Request request) {
    if (!inMethodCall.get()) {
        return request.getPath();
    } else {
        return null;
    }
}

...

api/samples/advanced-injected-probe/src/main/java/AdvancedAwtEventProbe.java에서 이 사용 사례의 실제 동작 예제를 확인할 수 있습니다.

제어 객체

Probe 어노테이션의 controlObjects 속성이 true로 설정되어 있지 않으면 제어 객체 뷰가 표시되지 않습니다. 제어 객체를 사용하려면 핸들러 메서드에 해당 타입의 파라미터를 선언하여 ProbeContext를 얻어야 합니다. 아래 샘플 코드는 제어 객체를 열고 이를 프로브 이벤트와 연결하는 방법을 보여줍니다.

@Probe(name = "Foo", controlObjects = true, customTypes = MyEventTypes.class)
public class FooProbe {
    @Interception(
            invokeOn = InvocationType.EXIT,
            method = @MethodSpec(className = "com.foo.ConnectionPool",
                         methodName = "createConnection",
                         parameterTypes = {},
                         returnType = "com.foo.Connection"))
    public static void openConnection(ProbeContext pc, @ReturnValue Connection c) {
        pc.openControlObject(c, c.getId());
    }

    @PayloadInterception(
            invokeOn = InvocationType.EXIT,
            method = @MethodSpec(className = "com.foo.ConnectionPool",
                         methodName = "createConnection",
                         parameterTypes = {"com.foo.Query", "com.foo.Connection"},
                         returnType = "com.foo.Connection"))
    public static Payload handleQuery(
        ProbeContext pc, @Parameter(0) Query query, @Parameter(1) Connection c) {
        return pc.createPayload(query.getVerbose(), c, MyEventTypes.QUERY);
    }

    ...

}

제어 객체는 정해진 수명을 가지며, 프로브 뷰는 타임라인과 제어 객체 뷰에 객체의 열림 및 닫힘 시간을 기록합니다. 가능한 경우 ProbeContext.openControlObject() 및 ProbeContext.closeControlObject()를 호출하여 명시적으로 제어 객체를 열고 닫아야 합니다. 그렇지 않으면 제어 객체의 표시 이름을 확인하는 @ControlObjectName 어노테이션이 지정된 정적 메서드를 선언해야 합니다.

핸들러 메서드가 String 대신 Payload 인스턴스를 반환하면 프로브 이벤트를 제어 객체와 연결할 수 있습니다. ProbeContext.createPayload() 메서드는 제어 객체와 프로브 이벤트 타입을 인수로 받습니다. 허용되는 이벤트 타입이 포함된 enum은 Probe 어노테이션의 customTypes 속성에 등록되어 있어야 합니다.

제어 객체는 메서드 진입 시점에 해당하는 시간 측정 시작 시 지정되어야 합니다. 경우에 따라 페이로드 문자열의 이름이 반환값이나 다른 인터셉션에 따라 달라지기 때문에 메서드 종료 시점에만 결정될 수 있습니다. 이러한 경우에는 ProbeContext.createPayloadWithDeferredName()을 사용하여 이름 없는 페이로드 객체를 생성할 수 있습니다. 바로 아래에 @AdditionalInterception(invokeOn = InvocationType.EXIT)으로 어노테이션이 지정된 인터셉션 핸들러를 정의하고 해당 메서드에서 String을 반환하면, 이 문자열이 자동으로 페이로드 문자열로 사용됩니다.

스레드 상태 오버라이드

데이터베이스 드라이버나 외부 리소스에 대한 네이티브 커넥터의 실행 시간을 측정할 때, JProfiler에 일부 메서드를 다른 스레드 상태로 배치하도록 지시해야 할 때가 있습니다. 예를 들어, 데이터베이스 호출을 "Net I/O" 스레드 상태로 분류하는 것이 유용합니다. 통신 메커니즘이 표준 Java I/O 기능을 사용하지 않고 네이티브 메커니즘을 사용하는 경우, 자동으로 이렇게 처리되지 않습니다.

ThreadState.NETIO.enter() 및 ThreadState.exit() 호출 쌍을 사용하면 프로파일링 에이전트가 그에 맞게 스레드 상태를 조정합니다.

...

@Interception(invokeOn = InvocationType.ENTER, method = ...)
public static void enterMethod(ProbeContext probeContext, @ThisValue JComponent component) {
    ThreadState.NETIO.enter();
}

@AdditionalInterception(InvocationType.EXIT)
public static void exitMethod() {
    ThreadState.exit();
}

...

배포

인젝티드 프로브를 배포하는 방법은 클래스패스에 배치할지 여부에 따라 두 가지가 있습니다. 다음 VM 파라미터를 사용하면

-Djprofiler.probeClassPath=...

프로파일링 에이전트에 의해 별도의 프로브 클래스패스가 설정됩니다. 프로브 클래스패스에는 Windows에서는 ';', 다른 플랫폼에서는 ':'로 구분된 디렉터리와 JAR 파일이 포함될 수 있습니다. 프로파일링 에이전트는 프로브 클래스패스를 스캔하여 모든 프로브 정의를 찾습니다.

프로브 클래스를 클래스패스에 배치하는 것이 더 편하다면 다음 VM 파라미터를

-Djprofiler.customProbes=...
쉼표로 구분된 정규화된 클래스 이름(FQCN) 목록으로 설정할 수 있습니다. 프로파일링 에이전트는 이러한 각 클래스 이름에 대해 인젝티드 프로브 로드를 시도합니다.