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 eeeef36  Satellite series labels, K8S_SERVICE network profiling, and 
async/pprof create-dialog fixes (#86)
eeeef36 is described below

commit eeeef36b71b1f9b11e34179d81d388853d6d07d8
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Sun Jun 28 23:42:21 2026 +0800

    Satellite series labels, K8S_SERVICE network profiling, and async/pprof 
create-dialog fixes (#86)
    
    ## Why
    
    A live-review pass against the public demo OAP surfaced a cluster of small 
defects in the dashboard render path and the per-layer profiling tabs, plus one 
missing capability. This bundles them.
    
    ## What
    
    **Dashboards**
    - **Satellite so11y series were all labelled `all`.** The dashboard parser 
named each labeled series off the *last* metric label. The Satellite 
event/queue metrics carry two labels — `pipe` (the series identity) and 
`status` (a constant aggregation dim whose value is `all`/`success`) — so every 
series collapsed onto the constant. Now named by the labels that *vary* across 
the result, dropping constant dims. Fixes Receive/Fetch Events, Queue 
Input/Output and the queue_io prefix; percen [...]
    
    **Profiling**
    - **K8S_SERVICE gains a Network Profiling tab** (Rover eBPF, the same 
capability Mesh has), documented on the layer page.
    - **Create dialogs**: the post-create hint counts down to a single list 
refresh (`refreshing in Ns`) instead of an opaque `N/4` counter; **Escape** now 
closes the Async and pprof create dialogs (parity with Trace/eBPF/Network); 
**Analyze** stays disabled until at least one instance is selected (Async, 
pprof).
    - **Async event-type picker** is derived from the selected task's captured 
events — only the JFR trees those events actually produce — instead of a static 
list of all five. Drops `PROFILER_LIVE_OBJECT` (no create-modal event produces 
it; needs async-profiler `--live`) and allocation types on non-ALLOC tasks, 
which rendered an empty graph. Mirrors booster-ui's behaviour.
---
 CHANGELOG.md                                       |  8 +++++
 .../src/bundled_templates/layers/k8s_service.json  |  1 +
 apps/bff/src/logic/dashboard/parsers.ts            | 30 +++++++++++++----
 .../layer/profiling/LayerAsyncProfilingView.vue    | 39 +++++++++++++++++-----
 .../src/layer/profiling/LayerEBPFProfilingView.vue |  4 +--
 .../layer/profiling/LayerNetworkProfilingView.vue  |  4 +--
 .../layer/profiling/LayerPprofProfilingView.vue    |  8 +++--
 .../layer/profiling/LayerTraceProfilingView.vue    |  4 +--
 apps/ui/src/layer/profiling/useEBPFProfiling.ts    |  4 +--
 apps/ui/src/layer/profiling/useNewTaskPoll.ts      | 18 +++++++---
 docs/dashboards/k8s_service.md                     | 10 +++---
 docs/dashboards/so11y_satellite.md                 |  2 +-
 12 files changed, 97 insertions(+), 35 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0398ddb..51b9b5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,12 @@ The version line is shared by every package in the monorepo 
(apps + shared packa
 
 - **pprof and async-profiling tasks open a detail modal with their captured 
logs.**
 
+- **K8S_SERVICE gains a Network Profiling tab.** Kubernetes services, already 
observed by SkyWalking Rover's eBPF probes, now expose Network Profiling — pick 
a pod and capture the process-to-process network conversations as a topology, 
the same capability the Mesh layer offers.
+
+- **Profiling create dialogs are clearer and harder to misuse.** After a 
create the hint counts down to its single list refresh (`refreshing in Ns`), 
Escape closes the Async and pprof create dialogs, and Analyze stays disabled 
until at least one instance is selected.
+
+- **The Async result's event-type picker shows only what the task captured.** 
It lists just the JFR trees the selected task's events produce 
(EXECUTION_SAMPLE, LOCK, OBJECT_ALLOCATION_*), dropping options like 
PROFILER_LIVE_OBJECT that no Horizon-created task can produce — so you can't 
pick a type that renders an empty graph.
+
 ### Alarms
 
 - **The alarm timeline reads more clearly** — a clearer selection band and 
legend, and the detail sidebar reflows cleanly on narrow windows. Hovering the 
timeline now hints both affordances — click a minute to filter, or drag across 
the timeline to select a range — so range-selection is no longer hidden.
@@ -44,6 +50,8 @@ The version line is shared by every package in the monorepo 
(apps + shared packa
 
 - **The Kubernetes Node dashboard gains a Pod Total card.** A compact card now 
sits directly under Node Status showing the current count of pods scheduled on 
the selected node (all phases) — the latest value of the same metric the "Pods 
on Node" trend already charts — so the space beside the status card is no 
longer blank.
 
+- **Satellite event and queue widgets break out per pipeline.** The 
SO11Y_SATELLITE Receive Events, Fetch Events, Queue Input / Output, and Queue 
Used widgets now label each series by its Satellite pipeline (`tracingpipe`, 
`jvmpipe`, `logpipe`, …) instead of collapsing every line onto a single `all`, 
so you can see which collection pipeline drives the rate.
+
 ### Performance & behavior tuning
 
 - **New `performance` section in `horizon.yaml`.** Tune how hard the BFF fans 
metric queries out to OAP — per-route bulk (request) sizes and concurrency for 
the topology, 3D-map, landing, and dashboard fan-outs — plus protective caps: 
the service-map render valve (`topologyMaxNodes` / `topologyMaxEdges`) and 
per-request record caps for traces / logs / browser logs. Operational, 
hot-reloaded, per-deployment; defaults match the previous built-in values, so 
the whole block is optional. Rais [...]
diff --git a/apps/bff/src/bundled_templates/layers/k8s_service.json 
b/apps/bff/src/bundled_templates/layers/k8s_service.json
index 8a541c5..2df29cc 100644
--- a/apps/bff/src/bundled_templates/layers/k8s_service.json
+++ b/apps/bff/src/bundled_templates/layers/k8s_service.json
@@ -24,6 +24,7 @@
     "traces": false,
     "logs": false,
     "ebpfProfiling": true,
+    "networkProfiling": true,
     "podLogs": true
   },
   "layer-header": {
diff --git a/apps/bff/src/logic/dashboard/parsers.ts 
b/apps/bff/src/logic/dashboard/parsers.ts
index 14d432c..a995480 100644
--- a/apps/bff/src/logic/dashboard/parsers.ts
+++ b/apps/bff/src/logic/dashboard/parsers.ts
@@ -55,8 +55,23 @@ export function parseLabeledSeries(
   fallbackLabel: string,
 ): Array<{ label: string; data: Array<number | null> }> | null {
   if (!r || r.error) return null;
+  const results = r.results ?? [];
+  // Name each series by its DISCRIMINATING label(s). A labeled metric can
+  // carry both an identity dimension (`pipe` / `pipeline`) and a constant
+  // aggregation dimension (`status='all'`); naming off the last label alone
+  // collapses every series onto the constant. Keep only labels whose value
+  // varies across the whole result, then join them for the series name.
+  const varyingKeys = new Set<string>();
+  {
+    const keys = new Set<string>();
+    for (const rs of results) for (const l of rs.metric?.labels ?? []) 
keys.add(l.key);
+    for (const key of keys) {
+      const vals = new Set(results.map((rs) => (rs.metric?.labels ?? 
[]).find((l) => l.key === key)?.value));
+      if (vals.size > 1) varyingKeys.add(key);
+    }
+  }
   const out: Array<{ label: string; data: Array<number | null> }> = [];
-  for (const rs of r.results ?? []) {
+  for (const rs of results) {
     const values = rs.values ?? [];
     if (values.length === 0) continue;
     const data = values.map((v) => {
@@ -64,12 +79,15 @@ export function parseLabeledSeries(
       const n = Number(v.value);
       return Number.isFinite(n) ? n : null;
     });
-    // For relabels() results OAP returns multi-result responses with
-    // metric.labels populated — take the last (most-derived) label
-    // value, e.g. `percentile='99'`. Single-series results have no
-    // labels; fall back to the operator's expression text.
+    // Series name = the varying label(s). Single-label relabels()/percentile
+    // (`p='99'`) keep their lone varying label; a constant-only or unlabeled
+    // series falls back to the last label, then the expression text.
     const labels = rs.metric?.labels ?? [];
-    const lbl = labels.length > 0 ? labels[labels.length - 1].value : 
fallbackLabel;
+    const varying = labels.filter((l) => varyingKeys.has(l.key));
+    const lbl =
+      varying.length > 0 ? varying.map((l) => l.value).join(', ')
+      : labels.length > 0 ? labels[labels.length - 1].value
+      : fallbackLabel;
     out.push({ label: lbl, data });
   }
   return out.length > 0 ? out : null;
diff --git a/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
index 01c6f17..7047712 100644
--- a/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
@@ -42,6 +42,7 @@ import ProfileFlameGraph from 
'@/layer/profiling/ProfileFlameGraph.vue';
 import AsyncProfilingTaskDetailModal from 
'@/layer/profiling/AsyncProfilingTaskDetailModal.vue';
 import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
 import Icon from '@/components/icons/Icon.vue';
+import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
 
 const { t } = useI18n();
 const route = useRoute();
@@ -71,6 +72,7 @@ const analyzeError = ref<string | null>(null);
 const analyzeLoading = ref(false);
 
 const showNewTask = ref(false);
+useEscapeToClose(() => showNewTask.value, () => (showNewTask.value = false));
 const newTask = reactive({
   instances: [] as string[],
   duration: 60,
@@ -78,7 +80,7 @@ const newTask = reactive({
   execArgs: '',
 });
 const newTaskError = ref<string | null>(null);
-const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
+const { polling, countdown, pollForNewTask } = useNewTaskPoll();
 
 const DURATION_OPTS = [
   { v: 30, label: '30 sec' },
@@ -88,6 +90,30 @@ const DURATION_OPTS = [
   { v: 900, label: '15 min' },
 ];
 const EVENTS: AsyncProfilingEvent[] = ['CPU', 'ALLOC', 'LOCK', 'WALL', 
'CTIMER', 'ITIMER'];
+// Each capture event produces one or more JFR trees (the analyze result holds
+// one tree per type). The result-side EVENT TYPE picker offers only the types
+// THIS task captured — never PROFILER_LIVE_OBJECT (no capture event above
+// produces it). ALLOC yields both the in- and outside-TLAB allocation trees.
+const EVENT_TO_JFR_TYPES: Record<string, string[]> = {
+  CPU: ['EXECUTION_SAMPLE'],
+  WALL: ['EXECUTION_SAMPLE'],
+  CTIMER: ['EXECUTION_SAMPLE'],
+  ITIMER: ['EXECUTION_SAMPLE'],
+  LOCK: ['LOCK'],
+  ALLOC: ['OBJECT_ALLOCATION_IN_NEW_TLAB', 'OBJECT_ALLOCATION_OUTSIDE_TLAB'],
+};
+const availableEventTypes = computed<Array<{ value: string; label: string 
}>>(() => {
+  const seen = new Set<string>();
+  const out: Array<{ value: string; label: string }> = [];
+  for (const ev of currentTask.value?.events ?? []) {
+    for (const jfr of EVENT_TO_JFR_TYPES[ev] ?? []) {
+      if (seen.has(jfr)) continue;
+      seen.add(jfr);
+      out.push({ value: jfr, label: jfr === 'EXECUTION_SAMPLE' ? 
'EXECUTION_SAMPLE (CPU/Wall/Timer)' : jfr });
+    }
+  }
+  return out;
+});
 
 watch(
   () => layerKey.value + '|' + (serviceId.value ?? ''),
@@ -260,7 +286,7 @@ function instanceName(id: string): string {
           >+ New Task</button>
         </div>
       </div>
-      <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
+      <div v-if="polling" class="poll-hint">Registering new task… refreshing 
in {{ countdown }}s</div>
       <div v-if="tasksError" class="side-err">{{ tasksError }}</div>
       <div v-else-if="tasksLoading && !tasks.length" 
class="side-empty">Loading…</div>
       <div v-else-if="!tasks.length" class="side-empty">
@@ -310,16 +336,13 @@ function instanceName(id: string): string {
         <div class="tb-block">
           <label class="lbl">Event type</label>
           <select v-model="eventType" class="sel">
-            <option value="EXECUTION_SAMPLE">EXECUTION_SAMPLE 
(CPU/Wall/Timer)</option>
-            <option value="LOCK">LOCK</option>
-            <option 
value="OBJECT_ALLOCATION_IN_NEW_TLAB">OBJECT_ALLOCATION_IN_NEW_TLAB</option>
-            <option 
value="OBJECT_ALLOCATION_OUTSIDE_TLAB">OBJECT_ALLOCATION_OUTSIDE_TLAB</option>
-            <option value="PROFILER_LIVE_OBJECT">PROFILER_LIVE_OBJECT</option>
+            <option v-for="o in availableEventTypes" :key="o.value" 
:value="o.value">{{ o.label }}</option>
           </select>
         </div>
         <button
           class="btn-primary"
-          :disabled="analyzeLoading || !currentTask"
+          :disabled="analyzeLoading || !currentTask || 
!selectedInstances.length"
+          :title="!selectedInstances.length ? 'Select at least one instance' : 
'Analyze the selected instances'"
           @click="runAnalyze"
         >{{ analyzeLoading ? 'Analyzing…' : 'Analyze' }}</button>
       </div>
diff --git a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
index d6f6874..23acec7 100644
--- a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
@@ -113,7 +113,7 @@ const {
   newTaskError,
   submitNewTask,
   polling,
-  pollRound,
+  countdown,
 } = useEBPFProfiling(layerKey, selectedId);
 
 // Display-only toggles owned by the view.
@@ -182,7 +182,7 @@ function onPickTask(t: EBPFTask): void {
           >+ New Task</button>
         </div>
       </div>
-      <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
+      <div v-if="polling" class="poll-hint">Registering new task… refreshing 
in {{ countdown }}s</div>
       <div v-if="tasksError" class="side-err">{{ tasksError }}</div>
       <div v-else-if="tasksLoading && !tasks.length" 
class="side-empty">Loading…</div>
       <div v-else-if="!tasks.length" class="side-empty">
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index c29a047..e7c1434 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -281,7 +281,7 @@ useEscapeToClose(() => showNewTask.value, () => 
(showNewTask.value = false));
 const newTaskError = ref<string | null>(null);
 const { processes: networkProcesses, loading: processesLoading } =
   useNetworkProcesses(selectedInstanceId, showNewTask);
-const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
+const { polling, countdown, pollForNewTask } = useNewTaskPoll();
 // requireComplete{Request,Response} are Boolean! — every sampling row must
 // carry the settings block or create 400s.
 const DEFAULT_SETTINGS = (): NetworkProfilingSampling['settings'] => ({
@@ -362,7 +362,7 @@ function fmtTime(ms: number): string {
           >+ New Task</button>
         </div>
       </div>
-      <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
+      <div v-if="polling" class="poll-hint">Registering new task… refreshing 
in {{ countdown }}s</div>
       <div v-if="tasksError" class="side-err">{{ tasksError }}</div>
       <div v-else-if="tasksLoading && !tasks.length" 
class="side-empty">Loading…</div>
       <div v-else-if="!tasks.length" class="side-empty">
diff --git a/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
index 0259bac..8c0d366 100644
--- a/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
@@ -41,6 +41,7 @@ import ProfileFlameGraph from 
'@/layer/profiling/ProfileFlameGraph.vue';
 import PprofTaskDetailModal from '@/layer/profiling/PprofTaskDetailModal.vue';
 import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
 import Icon from '@/components/icons/Icon.vue';
+import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
 
 const { t } = useI18n();
 const route = useRoute();
@@ -69,6 +70,7 @@ const taskDetailFor = ref<PprofTask | null>(null);
 const taskDetailLogs = ref<AsyncProfilingProgressLog[]>([]);
 
 const showNewTask = ref(false);
+useEscapeToClose(() => showNewTask.value, () => (showNewTask.value = false));
 const newTask = reactive({
   instances: [] as string[],
   // OAP measures pprof duration in MINUTES (capped at 15).
@@ -81,7 +83,7 @@ const newTask = reactive({
   dumpPeriod: 1,
 });
 const newTaskError = ref<string | null>(null);
-const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
+const { polling, countdown, pollForNewTask } = useNewTaskPoll();
 
 const PPROF_EVENTS = ['CPU', 'HEAP', 'BLOCK', 'GOROUTINE', 'MUTEX', 'ALLOCS', 
'THREADCREATE'];
 // Per OAP: duration applies to CPU/BLOCK/MUTEX; dumpPeriod to BLOCK/MUTEX.
@@ -239,7 +241,7 @@ function instanceName(id: string): string {
           <button class="btn-new" :disabled="!serviceId" :title="serviceId ? 
'Create a new pprof task' : 'Pick a service first'" @click="showNewTask = 
true">+ New Task</button>
         </div>
       </div>
-      <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
+      <div v-if="polling" class="poll-hint">Registering new task… refreshing 
in {{ countdown }}s</div>
       <div v-if="tasksError" class="side-err">{{ tasksError }}</div>
       <div v-else-if="tasksLoading && !tasks.length" 
class="side-empty">Loading…</div>
       <div v-else-if="!tasks.length" class="side-empty">
@@ -292,7 +294,7 @@ function instanceName(id: string): string {
           <label class="lbl">Event</label>
           <span class="event-fixed">{{ currentTask?.events ?? '—' }}</span>
         </div>
-        <button class="btn-primary" :disabled="analyzeLoading || !currentTask" 
@click="runAnalyze">
+        <button class="btn-primary" :disabled="analyzeLoading || !currentTask 
|| !selectedInstances.length" :title="!selectedInstances.length ? 'Select at 
least one instance' : 'Analyze the selected instances'" @click="runAnalyze">
           {{ analyzeLoading ? 'Analyzing…' : 'Analyze' }}
         </button>
       </div>
diff --git a/apps/ui/src/layer/profiling/LayerTraceProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerTraceProfilingView.vue
index c49ca37..423f2d8 100644
--- a/apps/ui/src/layer/profiling/LayerTraceProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerTraceProfilingView.vue
@@ -103,7 +103,7 @@ const tasks = ref<ProfileTask[]>([]);
 const tasksError = ref<string | null>(null);
 const tasksLoading = ref(false);
 const currentTask = ref<ProfileTask | null>(null);
-const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
+const { polling, countdown, pollForNewTask } = useNewTaskPoll();
 
 const segments = ref<ProfileSegment[]>([]);
 const segmentsLoading = ref(false);
@@ -310,7 +310,7 @@ function fmtTime(ms: number): string {
             >+ New Task</button>
           </div>
         </div>
-        <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
+        <div v-if="polling" class="poll-hint">Registering new task… refreshing 
in {{ countdown }}s</div>
         <div v-if="tasksError" class="side-err">{{ tasksError }}</div>
         <div v-else-if="tasksLoading && !tasks.length" 
class="side-empty">Loading…</div>
         <div v-else-if="!tasks.length" class="side-empty">
diff --git a/apps/ui/src/layer/profiling/useEBPFProfiling.ts 
b/apps/ui/src/layer/profiling/useEBPFProfiling.ts
index 3048f8b..10af2a9 100644
--- a/apps/ui/src/layer/profiling/useEBPFProfiling.ts
+++ b/apps/ui/src/layer/profiling/useEBPFProfiling.ts
@@ -67,7 +67,7 @@ export function useEBPFProfiling(layerKey: Ref<string>, 
selectedId: Ref<string |
   const analyzeLoading = ref(false);
 
   const newTaskError = ref<string | null>(null);
-  const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
+  const { polling, countdown, pollForNewTask } = useNewTaskPoll();
 
   watch(
     () => layerKey.value + '|' + (selectedId.value ?? ''),
@@ -299,6 +299,6 @@ export function useEBPFProfiling(layerKey: Ref<string>, 
selectedId: Ref<string |
     newTaskError,
     submitNewTask,
     polling,
-    pollRound,
+    countdown,
   };
 }
diff --git a/apps/ui/src/layer/profiling/useNewTaskPoll.ts 
b/apps/ui/src/layer/profiling/useNewTaskPoll.ts
index 66fd680..7decdbf 100644
--- a/apps/ui/src/layer/profiling/useNewTaskPoll.ts
+++ b/apps/ui/src/layer/profiling/useNewTaskPoll.ts
@@ -20,18 +20,18 @@
  * (trace / async / eBPF cpu / eBPF network / pprof). OAP acks a task
  * creation before the task is queryable — the task has to propagate to
  * the task-list query, which can take several seconds. So after a create
- * we refresh the list repeatedly until the new task shows up rather than
+ * we count down ~10s once and refresh the list a single time, rather than
  * leaving the operator looking at the stale pre-create list.
  *
  * Detection is id-based: capture the visible task ids *before* the create,
- * then after each refresh look for any id that wasn't there before. That
+ * then after the refresh look for any id that wasn't there before. That
  * covers both "empty → first task" and "a newer task appended on top of
  * existing ones", without depending on per-family timestamp field names.
  */
 
 import { ref } from 'vue';
 
-export const POLL_ROUNDS = 4;
+export const POLL_ROUNDS = 1;
 export const POLL_INTERVAL_MS = 10_000;
 
 export function useNewTaskPoll() {
@@ -39,6 +39,8 @@ export function useNewTaskPoll() {
   const polling = ref(false);
   /** 1-based round currently in flight (0 when idle). */
   const pollRound = ref(0);
+  /** Seconds until the next refresh (0 when idle) — drives the visible 
countdown. */
+  const countdown = ref(0);
 
   async function pollForNewTask(opts: {
     /** Task ids visible before the create call. */
@@ -56,7 +58,12 @@ export function useNewTaskPoll() {
     try {
       for (let i = 1; i <= POLL_ROUNDS; i++) {
         pollRound.value = i;
-        await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
+        // Count the inter-refresh wait down by the second so the operator sees
+        // the next check approaching, not an opaque round number.
+        for (let s = POLL_INTERVAL_MS / 1000; s >= 1; s--) {
+          countdown.value = s;
+          await new Promise((resolve) => setTimeout(resolve, 1000));
+        }
         await opts.refresh();
         if (appeared()) return true;
       }
@@ -64,8 +71,9 @@ export function useNewTaskPoll() {
     } finally {
       polling.value = false;
       pollRound.value = 0;
+      countdown.value = 0;
     }
   }
 
-  return { polling, pollRound, pollForNewTask };
+  return { polling, pollRound, countdown, pollForNewTask };
 }
diff --git a/docs/dashboards/k8s_service.md b/docs/dashboards/k8s_service.md
index 4f61b9f..0259d17 100644
--- a/docs/dashboards/k8s_service.md
+++ b/docs/dashboards/k8s_service.md
@@ -18,7 +18,7 @@ limitations under the License.
 
 The **K8S_SERVICE** layer monitors the network behavior of Kubernetes 
services, observed at the kernel level by SkyWalking Rover's eBPF probes. It 
captures the HTTP and TCP traffic flowing in and out of each service's pods — 
call rate, latency, status codes, header / body sizes, packet counts, and 
connection activity — without instrumenting the application. It belongs to the 
**Kubernetes** layer group.
 
-In Horizon's sidebar this layer is named **Kubernetes Services**. Its services 
are listed as **K8s services**, instances as **Pods**, and endpoints as 
**Endpoints** — service names are grouped and displayed by their Kubernetes 
namespace. The K8S_SERVICE layer enables the Service, Pod, Endpoint, Topology, 
eBPF Profiling, and Pod Logs sub-tabs. It does not enable an 
endpoint-dependency map, Traces, or Logs.
+In Horizon's sidebar this layer is named **Kubernetes Services**. Its services 
are listed as **K8s services**, instances as **Pods**, and endpoints as 
**Endpoints** — service names are grouped and displayed by their Kubernetes 
namespace. The K8S_SERVICE layer enables the Service, Pod, Endpoint, Topology, 
eBPF Profiling, Network Profiling, and Pod Logs sub-tabs. It does not enable an 
endpoint-dependency map, Traces, or Logs.
 
 This page is the **operator reference** for the bundled K8S_SERVICE dashboard: 
what you see on each scope and what each widget means.
 
@@ -120,11 +120,13 @@ The K8S_SERVICE layer ships a service topology with an 
instance-level drill-down
 
 For how these maps are read and navigated, see the [3D Infrastructure 
Map](../operate/infra-3d-map.md) for the cross-layer view, and the topology 
section of [Layer Dashboard Templates](../customization/layer-templates.md) for 
how the node and edge metrics are configured.
 
-## eBPF Profiling and Pod Logs
+## eBPF Profiling, Network Profiling, and Pod Logs
 
-Because K8S_SERVICE data comes from eBPF probes, this layer also enables two 
investigation tabs alongside the dashboards:
+Because K8S_SERVICE data comes from eBPF probes, this layer also enables three 
investigation tabs alongside the dashboards:
 
-- **eBPF Profiling** — on-CPU / off-CPU and network profiling tasks targeted 
at a selected service, with the flame-graph and span-attached results 
SkyWalking Rover reports.
+- **eBPF Profiling** — on-CPU / off-CPU profiling tasks targeted at a selected 
service, with the flame-graph and span-attached results SkyWalking Rover 
reports.
+
+- **Network Profiling** — the process-to-process network conversations within 
the service, rendered as a process-level topology, captured by SkyWalking Rover 
on a selected pod.
 
 - **Pod Logs** — the container logs collected from the service's pods, with 
the same filtering and search the logs surface provides elsewhere.
 
diff --git a/docs/dashboards/so11y_satellite.md 
b/docs/dashboards/so11y_satellite.md
index 633c1fd..adf0cbc 100644
--- a/docs/dashboards/so11y_satellite.md
+++ b/docs/dashboards/so11y_satellite.md
@@ -30,7 +30,7 @@ The layer landing page lists every Satellite service that has 
reported. This lay
 
 ## Service dashboard
 
-The dashboard for one selected Satellite collector. Every widget is a 
time-series line over the selected window, covering the collector's connection 
load, host CPU, internal queue, and the four stages of its event pipeline.
+The dashboard for one selected Satellite collector. Every widget is a 
time-series line over the selected window, covering the collector's connection 
load, host CPU, internal queue, and the four stages of its event pipeline. The 
queue and event widgets break their series out **per Satellite pipeline** 
(`tracingpipe`, `jvmpipe`, `logpipe`, `meterpipe`, …), so you can see which 
collection pipeline is driving the rate; Connection Count and CPU are single 
series.
 
 - **Connection Count** — the number of gRPC connections the collector 
currently holds, i.e. how many upstream agents and downstream OAP links are 
attached (`satellite_service_grpc_connect_count`).
 

Reply via email to