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 /// CSV serialization.
8 module tharsis.prof.csv;
9 
10 
11 import std.algorithm;
12 import std.exception;
13 import std.format;
14 import std.range;
15 import std.typetuple;
16 
17 import tharsis.prof.event;
18 import tharsis.prof.profiler;
19 import tharsis.prof.ranges;
20 
21 
22 /** Write Events from a range to CSV.
23  *
24  * Params:
25  *
26  * events = An InputRange of Events to write to CSV.
27  * output = An OutputRange of characters to write to.
28  *          Example output ranges: Appender!string or std.stdio.File.lockingTextWriter.
29  *
30  * No heap memory will be allocated $(B if) output does not allocate.
31  *
32  * Throws:
33  *
34  * Whatever (if anything) output throws on failure to write more data to it.
35  */
36 void writeCSVTo(ERange, ORange)(ERange events, ORange output)
37     @trusted
38     if(isInputRange!ERange && is(ElementType!ERange == Event) && isCharOutput!ORange)
39 {
40     outer: foreach(event; events) with(event)
41     {
42         output.formattedWrite("%s,%s,", id, time).assumeWontThrow;
43 
44         final switch(id)
45         {
46             case EventID.Checkpoint, EventID.ZoneStart, EventID.ZoneEnd:
47                 output.formattedWrite("\"\"\n");
48                 break;
49             case EventID.Info:
50                 if(!info.empty && !info.canFind!(c => ",\"".canFind(c)).assumeWontThrow)
51                 {
52                     output.formattedWrite("%s\n", info).assumeWontThrow;
53                     continue outer;
54                 }
55 
56                 // info is at most ubyte.max long, will be quoted and in the worst case
57                 // will be doubled in size.
58                 ubyte[ubyte.max * 2 + 2] quotedBuf;
59                 quotedBuf[0] = '"';
60                 size_t quotedSize = 1;
61                 // Only '"' is 'special' here, and it's ASCII so we can use ubyte[]
62                 foreach(ubyte c; cast(ubyte[])info)
63                 {
64                     quotedBuf[quotedSize++] = c;
65                     // quotes must be doubled
66                     if(c == '"') { quotedBuf[quotedSize++] = '"'; }
67                 }
68                 quotedBuf[quotedSize++] = '"';
69                 output.formattedWrite("%s\n", cast(char[])quotedBuf[0 .. quotedSize])
70                       .assumeWontThrow;
71                 break;
72             case EventID.Variable:
73                 output.formattedWrite("%s:", var.type);
74                 final switch(var.type)
75                 {
76                     case VariableType.Int:   output.formattedWrite("%s\n", var.varInt);   break;
77                     case VariableType.Uint:  output.formattedWrite("%s\n", var.varUint);  break;
78                     case VariableType.Float: output.formattedWrite("%s\n", var.varFloat); break;
79                 }
80                 break;
81         }
82     }
83 }
84 
85 /// Get a CSVEventRange parsing character data from input.
86 auto csvEventRange(Range)(Range input) nothrow
87 {
88     return CSVEventRange!Range(input);
89 }
90 
91 /** A range that parses CSV data from a character range (string, file, etc.) and lazily
92  * generates Events.
93  *
94  * front() and popFront() may throw ConvException or CSVException.
95  */
96 struct CSVEventRange(Range)
97 {
98 private:
99     import std.traits;
100     import std.csv;
101 
102     // The CSV reader type to use.
103     alias CSV = ReturnType!(csvReader!(CSVEvent, Malformed.throwException, Range));
104 
105     // The CSV we're reading events from.
106     CSV csv_;
107 
108     // Representation of an Event in CSV.
109     struct CSVEvent
110     {
111         /// Event.id .
112         EventID id;
113         /// Event.time .
114         ulong time;
115         /// The union in Event.
116         string typeSpecific;
117     }
118 
119     // No default constructor (need to read from a char range).
120     @disable this();
121 
122     // Construct a CSVEventRange parsing from specified character range.
123     this(Range input) nothrow
124     {
125         csv_ = csvReader!CSVEvent(input).assumeWontThrow;
126     }
127 
128 public:
129     /** Get the current event.
130      *
131      * Throws:
132      *
133      * ConvException on a failure to parse a value stored in the CSV.
134      * CSVException on a CSV format error.
135      */
136     Event front() @trusted pure
137     {
138         assert(!empty, "Can't get front of an empty range");
139         CSVEvent csvEvent = csv_.front();
140         auto event = Event(csvEvent.id, csvEvent.time);
141         final switch(event.id) with(EventID)
142         {
143             case Checkpoint, ZoneStart, ZoneEnd: event.info_ = null;
144                 break;
145             case Info:
146                 event.info_ = csvEvent.typeSpecific;
147                 break;
148             case Variable:
149                 import std.conv: to;
150                 auto parts = csvEvent.typeSpecific.splitter(":");
151                 event.var_.type_ = to!VariableType(parts.front);
152                 parts.popFront();
153                 final switch(event.var_.type_) with(VariableType)
154                 {
155                     case Int:   event.var_.int_   = parts.front.to!int;   break;
156                     case Uint:  event.var_.uint_  = parts.front.to!uint;  break;
157                     case Float: event.var_.float_ = parts.front.to!float; break;
158                 }
159                 break;
160         }
161         return event;
162     }
163 
164     /** Move to the next event in the range.
165      *
166      * Throws:
167      *
168      * CSVException on a CSV format error.
169      */
170     void popFront() @trusted
171     {
172         assert(!empty, "Can't pop front of an empty range");
173         csv_.popFront();
174     }
175 
176     /// Is the range empty (no more events)?
177     bool empty() @safe pure nothrow @nogc { return csv_.empty; }
178 }
179 ///
180 unittest
181 {
182     import tharsis.prof;
183 
184     auto storage  = new ubyte[Profiler.maxEventBytes + 2048];
185     auto profiler = new Profiler(storage);
186 
187     // Simulate 2 'frames'
188     foreach(frame; 0 .. 2)
189     {
190         Zone topLevel = Zone(profiler, "frame");
191 
192         // Simulate frame overhead. Replace this with your frame code.
193         {
194             Zone nested1 = Zone(profiler, "with,comma");
195             foreach(i; 0 .. 1000) { continue; }
196         }
197         {
198             Zone nested2 = Zone(profiler, "with\"quotes\" and\nnewline");
199             nested2.variableEvent!"float 3.14"(3.14f);
200             nested2.variableEvent!"float 10.1"(10.1f);
201             nested2.variableEvent!"int without comma"(314);
202             foreach(i; 0 .. 10000) { continue; }
203         }
204     }
205 
206     import std.stdio;
207     writeln("Tharsis.prof CSV writing example");
208     // Create an EventRange from profile data with UFCS syntax.
209     auto events = profiler.profileData.eventRange;
210 
211     import std.array;
212     auto appender = appender!string();
213 
214     events.writeCSVTo(appender);
215 
216     writeln(appender.data);
217 
218     writeln("Tharsis.prof CSV parsing example");
219     import std.csv;
220     import std.range;
221 
222     // Parse the CSV back into events.
223     //
224     // Direct file input could work like this (it might be faster to load the entire file
225     // to a buffer, though):
226     //
227     // import std.algorithm
228     // foreach(event; csvEventRange(File("values.csv").byLine.joiner))
229     foreach(original, parsed; lockstep(events, csvEventRange(appender.data)))
230     {
231         import std.conv: to;
232         assert(original == parsed,
233                original.to!string ~ "\n does not match\n" ~ parsed.to!string);
234         writeln(parsed);
235     }
236 }
237 
238 /// Determines if R is an output range of characters (of any character type).
239 enum bool isCharOutput(R) = isOutputRange!(R, char) ||
240                             isOutputRange!(R, wchar) ||
241                             isOutputRange!(R, dchar);
242