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 ed9852e  ui: eBPF profiling — stop flamegraph thrash + 
pin-auto-analyze + Intl time fmt
ed9852e is described below

commit ed9852efa758592c47fd066e5b6cc748e220fadd
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 08:19:56 2026 +0800

    ui: eBPF profiling — stop flamegraph thrash + pin-auto-analyze + Intl time 
fmt
    
    Three coupled fixes for /layer/:key/ebpf-profiling:
    
    1) Identity-churn re-render of the flamegraph.
       The template called `:trees="asProfileTrees()"` — a function returning
       a fresh `.map()` array on every parent reactive tick. The FlameGraph
       watches `props.trees` and re-runs its draw whenever the reference
       changes, so toolbar chip clicks, modal open/close, even keystrokes in
       the process-picker search box invalidated the cached draw.
       Convert to a `computed`. Reference is stable until `analyzeTrees`
       itself changes (i.e. only when an actual analyze response lands).
    
    2) Pinning / unpinning a process didn't auto-analyze.
       Toggling a row in the process picker only mutated local state; the
       operator had to click "Analyze" to refresh the graph. Add a deep
       watcher on `selectedProcessIds` that re-runs `runAnalyze`. Deep is
       required because `toggleProcessId` mutates the array in place
       (push / splice) — a shallow watch only fires on `.value` reassign.
    
       Other toolbar inputs (label chips, aggregate, display mode) stay
       manual on purpose — those are filter tweaks the operator typically
       stacks up before re-querying.
    
    3) `fmtTime` rebuilt on Intl.DateTimeFormat.
       The hand-rolled getFullYear/getMonth/getHours chain was already
       browser-local, but make the intent explicit via a shared
       Intl.DateTimeFormat (timeZone omitted = browser default). Format
       normalizes to `YYYY-MM-DD HH:mm:ss` so it matches the rest of the
       UI; locale conventions still drive the rendering.
    
       Note: OAP populates taskStartTime / createTime / schedule
       startTime+endTime via System.currentTimeMillis() (verified in
       EBPFProfilingMutationService.java / EBPFProfilingQueryService.java),
       so the wire values are true UTC epoch ms. No BFF-side TZ shift is
       needed; the browser handles the projection.
---
 .../src/layer/profiling/LayerEBPFProfilingView.vue | 57 ++++++++++++++++++----
 1 file changed, 48 insertions(+), 9 deletions(-)

diff --git a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
index 5e0bf0c..19282af 100644
--- a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
@@ -263,8 +263,15 @@ async function runAnalyze(): Promise<void> {
 // ProfileFlameGraph + ProfileStackTable expect the trace-profile element
 // shape `{id, parentId, codeSignature, count, duration, 
durationChildExcluded}`.
 // eBPF returns `{id, parentId, symbol, stackType, dumpCount}` — translate.
-function asProfileTrees(): ProfileAnalyzationTree[] {
-  return analyzeTrees.value.map((t) => ({
+//
+// MUST be a computed, not a function called from the template: the
+// FlameGraph re-renders whenever the `trees` prop's reference changes,
+// and a fresh `.map()` array returned by a function would make every
+// parent reactive tick (toolbar chip click, modal open, search input)
+// invalidate the cached draw. A computed gives a stable identity that
+// only changes when `analyzeTrees` itself changes.
+const profileTrees = computed<ProfileAnalyzationTree[]>(() =>
+  analyzeTrees.value.map((t) => ({
     elements: t.elements.map((e) => ({
       id: e.id,
       parentId: e.parentId,
@@ -273,8 +280,25 @@ function asProfileTrees(): ProfileAnalyzationTree[] {
       duration: e.dumpCount,
       durationChildExcluded: e.dumpCount,
     })),
-  }));
-}
+  })),
+);
+
+// Pinning / unpinning a process is a query-shape change; the operator
+// expects the flamegraph to follow that selection without having to
+// click Analyze. Other toolbar inputs (label chips, aggregate, display
+// mode) stay manual — they're filter-tweaks the operator typically
+// stacks up before re-querying. `pickTask` already nulls
+// `selectedProcessIds` via `resetFiltersForTask`; the watcher fires
+// after that reset with an empty list and harmlessly no-ops because
+// the matching set is unchanged from the task-pick analyze run.
+//
+// `deep: true` because toggleProcessId mutates the array in place
+// (push / splice). Without it the watcher only fires when the array
+// reference itself changes, which is never on pin/unpin.
+watch(selectedProcessIds, () => {
+  if (!schedules.value.length) return;
+  void runAnalyze();
+}, { deep: true });
 
 async function submitNewTask(): Promise<void> {
   if (!selectedId.value) {
@@ -308,11 +332,26 @@ async function submitNewTask(): Promise<void> {
   }
 }
 
+// OAP returns `taskStartTime` / `createTime` / schedule `startTime` and
+// `endTime` as standard 1970 ms timestamps (System.currentTimeMillis()
+// in EBPFProfilingMutationService). Render in the browser's local TZ
+// via Intl — explicit `timeZone` left at the browser default and a
+// stable formatter shared across rows (cheaper than a new Date split
+// + manual padding per cell).
+const TIME_FMT = new Intl.DateTimeFormat(undefined, {
+  year: 'numeric',
+  month: '2-digit',
+  day: '2-digit',
+  hour: '2-digit',
+  minute: '2-digit',
+  second: '2-digit',
+  hour12: false,
+});
 function fmtTime(ms: number): string {
   if (!ms) return '—';
-  const d = new Date(ms);
-  const z = (n: number) => String(n).padStart(2, '0');
-  return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())} 
${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`;
+  // Intl gives `2026/05/20, 17:00:00` on en-US — normalize to ISO-ish
+  // `2026-05-20 17:00:00` so the layout matches the rest of the UI.
+  return TIME_FMT.format(new Date(ms)).replace(/\//g, '-').replace(', ', ' ');
 }
 function attrLine(p: EBPFProcess): string {
   return (p.attributes ?? []).map((a) => `${a.name}=${a.value}`).join(' · ');
@@ -480,12 +519,12 @@ function toggleNewTaskLabel(l: string): void {
         <template v-if="analyzeTrees.length">
           <ProfileFlameGraph
             v-if="displayMode === 'flame'"
-            :trees="asProfileTrees()"
+            :trees="profileTrees"
             metric-key="count"
           />
           <ProfileStackTable
             v-else
-            :trees="asProfileTrees()"
+            :trees="profileTrees"
             :highlight-top="highlightTop"
             @toggle-highlight="highlightTop = !highlightTop"
           />

Reply via email to