Skip to content

Commit fc81f08

Browse files
committed
Initial implementation of a profiling efun for LDMud
0 parents  commit fc81f08

File tree

7 files changed

+298
-0
lines changed

7 files changed

+298
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
build
2+
dist
3+
*.egg-info
4+
setup.cfg

CONTRIBUTORS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The following people contributed to the ldmud_tracing package:
2+
3+
- Alexander Motzkau <Gnomi@UNItopia.de>

LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright (c) 2024 by <see contributors list>
2+
3+
Permission to use, copy, modify, and/or distribute this software for any
4+
purpose with or without fee is hereby granted, provided that the above
5+
copyright notice and this permission notice appear in all copies.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIMS ALL WARRANTIES
8+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
10+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Python Tracing package for LDMud
2+
3+
Python package to provide tracing information to LPC.
4+
5+
This package provides the following types:
6+
* `profile_result`
7+
8+
This package contains the following efuns:
9+
* `profile_result profile_call(mixed& result, closure fun, mixed arg, ...)`
10+
11+
## Usage
12+
13+
### Install from the python package index
14+
15+
The efun package can be downloaded from the python package index:
16+
17+
```
18+
pip3 install --user ldmud-tracing
19+
```
20+
21+
### Build & install the package yourself
22+
23+
You can build the package yourself.
24+
25+
First clone the repository
26+
```
27+
git clone https://github.com/ldmud/python-tracing.git
28+
```
29+
30+
Install the package
31+
```
32+
cd python-tracing
33+
python3 setup.py install --user
34+
```
35+
36+
### Automatically load the modules at startup
37+
38+
Also install the [LDMud Python efuns](https://github.com/ldmud/python-efuns) and use its
39+
[startup.py](https://github.com/ldmud/python-efuns/blob/master/startup.py) as the Python startup script for LDMud.
40+
It will automatically detect the installed Python efuns and load them.
41+
42+
### Manually load the modules at startup
43+
44+
Add the following lines to your startup script:
45+
```
46+
import ldmud_tracing.profile
47+
48+
ldmud_tracing.profile.register()
49+
```
50+
51+
## Profiling
52+
53+
The `profile_call` efun evaluates the given closure, any extra arguments will
54+
be passed to the closure. The result will be assigned to the first argument
55+
which needs to be passed as a reference.
56+
57+
The efun will return a `profile_result` object. This object provides evaluation
58+
cost and elapsed time information for each executed LPC code line. A complete
59+
list of functions is available in the efun documentation.
60+
61+
Have fun!

ldmud_tracing/__init__.py

Whitespace-only changes.

ldmud_tracing/profile.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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)

setup.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="ldmud-tracing",
8+
version="0.0.1",
9+
author="LDMud Team",
10+
author_email="ldmud-dev@UNItopia.DE",
11+
description="Python tracing package for LDMud",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/ldmud/python-dbus",
15+
packages=setuptools.find_packages(),
16+
classifiers=[
17+
"Programming Language :: Python :: 3",
18+
"Operating System :: OS Independent",
19+
],
20+
entry_points={
21+
'ldmud_efun': [
22+
'profile_call = ldmud_tracing.profile:efun_profile_call',
23+
],
24+
'ldmud_type': [
25+
'profile_result = ldmud_tracing.profile:profile_result',
26+
]
27+
},
28+
zip_safe=False,
29+
)

0 commit comments

Comments
 (0)