1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool.plugin.mutate.backend.utility;
11 
12 import core.time : Duration;
13 import logger = std.experimental.logger;
14 import std.algorithm : filter, map, splitter, sum, sort;
15 import std.array : appender, array;
16 import std.conv : to;
17 import std.typecons : Flag, No, Tuple;
18 import core.sync.mutex : Mutex;
19 
20 import my.from_;
21 
22 import my.hash : BuildChecksum128, toChecksum128;
23 import dextool.type : Path, AbsolutePath;
24 
25 public import my.hash : toBytes;
26 
27 public import dextool.plugin.mutate.backend.type;
28 
29 /// Execution profile result gathered from analysers.
30 private shared ProfileResults gProfile;
31 private shared Mutex gProfileMtx;
32 
33 alias BuildChecksum = BuildChecksum128;
34 alias toChecksum = toChecksum128;
35 
36 /// Returns: the profiling results gathered for this module.
37 ProfileResults getProfileResult() @trusted {
38     gProfileMtx.lock_nothrow;
39     scope (exit)
40         gProfileMtx.unlock_nothrow();
41     auto g = cast() gProfile;
42     return new ProfileResults(g.results.dup);
43 }
44 
45 void putProfile(string name, Duration time) @trusted {
46     gProfileMtx.lock_nothrow;
47     scope (exit)
48         gProfileMtx.unlock_nothrow();
49     auto g = cast() gProfile;
50     g.put(name, time);
51 }
52 
53 shared static this() {
54     gProfileMtx = new shared Mutex();
55     gProfile = cast(shared) new ProfileResults;
56 }
57 
58 @safe:
59 
60 Checksum checksum(const(ubyte)[] a) {
61     import my.hash : makeMurmur3;
62 
63     return makeMurmur3(a);
64 }
65 
66 Checksum checksum(AbsolutePath p) @trusted {
67     import std.mmfile : MmFile;
68 
69     try {
70         scope content = new MmFile(p.toString);
71         return checksum(cast(const(ubyte)[]) content[]);
72     } catch (Exception e) {
73     }
74 
75     return Checksum.init;
76 }
77 
78 /// Package the values to a checksum.
79 Checksum checksum(T)(const(T[2]) a) if (T.sizeof == 8) {
80     return Checksum(cast(ulong) a[0], cast(ulong) a[1]);
81 }
82 
83 /// Package the values to a checksum.
84 Checksum checksum(T)(const T a, const T b) if (T.sizeof == 8) {
85     return Checksum(cast(ulong) a, cast(ulong) b);
86 }
87 
88 /// Sleep for a random time that is min_ + rnd(0, span msecs)
89 void rndSleep(Duration min_, int span) nothrow @trusted {
90     import core.thread : Thread;
91     import core.time : dur;
92     import std.random : uniform;
93 
94     auto t_span = () {
95         try {
96             return uniform(0, span).dur!"msecs";
97         } catch (Exception e) {
98         }
99         return span.dur!"msecs";
100     }();
101 
102     Thread.sleep(min_ + t_span);
103 }
104 
105 /** Returns: the file content as an array of tokens.
106  *
107  * This is a bit slow, I think. Optimize by reducing the created strings.
108  * trusted: none of the unsafe accessed data escape this function.
109  *
110  * Params:
111  *  splitMultiLineTokens = a token, mostly comment tokens, can be over multiple
112  *      lines. If true then this split it into multiple tokens where a token is at
113  *      most one per line.
114  */
115 auto tokenize(Flag!"splitMultiLineTokens" splitTokens = No.splitMultiLineTokens)(
116         ref from.libclang_ast.context.ClangContext ctx, Path file) @trusted {
117     import std.range : enumerate;
118 
119     auto tu = ctx.makeTranslationUnit(file);
120     auto toks = appender!(Token[])();
121     foreach (ref t; tu.cursor.tokens) {
122         const ext = t.extent;
123         const start = ext.start;
124         const end = ext.end;
125         const spell = t.spelling;
126 
127         static if (splitTokens) {
128             // TODO: this do not correctly count the utf-8 graphems but rather
129             // the code points because `.length` is used.
130 
131             auto offset = Offset(start.offset, start.offset);
132             auto startLoc = SourceLoc(start.line, start.column);
133             auto endLoc = startLoc;
134             foreach (ts; spell.splitter('\n').enumerate) {
135                 offset = Offset(offset.end, cast(uint)(offset.end + ts.length));
136 
137                 if (ts.index == 0) {
138                     endLoc = SourceLoc(start.line, cast(uint)(start.column + ts.value.length));
139                 } else {
140                     startLoc = SourceLoc(startLoc.line + 1, 1);
141                     endLoc = SourceLoc(startLoc.line, cast(uint) ts.value.length);
142                 }
143 
144                 toks.put(Token(t.kind, offset, startLoc, endLoc, ts.value));
145             }
146         } else {
147             auto offset = Offset(start.offset, end.offset);
148             auto startLoc = SourceLoc(start.line, start.column);
149             auto endLoc = SourceLoc(end.line, end.column);
150             toks.put(Token(t.kind, offset, startLoc, endLoc, spell));
151         }
152     }
153 
154     return toks.data;
155 }
156 
157 struct TokenRange {
158     private {
159         Token[] tokens;
160     }
161 
162     Token front() @safe pure nothrow {
163         assert(!empty, "Can't get front of an empty range");
164         return tokens[0];
165     }
166 
167     void popFront() @safe pure nothrow {
168         assert(!empty, "Can't pop front of an empty range");
169         tokens = tokens[1 .. $];
170     }
171 
172     bool empty() @safe pure nothrow const @nogc {
173         return tokens.length == 0;
174     }
175 }
176 
177 /** Collect profiling results
178  *
179  */
180 class ProfileResults {
181     alias Result = Tuple!(string, "name", Duration, "time", double, "ratio");
182     /// The profiling for the same name is accumulated.
183     Duration[string] results;
184 
185     this() {
186     }
187 
188     this(typeof(results) results) {
189         this.results = results;
190     }
191 
192     void put(string name, Duration time) {
193         if (auto v = name in results) {
194             (*v) += time;
195         } else {
196             results[name] = time;
197         }
198     }
199 
200     /// Returns: the total wall time.
201     Duration totalTime() const {
202         return results.byValue.sum(Duration.zero);
203     }
204 
205     /// Returns;
206     Result[] toRows() const {
207         auto app = appender!(Result[])();
208         const double total = totalTime.total!"nsecs";
209 
210         foreach (a; results.byKeyValue.array.sort!((a, b) => a.value < b.value)) {
211             Result row;
212             row.name = a.key;
213             row.time = a.value;
214             row.ratio = cast(double) a.value.total!"nsecs" / total;
215             app.put(row);
216         }
217 
218         return app.data;
219     }
220 
221     /**
222      *
223      * This is an example from clang-tidy for how it could be reported to the user.
224      * For now it is *just* reported as it is running.
225      *
226      * ===-------------------------------------------------------------------------===
227      *                           clang-tidy checks profiling
228      * ===-------------------------------------------------------------------------===
229      *   Total Execution Time: 0.0021 seconds (0.0021 wall clock)
230      *
231      *    ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
232      *    0.0000 (  0.1%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.1%)  readability-misplaced-array-index
233      *    0.0000 (  0.2%)   0.0000 (  0.0%)   0.0000 (  0.1%)   0.0000 (  0.1%)  abseil-duration-division
234      *    0.0012 (100.0%)   0.0009 (100.0%)   0.0021 (100.0%)   0.0021 (100.0%)  Total
235      */
236     override string toString() const {
237         import std.algorithm : maxElement;
238         import std.format : format, formattedWrite;
239         import std.math : log10;
240 
241         const sec = 1000000000.0;
242         // 16 is the number of letters after the dot in "0.0000 (100.1%)" + 1 empty whitespace.
243         const wallMaxLen = cast(int) results.byValue.map!(a => a.total!"seconds")
244             .maxElement(1).log10 + 15;
245 
246         auto app = appender!string;
247         formattedWrite(app,
248                 "===-------------------------------------------------------------------------===\n");
249         formattedWrite(app, "                         dextool profiling\n");
250         formattedWrite(app,
251                 "===-------------------------------------------------------------------------===\n");
252         formattedWrite(app, "Total execution time: %.4f seconds\n\n",
253                 cast(double) totalTime.total!"nsecs" / sec);
254         formattedWrite(app, "---Wall Time--- ---Name---\n");
255 
256         void print(string name, Duration time, double ratio) {
257             auto wt = format!"%.4f (%.1f%%)"(cast(double) time.total!"nsecs" / sec, ratio * 100.0);
258             formattedWrite(app, "%-*s %s\n", wallMaxLen, wt, name);
259         }
260 
261         foreach (r; toRows) {
262             print(r.name, r.time, r.ratio);
263         }
264 
265         return app.data;
266     }
267 }
268 
269 /** Wall time profile of a task.
270  *
271  * If no results collector is specified the result is stored in the global
272  * collector.
273  */
274 struct Profile {
275     import std.datetime.stopwatch : StopWatch;
276 
277     string name;
278     StopWatch sw;
279     ProfileResults saveTo;
280 
281     this(T)(T name, ProfileResults saveTo = null) @safe nothrow {
282         try {
283             this.name = name.to!string;
284         } catch (Exception e) {
285             this.name = T.stringof;
286         }
287         this.saveTo = saveTo;
288         sw.start;
289     }
290 
291     ~this() @safe nothrow {
292         // uninitialized
293         if (name.length == 0)
294             return;
295 
296         try {
297             sw.stop;
298             if (saveTo is null) {
299                 putProfile(name, sw.peek);
300             } else {
301                 saveTo.put(name, sw.peek);
302             }
303         } catch (Exception e) {
304         }
305     }
306 }