com.google.speedtracer.client.visualizations.view.EventTraceBreakdown.java Source code

Java tutorial

Introduction

Here is the source code for com.google.speedtracer.client.visualizations.view.EventTraceBreakdown.java

Source

/*
 * 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);
        }
    }
}