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 }