Skip to content

Commit b9d91f9

Browse files
committed
Add initial flamegraph support
Disabled for now because I'll need to implement Rust Analyzer-based path resolution (converting function names to filenames and locations) in order for the LLM to make use of the flamegraph.
1 parent dc77262 commit b9d91f9

File tree

7 files changed

+148
-59
lines changed

7 files changed

+148
-59
lines changed

README.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,26 @@
55
1. Git clone
66
2. Install [`uv`](https://github.com/astral-sh/uv) if not already installed
77
3. To fix perf debuginfo issues: `cargo install addr2line --features="bin"`
8+
4. If you want support for sending flamegraphs to the LLM:
9+
a. `cargo install flamegraph`
10+
a. `cargo install resvg`
811

912
## Basic usage
1013

11-
First, build your target program with optimizations on and debuginfo enabled, and then profile it with `perf` using something like the following:
12-
13-
```console
14-
$ perf record -F99 --call-graph dwarf ./your-program
15-
```
16-
17-
Then, in the `accelerant` repository, run:
14+
In the `accelerant` repository, run:
1815

1916
```console
2017
$ uv run accelerant_server.py
2118
```
2219

23-
Finally, in a separate terminal, run:
20+
In a separate terminal, run:
2421

2522
```console
26-
$ curl 'http://127.0.0.1:5000/optimize?project=PATH_TO_PROJECT_ROOT&perfDataPath=ABSOLUTE_PATH_TO_PERF_DATA'
23+
$ curl 'http://127.0.0.1:5000/optimize?project=PATH_TO_PROJECT_ROOT&targetBinary=target/release/REST_OF_PATH_TO_EXECUTABLE_TO_OPTIMIZE'
2724
```
2825

29-
Alternatively, you can ask to optimize a specific line without `perf` information using the following:
26+
Accelerant will automatically build, run, and profile your project using `cargo` and `perf`.
3027

31-
```console
32-
$ curl 'http://127.0.0.1:5000/optimize?project=PATH_TO_PROJECT_ROOT&filename=RELATIVE_PATH_TO_FILE_IN_PROJECT&line=LINE_NUMBER_IN_FILE'
33-
```
28+
If you've already run the `perf` profiler and collected a `perf.data` file, you can give it to Accelerant by appending a `perfDataPath` query parameter with the path to the file.
29+
30+
Also, if you know a particular line in your project is a hotspot, you can pass the (relative) path to its containing file in a `filename` paramater, with the line number in `line`.

accelerant/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def run_agent(
3535
tools.edit_code,
3636
tools.check_codebase_for_errors,
3737
tools.run_perf_profiler,
38+
# tools.generate_flamegraph,
3839
tools.get_info,
3940
tools.get_references,
4041
tools.get_surrounding_code,

accelerant/flamegraph.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import base64
2+
from pathlib import Path
3+
import subprocess
4+
from tempfile import NamedTemporaryFile
5+
import re
6+
7+
8+
def make_flamegraph_png(perf_data_path: Path) -> bytes:
9+
svg_str = make_flamegraph_svg(perf_data_path)
10+
png_data = svg_to_png(svg_str)
11+
return png_data
12+
13+
14+
def make_flamegraph_svg(perf_data_path: Path) -> str:
15+
with NamedTemporaryFile(suffix=".svg") as output_svg_temp:
16+
subprocess.run(
17+
[
18+
"flamegraph",
19+
"--perfdata",
20+
perf_data_path,
21+
"--output",
22+
output_svg_temp.name,
23+
],
24+
check=True,
25+
)
26+
output_svg_temp.seek(0)
27+
svg_data = output_svg_temp.read().decode()
28+
return svg_data
29+
30+
31+
def svg_to_png(svg_str: str) -> bytes:
32+
# HACK: resvg doesn't understand the monospace font-family, so replace it with concrete fonts
33+
svg_str = re.sub(
34+
"font-family: ?monospace",
35+
"font-family: 'Fira Mono', 'DejaVu Sans Mono', 'Ubuntu Mono'",
36+
svg_str,
37+
)
38+
with NamedTemporaryFile(suffix=".svg") as svg_temp:
39+
svg_temp.write(svg_str.encode())
40+
svg_temp.flush()
41+
with NamedTemporaryFile(suffix=".png") as png_temp:
42+
subprocess.run(
43+
["resvg", "--zoom=2", svg_temp.name, png_temp.name],
44+
check=True,
45+
)
46+
png_temp.seek(0)
47+
png_data = png_temp.read()
48+
return png_data
49+
50+
51+
def png_to_data_url(png_data: bytes) -> str:
52+
b64_encoded = base64.b64encode(png_data).decode()
53+
data_url = f"data:image/png;base64,{b64_encoded}"
54+
return data_url

accelerant/llm.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ def on_trace_end(self, trace):
1616
del self.active_traces[trace.trace_id]
1717

1818
def on_span_start(self, span):
19-
print(f"[blue]Starting span:[/blue] {span.span_data.export()}")
19+
data = span.span_data.export()
20+
if "data:image/png;base64" in str(data):
21+
print("[blue]Starting span that includes image data:[/blue]")
22+
else:
23+
print(f"[blue]Starting span:[/blue] {data}")
2024
self.active_spans[span.span_id] = span
2125

2226
def on_span_end(self, span):
23-
print(f"[magenta]Ending span:[/magenta] {span.span_data.export()}")
27+
data = span.span_data.export()
28+
if "data:image/png;base64" in str(data):
29+
print("[magenta]Ending span that includes image data:[/magenta]")
30+
else:
31+
print(f"[magenta]Ending span:[/magenta] {data}")
2432
del self.active_spans[span.span_id]
2533

2634
def shutdown(self):

accelerant/perf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66

77

88
class PerfData:
9+
_path: Path
910
_data: AttributedPerf
1011

1112
def __init__(self, perf_data_path: Path, project_root: Path):
13+
self._path = perf_data_path
1214
self._data = get_perf_data(str(perf_data_path), str(project_root))
1315

16+
def data_path(self) -> Path:
17+
return self._path
18+
1419
def lookup_pct_time(self, loc: LineLoc) -> Optional[float]:
1520
if loc not in self._data.hit_count:
1621
return None

accelerant/project.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,21 @@ def lsp(self) -> LSP:
4141
return self._lsp
4242

4343
def perf_data(self, version: Optional[FsVersion] = None) -> Optional[PerfData]:
44+
perf_data_path = self.perf_data_path(version)
45+
if perf_data_path is None:
46+
return None
47+
48+
if perf_data_path not in self._perf_data_map:
49+
self._perf_data_map[perf_data_path] = PerfData(perf_data_path, self._root)
50+
return self._perf_data_map[perf_data_path]
51+
52+
def perf_data_path(self, version: Optional[FsVersion] = None) -> Optional[Path]:
4453
if version is None:
4554
version = self.fs_sandbox().version()
4655
if version not in self._perf_per_version:
4756
return None
4857
perf_data_path = self._perf_per_version[version]
49-
if perf_data_path not in self._perf_data_map:
50-
self._perf_data_map[perf_data_path] = PerfData(perf_data_path, self._root)
51-
return self._perf_data_map[perf_data_path]
58+
return perf_data_path
5259

5360
def add_perf_data(self, version: FsVersion, perf_data_path: Path) -> None:
5461
self._perf_per_version[version] = perf_data_path

accelerant/tools.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
from itertools import islice
33
import shutil
44
import subprocess
5-
from typing import Any, Optional
6-
from agents import RunContextWrapper, function_tool
5+
from typing import Optional
6+
from agents import RunContextWrapper, ToolOutputImage, function_tool
77
from llm_utils import number_group_of_lines
88
from perfparser import LineLoc
99

1010
from accelerant.chat_interface import CodeSuggestion
11+
from accelerant.flamegraph import make_flamegraph_png, png_to_data_url
1112
from accelerant.lsp import TOP_LEVEL_SYMBOL_KINDS, uri_to_relpath
1213
from accelerant.patch import apply_simultaneous_suggestions
14+
from accelerant.perf import PerfData
1315
from accelerant.util import find_symbol, truncate_for_llm
1416
from accelerant.project import Project
1517

@@ -56,50 +58,65 @@ def check_codebase_for_errors(
5658
return "OK: Codebase has no errors!"
5759

5860

61+
def _shared_build_and_run_perf(project: Project) -> PerfData:
62+
version = project.fs_sandbox().version()
63+
perf_data = project.perf_data(version)
64+
if perf_data is None:
65+
project.build_for_profiling()
66+
project.run_profiler()
67+
perf_data = project.perf_data(version)
68+
assert perf_data is not None, "perf data should be available after profiling"
69+
return perf_data
70+
71+
5972
@function_tool
6073
def run_perf_profiler(
6174
ctx: RunContextWrapper[AgentContext],
62-
) -> list[dict[str, Any]]:
75+
) -> list[dict]:
6376
"""Run a performance profiler on the target binary and return the top hotspots."""
64-
try:
65-
project = ctx.context.project
66-
version = project.fs_sandbox().version()
67-
perf_data = project.perf_data(version)
68-
if perf_data is None:
69-
project.build_for_profiling()
70-
project.run_profiler()
71-
perf_data = project.perf_data(version)
72-
assert perf_data is not None, "perf data should be available after profiling"
73-
perf_tabulated = perf_data.tabulate()
74-
NUM_HOTSPOTS = 5
75-
76-
def get_parent_region(loc: LineLoc) -> Optional[str]:
77-
parent_sym = project.lsp().syncexec(
78-
project.lsp().request_nearest_parent_symbol(
79-
loc.path, loc.line - 1, TOP_LEVEL_SYMBOL_KINDS
80-
),
81-
)
82-
if parent_sym is None:
83-
return None
84-
return parent_sym["name"]
85-
86-
hotspots = list(
87-
islice(
88-
map(
89-
lambda x: {
90-
"parent_region": get_parent_region(x[0]) or "<unknown>",
91-
"loc": x[0],
92-
"pct_time": x[1] * 100,
93-
},
94-
filter(lambda x: x[0].line > 0, perf_tabulated),
95-
),
96-
NUM_HOTSPOTS,
97-
)
77+
project = ctx.context.project
78+
perf_data = _shared_build_and_run_perf(project)
79+
perf_tabulated = perf_data.tabulate()
80+
NUM_HOTSPOTS = 5
81+
82+
def get_parent_region(loc: LineLoc) -> Optional[str]:
83+
parent_sym = project.lsp().syncexec(
84+
project.lsp().request_nearest_parent_symbol(
85+
loc.path, loc.line - 1, TOP_LEVEL_SYMBOL_KINDS
86+
),
9887
)
99-
return hotspots
100-
except Exception as e:
101-
print("ERROR", e)
102-
raise e
88+
if parent_sym is None:
89+
return None
90+
return parent_sym["name"]
91+
92+
hotspots = list(
93+
islice(
94+
map(
95+
lambda x: {
96+
"parent_region": get_parent_region(x[0]) or "<unknown>",
97+
"loc": x[0],
98+
"pct_time": x[1] * 100,
99+
},
100+
filter(lambda x: x[0].line > 0, perf_tabulated),
101+
),
102+
NUM_HOTSPOTS,
103+
)
104+
)
105+
return hotspots
106+
107+
108+
@function_tool
109+
def generate_flamegraph(
110+
ctx: RunContextWrapper[AgentContext],
111+
) -> ToolOutputImage:
112+
"""Generate a flamegraph PNG image from the performance data, building the project and running the profiler if necessary."""
113+
project = ctx.context.project
114+
perf_data = _shared_build_and_run_perf(project)
115+
116+
flamegraph_data = make_flamegraph_png(perf_data.data_path())
117+
flamegraph_data_url = png_to_data_url(flamegraph_data)
118+
flamegraph_output = ToolOutputImage(image_url=flamegraph_data_url, detail="high")
119+
return flamegraph_output
103120

104121

105122
@function_tool

0 commit comments

Comments
 (0)