1 // Copyright Ferdinand Majerech 2014. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 7 /// Despiker front-end, designed to be trivially controlled through a GUI. 8 module despiker.despiker; 9 10 11 import std.algorithm; 12 import std.array; 13 import std.stdio; 14 import std.typecons; 15 16 import tharsis.prof; 17 18 import despiker.backend; 19 import despiker.profdatasource; 20 21 22 /// Exception thrown at Despiker error. 23 class DespikerException : Exception 24 { 25 this(string msg, string file = __FILE__, int line = __LINE__) @safe pure nothrow 26 { 27 super(msg, file, line); 28 } 29 } 30 31 32 /// View of execution in the current frame provided by Despiker.currentFrameView(). 33 struct FrameView(Events) 34 { 35 /// View of execution in one thread. 36 struct ThreadFrameView 37 { 38 /// Info about execution in this thread during this frame (e.g. start, duration). 39 FrameInfo frameInfo; 40 /// Profiling events recorded during this frame in this thread. 41 Events events; 42 /// Range of all variables recorded during this frame in this thread. 43 VariableRange!Events vars; 44 /// Range of all zones recorded during this frame in this thread. 45 ZoneRange!Events zones; 46 } 47 48 /// View of each thread during this frame. 49 ThreadFrameView[] threads; 50 } 51 52 /** Despiker front-end, designed to be trivially controlled through a GUI. 53 * 54 * The Despiker class provides methods that should directly be called from the GUI 55 * (e.g. from button presses), so that GUI implementation can be as simple as possible. 56 */ 57 class Despiker 58 { 59 public: 60 /** Zones with info equal to this string (and matching nest level) are considered frames. 61 * 62 * If null, zone info does not matter for frame filtering. 63 */ 64 string frameInfo = "frame"; 65 66 /** Zones with nest level equal to this string (and matching info) are considered frames. 67 * 68 * If 0, zone nest level does not matter for frame filtering. 69 */ 70 ushort frameNestLevel = 0; 71 72 /// Despiker 'modes', i.e. what the user is doing with Despiker. 73 enum Mode 74 { 75 /// Track the newest frame, always updating the view with a graph for current frame. 76 NewestFrame, 77 /// Manually move between frames (next/previous frame, move to a specific frame) 78 Manual 79 } 80 81 /** Maximum number of profiling data chunks to receive on an update. 82 * 83 * Too low values may result in Despiker running behind the profiled app, but too 84 * high may result in Despiker getting into a 'death spiral' when each update takes 85 * longer time, during which the profiled app generates more data, which makes the 86 * next despiker frame longer, etc. 87 */ 88 uint maxChunksPerUpdate = 128; 89 90 private: 91 // Despiker backend, which stores profiling data and provides access to event lists. 92 Backend backend_; 93 94 // Profile data source used to read chunks that are then passed to backend. 95 ProfDataSource dataSource_; 96 97 // Current despiker mode. 98 Mode mode_ = Mode.NewestFrame; 99 100 // Readability alias. 101 alias Events = ChunkyEventList.Slice; 102 103 // View of the frame currently being viewed (events recorded in each thread, etc.). 104 FrameView!Events view_; 105 106 // In manual mode, this is the index of the frame we're currently viewing. 107 size_t manualFrameIndex_ = 0; 108 109 public: 110 /** Construct the Despiker. 111 * 112 * Params: 113 * 114 * dataSource = Profiling data source (e.g. from stdin or a file). 115 * 116 * 117 * Throws: 118 * 119 * DespikerException on failure. 120 */ 121 this(ProfDataSource dataSource) @trusted 122 { 123 dataSource_ = dataSource; 124 backend_ = new Backend(&frameFilter); 125 view_ = view_.init; 126 } 127 128 /// Destroy the Despiker. Must be destroyed manually to free any threads, files, etc. 129 ~this() @trusted nothrow 130 { 131 destroy(backend_).assumeWontThrow; 132 destroy(dataSource_).assumeWontThrow; 133 } 134 135 /** Update Despiker. Processes newly received profile data. 136 * 137 * Should be called on each iteration of the event loop. 138 */ 139 void update() @trusted nothrow 140 { 141 // Receive new chunks since the last update and add them to the backend. 142 ProfileDataChunk chunk; 143 foreach(c; 0 .. maxChunksPerUpdate) 144 { 145 if(!dataSource_.receiveChunk(chunk)) { break; } 146 backend_.addChunk(chunk); 147 } 148 149 // Allow the backend to process the new chunks. 150 backend_.update(); 151 152 const threadCount = backend_.threadCount; 153 // If there are no profiled threads, there's no profiling data to view. 154 // If there are no frames, we can't view anything either 155 // (also, manualFrameIndex (0 by default) would be out of range in that case). 156 if(threadCount == 0 || frameCount == 0) 157 { 158 destroy(view_.threads); 159 return; 160 } 161 162 // View the most recent frame zone for which we have profiling data from all threads. 163 view_.threads.assumeSafeAppend(); 164 view_.threads.length = threadCount; 165 foreach(thread; 0 .. threadCount) 166 { 167 FrameInfo frame; 168 final switch(mode_) 169 { 170 case Mode.NewestFrame: 171 // View the last frame we have profiling data from all threads for. 172 frame = backend_.frames(thread)[frameCount - 1]; 173 break; 174 case Mode.Manual: 175 // View the manually-selected frame. 176 frame = backend_.frames(thread)[manualFrameIndex_]; 177 break; 178 } 179 180 with(view_.threads[thread]) 181 { 182 frameInfo = frame; 183 events = backend_.events(thread).slice(frame.extents); 184 vars = VariableRange!Events(events); 185 // Range of zones in the viewed frame for this thread. 186 zones = ZoneRange!Events(events); 187 } 188 } 189 } 190 191 /** Get the 'view' of the current frame. 192 * 193 * Used by the GUI to get zones, variables, etc. to display. 194 * 195 * Returns: A 'frame view' including profiling events, zones and variables recorded 196 * in each profiled thread during the current frame. 197 */ 198 FrameView!Events currentFrameView() @safe pure nothrow const 199 { 200 return FrameView!Events(view_.threads.dup); 201 } 202 203 /// Get the current despiker mode. 204 Mode mode() @safe pure nothrow const @nogc { return mode_; } 205 206 /** Move to the next frame in manual mode. If newest frame mode, acts as pause(). 207 * 208 * Should be directly triggered by a 'next frame' button. 209 */ 210 void nextFrame() @safe pure nothrow @nogc 211 { 212 final switch(mode_) 213 { 214 case Mode.NewestFrame: pause(); break; 215 case Mode.Manual: 216 if(manualFrameIndex_ < frameCount - 1) { ++manualFrameIndex_; } 217 break; 218 } 219 } 220 221 /** Move to the previous frame in manual mode. If newest frame mode, acts as pause(). 222 * 223 * Should be directly triggered by a 'previous frame' button. 224 */ 225 void previousFrame() @safe pure nothrow @nogc 226 { 227 final switch(mode_) 228 { 229 case Mode.NewestFrame: pause(); break; 230 case Mode.Manual: 231 if(manualFrameIndex_ > 0 && frameCount > 0) { --manualFrameIndex_; } 232 break; 233 } 234 } 235 236 /** Set view to the frame with specified index (clamped to frameCount - 1). 237 * 238 * In 'newest first' mode, changes mode to 'manual'. 239 * 240 * Should be directly triggered by a 'view frame' button. 241 */ 242 void frame(size_t rhs) @safe pure nothrow @nogc 243 { 244 final switch(mode_) 245 { 246 case Mode.NewestFrame: 247 mode_ = Mode.Manual; 248 manualFrameIndex_ = min(frameCount - 1, rhs); 249 break; 250 case Mode.Manual: 251 manualFrameIndex_ = min(frameCount - 1, rhs); 252 break; 253 } 254 } 255 256 /// Get the index of the currently viewed frame. If there are 0 frames, returns size_t.max. 257 size_t frame() @safe pure nothrow const @nogc 258 { 259 if(frameCount == 0) { return size_t.max; } 260 final switch(mode_) 261 { 262 case Mode.NewestFrame: return frameCount - 1; 263 case Mode.Manual: return manualFrameIndex_; 264 } 265 } 266 267 /** Set mode to 'manual', pausing at the current frame. In manual mode, does nothing. 268 * 269 * Should be directly triggered by a 'pause' button. 270 */ 271 void pause() @safe pure nothrow @nogc 272 { 273 frame = frameCount - 1; 274 } 275 276 /** Resume viewing current frame. (NewestFrame mode). Ignored if we're already doing so. 277 * 278 * Should be directly triggered by a 'resume' button. 279 */ 280 void resume() @safe pure nothrow @nogc 281 { 282 final switch(mode_) 283 { 284 case Mode.NewestFrame: break; 285 case Mode.Manual: mode_ = Mode.NewestFrame; break; 286 } 287 } 288 289 /** Find and view the worst frame so far. 290 * 291 * In 'newest frame' mode, sets mode to 'manual'. 292 * 293 * Finds the frame that took the most time so far (comparing the slowest thread's 294 * time for each frame). 295 * 296 * Should be directly triggered by a 'worst frame' button. 297 */ 298 void worstFrame() @safe pure nothrow @nogc 299 { 300 // Duration of the worst frame. 301 ulong worstDuration = 0; 302 // Index of the worst frame. 303 size_t worstFrame = 0; 304 // In each frame, get the total duration for all threads, then get the max of that. 305 foreach(f; 0 .. frameCount) 306 { 307 // Get the real start/end time of the frame containing execution in all threads. 308 ulong start = ulong.max; 309 ulong end = ulong.min; 310 foreach(thread; 0 .. backend_.threadCount) 311 { 312 const frame = backend_.frames(thread)[f]; 313 start = min(start, frame.startTime); 314 end = max(end, frame.endTime); 315 } 316 const frameDuration = end - start; 317 318 if(frameDuration > worstDuration) 319 { 320 worstFrame = f; 321 worstDuration = frameDuration; 322 } 323 } 324 325 // Look at the worst frame. 326 frame(worstFrame); 327 } 328 329 /// Get the number of frames for which we have profiling data from all threads. 330 size_t frameCount() @safe pure nothrow const @nogc 331 { 332 if(backend_.threadCount == 0) { return 0; } 333 334 size_t result = size_t.max; 335 foreach(t; 0 .. backend_.threadCount) 336 { 337 result = min(result, backend_.frames(t).length); 338 } 339 return result; 340 } 341 342 private: 343 /** Function passed to Backend used to filter zones to determine which zones are frames. 344 * 345 * See_Also: frameInfo, frameNestLevel 346 */ 347 bool frameFilter(ZoneData zone) @safe nothrow @nogc 348 { 349 return (frameInfo is null || zone.info == frameInfo) && 350 (frameNestLevel == 0 || zone.nestLevel == frameNestLevel); 351 } 352 }