/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * ClassGranularityEffectLoadTimeTester.java * Creation date: Jul 17, 2006. * By: Edward Lam */ package org.openquark.cal.machine; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import org.openquark.cal.services.FileSystemResourceHelper; import org.openquark.util.FileChangeOutputStream; import org.openquark.util.FileSystemHelper; import org.openquark.util.TextEncodingUtilities; /** * This is a helper class in determining whether loading many small classes is substantially different from loading the equivalent code from fewer large classes. * This class generates java source for classes falling into two cases: * 1) Many small classes. * 2) One large class. This class contains the equivalent code to the classes generated in (1). * It is the intention that the generated classes for these two cases will have code that is roughly equivalent. * * These classes are compiled, and packaged into a .jar file. * A single test method for (1) or (2) exercises all the generated code. This is invoked when benchmarking over a number of runs. * * @author Edward Lam */ /* Results (from July 19/06) -- executing from .jar files, no jvm args. System: Dual P4 Xeon, 2.4 Ghz, 2GB RAM, WinXP Pro SP2 file sizes in bytes (not bytes on disk). Note: for "one compound", #file = 1; for "many simple", #files = scale + 1. jdk1.4.2_07 one compound many simple ratios Scale result file size time (ms) file size time (ms) file size time 100 4950 20,471 11 48,748 109 2.38132 9.909091 200 19900 40,915 18 97,820 207 2.39081 11.5 500 124750 102,415 45 245,120 493 2.393399 10.95556 1000 499500 204,915 95 490,620 988 2.394261 10.4 2000 1999000 412,915 237 986,620 2007 2.389402 8.468354 5000 12497500 1,036,915 835 2,474,620 5793 2.386522 6.937725 jdk1.6.0_beta2 100 4950 20,471 5 48,748 110 2.38132 22 200 19900 40,915 9 97,820 207 2.39081 23 500 124750 102,415 37 245,120 474 2.393399 12.81081 1000 499500 204,915 61 490,620 945 2.394261 15.4918 2000 1999000 412,915 124 986,620 1911 2.389402 15.41129 5000 12497500 1,036,915 312 2,474,620 4863 2.386522 15.58654 */ public class ClassGranularityEffectLoadTimeTester { /** The scale of the generated classes. This is the number of f() methods for each case. * The max number for this is somewhere between 5000-7500 before the generated Java code is too large. */ private static final int SCALE = 3; /** The number of performance runs when benchmarking. */ private static final int nRuns = 20; /** If true, the benchmark classloader will load generated class files from disk. * If false, the benchmark classloader will load generated class files from .jars. */ private static final boolean benchmarkClassFiles = false; /** The folder under which the generated files will go. */ private static final File workingFolder = new File("C:\\classSizeLoadingTest"); /** Args to javac. */ private static final String[] JAVAC_ARGS; static { String currentPath = System.getProperty("java.class.path", "."); JAVAC_ARGS = new String[] { // "-g:none", // with this arg, generated class files will not have any debug info. "-target", "1.4", "-source", "1.4", "-classpath", currentPath }; } /** System line separator */ private static final String lineSeparator = System.getProperty("line.separator"); /** The size of the indent in the source code for the generated classes. */ private static final int INDENT_SIZE = 4; /** The package for the generated classes. This *must* be a single package element - it must not contain dots. */ private static final String packageName = "foo"; private static final String packageDeclarationString = "package " + packageName + ";"; /** The name of the one large class */ private static final String compoundClassName = "AddCompound"; /** The name of the outer class containing the many small inner classes. */ private static final String smallClassesOuterClassName = "AddWithInner"; /** The method object used to invoke the javac compiler on the generated sources. */ private static Method compileMethod; static { // Initialize the compileMethod field. // First we need to get the compiler class. i.e. com.sun.tools.javac.Main Class<?> compilerClass = null; // Start by trying to get the class from the current class loader. try { compilerClass = ClassGranularityEffectLoadTimeTester.class.getClassLoader().loadClass ("com.sun.tools.javac.Main"); } catch (ClassNotFoundException e) { // The class is not on the current classpath. Try to locate tools.jar based on the jre home // directory and use a URLClassLoader. String homeDirectory = System.getProperty("java.home"); if (homeDirectory != null) { // In the standard JDC file layout tools.jar will be in the lib directory that is at the // same level as the JRE home directory. String fileSeparator = System.getProperty("file.separator", "\\"); // Trim the 'jre' off the home directory. homeDirectory = homeDirectory.substring(0, homeDirectory.lastIndexOf(fileSeparator)); // Create the file for tools.jar and check if it exists. File toolsFile = new File(homeDirectory + fileSeparator + "lib" + fileSeparator + "tools.jar"); if (FileSystemHelper.fileExists(toolsFile)) { try { // Since we've located tools.jar create a URLClassLoader with tools.jar as its path and // attempt to load the Main class. URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{toolsFile.toURI().toURL()}, ClassGranularityEffectLoadTimeTester.class.getClassLoader()); compilerClass = urlClassLoader.loadClass("com.sun.tools.javac.Main"); } catch (MalformedURLException e2) { // Simply fall through leaving compilerClass as null. } catch (ClassNotFoundException e2) { // Simply fall through leaving compilerClass as null. } } } } if (compilerClass != null) { try { // First we try to get the version of compile that takes a PrintWriter to handle output messages. ClassGranularityEffectLoadTimeTester.compileMethod = compilerClass.getMethod("compile", new Class[]{(new String[]{}).getClass(), PrintWriter.class}); } catch (NoSuchMethodException e) { // Simply fall through and leave compileMethod as null. } } else { System.err.println("Could not find javac."); } } /** * A simple container object to hold generated stats. * @author Edward Lam */ private static class Stats { public final double mean; public final double sd; public final double sdPercent; public final double se; Stats(double mean, double sd, double sdPercent, double se) { this.mean = mean; this.sd = sd; this.sdPercent = sdPercent; this.se = se; } /** * {@inheritDoc} */ public String toString() { return "Mean: " + mean + "\n" + "Std dev: " + sd + " (" + sdPercent + "%)\n" + "Std error: " + se; } } /** * Calculate statistics for a set of numbers. * @param results the set of numbers. * @return the stats for those numbers. */ private static Stats calculateStats(long[] results) { int nResults = results.length; double total = 0; for (int i = 0; i < nResults; i++) { total += results[i]; } double mean = total/nResults; double stDev = 0.0; double stdDevPercent; long sumDifsSquared = 0; for (int i = 0; i < nResults; i++) { double diff = ((double)results[i]) - mean; sumDifsSquared += (diff * diff); } if (nResults > 1) { stDev = sumDifsSquared / (nResults-1); stDev = Math.sqrt(stDev); stDev = ((int)(stDev * 100.0)) / 100.0; stdDevPercent = (stDev / (mean)) * 100.0; stdDevPercent = ((int)(stdDevPercent * 100.0)) / 100.0; } else { stDev = stdDevPercent = 0.0; } double standardError = stDev / Math.sqrt(nResults); return new Stats(mean, stDev, stdDevPercent, standardError); } /** * Helper method to add an indent string to a stringBuilder * @param sb the string builder to which INDENT_SIZE spaces will be appended. */ private static void indent(StringBuilder sb) { for (int i = 0; i < INDENT_SIZE; i++) { sb.append(' '); } } /** * Append a line of text to a string builder, followed by a newline. * * @param sb the stringBuilder to which the line will be added. * @param indent the number of indents to precede the line text. * @param text the text of the line to be added. */ private static void appendLine(StringBuilder sb, int indent, String text) { for (int i = 0; i < indent; i++) { indent(sb); } sb.append(text); sb.append(lineSeparator); } /** * Append a newline to a stringBuilder * @param sb the stringBuilder to which the newline will be appended. */ private static void appendEmptyLine(StringBuilder sb) { sb.append(lineSeparator); } /* class AddCompound { int tag; static int value0 = 0; static int value1 = 1; static int value2 = 2; AddCompound(int tag) { this.tag = tag; } static int f0(int n) { return n + value0; } static int f1(int n) { return n + value1; } static int f2(int n) { return n + value2; } int f(int n){ switch (tag) { case 0: return f0(n); case 1: return f1(n); case 2: return f2(n); default: throw new RuntimeException(); } } public static int test() { int result = 0; result = (new AddCompound(0)).f(result); result = (new AddCompound(1)).f(result); result = (new AddCompound(2)).f(result); return result; } } */ public static String getLargeClassText() { StringBuilder sb = new StringBuilder(); // package declaration appendLine(sb, 0, packageDeclarationString); appendEmptyLine(sb); // class declaration appendLine(sb, 0, "public class " + compoundClassName + " {"); appendEmptyLine(sb); { // int tag; appendLine(sb, 1, "private final int tag;"); appendEmptyLine(sb); // static value tags. for (int i = 0; i < SCALE; i++) { appendLine(sb, 1, "static int value" + i + " = " + i + ";"); } appendEmptyLine(sb); // constructor appendLine(sb, 1, "AddCompound(int tag) {"); { appendLine(sb, 2, "this.tag = tag;"); } appendLine(sb, 1, "}"); appendEmptyLine(sb); // factory methods for (int i = 0; i < SCALE; i++) { appendLine(sb, 1, "public static AddCompound makeAddCompound" + i + "() {"); { appendLine(sb, 2, "return new AddCompound(" + i + ");"); } appendLine(sb, 1, "}"); } // f methods for (int i = 0; i < SCALE; i++) { appendLine(sb, 1, "static int f" + i + "(int n) {"); { appendLine(sb, 2, "return n + value" + i + ";"); } appendLine(sb, 1, "}"); } // dispatch method appendLine(sb, 1, "int f(int n){"); { appendLine(sb, 2, "switch (tag) {"); { for (int i = 0; i < SCALE; i++) { appendLine(sb, 3, "case " + i + ": return f" + i + "(n);"); } appendLine(sb, 3, "default: throw new RuntimeException();"); } appendLine(sb, 2, "}"); } appendLine(sb, 1, "}"); { // test method appendLine(sb, 1, "public static int test() {"); { appendLine(sb, 2, "int result = 0;"); for (int i = 0; i < SCALE; i++) { appendLine(sb, 2, "result = makeAddCompound" + i + "().f(result);"); } appendLine(sb, 2, "return result;"); } appendLine(sb, 1, "}"); } } appendLine(sb, 0, "}"); return sb.toString(); } /* public class AddWithInner { static class Add0 { static int value0 = 0; Add0() { } int f(int n) { return n + value0; } } static class Add1 { static int value1 = 1; Add1() { } int f(int n) { return n + value1; } } static class Add2 { static int value2 = 2; Add2() { } int f(int n) { return n + value2; } } } */ public static String getSmallClassesText() { StringBuilder sb = new StringBuilder(); // package declaration appendLine(sb, 0, packageDeclarationString); appendEmptyLine(sb); // outer class declaration appendLine(sb, 0, "public class " + smallClassesOuterClassName + " {"); appendEmptyLine(sb); // Inner classes for (int i = 0; i < SCALE; i++) { // inner class declaration appendLine(sb, 1, "static class Add" + i + " {"); appendEmptyLine(sb); { // static value tag; appendLine(sb, 2, "static int value" + i + " = " + i + ";"); appendEmptyLine(sb); // constructor appendLine(sb, 2, "Add" + i + "() {"); appendLine(sb, 2, "}"); appendEmptyLine(sb); // f method appendLine(sb, 2, "int f(int n) {"); { appendLine(sb, 3, "return n + value" + i + ";"); } appendLine(sb, 2, "}"); } appendLine(sb, 1, "}"); } { // test method appendLine(sb, 1, "public static int test() {"); { appendLine(sb, 2, "int result = 0;"); for (int i = 0; i < SCALE; i++) { appendLine(sb, 2, "result = (new Add" + i + "()).f(result);"); } appendLine(sb, 2, "return result;"); } appendLine(sb, 1, "}"); } appendLine(sb, 0, "}"); return sb.toString(); } /** * Create the files and benchmark their performance. * @throws Exception */ public static void createAndBenchmarkFiles() throws Exception { // Create the output folder. File outputFolder = new File(workingFolder, packageName); FileSystemHelper.delTree(outputFolder); outputFolder.mkdirs(); if (!outputFolder.isDirectory()) { System.err.println("Could not create output folder: " + outputFolder); return; } System.out.println("Writing files to output folder: " + outputFolder); // Dump the files to the output folder. File compoundClassFile = new File(outputFolder, compoundClassName + ".java"); File smallOuterClassesFile = new File(outputFolder, smallClassesOuterClassName + ".java"); writeSourceFile(getLargeClassText(), compoundClassFile); writeSourceFile(getSmallClassesText(), smallOuterClassesFile); // Compile the files. System.out.println("Compiling files..."); compileFiles(new File[] {compoundClassFile, smallOuterClassesFile}); // Build the .jar file. System.out.println("Building .jar file..."); File jarFile = new File(workingFolder, "blah.jar"); buildJar(jarFile, outputFolder); String fullyQualifiedCompoundClassName = packageName + "." + compoundClassName; String fullyQualifiedSmallClassesOuterClassName = packageName + "." + smallClassesOuterClassName; long[] resultsFewCompound = new long[nRuns]; long[] resultsManySimple = new long[nRuns]; if (benchmarkClassFiles) { System.out.println("Executing using class files on disk..."); URL workingFolderURL = workingFolder.toURI().toURL(); for (int i = 0; i < nRuns; i++) { // Run each of the f methods a few times. runTestMethodWithTiming(workingFolderURL, fullyQualifiedCompoundClassName, "Iteration " + i + " few compound"); runTestMethodWithTiming(workingFolderURL, fullyQualifiedSmallClassesOuterClassName, "Iteration " + i + " many simple "); System.out.println(); } } else { System.out.println("Executing using jar files..."); URL jarURL = jarFile.toURI().toURL(); for (int i = 0; i < nRuns; i++) { // Run each of the f methods a few times. resultsFewCompound[i] = runTestMethodWithTiming(jarURL, fullyQualifiedCompoundClassName, "Iteration " + i + " few compound"); resultsManySimple[i] = runTestMethodWithTiming(jarURL, fullyQualifiedSmallClassesOuterClassName, "Iteration " + i + " many simple "); System.out.println(); } } System.out.println("Done."); System.out.println("Stats / few compound classes."); System.out.println(calculateStats(resultsFewCompound)); System.out.println(); System.out.println("Stats / many simple classes."); System.out.println(calculateStats(resultsManySimple)); } private static long runTestMethodWithTiming(URL classpathURL, String fullyQualifiedClassName, String testCaseName) throws Exception{ // Create a url classloader pointing to the classpath url. Its parent is the current classloader. ClassLoader classLoader = new URLClassLoader(new URL[] {classpathURL}, ClassGranularityEffectLoadTimeTester.class.getClassLoader()); // Time and run long before = System.currentTimeMillis(); // Get the static no-arg method "test", and run it. Class<?> clazz = classLoader.loadClass(fullyQualifiedClassName); Method method = clazz.getMethod("test", (Class[])null); Object result = method.invoke(null, (Object[])null); long after = System.currentTimeMillis(); System.out.println(testCaseName + ": " + (after - before) + " ms. Result: " + result); return (after - before); } /** * Write the given source into the given file. * @param source - String. The generated source. * @param targetFile - File. The target file. * @throws IOException */ private static void writeSourceFile(String source, File targetFile) throws IOException { InputStream inputStream = new ByteArrayInputStream(TextEncodingUtilities.getUTF8Bytes(source)); // Handle null source. try { FileChangeOutputStream outputStream = null; try { // throws IOException: outputStream = new FileChangeOutputStream(targetFile); FileSystemResourceHelper.transferData(inputStream, outputStream); } finally { try { if (outputStream != null) { outputStream.close(); } } catch (IOException ioe) { // Nothing can really be done at this point, and we don't bother reporting the error. } } } finally { inputStream.close(); } } /** * Compile source files * @param filesToCompile the files to compile. * @return the result code returned by the javac compile() method. */ private static int compileFiles(File[] filesToCompile) throws Exception { // Set up the classpath for compiling this module. String fileNames[] = new String [filesToCompile.length]; for (int i = 0; i < filesToCompile.length; i++) { fileNames[i] = filesToCompile[i].getPath(); } // Set up the classpath arguments for the call to 'compile'. String args[] = new String [fileNames.length + JAVAC_ARGS.length]; System.arraycopy(JAVAC_ARGS, 0, args, 0, JAVAC_ARGS.length); System.arraycopy(fileNames, 0, args, JAVAC_ARGS.length, fileNames.length); // Wrap System.out with a print writer, and pass this in to the compile method so that compile errors are dumped. PrintWriter pw = new PrintWriter(System.out); // Invoke the compiler. Object compileResult = compileMethod.invoke(null, new Object[]{args, pw}); // Close/flush the PrintWriter. pw.flush(); // pw.close(); // Extract and return the integer return code from the object result of 'invoke'. return ((Integer)compileResult).intValue(); } /** * Build the .jar file from the class files in the output folder. * * @param outputJarFile the jar file to create. * @param outputFolder the folder containing the class files to put in the jar file. * @throws FileNotFoundException * @throws IOException */ private static void buildJar(File outputJarFile, File outputFolder) throws FileNotFoundException, IOException { FileSystemHelper.ensureDirectoryExists(outputFolder); File[] outputFolderFiles = outputFolder.listFiles(); FileOutputStream outputStream = new FileOutputStream(outputJarFile); BufferedOutputStream bos = new BufferedOutputStream(outputStream, 1024); JarOutputStream jos = new JarOutputStream(bos, new Manifest()); try { // Set compression level. jos.setLevel(Deflater.DEFAULT_COMPRESSION); if (outputFolderFiles != null) { for (final File outputFolderFile : outputFolderFiles) { String fileName = outputFolderFile.getName(); if (fileName.endsWith(".class")) { // Write the zip entry. ZipEntry zipEntry = new ZipEntry(packageName + "/" + fileName); jos.putNextEntry(zipEntry); // Write the contents corresponding to the entry. InputStream inputStream = new FileInputStream(outputFolderFile); try { FileSystemResourceHelper.transferData(inputStream, jos); } finally { inputStream.close(); } } } } } finally { jos.flush(); bos.flush(); jos.close(); bos.close(); } } /** * Run target. * @param args * @throws Exception */ public static void main(String[] args) throws Exception { if (true) { createAndBenchmarkFiles(); } else { dumpFilesToStdOut(); } } /** * Dump the files to stdout. */ public static void dumpFilesToStdOut() { System.out.println("Scale: " + SCALE); System.out.println(getLargeClassText()); System.out.println(getSmallClassesText()); } }