reducing the dimension of an applet

initial post: 02 dec 2008
last update: 02 dec 2008

I will show in this post how I significantly reduced the size of an applet.
The applet jar was 232 KB plus some libraries I used that sum 1.63 MB.
And after some acrobatics I make it to be 168 KB as jar and 75.2 KB as pack200 pack.gz.

my applet: scanApplet.jar - 232K
Filters.jar - 330K
jxlayer.jar - 50K
swingx-0.9.4.jar - 1,191K
TimingFramework-1.0.jar - 100K

The first method I know was trough obfuscation. I am using ProGuard for this. First I had some problems because of dependencies of swingx but while searching for a solution, I found a tool made public by Romain Guy named Harvester. Here is the link:

Very interesting... In the same blog is a link to a similar tool, an ant task named ClassFileSet that is definitely more reliable.

ClassFileSet is inspecting dependencies into a bunch of classes starting from a root class (or multiple root classes), and then is copying the root classes with their dependencies in another directory.

My applet has dependences on swingx.jar, Filters.jar (com.jhlabs), TimingFramework.jar and JXLayer.

So here is for example some ant code that extracts classes from swingx:

<target name="make-swingx">
  <antcall target="clean-dir">
 <param name="param.dirName" value="${dir.applet.deploy}/temp"/>
  <unzip src="${dir.lib}/swing/swingx-0.9.4.jar" dest="${dir.applet.deploy}/temp"/>
  <copy todir="${dir.applet.deploy}/swingx-tmp">
 <classfileset id="reqdClasses" dir="${dir.applet.deploy}/temp">
   <root classname="org.jdesktop.swingx.JXPanel"/>
   <root classname="org.jdesktop.swingx.painter.BusyPainter"/>
   <root classname="org.jdesktop.swingx.painter.ImagePainter"/>
   <root classname="org.jdesktop.swingx.painter.PainterIcon"/>
   <root classname="org.jdesktop.swingx.painter.TextPainter"/>
   <root classname=""/>
   <root classname=""/>
   <root classname="org.jdesktop.swingx.image.GaussianBlurFilter"/>
   <root classname="org.jdesktop.swingx.icon.EmptyIcon"/>
  <jar jarfile="${dir.applet.deploy}/unsigned/mini-swingx.jar">
 <fileset dir="${dir.applet.deploy}/swingx-tmp">
   <include name="**/*.*"/>
  <antcall target="clean-dir">
 <param name="param.dirName" value="${dir.applet.deploy}/swingx-tmp"/>

After running the task: in swingx-tmp are copied 51 files (144K) from a total of 878 files (2,195 K).

Now my mini-swingx.jar has 78K.
The same with:
Filters.jar from 330K to 10K
jxlayer.jar from 50K to 22K
TimingFramework-1.0.jar from 100K to 36K

In root tags from classfileset are all my imports collected from my applet classes (fortunately not too many).

If you wonder what clean-dir is, here is the code:

    <target name="clean-dir">
        <antcall target="make-writable-dir">
            <param name="param.dirName" value="${param.dirName}"/>
        <delete failonerror="false" dir="${param.dirName}"/>
        <mkdir dir="${param.dirName}"/>

    <target name="make-writable-dir">
        <echo>Make writable: attrib -R ${param.dirName} /S /D</echo>
        <exec executable="attrib">
            <arg line="-R"/>
            <arg line="${param.dirName}\*"/>
            <arg line="/S"/>
            <arg line="/D"/>

Here is the main task that I call to make my applet:

<target name="build">
    <antcall target="clean-dir">
        <param name="param.dirName" value="${dir.applet.deploy}"/>
    <mkdir dir="${dir.applet.deploy}/unsigned"/>
    <mkdir dir="${dir.applet.deploy}/unsigned/obfuscated"/>
    <mkdir dir="${dir.applet.deploy}/signed"/>
    <mkdir dir="${dir.applet.deploy}/distributable"/>

    <antcall target="make-swingx"/>
    <antcall target="make-filters"/>
    <antcall target="make-timing"/>
    <antcall target="make-jxlayer"/>

    <antcall target="make.unsigned-jars"/>
    <antcall target="obfuscate.scan-jar"/>

    <antcall target="normalize.obfuscated-jar"/>

    <antcall target="sign.jar">
        <param name="param.jarsToSign" value="obfuscated/scanui.jar,morena.jar,morena_windows.jar,morena_osx.jar,morena_license.jar"/>

    <antcall target="pack200.signed-jar"/>

    <!--<antcall target="copy.jar-to-distributable">
        <param name="param.jarsToCopy" value="scanui.jar,morena.jar,morena_windows.jar,morena_osx.jar,morena_license.jar"/>

So, after extracting only the necessary classes from used libraries, I make my applet jar and copy the dependencies of my dependencies (filters.jar depends on java3d - vecmath.jar, and swingx on MultipleGradientPaint.jar and swing-worker-1.2.jar). I'm not completely sure that this is really necessary. This is done in make.unsigned-jars task:

    <target name="make.unsigned-jars">
        <echo>applet deploy to: ${dir.applet.deploy}</echo>
        <jar jarfile="${dir.applet.deploy}/unsigned/scanui.jar">
            <fileset dir="${dir.classes.compiled-by-ide}">
                <include name="**/*.*"/>
        <echo>Copy: ${lib.obfuscate},${lib.morena}</echo>
        <copy todir="${dir.applet.deploy}/unsigned" flatten="true">
            <filelist dir="${dir.lib}" files="${lib.obfuscate},${lib.morena}"/>

After that I go to the obfuscation:

<target name="obfuscate.scan-jar">
    <taskdef resource="proguard/ant/" classpath="../proGuard/proguard.jar" />
    <proguard printseeds="on" configuration="">
        <!-- Specify the input jars, output jars, and library jars. -->
        <injar file="${dir.applet.deploy}/unsigned/scanui.jar"/>
        <injar file="${dir.applet.deploy}/unsigned/mini-filters.jar"/>
        <injar file="${dir.applet.deploy}/unsigned/mini-jxlayer.jar"/>
        <injar file="${dir.applet.deploy}/unsigned/mini-timing.jar"/>
        <injar file="${dir.applet.deploy}/unsigned/mini-swingx.jar"/>
        <outjar file="${dir.applet.deploy}/unsigned/obfuscated/scanui.jar"/>
        <libraryjar file="${dir.applet.deploy}/unsigned/swing-worker-1.2.jar"/>
        <libraryjar file="${dir.applet.deploy}/unsigned/MultipleGradientPaint.jar"/>
        <libraryjar file="${dir.applet.deploy}/unsigned/vecmath.jar"/>
        <libraryjar file="${dir.applet.deploy}/unsigned/morena.jar"/>

        <libraryjar file="${java.home}\lib\resources.jar"/>
        <libraryjar file="${java.home}\lib\rt.jar"/>
        <libraryjar file="${java.home}\lib\jsse.jar"/>
        <libraryjar file="${java.home}\lib\jce.jar"/>
        <libraryjar file="${java.home}\lib\charsets.jar"/>
        <libraryjar file="${java.home}\lib\ext\dnsns.jar"/>
        <libraryjar file="${java.home}\lib\ext\localedata.jar"/>
        <libraryjar file="${java.home}\lib\ext\sunjce_provider.jar"/>
        <libraryjar file="${java.home}\lib\ext\sunmscapi.jar"/>
        <libraryjar file="${java.home}\lib\ext\sunpkcs11.jar"/>

Here, again, I added too much jars from jre.
Below are my obfuscation properties:
-optimizationpasses 9
-printmapping logs\proguard_printMapping.txt

# needed by org.jdesktop.animation.timing.interpolation.PropertySetter
-keepclassmembers class ro.arhinet.scan.ui.effects.message.DetailsView {
    private float alpha;
    public float getAlpha();
    public void setAlpha(float);

# needed by org.jdesktop.animation.timing.interpolation.PropertySetter
-keepclassmembers class ro.arhinet.scan.ui.effects.PulsatingBorder {
    private float thickness;
    public float getThickness();
    public void setThickness(float);

-keepclassmembers class * extends java.lang.Enum {
    public static **[] values();
    public static ** valueOf(java.lang.String);

# Keep - Applets. Keep all extensions of java.applet.Applet.
-keep public class * extends java.applet.Applet

# Also keep - Serialization code. Keep all fields and methods that are used for
# serialization.
-keepclassmembers class * extends {
    static final long serialVersionUID;
    static final[] serialPersistentFields;
    private void writeObject(;
    private void readObject(;
    java.lang.Object writeReplace();
    java.lang.Object readResolve();

# Also keep - Database drivers. Keep all implementations of java.sql.Driver.
-keep class * extends java.sql.Driver

# Also keep - Swing UI L&F. Keep all extensions of javax.swing.plaf.ComponentUI,
# along with the special 'createUI' method.
-keep class * extends javax.swing.plaf.ComponentUI {
    public static javax.swing.plaf.ComponentUI createUI(javax.swing.JComponent);

# Keep names - Native method names. Keep all native class/method names.
-keepclasseswithmembers,allowshrinking class * {
    native <methods>;

# Remove - System method calls. Remove all invocations of System
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.System {
    public static long currentTimeMillis();
    static java.lang.Class getCallerClass();
    public static int identityHashCode(java.lang.Object);
    public static java.lang.SecurityManager getSecurityManager();
    public static java.util.Properties getProperties();
    public static java.lang.String getProperty(java.lang.String);
    public static java.lang.String getenv(java.lang.String);
    public static java.lang.String mapLibraryName(java.lang.String);
    public static java.lang.String getProperty(java.lang.String,java.lang.String);

# Remove - Math method calls. Remove all invocations of Math
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.Math {
    public static double sin(double);
    public static double cos(double);
    public static double tan(double);
    public static double asin(double);
    public static double acos(double);
    public static double atan(double);
    public static double toRadians(double);
    public static double toDegrees(double);
    public static double exp(double);
    public static double log(double);
    public static double log10(double);
    public static double sqrt(double);
    public static double cbrt(double);
    public static double IEEEremainder(double,double);
    public static double ceil(double);
    public static double floor(double);
    public static double rint(double);
    public static double atan2(double,double);
    public static double pow(double,double);
    public static int round(float);
    public static long round(double);
    public static double random();
    public static int abs(int);
    public static long abs(long);
    public static float abs(float);
    public static double abs(double);
    public static int max(int,int);
    public static long max(long,long);
    public static float max(float,float);
    public static double max(double,double);
    public static int min(int,int);
    public static long min(long,long);
    public static float min(float,float);
    public static double min(double,double);
    public static double ulp(double);
    public static float ulp(float);
    public static double signum(double);
    public static float signum(float);
    public static double sinh(double);
    public static double cosh(double);
    public static double tanh(double);
    public static double hypot(double,double);
    public static double expm1(double);
    public static double log1p(double);

# Remove - Number method calls. Remove all invocations of Number
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.* extends java.lang.Number {
    public static java.lang.String toString(byte);
    public static java.lang.Byte valueOf(byte);
    public static byte parseByte(java.lang.String);
    public static byte parseByte(java.lang.String,int);
    public static java.lang.Byte valueOf(java.lang.String,int);
    public static java.lang.Byte valueOf(java.lang.String);
    public static java.lang.Byte decode(java.lang.String);
    public int compareTo(java.lang.Byte);
    public static java.lang.String toString(short);
    public static short parseShort(java.lang.String);
    public static short parseShort(java.lang.String,int);
    public static java.lang.Short valueOf(java.lang.String,int);
    public static java.lang.Short valueOf(java.lang.String);
    public static java.lang.Short valueOf(short);
    public static java.lang.Short decode(java.lang.String);
    public static short reverseBytes(short);
    public int compareTo(java.lang.Short);
    public static java.lang.String toString(int,int);
    public static java.lang.String toHexString(int);
    public static java.lang.String toOctalString(int);
    public static java.lang.String toBinaryString(int);
    public static java.lang.String toString(int);
    public static int parseInt(java.lang.String,int);
    public static int parseInt(java.lang.String);
    public static java.lang.Integer valueOf(java.lang.String,int);
    public static java.lang.Integer valueOf(java.lang.String);
    public static java.lang.Integer valueOf(int);
    public static java.lang.Integer getInteger(java.lang.String);
    public static java.lang.Integer getInteger(java.lang.String,int);
    public static java.lang.Integer getInteger(java.lang.String,java.lang.Integer);
    public static java.lang.Integer decode(java.lang.String);
    public static int highestOneBit(int);
    public static int lowestOneBit(int);
    public static int numberOfLeadingZeros(int);
    public static int numberOfTrailingZeros(int);
    public static int bitCount(int);
    public static int rotateLeft(int,int);
    public static int rotateRight(int,int);
    public static int reverse(int);
    public static int signum(int);
    public static int reverseBytes(int);
    public int compareTo(java.lang.Integer);
    public static java.lang.String toString(long,int);
    public static java.lang.String toHexString(long);
    public static java.lang.String toOctalString(long);
    public static java.lang.String toBinaryString(long);
    public static java.lang.String toString(long);
    public static long parseLong(java.lang.String,int);
    public static long parseLong(java.lang.String);
    public static java.lang.Long valueOf(java.lang.String,int);
    public static java.lang.Long valueOf(java.lang.String);
    public static java.lang.Long valueOf(long);
    public static java.lang.Long decode(java.lang.String);
    public static java.lang.Long getLong(java.lang.String);
    public static java.lang.Long getLong(java.lang.String,long);
    public static java.lang.Long getLong(java.lang.String,java.lang.Long);
    public static long highestOneBit(long);
    public static long lowestOneBit(long);
    public static int numberOfLeadingZeros(long);
    public static int numberOfTrailingZeros(long);
    public static int bitCount(long);
    public static long rotateLeft(long,int);
    public static long rotateRight(long,int);
    public static long reverse(long);
    public static int signum(long);
    public static long reverseBytes(long);
    public int compareTo(java.lang.Long);
    public static java.lang.String toString(float);
    public static java.lang.String toHexString(float);
    public static java.lang.Float valueOf(java.lang.String);
    public static java.lang.Float valueOf(float);
    public static float parseFloat(java.lang.String);
    public static boolean isNaN(float);
    public static boolean isInfinite(float);
    public static int floatToIntBits(float);
    public static int floatToRawIntBits(float);
    public static float intBitsToFloat(int);
    public static int compare(float,float);
    public boolean isNaN();
    public boolean isInfinite();
    public int compareTo(java.lang.Float);
    public static java.lang.String toString(double);
    public static java.lang.String toHexString(double);
    public static java.lang.Double valueOf(java.lang.String);
    public static java.lang.Double valueOf(double);
    public static double parseDouble(java.lang.String);
    public static boolean isNaN(double);
    public static boolean isInfinite(double);
    public static long doubleToLongBits(double);
    public static long doubleToRawLongBits(double);
    public static double longBitsToDouble(long);
    public static int compare(double,double);
    public boolean isNaN();
    public boolean isInfinite();
    public int compareTo(java.lang.Double);
    public <init>(byte);
    public <init>(short);
    public <init>(int);
    public <init>(long);
    public <init>(float);
    public <init>(double);
    public <init>(java.lang.String);
    public byte byteValue();
    public short shortValue();
    public int intValue();
    public long longValue();
    public float floatValue();
    public double doubleValue();
    public int compareTo(java.lang.Object);
    public boolean equals(java.lang.Object);
    public int hashCode();
    public java.lang.String toString();

# Remove - String method calls. Remove all invocations of String
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.String {
    public <init>();
    public <init>(byte[]);
    public <init>(byte[],int);
    public <init>(byte[],int,int);
    public <init>(byte[],int,int,int);
    public <init>(byte[],int,int,java.lang.String);
    public <init>(byte[],java.lang.String);
    public <init>(char[]);
    public <init>(char[],int,int);
    public <init>(java.lang.String);
    public <init>(java.lang.StringBuffer);
    public static java.lang.String copyValueOf(char[]);
    public static java.lang.String copyValueOf(char[],int,int);
    public static java.lang.String valueOf(boolean);
    public static java.lang.String valueOf(char);
    public static java.lang.String valueOf(char[]);
    public static java.lang.String valueOf(char[],int,int);
    public static java.lang.String valueOf(double);
    public static java.lang.String valueOf(float);
    public static java.lang.String valueOf(int);
    public static java.lang.String valueOf(java.lang.Object);
    public static java.lang.String valueOf(long);
    public boolean contentEquals(java.lang.StringBuffer);
    public boolean endsWith(java.lang.String);
    public boolean equalsIgnoreCase(java.lang.String);
    public boolean equals(java.lang.Object);
    public boolean matches(java.lang.String);
    public boolean regionMatches(boolean,int,java.lang.String,int,int);
    public boolean regionMatches(int,java.lang.String,int,int);
    public boolean startsWith(java.lang.String);
    public boolean startsWith(java.lang.String,int);
    public byte[] getBytes();
    public byte[] getBytes(java.lang.String);
    public char charAt(int);
    public char[] toCharArray();
    public int compareToIgnoreCase(java.lang.String);
    public int compareTo(java.lang.Object);
    public int compareTo(java.lang.String);
    public int hashCode();
    public int indexOf(int);
    public int indexOf(int,int);
    public int indexOf(java.lang.String);
    public int indexOf(java.lang.String,int);
    public int lastIndexOf(int);
    public int lastIndexOf(int,int);
    public int lastIndexOf(java.lang.String);
    public int lastIndexOf(java.lang.String,int);
    public int length();
    public java.lang.CharSequence subSequence(int,int);
    public java.lang.String concat(java.lang.String);
    public java.lang.String replaceAll(java.lang.String,java.lang.String);
    public java.lang.String replace(char,char);
    public java.lang.String replaceFirst(java.lang.String,java.lang.String);
    public java.lang.String[] split(java.lang.String);
    public java.lang.String[] split(java.lang.String,int);
    public java.lang.String substring(int);
    public java.lang.String substring(int,int);
    public java.lang.String toLowerCase();
    public java.lang.String toLowerCase(java.util.Locale);
    public java.lang.String toString();
    public java.lang.String toUpperCase();
    public java.lang.String toUpperCase(java.util.Locale);
    public java.lang.String trim();

# Remove - StringBuffer method calls. Remove all invocations of StringBuffer
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.StringBuffer {
    public <init>();
    public <init>(int);
    public <init>(java.lang.String);
    public <init>(java.lang.CharSequence);
    public java.lang.String toString();
    public char charAt(int);
    public int capacity();
    public int codePointAt(int);
    public int codePointBefore(int);
    public int indexOf(java.lang.String,int);
    public int lastIndexOf(java.lang.String);
    public int lastIndexOf(java.lang.String,int);
    public int length();
    public java.lang.String substring(int);
    public java.lang.String substring(int,int);

# Remove - StringBuilder method calls. Remove all invocations of StringBuilder
# methods without side effects whose return values are not used.
-assumenosideeffects public class java.lang.StringBuilder {
    public <init>();
    public <init>(int);
    public <init>(java.lang.String);
    public <init>(java.lang.CharSequence);
    public java.lang.String toString();
    public char charAt(int);
    public int capacity();
    public int codePointAt(int);
    public int codePointBefore(int);
    public int indexOf(java.lang.String,int);
    public int lastIndexOf(java.lang.String);
    public int lastIndexOf(java.lang.String,int);
    public int length();
    public java.lang.String substring(int);
    public java.lang.String substring(int,int);

Note that PropertySetter from TimingFramework use introspection. Also java 5 enums need some special attention. The rest of the properties are copied from ProGuard site.

After obfuscation, my whole jar has 157K.

For Pack200 I followed the examples from:

The sign task is:

<target name="sign.jar">
    <signjar destDir="${dir.applet.deploy}/signed"
        alias="arhinet" keystore="${basedir}/certif/arhinetstore"
        <filelist dir="${dir.applet.deploy}/unsigned" files="${param.jarsToSign}"/>

And tasks normalize.obfuscated-jar and pack200.signed-jar are simple system calls to ${java.home}/bin/pack200.exe:

<target name="normalize.obfuscated-jar">
    <exec executable="${java.home}/bin/pack200.exe" spawn="true">
        <arg line="--repack ${dir.applet.deploy}/unsigned/obfuscated/scanui.jar"/>

<target name="pack200.signed-jar">
    <exec executable="${java.home}/bin/pack200.exe" spawn="true">
        <arg line="${dir.applet.deploy}/signed/scanui.jar.pack.gz ${dir.applet.deploy}/signed/scanui.jar"/>

After signing the jar, my applet is approximately the same size: 168K.
But the pack.gz file is very small: scanui.jar.pack.gz - 75K

And miraculously the applet is still working!

No comments: