/******************************************************************************* * Copyright (c) 2000, 2009 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.test.performance.ui; import java.io.File; import java.io.PrintStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Resource; import org.eclipse.swt.widgets.Display; import org.eclipse.test.internal.performance.results.db.BuildResults; import org.eclipse.test.internal.performance.results.db.ConfigResults; import org.eclipse.test.internal.performance.results.db.DB_Results; import org.eclipse.test.internal.performance.results.db.ScenarioResults; import org.eclipse.test.internal.performance.results.utils.Util; /** * Abstract class to build graph with bars */ public class FingerPrintGraph { // Sizes static final int MARGIN= 5; // margin on all four sides static final int BAR_HEIGHT= 6; // height of bar static final int GAP= 10; // gap between bars static final int TGAP= 5; // gap between lines and labels static final int LINE_HEIGHT = 2*BAR_HEIGHT + GAP; // fraction of width reserved for bar graph static final double RATIO= 0.6; // Formatting constants static final NumberFormat NUMBER_FORMAT; static { NUMBER_FORMAT = NumberFormat.getInstance(); NUMBER_FORMAT.setMaximumFractionDigits(1); } // Graphic constants static final Display DEFAULT_DISPLAY = Display.getDefault(); static final Color BLACK= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_BLACK); static final Color BLUE= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_BLUE); static final Color GREEN= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_GREEN); static final Color RED = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_RED); static final Color GRAY = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_GRAY); static final Color DARK_GRAY = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_DARK_GRAY); static final Color YELLOW = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_YELLOW); static final Color WHITE = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_WHITE); // Bar graph kinds static final int NO_TIME = 0; // i.e. percentage static final int TIME_LINEAR = 1; static final int TIME_LOG = 2; static final int[] SUPPORTED_GRAPHS = { // NO_TIME, TIME_LINEAR, TIME_LOG, }; // Graphic fields GC gc; Image image; int imageWidth; int imageHeight; int graphWidth; int graphHeight; Map resources = new HashMap(); // Data fields int count = 0; ConfigResults[] results = new ConfigResults[10]; BarGraphArea[] areas; // Values double maxValue = 0.0; double minValue = Double.MAX_VALUE; // File info File outputDir; String imageName; private final String defaultDimName = DB_Results.getDefaultDimension().getName(); /* * Member class defining a bar graph area. * This area applies to a configuration results and is made of several zones. */ class BarGraphArea { List zones; private ConfigResults configResults; /* * Member class defining a zone inside a bar graph area. * Typically made of a rectangle and an associated text used as tooltip. */ class AreaZone { Rectangle zone; String title; AreaZone(Rectangle zone, String tooltip) { super(); this.zone = zone; this.title = tooltip; } void print(String url, PrintStream stream) { stream.print(" echo '<area shape=\"RECT\""); if (this.title != null) { stream.print(" title=\""+this.title+"\""); } stream.print("coords=\""); stream.print(this.zone.x); stream.print(','); stream.print(this.zone.y); stream.print(','); stream.print(this.zone.x+this.zone.width); stream.print(','); stream.print(this.zone.y+this.zone.height); stream.print('"'); if (url != null) { stream.print(" href=\""); stream.print(url); stream.print('"'); } stream.print(">';\n"); } } BarGraphArea(ConfigResults results) { this.configResults = results; this.zones = new ArrayList(); } void print(PrintStream stream) { String url = this.configResults.getName() + "/" + ((ScenarioResults) this.configResults.getParent()).getFileName() + ".html"; int size = this.zones.size(); for (int i=0; i<size; i++) { AreaZone zone = (AreaZone) this.zones.get(i); zone.print(url, stream); } } void addArea(Rectangle rec, String tooltip) { AreaZone zone = new AreaZone(rec, tooltip); this.zones.add(zone); } } FingerPrintGraph(File dir, String fileName, int width, List results) { super(); this.imageWidth = width; this.count = results.size(); this.results = new ConfigResults[this.count]; results.toArray(this.results); this.outputDir = dir; this.imageName = fileName; } /** */ void drawBars(int kind) { // Get/Set graphical resources Font italicFont = (Font) this.resources.get("italicFont"); if (italicFont == null) { String fontDataName = this.gc.getFont().getFontData()[0].toString(); FontData fdItalic = new FontData(fontDataName); fdItalic.setStyle(SWT.ITALIC); italicFont = new Font(DEFAULT_DISPLAY, fdItalic); this.resources.put("italicFont", italicFont); } Color blueref = (Color) this.resources.get("blueref"); if (blueref == null) { blueref = new Color(DEFAULT_DISPLAY, 200, 200, 255); this.resources.put("blueref", blueref); } Color lightyellow= (Color) this.resources.get("lightyellow"); if (lightyellow == null) { lightyellow = new Color(DEFAULT_DISPLAY, 255, 255, 160); this.resources.put("lightyellow", lightyellow); } Color darkyellow= (Color) this.resources.get("darkyellow"); if (darkyellow == null) { darkyellow = new Color(DEFAULT_DISPLAY, 160, 160, 0); this.resources.put("darkyellow", darkyellow); } Color okColor= (Color) this.resources.get("lightgreen"); if (okColor == null) { okColor = new Color(DEFAULT_DISPLAY, 95, 191, 95); this.resources.put("lightgreen", okColor); } Color failureColor = (Color) this.resources.get("lightred"); if (failureColor == null) { failureColor = new Color(DEFAULT_DISPLAY, 220, 50, 50); this.resources.put("lightred", failureColor); } // Build each scenario bar graph this.areas = new BarGraphArea[this.count]; double max = kind == TIME_LOG ? Math.log(this.maxValue) : this.maxValue; for (int i=0, y=MARGIN; i < this.count; i++, y+=LINE_HEIGHT) { // get builds info ConfigResults configResults = this.results[i]; this.areas[i] = new BarGraphArea(configResults); BarGraphArea graphArea = this.areas[i]; BuildResults currentBuildResults = configResults.getCurrentBuildResults(); double currentValue = currentBuildResults.getValue(); double currentError = currentBuildResults.getError(); double error = configResults.getError(); boolean singleTest = Double.isNaN(error); boolean isSignificant = singleTest || error < Utils.STANDARD_ERROR_THRESHOLD; boolean isCommented = currentBuildResults.getComment() != null; BuildResults baselineBuildResults = configResults.getBaselineBuildResults(); double baselineValue = baselineBuildResults.getValue(); double baselineError = baselineBuildResults.getError(); // draw baseline build bar Color whiteref = (Color) this.resources.get("whiteref"); if (whiteref == null) { whiteref = new Color(DEFAULT_DISPLAY, 240, 240, 248); this.resources.put("whiteref", whiteref); } this.gc.setBackground(whiteref); double baselineGraphValue = kind == TIME_LOG ? Math.log(baselineValue) : baselineValue; int baselineBarLength= (int) (baselineGraphValue / max * this.graphWidth); int baselineErrorLength= (int) (baselineError / max * this.graphWidth / 2); int labelxpos = MARGIN + baselineBarLength; if (kind == TIME_LOG || baselineErrorLength <= 1) { this.gc.fillRectangle(MARGIN, y + (GAP/2), baselineBarLength, BAR_HEIGHT); Rectangle rec = new Rectangle(MARGIN, y + (GAP/2), baselineBarLength, BAR_HEIGHT); this.gc.drawRectangle(rec); graphArea.addArea(rec, "Time for baseline build "+baselineBuildResults.getName()+": "+Util.timeString((long)baselineValue)); } else { int wr = baselineBarLength - baselineErrorLength; Rectangle recValue = new Rectangle(MARGIN, y + (GAP/2), wr, BAR_HEIGHT); this.gc.fillRectangle(recValue); this.gc.setBackground(YELLOW); Rectangle recError = new Rectangle(MARGIN+wr, y + (GAP/2), baselineErrorLength*2, BAR_HEIGHT); this.gc.fillRectangle(recError); Rectangle rec = new Rectangle(MARGIN, y + (GAP/2), baselineBarLength+baselineErrorLength, BAR_HEIGHT); this.gc.drawRectangle(rec); StringBuffer tooltip = new StringBuffer("Time for baseline build "); tooltip.append(baselineBuildResults.getName()); tooltip.append(": "); tooltip.append(Util.timeString((long)baselineValue)); tooltip.append(" [±"); tooltip.append(Util.timeString((long)baselineError)); tooltip.append(']'); graphArea.addArea(rec, tooltip.toString()); labelxpos += baselineErrorLength; } // set current build bar color if (baselineValue < currentValue) { if (isCommented) { this.gc.setBackground(GRAY); } else { this.gc.setBackground(failureColor); } } else { this.gc.setBackground(okColor); } // draw current build bar double currentGraphValue = kind == TIME_LOG ? Math.log(currentValue) : currentValue; int currentBarLength= (int) (currentGraphValue / max * this.graphWidth); int currentErrorLength= (int) (currentError / max * this.graphWidth / 2); if (kind == TIME_LOG || currentErrorLength <= 1) { this.gc.fillRectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength, BAR_HEIGHT); Rectangle rec = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength, BAR_HEIGHT); this.gc.drawRectangle(rec); String tooltip = "Time for current build "+currentBuildResults.getName()+": "+Util.timeString((long)currentValue); if (isCommented) { tooltip += ". " + currentBuildResults.getComment(); } graphArea.addArea(rec, tooltip); if (labelxpos < (MARGIN+currentBarLength)) { labelxpos = MARGIN + currentBarLength; } } else { int wr = currentBarLength - currentErrorLength; Rectangle recValue = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, wr, BAR_HEIGHT); this.gc.fillRectangle(recValue); this.gc.setBackground(YELLOW); Rectangle recError = new Rectangle(MARGIN+wr, y + (GAP/2) + BAR_HEIGHT, currentErrorLength*2, BAR_HEIGHT); this.gc.fillRectangle(recError); Rectangle rec = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength+currentErrorLength, BAR_HEIGHT); this.gc.drawRectangle(rec); StringBuffer tooltip = new StringBuffer("Time for current build "); tooltip.append(currentBuildResults.getName()); tooltip.append(": "); tooltip.append(Util.timeString((long)currentValue)); tooltip.append(" [±"); tooltip.append(Util.timeString((long)currentError)); tooltip.append(']'); if (isCommented) { tooltip.append(". "); tooltip.append(currentBuildResults.getComment()); } graphArea.addArea(rec, tooltip.toString()); if (labelxpos < (MARGIN+currentBarLength+currentErrorLength)) { labelxpos = MARGIN + currentBarLength+currentErrorLength; } } // set delta value style and color boolean hasFailure = currentBuildResults.getFailure() != null; if (hasFailure) { if (isCommented) { this.gc.setForeground(DARK_GRAY); } else { this.gc.setForeground(RED); } } else { this.gc.setForeground(BLACK); } // draw delta value double delta = -configResults.getDelta(); String label = delta > 0 ? "+" : ""; label += NUMBER_FORMAT.format(delta*100) + "%"; Point labelExtent= this.gc.stringExtent(label); int labelvpos= y + (LINE_HEIGHT - labelExtent.y) / 2; this.gc.drawString(label, labelxpos+TGAP, labelvpos, true); this.gc.setForeground(BLACK); this.gc.setFont(null); int titleStart = (int) (RATIO * this.imageWidth); if (singleTest || !isSignificant) { String deltaTooltip = null; if (singleTest) { deltaTooltip = "This test performed only one iteration; hence its reliability cannot be assessed"; } else if (!isSignificant) { deltaTooltip = "This test has a bad reliability: error is "+NUMBER_FORMAT.format(error*100)+"% (> 3%)!"; } Image warning = (Image) this.resources.get("warning"); int xi = labelxpos+TGAP+labelExtent.x; this.gc.drawImage(warning, xi, labelvpos); ImageData imageData = warning.getImageData(); // Set zones // - first one is between end of bar and warning image beginning Rectangle deltaZone = new Rectangle(labelxpos, labelvpos-2, xi-labelxpos, labelExtent.y+4); graphArea.addArea(deltaZone, null); // - second one is the warning image Rectangle warningZone = new Rectangle(xi, labelvpos, imageData.width, imageData.height); graphArea.addArea(warningZone, deltaTooltip); // - last one is between end of the warning image and the scenario title beginning int warningImageEnd = xi+imageData.width; Rectangle emptyZone = new Rectangle(warningImageEnd, labelvpos, titleStart-warningImageEnd, imageData.height); graphArea.addArea(emptyZone, deltaTooltip); } else { // No tooltip => delta zone is between end of bar and the scenario title beginning Rectangle deltaZone = new Rectangle(labelxpos, labelvpos-2, titleStart-labelxpos, labelExtent.y+4); graphArea.addArea(deltaZone, null); } // set title style Color oldfg= this.gc.getForeground(); this.gc.setForeground(BLUE); // draw scenario title int x= titleStart; ScenarioResults scenarioResults = (ScenarioResults) configResults.getParent(); String title = scenarioResults.getLabel() + " (" + this.defaultDimName + ")"; Point e= this.gc.stringExtent(title); this.gc.drawLine(x, labelvpos + e.y - 1, x + e.x, labelvpos + e.y - 1); this.gc.drawString(title, x, labelvpos, true); this.gc.setForeground(oldfg); this.gc.setFont(null); Rectangle titleZone = new Rectangle(x, labelvpos, e.x, e.y); graphArea.addArea(titleZone, null/*no tooltip*/); if (!configResults.isBaselined()) { Image warning = (Image) this.resources.get("warning"); this.gc.drawImage(warning, x+e.x, labelvpos); ImageData imageData = warning.getImageData(); Rectangle warningZone = new Rectangle(x+e.x, labelvpos, imageData.width, imageData.height); String titleTooltip = "This test has no baseline result, hence use build "+configResults.getBaselineBuildName()+" for reference!"; graphArea.addArea(warningZone, titleTooltip); } } } void drawLinearScale() { // Draw scale background drawScaleBackground(); // Draw scale grid lines int gridValue = 100; int n = (int) (this.maxValue / gridValue); while (n > 10) { switch (gridValue) { case 100: gridValue = 200; break; case 200: gridValue = 500; break; case 500: gridValue = 1000; break; default: gridValue += 1000; break; } n = (int) (this.maxValue / gridValue); } int gridWidth = (int) (this.graphWidth * gridValue / this.maxValue); int x = MARGIN; long value = 0; // TODO use minValue instead while (x < this.graphWidth) { // draw line this.gc.setForeground(GRAY); if (x > 0) { this.gc.setLineStyle(SWT.LINE_DOT); this.gc.drawLine(x, MARGIN, x, this.graphHeight + TGAP); } // draw value this.gc.setForeground(BLACK); String val= Util.timeString(value); Point point= this.gc.stringExtent(val); this.gc.drawString(val, x - point.x / 2, this.graphHeight + TGAP, true); // compute next grid position x += gridWidth; value += gridValue; // value is expressed in seconds } this.gc.setLineStyle(SWT.LINE_SOLID); this.gc.drawLine(0, this.graphHeight, this.graphWidth, this.graphHeight); } void drawLogarithmScale() { // Draw scale background drawScaleBackground(); // Draw scale grid lines double max = Math.log(this.maxValue); int gridValue = 100; int x = MARGIN; long value = 0; // TODO use minValue instead while (x < this.graphWidth) { // draw line this.gc.setForeground(GRAY); if (x > MARGIN) { this.gc.setLineStyle(SWT.LINE_DOT); this.gc.drawLine(x, MARGIN, x, this.graphHeight + TGAP); } // draw value this.gc.setForeground(BLACK); String str = Util.timeString(value); Point point= this.gc.stringExtent(str); this.gc.drawString(str, x - point.x / 2, this.graphHeight + TGAP, true); // compute next grid position value += gridValue; int v = (int) (value / 100); int c = 1; while (v > 10) { v = v / 10; c *= 10; } switch (v) { case 3: gridValue = 200*c; break; case 5: gridValue = 500*c; break; case 10: gridValue = 1000*c; break; } x = MARGIN + (int) (this.graphWidth * Math.log(value) / max); } this.gc.setLineStyle(SWT.LINE_SOLID); this.gc.drawLine(0, this.graphHeight, this.graphWidth, this.graphHeight); } /** * Draw the scale depending on the bar time graph kind. */ void drawScale(int kind) { switch (kind) { case TIME_LINEAR: drawLinearScale(); break; case TIME_LOG: drawLogarithmScale(); break; } } private void drawScaleBackground() { // Draw striped background Color lightblue = (Color) this.resources.get("lightblue"); if (lightblue == null) { lightblue = new Color(DEFAULT_DISPLAY, 237, 243, 254); this.resources.put("lightblue", lightblue); } this.gc.setBackground(lightblue); for (int i= 0; i<this.count; i++) { if (i % 2 == 0) { this.gc.fillRectangle(0, MARGIN + i * LINE_HEIGHT, this.imageWidth, LINE_HEIGHT); } } // Draw bottom vertical line int yy= MARGIN + this.count * LINE_HEIGHT; this.gc.drawLine(MARGIN, MARGIN, MARGIN, yy + TGAP); } String getImageName(int kind) { switch (kind) { case TIME_LINEAR: return this.imageName+"_linear"; case TIME_LOG: return this.imageName+"_log"; } return this.imageName; } void paint(int kind) { // Set image this.graphHeight = MARGIN + this.count * LINE_HEIGHT; this.imageHeight = this.graphHeight + GAP + 16 + MARGIN; this.image = new Image(DEFAULT_DISPLAY, this.imageWidth, this.imageHeight); this.gc = new GC(this.image); // draw white background this.gc.setBackground(WHITE); this.gc.fillRectangle(0, 0, this.imageWidth, this.imageHeight); // Set widths and heights int width= (int) (RATIO * this.imageWidth); // width for results bar this.graphWidth= width - this.gc.stringExtent("-999.9%").x - TGAP - MARGIN; // reserve space //$NON-NLS-1$ // Get warning image width Image warning = (Image) this.resources.get("warning"); if (warning == null) { warning = new Image(this.gc.getDevice(), new File(this.outputDir, Utils.WARNING_OBJ).toString()); this.resources.put("warning", warning); } this.graphWidth -= warning.getImageData().width; // Set maximum of values this.maxValue = 0.0; this.minValue = Double.MAX_VALUE; for (int i= 0; i<this.count; i++) { BuildResults baselineBuildResults = this.results[i].getBaselineBuildResults(); double value = baselineBuildResults.getValue(); double error = baselineBuildResults.getError(); if (!Double.isNaN(error)) value += Math.abs(error); if (value < 1000000 && value > this.maxValue) { this.maxValue = value; } if (value < this.minValue) { this.minValue = value; } BuildResults currentBuildResults = this.results[i].getCurrentBuildResults(); value = currentBuildResults.getValue(); error = currentBuildResults.getError(); if (!Double.isNaN(error)) value += Math.abs(error); if (value < 1000000 && value > this.maxValue) { this.maxValue = value; } if (value < this.minValue) { this.minValue = value; } } this.minValue = 0; // do not use minValue for now... // Draw the scale drawScale(kind); // Draw the bars drawBars(kind); // Dispose this.gc.dispose(); } /** * Create, paint and save all supported bar graphs and add the corresponding * image and map references in the given stream. * * @param stream */ final public void paint(PrintStream stream) { // Paint supported graphs int length = SUPPORTED_GRAPHS.length; for (int i=0; i<length; i++) { int kind = SUPPORTED_GRAPHS[i]; paint(kind); save(kind, stream); } // Dispose created graphic resources Iterator iterator = this.resources.values().iterator(); while (iterator.hasNext()) { Resource resource = (Resource) iterator.next(); resource.dispose(); } this.resources.clear(); } void print(int kind, PrintStream stream) { String imgName = getImageName(kind); stream.print(" if ($type==\"fp_type="+kind+"\") {\n"); stream.print(" echo '<img src=\""); stream.print(imgName); stream.print(".gif\" usemap=\"#"); stream.print(imgName); stream.print("\" name=\""); stream.print(imgName.substring(imgName.lastIndexOf('.'))); stream.print("\">';\n"); stream.print(" echo '<map name=\""); stream.print(imgName); stream.print("\">';\n"); if (this.areas != null) { for (int i=0; i<this.count; i++) { this.areas[i].print(stream); } } stream.print(" echo '</map>';\n"); stream.print(" }\n"); } void save(int kind, PrintStream stream) { File file = new File(this.outputDir, getImageName(kind)+".gif"); Utils.saveImage(file, this.image); if (file.exists()) { print(kind, stream); } else { stream.print("<br><br>There is no fingerprint for "); stream.print(this.imageName); stream.print(" (kind="); stream.print(kind); stream.print(")<br><br>\n"); } } }