root/trunk/ptrace/ptrace.d

Revision 220, 29.5 kB (checked in by Lutger, 9 months ago)

fixed to work with recent tango. Added option to directly generate html. bugfix: integer overflow in counters.

Line 
1 module ptrace;
2
3 import tango.io.FileConduit;
4 import tango.io.File;
5 import tango.io.Stdout;
6 import tango.text.convert.Integer;
7 import tango.core.Array;
8 import tango.stdc.ctype;
9 import tango.util.Arguments;
10 import tango.text.stream.LineIterator;
11 import tango.sys.Process;
12 import tango.sys.Environment;
13 import tango.stdc.stdio;
14 import Regex        = tango.text.Regex;
15 import Util         = tango.text.Util;
16 import ConvertFloat = tango.text.convert.Float;
17
18
19
20 enum SymbolMode { FQN, SEP, SYM }
21
22 SymbolMode timingsMode = SymbolMode.SEP;
23 SymbolMode graphMode = SymbolMode.SEP;
24
25 char[]  sourceFile;
26 char[]  targetFile;
27 char[]  targetTimingsFile;
28 char[]  targetGraphFile;
29
30 bool    showHelp = false;
31 bool    onePass = false;
32
33 enum ParseMode
34 {
35     Full,
36     QualifiedName
37 }
38
39 ParseMode mode = ParseMode.QualifiedName;
40 char[]  helpTxt =
41                  "  usage: ptrace [file] [options]\n"
42                  "    -t=<target>      write to target, must be with d extension for dmd\n"
43                  "    -s=<mode>        specifies how to write symbols, <mode> can be of one:\n"
44                  "                       FQN    fully qualified name\n"
45                  "                       SYM    identifier only, not type information\n"
46                  "                       SEP    seperate columns for type and identifier\n"
47                  "    -h               this help text\n"
48                  "    -t=<target>      write to target, must be with d extension for dmd\n"
49                  "    -f               do not ask for permission to overwrite existing files\n"
50                  "    -x               eXecute dmd and generate html instead of ddoc code\n\n"
51                  "  if no source file has been given, trace.log is assumed\n"
52                  "  if no target file has been given, -x is assumed\n";
53
54
55 /* TODO: implement these options correctly
56 "    -st=<mode>       idem, but only for timings\n"
57 "    -sg=<mode>       idem, but only for graph\n"
58 "    -tt=<target>     write only timings to target, must be with d extension for dmd\n"
59                  "    -tg=<target>     write only call graph to target, must be with d extension for dmd\n"
60 */
61
62 bool parseArgs(char[][] args)
63 {
64     bool force = false;
65
66     auto arguments = new Arguments(args, ["source"]);
67     if (arguments["source"].length == 1)
68         sourceFile = arguments["source"][0];
69     else if (arguments["source"].length > 1)
70     {
71         Stdout("wrong number of source files").newline;
72         showHelp = true;
73     }
74     else
75         sourceFile = "trace.log";
76
77     with(arguments)
78     {
79         addValidation("h", false, false);
80         addValidation("s", false, true);
81         addValidation("t", false, true);
82         addValidation("f", false, false);
83         addValidation("x", false, false);
84     }
85
86     if ("h" in arguments)
87         showHelp = true;
88
89     if ("x" in arguments)
90         onePass = true;
91
92     if ("f" in  arguments)
93         force = true;
94
95     if ("s" in arguments)
96     {
97         switch(arguments["s"][0])
98         {
99             case "FQN": graphMode = timingsMode = SymbolMode.FQN; break;
100             case "SYM": graphMode = timingsMode = SymbolMode.SYM; break;
101             case "SEP": graphMode = timingsMode = SymbolMode.SEP; break;
102             default: showHelp = true; break;
103         }
104     }
105     if (arguments["t"].length)
106         targetFile = arguments["t"][0];
107     else
108     {
109         targetFile = "ddoc_" ~ sourceFile ~ ".d";
110         onePass = true;
111     }
112
113     if( (new FilePath(targetFile)).exists() && !force)
114     {
115         char response;
116         while (true)
117         {
118             Stdout.format("Are you sure you want to overwrite {} (y/n)?", targetFile).newline;
119             response = getchar();
120             if (response == 'n' || response == 'N')
121                 return false;
122             else if (response == 'y' || response == 'Y')
123                 break;
124         }
125     }
126
127     return true;
128 }
129
130
131 int main(char[][] args)
132 {
133     Stdout("Ptrace. Utility to prettify trace.log files. ptrace -h for help.\n");
134     if (!parseArgs(args))
135         return 0;
136     if (showHelp)
137     {
138         Stdout(helpTxt);
139         return 0;
140     }
141
142     //Stdout.format("Reading from {0} and writing to {1}", sourceFile, targetFile).newline;
143
144     auto reader = new TraceReader(sourceFile, new TraceParser);
145
146     reader.read;
147
148     auto output = new FileConduit(targetFile, FileConduit.ReadWriteCreate);
149
150     char[] header()
151     {
152         switch (timingsMode)
153         {
154             case SymbolMode.FQN: return " $(HEADER_FUNCTION_NAME symbol)\n";
155             case SymbolMode.SYM: return " $(HEADER_FUNCTION_NAME identifier)\n";
156             case SymbolMode.SEP: return " $(HEADER_FUNCTION_NAME identifier) $(HEADER_FUNCTION_NAME type) \n";
157             default: assert(false); break;
158         }
159     }
160
161     Stdout("writing output to " ~ targetFile ~ "...").newline;
162     output.output.write("Ddoc\n");
163
164     if (onePass)
165     {
166         output.output.write(macros);
167     }
168
169     output.output.write("\n$(TIMINGS $(HEADER_CALLS Num calls) $(HEADER_TREE  Tree Time)"
170                         "$(HEADER_FUNC  Func Time) $(HEADER_CALL Per Call)" ~ header() );
171
172     char[] functionName(char[] mangled)
173     {
174         switch(timingsMode)
175         {
176             case SymbolMode.SYM: return " $(FUNCTION_NAME " ~ reader.getSYM(mangled) ~ ") )\n";
177             case SymbolMode.FQN: return " $(FUNCTION_NAME " ~ reader.getFQN(mangled) ~ ") )\n";
178             case SymbolMode.SEP: return " $(FUNCTION_NAME " ~ reader.getSYM(mangled) ~  ") "
179                                         " $(FUNCTION_NAME " ~ reader.getTYPE(mangled) ~  ") )\n";
180             default: assert(false); break;
181         }
182     }
183
184     foreach(timing; reader.timings)
185         output.output.write( " $(TIMING  $(CALLS " ~ toString(timing.calls) ~ ")"
186                              " $(TREE  " ~ toString(timing.tree) ~ ") $(FUNC  " ~ toString(timing.func) ~ ")"
187                              " $(CALL  " ~ toString(timing.call) ~ ")" ~ functionName(timing.mangled) );
188
189     output.output.write(")\n $(CALL_GRAPH \n");
190
191 /*
192 TODO:
193 make call graph output fqn/sym/type of symbol correctly: specifify colspan in trace.ddoc macro's
194
195 */
196
197     foreach(node; reader.parser.nodes)
198     {
199         output.output.write("$(CALL_SEP )\n");
200         foreach(fun; node.fan_in)
201             output.output.write("$(FAN $(FAN_FUNC " ~ reader.getFQN(fun.mangled) ~ ") $(FAN_CALLS " ~ toString(fun.numCalls) ~ "))\n");
202
203         output.output.write("$(CALL_FUN $(CALL_FUN_NAME 2, " ~ reader.getFQN(node.mangled) ~
204                             ") $(CALL_FUN_TREE  " ~ ConvertFloat.toString(cast(real)node.tree / reader.freq, 2, false ) ~ ")"
205                             " $(CALL_FUN_FUNC " ~ ConvertFloat.toString(cast(real)node.func / reader.freq, 2, false) ~ ")"
206                             " $(CALL_FUN_CALLS " ~ toString(node.count) ~ ") )\n" );
207
208         foreach(fun; node.fan_out)
209             output.output.write("$(FAN $(FAN_FUNC " ~ reader.demangleSymbol(fun.mangled) ~ ") $(FAN_CALLS  " ~ toString(fun.numCalls) ~ ") )\n");
210
211     }
212     output.output.write(")\n");
213     output.close;
214
215     if (onePass)
216     {
217         Stdout("generating html...").newline;
218         Process dmd = new Process(Environment.exePath("dmd").toString, "-D"[], targetFile, null);
219         dmd.execute();
220         auto result = dmd.wait();
221         Stdout("removing temporary files...").newline;
222         (new FilePath(targetFile)).remove();
223         return result.status;
224     }
225     return 0;
226 }
227
228 struct FuncCall
229 {
230     char[] mangled;
231     uint numCalls;
232 }
233
234 struct CallNode
235 {
236     char[] mangled;
237     FuncCall[] fan_in;
238     FuncCall[] fan_out;
239     ulong count;
240     ulong tree;
241     ulong func;
242 }
243
244 struct Timing
245 {
246     char[] mangled;
247     ulong calls;
248     // timings:
249     ulong tree;
250     ulong func;
251     ulong call;
252 }
253
254 class TraceParser
255 {
256     void processCall(char[][] fan_in, char[] func, char[][] fan_out)
257     {
258         auto isSpace = &Util.isSpace!(char);
259         nodes.length = nodes.length + 1;
260
261         void parseFan(ref FuncCall[] calls, ref char[][] data)
262         {
263             foreach (line; data)
264             {
265                 int numcalls;
266
267                 int index = findIf(line, &Util.isSpace!(char));
268
269                 if (index > 0)
270                     numcalls = toLong(line[0..index]);
271
272                 index = rfindIf(line, isSpace);
273                 if (index > 0)
274                 {
275                    calls ~= FuncCall(Util.trim(line[index..$]), numcalls);
276                 }
277             }
278         }
279
280         parseFan(nodes[$-1].fan_in, fan_in);
281         parseFan(nodes[$-1].fan_out, fan_out);
282
283         // function symbol
284         int index = findIf(func, isSpace);
285         if (index > 0)
286             nodes[$-1].mangled = Util.trim(func[0..index]).dup;
287
288         // tree time
289         func = Util.trim(func[index..$]);
290         index = findIf(func, isSpace);
291
292         if (index > 0)
293             nodes[$-1].count = toLong(func[0..index]);
294
295         // function time
296         func = Util.trim(func[index..$]);
297         index = rfindIf(func, isSpace);
298         if (index > 0)
299             nodes[$-1].tree = toLong(func[0..index]);
300
301         // number of calls
302         nodes[$-1].func = toLong(Util.trim(func[index..$]));
303     }
304
305     CallNode[] nodes;
306 }
307
308
309 Words words(char[] str)
310 {
311     return Words.iter(str);
312 }
313
314 struct Words
315 {
316     private char[] src;
317
318     static Words iter(char[] str)
319     {
320         Words result;
321         result.src = str;
322         return result;
323     }
324
325     int opApply (int delegate (ref char[] token) dg)
326     {
327         int result;
328         uint start = 0;
329         enum : ubyte
330         {
331             WORD,
332             SPACE
333         }
334         ubyte state = SPACE;
335
336         char[]  token;
337
338         foreach (index, elem; src)
339         {
340             if(state == SPACE)
341             {
342                 if (!Util.isSpace(elem))
343                 {
344                     state = WORD;
345                     start = index;
346                 }
347             }
348             else if (Util.isSpace(elem))
349             {
350                 token = src[start..index];
351                 dg(token);
352                 state = SPACE;
353             }
354
355         }
356         if (state == WORD && start < src.length)
357         {
358             token = src[start..$];
359             dg(token);
360         }
361         return result;
362     }
363 }
364
365 class ParseException : Exception
366 {
367     this(char[] msg)
368     {
369         super(msg);
370     }
371 }
372
373 class TraceReader
374 {
375     this(char[] filename, TraceParser parser)
376     {
377         this.fileName = filename;
378         this.parser = parser;
379     }
380
381     void read()
382     {
383         timings.length = 0;
384         auto contents = new FileConduit(fileName);
385         auto lines = new LineIterator!(char)(contents);
386         bool pastGraph = false;
387         char[] line;
388
389         if (lines.next)
390         {
391             line = Util.trim(lines.get);
392             if (line.length)
393             {
394                 if (line[0] != '-')
395                     throw new ParseException("unexpected format of trace log file");
396             }
397             else
398                 throw new ParseException("empty file");
399         }
400
401         char[][]    fanInLines;
402         char[][]    fanOutLines;
403         char[]      callLine;
404         bool        pastCall = false;
405         uint        count = 0;
406
407
408         while(lines.next && !pastGraph)
409         {
410             count++;
411
412             line = Util.trim(lines.get);
413
414             if(!line.length)
415                 continue;
416             else if(line[0] == '-' || line[0] == '=')
417             {
418                 parser.processCall(fanInLines, callLine, fanOutLines);
419                 fanInLines.length = 0;
420                 fanOutLines.length = 0;
421                 callLine.length = 0;
422                 pastCall = false;
423                 if (line[0] == '=')
424                     pastGraph = true;
425                 else
426                     continue;
427             }
428             else
429             {
430                 if (line[0] == '_')
431                 {
432                     pastCall = true;
433                     callLine = line.dup;
434                 }
435                 else if(!pastCall) // fan_in
436                     fanInLines ~= line.dup;
437                 else if(pastCall) // fan_Out
438                     fanOutLines ~= line.dup;
439             }
440         }
441
442         auto freqString = Regex.search(line, r"[\d]+").match(0);
443         if(freqString !is null)
444             freq = ConvertFloat.toFloat(freqString) / 1000000.0;
445
446         if (lines.next())
447             line = lines.get;
448
449         char[][]    values;
450
451         while(lines.next)
452         {
453             values.length = 0;
454             foreach(inout word; (lines.get()).words())
455                 values ~= word.dup;
456
457             if (values.length == 5)
458                 timings ~= Timing(values[4].dup, toInt(values[0]), toInt(values[1]), toInt(values[2]), toInt(values[3]));
459         }
460     }
461
462     /+char[] demangleSymbol(char[] mangledName)
463     {
464         auto ptr = mangledName in symbolTable;
465
466         if (ptr)
467             return *ptr;
468         char[] result;
469         try
470         {
471             result = demangle(mangledName);
472             symbolTable[mangledName] = result;
473         }
474         catch(Object error)
475         {
476             Stdout("demangle error (ignored): ")(error.toString).newline;
477             return mangledName;
478         }
479
480         return result;
481
482     }+/
483     alias getFQN demangleSymbol;
484
485
486      //TODO: catch appropiate exception to not choke on invalid string-8 symbols but instead ignore them
487     char[] getFQN(char[] mangledName)
488     {
489         return cachedLookup(mangledName, fqnMap, {
490             fqnMap[mangledName] = demangle(mangledName);
491             return fqnMap[mangledName];
492         });
493     }
494
495     char[] getSYM(char[] mangledName)
496     {
497         return cachedLookup(mangledName, symMap, {
498             char[] s;
499             char[] t;
500             s = demangle(mangledName, t);
501             symMap[mangledName] = s;
502             typeMap[mangledName] = t;
503             return s;
504         });
505     }
506
507     char[] getTYPE(char[] mangledName)
508     {
509         return cachedLookup(mangledName, typeMap, {
510             char[] s;
511             char[] t;
512             s = demangle(mangledName, t);
513             symMap[mangledName] = s;
514             typeMap[mangledName] = t;
515             return t;
516         } );
517     }
518
519     private
520     {
521         char[] cachedLookup(char[] key, ref char[][char[]] map,  char[] delegate() create)
522         {
523             auto ptr = key in map;
524             if (ptr)
525                 return *ptr;
526             return create();
527         }
528
529         real        freq = 0;
530         char[]      fileName;
531         TraceParser parser;
532         Timing[]    timings;
533         char[][char[]] fqnMap;
534         char[][char[]] symMap;
535         char[][char[]] typeMap;
536     }
537 }
538
539 private class MangleException : Exception
540 {
541     this()
542     {
543         super("MangleException");
544     }
545 }
546
547 const char[] macros =
548 `
549 macros:
550 TABLE = <table border="4" rules="none" cellpadding="6" cellspacing="6">$0</table>
551 TDR = <td align="right">$0</td>
552 TD_CSPAN = <td colspan="$1">$+</td>
553
554 TIMINGS = <h1> Timings (in microseconds) </h1>$(TABLE $0)
555 TIMING = <tr>$0</tr>
556 FUNCTION_NAME = <td>$0</td>
557 CALLS = <td align="right">$0</td>
558 TREE = <td align="right">$0</td>
559 FUNC = <td align="right">$0</td>
560 CALL = <td align="right">$0</td>
561
562 HEADER_FUNCTION_NAME = <th>$0</th>
563 HEADER_CALLS = <th>$0</th>
564 HEADER_TREE = <th>$0</th>
565 HEADER_FUNC = <th>$0</th>
566 HEADER_CALL = <th>$0</th>
567
568 CALL_GRAPH =
569         <br>
570         <br>
571         $(P <h1> Call Graph, times are in microseconds</h1>
572         $(TABLE
573
574         <tr><td>Functions that call <i>measured function</i> (fan in)</td>
575            <td>Num calls</td>
576         </tr>
577         <tr><td colspan="2"><b>Measured function symbol</b></td>
578            <td align="right">Tree Time</td>
579            <td align="right">Func Time</td>
580            <td align="right">Num Calls</td>
581         </tr>
582         <tr><td>Functions called by <i>measured function</i> (fan out)</td>
583            <td>Num calls</td>
584         </tr>
585         $(CALL_SEP)
586         $0) )
587 CALL_SEP = <tr><td>&nbsp;</td></tr>
588 CALL_FUN = <tr>$0</tr>
589 CALL_FUN_NAME = $(TD_CSPAN $1, $(B $+))
590 CALL_FUN_TREE = <td align="right">$0</td>
591 CALL_FUN_FUNC = <td align="right">$0</td>
592 CALL_FUN_CALLS = <td align="right">$0</td>
593 FAN =  <tr>$0</tr>
594 FAN_FUNC = <td>$0</td>
595 FAN_CALLS = <td>$0</td>
596
597 DDOC =  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
598         <html><head>
599         <META http-equiv="content-type" content="text/html; charset=utf-8">
600         <link rel="stylesheet" type="text/css" href="style.css">
601         </head><body>
602         $(BODY)
603         </body></html>
604 `;
605
606 char[] demangle(char[] name)
607 {
608     char[] dummy;
609     return demangle(name, dummy, ParseMode.Full);
610 }
611
612 /* This function is taken from phobos and ported to tango.*/
613 char[] demangle(char[] name, ref char[] type, ParseMode pMode = ParseMode.QualifiedName)
614 {
615
616     size_t ni = 2;
617     char[] delegate() fparseTemplateInstanceName;
618
619     static void error()
620     {
621         throw new MangleException();
622     }
623
624     static ubyte ascii2hex(char c)
625     {
626     if (!isxdigit(c))
627         error();
628     return cast(ubyte)
629           ( (c >= 'a') ? c - 'a' + 10 :
630             (c >= 'A') ? c - 'A' + 10 :
631                  c - '0'
632           );
633     }
634
635     size_t parseNumber()
636     {
637         size_t result;
638
639         while (