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 /// Support for <a href="https://github.com/kiith-sa/despiker">Despiker</a>
7 module tharsis.prof.despikersender;
8 
9 import std.algorithm;
10 import std.array;
11 import std.exception: assumeWontThrow, ErrnoException;
12 import std.string;
13 
14 import tharsis.prof.profiler;
15 
16 
17 /// Exception thrown at DespikerSender errors.
18 class DespikerSenderException : Exception
19 {
20     this(string msg, string file = __FILE__, int line = __LINE__) @safe pure nothrow
21     {
22         super(msg, file, line);
23     }
24 }
25 
26 // TODO: Replace writeln by logger once in Phobos 2014-10-06
27 /** Sends profiling data recorded by one or more profilers to the Desiker profiler.
28  *
29  * <a href="https://github.com/kiith-sa/despiker">Despiker</a> is a real-time graphical
30  * profiler based on Tharsis.prof. It visualizes zones in frames while the game is running
31  * and allows the user to move between frames and automatically find the worst frame. Note
32  * that at the moment, Despiker is extremely experimental and unstable.
33  *
34  * See <a href="http://defenestrate.eu/docs/despiker/index.html">Despiker
35  * documentation</a> for more info.
36  *
37  * Example:
38  * --------------------
39  * // Profiler profiler - construct it somewhere first
40  *
41  * auto sender = new DespikerSender([profiler]);
42  *
43  * for(;;)
44  * {
45  *     // Despiker will consider code in this zone (named "frame") a part of a single frame.
46  *     // Which zone is considered a frame can be changed by setting the 
47  *     // sender.frameFilter property.
48  *     auto frame = Zone(profiler, "frame");
49  *
50  *     ... frame code here, with more zones ...
51  *
52  *     if(... condition to start Despiker ...)
53  *     {
54  *         // Looks in the current directory, directory with the binary/exe and in PATH
55  *         // use sender.startDespiker("path/to/despiker") to specify explicit path
56  *         sender.startDespiker();
57  *     }
58  *
59  *     // No zones must be in progress while sender is being updated, so we end the frame
60  *     // early by destroying it.
61  *     destroy(frame);
62  *
63  *     // Update the sender, send profiling data to Despiker if it is running
64  *     sender.update();
65  * }
66  * --------------------
67  */
68 class DespikerSender
69 {
70     /// Maximum number of profilers we can send data from.
71     enum maxProfilers = 1024;
72 private:
73     /* Profilers we're sending data from.
74      *
75      * Despiker assumes that each profiler is used to profile a separate thread 
76      * (profilers_[0] - thread 0, profilers_[1] - thread 1, etc.).
77      */
78     Profiler[] profilers_;
79 
80     import std.process;
81     /* Pipes to send data to the Despiker (when running) along with pid to check its state.
82      *
83      * Reset by reset().
84      */
85     ProcessPipes despikerPipes_;
86 
87     /* Storage for bytesSentPerProfiler.
88      * 
89      * Using a fixed-size array for simplest/fastest allocation. 8kiB is acceptable but
90      * right at the edge of being acceptable... if we ever increase maxProfilers above
91      * 1024, we should use malloc.
92      */
93     size_t[1024] bytesSentPerProfilerStorage_;
94     /* Number of bytes of profiling data sent from each profiler in profilers_.
95      *
96      * Reset by reset().
97      */
98     size_t[] bytesSentPerProfiler_;
99 
100     // Are we sending data to a running Despiker right now?
101     bool sending_ = false;
102 
103     // Used to determine which zones are frames.
104     DespikerFrameFilter frameFilter_;
105 
106 public:
107     // TODO screenshot of a frame in despiker near 'matching their 'frame' zones' text
108     // 2014-10-08
109     /** Construct a DespikerSender sending data recorded by specified profilers.
110      *
111      * Despiker will show results from passed profilers side-by-side, matching their
112      * 'frame' zones. To get meaningful results, all profilers should start profiling
113      * at the same time and have one 'frame' zone for each frame of the profiled 
114      * game/program.
115      *
116      * The main use of multiple profilers is to profile code in multiple threads
117      * simultaneously. If you don't need this, pass a single profiler.
118      *
119      * Params:
120      *
121      * profilers = Profilers the DespikerSender will send data from. Must not be empty.
122      *             Must not have more than 1024 profilers.
123      *
124      * Note:
125      *
126      * At the moment, DespikerSender does not support resetting profilers. Neither of the
127      * passed profilers may be reset() while the DespikerSender is being used.
128      */
129     this(Profiler[] profilers) @safe pure nothrow @nogc
130     {
131         assert(!profilers.empty, "0 profilers passed to DespikerSender constructor");
132         assert(profilers.length <= maxProfilers,
133                "Despiker supports at most 1024 profilers at once");
134 
135         profilers_ = profilers;
136         bytesSentPerProfiler_ = bytesSentPerProfilerStorage_[0 .. profilers.length];
137         bytesSentPerProfiler_[] = 0;
138     }
139 
140     /// Is there a Despiker instance (that we are sending to) running at the moment?
141     bool sending() @safe pure nothrow const @nogc
142     {
143         return sending_;
144     }
145 
146     /** Set the filter to used by Despiker to determine which zones are frames.
147      *
148      * Affects the following calls to startDespiker(), does not affect the running
149      * Despiker instance, if any.
150      */
151     void frameFilter(DespikerFrameFilter rhs) @safe pure nothrow @nogc
152     {
153         assert(rhs.info != "NULL",
154                "DespikerFrameFilter.info must not be set to string value \"NULL\"");
155         frameFilter_ = rhs;
156     }
157 
158     /** Open a Despiker window and start sending profiling data to it.
159      *
160      * Can only be called when sending is false (after DespikerSender construction
161      * or reset).
162      *
163      * By default, looks for the Despiker binary in the following order:
164      *
165      * 0: defaultPath, if specified
166      * 1: 'despiker' in current working directory
167      * 2: 'despiker' in directory of the running binary
168      * 3: 'despiker' in PATH
169      *
170      * Note: the next update() call will send all profiling data recorded so far to the
171      * Despiker.
172      *
173      * Throws:
174      *
175      * DespikerSenderException on failure (couldn't find or run Despiker in any of the
176      * searched paths, or couldn't run Despiker with path specified by defaultPath).
177      */
178     void startDespiker(string defaultPath = null) @safe
179     {
180         assert(!sending_,
181                "Can't startDespiker() while we're already sending to a running Despiker");
182 
183         string[] errorStrings;
184 
185         // Tries to run Despiker at specified path. Adds a string to errorStrings on failure.
186         bool runDespiker(string path) @trusted nothrow
187         {
188             import std.stdio: StdioException;
189             try
190             {
191                 import std.conv: to;
192                 // Pass the frame filter through command-line arguments.
193                 auto args = [path,
194                              "--frameInfo",
195                              // "NULL" represents "don't care about frame info"
196                              frameFilter_.info is null ? "NULL" : frameFilter_.info,
197                              "--frameNestLevel",
198                              to!string(frameFilter_.nestLevel)];
199                 despikerPipes_ = pipeProcess(args, Redirect.stdin);
200                 sending_ = true;
201                 return true;
202             }
203             catch(ProcessException e) { errorStrings ~= e.msg; }
204             catch(StdioException e)   { errorStrings ~= e.msg; }
205             catch(Exception e)
206             {
207                 assert(false, "Unexpected exception when trying to start Despiker");
208             }
209 
210             return false;
211         }
212 
213         // Path specified by the user.
214         if(defaultPath !is null && runDespiker(defaultPath))
215         {
216             return;
217         }
218 
219         import std.file: thisExePath;
220         import std.path: dirName, buildPath;
221         // User didn't specify a path, we have to find Despiker ourselves.
222         // Try current working directory first, then the directory the game binary is
223         // in, then a despiker installation in $PATH
224         auto paths = ["./despiker",
225                       thisExePath().dirName.buildPath("despiker"),
226                       "despiker"];
227 
228         foreach(path; paths) if(runDespiker(path))
229         {
230             return;
231         }
232 
233         throw new DespikerSenderException
234             ("Failed to start Despiker.\n "
235              "Tried to look for it in:\n "
236              ~ defaultPath is null
237                ? "" : "0: path provided by caller: '%s'\n".format(defaultPath) ~
238              "1: working directory, 2: directory of the running binary, 3: PATH.\n"
239              "Got errors:\n" ~ errorStrings.join("\n"));
240     }
241 
242     /** Resets the sender.
243      *
244      * If Despiker is running, 'forgets' it, stops sending to it without closing it and
245      * the next startDespiker() call will launch a new Despiker instance.
246      */
247     void reset() @trusted nothrow @nogc
248     {
249         sending_ = false;
250         // Not @nogc yet, although ProcessPipes destruction should not use GC. 
251         // Probably don't need to explicitly destroy this here anyway.
252         // destroy(despikerPipes_).assumeWontThrow;
253         bytesSentPerProfiler_[] = 0;
254     }
255 
256     import std.stdio;
257     /** Update the sender.
258      *
259      * Must not be called if any profilers passed to DespikerSender constructor are in a
260      * zone, or are being accessed by other threads (any synchronization must be handled
261      * by the caller).
262      */
263     void update() @trusted nothrow
264     {
265         assert(!profilers_.canFind!(p => p.zoneNestLevel > 0),
266                "Can't update DespikerSender while one or more profilers are in a zone");
267 
268         if(!sending_) { return; }
269         // Check if Despiker got closed.
270         try
271         {
272             // tryWait may fail. There is no 'nice' way to handle that.
273             auto status = tryWait(despikerPipes_.pid);
274 
275             if(status.terminated)
276             {
277                 // Non-zero status means Despiker had an error.
278                 if(status.status != 0)
279                 {
280                     writeln("Despiker exited (crashed?) with non-zero status: ", status.status)
281                            .assumeWontThrow;
282                 }
283                 reset();
284                 return;
285             }
286         }
287         catch(ProcessException e)
288         {
289             writefln("tryWait failed: %s; assuming Despiker dead", e.msg).assumeWontThrow;
290             reset();
291             return;
292         }
293         catch(Exception e) { assert(false, "Unexpected exception"); }
294 
295         send().assumeWontThrow;
296         foreach(profiler; profilers_) { profiler.checkpointEvent(); }
297     }
298 
299 private:
300     /** Send profiled data (since the last send()) to Despiker.
301      *
302      * Despite not being nothrow, should never throw.
303      */
304     void send() @trusted
305     {
306         try foreach(uint threadIdx, profiler; profilers_)
307         {
308             const data = profiler.profileData;
309             const newData = data[bytesSentPerProfiler_[threadIdx] .. $];
310 
311             import std.array;
312             if(newData.empty) { continue; }
313             const uint bytes = cast(uint)newData.length;
314             // Send a chunk of raw profile data perfixed by a header of two 32-bit uints:
315             // thread index and chunks size in bytes.
316             uint[2] header;
317             header[0] = threadIdx;
318             header[1] = bytes;
319             despikerPipes_.stdin.rawWrite(header[]);
320             despikerPipes_.stdin.rawWrite(newData);
321             // Ensure 'somewhat real-time' sending.
322             despikerPipes_.stdin.flush();
323 
324             assert(bytesSentPerProfiler_[threadIdx] + newData.length == data.length,
325                    "Newly sent data doesn't add up to all recorded data");
326 
327             bytesSentPerProfiler_[threadIdx] = data.length;
328         }
329         catch(ErrnoException e) { writeln("Failed to send data to despiker: ", e); }
330         catch(Exception e) { writeln("Unhandled exception while sending to despiker: ", e); }
331     }
332 }
333 
334 /** Used to tell Despiker which zones are 'frames'.
335  *
336  * Despiker displays one frame at a time, and it lines up frames from multiple profilers
337  * enable profiling multiple threads. Properties of this struct determine which zones will
338  * be considered frames by Despiker.
339  *
340  * By default, it is enough to use a zone with info string set to "frame" (and to ensure
341  * no other zone uses the same info string).
342  */
343 struct DespikerFrameFilter
344 {
345     /** Info string of a zone must be equal to this for that zone to be considered a frame.
346      *
347      * If null, zone info does not determine which zones are frames. Must not be set to
348      * string value "NULL".
349      */
350     string info = "frame";
351 
352     /** Nest level of a zone must be equal to this for that zone to be considered a frame.
353      *
354      * If 0, zone nest level does not determine which zones are frames.
355      */
356     ushort nestLevel = 0;
357 }
358