2626VERSION_MODE_AWARE = 4
2727VERSION_DURATION = 5
2828VERSION_TIMESTAMP = 6
29+ # every stack sample ends with a 64-bit nanosecond timestamp
30+ VERSION_SAMPLE_TIME = 7
2931
3032PROFILE_MEMORY = 1
3133PROFILE_LINES = 2
3234PROFILE_NATIVE = 4
3335PROFILE_RPYTHON = 8
36+ PROFILE_REAL_TIME = 16
37+
38+ # A sample is weighted by the time elapsed since the previous one, so that
39+ # timer signals the process never received (it was off the cpu, or the
40+ # signal was still pending) are still accounted for. The gap is capped at
41+ # this many seconds: a process that was stopped in a debugger or suspended
42+ # with the laptop lid should not attribute minutes to a single frame.
43+ DEFAULT_MAX_SAMPLE_GAP = 1.0
3444
3545VMPROF_CODE_TAG = 1
3646VMPROF_BLACKHOLE_TAG = 2
@@ -96,6 +106,8 @@ def __init__(self, fileobj, state):
96106 self .state = state
97107 self .word_size = None
98108 self .addr_size = None
109+ # timestamp of the previous sample, see sample_weight()
110+ self .last_sample_time = {}
99111 self .setup ()
100112
101113 def setup (self ):
@@ -163,10 +175,12 @@ def read_header(self):
163175 s .profile_memory = (mode & PROFILE_MEMORY ) != 0
164176 s .profile_lines = (mode & PROFILE_LINES ) != 0
165177 s .profile_rpython = (mode & PROFILE_RPYTHON ) != 0
178+ s .profile_real_time = (mode & PROFILE_REAL_TIME ) != 0
166179 else :
167180 s .profile_memory = s .version == VERSION_MEMORY
168181 s .profile_lines = False
169182 s .profile_rpython = False
183+ s .profile_real_time = False
170184
171185 lgt = ord (fileobj .read (1 ))
172186 s .interp_name = fileobj .read (lgt )
@@ -230,7 +244,43 @@ def read_addresses(self, count):
230244 return addrs
231245
232246 def read_s64 (self ):
233- return struct .unpack ('q' , self .fileobj .read (8 ))[0 ]
247+ return struct .unpack ('<q' , self .fileobj .read (8 ))[0 ]
248+
249+ def sample_weight (self , thread_id , sample_time ):
250+ """ How many timer periods this sample stands for.
251+
252+ Files older than VERSION_SAMPLE_TIME carry no timestamps and every
253+ sample counts as one period. Otherwise the sample is worth the
254+ time since the previous sample divided by the period, at least 1,
255+ and the gap is capped at state.max_sample_gap seconds; time beyond
256+ the cap is summed up in state.lost_time.
257+
258+ In real time mode every registered thread receives its own signal
259+ per period, so the previous sample is tracked per thread. In cpu
260+ time mode there is one process wide timer whose signal goes to the
261+ running thread, and the timestamp is process cpu time, so the
262+ previous sample is tracked process wide.
263+ """
264+ s = self .state
265+ s .n_samples += 1
266+ if sample_time is None or s .period <= 0 :
267+ s .expected_samples += 1
268+ return 1
269+ key = thread_id if s .profile_real_time else None
270+ prev = self .last_sample_time .get (key )
271+ if prev is None or sample_time > prev :
272+ self .last_sample_time [key ] = sample_time
273+ if prev is None :
274+ weight = 1.0
275+ else :
276+ gap = sample_time - prev
277+ max_gap = s .max_sample_gap * 10 ** 9
278+ if gap > max_gap :
279+ s .lost_time += (gap - max_gap ) / 10.0 ** 9
280+ gap = max_gap
281+ weight = max (gap / (s .period * 1000.0 ), 1.0 )
282+ s .expected_samples += weight
283+ return weight
234284
235285 def read_time_and_zone (self ):
236286 return datetime .datetime .fromtimestamp (
@@ -274,12 +324,16 @@ def read_all(self):
274324 trace = self .read_trace (depth )
275325 thread_id = 0
276326 mem_in_kb = 0
327+ sample_time = None
277328 if s .version >= VERSION_THREAD_ID :
278329 thread_id = self .read_addr ()
279330 if s .profile_memory :
280331 mem_in_kb = self .read_addr ()
332+ if s .version >= VERSION_SAMPLE_TIME :
333+ sample_time = self .read_s64 ()
281334 trace .reverse ()
282- self .add_trace (trace , 1 , thread_id , mem_in_kb )
335+ weight = self .sample_weight (thread_id , sample_time )
336+ self .add_trace (trace , weight , thread_id , mem_in_kb )
283337 elif marker == MARKER_VIRTUAL_IP or marker == MARKER_NATIVE_SYMBOLS :
284338 unique_id = self .read_addr ()
285339 name = self .read_string ()
@@ -356,7 +410,7 @@ class ReaderState(object):
356410 pass
357411
358412class LogReaderState (ReaderState ):
359- def __init__ (self ):
413+ def __init__ (self , max_sample_gap = DEFAULT_MAX_SAMPLE_GAP ):
360414 self .virtual_ips = []
361415 self .profiles = []
362416 self .interp_name = None
@@ -365,14 +419,21 @@ def __init__(self):
365419 self .version = 0
366420 self .profile_memory = False
367421 self .profile_lines = False
422+ self .profile_real_time = False
368423 self .meta = {}
369424 self .little_endian = True
370- self .period = 0
371-
372- def _read_prof (fileobj , virtual_ips_only = False ):
425+ self .period = 0 # microseconds
426+ # see LogReader.sample_weight
427+ self .max_sample_gap = max_sample_gap
428+ self .n_samples = 0 # stack samples in the file
429+ self .expected_samples = 0 # timer periods those samples stand for
430+ self .lost_time = 0.0 # seconds cut off by max_sample_gap
431+
432+ def _read_prof (fileobj , virtual_ips_only = False ,
433+ max_sample_gap = DEFAULT_MAX_SAMPLE_GAP ):
373434 fileobj = gunzip (fileobj )
374435
375- state = LogReaderState ()
436+ state = LogReaderState (max_sample_gap )
376437 reader = LogReader (fileobj , state )
377438 reader .read_all ()
378439
0 commit comments