| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * 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.android.tools.perflib.vmtrace.viz; |
| |
| import com.android.annotations.NonNull; |
| import com.android.tools.perflib.vmtrace.Call; |
| import com.android.tools.perflib.vmtrace.ClockType; |
| import com.android.tools.perflib.vmtrace.MethodInfo; |
| import com.android.tools.perflib.vmtrace.ThreadInfo; |
| import com.android.tools.perflib.vmtrace.TimeSelector; |
| import com.android.tools.perflib.vmtrace.VmTraceData; |
| import com.android.utils.HtmlBuilder; |
| |
| import java.awt.*; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.text.DecimalFormat; |
| import java.util.Iterator; |
| import java.util.concurrent.TimeUnit; |
| |
| import static com.android.tools.perflib.vmtrace.ClockType.THREAD; |
| import static com.android.tools.perflib.vmtrace.ClockType.GLOBAL; |
| |
| /** Renders the call hierarchy rooted at a given call that is part of the trace. */ |
| public class CallHierarchyRenderer { |
| /** Height in pixels for a single call instance. Its length is proportional to its duration. */ |
| private static final int PER_LEVEL_HEIGHT_PX = 10; |
| private static final int PADDING = 1; |
| |
| private static final int TEXT_HEIGHT = 6; |
| private static final int TEXT_LEFT_PADDING = 5; |
| |
| private final VmTraceData mTraceData; |
| private final ThreadInfo mThread; |
| private final Call mTopCall; |
| private final int mYOffset; |
| private final TimeUnit mLayoutTimeUnits; |
| private final RenderContext mRenderContext; |
| |
| private final Rectangle2D mLayout = new Rectangle2D.Double(); |
| private final Point2D mTmpPoint1 = new Point2D.Double(); |
| private final Point2D mTmpPoint2 = new Point2D.Double(); |
| |
| private Font mFont; |
| |
| public CallHierarchyRenderer(@NonNull VmTraceData vmTraceData, @NonNull ThreadInfo thread, |
| int yOffset, TimeUnit defaultTimeUnits, RenderContext renderContext) { |
| mTraceData = vmTraceData; |
| mThread = thread; |
| mTopCall = thread.getTopLevelCall(); |
| mYOffset = yOffset; |
| mLayoutTimeUnits = defaultTimeUnits; |
| mRenderContext = renderContext; |
| } |
| |
| /** |
| * Renders the call hierarchy on a given graphics context. |
| * This essentially iterates through every single call in the hierarchy and renders it if it is |
| * visible in the current viewport. |
| */ |
| public void render(Graphics2D g, AffineTransform viewPortTransform) { |
| Rectangle clip = g.getClipBounds(); |
| |
| Iterator<Call> it = mTopCall.getCallHierarchyIterator(); |
| while (it.hasNext()) { |
| Call c = it.next(); |
| |
| // obtain layout in item space |
| fillLayoutBounds(c, mLayout); |
| |
| // transform based on the current viewport (scale + translate) |
| transformRect(viewPortTransform, mLayout); |
| |
| // no need to render if it is is not in the current viewport. |
| if (!clip.intersects(mLayout)) { |
| continue; |
| } |
| |
| // no need to render if it is too small (arbitrarily assumed to be < 1 px wide) |
| if (mLayout.getWidth() < 1) { |
| continue; |
| } |
| |
| // obtain the fill color based on its importance |
| Color fillColor = mRenderContext.getFillColor(c, mThread); |
| g.setColor(fillColor); |
| g.fill(mLayout); |
| |
| // paint its name within the rectangle if possible |
| String name = getName(c); |
| drawString(g, name, mLayout, mRenderContext.getFontColor(c, mThread)); |
| } |
| } |
| |
| private Rectangle2D transformRect(AffineTransform viewPortTransform, Rectangle2D rect) { |
| mTmpPoint1.setLocation(rect.getX(), rect.getY()); |
| mTmpPoint2.setLocation(rect.getWidth(), rect.getHeight()); |
| |
| viewPortTransform.transform(mTmpPoint1, mTmpPoint1); |
| viewPortTransform.deltaTransform(mTmpPoint2, mTmpPoint2); |
| |
| rect.setRect(mTmpPoint1.getX(), |
| mTmpPoint1.getY(), |
| mTmpPoint2.getX(), |
| mTmpPoint2.getY()); |
| return rect; |
| } |
| |
| private void drawString(Graphics2D g, String name, Rectangle2D bounds, Color fontColor) { |
| if (mFont == null) { |
| mFont = g.getFont().deriveFont(8.0f); |
| } |
| g.setFont(mFont); |
| g.setColor(fontColor); |
| |
| AffineTransform origTx = g.getTransform(); |
| |
| mTmpPoint1.setLocation(bounds.getX() + TEXT_LEFT_PADDING, bounds.getY() + TEXT_HEIGHT); |
| |
| double availableWidth = g.getTransform().getScaleX() * bounds.getWidth(); |
| |
| // When drawing a string, we want its location to be transformed by the current viewport |
| // transform, but not the text itself (we don't want it zoomed out or in). |
| origTx.transform(mTmpPoint1, mTmpPoint1); |
| g.setTransform(new AffineTransform()); |
| |
| double stringWidth = g.getFontMetrics().stringWidth(name); |
| if (availableWidth > stringWidth) { |
| g.drawString(name, (float) mTmpPoint1.getX(), (float) mTmpPoint1.getY()); |
| } |
| |
| g.setTransform(origTx); |
| } |
| |
| /** Fills the layout bounds corresponding to a given call in the given Rectangle object. */ |
| private void fillLayoutBounds(Call c, Rectangle2D layoutBounds) { |
| ClockType renderClock = mRenderContext.getRenderClock(); |
| double x = c.getEntryTime(renderClock, mLayoutTimeUnits) |
| - mTopCall.getEntryTime(renderClock, mLayoutTimeUnits) |
| + PADDING; |
| double y = c.getDepth() * PER_LEVEL_HEIGHT_PX + mYOffset + PADDING; |
| double width = c.getInclusiveTime(renderClock, mLayoutTimeUnits) - 2 * PADDING; |
| double height = PER_LEVEL_HEIGHT_PX - 2 * PADDING; |
| layoutBounds.setRect(x, y, width, height); |
| } |
| |
| /** Get the tooltip corresponding to given location (in item coordinates). */ |
| public String getToolTipFor(double x, double y) { |
| Iterator<Call> it = mTopCall.getCallHierarchyIterator(); |
| while (it.hasNext()) { |
| Call c = it.next(); |
| |
| fillLayoutBounds(c, mLayout); |
| if (mLayout.contains(x, y)) { |
| return formatToolTip(c); |
| } |
| } |
| |
| return null; |
| } |
| |
| private static final DecimalFormat PERCENTAGE_FORMATTER = new DecimalFormat("#.##"); |
| |
| private String formatToolTip(Call c) { |
| HtmlBuilder htmlBuilder = new HtmlBuilder(); |
| htmlBuilder.openHtmlBody(); |
| |
| htmlBuilder.addHeading(getMethodInfo(c).getFullName(), "black"); |
| |
| long span = c.getExitTime(GLOBAL, TimeUnit.NANOSECONDS) - |
| c.getEntryTime(GLOBAL, TimeUnit.NANOSECONDS); |
| TimeUnit unit = TimeUnit.NANOSECONDS; |
| String entryGlobal = TimeUtils.makeHumanReadable(c.getEntryTime(GLOBAL, unit), span, unit); |
| String entryThread = TimeUtils.makeHumanReadable(c.getEntryTime(THREAD, unit), span, unit); |
| String exitGlobal = TimeUtils.makeHumanReadable(c.getExitTime(GLOBAL, unit), span, unit); |
| String exitThread = TimeUtils.makeHumanReadable(c.getExitTime(THREAD, unit), span, unit); |
| String durationGlobal = TimeUtils.makeHumanReadable( |
| c.getExitTime(GLOBAL, unit) - c.getEntryTime(GLOBAL, unit), span, unit); |
| String durationThread = TimeUtils.makeHumanReadable( |
| c.getExitTime(THREAD, unit) - c.getEntryTime(THREAD, unit), span, unit); |
| |
| htmlBuilder.beginTable(); |
| htmlBuilder.addTableRow("Wallclock Time:", durationGlobal, |
| String.format("(from %s to %s)", entryGlobal, exitGlobal)); |
| htmlBuilder.addTableRow("CPU Time:", durationThread, |
| String.format("(from %s to %s)", entryThread, exitThread)); |
| htmlBuilder.endTable(); |
| |
| htmlBuilder.newline(); |
| htmlBuilder.add("Inclusive Time: "); |
| htmlBuilder.beginBold(); |
| double inclusivePercentage = mTraceData.getDurationPercentage(c, mThread, |
| mRenderContext.getRenderClock(), true /* use inclusive time */); |
| htmlBuilder.add(PERCENTAGE_FORMATTER.format(inclusivePercentage)); |
| htmlBuilder.add("%"); |
| htmlBuilder.endBold(); |
| |
| htmlBuilder.newline(); |
| htmlBuilder.add("Exclusive Time: "); |
| htmlBuilder.beginBold(); |
| double exclusivePercentage = mTraceData.getDurationPercentage(c, mThread, |
| mRenderContext.getRenderClock(), false /* don't use inclusive time */); |
| htmlBuilder.add(PERCENTAGE_FORMATTER.format(exclusivePercentage)); |
| htmlBuilder.add("%"); |
| htmlBuilder.endBold(); |
| |
| htmlBuilder.closeHtmlBody(); |
| return htmlBuilder.getHtml(); |
| } |
| |
| @NonNull |
| private String getName(@NonNull Call c) { |
| return getMethodInfo(c).getShortName(); |
| } |
| |
| private MethodInfo getMethodInfo(@NonNull Call c) { |
| long methodId = c.getMethodId(); |
| return mTraceData.getMethod(methodId); |
| } |
| } |