This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new 03b911f  ui: fix flame-graph "% of root" + add selected-frame highlight
03b911f is described below

commit 03b911fd43cbd37791af309eca90f6341f52d913
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 20:47:50 2026 +0800

    ui: fix flame-graph "% of root" + add selected-frame highlight
    
    - "% of root" was always 0%: the denominator read the virtual-root
      node's `count`, which is initialized to 0 and never rolled up (only
      `value` is). Compute the real total as the sum of the top-level root
      frames, using the same metric that sizes the boxes (count/duration).
    - Click now pins a white outline on the selected frame (in addition to
      d3-flame-graph's default zoom), re-applied after the zoom transition
      rebuilds the visible frames; selection clears when the data changes.
    
    Applies to all four profiling widgets (trace / async / eBPF / pprof) —
    they share this one ProfileFlameGraph component.
---
 apps/ui/src/layer/profiling/ProfileFlameGraph.vue | 52 ++++++++++++++++++++---
 1 file changed, 46 insertions(+), 6 deletions(-)

diff --git a/apps/ui/src/layer/profiling/ProfileFlameGraph.vue 
b/apps/ui/src/layer/profiling/ProfileFlameGraph.vue
index 3f751b1..f7a52f4 100644
--- a/apps/ui/src/layer/profiling/ProfileFlameGraph.vue
+++ b/apps/ui/src/layer/profiling/ProfileFlameGraph.vue
@@ -56,6 +56,24 @@ const root = ref<HTMLDivElement | null>(null);
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 let chart: any = null;
 
+// Origin id of the click-selected frame. Drives the persistent outline
+// (the zoom itself is d3-flame-graph's default click behavior).
+const selectedOriginId = ref<string | null>(null);
+function applySelectionHighlight(): void {
+  if (!root.value) return;
+  const sel = selectedOriginId.value;
+  d3.select(root.value)
+    .selectAll<SVGGElement, { data?: { originId?: string } }>('g')
+    .each(function (d) {
+      const isSel = !!sel && d?.data?.originId === sel;
+      d3.select(this)
+        .select('rect')
+        .attr('stroke', isSel ? '#ffffff' : null)
+        .attr('stroke-width', isSel ? 2 : null)
+        .attr('stroke-opacity', isSel ? 0.95 : null);
+    });
+}
+
 function metricFor(el: ProfileAnalyzationElement): number {
   if (props.metricKey === 'duration') return Math.max(0, el.duration);
   return Math.max(0, el.count);
@@ -168,7 +186,17 @@ function draw(): void {
     });
 
   chart.tooltip(false);
+  // Click selects + zooms. d3-flame-graph zooms to the clicked frame by
+  // default (the "expand"); we additionally pin a persistent outline on
+  // the selected frame so the operator can see WHICH cell is focused —
+  // re-applied after the zoom transition rebuilds the visible frames.
+  chart.onClick((d: { data?: { originId?: string } }) => {
+    selectedOriginId.value = d?.data?.originId ?? null;
+    applySelectionHighlight();
+    window.setTimeout(applySelectionHighlight, 470);
+  });
   d3.select(root.value).datum(tree).call(chart);
+  applySelectionHighlight();
   const svg = root.value.querySelector('svg');
   if (svg) {
     // Hover a frame → render a cursor-following info card (悬浮信息).
@@ -204,15 +232,26 @@ const hoveredFrame = ref<FlameNode | null>(null);
 const tipPos = reactive({ x: 0, y: 0 });
 let svgHandlers: { svg: SVGElement; onMove: (e: MouseEvent) => void; onLeave: 
() => void } | null = null;
 
-const rootCountForPct = computed<number>(() => {
+// "% of root" denominator = the whole profile's total, measured with
+// the SAME metric that sizes the boxes (count for trace, duration for
+// async / eBPF / pprof). Each top-level frame's value is cumulative
+// (includes its descendants), so the profile total is the sum across
+// the top-level roots. (The virtual-root node's own `count`/`duration`
+// stay 0 — only `value` is rolled up for d3 layout — so we must sum the
+// children here rather than read the virtual root.)
+function frameMetric(n: FlameNode): number {
+  return props.metricKey === 'duration' ? n.duration : n.count;
+}
+const rootTotalForPct = computed<number>(() => {
   const tree = buildVirtualRoot();
-  return tree?.count ?? 0;
+  if (!tree?.children?.length) return 0;
+  return tree.children.reduce((s, c) => s + Math.max(0, frameMetric(c)), 0);
 });
 const hoveredPctRoot = computed<string>(() => {
   const f = hoveredFrame.value;
-  const total = rootCountForPct.value;
+  const total = rootTotalForPct.value;
   if (!f || total === 0) return '0';
-  return ((f.count / total) * 100).toFixed(2);
+  return ((frameMetric(f) / total) * 100).toFixed(2);
 });
 
 // Tip position with viewport-edge clamping. Width is capped via CSS
@@ -237,9 +276,10 @@ onMounted(() => {
   draw();
 });
 watch(() => [props.trees, props.metricKey], () => {
-  // Drop any stale hover state when the data changes — keeps the card
-  // from briefly pointing at a frame that no longer exists.
+  // Drop any stale hover + selection when the data changes — keeps the
+  // card and the outline from pointing at a frame that no longer exists.
   hoveredFrame.value = null;
+  selectedOriginId.value = null;
   draw();
 });
 onBeforeUnmount(() => {

Reply via email to