1 /* ========================================================================
2    $File: tools/ctime/ctime.c $
3    $Date: 2016/05/08 04:16:55PM $
4    $Revision: 7 $
5    $Creator: Casey Muratori $
6    $Notice:
7 
8    The author of this software MAKES NO WARRANTY as to the RELIABILITY,
9    SUITABILITY, or USABILITY of this software. USE IT AT YOUR OWN RISK.
10 
11    This is a simple timing utility.  It is in the public domain.
12    Anyone can use it, modify it, roll'n'smoke hardcopies of the source
13    code, sell it to the terrorists, etc.
14 
15    But the author makes absolutely no warranty as to the reliability,
16    suitability, or usability of the software.  There might be bad bugs
17    in here.  It could delete all your files.  It could format your
18    hard drive.  I have no idea.  If you lose all your files from using
19    it, it is your fault.
20 
21    $
22 
23    ctime is a simple utility that helps you keep track of how much time
24    you spend building your projects.  You use it the same way you would
25    use a begin/end block profiler in your normal code, only instead of
26    profiling your code, you profile your build.
27 
28    BASIC INSTRUCTIONS
29    ------------------
30 
31    On the very first line of your build script, you do something like this:
32 
33        ctime -begin timings_file_for_this_build.ctm
34 
35    and then on the very last line of your build script, you do
36 
37        ctime -end timings_file_for_this_build.ctm
38 
39    That's all there is to it!  ctime will keep track of every build you
40    do, when you did it, and how long it took.  Later, when you'd like to
41    get a feel for how your build times have evolved, you can type
42 
43        ctime -stats timings_file_for_this_build.ctm
44 
45    and it will tell you a number of useful statistics!
46 
47 
48    ADVANCED INSTRUCTIONS
49    ---------------------
50 
51    ctime has the ability to track the difference between _failed_ builds
52    and _successful_ builds.  If you would like it to do so, you can capture
53    the error status in your build script at whatever point you want,
54    for example:
55 
56        set LastError=%ERRORLEVEL%
57 
58    and then when you eventually call ctime to end the profiling, you simply
59    pass that error code to it:
60 
61        ctime -end timings_file_for_this_build.ctm %LastError%
62 
63    ctime can also dump all timings from a timing file into a textual
64    format for use in other types of tools.  To get a CSV you can import
65    into a graphing program or database, use:
66 
67        ctime -csv timings_file_for_this_build.ctm
68 
69    Also, you may want to do things like timing multiple builds separately,
70    or timing builds based on what compiler flags are active.  To do this,
71    you can use separate timing files for each configuration by using
72    the shell variables for the build at the filename, eg.:
73 
74        ctime -begin timings_for_%BUILD_NAME%.ctm
75        ...
76        ctime -end timings_for_%BUILD_NAME%.ctm
77 
78    ======================================================================== */
79 
80 #define _CRT_SECURE_NO_DEPRECATE
81 #include <stdio.h>
82 #include <stdlib.h>
83 #include <time.h>
84 #include <string.h>
85 
86 #pragma pack(push,1)
87 
88 #define MAGIC_VALUE 0xCA5E713F
89 typedef struct timing_file_header
90 {
91     int unsigned MagicValue;
92 } timing_file_header;
93 
94 typedef struct timing_file_date
95 {
96     long long unsigned E;
97 } timing_file_date;
98 
99 enum timing_file_entry_flag
100 {
101     TFEF_Complete = 0x1,
102     TFEF_NoErrors = 0x2,
103 };
104 typedef struct timing_file_entry
105 {
106     timing_file_date StartDate;
107     int unsigned Flags;
108     int unsigned MillisecondsElapsed;
109 } timing_file_entry;
110 
111 #pragma pack(pop)
112 
113 typedef struct timing_entry_array
114 {
115     int EntryCount;
116     timing_file_entry *Entries;
117 } timing_entry_array;
118 
119 //
120 // TODO(casey): More platforms?  Sadly, ANSI C doesn't support high-resolution timing across runs of a process AFAICT :(
121 //
122 
123 #ifdef _WIN32
124 
125 #include <windows.h>
126 
127 static int unsigned
GetClock(void)128 GetClock(void)
129 {
130     if(sizeof(int unsigned) != sizeof(DWORD))
131     {
132         fprintf(stderr, "ERROR: Unexpected integer size - timing will not work on this platform!\n");
133     }
134 
135     return(timeGetTime());
136 }
137 
138 #elif __APPLE__
139 #include "TargetConditionals.h"
140 #    if TARGET_OS_MAC
141 #include <mach/mach_time.h>
142 
143 static int unsigned
GetClock(void)144 GetClock(void)
145 {
146     static mach_timebase_info_data_t ToNanoseconds;
147     if (ToNanoseconds.denom == 0)
148     {
149         mach_timebase_info(&ToNanoseconds);
150     }
151     uint64_t Ticks = mach_absolute_time();
152     int unsigned Result = Ticks *
153         ToNanoseconds.numer / 1000000 / ToNanoseconds.denom;
154     return Result;
155 }
156 #    else
157 #        error "Unknown Apple target"
158 #    endif
159 #else
160 
161 // This was written for Linux
162 
163 static int unsigned
GetClock(void)164 GetClock(void)
165 {
166     struct timespec TimeSpec;
167     int unsigned Result;
168 
169     clock_gettime(CLOCK_REALTIME, &TimeSpec);
170     Result = TimeSpec.tv_sec * 1000 + TimeSpec.tv_nsec / 1000000;
171 
172     return Result;
173 }
174 
175 #endif
176 
177 //
178 //
179 //
180 
181 static timing_file_date
GetDate(void)182 GetDate(void)
183 {
184     timing_file_date Result = {0};
185     Result.E = time(NULL);
186     return(Result);
187 }
188 
189 static void
PrintDate(timing_file_date Date)190 PrintDate(timing_file_date Date)
191 {
192     time_t Time;
193     struct tm *LocalTime;
194     char Str[256];
195 
196     Time = Date.E;
197     LocalTime = localtime(&Time);
198     strftime(Str, 256, "%Y-%m-%d %H:%M:%S", LocalTime);
199     fprintf(stdout, "%s", Str);
200 }
201 
202 static long long unsigned
SecondDifference(timing_file_date A,timing_file_date B)203 SecondDifference(timing_file_date A, timing_file_date B)
204 {
205     long long unsigned Result = A.E - B.E;
206     return Result;
207 }
208 
209 static int unsigned
DayIndex(timing_file_date A)210 DayIndex(timing_file_date A)
211 {
212     time_t Time;
213     struct tm *LocalTime;
214 
215     Time = A.E;
216     LocalTime = localtime(&Time);
217     return LocalTime->tm_yday;
218 }
219 
220 static void
Usage(void)221 Usage(void)
222 {
223     fprintf(stderr, "CTime v1.0 by Casey Muratori\n");
224     fprintf(stderr, "Usage:\n");
225     fprintf(stderr, "  ctime -begin <timing file>\n");
226     fprintf(stderr, "  ctime -end <timing file> [error level]\n");
227     fprintf(stderr, "  ctime -stats <timing file>\n");
228     fprintf(stderr, "  ctime -csv <timing file>\n");
229 }
230 
231 static timing_entry_array
ReadAllEntries(FILE * Handle)232 ReadAllEntries(FILE* Handle)
233 {
234     timing_entry_array Result = {0};
235 
236     int EntriesBegin = sizeof(timing_file_header);
237     int FileSize;
238     if((fseek(Handle, 0, SEEK_END) == 0) && ((FileSize = ftell(Handle)) >= 0))
239     {
240         int EntriesSize = FileSize - EntriesBegin;
241         Result.Entries = (timing_file_entry *)malloc(EntriesSize);
242         if(Result.Entries)
243         {
244             fseek(Handle, EntriesBegin, SEEK_SET);
245             int ReadSize = (int)fread(Result.Entries, 1, EntriesSize, Handle);
246             if(ReadSize == EntriesSize)
247             {
248                 Result.EntryCount = EntriesSize / sizeof(timing_file_entry);
249             }
250             else
251             {
252                 fprintf(stderr, "ERROR: Unable to read timing entries from file.\n");
253             }
254         }
255         else
256         {
257             fprintf(stderr, "ERROR: Unable to allocate %d for storing timing entries.\n", EntriesSize);
258         }
259     }
260     else
261     {
262         fprintf(stderr, "ERROR: Unable to determine file size of timing file.\n");
263     }
264 
265     return(Result);
266 }
267 
268 static void
FreeAllEntries(timing_entry_array Array)269 FreeAllEntries(timing_entry_array Array)
270 {
271     if(Array.Entries)
272     {
273         free(Array.Entries);
274         Array.EntryCount = 0;
275         Array.Entries = 0;
276     }
277 }
278 
279 static void
CSV(timing_entry_array Array,char * TimingFileName)280 CSV(timing_entry_array Array, char *TimingFileName)
281 {
282     int EntryIndex;
283     timing_file_entry *Entry = Array.Entries;
284 
285     fprintf(stdout, "%s Timings\n", TimingFileName);
286     fprintf(stdout, "ordinal, date, duration, status\n");
287     {for(EntryIndex = 0;
288          EntryIndex < Array.EntryCount;
289          ++EntryIndex, ++Entry)
290     {
291         fprintf(stdout, "%d, ", EntryIndex);
292         PrintDate(Entry->StartDate);
293         if(Entry->Flags & TFEF_Complete)
294         {
295             fprintf(stdout, ", %0.3fs, %s", (double)Entry->MillisecondsElapsed / 1000.0,
296                     (Entry->Flags & TFEF_NoErrors) ? "succeeded" : "failed");
297         }
298         else
299         {
300             fprintf(stdout, ", (never completed), failed");
301         }
302 
303         fprintf(stdout, "\n");
304     }}
305 }
306 
307 typedef struct time_part
308 {
309     char *Name;
310     double MillisecondsPer;
311 } time_part;
312 
313 static void
PrintTime(double Milliseconds)314 PrintTime(double Milliseconds)
315 {
316     double MillisecondsPerSecond = 1000;
317     double MillisecondsPerMinute = 60*MillisecondsPerSecond;
318     double MillisecondsPerHour = 60*MillisecondsPerMinute;
319     double MillisecondsPerDay = 24*MillisecondsPerHour;
320     double MillisecondsPerWeek = 7*MillisecondsPerDay;
321     time_part Parts[] =
322     {
323         {"week", MillisecondsPerWeek},
324         {"day", MillisecondsPerDay},
325         {"hour", MillisecondsPerHour},
326         {"minute", MillisecondsPerMinute},
327     };
328     int unsigned PartIndex;
329     double Q = Milliseconds;
330 
331     for(PartIndex = 0;
332         PartIndex < (sizeof(Parts)/sizeof(Parts[0]));
333         ++PartIndex)
334     {
335         double MsPer = Parts[PartIndex].MillisecondsPer;
336         double This = (double)(int)(Q / MsPer);
337 
338         if(This > 0)
339         {
340             fprintf(stdout, "%d %s%s, ", (int)This, Parts[PartIndex].Name,
341                 (This != 1) ? "s" : "");
342         }
343         Q -= This*MsPer;
344     }
345 
346     fprintf(stdout, "%0.3f seconds", (double)Q / 1000.0);
347 }
348 
349 static void
PrintTimeStat(char * Name,int unsigned Milliseconds)350 PrintTimeStat(char *Name, int unsigned Milliseconds)
351 {
352     fprintf(stdout, "%s: ", Name);
353     PrintTime((double)Milliseconds);
354     fprintf(stdout, "\n");
355 }
356 
357 typedef struct stat_group
358 {
359     int unsigned Count;
360 
361     int unsigned SlowestMs;
362     int unsigned FastestMs;
363     double TotalMs;
364 
365 } stat_group;
366 
367 #define GRAPH_HEIGHT 10
368 #define GRAPH_WIDTH 30
369 typedef struct graph
370 {
371     stat_group Buckets[GRAPH_WIDTH];
372 } graph;
373 
374 static void
PrintStatGroup(char * Title,stat_group * Group)375 PrintStatGroup(char *Title, stat_group *Group)
376 {
377     int unsigned AverageMs = 0;
378     if(Group->Count >= 1)
379     {
380         AverageMs = (int unsigned)(Group->TotalMs / (double)Group->Count);
381     }
382 
383     if(Group->Count > 0)
384     {
385         fprintf(stdout, "%s (%d):\n", Title, Group->Count);
386         PrintTimeStat("  Slowest", Group->SlowestMs);
387         PrintTimeStat("  Fastest", Group->FastestMs);
388         PrintTimeStat("  Average", AverageMs);
389         PrintTimeStat("  Total", (int unsigned)Group->TotalMs);
390     }
391 }
392 
393 static void
UpdateStatGroup(stat_group * Group,timing_file_entry * Entry)394 UpdateStatGroup(stat_group *Group, timing_file_entry *Entry)
395 {
396     if(Group->SlowestMs < Entry->MillisecondsElapsed)
397     {
398         Group->SlowestMs = Entry->MillisecondsElapsed;
399     }
400 
401     if(Group->FastestMs > Entry->MillisecondsElapsed)
402     {
403         Group->FastestMs = Entry->MillisecondsElapsed;
404     }
405 
406     Group->TotalMs += (double)Entry->MillisecondsElapsed;
407 
408     ++Group->Count;
409 }
410 
411 static int
MapToDiscrete(double Value,double InMax,double OutMax)412 MapToDiscrete(double Value, double InMax, double OutMax)
413 {
414     int Result;
415 
416     if(InMax == 0)
417     {
418         InMax = 1;
419     }
420 
421     Result = (int)((Value / InMax) * OutMax);
422 
423     return(Result);
424 }
425 
426 static void
PrintGraph(char * Title,double DaySpan,graph * Graph)427 PrintGraph(char *Title, double DaySpan, graph *Graph)
428 {
429     int BucketIndex;
430     int LineIndex;
431     int unsigned MaxCountInBucket = 0;
432     int unsigned SlowestMs = 0;
433     double DPB = DaySpan / (double)GRAPH_WIDTH;
434 
435     for(BucketIndex = 0;
436         BucketIndex < GRAPH_WIDTH;
437         ++BucketIndex)
438     {
439         stat_group *Group = Graph->Buckets + BucketIndex;
440 
441         if(Group->Count)
442         {
443 //            double AverageMs = Group->TotalMs / (double)Group->Count;
444             if(MaxCountInBucket < Group->Count)
445             {
446                 MaxCountInBucket = Group->Count;
447             }
448 
449             if(SlowestMs < Group->SlowestMs)
450             {
451                 SlowestMs = Group->SlowestMs;
452             }
453         }
454     }
455 
456     fprintf(stdout, "\n%s (%f day%s/bucket):\n", Title, DPB, (DPB == 1) ? "" : "s");
457     for(LineIndex = GRAPH_HEIGHT - 1;
458         LineIndex >= 0;
459         --LineIndex)
460     {
461         fputc('|', stdout);
462         for(BucketIndex = 0;
463             BucketIndex < GRAPH_WIDTH;
464             ++BucketIndex)
465         {
466             stat_group *Group = Graph->Buckets + BucketIndex;
467             int This = -1;
468             if(Group->Count)
469             {
470 //                double AverageMs = Group->TotalMs / (double)Group->Count;
471                 This = MapToDiscrete(Group->SlowestMs, SlowestMs, GRAPH_HEIGHT - 1);
472             }
473             fputc((This >= LineIndex) ? '*' : ' ', stdout);
474         }
475         if(LineIndex == (GRAPH_HEIGHT - 1))
476         {
477             fputc(' ', stdout);
478             PrintTime(SlowestMs);
479         }
480         fputc('\n', stdout);
481     }
482     fputc('+', stdout);
483     for(BucketIndex = 0; BucketIndex < GRAPH_WIDTH; ++BucketIndex) {fputc('-', stdout);}
484     fputc(' ', stdout);
485     PrintTime(0);
486     fputc('\n', stdout);
487     fputc('\n', stdout);
488     for(LineIndex = GRAPH_HEIGHT - 1;
489         LineIndex >= 0;
490         --LineIndex)
491     {
492         fputc('|', stdout);
493         for(BucketIndex = 0;
494             BucketIndex < GRAPH_WIDTH;
495             ++BucketIndex)
496         {
497             stat_group *Group = Graph->Buckets + BucketIndex;
498             int This = -1;
499             if(Group->Count)
500             {
501                 This = MapToDiscrete(Group->Count, MaxCountInBucket, GRAPH_HEIGHT - 1);
502             }
503             fputc((This >= LineIndex) ? '*' : ' ', stdout);
504         }
505         if(LineIndex == (GRAPH_HEIGHT - 1))
506         {
507             fprintf(stdout, " %u", MaxCountInBucket);
508         }
509         fputc('\n', stdout);
510     }
511     fputc('+', stdout);
512     for(BucketIndex = 0; BucketIndex < GRAPH_WIDTH; ++BucketIndex) {fputc('-', stdout);}
513     fprintf(stdout, " 0\n");
514 }
515 
516 static void
Stats(timing_entry_array Array,char * TimingFileName)517 Stats(timing_entry_array Array, char *TimingFileName)
518 {
519     stat_group WithErrors = {0};
520     stat_group NoErrors = {0};
521     stat_group AllStats = {0};
522 
523     int unsigned IncompleteCount = 0;
524     int unsigned DaysWithTimingCount = 0;
525     int unsigned DaySpanCount = 0;
526 
527     int EntryIndex;
528 
529     timing_file_entry *Entry = Array.Entries;
530     int unsigned LastDayIndex = 0;
531 
532     double AllMs = 0;
533 
534     double FirstDayAt = 0;
535     double LastDayAt = 0;
536     double DaySpan = 0;
537 
538     graph TotalGraph = {0};
539     graph RecentGraph = {0};
540 
541     WithErrors.FastestMs = 0xFFFFFFFF;
542     NoErrors.FastestMs = 0xFFFFFFFF;
543 
544     if(Array.EntryCount >= 2)
545     {
546         long long unsigned SecondD = SecondDifference(Array.Entries[Array.EntryCount - 1].StartDate, Array.Entries[0].StartDate);
547         DaySpanCount = (int unsigned)(SecondD / (60 * 60 * 24));
548 
549         FirstDayAt = (double)DayIndex(Array.Entries[0].StartDate);
550         LastDayAt = (double)DayIndex(Array.Entries[Array.EntryCount - 1].StartDate);
551         DaySpan = (LastDayAt - FirstDayAt);
552     }
553     DaySpan += 1;
554 
555     for(EntryIndex = 0;
556         EntryIndex < Array.EntryCount;
557         ++EntryIndex, ++Entry)
558     {
559         if(Entry->Flags & TFEF_Complete)
560         {
561             stat_group *Group = (Entry->Flags & TFEF_NoErrors) ? &NoErrors : &WithErrors;
562 
563             int unsigned ThisDayIndex = DayIndex(Entry->StartDate);
564             if(LastDayIndex != ThisDayIndex)
565             {
566                 LastDayIndex = ThisDayIndex;
567                 ++DaysWithTimingCount;
568             }
569 
570             UpdateStatGroup(Group, Entry);
571             UpdateStatGroup(&AllStats, Entry);
572 
573             AllMs += (double)Entry->MillisecondsElapsed;
574 
575             {
576                 int GraphIndex = (int)(((double)(ThisDayIndex-FirstDayAt)/DaySpan)*(double)GRAPH_WIDTH);
577                 UpdateStatGroup(TotalGraph.Buckets + GraphIndex, Entry);
578             }
579 
580             {
581                 int GraphIndex = (int)(ThisDayIndex - (LastDayAt - GRAPH_WIDTH + 1));
582                 if(GraphIndex >= 0)
583                 {
584                     UpdateStatGroup(RecentGraph.Buckets + GraphIndex, Entry);
585                 }
586             }
587         }
588         else
589         {
590             ++IncompleteCount;
591         }
592     }
593 
594     fprintf(stdout, "\n%s Statistics\n\n", TimingFileName);
595     fprintf(stdout, "Total complete timings: %d\n", WithErrors.Count + NoErrors.Count);
596     fprintf(stdout, "Total incomplete timings: %d\n", IncompleteCount);
597     fprintf(stdout, "Days with timings: %d\n", DaysWithTimingCount);
598     fprintf(stdout, "Days between first and last timing: %d\n", DaySpanCount);
599     PrintStatGroup("Timings marked successful", &NoErrors);
600     PrintStatGroup("Timings marked failed", &WithErrors);
601 
602     PrintGraph("All", (LastDayAt - FirstDayAt), &TotalGraph);
603     PrintGraph("Recent", GRAPH_WIDTH, &RecentGraph);
604 
605     fprintf(stdout, "\nTotal time spent: ");
606     PrintTime(AllMs);
607     fprintf(stdout, "\n");
608 }
609 
610 int
main(int ArgCount,char ** Args)611 main(int ArgCount, char **Args)
612 {
613     // TODO(casey): It would be nice if this supported 64-bit file sizes, but I can't really
614     // tell right now if "ANSI C" supports this.  I feel like it should by now, but the
615     // MSVC docs seem to suggest you have to use __int64 to do 64-bit stuff with the CRT
616     // low-level IO routines, and I'm pretty sure that isn't a portable type :(
617 
618     // NOTE(casey): We snap the clock time right on entry, to minimize any overhead on
619     // "end" times that might occur from opening the file.
620     int unsigned EntryClock = GetClock();
621 
622     if((ArgCount == 3) || (ArgCount == 4))
623     {
624         char *Mode = Args[1];
625         int ModeIsBegin = (strcmp(Mode, "-begin") == 0);
626         char *TimingFileName = Args[2];
627         timing_file_header Header = {0};
628 
629         FILE* Handle = fopen(TimingFileName, "r+b");
630         if(Handle != NULL)
631         {
632             // NOTE(casey): The file exists - check the magic value
633             fread(&Header, sizeof(Header), 1, Handle);
634             if(Header.MagicValue == MAGIC_VALUE)
635             {
636                 // NOTE(casey): The file is at least nominally valid.
637             }
638             else
639             {
640                 fprintf(stderr, "ERROR: Unable to verify that \"%s\" is actually a ctime-compatible file.\n", TimingFileName);
641 
642                 fclose(Handle);
643                 Handle = NULL;
644             }
645         }
646         else if(ModeIsBegin)
647         {
648             // NOTE(casey): The file doesn't exist and we're starting a new timing, so create it.
649 
650             Handle = fopen(TimingFileName, "w+b");
651             if(Handle != NULL)
652             {
653                 Header.MagicValue = MAGIC_VALUE;
654                 if(fwrite(&Header, sizeof(Header), 1, Handle) == 1)
655                 {
656                     // NOTE(casey): File creation was (presumably) successful.
657                 }
658                 else
659                 {
660                     fprintf(stderr, "ERROR: Unable to write header to \"%s\".\n", TimingFileName);
661                 }
662             }
663             else
664             {
665                 fprintf(stderr, "ERROR: Unable to create timing file \"%s\".\n", TimingFileName);
666             }
667         }
668 
669         if(Handle != NULL)
670         {
671             if(ModeIsBegin)
672             {
673                 timing_file_entry NewEntry = {0};
674                 NewEntry.StartDate = GetDate();
675                 NewEntry.MillisecondsElapsed = GetClock();
676                 if((fseek(Handle, 0, SEEK_END) == 0) &&
677                    (fwrite(&NewEntry, sizeof(NewEntry), 1, Handle) == 1))
678                 {
679                     // NOTE(casey): Timer begin entry was written successfully.
680                 }
681                 else
682                 {
683                     fprintf(stderr, "ERROR: Unable to append new entry to file \"%s\".\n", TimingFileName);
684                 }
685             }
686             else if(strcmp(Mode, "-end") == 0)
687             {
688                 timing_file_entry LastEntry = {0};
689                 if((fseek(Handle, -(int)sizeof(timing_file_entry), SEEK_END) == 0) &&
690                    (fread(&LastEntry, sizeof(LastEntry), 1, Handle) == 1))
691                 {
692                     if(!(LastEntry.Flags & TFEF_Complete))
693                     {
694                         int unsigned StartClockD = LastEntry.MillisecondsElapsed;
695                         int unsigned EndClockD = EntryClock;
696                         LastEntry.Flags |= TFEF_Complete;
697                         LastEntry.MillisecondsElapsed = 0;
698                         if(StartClockD < EndClockD)
699                         {
700                             LastEntry.MillisecondsElapsed = (EndClockD - StartClockD);
701                         }
702 
703                         if((ArgCount == 3) ||
704                            ((ArgCount == 4) && (atoi(Args[3]) == 0)))
705                         {
706                             LastEntry.Flags |= TFEF_NoErrors;
707                         }
708 
709                         if((fseek(Handle, -(int)sizeof(timing_file_entry), SEEK_END) == 0) &&
710                            (fwrite(&LastEntry, sizeof(LastEntry), 1, Handle) == 1))
711                         {
712                             fprintf(stdout, "CTIME: ");
713                             PrintTime(LastEntry.MillisecondsElapsed);
714                             fprintf(stdout, " (%s)\n", TimingFileName);
715                         }
716                         else
717                         {
718                             fprintf(stderr, "ERROR: Unable to rewrite last entry to file \"%s\".\n", TimingFileName);
719                         }
720                     }
721                     else
722                     {
723                         fprintf(stderr, "ERROR: Last entry in file \"%s\" is already closed - unbalanced/overlapped calls?\n", TimingFileName);
724                     }
725                 }
726                 else
727                 {
728                     fprintf(stderr, "ERROR: Unable to read last entry from file \"%s\".\n", TimingFileName);
729                 }
730             }
731             else if(strcmp(Mode, "-stats") == 0)
732             {
733                 timing_entry_array Array = ReadAllEntries(Handle);
734                 Stats(Array, TimingFileName);
735                 FreeAllEntries(Array);
736             }
737             else if(strcmp(Mode, "-csv") == 0)
738             {
739                 timing_entry_array Array = ReadAllEntries(Handle);
740                 CSV(Array, TimingFileName);
741                 FreeAllEntries(Array);
742             }
743             else
744             {
745                 fprintf(stderr, "ERROR: Unrecognized command \"%s\".\n", Mode);
746             }
747 
748             fclose(Handle);
749             Handle = NULL;
750         }
751         else
752         {
753             fprintf(stderr, "ERROR: Cannnot open file \"%s\".\n", TimingFileName);
754         }
755     }
756     else
757     {
758         Usage();
759     }
760 }
761