mardi 6 mai 2014

Eclipse vs Netbeans (or vs all the rest, not just IntelliJ)

I'm wondering why a lot of projects are still using Eclipse. Really, it's been a matter of choice in the past. It isn't anymore.

Eclipse is old and bloated, doesn't get basic things right, while all other IDEs got them long ago.

Here and there, you can find someone saying that IntelliJ is much better. It is, but it isn't the only one. You still have multiple valid choices. The truly bad choice is Eclipse, and it should be avoided at all cost:
  • Eclipse *doesn't* support maven well. It tries to read the project configuration and update some internal configuration from it. Doing it right, as other IDEs do, just involves using the damn pom.xml as is, and actually run mavan for any operation: compiling, building in general, even running. Eclipse still has its own build path etc. True, it works sometimes. That's not good enough, it really lock you in basic project configurations, as Eclipse doesn't get it right if you make your pom.xml too complex.

    It looks like there is no abstraction on what a project is, everything is an Eclipse project.
  • Eclipse doesn't get multitasking right. Just think of the "progress" (or lack of...) tab and the dreaded "Building project..." which takes ages when you have some "save actions" enabled and a big project.
  • Lots of exceptions, NullPointerExceptions or others, random behavior. This could happen in any IDE, but I see it twice a year in Netbeans, multiple times a day in Eclipse.
  • Eclipse looks like a bunch of plugins with very varying quality, global coherence is limited.
  • Eclipse can detect some simple programming errors. Netbeans or IntelliJ can correct them.
I use Netbeans daily, and there is no comparison. I tried IntelliJ and I am quite open to it, but I don't see the point of paying for features similar to the ones I get for free in Netbeans. I even had a quite good experience with JDeveloper (yes, I've been one of the few users). It looks to me that all 3 are good tools that you can use daily. In comparison, Eclipse looks like a broken toy.

vendredi 9 décembre 2011

Improve Glassfish deployment time by more than 1000%

(experimental source/binary patch below for the impatient)
While setting up clustering with Glassfish 3.1.1, we noticed that our EAR application took ages to deploy: 350 seconds on the DAS, where it isn't even started. At first, I didn't understand why: the same application deploys in about 120 seconds on our production infrastructure, using similar machines.

Now, the strange thing is that it seems to spend most of the time unzipping the application (90 mb ear). I did some quick tests, and could see that the same operation (unzipping all 41 modules) took 2.5 seconds from command-line.

After digging in Glassfish sources, I could find a way to improve things.

Here is how ear deployment works now:

for each module M of ear
get the name of the files that are in module M
for each filename F
extract(M, F)

Now, the method which extracts the file seems to work like this:

find M in ear
for each file F' in M
if F==F'
return F

It seems that, although the extract method is faster than I'd think, it's still not fast enough. Especially, I believe the big-O notation for all of this is of O(n²).

As another quick test, I tried to change the extract method to be faster. With something like this:

if M isn't on the disk somewhere, create a temp file T and write the content of M there
T.getEntry(M,F)
return F

Now, I used java.​util.​jar.​JarFile.getEntry(). It seems that this one is much faster. I don't know what its footprint is, but it seems it is quite fast, as the deployment time was now much lower:

EPCFull was successfully deployed in 32,283 milliseconds.

This is more than a 10:1 improvement :-)

And there is more: I don't see any reason for it to take much more than from command-line. So, this big app could deploy in... 2 seconds (I leave the starting of the application for now, I know this will take time). A developer's dream!

Indeed, a quick sample with the same archive and java.util.jar shows that there is room for further (big) improvements.

So, why does it work like this in Glassfish?

Actually, there is some sense to it. A couple of abstraction layers make it possible to use the same code for different scenarii. Glassfish copies files from a ReadableArchive to a WritableArchive. They could be anything, from a jar to a directory or /dev/null.

In our case, our ReadableArchive is implemented by InputJarArchive. The class responsible for deployment, GenericHandler.java in module internal-api, doesn't know about the implementations details. It just asks for a list of filenames; then asks for a copy of each file, one at a time. This leads to this O(n²) problem.

How to improve that, much further than the patch I applied (and without temp files)? Well, the same module which has an implementation of JarArchive (deployment-common), could have a method which copies the whole jar to some destination (a WritableArchive), at once. This requires modifications in at least three  modules, but would give a lightning fast unzipping, which seems to be the biggest part in our case.

(UPDATE: I did just that, and now have 12 seconds for unzipping, which is good but could go down to 4)

(btw, this isn't related to http://java.net/jira/browse/GLASSFISH-17094)

Here is my Glassfish patch with temp files, which already gives some big improvement in our case:

(module deployment-common, com.sun.enterprise.deployment.deploy.shared.InputJarArchive.java)

# This patch file was generated by NetBeans IDE
# It uses platform neutral UTF-8 encoding and \n newlines.
--- Base (BASE)
+++ Locally Modified (Based On LOCAL)
@@ -61,6 +61,7 @@
import java.util.zip.ZipEntry;
import java.net.URI;
import java.net.URISyntaxException;
+import org.glassfish.api.deployment.archive.WritableArchive;

/**
* This implementation of the Archive deal with reading
@@ -247,27 +248,40 @@
}
} else
if ((parentArchive != null) && (parentArchive.jarFile != null)) {
- JarEntry je;
- // close the current input stream
- if (jarIS!=null) {
- jarIS.close();
- }
-
- // reopen the embedded archive and position the input stream
- // at the beginning of the desired element
- JarEntry archiveJarEntry = (uri != null)? parentArchive.jarFile.getJarEntry(uri.getSchemeSpecificPart()) : null;
+ JarEntry archiveJarEntry = (uri != null) ? parentArchive.jarFile.getJarEntry(uri.getSchemeSpecificPart()) : null;
if (archiveJarEntry == null) {
return null;
}
- jarIS = new JarInputStream(parentArchive.jarFile.getInputStream(archiveJarEntry));
- do {
- je = jarIS.getNextJarEntry();
- } while (je!=null && !je.getName().equals(entryName));
- if (je!=null) {
- return new BufferedInputStream(jarIS);
- } else {
- return null;
+ InputStream inputStream = parentArchive.jarFile.getInputStream(archiveJarEntry);
+ BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
+
+ File tempFile = File.createTempFile("deploy_" + parentArchive.getName() + "_" + archiveJarEntry + "_", "");
+ tempFile.deleteOnExit();
+
+ BufferedOutputStream bufferedOutputStream = null;
+ try {
+ FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
+ bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+
+ FileUtils.copy(bufferedInputStream, fileOutputStream, archiveJarEntry.getSize()); // could work, too
+ } finally {
+ if (bufferedOutputStream != null) {
+ bufferedOutputStream.close();
}
+ bufferedInputStream.close();
+ }
+
+
+ jarFile = new JarFile(tempFile);
+
+ ZipEntry ze = jarFile.getEntry(entryName);
+
+ if (ze != null) {
+ return new BufferedInputStream(jarFile.getInputStream(ze));
+ }
+
+// System.out.println(" New stream time=" + newStreamTime);
+ return null;
} else {
return null;
}

lundi 12 avril 2010

JSF 2 Composite Google Maps Component

I needed a simple Google Maps component for some application. Although there were some examples, none of them was using the composite approach, which I wanted to use for its benefits.

I thought it would be useful to share my example, so here it is.

It accepts a couple of parameters (but has reasonible defaults for most of them): zoom, address, ...

Maybe I'll improve this later. Any suggestion welcome.

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:composite="http://java.sun.com/jsf/composite">

    <!-- INTERFACE -->
    <composite:interface>
        <composite:attribute name="zoom" type="java.lang.Integer" default="14" />
        <composite:attribute name="width" type="java.lang.Integer" default="400" />
        <composite:attribute name="height" type="java.lang.Integer" default="300" />
        <composite:attribute name="address" type="java.lang.String" required="true" />
        <composite:attribute name="mapType" type="java.lang.String" default="ROADMAP" />
        <composite:attribute name="lat" type="java.lang.Number" default="0" />
        <composite:attribute name="long" type="java.lang.Number" default="0" />
    </composite:interface>

    <!-- IMPLEMENTATION -->
    <composite:implementation>
        <h:outputScript target="head">
            var geocoder;
            var map;

            function loadApi(src) {
                var scriptElement = document.createElement("script");
                scriptElement.src = src;
                scriptElement.type = "text/javascript";
                var headElement = document.getElementsByTagName("head")[0];
                headElement.appendChild(scriptElement);
            }
           
            function loadGMapsApi() {
                if (google) {
                    return;
                }
                loadApi("http://maps.google.com/maps/api/js?sensor=false");
            }

            function initializeMap(elementId) {
                loadGMapsApi();
                geocoder = new google.maps.Geocoder();
                var latlng = new google.maps.LatLng(#{cc.attrs.lat}, #{cc.attrs.long});
                var myOptions = {
                    zoom: #{cc.attrs.zoom},
                    center: latlng,
                    mapTypeId: google.maps.MapTypeId.#{cc.attrs.mapType}
                }
                map = new google.maps.Map(document.getElementById(elementId), myOptions);
                gotoAddress("#{cc.attrs.address}");
                }

                function gotoAddress(address) {
                if (geocoder) {
                    geocoder.geocode( { 'address': address}, function(results, status) {
                        if (status == google.maps.GeocoderStatus.OK) {
                            map.setCenter(results[0].geometry.location);
                            var marker = new google.maps.Marker({
                                map: map,
                                position: results[0].geometry.location
                            });
                        } else {
                            //alert("Geocode was not successful for the following reason: " + status);
                        }
                    });
                }
            }
        </h:outputScript>
        <h:panelGroup layout="block" style="width: #{cc.attrs.width}px; height: #{cc.attrs.height}px;" id="map_canvas" />
        <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"/>
        <h:outputScript>
            initializeMap("#{cc.clientId}:map_canvas");
        </h:outputScript>

    </composite:implementation>
</html>