Skip to content

Commit 616216b

Browse files
authored
feat(profiling): Hide frames, Find frame and Pop out on eBPF, Async and pprof profiling (#209)
* feat(profiling): Hide frames, Find frame and Pop out on eBPF, Async and pprof profiling The bar above the Trace Profiling result now sits above every stack result: eBPF Profiling (Flame | Tree, moved out of its toolbar), Async Profiling and pprof (flame graph). The panel owns its pop-out, so every tab gets the dialog from one place, and takes the views, the flame metric and the duration unit from the page. The hidden-frame list is shared by the four tabs; presets are offered where they match a frame — the Java ones for async-profiler stacks, the Go ones for pprof. - eBPF self values are each frame's total dump count minus its children's. OAP reports totals only, and the Tree used to show the total as self, so Self Duration equalled Duration, top slow ranked the outermost frames and a folded group of hidden frames counted the same samples once per frame. - Async Profiling and pprof load their task list once the layer's service roster resolves the selected service; opened directly, they used to ask before the roster loaded and never again, so they showed no tasks. - The profiling component tests unmount after each test, so a floating panel's document listener no longer wakes against a later test's DOM. * fix(profiling): load Trace, eBPF and Network Profiling tasks once the service resolves Like Async Profiling and pprof, these tabs loaded their tasks on a change of the selected service id but sent the request only once the layer's roster had resolved that id to a service. Opened from a link naming a service, the load could run before the roster arrived, return early and never run again, so the tab listed no tasks. The load is now keyed on the resolved service.
1 parent cef6015 commit 616216b

13 files changed

Lines changed: 276 additions & 121 deletions

‎apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import type {
3737
AsyncProfilingTree,
3838
ProfileAnalyzationTree,
3939
} from '@/api/client';
40-
import ProfileFlameGraph from '@/layer/profiling/ProfileFlameGraph.vue';
40+
import ProfileStackPanel from '@/layer/profiling/ProfileStackPanel.vue';
41+
import { useHiddenFramePatterns } from '@/layer/profiling/useHiddenFramePatterns';
4142
import AsyncProfilingTaskDetailModal from '@/layer/profiling/AsyncProfilingTaskDetailModal.vue';
4243
import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
4344
import Icon from '@/components/icons/Icon.vue';
@@ -66,6 +67,9 @@ const taskDetailLogs = ref<AsyncProfilingProgressLog[]>([]);
6667
const selectedInstances = ref<string[]>([]);
6768
const eventType = ref<string>('EXECUTION_SAMPLE');
6869
const tree = ref<AsyncProfilingTree | null>(null);
70+
const displayMode = ref<'tree' | 'flame'>('flame');
71+
const hidePatterns = useHiddenFramePatterns();
72+
const frameSearch = ref('');
6973
const analyzeError = ref<string | null>(null);
7074
const analyzeLoading = ref(false);
7175
@@ -131,7 +135,10 @@ watch(currentTask, () => {
131135
});
132136
133137
watch(
134-
() => layerKey.value + '|' + (serviceId.value ?? ''),
138+
// Keyed on the service as the layer's roster resolves it, not the bare
139+
// selected id: opened directly, the tab knows the id before the roster has
140+
// loaded, and a load keyed on the id never ran again once it had.
141+
() => layerKey.value + '|' + (service.value?.id ?? ''),
135142
() => {
136143
taskDetailFor.value = null; // drop the detail modal on context change
137144
void refreshTasks();
@@ -386,10 +393,14 @@ function instanceCount(n: number): string {
386393
<div v-if="analyzeError" class="banner err">{{ analyzeError }}</div>
387394

388395
<div class="result">
389-
<ProfileFlameGraph
396+
<ProfileStackPanel
390397
v-if="profileTrees.length"
398+
v-model:display-mode="displayMode"
399+
v-model:hide-patterns="hidePatterns"
400+
v-model:search="frameSearch"
391401
:trees="profileTrees"
392-
metric-key="count"
402+
:views="['flame']"
403+
:popout-title="t('Async Profiling') + (serviceName ? ' · ' + serviceName : '')"
393404
/>
394405
<div v-else-if="!analyzeLoading" class="result-empty">
395406
{{ currentTask ? t('Pick instances + event type, then click Analyze.') : t('Select a task to inspect its async profile.') }}
@@ -715,6 +726,8 @@ function instanceCount(n: number): string {
715726
min-height: 0;
716727
position: relative;
717728
overflow: hidden;
729+
display: flex;
730+
flex-direction: column;
718731
}
719732
.result-empty {
720733
position: absolute;

‎apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue‎

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import { useLayerLanding } from '@/layer/useLayerLanding';
4141
import { useLayerServiceName } from '@/layer/useLayerServiceName';
4242
import { useSetupStore } from '@/state/setup';
4343
import type { EBPFTask, LayerDef } from '@/api/client';
44-
import ProfileFlameGraph from '@/layer/profiling/ProfileFlameGraph.vue';
45-
import ProfileStackTable from '@/layer/profiling/ProfileStackTable.vue';
44+
import ProfileStackPanel from '@/layer/profiling/ProfileStackPanel.vue';
45+
import { useHiddenFramePatterns } from '@/layer/profiling/useHiddenFramePatterns';
4646
import EBPFProcessPicker from '@/layer/profiling/EBPFProcessPicker.vue';
4747
import NewEBPFTaskModal from '@/layer/profiling/NewEBPFTaskModal.vue';
4848
import {
@@ -121,6 +121,8 @@ const {
121121
// Display-only toggles owned by the view.
122122
const displayMode = ref<'flame' | 'tree'>('flame');
123123
const highlightTop = ref(false);
124+
const hidePatterns = useHiddenFramePatterns();
125+
const frameSearch = ref('');
124126
125127
const showNewTask = ref(false);
126128
@@ -252,24 +254,6 @@ function onPickTask(task: EBPFTask): void {
252254
>{{ t('Duration') }}</button>
253255
</div>
254256
</div>
255-
<div class="tb-block">
256-
<label class="lbl">{{ t('Display') }}</label>
257-
<div class="display-row">
258-
<div class="seg">
259-
<button :class="{ on: displayMode === 'flame' }" @click="displayMode = 'flame'">{{ t('Flame') }}</button>
260-
<button :class="{ on: displayMode === 'tree' }" @click="displayMode = 'tree'">{{ t('Tree') }}</button>
261-
</div>
262-
<button
263-
v-if="displayMode === 'tree'"
264-
type="button"
265-
class="top-slow"
266-
:class="{ on: highlightTop }"
267-
:aria-pressed="highlightTop"
268-
:title="t('Highlight top 10 slow methods')"
269-
@click="highlightTop = !highlightTop"
270-
>{{ t('top slow') }}</button>
271-
</div>
272-
</div>
273257
<EBPFProcessPicker v-model="selectedProcessIds" :processes="processes" />
274258
<button
275259
class="btn-primary"
@@ -289,18 +273,16 @@ function onPickTask(task: EBPFTask): void {
289273

290274
<div class="result">
291275
<div v-if="analyzeTip" class="tip">{{ analyzeTip }}</div>
292-
<template v-if="analyzeTrees.length">
293-
<ProfileFlameGraph
294-
v-if="displayMode === 'flame'"
295-
:trees="profileTrees"
296-
metric-key="count"
297-
/>
298-
<ProfileStackTable
299-
v-else
300-
:trees="profileTrees"
301-
v-model:highlight-top="highlightTop"
302-
/>
303-
</template>
276+
<ProfileStackPanel
277+
v-if="analyzeTrees.length"
278+
v-model:display-mode="displayMode"
279+
v-model:hide-patterns="hidePatterns"
280+
v-model:highlight-top="highlightTop"
281+
v-model:search="frameSearch"
282+
:trees="profileTrees"
283+
:views="['flame', 'tree']"
284+
:popout-title="t('eBPF Profiling') + (serviceName ? ' · ' + serviceName : '')"
285+
/>
304286
<div v-else-if="!analyzeTip && !analyzeLoading" class="result-empty">
305287
{{
306288
currentTask
@@ -546,29 +528,6 @@ function onPickTask(task: EBPFTask): void {
546528
background: var(--sw-accent);
547529
color: #fff;
548530
}
549-
.display-row {
550-
display: inline-flex;
551-
align-items: center;
552-
gap: 6px;
553-
}
554-
.top-slow {
555-
font-family: var(--sw-sans);
556-
font-size: 11.5px;
557-
font-weight: 500;
558-
height: 24px;
559-
padding: 0 10px;
560-
border-radius: 4px;
561-
border: 1px solid var(--sw-line-2);
562-
background: var(--sw-bg-2);
563-
color: var(--sw-fg-1);
564-
cursor: pointer;
565-
white-space: nowrap;
566-
}
567-
.top-slow.on {
568-
background: var(--sw-accent-soft);
569-
border-color: var(--sw-accent-line);
570-
color: var(--sw-accent);
571-
}
572531
.btn-primary {
573532
background: var(--sw-accent);
574533
color: #fff;

‎apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ const currentTask = ref<EBPFTask | null>(null);
9696
const topologyLoading = ref(false);
9797
9898
// Tasks are listed per SERVICE, not the selected instance — a task's pod
99-
// may have been replaced; the task carries its own serviceInstanceId.
99+
// may have been replaced; the task carries its own serviceInstanceId. Keyed on
100+
// the service the roster resolves, not the bare selected id: opened from a
101+
// link, the id is known before the roster loads.
100102
watch(
101-
() => layerKey.value + '|' + (serviceId.value ?? ''),
103+
() => layerKey.value + '|' + (service.value?.id ?? ''),
102104
() => void refreshTasks(),
103105
{ immediate: true },
104106
);

‎apps/ui/src/layer/profiling/LayerPprofProfilingView.vue‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import type {
3636
PprofTree,
3737
ProfileAnalyzationTree,
3838
} from '@/api/client';
39-
import ProfileFlameGraph from '@/layer/profiling/ProfileFlameGraph.vue';
39+
import ProfileStackPanel from '@/layer/profiling/ProfileStackPanel.vue';
40+
import { useHiddenFramePatterns } from '@/layer/profiling/useHiddenFramePatterns';
4041
import PprofTaskDetailModal from '@/layer/profiling/PprofTaskDetailModal.vue';
4142
import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
4243
import Icon from '@/components/icons/Icon.vue';
@@ -58,6 +59,9 @@ const tasksLoading = ref(false);
5859
5960
const selectedInstances = ref<string[]>([]);
6061
const tree = ref<PprofTree | null>(null);
62+
const displayMode = ref<'tree' | 'flame'>('flame');
63+
const hidePatterns = useHiddenFramePatterns();
64+
const frameSearch = ref('');
6165
const analyzeError = ref<string | null>(null);
6266
const analyzeLoading = ref(false);
6367
@@ -115,7 +119,10 @@ watch(currentTask, () => {
115119
});
116120
117121
watch(
118-
() => layerKey.value + '|' + (serviceId.value ?? ''),
122+
// Keyed on the service as the layer's roster resolves it, not the bare
123+
// selected id: opened directly, the tab knows the id before the roster has
124+
// loaded, and a load keyed on the id never ran again once it had.
125+
() => layerKey.value + '|' + (service.value?.id ?? ''),
119126
() => {
120127
taskDetailFor.value = null; // drop the detail modal on context change
121128
void refreshTasks();
@@ -337,7 +344,15 @@ function instanceCount(n: number): string {
337344
</div>
338345
<div v-if="analyzeError" class="banner err">{{ analyzeError }}</div>
339346
<div class="result">
340-
<ProfileFlameGraph v-if="profileTrees.length" :trees="profileTrees" metric-key="count" />
347+
<ProfileStackPanel
348+
v-if="profileTrees.length"
349+
v-model:display-mode="displayMode"
350+
v-model:hide-patterns="hidePatterns"
351+
v-model:search="frameSearch"
352+
:trees="profileTrees"
353+
:views="['flame']"
354+
:popout-title="'pprof' + (serviceName ? ' · ' + serviceName : '')"
355+
/>
341356
<div v-else-if="!analyzeLoading" class="result-empty">
342357
{{ currentTask ? t('Pick instances, then click Analyze.') : t('Select a task.') }}
343358
</div>
@@ -667,6 +682,8 @@ function instanceCount(n: number): string {
667682
min-height: 0;
668683
position: relative;
669684
overflow: hidden;
685+
display: flex;
686+
flex-direction: column;
670687
}
671688
.result-empty {
672689
position: absolute;

‎apps/ui/src/layer/profiling/LayerTraceProfilingView.vue‎

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ import type {
5454
} from '@/api/client';
5555
import ProfileStackPanel from '@/layer/profiling/ProfileStackPanel.vue';
5656
import { useHiddenFramePatterns } from '@/layer/profiling/useHiddenFramePatterns';
57-
import Modal from '@/components/primitives/Modal.vue';
5857
import NativeTraceWaterfall from '@/layer/traces/NativeTraceWaterfall.vue';
5958
import NewTraceProfileTaskModal from '@/layer/profiling/NewTraceProfileTaskModal.vue';
6059
import type { NewTraceTaskPayload } from '@/layer/profiling/NewTraceProfileTaskModal.vue';
@@ -133,12 +132,12 @@ const dataMode = ref<'include' | 'exclude'>('include');
133132
const highlightTop = ref(false);
134133
const hidePatterns = useHiddenFramePatterns();
135134
const frameSearch = ref('');
136-
const popoutOpen = ref(false);
137135
watch(analyzeTrees, (trees) => {
138-
if (trees.length) return;
139-
popoutOpen.value = false;
140-
analyzedSpan.value = null;
136+
if (!trees.length) analyzedSpan.value = null;
141137
});
138+
const popoutTitle = computed(() =>
139+
analyzedSpan.value ? t('Thread Stack') + ' · ' + analyzedSpan.value.endpointName : t('Thread Stack'),
140+
);
142141
143142
/** Request tickets. A reply for the service the operator has already navigated
144143
* away from must not publish under the new one. */
@@ -159,7 +158,10 @@ watch([currentTask, currentSegment, currentSpan], () => {
159158
});
160159
161160
watch(
162-
() => layerKey.value + '|' + (selectedId.value ?? ''),
161+
// Keyed on the service as the layer's roster resolves it, not the bare
162+
// selected id: opened from a link, the tab knows the id before the roster has
163+
// loaded, and a load keyed on the id never ran again once it had.
164+
() => layerKey.value + '|' + (service.value?.id ?? ''),
163165
() => {
164166
void refreshTasks();
165167
},
@@ -490,28 +492,13 @@ function fmtTime(ms: number): string {
490492
v-model:highlight-top="highlightTop"
491493
v-model:search="frameSearch"
492494
:trees="analyzeTrees"
493-
@popout="popoutOpen = true"
495+
duration-unit="ms"
496+
:popout-title="popoutTitle"
494497
/>
495498
</div>
496499
</div>
497500
</div>
498501

499-
<Modal
500-
:open="popoutOpen"
501-
:title="analyzedSpan ? t('Thread Stack') + ' · ' + analyzedSpan.endpointName : t('Thread Stack')"
502-
width="calc(100vw - 32px)"
503-
fit-body
504-
@close="popoutOpen = false"
505-
>
506-
<ProfileStackPanel
507-
v-model:display-mode="displayMode"
508-
v-model:hide-patterns="hidePatterns"
509-
v-model:highlight-top="highlightTop"
510-
v-model:search="frameSearch"
511-
:trees="analyzeTrees"
512-
in-popout
513-
/>
514-
</Modal>
515502

516503
<NewTraceProfileTaskModal
517504
v-model:show="showNewTask"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import { afterEach, describe, expect, it } from 'vitest';
17+
import { enableAutoUnmount, mount } from '@vue/test-utils';
18+
import { nextTick } from 'vue';
19+
import type { ProfileAnalyzationTree } from '@/api/client';
20+
import { i18n } from '@/i18n';
21+
import ProfileStackPanel from './ProfileStackPanel.vue';
22+
23+
const TREE: ProfileAnalyzationTree = {
24+
elements: [
25+
{ id: '1', parentId: '0', codeSignature: 'runtime.goexit:1695', duration: 10, durationChildExcluded: 0, count: 2 },
26+
{ id: '2', parentId: '1', codeSignature: 'main.handler:30', duration: 10, durationChildExcluded: 10, count: 2 },
27+
],
28+
};
29+
30+
function mountPanel(views?: ('tree' | 'flame')[]) {
31+
return mount(ProfileStackPanel, {
32+
props: {
33+
trees: [TREE],
34+
views,
35+
displayMode: views?.[0] ?? 'tree',
36+
hidePatterns: [],
37+
search: '',
38+
popoutTitle: 'pprof · go-service',
39+
},
40+
attachTo: document.body,
41+
// d3-flame-graph needs a real layout engine; the panel is what is tested.
42+
global: { plugins: [i18n], stubs: { ProfileFlameGraph: true } },
43+
});
44+
}
45+
46+
// Unmounted after each test: a mounted floating panel keeps a click listener
47+
// on document, which a later test's clicks would wake against a wiped body.
48+
enableAutoUnmount(afterEach);
49+
afterEach(() => {
50+
document.body.innerHTML = '';
51+
});
52+
53+
describe('ProfileStackPanel', () => {
54+
it('offers the Tree | Flame switch only when the page has both views', () => {
55+
expect(mountPanel().findAll('.seg button').map((b) => b.text())).toEqual(['Tree', 'Flame']);
56+
expect(mountPanel(['flame', 'tree']).findAll('.seg button').map((b) => b.text())).toEqual(['Flame', 'Tree']);
57+
expect(mountPanel(['flame']).find('.seg').exists()).toBe(false);
58+
});
59+
60+
it('keeps top slow to the Tree', () => {
61+
expect(mountPanel(['flame']).find('.psp-joined').exists()).toBe(false);
62+
expect(mountPanel(['tree']).find('.psp-joined').exists()).toBe(true);
63+
});
64+
65+
it('pops out into a dialog with the same controls and no Pop out of its own', async () => {
66+
const w = mountPanel(['flame']);
67+
await w.find('.psp-pop').trigger('click');
68+
await nextTick();
69+
const dialog = document.body.querySelector('.modal');
70+
expect(dialog?.querySelector('.modal__title')?.textContent).toBe('pprof · go-service');
71+
expect(dialog?.querySelector('.psp-bar')).not.toBeNull();
72+
expect(dialog?.querySelector('.psp-pop')).toBeNull();
73+
});
74+
75+
it('closes the pop-out when the result goes away', async () => {
76+
const w = mountPanel(['flame']);
77+
await w.find('.psp-pop').trigger('click');
78+
await nextTick();
79+
await w.setProps({ trees: [] });
80+
await nextTick();
81+
expect(document.body.querySelector('.modal')).toBeNull();
82+
});
83+
});

0 commit comments

Comments
 (0)