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"
/>