1 //
2 // Copyright (c) 2010-2025 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using System;
8 using System.Collections.Generic;
9 using System.IO;
10 using System.Linq;
11 using System.Text;
12 using Antmicro.Renode.Logging;
13 
14 namespace Antmicro.Renode.Peripherals.CPU.GuestProfiling
15 {
16     public class CollapsedStackProfiler : BaseProfiler
17     {
CollapsedStackProfiler(TranslationCPU cpu, string filename, bool flushInstantly, long? fileSizeLimit = null, int? maximumNestedContexts = null)18         public CollapsedStackProfiler(TranslationCPU cpu, string filename, bool flushInstantly, long? fileSizeLimit = null, int? maximumNestedContexts = null)
19             : base(cpu, flushInstantly, maximumNestedContexts)
20         {
21             this.fileSizeLimit = fileSizeLimit;
22             stringBuffer = new StringBuilder();
23             fileStream = new StreamWriter(filename);
24         }
25 
Dispose()26         public override void Dispose()
27         {
28             base.Dispose();
29             fileStream.Close();
30         }
31 
StackFrameAdd(ulong currentAddress, ulong returnAddress, ulong instructionsCount)32         public override void StackFrameAdd(ulong currentAddress, ulong returnAddress, ulong instructionsCount)
33         {
34             var currentSymbol = GetSymbolName(currentAddress);
35             cpu.Log(LogLevel.Debug, "Profiler: Pushing new frame with symbol: {0}; returnAddress: 0x{1:X}; currentAddress: 0x{2:X}", currentSymbol, returnAddress, currentAddress);
36 
37             if(isFirstFrame)
38             {
39                 isFirstFrame = false;
40                 // We have to add the first frame, form which the first jump happened
41                 // To infer this frame we use the current PC value, which should point to the jump instruction that triggered this event
42                 var prevSymbol = GetSymbolName(cpu.PC.RawValue);
43                 currentStack.Push(prevSymbol);
44             }
45 
46             var instructionsElapsed = GetInstructionsDelta(instructionsCount);
47             AddStackToBufferWithDelta(instructionsElapsed);
48 
49             currentStack.Push(currentSymbol);
50 
51             CheckAndFlushBuffer();
52         }
53 
StackFramePop(ulong currentAddress, ulong returnAddress, ulong instructionsCount)54         public override void StackFramePop(ulong currentAddress, ulong returnAddress, ulong instructionsCount)
55         {
56             cpu.Log(LogLevel.Debug, "Profiler: Trying to pop frame with returnAddress: 0x{0:X} ({1}); currentAddress: 0x{2:X}", returnAddress, GetSymbolName(returnAddress), currentAddress);
57             if(currentStack.Count == 0)
58             {
59                 cpu.Log(LogLevel.Error, "Profiler: Trying to return from frame while internal stack tracking is empty");
60                 return;
61             }
62 
63             var instructionsElapsed = GetInstructionsDelta(instructionsCount);
64             AddStackToBufferWithDelta(instructionsElapsed);
65 
66             currentStack.Pop();
67 
68             CheckAndFlushBuffer();
69         }
70 
OnContextChange(ulong newContextId)71         public override void OnContextChange(ulong newContextId)
72         {
73             if(newContextId == currentContextId)
74             {
75                 return;
76             }
77 
78             var instructionsElapsed = GetInstructionsDelta(cpu.ExecutedInstructions);
79             AddStackToBufferWithDelta(instructionsElapsed);
80             PushCurrentContextSafe();
81 
82             cpu.Log(LogLevel.Debug, "Profiler: Changing context from: 0x{0:X} to 0x{1:X}", currentContextId, newContextId);
83 
84             if(!wholeExecution.ContainsKey(newContextId))
85             {
86                 wholeExecution.Add(newContextId, new ProfilerContext());
87             }
88 
89             currentContextId = newContextId;
90             currentContext.PopCurrentStack();
91 
92             CheckAndFlushBuffer();
93         }
94 
InterruptEnter(ulong interruptIndex)95         public override void InterruptEnter(ulong interruptIndex)
96         {
97             var instructionsElapsed = GetInstructionsDelta(cpu.ExecutedInstructions);
98             AddStackToBufferWithDelta(instructionsElapsed);
99 
100             cpu.Log(LogLevel.Debug, "Profiler: Interrupt entry (pc 0x{0:X})- saving the stack", cpu.PC);
101             PushCurrentContextSafe();
102 
103             CheckAndFlushBuffer();
104         }
105 
InterruptExit(ulong interruptIndex)106         public override void InterruptExit(ulong interruptIndex)
107         {
108             var instructionsElapsed = GetInstructionsDelta(cpu.ExecutedInstructions);
109             AddStackToBufferWithDelta(instructionsElapsed);
110 
111             cpu.Log(LogLevel.Debug, "Profiler: Interrupt exit - restoring the stack");
112             currentContext.PopCurrentStack();
113 
114             CheckAndFlushBuffer();
115         }
116 
FlushBuffer()117         public override void FlushBuffer()
118         {
119             // DisableProfiler() calls Dispose() which calls FlushBuffer()
120             // This flag is required to prevent an infinite loop
121             if(isDisposing)
122             {
123                 return;
124             }
125 
126             lock(bufferLock)
127             {
128                 if(fileSizeLimit.HasValue && (fileWrittenBytes + stringBuffer.Length) > fileSizeLimit)
129                 {
130                     isDisposing = true;
131                     cpu.Log(LogLevel.Warning, "Profiler: Maximum file size exceeded, removing profiler");
132                     cpu.DisableProfiler();
133                     return;
134                 }
135 
136                 fileStream.Write(stringBuffer.ToString());
137                 fileWrittenBytes += stringBuffer.Length;
138                 stringBuffer.Clear();
139             }
140         }
141 
GetInstructionsDelta(ulong currentInstructionsCount)142         private ulong GetInstructionsDelta(ulong currentInstructionsCount)
143         {
144             currentInstructionsCount += cpu.SkipInstructions + cpu.SkippedInstructions;
145             ulong instructionsElapsed = checked(currentInstructionsCount - lastInstructionsCount);
146             lastInstructionsCount = currentInstructionsCount;
147             return instructionsElapsed;
148         }
149 
AddStackToBufferWithDelta(ulong instructionDelta)150         private void AddStackToBufferWithDelta(ulong instructionDelta)
151         {
152             if(instructionDelta == 0)
153             {
154                 // Speedscope doesn't draw segments with length of 0
155                 // We can just skip them to save some file space
156                 return;
157             }
158 
159             var collapsedStack = FormatCollapsedStackString(currentStack);
160             AddToBuffer($"{collapsedStack} {instructionDelta}");
161         }
162 
AddToBuffer(string stringToAdd)163         private void AddToBuffer(string stringToAdd)
164         {
165             lock(bufferLock)
166             {
167                 stringBuffer.AppendLine(stringToAdd);
168             }
169         }
170 
CheckAndFlushBuffer()171         private void CheckAndFlushBuffer()
172         {
173             if(flushInstantly || stringBuffer.Length > BufferFlushLevel)
174             {
175                 FlushBuffer();
176             }
177         }
178 
FormatCollapsedStackString(Stack<string> stack)179         private string FormatCollapsedStackString(Stack<string> stack)
180         {
181             return string.Join(";", stack.Reverse());
182         }
183 
GetCurrentStack()184         public override string GetCurrentStack()
185         {
186             var result = FormatCollapsedStackString(currentStack);
187 
188             return result;
189         }
190 
191         private readonly StringBuilder stringBuffer;
192         private readonly StreamWriter fileStream;
193         private readonly long? fileSizeLimit;
194 
195         private ulong lastInstructionsCount;
196         private long fileWrittenBytes;
197         private bool isDisposing;
198 
199         private const int BufferFlushLevel = 1000000;
200     }
201 }
202