|
| 1 | +import ldmud, sys, collections, dataclasses, time |
| 2 | + |
| 3 | +time_ns = getattr(time, 'time_ns', None) |
| 4 | +if not time_ns: |
| 5 | + def time_ns(): |
| 6 | + return int(time.time()*1000000) |
| 7 | + |
| 8 | +class profile_result: |
| 9 | + @dataclasses.dataclass |
| 10 | + class LineInfo: |
| 11 | + cost: int = 0 # Eval cost |
| 12 | + time: int = 0 # Elapsed time in nanoseconds |
| 13 | + indirect_cost: int = 0 # Eval cost of called functions |
| 14 | + indirect_time: int = 0 # Elapsed time in nanoseconds of called functions |
| 15 | + |
| 16 | + @dataclasses.dataclass |
| 17 | + class FileInfo: |
| 18 | + cost: int = 0 # Eval cost |
| 19 | + time: int = 0 # Elapsed time in nanoseconds |
| 20 | + lines: dict = dataclasses.field(default_factory=lambda: collections.defaultdict(profile_result.LineInfo)) |
| 21 | + |
| 22 | + def __init__(self): |
| 23 | + self.files = collections.defaultdict(profile_result.FileInfo) |
| 24 | + |
| 25 | + def add_line_info(self, fname, line, ticks, time): |
| 26 | + info = self.files[fname] |
| 27 | + info.cost += ticks |
| 28 | + info.time += time |
| 29 | + info.lines[line].cost += ticks |
| 30 | + info.lines[line].time += time |
| 31 | + |
| 32 | + def add_line_indirect_info(self, fname, line, ticks, time): |
| 33 | + info = self.files[fname] |
| 34 | + info.lines[line].indirect_cost += ticks |
| 35 | + info.lines[line].indirect_time += time |
| 36 | + |
| 37 | + def lpc_get_files(self): |
| 38 | + return ldmud.Array(sorted(self.files.keys())) |
| 39 | + |
| 40 | + def lpc_get_first_line(self, fname: str): |
| 41 | + return min(self.files[fname].lines.keys()) |
| 42 | + |
| 43 | + def lpc_get_last_line(self, fname: str): |
| 44 | + return max(self.files[fname].lines.keys()) |
| 45 | + |
| 46 | + def lpc_get_file_cost(self, fname: str): |
| 47 | + return self.files[fname].cost |
| 48 | + |
| 49 | + def lpc_get_file_time(self, fname: str): |
| 50 | + return self.files[fname].time |
| 51 | + |
| 52 | + def lpc_get_line_cost(self, fname: str, line: int): |
| 53 | + return self.files[fname].lines[line].cost |
| 54 | + |
| 55 | + def lpc_get_line_time(self, fname: str, line: int): |
| 56 | + return self.files[fname].lines[line].time |
| 57 | + |
| 58 | + def lpc_get_line_indirect_cost(self, fname: str, line: int): |
| 59 | + return self.files[fname].lines[line].indirect_cost |
| 60 | + |
| 61 | + def lpc_get_line_indirect_time(self, fname: str, line: int): |
| 62 | + return self.files[fname].lines[line].indirect_time |
| 63 | + |
| 64 | + def lpc_is_empty(self): |
| 65 | + return not self.files |
| 66 | + |
| 67 | + def __efun_call_strict__(self, fun: str, *args): |
| 68 | + return getattr(self, "lpc_" + fun)(*args) |
| 69 | + |
| 70 | +def efun_profile_call(result: ldmud.Lvalue, fun: ldmud.Closure, *args) -> profile_result: |
| 71 | + """ |
| 72 | + SYNOPSIS |
| 73 | + profile_result profile_call(mixed& result, closure fun, mixed arg, ...) |
| 74 | +
|
| 75 | + DESCRIPTION |
| 76 | + Calls <fun> with the given arguments and stores the result in |
| 77 | + <result>, which must be passed by reference. |
| 78 | +
|
| 79 | + Gathers profiling information that will be returned. |
| 80 | +
|
| 81 | + The resulting object provides the following functions which can be |
| 82 | + called with call_strict (dot operator): |
| 83 | +
|
| 84 | + string* get_files() |
| 85 | + Returns a sorted list of programs that were traced. |
| 86 | +
|
| 87 | + int get_first_line(string filename) |
| 88 | + Returns the minimum line number of the given file where |
| 89 | + trace information is available. |
| 90 | +
|
| 91 | + int get_last_line(string filename) |
| 92 | + Returns the maximum line number of the given file where |
| 93 | + trace information is available. |
| 94 | +
|
| 95 | + int get_line_cost(string filename, int linenumber) |
| 96 | + Returns the accumulated costs for that line. |
| 97 | +
|
| 98 | + int get_line_indirect_cost(string filename, int linenumber) |
| 99 | + Returns those costs for that line, that are incurred in |
| 100 | + calls done by this line. |
| 101 | +
|
| 102 | + int get_file_cost(string filename) |
| 103 | + Returns the accumulated costs for that file. |
| 104 | +
|
| 105 | + int get_line_time(string filename, int linenumber) |
| 106 | + Returns the accumulated durations in nanoseconds for that line. |
| 107 | +
|
| 108 | + int get_line_indirect_time(string filename, int linenumber) |
| 109 | + Returns the time for that line, that is used for processing |
| 110 | + calls done by this line. |
| 111 | +
|
| 112 | + int get_file_time(string filename) |
| 113 | + Returns the accumulated durations in nanoseconds for that file. |
| 114 | +
|
| 115 | + int is_empty() |
| 116 | + Returns a value != 0, if there was no information |
| 117 | + collected. |
| 118 | + """ |
| 119 | + |
| 120 | + @dataclasses.dataclass |
| 121 | + class PreviousLine: |
| 122 | + fname: str = None |
| 123 | + line_number: int = None |
| 124 | + ns: int = None |
| 125 | + eval_cost: int = None |
| 126 | + |
| 127 | + if not isinstance(result, ldmud.Lvalue): |
| 128 | + raise TypeError("Bad arg 1 to profile_call(): expected mixed &.") |
| 129 | + |
| 130 | + pr = profile_result() |
| 131 | + stack = [None] * (len(ldmud.call_stack) - 1) |
| 132 | + last = PreviousLine() |
| 133 | + last.eval_cost = ldmud.call_stack[-1].eval_cost |
| 134 | + last.ns = time_ns() |
| 135 | + |
| 136 | + def hook(ob, instr): |
| 137 | + nonlocal last, stack |
| 138 | + |
| 139 | + cur_frame = ldmud.call_stack[-1] |
| 140 | + cur_fname = cur_frame.file_name |
| 141 | + cur_ns = time_ns() |
| 142 | + cur_depth = len(ldmud.call_stack) - 1 # Don't use the current frame. |
| 143 | + |
| 144 | + if last.fname and last.line_number: |
| 145 | + pr.add_line_info(last.fname, last.line_number, max(1, cur_frame.eval_cost - last.eval_cost), cur_ns - last.ns) |
| 146 | + last.ns = cur_ns |
| 147 | + last.eval_cost = cur_frame.eval_cost |
| 148 | + |
| 149 | + if instr is None: # Don't do stack cleanup at the end, we will only get the profile_call() call. |
| 150 | + return |
| 151 | + |
| 152 | + if cur_fname: |
| 153 | + last.fname = cur_fname |
| 154 | + last.line_number = cur_frame.line_number |
| 155 | + |
| 156 | + while cur_depth > len(stack): |
| 157 | + new_frame = ldmud.call_stack[len(stack)] |
| 158 | + if new_frame.type == ldmud.CALL_FRAME_TYPE_LFUN: |
| 159 | + prev = PreviousLine() |
| 160 | + prev.fname = new_frame.file_name |
| 161 | + prev.line_number = new_frame.line_number |
| 162 | + prev.ns = cur_ns |
| 163 | + prev.eval_cost = new_frame.eval_cost |
| 164 | + stack.append(prev) |
| 165 | + else: |
| 166 | + stack.append(None) |
| 167 | + |
| 168 | + while cur_depth < len(stack): |
| 169 | + prev = stack.pop() |
| 170 | + if prev is not None and prev.fname and prev.line_number: |
| 171 | + pr.add_line_indirect_info(prev.fname, prev.line_number, max(1, cur_frame.eval_cost - prev.eval_cost), cur_ns - prev.ns) |
| 172 | + |
| 173 | + ldmud.register_hook(ldmud.BEFORE_INSTRUCTION, hook) |
| 174 | + try: |
| 175 | + result.value = ldmud.efuns.funcall(fun, *args) |
| 176 | + finally: |
| 177 | + ldmud.unregister_hook(ldmud.BEFORE_INSTRUCTION, hook) |
| 178 | + hook(None, None) # Process last instruction |
| 179 | + |
| 180 | + return pr |
| 181 | + |
| 182 | + |
| 183 | +def register(): |
| 184 | + """ |
| 185 | + Register efuns and types. |
| 186 | + """ |
| 187 | + ldmud.register_type("profile_result", profile_result) |
| 188 | + ldmud.register_efun("profile_call", efun_profile_call) |
0 commit comments