org.apache.giraph.debugger.instrumenter.InstrumentGiraphClasses.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.giraph.debugger.instrumenter.InstrumentGiraphClasses.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.giraph.debugger.instrumenter;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.util.Collection;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.ConstPool;
import javassist.bytecode.Descriptor;

import org.apache.giraph.graph.Computation;
import org.apache.giraph.master.MasterCompute;
import org.apache.log4j.Logger;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * The main class that instruments user's ordinary Giraph Computation class with
 * necessary changes for debugging.
 */
public class InstrumentGiraphClasses {

    /**
     * Logger for this class.
     */
    private static Logger LOG = Logger.getLogger(InstrumentGiraphClasses.class);

    /**
     * Suffix to append to the original class names.
     */
    private static final String ORIGINAL_CLASS_NAME_SUFFIX = System.getProperty("giraph.debugger.classNameSuffix",
            "Original");
    /**
     * Path prefix for determining the temporary directory name used for
     * instrumentation.
     */
    private static final String TMP_DIR_NAME_PREFIX = InstrumentGiraphClasses.class.getSimpleName();

    /**
     * Disallowing instantiation.
     */
    private InstrumentGiraphClasses() {
    }

    /**
     * Main entry point for instrumenting Giraph Computation and MasterCompute
     * classes with Graft classes.
     *
     * @param args Command-line arguments.
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // CHECKSTYLE: stop Regexp
        if (args.length < 1) {
            System.err.println(
                    "Usage: java ... " + TMP_DIR_NAME_PREFIX + " GIRAPH_COMPUTATION_CLASS_NAME  [OUTPUT_DIR]");
            System.exit(1);
        }

        Collection<String> userComputationClassNames = Sets.newHashSet(args[0]);
        String outputDir = args.length > 1 ? args[1] : null;
        String masterComputeClassName = args.length > 2 ? args[2] : null;
        // Additional Computation classes
        boolean shouldAnalyzeMaster = masterComputeClassName != null;
        for (int i = 3; i < args.length; i++) {
            userComputationClassNames.add(args[i]);
            // Don't analyze the MasterCompute class when a chosen list of
            // Computation classes were given
            shouldAnalyzeMaster = false;
        }

        try {
            Collection<CtClass> classesModified = Sets.newHashSet();
            ClassPool classPool = ClassPool.getDefault();
            if (masterComputeClassName != null) {
                if (shouldAnalyzeMaster) {
                    // Collect all Computation class names referenced in
                    // masterComputeClassName
                    LOG.info("Analyzing MasterCompute class: " + masterComputeClassName);
                    userComputationClassNames
                            .addAll(collectComputationClassNames(masterComputeClassName, classPool));
                }
                LOG.info("Instrumenting MasterCompute class: " + masterComputeClassName);
                classesModified.addAll(instrumentSandwich(masterComputeClassName,
                        AbstractInterceptingMasterCompute.class.getName(), UserMasterCompute.class.getName(),
                        BottomInterceptingMasterCompute.class.getName(), classPool));
            }
            for (String userComputationClassName : userComputationClassNames) {
                LOG.info("Instrumenting Computation class: " + userComputationClassName);
                classesModified.addAll(instrumentSandwich(userComputationClassName,
                        AbstractInterceptingComputation.class.getName(), UserComputation.class.getCanonicalName(),
                        BottomInterceptingComputation.class.getName(), classPool));
            }

            // Finally, write the modified classes so that a new jar can be
            // created or an existing one can be updated.
            String jarRoot = outputDir != null ? outputDir
                    : Files.createTempDirectory(TMP_DIR_NAME_PREFIX).toString();
            LOG.info("Writing " + classesModified.size() + " instrumented classes to " + jarRoot);
            for (CtClass c : classesModified) {
                LOG.debug(" writing class " + c.getName());
                c.writeFile(jarRoot);
            }

            LOG.info("Finished instrumentation");

            if (outputDir == null) {
                // Show where we produced the instrumented .class files (unless
                // specified)
                System.out.println(jarRoot);
            }
            System.exit(0);
        } catch (NotFoundException e) {
            e.printStackTrace();
            System.err.println("Some Giraph Computation or MasterCompute classes " + "were not found");
            System.exit(1);
        } catch (CannotCompileException e) {
            e.printStackTrace();
            System.err.println("Cannot instrument the given Giraph Computation or " + "MasterCompute classes");
            System.exit(2);
        } catch (IOException e) {
            e.printStackTrace();
            System.err
                    .println("Cannot write the instrumented Giraph Computation and/or " + "MasterCompute classes");
            System.exit(4);
        }
        // CHECKSTYLE: resume Regexp
    }

    /**
     * Finds all class names referenced in the master compute class.
     *
     * @param masterComputeClassName The name of the master compute class.
     * @param classPool The Javassist class pool being used.
     * @return A collection of class names.
     * @throws NotFoundException
     */
    protected static Collection<String> collectComputationClassNames(String masterComputeClassName,
            ClassPool classPool) throws NotFoundException {
        Collection<String> classNames = Lists.newArrayList();
        CtClass computationClass = classPool.get(Computation.class.getName());
        CtClass rootMasterComputeClass = classPool.get(MasterCompute.class.getName());
        CtClass mc = classPool.get(masterComputeClassName);
        while (mc != null && !mc.equals(rootMasterComputeClass)) {
            // find all class names appearing in the master compute class
            @SuppressWarnings("unchecked")
            Collection<String> classNamesInMasterCompute = Lists.newArrayList(mc.getRefClasses());
            // as well as in string literals
            ConstPool constPool = mc.getClassFile().getConstPool();
            for (int i = 1; i < constPool.getSize(); i++) {
                switch (constPool.getTag(i)) {
                case ConstPool.CONST_String:
                    classNamesInMasterCompute.add(constPool.getStringInfo(i));
                    break;
                default:
                    break;
                }
            }
            // collect Computation class names
            for (String className : classNamesInMasterCompute) {
                // CHECKSTYLE: stop IllegalCatch
                try {
                    if (classPool.get(className).subtypeOf(computationClass)) {
                        classNames.add(className);
                    }
                } catch (NotFoundException e) {
                    // ignored
                    assert true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // CHECKSTYLE: resume IllegalCatch
            }

            // repeat above for all superclasses of given masterComputeClass
            mc = mc.getSuperclass();
        }
        return classNames;
    }

    /**
     * Instruments the given class with a bottom and top class (sandwich),
     * rewiring the inheritance chain and references.
     *
     * @param targetClassName
     *          The class to be placed in the middle of the sandwich
     *          instrumentation.
     * @param topClassName
     *          The class to be placed as a base class (on top) of the target
     *          class (middle), which intercepts any calls made from the target
     *          class.
     * @param mockTargetClassName
     *          The name of the mock class used to compile the top class.
     * @param bottomClassName
     *          The class to be placed as a extending class (beneath) of the
     *          target class, which will receive any calls to the target class
     *          first.
     * @param classPool
     *          The Javassist class pool being used.
     * @return A collection of instrumented Javassist CtClasses.
     * @throws NotFoundException
     * @throws CannotCompileException
     * @throws ClassNotFoundException
     */
    protected static Collection<CtClass> instrumentSandwich(String targetClassName, String topClassName,
            String mockTargetClassName, String bottomClassName, ClassPool classPool)
            throws NotFoundException, CannotCompileException, ClassNotFoundException {
        Collection<CtClass> classesModified = Sets.newHashSet();
        // Load the involved classes with Javassist
        LOG.debug("Looking for classes to instrument: " + targetClassName + "...");
        String alternativeClassName = targetClassName + ORIGINAL_CLASS_NAME_SUFFIX;
        CtClass targetClass = classPool.getAndRename(targetClassName, alternativeClassName);
        // We need two classes: one at the bottom (subclass) and
        // another at the top (superclass).
        CtClass topClass = classPool.get(topClassName);
        CtClass bottomClass = classPool.getAndRename(bottomClassName, targetClassName);

        LOG.debug("  target class to instrument (targetClass):\n" + getGenericsName(targetClass));
        LOG.debug("  class to instrument at top (topClass):\n" + getGenericsName(topClass));
        LOG.debug("  class to instrument at bottom (bottomClass):\n" + getGenericsName(bottomClass));

        // 1. To intercept other Giraph API calls by user's computation
        // class:
        // We need to inject a superclass at the highest level of the
        // inheritance hierarchy, i.e., make the top class become a
        // superclass of the class that directly extends
        // AbstractComputation.
        // 1-a. Find the user's base class that extends the top class'
        // superclass.
        LOG.debug("Looking for user's top class that extends " + getGenericsName(topClass.getSuperclass()));
        CtClass targetTopClass = targetClass;
        while (!targetTopClass.getName().equals(Object.class.getName())
                && !targetTopClass.getSuperclass().equals(topClass.getSuperclass())) {
            targetTopClass = targetTopClass.getSuperclass();
        }
        if (targetTopClass.getName().equals(Object.class.getName())) {
            throw new NotFoundException(
                    targetClass.getName() + " must extend " + topClass.getSuperclass().getName());
        }
        LOG.debug("  class to inject topClass on top of (targetTopClass):\n" + getGenericsName(targetTopClass));
        // 1-b. Mark user's class as abstract and erase any final modifier.
        LOG.debug("Marking targetClass as abstract and non-final...");
        int modClass = targetClass.getModifiers();
        modClass |= Modifier.ABSTRACT;
        modClass &= ~Modifier.FINAL;
        targetClass.setModifiers(modClass);
        classesModified.add(targetClass);
        if (!targetTopClass.equals(topClass)) {
            // 1-c. Inject the top class by setting it as the superclass of
            // user's class that extends its superclass (AbstractComputation).
            LOG.debug("Injecting topClass on top of targetTopClass...");
            targetTopClass.setSuperclass(topClass);
            targetTopClass.replaceClassName(topClass.getSuperclass().getName(), topClass.getName());
            classesModified.add(targetTopClass);
            // XXX Unless we take care of generic signature as well,
            // GiraphConfigurationValidator will complain.
            String jvmNameForTopClassSuperclass = Descriptor.of(topClass.getSuperclass()).replaceAll(";$", "");
            String jvmNameForTopClass = Descriptor.of(topClass).replaceAll(";$", "");
            String genSig = targetTopClass.getGenericSignature();
            if (genSig != null) {
                String genSig2 = genSig.replace(jvmNameForTopClassSuperclass, jvmNameForTopClass);
                targetTopClass.setGenericSignature(genSig2);
            }
        }
        // 1-d. Then, make the bottomClass extend user's computation, taking
        // care of generics signature as well.
        LOG.debug("Attaching bottomClass beneath targetClass...");
        bottomClass.replaceClassName(mockTargetClassName, targetClass.getName());
        bottomClass.setSuperclass(targetClass);
        bottomClass.setGenericSignature(Descriptor.of(targetClass));
        classesModified.add(bottomClass);

        // 2. To intercept compute() and other calls that originate from
        // Giraph:
        // We need to extend user's computation class pass that as the
        // computation class to Giraph. The new subclass will override
        // compute(), so we may have to remove the "final" marker on user's
        // compute() method.
        // 2-a. Find all methods that we override in bottomClass.
        LOG.debug("For each method to intercept," + " changing Generics signature of bottomClass, and"
                + " erasing final modifier...");
        for (CtMethod overridingMethod : bottomClass.getMethods()) {
            if (!overridingMethod.hasAnnotation(Intercept.class)) {
                continue;
            }
            Intercept annotation = (Intercept) overridingMethod.getAnnotation(Intercept.class);
            String targetMethodName = annotation.renameTo();
            if (targetMethodName == null || targetMethodName.isEmpty()) {
                targetMethodName = overridingMethod.getName();
            }
            // 2-b. Copy generics signature to the overriding method if
            // necessary.
            CtMethod targetMethod = targetClass.getMethod(targetMethodName, overridingMethod.getSignature());
            LOG.debug(" from: " + overridingMethod.getName() + overridingMethod.getGenericSignature());
            LOG.debug("   to: " + targetMethod.getName() + targetMethod.getGenericSignature());
            if (overridingMethod.getGenericSignature() != null) {
                overridingMethod.setGenericSignature(targetMethod.getGenericSignature());
                classesModified.add(overridingMethod.getDeclaringClass());
            }
            // 2-c. Remove final marks from them.
            int modMethod = targetMethod.getModifiers();
            if ((modMethod & Modifier.FINAL) == 0) {
                continue;
            }
            modMethod &= ~Modifier.FINAL;
            targetMethod.setModifiers(modMethod);
            LOG.debug(" erasing final modifier from " + targetMethod.getName() + "() of "
                    + targetMethod.getDeclaringClass());
            // 2-d. Rename the overriding method if necessary.
            if (!overridingMethod.getName().equals(targetMethodName)) {
                overridingMethod.setName(targetMethodName);
                classesModified.add(overridingMethod.getDeclaringClass());
            }
            // 2-e. Remember them for later.
            classesModified.add(targetMethod.getDeclaringClass());
        }
        LOG.debug("Finished instrumenting " + targetClassName);
        LOG.debug("            topClass=\n" + getGenericsName(topClass) + "\n" + topClass);
        LOG.debug("      targetTopClass=\n" + getGenericsName(targetTopClass) + "\n" + targetTopClass);
        LOG.debug("         targetClass=\n" + getGenericsName(targetClass) + "\n" + targetClass);
        LOG.debug("         bottomClass=\n" + getGenericsName(bottomClass) + "\n" + bottomClass);
        return classesModified;
    }

    /**
     * Format Javassist class name for log messages.
     * @param clazz The Javassist class.
     * @return Formatted name of the class including Generic type signature.
     */
    protected static String getGenericsName(CtClass clazz) {
        return clazz.getName() + " (" + clazz.getGenericSignature() + ")";
    }
}