Skip to content

Commit f6d9942

Browse files
committed
Merge branch 'master' of https://github.com/apache/skywalking into service-mtls
2 parents 6595c5b + 5afbc29 commit f6d9942

File tree

24 files changed

+1267
-32
lines changed

24 files changed

+1267
-32
lines changed

.github/workflows/skywalking.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,9 @@ jobs:
492492
config: test/e2e-v2/cases/profiling/trace/opensearch/e2e.yaml
493493
env: OPENSEARCH_VERSION=2.4.0
494494

495+
- name: Go Trace Profiling
496+
config: test/e2e-v2/cases/profiling/trace/go/e2e.yaml
497+
495498
- name: eBPF Profiling On CPU BanyanDB
496499
config: test/e2e-v2/cases/profiling/ebpf/oncpu/banyandb/e2e.yaml
497500
docker:
@@ -1118,4 +1121,4 @@ jobs:
11181121
[[ ${e2eJavaVersionResults} == 'success' ]] || [[ ${execute} != 'true' && ${e2eJavaVersionResults} == 'skipped' ]] || exit -7;
11191122
[[ ${timeConsumingITResults} == 'success' ]] || [[ ${execute} != 'true' && ${timeConsumingITResults} == 'skipped' ]] || exit -8;
11201123
1121-
exit 0;
1124+
exit 0;

docs/en/changes/changes.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,11 @@
112112
* Make MAL percentile align with OAL percentile calculation.
113113
* Update Grafana dashboards for OAP observability.
114114
* BanyanDB: fix query `getInstance` by instance ID.
115-
* Support the go agent(0.7.0 release) bundled pprof profiling feature.
115+
* Support the go agent(0.7.0 release) bundled pprof profiling feature.
116116
* Service and TCPService source support analyze TLS mode.
117-
117+
* Library-pprof-parser: feat: add PprofSegmentParser.
118+
* Storage: feat: add languageType column to ProfileThreadSnapshotRecord.
119+
* Feat: add go profile analyzer
118120

119121
#### UI
120122

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.skywalking.oap.server.core.profiling.trace;
19+
20+
/**
21+
* Language type for profile records. Stored as int in storage for compatibility.
22+
*/
23+
public enum ProfileLanguageType {
24+
JAVA(0),
25+
GO(1);
26+
27+
private final int value;
28+
29+
ProfileLanguageType(int value) {
30+
this.value = value;
31+
}
32+
33+
public int getValue() {
34+
return value;
35+
}
36+
37+
public static ProfileLanguageType fromValue(int value) {
38+
for (ProfileLanguageType language : values()) {
39+
if (language.value == value) {
40+
return language;
41+
}
42+
}
43+
return JAVA; // default to Java
44+
}
45+
}

oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileThreadSnapshotRecord.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class ProfileThreadSnapshotRecord extends Record {
5252
public static final String DUMP_TIME = "dump_time";
5353
public static final String SEQUENCE = "sequence";
5454
public static final String STACK_BINARY = "stack_binary";
55+
public static final String LANGUAGE_TYPE = "language_type";
5556

5657
@Column(name = TASK_ID)
5758
@SQLDatabase.CompositeIndex(withColumns = {SEGMENT_ID})
@@ -69,6 +70,8 @@ public class ProfileThreadSnapshotRecord extends Record {
6970
private int sequence;
7071
@Column(name = STACK_BINARY)
7172
private byte[] stackBinary;
73+
@Column(name = LANGUAGE_TYPE) // NoIndexing
74+
private ProfileLanguageType language = ProfileLanguageType.JAVA;
7275

7376
@Override
7477
public StorageID id() {
@@ -88,6 +91,8 @@ public ProfileThreadSnapshotRecord storage2Entity(final Convert2Entity converter
8891
snapshot.setSequence(((Number) converter.get(SEQUENCE)).intValue());
8992
snapshot.setTimeBucket(((Number) converter.get(TIME_BUCKET)).intValue());
9093
snapshot.setStackBinary(converter.getBytes(STACK_BINARY));
94+
final Number languageTypeNum = (Number) converter.get(LANGUAGE_TYPE);
95+
snapshot.setLanguage(ProfileLanguageType.fromValue(languageTypeNum != null ? languageTypeNum.intValue() : 0));
9196
return snapshot;
9297
}
9398

@@ -99,6 +104,8 @@ public void entity2Storage(final ProfileThreadSnapshotRecord storageData, final
99104
converter.accept(SEQUENCE, storageData.getSequence());
100105
converter.accept(TIME_BUCKET, storageData.getTimeBucket());
101106
converter.accept(STACK_BINARY, storageData.getStackBinary());
107+
ProfileLanguageType language = storageData.getLanguage();
108+
converter.accept(LANGUAGE_TYPE, language != null ? language.getValue() : ProfileLanguageType.JAVA.getValue());
102109
}
103110
}
104111
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.skywalking.oap.server.core.profiling.trace.analyze;
19+
20+
import com.google.perftools.profiles.ProfileProto;
21+
import java.util.Collections;
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.stream.Collectors;
27+
import java.util.ArrayDeque;
28+
import org.apache.skywalking.oap.server.core.profiling.trace.ProfileThreadSnapshotRecord;
29+
import org.apache.skywalking.oap.server.core.query.input.SegmentProfileAnalyzeQuery;
30+
import org.apache.skywalking.oap.server.core.query.type.ProfileAnalyzation;
31+
import org.apache.skywalking.oap.server.core.query.type.ProfileStackElement;
32+
import org.apache.skywalking.oap.server.core.query.type.ProfileStackTree;
33+
import org.apache.skywalking.oap.server.library.pprof.parser.PprofSegmentParser;
34+
import org.apache.skywalking.oap.server.library.pprof.parser.PprofParser;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
38+
/**
39+
* Analyzer for Go pprof samples. Builds a stack tree with total/self durations using sampling period.
40+
* This works independently from ThreadSnapshot, for Go profiles only.
41+
*/
42+
public class GoProfileAnalyzer {
43+
private static final Logger LOGGER = LoggerFactory.getLogger(GoProfileAnalyzer.class);
44+
45+
/**
46+
* Analyze a pprof profile for a specific segment and time window.
47+
*/
48+
public ProfileAnalyzation analyze(final String segmentId,
49+
final ProfileProto.Profile profile) {
50+
final long periodMs = PprofSegmentParser.resolvePeriodMillis(profile);
51+
52+
// Build ProfileStackElement directly (reuse FrameTreeBuilder's mergeSample logic)
53+
Map<String, Integer> key2Id = new HashMap<>(); // "parentId|name" -> id
54+
List<ProfileStackElement> elements = new ArrayList<>();
55+
56+
// Strict per-segment filtering
57+
final List<String> stringTable = profile.getStringTableList();
58+
59+
for (ProfileProto.Sample sample : profile.getSampleList()) {
60+
final String seg = PprofSegmentParser.extractSegmentIdFromLabels(sample.getLabelList(), stringTable);
61+
if (seg == null || !seg.equals(segmentId)) {
62+
continue;
63+
}
64+
long sampleCount = sample.getValueCount() > 0 ? sample.getValue(0) : 1L;
65+
long weightMs = sampleCount * periodMs;
66+
67+
// Build function stack then ensure root->leaf order for aggregation
68+
List<String> stack = PprofSegmentParser.extractStackFromSample(sample, profile);
69+
Collections.reverse(stack);
70+
71+
// Aggregate along path (similar to FrameTreeBuilder.mergeSample)
72+
int parentId = -1; // root
73+
for (String fn : stack) {
74+
String key = parentId + "|" + fn;
75+
Integer nodeId = key2Id.get(key);
76+
77+
if (nodeId == null) {
78+
ProfileStackElement element = new ProfileStackElement();
79+
element.setId(elements.size());
80+
element.setParentId(parentId);
81+
element.setCodeSignature(fn);
82+
element.setDuration(0);
83+
element.setDurationChildExcluded(0);
84+
element.setCount(0);
85+
elements.add(element);
86+
nodeId = element.getId();
87+
key2Id.put(key, nodeId);
88+
}
89+
90+
ProfileStackElement element = elements.get(nodeId);
91+
element.setDuration(element.getDuration() + (int) weightMs);
92+
element.setCount(element.getCount() + (int) sampleCount);
93+
94+
parentId = nodeId;
95+
}
96+
}
97+
98+
int rootCount = 0;
99+
for (ProfileStackElement e : elements) {
100+
if (e.getParentId() == -1) {
101+
rootCount++;
102+
}
103+
}
104+
if (rootCount > 1) {
105+
int virtualRootId = elements.size();
106+
ProfileStackElement virtualRoot = new ProfileStackElement();
107+
virtualRoot.setId(virtualRootId);
108+
virtualRoot.setParentId(-1);
109+
virtualRoot.setCodeSignature("root");
110+
virtualRoot.setDuration(0);
111+
virtualRoot.setDurationChildExcluded(0);
112+
virtualRoot.setCount(0);
113+
elements.add(virtualRoot);
114+
115+
for (ProfileStackElement e : elements) {
116+
if (e.getId() == virtualRootId) {
117+
continue;
118+
}
119+
if (e.getParentId() == -1) {
120+
e.setParentId(virtualRootId);
121+
virtualRoot.setDuration(virtualRoot.getDuration() + e.getDuration());
122+
virtualRoot.setCount(virtualRoot.getCount() + e.getCount());
123+
}
124+
}
125+
}
126+
127+
Map<Integer, Integer> childDurSum = new HashMap<>();
128+
for (ProfileStackElement child : elements) {
129+
int pid = child.getParentId();
130+
if (pid != -1) {
131+
childDurSum.put(pid, childDurSum.getOrDefault(pid, 0) + child.getDuration());
132+
}
133+
}
134+
for (ProfileStackElement elem : elements) {
135+
int childrenSum = childDurSum.getOrDefault(elem.getId(), 0);
136+
elem.setDurationChildExcluded(Math.max(0, elem.getDuration() - childrenSum));
137+
}
138+
139+
Integer rootId = null;
140+
for (ProfileStackElement e : elements) {
141+
if (e.getParentId() == -1) {
142+
rootId = e.getId();
143+
break;
144+
}
145+
}
146+
if (rootId != null) {
147+
Map<Integer, List<ProfileStackElement>> childrenMap = new HashMap<>();
148+
for (ProfileStackElement e : elements) {
149+
childrenMap.computeIfAbsent(e.getParentId(), k -> new ArrayList<>()).add(e);
150+
}
151+
152+
List<ProfileStackElement> ordered = new ArrayList<>();
153+
ArrayDeque<ProfileStackElement> queue = new ArrayDeque<>();
154+
// start from root
155+
for (ProfileStackElement e : elements) {
156+
if (e.getId() == rootId) {
157+
queue.add(e);
158+
break;
159+
}
160+
}
161+
while (!queue.isEmpty()) {
162+
ProfileStackElement cur = queue.removeFirst();
163+
ordered.add(cur);
164+
List<ProfileStackElement> children = childrenMap.get(cur.getId());
165+
if (children != null) {
166+
// sort children by duration desc to make primary path first
167+
children.sort((a, b) -> Integer.compare(b.getDuration(), a.getDuration()));
168+
queue.addAll(children);
169+
}
170+
}
171+
172+
Map<Integer, Integer> idRemap = new HashMap<>();
173+
for (int i = 0; i < ordered.size(); i++) {
174+
idRemap.put(ordered.get(i).getId(), i);
175+
}
176+
for (ProfileStackElement e : ordered) {
177+
int newId = idRemap.get(e.getId());
178+
int parentId = e.getParentId();
179+
e.setId(newId);
180+
if (parentId == -1) {
181+
e.setParentId(-1);
182+
} else {
183+
e.setParentId(idRemap.getOrDefault(parentId, -1));
184+
}
185+
}
186+
elements = ordered;
187+
}
188+
189+
ProfileStackTree tree = new ProfileStackTree();
190+
tree.setElements(elements);
191+
192+
ProfileAnalyzation result = new ProfileAnalyzation();
193+
result.getTrees().add(tree);
194+
return result;
195+
}
196+
197+
/**
198+
* Analyze multiple Go profile records and return combined results
199+
*/
200+
public ProfileAnalyzation analyzeRecords(List<ProfileThreadSnapshotRecord> records, List<SegmentProfileAnalyzeQuery> queries) {
201+
ProfileAnalyzation result = new ProfileAnalyzation();
202+
203+
// Build query map for O(1) lookup
204+
Map<String, SegmentProfileAnalyzeQuery> queryMap = queries.stream()
205+
.collect(Collectors.toMap(SegmentProfileAnalyzeQuery::getSegmentId, q -> q));
206+
207+
for (ProfileThreadSnapshotRecord record : records) {
208+
try {
209+
// Find the corresponding query for this segment
210+
SegmentProfileAnalyzeQuery query = queryMap.get(record.getSegmentId());
211+
212+
if (query == null) {
213+
LOGGER.warn("No query found for Go profile segment: {}", record.getSegmentId());
214+
continue;
215+
}
216+
217+
// Parse pprof data from stackBinary
218+
ProfileProto.Profile profile = PprofParser.parseProfile(record.getStackBinary());
219+
220+
// Analyze this record
221+
ProfileAnalyzation recordAnalyzation = analyze(
222+
record.getSegmentId(),
223+
profile
224+
);
225+
226+
if (recordAnalyzation != null && !recordAnalyzation.getTrees().isEmpty()) {
227+
result.getTrees().addAll(recordAnalyzation.getTrees());
228+
229+
if (LOGGER.isInfoEnabled()) {
230+
LOGGER.info("Go profile analysis completed: segmentId={}, window=[{}-{}], trees={}",
231+
record.getSegmentId(), query.getTimeRange().getStart(), query.getTimeRange().getEnd(),
232+
recordAnalyzation.getTrees().size());
233+
}
234+
}
235+
} catch (Exception e) {
236+
LOGGER.error("Failed to analyze Go profile record: segmentId={}, sequence={}, dumpTime={}",
237+
record.getSegmentId(), record.getSequence(), record.getDumpTime(), e);
238+
}
239+
}
240+
241+
return result;
242+
}
243+
}

0 commit comments

Comments
 (0)