Java tutorial
/* * Copyright 2010 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.speedtracer.client.visualizations.view; import com.google.gwt.coreext.client.JSOArray; import com.google.gwt.coreext.client.JsIntegerDoubleMap; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style; import com.google.gwt.graphics.client.Canvas; import com.google.gwt.graphics.client.Color; import com.google.gwt.graphics.client.ImageHandle; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.speedtracer.client.model.UiEvent; /** * Class used to position and style the event graph bar UI that lives next to * the Event Trace Tree. */ public class EventTraceBreakdown { /** * Css selectors. */ public interface Css extends CssResource { int borderThickness(); String eventGraph(); String eventGraphGuides(); int itemMargin(); int listMargin(); int masterHeight(); String masterRender(); int sideMargins(); int widgetWidth(); } /** * Overlay type that represents the {@link Element} associated the guide lines * for the event bar graph. */ public static class EventTraceGraphGuides extends Element { protected EventTraceGraphGuides() { } } /** * Allows clients to control the appearance and presentation of events in the * breakdown graphs. */ public interface Presenter { /** * Gets the color to use when rendering this event in a time breakdown. * * @param event * @return */ Color getColor(UiEvent event); /** * Get an event's dominant color. For insignifcant events that do not have a * dominant color and signifcant events, implementations should return * <code>null</code>. A dominant color is a color that replaces an * insignificant event's primary color when rendering an event breakdown. * * NOTE: {@link Presenter#hasDominantType(UiEvent, UiEvent, double)} will * always be called on an event prior to this method, so it is recommended * that any computation for calculating the dominant color be done there. * * @param event * @return */ Color getDominantTypeColor(UiEvent event); /** * Gets the theshold, in milliseconds, that determines if events are * considered insignificant. <code>msPerPixel</code> is provided to allow * implementers to base signifcance on pixel resolution. * * @param msPerPixel the number of milliseconds in each pixel used to render * the event breakdown * @return */ double getInsignificanceThreshold(double msPerPixel); /** * Indicates whether an event contains a dominant type. This method will * only be called on events where the duration is less than * {@link Presenter#getInsignificanceThreshold(double)}. This method will * always be called before {@link Presenter#getDominantTypeColor(UiEvent)} * and should generally be used to carry out the computation needed to * determine the dominant type. * * @see Presenter#getDominantTypeColor(UiEvent) * * @param event * @param rootEvent the top-level event that contains this event * @param msPerPixel the number of milliseconds in each pixel * @return */ boolean hasDominantType(UiEvent event, UiEvent rootEvent, double msPerPixel); } /** * Capable of updating the canvas rendering for a {@link UiEvent} inside of an * event tree. */ public class Renderer { private final Canvas canvas; private final UiEvent event; private Renderer(Canvas canvas, UiEvent event) { this.event = event; this.canvas = canvas; } public Element getElement() { return canvas.getElement(); } /** * Renders only time spent in self leaving gaps where time was spent in * children. */ public void renderOnlySelf() { final double domainToCoords = canvas.getCoordWidth() / event.getDuration(); canvas.clear(); canvas.setFillStyle(presenter.getColor(event)); canvas.fillRect(0, 0, canvas.getCoordWidth(), canvas.getCoordHeight()); // now cut out the immediate children. JSOArray<UiEvent> children = event.getChildren(); for (int i = 0, n = children.size(); i < n; i++) { // The simple act of getting children takes a long time. That is crazy. UiEvent child = children.get(i); double startX = (child.getTime() - event.getTime()) * domainToCoords; canvas.clearRect(startX, 0, domainToCoords * child.getDuration(), canvas.getCoordWidth()); } } /** * Renders time in self and overlays time spent in children. */ public void renderSelfAndChildren() { canvas.clear(); final Color dominantColor = presenter.getDominantTypeColor(event); // If this node has a dominant color set, then it is one of several // important ones... show it with a 1 pixel bar. if (dominantColor != null) { // Simple fill with this color. canvas.setFillStyle(dominantColor); canvas.fillRect(0, 0, canvas.getCoordWidth(), canvas.getCoordHeight()); } else { double sx = (event.getTime() - rootEvent.getTime()) * masterDomainToCoords; assert sx >= 0 : "An event starts before it's containing event."; double sw = event.getDuration() * masterDomainToCoords; // Prevent exception due to rounding errors that slightly exceed // MASTER_COORD_WIDTH sw = Math.min(sw, MASTER_COORD_WIDTH); // Calling drawImage with a width of exactly 0 throws an exception in // JavaScript. No need to even make the draw call in this situation. // This is a defensive guard since we have guarantees earlier on that // this will be non-zero. But leave it in just in case. if (sw > 0) { canvas.drawImage(getMasterImageHandle(), sx, 0, sw, COORD_HEIGHT, 0, 0, canvas.getCoordWidth(), COORD_HEIGHT); } } } } /** * Externalized resource interface for accessing Css. */ public interface Resources extends ClientBundle { @Source("resources/EventTraceBreakdown.css") Css eventTraceBreakdownCss(); } private static final int COORD_HEIGHT = 10; private static final int MASTER_COORD_WIDTH = 1000; // conversion factor for converting from domain space to pixel space. private final double domainToPixels; private final double insignificanceThreshold; private final double masterDomainToCoords; private final Resources resources; private final UiEvent rootEvent; private final Presenter presenter; /** * This element is used to display the full rendering for the rootEvent and * all its children. It is also used as the master copy so that * {@link Renderer}s can sample their region to avoid doing a full redraw. */ private Element masterCanvasElement; /** * Constructor. * * @param rootEvent the root event of the tree for which we are showing the * breakdown bar graphs. * @param resources the {@link EventTraceBreakdown.Resources} for the * breakdown. */ public EventTraceBreakdown(UiEvent rootEvent, Presenter presenter, EventTraceBreakdown.Resources resources) { this.presenter = presenter; this.resources = resources; this.rootEvent = rootEvent; domainToPixels = resources.eventTraceBreakdownCss().widgetWidth() / rootEvent.getDuration(); masterDomainToCoords = MASTER_COORD_WIDTH / ((rootEvent.getDuration() == 0) ? 1 : rootEvent.getDuration()); insignificanceThreshold = presenter.getInsignificanceThreshold(domainToPixels); } public Element cloneRenderedCanvasElement() { ensureMasterIsRendered(); int width = (int) (rootEvent.getDuration() * domainToPixels); final Canvas canvas = new Canvas(width, COORD_HEIGHT); canvas.setLineWidth(2); final Element element = canvas.getElement(); element.setClassName(resources.eventTraceBreakdownCss().masterRender()); element.getStyle().setPropertyPx("width", width); new Renderer(canvas, rootEvent).renderSelfAndChildren(); return element; } /** * Creates a {@link EventTraceGraphGuides} and sets its and position. * * @param parentElement the {@link Element} that this new guide will attach * to. * @param event the {@link UiEvent} associate with the node that this guide * represents. * @param nodeDepth the depth in the event trace tree for the associate node. * @return returns the newly created {@link EventTraceGraphGuides}. */ public EventTraceGraphGuides createBarGraphGuides(Element parentElement, UiEvent event, int nodeDepth) { Css css = resources.eventTraceBreakdownCss(); EventTraceGraphGuides barGuides = parentElement.getOwnerDocument().createDivElement().cast(); barGuides.setClassName(css.eventGraphGuides()); int leftOffset = getLeftOffset(event, nodeDepth); // the guides are attached as a child of the <ul> element. So we have // to also account for the border and margin for the list that contains us. leftOffset -= (css.listMargin() + css.borderThickness()); int width = getPixelWidth(event.getDuration()); // Set positioning information. barGuides.getStyle().setPropertyPx("left", leftOffset); barGuides.getStyle().setPropertyPx("width", width); parentElement.appendChild(barGuides); return barGuides; } public Renderer createRenderer(UiEvent event, int depth) { ensureMasterIsRendered(); int width = (int) (event.getDuration() * domainToPixels); // We may have truncated something that, on aggregate may matter. // If this node has a dominant color set, then it contains a child that is // one of the important ones... show it with a 1 pixel bar. if (width == 0 && presenter.hasDominantType(event, rootEvent, domainToPixels)) { width = 1; } Css css = resources.eventTraceBreakdownCss(); final Canvas canvas = new Canvas(width, COORD_HEIGHT); canvas.setLineWidth(2); final Element element = canvas.getElement(); final Style style = element.getStyle(); element.setClassName(css.eventGraph()); style.setPropertyPx("left", getLeftOffset(event, depth)); style.setPropertyPx("width", width); return new Renderer(canvas, event); } public Element getRenderedCanvasElement() { ensureMasterIsRendered(); return masterCanvasElement; } private void ensureMasterIsRendered() { if (masterCanvasElement != null) { return; } // See comment in renderNode. final JsIntegerDoubleMap accumlatedErrorByType = JsIntegerDoubleMap.create(); final Canvas canvas = new Canvas(MASTER_COORD_WIDTH, COORD_HEIGHT); traverseAndRender(canvas, null, rootEvent, accumlatedErrorByType); masterCanvasElement = canvas.getElement(); } /** * Returns the offset in pixels from the left of the EventTraceBreakdown * widget that we want to place an element. The offset is determined by how * deep a node is in the tree, and the start time of the node in the tree. * * We use negative positioning absolute positioning. So we also have to * account for margins and border thickness to get the things lined up. */ private int getLeftOffset(UiEvent event, int nodeDepth) { Css css = resources.eventTraceBreakdownCss(); // How far should the bar be offset from the left? double domainOffset = event.getTime() - rootEvent.getTime(); int offsetPixels = (css.widgetWidth() + css.sideMargins()) - (int) (domainOffset * domainToPixels); // include margins and borders offsetPixels += (nodeDepth * (css.listMargin() + css.itemMargin() + css.borderThickness())); return -offsetPixels; } private ImageHandle getMasterImageHandle() { return masterCanvasElement.cast(); } private int getPixelWidth(double duration) { return (int) Math.max(1, domainToPixels * duration); } private void renderNode(Canvas canvas, UiEvent parent, UiEvent node, JsIntegerDoubleMap accumulatedErrorByType) { double startX = (node.getTime() - rootEvent.getTime()) * masterDomainToCoords; double width = node.getDuration() * masterDomainToCoords; final int nodeType = node.getType(); // Insignificance is tricky. If we have lots of insignificant things, they // can add up to a significant thing. if (node.getDuration() < insignificanceThreshold) { // When the sub-pixel strokes are composited, we get a misleading color // blend. We use a unique aliasing scheme here where we suppress short // duration events but keep up with the total time we've suppressed for // each suppressed type. If the total suppressed time for a type ends up // being significant, we will synthesize a single aggregate event to // correct our accounting. double correctedTime = node.getSelfTime(); if (accumulatedErrorByType.hasKey(nodeType)) { correctedTime += accumulatedErrorByType.get(nodeType); } if (correctedTime < insignificanceThreshold) { accumulatedErrorByType.put(nodeType, correctedTime); return; } // We want to draw a discrete bar. width = insignificanceThreshold * masterDomainToCoords; // Reset the type specific aggregation. accumulatedErrorByType.put(nodeType, 0); } canvas.setFillStyle(presenter.getColor(node)); canvas.fillRect(startX, 0, width, COORD_HEIGHT); } /** * Should render back to front using a simple pre-order traversal. * * @param node the current node in the traversal */ private void traverseAndRender(Canvas canvas, UiEvent prev, UiEvent node, JsIntegerDoubleMap accumulatedErrorByType) { renderNode(canvas, prev, node, accumulatedErrorByType); JSOArray<UiEvent> children = node.getChildren(); for (int i = 0, n = children.size(); i < n; i++) { traverseAndRender(canvas, node, children.get(i), accumulatedErrorByType); } } }