1

I need your help in finding what's wrong with my implementation...

I'm trying to implement a simple JVM runtime profiler using byte-buddy. In general, what I need is that every method call will be logged in a stack which I manage in a separate object.

After reading several posts, I understood that it's better to use the "Advise" approach instead of "MethodDelegation", and here's what I came out with:

Agent.java:

package com.panaya.java.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public class Agent {
    private static List<Pattern> pkgIncl = new ArrayList<Pattern>();
    private static List<Pattern> pkgExcl = new ArrayList<Pattern>();

    private static void initMatcherPatterns(String argument) {
        if (argument == null) {
            System.out.println("Missing configuration argument");
            return;
        }

        System.out.println("Argument is: " + argument);

        String[] tokens = argument.split(";");

        if (tokens.length < 1) {
            System.out.println("Missing delimeter ;");
            return;
        }

        for (String token : tokens) {
            String[] args = token.split("=");
            if (args.length < 2) {
                System.out.println("Missing argument delimeter =:" + token);
                return;
            }

            String argtype = args[0];

            if (!argtype.equals("incl") && !argtype.equals("excl")) {
                System.out.println("Wrong argument: " + argtype);
                return;
            }

            String[] patterns = args[1].split(",");

            for (String pattern : patterns) {
                Pattern p = null;
                System.out.println("Compiling " + argtype + " pattern:" + pattern + "$");
                try {
                    p = Pattern.compile(pattern + "$");
                } catch (PatternSyntaxException pse) {
                    System.out.println("pattern: " + pattern + " not valid, ignoring");
                }
                if (argtype.equals("incl"))
                    pkgIncl.add(p);
                else
                    pkgExcl.add(p);
            }
        }

    }

    private static boolean isShouldInstrumentClass(String className) {
        System.out.println("Testing " + className + " for match.");
        boolean match = false;
        String name = className.replace("/", ".");

        for (Pattern p : pkgIncl) {
            Matcher m = p.matcher(name);
            if (m.matches()) {

                match = true;
                break;
            }
        }

        for (Pattern p : pkgExcl) {
            Matcher m = p.matcher(name);
            if (m.matches()) {

                match = false;
                break;
            }
        }

        if (match) {
            System.out.println("Class " + name + "should be instrumented.");
        } else {
            System.out.println("Skipping class: " + name);
        }
        return match;
    }

    public static void premain(String agentArgument, Instrumentation instrumentation) {
        System.out.println("Premain started");
        try {
            initMatcherPatterns(agentArgument);

            new AgentBuilder.Default()
                    .with(AgentBuilder.TypeStrategy.Default.REBASE)
                    .type(new AgentBuilder.RawMatcher() {
                        public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, Class<?> aClass, ProtectionDomain protectionDomain) {
                            return isShouldInstrumentClass(typeDescription.getActualName());
                        }
                    })
                    .transform((builder, typeDescription, classLoader) -> builder
                            .visit(Advice.to(ProfilingAdvice.class)
                                    .on(ElementMatchers.any()))).installOn(instrumentation);

        } catch (RuntimeException e) {
            System.out.println("Exception instrumenting code : " + e);
            e.printStackTrace();
        }

    }
}

And ProfilingAdvice.java:

package com.panaya.java.agent;

import com.panaya.java.profiler.MethodStackCollector;
import net.bytebuddy.asm.Advice;

public class ProfilingAdvice {
    @Advice.OnMethodEnter
    public static void enter(@Advice.Origin("#t.#m") String signature) {
        System.out.println("OnEnter :" + signature);
        try {
            MethodStackCollector.getInstance().push(signature);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Advice.OnMethodExit
    public static void exit(@Advice.Return long value) {
        System.out.println("OnExit - value = " + value);
        try {
            MethodStackCollector.getInstance().pop();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

For some reason, the "enter" and "exit" methods in ProfilingAdvice class, aren't invoked at all.

What am I doing wrong ?

Thanks, Elad.

esaar
  • 79
  • 1
  • 7
  • How does your agent look like? Did you register an `AgentBuilder.Listener` to see if there are any problems with the instrumentation? – Rafael Winterhalter Jul 28 '16 at 15:14
  • @RafaelWinterhalter, I'm not sure I understand your question... What does it mean "How does your agent look like"? could you please explain your question.... My agent class is as posted above...It's loaded OK, but the Advice mechanism doesn't work. – esaar Jul 29 '16 at 13:12
  • I wonder how your `premain` / `agentmain` method looks like. You need to assemble the advice somewhere using an `AgentBuilder`. – Rafael Winterhalter Jul 29 '16 at 13:29
  • I attached to the post the entire Agent code, you can see the premain method. – esaar Jul 29 '16 at 13:57
  • To ease you search: new AgentBuilder.Default() .with(AgentBuilder.TypeStrategy.Default.REBASE) .type(new AgentBuilder.RawMatcher() {public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, Class> aClass, ProtectionDomain protectionDomain) {return isShouldInstrumentClass(typeDescription.getActualName()); }}).transform((builder, typeDescription, classLoader) -> builder.visit(Advice.to(ProfilingAdvice.class).on(ElementMatchers.any()))).installOn(instrumentation); – esaar Jul 29 '16 at 13:58
  • Ah, sorry, my scrolling of your first code example is broken in my browser for some reason. – Rafael Winterhalter Jul 29 '16 at 15:00

2 Answers2

1

I tried your example and after reducing the exit advice to the print commands and your matcher to only match some class foo.Bar:

package foo;
public class Bar {
  public long qux() { return 0L; }
}

the instrumentation works without problem. I find it a bit fishy that your Advice matcher specifies any() while your ProfilingAdvice requires a long return type. Did you try only printing to the console without any annotations in your advice?

You can debug such issues by settting:

new AgentBuilder.Default()
   .with(AgentBuilder.Listener.StreamWriting.toSystemOut());

where potential errors during the instrumentation are printed to the console.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • When attaching a listener, I noticed that the advice methods were in fact invoked but indeed there was a problem with the return type. After removing the return parameter from the exit method, most of the instrumentation works fine, but in a specific case I am getting the following exception: java.lang.IllegalStateException: Unexpected remainder on the operand stack: -1 at net.bytebuddy.utility.StackAwareMethodVisitor.drainStack(StackAwareMethodVisitor.java:120).... I'll post a separate issue. – esaar Jul 29 '16 at 20:11
  • Please do. If you can provide a reconstruction, this would be great. Please update to 1.4.16 as well, I fixed a related issue recently. – Rafael Winterhalter Jul 29 '16 at 21:33
  • Even though the update to 1.4.16 solved the previous problem, it seems that when I change the "ElementMatchers" combination, it once again causes this exception. – esaar Jul 31 '16 at 12:08
  • I might have missed something then. Can you update your project to reproduce this error? – Rafael Winterhalter Jul 31 '16 at 13:18
  • Is there a way to contact you via eMail ? – esaar Jul 31 '16 at 13:24
  • Yes, you can find my mail in the parent POM of. Byte Buddy. – Rafael Winterhalter Jul 31 '16 at 19:57
0

@Advice.Return stands for the return value of the monitor methord.

@Advice.Return is not the return value of the enter(@Advice.Origin("#t.#m") String signature) method.

So, the following code is OK:

public class ProfilerInterceptor {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal();

    @Advice.OnMethodEnter
    public static void enter(@Advice.Origin("#t.#m") String signature) {
        System.out.printf("Enter: %s\n", signature);
        long start = System.currentTimeMillis();
        threadLocal.set(start);
        try {
            MethodStackCollector.getInstance().push(signature);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Advice.OnMethodExit
    public static void exit(@Advice.Origin("#t.#m") String signature) {
        long value = System.currentTimeMillis() - threadLocal.get();
        System.out.printf("Exit: %s\nTime: %d\n\n", signature, value);
        System.out.println("OnExit - value = " + value);
        try {
            MethodStackCollector.getInstance().pop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
diguage
  • 387
  • 4
  • 19