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