/*
* 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.annotations.Nullable;
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.VmTraceData;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.swing.JComponent;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
/**
* A canvas that displays the call hierarchy for a single thread. The trace and the the thread to be
* displayed are specified using {@link #setTrace} and {@link #displayThread} methods.
*/
public class TraceViewCanvas extends JComponent {
private static final Color BACKGROUND_COLOR =
UIManager.getLookAndFeelDefaults().getColor("EditorPane.background");
private static final int TOOLTIP_OFFSET = 10;
/**
* The time unit to use for all operations. Changing this changes the minimum resolution
* that can be viewed on the canvas.
*/
private static final TimeUnit DEFAULT_TIME_UNITS = TimeUnit.NANOSECONDS;
/**
* Interactor that listens to mouse events, interprets them as zoom/pan events, and provides the
* resultant viewport transform.
*/
private final ZoomPanInteractor mZoomPanInteractor;
/** The viewport transform takes into account the current zoom and translation/pan values. */
private AffineTransform mViewPortTransform;
/** Inverse of {@link #mViewPortTransform}. */
private AffineTransform mViewPortInverseTransform;
private VmTraceData mTraceData;
private Call mTopLevelCall;
private RenderContext mRenderContext;
private TimeScaleRenderer mTimeScaleRenderer;
private CallHierarchyRenderer mCallHierarchyRenderer;
private final Point2D mTmpPoint = new Point2D.Double();
public TraceViewCanvas() {
mViewPortTransform = new AffineTransform();
mViewPortInverseTransform = new AffineTransform();
mZoomPanInteractor = new ZoomPanInteractor();
addMouseListener(mZoomPanInteractor);
addMouseMotionListener(mZoomPanInteractor);
addMouseWheelListener(mZoomPanInteractor);
mZoomPanInteractor.addViewTransformListener(new ZoomPanInteractor.ViewTransformListener() {
@Override
public void transformChanged(@NonNull AffineTransform transform) {
updateViewPortTransform(transform);
}
});
addMouseMotionListener(ToolTipManager.sharedInstance());
// Listen for the first hierarchy bounds change so as to get the initial width,
// and then zoom fit once we know the width.
addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorMoved(HierarchyEvent e) {
}
@Override
public void ancestorResized(HierarchyEvent e) {
removeHierarchyBoundsListener(this);
zoomFit();
}
});
}
public void setTrace(@NonNull VmTraceData traceData, @NonNull ThreadInfo thread,
ClockType renderClock) {
mTraceData = traceData;
mRenderContext = new RenderContext(traceData, renderClock);
displayThread(thread);
}
public void displayThread(@NonNull ThreadInfo thread) {
mCallHierarchyRenderer = null;
mTimeScaleRenderer = null;
if (mTraceData == null) {
return;
}
mTopLevelCall = thread.getTopLevelCall();
if (mTopLevelCall == null) {
return;
}
mTimeScaleRenderer = new TimeScaleRenderer(
mTopLevelCall.getEntryTime(ClockType.GLOBAL, DEFAULT_TIME_UNITS),
DEFAULT_TIME_UNITS);
int yOffset = mTimeScaleRenderer.getLayoutHeight();
mCallHierarchyRenderer = new CallHierarchyRenderer(mTraceData, thread, yOffset,
DEFAULT_TIME_UNITS, mRenderContext);
zoomFit();
}
public void setRenderClock(ClockType clock) {
mRenderContext.setRenderClock(clock);
repaint();
}
public void setUseInclusiveTimeForColorAssignment(boolean en) {
mRenderContext.setUseInclusiveTimeForColorAssignment(en);
repaint();
}
public void setHighlightMethods(@Nullable Set<MethodInfo> methods) {
mRenderContext.setHighlightedMethods(methods);
repaint();
}
public void zoomFit() {
if (mTopLevelCall == null) {
return;
}
long start = mTopLevelCall.getEntryTime(ClockType.GLOBAL, DEFAULT_TIME_UNITS);
long end = mTopLevelCall.getExitTime(ClockType.GLOBAL, DEFAULT_TIME_UNITS);
// Scale so that the full trace occupies 90% of the screen width.
double width = getWidth();
double sx = width * .9f / (end - start);
// Guard against trying to zoom when the component doesn't know its width yet. Width is
// usually 0 in such cases, but we just make it slightly general and check for width < 10.
if (width < 10) {
sx = Math.max(sx, 0.2);
}
// Initialize display so that the full trace is visible and takes up most of the view.
mZoomPanInteractor.setToScaleX(sx, 1); // make everything visible
mZoomPanInteractor.translateBy(50, 0); // shift over the start of the trace
updateViewPortTransform(mZoomPanInteractor.getTransform());
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
setRenderingHints(g2d);
// fill with background color
g2d.setColor(BACKGROUND_COLOR);
g2d.fillRect(0, 0, getWidth(), getHeight());
if (mTraceData == null) {
return;
}
// paint stack layout view
if (mCallHierarchyRenderer != null) {
mCallHierarchyRenderer.render(g2d, mViewPortTransform);
}
// paint timeline at top
if (mTimeScaleRenderer != null) {
mTimeScaleRenderer.paint(g2d, mViewPortTransform, getWidth());
}
}
private void setRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}
@Override
public String getToolTipText(MouseEvent event) {
if (mTraceData == null || mCallHierarchyRenderer == null) {
return null;
}
mTmpPoint.setLocation(event.getPoint());
mViewPortInverseTransform.transform(mTmpPoint, mTmpPoint);
return mCallHierarchyRenderer.getToolTipFor(mTmpPoint.getX(), mTmpPoint.getY());
}
@Override
public Point getToolTipLocation(MouseEvent event) {
return new Point(event.getX() + TOOLTIP_OFFSET, event.getY() + TOOLTIP_OFFSET);
}
private void updateViewPortTransform(AffineTransform tx) {
mViewPortTransform = new AffineTransform(tx);
try {
mViewPortInverseTransform = mViewPortTransform.createInverse();
} catch (NoninvertibleTransformException e) {
// This can't occur since we just do scale or pan, both of which are invertible
mViewPortInverseTransform = new AffineTransform();
}
repaint();
}
}