| /* |
| * Copyright (c) 2014, 2015 Eike Stepper (Berlin, Germany) 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: |
| * Eike Stepper - initial API and implementation |
| */ |
| package org.eclipse.net4j.util.om.monitor; |
| |
| import org.eclipse.net4j.internal.util.bundle.OM; |
| import org.eclipse.net4j.internal.util.table.Cell; |
| import org.eclipse.net4j.internal.util.table.Dumper; |
| import org.eclipse.net4j.internal.util.table.Formula; |
| import org.eclipse.net4j.internal.util.table.Range; |
| import org.eclipse.net4j.internal.util.table.Range.Alignment; |
| import org.eclipse.net4j.internal.util.table.Table; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| |
| import java.lang.ref.ReferenceQueue; |
| import java.lang.ref.WeakReference; |
| import java.text.DecimalFormat; |
| import java.text.Format; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * An instrumented {@link SubMonitor sub monitor} that automatically collects and reports usage statistics. |
| * <p> |
| * It is normally very challenging to find out how much time a program really spends in the different parts of the monitored methods or how often these |
| * parts get executed. Stepping through the program with a debugger obviously leads to distortion that renders the observations meaningless and adding |
| * extra code to measure a runtime scenario realisticly is not nice from a maintenance point of view. |
| * <p> |
| * As a solution to this problem this class offers the possibility to transparently instrument {@link SubMonitor} instances such that they automatically |
| * collect and report all kinds of statistics that may help to enhance the user experience. Sometimes it would even indicate to remove some progress monitoring |
| * because it turns out that almost no time is being spent in a particular part of the program. Another typical result from the analysis is the understanding of |
| * <i>one time effects</i> that might need special consideration. |
| * <p> |
| * Instances of this class can be created explicitely with the {@link SubMonitor#convert(IProgressMonitor, SubMonitor.ProbingMode) Progress.progress()} factory methods |
| * that take a {@link SubMonitor.ProbingMode} argument. Implicit (automatic) instrumentation can be controlled with the "<code>submonitor.probing</code>" |
| * {@link System#setProperty(String, String) system property} as follows: |
| * <dl> |
| * <dt> <code>off</code> |
| * <dd> All {@link SubMonitor monitors} that are not created with an explicit probing mode will <b>not</b> collect any usage information. No extra heap space |
| * or CPU time is allocated to these monitors. |
| * <dt> <code>standard</code> |
| * <dd> All {@link SubMonitor monitors} that are not created with an explicit probing mode will collect and report only statistical usage information. |
| * The amount of heap space allocated for this information is constant over time. |
| * <dt> <code>full</code> |
| * <dd> All {@link SubMonitor monitors} that are not created with an explicit probing mode will store and report all collected probes. |
| * The amount of heap space allocated for this information scales with the number of {@link SubMonitor monitor} instances that are created over time. |
| * </dl> |
| * <p> |
| * This class registers a {@link Runtime#addShutdownHook(Thread) shutdown hook} that dumps the probing results to the console when the program ends. |
| * By setting the "<code>submonitor.probing.trace</code>" {@link System#setProperty(String, String) system property} to the value "<code>true</code>" |
| * probing results are continuously dumped as they become available. |
| * <p> |
| * The probing results are formatted as tables per monitored method. Each row of a table corresponds to a call to the {@link #worked()} or {@link #newChild()} |
| * method. The first column of a table displays the location of that call, the content of the following columns depends on the probing mode used for a monitored method. |
| * <p> |
| * Example of the probing mode {@link SubMonitor.ProbingMode#STANDARD STANDARD}: |
| * <p> |
| * <img src="doc-files/standard.png"> |
| * <p> |
| * Example of the probing mode {@link SubMonitor.ProbingMode#FULL FULL}: |
| * <p> |
| * <img src="doc-files/full.png"> |
| * <p> |
| * The tables in the two examples above have smooth borders that can only be displayed correctly in consoles with UTF-8 encoding. The console encoding |
| * can be configured on the <i>Common</i> tab of the launch configuration dialog. In addition the rendering of smooth table borders must be explicitely enabled |
| * by setting the "<code>submonitor.probing.borders</code>" {@link System#setProperty(String, String) system property} to the value "<code>smooth</code>". |
| * Without this setting tables are rendered with "-", "+" and "|" characters that display correctly regardless of the console encoding. |
| * <p> |
| * Quick navigation from the table cells to the corresponding code locations is supported by clickable links in the console tables. This way the |
| * collected information can be converted into real enhancements of the monitoring code quickly and easily. |
| * <p> |
| * <b>Important note</b>: Avoid to load this class (i.e., to call any of its methods) unless you intend to actually use probing monitors. When this class |
| * is loaded it spawns a thread to collect unused progress monitors and to free up the heap space allocated to their probes. The shutdown hook mentioned above |
| * is also registered when this class is loaded. |
| * |
| * @author Eike Stepper |
| * @since 3.4 |
| */ |
| public final class ProbingSubMonitor extends org.eclipse.net4j.util.om.monitor.SubMonitor |
| { |
| private static final boolean TRACE = Boolean.getBoolean("submonitor.probing.trace"); |
| |
| private static final Map<StackTraceElement, Statistics> STATISTICS = new HashMap<StackTraceElement, Statistics>(); |
| |
| private static final Map<Integer, KeyedWeakReference> MAP = new ConcurrentHashMap<Integer, KeyedWeakReference>(); |
| |
| private static final ReferenceQueue<ProbingSubMonitor> QUEUE = new ReferenceQueue<ProbingSubMonitor>(); |
| |
| private static final AtomicInteger COUNTER = new AtomicInteger(); |
| |
| private static final String NAME1 = SubMonitor.class.getName(); |
| |
| private static final String NAME2 = ProbingSubMonitor.class.getName(); |
| |
| private static final String NAME3 = RootInfo.class.getName(); |
| |
| private static final int FULL_MODE = 0x0010; |
| |
| private static final int DONE_FLAG = 0x0020; |
| |
| private final ProbingSubMonitor parent; |
| |
| private final int totalTicks; |
| |
| private final int key; |
| |
| private final Map<StackTraceElement, Probe> probes = new HashMap<StackTraceElement, Probe>(); |
| |
| private StackTraceElement location = determineLocation(); |
| |
| private long timeStamp = determineTimeStamp(); |
| |
| private StackTraceElement forkLocation; |
| |
| private StackTraceElement forkTarget; |
| |
| private int forkTicks; |
| |
| ProbingSubMonitor(ProbingSubMonitor parent, RootInfo rootInfo, int totalWork, int availableToChildren, int flags, |
| boolean full) |
| { |
| super(rootInfo, totalWork, availableToChildren, flags | (full ? FULL_MODE : 0)); |
| this.parent = parent; |
| totalTicks = totalWork; |
| |
| if (parent != null) |
| { |
| parent.forkTarget = location; |
| } |
| |
| key = COUNTER.incrementAndGet(); |
| MAP.put(key, new KeyedWeakReference(key, this)); |
| } |
| |
| @Override |
| SubMonitor createSubMonitor(RootInfo rootInfo, int totalWork, int availableToChildren, int flags) |
| { |
| timeStamp = determineTimeStamp(); |
| forkLocation = determineLocation(); |
| forkTicks = totalWork; |
| |
| boolean full = (flags & FULL_MODE) != 0; |
| return new ProbingSubMonitor(this, rootInfo, totalWork, availableToChildren, flags, full); |
| } |
| |
| @Override |
| void adjustLocation() |
| { |
| location = determineLocation(); |
| if (parent != null) |
| { |
| parent.forkTarget = location; |
| } |
| } |
| |
| @Override |
| public void worked(int work) |
| { |
| probe(work); |
| super.worked(work); |
| } |
| |
| @Override |
| public void done() |
| { |
| if ((flags & DONE_FLAG) == 0) |
| { |
| flags |= DONE_FLAG; |
| |
| try |
| { |
| finishProbe(this); |
| super.done(); |
| |
| if (parent != null) |
| { |
| parent.childDone(); |
| } |
| } |
| finally |
| { |
| forkLocation = null; |
| forkTarget = null; |
| forkTicks = 0; |
| |
| MAP.remove(key); |
| } |
| } |
| } |
| |
| @Override |
| public void childDone() |
| { |
| if (forkLocation != null) |
| { |
| probe(forkLocation, forkTicks); |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| return shorten(location); |
| } |
| |
| private void probe(int ticks) |
| { |
| StackTraceElement location = determineLocation(); |
| probe(location, ticks); |
| } |
| |
| private void probe(StackTraceElement location, int ticks) |
| { |
| try |
| { |
| long now = determineTimeStamp(); |
| int time = (int)(Math.abs(now - timeStamp) / 1000000); |
| |
| Probe probe = probes.get(location); |
| if (probe == null) |
| { |
| probe = new Probe(location); |
| probes.put(location, probe); |
| } |
| |
| probe.update(forkTarget, ticks, time); |
| |
| forkLocation = null; |
| forkTarget = null; |
| forkTicks = 0; |
| timeStamp = now; |
| } |
| catch (Exception ex) |
| { |
| OM.LOG.error(ex); |
| } |
| } |
| |
| private static String shorten(StackTraceElement location) |
| { |
| String className = location.getClassName(); |
| |
| boolean lastCharWasDigit = false; |
| int length = className.length(); |
| int i = length; |
| while (--i >= 0) |
| { |
| char c = className.charAt(i); |
| if ((c == '.' || c == '$') && !lastCharWasDigit) |
| { |
| ++i; |
| break; |
| } |
| |
| lastCharWasDigit = Character.isDigit(c); |
| } |
| |
| StringBuilder builder = new StringBuilder(); |
| builder.append(className, i, length); |
| builder.append('.'); |
| builder.append(location.getMethodName()); |
| |
| if (location.isNativeMethod()) |
| { |
| builder.append("(Native Method)"); |
| } |
| else |
| { |
| String fileName = location.getFileName(); |
| int lineNumber = location.getLineNumber(); |
| if (fileName != null && lineNumber >= 0) |
| { |
| builder.append('('); |
| builder.append(fileName); |
| builder.append(':'); |
| builder.append(lineNumber); |
| builder.append(')'); |
| } |
| else |
| { |
| if (fileName != null) |
| { |
| builder.append('('); |
| builder.append(fileName); |
| builder.append(')'); |
| } |
| else |
| { |
| builder.append("(Unknown Source)"); |
| } |
| } |
| } |
| |
| return builder.toString(); |
| } |
| |
| private static StackTraceElement determineLocation() |
| { |
| StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); |
| for (int i = 3; i < stackTrace.length; i++) |
| { |
| StackTraceElement stackTraceElement = stackTrace[i]; |
| |
| String className = stackTraceElement.getClassName(); |
| if (className != NAME1 && className != NAME2 && className != NAME3) |
| { |
| return stackTraceElement; |
| } |
| } |
| |
| return null; |
| } |
| |
| private static long determineTimeStamp() |
| { |
| return System.nanoTime(); |
| } |
| |
| private static void finishProbe(ProbingSubMonitor progress) |
| { |
| try |
| { |
| StackTraceElement location = progress.location; |
| |
| Statistics statistics = STATISTICS.get(location); |
| if (statistics == null) |
| { |
| statistics = new Statistics(location); |
| STATISTICS.put(location, statistics); |
| } |
| |
| statistics.update(progress); |
| } |
| catch (Exception ex) |
| { |
| OM.LOG.error(ex); |
| } |
| } |
| |
| public static void reportStatistics() |
| { |
| for (Statistics statistics : STATISTICS.values()) |
| { |
| statistics.report(); |
| } |
| } |
| |
| public static void resetStatistics() |
| { |
| STATISTICS.clear(); |
| } |
| |
| static |
| { |
| final AtomicBoolean shuttingDown = new AtomicBoolean(); |
| Runtime.getRuntime().addShutdownHook(new Thread("Progress Probe Shutdown Hook") |
| { |
| @Override |
| public void run() |
| { |
| shuttingDown.set(true); |
| |
| for (KeyedWeakReference reference : MAP.values()) |
| { |
| ProbingSubMonitor progress = reference.get(); |
| if (progress != null) |
| { |
| finishProbe(progress); |
| } |
| } |
| |
| reportStatistics(); |
| } |
| }); |
| |
| Thread monitor = new Thread("Progress Probe Monitor") |
| { |
| @Override |
| public void run() |
| { |
| while (!isInterrupted() && !shuttingDown.get()) |
| { |
| KeyedWeakReference reference = (KeyedWeakReference)QUEUE.poll(); |
| if (reference != null) |
| { |
| MAP.remove(reference.key); |
| |
| ProbingSubMonitor progress = reference.get(); |
| if (progress != null && (progress.flags & DONE_FLAG) == 0) |
| { |
| finishProbe(progress); |
| } |
| } |
| } |
| } |
| }; |
| |
| monitor.setDaemon(true); |
| monitor.start(); |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class KeyedWeakReference extends WeakReference<ProbingSubMonitor> |
| { |
| private final int key; |
| |
| public KeyedWeakReference(int key, ProbingSubMonitor progress) |
| { |
| super(progress); |
| this.key = key; |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class Probe |
| { |
| private final StackTraceElement location; |
| |
| private StackTraceElement[] forkTargets; |
| |
| private int ticks; |
| |
| private int time; |
| |
| public Probe(StackTraceElement location) |
| { |
| this.location = location; |
| } |
| |
| public void update(StackTraceElement forkTarget, int ticks, int time) |
| { |
| this.ticks += ticks; |
| this.time += time; |
| |
| if (forkTarget != null) |
| { |
| if (forkTargets == null) |
| { |
| forkTargets = new StackTraceElement[] { forkTarget }; |
| } |
| else |
| { |
| for (int i = 0; i < forkTargets.length; i++) |
| { |
| StackTraceElement location = forkTargets[i]; |
| if (location.equals(forkTarget)) |
| { |
| return; |
| } |
| } |
| |
| StackTraceElement[] newForkTargets = new StackTraceElement[forkTargets.length + 1]; |
| System.arraycopy(forkTargets, 0, newForkTargets, 0, forkTargets.length); |
| newForkTargets[forkTargets.length] = forkTarget; |
| |
| forkTargets = newForkTargets; |
| } |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| return location + " - " + time; |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class Statistics |
| { |
| public static final Format TICKS = new DecimalFormat("0 Ticks"); |
| |
| public static final Format MILLIS = new DecimalFormat("0 ms"); |
| |
| public static final Format MILLIS_PRECISE = new DecimalFormat("0.00 ms"); |
| |
| public static final Format PERCENT = new DecimalFormat("0 %"); |
| |
| private static final int DEFAULT_COLUMNS = 3; |
| |
| private static final boolean SMOOTH_BORDERS = "smooth" |
| .equalsIgnoreCase(System.getProperty("submonitor.probing.borders")); |
| |
| private static final Dumper DUMPER = SMOOTH_BORDERS ? Dumper.UTF8 : Dumper.ASCII; |
| |
| private final StackTraceElement location; |
| |
| private long totalTicks; |
| |
| private int columns; |
| |
| private Row[] rows; |
| |
| public Statistics(StackTraceElement location) |
| { |
| this.location = location; |
| } |
| |
| public void update(ProbingSubMonitor progress) |
| { |
| totalTicks += progress.totalTicks; |
| |
| List<Row> newRows; |
| if (rows == null) |
| { |
| newRows = new ArrayList<Row>(10); |
| } |
| else |
| { |
| newRows = new ArrayList<Row>(10 + rows.length); |
| for (int i = 0; i < rows.length; i++) |
| { |
| Row row = rows[i]; |
| Probe probe = progress.probes.remove(row.location); |
| if (probe == null) |
| { |
| row.skipProbes(1); |
| } |
| else |
| { |
| row.addProbe(probe); |
| } |
| |
| newRows.add(row); |
| } |
| } |
| |
| boolean full = (progress.flags & FULL_MODE) != 0; |
| |
| for (Probe newProbe : progress.probes.values()) |
| { |
| Row row = Row.create(newProbe.location, full); |
| |
| if (columns > 0) |
| { |
| row.skipProbes(columns); |
| } |
| |
| row.addProbe(newProbe); |
| newRows.add(row); |
| } |
| |
| rows = newRows.toArray(new Row[newRows.size()]); |
| ++columns; |
| |
| if (TRACE) |
| { |
| report(); |
| } |
| } |
| |
| public void report() |
| { |
| if (rows != null && rows.length != 0) |
| { |
| Arrays.sort(rows); |
| int rowCount = rows.length; |
| |
| Table table = new Table(DEFAULT_COLUMNS, 1 + rowCount); |
| table.cell(0, 0).value(shorten(location)); |
| table.cell(1, 0).value(totalTicks / columns); |
| table.column(1).format(TICKS); |
| |
| int lastCol = rows[0].header(table, rowCount); |
| for (int r = 0; r < rowCount; r++) |
| { |
| Row row = rows[r]; |
| table.cell(0, 1 + r).value(shorten(row.location)); |
| |
| Cell tickCell = table.cell(1, 1 + r); |
| tickCell.value(row.ticks / row.getRuns()); |
| |
| Formula percent = new Formula.Percent(table.column(1, 1), tickCell); |
| table.cell(2, 1 + r).format(PERCENT).value(percent); |
| |
| row.body(table, r + 1); |
| |
| if (row.forkTargets != null) |
| { |
| StringBuilder builder = new StringBuilder(); |
| for (int i = 0; i < row.forkTargets.length; i++) |
| { |
| if (builder.length() != 0) |
| { |
| builder.append(", "); |
| } |
| |
| StackTraceElement forkTarget = row.forkTargets[i]; |
| builder.append(shorten(forkTarget)); |
| } |
| |
| table.cell(lastCol + 1, r + 1).value(builder); |
| } |
| } |
| |
| int forkTargetCol = table.bottomRight().col; |
| if (forkTargetCol > lastCol) |
| { |
| table.cell(forkTargetCol, 0).value("Fork Targets"); |
| } |
| |
| rows[0].footer(table, rowCount); |
| |
| System.out.println(); |
| DUMPER.dump(System.out, table, 0, rowCount); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static abstract class Row implements Comparable<Row> |
| { |
| private final StackTraceElement location; |
| |
| private StackTraceElement[] forkTargets; |
| |
| private long ticks; |
| |
| public Row(StackTraceElement location) |
| { |
| this.location = location; |
| } |
| |
| public void addProbe(Probe probe) |
| { |
| if (probe.forkTargets != null) |
| { |
| if (forkTargets == null) |
| { |
| forkTargets = probe.forkTargets; |
| } |
| else |
| { |
| if (forkTargets.length != 1 || probe.forkTargets.length != 1 |
| || !forkTargets[0].equals(probe.forkTargets[0])) |
| { |
| Set<StackTraceElement> set = new HashSet<StackTraceElement>(); |
| set.addAll(Arrays.asList(forkTargets)); |
| set.addAll(Arrays.asList(probe.forkTargets)); |
| |
| forkTargets = set.toArray(new StackTraceElement[set.size()]); |
| } |
| } |
| } |
| |
| ticks += probe.ticks; |
| addProbe(probe.time); |
| } |
| |
| public abstract void addProbe(int time); |
| |
| public abstract void skipProbes(int count); |
| |
| public abstract int getRuns(); |
| |
| public abstract int header(Table table, int rows); |
| |
| public abstract void body(Table table, int row); |
| |
| public abstract void footer(Table table, int rows); |
| |
| public int compareTo(Row o) |
| { |
| int result = location.getFileName().compareTo(o.location.getFileName()); |
| if (result == 0) |
| { |
| result = location.getLineNumber() - o.location.getLineNumber(); |
| } |
| |
| return result; |
| } |
| |
| public static Row create(StackTraceElement location, boolean full) |
| { |
| return full ? new Full(location) : new Compressed(location); |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class Compressed extends Row |
| { |
| private int skips; |
| |
| private int runs; |
| |
| private long sum; |
| |
| private int min = Integer.MAX_VALUE; |
| |
| private int max = Integer.MIN_VALUE; |
| |
| public Compressed(StackTraceElement location) |
| { |
| super(location); |
| } |
| |
| @Override |
| public void addProbe(int time) |
| { |
| ++runs; |
| sum += time; |
| min = Math.min(min, time); |
| max = Math.max(max, time); |
| } |
| |
| @Override |
| public void skipProbes(int count) |
| { |
| ++skips; |
| } |
| |
| @Override |
| public int getRuns() |
| { |
| return runs; |
| } |
| |
| @Override |
| public int header(Table table, int rows) |
| { |
| int col = DEFAULT_COLUMNS - 1; |
| table.cell(++col, 0).value("Skips"); |
| table.cell(++col, 0).value("Runs"); |
| table.cell(++col, 0).value("Runs %"); |
| table.cell(++col, 0).value("Sum"); |
| table.cell(++col, 0).value("Min"); |
| table.cell(++col, 0).value("Max"); |
| table.cell(++col, 0).value("Range"); |
| table.cell(++col, 0).value("Average/Runs"); |
| table.cell(++col, 0).value("Average%"); |
| table.row(0, 1).alignment(Alignment.RIGHT); |
| return col; |
| } |
| |
| @Override |
| public void body(Table table, int row) |
| { |
| int col = DEFAULT_COLUMNS - 1; |
| table.cell(++col, row).value(skips); |
| |
| Cell runsCell = table.cell(++col, row); |
| runsCell.value(runs); |
| |
| Formula runsPercent = new Formula.Percent(table.range(col - 1, row, col, row), runsCell); |
| table.cell(++col, row).format(PERCENT).value(runsPercent); |
| |
| table.cell(++col, row).format(MILLIS).value(sum); |
| table.cell(++col, row).format(MILLIS).value(min); |
| table.cell(++col, row).format(MILLIS).value(max); |
| table.cell(++col, row).format(MILLIS).value(new Formula() |
| { |
| public Object evaluate() |
| { |
| return max - min; |
| } |
| }); |
| |
| Cell avgCell = table.cell(++col, row); |
| avgCell.format(MILLIS_PRECISE).value(sum / runs); |
| |
| Formula avgPercent = new Formula.Percent(table.column(avgCell.col(), 1), avgCell); |
| table.cell(++col, row).format(PERCENT).value(avgPercent); |
| } |
| |
| @Override |
| public void footer(Table table, int rows) |
| { |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class Full extends Row |
| { |
| private static final int SKIPPED = -1; |
| |
| private int[] times; |
| |
| public Full(StackTraceElement location) |
| { |
| super(location); |
| } |
| |
| @Override |
| public void addProbe(int time) |
| { |
| if (times == null) |
| { |
| times = new int[1]; |
| times[0] = time; |
| } |
| else |
| { |
| int[] newTimes = new int[times.length + 1]; |
| System.arraycopy(times, 0, newTimes, 0, times.length); |
| newTimes[times.length] = time; |
| times = newTimes; |
| } |
| } |
| |
| @Override |
| public void skipProbes(int count) |
| { |
| if (times == null) |
| { |
| times = new int[count]; |
| Arrays.fill(times, SKIPPED); |
| } |
| else |
| { |
| int length = times.length; |
| int newLength = length + count; |
| |
| int[] newTimes = new int[newLength]; |
| System.arraycopy(times, 0, newTimes, 0, length); |
| Arrays.fill(newTimes, length, newLength, SKIPPED); |
| |
| times = newTimes; |
| } |
| } |
| |
| @Override |
| public int getRuns() |
| { |
| if (times == null) |
| { |
| return 0; |
| } |
| |
| int runs = 0; |
| for (int i = 0; i < times.length; i++) |
| { |
| if (times[i] != SKIPPED) |
| { |
| ++runs; |
| } |
| } |
| |
| return runs; |
| } |
| |
| @Override |
| public int header(Table table, int rows) |
| { |
| for (int i = 0; i < times.length; i++) |
| { |
| Cell timeCell = table.cell(DEFAULT_COLUMNS + 2 * i, 0); |
| timeCell.alignment(Alignment.RIGHT).value("Run" + (i + 1)); |
| } |
| |
| Cell avgTimeCell = table.cell(DEFAULT_COLUMNS + 2 * times.length, 0); |
| avgTimeCell.alignment(Alignment.RIGHT).value("AVERAGE"); |
| |
| return avgTimeCell.col() + 1; |
| } |
| |
| @Override |
| public void body(Table table, int row) |
| { |
| Range avgTimeRange = null; |
| |
| for (int i = 0; i < times.length; i++) |
| { |
| int time = times[i]; |
| if (time != SKIPPED) |
| { |
| int timeCol = DEFAULT_COLUMNS + 2 * i; |
| |
| Cell timeCell = table.cell(timeCol, row); |
| timeCell.format(MILLIS).value(time); |
| |
| Cell ratioCell = timeCell.offset(1, 0); |
| ratioCell.format(PERCENT).value(new Formula.Percent(table.column(timeCol, 1), timeCell)); |
| |
| avgTimeRange = avgTimeRange == null ? timeCell : avgTimeRange.addRanges(timeCell); |
| } |
| } |
| |
| Cell avgTimeCell = table.cell(DEFAULT_COLUMNS + 2 * times.length, row); |
| avgTimeCell.format(MILLIS).value(new Formula.Avg(avgTimeRange)); |
| |
| Cell avgRatioCell = avgTimeCell.offset(1, 0); |
| avgRatioCell.format(PERCENT).value(new Formula.Percent(table.column(avgTimeCell.col(), 1), avgTimeCell)); |
| } |
| |
| @Override |
| public void footer(Table table, int rows) |
| { |
| table.cell(0, 1 + rows).value("TOTAL"); |
| table.cell(1, 1 + rows).format(TICKS).value(new Formula.Sum(table.column(1, 1, 1))); |
| |
| for (int i = 0; i <= times.length; i++) |
| { |
| int timeCol = DEFAULT_COLUMNS + 2 * i; |
| Cell sumCell = table.cell(timeCol, 1 + rows); |
| sumCell.format(MILLIS).value(new Formula.Sum(table.column(timeCol, 1, 1))); |
| } |
| } |
| } |
| } |
| } |
| } |