/** * Copyright (c) 2014 Richard Warburton (richard.warburton@gmail.com) * <p> * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * <p> * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * <p> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. **/ package com.insightfullogic.honest_profiler.ports.javafx.view; import static com.insightfullogic.honest_profiler.ports.javafx.view.Rendering.renderMethod; import static com.insightfullogic.honest_profiler.ports.javafx.view.Rendering.renderShortMethod; import static javafx.application.Platform.runLater; import java.util.ArrayList; import java.util.List; import java.util.Optional; import com.insightfullogic.honest_profiler.core.parser.Method; import com.insightfullogic.honest_profiler.core.profiles.FlameGraph; import com.insightfullogic.honest_profiler.core.profiles.FlameGraphListener; import com.insightfullogic.honest_profiler.core.profiles.FlameTrace; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Window; // TODO: remove any flame graph calculation logic from the canvas. public class FlameGraphCanvas extends Canvas implements FlameGraphListener { private static final Color START_COLOR = Color.BISQUE.deriveColor(0, 1.2, 1.0, 1.0); private static final int TEXT_WIDTH = 7; private static final int ROW_WRAP = 4; private final Tooltip tooltip = new Tooltip(); private Window window; private double rowHeight; private List<FlameTrace> traces; private List<MethodLocation> methodLocations; private FlameGraph graph; public FlameGraphCanvas() { setOnMouseMoved(this::displayMethodName); } private void displayMethodName(final MouseEvent mouseEvent) { final double x = mouseEvent.getX(); final double y = mouseEvent.getY(); final Optional<MethodLocation> methodLocation = methodLocations .stream() .filter(location -> location.contains(x, y)) .findFirst(); if (methodLocation.isPresent()) { tooltip.setText(renderMethod(methodLocation.get().getMethod())); tooltip.show(window, x, y); } else { tooltip.hide(); } } public void refresh() { if (graph != null) { runLater(() -> accept(graph)); } } @Override public void accept(final FlameGraph graph) { this.graph = graph; final Scene scene = getScene(); if (scene != null) { setOpacity(1.0); if (window == null) { window = scene.getWindow(); } methodLocations = new ArrayList<>(); final GraphicsContext graphics = getGraphicsContext2D(); // graphics.clearRect(); // graphics.setFill(Color.GRAY); graphics.clearRect(0, 0, getWidth(), getHeight()); graphics.setStroke(Color.WHITE); traces = graph.getTraces(); final long totalWeight = graph.totalWeight(); final int maxHeight = graph.maxTraceHeight(); final double columnWidth = getWidth() / totalWeight; rowHeight = getHeight() / maxHeight; final double initialY = getHeight() - rowHeight; for (int row = 0; row < maxHeight; row++) { double y = initialY - (row * rowHeight); final Color colour = colorAt(row); for (int col = 0; col < traces.size();) { FlameTrace stack = traces.get(col); final double stackWidth = stack.getWeight() * columnWidth; Method method = stack.at(row); final int numberOfConsecutiveTraces = numberOfConsecutiveTracesWith( method, col, row); double methodWidth = stackWidth * numberOfConsecutiveTraces; if (method != null) { final double x = col * stackWidth; graphics.setFill(colour); graphics.fillRect(x, y, methodWidth, rowHeight); methodLocations.add( new MethodLocation( new Rectangle(x, y, methodWidth, rowHeight), method)); final String title = renderShortMethod(method); if (!renderText(graphics, x, y, methodWidth, title)) { renderText(graphics, x, y, methodWidth, method.getMethodName()); } } col += numberOfConsecutiveTraces; } } } } private Color colorAt(final int row) { return START_COLOR.deriveColor(0, 1.15 * (1 + row % ROW_WRAP), 1.0, 1.0); } private boolean renderText(final GraphicsContext graphics, final double x, final double y, final double methodWidth, final String title) { if (title.length() * TEXT_WIDTH < methodWidth) { graphics.setFill(Color.ROYALBLUE); graphics.fillText(title, x, y + 0.75 * rowHeight); return true; } return false; } private int numberOfConsecutiveTracesWith( final Method method, final int initialCol, final int row) { int col = initialCol; while (col < traces.size() && traces.get(col).at(row) == method) { col++; } return col - initialCol; } public Tooltip getTooltip() { return tooltip; } }