com.google.caliper.cloud.client.BenchmarkDataViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.caliper.cloud.client.BenchmarkDataViewer.java

Source

/**
 * Copyright (C) 2009 Google Inc.
 *
 * 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 com.google.caliper.cloud.client;

import com.google.caliper.LinearTranslation;
import com.google.caliper.Measurement;
import com.google.caliper.MeasurementSet;
import com.google.caliper.MeasurementType;
import com.google.caliper.Run;
import com.google.caliper.Scenario;
import com.google.caliper.ScenarioResult;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.widgetideas.graphics.client.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * The core data viewer user interface. The variables are divided into three
 * categories:
 * <ul>
 *   <li>Fixed: variables with only one value
 *   <li>R Variables: variables whose values vary in the rows of the table
 *   <li>C Variable: a variable whose values make up the columns of the table
 * <ul>
 */
public final class BenchmarkDataViewer {
    private static final AsyncCallback<Void> NO_OP_CALLBACK = new AsyncCallback<Void>() {
        public void onFailure(Throwable unused) {
        }

        public void onSuccess(Void unused) {
        }
    };

    /** bar chart dimension in pixels */
    private static final int MAX_BAR_WIDTH = 200;
    private static final int MAX_BAR_HEIGHT = 18;

    private static final int DIGITS_OF_PRECISION = 3;

    private static final Map<MeasurementType, Map<String, Integer>> DEFAULT_UNITS = new HashMap<MeasurementType, Map<String, Integer>>();
    static {
        Map<String, Integer> timeUnits = new HashMap<String, Integer>();
        timeUnits.put("ns", 1);
        timeUnits.put("us", 1000);
        timeUnits.put("ms", 1000000);
        timeUnits.put("s", 1000000000);
        DEFAULT_UNITS.put(MeasurementType.TIME, timeUnits);
        Map<String, Integer> instanceUnits = new HashMap<String, Integer>();
        instanceUnits.put(" instances", 1);
        instanceUnits.put("K instances", 1000);
        instanceUnits.put("M instances", 1000000);
        instanceUnits.put("B instances", 1000000000);
        DEFAULT_UNITS.put(MeasurementType.INSTANCE, instanceUnits);
        Map<String, Integer> memoryUnits = new HashMap<String, Integer>();
        memoryUnits.put("B", 1);
        memoryUnits.put("KB", 1024);
        memoryUnits.put("MB", 1048576);
        memoryUnits.put("GB", 1073741824);
        DEFAULT_UNITS.put(MeasurementType.MEMORY, memoryUnits);
    }

    private final boolean editable;
    private final String benchmarkOwner;
    private final String benchmarkName;
    private final long snapshotId;
    private final RootPanel snapshotDisclaimerDiv;
    private final RootPanel resultsDiv;
    private final RootPanel fixedVariablesDiv;
    private final RootPanel runsDiv;
    private final SnapshotsTableDisplay snapshotsTableDisplay;
    private final EnvironmentsTableDisplay environmentsTableDisplay;
    private final EnvironmentsIndex environmentsIndex;
    private final List<RunMeta> runMetas = new ArrayList<RunMeta>();

    /** the full set of application variables */
    private final Map<String, Variable> variableMap = new HashMap<String, Variable>();
    private final List<Variable> variables = new ArrayList<Variable>();
    private Variable runVariable;

    /** these variables' values each get their own row. */
    private final List<Variable> rVariables = new ArrayList<Variable>();

    /** this variable's values each get their own column. */
    private Variable cVariable;
    private final List<Value> cValues = new ArrayList<Value>();
    /** An index of results by the variables involved in measuring it. */
    private final Map<Key, Datapoint> keysToDatapoints = new HashMap<Key, Datapoint>();

    private MeasurementType selectedType = MeasurementType.TIME;
    private List<MeasurementType> orderedMeasurementTypes = Arrays.asList(MeasurementType.values());
    private Map<MeasurementType, Integer> divideByMap;
    private Map<MeasurementType, String> unitMap;
    private Map<MeasurementType, Boolean> useRawMap;
    private Map<MeasurementType, Double> maxMap;
    private Map<MeasurementType, Double> minMap;
    private Map<MeasurementType, Double> referencePointMap;
    private Map<MeasurementType, NumberFormat> numberFormatMap;
    private final NumberFormat percentFormat = NumberFormat.getPercentFormat();

    /** We allow the user to toggle between linear and logarithmic bar charts. */
    private boolean logarithmic = false;

    /** We allow the user to toggle between HTML and plaintext */
    private boolean plainText;

    private Benchmark benchmark = null;
    private BenchmarkMeta benchmarkMeta = null;

    public BenchmarkDataViewer(boolean editable, String benchmarkOwner, String benchmarkName, long snapshotId,
            RootPanel snapshotDisclaimerDiv, RootPanel resultsDiv, RootPanel fixedVariablesDiv, RootPanel runsDiv,
            RootPanel environmentsDiv, RootPanel snapshotsDiv) {
        this.editable = editable;
        this.benchmarkOwner = benchmarkOwner;
        this.benchmarkName = benchmarkName;
        this.snapshotId = snapshotId;
        this.snapshotDisclaimerDiv = snapshotDisclaimerDiv;
        this.resultsDiv = resultsDiv;
        this.fixedVariablesDiv = fixedVariablesDiv;
        this.runsDiv = runsDiv;
        environmentsIndex = new EnvironmentsIndex();
        environmentsTableDisplay = new EnvironmentsTableDisplay(this, environmentsDiv, environmentsIndex);
        snapshotsTableDisplay = new SnapshotsTableDisplay(snapshotsDiv);
    }

    public void setBenchmarkMeta(BenchmarkMeta benchmarkMeta) {
        if (isSnapshot()) {
            if (benchmarkMeta == null) {
                throw new IllegalArgumentException("invalid snapshot id " + snapshotId);
            }
        }
        this.benchmark = benchmarkMeta.getBenchmark();
        this.benchmarkMeta = benchmarkMeta;
        this.runMetas.clear();

        if (benchmark.getRuns().isEmpty()) {
            resultsDiv.clear();
            resultsDiv.add(new Label("No runs."));
            fixedVariablesDiv.clear();
            fixedVariablesDiv.add(new Label("No runs."));
            runsDiv.clear();
            runsDiv.add(new Label("No runs."));
            environmentsTableDisplay.setNoRuns();
            rebuildSnapshotsTable();
            return;
        }

        this.runMetas.addAll(benchmark.getRuns());
        environmentsIndex.setRunMetas(this.runMetas);

        rebuildVariables(benchmark);
        rebuildIndex();
        rebuildValueIndices();
        rebuildCValues();

        rebuildSnapshotDisclaimer();
        rebuildResultsTable();
        rebuildVariablesTable();
        rebuildRunsTable();
        rebuildEnvironmentsTable();
        rebuildSnapshotsTable();
    }

    public void rebuildEnvironmentsTable() {
        environmentsTableDisplay.rebuild(editable);
    }

    public void rebuildSnapshotsTable() {
        snapshotsTableDisplay.rebuild(editable, benchmarkMeta, snapshotId);
    }

    private void rebuildSnapshotDisclaimer() {
        snapshotDisclaimerDiv.clear();
        if (isSnapshot()) {
            DateTimeFormat format = DateTimeFormat.getFormat("yyyy-MM-dd 'at' HH:mm:ss Z");
            long createdEpochTime = 0;
            for (BenchmarkSnapshotMeta snapshot : benchmarkMeta.getSnapshots()) {
                if (snapshot.getId() == snapshotId) {
                    createdEpochTime = snapshot.getCreated();
                }
            }
            String formattedDate = format.format(new Date(createdEpochTime));
            snapshotDisclaimerDiv.add(
                    new HTML("This is a snapshot taken on " + formattedDate + " (<a target=\"_blank\" href=\"/run/"
                            + URL.encode(benchmarkOwner) + "/" + URL.encode(benchmarkName) + "\">Original</a>)"));
        }
    }

    /**
     * Rebuilds the data structure that knows which variables exist and what their
     * range of values are. Also sets up initial ordering of variables.
     */
    private void rebuildVariables(Benchmark benchmark) {
        Map<String, Variable> allVariables = indexVariables();

        variableMap.clear();
        variableMap.putAll(allVariables);
        variables.clear();
        variables.addAll(allVariables.values());

        // if we've already built the rVariables, copy that set
        if (!rVariables.isEmpty()) {
            for (int i = 0; i < rVariables.size(); i++) {
                rVariables.set(i, allVariables.get(rVariables.get(i).getName()));
            }
            if (cVariable != null) {
                cVariable = allVariables.get(cVariable.getName());
            }

            // if the server provided a list of rVariables, use those
        } else if (benchmark.getRVariables() != null) {
            for (String variableName : benchmark.getRVariables()) {
                this.rVariables.add(allVariables.get(variableName));
            }
            if (benchmark.getCVariable() != null) {
                cVariable = allVariables.get(benchmark.getCVariable());
            }

            // otherwise, come up with a reasonable default
        } else {
            cVariable = runVariable;
        }

        // make sure all variables are represented
        for (Variable variable : variables) {
            if (!rVariables.contains(variable) && cVariable != variable) {
                rVariables.add(variable);
            }
        }

        // hide variables that have only one value
        for (Iterator<Variable> v = rVariables.iterator(); v.hasNext();) {
            if (!v.next().hasMultipleValues()) {
                v.remove();
            }
        }
        if (cVariable != null && !cVariable.hasMultipleValues()) {
            cVariable = null;
        }

        rebuildCValues();
    }

    /**
     * We consider the median element of a three-measurement MeasurementSet to be as good a
     * representation of the measurements as any of the three, so throw away the min and max.
     *
     * This ensures that any maximums and minimums kept are representative of what's displayed
     * in the results table.
     *
     * An alternative to this would be to have this.maxDisplayed and this.minDisplayed in addition
     * to this.max and this.min.
     */
    private ScenarioResult normalize(ScenarioResult scenarioResults) {
        Map<MeasurementType, MeasurementSet> measurementSetMap = new HashMap<MeasurementType, MeasurementSet>();
        for (MeasurementType measurementType : orderedMeasurementTypes) {
            MeasurementSet measurementSet = scenarioResults.getMeasurementSet(measurementType);
            if (measurementSet == null) {
                continue;
            }

            if (measurementSet.size() > 3) {
                measurementSetMap.put(measurementType, measurementSet);
            } else {
                measurementSetMap.put(measurementType,
                        new MeasurementSet(
                                new Measurement(measurementSet.getUnitNames(DEFAULT_UNITS.get(measurementType)),
                                        measurementSet.medianRaw(), measurementSet.medianUnits())));
            }
        }
        return new ScenarioResult(measurementSetMap.get(MeasurementType.TIME),
                scenarioResults.getEventLog(MeasurementType.TIME), measurementSetMap.get(MeasurementType.INSTANCE),
                scenarioResults.getEventLog(MeasurementType.INSTANCE),
                measurementSetMap.get(MeasurementType.MEMORY), scenarioResults.getEventLog(MeasurementType.MEMORY));
    }

    private Map<String, Variable> indexVariables() {
        Map<String, Variable> allVariables = new LinkedHashMap<String, Variable>();
        Map<String, Map<String, Boolean>> variableValuesShown = benchmark.getVariableValuesShown();
        int numVariables = 0;

        // create the special variable for the run
        runVariable = new Variable("run", numVariables++) {
            @Override
            public Value get(RunMeta runMeta, Scenario scenario) {
                return get(String.valueOf(runMeta.getId()));
            }
        };
        allVariables.put("run", runVariable); // TODO: something less likely to conflict with userspace

        // create the other variables by inspecting the runs
        for (final RunMeta runMeta : runMetas) {
            Run run = runMeta.getRun();
            Value runValue = new Value(String.valueOf(runMeta.getId())) {
                @Override
                public String getLabel() {
                    return runMeta.getName(); // we override this because the label can change
                }
            };
            if (variableValuesShown != null) {
                Map<String, Boolean> runValuesShown = variableValuesShown.get("run");
                if (runValuesShown != null) {
                    Boolean isShown = runValuesShown.get(runValue.getName());
                    runValue.setShown(isShown == null ? true : isShown);
                }
            }
            runVariable.addValue(runValue);

            for (Map.Entry<Scenario, ScenarioResult> kv : run.getMeasurements().entrySet()) {
                Scenario scenario = kv.getKey();

                for (Map.Entry<String, String> entry : scenario.getVariables().entrySet()) {
                    String name = entry.getKey();
                    String valueName = entry.getValue();

                    Variable variable = allVariables.get(name);
                    if (variable == null) {
                        variable = new Variable(name, numVariables++);
                        allVariables.put(name, variable);
                    }

                    Value value = variable.addValue(valueName);
                    if (variableValuesShown != null) {
                        Map<String, Boolean> valuesShown = variableValuesShown.get(name);
                        if (valuesShown != null) {
                            Boolean isShown = valuesShown.get(valueName);
                            value.setShown(isShown == null ? true : isShown);
                        }
                    }
                }
            }
        }
        return allVariables;
    }

    private void rebuildValueIndices() {
        for (Variable variable : variables) {
            for (Value value : variable.getValues()) {
                value.resetIndex();
            }
        }
        for (Value value : runVariable.getValues()) {
            value.resetIndex();
        }

        for (final RunMeta runMeta : runMetas) {
            Value runValue = runVariable.get(String.valueOf(runMeta.getId()));
            for (Map.Entry<Scenario, ScenarioResult> kv : runMeta.getRun().getMeasurements().entrySet()) {
                Scenario scenario = kv.getKey();
                ScenarioResult scenarioResults = normalize(kv.getValue());

                boolean isScenarioShown = runValue.isShown();
                for (Map.Entry<String, String> entry : scenario.getVariables().entrySet()) {
                    String name = entry.getKey();
                    String valueName = entry.getValue();
                    Variable variable = variableMap.get(name);
                    Value value = variable.get(valueName);
                    isScenarioShown = isScenarioShown && value.isShown();
                }
                if (!isScenarioShown) {
                    continue;
                }

                MeasurementSet measurementSet = scenarioResults.getMeasurementSet(selectedType);
                if (measurementSet != null) {
                    runValue.index(measurementSet, useRawMap.get(selectedType));
                    for (Map.Entry<String, String> entry : scenario.getVariables().entrySet()) {
                        String name = entry.getKey();
                        String valueName = entry.getValue();
                        Variable variable = variableMap.get(name);
                        Value value = variable.get(valueName);
                        value.index(measurementSet, useRawMap.get(selectedType));
                    }
                }
            }
        }
    }

    /**
     * Rebuilds the index from the combinations of variables to the
     * corresponding datapoint.
     */
    private void rebuildIndex() {
        this.unitMap = new HashMap<MeasurementType, String>();
        this.divideByMap = new HashMap<MeasurementType, Integer>();
        this.numberFormatMap = new HashMap<MeasurementType, NumberFormat>();
        this.maxMap = new HashMap<MeasurementType, Double>();
        this.minMap = new HashMap<MeasurementType, Double>();
        this.referencePointMap = new HashMap<MeasurementType, Double>();
        this.useRawMap = new HashMap<MeasurementType, Boolean>();
        for (MeasurementType measurementType : orderedMeasurementTypes) {
            if (measurementType == MeasurementType.DEBUG) {
                continue;
            }

            double min = Double.POSITIVE_INFINITY;
            double max = 0;

            // select the units to use - default to ns/us/ms/s if there are any differences in the
            // user-defined units between runs.
            Map<String, Integer> units = null;
            boolean useRaw = false;
            UNIT_SELECTION_LOOP: for (RunMeta runMeta : runMetas) {
                for (ScenarioResult scenarioResults : runMeta.getRun().getMeasurements().values()) {
                    MeasurementSet measurementSet = scenarioResults.getMeasurementSet(measurementType);
                    // if we have no measurement for this run, just skip this run.
                    if (measurementSet == null) {
                        continue UNIT_SELECTION_LOOP;
                    }
                    if (units == null) {
                        units = measurementSet.getUnitNames();
                    } else if (!units.equals(measurementSet.getUnitNames())) {
                        useRaw = true;
                        units = DEFAULT_UNITS.get(measurementType);
                        break UNIT_SELECTION_LOOP;
                    }
                }
            }
            useRawMap.put(measurementType, useRaw);
            if (units == null) {
                units = DEFAULT_UNITS.get(measurementType);
            }

            keysToDatapoints.clear();
            for (RunMeta runMeta : runMetas) {
                for (Map.Entry<Scenario, ScenarioResult> entry : runMeta.getRun().getMeasurements().entrySet()) {
                    ScenarioResult scenarioResult = normalize(entry.getValue());

                    Scenario scenario = entry.getKey();
                    Key key = new Key(variables.size());

                    boolean isShown = true;
                    for (Variable variable : variables) {
                        Value value = variable.get(runMeta, scenario);
                        if (value != null) {
                            isShown = isShown && value.isShown();
                        }
                        key.set(variable, value);
                    }

                    if (isShown) {
                        MeasurementSet measurementSet = scenarioResult.getMeasurementSet(measurementType);
                        if (measurementSet != null) {
                            min = Math.min(min, getMin(measurementType, measurementSet));
                            max = Math.max(max, getMax(measurementType, measurementSet));
                        }
                    }

                    keysToDatapoints.put(key, new Datapoint(scenarioResult, runMeta.getStyle()));
                }
            }

            this.maxMap.put(measurementType, max);
            this.minMap.put(measurementType, min);
            this.referencePointMap.put(measurementType, min);

            List<Map.Entry<String, Integer>> entries = new ArrayList<Map.Entry<String, Integer>>(units.entrySet());

            // sort entries in reverse order
            Collections.sort(entries, new Comparator<Map.Entry<String, Integer>>() {
                @Override
                public int compare(Map.Entry<String, Integer> a, Map.Entry<String, Integer> b) {
                    return b.getValue().compareTo(a.getValue());
                }
            });

            int numDigitsInMin = ceil(Math.log10(min));
            String unitCandidate = null;
            for (Map.Entry<String, Integer> entry : entries) {
                if (min / entry.getValue() >= 1) {
                    unitCandidate = entry.getKey();
                    break;
                }
            }
            if (unitCandidate == null) {
                // if no unit works, just use the smallest available unit.
                unitCandidate = entries.get(entries.size() - 1).getKey();
            }

            unitMap.put(measurementType, unitCandidate);
            divideByMap.put(measurementType, units.get(unitCandidate));
            int decimalDigits = ceil(Math.max(0,
                    Math.log10(divideByMap.get(measurementType)) + DIGITS_OF_PRECISION - numDigitsInMin));

            String format = "#,###,##0";
            if (decimalDigits > 0) {
                format += ".";
                for (int i = 0; i < decimalDigits; i++) {
                    format += "0";
                }
            }
            numberFormatMap.put(measurementType, NumberFormat.getFormat(format));
        }
    }

    private double getMin(MeasurementType measurementType, MeasurementSet measurementSet) {
        return useRawMap.get(measurementType) ? measurementSet.minRaw() : measurementSet.minUnits();
    }

    private double getMax(MeasurementType measurementType, MeasurementSet measurementSet) {
        return useRawMap.get(measurementType) ? measurementSet.maxRaw() : measurementSet.maxUnits();
    }

    private double getMedian(MeasurementType measurementType, MeasurementSet measurementSet) {
        return useRawMap.get(measurementType) ? measurementSet.medianRaw() : measurementSet.medianUnits();
    }

    private Collection<Value> rebuildCValues() {
        cValues.clear();

        if (cVariable != null) {
            for (Value value : cVariable.getValues()) {
                if (value.isShown()) {
                    cValues.add(value);
                }
            }
        } else {
            cValues.add(null);
        }
        return cValues;
    }

    private static int ceil(double d) {
        return (int) Math.ceil(d);
    }

    public void rebuildResultsTable() {
        if (plainText) {
            Label label = new Label();
            label.setStyleName("plaintext");
            label.setText(gridToString(toGrid()));

            resultsDiv.clear();
            resultsDiv.add(label);
            resultsDiv.add(new PlainTextEditor().getWidget());
            HTML dash = new HTML(" - ", false);
            dash.setStyleName("inline");
            resultsDiv.add(dash);
            resultsDiv.add(new SnapshotCreator().getWidget());
            return;
        }

        FlexTable table = new FlexTable();
        table.setStyleName("data");
        int r = 0;
        int c = 0;
        int evenRowMod = 0;

        // results header #1: cValue variables
        if (cVariable != null) {
            evenRowMod = 1;
            table.insertRow(r);
            table.getRowFormatter().setStyleName(r, "valueRow");
            table.getRowFormatter().addStyleName(r, "headerRow");

            table.addCell(r);
            table.getFlexCellFormatter().setColSpan(r, 0, rVariables.size());
            c++;
            for (Value cValue : cValues) {
                table.addCell(r);
                table.getFlexCellFormatter().setColSpan(r, c, 3);
                table.getCellFormatter().setStyleName(r, c, "parameterKey");

                Widget contents = newVariableLabel(cVariable, cValue.getLabel(), rVariables.size());
                contents.setStyleName("valueHeader");

                table.setWidget(r, c++, contents);
            }
            r++;
        }

        // results header 2: rValue variables, followed by "nanos/barchart" column pairs
        c = 0;
        table.insertRow(r);
        table.getRowFormatter().setStyleName(r, "evenRow");
        table.getRowFormatter().addStyleName(r, "headerRow");
        for (Variable variable : rVariables) {
            table.addCell(r);
            table.getCellFormatter().setStyleName(r, c, "parameterKey");
            table.setWidget(r, c, newVariableLabel(variable, variable.getName(), c));
            c++;
        }
        for (Value unused : cValues) {
            table.addCell(r);
            table.getCellFormatter().setStyleName(r, c, "parameterKey");
            table.setWidget(r, c++, newUnitLabel(unitMap.get(selectedType).trim()));

            table.addCell(r);
            table.getCellFormatter().setStyleName(r, c, "parameterKey");
            table.setWidget(r, c++, newRuntimeLabel());

            table.addCell(r);
            table.getCellFormatter().setStyleName(r, c, "parameterKey");
            table.setWidget(r, c++, new InlineLabel("%"));
        }
        r++;

        Key key = newDefaultKey();
        for (RowsIterator rows = new RowsIterator(rVariables); rows.nextRow();) {
            rows.updateKey(key);

            table.insertRow(r);
            table.getRowFormatter().setStyleName(r, r % 2 == evenRowMod ? "evenRow" : "oddRow");
            c = 0;
            for (int v = 0, size = rVariables.size(); v < size; v++) {
                table.addCell(r);
                table.setWidget(r, c++, new Label(rows.getRValue(v).getLabel()));
            }

            for (Value value : cValues) {
                table.addCell(r);
                table.addCell(r);

                if (cVariable != null) {
                    key.set(cVariable, value);
                }

                final Datapoint datapoint = keysToDatapoints.get(key);
                table.getCellFormatter().setStyleName(r, c, "numericCell");
                table.getCellFormatter().setStyleName(r, c + 1, "bar");
                table.getCellFormatter().setStyleName(r, c + 2, "numericCell");
                MeasurementSet measurementSet;
                if (datapoint != null
                        && (measurementSet = datapoint.scenarioResults.getMeasurementSet(selectedType)) != null) {
                    double rawMedian = getMedian(selectedType, measurementSet);
                    String displayedValue = numberFormatMap.get(selectedType)
                            .format(rawMedian / divideByMap.get(selectedType));
                    Anchor valueAnchor = new Anchor(displayedValue, false);
                    valueAnchor.setStyleName("subtleLink");
                    valueAnchor.setStyleName("nanos", true);

                    final DialogBox eventLogPopup = new DialogBox(true);
                    eventLogPopup.setText("");

                    valueAnchor.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent clickEvent) {
                            // Do this lazily since it takes quite a bit of time to render these popups for all
                            // the scenarios shown, and quite often they won't even be used.
                            if (eventLogPopup.getText().isEmpty()) {
                                eventLogPopup.setText("Event Log");
                                String eventLog = datapoint.scenarioResults.getEventLog(selectedType);
                                if (eventLog == null || eventLog.isEmpty()) {
                                    eventLog = "No event log recorded.";
                                }
                                FlowPanel panel = new FlowPanel();
                                for (String line : eventLog.split("\n")) {
                                    panel.add(new Label(line));
                                }
                                panel.setStyleName("eventLog");
                                eventLogPopup.add(panel);
                            }
                            eventLogPopup.center();
                            eventLogPopup.show();
                        }
                    });

                    table.setWidget(r, c, valueAnchor);
                    table.setWidget(r, c + 1, newBar(datapoint.style, measurementSet, value));
                    table.setWidget(r, c + 2, newPercentOfReferencePointLabel(rawMedian, value));
                } else {
                    table.setWidget(r, c, new Label(""));
                    table.setWidget(r, c + 1, new Label(""));
                    table.setWidget(r, c + 2, new Label(""));
                }
                c += 3;
            }

            r++;
        }
        resultsDiv.clear();
        resultsDiv.add(table);
        resultsDiv.add(new PlainTextEditor().getWidget());
        HTML dash = new HTML(" - ", false);
        dash.setStyleName("inline");
        resultsDiv.add(dash);
        resultsDiv.add(new SnapshotCreator().getWidget());
    }

    class SnapshotCreator implements ClickHandler {
        private static final String SAVING_TEXT = "Saving...";
        private static final String DEFAULT_TEXT = "Create Snapshot";
        private static final String FAILED_TEXT = "Failed";

        private final Anchor anchor;

        private SnapshotCreator() {
            anchor = new Anchor(DEFAULT_TEXT);
            anchor.addClickHandler(this);
            anchor.setStyleName("actionLink");
        }

        public void onClick(ClickEvent clickEvent) {
            anchor.setText(SAVING_TEXT);
            BenchmarkServiceAsync benchmarkService = GWT.create(BenchmarkService.class);
            if (benchmark != null) {
                List<String> rVariableNames = new ArrayList<String>();
                for (Variable rVariable : rVariables) {
                    rVariableNames.add(rVariable.getName());
                }
                String cVariableName = null;
                if (cVariable != null) {
                    cVariableName = cVariable.getName();
                }
                Benchmark benchmarkToSave = new Benchmark(benchmarkOwner, benchmarkName, runMetas, rVariableNames,
                        cVariableName, variableValuesShown());
                benchmarkService.createSnapshot(benchmarkToSave, new AsyncCallback<Long>() {
                    public void onFailure(Throwable throwable) {
                        anchor.setText(FAILED_TEXT);
                    }

                    public void onSuccess(Long snapshotId) {
                        anchor.setText(DEFAULT_TEXT);
                        // open a new tab/window with the snapshot
                        Window.open("/run/" + benchmarkOwner + "/" + benchmarkName + "/" + snapshotId, "_blank",
                                "");
                    }
                });
            }
        }

        public Anchor getWidget() {
            return anchor;
        }
    }

    class PlainTextEditor implements ClickHandler {
        private final Anchor anchor;

        private PlainTextEditor() {
            anchor = new Anchor(plainText ? "HTML" : "Plain Text");
            anchor.addClickHandler(this);
            anchor.setStyleName("actionLink");
        }

        public void onClick(ClickEvent clickEvent) {
            plainText = !plainText;
            rebuildResultsTable();
        }

        public Anchor getWidget() {
            return anchor;
        }
    }

    /**
     * Returns a grid containing the raw contents of the results table. Cells are
     * either string labels, integers or measurement sets.
     */
    public List<List<Object>> toGrid() {
        List<List<Object>> result = new ArrayList<List<Object>>();

        // results header #1: cValue variables
        if (cVariable != null) {
            List<Object> header1 = new ArrayList<Object>();
            for (Object unused : rVariables) {
                header1.add("");
            }
            for (Value cValue : cValues) {
                header1.add("");
                header1.add(cValue.getLabel());
                header1.add("");
            }
            result.add(header1);
        }

        // results header 2: rValue variables, followed by nanos, barchart, %
        List<Object> header2 = new ArrayList<Object>();
        for (Variable variable : rVariables) {
            header2.add(variable.getName());
        }
        for (Value unused : cValues) {
            header2.add(unitMap.get(selectedType).trim());
            header2.add(logarithmic ? "logarithmic runtime" : "linear runtime");
            header2.add("%");
        }
        result.add(header2);

        Key key = newDefaultKey();
        for (RowsIterator rows = new RowsIterator(rVariables); rows.nextRow();) {
            rows.updateKey(key);
            List<Object> row = new ArrayList<Object>();

            for (int v = 0, size = rVariables.size(); v < size; v++) {
                row.add(rows.getRValue(v).getLabel());
            }

            for (Value value : cValues) {
                if (cVariable != null) {
                    key.set(cVariable, value);
                }

                Datapoint datapoint = keysToDatapoints.get(key);
                MeasurementSet measurementSet;
                if (datapoint == null
                        || (measurementSet = datapoint.scenarioResults.getMeasurementSet(selectedType)) == null) {
                    // sparse dataset
                    row.add("");
                    row.add("");
                    row.add("");
                } else {
                    double rawMedian = getMedian(selectedType, measurementSet);
                    row.add(rawMedian / divideByMap.get(selectedType));
                    row.add(measurementSet);
                    row.add(percentFormat.format(getRatio(rawMedian, value)));
                }
            }
            result.add(row);
        }

        return result;
    }

    public String gridToString(List<List<Object>> grid) {
        List<Object> firstRow = grid.get(0);

        // compute max and min values per column
        double[] minValues = new double[firstRow.size()];
        for (int i = 0; i < minValues.length; i++) {
            minValues[i] = Double.POSITIVE_INFINITY;
        }
        double[] maxValues = new double[firstRow.size()];
        for (List<Object> row : grid) {
            int c = 0;
            for (Object cell : row) {
                if (cell instanceof MeasurementSet) {
                    double value = getMedian(selectedType, ((MeasurementSet) cell));
                    minValues[c] = Math.min(minValues[c], value);
                    maxValues[c] = Math.max(maxValues[c], value);
                }
                c++;
            }
        }

        // turn the grid of objects into a grid of strings
        int[] maxColumnWidths = new int[firstRow.size()];
        String[][] stringsGrid = new String[grid.size()][firstRow.size()];
        Map<Integer, Boolean> isMeasurementSetColumn = new HashMap<Integer, Boolean>();
        int r = 0;
        for (List<Object> row : grid) {
            int c = 0;
            for (Object cell : row) {
                if (cell instanceof String) {
                    stringsGrid[r][c] = (String) cell;
                } else if (cell instanceof Double) {
                    stringsGrid[r][c] = numberFormatMap.get(selectedType).format((Double) cell);
                } else if (cell instanceof MeasurementSet) {
                    isMeasurementSetColumn.put(c, true);
                    stringsGrid[r][c] = asciiArtBar(getMedian(selectedType, ((MeasurementSet) cell)), minValues[c],
                            maxValues[c]);
                }

                maxColumnWidths[c] = Math.max(maxColumnWidths[c], stringsGrid[r][c].length());
                c++;
            }
            r++;
        }

        // create one big string
        StringBuilder result = new StringBuilder();
        r = 0;
        for (String[] row : stringsGrid) {
            int c = 0;
            for (String cell : row) {
                boolean alignLeft = isMeasurementSetColumn.get(c) != null;
                int padding = maxColumnWidths[c] - cell.length();
                if (alignLeft) {
                    result.append(cell);
                    result.append(repeat(" ", padding));
                } else {
                    result.append(repeat(" ", padding));
                    result.append(cell);
                }
                if (c < firstRow.size()) {
                    result.append(" ");
                }
                c++;
            }
            result.append("\n");
            r++;
        }
        return result.toString();
    }

    private String repeat(String value, int times) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < times; i++) {
            result.append(value);
        }
        return result.toString();
    }

    private String asciiArtBar(double value, double minValue, double maxValue) {
        String bar = "==============================";
        int width;
        if (logarithmic && Math.abs(minValue - maxValue) > 1.0E-6) {
            LinearTranslation translation = new LinearTranslation(minValue, 1, maxValue, bar.length());
            width = Math.max(Math.min((int) translation.translate(value), bar.length()), 1);
        } else {
            width = (int) (bar.length() * value / maxValue);
        }
        return bar.substring(0, width);
    }

    private Key newDefaultKey() {
        Key key = new Key(variables.size());
        for (Variable v : variables) {
            if (!v.hasMultipleValues()) {
                key.set(v, v.getOnlyValue());
            }
        }
        return key;
    }

    /**
     * @param variable the variable to label. If null, no variable is being labelled.
     * @param rIndex the column the labelled variable will be displayed in, or
     *      rVariables.size() if this is a cVariable.
     */
    private Widget newVariableLabel(final Variable variable, String label, final int rIndex) {
        if (variable == null) {
            return new InlineLabel(label);
        }

        Panel panel = new FlowPanel();

        if (rIndex > 0 || rIndex == rVariables.size()) {
            Anchor shiftLeft = new Anchor("\u2190");
            shiftLeft.setStyleName("hiddentoggle");
            shiftLeft.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent clickEvent) {
                    moveVariable(variable, rIndex, rIndex - 1);
                }
            });
            panel.add(shiftLeft);
        }

        panel.add(new InlineLabel("\u00A0" + label + "\u00A0"));

        if (rIndex < rVariables.size()) {
            Anchor shiftRight = new Anchor("\u2192");
            shiftRight.setStyleName("hiddentoggle");
            shiftRight.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent clickEvent) {
                    moveVariable(variable, rIndex, rIndex + 1);
                }
            });
            panel.add(shiftRight);
        }

        return panel;
    }

    void moveVariable(Variable variable, int oldIndex, int newIndex) {
        // we're demoting the cVariable to an rVariable
        if (variable == cVariable) {
            rVariables.add(variable);
            cVariable = null;

            // we're promoting an rVariable to a cVariable
        } else if (newIndex == rVariables.size()) {
            rVariables.remove(oldIndex);
            if (cVariable != null) {
                rVariables.add(cVariable); // demote the current cVariable; we can have at most one
            }
            cVariable = variable;

            // we're reordering the rVariables
        } else {
            rVariables.remove(oldIndex);
            rVariables.add(newIndex, variable);
        }

        rebuildCValues();
        rebuildResultsTable();

        if (editable) {
            List<String> rVariableNames = new ArrayList<String>();
            for (Variable rVariable : rVariables) {
                rVariableNames.add(rVariable.getName());
            }
            String cVariableName = null;
            if (cVariable != null) {
                cVariableName = cVariable.getName();
            }
            BenchmarkServiceAsync benchmarkService = GWT.create(BenchmarkService.class);
            benchmarkService.reorderVariables(benchmarkOwner, benchmarkName, rVariableNames, cVariableName,
                    NO_OP_CALLBACK);
        }
    }

    private void cycleSelectedType() {
        selectedType = orderedMeasurementTypes
                .get((orderedMeasurementTypes.indexOf(selectedType) + 1) % orderedMeasurementTypes.size());
    }

    private Widget newUnitLabel(String unit) {
        Anchor result = new Anchor(unit);
        result.setStyleName("visibletoggle");
        result.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                cycleSelectedType();
                rebuildValueIndices();
                rebuildCValues();
                rebuildResultsTable();
            }
        });
        return result;
    }

    private Widget newRuntimeLabel() {
        String label = logarithmic ? "logarithmic runtime" : "linear runtime";
        Anchor result = new Anchor(label);
        result.setStyleName("visibletoggle");
        result.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                logarithmic = !logarithmic;
                rebuildResultsTable();
            }
        });
        return result;
    }

    private MeasurementSet log(MeasurementSet measurementSet) {
        List<Measurement> measurements = new ArrayList<Measurement>();
        for (Measurement oldMeasurement : measurementSet.getMeasurements()) {
            measurements.add(new Measurement(measurementSet.getUnitNames(), Math.log(oldMeasurement.getRaw()),
                    Math.log(oldMeasurement.getProcessed())));
        }
        return new MeasurementSet(measurements.toArray(new Measurement[measurements.size()]));
    }

    /**
     * Returns a measurement set that has had either its user-defined unit values or raw values
     * translated by {@code translation}, depending on whether we're using raw values.
     *
     * Unlike log, we do not simply translate both raw and processed values since the translation
     * passed in is in terms of one or the other.
     */
    private MeasurementSet translate(boolean raw, MeasurementSet measurementSet, LinearTranslation translation) {
        List<Measurement> measurements = new ArrayList<Measurement>();
        for (Measurement oldMeasurement : measurementSet.getMeasurements()) {
            if (raw) {
                measurements.add(new Measurement(measurementSet.getUnitNames(),
                        translation.translate(oldMeasurement.getRaw()), oldMeasurement.getProcessed()));
            } else {
                measurements.add(new Measurement(measurementSet.getUnitNames(), oldMeasurement.getRaw(),
                        translation.translate(oldMeasurement.getProcessed())));
            }
        }
        return new MeasurementSet(measurements.toArray(new Measurement[measurements.size()]));
    }

    private BoxPlot boxPlot = new BoxPlot(MAX_BAR_WIDTH, MAX_BAR_HEIGHT);

    private Widget newBar(int style, MeasurementSet measurementSet, Value cValue) {
        double max;
        double min;
        if (cVariable != null) {
            max = cValue.getMax();
            min = cValue.getMin();
        } else {
            max = this.maxMap.get(selectedType);
            min = this.minMap.get(selectedType);
        }

        if (logarithmic) {
            min = Math.log(min);
            max = Math.log(max);
            if (Math.abs(min - max) > 1.0E-6) {
                measurementSet = translate(useRawMap.get(selectedType), log(measurementSet),
                        new LinearTranslation(min, 1, max, 100));
                max = 100;
            }
        }

        return boxPlot.create(style, max, measurementSet, useRawMap.get(selectedType));
    }

    private double getRatio(double measurement, Value value) {
        double referencePoint = value != null ? value.getReferencePoint() : referencePointMap.get(selectedType);
        return measurement / referencePoint;
    }

    /**
     * A clickable label displaying something like "50%".
     */
    private Widget newPercentOfReferencePointLabel(final double measurement, final Value value) {
        final Anchor result = new Anchor(percentFormat.format(getRatio(measurement, value)));
        result.setStyleName("subtleLink");
        result.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                if (value != null) {
                    value.setReferencePoint(measurement);
                } else {
                    referencePointMap.put(selectedType, measurement);
                }
                rebuildResultsTable();
            }
        });
        return result;
    }

    public void rebuildVariablesTable() {
        FlexTable table = new FlexTable();
        table.setStyleName("data");

        int r = 0;
        for (Variable variable : variables) {
            table.insertRow(r);

            table.addCell(r);
            table.getRowFormatter().setStyleName(r, r % 2 == 0 ? "evenRow" : "oddrow");
            table.getCellFormatter().setStyleName(r, 0, "fixedParameterKey");
            table.setWidget(r, 0, new Label(variable.getName()));

            table.addCell(r);
            FlowPanel checkBoxes = new FlowPanel();
            if (variable.hasMultipleValues()) {
                for (Value value : variable.getValues()) {
                    checkBoxes.add(newShownCheckbox(value));
                }
            } else {
                checkBoxes.add(new Label(variable.getOnlyValue().getName()));
            }
            table.setWidget(r, 1, checkBoxes);

            r++;
        }

        fixedVariablesDiv.clear();
        fixedVariablesDiv.add(table);
    }

    public void rebuildRunsTable() {
        int columns = 4;
        if (editable) {
            columns += 1;
        }

        Grid table = new Grid(runMetas.size() + 1, columns);
        table.setStyleName("data");

        int r = 0;
        table.getRowFormatter().setStyleName(r, "evenRow");
        table.getCellFormatter().setStyleName(r, 1, "parameterKey");
        table.setWidget(r, 1, new Label("run"));
        table.getCellFormatter().setStyleName(r, 2, "parameterKey");
        table.setWidget(r, 2, new Label("executed"));
        table.getCellFormatter().setStyleName(r, 3, "parameterKey");
        table.setWidget(r, 3, new Label("environment"));
        r++;

        AsyncCallback<Void> runNameEditorCallback = new AsyncCallback<Void>() {
            public void onFailure(Throwable throwable) {
                // do nothing
            }

            public void onSuccess(Void unused) {
                rebuildResultsTable();
                rebuildVariablesTable();
            }
        };

        DateTimeFormat format = DateTimeFormat.getFormat("EEE MMM dd HH:mm:ss Z yyyy");
        for (RunMeta runMeta : runMetas) {
            Run run = runMeta.getRun();

            table.getRowFormatter().setStyleName(r, r % 2 == 0 ? "evenRow" : "oddRow");

            Anchor nameEditorLink = new Anchor("");
            nameEditorLink.setStyleName("labelField");

            table.setWidget(r, 0, swatch(runMeta));
            table.setWidget(r, 1,
                    new RunNameEditor(runMeta, nameEditorLink, new Label(""), editable, runNameEditorCallback)
                            .getWidget());
            table.setWidget(r, 2, new Label(format.format(run.getExecutedTimestamp())));
            table.setWidget(r, 3, newEnvironmentWidget(runMeta));
            if (editable) {
                table.setWidget(r, 4, new RunDeletedEditor(runMeta).getWidget());
            }

            r++;
        }

        runsDiv.clear();
        runsDiv.add(table);
    }

    private Widget newEnvironmentWidget(RunMeta runMeta) {
        Widget environmentWidget;
        EnvironmentMeta environmentMeta = runMeta.getEnvironmentMeta();
        if (environmentMeta == null) {
            Label noEnvironmentLabel = new Label("none");
            noEnvironmentLabel.setStyleName("placeholder");
            environmentWidget = noEnvironmentLabel;
        } else {
            Anchor environmentAnchor = new Anchor(environmentMeta.getName(), false, "#environments");
            environmentAnchor.setStyleName("subtlelink");
            environmentWidget = environmentAnchor;
        }
        return environmentWidget;
    }

    private Widget swatch(RunMeta runMeta) {
        Color[] colors = Colors.forStyle(runMeta.getStyle());

        FlowPanel result = new FlowPanel();
        InlineLabel dark = new InlineLabel("....");
        dark.getElement().setAttribute("style", "background-color: " + colors[0] + "; color: " + colors[0]);
        InlineLabel medium = new InlineLabel(".");
        medium.getElement().setAttribute("style", "background-color: " + colors[1] + "; color: " + colors[1]);
        InlineLabel light = new InlineLabel(".");
        light.getElement().setAttribute("style", "background-color: " + colors[2] + "; color: " + colors[2]);

        result.add(dark);
        result.add(medium);
        result.add(light);
        return result;
    }

    private CheckBox newShownCheckbox(final Value value) {
        CheckBox isShown = new CheckBox(value.getLabel());
        isShown.setValue(value.isShown());

        isShown.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
            public void onValueChange(ValueChangeEvent<Boolean> event) {
                value.setShown(!value.isShown());
                rebuildIndex();
                rebuildValueIndices();
                rebuildCValues();
                rebuildResultsTable();
                if (editable) {
                    Map<String, Map<String, Boolean>> variableValuesShown = variableValuesShown();
                    BenchmarkServiceAsync benchmarkService = GWT.create(BenchmarkService.class);
                    benchmarkService.setVariableValuesShown(benchmarkOwner, benchmarkName, variableValuesShown,
                            NO_OP_CALLBACK);
                }
            }
        });

        return isShown;
    }

    private Map<String, Map<String, Boolean>> variableValuesShown() {
        Map<String, Map<String, Boolean>> variableValuesShown = new HashMap<String, Map<String, Boolean>>();

        for (Variable variable : variables) {
            Map<String, Boolean> valuesShown = new HashMap<String, Boolean>();
            for (Value value : variable.getValues()) {
                valuesShown.put(value.getName(), value.isShown());
            }
            variableValuesShown.put(variable.getName(), valuesShown);
        }
        return variableValuesShown;
    }

    private static class Datapoint {
        final ScenarioResult scenarioResults;
        final int style;

        private Datapoint(ScenarioResult scenarioResults, int style) {
            this.scenarioResults = scenarioResults;
            this.style = style;
        }
    }

    private boolean isSnapshot() {
        return snapshotId != -1;
    }
}