* [PATCH v4 1/9] libdtrace: Support multiple DTrace handles per process
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 2/9] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
For use cases like PCP we require multiple DTrace handles
in a process; to fix this ensure we tie global map creation
to the handle and also allow BEGIN and END probes to already
exist. Later work can capitalize on efficiences like sharing
kernel type info, but for now this is enough to make multiple
instances work.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_bpf.c | 6 ++----
libdtrace/dt_impl.h | 1 +
libdtrace/dt_prov_dtrace.c | 6 +++++-
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/libdtrace/dt_bpf.c b/libdtrace/dt_bpf.c
index 146a66e3..be8a4229 100644
--- a/libdtrace/dt_bpf.c
+++ b/libdtrace/dt_bpf.c
@@ -25,8 +25,6 @@
#include <dt_btf.h>
#include <port.h>
-static boolean_t dt_gmap_done = 0;
-
#define BPF_CG_LICENSE "GPL";
int
@@ -1076,11 +1074,11 @@ int
dt_bpf_gmap_create(dtrace_hdl_t *dtp)
{
/* If we already created the global maps, return success. */
- if (dt_gmap_done)
+ if (dtp->dt_gmap_done)
return 0;
/* Mark global maps creation as completed. */
- dt_gmap_done = 1;
+ dtp->dt_gmap_done = 1;
#define CREATE_MAP(name) \
if (gmap_create_##name(dtp) == -1) \
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 5282efbd..7db2ba93 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -433,6 +433,7 @@ struct dtrace_hdl {
dt_list_t dt_lib_dep_sorted; /* dependency sorted library list */
dt_global_pcap_t dt_pcap; /* global tshark/pcap state */
char *dt_freopen_filename; /* filename for freopen() action */
+ boolean_t dt_gmap_done; /* global map created */
};
/*
diff --git a/libdtrace/dt_prov_dtrace.c b/libdtrace/dt_prov_dtrace.c
index 4b788507..8118ab23 100644
--- a/libdtrace/dt_prov_dtrace.c
+++ b/libdtrace/dt_prov_dtrace.c
@@ -243,7 +243,11 @@ static int attach(dtrace_hdl_t *dtp, const dt_probe_t *prp, int bpf_fd)
close(fd);
}
free(spec);
- if (rc == -1)
+ /*
+ * Multiple handles in a process mean we may have BEGIN/END
+ * uprobe events already.
+ * */
+ if (rc == -1 && errno != EEXIST)
return -ENOENT;
/* open format file */
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 2/9] libdtrace: share vmlinux BTF/CTF globally to support faster startup
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
2026-08-12 13:22 ` [PATCH v4 1/9] libdtrace: Support multiple DTrace handles per process Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 3/9] libdtrace: Refactor math functions into dt_math.h Alan Maguire
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
In a multiple DTrace session per process environment, it makes sense
to avoid doing expensive operations like reading in vmlinux BTF and
converting it to CTF more than once. Shared (vmlinux) BTF and CTF
generated from it are shared and reference-counted globally here
to cut down on cost. With this change, we can launch ~1 DTrace
session per second.
Note that we need to handle case where -xbtfpath/DTRACE_OPT_BTFPATH
are used as non-default BTF paths; in such cases global shared
BTF is not used in the handle.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_btf.c | 79 +++++++++++++++++++++++++++++++++++++++++++--
libdtrace/dt_btf.h | 1 +
libdtrace/dt_impl.h | 4 +--
libdtrace/dt_open.c | 5 +--
4 files changed, 81 insertions(+), 8 deletions(-)
diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
index 1f793397..7ec824be 100644
--- a/libdtrace/dt_btf.c
+++ b/libdtrace/dt_btf.c
@@ -693,6 +693,9 @@ dt_btf_to_ctf(dtrace_hdl_t *dtp, dt_module_t *dmp, dt_btf_t *btf)
ctf_dict_t *ctf;
ctf_encoding_t enc = { CTF_INT_SIGNED, 0, 0 };
+ if (dmp == NULL && dtp->dt_shared_ctf)
+ return dtp->dt_shared_ctf;
+
ctf = ctf_create(&dtp->dt_ctferr);
if (ctf == NULL)
return NULL;
@@ -764,6 +767,68 @@ out:
}
#endif
+static dt_btf_t *dt_shared_btf = NULL;
+#ifdef HAVE_LIBCTF
+static ctf_dict_t *dt_shared_ctf = NULL;
+#endif
+static pthread_mutex_t dt_shared_mutex = PTHREAD_MUTEX_INITIALIZER;
+static int dt_shared_refcnt = 0;
+
+static int
+dt_shared_get(dtrace_hdl_t *dtp, dt_module_t *dmp)
+{
+ pthread_mutex_lock(&dt_shared_mutex);
+ if (!dt_shared_btf)
+ dt_shared_btf = dt_btf_load_file(dtp, "/sys/kernel/btf/vmlinux");
+ if (dt_shared_btf) {
+ dt_shared_refcnt++;
+ dtp->dt_shared_btf = dmp->dm_btf = dt_shared_btf;
+#ifdef HAVE_LIBCTF
+ if (!dt_shared_ctf) {
+ ctf_dict_t *ctf = dt_btf_to_ctf(dtp, NULL, dt_shared_btf);
+
+ if (ctf)
+ dtp->dt_shared_ctf = dt_shared_ctf = dmp->dm_ctfp = ctf;
+ } else {
+ dtp->dt_shared_ctf = dmp->dm_ctfp = dt_shared_ctf;
+ }
+#endif
+ }
+ pthread_mutex_unlock(&dt_shared_mutex);
+ if (!dt_shared_btf)
+ return -1;
+ return 0;
+}
+
+void
+dt_shared_put(dtrace_hdl_t *dtp)
+{
+ int shared;
+
+ pthread_mutex_lock(&dt_shared_mutex);
+ shared = dtp->dt_shared_btf && dtp->dt_shared_btf == dt_shared_btf;
+ if (shared && --dt_shared_refcnt != 0)
+ goto out;
+
+ dt_btf_destroy(dtp, dtp->dt_shared_btf);
+ dtp->dt_shared_btf = NULL;
+ if (shared)
+ dt_shared_btf = NULL;
+
+#ifdef HAVE_LIBCTF
+ if (shared) {
+ ctf_close(dt_shared_ctf);
+ dt_shared_ctf = NULL;
+ } else {
+ ctf_close(dtp->dt_shared_ctf);
+ }
+ dtp->dt_shared_ctf = NULL;
+#endif
+
+out:
+ pthread_mutex_unlock(&dt_shared_mutex);
+}
+
dt_btf_t *
dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
{
@@ -774,6 +839,13 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
if (dmp->dm_btf)
return dmp->dm_btf;
+ /* Share only the system-default vmlinux BTF. */
+ if (dtp->dt_btf_path == NULL &&
+ strcmp(dmp->dm_name, "vmlinux") == 0) {
+ if (dt_shared_get(dtp, dmp) == 0)
+ return dmp->dm_btf;
+ }
+
/*
* Default: /sys/kernel/btf/<module>
* If "none", disable BTF.
@@ -899,9 +971,12 @@ dt_btf_lookup_name_kind(dtrace_hdl_t *dtp, dt_module_t *dmp, const char *name,
if (strcmp(name, "void") == 0)
return 0;
- /* Ensure the shared BTF is loaded. */
- if (!dtp->dt_shared_btf)
+ /* Ensure the vmlinux BTF base is loaded before considering a module. */
+ if (!dtp->dt_shared_btf) {
dt_btf_load_module(dtp, dtp->dt_exec);
+ if (!dtp->dt_shared_btf)
+ return -ENOENT;
+ }
/* If the module does not have BTF data yet, try to load it. */
if (!btf) {
diff --git a/libdtrace/dt_btf.h b/libdtrace/dt_btf.h
index 2c921a03..b1ed2b65 100644
--- a/libdtrace/dt_btf.h
+++ b/libdtrace/dt_btf.h
@@ -18,6 +18,7 @@ typedef struct dt_btf dt_btf_t;
typedef struct bpf_btf_info btf_info_t;
extern void dt_btf_destroy(dtrace_hdl_t *, dt_btf_t *);
+extern void dt_shared_put(dtrace_hdl_t *);
extern dt_btf_t *dt_btf_load_module(dtrace_hdl_t *, dt_module_t *);
extern ctf_dict_t *dt_btf_module_ctf(dtrace_hdl_t *, dt_module_t *);
extern const char *dt_btf_get_string(dtrace_hdl_t *, const dt_btf_t *,
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 7db2ba93..7ccdc271 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -305,8 +305,8 @@ struct dtrace_hdl {
char *dt_ctfa_path; /* path to vmlinux.ctfa */
ctf_archive_t *dt_ctfa; /* ctf archive for the entire kernel tree */
char *dt_btf_path; /* path to vmlinux.btf */
- struct dt_btf *dt_shared_btf; /* BTF data for the kernel (shared) */
- ctf_file_t *dt_shared_ctf; /* Handle to the shared CTF */
+ struct dt_btf *dt_shared_btf; /* BTF data shared by kernel modules */
+ ctf_file_t *dt_shared_ctf; /* CTF shared by kernel modules */
dt_htab_t *dt_kernpaths; /* hash table of dt_kern_path_t's */
dt_module_t *dt_exec; /* pointer to executable module */
dt_module_t *dt_cdefs; /* pointer to C dynamic type module */
diff --git a/libdtrace/dt_open.c b/libdtrace/dt_open.c
index 30b8758d..352cec49 100644
--- a/libdtrace/dt_open.c
+++ b/libdtrace/dt_open.c
@@ -1299,10 +1299,7 @@ dtrace_close(dtrace_hdl_t *dtp)
dt_htab_destroy(dtp->dt_mods);
dt_htab_destroy(dtp->dt_kernpaths);
- if (dtp->dt_shared_btf != NULL)
- dt_btf_destroy(dtp, dtp->dt_shared_btf);
- if (dtp->dt_shared_ctf != NULL)
- ctf_close(dtp->dt_shared_ctf);
+ dt_shared_put(dtp);
if (dtp->dt_ctfa != NULL)
ctf_arc_close(dtp->dt_ctfa);
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 3/9] libdtrace: Refactor math functions into dt_math.h
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
2026-08-12 13:22 ` [PATCH v4 1/9] libdtrace: Support multiple DTrace handles per process Alan Maguire
2026-08-12 13:22 ` [PATCH v4 2/9] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 4/9] python: Add cpython bindings for libdtrace Alan Maguire
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
No functional change.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_aggregate.c | 1 +
libdtrace/dt_consume.c | 340 +------------------------------------
libdtrace/dt_impl.h | 2 -
libdtrace/dt_math.h | 358 +++++++++++++++++++++++++++++++++++++++
libdtrace/dt_printf.c | 1 +
5 files changed, 361 insertions(+), 341 deletions(-)
create mode 100644 libdtrace/dt_math.h
diff --git a/libdtrace/dt_aggregate.c b/libdtrace/dt_aggregate.c
index 1fc8294d..00de1e0d 100644
--- a/libdtrace/dt_aggregate.c
+++ b/libdtrace/dt_aggregate.c
@@ -18,6 +18,7 @@
#include <port.h>
#include <dt_aggregate.h>
#include <dt_bpf.h>
+#include <dt_math.h>
typedef struct dt_ahashent {
struct dt_ahashent *dtahe_prev; /* prev on hash chain */
diff --git a/libdtrace/dt_consume.c b/libdtrace/dt_consume.c
index eca2139f..a66b80e2 100644
--- a/libdtrace/dt_consume.c
+++ b/libdtrace/dt_consume.c
@@ -15,6 +15,7 @@
#include <alloca.h>
#include <dt_impl.h>
#include <dt_aggregate.h>
+#include <dt_math.h>
#include <dt_dctx.h>
#include <dt_module.h>
#include <dt_pcap.h>
@@ -28,8 +29,6 @@
#include <sys/eventfd.h>
#include <linux/perf_event.h>
-#define DT_MASK_LO 0x00000000FFFFFFFFULL
-
typedef struct dt_spec_buf_data {
dt_list_t dsbd_list; /* linked-list forward/back pointers */
unsigned int dsbd_cpu; /* cpu for data */
@@ -48,343 +47,6 @@ typedef struct dt_spec_buf {
struct dt_hentry dtsb_he; /* htab links */
} dt_spec_buf_t;
-/*
- * We declare this here because (1) we need it and (2) we want to avoid a
- * dependency on libm in libdtrace.
- */
-static long double
-dt_fabsl(long double x)
-{
- if (x < 0)
- return -x;
-
- return x;
-}
-
-/*
- * 128-bit arithmetic functions needed to support the stddev() aggregating
- * action.
- */
-static int
-dt_gt_128(uint64_t *a, uint64_t *b)
-{
- return a[1] > b[1] || (a[1] == b[1] && a[0] > b[0]);
-}
-
-static int
-dt_ge_128(uint64_t *a, uint64_t *b)
-{
- return a[1] > b[1] || (a[1] == b[1] && a[0] >= b[0]);
-}
-
-static int
-dt_le_128(uint64_t *a, uint64_t *b)
-{
- return a[1] < b[1] || (a[1] == b[1] && a[0] <= b[0]);
-}
-
-/*
- * Shift the 128-bit value in a by b. If b is positive, shift left.
- * If b is negative, shift right.
- */
-static void
-dt_shift_128(uint64_t *a, int b)
-{
- uint64_t mask;
-
- if (b == 0)
- return;
-
- if (b < 0) {
- b = -b;
- if (b >= 64) {
- a[0] = a[1] >> (b - 64);
- a[1] = 0;
- } else {
- a[0] >>= b;
- mask = 1LL << (64 - b);
- mask -= 1;
- a[0] |= ((a[1] & mask) << (64 - b));
- a[1] >>= b;
- }
- } else {
- if (b >= 64) {
- a[1] = a[0] << (b - 64);
- a[0] = 0;
- } else {
- a[1] <<= b;
- mask = a[0] >> (64 - b);
- a[1] |= mask;
- a[0] <<= b;
- }
- }
-}
-
-static int
-dt_nbits_128(uint64_t *a)
-{
- int nbits = 0;
- uint64_t tmp[2];
- uint64_t zero[2] = { 0, 0 };
-
- tmp[0] = a[0];
- tmp[1] = a[1];
-
- dt_shift_128(tmp, -1);
- while (dt_gt_128(tmp, zero)) {
- dt_shift_128(tmp, -1);
- nbits++;
- }
-
- return nbits;
-}
-
-static void
-dt_subtract_128(uint64_t *minuend, uint64_t *subtrahend, uint64_t *difference)
-{
- uint64_t result[2];
-
- result[0] = minuend[0] - subtrahend[0];
- result[1] = minuend[1] - subtrahend[1] -
- (minuend[0] < subtrahend[0] ? 1 : 0);
-
- difference[0] = result[0];
- difference[1] = result[1];
-}
-
-static void
-dt_add_128(uint64_t *addend1, uint64_t *addend2, uint64_t *sum)
-{
- uint64_t result[2];
-
- result[0] = addend1[0] + addend2[0];
- result[1] = addend1[1] + addend2[1] +
- (result[0] < addend1[0] || result[0] < addend2[0] ? 1 : 0);
-
- sum[0] = result[0];
- sum[1] = result[1];
-}
-
-/*
- * The basic idea is to break the 2 64-bit values into 4 32-bit values,
- * use native multiplication on those, and then re-combine into the
- * resulting 128-bit value.
- *
- * (hi1 << 32 + lo1) * (hi2 << 32 + lo2) =
- * hi1 * hi2 << 64 +
- * hi1 * lo2 << 32 +
- * hi2 * lo1 << 32 +
- * lo1 * lo2
- */
-static void
-dt_multiply_128(uint64_t factor1, uint64_t factor2, uint64_t *product)
-{
- uint64_t hi1, hi2, lo1, lo2;
- uint64_t tmp[2];
-
- hi1 = factor1 >> 32;
- hi2 = factor2 >> 32;
-
- lo1 = factor1 & DT_MASK_LO;
- lo2 = factor2 & DT_MASK_LO;
-
- product[0] = lo1 * lo2;
- product[1] = hi1 * hi2;
-
- tmp[0] = hi1 * lo2;
- tmp[1] = 0;
- dt_shift_128(tmp, 32);
- dt_add_128(product, tmp, product);
-
- tmp[0] = hi2 * lo1;
- tmp[1] = 0;
- dt_shift_128(tmp, 32);
- dt_add_128(product, tmp, product);
-}
-
-/*
- * This is long-hand division.
- *
- * We initialize subtrahend by shifting divisor left as far as possible. We
- * loop, comparing subtrahend to dividend: if subtrahend is smaller, we
- * subtract and set the appropriate bit in the result. We then shift
- * subtrahend right by one bit for the next comparison.
- */
-static void
-dt_divide_128(uint64_t *dividend, uint64_t divisor, uint64_t *quotient)
-{
- uint64_t result[2] = { 0, 0 };
- uint64_t remainder[2];
- uint64_t subtrahend[2];
- uint64_t divisor_128[2];
- uint64_t mask[2] = { 1, 0 };
- int log = 0;
-
- assert(divisor != 0);
-
- divisor_128[0] = divisor;
- divisor_128[1] = 0;
-
- remainder[0] = dividend[0];
- remainder[1] = dividend[1];
-
- subtrahend[0] = divisor;
- subtrahend[1] = 0;
-
- while (divisor > 0) {
- log++;
- divisor >>= 1;
- }
-
- dt_shift_128(subtrahend, 128 - log);
- dt_shift_128(mask, 128 - log);
-
- while (dt_ge_128(remainder, divisor_128)) {
- if (dt_ge_128(remainder, subtrahend)) {
- dt_subtract_128(remainder, subtrahend, remainder);
- result[0] |= mask[0];
- result[1] |= mask[1];
- }
-
- dt_shift_128(subtrahend, -1);
- dt_shift_128(mask, -1);
- }
-
- quotient[0] = result[0];
- quotient[1] = result[1];
-}
-
-/*
- * This is the long-hand method of calculating a square root.
- * The algorithm is as follows:
- *
- * 1. Group the digits by 2 from the right.
- * 2. Over the leftmost group, find the largest single-digit number
- * whose square is less than that group.
- * 3. Subtract the result of the previous step (2 or 4, depending) and
- * bring down the next two-digit group.
- * 4. For the result R we have so far, find the largest single-digit number
- * x such that 2 * R * 10 * x + x^2 is less than the result from step 3.
- * (Note that this is doubling R and performing a decimal left-shift by 1
- * and searching for the appropriate decimal to fill the one's place.)
- * The value x is the next digit in the square root.
- * Repeat steps 3 and 4 until the desired precision is reached. (We're
- * dealing with integers, so the above is sufficient.)
- *
- * In decimal, the square root of 582,734 would be calculated as so:
- *
- * __7__6__3
- * | 58 27 34
- * -49 (7^2 == 49 => 7 is the first digit in the square root)
- * --
- * 9 27 (Subtract and bring down the next group.)
- * 146 8 76 (2 * 7 * 10 * 6 + 6^2 == 876 => 6 is the next digit in
- * ----- the square root)
- * 51 34 (Subtract and bring down the next group.)
- * 1523 45 69 (2 * 76 * 10 * 3 + 3^2 == 4569 => 3 is the next digit in
- * ----- the square root)
- * 5 65 (remainder)
- *
- * The above algorithm applies similarly in binary, but note that the
- * only possible non-zero value for x in step 4 is 1, so step 4 becomes a
- * simple decision: is 2 * R * 2 * 1 + 1^2 (aka R << 2 + 1) less than the
- * preceding difference?
- *
- * In binary, the square root of 11011011 would be calculated as so:
- *
- * __1__1__1__0
- * | 11 01 10 11
- * 01 (0 << 2 + 1 == 1 < 11 => this bit is 1)
- * --
- * 10 01 10 11
- * 101 1 01 (1 << 2 + 1 == 101 < 1001 => next bit is 1)
- * -----
- * 1 00 10 11
- * 1101 11 01 (11 << 2 + 1 == 1101 < 10010 => next bit is 1)
- * -------
- * 1 01 11
- * 11101 1 11 01 (111 << 2 + 1 == 11101 > 10111 => last bit is 0)
- *
- */
-static uint64_t
-dt_sqrt_128(uint64_t *square)
-{
- uint64_t result[2] = { 0, 0 };
- uint64_t diff[2] = { 0, 0 };
- uint64_t one[2] = { 1, 0 };
- uint64_t next_pair[2];
- uint64_t next_try[2];
- uint64_t bit_pairs, pair_shift;
- int i;
-
- bit_pairs = dt_nbits_128(square) / 2;
- pair_shift = bit_pairs * 2;
-
- for (i = 0; i <= bit_pairs; i++) {
- /*
- * Bring down the next pair of bits.
- */
- next_pair[0] = square[0];
- next_pair[1] = square[1];
- dt_shift_128(next_pair, -pair_shift);
- next_pair[0] &= 0x3;
- next_pair[1] = 0;
-
- dt_shift_128(diff, 2);
- dt_add_128(diff, next_pair, diff);
-
- /*
- * next_try = R << 2 + 1
- */
- next_try[0] = result[0];
- next_try[1] = result[1];
- dt_shift_128(next_try, 2);
- dt_add_128(next_try, one, next_try);
-
- if (dt_le_128(next_try, diff)) {
- dt_subtract_128(diff, next_try, diff);
- dt_shift_128(result, 1);
- dt_add_128(result, one, result);
- } else {
- dt_shift_128(result, 1);
- }
-
- pair_shift -= 2;
- }
-
- assert(result[1] == 0);
-
- return result[0];
-}
-
-uint64_t
-dt_stddev(uint64_t *data, uint64_t normal)
-{
- uint64_t avg_of_squares[2];
- uint64_t square_of_avg[2];
- int64_t norm_avg;
- uint64_t diff[2];
-
- /*
- * The standard approximation for standard deviation is
- * sqrt(average(x**2) - average(x)**2), i.e. the square root
- * of the average of the squares minus the square of the average.
- */
- dt_divide_128(data + 2, normal, avg_of_squares);
- dt_divide_128(avg_of_squares, data[0], avg_of_squares);
-
- norm_avg = (int64_t)data[1] / (int64_t)normal / (int64_t)data[0];
-
- if (norm_avg < 0)
- norm_avg = -norm_avg;
-
- dt_multiply_128((uint64_t)norm_avg, (uint64_t)norm_avg, square_of_avg);
-
- dt_subtract_128(avg_of_squares, square_of_avg, diff);
-
- return dt_sqrt_128(diff);
-}
-
static uint32_t
dt_spec_buf_hval(const dt_spec_buf_t *head)
{
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 7ccdc271..b83ff95a 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -727,8 +727,6 @@ extern void dt_buffered_disable(dtrace_hdl_t *);
extern void dt_buffered_destroy(dtrace_hdl_t *);
extern int dt_read_scalar(caddr_t, const dtrace_recdesc_t *, uint64_t *);
-extern uint64_t dt_stddev(uint64_t *, uint64_t);
-
extern void dt_setcontext(dtrace_hdl_t *, const dtrace_probedesc_t *);
extern void dt_endcontext(dtrace_hdl_t *);
diff --git a/libdtrace/dt_math.h b/libdtrace/dt_math.h
new file mode 100644
index 00000000..8ff5c64c
--- /dev/null
+++ b/libdtrace/dt_math.h
@@ -0,0 +1,358 @@
+/*
+ * Oracle Linux DTrace.
+ * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved.
+ * Licensed under the Universal Permissive License v 1.0 as shown at
+ * http://oss.oracle.com/licenses/upl.
+ */
+
+#ifndef _DT_MATH_H
+#define _DT_MATH_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define DT_MASK_LO 0x00000000FFFFFFFFULL
+
+/*
+ * We declare this here because (1) we need it and (2) we want to avoid a
+ * dependency on libm in libdtrace.
+ */
+static inline long double
+dt_fabsl(long double x)
+{
+ if (x < 0)
+ return -x;
+
+ return x;
+}
+
+/*
+ * 128-bit arithmetic functions needed to support the stddev() aggregating
+ * action.
+ */
+static inline int
+dt_gt_128(uint64_t *a, uint64_t *b)
+{
+ return a[1] > b[1] || (a[1] == b[1] && a[0] > b[0]);
+}
+
+static inline int
+dt_ge_128(uint64_t *a, uint64_t *b)
+{
+ return a[1] > b[1] || (a[1] == b[1] && a[0] >= b[0]);
+}
+
+static inline int
+dt_le_128(uint64_t *a, uint64_t *b)
+{
+ return a[1] < b[1] || (a[1] == b[1] && a[0] <= b[0]);
+}
+
+/*
+ * Shift the 128-bit value in a by b. If b is positive, shift left.
+ * If b is negative, shift right.
+ */
+static inline void
+dt_shift_128(uint64_t *a, int b)
+{
+ uint64_t mask;
+
+ if (b == 0)
+ return;
+
+ if (b < 0) {
+ b = -b;
+ if (b >= 64) {
+ a[0] = a[1] >> (b - 64);
+ a[1] = 0;
+ } else {
+ a[0] >>= b;
+ mask = 1LL << (64 - b);
+ mask -= 1;
+ a[0] |= ((a[1] & mask) << (64 - b));
+ a[1] >>= b;
+ }
+ } else {
+ if (b >= 64) {
+ a[1] = a[0] << (b - 64);
+ a[0] = 0;
+ } else {
+ a[1] <<= b;
+ mask = a[0] >> (64 - b);
+ a[1] |= mask;
+ a[0] <<= b;
+ }
+ }
+}
+
+static inline int
+dt_nbits_128(uint64_t *a)
+{
+ int nbits = 0;
+ uint64_t tmp[2];
+ uint64_t zero[2] = { 0, 0 };
+
+ tmp[0] = a[0];
+ tmp[1] = a[1];
+
+ dt_shift_128(tmp, -1);
+ while (dt_gt_128(tmp, zero)) {
+ dt_shift_128(tmp, -1);
+ nbits++;
+ }
+
+ return nbits;
+}
+
+static inline void
+dt_subtract_128(uint64_t *minuend, uint64_t *subtrahend, uint64_t *difference)
+{
+ uint64_t result[2];
+
+ result[0] = minuend[0] - subtrahend[0];
+ result[1] = minuend[1] - subtrahend[1] -
+ (minuend[0] < subtrahend[0] ? 1 : 0);
+
+ difference[0] = result[0];
+ difference[1] = result[1];
+}
+
+static inline void
+dt_add_128(uint64_t *addend1, uint64_t *addend2, uint64_t *sum)
+{
+ uint64_t result[2];
+
+ result[0] = addend1[0] + addend2[0];
+ result[1] = addend1[1] + addend2[1] +
+ (result[0] < addend1[0] || result[0] < addend2[0] ? 1 : 0);
+
+ sum[0] = result[0];
+ sum[1] = result[1];
+}
+
+/*
+ * The basic idea is to break the 2 64-bit values into 4 32-bit values,
+ * use native multiplication on those, and then re-combine into the
+ * resulting 128-bit value.
+ *
+ * (hi1 << 32 + lo1) * (hi2 << 32 + lo2) =
+ * hi1 * hi2 << 64 +
+ * hi1 * lo2 << 32 +
+ * hi2 * lo1 << 32 +
+ * lo1 * lo2
+ */
+static inline void
+dt_multiply_128(uint64_t factor1, uint64_t factor2, uint64_t *product)
+{
+ uint64_t hi1, hi2, lo1, lo2;
+ uint64_t tmp[2];
+
+ hi1 = factor1 >> 32;
+ hi2 = factor2 >> 32;
+
+ lo1 = factor1 & DT_MASK_LO;
+ lo2 = factor2 & DT_MASK_LO;
+
+ product[0] = lo1 * lo2;
+ product[1] = hi1 * hi2;
+
+ tmp[0] = hi1 * lo2;
+ tmp[1] = 0;
+ dt_shift_128(tmp, 32);
+ dt_add_128(product, tmp, product);
+
+ tmp[0] = hi2 * lo1;
+ tmp[1] = 0;
+ dt_shift_128(tmp, 32);
+ dt_add_128(product, tmp, product);
+}
+
+/*
+ * This is long-hand division.
+ *
+ * We initialize subtrahend by shifting divisor left as far as possible. We
+ * loop, comparing subtrahend to dividend: if subtrahend is smaller, we
+ * subtract and set the appropriate bit in the result. We then shift
+ * subtrahend right by one bit for the next comparison.
+ */
+static inline void
+dt_divide_128(uint64_t *dividend, uint64_t divisor, uint64_t *quotient)
+{
+ uint64_t result[2] = { 0, 0 };
+ uint64_t remainder[2];
+ uint64_t subtrahend[2];
+ uint64_t divisor_128[2];
+ uint64_t mask[2] = { 1, 0 };
+ int log = 0;
+
+ assert(divisor != 0);
+
+ divisor_128[0] = divisor;
+ divisor_128[1] = 0;
+
+ remainder[0] = dividend[0];
+ remainder[1] = dividend[1];
+
+ subtrahend[0] = divisor;
+ subtrahend[1] = 0;
+
+ while (divisor > 0) {
+ log++;
+ divisor >>= 1;
+ }
+
+ dt_shift_128(subtrahend, 128 - log);
+ dt_shift_128(mask, 128 - log);
+
+ while (dt_ge_128(remainder, divisor_128)) {
+ if (dt_ge_128(remainder, subtrahend)) {
+ dt_subtract_128(remainder, subtrahend, remainder);
+ result[0] |= mask[0];
+ result[1] |= mask[1];
+ }
+
+ dt_shift_128(subtrahend, -1);
+ dt_shift_128(mask, -1);
+ }
+
+ quotient[0] = result[0];
+ quotient[1] = result[1];
+}
+
+/*
+ * This is the long-hand method of calculating a square root.
+ * The algorithm is as follows:
+ *
+ * 1. Group the digits by 2 from the right.
+ * 2. Over the leftmost group, find the largest single-digit number
+ * whose square is less than that group.
+ * 3. Subtract the result of the previous step (2 or 4, depending) and
+ * bring down the next two-digit group.
+ * 4. For the result R we have so far, find the largest single-digit number
+ * x such that 2 * R * 10 * x + x^2 is less than the result from step 3.
+ * (Note that this is doubling R and performing a decimal left-shift by 1
+ * and searching for the appropriate decimal to fill the one's place.)
+ * The value x is the next digit in the square root.
+ * Repeat steps 3 and 4 until the desired precision is reached. (We're
+ * dealing with integers, so the above is sufficient.)
+ *
+ * In decimal, the square root of 582,734 would be calculated as so:
+ *
+ * __7__6__3
+ * | 58 27 34
+ * -49 (7^2 == 49 => 7 is the first digit in the square root)
+ * --
+ * 9 27 (Subtract and bring down the next group.)
+ * 146 8 76 (2 * 7 * 10 * 6 + 6^2 == 876 => 6 is the next digit in
+ * ----- the square root)
+ * 51 34 (Subtract and bring down the next group.)
+ * 1523 45 69 (2 * 76 * 10 * 3 + 3^2 == 4569 => 3 is the next digit in
+ * ----- the square root)
+ * 5 65 (remainder)
+ *
+ * The above algorithm applies similarly in binary, but note that the
+ * only possible non-zero value for x in step 4 is 1, so step 4 becomes a
+ * simple decision: is 2 * R * 2 * 1 + 1^2 (aka R << 2 + 1) less than the
+ * preceding difference?
+ *
+ * In binary, the square root of 11011011 would be calculated as so:
+ *
+ * __1__1__1__0
+ * | 11 01 10 11
+ * 01 (0 << 2 + 1 == 1 < 11 => this bit is 1)
+ * --
+ * 10 01 10 11
+ * 101 1 01 (1 << 2 + 1 == 101 < 1001 => next bit is 1)
+ * -----
+ * 1 00 10 11
+ * 1101 11 01 (11 << 2 + 1 == 1101 < 10010 => next bit is 1)
+ * -------
+ * 1 01 11
+ * 11101 1 11 01 (111 << 2 + 1 == 11101 > 10111 => last bit is 0)
+ *
+ */
+static inline uint64_t
+dt_sqrt_128(uint64_t *square)
+{
+ uint64_t result[2] = { 0, 0 };
+ uint64_t diff[2] = { 0, 0 };
+ uint64_t one[2] = { 1, 0 };
+ uint64_t next_pair[2];
+ uint64_t next_try[2];
+ uint64_t bit_pairs, pair_shift;
+ uint64_t i;
+
+ bit_pairs = dt_nbits_128(square) / 2;
+ pair_shift = bit_pairs * 2;
+
+ for (i = 0; i <= bit_pairs; i++) {
+ /*
+ * Bring down the next pair of bits.
+ */
+ next_pair[0] = square[0];
+ next_pair[1] = square[1];
+ dt_shift_128(next_pair, -pair_shift);
+ next_pair[0] &= 0x3;
+ next_pair[1] = 0;
+
+ dt_shift_128(diff, 2);
+ dt_add_128(diff, next_pair, diff);
+
+ /*
+ * next_try = R << 2 + 1
+ */
+ next_try[0] = result[0];
+ next_try[1] = result[1];
+ dt_shift_128(next_try, 2);
+ dt_add_128(next_try, one, next_try);
+
+ if (dt_le_128(next_try, diff)) {
+ dt_subtract_128(diff, next_try, diff);
+ dt_shift_128(result, 1);
+ dt_add_128(result, one, result);
+ } else {
+ dt_shift_128(result, 1);
+ }
+
+ pair_shift -= 2;
+ }
+
+ assert(result[1] == 0);
+
+ return result[0];
+}
+
+static inline uint64_t
+dt_stddev(uint64_t *data, uint64_t normal)
+{
+ uint64_t avg_of_squares[2];
+ uint64_t square_of_avg[2];
+ int64_t norm_avg;
+ uint64_t diff[2];
+
+ /*
+ * The standard approximation for standard deviation is
+ * sqrt(average(x**2) - average(x)**2), i.e. the square root
+ * of the average of the squares minus the square of the average.
+ */
+ dt_divide_128(data + 2, normal, avg_of_squares);
+ dt_divide_128(avg_of_squares, data[0], avg_of_squares);
+
+ norm_avg = (int64_t)data[1] / (int64_t)normal / (int64_t)data[0];
+
+ if (norm_avg < 0)
+ norm_avg = -norm_avg;
+
+ dt_multiply_128((uint64_t)norm_avg, (uint64_t)norm_avg, square_of_avg);
+
+ dt_subtract_128(avg_of_squares, square_of_avg, diff);
+
+ return dt_sqrt_128(diff);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _DT_MATH_H */
diff --git a/libdtrace/dt_printf.c b/libdtrace/dt_printf.c
index 4f814c4e..f09e684b 100644
--- a/libdtrace/dt_printf.c
+++ b/libdtrace/dt_printf.c
@@ -16,6 +16,7 @@
#include <dt_impl.h>
#include <dt_aggregate.h>
+#include <dt_math.h>
#include <dt_module.h>
#include <dt_printf.h>
#include <dt_string.h>
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 4/9] python: Add cpython bindings for libdtrace
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (2 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 3/9] libdtrace: Refactor math functions into dt_math.h Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 5/9] dtrace.spec: add python bindings packaging Alan Maguire
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
This set of bindings allows interaction with DTrace programs
from python using libdtrace interfaces under the hood.
The process is to
1. Instantiate a PyDTraceSession
2. Compile a program string via session.compile()
3. Enable probes via session.enable(program)
4. Control execution via session.go(), session.work(), session.stop()
5. Consume data: use session.work() to retrieve probe data
For example to run DTrace for 5 seconds, collecting systemcalls
aggregated by executable name and system call:
from dtrace import DTraceSession
import time
DURATION=5
PROGRAM = r"""
syscall:::entry
{
@counts[execname, probefunc] = count();
}
"""
with DTraceSession() as dt:
prog = dt.compile(PROGRAM)
info = dt.enable(prog)
dt.go()
end = time.monotonic() + DURATION
print(f"Enabled program; matched {info['matches']} probes.")
try:
while time.monotonic() < end:
time.sleep(0.1) # reduce CPU burn when idle
except KeyboardInterrupt:
print(f"Exiting...\n")
finally:
dt.work()
dt.stop()
# Snapshot aggregations and display results
dt.agg_snap()
for record in dt.agg_walk("valrev"):
print(f"{record['keys'][0]:<40} {record['keys'][1]:<40} {record['value']:<10}")
$ sudo python3 syscall.py
Enabled program; matched 367 probes.
python3 pselect6 50
ruby futex 34
Python bindings are built by default, but specifying --without-python
to ./configure avoids building them.
Aggregation keys are represented as a list; if a key is a stack()
or ustack(), it is represented as an ordered list of symbols
from root to leaf.
Aggregation values are represented as integers for the most part;
quantized aggregation values are represented as a dict keyed by bucket.
See bindings/python/README.md for more details.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
Assisted-by: OpenAI Codex CLI
---
GNUmakefile | 2 +
bindings/Build | 35 +
bindings/python/README.md | 167 +++
bindings/python/pyproject.toml | 3 +
bindings/python/setup.py | 67 +
bindings/python/src/pydtrace_module.c | 1997 +++++++++++++++++++++++++
configure | 4 +-
7 files changed, 2274 insertions(+), 1 deletion(-)
create mode 100644 bindings/Build
create mode 100644 bindings/python/README.md
create mode 100644 bindings/python/pyproject.toml
create mode 100644 bindings/python/setup.py
create mode 100644 bindings/python/src/pydtrace_module.c
diff --git a/GNUmakefile b/GNUmakefile
index 10530f3c..a2f906cb 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -106,6 +106,8 @@ INSTPKGCONFIGDIR = $(DESTDIR)$(PKGCONFIGDIR)
TESTDIR = $(LIBDIR)/dtrace/testsuite
INSTTESTDIR = $(DESTDIR)$(TESTDIR)
WITH_SYSTEMD = y
+WITH_PYTHON = y
+PYTHON ?= python3
TARGETS =
DTRACE ?= $(objdir)/dtrace
diff --git a/bindings/Build b/bindings/Build
new file mode 100644
index 00000000..16b82df5
--- /dev/null
+++ b/bindings/Build
@@ -0,0 +1,35 @@
+# Python bindings integration
+
+PYTHON_SRC_DIR := bindings/python
+PYTHON_BINDINGS_OUT := $(objdir)/bindings/python
+PYTHON_BINDINGS_STAMP := $(PYTHON_BINDINGS_OUT)/.built
+PYTHON_SITEARCH ?= $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_path("platlib"))')
+
+ifeq ($(WITH_PYTHON),y)
+TARGETS += bindings-python
+PHONIES += bindings-python install-python
+
+bindings-python: $(PYTHON_BINDINGS_STAMP)
+
+$(PYTHON_BINDINGS_STAMP): $(objdir)/libdtrace.so \
+ $(wildcard $(PYTHON_SRC_DIR)/src/*.c) \
+ $(PYTHON_SRC_DIR)/setup.py \
+ $(PYTHON_SRC_DIR)/pyproject.toml
+ $(call describe-target,PYTHON,$(PYTHON_BINDINGS_OUT))
+ mkdir -p $(PYTHON_BINDINGS_OUT)
+ cd $(PYTHON_SRC_DIR) && \
+ DTRACE_OBJDIR="$(abspath $(objdir))" $(PYTHON) setup.py build_ext \
+ --build-lib $(abspath $(PYTHON_BINDINGS_OUT)) \
+ --build-temp $(abspath $(PYTHON_BINDINGS_OUT))/temp
+ touch $@
+
+install:: install-python
+
+install-python: $(PYTHON_BINDINGS_STAMP)
+ $(call describe-install-target,$(PYTHON_SITEARCH),$(notdir $(wildcard $(PYTHON_BINDINGS_OUT)/dtrace*.so)))
+ mkdir -p $(DESTDIR)$(PYTHON_SITEARCH)
+ install -m 755 $(PYTHON_BINDINGS_OUT)/dtrace*.so $(DESTDIR)$(PYTHON_SITEARCH)
+endif
+
+clean::
+ rm -rf $(PYTHON_BINDINGS_OUT)
diff --git a/bindings/python/README.md b/bindings/python/README.md
new file mode 100644
index 00000000..0dd7bbd3
--- /dev/null
+++ b/bindings/python/README.md
@@ -0,0 +1,167 @@
+# Python bindings for libdtrace
+
+This directory contains Python bindings for the `libdtrace` consumer API. The
+extension allows Python applications to compile D programs, enable and control
+tracing, and inspect aggregation results (including associative arrays) without
+shelling out to the `dtrace` CLI.
+
+## Requirements
+
+- Python 3.6 or newer
+- libdtrace
+- A working C compiler toolchain and Python development headers
+
+## Building and installing
+
+```bash
+# Build dtrace first (from repository root)
+$ make
+$ sudo make install
+```
+
+Alternatively you can install using pip:
+
+```
+# Install the bindings into your current Python environment
+$ cd bindings/python
+$ python3 -m pip install --upgrade build
+$ python3 -m pip install -e .
+```
+
+## Quick start
+
+```python
+from dtrace import DTraceSession
+
+program = """
+#pragma D option quiet
+syscall::open*:entry
+{
+ @counts[execname] = count();
+}
+"""
+
+with DTraceSession() as dt:
+ compiled = dt.compile(program)
+ dt.enable(compiled)
+ dt.go()
+ # ... run workload here ...
+ dt.work()
+ dt.agg_snap()
+ for entry in dt.agg_walk():
+ print(entry["keys"], entry["samples"], entry["value"])
+```
+
+## API Guide
+
+### Sessions and lifecycle
+
+`dtrace.DTraceSession` is the entry point. Sessions implement the context
+manager protocol so the recommended pattern is:
+
+```python
+from dtrace import DTraceSession
+
+with DTraceSession() as dt:
+ ...
+```
+
+Upon construction the binding calls `dtrace_open()`/`dtrace_init()` and installs
+its default buffer sizing (`aggsize`/`bufsize`). The session must remain open
+for the lifetime of any compiled programs or grabbed processes. If you create a
+session without a context manager, remember to call `close()` when finished.
+
+Use `setopt(option, value=None)` to tune libdtrace options before enabling a
+program. Any value is converted to a string; passing `None` clears an option.
+
+### Compiling and enabling programs
+
+`compile(program, cflags=0, argv=None, spec=DTRACE_PROBESPEC_NAME)` wraps
+`dtrace_program_strcompile()` and returns a `DTraceProgram` object bound to the
+session. The optional `argv` sequence is encoded to UTF-8 and supplied as the
+compiler argument vector. Each compiled program must be passed into
+`enable(program)` which returns a dictionary summarising the probe attributes
+(`aggregations`, `recgens`, `matches`, `speculations`, `descattr`, `stmtattr`).
+
+### Running the tracing loop
+
+Invoke `go(cflags=0)` to transition the session into a running state. Typical
+loops alternate between driving the consumer and handling results:
+
+```python
+dt.go()
+while dt.status() == dtrace.DTRACE_STATUS_OKAY:
+ status, probes = dt.work(return_records=True)
+ # inspect `probes`, drive workload, or break once done
+
+dt.stop()
+```
+
+`status()` wraps `dtrace_status()` but also reports synthetic values tracked by
+the binding when the traced processes exit or when an `exit()` action fires.
+`go()` may be followed by `update()` to re-scan loaded kernel modules if probes
+are added dynamically, and `stop()` can be called manually to halt collection.
+
+`work(return_records=False)` executes `dtrace_work()`. Its return value is a
+`(status, probes)` tuple where `status` is one of the exported
+`DTRACE_STATUS_*` constants and `probes` is a list of dictionaries describing
+each consumed probe firing. By default only the status is returned; passing
+`return_records=True` captures full probe metadata plus a list of record
+descriptors (size, action, alignment, raw bytes, and any libdtrace metadata).
+If you are just dealing with aggregations, there is no need to return records;
+aggregation snapshot walk is all you will need.
+
+### Aggregations and snapshots
+
+`agg_snap()` issues `dtrace_aggregate_snap()` to freeze the aggregation buffer;
+`agg_walk(mode="values")` walks that snapshot using the selected ordering mode
+(`"default"`, `"values"`, `"valrev"`, `"keys"`, etc.) and returns a list of
+entries. Each entry is a dictionary with:
+
+- `keys`: ordered list of aggregation keys.
+- `samples`: raw sample count collected for the entry.
+- `normal`: the normalisation factor applied to the aggregate (1 if unused).
+- `value`: the converted aggregate value.
+- `raw`: the raw byte payload backing `value`.
+
+Stacks produced by `stack()`, `ustack()`, or `jstack()` actions are converted to
+Python lists ordered from root to leaf (the binding inserts the deepest frame at
+the end of the list). Quantization actions (`quantize`, `lquantize`,
+`llquantize`) are represented as dictionaries mapping bucket identifiers to
+counts, already divided by any aggregation normaliser. Buckets use the same
+numeric semantics as libdtrace (e.g., powers-of-two for `quantize` or explicit
+boundaries for `lquantize`).
+
+The raw data remains available in `entry["raw"]` when you need the original
+binary layout, for example to re-run libdtrace helpers.
+
+### Process control helpers
+
+The binding exposes libdtrace process control for coordinated tracing:
+
+- `proc_create(args)` forks a new traced process, using the provided argument
+ list. It returns a `DTraceProc` wrapper that keeps the underlying handle.
+- `proc_grab_pid(pid)` attaches to an existing process by pid.
+- `proc_continue(proc)` resumes a previously created or grabbed process and
+ marks it as live so the session can detect when all traced processes exit.
+- `proc_release(proc)` releases libdtrace’s hold on the process once finished.
+
+`DTraceProc` currently exposes `getpid()` to query the grabbed pid. The session
+tracks how many processes are created or grabbed so that when the last one
+exits the consumer loop observes `DTRACE_STATUS_EXITED` and stops automatically.
+
+### Detecting completion or stop conditions
+
+The binding raises `DTraceError` when libdtrace reports failures. To detect a
+graceful stop, inspect either `status()` or the first element of the tuple
+returned by `work()`. When libdtrace reports `DTRACE_STATUS_STOPPED` or
+`DTRACE_STATUS_FILLED`, you can drain remaining data, call `agg_snap()`/
+`agg_walk()` if needed, and then `close()` the session. A session observing a
+`DTRACEACT_EXIT` record records the exit status internally, and a stopped state
+is reported even if `dtrace_status()` still returns `DTRACE_STATUS_OKAY`, so
+applications can rely on `status()` reflecting exits from traced processes.
+
+When running under a context manager, exiting the `with` block closes the
+session automatically, regardless of whether tracing finished normally or due
+to `stop()`.
+
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
new file mode 100644
index 00000000..7b959378
--- /dev/null
+++ b/bindings/python/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=64", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
new file mode 100644
index 00000000..331e67df
--- /dev/null
+++ b/bindings/python/setup.py
@@ -0,0 +1,67 @@
+import os
+import subprocess
+from pathlib import Path
+from typing import List
+
+from setuptools import Extension, setup
+
+ROOT = Path(__file__).resolve().parents[2]
+OBJDIR = Path(os.environ.get("DTRACE_OBJDIR", ROOT / "build"))
+
+def project_version() -> str:
+ """Return the DTrace version used by the top-level build."""
+ version = os.environ.get("DTRACE_VERSION")
+ if version:
+ return version
+
+ return subprocess.check_output(
+ [str(ROOT / "libdtrace" / "mkvers"), "-vcurrent=t",
+ str(ROOT / "libdtrace" / "versions.list")],
+ universal_newlines=True,
+ ).strip()
+
+# Default include directories assume an in-tree build of libdtrace.
+default_include_dirs = [
+ str(ROOT / "include"),
+ str(ROOT / "libdtrace"),
+ str(ROOT / "uts" / "common"),
+ str(ROOT / "include" / "dtrace"),
+ str(OBJDIR),
+]
+
+default_library_dirs: List[str] = []
+if OBJDIR.exists():
+ default_library_dirs.append(str(OBJDIR))
+
+extra_include = os.environ.get("DTRACE_INCLUDE_DIRS")
+if extra_include:
+ default_include_dirs.extend(p for p in extra_include.split(os.pathsep) if p)
+
+extra_library = os.environ.get("DTRACE_LIBRARY_DIRS")
+if extra_library:
+ default_library_dirs.extend(p for p in extra_library.split(os.pathsep) if p)
+
+extra_link_args = os.environ.get("DTRACE_EXTRA_LINK_ARGS", "").split()
+extra_compile_args = os.environ.get("DTRACE_EXTRA_COMPILE_ARGS", "").split()
+
+ext_modules = [
+ Extension(
+ "dtrace",
+ sources=["src/pydtrace_module.c"],
+ include_dirs=default_include_dirs,
+ libraries=["dtrace"],
+ library_dirs=default_library_dirs,
+ extra_compile_args=extra_compile_args,
+ extra_link_args=extra_link_args,
+ )
+]
+
+setup(
+ name="dtrace",
+ version=project_version(),
+ description="Python bindings for libdtrace",
+ author="Oracle Linux DTrace maintainers",
+ license="UPL",
+ python_requires=">=3.6",
+ ext_modules=ext_modules,
+)
diff --git a/bindings/python/src/pydtrace_module.c b/bindings/python/src/pydtrace_module.c
new file mode 100644
index 00000000..ac361307
--- /dev/null
+++ b/bindings/python/src/pydtrace_module.c
@@ -0,0 +1,1997 @@
+/*
+ * Python bindings for libdtrace.
+ *
+ * This module exposes a thin object oriented wrapper around libdtrace so that
+ * Python applications can compile, enable, and control tracing programs while
+ * also consuming aggregation results as native Python objects.
+ *
+ * Oracle Linux DTrace is licensed under the Universal Permissive License v 1.0.
+ */
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <pthread.h>
+
+#include <dtrace.h>
+#include "dt_aggregate.h"
+#include "dt_math.h"
+
+#ifndef UNUSED
+#define UNUSED(x) ((void)(x))
+#endif
+
+typedef struct {
+ PyObject_HEAD
+ dtrace_hdl_t *dtp;
+ FILE *fp;
+ int pids; /* pid()s _create()ed or grab()bed */
+ int pids_live; /* _continue()d active processes */
+ int status; /* EXITed or STOPPED */
+ int exit_status;
+ int closed;
+} PyDTraceSession;
+
+typedef struct {
+ PyObject_HEAD
+ PyDTraceSession *session;
+ dtrace_prog_t *prog;
+} PyDTraceProgram;
+
+typedef struct {
+ PyObject_HEAD
+ PyDTraceSession *session;
+ struct dtrace_proc *proc;
+} PyDTraceProc;
+
+static PyObject *PyExc_DTraceError = NULL;
+
+/* ------------------------------------------------------------------------- */
+/* Utility helpers */
+/* ------------------------------------------------------------------------- */
+
+static int
+ensure_open(PyDTraceSession *self)
+{
+ if (self->closed || self->dtp == NULL) {
+ PyErr_SetString(PyExc_DTraceError, "DTrace session is closed");
+ return -1;
+ }
+
+ return 0;
+}
+
+static PyObject *
+raise_dtrace_error_with_code(dtrace_hdl_t *dtp, int error, const char *ctx)
+{
+ const char *msg =
+ dtp != NULL ? dtrace_errmsg(dtp, error) : "unknown error";
+
+ if (ctx != NULL)
+ PyErr_Format(PyExc_DTraceError, "%s: %s", ctx, msg);
+ else
+ PyErr_SetString(PyExc_DTraceError, msg);
+
+ return NULL;
+}
+
+static PyObject *
+raise_dtrace_error(PyDTraceSession *self, const char *ctx)
+{
+ int error = dtrace_errno(self->dtp);
+ return raise_dtrace_error_with_code(self->dtp, error, ctx);
+}
+
+static int
+dict_set_ulong(PyObject *dict, const char *key, unsigned long value)
+{
+ PyObject *obj = PyLong_FromUnsignedLong(value);
+ int rc;
+
+ if (obj == NULL)
+ return -1;
+
+ rc = PyDict_SetItemString(dict, key, obj);
+ Py_DECREF(obj);
+ return rc;
+}
+
+/* ------------------------------------------------------------------------- */
+/* Aggregation walk context */
+/* ------------------------------------------------------------------------- */
+
+typedef struct {
+ PyObject *list;
+ PyDTraceSession *session;
+} agg_walk_ctx_t;
+
+typedef struct {
+ PyObject *probes;
+ PyObject *current_probe;
+ PyObject *records;
+ PyDTraceSession *session;
+ int capture;
+ int aborted;
+} work_ctx_t;
+
+static inline uint64_t
+read_uint(const void *addr, size_t size)
+{
+ switch (size) {
+ case 1:
+ return *((const uint8_t *)addr);
+ case 2:
+ return *((const uint16_t *)addr);
+ case 4:
+ return *((const uint32_t *)addr);
+ case 8:
+ return *((const uint64_t *)addr);
+ default:
+ return 0;
+ }
+}
+
+static inline int64_t
+read_sint(const void *addr, size_t size)
+{
+ switch (size) {
+ case 1:
+ return *((const int8_t *)addr);
+ case 2:
+ return *((const int16_t *)addr);
+ case 4:
+ return *((const int32_t *)addr);
+ case 8:
+ return *((const int64_t *)addr);
+ default:
+ return 0;
+ }
+}
+
+static PyObject *
+bytes_or_string_from_buffer(const char *addr, size_t size)
+{
+ if (size == 0)
+ return PyBytes_FromStringAndSize("", 0);
+
+ if (addr[size - 1] == '\0') {
+ size_t n = strnlen(addr, size);
+ return PyUnicode_DecodeUTF8(addr, (Py_ssize_t)n, "replace");
+ }
+
+ return PyBytes_FromStringAndSize(addr, (Py_ssize_t)size);
+}
+
+static const char *
+agg_action_label(uint16_t action)
+{
+ switch (action) {
+ case DT_AGG_AVG:
+ return "avg";
+ case DT_AGG_COUNT:
+ return "count";
+ case DT_AGG_LLQUANTIZE:
+ return "llquantize";
+ case DT_AGG_LQUANTIZE:
+ return "lquantize";
+ case DT_AGG_MAX:
+ return "max";
+ case DT_AGG_MIN:
+ return "min";
+ case DT_AGG_QUANTIZE:
+ return "quantize";
+ case DT_AGG_STDDEV:
+ return "stddev";
+ case DT_AGG_SUM:
+ return "sum";
+ default:
+ return "unknown";
+ }
+}
+
+static inline PyObject *
+quant_dict_add(PyObject *dict, long long bucket, long long count)
+{
+ PyObject *key = PyLong_FromLongLong(bucket);
+ PyObject *val;
+
+ if (key == NULL)
+ return NULL;
+
+ val = PyLong_FromLongLong(count);
+ if (val == NULL) {
+ Py_DECREF(key);
+ return NULL;
+ }
+
+ if (PyDict_SetItem(dict, key, val) < 0) {
+ Py_DECREF(key);
+ Py_DECREF(val);
+ return NULL;
+ }
+
+ Py_DECREF(key);
+ Py_DECREF(val);
+ return dict;
+}
+
+static PyObject *
+convert_quantize(const int64_t *data, size_t nbins, uint64_t normal)
+{
+ PyObject *dict = PyDict_New();
+
+ if (dict == NULL)
+ return NULL;
+
+ if (normal == 0)
+ normal = 1;
+
+ for (size_t i = 0; i < nbins; i++) {
+ int64_t count = data[i];
+
+ if (count == 0)
+ continue;
+
+ count /= (int64_t)normal;
+ if (count == 0)
+ continue;
+
+ if (quant_dict_add(dict, DTRACE_QUANTIZE_BUCKETVAL((uint64_t)i),
+ count) == NULL) {
+ Py_DECREF(dict);
+ return NULL;
+ }
+ }
+
+ return dict;
+}
+
+static PyObject *
+convert_lquantize(const int64_t *data, uint64_t sig, size_t size,
+ uint64_t normal)
+{
+ uint16_t levels = DTRACE_LQUANTIZE_LEVELS(sig);
+ uint16_t step = DTRACE_LQUANTIZE_STEP(sig);
+ int32_t base = DTRACE_LQUANTIZE_BASE(sig);
+ size_t expected;
+ PyObject *dict;
+
+ if (normal == 0)
+ normal = 1;
+
+ expected = (size_t)(levels + 2) * sizeof(uint64_t);
+ if (size != expected)
+ return PyBytes_FromStringAndSize((const char *)data,
+ (Py_ssize_t)size);
+
+ dict = PyDict_New();
+ if (dict == NULL)
+ return NULL;
+
+ for (uint16_t i = 0; i < levels + 2; i++) {
+ int64_t count = data[i];
+ long long bucket;
+
+ if (count == 0)
+ continue;
+
+ count /= (int64_t)normal;
+ if (count == 0)
+ continue;
+
+ if (i == 0)
+ bucket = base - 1;
+ else if (i == levels + 1)
+ bucket = base + (long long)levels * step;
+ else
+ bucket = base + (long long)(i - 1) * step;
+
+ if (quant_dict_add(dict, bucket, count) == NULL) {
+ Py_DECREF(dict);
+ return NULL;
+ }
+ }
+
+ return dict;
+}
+
+static PyObject *
+convert_llquantize(const int64_t *data, uint64_t sig, size_t size,
+ uint64_t normal)
+{
+ int factor = DTRACE_LLQUANTIZE_FACTOR(sig);
+ int lmag = DTRACE_LLQUANTIZE_LMAG(sig);
+ int hmag = DTRACE_LLQUANTIZE_HMAG(sig);
+ int steps = DTRACE_LLQUANTIZE_STEPS(sig);
+ int steps_factor = steps / factor;
+ int bin0;
+ size_t nbins;
+ PyObject *dict;
+
+ if (normal == 0)
+ normal = 1;
+
+ bin0 = 1 + (hmag - lmag + 1) * (steps - steps_factor);
+ nbins = (size_t)(hmag - lmag + 1) * (steps - steps_factor) * 2 + 3;
+
+ if (size != nbins * sizeof(uint64_t))
+ return PyBytes_FromStringAndSize((const char *)data,
+ (Py_ssize_t)size);
+
+ dict = PyDict_New();
+ if (dict == NULL)
+ return NULL;
+
+ for (size_t i = 0; i < nbins; i++) {
+ int64_t count = data[i];
+ long long bucket;
+
+ if (count == 0)
+ continue;
+
+ count /= (int64_t)normal;
+ if (count == 0)
+ continue;
+
+ if (i == 0)
+ bucket = -(long long)powl((double)factor,
+ (double)(hmag + 1));
+ else if (i == nbins - 1)
+ bucket = (long long)powl((double)factor,
+ (double)(hmag + 1));
+ else if (i == (size_t)bin0) {
+ bucket = 0;
+ } else {
+ int left = (int)i - bin0;
+ int is_neg = left < 0;
+ int steps_rem = is_neg ? bin0 - (int)i : left;
+ long long scale;
+ int mag = lmag;
+
+ if (steps_rem == 0) {
+ bucket = 0;
+ goto have_bucket;
+ }
+
+ if (lmag == 0 && steps > factor) {
+ for (int val = 2;
+ val <= factor && steps_rem > 0; val++) {
+ steps_rem -= steps_factor;
+ if (steps_rem == 0) {
+ bucket = is_neg ? -val : val;
+ goto have_bucket;
+ }
+ }
+ mag++;
+ }
+
+ scale = (long long)(powl((double)factor,
+ (double)(mag + 1)) /
+ (double)steps);
+
+ while (mag <= hmag) {
+ for (int step = steps_factor + 1; step <= steps;
+ step++) {
+ steps_rem--;
+ if (steps_rem == 0) {
+ bucket = step * scale;
+ if (is_neg)
+ bucket = -bucket;
+ goto have_bucket;
+ }
+ }
+ scale *= factor;
+ mag++;
+ }
+
+ bucket = is_neg ? -(long long)powl((double)factor,
+ (double)(hmag + 1))
+ : (long long)powl((double)factor,
+ (double)(hmag + 1));
+ }
+
+ have_bucket:
+ if (quant_dict_add(dict, bucket, count) == NULL) {
+ Py_DECREF(dict);
+ return NULL;
+ }
+ }
+
+ return dict;
+}
+
+static PyObject *
+convert_quantized(const dtrace_recdesc_t *rec, caddr_t base, uint64_t normal,
+ const dtrace_aggdata_t *aggdata)
+{
+ caddr_t addr = base + rec->dtrd_offset;
+ size_t size = rec->dtrd_size;
+ const int64_t *data = (const int64_t *)addr;
+ const dtrace_aggdesc_t *agg = aggdata ? aggdata->dtada_desc : NULL;
+ uint64_t sig = agg ? agg->dtagd_sig : 0;
+
+ switch (rec->dtrd_action) {
+ case DT_AGG_QUANTIZE:
+ if (size % sizeof(uint64_t) != 0)
+ return PyBytes_FromStringAndSize((const char *)addr,
+ (Py_ssize_t)size);
+ return convert_quantize(data, size / sizeof(uint64_t), normal);
+ case DT_AGG_LQUANTIZE:
+ if (sig == 0)
+ return PyBytes_FromStringAndSize((const char *)addr,
+ (Py_ssize_t)size);
+ return convert_lquantize(data, sig, size, normal);
+ case DT_AGG_LLQUANTIZE:
+ if (sig == 0)
+ return PyBytes_FromStringAndSize((const char *)addr,
+ (Py_ssize_t)size);
+ return convert_llquantize(data, sig, size, normal);
+ default:
+ break;
+ }
+
+ return PyBytes_FromStringAndSize((const char *)addr, (Py_ssize_t)size);
+}
+
+static PyObject *
+convert_record_value(PyDTraceSession *session, const dtrace_recdesc_t *rec,
+ caddr_t base, uint64_t normal,
+ const dtrace_aggdata_t *aggdata)
+{
+ caddr_t addr = base + rec->dtrd_offset;
+ size_t size = rec->dtrd_size;
+ const uint64_t *words = (const uint64_t *)addr;
+ uint32_t depth = 0, tgid = 0;
+ char buf[PATH_MAX * 2];
+ PyObject *sym;
+ uint64_t pc;
+ int len;
+
+ switch (rec->dtrd_action) {
+ case DT_AGG_AVG: {
+ if (size < sizeof(int64_t) * 2)
+ return PyFloat_FromDouble(0.0);
+
+ const int64_t *data = (const int64_t *)addr;
+ if (data[0] == 0)
+ return PyFloat_FromDouble(0.0);
+
+ long double avg = (long double)data[1] / (long double)normal /
+ (long double)data[0];
+ return PyFloat_FromDouble((double)avg);
+ }
+
+ case DT_AGG_SUM:
+ case DT_AGG_MIN:
+ case DT_AGG_MAX:
+ case DT_AGG_COUNT: {
+ int64_t value = read_sint(addr, size);
+ value /= (int64_t)normal;
+ return PyLong_FromLongLong(value);
+ }
+
+ case DT_AGG_STDDEV: {
+ uint64_t stddev = dt_stddev((uint64_t *)addr, normal);
+
+ return PyLong_FromUnsignedLongLong(stddev);
+ }
+ case DT_AGG_QUANTIZE:
+ case DT_AGG_LQUANTIZE:
+ case DT_AGG_LLQUANTIZE:
+ return convert_quantized(rec, base, normal, aggdata);
+
+ case DTRACEACT_JSTACK:
+ case DTRACEACT_USTACK:
+ case DTRACEACT_STACK: {
+ depth = *(uint32_t *)words;
+ uint32_t frames =
+ depth < rec->dtrd_size / sizeof(uint64_t)
+ ? depth
+ : (uint32_t)(rec->dtrd_size / sizeof(uint64_t));
+ PyObject *list = PyList_New((Py_ssize_t)0);
+ if (list == NULL)
+ return NULL;
+
+ if (rec->dtrd_action != DTRACEACT_STACK)
+ tgid = ((uint32_t *)words)[3];
+ for (uint32_t i = 0; i < frames; i++) {
+ pc = words[i + 2];
+
+ if (pc == 0)
+ break;
+ len = rec->dtrd_action != DTRACEACT_STACK
+ ? dtrace_uaddr2str(session->dtp, tgid, pc,
+ buf, sizeof(buf))
+ : dtrace_addr2str(session->dtp, pc, buf,
+ sizeof(buf));
+ if (len < 0) {
+ Py_DECREF(list);
+ return raise_dtrace_error(session,
+ "dtrace_addr2str");
+ }
+ PyObject *frame = PyUnicode_FromString(buf);
+ if (frame == NULL) {
+ Py_DECREF(list);
+ return NULL;
+ }
+ if (PyList_Insert(list, 0, frame) < 0) {
+ Py_DECREF(frame);
+ Py_DECREF(list);
+ return NULL;
+ }
+ Py_DECREF(frame);
+ }
+ return list;
+ }
+ case DTRACEACT_SYM:
+ case DTRACEACT_MOD: {
+ pc = words[0];
+ dtrace_syminfo_t dts;
+
+ if (dtrace_lookup_by_addr(session->dtp, pc, NULL, &dts) == 0) {
+ if (rec->dtrd_action == DTRACEACT_SYM)
+ snprintf(buf, sizeof(buf), "%s`%s", dts.object,
+ dts.name);
+ else
+ snprintf(buf, sizeof(buf), "%s", dts.object);
+ } else {
+ snprintf(buf, sizeof(buf), "0x%llx",
+ (unsigned long long)pc);
+ }
+ sym = PyUnicode_FromString(buf);
+ if (sym == NULL)
+ return NULL;
+ return sym;
+ }
+ case DTRACEACT_UADDR: {
+ tgid = ((uint32_t *)words)[0];
+ pc = words[1];
+ len = dtrace_uaddr2str(session->dtp, tgid, pc, buf,
+ sizeof(buf));
+ if (len < 0)
+ return raise_dtrace_error(session, "dtrace_addr2str");
+ sym = PyUnicode_FromString(buf);
+ if (sym == NULL)
+ return NULL;
+ return sym;
+ }
+ /*
+ * These two functions require grabbing pid lock etc, no interfaces yet
+ * in libdtrace for this.
+ */
+ case DTRACEACT_USYM:
+ case DTRACEACT_UMOD:
+ default:
+ break;
+ }
+
+ switch (size) {
+ case 1:
+ case 2:
+ case 4:
+ case 8: {
+ int64_t value = read_sint(addr, size);
+ value /= (int64_t)normal;
+ return PyLong_FromLongLong(value);
+ }
+ default:
+ return bytes_or_string_from_buffer((const char *)addr, size);
+ }
+}
+
+static inline int
+key_requires_format(dtrace_actkind_t action)
+{
+ switch (action) {
+ case DTRACEACT_STACK:
+ case DTRACEACT_USTACK:
+ case DTRACEACT_JSTACK:
+ case DTRACEACT_SYM:
+ case DTRACEACT_USYM:
+ case DTRACEACT_MOD:
+ case DTRACEACT_UMOD:
+ case DTRACEACT_UADDR:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static PyObject *
+convert_aggdata(PyDTraceSession *session, agg_walk_ctx_t *ctx,
+ const dtrace_aggdata_t *aggdata)
+{
+ const dtrace_aggdesc_t *agg = aggdata->dtada_desc;
+ const dtrace_recdesc_t *counter_rec =
+ &agg->dtagd_drecs[DT_AGGDATA_COUNTER];
+ const dtrace_recdesc_t *value_rec =
+ &agg->dtagd_drecs[DT_AGGDATA_RECORD];
+ PyObject *entry = PyDict_New();
+
+ if (entry == NULL)
+ return NULL;
+
+ PyObject *name = agg->dtagd_name != NULL
+ ? PyUnicode_FromString(agg->dtagd_name)
+ : Py_None;
+ if (name == NULL)
+ goto error;
+ if (name == Py_None)
+ Py_INCREF(Py_None);
+ if (PyDict_SetItemString(entry, "name", name) < 0) {
+ Py_DECREF(name);
+ goto error;
+ }
+ Py_DECREF(name);
+
+ PyObject *aggid = PyLong_FromLong(agg->dtagd_varid);
+ if (aggid == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "id", aggid) < 0) {
+ Py_DECREF(aggid);
+ goto error;
+ }
+ Py_DECREF(aggid);
+
+ PyObject *action =
+ PyUnicode_FromString(agg_action_label(value_rec->dtrd_action));
+ if (action == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "action", action) < 0) {
+ Py_DECREF(action);
+ goto error;
+ }
+ Py_DECREF(action);
+
+ PyObject *keys = PyList_New(0);
+ if (keys == NULL)
+ goto error;
+
+ for (uint_t i = 1; i < agg->dtagd_nkrecs; i++) {
+ const dtrace_recdesc_t *rec = &agg->dtagd_krecs[i];
+ PyObject *item = convert_record_value(
+ session, rec, aggdata->dtada_key, 0, aggdata);
+
+ if (item == NULL) {
+ Py_DECREF(keys);
+ goto error;
+ }
+
+ if (PyList_Append(keys, item) < 0) {
+ Py_DECREF(item);
+ Py_DECREF(keys);
+ goto error;
+ }
+ Py_DECREF(item);
+ }
+
+ if (PyDict_SetItemString(entry, "keys", keys) < 0) {
+ Py_DECREF(keys);
+ goto error;
+ }
+ Py_DECREF(keys);
+
+ uint64_t samples =
+ read_uint(aggdata->dtada_data + counter_rec->dtrd_offset,
+ counter_rec->dtrd_size);
+ PyObject *samples_obj = PyLong_FromUnsignedLongLong(samples);
+ if (samples_obj == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "samples", samples_obj) < 0) {
+ Py_DECREF(samples_obj);
+ goto error;
+ }
+ Py_DECREF(samples_obj);
+
+ uint64_t normal = agg->dtagd_normal ? agg->dtagd_normal : 1;
+ PyObject *normal_obj = PyLong_FromUnsignedLongLong(normal);
+ if (normal_obj == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "normal", normal_obj) < 0) {
+ Py_DECREF(normal_obj);
+ goto error;
+ }
+ Py_DECREF(normal_obj);
+
+ PyObject *value = convert_record_value(
+ session, value_rec, aggdata->dtada_data, normal, aggdata);
+ if (value == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "value", value) < 0) {
+ Py_DECREF(value);
+ goto error;
+ }
+ Py_DECREF(value);
+
+ PyObject *raw = PyBytes_FromStringAndSize(
+ (const char *)(aggdata->dtada_data + value_rec->dtrd_offset),
+ (Py_ssize_t)value_rec->dtrd_size);
+ if (raw == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "raw", raw) < 0) {
+ Py_DECREF(raw);
+ goto error;
+ }
+ Py_DECREF(raw);
+
+ return entry;
+
+error:
+ Py_DECREF(entry);
+ return NULL;
+}
+
+static int
+agg_walk_callback(const dtrace_aggdata_t *aggdata, void *arg)
+{
+ agg_walk_ctx_t *ctx = (agg_walk_ctx_t *)arg;
+ PyObject *entry = convert_aggdata(ctx->session, ctx, aggdata);
+
+ if (entry == NULL)
+ return DTRACE_AGGWALK_ERROR;
+
+ if (PyList_Append(ctx->list, entry) < 0) {
+ Py_DECREF(entry);
+ return DTRACE_AGGWALK_ERROR;
+ }
+
+ Py_DECREF(entry);
+ return DTRACE_AGGWALK_NEXT;
+}
+
+/* ------------------------------------------------------------------------- */
+/* PyDTraceProgram */
+/* ------------------------------------------------------------------------- */
+
+static void
+PyDTraceProgram_dealloc(PyDTraceProgram *self)
+{
+ if (self->session)
+ Py_XDECREF(self->session);
+ PyObject_Del(self);
+}
+
+static PyTypeObject PyDTraceProgramType;
+
+static PyObject *
+PyDTraceProgram_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ UNUSED(args);
+ UNUSED(kwds);
+ PyDTraceProgram *self = (PyDTraceProgram *)type->tp_alloc(type, 0);
+ if (self != NULL) {
+ self->session = NULL;
+ self->prog = NULL;
+ }
+ return (PyObject *)self;
+}
+
+static PyMethodDef PyDTraceProgram_methods[] = {{NULL, NULL, 0, NULL}};
+
+static PyTypeObject PyDTraceProgramType = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "dtrace.DTraceProgram",
+ .tp_basicsize = sizeof(PyDTraceProgram),
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = PyDTraceProgram_new,
+ .tp_dealloc = (destructor)PyDTraceProgram_dealloc,
+ .tp_methods = PyDTraceProgram_methods,
+};
+
+/* ------------------------------------------------------------------------- */
+/* PyDTraceSession */
+/* ------------------------------------------------------------------------- */
+
+static void
+PyDTraceSession_dealloc(PyDTraceSession *self)
+{
+ if (self->dtp != NULL && !self->closed) {
+ Py_BEGIN_ALLOW_THREADS dtrace_close(self->dtp);
+ Py_END_ALLOW_THREADS
+ }
+ self->dtp = NULL;
+ if (self->fp)
+ fclose(self->fp);
+ self->closed = 1;
+ PyObject_Del(self);
+}
+
+static PyObject *
+PyDTraceSession_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ UNUSED(args);
+ UNUSED(kwds);
+ PyDTraceSession *self = (PyDTraceSession *)type->tp_alloc(type, 0);
+ if (self != NULL) {
+ self->dtp = NULL;
+ self->closed = 1;
+ }
+ return (PyObject *)self;
+}
+
+static void
+prochandler(pid_t pid, const char *msg, void *arg)
+{
+ PyDTraceSession *self = arg;
+
+ if (pid < 0 && self->pids_live) {
+ self->pids_live--;
+ if (self->pids_live == 0)
+ dtrace_stop(self->dtp);
+ }
+}
+
+static int
+PyDTraceSession_init(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"version", "flags", NULL};
+ int version = DTRACE_VERSION;
+ unsigned int flags = 0;
+ int err = 0;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Ii", kwlist, &version,
+ &flags))
+ return -1;
+
+ self->dtp = dtrace_open(version, flags, &err);
+ if (self->dtp == NULL)
+ return raise_dtrace_error_with_code(NULL, err, "dtrace_open") ==
+ NULL
+ ? -1
+ : -1;
+
+ self->fp = fopen("/dev/null", "a");
+ if (dtrace_init(self->dtp) != 0) {
+ raise_dtrace_error(self, "dtrace_init");
+ dtrace_close(self->dtp);
+ fclose(self->fp);
+ self->dtp = NULL;
+ self->fp = NULL;
+ return -1;
+ }
+ if (dtrace_handle_proc(self->dtp, &prochandler, self) != 0 ||
+ dtrace_setopt(self->dtp, "aggsize", "4m") != 0 ||
+ dtrace_setopt(self->dtp, "bufsize", "4m") != 0) {
+ raise_dtrace_error(self, "dtrace_setopt");
+ dtrace_close(self->dtp);
+ fclose(self->fp);
+ self->dtp = NULL;
+ self->fp = NULL;
+ return -1;
+ }
+ self->closed = 0;
+ return 0;
+}
+
+static PyObject *
+PyDTraceSession_close(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (!self->closed && self->dtp != NULL) {
+ Py_BEGIN_ALLOW_THREADS dtrace_close(self->dtp);
+ Py_END_ALLOW_THREADS fclose(self->fp);
+ self->dtp = NULL;
+ self->fp = NULL;
+ self->closed = 1;
+ }
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_enter(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ Py_INCREF(self);
+ return (PyObject *)self;
+}
+
+static PyObject *
+PyDTraceSession_exit(PyDTraceSession *self, PyObject *args)
+{
+ UNUSED(args);
+
+ if (!self->closed && self->dtp != NULL) {
+ Py_BEGIN_ALLOW_THREADS dtrace_close(self->dtp);
+ Py_END_ALLOW_THREADS if (self->fp) fclose(self->fp);
+ self->dtp = NULL;
+ self->fp = NULL;
+ self->closed = 1;
+ }
+
+ Py_RETURN_FALSE;
+}
+
+static PyObject *
+PyDTraceSession_setopt(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"option", "value", NULL};
+ const char *opt = NULL;
+ PyObject *value_obj = Py_None;
+ PyObject *value_str = NULL;
+ const char *cvalue = NULL;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|O", kwlist, &opt,
+ &value_obj))
+ return NULL;
+
+ if (value_obj == Py_None) {
+ cvalue = NULL;
+ } else {
+ PyObject *string_obj = PyObject_Str(value_obj);
+ if (string_obj == NULL)
+ return NULL;
+ value_str = PyUnicode_AsEncodedString(string_obj, "utf-8",
+ "replace");
+ Py_DECREF(string_obj);
+ if (value_str == NULL)
+ return NULL;
+ cvalue = PyBytes_AS_STRING(value_str);
+ }
+
+ if (dtrace_setopt(self->dtp, opt, cvalue) != 0) {
+ Py_XDECREF(value_str);
+ return raise_dtrace_error(self, "dtrace_setopt");
+ }
+
+ Py_XDECREF(value_str);
+ Py_RETURN_NONE;
+}
+
+/*
+ * Need a lock because compilation uses global state; we would like to remove
+ * this eventually.
+ */
+pthread_mutex_t compile_lock = PTHREAD_MUTEX_INITIALIZER;
+
+static PyObject *
+PyDTraceSession_compile(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"program", "cflags", "argv", "spec", NULL};
+ const char *source = NULL;
+ unsigned int cflags = 0;
+ PyObject *argv_obj = NULL;
+ int spec = DTRACE_PROBESPEC_NAME;
+ PyObject *argv_seq = NULL;
+ char **argv = NULL;
+ PyObject **encoded = NULL;
+ Py_ssize_t argc = 0;
+ dtrace_prog_t *prog = NULL;
+ PyDTraceProgram *wrapper = NULL;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|IOi", kwlist, &source,
+ &cflags, &argv_obj, &spec))
+ return NULL;
+
+ if (argv_obj != NULL && argv_obj != Py_None) {
+ argv_seq = PySequence_Fast(
+ argv_obj, "argv must be a sequence of strings");
+ if (argv_seq == NULL)
+ return NULL;
+
+ argc = PySequence_Fast_GET_SIZE(argv_seq);
+ if (argc > 0) {
+ Py_ssize_t i;
+
+ argv = PyMem_Calloc((size_t)argc + 1, sizeof(char *));
+ encoded =
+ PyMem_Calloc((size_t)argc, sizeof(PyObject *));
+ if (argv == NULL || encoded == NULL) {
+ PyMem_Free(argv);
+ PyMem_Free(encoded);
+ Py_DECREF(argv_seq);
+ return PyErr_NoMemory();
+ }
+
+ for (i = 0; i < argc; i++) {
+ PyObject *item =
+ PySequence_Fast_GET_ITEM(argv_seq, i);
+ PyObject *string_obj = PyObject_Str(item);
+ if (string_obj == NULL) {
+ Py_DECREF(argv_seq);
+ goto compile_error;
+ }
+ encoded[i] = PyUnicode_AsEncodedString(
+ string_obj, "utf-8", "replace");
+ Py_DECREF(string_obj);
+ if (encoded[i] == NULL) {
+ Py_DECREF(argv_seq);
+ goto compile_error;
+ }
+ argv[i] = PyBytes_AS_STRING(encoded[i]);
+ }
+ argv[i] = NULL;
+ }
+ }
+
+ pthread_mutex_lock(&compile_lock);
+ prog = dtrace_program_strcompile(self->dtp, source,
+ (dtrace_probespec_t)spec, cflags,
+ (int)argc, argv);
+ pthread_mutex_unlock(&compile_lock);
+
+ Py_XDECREF(argv_seq);
+
+ if (prog == NULL) {
+ for (Py_ssize_t i = 0; i < argc; i++)
+ Py_XDECREF(encoded[i]);
+ PyMem_Free(encoded);
+ PyMem_Free(argv);
+ return raise_dtrace_error(self, "dtrace_program_strcompile");
+ }
+
+ wrapper = (PyDTraceProgram *)PyObject_CallObject(
+ (PyObject *)&PyDTraceProgramType, NULL);
+ if (wrapper == NULL) {
+ for (Py_ssize_t i = 0; i < argc; i++)
+ Py_XDECREF(encoded[i]);
+ PyMem_Free(encoded);
+ PyMem_Free(argv);
+ return NULL;
+ }
+
+ Py_INCREF(self);
+ wrapper->session = self;
+ wrapper->prog = prog;
+
+ for (Py_ssize_t i = 0; i < argc; i++)
+ Py_XDECREF(encoded[i]);
+ PyMem_Free(encoded);
+ PyMem_Free(argv);
+
+ return (PyObject *)wrapper;
+
+compile_error:
+ for (Py_ssize_t i = 0; i < argc; i++)
+ Py_XDECREF(encoded[i]);
+ PyMem_Free(encoded);
+ PyMem_Free(argv);
+ return NULL;
+}
+
+static PyObject *
+PyDTraceSession_enable(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"program", NULL};
+ PyDTraceProgram *program = NULL;
+ dtrace_proginfo_t info;
+ PyObject *result = NULL;
+ PyObject *attr = NULL;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist,
+ &PyDTraceProgramType, &program))
+ return NULL;
+
+ if (program->session != self) {
+ PyErr_SetString(PyExc_ValueError,
+ "program was created by a different session");
+ return NULL;
+ }
+
+ memset(&info, 0, sizeof(info));
+
+ if (dtrace_program_exec(self->dtp, program->prog, &info) != 0)
+ return raise_dtrace_error(self, "dtrace_program_exec");
+
+ result = PyDict_New();
+ if (result == NULL)
+ return NULL;
+
+ if (dict_set_ulong(result, "aggregations", info.dpi_aggregates) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "recgens", info.dpi_recgens) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "matches", info.dpi_matches) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "speculations", info.dpi_speculations) < 0)
+ goto enable_error;
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "name", info.dpi_descattr.dtat_name) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "data", info.dpi_descattr.dtat_data) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "class", info.dpi_descattr.dtat_class) < 0)
+ goto enable_error;
+
+ if (PyDict_SetItemString(result, "descattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "name", info.dpi_stmtattr.dtat_name) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "data", info.dpi_stmtattr.dtat_data) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "class", info.dpi_stmtattr.dtat_class) < 0)
+ goto enable_error;
+
+ if (PyDict_SetItemString(result, "stmtattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ return result;
+
+enable_error:
+ Py_XDECREF(attr);
+ Py_DECREF(result);
+ return NULL;
+}
+
+static PyObject *
+PyDTraceSession_go(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"cflags", NULL};
+ unsigned int cflags = 0;
+ int rc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|I", kwlist, &cflags))
+ return NULL;
+
+ Py_BEGIN_ALLOW_THREADS rc = dtrace_go(self->dtp, cflags);
+ Py_END_ALLOW_THREADS
+
+ if (rc != 0) return raise_dtrace_error(self, "dtrace_go");
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_stop(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (dtrace_stop(self->dtp) != 0)
+ return raise_dtrace_error(self, "dtrace_stop");
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_update(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (dtrace_update(self->dtp) != 0)
+ return raise_dtrace_error(self, "dtrace_update");
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_status(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ int status = dtrace_status(self->dtp);
+ if (status == DTRACE_STATUS_ERROR)
+ return raise_dtrace_error(self, "dtrace_status");
+ /*
+ * We maintain some local status too for forced exit (see consume_rec()
+ * below)
+ */
+ if (status == DTRACE_STATUS_OKAY && self->status)
+ status = self->status;
+ return PyLong_FromLong(status);
+}
+
+static int
+consume_rec(const dtrace_probedata_t *data, const dtrace_recdesc_t *rec,
+ void *arg)
+{
+ work_ctx_t *ctx = arg;
+ PyDTraceSession *self = ctx->session;
+ dtrace_actkind_t act;
+ uintptr_t addr;
+
+ if (!rec)
+ return DTRACE_CONSUME_NEXT;
+
+ act = rec->dtrd_action;
+ addr = (uintptr_t)data->dtpda_data;
+
+ switch (act) {
+ case DTRACEACT_EXIT:
+ self->exit_status = *(uint32_t *)addr;
+ self->status = DTRACE_STATUS_EXITED;
+ return DTRACE_CONSUME_NEXT;
+ default:
+ if (!ctx->capture || ctx->current_probe == NULL)
+ return DTRACE_CONSUME_THIS;
+
+ PyObject *record = PyDict_New();
+ PyObject *records = ctx->records;
+
+ if (record == NULL || records == NULL)
+ goto error;
+
+ PyObject *action = PyLong_FromUnsignedLong(act);
+ PyObject *size = PyLong_FromUnsignedLong(rec->dtrd_size);
+ PyObject *offset = PyLong_FromUnsignedLong(rec->dtrd_offset);
+ PyObject *alignment =
+ PyLong_FromUnsignedLong(rec->dtrd_alignment);
+ PyObject *arg_obj = PyLong_FromUnsignedLongLong(rec->dtrd_arg);
+ PyObject *raw = PyBytes_FromStringAndSize(
+ (const char *)(data->dtpda_data + rec->dtrd_offset),
+ rec->dtrd_size);
+
+ if (action == NULL || size == NULL || offset == NULL ||
+ alignment == NULL || arg_obj == NULL || raw == NULL)
+ goto error_record;
+
+ if (PyDict_SetItemString(record, "action", action) < 0)
+ goto error_record;
+ if (PyDict_SetItemString(record, "size", size) < 0)
+ goto error_record;
+ if (PyDict_SetItemString(record, "offset", offset) < 0)
+ goto error_record;
+ if (PyDict_SetItemString(record, "alignment", alignment) < 0)
+ goto error_record;
+ if (PyDict_SetItemString(record, "arg", arg_obj) < 0)
+ goto error_record;
+ if (PyDict_SetItemString(record, "data", raw) < 0)
+ goto error_record;
+
+ Py_DECREF(action);
+ Py_DECREF(size);
+ Py_DECREF(offset);
+ Py_DECREF(alignment);
+ Py_DECREF(arg_obj);
+ Py_DECREF(raw);
+
+ if (PyList_Append(records, record) < 0)
+ goto error_record;
+ Py_DECREF(record);
+
+ return DTRACE_CONSUME_THIS;
+
+ error_record:
+ Py_XDECREF(action);
+ Py_XDECREF(size);
+ Py_XDECREF(offset);
+ Py_XDECREF(alignment);
+ Py_XDECREF(arg_obj);
+ Py_XDECREF(raw);
+ Py_XDECREF(record);
+
+ error:
+ ctx->aborted = 1;
+ return DTRACE_CONSUME_ABORT;
+ }
+}
+
+static int
+consume_probe(const dtrace_probedata_t *data, void *arg)
+{
+ work_ctx_t *ctx = arg;
+
+ if (!ctx->capture)
+ return DTRACE_CONSUME_THIS;
+
+ PyObject *probe = PyDict_New();
+ PyObject *records = PyList_New(0);
+ PyObject *stid =
+ PyLong_FromUnsignedLong((unsigned long)data->dtpda_stid);
+ PyObject *cpu = PyLong_FromUnsignedLong(data->dtpda_cpu);
+ PyObject *flow = PyLong_FromLong(data->dtpda_flow);
+ PyObject *indent = PyLong_FromLong(data->dtpda_indent);
+ PyObject *prefix = NULL;
+
+ if (probe == NULL || records == NULL || stid == NULL || cpu == NULL ||
+ flow == NULL || indent == NULL)
+ goto error;
+
+ if (PyDict_SetItemString(probe, "stid", stid) < 0)
+ goto error;
+ if (PyDict_SetItemString(probe, "cpu", cpu) < 0)
+ goto error;
+ if (PyDict_SetItemString(probe, "flow", flow) < 0)
+ goto error;
+ if (PyDict_SetItemString(probe, "indent", indent) < 0)
+ goto error;
+ if (PyDict_SetItemString(probe, "records", records) < 0)
+ goto error;
+
+ if (data->dtpda_prefix != NULL) {
+ prefix = PyUnicode_FromString(data->dtpda_prefix);
+ if (prefix == NULL)
+ goto error;
+ if (PyDict_SetItemString(probe, "prefix", prefix) < 0)
+ goto error;
+ }
+
+ if (data->dtpda_ddesc != NULL) {
+ PyObject *argc = PyLong_FromLong(data->dtpda_ddesc->dtdd_nrecs);
+ PyObject *user_data = PyLong_FromUnsignedLongLong(
+ data->dtpda_ddesc->dtdd_uarg);
+
+ if (argc == NULL || user_data == NULL)
+ goto error;
+ if (PyDict_SetItemString(probe, "argc", argc) < 0) {
+ Py_DECREF(argc);
+ Py_DECREF(user_data);
+ goto error;
+ }
+ Py_DECREF(argc);
+ if (PyDict_SetItemString(probe, "user_data", user_data) < 0) {
+ Py_DECREF(user_data);
+ goto error;
+ }
+ Py_DECREF(user_data);
+ }
+
+ if (data->dtpda_pdesc != NULL) {
+ const dtrace_probedesc_t *pdesc = data->dtpda_pdesc;
+ PyObject *info = PyDict_New();
+ PyObject *id = PyLong_FromUnsignedLong(pdesc->id);
+
+ if (info == NULL || id == NULL)
+ goto error;
+ if (PyDict_SetItemString(info, "id", id) < 0) {
+ Py_DECREF(id);
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(id);
+
+ if (pdesc->prv != NULL) {
+ PyObject *prv = PyUnicode_FromString(pdesc->prv);
+ if (prv == NULL) {
+ Py_DECREF(info);
+ goto error;
+ }
+ if (PyDict_SetItemString(info, "provider", prv) < 0) {
+ Py_DECREF(prv);
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(prv);
+ }
+
+ if (pdesc->mod != NULL) {
+ PyObject *mod = PyUnicode_FromString(pdesc->mod);
+ if (mod == NULL) {
+ Py_DECREF(info);
+ goto error;
+ }
+ if (PyDict_SetItemString(info, "module", mod) < 0) {
+ Py_DECREF(mod);
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(mod);
+ }
+
+ if (pdesc->fun != NULL) {
+ PyObject *fun = PyUnicode_FromString(pdesc->fun);
+ if (fun == NULL) {
+ Py_DECREF(info);
+ goto error;
+ }
+ if (PyDict_SetItemString(info, "function", fun) < 0) {
+ Py_DECREF(fun);
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(fun);
+ }
+
+ if (pdesc->prb != NULL) {
+ PyObject *prb = PyUnicode_FromString(pdesc->prb);
+ if (prb == NULL) {
+ Py_DECREF(info);
+ goto error;
+ }
+ if (PyDict_SetItemString(info, "name", prb) < 0) {
+ Py_DECREF(prb);
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(prb);
+ }
+
+ if (PyDict_SetItemString(probe, "probe", info) < 0) {
+ Py_DECREF(info);
+ goto error;
+ }
+ Py_DECREF(info);
+ }
+
+ if (PyList_Append(ctx->probes, probe) < 0)
+ goto error;
+
+ ctx->current_probe = probe;
+ ctx->records = records;
+
+ Py_DECREF(stid);
+ Py_DECREF(cpu);
+ Py_DECREF(flow);
+ Py_DECREF(indent);
+ Py_DECREF(records);
+ Py_XDECREF(prefix);
+ Py_DECREF(probe);
+
+ return DTRACE_CONSUME_THIS;
+
+error:
+ ctx->aborted = 1;
+ Py_XDECREF(probe);
+ Py_XDECREF(records);
+ Py_XDECREF(stid);
+ Py_XDECREF(cpu);
+ Py_XDECREF(flow);
+ Py_XDECREF(indent);
+ Py_XDECREF(prefix);
+ return DTRACE_CONSUME_ABORT;
+}
+
+static PyObject *
+PyDTraceSession_work(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"return_records", NULL};
+ PyObject *captures = Py_False;
+ work_ctx_t ctx = {0};
+ PyObject *status_obj = NULL;
+ PyObject *result = NULL;
+ int status;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &captures))
+ return NULL;
+
+ ctx.capture = PyObject_IsTrue(captures);
+ if (ctx.capture < 0)
+ return NULL;
+
+ ctx.session = self;
+
+ if (ctx.capture) {
+ ctx.probes = PyList_New(0);
+ if (ctx.probes == NULL)
+ return NULL;
+ }
+
+ status = dtrace_work(self->dtp, self->fp, consume_probe, consume_rec,
+ &ctx);
+
+ if (status == DTRACE_WORKSTATUS_ERROR || (ctx.capture && ctx.aborted)) {
+ Py_XDECREF(ctx.probes);
+ return raise_dtrace_error(self, "dtrace_work");
+ }
+
+ if (status == DTRACE_STATUS_OKAY && self->pids > 0 &&
+ self->pids_live == 0)
+ status = DTRACE_STATUS_EXITED;
+
+ status_obj = PyLong_FromLong(status);
+ if (status_obj == NULL) {
+ Py_XDECREF(ctx.probes);
+ return NULL;
+ }
+
+ if (ctx.probes == NULL) {
+ ctx.probes = PyList_New(0);
+ if (ctx.probes == NULL) {
+ Py_DECREF(status_obj);
+ return NULL;
+ }
+ }
+
+ result = PyTuple_Pack(2, status_obj, ctx.probes);
+ Py_DECREF(status_obj);
+ Py_DECREF(ctx.probes);
+ return result;
+}
+
+static PyObject *
+PyDTraceSession_agg_snap(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (dtrace_aggregate_snap(self->dtp) != 0)
+ return raise_dtrace_error(self, "dtrace_aggregate_snap");
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_agg_clear(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ dtrace_aggregate_clear(self->dtp);
+ Py_RETURN_NONE;
+}
+
+static dtrace_aggregate_walk_f *
+resolve_agg_walk(const char *mode)
+{
+ if (mode == NULL || strcmp(mode, "default") == 0)
+ return dtrace_aggregate_walk;
+ if (strcmp(mode, "keys") == 0)
+ return dtrace_aggregate_walk_keysorted;
+ if (strcmp(mode, "values") == 0)
+ return dtrace_aggregate_walk_valsorted;
+ if (strcmp(mode, "keyrev") == 0)
+ return dtrace_aggregate_walk_keyrevsorted;
+ if (strcmp(mode, "valrev") == 0)
+ return dtrace_aggregate_walk_valrevsorted;
+ if (strcmp(mode, "keyvar") == 0)
+ return dtrace_aggregate_walk_keyvarsorted;
+ if (strcmp(mode, "valvar") == 0)
+ return dtrace_aggregate_walk_valvarsorted;
+ if (strcmp(mode, "keyvarrev") == 0)
+ return dtrace_aggregate_walk_keyvarrevsorted;
+ if (strcmp(mode, "valvarrev") == 0)
+ return dtrace_aggregate_walk_valvarrevsorted;
+ return NULL;
+}
+
+static PyObject *
+PyDTraceSession_agg_walk(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"mode", NULL};
+ const char *mode = "values";
+ dtrace_aggregate_walk_f *walker = NULL;
+ agg_walk_ctx_t ctx;
+ int rc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", kwlist, &mode))
+ return NULL;
+
+ walker = resolve_agg_walk(mode);
+ if (walker == NULL) {
+ PyErr_SetString(PyExc_ValueError,
+ "unknown aggregation walk mode");
+ return NULL;
+ }
+
+ ctx.list = PyList_New(0);
+ if (ctx.list == NULL)
+ return NULL;
+ ctx.session = self;
+
+ rc = walker(self->dtp, agg_walk_callback, &ctx);
+ if (rc != 0) {
+ Py_DECREF(ctx.list);
+ return raise_dtrace_error(self, "dtrace_aggregate_walk");
+ }
+
+ return ctx.list;
+}
+
+/* ------------------------------------------------------------------------- */
+/* PyDTraceProc */
+/* ------------------------------------------------------------------------- */
+
+static PyObject *
+PyDTraceProc_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ UNUSED(args);
+ UNUSED(kwds);
+ PyDTraceProc *self = (PyDTraceProc *)type->tp_alloc(type, 0);
+ if (self != NULL) {
+ self->session = NULL;
+ self->proc = NULL;
+ }
+ return (PyObject *)self;
+}
+
+static void
+PyDTraceProc_dealloc(PyDTraceProc *self)
+{
+ if (self->session) {
+ if (self->proc && self->session->dtp)
+ dtrace_proc_release(self->session->dtp, self->proc);
+
+ Py_XDECREF(self->session);
+ }
+ PyObject_Del(self);
+}
+
+static PyObject *
+PyDTraceProc_getpid(PyDTraceProc *self, PyObject *Py_UNUSED(args))
+{
+ int pid;
+
+ if (self->session == NULL || self->session->closed ||
+ self->session->dtp == NULL || self->proc == NULL) {
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is released");
+ return NULL;
+ }
+
+ pid = dtrace_proc_getpid(self->session->dtp, self->proc);
+
+ return PyLong_FromLong(pid);
+}
+
+static PyMethodDef PyDTraceProc_methods[] = {
+ {"getpid", (PyCFunction)PyDTraceProc_getpid, METH_NOARGS, "Get pid."},
+ {NULL, NULL, 0, NULL}};
+
+static PyTypeObject PyDTraceProcType = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "dtrace.DTraceProc",
+ .tp_basicsize = sizeof(PyDTraceProc),
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = PyDTraceProc_new,
+ .tp_dealloc = (destructor)PyDTraceProc_dealloc,
+ .tp_methods = PyDTraceProc_methods,
+};
+
+#define PROC_CREATE_ARGC_MAX 32
+
+static PyObject *
+PyDTraceSession_proc_create(PyDTraceSession *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *kwlist[] = {"args", NULL};
+ PyObject *arglist = NULL;
+ PyObject *encoded[PROC_CREATE_ARGC_MAX] = {};
+ char *argvlist[PROC_CREATE_ARGC_MAX + 1];
+ struct dtrace_proc *proc = NULL;
+ PyDTraceProc *retproc;
+ PyObject *ret = NULL;
+ Py_ssize_t i, size;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, &PyList_Type,
+ &arglist))
+ return NULL;
+ size = PyList_Size(arglist);
+ if (size < 1) {
+ PyErr_SetString(PyExc_ValueError,
+ "empty arglist for proc_create");
+ return NULL;
+ }
+ if (size >= PROC_CREATE_ARGC_MAX) {
+ PyErr_SetString(PyExc_ValueError,
+ "too many arguments for proc_create");
+ return NULL;
+ }
+ for (i = 0; i < size; i++) {
+ PyObject *item = PyList_GetItem(arglist, i);
+
+ if (!PyUnicode_Check(item)) {
+ PyErr_SetString(PyExc_ValueError,
+ "non-string in argument list");
+ goto error;
+ }
+ encoded[i] = PyUnicode_AsUTF8String(item);
+ if (encoded[i] == NULL)
+ goto error;
+ argvlist[i] = PyBytes_AS_STRING(encoded[i]);
+ }
+ argvlist[i] = NULL;
+ proc = dtrace_proc_create(self->dtp, argvlist[0], argvlist, 0);
+ if (proc == NULL)
+ goto error_raise;
+
+ ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
+ if (ret == NULL) {
+ dtrace_proc_release(self->dtp, proc);
+ goto error;
+ }
+ self->pids++;
+ retproc = (PyDTraceProc *)ret;
+ retproc->session = self;
+ Py_INCREF(self);
+ retproc->proc = proc;
+ for (i = 0; i < size; i++)
+ Py_DECREF(encoded[i]);
+
+ return ret;
+
+error_raise:
+ ret = raise_dtrace_error(self, "dtrace_proc_create");
+error:
+ while (i-- > 0)
+ Py_XDECREF(encoded[i]);
+ return ret;
+}
+
+static PyObject *
+PyDTraceSession_proc_grab_pid(PyDTraceSession *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *kwlist[] = {"pid", NULL};
+ struct dtrace_proc *proc = NULL;
+ PyDTraceProc *retproc;
+ PyObject *ret;
+ int pid;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "i", kwlist, &pid))
+ return NULL;
+
+ proc = dtrace_proc_grab_pid(self->dtp, pid, 0);
+ if (!proc)
+ return raise_dtrace_error(self, "dtrace_proc_grab_pid");
+ ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
+ if (ret == NULL) {
+ dtrace_proc_release(self->dtp, proc);
+ return ret;
+ }
+ self->pids++;
+ retproc = (PyDTraceProc *)ret;
+ retproc->session = self;
+ Py_INCREF(self);
+ retproc->proc = proc;
+
+ return ret;
+}
+
+static PyObject *
+PyDTraceSession_proc_continue(PyDTraceSession *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *kwlist[] = {"proc", NULL};
+ PyDTraceProc *proc = NULL;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist,
+ &PyDTraceProcType, &proc))
+ return NULL;
+ if (proc->session != self) {
+ PyErr_SetString(PyExc_ValueError,
+ "proc was created by a different session");
+ return NULL;
+ }
+
+ if (proc->proc == NULL) {
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is released");
+ return NULL;
+ }
+
+ dtrace_proc_continue(self->dtp, proc->proc);
+ self->pids_live++;
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_proc_release(PyDTraceSession *self, PyObject *args,
+ PyObject *kwds)
+{
+ static char *kwlist[] = {"proc", NULL};
+ PyDTraceProc *proc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist,
+ &PyDTraceProcType, &proc))
+ return NULL;
+
+ if (proc->session != self) {
+ PyErr_SetString(PyExc_ValueError,
+ "proc was created by a different session");
+ return NULL;
+ }
+
+ if (proc->proc == NULL) {
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is already released");
+ return NULL;
+ }
+
+ dtrace_proc_release(self->dtp, proc->proc);
+ proc->proc = NULL;
+
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef PyDTraceSession_methods[] = {
+ {"close", (PyCFunction)PyDTraceSession_close, METH_NOARGS,
+ "Close the session."},
+ {"__enter__", (PyCFunction)PyDTraceSession_enter, METH_NOARGS,
+ "Context manager entry."},
+ {"__exit__", (PyCFunction)PyDTraceSession_exit, METH_VARARGS,
+ "Context manager exit."},
+ {"setopt", (PyCFunction)PyDTraceSession_setopt,
+ METH_VARARGS | METH_KEYWORDS, "Set a DTrace option."},
+ {"compile", (PyCFunction)PyDTraceSession_compile,
+ METH_VARARGS | METH_KEYWORDS, "Compile a program from a string."},
+ {"enable", (PyCFunction)PyDTraceSession_enable,
+ METH_VARARGS | METH_KEYWORDS, "Enable a compiled program."},
+ {"go", (PyCFunction)PyDTraceSession_go, METH_VARARGS | METH_KEYWORDS,
+ "Start tracing."},
+ {"stop", (PyCFunction)PyDTraceSession_stop, METH_NOARGS,
+ "Stop tracing."},
+ {"update", (PyCFunction)PyDTraceSession_update, METH_NOARGS,
+ "Update module cache."},
+ {"status", (PyCFunction)PyDTraceSession_status, METH_NOARGS,
+ "Retrieve tracing status."},
+ {"work", (PyCFunction)PyDTraceSession_work,
+ METH_VARARGS | METH_KEYWORDS, "Do consumer loop work."},
+ {"agg_snap", (PyCFunction)PyDTraceSession_agg_snap, METH_NOARGS,
+ "Snapshot aggregation buffers."},
+ {"agg_clear", (PyCFunction)PyDTraceSession_agg_clear, METH_NOARGS,
+ "Clear aggregation buffers."},
+ {"agg_walk", (PyCFunction)PyDTraceSession_agg_walk,
+ METH_VARARGS | METH_KEYWORDS, "Return aggregation results."},
+ {"proc_create", (PyCFunction)PyDTraceSession_proc_create,
+ METH_VARARGS | METH_KEYWORDS, "Create process with args for tracing."},
+ {"proc_grab_pid", (PyCFunction)PyDTraceSession_proc_grab_pid,
+ METH_VARARGS | METH_KEYWORDS, "Grab pid."},
+ {"proc_continue", (PyCFunction)PyDTraceSession_proc_continue,
+ METH_VARARGS | METH_KEYWORDS, "Continue proc execution."},
+ {"proc_release", (PyCFunction)PyDTraceSession_proc_release,
+ METH_VARARGS | METH_KEYWORDS, "Relase proc."},
+ {NULL, NULL, 0, NULL}};
+
+static PyTypeObject PyDTraceSessionType = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "dtrace.DTraceSession",
+ .tp_basicsize = sizeof(PyDTraceSession),
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = PyDTraceSession_new,
+ .tp_init = (initproc)PyDTraceSession_init,
+ .tp_dealloc = (destructor)PyDTraceSession_dealloc,
+ .tp_methods = PyDTraceSession_methods,
+};
+
+/* ------------------------------------------------------------------------- */
+/* Module definition */
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef pydtrace_module = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "dtrace",
+ .m_doc = "Python bindings for libdtrace.",
+ .m_size = -1,
+};
+
+PyMODINIT_FUNC
+PyInit_dtrace(void)
+{
+ PyObject *m;
+
+ if (PyType_Ready(&PyDTraceSessionType) < 0)
+ return NULL;
+ if (PyType_Ready(&PyDTraceProgramType) < 0)
+ return NULL;
+ if (PyType_Ready(&PyDTraceProcType) < 0)
+ return NULL;
+
+ m = PyModule_Create(&pydtrace_module);
+ if (m == NULL)
+ return NULL;
+
+ PyExc_DTraceError = PyErr_NewException("dtrace.DTraceError",
+ PyExc_RuntimeError, NULL);
+ if (PyExc_DTraceError == NULL) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceSessionType);
+ if (PyModule_AddObject(m, "DTraceSession",
+ (PyObject *)&PyDTraceSessionType) < 0) {
+ Py_DECREF(&PyDTraceSessionType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceProgramType);
+ if (PyModule_AddObject(m, "DTraceProgram",
+ (PyObject *)&PyDTraceProgramType) < 0) {
+ Py_DECREF(&PyDTraceProgramType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceProcType);
+ if (PyModule_AddObject(m, "DTraceProc", (PyObject *)&PyDTraceProcType) <
+ 0) {
+ Py_DECREF(&PyDTraceProcType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(PyExc_DTraceError);
+ if (PyModule_AddObject(m, "DTraceError", PyExc_DTraceError) < 0) {
+ Py_DECREF(PyExc_DTraceError);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_VERSION", DTRACE_VERSION) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_C_CPP", DTRACE_C_CPP) < 0 ||
+ PyModule_AddIntConstant(m, "DTRACE_C_ZDEFS", DTRACE_C_ZDEFS) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_NONE",
+ DTRACE_STATUS_NONE) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_OKAY",
+ DTRACE_STATUS_OKAY) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_EXITED",
+ DTRACE_STATUS_EXITED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_FILLED",
+ DTRACE_STATUS_FILLED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_STOPPED",
+ DTRACE_STATUS_STOPPED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_DONE",
+ DTRACE_WORKSTATUS_DONE) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_OKAY",
+ DTRACE_WORKSTATUS_OKAY) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ return m;
+}
diff --git a/configure b/configure
index 403b1a09..1927b17f 100755
--- a/configure
+++ b/configure
@@ -112,6 +112,7 @@ EOF
make help-overrides-header help-overrides-option
cat >&2 <<'EOF'
--with-systemd=[yes/no] Install the systemd unit files (default: yes)
+--with-python=[yes/no] Build the Python bindings (default: yes)
EOF
echo >&2
make help-overrides
@@ -199,6 +200,8 @@ for option in "$@"; do
--kernel-obj-suffix=*) write_make_var KERNELBLDNAME "$option";;
--with-systemd|--with-systemd=y*) write_make_var WITH_SYSTEMD "y";;
--with-systemd=n*|--without-systemd) write_make_var WITH_SYSTEMD "";;
+ --with-python|--with-python=y*) write_make_var WITH_PYTHON "y";;
+ --with-python=n*|--without-python) write_make_var WITH_PYTHON "";;
HAVE_ELF_GETSHDRSTRNDX=*) write_config_var ELF_GETSHDRSTRNDX "$option";;
--with-libctf=*) write_config_var LIBCTF "$option";;
HAVE_LIBCTF=*) write_config_var LIBCTF "$option";;
@@ -245,4 +248,3 @@ cat build/.config/*.h > build/config.h
cat build/.config/*.mk > build/config.mk
exit 0
-
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 5/9] dtrace.spec: add python bindings packaging
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (3 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 4/9] python: Add cpython bindings for libdtrace Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 6/9] runtest.sh: Export PYTHONPATH when running tests in-tree Alan Maguire
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Version is derived from libdtrace version.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
dtrace.spec | 29 +++++++++++++++++++++++++++--
1 file changed, 27 insertions(+), 2 deletions(-)
diff --git a/dtrace.spec b/dtrace.spec
index d72baf9d..a16cb62a 100644
--- a/dtrace.spec
+++ b/dtrace.spec
@@ -40,12 +40,18 @@
# Build DTrace without LTO.
%global _lto_cflags %{nil}
+%bcond_without python
+
BuildRequires: rpm
Name: dtrace
License: Universal Permissive License (UPL), Version 1.0
Group: Development/Tools
Requires: cpp elfutils-libelf zlib libpcap libpfm
BuildRequires: glibc-headers bison flex zlib-devel elfutils-libelf-devel
+
+%if %{with python}
+BuildRequires: python3-devel python3-setuptools
+%endif
BuildRequires: systemd systemd-devel glibc-static %{glibc32} wireshark
BuildRequires: libpcap-devel valgrind-devel libpfm-devel libbpf-devel
%if "%{?dist}" == ".el7"
@@ -128,6 +134,18 @@ Group: Development/System
%description testsuite
The DTrace testsuite.
+%if %{with python}
+%package -n python3-dtrace
+Summary: Python bindings for libdtrace
+Requires: python3
+Requires: %{name}%{?_isa} = %{version}-%{release}
+Group: Development/Libraries
+
+%description -n python3-dtrace
+Python extension module providing access to libdtrace.
+
+%endif
+
Installed in /usr/lib64/dtrace/testsuite.
'make check' here is just like 'make check' in the source tree, except that
@@ -137,7 +155,7 @@ it always tests the installed DTrace.
%setup -q
%build
-make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2}
+make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2} %{?with_python:WITH_PYTHON=y PYTHON=%{__python3}}
# Force off debuginfo splitting. We have no debuginfo in dtrace proper,
# and the testsuite requires debuginfo for proper operation.
@@ -152,7 +170,7 @@ make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2}
mkdir -p $RPM_BUILD_ROOT/usr/sbin
make DESTDIR=$RPM_BUILD_ROOT VERSION=%{version} \
HDRPREFIX="$RPM_BUILD_ROOT/usr/include" \
- install install-test
+ install install-test %{?with_python:install-python} PYTHON=%{__python3}
%if "%{?dist}" == ".el7"
sed -i '/^ProtectSystem=/d; /^ProtectControlGroups=/d; /^RuntimeDirectory/d;' $RPM_BUILD_ROOT/usr/lib/systemd/system/dtprobed.service
@@ -230,6 +248,13 @@ systemctl start dtprobed || :
%{_includedir}/sys/dtrace.h
%{_includedir}/sys/dtrace_types.h
+
+%if %{with python}
+%files -n python3-dtrace
+%defattr(-,root,root,-)
+%{python3_sitearch}/dtrace*.so
+%doc bindings/python/README.md
+%endif
%files testsuite
%defattr(-,root,root,-)
%{_libdir}/dtrace/testsuite
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 6/9] runtest.sh: Export PYTHONPATH when running tests in-tree
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (4 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 5/9] dtrace.spec: add python bindings packaging Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 7/9] test: add tests for python bindings Alan Maguire
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
This will allow us to test python bindings when built, but not
installed.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
runtest.sh | 2 ++
1 file changed, 2 insertions(+)
diff --git a/runtest.sh b/runtest.sh
index 9f06a499..c0ccffc6 100755
--- a/runtest.sh
+++ b/runtest.sh
@@ -552,6 +552,7 @@ export LANGUAGE=C
if [[ -z $USE_INSTALLED ]]; then
dtrace="$(pwd)/build*/dtrace"
+ PYTHONPATH="$(pwd)/build/bindings/python${PYTHONPATH:+:$PYTHONPATH}"
test_libdir="$(pwd)/build/dlibs"
test_ldflags="-L$(pwd)/build"
test_cppflags="-I$(pwd)/include -I$(pwd)/uts/common -I$(pwd)/build -I$(pwd)/libdtrace -DARCH_$arch"
@@ -562,6 +563,7 @@ if [[ -z $USE_INSTALLED ]]; then
dtprobed_flags="-n $helper_device -s${tmpdir}/run/dtrace -F"
export DTRACE_DOF_INIT_DEVNAME="/dev/$helper_device"
export DTRACE_OPT_DOFSTASHPATH="${tmpdir}/run/dtrace"
+ export PYTHONPATH
if [[ -z $(eval echo $dtrace) ]]; then
echo "No dtraces available." >&2
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 7/9] test: add tests for python bindings
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (5 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 6/9] runtest.sh: Export PYTHONPATH when running tests in-tree Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 8/9] dtrace: Add a PCP PMDA to expose DTrace data as metrics Alan Maguire
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Add tests covering aggregation snapshot, update, handling of
stack keys. Also test process creation/grabbing, and ensure
we detect processes going away or exit() calls in dt.work().
Also ensure that multiple sessions can exist in the process
image, and that closing a handle does not effect operation of
another handle in the same process.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
test/unittest/python/tst.aggr-actions.sh | 133 ++++++++++++++++++
test/unittest/python/tst.aggr-change.sh | 75 ++++++++++
test/unittest/python/tst.aggr-stack.sh | 61 ++++++++
test/unittest/python/tst.aggr.sh | 58 ++++++++
test/unittest/python/tst.exit.sh | 64 +++++++++
test/unittest/python/tst.multi-session-end.sh | 75 ++++++++++
test/unittest/python/tst.multi-session.sh | 58 ++++++++
test/unittest/python/tst.proc-create.sh | 56 ++++++++
test/unittest/python/tst.proc-exit.sh | 57 ++++++++
test/unittest/python/tst.proc-grab.sh | 81 +++++++++++
10 files changed, 718 insertions(+)
create mode 100755 test/unittest/python/tst.aggr-actions.sh
create mode 100755 test/unittest/python/tst.aggr-change.sh
create mode 100755 test/unittest/python/tst.aggr-stack.sh
create mode 100755 test/unittest/python/tst.aggr.sh
create mode 100755 test/unittest/python/tst.exit.sh
create mode 100755 test/unittest/python/tst.multi-session-end.sh
create mode 100755 test/unittest/python/tst.multi-session.sh
create mode 100755 test/unittest/python/tst.proc-create.sh
create mode 100755 test/unittest/python/tst.proc-exit.sh
create mode 100755 test/unittest/python/tst.proc-grab.sh
diff --git a/test/unittest/python/tst.aggr-actions.sh b/test/unittest/python/tst.aggr-actions.sh
new file mode 100755
index 00000000..8fd22c8c
--- /dev/null
+++ b/test/unittest/python/tst.aggr-actions.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure aggregation actions return expected values.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession
+
+prog = r"""
+BEGIN
+{
+ @counts = count();
+ @counts = count();
+ @counts = count();
+ @sums = sum(100);
+ @sums = sum(50);
+ @sums = sum(32);
+ @avgs = avg(4);
+ @avgs = avg(5);
+ @mins = min(100);
+ @mins = min(99);
+ @mins = min(101);
+ @maxs = max(12);
+ @maxs = max(19);
+ @maxs = max(-4);
+ @stddevs = stddev(5000000000);
+ @stddevs = stddev(5000000100);
+ @stddevs = stddev(5000000200);
+ @stddevs = stddev(5000000300);
+ @stddevs = stddev(5000000400);
+ @stddevs = stddev(5000000500);
+ @stddevs = stddev(5000000600);
+ @stddevs = stddev(5000000700);
+ @stddevs = stddev(5000000800);
+ @stddevs = stddev(5000000900);
+ @quants = quantize(1);
+ @quants = quantize(2);
+ @quants = quantize(1);
+ @quants = quantize(3);
+ @quants = quantize(128);
+ @lquants = lquantize(1, 1, 10, 2);
+ @lquants = lquantize(3, 1, 10, 2);
+ @lquants = lquantize(1, 1, 10, 2);
+ @lquants = lquantize(9, 1, 10, 2);
+ @lquants = lquantize(9, 1, 10, 2);
+ @lquants = lquantize(1, 1, 10, 2);
+ @llquants = llquantize(1, 3, 1, 4, 3);
+ @llquants = llquantize(26, 3, 1, 4, 3);
+ @llquants = llquantize(2, 3, 1, 4, 3);
+ @llquants = llquantize(13, 3, 1, 4, 3);
+ @llquants = llquantize(6, 3, 1, 4, 3);
+ @llquants = llquantize(81, 3, 1, 4, 3);
+ exit(0);
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ dt.stop()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+for row in rows:
+ if row["name"] == "counts":
+ v = int(row["value"])
+ if v != 3:
+ raise SystemExit(f"unexpected count value {v}")
+ if row["name"] == "avgs":
+ v = float(row["value"])
+ if v != 4.5:
+ raise SystemExit(f"unexpected avg value {v}")
+ if row["name"] == "sums":
+ v = int(row["value"])
+ if v != 182:
+ raise SystemExit(f"unexpected sum value {v}")
+ if row["name"] == "mins":
+ v = int(row["value"])
+ if v != 99:
+ raise SystemExit(f"unexpected min value {v}")
+ if row["name"] == "maxs":
+ v = int(row["value"])
+ if v != 19:
+ raise SystemExit(f"unexpected max value {v}")
+ if row["name"] == "stddevs":
+ v = int(row["value"])
+ if v != 287:
+ raise SystemExit(f"unexecpted stddev value {v}")
+ if row["name"] == "quants":
+ v = row["value"]
+ if v != {1:2, 2:2, 128:1}:
+ raise SystemExit(f"unexecpted quantize value {v}")
+ if row["name"] == "lquants":
+ v = row["value"]
+ if v != {1:3, 3:1, 9:2}:
+ raise SystemExit(f"unexecpted lquantize value {v}")
+ if row["name"] == "llquants":
+ v = row["value"]
+ if v != {0:2, 9:1, 18:1, 27:1, 162:1}:
+ raise SystemExit(f"unexecpted llquantize value {v}")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.aggr-change.sh b/test/unittest/python/tst.aggr-change.sh
new file mode 100755
index 00000000..318e17ba
--- /dev/null
+++ b/test/unittest/python/tst.aggr-change.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure aggregation values are updated across dt.work() calls.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession
+
+prog = r"""
+syscall:::entry
+{
+ @counts["syscalls"] = count();
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ dt.work()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+ if not rows:
+ raise SystemExit("no rows returned")
+ entry = rows[0]
+ val1 = int(entry["value"])
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ dt.work()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+ if not rows:
+ raise SystemExit("no rows returned")
+ entry = rows[0]
+ val2 = int(entry["value"])
+ if val2 <= val1:
+ raise SystemExit("aggr value unchanged")
+ dt.stop()
+
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+entry = rows[0]
+if "keys" not in entry or "value" not in entry:
+ raise SystemExit("missing keys or value")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.aggr-stack.sh b/test/unittest/python/tst.aggr-stack.sh
new file mode 100755
index 00000000..a9259636
--- /dev/null
+++ b/test/unittest/python/tst.aggr-stack.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure aggregation can retrieve kernel stack key.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession
+
+prog = r"""
+syscall:::entry
+{
+ @counts[stack()] = count();
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ dt.stop()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+entry = rows[0]
+if "keys" not in entry or "value" not in entry:
+ raise SystemExit("missing keys or value")
+
+if len(entry["keys"][0]) < 2:
+ raise SystemExit("missing stack list")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.aggr.sh b/test/unittest/python/tst.aggr.sh
new file mode 100755
index 00000000..15c46f0c
--- /dev/null
+++ b/test/unittest/python/tst.aggr.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure aggregation can be retrieved from simple D script.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession
+
+prog = r"""
+syscall:::entry
+{
+ @counts[execname, probefunc] = count();
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ dt.stop()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+entry = rows[0]
+if "keys" not in entry or "value" not in entry:
+ raise SystemExit("missing keys or value")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.exit.sh b/test/unittest/python/tst.exit.sh
new file mode 100755
index 00000000..def6b0ec
--- /dev/null
+++ b/test/unittest/python/tst.exit.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure we see exit() from script with dt.status().
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession, DTRACE_STATUS_EXITED
+
+prog = r"""
+syscall:::entry
+{
+ c++;
+}
+
+syscall:::entry
+/ c > 1024 /
+{
+ exit(0);
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ exited = False
+ iters = 0
+ while iters < 10:
+ pid = Popen(["/bin/sleep", "1"]).wait()
+ if dt.status() == DTRACE_STATUS_EXITED:
+ exited = True
+ iters += 1
+ dt.stop()
+
+if exited != True:
+ raise SystemExit("missing DTRACE_STATUS_EXIT")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.multi-session-end.sh b/test/unittest/python/tst.multi-session-end.sh
new file mode 100755
index 00000000..dfc3503f
--- /dev/null
+++ b/test/unittest/python/tst.multi-session-end.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure END probes remain attached when another DTrace session in the same
+# process closes.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir/script.d" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+
+program = r'''
+END
+{
+ @ended = count();
+}
+'''
+
+
+def start_session():
+ session = DTraceSession()
+ program_handle = session.compile(program)
+ session.enable(program_handle)
+ session.go()
+ return session
+
+
+def assert_end_fired(session, survivor):
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+ for row in rows:
+ if row["name"] == "ended" and int(row["value"]) == 1:
+ return
+
+ raise SystemExit(f"END probe did not fire for surviving handle {survivor}")
+
+
+# Closing A must not remove B's shared END uprobe.
+handle_a = start_session()
+handle_b = start_session()
+try:
+ handle_a.close()
+ assert_end_fired(handle_b, "B")
+finally:
+ handle_b.close()
+
+# Closing B must not remove A's shared END uprobe.
+handle_a = start_session()
+handle_b = start_session()
+try:
+ handle_b.close()
+ assert_end_fired(handle_a, "A")
+finally:
+ handle_a.close()
+EOF
+
+chmod +x "$tmpdir/script.d"
+"$tmpdir/script.d"
diff --git a/test/unittest/python/tst.multi-session.sh b/test/unittest/python/tst.multi-session.sh
new file mode 100755
index 00000000..7389c126
--- /dev/null
+++ b/test/unittest/python/tst.multi-session.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure multiple sessions within same process image work.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession
+
+prog = r"""
+syscall::read:entry
+{
+ @counts["syscalls"] = count();
+}
+"""
+
+dts = {}
+max_sessions = 10
+
+try:
+ for i in range(max_sessions):
+ dts[i] = DTraceSession()
+ p = dts[i].compile(prog)
+ dts[i].enable(p)
+ dts[i].go()
+ dts[i].work()
+except Exception as exc:
+ raise SystemExit(f"got err with {i} multiple sessions: {exc}")
+finally:
+ for j in range(i):
+ dts[i].close()
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.proc-create.sh b/test/unittest/python/tst.proc-create.sh
new file mode 100755
index 00000000..e9d2d466
--- /dev/null
+++ b/test/unittest/python/tst.proc-create.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Validate tracing of a spawned command.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.py <<'EOF'
+#!/usr/bin/env python3
+from subprocess import Popen
+from dtrace import DTraceSession
+
+prog = r"""
+syscall::read:entry
+{
+ @calls[execname] = count();
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ proc = dt.proc_create(["/bin/echo", "hello"])
+ dt.proc_continue(proc)
+ dt.proc_release(proc)
+ dt.stop()
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no aggregation rows")
+
+names = {tuple(entry["keys"]) for entry in rows}
+if not any("echo" in key for key in names):
+ raise SystemExit("expected echo entry in aggregation")
+EOF
+
+chmod +x "$tmpdir/script.py"
+
+$tmpdir/script.py
diff --git a/test/unittest/python/tst.proc-exit.sh b/test/unittest/python/tst.proc-exit.sh
new file mode 100755
index 00000000..d4962f37
--- /dev/null
+++ b/test/unittest/python/tst.proc-exit.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure we see stopped state when traced process goes away.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir"/script.d <<EOF
+#!/usr/bin/env python3
+from pathlib import Path
+from subprocess import Popen
+
+from dtrace import DTraceSession, DTRACE_STATUS_STOPPED
+
+prog = r"""
+syscall:::entry
+{
+ c++;
+}
+"""
+
+with DTraceSession() as dt:
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ proc = dt.proc_create(["/bin/sleep", "5"])
+ dt.proc_continue(proc)
+ Popen(["/bin/sleep", "10"]).wait()
+ dt.work()
+ status = dt.status()
+ dt.proc_release(proc)
+ dt.stop()
+
+if status != DTRACE_STATUS_STOPPED:
+ raise SystemExit(f"missing DTRACE_STATUS_STOPPED: got {status}")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+$tmpdir/script.d
diff --git a/test/unittest/python/tst.proc-grab.sh b/test/unittest/python/tst.proc-grab.sh
new file mode 100755
index 00000000..21faca65
--- /dev/null
+++ b/test/unittest/python/tst.proc-grab.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure Python bindings can grab an existing process and trace its syscalls.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+dtrace=$1
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+# Start a long-lived helper shell to grab.
+sleep_inf="$tmpdir/sleep.sh"
+cat >"$sleep_inf" <<'EOF'
+#!/bin/bash
+while :; do
+ sleep 1
+done
+EOF
+chmod +x "$sleep_inf"
+"$sleep_inf" &
+helper=$!
+
+cat >"$tmpdir"/script.py <<'EOF'
+#!/usr/bin/env python3
+import os
+import sys
+from subprocess import Popen
+from dtrace import DTraceSession
+
+prog = r"""
+syscall:::entry
+/ pid == $target /
+{
+ @counts[execname, probefunc] = count();
+}
+"""
+
+pid = int(os.environ["TARGET_PID"])
+
+with DTraceSession() as dt:
+ proc = dt.proc_grab_pid(pid)
+ if proc.getpid() != pid:
+ raise SystemExit("grabbed PID mismatch")
+ p = dt.compile(prog)
+ dt.enable(p)
+ dt.go()
+ dt.proc_continue(proc)
+ Popen(["/bin/sleep", "5"]).wait()
+ dt.stop()
+ dt.proc_release(proc)
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+entry = rows[0]
+if "keys" not in entry or "value" not in entry:
+ raise SystemExit("missing keys or value")
+EOF
+
+chmod +x "$tmpdir/script.py"
+export TARGET_PID=$helper
+$tmpdir/script.py
+ret=$?
+
+kill $helper
+wait $helper 2>/dev/null || true
+exit $ret
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 8/9] dtrace: Add a PCP PMDA to expose DTrace data as metrics
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (6 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 7/9] test: add tests for python bindings Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-12 13:22 ` [PATCH v4 9/9] dtrace.spec: Add optional PCP PMDA packaging pcp-pmda-dtrace Alan Maguire
2026-08-13 21:01 ` [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Kris Van Hees
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
This PMDA exposes a way of running DTrace scripts through PCP via the
libdtrace Python bindings. Scripts can be registered dynamically via
`pmstore(1)` calls and optionally autostarted from an on-disk directory when
the PMDA is launched using the autostart.d/ directory.
To register and run a DTrace script to count read system calls:
$ pmstore dtrace.control.register '{"name":"read_syscalls","program":"syscall::read:entry { @c[probefunc] = count(); }", "autostart":"true"}'
Now we see the aggregation data exposed as the metric
`dtrace.scripts.data.read_syscalls.c`, with one instance per
aggregation key:
$ pminfo -f dtrace
dtrace.scripts.data.read_syscalls.c
inst [0 or "read"] value 9536
dtrace.scripts.runtime_seconds
inst [0 or "read_syscalls"] value 2
...
To stop, unregister
$ pmstore dtrace.control.stop read_syscalls
$ pmstore dtrace.control.unregister read_syscalls
By default, dynamic registration is restricted to root only;
other users can be added in dtrace.conf.
See pcp/README.md for details.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
pcp/Build | 56 ++
pcp/Install | 37 +
pcp/README.md | 260 ++++++
pcp/Remove | 27 +
pcp/autostart.d/.gitkeep | 0
pcp/dtrace.conf | 12 +
pcp/examples/packet_drop_reasons.d | 14 +
pcp/examples/packet_drop_reasons.json | 8 +
pcp/examples/syscall_counts.d | 11 +
pcp/examples/syscall_counts.json | 8 +
pcp/pmdadtrace.python | 1232 +++++++++++++++++++++++++
11 files changed, 1665 insertions(+)
create mode 100644 pcp/Build
create mode 100755 pcp/Install
create mode 100644 pcp/README.md
create mode 100755 pcp/Remove
create mode 100644 pcp/autostart.d/.gitkeep
create mode 100644 pcp/dtrace.conf
create mode 100644 pcp/examples/packet_drop_reasons.d
create mode 100644 pcp/examples/packet_drop_reasons.json
create mode 100644 pcp/examples/syscall_counts.d
create mode 100644 pcp/examples/syscall_counts.json
create mode 100755 pcp/pmdadtrace.python
diff --git a/pcp/Build b/pcp/Build
new file mode 100644
index 00000000..754db762
--- /dev/null
+++ b/pcp/Build
@@ -0,0 +1,56 @@
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+
+PMDA_NAME := dtrace
+PMDA_OBJDIR := $(DESTDIR)/var/lib/pcp/pmdas/$(PMDA_NAME)
+PMDA_AUTOSTART_OBJDIR := $(PMDA_OBJDIR)/autostart.d
+PMDA_EXAMPLE_OBJDIR := $(PMDA_OBJDIR)/examples
+
+PMDA_EXEC_SCRIPTS := Install Remove pmdadtrace.python
+PMDA_DATA_FILES := README.md dtrace.conf
+PMDA_AUTOSTART_SRC := $(wildcard pcp/autostart.d/*)
+PMDA_AUTOSTART_FILES := $(notdir $(PMDA_AUTOSTART_SRC))
+PMDA_EXAMPLE_SRC := $(wildcard pcp/examples/*)
+PMDA_EXAMPLE_FILES := $(notdir $(PMDA_EXAMPLE_SRC))
+
+PHONIES += install-pmda-$(PMDA_NAME) install-pmda
+
+install:: install-pmda
+
+install-pmda:: install-pmda-$(PMDA_NAME)
+
+install-pmda-$(PMDA_NAME)::
+ $(call describe-install-target,$(PMDA_OBJDIR),$(PMDA_EXEC_SCRIPTS) $(PMDA_DATA_FILES))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR)
+ for f in $(PMDA_EXEC_SCRIPTS); do \
+ install -m 755 pcp/$$f $(PMDA_OBJDIR); \
+ done
+ for f in $(PMDA_DATA_FILES); do \
+ install -m 644 pcp/$$f $(PMDA_OBJDIR); \
+ done
+ifneq ($(PMDA_AUTOSTART_FILES),)
+ $(call describe-install-target,$(PMDA_AUTOSTART_OBJDIR),$(PMDA_AUTOSTART_FILES))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR)
+ for f in $(PMDA_AUTOSTART_FILES); do \
+ mode=644; \
+ case $$f in \
+ *.d) mode=755 ;; \
+ *) ;; \
+ esac; \
+ install -m $$mode pcp/autostart.d/$$f $(PMDA_AUTOSTART_OBJDIR); \
+ done
+endif
+ifneq ($(PMDA_EXAMPLE_FILES),)
+ $(call describe-install-target,$(PMDA_EXAMPLE_OBJDIR),$(PMDA_EXAMPLE_FILES))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_EXAMPLE_OBJDIR)
+ for f in $(PMDA_EXAMPLE_FILES); do \
+ mode=644; \
+ case $$f in \
+ *.d) mode=755 ;; \
+ *) ;; \
+ esac; \
+ install -m $$mode pcp/examples/$$f $(PMDA_EXAMPLE_OBJDIR); \
+ done
+endif
diff --git a/pcp/Install b/pcp/Install
new file mode 100755
index 00000000..1103bbd2
--- /dev/null
+++ b/pcp/Install
@@ -0,0 +1,37 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+domain=487
+pmda_interface=2
+# Extend PMNS with the shipped namespace fragment
+pmns_update=true
+pmnsfile=$iam
+python_user=root
+python_group=root
+python_opt=true
+daemon_opt=false
+
+ipc_prot="binary notready"
+
+pmdaSetup
+pmdaInstall -U root
+exit
diff --git a/pcp/README.md b/pcp/README.md
new file mode 100644
index 00000000..b8c7b012
--- /dev/null
+++ b/pcp/README.md
@@ -0,0 +1,260 @@
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+# dtrace PMDA
+
+This PMDA provides support to collect PCP metrics from DTrace scripts via the
+libdtrace Python bindings. Scripts can be registered dynamically via
+`pmstore(1)` calls and optionally autostarted from an on-disk directory when
+the PMDA is launched.
+
+# Dynamic-script authorization
+
+Dynamic script registration is enabled only for `root` by default. To change
+the policy, edit the root-owned `$PCP_PMDAS_DIR/dtrace/dtrace.conf` file:
+
+```
+[dynamic_scripts]
+enabled = true
+auth_enabled = true
+allowed_users = root,mydtraceuser
+```
+
+When authentication is enabled, every write to `dtrace.control.*` must come
+from an authenticated PCP user named in `allowed_users`. Local `pmstore`
+clients are identified using their Unix UID; remote clients require PCP
+authentication. Leave `auth_enabled` set to `true`: disabling it allows every
+client with `pmcd` store permission to run DTrace programs with the PMDA's
+privileges.
+
+# Metrics
+
+- `dtrace.control.*` metrics are writable strings consumed by the PMDA to
+ register, unregister, start, stop, or reload scripts. Payloads for
+ `dtrace.control.register` must be JSON objects containing at least the
+ `name` and `program` fields and optional `options` (libdtrace `setopt`
+ key/value pairs), `autostart`, `pid`, `command`, and `defines` fields. `pid` attaches
+ to an existing process, while `command` is the JSON equivalent of
+ `dtrace -c`: it starts a whitespace- and quote-split command under DTrace
+ control. The two target fields are mutually exclusive and initialize
+ `$target` before the program is compiled. `defines` may be an object mapping
+ macro names to values (use `null` for an unvalued macro), or an array of
+ `NAME`/`NAME=VALUE` strings; it enables preprocessing and passes definitions
+ to cpp.
+- `dtrace.scripts.*` metrics form an instance domain over the registered
+ scripts and report each script's current state, autostart flag, last error
+ message, and runtime in seconds.
+
+For example, to create a script which counts system calls that automatically
+starts and collects metrics:
+
+```
+$ pmstore dtrace.control.register '{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":"true"}'
+dtrace.control.register old value="" new value="{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":"true"}"
+$ pminfo -f dtrace
+
+dtrace.scripts.data.syscalls.c
+ inst [0 or "bpf"] value 5360
+ inst [1 or "access"] value 278
+ inst [2 or "poll"] value 7949
+ inst [3 or "close"] value 7693
+ inst [4 or "mmap"] value 525
+ inst [5 or "rename"] value 26
+...
+```
+
+Each instance of the metric 'dtrace.scripts.data.syscalls.c' represents
+the aggregation keys/values associated with aggregation '@c'.
+
+Aggregation names beginning with `__` are reserved for internal DTrace use.
+They are still available to the script, but the PMDA does not publish them as
+PCP metrics. For example, `@__scratch = count();` is intentionally hidden,
+while `@scratch = count();` is exported.
+
+To target a running process, supply its PID and use `$target` in the program:
+
+```
+$ pmstore dtrace.control.register '{"name":"target_reads","pid":1234,"program":"pid$target::read:entry { @reads = count(); }","autostart":true}'
+```
+
+To create a target process, use `command` (the JSON equivalent of `dtrace -c`):
+
+```
+$ pmstore dtrace.control.register '{"name":"sleep","command":"/bin/sleep 30","program":"pid$target::sleep:entry { @calls = count(); }","autostart":true}'
+```
+
+The register payload supports the following keys. `name` and `program` are
+required; the other keys are optional:
+
+```json
+{
+ "name": "example",
+ "program": "syscall:::entry { @calls[PROBEFUNC] = count(); }",
+ "autostart": true,
+ "separator": ";",
+ "options": {
+ "bufsize": "4m",
+ "aggrate": "1s",
+ "quiet": true
+ },
+ "compile": {
+ "zdefs": true
+ },
+ "defines": {
+ "SAMPLE_RATE": 97,
+ "BUILD_LABEL": "production",
+ "FEATURE_ENABLED": null
+ }
+}
+```
+
+The `compile` object controls compile-time flags. `zdefs` is equivalent to
+the `dtrace -Z` option and allows probe descriptions that match no probes.
+
+`defines` enables the C preprocessor and accepts either an object, as above,
+or an array of `NAME`/`NAME=VALUE` strings:
+
+```json
+{
+ "name": "conditional",
+ "program": "BEGIN { trace(SAMPLE_RATE); }",
+ "defines": ["SAMPLE_RATE=10", "FEATURE_ENABLED"]
+}
+```
+
+For target selection, specify exactly one of `pid` or `command`:
+
+```json
+{
+ "name": "existing-process",
+ "pid": 1234,
+ "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+```json
+{
+ "name": "new-process",
+ "command": "/usr/bin/sleep 30",
+ "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+To stop and unregister the script
+
+```
+$ pmstore dtrace.control.unregister syscalls
+```
+
+or to simply stop (while retaining metrics):
+
+```
+$ pmstore dtrace.control.stop syscalls
+```
+
+# Profiling
+
+It is possible to profile using stack keys, and the instance names
+that represent the call stacks can be made to be compatible with
+the expected flamegraph format of `function1;function2`. To do this,
+the default "." key separator that is used to concatenate key values
+must be overridden with a ";" , i.e.
+
+```
+"separator":";"
+```
+
+For example:
+
+```
+$ pmstore dtrace.control.register '{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":"true", "separator":";"}'
+dtrace.control.register old value="" new value="{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":"true"}"
+
+$ pminfo -f dtrace
+
+dtrace.scripts.data.profile.profile
+ inst [0 or "vmlinux`entry_SYSCALL_64_after_hwframe+0x76;vmlinux`do_syscall_64+0xb1;vmlinux`x64_sys_call+0x1cc6;vmlinux`__x64_sys_read+0x1d;vmlinux`ksys_read+0x6d;vmlinux`vfs_read+0xbf;vmlinux`seq_read+0xf9;vmlinux`seq_read_iter+0x2c6;vmlinux`show_smap+0xe7;vmlinux`__show_smap+0x1d1;vmlinux`seq_put_decimal_ull_width+0xae;vmlinux`strlen+0xc"] value 1
+ inst [1 or "vmlinux`entry_SYSCALL_64_after_hwframe+0x76;vmlinux`__audit_syscall_exit+0xa"] value 1
+...
+```
+
+# Autostart
+
+Scripts placed under `$PCP_PMDAS_DIR/dtrace/autostart.d/` with a `.d`
+extension are started automatically when the PMDA becomes ready. Optional
+metadata can be provided by placing a matching `.json` file alongside the `.d`
+file; its contents should mirror the register payload structure (for example,
+to define libdtrace options).
+
+An example pair is provided:
+
+- `examples/syscall_counts.d` – counts system call entries by name.
+- `examples/syscall_counts.json` – marks the script for autostart and
+ enlarges the libdtrace buffers to reduce drops under load.
+- metrics then appear under dtrace.scripts.data.syscall_counts.counts`,
+ with one instance per aggregation key (in this case syscall name):
+
+```
+# pminfo -f dtrace.scripts.data.syscall_counts.counts
+
+dtrace.scripts.data.syscall_counts.counts
+ inst [0 or "mmap"] value 7959
+ inst [1 or "futex"] value 574830
+ inst [2 or "exit"] value 202
+ inst [3 or "dup2"] value 168
+ inst [4 or "times"] value 575
+...
+```
+
+# Installation
+
+First ensure that dtrace and its associated python bindings are installed
+and running.
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+```
+
+Check there is no clash in the Performance Metrics domain defined in
+as `domain=` in `Install`. If there is a clash, edit the file.
+
+Then run
+
+```
+ # sudo ./Install
+```
+
+Verify PMDA Is running
+
+```
+ # pminfo -f dtrace
+```
+
+# De-installation
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+# sudo ./Remove
+```
+
+# Troubleshooting
+
+ + Ensure the DTrace Python bindings are installed (`python3 -c 'import dtrace'`).
+ + Confirm the PMDA log (`$PCP_LOG_DIR/pmcd/dtrace.log`) for script errors.
+ + When debugging autostart scripts, temporarily move files out of
+ `autostart.d/` to disable them.
diff --git a/pcp/Remove b/pcp/Remove
new file mode 100755
index 00000000..c8e6315a
--- /dev/null
+++ b/pcp/Remove
@@ -0,0 +1,27 @@
+#! /bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+
+pmdaSetup
+pmdaRemove
+exit
+
diff --git a/pcp/autostart.d/.gitkeep b/pcp/autostart.d/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/pcp/dtrace.conf b/pcp/dtrace.conf
new file mode 100644
index 00000000..6863cc51
--- /dev/null
+++ b/pcp/dtrace.conf
@@ -0,0 +1,12 @@
+# DTrace PMDA configuration
+#
+# Dynamic scripts execute with the PMDA's privileges. The default policy
+# permits only the authenticated root user to control them.
+[dynamic_scripts]
+enabled = true
+
+# When enabled, only authenticated users named in allowed_users may write to
+# dtrace.control.*. Disabling this is unsafe: any client permitted by pmcd to
+# store a metric can execute a DTrace program as the PMDA user.
+auth_enabled = true
+allowed_users = root
diff --git a/pcp/examples/packet_drop_reasons.d b/pcp/examples/packet_drop_reasons.d
new file mode 100644
index 00000000..c2516691
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.d
@@ -0,0 +1,14 @@
+/* Count packet drops by reason (SOCKET_CLOSE) etc */
+
+BEGIN
+{
+ drop_reasons = (char **)`drop_reasons_core;
+ num_core_reasons = *(size_t *)(`drop_reasons_core+sizeof(char *));
+}
+
+sdt:::kfree_skb
+/arg4 < num_core_reasons /
+{
+ reason = stringof(drop_reasons[arg4]);
+ @drops[reason] = count();
+}
diff --git a/pcp/examples/packet_drop_reasons.json b/pcp/examples/packet_drop_reasons.json
new file mode 100644
index 00000000..3b26f0f4
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.json
@@ -0,0 +1,8 @@
+{
+ "name": "packet_drop_reasons",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/examples/syscall_counts.d b/pcp/examples/syscall_counts.d
new file mode 100644
index 00000000..ec475bcf
--- /dev/null
+++ b/pcp/examples/syscall_counts.d
@@ -0,0 +1,11 @@
+/* Count system call invocations per syscall name */
+
+syscall:::entry
+{
+ @counts[probefunc] = count();
+}
+
+END
+{
+ printf("Tracing stopped for syscall_counts (see PCP metrics for live data)\n");
+}
diff --git a/pcp/examples/syscall_counts.json b/pcp/examples/syscall_counts.json
new file mode 100644
index 00000000..d4248fbc
--- /dev/null
+++ b/pcp/examples/syscall_counts.json
@@ -0,0 +1,8 @@
+{
+ "name": "syscall_counts",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/pmdadtrace.python b/pcp/pmdadtrace.python
new file mode 100755
index 00000000..b8429f3d
--- /dev/null
+++ b/pcp/pmdadtrace.python
@@ -0,0 +1,1232 @@
+#!/usr/bin/env pmpython
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+
+# Copyright (c) 2026, Oracle and/or its affiliates.
+
+"""DTrace Performance Metrics Domain Agent.
+
+This PMDA allows DTrace scripts to be registered dynamically through pmstore
+and supports autostarting scripts from a configuration directory. Scripts are
+executed using the libdtrace Python bindings.
+"""
+
+import atexit
+import configparser
+import json
+import os
+import pwd
+import re
+import shlex
+import threading
+import time
+from collections import defaultdict
+from numbers import Number
+from pathlib import Path
+from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple
+
+try:
+ from dtrace import DTraceError, DTraceSession, DTraceProgram # type: ignore
+except ImportError: # pragma: no cover - fallback for in-tree testing
+ _HERE = Path(__file__).resolve()
+ for candidate in (_HERE.parents[2] / "bindings" / "python" / "src",):
+ if candidate.is_dir() and str(candidate) not in os.sys.path:
+ os.sys.path.insert(0, str(candidate))
+ from dtrace import DTraceError, DTraceSession, DTraceProgram # type: ignore # noqa: E402
+
+import cpmapi as c_api
+import cpmda
+
+_DTRACE_GLOBAL_LOCK = threading.RLock()
+from pcp.pmapi import pmContext as PCP
+from pcp.pmapi import pmUnits
+from pcp.pmda import PMDA, pmdaGetContext, pmdaIndom, pmdaInstid, pmdaMetric
+
+_SANITIZE_COMPONENT = re.compile(r"[^A-Za-z0-9_\+\.;`]+")
+_IGNORED_AGGREGATION_PREFIX = "__"
+
+def _sanitize_component(value: Any) -> str:
+ if isinstance(value, bytes):
+ value = value.decode("utf-8", errors="ignore")
+ text = str(value) if value is not None else ""
+ safe = _SANITIZE_COMPONENT.sub("_", text).strip("_")
+ return safe or "value"
+
+class AggregationEntry(NamedTuple):
+ metric: str
+ instance: str
+ script: str
+ source: str
+ value: int
+ samples: Optional[int]
+ normal: Optional[int]
+ action: str
+ keys: Tuple[str, ...]
+
+class DataInstance:
+
+ def __init__(
+ self,
+ metric: 'DataMetric',
+ instance: str,
+ id:int,
+ value: Any
+ ) -> None:
+ self._metric = metric
+ self._instance = instance
+ self._instance_id = id
+ self._value = value
+ self._instid = pmdaInstid(self._instance_id, instance)
+ metric._data_indom_insts.append(self._instid)
+
+class DataMetric:
+
+ def __init__(
+ self,
+ metric: pmdaMetric,
+ pmid: str,
+ name: str,
+ cluster: int,
+ script: str,
+ variant: str,
+ indom: int,
+ indom_id: int,
+ metric_id: int,
+ instance_next: int,
+ ) -> None:
+ self.metric = metric
+ self.pmid = pmid
+ self.name = name
+ self.cluster = cluster
+ self.script = script
+ self.variant = variant
+ self.indom = indom
+ self.indom_id = indom_id
+ self.metric_id = metric_id
+ self._instance_next = instance_next
+ self._data_instances = {}
+ self._data_instance_ids = {}
+ self._data_indom_insts = []
+
+class ManagedDTraceScript:
+ """Wraps a DTrace script lifecycle using libdtrace."""
+
+ _START_TIMEOUT = 10.0
+ _POLL_INTERVAL = 1.0
+
+ def __init__(
+ self,
+ name: str,
+ program: str,
+ autostart: bool,
+ options: Optional[Dict[str, Any]],
+ separator: Optional[str],
+ pid: Optional[int],
+ command: Optional[List[str]],
+ defines: Optional[List[str]],
+ compile_flags: int,
+ logger,
+ ) -> None:
+ self.name = name
+ self.program = program
+ self.autostart = autostart
+ self.options = dict(options or {})
+ self.options.setdefault("quiet", True)
+ self.separator = separator or "."
+ self.pid = pid
+ self.command = list(command) if command else None
+ self.defines = list(defines or [])
+ self.compile_flags = compile_flags
+ self._log = logger
+ self._lock = threading.RLock()
+ self._thread: Optional[threading.Thread] = None
+ self._stop_thread: Optional[threading.Thread] = None
+ self._stop = threading.Event()
+ self._session: Optional[DTraceSession] = None
+ self._state = "stopped"
+ self._last_error = ""
+ self._started_at: Optional[float] = None
+ self._data_callbacks = {
+ }
+ self._aggregation_cache: Dict[str, AggregationEntry] = {}
+
+ # ------------------------------------------------------------------
+ # Public accessors
+ # ------------------------------------------------------------------
+ def metric_prefix(self) -> str:
+ return "dtrace.scripts.data." + _sanitize_component(self.name) + "."
+
+ def state(self) -> str:
+ with self._lock:
+ return self._state
+
+ def last_error(self) -> str:
+ with self._lock:
+ return self._last_error
+
+ def is_running(self) -> bool:
+ return self.state() == "running"
+
+ def runtime_seconds(self) -> int:
+ with self._lock:
+ if self._state != "running" or self._started_at is None:
+ return 0
+ return int(time.time() - self._started_at)
+
+ # ------------------------------------------------------------------
+ # Lifecycle helpers
+ # ------------------------------------------------------------------
+ def start(self) -> Tuple[bool, str]:
+ self._log(f"script {self.name} starting")
+ with self._lock:
+ if self._thread and self._thread.is_alive():
+ return True, ""
+ self._stop.clear()
+ self._state = "starting"
+ self._thread = threading.Thread(
+ target=self._run, name=f"dtrace:{self.name}", daemon=True
+ )
+ self._thread.start()
+
+ deadline = time.time() + self._START_TIMEOUT
+ while time.time() < deadline:
+ state = self.state()
+ if state == "running":
+ return True, ""
+ if state == "error":
+ return False, self.last_error() or "failed to start"
+ time.sleep(0.1)
+
+ if self.state() == "running":
+ return True, ""
+ return False, self.last_error() or "timeout waiting for DTrace startup"
+
+ def stop(self) -> None:
+ self._log(f"script {self.name} stopping")
+ with self._lock:
+ if not self._thread:
+ self._state = "stopped"
+ return
+ self._stop.set()
+
+ def update_definition(
+ self,
+ program: str,
+ options: Optional[Dict[str, Any]],
+ pid: Optional[int],
+ command: Optional[List[str]],
+ defines: Optional[List[str]],
+ compile_flags: int,
+ ) -> None:
+ restart = self.is_running()
+ if restart:
+ self.stop()
+ with self._lock:
+ self.program = program
+ if options is not None:
+ self.options = options
+ self.pid = pid
+ self.command = list(command) if command else None
+ self.defines = list(defines or [])
+ self.compile_flags = compile_flags
+ if restart:
+ ok, error = self.start()
+ if not ok:
+ self._log(f"failed to restart script {self.name}: {error}")
+
+ # ------------------------------------------------------------------
+ # Internal execution loop
+ # ------------------------------------------------------------------
+ def _run(self) -> None:
+ self._log(f"script {self.name} running")
+ target = None
+ try:
+ session = DTraceSession()
+ except Exception as exc: # pylint: disable=broad-except
+ self._fail("session creation", str(exc))
+ return
+
+ with self._lock:
+ self._session = session
+ self._last_error = ""
+
+ try:
+ # Compilation uses global variables; lock to prevent interference
+ with _DTRACE_GLOBAL_LOCK:
+ self._apply_options(session)
+ for macro in self.defines:
+ session.setopt("cpp", None)
+ session.setopt("define", macro)
+ if self.pid is not None:
+ target = session.proc_grab_pid(self.pid)
+ elif self.command is not None:
+ target = session.proc_create(self.command)
+ compiled = session.compile(self.program, cflags=self.compile_flags)
+ session.enable(compiled)
+ session.go()
+ if target is not None:
+ session.proc_continue(target)
+ with self._lock:
+ self._state = "running"
+ self._started_at = time.time()
+
+ while not self._stop.is_set():
+ set = self._stop.wait(timeout=self._POLL_INTERVAL)
+ if set:
+ break
+ try:
+ session.work()
+ session.agg_snap()
+ updates = self._walk_aggregations(session)
+ # encourage thread yield
+ time.sleep(0.001)
+ except DTraceError as exc: # pragma: no cover - runtime error path
+ self._fail("session execution", str(exc))
+ break
+
+ except Exception as exc: # pylint: disable=broad-except
+ self._fail("session initiation", str(exc))
+ finally:
+ try:
+ session.stop()
+ # Ensure we get final view of data (truncated aggregations etc)
+ session.agg_snap()
+ updates = self._walk_aggregations(session)
+ self._log(f"session stopped for {self.name}")
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ if target is not None:
+ try:
+ session.proc_release(target)
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ try:
+ session.close()
+ self._log(f"session closed for {self.name}")
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ with self._lock:
+ if self._state == "running":
+ self._state = "stopped"
+ self._session = None
+
+ def _apply_options(self, session: DTraceSession) -> None:
+ self._log(f"applying options to {self.name}")
+ if not self.options:
+ return
+ for key, value in self._normalize_options(self.options):
+ try:
+ session_setopt = session.setopt
+ except AttributeError: # pragma: no cover - safety net
+ continue
+ try:
+ session_setopt(key, value)
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"unable to set option '{key}' for script {self.name}: {exc}"
+ )
+
+ @staticmethod
+ def _normalize_options(options: Dict[str, Any]) -> Iterable[Tuple[str, Optional[str]]]:
+ for key, value in options.items():
+ if isinstance(value, bool):
+ yield str(key), None if value else "0"
+ elif value is None:
+ yield str(key), None
+ else:
+ yield str(key), str(value)
+
+ def _fail(self, ctx: str, message: str) -> None:
+ with self._lock:
+ self._state = "error"
+ self._last_error = message
+ self._log(f"script {self.name} encountered an error during {ctx}: {message}")
+ with self._lock:
+ self._aggregation_cache.clear()
+
+
+ def register_data_callbacks(
+ self,
+ pmda
+ ) -> None:
+ with self._lock:
+ for key in pmda._data_callbacks.keys():
+ self._data_callbacks[key] = pmda._data_callbacks[key]
+
+ def unregister_data_callbacks(
+ self,
+ pmda
+ ):
+ remove_metrics = None
+ with self._lock:
+ if "remove_metrics" in self._data_callbacks:
+ remove_metrics = self._data_callbacks["remove_metrics"]
+ self._data_callbacks = {}
+ if remove_metrics is not None:
+ remove_metrics(self)
+
+ def latest_aggregations(self) -> Dict[str, AggregationEntry]:
+ with self._lock:
+ return dict(self._aggregation_cache)
+
+ def _walk_aggregations(self, session: DTraceSession) -> Dict[str, AggregationEntry]:
+ try:
+ snapshot = session.agg_walk()
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(f"aggregation walk failed for {self.name}: {exc}")
+ return {}
+
+ updates: Dict[Tuple, AggregationEntry] = {}
+ for record in snapshot:
+ metric_name = self._metric_name_from_record(record)
+ if not metric_name:
+ continue
+ instance_name = self._data_instance_from_record(record)
+ if not instance_name:
+ continue
+ entry = AggregationEntry(
+ metric=metric_name,
+ instance=instance_name,
+ script=self.name,
+ source=record.get("name", ""),
+ value=self._coerce_int(record.get("value")),
+ samples=self._coerce_int(record.get("samples")),
+ normal=self._coerce_int(record.get("normal")),
+ action=str(record.get("action", "")),
+ keys=tuple(str(k) for k in record.get("keys", [])),
+ )
+ updates[(metric_name, instance_name)] = entry
+ with self._lock:
+ previous = self._aggregation_cache
+ self._aggregation_cache = updates
+
+ current_keys = set(updates)
+ previous_keys = set(previous)
+ created = current_keys - previous_keys
+ removed = previous_keys - current_keys
+
+ refresh = False
+ for key in created:
+ entry = updates[key]
+ try:
+ with self._lock:
+ if "create_metric" in self._data_callbacks:
+ self._data_callbacks["create_metric"](entry)
+ if "create_instance" in self._data_callbacks:
+ self._data_callbacks["create_instance"](entry)
+ refresh = True
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"create callback error for {self.name}:{entry.metric}: {exc}"
+ )
+
+ for key in current_keys & previous_keys:
+ entry = updates[key]
+ try:
+ with self._lock:
+ if "update_instance" in self._data_callbacks:
+ self._data_callbacks["update_instance"](entry)
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"update callback error for {self.name}:{entry.metric}: {exc}"
+ )
+
+ for key in removed:
+ entry = previous[key]
+ try:
+ with self._lock:
+ if "remove_instance" in self._data_callbacks:
+ self._data_callbacks["remove_instance"](entry)
+ refresh = True
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"remove callback error for {self.name}:{entry.metric}: {exc}"
+ )
+ if refresh:
+ with self._lock:
+ if "refresh_metrics" in self._data_callbacks:
+ self._data_callbacks["refresh_metrics"]()
+ return updates
+
+ def _metric_name_from_record(self, record: Dict[str, Any]) -> Optional[str]:
+ source = record.get("name")
+ if not source or str(source).startswith(_IGNORED_AGGREGATION_PREFIX):
+ return None
+ return self.metric_prefix() + _sanitize_component(source)
+
+ def _data_instance_from_record(self, record: Dict[str, Any]) -> Optional[str]:
+ parts = []
+ for key in record.get("keys", []):
+ # key component may be a list (stack)
+ if isinstance(key, list):
+ k = self.separator.join(key)
+ else:
+ k = key
+ parts.append(_sanitize_component(k))
+ return self.separator.join(parts)
+
+ @staticmethod
+ def _coerce_int(value: Any) -> Optional[int]:
+ if value is None:
+ return None
+ if isinstance(value, int):
+ return value
+ if isinstance(value, Number):
+ return int(value)
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ return None
+
+class DTracePMDA(PMDA):
+ """PCP PMDA providing DTrace control and state metrics."""
+
+ class Control:
+ CLUSTER = 0
+ REGISTER = 0
+ UNREGISTER = 1
+ START = 2
+ STOP = 3
+ RELOAD = 4
+
+ class Scripts:
+ CLUSTER = 1
+ STATE = 0
+ AUTOSTART = 1
+ LAST_ERROR = 2
+ RUNTIME = 3
+
+ class Data:
+ CLUSTER = 2
+
+ def _create_metric(self, entry: AggregationEntry):
+ if entry.metric in self._data_metrics:
+ return
+ metric_id = self._metric_next
+ indom = self._indom_next
+ indom_id = self.indom(indom)
+ self.add_indom(pmdaIndom(indom_id, []), "DTrace metric data", "DTrace data for metric")
+ self._indom_next += 1
+
+ metric = pmdaMetric(
+ self.pmid(indom, metric_id),
+ c_api.PM_TYPE_DOUBLE,
+ indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ )
+ try:
+ self.add_metric(entry.metric, metric)
+ except Exception as exc:
+ self.log(f"failed to add metric {entry.metric}: {exc}")
+ self._data_metrics[entry.metric] = DataMetric(
+ metric=metric,
+ pmid=metric.m_desc.pmid,
+ name=entry.metric,
+ script=entry.script,
+ cluster=self.Data.CLUSTER,
+ variant=entry.source,
+ indom=indom,
+ indom_id=indom_id,
+ metric_id=metric_id,
+ instance_next=0
+ )
+ self._data_metric_ids[(indom, metric_id)] = self._data_metrics[entry.metric]
+
+ def _create_instance(self, entry: AggregationEntry):
+
+ names = (entry.metric, entry.instance)
+ if entry.metric not in self._data_metrics:
+ return
+ metric = self._data_metrics[entry.metric]
+ if entry.instance in metric._data_instances:
+ return
+ instance_id = metric._instance_next
+ metric._instance_next += 1
+ metric._data_instances[entry.instance] = DataInstance(metric,
+ entry.instance,
+ instance_id,
+ entry.value)
+ metric._data_instance_ids[instance_id] = metric._data_instances[entry.instance]
+
+ def _update_instance(self, entry: AggregationEntry):
+ if entry.metric not in self._data_metrics:
+ return
+ metric = self._data_metrics[entry.metric]
+ if entry.instance not in metric._data_instances:
+ return
+ instance = metric._data_instances[entry.instance]
+ instance._value = entry.value
+
+ def _remove_instance_by_name(self, m:str, i:str):
+ if m not in self._data_metrics:
+ return
+ metric = self._data_metrics[m]
+ if i not in metric._data_instances:
+ return
+ instance = metric._data_instances[i]
+ if instance._instid in metric._data_indom_insts:
+ metric._data_indom_insts.remove(instance._instid)
+ if instance._instance_id in metric._data_instance_ids:
+ metric._data_instance_ids.pop(instance._instance_id, None)
+
+ def _remove_instance(self, entry: AggregationEntry):
+ self._log(f"removing instance {entry.metric}, {entry.instance}")
+ self._remove_instance_by_name(entry.metric, entry.instance)
+
+ def _remove_metrics(self, script:ManagedDTraceScript):
+ prefix = script.metric_prefix()
+ self.log(f"removing metrics for {script.name}")
+ for m in list(self._data_metrics.keys()):
+ if not m.startswith(prefix):
+ continue
+ metric = self._data_metrics[m]
+ indom = metric.indom
+ metric_id = metric.metric_id
+ self.replace_indom(metric.indom_id, [])
+ self._data_metrics.pop(m, None)
+ self._data_metric_ids.pop((indom, metric_id), None)
+ self.remove_metric(m, metric.metric)
+
+ def _refresh_metrics(self):
+ self._rebuild_data_indom()
+ self.set_notify_change()
+ cpmda.set_need_refresh()
+
+ def __init__(self, name: str, domain: int) -> None:
+ super().__init__(name, domain)
+
+ self.set_user("root")
+
+ self._lock = threading.RLock()
+ (
+ self._dynamic_scripts_enabled,
+ self._auth_enabled,
+ self._allowed_users,
+ ) = self._load_control_config()
+ self._ctx_usernames: Dict[int, str] = {}
+ self._scripts: Dict[str, ManagedDTraceScript] = {}
+ self._inst_map: Dict[Tuple, str] = {}
+ self._next_inst = 0
+ self._indom_next = 2
+ self._data_metrics: Dict[str, DataMetric] = {}
+ self._data_metric_ids: Dict[Tuple, DataMetric] = {}
+ self._metric_next = 0
+
+ self.script_indom_id = self.indom(0)
+
+ self._data_callbacks = {
+ "create_metric" : self._create_metric,
+ "remove_metrics" : self._remove_metrics,
+ "refresh_metrics" : self._refresh_metrics,
+ "create_instance" : self._create_instance,
+ "update_instance" : self._update_instance,
+ "remove_instance" : self._remove_instance
+ }
+ self.script_indom = pmdaIndom(self.script_indom_id, [])
+ self.add_indom(self.script_indom, "DTrace scripts", "Registered scripts")
+
+ self._units_none = pmUnits()
+ self._units_seconds = pmUnits(0, 1, 0, 0, c_api.PM_TIME_SEC, 0)
+
+ # Require credential PDUs to avoid TYPE-0 handshake downgrade.
+ self.set_comm_flags(cpmda.PMDA_FLAG_AUTHORIZE)
+
+ self.log(f"Adding metrics for PMDA..")
+ self.add_metric(
+ "dtrace.control.register",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.REGISTER),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Register a DTrace script via JSON payload",
+ )
+ self.add_metric(
+ "dtrace.control.unregister",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.UNREGISTER),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Unregister an existing DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.start",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.START),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Start a registered DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.stop",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.STOP),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Stop a running DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.reload",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.RELOAD),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Reload autostart scripts from disk",
+ )
+
+ self.add_metric(
+ "dtrace.scripts.state",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.STATE),
+ c_api.PM_TYPE_STRING,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "State of each registered DTrace script",
+ )
+ self.add_metric(
+ "dtrace.scripts.autostart",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.AUTOSTART),
+ c_api.PM_TYPE_U32,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "Whether the script autostarts on PMDA load",
+ )
+ self.add_metric(
+ "dtrace.scripts.last_error",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.LAST_ERROR),
+ c_api.PM_TYPE_STRING,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "Most recent error message for the script",
+ )
+ self.add_metric(
+ "dtrace.scripts.runtime_seconds",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.RUNTIME),
+ c_api.PM_TYPE_U64,
+ self.script_indom_id,
+ c_api.PM_SEM_INSTANT,
+ self._units_seconds,
+ ),
+ "Seconds the script has been running",
+ )
+
+ self.set_fetch_callback(self._fetch_callback)
+ self.set_store_callback(self._store_callback)
+ self.set_attribute_callback(self._attribute_callback)
+ self.set_endcontext_callback(self._endcontext_callback)
+ self.set_user("root")
+
+ self._autostart_dir = (
+ Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / self.read_name() / "autostart.d"
+ )
+
+ atexit.register(self._shutdown_scripts)
+
+ if not self._in_pmda_setup():
+ # If running a newer pcp python binding that supports credentials,
+ # explicitly tell the C layer how to fall back if the server is old.
+ if cpmda and hasattr(cpmda, 'pmdaSetFlags'):
+ try:
+ # PMDA_FLAG_CREDS_OPTIONAL allows the agent to handle
+ # credentials if sent, but fall back seamlessly if an
+ # older pmcd sends nothing.
+ # PMDA_FLAG_CREDS_OPTIONAL constant value is typically 0x4
+ cpmda.pmdaSetFlags(0x4)
+ except Exception:
+ pass
+ try:
+ self.connect_pmcd()
+ except Exception as e:
+ sys.stderr.write(f"Connection failed: {str(e)}\n")
+ sys.exit(1)
+ self.reload_autostart()
+ self.pmda_ready()
+ self.log("Ready to process DTrace control requests.")
+
+ # ------------------------------------------------------------------
+ # Store callback handling control metrics
+ # ------------------------------------------------------------------
+ @staticmethod
+ def _load_control_config() -> Tuple[bool, bool, Set[str]]:
+ """Load the root-owned dynamic-script authorization policy."""
+ config = configparser.ConfigParser()
+ config_path = (
+ Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / "dtrace" / "dtrace.conf"
+ )
+
+ # Secure defaults: do not permit dynamically supplied programs.
+ enabled = False
+ auth_enabled = True
+ allowed_users: Set[str] = set()
+
+ try:
+ config.read(config_path)
+ if not config.has_section("dynamic_scripts"):
+ return enabled, auth_enabled, allowed_users
+
+ enabled = config.getboolean(
+ "dynamic_scripts", "enabled", fallback=enabled
+ )
+ auth_enabled = config.getboolean(
+ "dynamic_scripts", "auth_enabled", fallback=auth_enabled
+ )
+ configured_users = config.get(
+ "dynamic_scripts", "allowed_users", fallback=""
+ )
+ except (OSError, ValueError, configparser.Error):
+ return False, True, set()
+
+ allowed_users = {
+ user.strip() for user in configured_users.split(",") if user.strip()
+ }
+ return enabled, auth_enabled, allowed_users
+
+ def _attribute_callback(self, ctx: int, attr: int, value: str) -> None:
+ """Associate an authenticated client identity with its PCP context."""
+ if attr == cpmda.PMDA_ATTR_USERNAME:
+ with self._lock:
+ self._ctx_usernames[ctx] = value
+ return
+
+ if attr != cpmda.PMDA_ATTR_USERID:
+ return
+
+ try:
+ username = pwd.getpwuid(int(value)).pw_name
+ except (KeyError, OverflowError, ValueError):
+ # Do not retain an earlier, less trustworthy identity.
+ with self._lock:
+ self._ctx_usernames.pop(ctx, None)
+ return
+
+ with self._lock:
+ self._ctx_usernames[ctx] = username
+
+ def _endcontext_callback(self, ctx: int) -> None:
+ with self._lock:
+ self._ctx_usernames.pop(ctx, None)
+
+ def _control_store_permitted(self) -> bool:
+ if not self._dynamic_scripts_enabled:
+ return False
+ if not self._auth_enabled:
+ return True
+
+ try:
+ ctx = pmdaGetContext()
+ except Exception: # pragma: no cover - PMDA library failure
+ return False
+
+ with self._lock:
+ username = self._ctx_usernames.get(ctx)
+ return username is not None and username in self._allowed_users
+
+ def _store_callback(self, cluster: int, item: int, inst: int, value: Any) -> int:
+ self.log(f"store callback")
+ if cluster != self.Control.CLUSTER:
+ return c_api.PM_ERR_PMID
+
+ if not self._control_store_permitted():
+ self.err("DTrace control store denied by dynamic-script policy")
+ return c_api.PM_ERR_PERMISSION
+
+ if isinstance(value, bytes):
+ text = value.decode("utf-8", errors="ignore")
+ else:
+ text = str(value)
+
+ if item == self.Control.REGISTER:
+ return self._handle_register(text)
+ if item == self.Control.UNREGISTER:
+ return self._handle_unregister(text)
+ if item == self.Control.START:
+ return self._handle_start(text)
+ if item == self.Control.STOP:
+ return self._handle_stop(text)
+ if item == self.Control.RELOAD:
+ self.reload_autostart()
+ return 0
+ return c_api.PM_ERR_PMID
+
+ def _handle_register(self, payload: str) -> int:
+ self.log(f"handle register {payload}")
+ try:
+ data = json.loads(payload)
+ except json.JSONDecodeError:
+ self.err("register payload must be valid JSON")
+ return c_api.PM_ERR_VALUE
+ if not isinstance(data, dict):
+ self.err("register payload must be a JSON object")
+ return c_api.PM_ERR_VALUE
+
+ name = data.get("name")
+ program = data.get("program")
+ if not name or not isinstance(name, str) or not program:
+ self.err("register payload requires string 'name' and 'program'")
+ return c_api.PM_ERR_VALUE
+
+ self.log(f"program is {program}")
+ options = data.get("options") if isinstance(data.get("options"), dict) else {}
+ separator = data.get("separator") if isinstance(data.get("separator"), str) else "."
+ autostart = bool(data.get("autostart", False))
+ pid, command = self._parse_target(data)
+ if pid is None and command is None and (
+ "pid" in data or "command" in data
+ ):
+ return c_api.PM_ERR_VALUE
+ defines = self._parse_defines(data.get("defines"))
+ if defines is None:
+ return c_api.PM_ERR_VALUE
+ compile_flags = self._parse_compile_flags(data.get("compile"))
+ if compile_flags is None:
+ return c_api.PM_ERR_VALUE
+ self.log(f"autostart is {autostart}")
+
+ with self._lock:
+ script = self._scripts.get(name)
+ if script:
+ script.autostart = autostart
+ script.separator = separator
+ script.update_definition(
+ program, options, pid, command, defines, compile_flags
+ )
+ else:
+ script = ManagedDTraceScript(
+ name, program, autostart, options, separator, pid, command,
+ defines,
+ compile_flags,
+ self.log,
+ )
+ self._scripts[name] = script
+ self._assign_instance(name)
+ script.register_data_callbacks(self)
+
+ if autostart:
+ self.log(f"starting script {name}...")
+ ok, error = script.start()
+ if not ok:
+ self.err(f"failed to start script {name}: {error}")
+ self.log(f"script {name} started ok")
+ return 0
+
+ def _parse_target(
+ self, data: Dict[str, Any]
+ ) -> Tuple[Optional[int], Optional[List[str]]]:
+ """Validate JSON equivalents of dtrace(1)'s -p and -c options."""
+ pid = data.get("pid")
+ command = data.get("command")
+
+ if pid is not None and command is not None:
+ self.err("register payload must specify only one of 'pid' or 'command'")
+ return None, None
+
+ if pid is not None:
+ if isinstance(pid, bool) or not isinstance(pid, int) or pid <= 0:
+ self.err("register payload 'pid' must be a positive integer")
+ return None, None
+ if pid > 0x7fffffff:
+ self.err("register payload 'pid' is out of range")
+ return None, None
+ return pid, None
+
+ if command is not None:
+ if not isinstance(command, str):
+ self.err("register payload 'command' must be a non-empty string")
+ return None, None
+ try:
+ argv = shlex.split(command)
+ except ValueError:
+ self.err("register payload 'command' has invalid shell quoting")
+ return None, None
+ if not argv:
+ self.err("register payload 'command' must be a non-empty string")
+ return None, None
+ return None, argv
+
+ return None, None
+
+ def _parse_defines(self, value: Any) -> Optional[List[str]]:
+ """Validate macro definitions and return NAME[=VALUE] strings."""
+ if value is None:
+ return []
+ items = value.items() if isinstance(value, dict) else value
+ if not isinstance(value, (dict, list)):
+ self.err("register payload 'defines' must be an object or array")
+ return None
+ result = []
+ for item in items:
+ if isinstance(value, dict):
+ name, macro_value = item
+ if not isinstance(name, str) or not re.fullmatch(
+ r"[A-Za-z_][A-Za-z0-9_]*", name
+ ):
+ self.err("register payload has invalid macro name")
+ return None
+ result.append(name if macro_value is None else f"{name}={macro_value}")
+ elif not isinstance(item, str) or not re.fullmatch(
+ r"[A-Za-z_][A-Za-z0-9_]*(=.*)?", item
+ ):
+ self.err(
+ "register payload 'defines' entries must be NAME or NAME=VALUE"
+ )
+ return None
+ else:
+ result.append(item)
+ return result
+
+ def _parse_compile_flags(self, value: Any) -> Optional[int]:
+ """Translate supported compile-time flags to libdtrace cflags."""
+ if value is None:
+ return 0
+ if not isinstance(value, dict):
+ self.err("register payload 'compile' must be an object")
+ return None
+ flags = 0
+ if value.get("zdefs", False):
+ flags |= c_api.DTRACE_C_ZDEFS
+ unknown = set(value) - {"zdefs"}
+ if unknown:
+ self.err("unsupported compile option(s): " + ", ".join(sorted(unknown)))
+ return None
+ return flags
+
+ def _handle_unregister(self, name: str) -> int:
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = None
+ with self._lock:
+ script = self._scripts.pop(name, None)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ self._rebuild_instances_locked()
+ if script is not None:
+ if script._state != "stopped":
+ script.stop()
+ script.unregister_data_callbacks(self)
+ return 0
+
+ def _handle_start(self, name: str) -> int:
+ self.log(f"handle start {name}")
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = self._scripts.get(name)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ ok, error = script.start()
+ if not ok:
+ self.err(f"start failed for {name}: {error}")
+ return c_api.PM_ERR_GENERIC
+ return 0
+
+ def _handle_stop(self, name: str) -> int:
+ self.log(f"handle stop {name}")
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = self._scripts.get(name)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ script.stop()
+ return 0
+
+ # ------------------------------------------------------------------
+ # Fetch callback serving metric values
+ # ------------------------------------------------------------------
+ def _fetch_callback(self, cluster: int, item: int, inst: int):
+ if cluster > self.Scripts.CLUSTER:
+ ids = (cluster, item)
+ if ids not in self._data_metric_ids:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ metric = self._data_metric_ids[(cluster, item)]
+ if inst not in metric._data_instance_ids:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ instance = metric._data_instance_ids[inst]
+ value = instance._value
+ if value is None:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ return [value, 1]
+ if cluster == self.Control.CLUSTER:
+ # Control metrics are write-only, but returning NOVALUES causes
+ # clients like pmstore to abort before issuing the store PDU.
+ # Hand back an empty string so the store callback still fires.
+ return ["", 1]
+
+ if cluster != self.Scripts.CLUSTER:
+ return [c_api.PM_ERR_PMID, 0]
+
+ name = self._inst_map.get(inst)
+ if not name:
+ return [c_api.PM_ERR_INST, 0]
+ script = self._scripts.get(name)
+ if not script:
+ return [c_api.PM_ERR_INST, 0]
+
+ if item == self.Scripts.STATE:
+ return [script.state(), 1]
+ if item == self.Scripts.AUTOSTART:
+ return [1 if script.autostart else 0, 1]
+ if item == self.Scripts.LAST_ERROR:
+ return [script.last_error(), 1]
+ if item == self.Scripts.RUNTIME:
+ return [script.runtime_seconds(), 1]
+
+ return [c_api.PM_ERR_PMID, 0]
+
+ # ------------------------------------------------------------------
+ # Autostart support
+ # ------------------------------------------------------------------
+ def reload_autostart(self) -> None:
+ if not self._autostart_dir.is_dir():
+ return
+
+ autostart_names = set()
+ for script_path in sorted(self._autostart_dir.glob("*.d")):
+ try:
+ program = script_path.read_text()
+ except OSError as exc: # pragma: no cover - filesystem error
+ self.err(f"unable to read {script_path}: {exc}")
+ continue
+
+ metadata = self._load_metadata(script_path)
+ name = metadata.get("name") or script_path.stem
+ options = metadata.get("options") if isinstance(metadata.get("options"), dict) else {}
+ separator = metadata.get("separator") if isinstance(metadata.get("separator"), str) else "."
+ pid, command = self._parse_target(metadata)
+ if pid is None and command is None and (
+ "pid" in metadata or "command" in metadata
+ ):
+ self.err(f"invalid target in autostart metadata {script_path}")
+ continue
+ defines = self._parse_defines(metadata.get("defines"))
+ if defines is None:
+ self.err(f"invalid macro definitions in {script_path}")
+ continue
+ compile_flags = self._parse_compile_flags(metadata.get("compile"))
+ if compile_flags is None:
+ self.err(f"invalid compile options in {script_path}")
+ continue
+
+ autostart_names.add(name)
+
+ with self._lock:
+ script = self._scripts.get(name)
+ if script:
+ script.autostart = True
+ script.separator = separator
+ script.update_definition(
+ program, options, pid, command, defines, compile_flags
+ )
+ else:
+ script = ManagedDTraceScript(
+ name, program, True, options, separator, pid, command,
+ defines,
+ compile_flags,
+ self.log,
+ )
+ self._scripts[name] = script
+ self._assign_instance(name)
+ script.register_data_callbacks(self)
+
+ ok, error = script.start()
+ if not ok:
+ self.err(f"autostart of {name} failed: {error}")
+
+ with self._lock:
+ for name, script in self._scripts.items():
+ script.autostart = name in autostart_names or script.autostart
+
+ def _load_metadata(self, script_path: Path) -> Dict[str, Any]:
+ self.log("load metadata")
+ meta_path = script_path.with_suffix(".json")
+ if not meta_path.is_file():
+ return {}
+ try:
+ return json.loads(meta_path.read_text())
+ except (OSError, json.JSONDecodeError) as exc: # pragma: no cover
+ self.err(f"failed to parse metadata {meta_path}: {exc}")
+ return {}
+
+ # ------------------------------------------------------------------
+ # Instance domain management
+ # ------------------------------------------------------------------
+ def _assign_instance(self, name: str) -> None:
+ self.log("assign instance")
+ with self._lock:
+ inst_id = self._next_inst
+ self._next_inst += 1
+ self._inst_map[inst_id] = name
+ self._rebuild_indom()
+
+ def _rebuild_instances_locked(self) -> None:
+ # assumes caller holds self._lock
+ self._inst_map = {
+ idx: name for idx, name in enumerate(sorted(self._scripts.keys()))
+ }
+ self._next_inst = len(self._inst_map)
+ self._rebuild_indom()
+
+ def _rebuild_data_indom(self) -> None:
+ with self._lock:
+ for m in self._data_metrics.values():
+ self.replace_indom(m.indom_id, m._data_indom_insts)
+
+
+ def _rebuild_indom(self) -> None:
+ insts = [pmdaInstid(inst, name) for inst, name in sorted(self._inst_map.items())]
+ self.replace_indom(self.script_indom_id, insts)
+
+ # ------------------------------------------------------------------
+ # Teardown helpers
+ # ------------------------------------------------------------------
+ def _shutdown_scripts(self) -> None:
+ with self._lock:
+ scripts = list(self._scripts.values())
+ for script in scripts:
+ script.unregister_data_callbacks(self)
+ script.stop()
+
+ # ------------------------------------------------------------------
+ @staticmethod
+ def _in_pmda_setup() -> bool:
+ return bool(os.environ.get("PCP_PYTHON_DOMAIN") or os.environ.get("PCP_PYTHON_PMNS"))
+
+
+def main() -> None:
+ domain = -1
+ with open(os.path.dirname(os.path.abspath(__file__)) + "/Install") as f:
+ for line in f:
+ if "domain=" in line:
+ domain = int(line.split("=")[1].strip())
+ if domain == -1 :
+ print("No domain value in Install, exiting.")
+ else:
+ DTracePMDA("dtrace", 487).run()
+
+
+if __name__ == "__main__":
+ main()
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v4 9/9] dtrace.spec: Add optional PCP PMDA packaging pcp-pmda-dtrace
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (7 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 8/9] dtrace: Add a PCP PMDA to expose DTrace data as metrics Alan Maguire
@ 2026-08-12 13:22 ` Alan Maguire
2026-08-13 21:01 ` [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Kris Van Hees
9 siblings, 0 replies; 11+ messages in thread
From: Alan Maguire @ 2026-08-12 13:22 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Add a PCP build conditional and package the DTrace PMDA when PCP and
Python support are enabled. Install it under an overridable PCP target
directory, defaulting to /var/lib/pcp/pmdas/dtrace, and require
python3-pcp at runtime.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
dtrace.spec | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/dtrace.spec b/dtrace.spec
index a16cb62a..5747665c 100644
--- a/dtrace.spec
+++ b/dtrace.spec
@@ -29,6 +29,9 @@
# By default, do not build with 32-on-64 support.
%define glibc32 %{nil}
+# Directory in which to install the DTrace PCP PMDA.
+%global pcp_targetdir %{?pcp_targetdir}%{!?pcp_targetdir:/var/lib/pcp/pmdas/dtrace}
+
# Enable it for non-ARM64 builds excpet for OL10.
# ARM64 does not support 32-on-64 either.
%ifnarch aarch64
@@ -41,6 +44,7 @@
%global _lto_cflags %{nil}
%bcond_without python
+%bcond_without pcp
BuildRequires: rpm
Name: dtrace
@@ -146,6 +150,18 @@ Python extension module providing access to libdtrace.
%endif
+%if %{with pcp} && %{with python}
+%package -n pcp-pmda-dtrace
+Summary: PCP PMDA for DTrace
+Requires: python3-dtrace = %{version}-%{release}
+Requires: python3-pcp
+Group: Applications/System
+
+%description -n pcp-pmda-dtrace
+Performance Co-Pilot PMDA for exposing DTrace data as metrics.
+
+%endif
+
Installed in /usr/lib64/dtrace/testsuite.
'make check' here is just like 'make check' in the source tree, except that
@@ -170,8 +186,16 @@ make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2} %{?with_python:W
mkdir -p $RPM_BUILD_ROOT/usr/sbin
make DESTDIR=$RPM_BUILD_ROOT VERSION=%{version} \
HDRPREFIX="$RPM_BUILD_ROOT/usr/include" \
+ PMDA_OBJDIR="$RPM_BUILD_ROOT%{pcp_targetdir}" \
install install-test %{?with_python:install-python} PYTHON=%{__python3}
+%if %{without pcp}
+rm -rf "$RPM_BUILD_ROOT%{pcp_targetdir}"
+%endif
+%if %{without python}
+rm -rf "$RPM_BUILD_ROOT%{pcp_targetdir}"
+%endif
+
%if "%{?dist}" == ".el7"
sed -i '/^ProtectSystem=/d; /^ProtectControlGroups=/d; /^RuntimeDirectory/d;' $RPM_BUILD_ROOT/usr/lib/systemd/system/dtprobed.service
%endif
@@ -255,6 +279,11 @@ systemctl start dtprobed || :
%{python3_sitearch}/dtrace*.so
%doc bindings/python/README.md
%endif
+%if %{with pcp} && %{with python}
+%files -n pcp-pmda-dtrace
+%defattr(-,root,root,-)
+%{pcp_targetdir}
+%endif
%files testsuite
%defattr(-,root,root,-)
%{_libdir}/dtrace/testsuite
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces
2026-08-12 13:22 [PATCH v4 0/9] Add python, PCP support via libdtrace interfaces Alan Maguire
` (8 preceding siblings ...)
2026-08-12 13:22 ` [PATCH v4 9/9] dtrace.spec: Add optional PCP PMDA packaging pcp-pmda-dtrace Alan Maguire
@ 2026-08-13 21:01 ` Kris Van Hees
9 siblings, 0 replies; 11+ messages in thread
From: Kris Van Hees @ 2026-08-13 21:01 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
As a preliminary review, here is what Codex has to say about the series.
I run every posted series through it as a first level of review, and to
evaluate how valid its findings are.
Here goes:
• Found four issues in the nine commits:
- P1 — --without python RPM builds still force Python bindings.
dtrace.spec:174 and :190 use %{?with_python:...}; %bcond_without python
defines that macro even when its value is 0. Thus a Python-disabled build
still runs WITH_PYTHON=y/install-python, despite omitting Python build
requirements. Use %if %{with python} around those arguments.
- P1 — Dynamic PMDA aggregation instances cannot recover after disappearing.
pcp/pmdadtrace.python:560 removes the instance from the PMDA indom and ID
map but leaves _data_instances[instance]. When that aggregation key returns,
pcp/pmdadtrace.python:541 returns early, so it is never re-added and fetches
yield no value. Remove the name-map entry too, or restore its indom/ID
mapping.
- P2 — Changing the PMDA domain as the installation instructions require has
no effect. pcp/pmdadtrace.python:1219 parses domain= from Install, but pcp/
pmdadtrace.python:1228 instantiates the PMDA with hard-coded 487. A
collision workaround described in the README leaves pmcd and metric PMIDs on
different domains. Pass domain instead.
- P2 — The multiple-session regression test does not close the sessions it
creates. test/unittest/python/tst.multi-session.sh:50 loops over j but
always closes dts[i], excludes the final session, and leaves the others
open. It therefore fails to test the close/refcount lifecycle that the
preceding BTF-sharing commit changes.
On Wed, Aug 12, 2026 at 02:22:01PM +0100, Alan Maguire wrote:
> Having python bindings for libdtrace interfaces to compile, run
> and collect information from DTrace programs is valuable because
> it could help integration into metric collection frameworks like
> PCP. This series adds python bindings for DTrace (patch 4)
> and provides tests for them (patch 7). The support is
> similar to what cmd/dtrace.c can do; compile a program,
> enable probes, run it and collect data. Support is also
> added to grab or create processes as is done by
> dtrace -p, -c options. Aggregation snapshot walk is also
> supported, with aggregation values filled out as raw values,
> ints, lists of function names for stack keys and dicts for
> quantized aggregations.
>
> For detailed description/usage see the README in patch 4 and the
> tests.
>
> Patch 8 then provides a consumer of python support - a Performance
> Co-Pilot (PCP) Performance Metric Data Agent (PMDA) which enables
> flexible metric collection via DTrace scripts. See the README.md
> in patch 8 for more details.
>
> In order to support an environment where we have multiple handles
> per process, some prep work is required. Patch 1 fixes some issues
> with multiple handle use, while patch 2 improves sharing for
> vmlinux BTF (and CTF generated from it if needed) which multiple
> handles in a process image will benefit from. The upper bound
> of per-process handles is now limited by the maximum number of
> BPF programs attachable to a uprobe which is 64 (BPF_TRACE_MAX_PROGS)
> which is encountered with attaches to the BEGIN probe.
>
> Patch 3 makes available some of the functions used in calculating
> stddev() values which is useful for the python bindings since they
> do not use the print callbacks to do this.
>
> Patch 4 is the cpython bindings themselves; the README.md describes
> their usage. Patch 5 packages them and patches 6 and 7 facilitate
> testing them.
>
> Patch 8 then builds the PCP PMDA using the python bindings while
> patch 9 packages them in a pcp-pmda-dtrace package.
>
> Changes since v3:
>
> - Fixed up issues identified by Kris around custom BTF path (patch 2)
> - Added PCP patches 8/9
>
> Changes since v2:
>
> - Fixed some handle lifetime issues and related potential segfaults
> in error paths (patch 4)
> - Fixed up packaging to derive python package version from DTrace
> version (patch 5)
>
> Changes since v1:
>
> - Fixed multi-handle issues (patches 1/2)
> - Improved aggregation representation of stacks, added stddev and
> *quantize representations (patch 4)
>
> Alan Maguire (9):
> libdtrace: Support multiple DTrace handles per process
> libdtrace: share vmlinux BTF/CTF globally to support faster startup
> libdtrace: Refactor math functions into dt_math.h
> python: Add cpython bindings for libdtrace
> dtrace.spec: add python bindings packaging
> runtest.sh: Export PYTHONPATH when running tests in-tree
> test: add tests for python bindings
> dtrace: Add a PCP PMDA to expose DTrace data as metrics
> dtrace.spec: Add optional PCP PMDA packaging pcp-pmda-dtrace
>
> GNUmakefile | 2 +
> bindings/Build | 35 +
> bindings/python/README.md | 167 ++
> bindings/python/pyproject.toml | 3 +
> bindings/python/setup.py | 67 +
> bindings/python/src/pydtrace_module.c | 1997 +++++++++++++++++
> configure | 4 +-
> dtrace.spec | 58 +-
> libdtrace/dt_aggregate.c | 1 +
> libdtrace/dt_bpf.c | 6 +-
> libdtrace/dt_btf.c | 79 +-
> libdtrace/dt_btf.h | 1 +
> libdtrace/dt_consume.c | 340 +--
> libdtrace/dt_impl.h | 7 +-
> libdtrace/dt_math.h | 358 +++
> libdtrace/dt_open.c | 5 +-
> libdtrace/dt_printf.c | 1 +
> libdtrace/dt_prov_dtrace.c | 6 +-
> pcp/Build | 56 +
> pcp/Install | 37 +
> pcp/README.md | 260 +++
> pcp/Remove | 27 +
> pcp/autostart.d/.gitkeep | 0
> pcp/dtrace.conf | 12 +
> pcp/examples/packet_drop_reasons.d | 14 +
> pcp/examples/packet_drop_reasons.json | 8 +
> pcp/examples/syscall_counts.d | 11 +
> pcp/examples/syscall_counts.json | 8 +
> pcp/pmdadtrace.python | 1232 ++++++++++
> runtest.sh | 2 +
> test/unittest/python/tst.aggr-actions.sh | 133 ++
> test/unittest/python/tst.aggr-change.sh | 75 +
> test/unittest/python/tst.aggr-stack.sh | 61 +
> test/unittest/python/tst.aggr.sh | 58 +
> test/unittest/python/tst.exit.sh | 64 +
> test/unittest/python/tst.multi-session-end.sh | 75 +
> test/unittest/python/tst.multi-session.sh | 58 +
> test/unittest/python/tst.proc-create.sh | 56 +
> test/unittest/python/tst.proc-exit.sh | 57 +
> test/unittest/python/tst.proc-grab.sh | 81 +
> 40 files changed, 5165 insertions(+), 357 deletions(-)
> create mode 100644 bindings/Build
> create mode 100644 bindings/python/README.md
> create mode 100644 bindings/python/pyproject.toml
> create mode 100644 bindings/python/setup.py
> create mode 100644 bindings/python/src/pydtrace_module.c
> create mode 100644 libdtrace/dt_math.h
> create mode 100644 pcp/Build
> create mode 100755 pcp/Install
> create mode 100644 pcp/README.md
> create mode 100755 pcp/Remove
> create mode 100644 pcp/autostart.d/.gitkeep
> create mode 100644 pcp/dtrace.conf
> create mode 100644 pcp/examples/packet_drop_reasons.d
> create mode 100644 pcp/examples/packet_drop_reasons.json
> create mode 100644 pcp/examples/syscall_counts.d
> create mode 100644 pcp/examples/syscall_counts.json
> create mode 100755 pcp/pmdadtrace.python
> create mode 100755 test/unittest/python/tst.aggr-actions.sh
> create mode 100755 test/unittest/python/tst.aggr-change.sh
> create mode 100755 test/unittest/python/tst.aggr-stack.sh
> create mode 100755 test/unittest/python/tst.aggr.sh
> create mode 100755 test/unittest/python/tst.exit.sh
> create mode 100755 test/unittest/python/tst.multi-session-end.sh
> create mode 100755 test/unittest/python/tst.multi-session.sh
> create mode 100755 test/unittest/python/tst.proc-create.sh
> create mode 100755 test/unittest/python/tst.proc-exit.sh
> create mode 100755 test/unittest/python/tst.proc-grab.sh
>
> --
> 2.43.5
>
^ permalink raw reply [flat|nested] 11+ messages in thread