Быстрое преобразование только одного класса с помощью агента Java

Я хочу измерить время запуска сервера без значительных накладных расходов.

На самом деле я хочу измерить время от выполнения серверного процесса до момента, когда сервер начинает прослушивать известный порт.

Например, я хочу измерить время запуска простого Netty Server. то есть время от запуска до момента, когда он готов принимать запросы.

Я разработал агент Java, используя Byte-Buddy.

public class Agent {

public static void premain(String arg, Instrumentation instrumentation) {
    new AgentBuilder.Default()
            .type(ElementMatchers.named("io.netty.bootstrap.AbstractBootstrap"))
            .transform((builder, typeDescription, classLoader, javaModule) ->
                    builder.visit(Advice.to(TimeAdvice.class)
                            .on(ElementMatchers.named("bind").and(ElementMatchers.takesArguments(SocketAddress.class)))))
            .installOn(instrumentation);
}
}

Ниже приведен исходный код TimeAdvice.

public class TimeAdvice {

@Advice.OnMethodExit
static void exit(@Advice.Origin String method) {
    System.out.println(String.format("Server started. Current Time (ms): %d", System.currentTimeMillis()));
    System.out.println(String.format("Server started. Current Uptime (ms): %d",
            ManagementFactory.getRuntimeMXBean().getUptime()));
}
}

С этим агентом время запуска составляет около 1400 мс. Однако, когда я измеряю время запуска, изменяя код сервера, время запуска сервера составляет около 650 мс.

Таким образом, кажется, что есть значительные накладные расходы при использовании агента Java с помощником по байтам при рассмотрении времени запуска.

Я также попробовал другой Java-агент с Javassist.

public class Agent {

private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

public static void premain(String arg, Instrumentation instrumentation) {
    instrumentation.addTransformer((classLoader, s, aClass, protectionDomain, bytes) -> {
        if (NETTY_CLASS.equals(s)) {
            System.out.println(aClass);
            long start = System.nanoTime();
            // Javassist
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("io.netty.bootstrap.AbstractBootstrap");
                CtMethod m = cc.getDeclaredMethod("bind", new CtClass[]{cp.get("java.net.SocketAddress")});
                m.insertAfter("{ System.out.println(\"Server started. Current Uptime (ms): \" + " +
                        "java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime());}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                System.out.println(String.format("Agent - Transformation Time (ms): %d", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
            }
        }

        return null;
    });
}
}

С этим агентом время запуска составляет около 800 мс.

Как минимизировать накладные расходы и измерить время запуска? Есть ли способ напрямую преобразовать конкретный класс, не проходя через все классы? Если я могу преобразовать класс напрямую, я думаю, что смогу максимально минимизировать накладные расходы.


person Isuru Perera    schedule 03.01.2018    source источник
comment
Вы просите свой инструмент проанализировать и изменить байт-код целевого класса. Это будет дорого. Это большая проблема для чего-то, что вы можете просто закодировать на своем сервере. Зачем вам нужно делать это с помощью манипуляций с байт-кодом?   -  person Jim Garrison    schedule 03.01.2018
comment
Я хочу использовать этот агент для нескольких серверов (и мы не сможем напрямую изменить исходный код). Вот почему я подумал об использовании подхода Java Agent.   -  person Isuru Perera    schedule 03.01.2018
comment
Достаточно справедливо, но это займет то, что нужно. Просто в некоторых случаях запуск вашего инструмента занимает больше времени, чем процесс, который вы измеряете.   -  person Jim Garrison    schedule 03.01.2018
comment
Ага. Я понимаю, что с агентом точно будут накладные расходы. Я просто хочу знать, есть ли способы еще больше минимизировать эти накладные расходы.   -  person Isuru Perera    schedule 03.01.2018
comment
Я думаю, Вы можете скорректировать свою стратегию здесь. Что я вижу из кода У вас есть только совет для выхода из метода - так что вы считаете все с начала запуска JVM. Лучшим подходом может быть совет по вводу метода также в каком-либо коде для начальной загрузки Netty и учету разницы в выходе метода.   -  person kaos    schedule 03.01.2018


Ответы (1)


Поскольку вы находитесь в premain, вы, вероятно, измеряете время загрузки и инициализации многих классов, которые раньше не использовались. Вполне возможно, что значительное количество этих классов в любом случае будет загружено и инициализировано позже, когда приложение использует их в первый раз, без измерения как «время запуска», поэтому это время будет перемещено в измеренное время запуска. время не может быть актуальной проблемой.

Обратите внимание, что вы используете лямбда-выражения в обоих вариантах, что вызывает инициализацию их серверной части, предоставляемой JRE. В случае OpenJDK он использует ASM под капотом, но, поскольку он был переупакован, чтобы избежать конфликтов с приложениями, использующими ASM, это не те классы, которые Byte-Buddy использует внутри, поэтому вы платите цену за инициализацию ASM дважды здесь .

Как было сказано, если эти классы будут использоваться в любом случае, т.е. если приложение будет использовать лямбда-выражения или ссылки на методы позже, вам не следует беспокоиться об этом, так как «оптимизация» только сдвинет инициализацию на более позднее время. Но если приложение не использует лямбда-выражения или ссылки на методы или вы хотите любой ценой удалить этот промежуток времени из измеренного времени запуска, вы можете прибегнуть к обычной реализации интерфейса, используя внутренний класс или позволив Agent реализовать интерфейс.

Чтобы еще больше сократить время запуска, вы можете использовать ASM напрямую, пропустив инициализацию классов Byte-Buddy, например.

import java.lang.instrument.*;
import java.lang.management.ManagementFactory;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

public class Agent extends ClassVisitor implements ClassFileTransformer {
    private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

    public static void premain(String arg, Instrumentation instrumentation) {
        instrumentation.addTransformer(new Agent());
    }

    public Agent() {
        super(Opcodes.ASM5);
    }
    public byte[] transform(ClassLoader loader, String className, Class<?> cl,
                            ProtectionDomain pd, byte[] classfileBuffer) {
        if(!NETTY_CLASS.equals(className)) return null;

        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, 0);
        synchronized(this) {
            super.cv = cw;
            try { cr.accept(this, 0); }
            finally { super.cv = null; }
        }
        return cw.toByteArray();
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if(name.equals("bind")
        && desc.equals("(Ljava/net/SocketAddress;)Lio/netty/channel/ChannelFuture;")) {
            return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                @Override
                protected void onMethodExit(int opcode) {
                    super.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Agent.class.getName().replace('.', '/'),
                        "injectedMethod", "()V", false);
                    super.onMethodExit(opcode);
                }
            };
        }
        return mv;
    }
    public static void injectedMethod() {
        System.out.printf("Server started. Current Time (ms): %d",
                          System.currentTimeMillis());
        System.out.printf("Server started. Current Uptime (ms): %d",
                          ManagementFactory.getRuntimeMXBean().getUptime());
    }
}

(не испытано)

Очевидно, что этот код сложнее, чем код, использующий Byte-Buddy, поэтому вам придется решить, на какой компромисс пойти.

ASM уже очень легкий. Если пойти еще глубже, это будет означать выполнение преобразования файла класса с использованием только ByteBuffer и HashMap; это возможно, но, конечно, не тот путь, по которому вы хотите пойти…

person Holger    schedule 04.01.2018
comment
Привет Хольгер, Большое спасибо за ваш ответ. Я попробую этот код и дам вам знать. - person Isuru Perera; 05.01.2018