/* * Copyright 2011 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.gwt.reference.microbenchmark.client; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Cookies; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.Window.ClosingEvent; import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; import java.util.Collections; import java.util.Date; import java.util.List; /** * An implementation of {@link Microbenchmark} that surveys multiple timed * tests. */ public class MicrobenchmarkSurvey implements Microbenchmark { /** * A single runnable test that makes up the survey. */ static abstract class NanoTest { private final String name; /** * Construct a new {@link NanoTest}. * * @param name the display name */ public NanoTest(String name) { this.name = name; } public String getName() { return name; } /** * Get the widget to display in a popup when the user clicks on the test * name. * * @return the popup widget, or null not to show one */ public Widget getPopup() { return null; } /** * Run the test. */ public abstract void runTest(); /** * Setup the test before starting the timer. Override this method to prepare * the test before it starts running. */ public void setup() { // No-op by default. } /** * Tear down the test after stopping the timer. Override this method to * cleanup the test after it completes. */ public void teardown() { // No-op by default. } } /** * A nano test that makes a widget and attaches it to the {@link RootPanel}. */ static abstract class WidgetMaker extends NanoTest { private final RootPanel root = RootPanel.get(); private Widget popupWidget; private Widget w; public WidgetMaker(String name) { super(name); } @Override public Widget getPopup() { if (popupWidget == null) { popupWidget = make(); } return popupWidget; } @Override public void runTest() { w = make(); root.add(w); /* * Force a layout by finding the body's offsetTop and height. We avoid * doing setTimeout(0), which would allow paint to happen, to keep the * test synchronous and because different browsers round that zero to * different minimums. Layout should be the bulk of the time. */ Document.get().getBody().getOffsetTop(); Document.get().getBody().getOffsetHeight(); w.getOffsetHeight(); } @Override public void teardown() { // Clean up to keep the dom. Attached widgets will affect later tests. root.remove(w); } /** * Make the widget to test. * * @return the widget */ protected abstract Widget make(); } /** * A nano test that updates an existing widget that is already attached to the * {@link RootPanel}. * * @param <W> the widget type */ static abstract class WidgetUpdater<W extends Widget> extends MicrobenchmarkSurvey.NanoTest { private final RootPanel root = RootPanel.get(); private W w; public WidgetUpdater(String name) { super(name); } @Override public Widget getPopup() { return ensureWidget(); } @Override public void setup() { root.add(ensureWidget()); } @Override public void runTest() { updateWidget(w); } @Override public void teardown() { root.remove(w); } /** * Make the widget to test. * * @return the widget */ protected abstract W make(); /** * Update the widget. * * @param w the widget to update */ protected abstract void updateWidget(W w); private W ensureWidget() { if (w == null) { w = make(); } return w; } } interface Binder extends UiBinder<Widget, MicrobenchmarkSurvey> { } private static final Binder BINDER = GWT.create(Binder.class); private static final String COOKIE = "gwt_microb_survey"; private static final int DEFAULT_INSTANCES = 100; public static native void log(String msg) /*-{ var logger = $wnd.console; if (logger) { logger.log(msg); if (logger.markTimeline) { logger.markTimeline(msg); } } }-*/; @UiField(provided = true) Grid grid; @UiField CheckBox includeLargeWidget; @UiField TextBox number; @UiField Widget root; final String name; private final List<NanoTest> nanos; /** * Construct a new {@link MicrobenchmarkSurvey} micro benchmark. * * @param name the name of the benchmark * @param nanos the {@link NanoTest}s that make up the survey */ public MicrobenchmarkSurvey(String name, List<NanoTest> nanos) { this.name = name; this.nanos = Collections.unmodifiableList(nanos); int instances = DEFAULT_INSTANCES; try { instances = Integer.parseInt(Cookies.getCookie(COOKIE)); } catch (NumberFormatException ignored) { } // Initialize the grid. grid = new Grid(nanos.size() + 2, 3); grid.setText(0, 0, "median"); grid.setText(0, 1, "mean"); int row = 1; for (final NanoTest nano : nanos) { grid.setText(row, 0, "0"); grid.setText(row, 1, "0"); InlineLabel a = new InlineLabel(); a.setText(nano.getName()); a.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { Widget toDisplay = nano.getPopup(); if (toDisplay != null) { PopupPanel popup = new PopupPanel(true, true); ScrollPanel container = new ScrollPanel(toDisplay); container.setPixelSize(500, 500); popup.setWidget(container); popup.center(); } } }); // TODO: popup. grid.setWidget(row, 2, a); row++; } // Create the widget. root = BINDER.createAndBindUi(this); number.setVisibleLength(7); number.setValue("" + instances); number.addBlurHandler(new BlurHandler() { public void onBlur(BlurEvent event) { saveInstances(); } }); Window.addWindowClosingHandler(new ClosingHandler() { public void onWindowClosing(ClosingEvent event) { saveInstances(); } }); } public String getName() { return name; } public Widget getWidget() { return root; } public void run() { RootPanel root = RootPanel.get(); // Add a large widget to the root to reflect a typical application. FlowPanel largeWidget = null; if (includeLargeWidget.getValue()) { largeWidget = new FlowPanel(); TestWidgetBinder.Maker widgetMaker = new TestWidgetBinder.Maker(); for (int i = 0; i < 100; i++) { largeWidget.add(widgetMaker.make()); } root.add(largeWidget); } int nanosCount = nanos.size(); double[] times = new double[nanosCount]; int column = grid.getColumnCount(); grid.resizeColumns(column + 1); grid.setText(0, column, "Run " + (column - 3)); final int instances = getInstances(); boolean forward = false; for (int i = 0; i < instances; ++i) { forward = !forward; for (int m = 0; m < nanosCount; m++) { /* * Alternate the order that we invoke the makers to cancel out the * performance impact of adding elements to the DOM, which would cause * later tests to run more slowly than earlier tests. */ NanoTest nano = nanos.get(forward ? m : (nanosCount - 1 - m)); nano.setup(); // Execute the test. log(i + ": " + nano.name); double start = Duration.currentTimeMillis(); nano.runTest(); // Record the end time. double thisTime = Duration.currentTimeMillis() - start; times[m] += thisTime; // Cleanup after the test. nano.teardown(); } } // Record the times. double allTimes = 0; for (int m = 0; m < nanosCount; ++m) { record(m + 1, times[m]); allTimes += times[m]; } grid.setText(grid.getRowCount() - 1, grid.getColumnCount() - 1, Util.format(allTimes)); // Cleanup the dom. if (largeWidget != null) { root.remove(largeWidget); } } private int getInstances() { try { int instances = Integer.parseInt(number.getValue()); return instances; } catch (NumberFormatException ignored) { return 0; } } private void record(int row, double thisTime) { final int columns = grid.getColumnCount(); grid.setText(row, columns - 1, Util.format(thisTime)); double max = 0, min = 0, mean = 0; for (int column = 3; column < columns; column++) { double value = Double.parseDouble(grid.getText(row, column)); mean += value; max = Math.max(max, value); if (min == 0) { min = max; } else { min = Math.min(min, value); } } double range = max - min; double halfRange = range / 2; double median = min + halfRange; grid.setText(row, 0, Util.format(Util.roundToTens(median))); mean = mean / (columns - 3); grid.setText(row, 1, Util.format(Util.roundToTens(mean))); } @SuppressWarnings("deprecation") private void saveInstances() { String value = number.getValue(); Date expires = new Date(); expires.setYear(expires.getYear() + 3); Cookies.setCookie(COOKIE, value, expires); } }