12

Occasionally a slight modification to a Java source file like some additional explicit casts to help the compiler can improve compile time from 4 minutes to 3 seconds for a single java file (Especially in Java 8).

The problem is: In a large java project, how do you find which particular .java files are compiling slowly?

Is there a way to get Ant to time how long it takes to compile each individual .java file?

Jayan
  • 18,003
  • 15
  • 89
  • 143
clinux
  • 2,984
  • 2
  • 23
  • 26
  • In theory you could probably do this with a custom Ant goal using JSR 199 (Java compiler API) to invoke the compiler but that sounds like a lot of work. Could you just look at the timestamps of the .class files? – Pace Apr 14 '16 at 02:10
  • Possibly. Then just check the deltas between the modification dates?. I wonder if the javac command could be wrapped in a batch file that times how long it takes to execute. Then just get ant to use something else rather than javac. – clinux Apr 14 '16 at 03:00
  • 4
    The problem is javac does not get called one file at a time. javac is not an incremental compiler and so must take in every file at the same time. So timing how long javac takes is really not much more different than timing how long the entire compilation takes. – Pace Apr 14 '16 at 12:59
  • (OT) @clinux: do you have an example where specific case improves compiler speed? – Jayan Apr 19 '16 at 02:50
  • I would say better use Maven or Gradle. ANT complies slowly if .java file has 1000+ lines. I did experience that. Also, use Jenkins or Hudson it will compile time to time. – Pratiyush Kumar Singh Apr 19 '16 at 17:20
  • 1
    @Jayan: the compiler performs poorly when inferring types through a large number of varargs (like 10 or so), when you typecast each vararg argument it runs fast again. – clinux Apr 20 '16 at 02:08
  • @Jayan: For the vararg unification case it seems to take O(2^n) to unify n arguments together. – clinux Apr 20 '16 at 02:42
  • @clinux: Thanks. That must a case for @ Brian Goetz to comment. Added javac tag – Jayan Apr 20 '16 at 02:44

1 Answers1

4

I think that this might be possible. Here's what I've found:

If you're using Java 8, you can register a Plugin with the compiler to add some additional functionality during compilation. The documentation has this to say about plugins:

It is expected that a typical plug-in will simply register a TaskListener to be informed of events during the execution of the compilation, and that the rest of the work will be done by the task listener.

So you can setup a plugin to use a TaskListener, and have the task listener log timestamps when class are being generated.

package xyz;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;

public class TimestampPlugin implements Plugin {


    @Override
    public String getName() {
        return "Timestamp_Plugin";
    }

    @Override
    public void init(JavacTask task, String... strings) {
        task.setTaskListener(new FileTimestampListener());
    }
}

Documentation for TaskListener. A task listener is passed a TaskEvent, which has a Kind. In your case it sounds like you're interested in generation.

package xyz;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;

import java.util.HashMap;

public class FileTimestampListener implements TaskListener {
    HashMap<String, Long> timeStampMap = new HashMap<>();

    @Override
    public void started(TaskEvent taskEvent) {
        if(TaskEvent.Kind.GENERATE.equals(taskEvent.getKind())) {
            String name = taskEvent.getSourceFile().getName();
            timeStampMap.put(name, System.currentTimeMillis());
        }
    }

    @Override
    public void finished(TaskEvent taskEvent) {
        if(TaskEvent.Kind.GENERATE.equals(taskEvent.getKind())) {
            String name = taskEvent.getSourceFile().getName();
            System.out.println("Generated " + name + " over " + (System.currentTimeMillis() - timeStampMap.get(name)) + " milliseconds");
        }
    }
}

This is a simple example but it should be straightforward from here to set up something like a log file to store the information gathered. As you can see in the plugin's init function, arguments can be passed to the Plugin from the command line.

The plugin is configured by specifying it with the -Xplugin compiler argument. I'm not sure why but there doesn't appear to be any documentation on this page about it, but it can used by setting up a file called com.sun.source.util.Plugin (the FQ class name of the interface to implement) in your META-INF/services directory. So:

META-INF
|-- services
    |-- com.sun.source.util.Plugin

And in that file list the FQ class name of your implementation of this class. So the file contents would be:

xyz.TimestampPlugin

In your Ant task you'll just need to specify a compiler flag -Xplugin:Timestamp_Plugin (note this is the name provided by the Plugin's getName() function). You'll also need to provide the compiled Plugin and runtime dependencies on the classpath, or the annotation processor path, if one is specified.

heisbrandon
  • 1,180
  • 7
  • 8
  • I was in need of this just now (7 years later) and it works perfectly. Thank you! – clinux Jun 06 '23 at 05:54
  • In my case it was TaskEvent.Kind.ANALYZE taking an awful long time, instead of TaskEvent.Kind.GENERATE. – clinux Jun 06 '23 at 06:41