├── .gitignore ├── LICENSE_1_0.txt ├── README.md ├── dmdprof.py ├── linkify.d └── longest.d /.gitignore: -------------------------------------------------------------------------------- 1 | /linkify 2 | /profile.json 3 | /longest 4 | -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dmdprof 2 | ======= 3 | 4 | This is a DMD compilation time profiler. 5 | It allows profiling and visualizing which parts of the D code take up most of the time compiling. 6 | 7 | Example 8 | ======= 9 | 10 | ![](https://dump.thecybershadow.net/5d3aa8aecb791fb421d97de09685c7e4/profile.svg) 11 | 12 | Click image above for working tooltips and hyperlinks. 13 | 14 | Example taken [from here](https://github.com/dlang/phobos/pull/5916#issuecomment-362896993). 15 | 16 | Usage 17 | ===== 18 | 19 | 1. Build a debug DMD, e.g. using `make -f posix.mak BUILD=debug` in [dmd](https://github.com/dlang/dmd)/src. 20 | 21 | 2. Run the debug DMD you built under GDB, and pass the program to compile and profile: 22 | 23 | $ gdb --args .../dmd/generated/linux/debug/64/dmd -o- your_program.d 24 | 25 | If you use a build tool like rdmd or Dub, you need to first find the dmd invocation it uses, then use that. Try enabling verbose output. 26 | 27 | 3. Load the `dmdprof.py` GDB script from this repository: 28 | 29 | (gdb) source path/to/dmdprof.py 30 | 31 | 4. Run the profiler: 32 | 33 | (gdb) python DMDProfiler().profile() 34 | 35 | You can optionally specify a sampling interval: 36 | 37 | (gdb) python DMDProfiler(0.001).profile() 38 | 39 | The sampling interval specifies the duration (in seconds) between samples taken. The default is 0.01, and the lowest possible value is 0, meaning to take samples as quickly as possible. 40 | 41 | Note that currently GDB leaks about 15-30 MB of memory per sample (!), so watch your memory usage to avoid crashing your machine. 42 | 43 | The profiler will save the results to a `profile.json` file. 44 | 45 | 5. Use [gprof2dot](https://github.com/jrfonseca/gprof2dot/) to generate a GraphViz .dot file, pipe it through the `linkify` program in this repository, and pipe the result into `dot`: 46 | 47 | ./gprof2dot.py -f json path/to/profile.json --root=-2:-2 -n 2 -e 0 | rdmd path/to/linkify.d | dot -Tsvg > profile.svg 48 | 49 | Adjust gprof2dot `-n` parameter to taste. 50 | 51 | To generate correct, permanent links in the output file, you can specify Phobos and Druntime versions as Git commit SHA1s or tag names using the `--phobos` and `--druntime` arguments to `linkify`. 52 | -------------------------------------------------------------------------------- /dmdprof.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | from collections import defaultdict 3 | import time 4 | import os 5 | import os.path 6 | import signal 7 | from threading import Thread 8 | import json 9 | import re 10 | 11 | 12 | def dmdprof_get_loc(val): 13 | if (val.type.code == gdb.TYPE_CODE_PTR 14 | and val.type.target().name is not None 15 | and val.type.target().name != "void" 16 | ): 17 | return dmdprof_get_loc(val.referenced_value()) 18 | if val.type.name is None: 19 | return None 20 | 21 | # Modules 22 | try: 23 | fn = val["srcfile"]["name"]["str"].string("utf-8") 24 | return (fn, 0, 0) 25 | except: 26 | pass 27 | 28 | # Symbols, statements, declarations, and expressions 29 | try: 30 | loc = val["loc"] 31 | return (loc["filename"].string("utf-8"), int(loc["linnum"]), int(loc["charnum"])) 32 | except: 33 | pass 34 | 35 | # Backend elems 36 | try: 37 | srcpos = val["Esrcpos"] 38 | return (srcpos["Sfilename"].string("utf-8"), int(srcpos["Slinnum"]), int(srcpos["Scharnum"])) 39 | except: 40 | pass 41 | 42 | return None 43 | 44 | 45 | def dmdprof_get_stack(): 46 | oldloc = () 47 | stack = [] 48 | frame = gdb.newest_frame() 49 | last_frame = frame 50 | while frame: 51 | try: 52 | block = frame.block() 53 | except RuntimeError: 54 | block = None 55 | while block: 56 | if not block.is_global: 57 | for symbol in block: 58 | if symbol.is_argument: 59 | loc = dmdprof_get_loc(symbol.value(frame)) 60 | if loc is not None and loc != oldloc: 61 | stack.append(loc) 62 | oldloc = loc 63 | last_frame = frame 64 | break # Consider just the first argument with a Loc 65 | block = block.superblock 66 | frame = frame.older() 67 | 68 | frame = last_frame 69 | while frame: 70 | name = frame.name() 71 | name = re.sub(r"\(.*", "", name) 72 | if name == "Module::accept" or name == "dmd.mars.tryMain": 73 | pass 74 | elif name == "D main": 75 | stack.append((name, -2, -2)) 76 | else: 77 | stack.append((name, -1, -1)) 78 | frame = frame.older() 79 | 80 | return tuple(stack) 81 | 82 | 83 | def dmdprof_print_stack(): 84 | stack = dmdprof_get_stack() 85 | for loc in stack: 86 | (filename, line, char) = loc 87 | locstr = "{}({},{})".format(filename, line, char) 88 | if line > 0 and os.path.exists(filename): 89 | linestr = open(filename).readlines()[line-1] 90 | locstr += ": " + linestr.rstrip('\n') 91 | print(locstr) 92 | 93 | 94 | class Executor: 95 | def __init__(self, cmd): 96 | self.__cmd = cmd 97 | 98 | def __call__(self): 99 | gdb.execute(self.__cmd) 100 | 101 | 102 | class DMDProfiler: 103 | def __init__(self, 104 | period = 0.01, 105 | output_filename = "profile.json", 106 | quit_on_exit = False): 107 | self.period = period 108 | self.output_filename = output_filename 109 | self.quit_on_exit = quit_on_exit 110 | 111 | def stop_func(self): 112 | try: 113 | os.kill(self.pid, signal.SIGINT) 114 | except ProcessLookupError: 115 | pass 116 | 117 | def threaded_function(self): 118 | time.sleep(self.period) 119 | # gdb.post_event(Executor("interrupt")) 120 | gdb.post_event(self.stop_func) 121 | 122 | def stop_handler(self, event): 123 | try: 124 | self.callchains.append(dmdprof_get_stack()) 125 | except RuntimeError: 126 | pass 127 | 128 | gdb.post_event(Executor("continue")) 129 | 130 | def cont_handler(self, event): 131 | (Thread(target = self.threaded_function)).start() 132 | 133 | def exit_handler(self, event): 134 | gdb.events.cont.disconnect(self.cont_handler) 135 | gdb.events.stop.disconnect(self.stop_handler) 136 | gdb.events.exited.disconnect(self.exit_handler) 137 | 138 | print("\nProfiling complete with %d samples." % len(self.callchains)) 139 | self.save_results() 140 | if self.quit_on_exit: 141 | gdb.execute("quit") 142 | 143 | def save_results(self): 144 | """Save results as gprof2dot JSON format.""" 145 | 146 | res = {"version":0, "functions":[], "events":[]} 147 | functions = {} 148 | 149 | for callchain in self.callchains: 150 | if len(callchain) == 0: 151 | continue 152 | 153 | funcs = [] 154 | for line in callchain: 155 | if line in functions: 156 | fun_id = functions[line] 157 | else: 158 | fun_id = len(functions) 159 | functions[line] = fun_id 160 | res["functions"].append({ 161 | # We don't know the actual function name, so 162 | # abuse the module field for some hierarchy 163 | "module" : line[0], 164 | # "module" : self.abbreviate_file_name(line[0]), 165 | "name" : str(line[1]) + ":" + str(line[2]) 166 | }) 167 | funcs.append(fun_id) 168 | 169 | res["events"].append({ 170 | "callchain" : funcs, 171 | "cost" : [self.period] 172 | }) 173 | json.dump(res, open(self.output_filename, 'w')) 174 | print(self.output_filename + " written.") 175 | 176 | # def abbreviate_file_name(self, fn): 177 | # fn = re.sub(r".*/phobos/", "", fn) 178 | # fn = re.sub(r".*/druntime/import/", "", fn) 179 | # return fn 180 | 181 | def profile(self): 182 | gdb.execute("set pagination off") 183 | gdb.execute("start") 184 | 185 | self.pid = gdb.selected_inferior().pid 186 | self.callchains = [] 187 | 188 | gdb.events.cont.connect(self.cont_handler) 189 | gdb.events.stop.connect(self.stop_handler) 190 | gdb.events.exited.connect(self.exit_handler) 191 | gdb.post_event(Executor("continue")) 192 | -------------------------------------------------------------------------------- /linkify.d: -------------------------------------------------------------------------------- 1 | import std.algorithm.searching; 2 | import std.conv; 3 | import std.file; 4 | import std.getopt; 5 | import std.json; 6 | import std.regex; 7 | import std.stdio; 8 | import std.string; 9 | 10 | void main(string[] args) 11 | { 12 | string phobosVer = "master", druntimeVer = "master"; 13 | getopt(args, 14 | "phobos", "Phobos version in URLs", &phobosVer, 15 | "druntime", "Druntime version in URLs", &druntimeVer, 16 | ); 17 | 18 | foreach (line; stdin.byLine) 19 | { 20 | if (auto m = line.matchFirst(re!`, label=("[^"]*")];$`)) 21 | { 22 | auto label = m[1].parseDotStr.splitLines(); 23 | const(char)[] fn = label[0]; 24 | auto lineNumber = label[1].findSplit(":")[0].to!int; 25 | 26 | string tooltip = null; 27 | if (lineNumber > 0 && fn.exists) 28 | tooltip = fn.readText.splitLines[lineNumber - 1]; 29 | if (lineNumber == 0) 30 | label[1] = "(module)"; 31 | else 32 | if (lineNumber < 0) 33 | label = label[0] ~ label[2..$]; 34 | 35 | const(char)[] url = null; 36 | 37 | if (lineNumber == -2) 38 | fn = "DMD root"; 39 | else 40 | if (lineNumber == -1) 41 | fn = "DMD: " ~ fn; 42 | else 43 | if (auto p = fn.matchFirst(re!`/phobos/`)) 44 | { 45 | fn = p.post; 46 | url = "https://github.com/dlang/phobos/blob/" ~ phobosVer ~ "/" ~ fn; 47 | } 48 | else 49 | if (auto p = fn.matchFirst(re!`/druntime/(src|import)/`)) 50 | { 51 | fn = p.post; 52 | url = "https://github.com/dlang/druntime/blob/" ~ druntimeVer ~ "/src/" ~ fn; 53 | } 54 | 55 | if (url && lineNumber > 0) 56 | url ~= "#L" ~ lineNumber.text; 57 | 58 | line = m.pre; 59 | line ~= ", label=" ~ ([fn.idup] ~ label[1..$]).join("\n").toDotStr(); 60 | if (url) 61 | line ~= ", URL=" ~ url.toDotStr; 62 | if (tooltip) 63 | line ~= ", tooltip=" ~ tooltip.toDotStr; 64 | line ~= "];"; 65 | } 66 | writeln(line); 67 | } 68 | } 69 | 70 | string parseDotStr(in char[] s) { return s.parseJSON.str; } 71 | string toDotStr(in char[] s) { auto j = JSONValue(s); return toJSON(j, false, JSONOptions.doNotEscapeSlashes); } 72 | 73 | Regex!char re(string pattern, alias flags = [])() 74 | { 75 | static Regex!char r; 76 | if (r.empty) 77 | r = regex(pattern, flags); 78 | return r; 79 | } 80 | -------------------------------------------------------------------------------- /longest.d: -------------------------------------------------------------------------------- 1 | import std.algorithm.iteration; 2 | import std.algorithm.searching; 3 | import std.conv; 4 | import std.file : readText, exists; 5 | import std.range.primitives; 6 | import std.stdio; 7 | import std.string; 8 | 9 | /// Print longest chain in a profile.json 10 | 11 | import ae.utils.funopt; 12 | import ae.utils.json; 13 | import ae.utils.main; 14 | 15 | void longest(string profileJson, bool countModules, string including = null) 16 | { 17 | @JSONPartial 18 | struct Profile 19 | { 20 | struct Function 21 | { 22 | @JSONName("module") string fileName; 23 | @JSONName("name") string pos; 24 | } 25 | Function[] functions; 26 | 27 | struct Event 28 | { 29 | int[] callchain; 30 | float[] cost; 31 | } 32 | Event[] events; 33 | } 34 | 35 | Profile profile = profileJson.readText.jsonParse!Profile; 36 | int[] bestCallchain; size_t bestLength; 37 | 38 | foreach (event; profile.events) 39 | { 40 | bool ok; 41 | if (including) 42 | { 43 | foreach (f; event.callchain) 44 | if (profile.functions[f].fileName.canFind(including)) 45 | ok = true; 46 | } 47 | else 48 | ok = true; 49 | if (!ok) 50 | continue; 51 | 52 | size_t length; 53 | if (countModules) 54 | length = event.callchain.map!(f => profile.functions[f].fileName).uniq.walkLength; 55 | else 56 | length = event.callchain.length; 57 | 58 | if (bestLength < length) 59 | { 60 | bestCallchain = event.callchain; 61 | bestLength = length; 62 | } 63 | } 64 | 65 | foreach (f; bestCallchain) 66 | { 67 | auto fn = profile.functions[f].fileName; 68 | write(fn); 69 | auto line = profile.functions[f].pos.findSplit(":")[0].to!int; 70 | if (line) 71 | write("(", profile.functions[f].pos, ")"); 72 | if (line && fn.exists) 73 | write(":", fn.readText.splitLines[line-1]); 74 | writeln; 75 | } 76 | } 77 | 78 | mixin main!(funopt!longest); 79 | --------------------------------------------------------------------------------