* [PATCH v3 0/7] Add python bindings for libdtrace interfaces
@ 2026-07-21 15:24 Alan Maguire
2026-07-21 15:24 ` [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process Alan Maguire
` (6 more replies)
0 siblings, 7 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
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.
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.
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)
- Expanded tests to cover multi-handle, aggregation value validation
(patch 7)
Alan Maguire (7):
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
GNUmakefile | 2 +
bindings/Build | 35 +
bindings/python/README.md | 167 ++
bindings/python/pyproject.toml | 3 +
bindings/python/setup.py | 66 +
bindings/python/src/pydtrace_module.c | 1932 +++++++++++++++++++++
configure | 4 +-
dtrace.spec | 29 +-
libdtrace/dt_aggregate.c | 1 +
libdtrace/dt_bpf.c | 6 +-
libdtrace/dt_btf.c | 61 +-
libdtrace/dt_btf.h | 1 +
libdtrace/dt_consume.c | 340 +---
libdtrace/dt_impl.h | 3 +-
libdtrace/dt_math.h | 358 ++++
libdtrace/dt_open.c | 5 +-
libdtrace/dt_printf.c | 1 +
libdtrace/dt_prov_dtrace.c | 6 +-
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.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 +
28 files changed, 3309 insertions(+), 356 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 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.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] 10+ messages in thread
* [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-22 21:09 ` Kris Van Hees
2026-07-21 15:24 ` [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
` (5 subsequent siblings)
6 siblings, 1 reply; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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] 10+ messages in thread
* [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
2026-07-21 15:24 ` [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-22 21:21 ` Kris Van Hees
2026-07-21 15:24 ` [PATCH v3 3/7] libdtrace: Refactor math functions into dt_math.h Alan Maguire
` (4 subsequent siblings)
6 siblings, 1 reply; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_btf.c | 61 ++++++++++++++++++++++++++++++++++++++++++---
libdtrace/dt_btf.h | 1 +
libdtrace/dt_open.c | 5 +---
3 files changed, 60 insertions(+), 7 deletions(-)
diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
index 1f793397..e46336f9 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,56 @@ out:
}
#endif
+static dt_btf_t *dt_shared_btf = NULL;
+static ctf_dict_t *dt_shared_ctf = NULL;
+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)
+{
+ pthread_mutex_lock(&dt_shared_mutex);
+ if (dtp->dt_shared_btf && dtp->dt_shared_btf == dt_shared_btf) {
+ if (--dt_shared_refcnt == 0) {
+ dt_btf_destroy(dtp, dtp->dt_shared_btf);
+ dtp->dt_shared_btf = dt_shared_btf = NULL;
+ }
+ }
+#ifdef HAVE_LIBCTF
+ if (dt_shared_refcnt == 0) {
+ ctf_close(dtp->dt_shared_ctf);
+ dtp->dt_shared_ctf = dt_shared_ctf = NULL;
+ }
+#endif
+ pthread_mutex_unlock(&dt_shared_mutex);
+}
+
dt_btf_t *
dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
{
@@ -774,6 +827,11 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
if (dmp->dm_btf)
return dmp->dm_btf;
+ if (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.
@@ -792,9 +850,6 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
btf = dt_btf_load_file(dtp, fn);
free(fn);
- if (btf && !dtp->dt_shared_btf && strcmp(dmp->dm_name, "vmlinux") == 0)
- dtp->dt_shared_btf = btf;
-
return dmp->dm_btf = 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_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] 10+ messages in thread
* [PATCH v3 3/7] libdtrace: Refactor math functions into dt_math.h
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
2026-07-21 15:24 ` [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process Alan Maguire
2026-07-21 15:24 ` [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-21 15:24 ` [PATCH v3 4/7] python: Add cpython bindings for libdtrace Alan Maguire
` (3 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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 7db2ba93..540adfed 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] 10+ messages in thread
* [PATCH v3 4/7] python: Add cpython bindings for libdtrace
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
` (2 preceding siblings ...)
2026-07-21 15:24 ` [PATCH v3 3/7] libdtrace: Refactor math functions into dt_math.h Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-21 15:24 ` [PATCH v3 5/7] dtrace.spec: add python bindings packaging Alan Maguire
` (2 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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 | 66 +
bindings/python/src/pydtrace_module.c | 1932 +++++++++++++++++++++++++
configure | 4 +-
7 files changed, 2208 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..6aa00d2e
--- /dev/null
+++ b/bindings/python/setup.py
@@ -0,0 +1,66 @@
+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 / "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..2350816f
--- /dev/null
+++ b/bindings/python/src/pydtrace_module.c
@@ -0,0 +1,1932 @@
+/*
+ * 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);
+}
+
+/* ------------------------------------------------------------------------- */
+/* 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((int)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 *tmp = 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;
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_aggregates);
+ if (tmp == NULL || PyDict_SetItemString(result, "aggregations", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_recgens);
+ if (tmp == NULL || PyDict_SetItemString(result, "recgens", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_matches);
+ if (tmp == NULL || PyDict_SetItemString(result, "matches", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_speculations);
+ if (tmp == NULL || PyDict_SetItemString(result, "speculations", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_descattr.dtat_name);
+ if (tmp == NULL || PyDict_SetItemString(attr, "name", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_descattr.dtat_data);
+ if (tmp == NULL || PyDict_SetItemString(attr, "data", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_descattr.dtat_class);
+ if (tmp == NULL || PyDict_SetItemString(attr, "class", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ if (PyDict_SetItemString(result, "descattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_stmtattr.dtat_name);
+ if (tmp == NULL || PyDict_SetItemString(attr, "name", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_stmtattr.dtat_data);
+ if (tmp == NULL || PyDict_SetItemString(attr, "data", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ tmp = PyLong_FromUnsignedLong(info.dpi_stmtattr.dtat_class);
+ if (tmp == NULL || PyDict_SetItemString(attr, "class", tmp) < 0)
+ goto enable_error;
+ Py_DECREF(tmp);
+
+ if (PyDict_SetItemString(result, "stmtattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ return result;
+
+enable_error:
+ Py_XDECREF(attr);
+ Py_XDECREF(tmp);
+ 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 (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_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] 10+ messages in thread
* [PATCH v3 5/7] dtrace.spec: add python bindings packaging
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
` (3 preceding siblings ...)
2026-07-21 15:24 ` [PATCH v3 4/7] python: Add cpython bindings for libdtrace Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-21 15:24 ` [PATCH v3 6/7] runtest.sh: Export PYTHONPATH when running tests in-tree Alan Maguire
2026-07-21 15:24 ` [PATCH v3 7/7] test: add tests for python bindings Alan Maguire
6 siblings, 0 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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] 10+ messages in thread
* [PATCH v3 6/7] runtest.sh: Export PYTHONPATH when running tests in-tree
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
` (4 preceding siblings ...)
2026-07-21 15:24 ` [PATCH v3 5/7] dtrace.spec: add python bindings packaging Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
2026-07-21 15:24 ` [PATCH v3 7/7] test: add tests for python bindings Alan Maguire
6 siblings, 0 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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] 10+ messages in thread
* [PATCH v3 7/7] test: add tests for python bindings
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
` (5 preceding siblings ...)
2026-07-21 15:24 ` [PATCH v3 6/7] runtest.sh: Export PYTHONPATH when running tests in-tree Alan Maguire
@ 2026-07-21 15:24 ` Alan Maguire
6 siblings, 0 replies; 10+ messages in thread
From: Alan Maguire @ 2026-07-21 15:24 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.
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.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 +++++++++++++
9 files changed, 643 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.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.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] 10+ messages in thread
* Re: [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process
2026-07-21 15:24 ` [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process Alan Maguire
@ 2026-07-22 21:09 ` Kris Van Hees
0 siblings, 0 replies; 10+ messages in thread
From: Kris Van Hees @ 2026-07-22 21:09 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
On Tue, Jul 21, 2026 at 04:24:45PM +0100, Alan Maguire wrote:
> 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;
ACK.
>
> #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 */
ACK.
> };
>
> /*
> 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;
ACK, but I do wonder whether the use case of multiple handles in a process
may cause trouble when one of those handles tries to remove the uprobe for
BEGIN/END? This really should have a test case that shows that two handles
can be operative at the same time, and also a test that shows that closing one
while keeping the other open does not stop the second one from seeing its own
END probe firing. Ideally, it would be two test cases:
- one that creates handles A and B, and then closes A and checks B still works
- one that creates handles A and B, and then closes B and checks A still works
>
> /* open format file */
> --
> 2.43.5
>
>
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup
2026-07-21 15:24 ` [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
@ 2026-07-22 21:21 ` Kris Van Hees
0 siblings, 0 replies; 10+ messages in thread
From: Kris Van Hees @ 2026-07-22 21:21 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
On Tue, Jul 21, 2026 at 04:24:46PM +0100, Alan Maguire wrote:
> 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.
There are a few logic issues here that need to be address I think. Since we
do support -xbtfpath and DTRACE_OPT_BTFPATH, hardcoding the path to vmlinux
is an issue.
Ideally, this code should allow for both shared and private type data. I.e.
if a -xbtfpath opion is given or DTRACE_OPT_BTFPATH is set, then the instance
will use a private copy. Otherwise, it will use a shared one that is sourced
from the system default.
More fancy would be to allow multiple shared ones, keyed by full path (and
considering that it will be very very rare to ever have more than one).
So, I would suggest going with the first option (shared and private), and the
close code can check whether dtp->dt_shared_btf == dt_shared_btf. If so, it
is a hared one so handle it that way. If not, clean up as a private copy.
> Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
> ---
> libdtrace/dt_btf.c | 61 ++++++++++++++++++++++++++++++++++++++++++---
> libdtrace/dt_btf.h | 1 +
> libdtrace/dt_open.c | 5 +---
> 3 files changed, 60 insertions(+), 7 deletions(-)
>
> diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
> index 1f793397..e46336f9 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,56 @@ out:
> }
> #endif
>
> +static dt_btf_t *dt_shared_btf = NULL;
> +static ctf_dict_t *dt_shared_ctf = NULL;
> +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)
> +{
> + pthread_mutex_lock(&dt_shared_mutex);
> + if (dtp->dt_shared_btf && dtp->dt_shared_btf == dt_shared_btf) {
> + if (--dt_shared_refcnt == 0) {
> + dt_btf_destroy(dtp, dtp->dt_shared_btf);
> + dtp->dt_shared_btf = dt_shared_btf = NULL;
> + }
> + }
> +#ifdef HAVE_LIBCTF
> + if (dt_shared_refcnt == 0) {
> + ctf_close(dtp->dt_shared_ctf);
> + dtp->dt_shared_ctf = dt_shared_ctf = NULL;
> + }
> +#endif
> + pthread_mutex_unlock(&dt_shared_mutex);
> +}
> +
> dt_btf_t *
> dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
> {
> @@ -774,6 +827,11 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
> if (dmp->dm_btf)
> return dmp->dm_btf;
>
> + if (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.
> @@ -792,9 +850,6 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
> btf = dt_btf_load_file(dtp, fn);
> free(fn);
>
> - if (btf && !dtp->dt_shared_btf && strcmp(dmp->dm_name, "vmlinux") == 0)
> - dtp->dt_shared_btf = btf;
> -
> return dmp->dm_btf = 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_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 [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-07-22 21:21 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-21 15:24 [PATCH v3 0/7] Add python bindings for libdtrace interfaces Alan Maguire
2026-07-21 15:24 ` [PATCH v3 1/7] libdtrace: Support multiple DTrace handles per process Alan Maguire
2026-07-22 21:09 ` Kris Van Hees
2026-07-21 15:24 ` [PATCH v3 2/7] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
2026-07-22 21:21 ` Kris Van Hees
2026-07-21 15:24 ` [PATCH v3 3/7] libdtrace: Refactor math functions into dt_math.h Alan Maguire
2026-07-21 15:24 ` [PATCH v3 4/7] python: Add cpython bindings for libdtrace Alan Maguire
2026-07-21 15:24 ` [PATCH v3 5/7] dtrace.spec: add python bindings packaging Alan Maguire
2026-07-21 15:24 ` [PATCH v3 6/7] runtest.sh: Export PYTHONPATH when running tests in-tree Alan Maguire
2026-07-21 15:24 ` [PATCH v3 7/7] test: add tests for python bindings Alan Maguire
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox