Tuesday, April 27, 2004

Java, JDBC - How to Handle Exceptions and Errors When Closing, or Freeing, Database Resources

When closing database resources it is possible to get multiple exceptions and it is important not to lose any of them. All of the tutorials that I have read only show the use of printStackTrace() during exception handling. However, that approach is not sophisticated enough for Enterprise applications. I use the following technique: Exceptions (and Errors if applicable) are held in an ArrayList until all resources are closed. Then the list is examined. If exceptions are present, they are processed as needed.

    // This code is part of my DataAccess class. Other code in the class is responsible for
    // opening the database connection and creating a prepared statement - both are
    // static objects.

    public synchronized static void free() {
        // This list holds any exception objects that are caught.
        List caughtExceptions = new ArrayList();
        
        if (ps != null) {
            try {
                ps.close();
                logger.debug("free; closed prepared statement.");
            } catch (SQLException e) {
                caughtExceptions.add(e);
            }
        }
        if (con != null) {
            try {
                con.close();
	    	logger.debuf("free; closed database connection.");
            } catch (SQLException e) {
                caughtExceptions.add(e);
            }
        }
        
        if (caughtExceptions.size() > 0) {
            LogConfiguration.message(caughtExceptions, "Problem closing database resources.");
        }
    }

    // Here is the LogConfiguration.message() method which resides in a different class
    // from the above code.

	public static void message(final List exceptions, final String message) {
	    logger.fatal(message);
	    int throwableIndex = 1;
	    int throwableCount = exceptions.size();
	    for (Iterator iter = exceptions.iterator(); iter.hasNext(); ) {
	        Throwable t = (Throwable) iter.next();
		    logger.fatal("Exception [" + throwableIndex + "] of [" + throwableCount + "]", t);
		    throwableIndex++;
	    }
	}

Monday, April 26, 2004

POI Optimization - eliminating trailing empty rows from an HSSFSheet.

While my spreadsheet has only 7 rows with data, POI creates over 65,000 rows in the HSSFSheet object. This leads to a large amount of essentially unused memory. In order to free that memory, the following code snippet from my version of the public HSSFWorkbook(POIFSFileSystem fs, boolean preserveNodes) method works from bottom towards the top of the sheet removing empty rows. The code in bold performs the optimization.

            HSSFSheet hsheet = new HSSFSheet(workbook, sheet);

            boolean stop = false;
            boolean nonBlankRowFound;
            short c;
            HSSFRow lastRow = null;
            HSSFCell cell = null;

            while (stop == false) {
                nonBlankRowFound = false;
                lastRow = hsheet.getRow(hsheet.getLastRowNum());
                for (c = lastRow.getFirstCellNum(); c <= lastRow.getLastCellNum(); c++) {
                    cell = lastRow.getCell(c);
                    if (cell != null && lastRow.getCell(c).getCellType() != HSSFCell.CELL_TYPE_BLANK) {
                        nonBlankRowFound = true;
                    }
                }
                if (nonBlankRowFound == true) {
                    stop = true;
                } else {
                    hsheet.removeRow(lastRow);
                }
            }

            sheets.add(hsheet);

Thursday, April 22, 2004

POI Optimization - Speeding up org.apache.poi.hssf.usermodel.HSSFSheet.setPropertiesFromSheet()

This setPropertiesFromSheet() method potentially logs a lot of information. The logging calls are expensive because of the objects created when assembling the log messages. Using the check() method of the logging sub-system can prevent object creation when debugging is turned off. This change reduced execution time of my test case by 14.2 seconds and prevented the creation of 781,632 objects.

    /**
     * used internally to set the properties given a Sheet object
     */
    private void setPropertiesFromSheet(Sheet sheet) {
        long timestart = 0; // only used for debugging.
        int sloc = sheet.getLoc();
        RowRecord row = sheet.getNextRow();

        while (row != null) {
            createRowFromRecord(row);
            row = sheet.getNextRow();
        }
        sheet.setLoc(sloc);
        CellValueRecordInterface cval = sheet.getNextValueRecord();

        if (log.check(DEBUG)) {
            timestart = System.currentTimeMillis();
            log.log(DEBUG, "Time at start of cell creating in HSSF sheet = ", new Long(timestart));
        }
        HSSFRow lastrow = null;

        while (cval != null) {
            long cellstart = System.currentTimeMillis();
            HSSFRow hrow = lastrow;

            if ((lastrow == null) || (lastrow.getRowNum() != cval.getRow())) {
                hrow = getRow(cval.getRow());
            }
            if (hrow != null) {
                lastrow = hrow;
                if (log.check(DEBUG)) {
                    log.log(DEBUG, "record id = " + Integer.toHexString(((Record) cval).getSid()));
                }
                hrow.createCellFromRecord(cval);
                cval = sheet.getNextValueRecord();
                if (log.check(DEBUG)) {
                    log.log(DEBUG, "record took ", new Long(System.currentTimeMillis() - cellstart));
                }
            } else {
                cval = null;
            }
        }
        if (log.check(DEBUG)) {
            log.log(DEBUG, "total sheet cell creation took ", new Long(System.currentTimeMillis() - timestart));
        }
    }

POI Optimization - Speeding up org.apache.poi.hssf.record.BlankRecord.compareTo()

This optimization is nearly identical to the one applied to HSSFRow.compareTo() - just used a local copy of the row and column values. This change reduced exceution time in my test case by 17.5 seconds.

    /**
     * switched to using a local copy of the row and column. Also moved the equality test to 
	 * the last test because it is most complex and probably least likely to be true.
     */
    public int compareTo(Object obj) {
        int rv = -1;
        CellValueRecordInterface loc = (CellValueRecordInterface) obj;

        int thisRow = this.getRow();
        int locRow = loc.getRow();

        if (thisRow < locRow) {
            rv = -1;
        } else if (thisRow > locRow) {
            rv = 1;
        } else {
            int thisColumn = this.getColumn();
            int locColumn = loc.getColumn();

            if (thisColumn > locColumn) {
                rv = 1;
            } else if (thisColumn < locColumn) {
                rv = -1;
            } else if ((thisRow == locRow) && (thisColumn == locColumn)) {
                rv = 0;
            }
        }
        return rv;
    }

POI Optimization - Speeding up org.apache.poi.hssf.usermodel.HSSFRow.compareTo()

The compareTo() method calls getRowNum() up to three times. Normally this isn't a problem however the compareTo() method is called a lot - over three million times in my test case which uses a small Excel spreadsheet.

Using a local int to cache the values reduced the exection time by 26.3 seconds. Here is the new version of compareTo():

    public int compareTo(Object obj) {
        HSSFRow loc = (HSSFRow) obj;
        int rv = -1;
        int thisRowNum = this.getRowNum(); 
        int locRowNum = loc.getRowNum(); 

        if (thisRowNum == locRowNum) {
            rv = 0;
        } else if (thisRowNum < locRowNum) {
            rv = -1;
        } if (thisRowNum > locRowNum) {
            rv = 1;
        }
        return rv;
    }

Java; POI Optimization - Speeding up the RecordFactory class.

My last post introduced a shortToConstructorCache class. This post shows a changed RecordFactory class that uses it. In my test case, I was able to reduce the execution time of RecordFactory.createRecord() by 3.6 seconds and reduce the number of objects created by 466,586.

/*
 * ==================================================================== 
 * Copyright 2002-2004 Apache Software Foundation
 * 
 * Licensed 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.poi.hssf.record;

import org.apache.poi.util.LittleEndian;
import org.wwre.common.shortToShortCache;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.*;

/**
 * Title: Record Factory
 * 

* Description: Takes a stream and outputs an array of Record objects. *

* * @deprecated use EventRecordFactory instead * @see org.apache.poi.hssf.eventmodel.EventRecordFactory * @author Andrew C. Oliver (acoliver at apache dot org) * @author Marc Johnson (mjohnson at apache dot org) * @author Glen Stampoultzis (glens at apache.org) * @author Csaba Nagy (ncsaba at yahoo dot com) * @version 1.0-pre */ public class RecordFactory { private static int NUM_RECORDS = 10000; private static shortToConstructorCache scm = null; private static shortToShortCache ssc = new shortToShortCache(); static { Class[] records; if (FormulaRecord.EXPERIMENTAL_FORMULA_SUPPORT_ENABLED) { records = new Class[] { BOFRecord.class, InterfaceHdrRecord.class, MMSRecord.class, InterfaceEndRecord.class, WriteAccessRecord.class, CodepageRecord.class, DSFRecord.class, TabIdRecord.class, FnGroupCountRecord.class, WindowProtectRecord.class, ProtectRecord.class, PasswordRecord.class, ProtectionRev4Record.class, PasswordRev4Record.class, WindowOneRecord.class, BackupRecord.class, HideObjRecord.class, DateWindow1904Record.class, PrecisionRecord.class, RefreshAllRecord.class, BookBoolRecord.class, FontRecord.class, FormatRecord.class, ExtendedFormatRecord.class, StyleRecord.class, UseSelFSRecord.class, BoundSheetRecord.class, CountryRecord.class, SSTRecord.class, ExtSSTRecord.class, EOFRecord.class, IndexRecord.class, CalcModeRecord.class, CalcCountRecord.class, RefModeRecord.class, IterationRecord.class, DeltaRecord.class, SaveRecalcRecord.class, PrintHeadersRecord.class, PrintGridlinesRecord.class, GridsetRecord.class, GutsRecord.class, DefaultRowHeightRecord.class, WSBoolRecord.class, HeaderRecord.class, FooterRecord.class, HCenterRecord.class, VCenterRecord.class, PrintSetupRecord.class, DefaultColWidthRecord.class, DimensionsRecord.class, RowRecord.class, LabelSSTRecord.class, RKRecord.class, NumberRecord.class, DBCellRecord.class, WindowTwoRecord.class, SelectionRecord.class, ContinueRecord.class, LabelRecord.class, BlankRecord.class, ColumnInfoRecord.class, MulRKRecord.class, MulBlankRecord.class, MergeCellsRecord.class, FormulaRecord.class, BoolErrRecord.class, ExternSheetRecord.class, NameRecord.class, LeftMarginRecord.class, RightMarginRecord.class, TopMarginRecord.class, BottomMarginRecord.class, DrawingRecord.class, DrawingGroupRecord.class, DrawingSelectionRecord.class, ObjRecord.class, TextObjectRecord.class, PaletteRecord.class, StringRecord.class, RecalcIdRecord.class, SharedFormulaRecord.class, HorizontalPageBreakRecord.class, VerticalPageBreakRecord.class }; } else { records = new Class[] { BOFRecord.class, InterfaceHdrRecord.class, MMSRecord.class, InterfaceEndRecord.class, WriteAccessRecord.class, CodepageRecord.class, DSFRecord.class, TabIdRecord.class, FnGroupCountRecord.class, WindowProtectRecord.class, ProtectRecord.class, PasswordRecord.class, ProtectionRev4Record.class, PasswordRev4Record.class, WindowOneRecord.class, BackupRecord.class, HideObjRecord.class, DateWindow1904Record.class, PrecisionRecord.class, RefreshAllRecord.class, BookBoolRecord.class, FontRecord.class, FormatRecord.class, ExtendedFormatRecord.class, StyleRecord.class, UseSelFSRecord.class, BoundSheetRecord.class, CountryRecord.class, SSTRecord.class, ExtSSTRecord.class, EOFRecord.class, IndexRecord.class, CalcModeRecord.class, CalcCountRecord.class, RefModeRecord.class, IterationRecord.class, DeltaRecord.class, SaveRecalcRecord.class, PrintHeadersRecord.class, PrintGridlinesRecord.class, GridsetRecord.class, GutsRecord.class, DefaultRowHeightRecord.class, WSBoolRecord.class, HeaderRecord.class, FooterRecord.class, HCenterRecord.class, VCenterRecord.class, PrintSetupRecord.class, DefaultColWidthRecord.class, DimensionsRecord.class, RowRecord.class, LabelSSTRecord.class, RKRecord.class, NumberRecord.class, DBCellRecord.class, WindowTwoRecord.class, SelectionRecord.class, ContinueRecord.class, LabelRecord.class, BlankRecord.class, ColumnInfoRecord.class, MulRKRecord.class, MulBlankRecord.class, MergeCellsRecord.class, BoolErrRecord.class, ExternSheetRecord.class, NameRecord.class, LeftMarginRecord.class, RightMarginRecord.class, TopMarginRecord.class, BottomMarginRecord.class, PaletteRecord.class, StringRecord.class, RecalcIdRecord.class, SharedFormulaRecord.class, DrawingRecord.class, DrawingGroupRecord.class, DrawingSelectionRecord.class, ObjRecord.class, TextObjectRecord.class, HorizontalPageBreakRecord.class, VerticalPageBreakRecord.class}; } scm = new shortToConstructorCache(records); } /** * changes the default capacity (10000) to handle larger files */ public static void setCapacity(int capacity) { NUM_RECORDS = capacity; } /** * Create an array of records from an input stream * * @param in the InputStream from which the records will be obtained * * @return an array of Records created from the InputStream * * @exception RecordFormatException on error processing the InputStream */ public static List createRecords(InputStream in) throws RecordFormatException { ArrayList records = new ArrayList(NUM_RECORDS); Record last_record = null; try { short rectype = 0; do { rectype = LittleEndian.readShort(in); if (rectype != 0) { short recsize = LittleEndian.readShort(in); byte[] data = new byte[recsize]; in.read(data); Record[] recs = createRecord(rectype, recsize, data); // handle MulRK records if (recs.length > 1) { for (int k = 0; k < recs.length; k++) { records.add(recs[k]); // these will be number records last_record = recs[k]; // do to keep the algorythm homogenous...you can't } // actually continue a number record anyhow. } else { Record record = recs[0]; if (record != null) { if (rectype == ContinueRecord.sid && !(last_record instanceof ContinueRecord) && // include continuation records after !(last_record instanceof UnknownRecord)) // unknown records or previous continuation records { if (last_record == null) { throw new RecordFormatException("First record is a ContinueRecord??"); } last_record.processContinueRecord(data); } else { last_record = record; records.add(record); } } } } } while (rectype != 0); } catch (IOException e) { throw new RecordFormatException("Error reading bytes"); } // Record[] retval = new Record[ records.size() ]; // retval = ( Record [] ) records.toArray(retval); return records; } public static Record[] createRecord(short rectype, short size, byte[] data) { Record retval = null; Record[] realretval = null; try { Constructor constructor = scm.get(rectype); if (constructor != null) { retval = (Record) constructor.newInstance(new Object[] {ssc.get(rectype), ssc.get(size), data}); } else { retval = new UnknownRecord(rectype, size, data); } } catch (Exception introspectionException) { introspectionException.printStackTrace(); throw new RecordFormatException("Unable to construct record instance, the following exception occured: " + introspectionException.getMessage()); } if (retval instanceof RKRecord) { RKRecord rk = (RKRecord) retval; NumberRecord num = new NumberRecord(); num.setColumn(rk.getColumn()); num.setRow(rk.getRow()); num.setXFIndex(rk.getXFIndex()); num.setValue(rk.getRKNumber()); retval = num; } else if (retval instanceof DBCellRecord) { retval = null; } else if (retval instanceof MulRKRecord) { MulRKRecord mrk = (MulRKRecord) retval; realretval = new Record[mrk.getNumColumns()]; for (int k = 0; k < mrk.getNumColumns(); k++) { NumberRecord nr = new NumberRecord(); nr.setColumn((short) (k + mrk.getFirstColumn())); nr.setRow(mrk.getRow()); nr.setXFIndex(mrk.getXFAt(k)); nr.setValue(mrk.getRKNumberAt(k)); realretval[k] = nr; } } else if (retval instanceof MulBlankRecord) { MulBlankRecord mb = (MulBlankRecord) retval; realretval = new Record[mb.getNumColumns()]; for (int k = 0; k < mb.getNumColumns(); k++) { BlankRecord br = new BlankRecord(); br.setColumn((short) (k + mb.getFirstColumn())); br.setRow(mb.getRow()); br.setXFIndex(mb.getXFAt(k)); realretval[k] = br; } } if (realretval == null) { realretval = new Record[1]; realretval[0] = retval; } return realretval; } public static short[] getAllKnownRecordSIDs() { return scm.getTable(); } }

Java; POI Optimization - Speeding up the RecordFactory class.

This optimization needs two posts. This post presents a class that maps short values to a Constructor object. The RecordFactory class maps the short id value in the Excel file to a Java class. In the current version of POI, that relationship is kept, quite logically, in a Map object. However, using a Map object required that the short value be converted into a Short object quite frequently. This conversion is inefficient and, if some specially coding is done, unneeded. Note that the following class, shortToConstructorCache, is derivative of my earlier shortShortCache class.

/**
 * Cache a mapping from a short value to a Constructor object.
 * 
 * This class is designed to optimize the RecordFactory.createRecord()
 * method. It might be useful in other contexts, but I did not check.
 */
public class shortToConstructorCache {

    /**
     * The number of entries in the cache.
     */
    protected int       distinct;

    /**
     * The cached short values.
     */
    private short       table[];

    /**
     * The cached constructor methods
     */
    private Constructor values[];

    /**
     * RecordFactory uses a statically created array of
     * classes to initialize the cache and the entries
     * in the cache are never changed nor added to.  
     */
    public shortToConstructorCache(Class[] records) {
        super();
        
    	this.table = new short[records.length];
    	this.values = new Constructor[records.length];

        Constructor constructor;

        for (int i = 0; i < records.length; i++) {
            Class record = null;
            short sid = 0;

            record = records[i];
            try {
                sid = record.getField("sid").getShort(null);
                constructor = record.getConstructor(new Class[] {short.class, short.class, byte[].class});
            } catch (Exception illegalArgumentException) {
                illegalArgumentException.printStackTrace();
                throw new RecordFormatException("Unable to determine record types");
            }

            if (constructor == null) {
                throw new RecordFormatException("Unable to get constructor for sid [" + sid + "].");
            } else {
                this.table[this.distinct] = sid;
                this.values[this.distinct] = constructor;
                this.distinct++;
            }
        }
    }

    /** Gets the Constructor object related to a given
     * key.
     */
    public Constructor get(short key) {
        Constructor rv = null;
        
        	for (int i = 0; i < this.distinct; i++) {
        	    if (this.table[i] == key) {
        	        rv = this.values[i];
        	    }
        	}
        
        return rv;
    }
    
    /** Returns the number of entries in the cache. */
    public int size() {
        return this.distinct;
    }
    
    /** We're breaking encapsulation but some code in RecordFactory
     * wants the information and I want to change RecordFactory as
     * little as possible.
     */
    public short[] getTable() {
        return this.table;
    }

}

Java; Caching Short Objects

While looking at the POI source code, I noticed that a lot of Short objects were being created. So I looked around for a small stand-alone class that would allow me to cache Short object. I did see some pages devoted to sparse arrays and matrixes (notably the COLT package) however they were too large for my purposes.

I wrote the shortShortCache class shown below. It usage should be pretty obvious but please contact me if you have any difficulty using the class or suggestions for improvement.

/**
 * Provides a cache for Short objects. This class should be used when the same short values 
 * are used over and over to avoid the cost of continously creating the same Short objects.
 * 
 * Since the get() method creates Short objects and adds them to the cache, the put() 
 * method never needs to be called outside this class.
 */

public class shortShortCache {

    /** This is how the cache is used. */
    public static void main(String[] args) {
        shortShortCache ssm = new shortShortCache();
        short numEntries = (short) 2000;
        short start = (short) 23454;
        short middle = (short) (start + (numEntries / 2));
        short end = (short) (start + numEntries);
        for (short i = start; i <= end; i++) {
            ssm.put(i);
        }
        // is the first short cached?
        System.out.println(start + ": " + ssm.get(start));
        // is the middle short cached?.
        System.out.println(middle + ": " + ssm.get(middle));
        // is the last short cached?
        System.out.println(end + ": " + ssm.get(end));
        System.out.println("Done.");
    }

    /** The initalize size of the cache. */
    private int   initialSize     = 500;

    /** How much to grow the cache when its capacity is reached. */
    private int   increment       = 500;

    /** The size of the cache, which is not the same as the number of entries in the caceh. */
    private int   currentCapacity = 0;

    /**
     * The number of entries in the cache.
     */
    protected int distinct        = 0;

    /** The maximum number of entries so that the cache doesn't grow unbounded. */
    protected int maxEntries      = 3000;

    /**
     * The cached short values.
     */
    private short table[];

    /**
     * The cached Short methods
     */
    private Short values[];

    /** A no-args constructor which uses all of the defaults. */
    public shortShortCache() {
        super();
        clear();
    }

    /** A constructor that lets the user set the control variables. */
    public shortShortCache(final int _initialSize, final int _increment, final int _maxEntries) {
        super();
        this.initialSize = _initialSize;
        this.increment = _increment;
        // we quietly handle the error of a _maxEntries parameter less than the _initialSize parameter.
        if (_maxEntries < _initialSize) {
            this.maxEntries = _initialSize;
        } else if (_maxEntries > Short.MAX_VALUE) {
            this.maxEntries = Short.MAX_VALUE;
        } else {
            this.maxEntries = _maxEntries;
        }
        clear();
    }

    /** Create and/or clear the cache. */
    public void clear() {
        this.table = new short[this.initialSize];
        this.values = new Short[this.initialSize];
        this.currentCapacity = this.initialSize;
        this.distinct = 0;
    }

    /**
     * Returns the value to which this map maps the specified key. If the short value 
     * is not in the cache, then create a Short object automatically.
     */
    public Short get(short key) {
        Short rv = null;

        for (int i = 0; i < this.distinct; i++) {
            if (this.table[i] == key) {
                rv = this.values[i];

            }
        }

        // If the key is not in the cache, then add it
        // to the cache.
        if (rv == null) {
            rv = new Short(key);
            if (this.currentCapacity < this.maxEntries) {
                put(key);
            }
        }

        return rv;
    }

    /**
     * Add a mapping from a short to a Short object.
     * 
     * If the size of the cache is too small, then expand it. If the size of the cache 
     * is more than the maxEntries, then return. The get() method automatically 
     * creates a Short object for any short values not in the cache.
     */
    public void put(short key) {
        if (this.currentCapacity < this.maxEntries) {
            if (this.distinct == this.currentCapacity) {
                int newCapacity = this.currentCapacity + this.increment;

                // store the current cache.
                short oldTable[] = this.table;
                Short oldValues[] = this.values;

                // create new arrays.
                short newTable[] = new short[newCapacity];
                Short newValues[] = new Short[newCapacity];

                this.table = newTable;
                this.values = newValues;

                // move info from old table to new table.
                for (int i = this.currentCapacity; i-- > 0;) {
                    newTable[i] = oldTable[i];
                    newValues[i] = oldValues[i];
                }

                this.currentCapacity = newCapacity;
            }
            this.table[this.distinct] = key;
            this.values[this.distinct] = new Short(key);
            this.distinct++;
        }
    }

    /** Returns the number of key-value mappings in this map. */
    public int size() {
        return this.distinct;
    }

    /**
     * Returns true if this map contains a mapping for the specified key.
     */
    public boolean containsKey(short key) {
        boolean rv = false;

        for (int i = 0; i < this.distinct; i++) {
            if (this.table[i] == key) {
                rv = true;
                break;
            }
        }
        return rv;
    }

    /**
     * Returns true if this map maps one or more keys to the specified value.
     */
    public boolean containsValue(Short value) {
        boolean rv = false;

        if (value != null) {
            for (int i = 0; i < this.distinct; i++) {
                if (this.values[i].equals(value)) {
                    rv = true;
                    break;
                }
            }
        }
        return rv;
    }

    /**
     * Returns true if this map contains no key-value mappings.
     */
    public boolean isEmpty() {
        boolean rv = false;
        if (this.distinct > 0) {
            rv = true;
        }
        return rv;
    }

    /**
     * From http://www.ftponline.com/javapro/2004_03/online/rule4_03_31_04/:
     * 
     * Java's object cloning mechanism can allow an attacker to manufacture new 
     * instances of classes that you define�without executing any of the class's 
     * constructors. Even if your class is not cloneable, the attacker can define a 
     * subclass of your class, make the subclass implement java.lang.Cloneable, 
     * and then create new instances of your class by copying the memory images 
     * of existing objects. By defining this clone method, you will prevent such attacks.
     */
    public final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /**
     * List all short values in the cache.
     */
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("shortShortMap[" + this.distinct + "]: ");
        for (int i = 0; i < this.distinct; i++) {
            sb.append(this.table[i] + ", ");
        }
        return sb.toString();
    }
}

Wednesday, April 21, 2004

Catching Java Errors in the main() Method.

I've been using Java for more years than I care to mention but I never realized that you could catch errors as well as exceptions. So I've just added one more item to my toolbox. My main() method always uses this template:

try {
  // do something.
} catch (Error e) {
  e.printStackTrace();
} catch (Exception e) {
  e.printStrackTrace();
} finally {
  System.out.println("Done.");
}

Of course, in my real code I use log4j so that the errors are persisted.

Thursday, April 08, 2004

Optimization for JudoScript: JavaObject.pickMethod is an expensive call.

The getMethods() method of the Class class is an expensive call to make. So I added a static map to hold the method array between invokations of pickMethod(). This change reduced execution time of my test case by 3.4%. Instead of calling getMethods() 15,032 times it is only called once. And instead of creating 3.5 million objects only 386 objects are created. A nice bit of optimization if I do say so myself!

    private static final Map methodsInClass = new HashMap();
    
    final MatchFinder pickMethod(String methodName, Variable[] paramVals, int[] javaTypes) throws Exception {
        Class cls = null;
        for (int x = 0; x < this.classes.length; x++) {
            cls = this.classes[x];
            
            // make sure the map can't grow unbounded.
            if (methodsInClass.size() > 200) {
                methodsInClass.clear();
            }
            Method[] mthds = (Method[]) methodsInClass.get(cls);
            if (mthds == null) {
                mthds = cls.getMethods();
                methodsInClass.put(cls, mthds);
            }
            
            int len = (paramVals == null) ? 0 : paramVals.length;
            MatchFinder ret = new MatchFinder(0.0, len, javaTypes);
            for (int i = 0; i < mthds.length; i++) {
                if (mthds[i].getName().equals(methodName)) {
                    MatchFinder mf = matchParams(mthds[i].getParameterTypes(), paramVals, javaTypes);
                    if (mf.score > ret.score) {
                        ret = mf;
                        ret.method = mthds[i];
                    }
                }
            }
            if (ret.score > 0.0)
                return ret;
        }
        return NOT_MATCH;
    }

Optimization for JudoScript: JudoUtil.registerToBSF is an expensive method to call.

It seems to me that a scripting engine only needs to register with the BSF engine once. I think that JudoScript registers with the engine every time a script is invoked. In order to avoid this situation, I added a static variable to track the bsfStatus. In the test case that I was executed, I was able to reduce calls to Class.forName() from 8,061 to 43 and the created objects from 649,620 to 162. This also saved 2.6% of execution time.

    public static String bsfStatus = "UNKNOWN";

    public static void registerToBSF() {
        if (bsfStatus.equals("UNKNOWN")) {
            try {
                Class[] params = new Class[] {String.class, String.class, Class.forName("[Ljava.lang.String;")};
                Method m = Class.forName("com.ibm.bsf.BSFManager").getMethod("registerScriptingEngine", params);
                Object[] vals = new Object[] {"judoscript", "com.judoscript.BSFJudoEngine", new String[] {"judo", "jud"}};
                m.invoke(null, vals);
                bsfStatus = "AVAILABLE";
            } catch (Exception e) {
                bsfStatus = "UNAVAILABLE";
            } // if BSF is not there, so be it.
        }
    }

Monday, April 05, 2004

Jprobe Analysis Reduces Execution Time by 5%

Using JProbe, I noticed that my application was repeatedly compiling the same JudoScript scripts. I added a cache for compiled JudoScript scripts which reduced the calls to ParserHelper.parse():

RunCallsCumulative TimeCumulative Objects PercentCumulative Objects
11,3005.5%13.6%1,116,603
2130.3%0.3%23,456

Not a bad first optimization for our first use of the JProbe profiler.