1 /**
2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Utility for manually profiling of parts of a program.
7 */
8 module my.profile;
9 
10 import core.sync.mutex : Mutex;
11 import std.datetime : Duration;
12 
13 /// Execution profile result gathered from analysers.
14 private shared ProfileResults gProfile;
15 private shared Mutex gProfileMtx;
16 
17 /// Returns: the profiling results gathered for this module.
18 ProfileResults getProfileResult() @trusted {
19     gProfileMtx.lock_nothrow;
20     scope (exit)
21         gProfileMtx.unlock_nothrow();
22     auto g = cast() gProfile;
23     return new ProfileResults(g.results.dup);
24 }
25 
26 void putProfile(string name, Duration time) @trusted {
27     gProfileMtx.lock_nothrow;
28     scope (exit)
29         gProfileMtx.unlock_nothrow();
30     auto g = cast() gProfile;
31     g.add(name, time);
32 }
33 
34 shared static this() {
35     gProfileMtx = new shared Mutex();
36     gProfile = cast(shared) new ProfileResults;
37 }
38 
39 @safe:
40 
41 /** Wall time profile of a task.
42  *
43  * If no results collector is specified the result is stored in the global
44  * collector.
45  */
46 struct Profile {
47     import std.datetime.stopwatch : StopWatch;
48 
49     string name;
50     StopWatch sw;
51     ProfileResults saveTo;
52 
53     this(T)(T name, ProfileResults saveTo = null) @safe nothrow {
54         static if (is(T == string)) {
55             this.name = name;
56         } else {
57             try {
58                 this.name = name.to!string;
59             } catch (Exception e) {
60                 this.name = T.stringof;
61             }
62         }
63         this.saveTo = saveTo;
64         sw.start;
65     }
66 
67     ~this() @safe nothrow {
68         // uninitialized
69         if (name.length == 0)
70             return;
71 
72         try {
73             sw.stop;
74             if (saveTo is null) {
75                 putProfile(name, sw.peek);
76             } else {
77                 saveTo.add(name, sw.peek);
78             }
79         } catch (Exception e) {
80         }
81     }
82 }
83 
84 /** Collect profiling results.
85  */
86 class ProfileResults {
87     import std.algorithm : sum, map, sort;
88     import std.array : array, appender;
89 
90     struct Result {
91         string name;
92         Duration time;
93         double ratio;
94     }
95 
96     /// The profiling for the same name is accumulated.
97     Duration[string] results;
98 
99     this() {
100     }
101 
102     this(typeof(results) results) {
103         this.results = results;
104     }
105 
106     /// Add `time` to `name`.
107     void add(string name, Duration time) {
108         results.update(name, () => time, (ref Duration a) { a += time; });
109     }
110 
111     /// Set `name` to `time`.
112     void set(string name, Duration time) {
113         results.update(name, () => time, (ref Duration a) { a = time; });
114     }
115 
116     /// Returns: the total wall time.
117     Duration totalTime() const {
118         return results.byValue.sum(Duration.zero);
119     }
120 
121     /// Returns;
122     Result[] toRows() const {
123         auto app = appender!(Result[])();
124         const double total = totalTime.total!"nsecs";
125 
126         foreach (a; results.byKeyValue.array.sort!((a, b) => a.value < b.value)) {
127             Result row;
128             row.name = a.key;
129             row.time = a.value;
130             row.ratio = cast(double) a.value.total!"nsecs" / total;
131             app.put(row);
132         }
133 
134         return app.data;
135     }
136 
137     /**
138      *
139      * This is an example from clang-tidy for how it could be reported to the user.
140      * For now it is *just* reported as it is running.
141      *
142      * ===-------------------------------------------------------------------------===
143      *                           clang-tidy checks profiling
144      * ===-------------------------------------------------------------------------===
145      *   Total Execution Time: 0.0021 seconds (0.0021 wall clock)
146      *
147      *    ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
148      *    0.0000 (  0.1%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.1%)  readability-misplaced-array-index
149      *    0.0000 (  0.2%)   0.0000 (  0.0%)   0.0000 (  0.1%)   0.0000 (  0.1%)  abseil-duration-division
150      *    0.0012 (100.0%)   0.0009 (100.0%)   0.0021 (100.0%)   0.0021 (100.0%)  Total
151      */
152     override string toString() const {
153         import std.algorithm : maxElement;
154         import std.format : format, formattedWrite;
155         import std.math : log10;
156 
157         const sec = 1000000000.0;
158         // 16 is the number of letters after the dot in "0.0000 (100.1%)" + 1 empty whitespace.
159         const wallMaxLen = cast(int) results.byValue.map!(a => a.total!"seconds")
160             .maxElement(1).log10 + 15;
161 
162         auto app = appender!string;
163         formattedWrite(app,
164                 "===-------------------------------------------------------------------------===\n");
165         formattedWrite(app, "                         dextool profiling\n");
166         formattedWrite(app,
167                 "===-------------------------------------------------------------------------===\n");
168         formattedWrite(app, "Total execution time: %.4f seconds\n\n",
169                 cast(double) totalTime.total!"nsecs" / sec);
170         formattedWrite(app, "---Wall Time--- ---Name---\n");
171 
172         void print(string name, Duration time, double ratio) {
173             auto wt = format!"%.4f (%.1f%%)"(cast(double) time.total!"nsecs" / sec, ratio * 100.0);
174             formattedWrite(app, "%-*s %s\n", wallMaxLen, wt, name);
175         }
176 
177         foreach (r; toRows) {
178             print(r.name, r.time, r.ratio);
179         }
180 
181         return app.data;
182     }
183 }
184 
185 @("shall accumulate profiling results")
186 unittest {
187     import std.conv : to;
188     import std.stdio : writeln;
189 
190     auto pc = new ProfileResults;
191 
192     foreach (_; 0 .. 10)
193         auto p = Profile("a", pc);
194     assert(pc.toRows.length == 1);
195 
196     foreach (i; 0 .. 10)
197         auto p = Profile("a" ~ i.to!string, pc);
198     assert(pc.toRows.length == 11);
199 
200     writeln(pc);
201 }