Skip to content

Commit 16dde3d

Browse files
authored
AutoProfile integration (#273)
* AutoProfile integration * Fix unit test * Remove profile debug log
1 parent f16e559 commit 16dde3d

25 files changed

+1383
-1
lines changed

example/autoprofile/app.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
import threading
3+
import random
4+
import traceback
5+
import sys
6+
import os
7+
8+
sys.path.append('../..')
9+
os.environ['INSTANA_DEBUG'] = 'yes'
10+
os.environ['INSTANA_AUTOPROFILE'] = 'yes'
11+
import instana
12+
13+
try:
14+
# python 2
15+
from urllib2 import urlopen
16+
except ImportError:
17+
# python 3
18+
from urllib.request import urlopen
19+
20+
21+
# Simulate CPU intensive work
22+
def simulate_cpu():
23+
for i in range(5000000):
24+
text = "text1" + str(i)
25+
text = text + "text2"
26+
27+
28+
# Simulate memory leak
29+
def simulate_mem_leak():
30+
while True:
31+
mem1 = []
32+
33+
for j in range(0, 1800):
34+
mem2 = []
35+
for i in range(0, 1000):
36+
obj1 = {'v': random.randint(0, 1000000)}
37+
mem1.append(obj1)
38+
39+
obj2 = {'v': random.randint(0, 1000000)}
40+
mem2.append(obj2)
41+
42+
time.sleep(1)
43+
44+
threading.Thread(target=simulate_mem_leak).start()
45+
46+
47+
# Simulate lock
48+
def simulate_lock():
49+
lock = threading.Lock()
50+
51+
def lock_wait():
52+
lock.acquire()
53+
lock.release()
54+
55+
while True:
56+
lock.acquire()
57+
58+
threading.Thread(target=lock_wait).start()
59+
60+
time.sleep(1)
61+
lock.release()
62+
time.sleep(1)
63+
64+
threading.Thread(target=simulate_lock).start()
65+
66+
67+
while True:
68+
simulate_cpu()
69+
time.sleep(1)

instana/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ def boot_agent():
171171
print("Instana: No use in monitoring this process type (%s). "
172172
"Will go sit in a corner quietly." % os.path.basename(sys.argv[0]))
173173
else:
174+
# AutoProfile
175+
if "INSTANA_AUTOPROFILE" in os.environ:
176+
from .singletons import get_profiler
177+
profiler = get_profiler()
178+
if profiler:
179+
profiler.start()
180+
174181
if "INSTANA_MAGIC" in os.environ:
175182
pkg_resources.working_set.add_entry("/tmp/.instana/python")
176183
# The following path is deprecated: To be removed at a future date

instana/agent/host.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,18 @@ def report_data_payload(self, payload):
241241
if response is not None and 200 <= response.status_code <= 204:
242242
self.last_seen = datetime.now()
243243

244+
# Report profiles (if any)
245+
profile_count = len(payload['profiles'])
246+
if profile_count > 0:
247+
logger.debug("Reporting %d profiles", profile_count)
248+
response = self.client.post(self.__profiles_url(),
249+
data=to_json(payload['profiles']),
250+
headers={"Content-Type": "application/json"},
251+
timeout=0.8)
252+
253+
if response is not None and 200 <= response.status_code <= 204:
254+
self.last_seen = datetime.now()
255+
244256
# Report metrics
245257
metric_bundle = payload["metrics"]["plugins"][0]["data"]
246258
response = self.client.post(self.__data_url(),
@@ -365,6 +377,13 @@ def __traces_url(self):
365377
path = "com.instana.plugin.python/traces.%d" % self.announce_data.pid
366378
return "http://%s:%s/%s" % (self.options.agent_host, self.options.agent_port, path)
367379

380+
def __profiles_url(self):
381+
"""
382+
URL for posting profiles to the host agent. Only valid when announced.
383+
"""
384+
path = "com.instana.plugin.python/profiles.%d" % self.announce_data.pid
385+
return "http://%s:%s/%s" % (self.options.agent_host, self.options.agent_port, path)
386+
368387
def __response_url(self, message_id):
369388
"""
370389
URL for responding to agent requests.

instana/autoprofile/__init__.py

Whitespace-only changes.

instana/autoprofile/frame_cache.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
import threading
3+
import os
4+
import re
5+
import importlib
6+
7+
from .runtime import runtime_info
8+
9+
class FrameCache(object):
10+
MAX_CACHE_SIZE = 2500
11+
12+
def __init__(self, profiler):
13+
self.profiler = profiler
14+
self.profiler_frame_cache = None
15+
16+
self.include_profiler_frames = None
17+
18+
self.profiler_dir = os.path.dirname(os.path.realpath(__file__))
19+
20+
def start(self):
21+
self.profiler_frame_cache = dict()
22+
23+
self.include_profiler_frames = self.profiler.get_option('include_profiler_frames', False)
24+
25+
def stop(self):
26+
pass
27+
28+
def is_profiler_frame(self, filename):
29+
if filename in self.profiler_frame_cache:
30+
return self.profiler_frame_cache[filename]
31+
32+
profiler_frame = False
33+
34+
if not self.include_profiler_frames:
35+
if filename.startswith(self.profiler_dir):
36+
profiler_frame = True
37+
38+
if len(self.profiler_frame_cache) < self.MAX_CACHE_SIZE:
39+
self.profiler_frame_cache[filename] = profiler_frame
40+
41+
return profiler_frame

instana/autoprofile/profile.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import math
2+
import os
3+
import uuid
4+
import time
5+
6+
7+
class Profile(object):
8+
CATEGORY_CPU = 'cpu'
9+
CATEGORY_MEMORY = 'memory'
10+
CATEGORY_TIME = 'time'
11+
TYPE_CPU_USAGE = 'cpu-usage'
12+
TYPE_MEMORY_ALLOCATION_RATE = 'memory-allocation-rate'
13+
TYPE_BLOCKING_CALLS = 'blocking-calls'
14+
UNIT_NONE = ''
15+
UNIT_MILLISECOND = 'millisecond'
16+
UNIT_MICROSECOND = 'microsecond'
17+
UNIT_NANOSECOND = 'nanosecond'
18+
UNIT_BYTE = 'byte'
19+
UNIT_KILOBYTE = 'kilobyte'
20+
UNIT_PERCENT = 'percent'
21+
UNIT_SAMPLE = 'sample'
22+
RUNTIME_PYTHON = 'python'
23+
24+
def __init__(self, category, typ, unit, roots, duration, timespan):
25+
self.process_id = str(os.getpid())
26+
self.id = generate_uuid()
27+
self.runtime = Profile.RUNTIME_PYTHON
28+
self.category = category
29+
self.type = typ
30+
self.unit = unit
31+
self.roots = roots
32+
self.duration = duration
33+
self.timespan = timespan
34+
self.timestamp = millis()
35+
36+
def to_dict(self):
37+
profile_dict = {
38+
'pid': self.process_id,
39+
'id': self.id,
40+
'runtime': self.runtime,
41+
'category': self.category,
42+
'type': self.type,
43+
'unit': self.unit,
44+
'roots': [root.to_dict() for root in self.roots],
45+
'duration': self.duration,
46+
'timespan': self.timespan,
47+
'timestamp': self.timestamp
48+
}
49+
50+
return profile_dict
51+
52+
53+
class CallSite:
54+
__slots__ = [
55+
'method_name',
56+
'file_name',
57+
'file_line',
58+
'measurement',
59+
'num_samples',
60+
'children'
61+
]
62+
63+
def __init__(self, method_name, file_name, file_line):
64+
self.method_name = method_name
65+
self.file_name = file_name
66+
self.file_line = file_line
67+
self.measurement = 0
68+
self.num_samples = 0
69+
self.children = dict()
70+
71+
def create_key(self, method_name, file_name, file_line):
72+
return '{0} ({1}:{2})'.format(method_name, file_name, file_line)
73+
74+
def find_child(self, method_name, file_name, file_line):
75+
key = self.create_key(method_name, file_name, file_line)
76+
if key in self.children:
77+
return self.children[key]
78+
79+
return None
80+
81+
def add_child(self, child):
82+
self.children[self.create_key(child.method_name, child.file_name, child.file_line)] = child
83+
84+
def remove_child(self, child):
85+
del self.children[self.create_key(child.method_name, child.file_name, child.file_line)]
86+
87+
def find_or_add_child(self, method_name, file_name, file_line):
88+
child = self.find_child(method_name, file_name, file_line)
89+
if child == None:
90+
child = CallSite(method_name, file_name, file_line)
91+
self.add_child(child)
92+
93+
return child
94+
95+
def increment(self, value, count):
96+
self.measurement += value
97+
self.num_samples += count
98+
99+
def normalize(self, factor):
100+
self.measurement = self.measurement / factor
101+
self.num_samples = int(math.ceil(self.num_samples / factor))
102+
103+
for child in self.children.values():
104+
child.normalize(factor)
105+
106+
def floor(self):
107+
self.measurement = int(self.measurement)
108+
109+
for child in self.children.values():
110+
child.floor()
111+
112+
def to_dict(self):
113+
children_dicts = []
114+
for child in self.children.values():
115+
children_dicts.append(child.to_dict())
116+
117+
call_site_dict = {
118+
'method_name': self.method_name,
119+
'file_name': self.file_name,
120+
'file_line': self.file_line,
121+
'measurement': self.measurement,
122+
'num_samples': self.num_samples,
123+
'children': children_dicts
124+
}
125+
126+
return call_site_dict
127+
128+
129+
def millis():
130+
return int(round(time.time() * 1000))
131+
132+
133+
def generate_uuid():
134+
return str(uuid.uuid4())

0 commit comments

Comments
 (0)