From 916944ea8a8452a8df1b90d66c845da568f70e43 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 14:27:52 +0200 Subject: [PATCH 01/23] chore: Replace render config by setFrameRender --- .../bric3/fireplace/ui/FlamegraphPane.kt | 45 ++++++++++--------- .../fireplace/flamegraph/FlamegraphView.java | 10 +++-- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt index 96f2012..3a323c9 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt @@ -15,6 +15,7 @@ import io.github.bric3.fireplace.core.ui.Colors.Palette import io.github.bric3.fireplace.core.ui.LightDarkColor import io.github.bric3.fireplace.core.ui.SwingUtils import io.github.bric3.fireplace.flamegraph.ColorMapper +import io.github.bric3.fireplace.flamegraph.DefaultFrameRenderer import io.github.bric3.fireplace.flamegraph.DimmingFrameColorProvider import io.github.bric3.fireplace.flamegraph.FlamegraphView import io.github.bric3.fireplace.flamegraph.FlamegraphView.HoverListener @@ -245,28 +246,30 @@ class FlamegraphPane : JPanel(BorderLayout()) { private const val defaultIcicleMode = true private fun getJfrFlamegraphView(): FlamegraphView { val flamegraphView = FlamegraphView() - flamegraphView.setRenderConfiguration( - FrameTextsProvider.of( - Function { frame -> if (frame.isRoot) "root" else frame.actualNode.frame.humanReadableShortString }, - Function { frame -> - if (frame.isRoot) "" else FormatToolkit.getHumanReadable( - frame.actualNode.frame.method, - false, - false, - false, - false, - true, - false + flamegraphView.setFrameRender( + DefaultFrameRenderer( + FrameTextsProvider.of( + Function { frame -> if (frame.isRoot) "root" else frame.actualNode.frame.humanReadableShortString }, + Function { frame -> + if (frame.isRoot) "" else FormatToolkit.getHumanReadable( + frame.actualNode.frame.method, + false, + false, + false, + false, + true, + false + ) + }, + Function { frame -> if (frame.isRoot) "" else frame.actualNode.frame.method.methodName } + ), + DimmingFrameColorProvider( + defaultFrameColorMode.colorMapperUsing( + ColorMapper.ofObjectHashUsing(*defaultColorPalette.colors()) ) - }, - Function { frame -> if (frame.isRoot) "" else frame.actualNode.frame.method.methodName } - ), - DimmingFrameColorProvider( - defaultFrameColorMode.colorMapperUsing( - ColorMapper.ofObjectHashUsing(*defaultColorPalette.colors()) - ) - ), - FrameFontProvider.defaultFontProvider() + ), + FrameFontProvider.defaultFontProvider() + ) ) val ref = AtomicReference>() diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index ef612c0..87ff2a6 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -263,10 +263,12 @@ static Point getPointLeveledToFrameDepth(@NotNull MouseEvent mouseEvent, @NotNul public FlamegraphView() { canvas = new FlamegraphCanvas<>(this); // default configuration - setRenderConfiguration( - FrameTextsProvider.of(frameBox -> frameBox.actualNode.toString()), - FrameColorProvider.defaultColorProvider(f -> UIManager.getColor("Button.background")), - FrameFontProvider.defaultFontProvider() + setFrameRender( + new DefaultFrameRenderer<>( + FrameTextsProvider.of(frameBox -> frameBox.actualNode.toString()), + FrameColorProvider.defaultColorProvider(f -> UIManager.getColor("Button.background")), + FrameFontProvider.defaultFontProvider() + ) ); canvas.putClientProperty(OWNER_KEY, this); scrollPaneListener = new FlamegraphHoveringScrollPaneMouseListener<>(canvas); From a0cae793532ff40d2623a8136cac07f91e6e3e44 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 14:25:20 +0200 Subject: [PATCH 02/23] chore: tweak borders --- .../io/github/bric3/fireplace/ui/FlamegraphPane.kt | 4 ++++ .../io/github/bric3/fireplace/ui/ThreadFlamegraphView.kt | 9 ++++++++- .../bric3/fireplace/flamegraph/FlamegraphView.java | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt index 3a323c9..917ae3e 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt @@ -9,6 +9,7 @@ */ package io.github.bric3.fireplace.ui +import com.formdev.flatlaf.FlatClientProperties import io.github.bric3.fireplace.Utils import io.github.bric3.fireplace.core.ui.Colors import io.github.bric3.fireplace.core.ui.Colors.Palette @@ -115,6 +116,9 @@ class FlamegraphPane : JPanel(BorderLayout()) { addActionListener { jfrFlamegraphView.resetZoom() } } val searchField = JTextField("").apply { + putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, "Search") + putClientProperty( FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true) + addActionListener { val searched = text if (searched.isEmpty()) { diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/ThreadFlamegraphView.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/ThreadFlamegraphView.kt index 1491bdc..0fc33ce 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/ThreadFlamegraphView.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/ThreadFlamegraphView.kt @@ -26,6 +26,7 @@ import java.awt.event.MouseEvent import java.util.concurrent.CompletableFuture import java.util.function.Supplier import javax.swing.* +import javax.swing.border.EmptyBorder import kotlin.collections.Map.Entry abstract class ThreadFlamegraphView(protected val jfrBinder: JFRLoaderBinder) : ViewPanel { @@ -126,7 +127,13 @@ abstract class ThreadFlamegraphView(protected val jfrBinder: JFRLoaderBinder) : } ) - JSplitPane(JSplitPane.HORIZONTAL_SPLIT, JScrollPane(threadList), charts).apply { + JSplitPane( + JSplitPane.HORIZONTAL_SPLIT, + JScrollPane(threadList).apply { + border = EmptyBorder(0, 0, 0, 0) + }, + charts + ).apply { autoSize(0.2) } } diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 87ff2a6..472dcb9 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -16,6 +16,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; +import javax.swing.border.EmptyBorder; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import java.awt.*; @@ -303,7 +304,7 @@ public FlamegraphView() { }); component = wrap(layeredScrollPane, bg -> { - scrollPane.setBorder(null); + scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); scrollPane.setBackground(bg); scrollPane.getVerticalScrollBar().setBackground(bg); scrollPane.getHorizontalScrollBar().setBackground(bg); From de3a372a8907e449ae0fd723b29fdd8fb9bcf996 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 14:34:04 +0200 Subject: [PATCH 03/23] chore: Apply default config --- .../io/github/bric3/fireplace/ui/FlamegraphPane.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt index 917ae3e..9291b4c 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt @@ -117,7 +117,7 @@ class FlamegraphPane : JPanel(BorderLayout()) { } val searchField = JTextField("").apply { putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, "Search") - putClientProperty( FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true) + putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true) addActionListener { val searched = text @@ -245,9 +245,10 @@ class FlamegraphPane : JPanel(BorderLayout()) { companion object { private val defaultColorPalette = Colors.Palette.DATADOG private val defaultFrameColorMode = BY_PACKAGE - private const val defaultPaintFrameBorder = true + private const val defaultPaintHoveredFrameBorder = false private const val defaultShowMinimap = true private const val defaultIcicleMode = true + private const val defaultRoundedFrame = true private fun getJfrFlamegraphView(): FlamegraphView { val flamegraphView = FlamegraphView() flamegraphView.setFrameRender( @@ -273,7 +274,10 @@ class FlamegraphPane : JPanel(BorderLayout()) { ) ), FrameFontProvider.defaultFontProvider() - ) + ).apply { + isPaintHoveredFrameBorder = defaultPaintHoveredFrameBorder + isRoundedFrame = defaultRoundedFrame + } ) val ref = AtomicReference>() From fce036f4b52196556defa83188db8ef56e3fbc17 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 14:37:28 +0200 Subject: [PATCH 04/23] chore: cleanup --- .../bric3/fireplace/flamegraph/FlamegraphView.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 472dcb9..70ddb73 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -1031,12 +1031,15 @@ public void componentShown(ComponentEvent e) { } }); - installMinimapTriggers(fgCanvas, vsb); - installVerticalScrollBarListeners(fgCanvas, vsb); + installMinimapTriggers(fgCanvas); + installGraphModeListener(fgCanvas, vsb); } } - private void installVerticalScrollBarListeners(FlamegraphCanvas fgCanvas, JScrollBar vsb) { + private void installGraphModeListener( + FlamegraphCanvas fgCanvas, + JScrollBar vsb + ) { fgCanvas.addPropertyChangeListener(GRAPH_MODE_PROPERTY, evt -> SwingUtilities.invokeLater(() -> { var value = vsb.getValue(); var bounds = fgCanvas.getBounds(); @@ -1062,7 +1065,7 @@ private void installVerticalScrollBarListeners(FlamegraphCanvas fgCanvas, JSc })); } - private void installMinimapTriggers(FlamegraphCanvas fgCanvas, JScrollBar vsb) { + private void installMinimapTriggers(FlamegraphCanvas fgCanvas) { PropertyChangeListener triggerMinimapOnPropertyChange = evt -> { var propertyName = evt.getPropertyName(); if (!propertyName.equals("preferredSize") From dd78f0e8a4bbc28c4869701f45c872f7067a89f4 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 21 May 2024 21:53:09 +0200 Subject: [PATCH 05/23] feature: Expand frame horizontally only --- .../bric3/fireplace/ui/FlamegraphPane.kt | 3 +- .../flamegraph/DimmingFrameColorProvider.java | 27 ++- .../flamegraph/FlamegraphRenderEngine.java | 156 ++++++++++++------ .../fireplace/flamegraph/FlamegraphView.java | 122 +++++++++++--- 4 files changed, 228 insertions(+), 80 deletions(-) diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt index 9291b4c..02e0574 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt @@ -79,6 +79,7 @@ class FlamegraphPane : JPanel(BorderLayout()) { ) ) jfrFlamegraphView.frameColorProvider = DimmingFrameColorProvider(frameBoxColorFunction) + .withDimNonFocusedFlame(false) jfrFlamegraphView.requestRepaint() }.also { colorPaletteJComboBox.addActionListener(it) @@ -272,7 +273,7 @@ class FlamegraphPane : JPanel(BorderLayout()) { defaultFrameColorMode.colorMapperUsing( ColorMapper.ofObjectHashUsing(*defaultColorPalette.colors()) ) - ), + ).withDimNonFocusedFlame(false), FrameFontProvider.defaultFontProvider() ).apply { isPaintHoveredFrameBorder = defaultPaintHoveredFrameBorder diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java index 858dbcc..489f687 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java @@ -72,6 +72,7 @@ public class DimmingFrameColorProvider implements FrameColorProvider<@NotNull private Color rootBackGroundColor = ROOT_BACKGROUND_COLOR; private Color dimmedTextColor = DIMMED_TEXT_COLOR; + private boolean dimmedNonFocusedFlames = true; /** * Builds a basic frame color provider. @@ -106,7 +107,7 @@ public ColorModel getColors(@NotNull FrameBox<@NotNull T> frame, int flags) { ); } - boolean shouldDimFocusedFlame = isFocusing(flags) && isInFocusedFlame(flags) && !isHighlightedFrame(flags); + var shouldDimFocusedFlame = shouldDimFocusedFlame(flags); if (!rootNode && shouldDim(flags) && !shouldDimFocusedFlame) { backgroundColor = dimmedBackground(baseBackgroundColor); foreground = dimmedTextColor; @@ -173,6 +174,19 @@ public ColorModel getColors(@NotNull FrameBox<@NotNull T> frame, int flags) { return halfDimmedColorCache.computeIfAbsent(backgroundColor, Colors::halfDim); } + /** + * Should dim the frame if it's in the focused flame. + * + * @param flags + * @return + */ + private boolean shouldDimFocusedFlame(int flags) { + return dimmedNonFocusedFlames + && isFocusing(flags) + && isInFocusedFlame(flags) + && !isHighlightedFrame(flags); + } + /** * Dim only if not highlighted or not focused *

@@ -192,10 +206,11 @@ private boolean shouldDim(int flags) { var inFocusedFlame = isInFocusedFlame(flags); var dimmedForHighlighting = highlighting && !highlightedFrame; - var dimmedForFocus = focusing && !inFocusedFlame; + var dimmedForFocus = dimmedNonFocusedFlames && focusing && !inFocusedFlame; + var dimmedInFocusedFlame = dimmedNonFocusedFlames && focusing && inFocusedFlame; return (dimmedForHighlighting || dimmedForFocus) - && !(focusing && inFocusedFlame) // don't dim frames that are in focused flame + && !dimmedInFocusedFlame // don't dim frames that are in focused flame // && !(highlighting && highlightedFrame) // this dim highlighted that are not in focused flame ; } @@ -211,4 +226,10 @@ public DimmingFrameColorProvider withDimmedTextColor(@NotNull Color dimmedTex this.dimmedTextColor = Objects.requireNonNull(dimmedTextColor); return this; } + + @NotNull + public DimmingFrameColorProvider withDimNonFocusedFlame(boolean dimmedNonFocusedFlames) { + this.dimmedNonFocusedFlames = dimmedNonFocusedFlames; + return this; + } } diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphRenderEngine.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphRenderEngine.java index 51ee1b4..4c1f29c 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphRenderEngine.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphRenderEngine.java @@ -86,22 +86,22 @@ public int getVisibleDepth() { * Cache for the pre-computed depth given the canvas width. * *

- * This cache leverages the WeakHashMap to cleanup keys that - * are not anymore referenced, this is is useful when the canvas - * changes width to avoid re-computation (i.e traversing the framebox - * list again). + * This cache leverages the WeakHashMap to cleanup keys that + * are not anymore referenced, this is is useful when the canvas + * changes width to avoid re-computation (i.e traversing the framebox + * list again). *

*

- * Note about {@link Integer} cache: - * The default Integer cache goes from -128 to 127 by default (this is tunable), - * these entries won't be reclaimed by GC ! However his code assumes the - * canvas width will be higher, or way higher, in practice than 127. - * If a width in the Integer cache range is entered this means it's - * value won't be reclaimed as well, this is might be acceptable given the - * size of the value, an Integer. - * Currently there's no contingency plan if this get a problem, but if - * if is we might want to look at VM params like {@code -XX:AutoBoxCacheMax} - * and/or {@code java.lang.Integer.IntegerCache.high} property. + * Note about {@link Integer} cache: + * The default Integer cache goes from -128 to 127 by default (this is tunable), + * these entries won't be reclaimed by GC ! However his code assumes the + * canvas width will be higher, or way higher, in practice than 127. + * If a width in the Integer cache range is entered this means it's + * value won't be reclaimed as well, this is might be acceptable given the + * size of the value, an Integer. + * Currently there's no contingency plan if this get a problem, but if + * if is we might want to look at VM params like {@code -XX:AutoBoxCacheMax} + * and/or {@code java.lang.Integer.IntegerCache.high} property. *

*/ private final WeakHashMap visibleDepthCache = new WeakHashMap<>(); @@ -186,11 +186,11 @@ public int computeVisibleFlamegraphMinimapHeight(int thumbnailWidth) { * and this depends on the font metrics). * *

- * This methods don't update internal fields. + * This methods don't update internal fields. *

* - * @param g2 the graphics target ({@code null} not permitted), used for font metrics. - * @param canvasWidth the current canvas width + * @param g2 the graphics target ({@code null} not permitted), used for font metrics. + * @param canvasWidth the current canvas width * @return The height of the visible frames in this flamegraph */ public int computeVisibleFlamegraphHeight( @@ -204,9 +204,9 @@ public int computeVisibleFlamegraphHeight( * Computes the dimensions of the flamegraph for the specified width (just the height needs calculating, * and this depends on the font metrics). * - * @param g2 the graphics target ({@code null} not permitted), used for font metrics. - * @param canvasWidth the current canvas width - * @param update whether to update the internal fields. + * @param g2 the graphics target ({@code null} not permitted), used for font metrics. + * @param canvasWidth the current canvas width + * @param update whether to update the internal fields. * @return The height of the visible frames in this flamegraph */ public int computeVisibleFlamegraphHeight( @@ -244,24 +244,24 @@ public int computeVisibleFlamegraphHeight( * Draws the subset of the flame graph that fits within {@code viewRect} assuming that the whole * flame graph is being rendered within the specified {@code bounds}. * - * @param g2 the graphics target ({@code null} not permitted). - * @param bounds the flame graph bounds ({@code null} not permitted). - * @param viewRect the subset that is being viewed/rendered ({@code null} not permitted). + * @param g2 the graphics target ({@code null} not permitted). + * @param canvasBounds the flame graph canvas bounds ({@code null} not permitted). + * @param viewRect the subset that is being viewed/rendered ({@code null} not permitted). */ public void paint( @NotNull Graphics2D g2, - @NotNull Rectangle2D bounds, + @NotNull Rectangle2D canvasBounds, @NotNull Rectangle2D viewRect ) { - internalPaint(g2, bounds, viewRect, false, icicle); + internalPaint(g2, canvasBounds, viewRect, false, icicle); } /** * Draws the subset of the flame graph that fits within {@code viewRect} assuming that the whole * flame graph is being rendered within the specified {@code bounds}. * - * @param g2 the graphics target ({@code null} not permitted). - * @param size the flame graph bounds ({@code null} not permitted). + * @param g2 the graphics target ({@code null} not permitted). + * @param size the flame graph bounds ({@code null} not permitted). */ public void paintToImage( @NotNull Graphics2D g2, @@ -274,19 +274,19 @@ public void paintToImage( /** * Paints the minimap (always the entire flame graph). * - * @param g2 the graphics target ({@code null} not permitted). - * @param bounds the bounds ({@code null} not permitted). + * @param g2 the graphics target ({@code null} not permitted). + * @param canvasBounds the bounds ({@code null} not permitted). */ public void paintMinimap( @NotNull Graphics2D g2, - @NotNull Rectangle2D bounds + @NotNull Rectangle2D canvasBounds ) { - internalPaint(g2, bounds, bounds, true, icicle); + internalPaint(g2, canvasBounds, canvasBounds, true, icicle); } private void internalPaint( @NotNull Graphics2D g2, - @NotNull Rectangle2D bounds, + @NotNull Rectangle2D canvasBounds, @NotNull Rectangle2D viewRect, boolean minimapMode, boolean icicle @@ -296,12 +296,12 @@ private void internalPaint( } Objects.requireNonNull(g2); - Objects.requireNonNull(bounds); + Objects.requireNonNull(canvasBounds); Objects.requireNonNull(viewRect); Graphics2D g2d = (Graphics2D) g2.create(); identifyDisplayScale(g2d); var frameBoxHeight = minimapMode ? minimapFrameBoxHeight : frameRenderer.getFrameBoxHeight(g2); - var flameGraphWidth = minimapMode ? viewRect.getWidth() : bounds.getWidth(); + var flameGraphWidth = minimapMode ? viewRect.getWidth() : canvasBounds.getWidth(); var frameRect = new Rectangle2D.Double(); // reusable rectangle var frames = frameModel.frames; @@ -313,7 +313,7 @@ private void internalPaint( int internalPadding = 0; // Remove ? frameRect.x = (int) (flameGraphWidth * rootFrame.startX) + internalPadding; frameRect.width = ((int) (flameGraphWidth * rootFrame.endX)) - frameRect.x - internalPadding; - frameRect.y = computeFrameRectY(bounds, frameBoxHeight, rootFrame.stackDepth, icicle); + frameRect.y = computeFrameRectY(canvasBounds, frameBoxHeight, rootFrame.stackDepth, icicle); frameRect.height = frameBoxHeight; rootFrameShape.setFrame(frameRect); @@ -350,7 +350,7 @@ private void internalPaint( continue; } - frameRect.y = computeFrameRectY(bounds, frameBoxHeight, frame.stackDepth, icicle); + frameRect.y = computeFrameRectY(canvasBounds, frameBoxHeight, frame.stackDepth, icicle); frameRect.height = frameBoxHeight; frameShape.setFrame(frameRect); @@ -383,18 +383,39 @@ private void internalPaint( } private static int computeFrameRectY( - @NotNull Rectangle2D bounds, + @NotNull Rectangle2D canvasBounds, int frameBoxHeight, int stackDepth, boolean icicle ) { + // TODO model root box height + @SuppressWarnings("UnnecessaryLocalVariable") + var rootBoxHeight = frameBoxHeight; if (icicle) { - return frameBoxHeight * stackDepth; + // In Icicle, the y increases from 0 in the flamegraph canvas. + // The formula is: adding the root bow height, then the frame box height times the stack depth + // then subtracting the frame box height, as this code returns the y coordinate + // + // | root + // | f1 + // | f2 + // | f3 ↖ y = 3 x frameBoxHeight + return /* 0 + */ rootBoxHeight + (frameBoxHeight * stackDepth - frameBoxHeight); } - var flamegraphHeight = bounds.getHeight(); - - return (int) (flamegraphHeight - frameBoxHeight) - (frameBoxHeight * stackDepth); + var flamegraphHeight = canvasBounds.getHeight(); + + // In flamegraph, the y decreases from the top of the flamegraph canvas. + // The formula is: canvas height minus root box height minus the size + // of the frame box times the stack depth + // + // the bottom y of the frame box is + // the flamegraph height minus the size of the frame box times the stack depth + // | f3 ↖ y = flamegraph height - root box height - (3 x frameBoxHeight) + // | f2 + // | f1 + // | root + return (int) flamegraphHeight - rootBoxHeight - (frameBoxHeight * stackDepth); } private void checkReady() { @@ -589,6 +610,32 @@ public void stopHover( }); } + /** + * As for {@link #calculateZoomTargetForFrameAt(Graphics2D, Rectangle2D, Rectangle2D, Point)} but + * only adjusts the horizontal zoom. + * + * @param g2 the graphics target ({@code null} not permitted). + * @param bounds the bounds within which the flame graph is currently rendered. + * @param viewRect the subset of the bounds that is actually visible + * @param point the coordinates at which to look for a frame. + * @return An optional zoom target. + */ + public Optional> calculateHorizontalZoomTargetForFrameAt( + Graphics2D g2, + Rectangle2D bounds, + Rectangle2D viewRect, + Point point + ) { + if (frameModel.frames.isEmpty()) { + return Optional.empty(); + } + + return getFrameAt(g2, bounds, point).map(frame -> { + this.selectedFrame = frame; + return calculateZoomTargetFrame(g2, bounds, viewRect, frame, -1, 0); + }); + } + /** * Compute the {@code ZoomTarget} for the passed frame. *

@@ -599,7 +646,7 @@ public void stopHover( * @param bounds the bounds within which the flame graph is currently rendered. * @param viewRect the subset of the bounds that is actually visible * @param frame the frame. - * @param contextBefore number of contextual parents + * @param contextBefore number of contextual parents, if -1 don't relocate vertically * @param contextLeftRight the contextual frames on the left and right (unused at this time) * @return A zoom target. */ @@ -611,7 +658,7 @@ public void stopHover( @NotNull Rectangle2D viewRect, @NotNull FrameBox<@NotNull T> frame, int contextBefore, - int contextLeftRight + int contextLeftRight // TODO for future left right "context" padding ) { checkReady(); @@ -619,8 +666,9 @@ public void stopHover( var frameBoxHeight = frameRenderer.getFrameBoxHeight(g2); var factor = getScaleFactor(viewRect.getWidth(), bounds.getWidth(), frameWidthX); - // Change offset to center the flame from this frame + // calculate the new width so the current frame will occupy the full width of the view var newCanvasWidth = (int) (bounds.getWidth() * factor); + // Change offset to center the flame from this frame var newCanvasHeight = computeVisibleFlamegraphHeight( g2, newCanvasWidth @@ -632,21 +680,31 @@ public void stopHover( newCanvasWidth, newCanvasHeight ); + + int frameDepthAtTopOfView = icicle ? + (int) (viewRect.getY() / frameBoxHeight) : + (int) ((bounds.getHeight() - viewRect.getMaxY()) / frameBoxHeight); + + int newFrameDepthAtTopOfView = contextBefore >= 0 ? + Math.max(frame.stackDepth - contextBefore, 0) : + frameDepthAtTopOfView; // contextBefore is -1, so keep the same vertical location + var frameY = computeFrameRectY( newDimension, frameBoxHeight, - Math.max(frame.stackDepth - contextBefore, 0), icicle + newFrameDepthAtTopOfView, + icicle ); var viewLocationY = icicle ? - Math.max(0, frameY) : + Math.max(0, frameY) : // icicle mode, don't go negative, use frame Math.min( (int) (newCanvasHeight - viewRect.getHeight()), (int) (frameY + frameBoxHeight - viewRect.getHeight()) ); return new ZoomTarget<>( - - (int) (frame.startX * newCanvasWidth), - - viewLocationY, + -(int) (frame.startX * newCanvasWidth), + -viewLocationY, newCanvasWidth, newCanvasHeight, frame @@ -663,7 +721,7 @@ public void stopHover( * factor = ---------------------------- * frameWidthX * bounds.width * - * + *

* Note that to retrieve the zoom factor one should use {@code 1 / factor}. */ protected static double getScaleFactor(double visibleWidth, double canvasWidth, double frameWidthX) { diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 70ddb73..c57ee1c 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -135,6 +135,11 @@ public enum Mode { FLAMEGRAPH, ICICLEGRAPH } + public enum FrameClickAction { + EXPAND_FRAME, + SELECT_FRAME, + } + /** * Represents a custom action when zooming. */ @@ -624,6 +629,24 @@ public FlamegraphView.Mode getMode() { return canvas.getMode(); } + /** + * Sets the frame click action. + * + * @param frameClickAction The zoom action. + */ + public void setFrameClickAction(@NotNull FlamegraphView.FrameClickAction frameClickAction) { + canvas.setFrameClickBehavior(frameClickAction); + } + + /** + * Returns the current frame click action. + * + * @return the current frame click action. + */ + public @NotNull FlamegraphView.FrameClickAction getFrameClickAction() { + return canvas.getFrameClickBehavior(); + } + /** * Replaces the default tooltip component. * @@ -858,7 +881,7 @@ public void overrideZoomAction(@NotNull FlamegraphView.ZoomAction zoomActionOver * Reset the zoom to 1:1. */ public void resetZoom() { - zoom(canvas, canvas.getResetZoomTarget()); + zoom(canvas, canvas.getResetZoomTarget(false)); } /** @@ -958,6 +981,8 @@ static class FlamegraphCanvas extends JPanel implements ZoomableComponent private boolean showMinimap = true; @Nullable private Supplier<@NotNull JToolTip> tooltipComponentSupplier; + @NotNull + public FrameClickAction frameClickBehavior = FrameClickAction.SELECT_FRAME; @Nullable private ZoomAction zoomActionOverride; @Nullable @@ -966,6 +991,7 @@ static class FlamegraphCanvas extends JPanel implements ZoomableComponent private BiConsumer<@NotNull FrameBox<@NotNull T>, @NotNull MouseEvent> selectedFrameConsumer; @NotNull private final FlamegraphView<@NotNull T> flamegraphView; + @NotNull private final ZoomModel zoomModel = new ZoomModel<>(); private long lastDrawTime; @@ -1538,8 +1564,17 @@ public void setSelectedFrameConsumer( return selectedFrameConsumer; } + public void setFrameClickBehavior(@NotNull FrameClickAction frameClickBehavior) { + this.frameClickBehavior = frameClickBehavior; + } + + @NotNull + public FrameClickAction getFrameClickBehavior() { + return frameClickBehavior; + } + @Nullable - public ZoomTarget<@NotNull T> getResetZoomTarget() { + public ZoomTarget<@NotNull T> getResetZoomTarget(boolean resetHorizontalOnly) { var graphics = (Graphics2D) getGraphics(); if (graphics == null) { return null; @@ -1553,9 +1588,12 @@ public void setSelectedFrameConsumer( visibleRect.width ); + int newY = resetHorizontalOnly ? + bounds.y : + (getMode() == Mode.FLAMEGRAPH ? -(bounds.height - visibleRect.height) : 0); return new ZoomTarget<>( 0, - getMode() == Mode.FLAMEGRAPH ? -(bounds.height - visibleRect.height) : 0, + newY, visibleRect.width, newHeight, null @@ -1674,32 +1712,62 @@ public void mouseClicked(@NotNull MouseEvent e) { return; } - var flamegraphView = FlamegraphView.from(canvas).get(); - - if (e.getClickCount() == 2) { - // find zoom target then do an animated transition - canvas.getFlamegraphRenderEngine().calculateZoomTargetForFrameAt( - (Graphics2D) canvas.getGraphics(), - canvas.getBounds(tmpBounds), - canvas.getVisibleRect(), - latestMouseLocation - ).ifPresent(zoomTarget -> { - if (Objects.equals(canvas.getBounds(), zoomTarget.getTargetBounds())) { - flamegraphView.resetZoom(); - } else { - zoom(canvas, zoomTarget); + switch (canvas.frameClickBehavior) { + case EXPAND_FRAME: + if (e.getClickCount() == 1) { + canvas.getFlamegraphRenderEngine() + .toggleSelectedFrameAt( + (Graphics2D) viewPort.getView().getGraphics(), + canvas.getBounds(tmpBounds), + latestMouseLocation, + (frame, r) -> canvas.repaint() + ); + + // TODO broken on iciclegraph, both expand and shrink + // this appear to be related to the horizontal scrollbar + canvas.getFlamegraphRenderEngine().calculateHorizontalZoomTargetForFrameAt( + (Graphics2D) canvas.getGraphics(), + canvas.getBounds(tmpBounds), + canvas.getVisibleRect(), + latestMouseLocation + ).ifPresent(zoomTarget -> { + if (Objects.equals(canvas.getBounds(tmpBounds), zoomTarget.getTargetBounds())) { + zoom(canvas, canvas.getResetZoomTarget(true)); + } else { + zoom(canvas, zoomTarget); + } + }); + } + break; + case SELECT_FRAME: + if (e.getClickCount() == 2) { + // find zoom target then do an animated transition + canvas.getFlamegraphRenderEngine().calculateZoomTargetForFrameAt( + (Graphics2D) canvas.getGraphics(), + canvas.getBounds(tmpBounds), + canvas.getVisibleRect(), + latestMouseLocation + ).ifPresent(zoomTarget -> { + if (Objects.equals(canvas.getBounds(tmpBounds), zoomTarget.getTargetBounds())) { + zoom(canvas, canvas.getResetZoomTarget(false)); + } else { + zoom(canvas, zoomTarget); + } + }); + return; } - }); - return; - } - canvas.getFlamegraphRenderEngine() - .toggleSelectedFrameAt( - (Graphics2D) viewPort.getView().getGraphics(), - canvas.getBounds(tmpBounds), - latestMouseLocation, - (frame, r) -> canvas.repaint() - ); + if (e.getClickCount() == 1) { + canvas.getFlamegraphRenderEngine() + .toggleSelectedFrameAt( + (Graphics2D) viewPort.getView().getGraphics(), + canvas.getBounds(tmpBounds), + latestMouseLocation, + (frame, r) -> canvas.repaint() + ); + } + break; + } } From e9cf1fab82e16e18ce3ce4adf72515a03660c6c6 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 16:56:28 +0200 Subject: [PATCH 06/23] fix: Move the horizontal bar detection later, when applying the zoom bounds --- .../fireplace/flamegraph/FlamegraphView.java | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index c57ee1c..5ec6e01 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -911,21 +911,6 @@ private static void zoom(@NotNull FlamegraphCanvas<@NotNull T> canvas, @Null return; } - // adjust zoom target location for horizontal scrollbar height if canvas bigger than viewRect - if (canvas.getMode() == Mode.FLAMEGRAPH) { - var visibleRect = canvas.getVisibleRect(); - var viewPort = (JViewport) SwingUtilities.getUnwrappedParent(canvas); - var scrollPane = (JScrollPane) viewPort.getParent(); - - var hsb = scrollPane.getHorizontalScrollBar(); - if (!hsb.isVisible() && visibleRect.getWidth() < zoomTarget.getWidth()) { - var modifiedRect = zoomTarget.getTargetBounds(); - modifiedRect.y -= hsb.getPreferredSize().height; - - zoomTarget = new ZoomTarget<>(modifiedRect, zoomTarget.targetFrame); - } - } - // Set the zoom model to the Zoom Target canvas.zoomModel.recordLastPositionFromZoomTarget(canvas, zoomTarget); @@ -1588,9 +1573,11 @@ public FrameClickAction getFrameClickBehavior() { visibleRect.width ); + // flamegraphRenderEngine.fracalculateHorizontalZoomTargetForFrameAt() + boolean isFlameGraph = getMode() == Mode.FLAMEGRAPH; int newY = resetHorizontalOnly ? - bounds.y : - (getMode() == Mode.FLAMEGRAPH ? -(bounds.height - visibleRect.height) : 0); + (isFlameGraph ? bounds.y + 12 : bounds.y) : + (isFlameGraph ? -(bounds.height - visibleRect.height) : 0); return new ZoomTarget<>( 0, newY, @@ -1628,6 +1615,22 @@ public void zoom(@NotNull ZoomTarget<@NotNull T> zoomTarget) { // Changing the size triggers a revalidation, which triggers a layout // Not calling setBounds from the Timeline may provoke EDT violations // however calling invokeLater makes the animation out of order, and not smooth. + + // adjust zoom target location for horizontal scrollbar height if canvas bigger than viewRect + if (getMode() == Mode.FLAMEGRAPH) { + var visibleRect = getVisibleRect(); + var viewPort = (JViewport) SwingUtilities.getUnwrappedParent(this); + var scrollPane = (JScrollPane) viewPort.getParent(); + + var hsb = scrollPane.getHorizontalScrollBar(); + if (!hsb.isVisible() && visibleRect.getWidth() < zoomTarget.getWidth()) { + var modifiedRect = zoomTarget.getTargetBounds(); + modifiedRect.y -= hsb.getPreferredSize().height; + + zoomTarget = new ZoomTarget<>(modifiedRect, zoomTarget.targetFrame); + } + } + setBounds(zoomTarget.getTargetBounds()); } } @@ -1712,11 +1715,11 @@ public void mouseClicked(@NotNull MouseEvent e) { return; } + var fgre = canvas.getFlamegraphRenderEngine(); switch (canvas.frameClickBehavior) { case EXPAND_FRAME: if (e.getClickCount() == 1) { - canvas.getFlamegraphRenderEngine() - .toggleSelectedFrameAt( + fgre.toggleSelectedFrameAt( (Graphics2D) viewPort.getView().getGraphics(), canvas.getBounds(tmpBounds), latestMouseLocation, @@ -1725,7 +1728,7 @@ public void mouseClicked(@NotNull MouseEvent e) { // TODO broken on iciclegraph, both expand and shrink // this appear to be related to the horizontal scrollbar - canvas.getFlamegraphRenderEngine().calculateHorizontalZoomTargetForFrameAt( + fgre.calculateHorizontalZoomTargetForFrameAt( (Graphics2D) canvas.getGraphics(), canvas.getBounds(tmpBounds), canvas.getVisibleRect(), @@ -1742,7 +1745,7 @@ public void mouseClicked(@NotNull MouseEvent e) { case SELECT_FRAME: if (e.getClickCount() == 2) { // find zoom target then do an animated transition - canvas.getFlamegraphRenderEngine().calculateZoomTargetForFrameAt( + fgre.calculateZoomTargetForFrameAt( (Graphics2D) canvas.getGraphics(), canvas.getBounds(tmpBounds), canvas.getVisibleRect(), @@ -1758,8 +1761,7 @@ public void mouseClicked(@NotNull MouseEvent e) { } if (e.getClickCount() == 1) { - canvas.getFlamegraphRenderEngine() - .toggleSelectedFrameAt( + fgre.toggleSelectedFrameAt( (Graphics2D) viewPort.getView().getGraphics(), canvas.getBounds(tmpBounds), latestMouseLocation, From 7c17ed97ba7a024c19e1926ebef6438a688c781c Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 17:00:27 +0200 Subject: [PATCH 07/23] chore: reformat --- .../fireplace/flamegraph/FlamegraphView.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 5ec6e01..05bcd3b 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -271,9 +271,9 @@ public FlamegraphView() { // default configuration setFrameRender( new DefaultFrameRenderer<>( - FrameTextsProvider.of(frameBox -> frameBox.actualNode.toString()), - FrameColorProvider.defaultColorProvider(f -> UIManager.getColor("Button.background")), - FrameFontProvider.defaultFontProvider() + FrameTextsProvider.of(frameBox -> frameBox.actualNode.toString()), + FrameColorProvider.defaultColorProvider(f -> UIManager.getColor("Button.background")), + FrameFontProvider.defaultFontProvider() ) ); canvas.putClientProperty(OWNER_KEY, this); @@ -1720,11 +1720,11 @@ public void mouseClicked(@NotNull MouseEvent e) { case EXPAND_FRAME: if (e.getClickCount() == 1) { fgre.toggleSelectedFrameAt( - (Graphics2D) viewPort.getView().getGraphics(), - canvas.getBounds(tmpBounds), - latestMouseLocation, - (frame, r) -> canvas.repaint() - ); + (Graphics2D) viewPort.getView().getGraphics(), + canvas.getBounds(tmpBounds), + latestMouseLocation, + (frame, r) -> canvas.repaint() + ); // TODO broken on iciclegraph, both expand and shrink // this appear to be related to the horizontal scrollbar @@ -1762,11 +1762,11 @@ public void mouseClicked(@NotNull MouseEvent e) { if (e.getClickCount() == 1) { fgre.toggleSelectedFrameAt( - (Graphics2D) viewPort.getView().getGraphics(), - canvas.getBounds(tmpBounds), - latestMouseLocation, - (frame, r) -> canvas.repaint() - ); + (Graphics2D) viewPort.getView().getGraphics(), + canvas.getBounds(tmpBounds), + latestMouseLocation, + (frame, r) -> canvas.repaint() + ); } break; } From b2ef1c8d9eb2a37dc364eab285e32d17a1a08745 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 17:02:11 +0200 Subject: [PATCH 08/23] fix: Horizontal scrollbar was not always visible on zoom This change, updates both the scrollbar policy and the visibility. --- .../fireplace/flamegraph/FlamegraphView.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 05bcd3b..e327e84 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -353,16 +353,7 @@ public void layoutContainer(Container parent) { int oldVpWidth = oldViewPortSize.width; var vpSize = vp.getSize(oldViewPortSize); - // Never show the horizontal scrollbar when the scale factor is 1.0 - // Only change it when necessary - int horizontalScrollBarPolicy = jScrollPane.getHorizontalScrollBarPolicy(); double lastScaleFactor = canvas.zoomModel.getLastScaleFactor(); - int newPolicy = lastScaleFactor == 1.0 ? - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER : - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; - if (horizontalScrollBarPolicy != newPolicy) { - jScrollPane.setHorizontalScrollBarPolicy(newPolicy); - } // view port has been resized if (vpSize.width != oldVpWidth) { @@ -393,6 +384,21 @@ public void layoutContainer(Container parent) { canvas.getSize(flamegraphSize); canvas.getLocation(flamegraphLocation); } + + { + // Never show the horizontal scrollbar when the scale factor is 1.0 + int newPolicy = lastScaleFactor == 1.0 ? + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER : + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; + // Only change it when necessary + if (jScrollPane.getHorizontalScrollBarPolicy() != newPolicy) { + jScrollPane.setHorizontalScrollBarPolicy(newPolicy); + } + + // show the horizontal scrollbar if the flamegraph is wider than the viewport + jScrollPane.getHorizontalScrollBar() + .setVisible(lastScaleFactor != 1.0 && oldVpWidth < flamegraphSize.width); + } } }; } From b04b7abcbb3896db425b823b55308ef9f1a3b55d Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 17:03:36 +0200 Subject: [PATCH 09/23] fix: Handle flamegraph visible depth change when resizing viewport --- .../fireplace/flamegraph/FlamegraphView.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index e327e84..39b2f35 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -352,6 +352,7 @@ public void layoutContainer(Container parent) { var canvas = (FlamegraphCanvas) vp.getView(); int oldVpWidth = oldViewPortSize.width; var vpSize = vp.getSize(oldViewPortSize); + var oldFlamegraphHeight = flamegraphSize.height; double lastScaleFactor = canvas.zoomModel.getLastScaleFactor(); @@ -364,6 +365,16 @@ public void layoutContainer(Container parent) { ); vp.setViewSize(flamegraphSize); + // Handles the view location change when the flamegraph is changing its height, + // i.e., there are less or more frames visible + // First compute the last y offset from the bottom + int flamegraphYFromBottom = oldFlamegraphHeight - Math.abs(flamegraphLocation.y); + // then compute the new y offset from the bottom using the new flamegraph height + int yLocation = canvas.getMode() == Mode.FLAMEGRAPH ? + flamegraphSize.height - flamegraphYFromBottom : + flamegraphLocation.y; + flamegraphLocation.y = Math.abs(yLocation); + // if view position X > 0 // the fg is zoomed // => get the latest position ratio resulting from user interaction @@ -373,10 +384,9 @@ public void layoutContainer(Container parent) { double positionRatio = canvas.zoomModel.getLastUserInteractionStartX(); flamegraphLocation.x = Math.abs((int) (positionRatio * flamegraphSize.width)); - flamegraphLocation.y = Math.abs(flamegraphLocation.y); - - vp.setViewPosition(flamegraphLocation); } + + vp.setViewPosition(flamegraphLocation); } else { super.layoutContainer(parent); // capture the sizes From 5ded60cc8fd75524884a48540d1c05722e26cd78 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 18:03:20 +0200 Subject: [PATCH 10/23] fix: Improves flamegraph zoom behavior --- .../fireplace/flamegraph/FlamegraphView.java | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 39b2f35..15e248c 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -96,6 +96,7 @@ * @see FlamegraphRenderEngine * @see DefaultFrameRenderer */ +@SuppressWarnings("unused") public class FlamegraphView { /** * Internal key to get the Flamegraph from the component. @@ -927,6 +928,8 @@ private static void zoom(@NotNull FlamegraphCanvas<@NotNull T> canvas, @Null return; } + zoomTarget = canvas.adjustedZoomTargetForHsbVisibility(zoomTarget, false); + // Set the zoom model to the Zoom Target canvas.zoomModel.recordLastPositionFromZoomTarget(canvas, zoomTarget); @@ -1020,7 +1023,7 @@ public void addNotify() { // from appearing on first display, see #96. // Since a scrollbar is made visible once, this listener is called only once, // which is the intended behavior (otherwise it affects zooming). - var parent = SwingUtilities.getUnwrappedParent(fgCanvas); + var parent = fgCanvas.getParent(); if (parent instanceof JViewport) { var viewport = (JViewport) parent; var scrollPane = (JScrollPane) viewport.getParent(); @@ -1582,18 +1585,17 @@ public FrameClickAction getFrameClickBehavior() { } var visibleRect = getVisibleRect(); - var bounds = getBounds(); + var canvasBounds = getBounds(); var newHeight = flamegraphRenderEngine.computeVisibleFlamegraphHeight( graphics, visibleRect.width ); - // flamegraphRenderEngine.fracalculateHorizontalZoomTargetForFrameAt() boolean isFlameGraph = getMode() == Mode.FLAMEGRAPH; int newY = resetHorizontalOnly ? - (isFlameGraph ? bounds.y + 12 : bounds.y) : - (isFlameGraph ? -(bounds.height - visibleRect.height) : 0); + (isFlameGraph ? -(newHeight - (canvasBounds.height - Math.abs(canvasBounds.y))) : canvasBounds.y) : + (isFlameGraph ? -(canvasBounds.height - visibleRect.height) : 0); return new ZoomTarget<>( 0, newY, @@ -1632,22 +1634,34 @@ public void zoom(@NotNull ZoomTarget<@NotNull T> zoomTarget) { // Not calling setBounds from the Timeline may provoke EDT violations // however calling invokeLater makes the animation out of order, and not smooth. - // adjust zoom target location for horizontal scrollbar height if canvas bigger than viewRect - if (getMode() == Mode.FLAMEGRAPH) { - var visibleRect = getVisibleRect(); - var viewPort = (JViewport) SwingUtilities.getUnwrappedParent(this); - var scrollPane = (JScrollPane) viewPort.getParent(); + setBounds(zoomTarget.getTargetBounds()); + } + + + /** + * Adjust the zoom target location for horizontal scrollbar height if canvas bigger than viewRect. + * This only applies to flamegraph mode. + * + * @param zoomTarget The zoom target. + * @return An adjusted zoom target instance, or the passed zoom target if no adjustment is needed. + */ + private ZoomTarget<@NotNull T> adjustedZoomTargetForHsbVisibility( + @NotNull ZoomTarget<@NotNull T> zoomTarget, + boolean ignoreVisibility + ) { + if (this.getMode() == Mode.FLAMEGRAPH) { + var visibleRect = this.getVisibleRect(); + var scrollPane = (JScrollPane) this.getParent().getParent(); var hsb = scrollPane.getHorizontalScrollBar(); - if (!hsb.isVisible() && visibleRect.getWidth() < zoomTarget.getWidth()) { + if ((ignoreVisibility || !hsb.isVisible()) && visibleRect.getWidth() < zoomTarget.getWidth()) { var modifiedRect = zoomTarget.getTargetBounds(); - modifiedRect.y -= hsb.getPreferredSize().height; + modifiedRect.y += hsb.getPreferredSize().height; zoomTarget = new ZoomTarget<>(modifiedRect, zoomTarget.targetFrame); } } - - setBounds(zoomTarget.getTargetBounds()); + return zoomTarget; } } @@ -1741,8 +1755,7 @@ public void mouseClicked(@NotNull MouseEvent e) { latestMouseLocation, (frame, r) -> canvas.repaint() ); - - // TODO broken on iciclegraph, both expand and shrink + // TODO weird behavior on iciclegraph, both expand and shrink // this appear to be related to the horizontal scrollbar fgre.calculateHorizontalZoomTargetForFrameAt( (Graphics2D) canvas.getGraphics(), From 5d63289bbf40999adf7064596919c82442d9f810 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 22 May 2024 18:05:02 +0200 Subject: [PATCH 11/23] docs: Update doc to new framerender --- .../io/github/bric3/fireplace/ui/FlamegraphPane.kt | 2 ++ .../bric3/fireplace/flamegraph/FlamegraphView.java | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt index 02e0574..1dd0fab 100644 --- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt +++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/FlamegraphPane.kt @@ -19,6 +19,7 @@ import io.github.bric3.fireplace.flamegraph.ColorMapper import io.github.bric3.fireplace.flamegraph.DefaultFrameRenderer import io.github.bric3.fireplace.flamegraph.DimmingFrameColorProvider import io.github.bric3.fireplace.flamegraph.FlamegraphView +import io.github.bric3.fireplace.flamegraph.FlamegraphView.FrameClickAction.EXPAND_FRAME import io.github.bric3.fireplace.flamegraph.FlamegraphView.HoverListener import io.github.bric3.fireplace.flamegraph.FrameBox import io.github.bric3.fireplace.flamegraph.FrameFontProvider @@ -252,6 +253,7 @@ class FlamegraphPane : JPanel(BorderLayout()) { private const val defaultRoundedFrame = true private fun getJfrFlamegraphView(): FlamegraphView { val flamegraphView = FlamegraphView() + flamegraphView.frameClickAction = EXPAND_FRAME flamegraphView.setFrameRender( DefaultFrameRenderer( FrameTextsProvider.of( diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java index 15e248c..428f882 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java @@ -59,10 +59,12 @@ *


  * var flamegraphView = new FlamegraphView<MyNode>();
  * flamegraphView.setShowMinimap(false);
- * flamegraphView.setRenderConfiguration(
- *     frameTextProvider,           // string representation candidates
- *     frameColorProvider,          // color the frame
- *     frameFontProvider,           // returns a given font for a frame
+ * flamegraphView.setFrameRender(
+ *     new DefaultFrameRenderer(
+ *         frameTextProvider,           // string representation candidates
+ *         frameColorProvider,          // color the frame
+ *         frameFontProvider,           // returns a given font for a frame
+ *     )
  * );
  * flamegraphView.setTooltipTextFunction(
  *     frameToToolTipTextFunction   // text tooltip function

From d4d07ae1c30feb2ac379bd658d62affcc5bd63df Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Thu, 23 May 2024 10:18:21 +0200
Subject: [PATCH 12/23] fix: Handle single click rest zoom for taller viewport
 than flamegraph

---
 .../fireplace/flamegraph/FlamegraphView.java     | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index 428f882..48c4e3c 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -1747,13 +1747,14 @@ public void mouseClicked(@NotNull MouseEvent e) {
                 return;
             }
 
+            var canvasBounds = canvas.getBounds(tmpBounds);
             var fgre = canvas.getFlamegraphRenderEngine();
             switch (canvas.frameClickBehavior) {
                 case EXPAND_FRAME:
                     if (e.getClickCount() == 1) {
                         fgre.toggleSelectedFrameAt(
                                 (Graphics2D) viewPort.getView().getGraphics(),
-                                canvas.getBounds(tmpBounds),
+                                canvasBounds,
                                 latestMouseLocation,
                                 (frame, r) -> canvas.repaint()
                         );
@@ -1761,11 +1762,14 @@ public void mouseClicked(@NotNull MouseEvent e) {
                         // this appear to be related to the horizontal scrollbar
                         fgre.calculateHorizontalZoomTargetForFrameAt(
                                 (Graphics2D) canvas.getGraphics(),
-                                canvas.getBounds(tmpBounds),
+                                canvasBounds,
                                 canvas.getVisibleRect(),
                                 latestMouseLocation
                         ).ifPresent(zoomTarget -> {
-                            if (Objects.equals(canvas.getBounds(tmpBounds), zoomTarget.getTargetBounds())) {
+                            var targetBounds = zoomTarget.getTargetBounds();
+                            // Don't include height as the view rect might be taller that the flamegraph height
+                            if (canvasBounds.x == targetBounds.x && canvasBounds.y == targetBounds.y
+                                && canvasBounds.width == targetBounds.width) {
                                 zoom(canvas, canvas.getResetZoomTarget(true));
                             } else {
                                 zoom(canvas, zoomTarget);
@@ -1778,11 +1782,11 @@ public void mouseClicked(@NotNull MouseEvent e) {
                         // find zoom target then do an animated transition
                         fgre.calculateZoomTargetForFrameAt(
                                 (Graphics2D) canvas.getGraphics(),
-                                canvas.getBounds(tmpBounds),
+                                canvasBounds,
                                 canvas.getVisibleRect(),
                                 latestMouseLocation
                         ).ifPresent(zoomTarget -> {
-                            if (Objects.equals(canvas.getBounds(tmpBounds), zoomTarget.getTargetBounds())) {
+                            if (Objects.equals(canvasBounds, zoomTarget.getTargetBounds())) {
                                 zoom(canvas, canvas.getResetZoomTarget(false));
                             } else {
                                 zoom(canvas, zoomTarget);
@@ -1794,7 +1798,7 @@ public void mouseClicked(@NotNull MouseEvent e) {
                     if (e.getClickCount() == 1) {
                         fgre.toggleSelectedFrameAt(
                                 (Graphics2D) viewPort.getView().getGraphics(),
-                                canvas.getBounds(tmpBounds),
+                                canvasBounds,
                                 latestMouseLocation,
                                 (frame, r) -> canvas.repaint()
                         );

From 12de751d6fd8bd776332d48c0a45c8610fe6fbc8 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Thu, 23 May 2024 11:55:54 +0200
Subject: [PATCH 13/23] fix: Rare case, in Flamegraph mode when resize could
 enter a loop

Computes the dimension as if a vertical scrollbar was needed.

Otherwise, the layout can enter a loop:
Because the view port width is called once with full width,
which computes a canvas with a taller dimension than viewport.
This triggers the horizontal scrollbar to be added, which
triggers another layout.
In this layout, the view port width is shorter by the scrollbar width,
which makes the canvas fitting in the view port,
which then triggers annoter layout that removes the vertical scrollbar,
and then starts again.

Note the scrollbar visibility is updated at the end of this control block
---
 .../fireplace/flamegraph/FlamegraphView.java  | 33 ++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index 48c4e3c..9e83ac8 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -361,10 +361,27 @@ public void layoutContainer(Container parent) {
 
                         // view port has been resized
                         if (vpSize.width != oldVpWidth) {
+                            // Computes the dimension as if a vertical scrollbar was needed.
+                            //
+                            // Otherwise, the layout can enter a loop:
+                            // Because the view port width is called once with full width,
+                            // which computes a canvas with a taller dimension than viewport.
+                            // This triggers the horizontal scrollbar to be added, which
+                            // triggers another layout.
+                            // In this layout, the view port width is shorter by the scrollbar width,
+                            // which makes the canvas fitting in the view port,
+                            // which then triggers annoter layout that removes the vertical scrollbar,
+                            // and then starts again.
+                            //
+                            // Note the scrollbar visibility is updated at the end of this control block
+
+                            var vsb = jScrollPane.getVerticalScrollBar();
+                            int currentVpWidth = vpSize.width - (vsb.isVisible() ? 0 : vsb.getWidth());
+
                             // scale the fg size with the new viewport width
                             canvas.updateFlamegraphDimension(
                                     flamegraphSize,
-                                    (int) (((double) vpSize.width) / lastScaleFactor)
+                                    (int) (((double) currentVpWidth) / lastScaleFactor)
                             );
                             vp.setViewSize(flamegraphSize);
 
@@ -397,6 +414,20 @@ public void layoutContainer(Container parent) {
                             canvas.getSize(flamegraphSize);
                             canvas.getLocation(flamegraphLocation);
                         }
+                        
+                        {
+                            // Never show the vertical scrollbar when the flamegraph fits in the vp
+                            int newPolicy = flamegraphSize.height <= vpSize.height ?
+                                            ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER :
+                                            ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
+                            // Only change it when necessary
+                            if (jScrollPane.getVerticalScrollBarPolicy() != newPolicy) {
+                                jScrollPane.setVerticalScrollBarPolicy(newPolicy);
+                            }
+
+                            // show the horizontal scrollbar if the flamegraph is wider than the viewport
+                            jScrollPane.getVerticalScrollBar().setVisible(flamegraphSize.height > vpSize.height);
+                        }
 
                         {
                             // Never show the horizontal scrollbar when the scale factor is 1.0

From c40ba38aba60251dad562d33c2f21cd9a0cd566f Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Tue, 28 May 2024 09:44:39 +0200
Subject: [PATCH 14/23] chore: Replace gradle wrapper validation coordinates

---
 .github/workflows/build.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 80ed6c5..b12cfb1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,6 +32,9 @@ jobs:
       #      - name: debug
       #        run: echo "${{ toJSON(github.event)}}"
 
+      - name: Gradle Wrapper Validation
+        uses: gradle/actions/wrapper-validation@v3
+
       - name: Set up JDK
         uses: actions/setup-java@v4
         with:

From f17f149a81fd8eb6ce3a2873dd868ed87f00a790 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Sat, 1 Jun 2024 19:47:57 +0200
Subject: [PATCH 15/23] fix: Now canvas at least vp height if fg height is
 inferior

Fixes a jittering rendering when the viewport height was higher than
flamegraph height
---
 .../io/github/bric3/fireplace/flamegraph/FlamegraphView.java    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index 9e83ac8..29370a3 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -383,6 +383,8 @@ public void layoutContainer(Container parent) {
                                     flamegraphSize,
                                     (int) (((double) currentVpWidth) / lastScaleFactor)
                             );
+                            // Ensure the canvas take up the whole height (helps when drawing the minimap)
+                            flamegraphSize.height = Math.max(vpSize.height, flamegraphSize.height);
                             vp.setViewSize(flamegraphSize);
 
                             // Handles the view location change when the flamegraph is changing its height,

From e054f6d8bc2f44902dcaa8487535a0beb99c41db Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Sat, 1 Jun 2024 19:48:39 +0200
Subject: [PATCH 16/23] chore: disable dimming non focused flames by default

---
 .../bric3/fireplace/flamegraph/DimmingFrameColorProvider.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java
index 489f687..8768251 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/DimmingFrameColorProvider.java
@@ -72,7 +72,7 @@ public class DimmingFrameColorProvider implements FrameColorProvider<@NotNull
 
     private Color rootBackGroundColor = ROOT_BACKGROUND_COLOR;
     private Color dimmedTextColor = DIMMED_TEXT_COLOR;
-    private boolean dimmedNonFocusedFlames = true;
+    private boolean dimmedNonFocusedFlames = false;
 
     /**
      * Builds a basic frame color provider.

From 8681ffd8ec49af8510d270b7b894576cdf3677fd Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Sat, 1 Jun 2024 19:51:41 +0200
Subject: [PATCH 17/23] chore: Tweak fireplace app build

---
 build-logic/build.gradle.kts                               | 1 +
 .../src/main/kotlin/fireplace.application.gradle.kts       | 7 ++++++-
 fireplace-app/build.gradle.kts                             | 1 -
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index de88216..4268f14 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -21,6 +21,7 @@ repositories {
 }
 
 dependencies {
+    implementation(libs.gradlePlugin.kotlin.jvm)
     implementation(libs.gradlePlugin.bnd)
     implementation(libs.gradlePlugin.semver)
     implementation(libs.gradlePlugin.testLogger)
diff --git a/build-logic/src/main/kotlin/fireplace.application.gradle.kts b/build-logic/src/main/kotlin/fireplace.application.gradle.kts
index 06f2032..7fdf5c9 100644
--- a/build-logic/src/main/kotlin/fireplace.application.gradle.kts
+++ b/build-logic/src/main/kotlin/fireplace.application.gradle.kts
@@ -11,15 +11,20 @@
 plugins {
     application
     id("fireplace.tests")
+    id("org.jetbrains.kotlin.jvm")
 }
 
-val javaVersion = 21
+val javaVersion = 22
 java {
     toolchain {
         languageVersion.set(JavaLanguageVersion.of(javaVersion))
     }
 }
 
+kotlin {
+    jvmToolchain(javaVersion)
+}
+
 repositories {
     mavenCentral()
     maven {
diff --git a/fireplace-app/build.gradle.kts b/fireplace-app/build.gradle.kts
index 241cda6..5a097d6 100644
--- a/fireplace-app/build.gradle.kts
+++ b/fireplace-app/build.gradle.kts
@@ -14,7 +14,6 @@ plugins {
     // https://github.com/johnrengelman/shadow/pull/876
     // https://github.com/johnrengelman/shadow/issues/908
     id("io.github.goooler.shadow") version "8.1.7"
-    kotlin("jvm") version "2.0.0"
 }
 
 description = "Opens a JFR file to inspect its content."

From aea300aa46708dd35fd1c9d61b20e7d0a4ee25dd Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Sun, 2 Jun 2024 21:22:50 +0200
Subject: [PATCH 18/23] chore: Don't show tip if owner window is not active or
 focused

---
 .../bric3/fireplace/ui/toolkit/FollowingTipService.kt    | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
index 50dfa38..0b37973 100644
--- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
+++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
@@ -69,6 +69,11 @@ private class FollowingTip {
         val component: Component
         when (e.id) {
             MOUSE_ENTERED, MOUSE_MOVED, MOUSE_DRAGGED, MOUSE_WHEEL -> {
+                // Don't bother to show tip if the owner window is not focused or active
+                if (!ownerWindow.isActive || !ownerWindow.isFocused) {
+                    tipWindow.isVisible = false
+                    return@AWTEventListener
+                }
                 event = e as MouseEvent
                 component = e.component
                 if (ownerWindow.isAncestorOf(component) && component is JComponent) {
@@ -85,7 +90,7 @@ private class FollowingTip {
                     }
 
                     val content = contentProvider?.invoke(component, event)
-                    if (content == null) {
+                    if (content == null || !ownerWindow.isActive || !ownerWindow.isFocused) {
                         tipWindow.isVisible = false
                         return@AWTEventListener
                     }
@@ -101,7 +106,7 @@ private class FollowingTip {
                 event = e as MouseEvent
                 component = e.component
                 val p = SwingUtilities.convertPoint(component, event.point, ownerWindow)
-                if (!ownerWindow.contains(p)) {
+                if (!ownerWindow.contains(p) || !ownerWindow.isActive || !ownerWindow.isFocused) {
                     tipWindow.isVisible = false
                 }
             }

From a4abd65d9485505e878b661479cc25947b68d42a Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Mon, 3 Jun 2024 10:40:04 +0200
Subject: [PATCH 19/23] chore: Rename deinstall to uninstall

---
 .../github/bric3/fireplace/ui/toolkit/FollowingTipService.kt  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
index 0b37973..c10404a 100644
--- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
+++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/ui/toolkit/FollowingTipService.kt
@@ -40,7 +40,7 @@ object FollowingTipService {
     }
 
     fun disableFor(component: JComponent) {
-        followingTip.deinstall(component)
+        followingTip.uninstall(component)
     }
 }
 
@@ -143,7 +143,7 @@ private class FollowingTip {
         }
     }
 
-    fun deinstall(component: JComponent) {
+    fun uninstall(component: JComponent) {
         val location = tipWindow.locationOnScreen.apply {
             SwingUtilities.convertPointFromScreen(this, component)
         }

From 84f5ee24b5fbfa844f2abf0055650405ed492355 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Wed, 5 Jun 2024 16:00:56 +0200
Subject: [PATCH 20/23] chore: flamegraph mode now have the scrollbar at the
 top

---
 .../fireplace/flamegraph/FlamegraphView.java  | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index 29370a3..af8a9eb 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -352,7 +352,13 @@ public void layoutContainer(Container parent) {
                         // The view location is also updated.
 
                         var vp = (JViewport) parent;
-                        var canvas = (FlamegraphCanvas) vp.getView();
+                        var view = vp.getView();
+                        if (!(view instanceof FlamegraphCanvas)) {
+                            // failsafe in case this layout is used elsewhere
+                            super.layoutContainer(parent);
+                        }
+
+                        var canvas = (FlamegraphCanvas) view;
                         int oldVpWidth = oldViewPortSize.width;
                         var vpSize = vp.getSize(oldViewPortSize);
                         var oldFlamegraphHeight = flamegraphSize.height;
@@ -1097,13 +1103,17 @@ public void componentShown(ComponentEvent e) {
                 });
 
                 installMinimapTriggers(fgCanvas);
-                installGraphModeListener(fgCanvas, vsb);
+
+                var hsb = scrollPane.getHorizontalScrollBar();
+                installGraphModeListener(fgCanvas, scrollPane, vsb, hsb);
             }
         }
 
         private void installGraphModeListener(
                 FlamegraphCanvas fgCanvas,
-                JScrollBar vsb
+                JScrollPane scrollPane,
+                JScrollBar vsb,
+                JScrollBar hsb
         ) {
             fgCanvas.addPropertyChangeListener(GRAPH_MODE_PROPERTY, evt -> SwingUtilities.invokeLater(() -> {
                 var value = vsb.getValue();
@@ -1113,6 +1123,8 @@ private void installGraphModeListener(
                 // This computes the new view location based on the current view location
                 switch (fgCanvas.getMode()) {
                     case ICICLEGRAPH:
+                        // use the component add rather than setHorizontalScrollBar which does more things
+                        scrollPane.add(hsb, ScrollPaneConstants.HORIZONTAL_SCROLLBAR);
                         vsb.setValue(
                                 value == vsb.getMaximum() ?
                                 vsb.getMinimum() :
@@ -1120,6 +1132,7 @@ private void installGraphModeListener(
                         );
                         break;
                     case FLAMEGRAPH:
+                        scrollPane.setColumnHeaderView(hsb);
                         vsb.setValue(
                                 value == vsb.getMinimum() ?
                                 vsb.getMaximum() :

From e8a8aafa3a7288421de231e586e8b98d0b4314e5 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Wed, 5 Jun 2024 16:45:38 +0200
Subject: [PATCH 21/23] chore: flamegraph mode now have the minimap at the top

---
 .../fireplace/flamegraph/FlamegraphView.java  | 60 ++++++++++---------
 1 file changed, 33 insertions(+), 27 deletions(-)

diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index af8a9eb..942183a 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -1211,27 +1211,43 @@ protected Dimension updateFlamegraphDimension(@NotNull Dimension dimension, int
             return dimension;
         }
 
+
+        private final Rectangle reusableMinimapRect = new Rectangle();
+        private final Rectangle reusableVisibleRect = new Rectangle();
+
+        private Rectangle computeMinimapRect() {
+            computeVisibleRect(reusableVisibleRect);
+            reusableMinimapRect.setBounds(
+                    reusableVisibleRect.x + minimapBounds.x,
+                    reusableVisibleRect.y + (getMode() == Mode.ICICLEGRAPH ? reusableVisibleRect.height - minimapBounds.height - minimapBounds.y : minimapBounds.y),
+                    minimapBounds.width + (2 * minimapInset),
+                    minimapBounds.height + (2 * minimapInset)
+            );
+
+            return reusableMinimapRect;
+        }
+
         @Override
         protected void paintComponent(@NotNull Graphics g) {
             long start = System.currentTimeMillis();
 
             super.paintComponent(g);
             var g2 = (Graphics2D) g.create();
-            var visibleRect = getVisibleRect();
+            computeVisibleRect(reusableVisibleRect);
             if (flamegraphRenderEngine == null) {
                 String message = "No data to display";
                 var font = g2.getFont();
                 // calculate center position
                 var bounds = g2.getFontMetrics(font).getStringBounds(message, g2);
-                int xx = visibleRect.x + (int) ((visibleRect.width - bounds.getWidth()) / 2.0);
-                int yy = visibleRect.y + (int) ((visibleRect.height + bounds.getHeight()) / 2.0);
+                int xx = reusableVisibleRect.x + (int) ((reusableVisibleRect.width - bounds.getWidth()) / 2.0);
+                int yy = reusableVisibleRect.y + (int) ((reusableVisibleRect.height + bounds.getHeight()) / 2.0);
                 g2.drawString(message, xx, yy);
                 g2.dispose();
                 return;
             }
 
-            flamegraphRenderEngine.paint(g2, getBounds(), visibleRect);
-            paintMinimap(g2, visibleRect);
+            flamegraphRenderEngine.paint(g2, getBounds(), reusableVisibleRect);
+            paintMinimap(g2, reusableVisibleRect);
 
             lastDrawTime = System.currentTimeMillis() - start;
             paintDetails(g2);
@@ -1270,13 +1286,14 @@ private void paintDetails(@NotNull Graphics2D g2) {
             }
         }
 
-        private void paintMinimap(@NotNull Graphics g, @NotNull Rectangle visibleRect) {
+        private void paintMinimap(@NotNull Graphics g, Rectangle visibleRect) {
             if (showMinimap && minimap != null) {
+                var minimapRect = computeMinimapRect();
                 var g2 = (Graphics2D) g.create(
-                        visibleRect.x + minimapBounds.x,
-                        visibleRect.y + visibleRect.height - minimapBounds.height - minimapBounds.y,
-                        minimapBounds.width + minimapInset * 2,
-                        minimapBounds.height + minimapInset * 2
+                        minimapRect.x,
+                        minimapRect.y,
+                        minimapRect.width,
+                        minimapRect.height
                 );
 
                 g2.setColor(getBackground());
@@ -1347,15 +1364,8 @@ public boolean isInsideMinimap(@NotNull Point point) {
             if (!showMinimap) {
                 return false;
             }
-            var visibleRect = getVisibleRect();
-            var rectangle = new Rectangle(
-                    visibleRect.x + minimapBounds.y,
-                    visibleRect.y + visibleRect.height - minimapBounds.height - minimapBounds.y,
-                    minimapBounds.width + 2 * minimapInset,
-                    minimapBounds.height + 2 * minimapInset
-            );
 
-            return rectangle.contains(point);
+            return computeMinimapRect().contains(point);
         }
 
         public void setToolTipText(FrameBox frame) {
@@ -1423,11 +1433,7 @@ private void setMinimapImage(@NotNull BufferedImage minimapImage) {
         }
 
         private void repaintMinimapArea() {
-            var visibleRect = getVisibleRect();
-            repaint(visibleRect.x + minimapBounds.x,
-                    visibleRect.y + visibleRect.height - minimapBounds.height - minimapBounds.y,
-                    minimapBounds.width + minimapInset * 2,
-                    minimapBounds.height + minimapInset * 2);
+            repaint(computeMinimapRect());
         }
 
         public void setupListeners(@NotNull JScrollPane scrollPane) {
@@ -1491,18 +1497,18 @@ private void processMinimapMouseEvent(@NotNull MouseEvent e) {
                     if (!(e.getComponent() instanceof FlamegraphView.FlamegraphCanvas)) {
                         return;
                     }
+                    var canvas = (FlamegraphCanvas) e.getComponent();
 
-                    var visibleRect = ((FlamegraphCanvas) e.getComponent()).getVisibleRect();
 
                     double zoomZoneScaleX = (double) minimapBounds.width / flamegraphDimension.width;
                     double zoomZoneScaleY = (double) minimapBounds.height / flamegraphDimension.height;
+                    var minimapRect = canvas.computeMinimapRect();
 
-                    var h = (pt.x - (visibleRect.x + minimapBounds.x)) / zoomZoneScaleX;
+                    var h = (pt.x - minimapRect.x) / zoomZoneScaleX;
                     var horizontalBarModel = scrollPane.getHorizontalScrollBar().getModel();
                     horizontalBarModel.setValue((int) h - horizontalBarModel.getExtent());
 
-
-                    var v = (pt.y - (visibleRect.y + visibleRect.height - minimapBounds.height - minimapBounds.y)) / zoomZoneScaleY;
+                    var v = (pt.y - minimapRect.y) / zoomZoneScaleY;
                     var verticalBarModel = scrollPane.getVerticalScrollBar().getModel();
                     verticalBarModel.setValue((int) v - verticalBarModel.getExtent());
                 }

From 402fcb2e40825cf8a0966581cea517293a544004 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Mon, 10 Jun 2024 11:27:16 +0200
Subject: [PATCH 22/23] chore: use nanoTime

---
 .../fireplace/ui/debug/EventDispatchThreadHangMonitor.java  | 6 +++---
 .../src/main/kotlin/io/github/bric3/fireplace/Utils.kt      | 4 ++--
 .../github/bric3/fireplace/flamegraph/FlamegraphView.java   | 4 ++--
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/debug/EventDispatchThreadHangMonitor.java b/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/debug/EventDispatchThreadHangMonitor.java
index fbe043d..7f1a0b6 100644
--- a/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/debug/EventDispatchThreadHangMonitor.java
+++ b/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/debug/EventDispatchThreadHangMonitor.java
@@ -194,7 +194,7 @@ private static class DispatchInfo {
         private final Thread eventDispatchThread = Thread.currentThread();
 
         // The last time in milliseconds at which we saw a dispatch on the above thread.
-        private long lastDispatchTimeMillis = System.currentTimeMillis();
+        private long lastDispatchTimeNanos = System.nanoTime();
 
         DispatchInfo() {
             // All initialization is done by the field initializers.
@@ -272,7 +272,7 @@ private static boolean stacksEqual(StackTraceElement[] a, StackTraceElement[] b)
          * Returns how long this dispatch has been going on (in milliseconds).
          */
         private long timeSoFar() {
-            return (System.currentTimeMillis() - lastDispatchTimeMillis);
+            return (System.nanoTime() - lastDispatchTimeNanos) / 1000000;
         }
 
         public void dispose() {
@@ -349,7 +349,7 @@ private synchronized void postDispatchEvent() {
             var currentEventDispatchThread = Thread.currentThread();
             for (var dispatchInfo : dispatches) {
                 if (dispatchInfo.eventDispatchThread == currentEventDispatchThread) {
-                    dispatchInfo.lastDispatchTimeMillis = System.currentTimeMillis();
+                    dispatchInfo.lastDispatchTimeNanos = System.nanoTime();
                 }
             }
         }
diff --git a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/Utils.kt b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/Utils.kt
index 20f738f..d54ec8c 100644
--- a/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/Utils.kt
+++ b/fireplace-app/src/main/kotlin/io/github/bric3/fireplace/Utils.kt
@@ -66,12 +66,12 @@ object Utils {
             return block()
         }
 
-        val start: Long = System.currentTimeMillis()
+        val start: Long = System.nanoTime()
 
         try {
             return block()
         } finally {
-            val elapsed: Long = System.currentTimeMillis() - start
+            val elapsed: Long = (System.nanoTime() - start) / 1_000_000 // ns -> ms
             println("[${Thread.currentThread().name}] $name took $elapsed ms")
         }
     }
diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
index 942183a..57eeefd 100644
--- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
+++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FlamegraphView.java
@@ -1229,7 +1229,7 @@ private Rectangle computeMinimapRect() {
 
         @Override
         protected void paintComponent(@NotNull Graphics g) {
-            long start = System.currentTimeMillis();
+            long startNanos = System.nanoTime();
 
             super.paintComponent(g);
             var g2 = (Graphics2D) g.create();
@@ -1249,7 +1249,7 @@ protected void paintComponent(@NotNull Graphics g) {
             flamegraphRenderEngine.paint(g2, getBounds(), reusableVisibleRect);
             paintMinimap(g2, reusableVisibleRect);
 
-            lastDrawTime = System.currentTimeMillis() - start;
+            lastDrawTime = (System.nanoTime() - startNanos) / 1_000_000;
             paintDetails(g2);
             g2.dispose();
         }

From b5accd6be1de9dc0075dde4de91ee831ced844f3 Mon Sep 17 00:00:00 2001
From: Brice Dutheil 
Date: Sun, 18 Aug 2024 17:15:17 +0200
Subject: [PATCH 23/23] chore(deps): Upgrade Gradle to 8.9

---
 gradle/wrapper/gradle-wrapper.properties |  2 +-
 settings.gradle.kts                      | 99 +++++++++++++-----------
 2 files changed, 53 insertions(+), 48 deletions(-)

diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23..09523c0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index dbd3537..e0ca0fe 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,7 +8,7 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 plugins {
-    `gradle-enterprise`
+    id("com.gradle.develocity") version "3.17.6"
     id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0")
 }
 
@@ -23,58 +23,63 @@ include(
 )
 enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
 
-gradleEnterprise {
-    if (providers.environmentVariable("CI").isPresent) {
-        println("CI")
-        buildScan {
-            termsOfServiceUrl = "https://gradle.com/terms-of-service"
-            termsOfServiceAgree = "yes"
-            publishAlways()
-            tag("CI")
+develocity {
+    buildScan {
+        termsOfUseUrl = "https://gradle.com/terms-of-service"
+        termsOfUseAgree = "yes"
+        // publishAlways()
+        val isCI = providers.environmentVariable("CI").isPresent
+        publishing.onlyIf { isCI }
+        tag("CI")
 
-            if (providers.environmentVariable("GITHUB_ACTIONS").isPresent) {
-                link("GitHub Repository", "https://github.com/" + System.getenv("GITHUB_REPOSITORY"))
-                link(
-                    "GitHub Commit",
-                    "https://github.com/" + System.getenv("GITHUB_REPOSITORY") + "/commits/" + System.getenv("GITHUB_SHA")
-                )
+        buildScanPublished {
+            File("build-scan.txt").printWriter().use { writer ->
+                writer.println(buildScanUri)
+            }
+        }
 
+        if (providers.environmentVariable("GITHUB_ACTIONS").isPresent) {
+            link("GitHub Repository", "https://github.com/" + System.getenv("GITHUB_REPOSITORY"))
+            link(
+                "GitHub Commit",
+                "https://github.com/" + System.getenv("GITHUB_REPOSITORY") + "/commits/" + System.getenv("GITHUB_SHA")
+            )
 
-                listOf(
-                    "GITHUB_ACTION_REPOSITORY",
-                    "GITHUB_EVENT_NAME",
-                    "GITHUB_ACTOR",
-                    "GITHUB_BASE_REF",
-                    "GITHUB_HEAD_REF",
-                    "GITHUB_JOB",
-                    "GITHUB_REF",
-                    "GITHUB_REF_NAME",
-                    "GITHUB_REPOSITORY",
-                    "GITHUB_RUN_ID",
-                    "GITHUB_RUN_NUMBER",
-                    "GITHUB_SHA",
-                    "GITHUB_WORKFLOW"
-                ).forEach { e ->
-                    val v = System.getenv(e)
-                    if (v != null) {
-                        value(e, v)
-                    }
+
+            listOf(
+                "GITHUB_ACTION_REPOSITORY",
+                "GITHUB_EVENT_NAME",
+                "GITHUB_ACTOR",
+                "GITHUB_BASE_REF",
+                "GITHUB_HEAD_REF",
+                "GITHUB_JOB",
+                "GITHUB_REF",
+                "GITHUB_REF_NAME",
+                "GITHUB_REPOSITORY",
+                "GITHUB_RUN_ID",
+                "GITHUB_RUN_NUMBER",
+                "GITHUB_SHA",
+                "GITHUB_WORKFLOW"
+            ).forEach { e ->
+                val v = System.getenv(e)
+                if (v != null) {
+                    value(e, v)
                 }
+            }
 
-                providers.environmentVariable("GITHUB_SERVER_URL").orNull?.let { ghUrl ->
-                    val ghRepo = System.getenv("GITHUB_REPOSITORY")
-                    val ghRunId = System.getenv("GITHUB_RUN_ID")
-                    link("Summary", "$ghUrl/$ghRepo/actions/runs/$ghRunId")
-                    link("PRs", "$ghUrl/$ghRepo/pulls")
+            providers.environmentVariable("GITHUB_SERVER_URL").orNull?.let { ghUrl ->
+                val ghRepo = System.getenv("GITHUB_REPOSITORY")
+                val ghRunId = System.getenv("GITHUB_RUN_ID")
+                link("Summary", "$ghUrl/$ghRepo/actions/runs/$ghRunId")
+                link("PRs", "$ghUrl/$ghRepo/pulls")
 
-                    // see .github/workflows/build.yaml
-                    providers.environmentVariable("GITHUB_PR_NUMBER")
-                        .orNull
-                        .takeUnless { it.isNullOrBlank() }
-                        .let { prNumber ->
-                            link("PR", "$ghUrl/$ghRepo/pulls/$prNumber")
-                        }
-                }
+                // see .github/workflows/build.yaml
+                providers.environmentVariable("GITHUB_PR_NUMBER")
+                    .orNull
+                    .takeUnless { it.isNullOrBlank() }
+                    .let { prNumber ->
+                        link("PR", "$ghUrl/$ghRepo/pulls/$prNumber")
+                    }
             }
         }
     }