Skip to content

Commit 5e28304

Browse files
author
Yuriy Bezsonov
committed
refactor(perf-platform): rename collector classes, drop multi-arch
Pre-content cleanup of /apps/perf-* before the workshop module rewrite. The shape of both apps stays the same; this commit only renames classes for clarity and drops the multi-architecture plumbing that adds noise without value at workshop scale. Collector - AsyncProfilerAttach -> Profiler. The class is the perf-collector's service-shaped profiling component, not an action class. "Attach" is one method on it. - DumpController -> CollectorController. Named after the collector's external API surface (POST /dump and any future endpoints), not one of the verbs it serves. - DumpService -> CollectorService. Matches the controller. - Drop ArchPostProcessor and META-INF/spring.factories. They existed only to pick /opt/perf-collector/{amd64,arm64}/ at runtime. The workshop ships a single-arch (linux/amd64) image - multi-arch is out of scope. Saves ~40 lines of code plus a Spring 4-deprecated EnvironmentPostProcessor. - pom.xml drops 4 download-maven-plugin executions (arm64 fetches), 2 maven-resources-plugin executions (arm64 staging), and one of two jib platforms. ~70 lines. - application.yaml flattens /opt/perf-collector/${arch}/... to /opt/perf-collector/... Analyzer - Extract GitHubSourceCodeTool from inside AiService into its own top-level file. The tool is the workshop's source-citation wow moment in chapter 3 - it deserves a file we can point at and walk through. AiService keeps the system prompt and the prompt builder; the source-code Tool implementation is now a peer of PyroscopeTool. Verified end-to-end on the live cluster: collector image rebuilt and pushed (linux/amd64 only), DaemonSet restarted, async-profiler attaches with the new logger names, JFR push to Pyroscope continues. Analyzer image rebuilt and pushed, deployment restarted, on-demand analysis runs all four lanes (CPU top, wall top, JFR, thread dump), GitHubSourceCodeTool cites unicorn-store-spring source files in the AI report.
1 parent ef4c743 commit 5e28304

10 files changed

Lines changed: 122 additions & 238 deletions

File tree

apps/perf-analyzer/src/main/java/com/example/perf/analyzer/AiService.java

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
package com.example.perf.analyzer;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import org.slf4j.Logger;
54
import org.slf4j.LoggerFactory;
65
import org.springframework.ai.chat.client.ChatClient;
7-
import org.springframework.ai.tool.annotation.Tool;
86
import org.springframework.beans.factory.annotation.Value;
97
import org.springframework.stereotype.Service;
10-
import org.springframework.web.client.RestClient;
11-
12-
import java.util.Base64;
138

149
/**
1510
* Spring AI + Amazon Bedrock Converse. Builds one {@link ChatClient}
1611
* configured with the system prompt and the always-registered
17-
* {@link PyroscopeTool}, then per-analysis layers in a
18-
* {@link GitHubSourceCodeTool} if the target workload advertised a
12+
* {@link PyroscopeTool}, then per-analysis layers in a fresh
13+
* {@link GitHubSourceCodeTool} when the target workload advertises a
1914
* GitHub repo via pod annotation or task tag.
2015
*
2116
* Per-analysis tool registration lets the analyzer serve many workloads
@@ -26,7 +21,6 @@
2621
public class AiService {
2722

2823
private static final Logger logger = LoggerFactory.getLogger(AiService.class);
29-
private static final ObjectMapper MAPPER = new ObjectMapper();
3024

3125
private static final String SYSTEM_PROMPT = """
3226
You are a Java performance engineer. You receive:
@@ -190,68 +184,4 @@ private static String truncateLines(String text, int maxLines) {
190184
sb.append("... (truncated, ").append(lines.length - maxLines).append(" more lines)");
191185
return sb.toString();
192186
}
193-
194-
/**
195-
* Spring AI @Tool: fetch source code from a GitHub repository via the
196-
* REST API. The repo coordinates are per-analysis — the target
197-
* workload advertises them via the pod annotation
198-
* {@code perf-profile/github-repo} (or the ECS task tag
199-
* {@code perf-profile:github-repo}). The analyzer constructs a fresh
200-
* instance each request.
201-
*
202-
* Compare {@link PyroscopeTool}, which is a top-level @Component
203-
* because {@link AnalysisService} also invokes it directly for the
204-
* pre-fetched prompt section.
205-
*/
206-
static class GitHubSourceCodeTool {
207-
208-
private final RestClient restClient;
209-
private final String repoPath;
210-
211-
/**
212-
* @param repo "{owner}/{name}" (e.g. "aws-samples/java-on-aws")
213-
* @param path optional path-prefix inside the repo (e.g. "apps/unicorn-store-spring")
214-
* @param token optional GitHub PAT for private repos
215-
*/
216-
GitHubSourceCodeTool(String repo, String path, String token) {
217-
if (repo == null || repo.isBlank()) {
218-
throw new IllegalArgumentException("repo must not be blank");
219-
}
220-
var builder = RestClient.builder()
221-
.baseUrl("https://api.github.com/repos/" + repo.replaceAll("/$", ""))
222-
.defaultHeader("Accept", "application/vnd.github.v3+json")
223-
.defaultHeader("User-Agent", "perf-analyzer");
224-
if (token != null && !token.isBlank()) {
225-
builder.defaultHeader("Authorization", "token " + token);
226-
}
227-
this.restClient = builder.build();
228-
this.repoPath = (path != null && !path.isBlank())
229-
? path.replaceAll("/$", "") : "";
230-
}
231-
232-
@Tool(description = """
233-
Fetch a source code file from the application GitHub repository.
234-
Provide the path relative to the application root, e.g.
235-
src/main/java/com/unicorn/store/service/UnicornService.java — the
236-
repository base path is prepended automatically.
237-
Use this to look up Java source files referenced in stack traces,
238-
thread dumps, and JFR event summaries so recommendations can cite
239-
file paths and line numbers.
240-
""")
241-
public String fetchSourceCode(String filePath) {
242-
var fullPath = repoPath.isEmpty() ? filePath : repoPath + "/" + filePath;
243-
try {
244-
var json = restClient.get()
245-
.uri("/contents/{path}", fullPath)
246-
.retrieve()
247-
.body(String.class);
248-
var node = MAPPER.readTree(json);
249-
var encoded = node.get("content").asText();
250-
return new String(Base64.getMimeDecoder().decode(encoded));
251-
} catch (Exception e) {
252-
logger.warn("Failed to fetch source code for {}: {}", fullPath, e.getMessage());
253-
return "Source code not available: " + e.getMessage();
254-
}
255-
}
256-
}
257187
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.example.perf.analyzer;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.ai.tool.annotation.Tool;
7+
import org.springframework.web.client.RestClient;
8+
9+
import java.util.Base64;
10+
11+
/**
12+
* Spring AI @Tool: fetch source code from a GitHub repository via the
13+
* REST API.
14+
*
15+
* The repo coordinates are per-analysis — the target workload advertises
16+
* them via the pod annotation {@code perf-profile/github-repo} (or the
17+
* ECS task tag {@code perf-profile:github-repo}). The analyzer constructs
18+
* a fresh instance each request, picking up whichever workload's repo
19+
* coordinates the current analysis context carries.
20+
*
21+
* Compare {@link PyroscopeTool}, which is a top-level @Component because
22+
* {@link AnalysisService} also invokes it directly for the pre-fetched
23+
* prompt sections.
24+
*/
25+
public class GitHubSourceCodeTool {
26+
27+
private static final Logger logger = LoggerFactory.getLogger(GitHubSourceCodeTool.class);
28+
private static final ObjectMapper MAPPER = new ObjectMapper();
29+
30+
private final RestClient restClient;
31+
private final String repoPath;
32+
33+
/**
34+
* @param repo "{owner}/{name}" (e.g. "aws-samples/java-on-aws")
35+
* @param path optional path-prefix inside the repo (e.g. "apps/unicorn-store-spring")
36+
* @param token optional GitHub PAT for private repos
37+
*/
38+
public GitHubSourceCodeTool(String repo, String path, String token) {
39+
if (repo == null || repo.isBlank()) {
40+
throw new IllegalArgumentException("repo must not be blank");
41+
}
42+
var builder = RestClient.builder()
43+
.baseUrl("https://api.github.com/repos/" + repo.replaceAll("/$", ""))
44+
.defaultHeader("Accept", "application/vnd.github.v3+json")
45+
.defaultHeader("User-Agent", "perf-analyzer");
46+
if (token != null && !token.isBlank()) {
47+
builder.defaultHeader("Authorization", "token " + token);
48+
}
49+
this.restClient = builder.build();
50+
this.repoPath = (path != null && !path.isBlank())
51+
? path.replaceAll("/$", "") : "";
52+
}
53+
54+
@Tool(description = """
55+
Fetch a source code file from the application GitHub repository.
56+
Provide the path relative to the application root, e.g.
57+
src/main/java/com/unicorn/store/service/UnicornService.java — the
58+
repository base path is prepended automatically.
59+
Use this to look up Java source files referenced in stack traces,
60+
thread dumps, and JFR event summaries so recommendations can cite
61+
file paths and line numbers.
62+
""")
63+
public String fetchSourceCode(String filePath) {
64+
var fullPath = repoPath.isEmpty() ? filePath : repoPath + "/" + filePath;
65+
try {
66+
var json = restClient.get()
67+
.uri("/contents/{path}", fullPath)
68+
.retrieve()
69+
.body(String.class);
70+
var node = MAPPER.readTree(json);
71+
var encoded = node.get("content").asText();
72+
return new String(Base64.getMimeDecoder().decode(encoded));
73+
} catch (Exception e) {
74+
logger.warn("Failed to fetch source code for {}: {}", fullPath, e.getMessage());
75+
return "Source code not available: " + e.getMessage();
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)