Skip to content

Commit 88e8d4b

Browse files
committed
Add a timestamp for the samples and weight the report by the sample duration
1 parent bac223f commit 88e8d4b

12 files changed

Lines changed: 329 additions & 34 deletions

File tree

‎src/compat.c‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,48 @@ int vmp_write_time_now(int marker) {
9696
vmp_write_all(buffer, __SIZE);
9797
return 0;
9898
}
99+
100+
#if defined(VMPROF_APPLE) && !defined(CLOCK_MONOTONIC)
101+
/* SDK older than macOS 10.12: no clock_gettime. mach_absolute_time is a
102+
monotonic clock; there is no signal-safe process cpu clock, so both
103+
modes use it (it is what setitimer(ITIMER_REAL) measures anyway). */
104+
#include <mach/mach_time.h>
105+
int64_t vmp_sample_time_ns(int cpu_time)
106+
{
107+
mach_timebase_info_data_t tb;
108+
uint64_t t = mach_absolute_time();
109+
(void)cpu_time;
110+
if (mach_timebase_info(&tb) != KERN_SUCCESS || tb.denom == 0)
111+
return (int64_t)t;
112+
return (int64_t)(t / tb.denom * tb.numer + t % tb.denom * tb.numer / tb.denom);
113+
}
114+
#else
115+
int64_t vmp_sample_time_ns(int cpu_time)
116+
{
117+
struct timespec ts;
118+
clockid_t clk = cpu_time ? CLOCK_PROCESS_CPUTIME_ID : CLOCK_MONOTONIC;
119+
if (clock_gettime(clk, &ts) != 0)
120+
return 0;
121+
return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
122+
}
123+
#endif
99124
#endif
100125

101126
#ifdef VMPROF_WINDOWS
127+
int64_t vmp_sample_time_ns(int cpu_time)
128+
{
129+
/* the frequency is fixed at boot, so it is safe to cache it */
130+
static LARGE_INTEGER freq;
131+
LARGE_INTEGER now;
132+
(void)cpu_time;
133+
if (freq.QuadPart == 0 && !QueryPerformanceFrequency(&freq))
134+
return 0;
135+
if (!QueryPerformanceCounter(&now))
136+
return 0;
137+
return (int64_t)(now.QuadPart / freq.QuadPart * 1000000000LL +
138+
now.QuadPart % freq.QuadPart * 1000000000LL / freq.QuadPart);
139+
}
140+
102141
int vmp_write_time_now(int marker) {
103142
char buffer[__SIZE];
104143
struct timezone_buf buf;

‎src/compat.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,13 @@ int vmp_write_all(const char *buf, size_t bufsize);
2323
int vmp_write_time_now(int marker);
2424
int vmp_write_meta(const char * key, const char * value);
2525

26+
/* Nanosecond timestamp stored at the end of every stack sample. With
27+
'cpu_time' set it reads the process cpu-time clock, which is the clock
28+
that drives ITIMER_PROF; otherwise it reads a monotonic wall clock, which
29+
drives ITIMER_REAL and the Windows sampler thread. The reader weights
30+
each sample by the distance to the previous one on the same clock, so
31+
timer signals that were dropped still count. Async-signal-safe. */
32+
int64_t vmp_sample_time_ns(int cpu_time);
33+
2634
int vmp_profile_fileno(void);
2735
void vmp_set_profile_fileno(int fileno);

‎src/vmprof.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
#define VERSION_MODE_AWARE '\x04'
2828
#define VERSION_DURATION '\x05'
2929
#define VERSION_TIMESTAMP '\x06'
30+
/* every MARKER_STACKTRACE record ends with a 64-bit nanosecond timestamp,
31+
see vmp_sample_time_ns() */
32+
#define VERSION_SAMPLE_TIME '\x07'
3033

3134
#define PROFILE_MEMORY '\x01'
3235
#define PROFILE_LINES '\x02'

‎src/vmprof_common.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ int opened_profile(const char *interp_name, int memory, int proflines, int nativ
135135
}
136136
header.interp_name[0] = MARKER_HEADER;
137137
header.interp_name[1] = '\x00';
138-
header.interp_name[2] = VERSION_TIMESTAMP;
138+
header.interp_name[2] = VERSION_SAMPLE_TIME;
139139
header.interp_name[3] = memory*PROFILE_MEMORY + proflines*PROFILE_LINES + \
140140
native*PROFILE_NATIVE + real_time*PROFILE_REAL_TIME;
141141
#ifdef RPYTHON_VMPROF

‎src/vmprof_common.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ ssize_t remove_threads(void);
4141
#define MAX_STACK_DEPTH \
4242
((SINGLE_BUF_SIZE - sizeof(struct prof_stacktrace_s)) / sizeof(void *))
4343

44+
/* Slots at the end of a stack sample that are not stack entries: the thread
45+
id, the (optional) rss and the 64-bit sample timestamp. Walk at most
46+
MAX_STACK_DEPTH - STACK_TRAILER_SLOTS frames so that they always fit. */
47+
#define STACK_TRAILER_SLOTS \
48+
(2 + (sizeof(int64_t) + sizeof(void *) - 1) / sizeof(void *))
49+
4450
/*
4551
* NOTE SHOULD NOT BE DONE THIS WAY. Here is an example why:
4652
* assume the following struct content:
@@ -73,6 +79,9 @@ typedef struct prof_stacktrace_s {
7379
char marker;
7480
long count, depth;
7581
void *stack[];
82+
/* the file record continues after stack[depth] with the thread id,
83+
the rss if memory profiling is on, and an int64_t timestamp
84+
(VERSION_SAMPLE_TIME, see vmp_sample_time_ns) */
7685
} prof_stacktrace_s;
7786

7887
#define SIZEOF_PROF_STACKTRACE sizeof(long)+sizeof(long)+sizeof(char)

‎src/vmprof_unix.c‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,16 @@ void segfault_handler(int arg)
9595
int _vmprof_sample_stack(struct profbuf_s *p, PY_THREAD_STATE_T * tstate, ucontext_t * uc)
9696
{
9797
int depth;
98+
int64_t sample_time;
9899
struct prof_stacktrace_s *st = (struct prof_stacktrace_s *)p->data;
99100
st->marker = MARKER_STACKTRACE;
100101
st->count = 1;
101102
#ifdef RPYTHON_VMPROF
102-
depth = get_stack_trace(get_vmprof_stack(), st->stack, MAX_STACK_DEPTH-1, (intptr_t)GetPC(uc));
103+
depth = get_stack_trace(get_vmprof_stack(), st->stack,
104+
MAX_STACK_DEPTH - STACK_TRAILER_SLOTS, (intptr_t)GetPC(uc));
103105
#else
104-
depth = get_stack_trace(tstate, st->stack, MAX_STACK_DEPTH-1, (intptr_t)NULL);
106+
depth = get_stack_trace(tstate, st->stack,
107+
MAX_STACK_DEPTH - STACK_TRAILER_SLOTS, (intptr_t)NULL);
105108
#endif
106109
// useful for tests (see test_stop_sampling)
107110
#ifndef RPYTHON_LL2CTYPES
@@ -114,8 +117,13 @@ int _vmprof_sample_stack(struct profbuf_s *p, PY_THREAD_STATE_T * tstate, uconte
114117
long rss = get_current_proc_rss();
115118
if (rss >= 0)
116119
st->stack[depth++] = (void*)rss;
120+
/* ITIMER_PROF counts process cpu time, ITIMER_REAL wall time: stamp the
121+
sample with the clock that drives the timer. memcpy: the slot is only
122+
pointer-aligned, which is less than int64_t needs on 32 bit. */
123+
sample_time = vmp_sample_time_ns(vmprof_get_itimer_type() == ITIMER_PROF);
124+
memcpy(&st->stack[depth], &sample_time, sizeof(sample_time));
117125
p->data_offset = offsetof(struct prof_stacktrace_s, marker);
118-
p->data_size = (depth * sizeof(void *) +
126+
p->data_size = (depth * sizeof(void *) + sizeof(sample_time) +
119127
sizeof(struct prof_stacktrace_s) -
120128
offsetof(struct prof_stacktrace_s, marker));
121129
return 1;

‎src/vmprof_win.c‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,17 @@ HANDLE write_mutex;
9090

9191
#include "vmprof_common.h"
9292

93+
/* Fill 'stack' with a sample of the given thread. Returns the number of
94+
pointer-sized slots written after the header (frames plus the thread id),
95+
or <= 0 if nothing was sampled. The record is followed by the int64_t
96+
sample timestamp, so the caller must write
97+
SIZEOF_PROF_STACKTRACE + depth * sizeof(void*) + sizeof(int64_t) bytes. */
9398
int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_stacktrace_s *stack)
9499
{
95100
HRESULT result;
96101
HANDLE hThread;
97102
int depth;
103+
int64_t sample_time;
98104
CONTEXT ctx;
99105
#ifdef RPYTHON_LL2CTYPES
100106
return 0; // not much we can do
@@ -115,9 +121,11 @@ int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_st
115121
if (!GetThreadContext(hThread, &ctx))
116122
return -1;
117123
depth = get_stack_trace(tstate->vmprof_tl_stack,
118-
stack->stack, MAX_STACK_DEPTH-2, ctx.Eip);
124+
stack->stack, MAX_STACK_DEPTH - STACK_TRAILER_SLOTS, ctx.Eip);
119125
stack->depth = depth;
120126
stack->stack[depth++] = thread_id;
127+
sample_time = vmp_sample_time_ns(0);
128+
memcpy(&stack->stack[depth], &sample_time, sizeof(sample_time));
121129
stack->count = 1;
122130
stack->marker = MARKER_STACKTRACE;
123131
ResumeThread(hThread);
@@ -142,9 +150,9 @@ int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_st
142150
#else
143151
frame = PyThreadState_GetFrame(tstate);
144152
#endif
145-
/* leave room for the thread id appended below */
153+
/* leave room for the thread id and timestamp appended below */
146154
depth = vmp_walk_and_record_stack(frame, stack->stack,
147-
MAX_STACK_DEPTH - 1, 0, 0);
155+
MAX_STACK_DEPTH - STACK_TRAILER_SLOTS, 0, 0);
148156
#ifdef _MSC_VER
149157
} __except (EXCEPTION_EXECUTE_HANDLER) {
150158
depth = -1;
@@ -160,6 +168,9 @@ int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_st
160168
}
161169
stack->depth = depth;
162170
stack->stack[depth++] = (void*)((ULONG_PTR)thread_id);
171+
/* the sampler thread sleeps in wall time, so use the wall clock */
172+
sample_time = vmp_sample_time_ns(0);
173+
memcpy(&stack->stack[depth], &sample_time, sizeof(sample_time));
163174
stack->count = 1;
164175
stack->marker = MARKER_STACKTRACE;
165176
ResumeThread(hThread);
@@ -226,7 +237,8 @@ long __stdcall vmprof_mainloop(void *arg)
226237
depth = vmprof_snapshot_thread(tstate->thread_id, tstate, stack);
227238
if (depth > 0) {
228239
vmp_write_all((char*)stack + offsetof(prof_stacktrace_s, marker),
229-
SIZEOF_PROF_STACKTRACE + depth * sizeof(void*));
240+
SIZEOF_PROF_STACKTRACE + depth * sizeof(void*) +
241+
sizeof(int64_t));
230242
}
231243
sampler_busy = 0;
232244
}
@@ -246,7 +258,7 @@ long __stdcall vmprof_mainloop(void *arg)
246258
depth = vmprof_snapshot_thread(tstate->thread_ident, tstate, stack);
247259
if (depth > 0) {
248260
vmp_write_all((char*)stack + offsetof(prof_stacktrace_s, marker),
249-
depth * sizeof(void *) +
261+
depth * sizeof(void *) + sizeof(int64_t) +
250262
sizeof(struct prof_stacktrace_s) -
251263
offsetof(struct prof_stacktrace_s, marker));
252264
}

‎vmprof/profiler.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import tempfile
33

44
from vmprof.stats import Stats
5-
from vmprof.reader import _read_prof
5+
from vmprof.reader import _read_prof, DEFAULT_MAX_SAMPLE_GAP
66

77

88
class VMProfError(Exception):
@@ -32,12 +32,18 @@ def __exit__(self, type, value, traceback):
3232
self.done = True
3333

3434

35-
def read_profile(prof_file):
35+
def read_profile(prof_file, max_sample_gap=DEFAULT_MAX_SAMPLE_GAP):
36+
""" Read a profile file into a Stats object.
37+
38+
max_sample_gap caps, in seconds, how much time a single sample may
39+
stand for when the timestamps show that timer signals were lost;
40+
see vmprof.reader.LogReader.sample_weight.
41+
"""
3642
file_to_close = None
3743
if not hasattr(prof_file, 'read'):
3844
prof_file = file_to_close = open(str(prof_file), 'rb')
3945

40-
state = _read_prof(prof_file)
46+
state = _read_prof(prof_file, max_sample_gap=max_sample_gap)
4147

4248
if file_to_close:
4349
file_to_close.close()

‎vmprof/reader.py‎

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,21 @@
2626
VERSION_MODE_AWARE = 4
2727
VERSION_DURATION = 5
2828
VERSION_TIMESTAMP = 6
29+
# every stack sample ends with a 64-bit nanosecond timestamp
30+
VERSION_SAMPLE_TIME = 7
2931

3032
PROFILE_MEMORY = 1
3133
PROFILE_LINES = 2
3234
PROFILE_NATIVE = 4
3335
PROFILE_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

3545
VMPROF_CODE_TAG = 1
3646
VMPROF_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

358412
class 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

Comments
 (0)