* [PATCH v5 01/11] libdtrace: Support multiple DTrace handles per process
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-07 9:26 ` Kris Van Hees
2026-08-21 16:10 ` [PATCH v5 02/11] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
` (9 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 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] 18+ messages in thread* Re: [PATCH v5 01/11] libdtrace: Support multiple DTrace handles per process
2026-08-21 16:10 ` [PATCH v5 01/11] libdtrace: Support multiple DTrace handles per process Alan Maguire
@ 2026-10-07 9:26 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-07 9:26 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
On Fri, Aug 21, 2026 at 05:10:49PM +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>
Reviewed-by: Kris Van Hees <kris.van.hees@oracle.com>
... with a small change, renaming dt_gmap_done to dt_gmaps_done (which I
should have done rom the beginning), and moving the dt_gmaps_done member
in dtrace_hdl_t at the end of the other booleans rather than putting it at
the very end of the struct.
Thanks for the patch - it certainly fixes a problem I introduced when I
did the BPF port. Making libdtrace be able to handle multiple instances
again is important!
> ---
> 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 [flat|nested] 18+ messages in thread
* [PATCH v5 02/11] libdtrace: share vmlinux BTF/CTF globally to support faster startup
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
2026-08-21 16:10 ` [PATCH v5 01/11] libdtrace: Support multiple DTrace handles per process Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-07 10:05 ` Kris Van Hees
2026-08-21 16:10 ` [PATCH v5 03/11] libdtrace: avoid BTF probes for ranged built-in modules Alan Maguire
` (8 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
In a multiple DTrace session per process environment, it makes sense
to avoid doing expensive operations like reading in vmlinux BTF and
converting it to CTF more than once. Shared (vmlinux) BTF and CTF
generated from it are shared and reference-counted globally here
to cut down on cost. With this change, we can launch ~1 DTrace
session per second.
Note that we need to handle case where -xbtfpath/DTRACE_OPT_BTFPATH
are used as non-default BTF paths; in such cases global shared
BTF is not used in the handle.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_btf.c | 92 ++++++++++++++++++++++++++++++++++++++++++++-
libdtrace/dt_btf.h | 1 +
libdtrace/dt_impl.h | 4 +-
libdtrace/dt_open.c | 5 +--
4 files changed, 94 insertions(+), 8 deletions(-)
diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
index 1f793397..e7980969 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,81 @@ out:
}
#endif
+static dt_btf_t *dt_shared_btf = NULL;
+#ifdef HAVE_LIBCTF
+static ctf_dict_t *dt_shared_ctf = NULL;
+static int dt_shared_ctfmodel = -1;
+#endif
+static pthread_mutex_t dt_shared_mutex = PTHREAD_MUTEX_INITIALIZER;
+static int dt_shared_refcnt = 0;
+
+static int
+dt_shared_get(dtrace_hdl_t *dtp, dt_module_t *dmp)
+{
+ int rc = -1;
+
+ 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)
+ goto out;
+
+#ifdef HAVE_LIBCTF
+ /* BTF-to-CTF type IDs and CTF data models cannot be shared. */
+ if (dt_shared_ctf && dt_shared_ctfmodel != dtp->dt_conf.dtc_ctfmodel)
+ goto out;
+
+ if (!dt_shared_ctf) {
+ dt_shared_ctf = dt_btf_to_ctf(dtp, NULL, dt_shared_btf);
+ if (!dt_shared_ctf) {
+ dt_btf_destroy(dtp, dt_shared_btf);
+ dt_shared_btf = NULL;
+ goto out;
+ }
+ dt_shared_ctfmodel = dtp->dt_conf.dtc_ctfmodel;
+ }
+ dtp->dt_shared_ctf = dmp->dm_ctfp = dt_shared_ctf;
+#endif
+ dtp->dt_shared_btf = dmp->dm_btf = dt_shared_btf;
+ dt_shared_refcnt++;
+ rc = 0;
+out:
+ pthread_mutex_unlock(&dt_shared_mutex);
+ return rc;
+}
+
+void
+dt_shared_put(dtrace_hdl_t *dtp)
+{
+ int shared;
+
+ pthread_mutex_lock(&dt_shared_mutex);
+ shared = dtp->dt_shared_btf && dtp->dt_shared_btf == dt_shared_btf;
+ if (shared && --dt_shared_refcnt != 0)
+ goto out;
+
+ dt_btf_destroy(dtp, dtp->dt_shared_btf);
+ dtp->dt_shared_btf = NULL;
+ if (shared)
+ dt_shared_btf = NULL;
+
+#ifdef HAVE_LIBCTF
+ if (shared) {
+ if (dt_shared_ctf != NULL)
+ ctf_close(dt_shared_ctf);
+ dt_shared_ctf = NULL;
+ dt_shared_ctfmodel = -1;
+ } else {
+ if (dtp->dt_shared_ctf != NULL)
+ ctf_close(dtp->dt_shared_ctf);
+ }
+ dtp->dt_shared_ctf = NULL;
+#endif
+
+out:
+ pthread_mutex_unlock(&dt_shared_mutex);
+}
+
dt_btf_t *
dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
{
@@ -774,6 +852,13 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
if (dmp->dm_btf)
return dmp->dm_btf;
+ /* Share only the system-default vmlinux BTF. */
+ if (dtp->dt_btf_path == NULL &&
+ strcmp(dmp->dm_name, "vmlinux") == 0) {
+ if (dt_shared_get(dtp, dmp) == 0)
+ return dmp->dm_btf;
+ }
+
/*
* Default: /sys/kernel/btf/<module>
* If "none", disable BTF.
@@ -899,9 +984,12 @@ dt_btf_lookup_name_kind(dtrace_hdl_t *dtp, dt_module_t *dmp, const char *name,
if (strcmp(name, "void") == 0)
return 0;
- /* Ensure the shared BTF is loaded. */
- if (!dtp->dt_shared_btf)
+ /* Ensure the vmlinux BTF base is loaded before considering a module. */
+ if (!dtp->dt_shared_btf) {
dt_btf_load_module(dtp, dtp->dt_exec);
+ if (!dtp->dt_shared_btf)
+ return -ENOENT;
+ }
/* If the module does not have BTF data yet, try to load it. */
if (!btf) {
diff --git a/libdtrace/dt_btf.h b/libdtrace/dt_btf.h
index 2c921a03..b1ed2b65 100644
--- a/libdtrace/dt_btf.h
+++ b/libdtrace/dt_btf.h
@@ -18,6 +18,7 @@ typedef struct dt_btf dt_btf_t;
typedef struct bpf_btf_info btf_info_t;
extern void dt_btf_destroy(dtrace_hdl_t *, dt_btf_t *);
+extern void dt_shared_put(dtrace_hdl_t *);
extern dt_btf_t *dt_btf_load_module(dtrace_hdl_t *, dt_module_t *);
extern ctf_dict_t *dt_btf_module_ctf(dtrace_hdl_t *, dt_module_t *);
extern const char *dt_btf_get_string(dtrace_hdl_t *, const dt_btf_t *,
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 7db2ba93..7ccdc271 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -305,8 +305,8 @@ struct dtrace_hdl {
char *dt_ctfa_path; /* path to vmlinux.ctfa */
ctf_archive_t *dt_ctfa; /* ctf archive for the entire kernel tree */
char *dt_btf_path; /* path to vmlinux.btf */
- struct dt_btf *dt_shared_btf; /* BTF data for the kernel (shared) */
- ctf_file_t *dt_shared_ctf; /* Handle to the shared CTF */
+ struct dt_btf *dt_shared_btf; /* BTF data shared by kernel modules */
+ ctf_file_t *dt_shared_ctf; /* CTF shared by kernel modules */
dt_htab_t *dt_kernpaths; /* hash table of dt_kern_path_t's */
dt_module_t *dt_exec; /* pointer to executable module */
dt_module_t *dt_cdefs; /* pointer to C dynamic type module */
diff --git a/libdtrace/dt_open.c b/libdtrace/dt_open.c
index 30b8758d..352cec49 100644
--- a/libdtrace/dt_open.c
+++ b/libdtrace/dt_open.c
@@ -1299,10 +1299,7 @@ dtrace_close(dtrace_hdl_t *dtp)
dt_htab_destroy(dtp->dt_mods);
dt_htab_destroy(dtp->dt_kernpaths);
- if (dtp->dt_shared_btf != NULL)
- dt_btf_destroy(dtp, dtp->dt_shared_btf);
- if (dtp->dt_shared_ctf != NULL)
- ctf_close(dtp->dt_shared_ctf);
+ dt_shared_put(dtp);
if (dtp->dt_ctfa != NULL)
ctf_arc_close(dtp->dt_ctfa);
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH v5 02/11] libdtrace: share vmlinux BTF/CTF globally to support faster startup
2026-08-21 16:10 ` [PATCH v5 02/11] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
@ 2026-10-07 10:05 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-07 10:05 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
First of all, this is great work and definitely a major improvement for
start-up time for DTrace.
That said, I am debating with myself whether to take it as is, or to do
more work on this. The point being that while your patch is certainly
an improvement, the code it deals with is in need of a major overhaul.
There are multiple problems with it, the biggest one being that DTrace
provides dtrace_update() as an API function, and its result is a reload
of all modules and their data (including BTF and CTF), but the shared
pointers in dtp are never updated to new versions. In practice that is
thankfully not a problem since the kernel is not getting changed from
underneath us (at least not unfil live update comes along). But it is
wrong either way.
There is also the point that the dtp->dt_shared_ctf and
dtp->dt_shared_btf pointers are just ugly, and shouldn't exist. It is
easy enough to just use the dmp->dm_ctf and dmp->dm_btf pointers from
the vmlinux module if the "shared" CTF or BTF is needed. Since the
shared version is mostly used for BTF because modules' BTF is an
extension of the kernel BTF, introducing a parent BTF pointer in
dt_module_t is probably better.
I had work going on in that area a few months ago, and I'll dig it up to
see if that is sufficiently complete to replace your patch and get the
same overall functionality or whether it needs more work.
Stay tuned...
On Fri, Aug 21, 2026 at 05:10:50PM +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.
>
> Note that we need to handle case where -xbtfpath/DTRACE_OPT_BTFPATH
> are used as non-default BTF paths; in such cases global shared
> BTF is not used in the handle.
>
> Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
> ---
> libdtrace/dt_btf.c | 92 ++++++++++++++++++++++++++++++++++++++++++++-
> libdtrace/dt_btf.h | 1 +
> libdtrace/dt_impl.h | 4 +-
> libdtrace/dt_open.c | 5 +--
> 4 files changed, 94 insertions(+), 8 deletions(-)
>
> diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
> index 1f793397..e7980969 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,81 @@ out:
> }
> #endif
>
> +static dt_btf_t *dt_shared_btf = NULL;
> +#ifdef HAVE_LIBCTF
> +static ctf_dict_t *dt_shared_ctf = NULL;
> +static int dt_shared_ctfmodel = -1;
> +#endif
> +static pthread_mutex_t dt_shared_mutex = PTHREAD_MUTEX_INITIALIZER;
> +static int dt_shared_refcnt = 0;
> +
> +static int
> +dt_shared_get(dtrace_hdl_t *dtp, dt_module_t *dmp)
> +{
> + int rc = -1;
> +
> + 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)
> + goto out;
> +
> +#ifdef HAVE_LIBCTF
> + /* BTF-to-CTF type IDs and CTF data models cannot be shared. */
> + if (dt_shared_ctf && dt_shared_ctfmodel != dtp->dt_conf.dtc_ctfmodel)
> + goto out;
> +
> + if (!dt_shared_ctf) {
> + dt_shared_ctf = dt_btf_to_ctf(dtp, NULL, dt_shared_btf);
> + if (!dt_shared_ctf) {
> + dt_btf_destroy(dtp, dt_shared_btf);
> + dt_shared_btf = NULL;
> + goto out;
> + }
> + dt_shared_ctfmodel = dtp->dt_conf.dtc_ctfmodel;
> + }
> + dtp->dt_shared_ctf = dmp->dm_ctfp = dt_shared_ctf;
> +#endif
> + dtp->dt_shared_btf = dmp->dm_btf = dt_shared_btf;
> + dt_shared_refcnt++;
> + rc = 0;
> +out:
> + pthread_mutex_unlock(&dt_shared_mutex);
> + return rc;
> +}
> +
> +void
> +dt_shared_put(dtrace_hdl_t *dtp)
> +{
> + int shared;
> +
> + pthread_mutex_lock(&dt_shared_mutex);
> + shared = dtp->dt_shared_btf && dtp->dt_shared_btf == dt_shared_btf;
> + if (shared && --dt_shared_refcnt != 0)
> + goto out;
> +
> + dt_btf_destroy(dtp, dtp->dt_shared_btf);
> + dtp->dt_shared_btf = NULL;
> + if (shared)
> + dt_shared_btf = NULL;
> +
> +#ifdef HAVE_LIBCTF
> + if (shared) {
> + if (dt_shared_ctf != NULL)
> + ctf_close(dt_shared_ctf);
> + dt_shared_ctf = NULL;
> + dt_shared_ctfmodel = -1;
> + } else {
> + if (dtp->dt_shared_ctf != NULL)
> + ctf_close(dtp->dt_shared_ctf);
> + }
> + dtp->dt_shared_ctf = NULL;
> +#endif
> +
> +out:
> + pthread_mutex_unlock(&dt_shared_mutex);
> +}
> +
> dt_btf_t *
> dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
> {
> @@ -774,6 +852,13 @@ dt_btf_load_module(dtrace_hdl_t *dtp, dt_module_t *dmp)
> if (dmp->dm_btf)
> return dmp->dm_btf;
>
> + /* Share only the system-default vmlinux BTF. */
> + if (dtp->dt_btf_path == NULL &&
> + strcmp(dmp->dm_name, "vmlinux") == 0) {
> + if (dt_shared_get(dtp, dmp) == 0)
> + return dmp->dm_btf;
> + }
> +
> /*
> * Default: /sys/kernel/btf/<module>
> * If "none", disable BTF.
> @@ -899,9 +984,12 @@ dt_btf_lookup_name_kind(dtrace_hdl_t *dtp, dt_module_t *dmp, const char *name,
> if (strcmp(name, "void") == 0)
> return 0;
>
> - /* Ensure the shared BTF is loaded. */
> - if (!dtp->dt_shared_btf)
> + /* Ensure the vmlinux BTF base is loaded before considering a module. */
> + if (!dtp->dt_shared_btf) {
> dt_btf_load_module(dtp, dtp->dt_exec);
> + if (!dtp->dt_shared_btf)
> + return -ENOENT;
> + }
>
> /* If the module does not have BTF data yet, try to load it. */
> if (!btf) {
> diff --git a/libdtrace/dt_btf.h b/libdtrace/dt_btf.h
> index 2c921a03..b1ed2b65 100644
> --- a/libdtrace/dt_btf.h
> +++ b/libdtrace/dt_btf.h
> @@ -18,6 +18,7 @@ typedef struct dt_btf dt_btf_t;
> typedef struct bpf_btf_info btf_info_t;
>
> extern void dt_btf_destroy(dtrace_hdl_t *, dt_btf_t *);
> +extern void dt_shared_put(dtrace_hdl_t *);
> extern dt_btf_t *dt_btf_load_module(dtrace_hdl_t *, dt_module_t *);
> extern ctf_dict_t *dt_btf_module_ctf(dtrace_hdl_t *, dt_module_t *);
> extern const char *dt_btf_get_string(dtrace_hdl_t *, const dt_btf_t *,
> diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
> index 7db2ba93..7ccdc271 100644
> --- a/libdtrace/dt_impl.h
> +++ b/libdtrace/dt_impl.h
> @@ -305,8 +305,8 @@ struct dtrace_hdl {
> char *dt_ctfa_path; /* path to vmlinux.ctfa */
> ctf_archive_t *dt_ctfa; /* ctf archive for the entire kernel tree */
> char *dt_btf_path; /* path to vmlinux.btf */
> - struct dt_btf *dt_shared_btf; /* BTF data for the kernel (shared) */
> - ctf_file_t *dt_shared_ctf; /* Handle to the shared CTF */
> + struct dt_btf *dt_shared_btf; /* BTF data shared by kernel modules */
> + ctf_file_t *dt_shared_ctf; /* CTF shared by kernel modules */
> dt_htab_t *dt_kernpaths; /* hash table of dt_kern_path_t's */
> dt_module_t *dt_exec; /* pointer to executable module */
> dt_module_t *dt_cdefs; /* pointer to C dynamic type module */
> diff --git a/libdtrace/dt_open.c b/libdtrace/dt_open.c
> index 30b8758d..352cec49 100644
> --- a/libdtrace/dt_open.c
> +++ b/libdtrace/dt_open.c
> @@ -1299,10 +1299,7 @@ dtrace_close(dtrace_hdl_t *dtp)
> dt_htab_destroy(dtp->dt_mods);
> dt_htab_destroy(dtp->dt_kernpaths);
>
> - if (dtp->dt_shared_btf != NULL)
> - dt_btf_destroy(dtp, dtp->dt_shared_btf);
> - if (dtp->dt_shared_ctf != NULL)
> - ctf_close(dtp->dt_shared_ctf);
> + dt_shared_put(dtp);
> if (dtp->dt_ctfa != NULL)
> ctf_arc_close(dtp->dt_ctfa);
>
> --
> 2.43.5
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v5 03/11] libdtrace: avoid BTF probes for ranged built-in modules
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
2026-08-21 16:10 ` [PATCH v5 01/11] libdtrace: Support multiple DTrace handles per process Alan Maguire
2026-08-21 16:10 ` [PATCH v5 02/11] libdtrace: share vmlinux BTF/CTF globally to support faster startup Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-07 10:20 ` Kris Van Hees
2026-08-21 16:10 ` [PATCH v5 04/11] libdtrace: Add aggregation key/value computation APIs Alan Maguire
` (7 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
When /proc/kallsyms is used with modules.builtin.ranges, libdtrace can
attribute symbols linked into vmlinux to their individual built-in objects,
such as bitrev or atomic64_test. This creates module objects for those
names.
These objects have no per-module BTF file under /sys/kernel/btf. Avoid
probing for one when modules.builtin.ranges has identified the module as
built in, and use the shared vmlinux BTF directly instead.
Keep the existing lookup path for loadable modules and for an explicitly
configured BTF directory.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_btf.c | 8 ++++++--
libdtrace/dt_impl.h | 1 +
libdtrace/dt_module.c | 20 +++++++++++++++++++-
3 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
index e7980969..93ecf5cd 100644
--- a/libdtrace/dt_btf.c
+++ b/libdtrace/dt_btf.c
@@ -915,8 +915,12 @@ dt_btf_module_ctf(dtrace_hdl_t *dtp, dt_module_t *dmp)
if (!dtp->dt_shared_ctf && dtp->dt_shared_btf && btf == dtp->dt_shared_btf)
dtp->dt_shared_ctf = ctf;
- dt_dprintf("Generated %s CTF from BTF (%d types).\n", dmp->dm_name,
- btf->type_cnt);
+ if (btf == dtp->dt_shared_btf && strcmp(dmp->dm_name, "vmlinux") != 0)
+ dt_dprintf("Created %s CTF import from shared vmlinux BTF.\n",
+ dmp->dm_name);
+ else
+ dt_dprintf("Generated %s CTF from BTF (%d types).\n", dmp->dm_name,
+ btf->type_cnt);
#endif
return ctf;
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 7ccdc271..38c5ff0b 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -201,6 +201,7 @@ typedef struct dt_kern_path {
#define DT_DM_KERNEL 0x2 /* module is associated with a kernel object */
#define DT_DM_CTF_ARCHIVED 0x4 /* module found in a CTF archive */
#define DT_DM_KERN_UNLOADED 0x8 /* module not loaded into the kernel */
+#define DT_DM_KERN_BUILTIN 0x10 /* module is linked into vmlinux */
/*
* Why do we need (only) 4 slots? The maximum amount of string arguments to
diff --git a/libdtrace/dt_module.c b/libdtrace/dt_module.c
index 00978911..ea6d56c3 100644
--- a/libdtrace/dt_module.c
+++ b/libdtrace/dt_module.c
@@ -871,6 +871,19 @@ dt_kern_module_find_btf(dtrace_hdl_t *dtp, dt_module_t *dmp)
if (dtp->dt_shared_btf == NULL && strcmp(dmp->dm_name, "vmlinux") != 0)
return;
+ /*
+ * modules.builtin.ranges identifies this object as linked into vmlinux.
+ * Such objects cannot have a per-module BTF object, so use the shared
+ * vmlinux BTF directly. Retain the usual lookup for an explicitly
+ * configured BTF directory and every other module.
+ */
+ if (dtp->dt_btf_path == NULL && (dmp->dm_flags & DT_DM_KERN_BUILTIN)) {
+ dmp->dm_btf = dtp->dt_shared_btf;
+ dt_dprintf("Using shared vmlinux BTF for built-in module %s.\n",
+ dmp->dm_name);
+ return;
+ }
+
dt_dprintf("Loading BTF for module %s.\n", dmp->dm_name);
/*
@@ -1405,6 +1418,7 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
{
dt_kallsym_t sym;
char *modname;
+ int builtin = 0;
if ((line[0] == '\n') || (line[0] == 0))
return 0;
@@ -1440,8 +1454,10 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
char *s;
s = dt_kasstate_modname(state, sym.addr, sym.name);
- if (s != NULL)
+ if (s != NULL) {
strncpy(sym.mod, s, PATH_MAX);
+ builtin = 1;
+ }
}
}
@@ -1511,6 +1527,8 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
if (err != 0)
return err;
}
+ if (builtin)
+ dmp->dm_flags |= DT_DM_KERN_BUILTIN;
err = dt_modsym_addsym(dtp, dmp, &sym, state);
if (err != 0)
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH v5 03/11] libdtrace: avoid BTF probes for ranged built-in modules
2026-08-21 16:10 ` [PATCH v5 03/11] libdtrace: avoid BTF probes for ranged built-in modules Alan Maguire
@ 2026-10-07 10:20 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-07 10:20 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
Some questions below.
On Fri, Aug 21, 2026 at 05:10:51PM +0100, Alan Maguire wrote:
> When /proc/kallsyms is used with modules.builtin.ranges, libdtrace can
> attribute symbols linked into vmlinux to their individual built-in objects,
> such as bitrev or atomic64_test. This creates module objects for those
> names.
>
> These objects have no per-module BTF file under /sys/kernel/btf. Avoid
> probing for one when modules.builtin.ranges has identified the module as
> built in, and use the shared vmlinux BTF directly instead.
>
> Keep the existing lookup path for loadable modules and for an explicitly
> configured BTF directory.
>
> Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
> ---
> libdtrace/dt_btf.c | 8 ++++++--
> libdtrace/dt_impl.h | 1 +
> libdtrace/dt_module.c | 20 +++++++++++++++++++-
> 3 files changed, 26 insertions(+), 3 deletions(-)
>
> diff --git a/libdtrace/dt_btf.c b/libdtrace/dt_btf.c
> index e7980969..93ecf5cd 100644
> --- a/libdtrace/dt_btf.c
> +++ b/libdtrace/dt_btf.c
> @@ -915,8 +915,12 @@ dt_btf_module_ctf(dtrace_hdl_t *dtp, dt_module_t *dmp)
> if (!dtp->dt_shared_ctf && dtp->dt_shared_btf && btf == dtp->dt_shared_btf)
> dtp->dt_shared_ctf = ctf;
>
> - dt_dprintf("Generated %s CTF from BTF (%d types).\n", dmp->dm_name,
> - btf->type_cnt);
> + if (btf == dtp->dt_shared_btf && strcmp(dmp->dm_name, "vmlinux") != 0)
> + dt_dprintf("Created %s CTF import from shared vmlinux BTF.\n",
> + dmp->dm_name);
> + else
> + dt_dprintf("Generated %s CTF from BTF (%d types).\n", dmp->dm_name,
> + btf->type_cnt);
> #endif
>
> return ctf;
> diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
> index 7ccdc271..38c5ff0b 100644
> --- a/libdtrace/dt_impl.h
> +++ b/libdtrace/dt_impl.h
> @@ -201,6 +201,7 @@ typedef struct dt_kern_path {
> #define DT_DM_KERNEL 0x2 /* module is associated with a kernel object */
> #define DT_DM_CTF_ARCHIVED 0x4 /* module found in a CTF archive */
> #define DT_DM_KERN_UNLOADED 0x8 /* module not loaded into the kernel */
> +#define DT_DM_KERN_BUILTIN 0x10 /* module is linked into vmlinux */
>
> /*
> * Why do we need (only) 4 slots? The maximum amount of string arguments to
> diff --git a/libdtrace/dt_module.c b/libdtrace/dt_module.c
> index 00978911..ea6d56c3 100644
> --- a/libdtrace/dt_module.c
> +++ b/libdtrace/dt_module.c
> @@ -871,6 +871,19 @@ dt_kern_module_find_btf(dtrace_hdl_t *dtp, dt_module_t *dmp)
> if (dtp->dt_shared_btf == NULL && strcmp(dmp->dm_name, "vmlinux") != 0)
> return;
>
> + /*
> + * modules.builtin.ranges identifies this object as linked into vmlinux.
> + * Such objects cannot have a per-module BTF object, so use the shared
> + * vmlinux BTF directly. Retain the usual lookup for an explicitly
> + * configured BTF directory and every other module.
> + */
> + if (dtp->dt_btf_path == NULL && (dmp->dm_flags & DT_DM_KERN_BUILTIN)) {
> + dmp->dm_btf = dtp->dt_shared_btf;
> + dt_dprintf("Using shared vmlinux BTF for built-in module %s.\n",
> + dmp->dm_name);
> + return;
> + }
This does not look right to me, because if a dt_btf_path is specified, I
would expect the handle to still provided a handle-wide shared CTF and
BTF, even if it may be different from the system-wide shared CTF and
BTF. In other words, I think that sharing should be based on:
System standard CTF and BTF -> dt_shared_ctf and dt_share_btf globals
Non-standard CTF or BTF -> dtp->dt_shared_ctf and dtp->dt_shared_btf
So dropping the dtp->dt_btf_path == NULL in the conditional clause above
should be fine, right?
> +
> dt_dprintf("Loading BTF for module %s.\n", dmp->dm_name);
>
> /*
> @@ -1405,6 +1418,7 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
> {
> dt_kallsym_t sym;
> char *modname;
> + int builtin = 0;
>
> if ((line[0] == '\n') || (line[0] == 0))
> return 0;
> @@ -1440,8 +1454,10 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
> char *s;
>
> s = dt_kasstate_modname(state, sym.addr, sym.name);
> - if (s != NULL)
> + if (s != NULL) {
> strncpy(sym.mod, s, PATH_MAX);
> + builtin = 1;
> + }
> }
> }
>
> @@ -1511,6 +1527,8 @@ dt_modsym_update(dtrace_hdl_t *dtp, const char *line, dt_kasstate_t *state)
> if (err != 0)
> return err;
> }
> + if (builtin)
> + dmp->dm_flags |= DT_DM_KERN_BUILTIN;
>
> err = dt_modsym_addsym(dtp, dmp, &sym, state);
> if (err != 0)
> --
> 2.43.5
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v5 04/11] libdtrace: Add aggregation key/value computation APIs
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (2 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 03/11] libdtrace: avoid BTF probes for ranged built-in modules Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-07 10:51 ` Kris Van Hees
2026-08-21 16:10 ` [PATCH v5 05/11] python: Add cpython bindings for libdtrace Alan Maguire
` (6 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
To provide a way for consumers to emit aggregation keys
and compute aggregation values, provide new APIs in libdtrace:
int dtrace_aggregate_value(const dtrace_aggdata_t *aggdata,
dtrace_aggvalue_t *value);
int dtrace_aggregate_keys(const dtrace_aggdata_t *data,
dtrace_aggkey_t *keys, size_t num_keys);
void dtrace_aggregate_key_free(dtrace_aggkey_t *key);
These provide a way to compute aggregation values from snapshots,
print them and free the representation used to print them.
This allows consumers like python bindings to avoid having to
understand aggregation internals.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
libdtrace/dt_aggregate.c | 517 +++++++++++++++++++++++++++++++++++++++
libdtrace/dtrace.h | 83 +++++++
libdtrace/libdtrace.ver | 3 +
3 files changed, 603 insertions(+)
diff --git a/libdtrace/dt_aggregate.c b/libdtrace/dt_aggregate.c
index 1fc8294d..afc6a01a 100644
--- a/libdtrace/dt_aggregate.c
+++ b/libdtrace/dt_aggregate.c
@@ -13,11 +13,15 @@
#include <assert.h>
#include <alloca.h>
#include <limits.h>
+#include <stdio.h>
+#include <string.h>
#include <libproc.h>
#include <port.h>
#include <dt_aggregate.h>
#include <dt_bpf.h>
+#include <math.h>
+#include <limits.h>
typedef struct dt_ahashent {
struct dt_ahashent *dtahe_prev; /* prev on hash chain */
@@ -44,6 +48,519 @@ struct dt_aggregate {
#define DTRACE_AHASHSIZE 32779 /* big 'ol prime */
+static int
+dt_aggregate_read_signed(const char *addr, size_t size, int64_t *value)
+{
+ int8_t v8;
+ int16_t v16;
+ int32_t v32;
+ int64_t v64;
+
+ switch (size) {
+ case sizeof(v8):
+ memcpy(&v8, addr, sizeof(v8));
+ *value = v8;
+ return 0;
+ case sizeof(v16):
+ memcpy(&v16, addr, sizeof(v16));
+ *value = v16;
+ return 0;
+ case sizeof(v32):
+ memcpy(&v32, addr, sizeof(v32));
+ *value = v32;
+ return 0;
+ case sizeof(v64):
+ memcpy(&v64, addr, sizeof(v64));
+ *value = v64;
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+static int
+dt_aggregate_read_unsigned(const char *addr, size_t size, uint64_t *value)
+{
+ uint8_t v8;
+ uint16_t v16;
+ uint32_t v32;
+ uint64_t v64;
+
+ switch (size) {
+ case sizeof(v8):
+ memcpy(&v8, addr, sizeof(v8));
+ *value = v8;
+ return 0;
+ case sizeof(v16):
+ memcpy(&v16, addr, sizeof(v16));
+ *value = v16;
+ return 0;
+ case sizeof(v32):
+ memcpy(&v32, addr, sizeof(v32));
+ *value = v32;
+ return 0;
+ case sizeof(v64):
+ memcpy(&v64, addr, sizeof(v64));
+ *value = v64;
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+int
+dtrace_aggregate_value(const dtrace_aggdata_t *aggdata, dtrace_aggvalue_t *value)
+{
+ const dtrace_aggdesc_t *agg;
+ const dtrace_recdesc_t *rec;
+ const char *addr;
+ uint64_t normal;
+
+ if (aggdata == NULL || aggdata->dtada_desc == NULL ||
+ aggdata->dtada_data == NULL || value == NULL)
+ return aggdata && aggdata->dtada_hdl ?
+ dt_set_errno(aggdata->dtada_hdl, EINVAL) : -1;
+
+ agg = aggdata->dtada_desc;
+ rec = &agg->dtagd_drecs[DT_AGGDATA_RECORD];
+ addr = aggdata->dtada_data + rec->dtrd_offset;
+ normal = agg->dtagd_normal ? agg->dtagd_normal : 1;
+ value->dtagv_action = rec->dtrd_action;
+ value->dtagv_normal = normal;
+ value->dtagv_bucket_count = 0;
+
+ switch (rec->dtrd_action) {
+ case DT_AGG_AVG: {
+ const int64_t *data = (const int64_t *)addr;
+
+ if (rec->dtrd_size < sizeof(*data) * 2)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ value->dtagv_kind = DTRACE_AGGVALUE_FLOAT;
+ value->dtagv_value.real = data[0] == 0 ? 0.0 :
+ (double)((long double)data[1] / normal / data[0]);
+ return 0;
+ }
+ case DT_AGG_STDDEV: {
+ uint64_t data[4];
+
+ if (rec->dtrd_size < sizeof(data))
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ memcpy(data, addr, sizeof(data));
+ value->dtagv_kind = DTRACE_AGGVALUE_UNSIGNED;
+ value->dtagv_value.unsigned_integer =
+ data[0] == 0 ? 0 : dt_stddev(data, normal);
+ return 0;
+ }
+ case DT_AGG_SUM:
+ case DT_AGG_MIN:
+ case DT_AGG_MAX:
+ value->dtagv_kind = DTRACE_AGGVALUE_INTEGER;
+ if (dt_aggregate_read_signed(addr, rec->dtrd_size,
+ &value->dtagv_value.integer) != 0)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ value->dtagv_value.integer /= (int64_t)normal;
+ return 0;
+ case DT_AGG_COUNT:
+ value->dtagv_kind = DTRACE_AGGVALUE_UNSIGNED;
+ if (dt_aggregate_read_unsigned(addr, rec->dtrd_size,
+ &value->dtagv_value.unsigned_integer) != 0)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ value->dtagv_value.unsigned_integer /= normal;
+ return 0;
+ case DT_AGG_QUANTIZE: {
+ const int64_t *data = (const int64_t *)addr;
+ size_t i, nbins = rec->dtrd_size / sizeof(*data);
+
+ if (rec->dtrd_size % sizeof(*data) != 0 ||
+ value->dtagv_buckets == NULL ||
+ value->dtagv_bucket_capacity < nbins)
+ return dt_set_errno(aggdata->dtada_hdl, EINVAL);
+ for (i = 0; i < nbins; i++) {
+ if (data[i] < 0)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ }
+ for (i = 0; i < nbins; i++) {
+ if (data[i] == 0)
+ continue;
+ value->dtagv_buckets[value->dtagv_bucket_count].bucket =
+ DTRACE_QUANTIZE_BUCKETVAL((uint64_t)i);
+ value->dtagv_buckets[value->dtagv_bucket_count].count =
+ (uint64_t)(data[i] / (int64_t)normal);
+ if (value->dtagv_buckets[value->dtagv_bucket_count].count != 0)
+ value->dtagv_bucket_count++;
+ }
+ value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
+ return 0;
+ }
+ case DT_AGG_LQUANTIZE: {
+ uint16_t levels = DTRACE_LQUANTIZE_LEVELS(agg->dtagd_sig);
+ uint16_t step = DTRACE_LQUANTIZE_STEP(agg->dtagd_sig);
+ int32_t base = DTRACE_LQUANTIZE_BASE(agg->dtagd_sig);
+ size_t i, nbins = (size_t)levels + 2;
+ const int64_t *data = (const int64_t *)addr;
+
+ if (rec->dtrd_size != nbins * sizeof(*data) ||
+ value->dtagv_buckets == NULL ||
+ value->dtagv_bucket_capacity < nbins)
+ return dt_set_errno(aggdata->dtada_hdl, EINVAL);
+ for (i = 0; i < nbins; i++) {
+ if (data[i] < 0)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ }
+ for (i = 0; i < nbins; i++) {
+ int64_t count = data[i] / (int64_t)normal;
+ int64_t bucket;
+
+ if (count == 0)
+ continue;
+ if (i == 0)
+ bucket = base - 1;
+ else if (i == levels + 1)
+ bucket = base + (int64_t)levels * step;
+ else
+ bucket = base + (int64_t)(i - 1) * step;
+ value->dtagv_buckets[value->dtagv_bucket_count++] =
+ (dtrace_aggvalue_bucket_t){ bucket, (uint64_t)count };
+ }
+ value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
+ return 0;
+ }
+ case DT_AGG_LLQUANTIZE: {
+ int factor = DTRACE_LLQUANTIZE_FACTOR(agg->dtagd_sig);
+ int lmag = DTRACE_LLQUANTIZE_LMAG(agg->dtagd_sig);
+ int hmag = DTRACE_LLQUANTIZE_HMAG(agg->dtagd_sig);
+ int steps = DTRACE_LLQUANTIZE_STEPS(agg->dtagd_sig);
+ int steps_factor, bin0;
+ size_t i, nbins;
+ const int64_t *data = (const int64_t *)addr;
+ long double limit = (long double)INT64_MAX;
+
+ if (factor < 2 || steps < factor || lmag > hmag)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ steps_factor = steps / factor;
+ bin0 = 1 + (hmag - lmag + 1) * (steps - steps_factor);
+ nbins = (size_t)(hmag - lmag + 1) *
+ (steps - steps_factor) * 2 + 3;
+ if (!isfinite((double)powl(factor, hmag + 1)) ||
+ powl(factor, hmag + 1) > limit)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+
+ if (rec->dtrd_size != nbins * sizeof(*data) ||
+ value->dtagv_buckets == NULL ||
+ value->dtagv_bucket_capacity < nbins)
+ return dt_set_errno(aggdata->dtada_hdl, EINVAL);
+ for (i = 0; i < nbins; i++) {
+ if (data[i] < 0)
+ return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
+ }
+ for (i = 0; i < nbins; i++) {
+ int64_t count = data[i] / (int64_t)normal;
+ int64_t bucket;
+ int left, negative, remaining, magnitude;
+
+ if (count == 0)
+ continue;
+ if (i == 0)
+ bucket = -(int64_t)powl(factor, hmag + 1);
+ else if (i == nbins - 1)
+ bucket = (int64_t)powl(factor, hmag + 1);
+ else if (i == (size_t)bin0)
+ bucket = 0;
+ else {
+ left = (int)i - bin0;
+ negative = left < 0;
+ remaining = negative ? bin0 - (int)i : left;
+ magnitude = lmag;
+ if (lmag == 0 && steps > factor) {
+ for (int n = 2; n <= factor && remaining > 0; n++) {
+ remaining -= steps_factor;
+ if (remaining == 0) {
+ bucket = negative ? -n : n;
+ goto ll_have_bucket;
+ }
+ }
+ magnitude++;
+ }
+ bucket = negative ? -1 : 1;
+ while (magnitude <= hmag) {
+ int64_t scale = (int64_t)(powl(factor, magnitude + 1) /
+ (double)steps);
+ int n;
+
+ for (n = steps_factor + 1; n <= steps; n++) {
+ if (--remaining == 0) {
+ bucket = n * scale * (negative ? -1 : 1);
+ goto ll_have_bucket;
+ }
+ }
+ magnitude++;
+ }
+ bucket *= (int64_t)powl(factor, hmag + 1);
+ }
+ll_have_bucket:
+ value->dtagv_buckets[value->dtagv_bucket_count++] =
+ (dtrace_aggvalue_bucket_t){ bucket, (uint64_t)count };
+ }
+ value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
+ return 0;
+ }
+ default:
+ value->dtagv_kind = DTRACE_AGGVALUE_RAW;
+ value->dtagv_value.raw.data = addr;
+ value->dtagv_value.raw.size = rec->dtrd_size;
+ return 0;
+ }
+}
+
+void dtrace_aggregate_key_free(dtrace_aggkey_t *key)
+{
+ size_t i;
+
+ if (!key)
+ return;
+ switch (key->dtagk_kind) {
+ case DTRACE_AGGKEY_BYTES:
+ free(key->dtagk_value.bytes.data);
+ break;
+ case DTRACE_AGGKEY_STRING:
+ case DTRACE_AGGKEY_SYMBOL:
+ case DTRACE_AGGKEY_MODULE:
+ free(key->dtagk_value.name);
+ break;
+ case DTRACE_AGGKEY_ADDRESS:
+ break;
+ case DTRACE_AGGKEY_STACK:
+ for (i = 0; i < key->dtagk_value.stack.depth; i++)
+ free(key->dtagk_value.stack.frames[i]);
+ free(key->dtagk_value.stack.frames);
+ break;
+ default:
+ break;
+ }
+ memset(key, 0, sizeof(*key));
+}
+
+int dtrace_aggregate_keys(const dtrace_aggdata_t *data,
+ dtrace_aggkey_t *keys, size_t num_keys)
+{
+ int error = EDT_NOMEM;
+ dtrace_hdl_t *dtp;
+ size_t i, n;
+
+ if (!data || !data->dtada_desc)
+ return data && data->dtada_hdl ?
+ dt_set_errno(data->dtada_hdl, EINVAL) : -1;
+ dtp = data->dtada_hdl;
+ if (data->dtada_desc->dtagd_nkrecs == 0)
+ return 0;
+ n = data->dtada_desc->dtagd_nkrecs - 1;
+ if (n == 0)
+ return 0;
+ if (!data->dtada_key)
+ return dt_set_errno(dtp, EDT_BADAGG);
+ if (!keys || num_keys < n)
+ return dt_set_errno(dtp, EINVAL);
+ memset(keys, 0, n * sizeof(*keys));
+
+ for (i = 0; i < n; i++) {
+ const dtrace_recdesc_t *rec = &data->dtada_desc->dtagd_krecs[i + 1];
+ char *addr = data->dtada_key + rec->dtrd_offset;
+ if ((size_t)rec->dtrd_offset > data->dtada_desc->dtagd_ksize ||
+ rec->dtrd_size > data->dtada_desc->dtagd_ksize -
+ (size_t)rec->dtrd_offset)
+ goto badagg;
+
+ switch (rec->dtrd_action) {
+ case DTRACEACT_STACK:
+ case DTRACEACT_USTACK:
+ case DTRACEACT_JSTACK: {
+ uint32_t depth, max, frames, j, tgid = 0;
+
+ if (rec->dtrd_size < 2 * sizeof(uint64_t) ||
+ (rec->dtrd_action != DTRACEACT_STACK &&
+ rec->dtrd_size < 4 * sizeof(uint64_t)))
+ goto badagg;
+ memcpy(&depth, addr, sizeof(depth));
+ max = rec->dtrd_size >= 2 * sizeof(uint64_t) ?
+ rec->dtrd_size / sizeof(uint64_t) - 2 : 0;
+ frames = depth < max ? depth : max;
+ if (rec->dtrd_action != DTRACEACT_STACK) {
+ memcpy(&tgid, addr + 3 * sizeof(uint32_t),
+ sizeof(tgid));
+ }
+
+ keys[i].dtagk_kind = DTRACE_AGGKEY_STACK;
+ keys[i].dtagk_value.stack.frames = frames == 0 ? NULL :
+ calloc(frames, sizeof(char *));
+ keys[i].dtagk_value.stack.depth = 0;
+ if (frames != 0 && !keys[i].dtagk_value.stack.frames)
+ goto error;
+ for (j = 0; j < frames; j++) {
+ char text[PATH_MAX * 2];
+ uint64_t pc;
+ int rc;
+
+ memcpy(&pc, addr + (j + 2) * sizeof(uint64_t),
+ sizeof(pc));
+ if (pc == 0)
+ break;
+ rc = rec->dtrd_action == DTRACEACT_STACK ?
+ dtrace_addr2str(dtp, pc, text,
+ sizeof(text)) :
+ dtrace_uaddr2str(dtp, tgid, pc,
+ text, sizeof(text));
+ if (rc < 0) {
+ error = EDT_BADAGG;
+ goto error;
+ }
+ if (!(keys[i].dtagk_value.stack.frames[j] = strdup(text)))
+ goto error;
+ keys[i].dtagk_value.stack.depth++;
+ }
+ break;
+ }
+ case DTRACEACT_SYM:
+ case DTRACEACT_MOD: {
+ dtrace_syminfo_t info;
+ char text[PATH_MAX * 2];
+ uint64_t pc;
+
+ memcpy(&pc, addr, sizeof(pc));
+
+ if (dtrace_lookup_by_addr(dtp, pc,
+ NULL, &info) == 0) {
+ snprintf(text, sizeof(text), "%s%s%s",
+ info.object,
+ rec->dtrd_action == DTRACEACT_SYM ? "`" : "",
+ rec->dtrd_action == DTRACEACT_SYM ? info.name : "");
+ } else {
+ snprintf(text, sizeof(text), "0x%llx",
+ (unsigned long long)pc);
+ }
+ keys[i].dtagk_kind = rec->dtrd_action == DTRACEACT_SYM ?
+ DTRACE_AGGKEY_SYMBOL : DTRACE_AGGKEY_MODULE;
+ keys[i].dtagk_value.name = strdup(text);
+ if (!keys[i].dtagk_value.name)
+ goto error;
+ break;
+ }
+ case DTRACEACT_UADDR:
+ case DTRACEACT_USYM:
+ case DTRACEACT_UMOD: {
+ char text[PATH_MAX * 2];
+ uint32_t tgid;
+ uint64_t pc;
+
+ /*
+ * The target PID occupies the first word and the address
+ * the second; the record is aligned to 64 bits.
+ */
+ if (rec->dtrd_size < 2 * sizeof(uint64_t))
+ goto badagg;
+ memcpy(&tgid, addr, sizeof(tgid));
+ memcpy(&pc, addr + sizeof(uint64_t), sizeof(pc));
+ if (rec->dtrd_action == DTRACEACT_UADDR) {
+ keys[i].dtagk_kind = DTRACE_AGGKEY_ADDRESS;
+ keys[i].dtagk_value.user_address.pid = tgid;
+ keys[i].dtagk_value.user_address.address = pc;
+ break;
+ }
+
+ if (dtrace_uaddr2str(dtp, tgid, pc, text,
+ sizeof(text)) < 0)
+ goto badagg;
+ if (rec->dtrd_action == DTRACEACT_UMOD) {
+ char *tick = strchr(text, '`');
+ if (tick)
+ *tick = '\0';
+ }
+ keys[i].dtagk_kind = rec->dtrd_action == DTRACEACT_USYM ?
+ DTRACE_AGGKEY_SYMBOL : DTRACE_AGGKEY_MODULE;
+ keys[i].dtagk_value.name = strdup(text);
+ if (!keys[i].dtagk_value.name)
+ goto error;
+ break;
+ }
+ default:
+ keys[i].dtagk_kind = DTRACE_AGGKEY_INTEGER;
+ switch (rec->dtrd_size) {
+ case 1: {
+ int8_t v;
+
+ memcpy(&v, addr, 1);
+ keys[i].dtagk_value.integer = v;
+ break;
+ }
+ case 2: {
+ int16_t v;
+
+ memcpy(&v, addr, 2);
+ keys[i].dtagk_value.integer = v;
+ break;
+ }
+ case 4: {
+ int32_t v;
+
+ memcpy(&v, addr, 4);
+ keys[i].dtagk_value.integer = v;
+ break;
+ }
+ case 8: {
+ int64_t v;
+
+ memcpy(&v, addr, 8);
+ keys[i].dtagk_value.integer = v;
+ break;
+ }
+ default: {
+ char *nul = memchr(addr, '\0', rec->dtrd_size);
+
+ /*
+ * This is a heuristic: key descriptors do not
+ * retain type information. DTrace strings are
+ * NUL-terminated and padded with zeros to their
+ * fixed record size.
+ */
+ if (nul != NULL) {
+ char *p;
+
+ for (p = nul + 1; p < addr + rec->dtrd_size; p++) {
+ if (*p != '\0')
+ break;
+ }
+ if (p == addr + rec->dtrd_size) {
+ keys[i].dtagk_kind = DTRACE_AGGKEY_STRING;
+ keys[i].dtagk_value.name = strndup(addr,
+ rec->dtrd_size);
+ if (!keys[i].dtagk_value.name)
+ goto error;
+ break;
+ }
+ }
+ keys[i].dtagk_kind = DTRACE_AGGKEY_BYTES;
+ keys[i].dtagk_value.bytes.data = malloc(rec->dtrd_size);
+ if (!keys[i].dtagk_value.bytes.data)
+ goto error;
+ memcpy(keys[i].dtagk_value.bytes.data, addr,
+ rec->dtrd_size);
+ keys[i].dtagk_value.bytes.size = rec->dtrd_size;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ return (int)n;
+error:
+ for (i = 0; i < n; i++)
+ dtrace_aggregate_key_free(&keys[i]);
+ return dt_set_errno(dtp, error);
+badagg:
+ for (i = 0; i < n; i++)
+ dtrace_aggregate_key_free(&keys[i]);
+ return dt_set_errno(dtp, EDT_BADAGG);
+}
+
/*
* Because qsort(3C) does not allow an argument to be passed to a comparison
* function, the variables that affect comparison must regrettably be global;
diff --git a/libdtrace/dtrace.h b/libdtrace/dtrace.h
index 82965fbd..a108080f 100644
--- a/libdtrace/dtrace.h
+++ b/libdtrace/dtrace.h
@@ -379,6 +379,89 @@ struct dtrace_aggdata {
caddr_t *dtada_percpu; /* per CPU data, if avail */
};
+typedef enum dtrace_aggvalue_kind {
+ DTRACE_AGGVALUE_INTEGER,
+ DTRACE_AGGVALUE_UNSIGNED,
+ DTRACE_AGGVALUE_FLOAT,
+ DTRACE_AGGVALUE_RAW,
+ DTRACE_AGGVALUE_QUANTIZED
+} dtrace_aggvalue_kind_t;
+
+typedef struct dtrace_aggvalue_bucket {
+ int64_t bucket;
+ uint64_t count;
+} dtrace_aggvalue_bucket_t;
+
+/* Quantized buckets are supplied by the caller through dtagv_buckets. */
+typedef struct dtrace_aggvalue {
+ dtrace_aggvalue_kind_t dtagv_kind;
+ uint16_t dtagv_action;
+ uint64_t dtagv_normal;
+ dtrace_aggvalue_bucket_t *dtagv_buckets;
+ size_t dtagv_bucket_capacity;
+ size_t dtagv_bucket_count;
+ union {
+ int64_t integer;
+ uint64_t unsigned_integer;
+ double real;
+ struct {
+ const void *data;
+ size_t size;
+ } raw;
+ } dtagv_value;
+} dtrace_aggvalue_t;
+
+typedef enum dtrace_aggkey_kind {
+ DTRACE_AGGKEY_INTEGER,
+ DTRACE_AGGKEY_STRING,
+ DTRACE_AGGKEY_BYTES,
+ DTRACE_AGGKEY_STACK,
+ DTRACE_AGGKEY_SYMBOL,
+ DTRACE_AGGKEY_MODULE,
+ DTRACE_AGGKEY_USER_ADDRESS,
+ DTRACE_AGGKEY_ADDRESS = DTRACE_AGGKEY_USER_ADDRESS
+} dtrace_aggkey_kind_t;
+
+typedef struct dtrace_aggkey {
+ dtrace_aggkey_kind_t dtagk_kind;
+ union {
+ int64_t integer;
+ struct {
+ char *data;
+ size_t size;
+ } bytes;
+ struct {
+ char **frames;
+ size_t depth;
+ } stack;
+ char *name;
+ /* User addresses include the target PID to disambiguate ASLR. */
+ struct {
+ uint32_t pid;
+ uint64_t address;
+ } user_address;
+ } dtagk_value;
+} dtrace_aggkey_t;
+
+/*
+ * Decode all keys in an aggregation snapshot. The caller supplies an array
+ * with capacity for dtagd_nkrecs - 1 descriptors. Returned strings, byte
+ * buffers, and stack frames are owned by the caller and must be released
+ * with dtrace_aggregate_key_free(). Zero-key aggregations return zero.
+ */
+extern int dtrace_aggregate_keys(const dtrace_aggdata_t *, dtrace_aggkey_t *,
+ size_t);
+extern void dtrace_aggregate_key_free(dtrace_aggkey_t *);
+
+/*
+ * Decode one aggregation value. Integer values can be signed or unsigned.
+ * Raw data points into the snapshot and is valid only while that snapshot
+ * remains valid. Quantized values require a caller-provided bucket array and
+ * capacity.
+ */
+extern int dtrace_aggregate_value(const dtrace_aggdata_t *,
+ dtrace_aggvalue_t *);
+
typedef struct dtrace_print_aggdata {
dtrace_hdl_t *dtpa_dtp; /* handle to DTrace library */
dtrace_aggid_t dtpa_id; /* aggregation variable */
diff --git a/libdtrace/libdtrace.ver b/libdtrace/libdtrace.ver
index edbd5a1d..87d561b2 100644
--- a/libdtrace/libdtrace.ver
+++ b/libdtrace/libdtrace.ver
@@ -8,8 +8,11 @@ LIBDTRACE_1.0 {
global:
dtrace_addr2str;
dtrace_aggregate_clear;
+ dtrace_aggregate_keys;
+ dtrace_aggregate_key_free;
dtrace_aggregate_print;
dtrace_aggregate_snap;
+ dtrace_aggregate_value;
dtrace_aggregate_walk;
dtrace_aggregate_walk_joined;
dtrace_aggregate_walk_keyrevsorted;
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH v5 04/11] libdtrace: Add aggregation key/value computation APIs
2026-08-21 16:10 ` [PATCH v5 04/11] libdtrace: Add aggregation key/value computation APIs Alan Maguire
@ 2026-10-07 10:51 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-07 10:51 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
I am lookingaand t this but there are a few issues already in terms of
safety. Since these are API functions, we cannot count on the caller
ensuring safety. E.g. dtrace_aggregate_keys() would result in an out of
bounds memory write if keys has room for 2 keys, num_keys is 3, and
dtagd_nkrecs >= 4. Perhaps the function should do its own allocation
and return the number of keys upon success instead?
I would do the same for the dtrace_aggregate_value() function.
On Fri, Aug 21, 2026 at 05:10:52PM +0100, Alan Maguire wrote:
> To provide a way for consumers to emit aggregation keys
> and compute aggregation values, provide new APIs in libdtrace:
>
> int dtrace_aggregate_value(const dtrace_aggdata_t *aggdata,
> dtrace_aggvalue_t *value);
> int dtrace_aggregate_keys(const dtrace_aggdata_t *data,
> dtrace_aggkey_t *keys, size_t num_keys);
> void dtrace_aggregate_key_free(dtrace_aggkey_t *key);
>
> These provide a way to compute aggregation values from snapshots,
> print them and free the representation used to print them.
>
> This allows consumers like python bindings to avoid having to
> understand aggregation internals.
>
> Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
> ---
> libdtrace/dt_aggregate.c | 517 +++++++++++++++++++++++++++++++++++++++
> libdtrace/dtrace.h | 83 +++++++
> libdtrace/libdtrace.ver | 3 +
> 3 files changed, 603 insertions(+)
>
> diff --git a/libdtrace/dt_aggregate.c b/libdtrace/dt_aggregate.c
> index 1fc8294d..afc6a01a 100644
> --- a/libdtrace/dt_aggregate.c
> +++ b/libdtrace/dt_aggregate.c
> @@ -13,11 +13,15 @@
> #include <assert.h>
> #include <alloca.h>
> #include <limits.h>
> +#include <stdio.h>
> +#include <string.h>
>
> #include <libproc.h>
> #include <port.h>
> #include <dt_aggregate.h>
> #include <dt_bpf.h>
> +#include <math.h>
> +#include <limits.h>
>
> typedef struct dt_ahashent {
> struct dt_ahashent *dtahe_prev; /* prev on hash chain */
> @@ -44,6 +48,519 @@ struct dt_aggregate {
>
> #define DTRACE_AHASHSIZE 32779 /* big 'ol prime */
>
> +static int
> +dt_aggregate_read_signed(const char *addr, size_t size, int64_t *value)
> +{
> + int8_t v8;
> + int16_t v16;
> + int32_t v32;
> + int64_t v64;
> +
> + switch (size) {
> + case sizeof(v8):
> + memcpy(&v8, addr, sizeof(v8));
> + *value = v8;
> + return 0;
> + case sizeof(v16):
> + memcpy(&v16, addr, sizeof(v16));
> + *value = v16;
> + return 0;
> + case sizeof(v32):
> + memcpy(&v32, addr, sizeof(v32));
> + *value = v32;
> + return 0;
> + case sizeof(v64):
> + memcpy(&v64, addr, sizeof(v64));
> + *value = v64;
> + return 0;
> + default:
> + return -1;
> + }
> +}
> +
> +static int
> +dt_aggregate_read_unsigned(const char *addr, size_t size, uint64_t *value)
> +{
> + uint8_t v8;
> + uint16_t v16;
> + uint32_t v32;
> + uint64_t v64;
> +
> + switch (size) {
> + case sizeof(v8):
> + memcpy(&v8, addr, sizeof(v8));
> + *value = v8;
> + return 0;
> + case sizeof(v16):
> + memcpy(&v16, addr, sizeof(v16));
> + *value = v16;
> + return 0;
> + case sizeof(v32):
> + memcpy(&v32, addr, sizeof(v32));
> + *value = v32;
> + return 0;
> + case sizeof(v64):
> + memcpy(&v64, addr, sizeof(v64));
> + *value = v64;
> + return 0;
> + default:
> + return -1;
> + }
> +}
> +
> +int
> +dtrace_aggregate_value(const dtrace_aggdata_t *aggdata, dtrace_aggvalue_t *value)
> +{
> + const dtrace_aggdesc_t *agg;
> + const dtrace_recdesc_t *rec;
> + const char *addr;
> + uint64_t normal;
> +
> + if (aggdata == NULL || aggdata->dtada_desc == NULL ||
> + aggdata->dtada_data == NULL || value == NULL)
> + return aggdata && aggdata->dtada_hdl ?
> + dt_set_errno(aggdata->dtada_hdl, EINVAL) : -1;
> +
> + agg = aggdata->dtada_desc;
> + rec = &agg->dtagd_drecs[DT_AGGDATA_RECORD];
> + addr = aggdata->dtada_data + rec->dtrd_offset;
> + normal = agg->dtagd_normal ? agg->dtagd_normal : 1;
> + value->dtagv_action = rec->dtrd_action;
> + value->dtagv_normal = normal;
> + value->dtagv_bucket_count = 0;
> +
> + switch (rec->dtrd_action) {
> + case DT_AGG_AVG: {
> + const int64_t *data = (const int64_t *)addr;
> +
> + if (rec->dtrd_size < sizeof(*data) * 2)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + value->dtagv_kind = DTRACE_AGGVALUE_FLOAT;
> + value->dtagv_value.real = data[0] == 0 ? 0.0 :
> + (double)((long double)data[1] / normal / data[0]);
> + return 0;
> + }
> + case DT_AGG_STDDEV: {
> + uint64_t data[4];
> +
> + if (rec->dtrd_size < sizeof(data))
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + memcpy(data, addr, sizeof(data));
> + value->dtagv_kind = DTRACE_AGGVALUE_UNSIGNED;
> + value->dtagv_value.unsigned_integer =
> + data[0] == 0 ? 0 : dt_stddev(data, normal);
> + return 0;
> + }
> + case DT_AGG_SUM:
> + case DT_AGG_MIN:
> + case DT_AGG_MAX:
> + value->dtagv_kind = DTRACE_AGGVALUE_INTEGER;
> + if (dt_aggregate_read_signed(addr, rec->dtrd_size,
> + &value->dtagv_value.integer) != 0)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + value->dtagv_value.integer /= (int64_t)normal;
> + return 0;
> + case DT_AGG_COUNT:
> + value->dtagv_kind = DTRACE_AGGVALUE_UNSIGNED;
> + if (dt_aggregate_read_unsigned(addr, rec->dtrd_size,
> + &value->dtagv_value.unsigned_integer) != 0)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + value->dtagv_value.unsigned_integer /= normal;
> + return 0;
> + case DT_AGG_QUANTIZE: {
> + const int64_t *data = (const int64_t *)addr;
> + size_t i, nbins = rec->dtrd_size / sizeof(*data);
> +
> + if (rec->dtrd_size % sizeof(*data) != 0 ||
> + value->dtagv_buckets == NULL ||
> + value->dtagv_bucket_capacity < nbins)
> + return dt_set_errno(aggdata->dtada_hdl, EINVAL);
> + for (i = 0; i < nbins; i++) {
> + if (data[i] < 0)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + }
> + for (i = 0; i < nbins; i++) {
> + if (data[i] == 0)
> + continue;
> + value->dtagv_buckets[value->dtagv_bucket_count].bucket =
> + DTRACE_QUANTIZE_BUCKETVAL((uint64_t)i);
> + value->dtagv_buckets[value->dtagv_bucket_count].count =
> + (uint64_t)(data[i] / (int64_t)normal);
> + if (value->dtagv_buckets[value->dtagv_bucket_count].count != 0)
> + value->dtagv_bucket_count++;
> + }
> + value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
> + return 0;
> + }
> + case DT_AGG_LQUANTIZE: {
> + uint16_t levels = DTRACE_LQUANTIZE_LEVELS(agg->dtagd_sig);
> + uint16_t step = DTRACE_LQUANTIZE_STEP(agg->dtagd_sig);
> + int32_t base = DTRACE_LQUANTIZE_BASE(agg->dtagd_sig);
> + size_t i, nbins = (size_t)levels + 2;
> + const int64_t *data = (const int64_t *)addr;
> +
> + if (rec->dtrd_size != nbins * sizeof(*data) ||
> + value->dtagv_buckets == NULL ||
> + value->dtagv_bucket_capacity < nbins)
> + return dt_set_errno(aggdata->dtada_hdl, EINVAL);
> + for (i = 0; i < nbins; i++) {
> + if (data[i] < 0)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + }
> + for (i = 0; i < nbins; i++) {
> + int64_t count = data[i] / (int64_t)normal;
> + int64_t bucket;
> +
> + if (count == 0)
> + continue;
> + if (i == 0)
> + bucket = base - 1;
> + else if (i == levels + 1)
> + bucket = base + (int64_t)levels * step;
> + else
> + bucket = base + (int64_t)(i - 1) * step;
> + value->dtagv_buckets[value->dtagv_bucket_count++] =
> + (dtrace_aggvalue_bucket_t){ bucket, (uint64_t)count };
> + }
> + value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
> + return 0;
> + }
> + case DT_AGG_LLQUANTIZE: {
> + int factor = DTRACE_LLQUANTIZE_FACTOR(agg->dtagd_sig);
> + int lmag = DTRACE_LLQUANTIZE_LMAG(agg->dtagd_sig);
> + int hmag = DTRACE_LLQUANTIZE_HMAG(agg->dtagd_sig);
> + int steps = DTRACE_LLQUANTIZE_STEPS(agg->dtagd_sig);
> + int steps_factor, bin0;
> + size_t i, nbins;
> + const int64_t *data = (const int64_t *)addr;
> + long double limit = (long double)INT64_MAX;
> +
> + if (factor < 2 || steps < factor || lmag > hmag)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + steps_factor = steps / factor;
> + bin0 = 1 + (hmag - lmag + 1) * (steps - steps_factor);
> + nbins = (size_t)(hmag - lmag + 1) *
> + (steps - steps_factor) * 2 + 3;
> + if (!isfinite((double)powl(factor, hmag + 1)) ||
> + powl(factor, hmag + 1) > limit)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> +
> + if (rec->dtrd_size != nbins * sizeof(*data) ||
> + value->dtagv_buckets == NULL ||
> + value->dtagv_bucket_capacity < nbins)
> + return dt_set_errno(aggdata->dtada_hdl, EINVAL);
> + for (i = 0; i < nbins; i++) {
> + if (data[i] < 0)
> + return dt_set_errno(aggdata->dtada_hdl, EDT_BADAGG);
> + }
> + for (i = 0; i < nbins; i++) {
> + int64_t count = data[i] / (int64_t)normal;
> + int64_t bucket;
> + int left, negative, remaining, magnitude;
> +
> + if (count == 0)
> + continue;
> + if (i == 0)
> + bucket = -(int64_t)powl(factor, hmag + 1);
> + else if (i == nbins - 1)
> + bucket = (int64_t)powl(factor, hmag + 1);
> + else if (i == (size_t)bin0)
> + bucket = 0;
> + else {
> + left = (int)i - bin0;
> + negative = left < 0;
> + remaining = negative ? bin0 - (int)i : left;
> + magnitude = lmag;
> + if (lmag == 0 && steps > factor) {
> + for (int n = 2; n <= factor && remaining > 0; n++) {
> + remaining -= steps_factor;
> + if (remaining == 0) {
> + bucket = negative ? -n : n;
> + goto ll_have_bucket;
> + }
> + }
> + magnitude++;
> + }
> + bucket = negative ? -1 : 1;
> + while (magnitude <= hmag) {
> + int64_t scale = (int64_t)(powl(factor, magnitude + 1) /
> + (double)steps);
> + int n;
> +
> + for (n = steps_factor + 1; n <= steps; n++) {
> + if (--remaining == 0) {
> + bucket = n * scale * (negative ? -1 : 1);
> + goto ll_have_bucket;
> + }
> + }
> + magnitude++;
> + }
> + bucket *= (int64_t)powl(factor, hmag + 1);
> + }
> +ll_have_bucket:
> + value->dtagv_buckets[value->dtagv_bucket_count++] =
> + (dtrace_aggvalue_bucket_t){ bucket, (uint64_t)count };
> + }
> + value->dtagv_kind = DTRACE_AGGVALUE_QUANTIZED;
> + return 0;
> + }
> + default:
> + value->dtagv_kind = DTRACE_AGGVALUE_RAW;
> + value->dtagv_value.raw.data = addr;
> + value->dtagv_value.raw.size = rec->dtrd_size;
> + return 0;
> + }
> +}
> +
> +void dtrace_aggregate_key_free(dtrace_aggkey_t *key)
> +{
> + size_t i;
> +
> + if (!key)
> + return;
> + switch (key->dtagk_kind) {
> + case DTRACE_AGGKEY_BYTES:
> + free(key->dtagk_value.bytes.data);
> + break;
> + case DTRACE_AGGKEY_STRING:
> + case DTRACE_AGGKEY_SYMBOL:
> + case DTRACE_AGGKEY_MODULE:
> + free(key->dtagk_value.name);
> + break;
> + case DTRACE_AGGKEY_ADDRESS:
> + break;
> + case DTRACE_AGGKEY_STACK:
> + for (i = 0; i < key->dtagk_value.stack.depth; i++)
> + free(key->dtagk_value.stack.frames[i]);
> + free(key->dtagk_value.stack.frames);
> + break;
> + default:
> + break;
> + }
> + memset(key, 0, sizeof(*key));
> +}
> +
> +int dtrace_aggregate_keys(const dtrace_aggdata_t *data,
> + dtrace_aggkey_t *keys, size_t num_keys)
> +{
> + int error = EDT_NOMEM;
> + dtrace_hdl_t *dtp;
> + size_t i, n;
> +
> + if (!data || !data->dtada_desc)
> + return data && data->dtada_hdl ?
> + dt_set_errno(data->dtada_hdl, EINVAL) : -1;
> + dtp = data->dtada_hdl;
> + if (data->dtada_desc->dtagd_nkrecs == 0)
> + return 0;
> + n = data->dtada_desc->dtagd_nkrecs - 1;
> + if (n == 0)
> + return 0;
> + if (!data->dtada_key)
> + return dt_set_errno(dtp, EDT_BADAGG);
> + if (!keys || num_keys < n)
> + return dt_set_errno(dtp, EINVAL);
> + memset(keys, 0, n * sizeof(*keys));
> +
> + for (i = 0; i < n; i++) {
> + const dtrace_recdesc_t *rec = &data->dtada_desc->dtagd_krecs[i + 1];
> + char *addr = data->dtada_key + rec->dtrd_offset;
> + if ((size_t)rec->dtrd_offset > data->dtada_desc->dtagd_ksize ||
> + rec->dtrd_size > data->dtada_desc->dtagd_ksize -
> + (size_t)rec->dtrd_offset)
> + goto badagg;
> +
> + switch (rec->dtrd_action) {
> + case DTRACEACT_STACK:
> + case DTRACEACT_USTACK:
> + case DTRACEACT_JSTACK: {
> + uint32_t depth, max, frames, j, tgid = 0;
> +
> + if (rec->dtrd_size < 2 * sizeof(uint64_t) ||
> + (rec->dtrd_action != DTRACEACT_STACK &&
> + rec->dtrd_size < 4 * sizeof(uint64_t)))
> + goto badagg;
> + memcpy(&depth, addr, sizeof(depth));
> + max = rec->dtrd_size >= 2 * sizeof(uint64_t) ?
> + rec->dtrd_size / sizeof(uint64_t) - 2 : 0;
> + frames = depth < max ? depth : max;
> + if (rec->dtrd_action != DTRACEACT_STACK) {
> + memcpy(&tgid, addr + 3 * sizeof(uint32_t),
> + sizeof(tgid));
> + }
> +
> + keys[i].dtagk_kind = DTRACE_AGGKEY_STACK;
> + keys[i].dtagk_value.stack.frames = frames == 0 ? NULL :
> + calloc(frames, sizeof(char *));
> + keys[i].dtagk_value.stack.depth = 0;
> + if (frames != 0 && !keys[i].dtagk_value.stack.frames)
> + goto error;
> + for (j = 0; j < frames; j++) {
> + char text[PATH_MAX * 2];
> + uint64_t pc;
> + int rc;
> +
> + memcpy(&pc, addr + (j + 2) * sizeof(uint64_t),
> + sizeof(pc));
> + if (pc == 0)
> + break;
> + rc = rec->dtrd_action == DTRACEACT_STACK ?
> + dtrace_addr2str(dtp, pc, text,
> + sizeof(text)) :
> + dtrace_uaddr2str(dtp, tgid, pc,
> + text, sizeof(text));
> + if (rc < 0) {
> + error = EDT_BADAGG;
> + goto error;
> + }
> + if (!(keys[i].dtagk_value.stack.frames[j] = strdup(text)))
> + goto error;
> + keys[i].dtagk_value.stack.depth++;
> + }
> + break;
> + }
> + case DTRACEACT_SYM:
> + case DTRACEACT_MOD: {
> + dtrace_syminfo_t info;
> + char text[PATH_MAX * 2];
> + uint64_t pc;
> +
> + memcpy(&pc, addr, sizeof(pc));
> +
> + if (dtrace_lookup_by_addr(dtp, pc,
> + NULL, &info) == 0) {
> + snprintf(text, sizeof(text), "%s%s%s",
> + info.object,
> + rec->dtrd_action == DTRACEACT_SYM ? "`" : "",
> + rec->dtrd_action == DTRACEACT_SYM ? info.name : "");
> + } else {
> + snprintf(text, sizeof(text), "0x%llx",
> + (unsigned long long)pc);
> + }
> + keys[i].dtagk_kind = rec->dtrd_action == DTRACEACT_SYM ?
> + DTRACE_AGGKEY_SYMBOL : DTRACE_AGGKEY_MODULE;
> + keys[i].dtagk_value.name = strdup(text);
> + if (!keys[i].dtagk_value.name)
> + goto error;
> + break;
> + }
> + case DTRACEACT_UADDR:
> + case DTRACEACT_USYM:
> + case DTRACEACT_UMOD: {
> + char text[PATH_MAX * 2];
> + uint32_t tgid;
> + uint64_t pc;
> +
> + /*
> + * The target PID occupies the first word and the address
> + * the second; the record is aligned to 64 bits.
> + */
> + if (rec->dtrd_size < 2 * sizeof(uint64_t))
> + goto badagg;
> + memcpy(&tgid, addr, sizeof(tgid));
> + memcpy(&pc, addr + sizeof(uint64_t), sizeof(pc));
> + if (rec->dtrd_action == DTRACEACT_UADDR) {
> + keys[i].dtagk_kind = DTRACE_AGGKEY_ADDRESS;
> + keys[i].dtagk_value.user_address.pid = tgid;
> + keys[i].dtagk_value.user_address.address = pc;
> + break;
> + }
> +
> + if (dtrace_uaddr2str(dtp, tgid, pc, text,
> + sizeof(text)) < 0)
> + goto badagg;
> + if (rec->dtrd_action == DTRACEACT_UMOD) {
> + char *tick = strchr(text, '`');
> + if (tick)
> + *tick = '\0';
> + }
> + keys[i].dtagk_kind = rec->dtrd_action == DTRACEACT_USYM ?
> + DTRACE_AGGKEY_SYMBOL : DTRACE_AGGKEY_MODULE;
> + keys[i].dtagk_value.name = strdup(text);
> + if (!keys[i].dtagk_value.name)
> + goto error;
> + break;
> + }
> + default:
> + keys[i].dtagk_kind = DTRACE_AGGKEY_INTEGER;
> + switch (rec->dtrd_size) {
> + case 1: {
> + int8_t v;
> +
> + memcpy(&v, addr, 1);
> + keys[i].dtagk_value.integer = v;
> + break;
> + }
> + case 2: {
> + int16_t v;
> +
> + memcpy(&v, addr, 2);
> + keys[i].dtagk_value.integer = v;
> + break;
> + }
> + case 4: {
> + int32_t v;
> +
> + memcpy(&v, addr, 4);
> + keys[i].dtagk_value.integer = v;
> + break;
> + }
> + case 8: {
> + int64_t v;
> +
> + memcpy(&v, addr, 8);
> + keys[i].dtagk_value.integer = v;
> + break;
> + }
> + default: {
> + char *nul = memchr(addr, '\0', rec->dtrd_size);
> +
> + /*
> + * This is a heuristic: key descriptors do not
> + * retain type information. DTrace strings are
> + * NUL-terminated and padded with zeros to their
> + * fixed record size.
> + */
> + if (nul != NULL) {
> + char *p;
> +
> + for (p = nul + 1; p < addr + rec->dtrd_size; p++) {
> + if (*p != '\0')
> + break;
> + }
> + if (p == addr + rec->dtrd_size) {
> + keys[i].dtagk_kind = DTRACE_AGGKEY_STRING;
> + keys[i].dtagk_value.name = strndup(addr,
> + rec->dtrd_size);
> + if (!keys[i].dtagk_value.name)
> + goto error;
> + break;
> + }
> + }
> + keys[i].dtagk_kind = DTRACE_AGGKEY_BYTES;
> + keys[i].dtagk_value.bytes.data = malloc(rec->dtrd_size);
> + if (!keys[i].dtagk_value.bytes.data)
> + goto error;
> + memcpy(keys[i].dtagk_value.bytes.data, addr,
> + rec->dtrd_size);
> + keys[i].dtagk_value.bytes.size = rec->dtrd_size;
> + break;
> + }
> + }
> + break;
> + }
> + }
> + return (int)n;
> +error:
> + for (i = 0; i < n; i++)
> + dtrace_aggregate_key_free(&keys[i]);
> + return dt_set_errno(dtp, error);
> +badagg:
> + for (i = 0; i < n; i++)
> + dtrace_aggregate_key_free(&keys[i]);
> + return dt_set_errno(dtp, EDT_BADAGG);
> +}
> +
> /*
> * Because qsort(3C) does not allow an argument to be passed to a comparison
> * function, the variables that affect comparison must regrettably be global;
> diff --git a/libdtrace/dtrace.h b/libdtrace/dtrace.h
> index 82965fbd..a108080f 100644
> --- a/libdtrace/dtrace.h
> +++ b/libdtrace/dtrace.h
> @@ -379,6 +379,89 @@ struct dtrace_aggdata {
> caddr_t *dtada_percpu; /* per CPU data, if avail */
> };
>
> +typedef enum dtrace_aggvalue_kind {
> + DTRACE_AGGVALUE_INTEGER,
> + DTRACE_AGGVALUE_UNSIGNED,
> + DTRACE_AGGVALUE_FLOAT,
> + DTRACE_AGGVALUE_RAW,
> + DTRACE_AGGVALUE_QUANTIZED
> +} dtrace_aggvalue_kind_t;
> +
> +typedef struct dtrace_aggvalue_bucket {
> + int64_t bucket;
> + uint64_t count;
> +} dtrace_aggvalue_bucket_t;
> +
> +/* Quantized buckets are supplied by the caller through dtagv_buckets. */
> +typedef struct dtrace_aggvalue {
> + dtrace_aggvalue_kind_t dtagv_kind;
> + uint16_t dtagv_action;
> + uint64_t dtagv_normal;
> + dtrace_aggvalue_bucket_t *dtagv_buckets;
> + size_t dtagv_bucket_capacity;
> + size_t dtagv_bucket_count;
> + union {
> + int64_t integer;
> + uint64_t unsigned_integer;
> + double real;
> + struct {
> + const void *data;
> + size_t size;
> + } raw;
> + } dtagv_value;
> +} dtrace_aggvalue_t;
> +
> +typedef enum dtrace_aggkey_kind {
> + DTRACE_AGGKEY_INTEGER,
> + DTRACE_AGGKEY_STRING,
> + DTRACE_AGGKEY_BYTES,
> + DTRACE_AGGKEY_STACK,
> + DTRACE_AGGKEY_SYMBOL,
> + DTRACE_AGGKEY_MODULE,
> + DTRACE_AGGKEY_USER_ADDRESS,
> + DTRACE_AGGKEY_ADDRESS = DTRACE_AGGKEY_USER_ADDRESS
> +} dtrace_aggkey_kind_t;
> +
> +typedef struct dtrace_aggkey {
> + dtrace_aggkey_kind_t dtagk_kind;
> + union {
> + int64_t integer;
> + struct {
> + char *data;
> + size_t size;
> + } bytes;
> + struct {
> + char **frames;
> + size_t depth;
> + } stack;
> + char *name;
> + /* User addresses include the target PID to disambiguate ASLR. */
> + struct {
> + uint32_t pid;
> + uint64_t address;
> + } user_address;
> + } dtagk_value;
> +} dtrace_aggkey_t;
> +
> +/*
> + * Decode all keys in an aggregation snapshot. The caller supplies an array
> + * with capacity for dtagd_nkrecs - 1 descriptors. Returned strings, byte
> + * buffers, and stack frames are owned by the caller and must be released
> + * with dtrace_aggregate_key_free(). Zero-key aggregations return zero.
> + */
> +extern int dtrace_aggregate_keys(const dtrace_aggdata_t *, dtrace_aggkey_t *,
> + size_t);
> +extern void dtrace_aggregate_key_free(dtrace_aggkey_t *);
> +
> +/*
> + * Decode one aggregation value. Integer values can be signed or unsigned.
> + * Raw data points into the snapshot and is valid only while that snapshot
> + * remains valid. Quantized values require a caller-provided bucket array and
> + * capacity.
> + */
> +extern int dtrace_aggregate_value(const dtrace_aggdata_t *,
> + dtrace_aggvalue_t *);
> +
> typedef struct dtrace_print_aggdata {
> dtrace_hdl_t *dtpa_dtp; /* handle to DTrace library */
> dtrace_aggid_t dtpa_id; /* aggregation variable */
> diff --git a/libdtrace/libdtrace.ver b/libdtrace/libdtrace.ver
> index edbd5a1d..87d561b2 100644
> --- a/libdtrace/libdtrace.ver
> +++ b/libdtrace/libdtrace.ver
> @@ -8,8 +8,11 @@ LIBDTRACE_1.0 {
> global:
> dtrace_addr2str;
> dtrace_aggregate_clear;
> + dtrace_aggregate_keys;
> + dtrace_aggregate_key_free;
> dtrace_aggregate_print;
> dtrace_aggregate_snap;
> + dtrace_aggregate_value;
> dtrace_aggregate_walk;
> dtrace_aggregate_walk_joined;
> dtrace_aggregate_walk_keyrevsorted;
> --
> 2.43.5
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v5 05/11] python: Add cpython bindings for libdtrace
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (3 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 04/11] libdtrace: Add aggregation key/value computation APIs Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-05 16:23 ` Kris Van Hees
2026-08-21 16:10 ` [PATCH v5 06/11] dtrace.spec: add python bindings packaging Alan Maguire
` (5 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 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"):
execname = record["keys"][0][0]
probefunc = record["keys"][1][0]
print(f"{execname:<40} {probefunc:<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; each key is itself
a list so a multi-key aggregation is a list of a list.
If a key is a stack() or ustack() it is represented as an
ordered list of symbols from leaf to root; if the key is null
it is an empty list as in @foo = count();
Aggregation values are represented as integers/double 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 | 3 +
bindings/Build | 39 +
bindings/python/README.md | 205 +++
bindings/python/pyproject.toml | 3 +
bindings/python/setup.py | 67 +
bindings/python/src/pydtrace_module.c | 2057 +++++++++++++++++++++++++
configure | 4 +-
7 files changed, 2377 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..e26a1d84 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -106,6 +106,9 @@ INSTPKGCONFIGDIR = $(DESTDIR)$(PKGCONFIGDIR)
TESTDIR = $(LIBDIR)/dtrace/testsuite
INSTTESTDIR = $(DESTDIR)$(TESTDIR)
WITH_SYSTEMD = y
+PYTHON ?= python3
+PYTHON_BINDINGS_AVAILABLE := $(shell $(PYTHON) -c 'import os, sysconfig, setuptools; assert os.path.isfile(os.path.join(sysconfig.get_path("include"), "Python.h"))' >/dev/null 2>&1 && echo y)
+WITH_PYTHON ?= $(PYTHON_BINDINGS_AVAILABLE)
TARGETS =
DTRACE ?= $(objdir)/dtrace
diff --git a/bindings/Build b/bindings/Build
new file mode 100644
index 00000000..d02ead05
--- /dev/null
+++ b/bindings/Build
@@ -0,0 +1,39 @@
+# 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)
+ifeq ($(PYTHON_BINDINGS_AVAILABLE),)
+$(error Python bindings requested, but $(PYTHON) development headers or setuptools are unavailable; install them or use --without-python)
+endif
+
+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..a39d9176
--- /dev/null
+++ b/bindings/python/README.md
@@ -0,0 +1,205 @@
+# 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,
+defines=None)` returns a `DTraceProgram` object bound to the session. The
+optional `argv` sequence is encoded to UTF-8 and supplied as the D-script
+argument vector (for positional `$1`, `$2`, and so on); it is not a C
+preprocessor argument list.
+
+`defines` accepts a sequence of macro definitions such as
+`["FEATURE=1", "DEBUG"]`. Supplying it enables C preprocessing and passes
+each definition to cpp. Compile-time flags such as `DTRACE_C_ZDEFS` can be
+combined with it:
+
+```python
+from dtrace import DTRACE_C_ZDEFS, DTraceSession
+
+program = """
+missing-provider:::probe { trace(1); }
+BEGIN { @value = sum(FEATURE); }
+"""
+
+with DTraceSession() as dt:
+ compiled = dt.compile(
+ program,
+ cflags=DTRACE_C_ZDEFS,
+ defines=["FEATURE=42"],
+ )
+ dt.enable(compiled)
+```
+
+For preprocessing without macro definitions, pass `DTRACE_C_CPP` explicitly.
+This is required for C preprocessor directives such as `#include`:
+
+```python
+from dtrace import DTRACE_C_CPP, DTraceSession
+
+program = '#include "my_dtrace_defs.h"\nBEGIN { trace(MY_CONSTANT); }'
+
+with DTraceSession() as dt:
+ compiled = dt.compile(program, cflags=DTRACE_C_CPP)
+ dt.enable(compiled)
+```
+
+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. With `return_records=False`, `probes` is empty;
+passing `return_records=True` captures full probe metadata plus a list of
+record descriptors (size, action, alignment, raw bytes, and any libdtrace
+metadata).
+If you are just dealing with aggregations, there is no need to return records;
+aggregation snapshot walk is all you will need.
+
+### Aggregations and snapshots
+
+`agg_snap()` issues `dtrace_aggregate_snap()` to freeze the aggregation buffer;
+`agg_walk(mode="values")` walks that snapshot using the selected ordering mode
+(`"default"`, `"values"`, `"valrev"`, `"keys"`, etc.) and returns a list of
+entries. Each entry is a dictionary with:
+
+- `keys`: ordered list of aggregation keys.
+- `samples`: raw sample count collected for the entry.
+- `normal`: the normalisation factor applied to the aggregate (1 if unused).
+- `value`: the converted aggregate value.
+- `raw`: the raw byte payload backing `value`.
+
+Stacks produced by `stack()`, `ustack()`, or `jstack()` actions are converted to
+Python lists ordered from root to leaf (the binding inserts the deepest frame at
+the end of the list). Quantization actions (`quantize`, `lquantize`,
+`llquantize`) are represented as dictionaries mapping bucket identifiers to
+counts, already divided by any aggregation normaliser. Buckets use the same
+numeric semantics as libdtrace (e.g., powers-of-two for `quantize` or explicit
+boundaries for `lquantize`).
+
+The raw data remains available in `entry["raw"]` when you need the original
+binary layout, for example to re-run libdtrace helpers.
+
+### Process control helpers
+
+The binding exposes libdtrace process control for coordinated tracing:
+
+- `proc_create(args)` forks a new traced process, using the provided argument
+ list. It returns a `DTraceProc` wrapper that keeps the underlying handle.
+- `proc_grab_pid(pid)` attaches to an existing process by pid.
+- `proc_continue(proc)` resumes a previously created or grabbed process and
+ marks it as live so the session can detect when all traced processes exit.
+- `proc_release(proc)` releases libdtrace’s hold on the process once finished.
+
+`DTraceProc` currently exposes `getpid()` to query the grabbed pid. The session
+tracks how many processes are created or grabbed so that when the last one
+exits the consumer loop observes `DTRACE_STATUS_EXITED` and stops automatically.
+
+### Detecting completion or stop conditions
+
+The binding raises `DTraceError` when libdtrace reports failures. To detect a
+graceful stop, inspect either `status()` or the first element of the tuple
+returned by `work()`. When libdtrace reports `DTRACE_STATUS_STOPPED` or
+`DTRACE_STATUS_FILLED`, you can drain remaining data, call `agg_snap()`/
+`agg_walk()` if needed, and then `close()` the session. A session observing a
+`DTRACEACT_EXIT` record records the exit status internally, and a stopped state
+is reported even if `dtrace_status()` still returns `DTRACE_STATUS_OKAY`, so
+applications can rely on `status()` reflecting exits from traced processes.
+
+When running under a context manager, exiting the `with` block closes the
+session automatically, regardless of whether tracing finished normally or due
+to `stop()`.
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
new file mode 100644
index 00000000..7b959378
--- /dev/null
+++ b/bindings/python/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=64", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
new file mode 100644
index 00000000..331e67df
--- /dev/null
+++ b/bindings/python/setup.py
@@ -0,0 +1,67 @@
+import os
+import subprocess
+from pathlib import Path
+from typing import List
+
+from setuptools import Extension, setup
+
+ROOT = Path(__file__).resolve().parents[2]
+OBJDIR = Path(os.environ.get("DTRACE_OBJDIR", ROOT / "build"))
+
+def project_version() -> str:
+ """Return the DTrace version used by the top-level build."""
+ version = os.environ.get("DTRACE_VERSION")
+ if version:
+ return version
+
+ return subprocess.check_output(
+ [str(ROOT / "libdtrace" / "mkvers"), "-vcurrent=t",
+ str(ROOT / "libdtrace" / "versions.list")],
+ universal_newlines=True,
+ ).strip()
+
+# Default include directories assume an in-tree build of libdtrace.
+default_include_dirs = [
+ str(ROOT / "include"),
+ str(ROOT / "libdtrace"),
+ str(ROOT / "uts" / "common"),
+ str(ROOT / "include" / "dtrace"),
+ str(OBJDIR),
+]
+
+default_library_dirs: List[str] = []
+if OBJDIR.exists():
+ default_library_dirs.append(str(OBJDIR))
+
+extra_include = os.environ.get("DTRACE_INCLUDE_DIRS")
+if extra_include:
+ default_include_dirs.extend(p for p in extra_include.split(os.pathsep) if p)
+
+extra_library = os.environ.get("DTRACE_LIBRARY_DIRS")
+if extra_library:
+ default_library_dirs.extend(p for p in extra_library.split(os.pathsep) if p)
+
+extra_link_args = os.environ.get("DTRACE_EXTRA_LINK_ARGS", "").split()
+extra_compile_args = os.environ.get("DTRACE_EXTRA_COMPILE_ARGS", "").split()
+
+ext_modules = [
+ Extension(
+ "dtrace",
+ sources=["src/pydtrace_module.c"],
+ include_dirs=default_include_dirs,
+ libraries=["dtrace"],
+ library_dirs=default_library_dirs,
+ extra_compile_args=extra_compile_args,
+ extra_link_args=extra_link_args,
+ )
+]
+
+setup(
+ name="dtrace",
+ version=project_version(),
+ description="Python bindings for libdtrace",
+ author="Oracle Linux DTrace maintainers",
+ license="UPL",
+ python_requires=">=3.6",
+ ext_modules=ext_modules,
+)
diff --git a/bindings/python/src/pydtrace_module.c b/bindings/python/src/pydtrace_module.c
new file mode 100644
index 00000000..917c4604
--- /dev/null
+++ b/bindings/python/src/pydtrace_module.c
@@ -0,0 +1,2057 @@
+/*
+ * 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 <errno.h>
+#include <math.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <pthread.h>
+
+#include <dtrace.h>
+#include "dt_aggregate.h"
+
+#ifndef UNUSED
+#define UNUSED(x) ((void)(x))
+#endif
+
+/*
+ * libdtrace handles are serialized by a per-session lock. It protects a
+ * handle across all libdtrace operations, including calls that release the
+ * GIL while they may block. The GIL is dropped only where the libdtrace call
+ * and its callbacks do not access Python objects.
+ *
+ * Compilation additionally uses a process-wide lock because libdtrace's
+ * compiler has global state shared by all sessions.
+ */
+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;
+ pthread_mutex_t dt_lock; /* Serialize access to this libdtrace handle. */
+} 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;
+}
+
+/*
+ * Do not hold the GIL while waiting for a libdtrace operation in another
+ * thread. In particular, work() drops the GIL while it waits for data but
+ * retains this lock to keep its handle alive.
+ */
+static void
+session_dtrace_lock(PyDTraceSession *self)
+{
+ Py_BEGIN_ALLOW_THREADS
+ pthread_mutex_lock(&self->dt_lock);
+ Py_END_ALLOW_THREADS
+}
+
+static void
+session_dtrace_unlock(PyDTraceSession *self)
+{
+ pthread_mutex_unlock(&self->dt_lock);
+}
+
+static int
+session_dtrace_lock_open(PyDTraceSession *self)
+{
+ session_dtrace_lock(self);
+ if (self->closed || self->dtp == NULL) {
+ session_dtrace_unlock(self);
+ PyErr_SetString(PyExc_DTraceError, "DTrace session is closed");
+ return -1;
+ }
+ return 0;
+}
+
+static PyObject *
+raise_dtrace_error_with_code(dtrace_hdl_t *dtp, int error, const char *ctx)
+{
+ const char *msg =
+ dtp != NULL ? dtrace_errmsg(dtp, error) : "unknown error";
+
+ if (ctx != NULL)
+ PyErr_Format(PyExc_DTraceError, "%s: %s", ctx, msg);
+ else
+ PyErr_SetString(PyExc_DTraceError, msg);
+
+ return NULL;
+}
+
+static PyObject *
+raise_dtrace_error(PyDTraceSession *self, const char *ctx)
+{
+ int error = dtrace_errno(self->dtp);
+ return raise_dtrace_error_with_code(self->dtp, error, ctx);
+}
+
+static int
+dict_set_ulong(PyObject *dict, const char *key, unsigned long value)
+{
+ PyObject *obj = PyLong_FromUnsignedLong(value);
+ int rc;
+
+ if (obj == NULL)
+ return -1;
+
+ rc = PyDict_SetItemString(dict, key, obj);
+ Py_DECREF(obj);
+ return rc;
+}
+
+/* ------------------------------------------------------------------------- */
+/* Aggregation walk context */
+/* ------------------------------------------------------------------------- */
+
+typedef struct {
+ PyObject *list;
+ PyDTraceSession *session;
+} agg_walk_ctx_t;
+
+typedef struct {
+ PyObject *probes;
+ PyObject *current_probe;
+ PyObject *records;
+ PyDTraceSession *session;
+ int capture;
+ int aborted;
+} work_ctx_t;
+
+static inline uint64_t
+read_uint(const void *addr, size_t size)
+{
+ uint8_t v8;
+ uint16_t v16;
+ uint32_t v32;
+ uint64_t v64;
+
+ switch (size) {
+ case 1:
+ memcpy(&v8, addr, sizeof(v8));
+ return v8;
+ case 2:
+ memcpy(&v16, addr, sizeof(v16));
+ return v16;
+ case 4:
+ memcpy(&v32, addr, sizeof(v32));
+ return v32;
+ case 8:
+ memcpy(&v64, addr, sizeof(v64));
+ return v64;
+ default:
+ return 0;
+ }
+}
+
+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, int64_t bucket, uint64_t count)
+{
+ PyObject *key = PyLong_FromLongLong(bucket);
+ PyObject *val;
+
+ if (key == NULL)
+ return NULL;
+
+ val = PyLong_FromUnsignedLongLong(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_aggregate_value(PyDTraceSession *session,
+ const dtrace_aggdata_t *aggdata,
+ const dtrace_recdesc_t *rec)
+{
+ dtrace_aggvalue_bucket_t *buckets;
+ dtrace_aggvalue_t value = { 0 };
+ PyObject *result = NULL;
+
+ buckets = calloc(rec->dtrd_size / sizeof(int64_t) + 2,
+ sizeof(*buckets));
+ if (buckets == NULL)
+ return PyErr_NoMemory();
+ value.dtagv_buckets = buckets;
+ value.dtagv_bucket_capacity = rec->dtrd_size / sizeof(int64_t) + 2;
+ if (dtrace_aggregate_value(aggdata, &value) < 0) {
+ free(buckets);
+ return raise_dtrace_error(session, "dtrace_aggregate_value");
+ }
+ if (value.dtagv_kind == DTRACE_AGGVALUE_QUANTIZED) {
+ result = PyDict_New();
+ for (size_t i = 0; result != NULL && i < value.dtagv_bucket_count; i++)
+ if (quant_dict_add(result, value.dtagv_buckets[i].bucket,
+ value.dtagv_buckets[i].count) == NULL) {
+ Py_DECREF(result);
+ result = NULL;
+ }
+ } else if (value.dtagv_kind == DTRACE_AGGVALUE_FLOAT) {
+ result = PyFloat_FromDouble(value.dtagv_value.real);
+ } else if (value.dtagv_kind == DTRACE_AGGVALUE_INTEGER) {
+ result = PyLong_FromLongLong(value.dtagv_value.integer);
+ } else if (value.dtagv_kind == DTRACE_AGGVALUE_UNSIGNED) {
+ result = PyLong_FromUnsignedLongLong(
+ value.dtagv_value.unsigned_integer);
+ } else {
+ result = PyBytes_FromStringAndSize(value.dtagv_value.raw.data,
+ value.dtagv_value.raw.size);
+ }
+ free(buckets);
+ return result;
+}
+
+static PyObject *
+quantization_metadata(const dtrace_aggdesc_t *agg, uint16_t action)
+{
+ uint64_t sig = agg->dtagd_sig;
+
+ switch (action) {
+ case DT_AGG_LQUANTIZE:
+ return Py_BuildValue("{s:s,s:i,s:I,s:I}",
+ "action", "lquantize",
+ "base", DTRACE_LQUANTIZE_BASE(sig),
+ "step", (unsigned int)DTRACE_LQUANTIZE_STEP(sig),
+ "levels", (unsigned int)DTRACE_LQUANTIZE_LEVELS(sig));
+ case DT_AGG_LLQUANTIZE:
+ return Py_BuildValue("{s:s,s:I,s:I,s:I,s:I}",
+ "action", "llquantize",
+ "factor", (unsigned int)DTRACE_LLQUANTIZE_FACTOR(sig),
+ "lmag", (unsigned int)DTRACE_LLQUANTIZE_LMAG(sig),
+ "hmag", (unsigned int)DTRACE_LLQUANTIZE_HMAG(sig),
+ "steps", (unsigned int)DTRACE_LLQUANTIZE_STEPS(sig));
+ default:
+ return NULL;
+ }
+}
+
+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_aggregate_key_descriptor(const dtrace_aggkey_t *key)
+{
+ if (key->dtagk_kind == DTRACE_AGGKEY_INTEGER)
+ return PyLong_FromLongLong(key->dtagk_value.integer);
+ if (key->dtagk_kind == DTRACE_AGGKEY_BYTES)
+ return PyBytes_FromStringAndSize(key->dtagk_value.bytes.data,
+ (Py_ssize_t)key->dtagk_value.bytes.size);
+ if (key->dtagk_kind == DTRACE_AGGKEY_STRING ||
+ key->dtagk_kind == DTRACE_AGGKEY_SYMBOL ||
+ key->dtagk_kind == DTRACE_AGGKEY_MODULE)
+ return PyUnicode_FromString(key->dtagk_value.name);
+ if (key->dtagk_kind == DTRACE_AGGKEY_ADDRESS) {
+ PyObject *address = PyLong_FromUnsignedLongLong(
+ key->dtagk_value.user_address.address);
+ PyObject *pid = PyLong_FromUnsignedLong(
+ (unsigned long)key->dtagk_value.user_address.pid);
+ PyObject *result;
+ if (!address || !pid) {
+ Py_XDECREF(address);
+ Py_XDECREF(pid);
+ return NULL;
+ }
+ result = PyTuple_Pack(2, pid, address);
+ Py_DECREF(pid);
+ Py_DECREF(address);
+ return result;
+ }
+ if (key->dtagk_kind == DTRACE_AGGKEY_STACK) {
+ PyObject *list = PyList_New((Py_ssize_t)key->dtagk_value.stack.depth);
+ if (!list)
+ return NULL;
+ for (size_t i = 0; i < key->dtagk_value.stack.depth; i++) {
+ PyObject *frame = PyUnicode_FromString(
+ key->dtagk_value.stack.frames[i]);
+ if (!frame) {
+ Py_DECREF(list);
+ return NULL;
+ }
+ PyList_SET_ITEM(list, (Py_ssize_t)i, frame);
+ }
+ return list;
+ }
+ PyErr_SetString(PyExc_ValueError, "unknown aggregation key kind");
+ return NULL;
+}
+
+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;
+ /* Keep a uniform list representation: [] for zero keys, one element
+ * for scalar keys, and a nested list for stack keys. */
+
+ uint_t nkeys = agg->dtagd_nkrecs ? agg->dtagd_nkrecs - 1 : 0;
+ dtrace_aggkey_t *keydesc = nkeys ? calloc(nkeys, sizeof(*keydesc)) : NULL;
+ if (nkeys && !keydesc)
+ goto error;
+ if (dtrace_aggregate_keys(aggdata, keydesc, nkeys) < 0) {
+ free(keydesc);
+ Py_DECREF(keys);
+ goto error;
+ }
+ for (uint_t i = 0; i < nkeys; i++) {
+ PyObject *item = convert_aggregate_key_descriptor(&keydesc[i]);
+ PyObject *key_list;
+
+ if (item == NULL) {
+ for (uint_t j = 0; j < nkeys; j++)
+ dtrace_aggregate_key_free(&keydesc[j]);
+ free(keydesc);
+ Py_DECREF(keys);
+ goto error;
+ }
+ /* Every key is represented as a list. Stack keys therefore become
+ * a nested list of frame strings. */
+ key_list = PyList_New(1);
+ if (key_list == NULL) {
+ Py_DECREF(item);
+ for (uint_t j = 0; j < nkeys; j++)
+ dtrace_aggregate_key_free(&keydesc[j]);
+ free(keydesc);
+ Py_DECREF(keys);
+ goto error;
+ }
+ PyList_SET_ITEM(key_list, 0, item);
+ if (PyList_Append(keys, key_list) < 0) {
+ Py_DECREF(key_list);
+ for (uint_t j = 0; j < nkeys; j++)
+ dtrace_aggregate_key_free(&keydesc[j]);
+ free(keydesc);
+ Py_DECREF(keys);
+ goto error;
+ }
+ Py_DECREF(key_list);
+ }
+ for (uint_t i = 0; i < nkeys; i++)
+ dtrace_aggregate_key_free(&keydesc[i]);
+ free(keydesc);
+
+ 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_aggregate_value(session, aggdata, value_rec);
+ if (value == NULL)
+ goto error;
+ if (PyDict_SetItemString(entry, "value", value) < 0) {
+ Py_DECREF(value);
+ goto error;
+ }
+ Py_DECREF(value);
+
+ PyObject *quantization = quantization_metadata(agg, value_rec->dtrd_action);
+ if (quantization != NULL) {
+ if (PyDict_SetItemString(entry, "quantization", quantization) < 0) {
+ Py_DECREF(quantization);
+ goto error;
+ }
+ Py_DECREF(quantization);
+ }
+
+ 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)
+{
+ session_dtrace_lock(self);
+ if (self->dtp != NULL && !self->closed) {
+ Py_BEGIN_ALLOW_THREADS dtrace_close(self->dtp);
+ Py_END_ALLOW_THREADS
+ }
+ self->dtp = NULL;
+ session_dtrace_unlock(self);
+ pthread_mutex_destroy(&self->dt_lock);
+ 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;
+ if (pthread_mutex_init(&self->dt_lock, NULL) != 0) {
+ PyObject_Del(self);
+ PyErr_SetString(PyExc_RuntimeError,
+ "unable to initialize DTrace session lock");
+ return NULL;
+ }
+ }
+ 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;
+ int rc;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iI", kwlist, &version,
+ &flags))
+ return -1;
+
+ Py_BEGIN_ALLOW_THREADS
+ self->dtp = dtrace_open(version, flags, &err);
+ Py_END_ALLOW_THREADS
+ if (self->dtp == NULL)
+ return raise_dtrace_error_with_code(NULL, err, "dtrace_open") ==
+ NULL
+ ? -1
+ : -1;
+
+ self->fp = fopen("/dev/null", "a");
+ if (self->fp == NULL) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ dtrace_close(self->dtp);
+ self->dtp = NULL;
+ return -1;
+ }
+ Py_BEGIN_ALLOW_THREADS
+ rc = dtrace_init(self->dtp);
+ Py_END_ALLOW_THREADS
+ if (rc != 0) {
+ raise_dtrace_error(self, "dtrace_init");
+ dtrace_close(self->dtp);
+ if (self->fp)
+ 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);
+ if (self->fp)
+ 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))
+{
+ session_dtrace_lock(self);
+ 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;
+ }
+ session_dtrace_unlock(self);
+
+ 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);
+
+ session_dtrace_lock(self);
+ 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;
+ }
+ session_dtrace_unlock(self);
+
+ 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 (session_dtrace_lock_open(self) < 0) {
+ Py_XDECREF(value_str);
+ return NULL;
+ }
+ if (dtrace_setopt(self->dtp, opt, cvalue) != 0) {
+ session_dtrace_unlock(self);
+ Py_XDECREF(value_str);
+ return raise_dtrace_error(self, "dtrace_setopt");
+ }
+ session_dtrace_unlock(self);
+
+ 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",
+ "defines", NULL};
+ const char *source = NULL;
+ unsigned int cflags = 0;
+ PyObject *argv_obj = NULL;
+ PyObject *defines_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|IOiO", kwlist, &source,
+ &cflags, &argv_obj, &spec, &defines_obj))
+ return NULL;
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ pthread_mutex_lock(&compile_lock);
+
+ if (defines_obj != NULL && defines_obj != Py_None) {
+ PyObject *defines_seq = PySequence_Fast(
+ defines_obj, "defines must be a sequence of strings");
+ Py_ssize_t i, n;
+
+ if (defines_seq == NULL) {
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ return NULL;
+ }
+ n = PySequence_Fast_GET_SIZE(defines_seq);
+ for (i = 0; i < n; i++) {
+ PyObject *item = PySequence_Fast_GET_ITEM(defines_seq, i);
+ PyObject *string_obj = PyObject_Str(item);
+ PyObject *encoded_obj;
+ int rc;
+
+ if (string_obj == NULL) {
+ Py_DECREF(defines_seq);
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ return NULL;
+ }
+ encoded_obj = PyUnicode_AsEncodedString(string_obj, "utf-8",
+ "replace");
+ Py_DECREF(string_obj);
+ if (encoded_obj == NULL) {
+ Py_DECREF(defines_seq);
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ return NULL;
+ }
+ rc = dtrace_setopt(self->dtp, "define",
+ PyBytes_AS_STRING(encoded_obj));
+ Py_DECREF(encoded_obj);
+ if (rc != 0) {
+ Py_DECREF(defines_seq);
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ return raise_dtrace_error(self, "dtrace_setopt");
+ }
+ }
+ Py_DECREF(defines_seq);
+ cflags |= DTRACE_C_CPP;
+ }
+ if ((cflags & DTRACE_C_CPP) && spec != DTRACE_PROBESPEC_NAME) {
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ PyErr_SetString(PyExc_ValueError,
+ "non-default spec is unsupported with preprocessing");
+ 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) {
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ 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);
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ 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;
+ }
+ }
+
+ if (cflags & DTRACE_C_CPP) {
+ FILE *fp = tmpfile();
+
+ if (fp == NULL) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto compile_error;
+ }
+ if (fputs(source, fp) == EOF || fflush(fp) != 0 ||
+ fseek(fp, 0, SEEK_SET) != 0) {
+ fclose(fp);
+ PyErr_SetFromErrno(PyExc_IOError);
+ goto compile_error;
+ }
+ prog = dtrace_program_fcompile(self->dtp, fp, cflags,
+ (int)argc, argv);
+ fclose(fp);
+ } else {
+ prog = dtrace_program_strcompile(self->dtp, source,
+ (dtrace_probespec_t)spec, cflags,
+ (int)argc, argv);
+ }
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+
+ 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:
+ pthread_mutex_unlock(&compile_lock);
+ session_dtrace_unlock(self);
+ for (Py_ssize_t i = 0; i < argc; i++)
+ Py_XDECREF(encoded[i]);
+ PyMem_Free(encoded);
+ PyMem_Free(argv);
+ return NULL;
+}
+
+static PyObject *
+PyDTraceSession_enable(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"program", NULL};
+ PyDTraceProgram *program = NULL;
+ dtrace_proginfo_t info;
+ PyObject *result = NULL;
+ PyObject *attr = NULL;
+ int rc;
+
+ 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 (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ Py_BEGIN_ALLOW_THREADS
+ rc = dtrace_program_exec(self->dtp, program->prog, &info);
+ Py_END_ALLOW_THREADS
+ if (rc != 0) {
+ session_dtrace_unlock(self);
+ return raise_dtrace_error(self, "dtrace_program_exec");
+ }
+ session_dtrace_unlock(self);
+
+ result = PyDict_New();
+ if (result == NULL)
+ return NULL;
+
+ if (dict_set_ulong(result, "aggregations", info.dpi_aggregates) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "recgens", info.dpi_recgens) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "matches", info.dpi_matches) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(result, "speculations", info.dpi_speculations) < 0)
+ goto enable_error;
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "name", info.dpi_descattr.dtat_name) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "data", info.dpi_descattr.dtat_data) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "class", info.dpi_descattr.dtat_class) < 0)
+ goto enable_error;
+
+ if (PyDict_SetItemString(result, "descattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ attr = PyDict_New();
+ if (attr == NULL)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "name", info.dpi_stmtattr.dtat_name) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "data", info.dpi_stmtattr.dtat_data) < 0)
+ goto enable_error;
+
+ if (dict_set_ulong(attr, "class", info.dpi_stmtattr.dtat_class) < 0)
+ goto enable_error;
+
+ if (PyDict_SetItemString(result, "stmtattr", attr) < 0)
+ goto enable_error;
+ Py_DECREF(attr);
+ attr = NULL;
+
+ return result;
+
+enable_error:
+ Py_XDECREF(attr);
+ Py_DECREF(result);
+ return NULL;
+}
+
+static PyObject *
+PyDTraceSession_go(PyDTraceSession *self, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"cflags", NULL};
+ unsigned int cflags = 0;
+ int rc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|I", kwlist, &cflags))
+ return NULL;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ Py_BEGIN_ALLOW_THREADS rc = dtrace_go(self->dtp, cflags);
+ Py_END_ALLOW_THREADS
+ session_dtrace_unlock(self);
+
+ 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 (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ if (dtrace_stop(self->dtp) != 0) {
+ session_dtrace_unlock(self);
+ return raise_dtrace_error(self, "dtrace_stop");
+ }
+ session_dtrace_unlock(self);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_update(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ int rc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ Py_BEGIN_ALLOW_THREADS
+ rc = dtrace_update(self->dtp);
+ Py_END_ALLOW_THREADS
+ if (rc != 0) {
+ session_dtrace_unlock(self);
+ return raise_dtrace_error(self, "dtrace_update");
+ }
+ session_dtrace_unlock(self);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_status(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ int status;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ status = dtrace_status(self->dtp);
+ if (status == DTRACE_STATUS_ERROR) {
+ session_dtrace_unlock(self);
+ 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;
+ session_dtrace_unlock(self);
+ 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:
+ memcpy(&self->exit_status, (const void *)addr,
+ sizeof(self->exit_status));
+ 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);
+ /* dtrace_work() has already advanced dtpda_data to this record. */
+ PyObject *raw = PyBytes_FromStringAndSize(
+ (const char *)data->dtpda_data,
+ 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 workstatus;
+ 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;
+ }
+
+ if (session_dtrace_lock_open(self) < 0) {
+ Py_XDECREF(ctx.probes);
+ return NULL;
+ }
+ if (ctx.capture) {
+ workstatus = dtrace_work(self->dtp, self->fp, consume_probe,
+ consume_rec, &ctx);
+ } else {
+ /* The no-capture callbacks do not access Python objects. */
+ Py_BEGIN_ALLOW_THREADS
+ workstatus = dtrace_work(self->dtp, self->fp, consume_probe,
+ consume_rec, &ctx);
+ Py_END_ALLOW_THREADS
+ }
+
+ /* An interrupted polling wait does not invalidate the DTrace session. */
+ if (workstatus == DTRACE_WORKSTATUS_ERROR &&
+ dtrace_errno(self->dtp) == EINTR)
+ workstatus = DTRACE_WORKSTATUS_OKAY;
+
+ if (workstatus == DTRACE_WORKSTATUS_ERROR ||
+ (ctx.capture && ctx.aborted)) {
+ result = raise_dtrace_error(self, "dtrace_work");
+ session_dtrace_unlock(self);
+ Py_XDECREF(ctx.probes);
+ return result;
+ }
+
+ status = dtrace_status(self->dtp);
+ if (status == DTRACE_STATUS_ERROR) {
+ result = raise_dtrace_error(self, "dtrace_status");
+ session_dtrace_unlock(self);
+ Py_XDECREF(ctx.probes);
+ return result;
+ }
+ if (status == DTRACE_STATUS_OKAY && self->status)
+ status = self->status;
+ if (status == DTRACE_STATUS_OKAY && self->pids > 0 &&
+ self->pids_live == 0) {
+ status = DTRACE_STATUS_EXITED;
+ self->status = status;
+ }
+ session_dtrace_unlock(self);
+
+ 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))
+{
+ int rc;
+
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ Py_BEGIN_ALLOW_THREADS
+ rc = dtrace_aggregate_snap(self->dtp);
+ Py_END_ALLOW_THREADS
+ if (rc != 0) {
+ session_dtrace_unlock(self);
+ return raise_dtrace_error(self, "dtrace_aggregate_snap");
+ }
+ session_dtrace_unlock(self);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PyDTraceSession_agg_clear(PyDTraceSession *self, PyObject *Py_UNUSED(args))
+{
+ if (ensure_open(self) < 0)
+ return NULL;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ dtrace_aggregate_clear(self->dtp);
+ session_dtrace_unlock(self);
+ 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;
+
+ if (session_dtrace_lock_open(self) < 0) {
+ Py_DECREF(ctx.list);
+ return NULL;
+ }
+ rc = walker(self->dtp, agg_walk_callback, &ctx);
+ if (rc != 0) {
+ PyObject *error = raise_dtrace_error(self, "dtrace_aggregate_walk");
+
+ session_dtrace_unlock(self);
+ Py_DECREF(ctx.list);
+ return error;
+ }
+ session_dtrace_unlock(self);
+
+ 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) {
+ session_dtrace_lock(self->session);
+ if (self->session->dtp != NULL)
+ dtrace_proc_release(self->session->dtp, self->proc);
+ session_dtrace_unlock(self->session);
+ }
+
+ 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;
+ }
+
+ if (session_dtrace_lock_open(self->session) < 0)
+ return NULL;
+ if (self->proc == NULL) {
+ session_dtrace_unlock(self->session);
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is released");
+ return NULL;
+ }
+ pid = dtrace_proc_getpid(self->session->dtp, self->proc);
+ session_dtrace_unlock(self->session);
+
+ 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;
+ if (session_dtrace_lock_open(self) < 0)
+ goto error;
+ proc = dtrace_proc_create(self->dtp, argvlist[0], argvlist, 0);
+ if (proc == NULL) {
+ ret = raise_dtrace_error(self, "dtrace_proc_create");
+ session_dtrace_unlock(self);
+ goto error;
+ }
+
+ ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
+ if (ret == NULL) {
+ dtrace_proc_release(self->dtp, proc);
+ session_dtrace_unlock(self);
+ goto error;
+ }
+ self->pids++;
+ session_dtrace_unlock(self);
+ 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:
+ 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;
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ proc = dtrace_proc_grab_pid(self->dtp, pid, 0);
+ if (!proc) {
+ ret = raise_dtrace_error(self, "dtrace_proc_grab_pid");
+ session_dtrace_unlock(self);
+ return ret;
+ }
+ ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
+ if (ret == NULL) {
+ dtrace_proc_release(self->dtp, proc);
+ session_dtrace_unlock(self);
+ return ret;
+ }
+ self->pids++;
+ session_dtrace_unlock(self);
+ 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;
+ }
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ if (proc->proc == NULL) {
+ session_dtrace_unlock(self);
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is released");
+ return NULL;
+ }
+ dtrace_proc_continue(self->dtp, proc->proc);
+ self->pids_live++;
+ session_dtrace_unlock(self);
+
+ 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;
+ }
+
+ if (session_dtrace_lock_open(self) < 0)
+ return NULL;
+ if (proc->proc == NULL) {
+ session_dtrace_unlock(self);
+ PyErr_SetString(PyExc_DTraceError,
+ "DTrace process handle is already released");
+ return NULL;
+ }
+ dtrace_proc_release(self->dtp, proc->proc);
+ session_dtrace_unlock(self);
+ proc->proc = NULL;
+
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef PyDTraceSession_methods[] = {
+ {"close", (PyCFunction)PyDTraceSession_close, METH_NOARGS,
+ "Close the session."},
+ {"__enter__", (PyCFunction)PyDTraceSession_enter, METH_NOARGS,
+ "Context manager entry."},
+ {"__exit__", (PyCFunction)PyDTraceSession_exit, METH_VARARGS,
+ "Context manager exit."},
+ {"setopt", (PyCFunction)PyDTraceSession_setopt,
+ METH_VARARGS | METH_KEYWORDS, "Set a DTrace option."},
+ {"compile", (PyCFunction)PyDTraceSession_compile,
+ METH_VARARGS | METH_KEYWORDS, "Compile a program from a string."},
+ {"enable", (PyCFunction)PyDTraceSession_enable,
+ METH_VARARGS | METH_KEYWORDS, "Enable a compiled program."},
+ {"go", (PyCFunction)PyDTraceSession_go, METH_VARARGS | METH_KEYWORDS,
+ "Start tracing."},
+ {"stop", (PyCFunction)PyDTraceSession_stop, METH_NOARGS,
+ "Stop tracing."},
+ {"update", (PyCFunction)PyDTraceSession_update, METH_NOARGS,
+ "Update module cache."},
+ {"status", (PyCFunction)PyDTraceSession_status, METH_NOARGS,
+ "Retrieve tracing status."},
+ {"work", (PyCFunction)PyDTraceSession_work,
+ METH_VARARGS | METH_KEYWORDS, "Do consumer loop work."},
+ {"agg_snap", (PyCFunction)PyDTraceSession_agg_snap, METH_NOARGS,
+ "Snapshot aggregation buffers."},
+ {"agg_clear", (PyCFunction)PyDTraceSession_agg_clear, METH_NOARGS,
+ "Clear aggregation buffers."},
+ {"agg_walk", (PyCFunction)PyDTraceSession_agg_walk,
+ METH_VARARGS | METH_KEYWORDS, "Return aggregation results."},
+ {"proc_create", (PyCFunction)PyDTraceSession_proc_create,
+ METH_VARARGS | METH_KEYWORDS, "Create process with args for tracing."},
+ {"proc_grab_pid", (PyCFunction)PyDTraceSession_proc_grab_pid,
+ METH_VARARGS | METH_KEYWORDS, "Grab pid."},
+ {"proc_continue", (PyCFunction)PyDTraceSession_proc_continue,
+ METH_VARARGS | METH_KEYWORDS, "Continue proc execution."},
+ {"proc_release", (PyCFunction)PyDTraceSession_proc_release,
+ METH_VARARGS | METH_KEYWORDS, "Relase proc."},
+ {NULL, NULL, 0, NULL}};
+
+static PyTypeObject PyDTraceSessionType = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "dtrace.DTraceSession",
+ .tp_basicsize = sizeof(PyDTraceSession),
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = PyDTraceSession_new,
+ .tp_init = (initproc)PyDTraceSession_init,
+ .tp_dealloc = (destructor)PyDTraceSession_dealloc,
+ .tp_methods = PyDTraceSession_methods,
+};
+
+/* ------------------------------------------------------------------------- */
+/* Module definition */
+/* ------------------------------------------------------------------------- */
+
+static struct PyModuleDef pydtrace_module = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "dtrace",
+ .m_doc = "Python bindings for libdtrace.",
+ .m_size = -1,
+};
+
+PyMODINIT_FUNC
+PyInit_dtrace(void)
+{
+ PyObject *m;
+
+ if (PyType_Ready(&PyDTraceSessionType) < 0)
+ return NULL;
+ if (PyType_Ready(&PyDTraceProgramType) < 0)
+ return NULL;
+ if (PyType_Ready(&PyDTraceProcType) < 0)
+ return NULL;
+
+ m = PyModule_Create(&pydtrace_module);
+ if (m == NULL)
+ return NULL;
+
+ PyExc_DTraceError = PyErr_NewException("dtrace.DTraceError",
+ PyExc_RuntimeError, NULL);
+ if (PyExc_DTraceError == NULL) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceSessionType);
+ if (PyModule_AddObject(m, "DTraceSession",
+ (PyObject *)&PyDTraceSessionType) < 0) {
+ Py_DECREF(&PyDTraceSessionType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceProgramType);
+ if (PyModule_AddObject(m, "DTraceProgram",
+ (PyObject *)&PyDTraceProgramType) < 0) {
+ Py_DECREF(&PyDTraceProgramType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(&PyDTraceProcType);
+ if (PyModule_AddObject(m, "DTraceProc", (PyObject *)&PyDTraceProcType) <
+ 0) {
+ Py_DECREF(&PyDTraceProcType);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ Py_INCREF(PyExc_DTraceError);
+ if (PyModule_AddObject(m, "DTraceError", PyExc_DTraceError) < 0) {
+ Py_DECREF(PyExc_DTraceError);
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_VERSION", DTRACE_VERSION) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_C_CPP", DTRACE_C_CPP) < 0 ||
+ PyModule_AddIntConstant(m, "DTRACE_C_ZDEFS", DTRACE_C_ZDEFS) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_NONE",
+ DTRACE_STATUS_NONE) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_OKAY",
+ DTRACE_STATUS_OKAY) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_EXITED",
+ DTRACE_STATUS_EXITED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_FILLED",
+ DTRACE_STATUS_FILLED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_STATUS_STOPPED",
+ DTRACE_STATUS_STOPPED) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_DONE",
+ DTRACE_WORKSTATUS_DONE) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+ if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_OKAY",
+ DTRACE_WORKSTATUS_OKAY) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+ return m;
+}
diff --git a/configure b/configure
index 403b1a09..fce22e9f 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: enabled when prerequisites are present)
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] 18+ messages in thread* Re: [PATCH v5 05/11] python: Add cpython bindings for libdtrace
2026-08-21 16:10 ` [PATCH v5 05/11] python: Add cpython bindings for libdtrace Alan Maguire
@ 2026-10-05 16:23 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-05 16:23 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
This is great work!
A few initial comments below as a result of trying to merge the code
to do a thorough review. I am addressing the blocking ones as I merge
the code to a temp tree, but wanted to make you aware.
On Fri, Aug 21, 2026 at 05:10:53PM +0100, Alan Maguire wrote:
> 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"):
> execname = record["keys"][0][0]
> probefunc = record["keys"][1][0]
> print(f"{execname:<40} {probefunc:<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; each key is itself
> a list so a multi-key aggregation is a list of a list.
> If a key is a stack() or ustack() it is represented as an
> ordered list of symbols from leaf to root; if the key is null
> it is an empty list as in @foo = count();
>
> Aggregation values are represented as integers/double 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 | 3 +
> bindings/Build | 39 +
> bindings/python/README.md | 205 +++
> bindings/python/pyproject.toml | 3 +
> bindings/python/setup.py | 67 +
> bindings/python/src/pydtrace_module.c | 2057 +++++++++++++++++++++++++
> configure | 4 +-
> 7 files changed, 2377 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..e26a1d84 100644
> --- a/GNUmakefile
> +++ b/GNUmakefile
> @@ -106,6 +106,9 @@ INSTPKGCONFIGDIR = $(DESTDIR)$(PKGCONFIGDIR)
> TESTDIR = $(LIBDIR)/dtrace/testsuite
> INSTTESTDIR = $(DESTDIR)$(TESTDIR)
> WITH_SYSTEMD = y
> +PYTHON ?= python3
> +PYTHON_BINDINGS_AVAILABLE := $(shell $(PYTHON) -c 'import os, sysconfig, setuptools; assert os.path.isfile(os.path.join(sysconfig.get_path("include"), "Python.h"))' >/dev/null 2>&1 && echo y)
> +WITH_PYTHON ?= $(PYTHON_BINDINGS_AVAILABLE)
I think we should do this availability-checking in Makeconfig, since
that is where we do it for other cases. The configure script then
should also get a --with/without-python option to let the user specify
whether they want it.
> TARGETS =
>
> DTRACE ?= $(objdir)/dtrace
> diff --git a/bindings/Build b/bindings/Build
> new file mode 100644
> index 00000000..d02ead05
> --- /dev/null
> +++ b/bindings/Build
This Build file should be in bindings/python, and bindings/Build should
just be a simple driver for building its subdirectories.
> @@ -0,0 +1,39 @@
> +# 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)
> +ifeq ($(PYTHON_BINDINGS_AVAILABLE),)
> +$(error Python bindings requested, but $(PYTHON) development headers or setuptools are unavailable; install them or use --without-python)
> +endif
> +
> +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
For consistency, we should specify -q on the build command wben
verbose=no.
> + 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..a39d9176
> --- /dev/null
> +++ b/bindings/python/README.md
> @@ -0,0 +1,205 @@
> +# 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,
> +defines=None)` returns a `DTraceProgram` object bound to the session. The
> +optional `argv` sequence is encoded to UTF-8 and supplied as the D-script
> +argument vector (for positional `$1`, `$2`, and so on); it is not a C
> +preprocessor argument list.
> +
> +`defines` accepts a sequence of macro definitions such as
> +`["FEATURE=1", "DEBUG"]`. Supplying it enables C preprocessing and passes
> +each definition to cpp. Compile-time flags such as `DTRACE_C_ZDEFS` can be
> +combined with it:
> +
> +```python
> +from dtrace import DTRACE_C_ZDEFS, DTraceSession
> +
> +program = """
> +missing-provider:::probe { trace(1); }
> +BEGIN { @value = sum(FEATURE); }
> +"""
> +
> +with DTraceSession() as dt:
> + compiled = dt.compile(
> + program,
> + cflags=DTRACE_C_ZDEFS,
> + defines=["FEATURE=42"],
> + )
> + dt.enable(compiled)
> +```
> +
> +For preprocessing without macro definitions, pass `DTRACE_C_CPP` explicitly.
> +This is required for C preprocessor directives such as `#include`:
> +
> +```python
> +from dtrace import DTRACE_C_CPP, DTraceSession
> +
> +program = '#include "my_dtrace_defs.h"\nBEGIN { trace(MY_CONSTANT); }'
> +
> +with DTraceSession() as dt:
> + compiled = dt.compile(program, cflags=DTRACE_C_CPP)
> + dt.enable(compiled)
> +```
> +
> +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. With `return_records=False`, `probes` is empty;
> +passing `return_records=True` captures full probe metadata plus a list of
> +record descriptors (size, action, alignment, raw bytes, and any libdtrace
> +metadata).
> +If you are just dealing with aggregations, there is no need to return records;
> +aggregation snapshot walk is all you will need.
> +
> +### Aggregations and snapshots
> +
> +`agg_snap()` issues `dtrace_aggregate_snap()` to freeze the aggregation buffer;
> +`agg_walk(mode="values")` walks that snapshot using the selected ordering mode
> +(`"default"`, `"values"`, `"valrev"`, `"keys"`, etc.) and returns a list of
> +entries. Each entry is a dictionary with:
> +
> +- `keys`: ordered list of aggregation keys.
> +- `samples`: raw sample count collected for the entry.
> +- `normal`: the normalisation factor applied to the aggregate (1 if unused).
> +- `value`: the converted aggregate value.
> +- `raw`: the raw byte payload backing `value`.
> +
> +Stacks produced by `stack()`, `ustack()`, or `jstack()` actions are converted to
> +Python lists ordered from root to leaf (the binding inserts the deepest frame at
> +the end of the list). Quantization actions (`quantize`, `lquantize`,
> +`llquantize`) are represented as dictionaries mapping bucket identifiers to
> +counts, already divided by any aggregation normaliser. Buckets use the same
> +numeric semantics as libdtrace (e.g., powers-of-two for `quantize` or explicit
> +boundaries for `lquantize`).
> +
> +The raw data remains available in `entry["raw"]` when you need the original
> +binary layout, for example to re-run libdtrace helpers.
> +
> +### Process control helpers
> +
> +The binding exposes libdtrace process control for coordinated tracing:
> +
> +- `proc_create(args)` forks a new traced process, using the provided argument
> + list. It returns a `DTraceProc` wrapper that keeps the underlying handle.
> +- `proc_grab_pid(pid)` attaches to an existing process by pid.
> +- `proc_continue(proc)` resumes a previously created or grabbed process and
> + marks it as live so the session can detect when all traced processes exit.
> +- `proc_release(proc)` releases libdtrace’s hold on the process once finished.
> +
> +`DTraceProc` currently exposes `getpid()` to query the grabbed pid. The session
> +tracks how many processes are created or grabbed so that when the last one
> +exits the consumer loop observes `DTRACE_STATUS_EXITED` and stops automatically.
> +
> +### Detecting completion or stop conditions
> +
> +The binding raises `DTraceError` when libdtrace reports failures. To detect a
> +graceful stop, inspect either `status()` or the first element of the tuple
> +returned by `work()`. When libdtrace reports `DTRACE_STATUS_STOPPED` or
> +`DTRACE_STATUS_FILLED`, you can drain remaining data, call `agg_snap()`/
> +`agg_walk()` if needed, and then `close()` the session. A session observing a
> +`DTRACEACT_EXIT` record records the exit status internally, and a stopped state
> +is reported even if `dtrace_status()` still returns `DTRACE_STATUS_OKAY`, so
> +applications can rely on `status()` reflecting exits from traced processes.
> +
> +When running under a context manager, exiting the `with` block closes the
> +session automatically, regardless of whether tracing finished normally or due
> +to `stop()`.
> diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
> new file mode 100644
> index 00000000..7b959378
> --- /dev/null
> +++ b/bindings/python/pyproject.toml
> @@ -0,0 +1,3 @@
> +[build-system]
> +requires = ["setuptools>=64", "wheel"]
> +build-backend = "setuptools.build_meta"
> diff --git a/bindings/python/setup.py b/bindings/python/setup.py
> new file mode 100644
> index 00000000..331e67df
> --- /dev/null
> +++ b/bindings/python/setup.py
> @@ -0,0 +1,67 @@
> +import os
> +import subprocess
> +from pathlib import Path
> +from typing import List
> +
> +from setuptools import Extension, setup
> +
> +ROOT = Path(__file__).resolve().parents[2]
> +OBJDIR = Path(os.environ.get("DTRACE_OBJDIR", ROOT / "build"))
> +
> +def project_version() -> str:
> + """Return the DTrace version used by the top-level build."""
> + version = os.environ.get("DTRACE_VERSION")
> + if version:
> + return version
> +
> + return subprocess.check_output(
> + [str(ROOT / "libdtrace" / "mkvers"), "-vcurrent=t",
> + str(ROOT / "libdtrace" / "versions.list")],
> + universal_newlines=True,
> + ).strip()
> +
> +# Default include directories assume an in-tree build of libdtrace.
> +default_include_dirs = [
> + str(ROOT / "include"),
> + str(ROOT / "libdtrace"),
> + str(ROOT / "uts" / "common"),
> + str(ROOT / "include" / "dtrace"),
> + str(OBJDIR),
> +]
> +
> +default_library_dirs: List[str] = []
> +if OBJDIR.exists():
> + default_library_dirs.append(str(OBJDIR))
> +
> +extra_include = os.environ.get("DTRACE_INCLUDE_DIRS")
> +if extra_include:
> + default_include_dirs.extend(p for p in extra_include.split(os.pathsep) if p)
> +
> +extra_library = os.environ.get("DTRACE_LIBRARY_DIRS")
> +if extra_library:
> + default_library_dirs.extend(p for p in extra_library.split(os.pathsep) if p)
> +
> +extra_link_args = os.environ.get("DTRACE_EXTRA_LINK_ARGS", "").split()
> +extra_compile_args = os.environ.get("DTRACE_EXTRA_COMPILE_ARGS", "").split()
> +
> +ext_modules = [
> + Extension(
> + "dtrace",
> + sources=["src/pydtrace_module.c"],
> + include_dirs=default_include_dirs,
> + libraries=["dtrace"],
> + library_dirs=default_library_dirs,
> + extra_compile_args=extra_compile_args,
> + extra_link_args=extra_link_args,
> + )
> +]
> +
> +setup(
> + name="dtrace",
> + version=project_version(),
> + description="Python bindings for libdtrace",
> + author="Oracle Linux DTrace maintainers",
> + license="UPL",
> + python_requires=">=3.6",
> + ext_modules=ext_modules,
> +)
> diff --git a/bindings/python/src/pydtrace_module.c b/bindings/python/src/pydtrace_module.c
> new file mode 100644
> index 00000000..917c4604
> --- /dev/null
> +++ b/bindings/python/src/pydtrace_module.c
> @@ -0,0 +1,2057 @@
> +/*
> + * 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 <errno.h>
> +#include <math.h>
> +#include <sys/param.h>
> +#include <sys/types.h>
> +#include <pthread.h>
> +
> +#include <dtrace.h>
> +#include "dt_aggregate.h"
> +
> +#ifndef UNUSED
> +#define UNUSED(x) ((void)(x))
> +#endif
> +
> +/*
> + * libdtrace handles are serialized by a per-session lock. It protects a
> + * handle across all libdtrace operations, including calls that release the
> + * GIL while they may block. The GIL is dropped only where the libdtrace call
> + * and its callbacks do not access Python objects.
> + *
> + * Compilation additionally uses a process-wide lock because libdtrace's
> + * compiler has global state shared by all sessions.
> + */
> +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;
> + pthread_mutex_t dt_lock; /* Serialize access to this libdtrace handle. */
> +} 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;
> +}
> +
> +/*
> + * Do not hold the GIL while waiting for a libdtrace operation in another
> + * thread. In particular, work() drops the GIL while it waits for data but
> + * retains this lock to keep its handle alive.
> + */
> +static void
> +session_dtrace_lock(PyDTraceSession *self)
> +{
> + Py_BEGIN_ALLOW_THREADS
> + pthread_mutex_lock(&self->dt_lock);
> + Py_END_ALLOW_THREADS
> +}
> +
> +static void
> +session_dtrace_unlock(PyDTraceSession *self)
> +{
> + pthread_mutex_unlock(&self->dt_lock);
> +}
> +
> +static int
> +session_dtrace_lock_open(PyDTraceSession *self)
> +{
> + session_dtrace_lock(self);
> + if (self->closed || self->dtp == NULL) {
> + session_dtrace_unlock(self);
> + PyErr_SetString(PyExc_DTraceError, "DTrace session is closed");
> + return -1;
> + }
> + return 0;
> +}
> +
> +static PyObject *
> +raise_dtrace_error_with_code(dtrace_hdl_t *dtp, int error, const char *ctx)
> +{
> + const char *msg =
> + dtp != NULL ? dtrace_errmsg(dtp, error) : "unknown error";
> +
> + if (ctx != NULL)
> + PyErr_Format(PyExc_DTraceError, "%s: %s", ctx, msg);
> + else
> + PyErr_SetString(PyExc_DTraceError, msg);
> +
> + return NULL;
> +}
> +
> +static PyObject *
> +raise_dtrace_error(PyDTraceSession *self, const char *ctx)
> +{
> + int error = dtrace_errno(self->dtp);
> + return raise_dtrace_error_with_code(self->dtp, error, ctx);
> +}
> +
> +static int
> +dict_set_ulong(PyObject *dict, const char *key, unsigned long value)
> +{
> + PyObject *obj = PyLong_FromUnsignedLong(value);
> + int rc;
> +
> + if (obj == NULL)
> + return -1;
> +
> + rc = PyDict_SetItemString(dict, key, obj);
> + Py_DECREF(obj);
> + return rc;
> +}
> +
> +/* ------------------------------------------------------------------------- */
> +/* Aggregation walk context */
> +/* ------------------------------------------------------------------------- */
> +
> +typedef struct {
> + PyObject *list;
> + PyDTraceSession *session;
> +} agg_walk_ctx_t;
> +
> +typedef struct {
> + PyObject *probes;
> + PyObject *current_probe;
> + PyObject *records;
> + PyDTraceSession *session;
> + int capture;
> + int aborted;
> +} work_ctx_t;
> +
> +static inline uint64_t
> +read_uint(const void *addr, size_t size)
> +{
> + uint8_t v8;
> + uint16_t v16;
> + uint32_t v32;
> + uint64_t v64;
> +
> + switch (size) {
> + case 1:
> + memcpy(&v8, addr, sizeof(v8));
> + return v8;
> + case 2:
> + memcpy(&v16, addr, sizeof(v16));
> + return v16;
> + case 4:
> + memcpy(&v32, addr, sizeof(v32));
> + return v32;
> + case 8:
> + memcpy(&v64, addr, sizeof(v64));
> + return v64;
> + default:
> + return 0;
> + }
> +}
> +
> +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, int64_t bucket, uint64_t count)
> +{
> + PyObject *key = PyLong_FromLongLong(bucket);
> + PyObject *val;
> +
> + if (key == NULL)
> + return NULL;
> +
> + val = PyLong_FromUnsignedLongLong(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_aggregate_value(PyDTraceSession *session,
> + const dtrace_aggdata_t *aggdata,
> + const dtrace_recdesc_t *rec)
> +{
> + dtrace_aggvalue_bucket_t *buckets;
> + dtrace_aggvalue_t value = { 0 };
> + PyObject *result = NULL;
> +
> + buckets = calloc(rec->dtrd_size / sizeof(int64_t) + 2,
> + sizeof(*buckets));
> + if (buckets == NULL)
> + return PyErr_NoMemory();
> + value.dtagv_buckets = buckets;
> + value.dtagv_bucket_capacity = rec->dtrd_size / sizeof(int64_t) + 2;
> + if (dtrace_aggregate_value(aggdata, &value) < 0) {
> + free(buckets);
> + return raise_dtrace_error(session, "dtrace_aggregate_value");
> + }
> + if (value.dtagv_kind == DTRACE_AGGVALUE_QUANTIZED) {
> + result = PyDict_New();
> + for (size_t i = 0; result != NULL && i < value.dtagv_bucket_count; i++)
> + if (quant_dict_add(result, value.dtagv_buckets[i].bucket,
> + value.dtagv_buckets[i].count) == NULL) {
> + Py_DECREF(result);
> + result = NULL;
> + }
> + } else if (value.dtagv_kind == DTRACE_AGGVALUE_FLOAT) {
> + result = PyFloat_FromDouble(value.dtagv_value.real);
> + } else if (value.dtagv_kind == DTRACE_AGGVALUE_INTEGER) {
> + result = PyLong_FromLongLong(value.dtagv_value.integer);
> + } else if (value.dtagv_kind == DTRACE_AGGVALUE_UNSIGNED) {
> + result = PyLong_FromUnsignedLongLong(
> + value.dtagv_value.unsigned_integer);
> + } else {
> + result = PyBytes_FromStringAndSize(value.dtagv_value.raw.data,
> + value.dtagv_value.raw.size);
> + }
> + free(buckets);
> + return result;
> +}
> +
> +static PyObject *
> +quantization_metadata(const dtrace_aggdesc_t *agg, uint16_t action)
> +{
> + uint64_t sig = agg->dtagd_sig;
> +
> + switch (action) {
> + case DT_AGG_LQUANTIZE:
> + return Py_BuildValue("{s:s,s:i,s:I,s:I}",
> + "action", "lquantize",
> + "base", DTRACE_LQUANTIZE_BASE(sig),
> + "step", (unsigned int)DTRACE_LQUANTIZE_STEP(sig),
> + "levels", (unsigned int)DTRACE_LQUANTIZE_LEVELS(sig));
> + case DT_AGG_LLQUANTIZE:
> + return Py_BuildValue("{s:s,s:I,s:I,s:I,s:I}",
> + "action", "llquantize",
> + "factor", (unsigned int)DTRACE_LLQUANTIZE_FACTOR(sig),
> + "lmag", (unsigned int)DTRACE_LLQUANTIZE_LMAG(sig),
> + "hmag", (unsigned int)DTRACE_LLQUANTIZE_HMAG(sig),
> + "steps", (unsigned int)DTRACE_LLQUANTIZE_STEPS(sig));
> + default:
> + return NULL;
> + }
> +}
> +
> +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_aggregate_key_descriptor(const dtrace_aggkey_t *key)
> +{
> + if (key->dtagk_kind == DTRACE_AGGKEY_INTEGER)
> + return PyLong_FromLongLong(key->dtagk_value.integer);
> + if (key->dtagk_kind == DTRACE_AGGKEY_BYTES)
> + return PyBytes_FromStringAndSize(key->dtagk_value.bytes.data,
> + (Py_ssize_t)key->dtagk_value.bytes.size);
> + if (key->dtagk_kind == DTRACE_AGGKEY_STRING ||
> + key->dtagk_kind == DTRACE_AGGKEY_SYMBOL ||
> + key->dtagk_kind == DTRACE_AGGKEY_MODULE)
> + return PyUnicode_FromString(key->dtagk_value.name);
> + if (key->dtagk_kind == DTRACE_AGGKEY_ADDRESS) {
> + PyObject *address = PyLong_FromUnsignedLongLong(
> + key->dtagk_value.user_address.address);
> + PyObject *pid = PyLong_FromUnsignedLong(
> + (unsigned long)key->dtagk_value.user_address.pid);
> + PyObject *result;
> + if (!address || !pid) {
> + Py_XDECREF(address);
> + Py_XDECREF(pid);
> + return NULL;
> + }
> + result = PyTuple_Pack(2, pid, address);
> + Py_DECREF(pid);
> + Py_DECREF(address);
> + return result;
> + }
> + if (key->dtagk_kind == DTRACE_AGGKEY_STACK) {
> + PyObject *list = PyList_New((Py_ssize_t)key->dtagk_value.stack.depth);
> + if (!list)
> + return NULL;
> + for (size_t i = 0; i < key->dtagk_value.stack.depth; i++) {
> + PyObject *frame = PyUnicode_FromString(
> + key->dtagk_value.stack.frames[i]);
> + if (!frame) {
> + Py_DECREF(list);
> + return NULL;
> + }
> + PyList_SET_ITEM(list, (Py_ssize_t)i, frame);
> + }
> + return list;
> + }
> + PyErr_SetString(PyExc_ValueError, "unknown aggregation key kind");
> + return NULL;
> +}
> +
> +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;
> + /* Keep a uniform list representation: [] for zero keys, one element
> + * for scalar keys, and a nested list for stack keys. */
> +
> + uint_t nkeys = agg->dtagd_nkrecs ? agg->dtagd_nkrecs - 1 : 0;
> + dtrace_aggkey_t *keydesc = nkeys ? calloc(nkeys, sizeof(*keydesc)) : NULL;
> + if (nkeys && !keydesc)
> + goto error;
> + if (dtrace_aggregate_keys(aggdata, keydesc, nkeys) < 0) {
> + free(keydesc);
> + Py_DECREF(keys);
> + goto error;
> + }
> + for (uint_t i = 0; i < nkeys; i++) {
> + PyObject *item = convert_aggregate_key_descriptor(&keydesc[i]);
> + PyObject *key_list;
> +
> + if (item == NULL) {
> + for (uint_t j = 0; j < nkeys; j++)
> + dtrace_aggregate_key_free(&keydesc[j]);
> + free(keydesc);
> + Py_DECREF(keys);
> + goto error;
> + }
> + /* Every key is represented as a list. Stack keys therefore become
> + * a nested list of frame strings. */
> + key_list = PyList_New(1);
> + if (key_list == NULL) {
> + Py_DECREF(item);
> + for (uint_t j = 0; j < nkeys; j++)
> + dtrace_aggregate_key_free(&keydesc[j]);
> + free(keydesc);
> + Py_DECREF(keys);
> + goto error;
> + }
> + PyList_SET_ITEM(key_list, 0, item);
> + if (PyList_Append(keys, key_list) < 0) {
> + Py_DECREF(key_list);
> + for (uint_t j = 0; j < nkeys; j++)
> + dtrace_aggregate_key_free(&keydesc[j]);
> + free(keydesc);
> + Py_DECREF(keys);
> + goto error;
> + }
> + Py_DECREF(key_list);
> + }
> + for (uint_t i = 0; i < nkeys; i++)
> + dtrace_aggregate_key_free(&keydesc[i]);
> + free(keydesc);
> +
> + 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_aggregate_value(session, aggdata, value_rec);
> + if (value == NULL)
> + goto error;
> + if (PyDict_SetItemString(entry, "value", value) < 0) {
> + Py_DECREF(value);
> + goto error;
> + }
> + Py_DECREF(value);
> +
> + PyObject *quantization = quantization_metadata(agg, value_rec->dtrd_action);
> + if (quantization != NULL) {
> + if (PyDict_SetItemString(entry, "quantization", quantization) < 0) {
> + Py_DECREF(quantization);
> + goto error;
> + }
> + Py_DECREF(quantization);
> + }
> +
> + 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)
> +{
> + session_dtrace_lock(self);
> + if (self->dtp != NULL && !self->closed) {
> + Py_BEGIN_ALLOW_THREADS dtrace_close(self->dtp);
> + Py_END_ALLOW_THREADS
> + }
> + self->dtp = NULL;
> + session_dtrace_unlock(self);
> + pthread_mutex_destroy(&self->dt_lock);
> + 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;
> + if (pthread_mutex_init(&self->dt_lock, NULL) != 0) {
> + PyObject_Del(self);
> + PyErr_SetString(PyExc_RuntimeError,
> + "unable to initialize DTrace session lock");
> + return NULL;
> + }
> + }
> + 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;
> + int rc;
> +
> + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iI", kwlist, &version,
> + &flags))
> + return -1;
> +
> + Py_BEGIN_ALLOW_THREADS
> + self->dtp = dtrace_open(version, flags, &err);
> + Py_END_ALLOW_THREADS
> + if (self->dtp == NULL)
> + return raise_dtrace_error_with_code(NULL, err, "dtrace_open") ==
> + NULL
> + ? -1
> + : -1;
> +
> + self->fp = fopen("/dev/null", "a");
> + if (self->fp == NULL) {
> + PyErr_SetFromErrno(PyExc_OSError);
> + dtrace_close(self->dtp);
> + self->dtp = NULL;
> + return -1;
> + }
> + Py_BEGIN_ALLOW_THREADS
> + rc = dtrace_init(self->dtp);
> + Py_END_ALLOW_THREADS
> + if (rc != 0) {
> + raise_dtrace_error(self, "dtrace_init");
> + dtrace_close(self->dtp);
> + if (self->fp)
> + 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) {
The dtrace_setopt() calls should be done before dtrace_init(). I would do
it right after the dtrace_open() call, to be consistent with cmd/dtrace.
You should also call dtrace_setoptenv(self->dtp, "DTRACE_OPT_") right
after the dtrace_setopt() calls. That is important for ensuring that we
can set DTRACE_OPT_SYSLIBDIR to specify the location of the DTrace BPF
libs (which is needed when testing DTrace on a system where dtrace is
not yet installed).
The dtrace_handle_proc() call can stay here.
> + raise_dtrace_error(self, "dtrace_setopt");
> + dtrace_close(self->dtp);
> + if (self->fp)
> + 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))
> +{
> + session_dtrace_lock(self);
> + 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;
> + }
> + session_dtrace_unlock(self);
> +
> + 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);
> +
> + session_dtrace_lock(self);
> + 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;
> + }
> + session_dtrace_unlock(self);
> +
> + 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 (session_dtrace_lock_open(self) < 0) {
> + Py_XDECREF(value_str);
> + return NULL;
> + }
> + if (dtrace_setopt(self->dtp, opt, cvalue) != 0) {
> + session_dtrace_unlock(self);
> + Py_XDECREF(value_str);
> + return raise_dtrace_error(self, "dtrace_setopt");
> + }
> + session_dtrace_unlock(self);
> +
> + 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",
> + "defines", NULL};
> + const char *source = NULL;
> + unsigned int cflags = 0;
> + PyObject *argv_obj = NULL;
> + PyObject *defines_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|IOiO", kwlist, &source,
> + &cflags, &argv_obj, &spec, &defines_obj))
> + return NULL;
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + pthread_mutex_lock(&compile_lock);
> +
> + if (defines_obj != NULL && defines_obj != Py_None) {
> + PyObject *defines_seq = PySequence_Fast(
> + defines_obj, "defines must be a sequence of strings");
> + Py_ssize_t i, n;
> +
> + if (defines_seq == NULL) {
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + return NULL;
> + }
> + n = PySequence_Fast_GET_SIZE(defines_seq);
> + for (i = 0; i < n; i++) {
> + PyObject *item = PySequence_Fast_GET_ITEM(defines_seq, i);
> + PyObject *string_obj = PyObject_Str(item);
> + PyObject *encoded_obj;
> + int rc;
> +
> + if (string_obj == NULL) {
> + Py_DECREF(defines_seq);
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + return NULL;
> + }
> + encoded_obj = PyUnicode_AsEncodedString(string_obj, "utf-8",
> + "replace");
> + Py_DECREF(string_obj);
> + if (encoded_obj == NULL) {
> + Py_DECREF(defines_seq);
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + return NULL;
> + }
> + rc = dtrace_setopt(self->dtp, "define",
> + PyBytes_AS_STRING(encoded_obj));
> + Py_DECREF(encoded_obj);
> + if (rc != 0) {
> + Py_DECREF(defines_seq);
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + return raise_dtrace_error(self, "dtrace_setopt");
> + }
> + }
> + Py_DECREF(defines_seq);
> + cflags |= DTRACE_C_CPP;
> + }
> + if ((cflags & DTRACE_C_CPP) && spec != DTRACE_PROBESPEC_NAME) {
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + PyErr_SetString(PyExc_ValueError,
> + "non-default spec is unsupported with preprocessing");
> + 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) {
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + 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);
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + 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;
> + }
> + }
> +
> + if (cflags & DTRACE_C_CPP) {
> + FILE *fp = tmpfile();
> +
> + if (fp == NULL) {
> + PyErr_SetFromErrno(PyExc_OSError);
> + goto compile_error;
> + }
> + if (fputs(source, fp) == EOF || fflush(fp) != 0 ||
> + fseek(fp, 0, SEEK_SET) != 0) {
> + fclose(fp);
> + PyErr_SetFromErrno(PyExc_IOError);
> + goto compile_error;
> + }
> + prog = dtrace_program_fcompile(self->dtp, fp, cflags,
> + (int)argc, argv);
> + fclose(fp);
> + } else {
> + prog = dtrace_program_strcompile(self->dtp, source,
> + (dtrace_probespec_t)spec, cflags,
> + (int)argc, argv);
> + }
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> +
> + 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:
> + pthread_mutex_unlock(&compile_lock);
> + session_dtrace_unlock(self);
> + for (Py_ssize_t i = 0; i < argc; i++)
> + Py_XDECREF(encoded[i]);
> + PyMem_Free(encoded);
> + PyMem_Free(argv);
> + return NULL;
> +}
> +
> +static PyObject *
> +PyDTraceSession_enable(PyDTraceSession *self, PyObject *args, PyObject *kwds)
> +{
> + static char *kwlist[] = {"program", NULL};
> + PyDTraceProgram *program = NULL;
> + dtrace_proginfo_t info;
> + PyObject *result = NULL;
> + PyObject *attr = NULL;
> + int rc;
> +
> + 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 (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + Py_BEGIN_ALLOW_THREADS
> + rc = dtrace_program_exec(self->dtp, program->prog, &info);
> + Py_END_ALLOW_THREADS
> + if (rc != 0) {
> + session_dtrace_unlock(self);
> + return raise_dtrace_error(self, "dtrace_program_exec");
> + }
> + session_dtrace_unlock(self);
> +
> + result = PyDict_New();
> + if (result == NULL)
> + return NULL;
> +
> + if (dict_set_ulong(result, "aggregations", info.dpi_aggregates) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(result, "recgens", info.dpi_recgens) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(result, "matches", info.dpi_matches) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(result, "speculations", info.dpi_speculations) < 0)
> + goto enable_error;
> +
> + attr = PyDict_New();
> + if (attr == NULL)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "name", info.dpi_descattr.dtat_name) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "data", info.dpi_descattr.dtat_data) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "class", info.dpi_descattr.dtat_class) < 0)
> + goto enable_error;
> +
> + if (PyDict_SetItemString(result, "descattr", attr) < 0)
> + goto enable_error;
> + Py_DECREF(attr);
> + attr = NULL;
> +
> + attr = PyDict_New();
> + if (attr == NULL)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "name", info.dpi_stmtattr.dtat_name) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "data", info.dpi_stmtattr.dtat_data) < 0)
> + goto enable_error;
> +
> + if (dict_set_ulong(attr, "class", info.dpi_stmtattr.dtat_class) < 0)
> + goto enable_error;
> +
> + if (PyDict_SetItemString(result, "stmtattr", attr) < 0)
> + goto enable_error;
> + Py_DECREF(attr);
> + attr = NULL;
> +
> + return result;
> +
> +enable_error:
> + Py_XDECREF(attr);
> + Py_DECREF(result);
> + return NULL;
> +}
> +
> +static PyObject *
> +PyDTraceSession_go(PyDTraceSession *self, PyObject *args, PyObject *kwds)
> +{
> + static char *kwlist[] = {"cflags", NULL};
> + unsigned int cflags = 0;
> + int rc;
> +
> + if (ensure_open(self) < 0)
> + return NULL;
> +
> + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|I", kwlist, &cflags))
> + return NULL;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + Py_BEGIN_ALLOW_THREADS rc = dtrace_go(self->dtp, cflags);
> + Py_END_ALLOW_THREADS
> + session_dtrace_unlock(self);
> +
> + 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 (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + if (dtrace_stop(self->dtp) != 0) {
> + session_dtrace_unlock(self);
> + return raise_dtrace_error(self, "dtrace_stop");
> + }
> + session_dtrace_unlock(self);
> +
> + Py_RETURN_NONE;
> +}
> +
> +static PyObject *
> +PyDTraceSession_update(PyDTraceSession *self, PyObject *Py_UNUSED(args))
> +{
> + int rc;
> +
> + if (ensure_open(self) < 0)
> + return NULL;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + Py_BEGIN_ALLOW_THREADS
> + rc = dtrace_update(self->dtp);
> + Py_END_ALLOW_THREADS
> + if (rc != 0) {
> + session_dtrace_unlock(self);
> + return raise_dtrace_error(self, "dtrace_update");
> + }
> + session_dtrace_unlock(self);
> +
> + Py_RETURN_NONE;
> +}
> +
> +static PyObject *
> +PyDTraceSession_status(PyDTraceSession *self, PyObject *Py_UNUSED(args))
> +{
> + int status;
> +
> + if (ensure_open(self) < 0)
> + return NULL;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + status = dtrace_status(self->dtp);
> + if (status == DTRACE_STATUS_ERROR) {
> + session_dtrace_unlock(self);
> + 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;
> + session_dtrace_unlock(self);
> + 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:
> + memcpy(&self->exit_status, (const void *)addr,
> + sizeof(self->exit_status));
> + 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);
> + /* dtrace_work() has already advanced dtpda_data to this record. */
> + PyObject *raw = PyBytes_FromStringAndSize(
> + (const char *)data->dtpda_data,
> + 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 workstatus;
> + 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;
> + }
> +
> + if (session_dtrace_lock_open(self) < 0) {
> + Py_XDECREF(ctx.probes);
> + return NULL;
> + }
> + if (ctx.capture) {
> + workstatus = dtrace_work(self->dtp, self->fp, consume_probe,
> + consume_rec, &ctx);
> + } else {
> + /* The no-capture callbacks do not access Python objects. */
> + Py_BEGIN_ALLOW_THREADS
> + workstatus = dtrace_work(self->dtp, self->fp, consume_probe,
> + consume_rec, &ctx);
> + Py_END_ALLOW_THREADS
> + }
> +
> + /* An interrupted polling wait does not invalidate the DTrace session. */
> + if (workstatus == DTRACE_WORKSTATUS_ERROR &&
> + dtrace_errno(self->dtp) == EINTR)
> + workstatus = DTRACE_WORKSTATUS_OKAY;
> +
> + if (workstatus == DTRACE_WORKSTATUS_ERROR ||
> + (ctx.capture && ctx.aborted)) {
> + result = raise_dtrace_error(self, "dtrace_work");
> + session_dtrace_unlock(self);
> + Py_XDECREF(ctx.probes);
> + return result;
> + }
> +
> + status = dtrace_status(self->dtp);
> + if (status == DTRACE_STATUS_ERROR) {
> + result = raise_dtrace_error(self, "dtrace_status");
> + session_dtrace_unlock(self);
> + Py_XDECREF(ctx.probes);
> + return result;
> + }
> + if (status == DTRACE_STATUS_OKAY && self->status)
> + status = self->status;
> + if (status == DTRACE_STATUS_OKAY && self->pids > 0 &&
> + self->pids_live == 0) {
> + status = DTRACE_STATUS_EXITED;
> + self->status = status;
> + }
> + session_dtrace_unlock(self);
> +
> + 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))
> +{
> + int rc;
> +
> + if (ensure_open(self) < 0)
> + return NULL;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + Py_BEGIN_ALLOW_THREADS
> + rc = dtrace_aggregate_snap(self->dtp);
> + Py_END_ALLOW_THREADS
> + if (rc != 0) {
> + session_dtrace_unlock(self);
> + return raise_dtrace_error(self, "dtrace_aggregate_snap");
> + }
> + session_dtrace_unlock(self);
> +
> + Py_RETURN_NONE;
> +}
> +
> +static PyObject *
> +PyDTraceSession_agg_clear(PyDTraceSession *self, PyObject *Py_UNUSED(args))
> +{
> + if (ensure_open(self) < 0)
> + return NULL;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + dtrace_aggregate_clear(self->dtp);
> + session_dtrace_unlock(self);
> + 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;
> +
> + if (session_dtrace_lock_open(self) < 0) {
> + Py_DECREF(ctx.list);
> + return NULL;
> + }
> + rc = walker(self->dtp, agg_walk_callback, &ctx);
> + if (rc != 0) {
> + PyObject *error = raise_dtrace_error(self, "dtrace_aggregate_walk");
> +
> + session_dtrace_unlock(self);
> + Py_DECREF(ctx.list);
> + return error;
> + }
> + session_dtrace_unlock(self);
> +
> + 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) {
> + session_dtrace_lock(self->session);
> + if (self->session->dtp != NULL)
> + dtrace_proc_release(self->session->dtp, self->proc);
> + session_dtrace_unlock(self->session);
> + }
> +
> + 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;
> + }
> +
> + if (session_dtrace_lock_open(self->session) < 0)
> + return NULL;
> + if (self->proc == NULL) {
> + session_dtrace_unlock(self->session);
> + PyErr_SetString(PyExc_DTraceError,
> + "DTrace process handle is released");
> + return NULL;
> + }
> + pid = dtrace_proc_getpid(self->session->dtp, self->proc);
> + session_dtrace_unlock(self->session);
> +
> + 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;
> + if (session_dtrace_lock_open(self) < 0)
> + goto error;
> + proc = dtrace_proc_create(self->dtp, argvlist[0], argvlist, 0);
> + if (proc == NULL) {
> + ret = raise_dtrace_error(self, "dtrace_proc_create");
> + session_dtrace_unlock(self);
> + goto error;
> + }
> +
> + ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
> + if (ret == NULL) {
> + dtrace_proc_release(self->dtp, proc);
> + session_dtrace_unlock(self);
> + goto error;
> + }
> + self->pids++;
> + session_dtrace_unlock(self);
> + 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:
> + 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;
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + proc = dtrace_proc_grab_pid(self->dtp, pid, 0);
> + if (!proc) {
> + ret = raise_dtrace_error(self, "dtrace_proc_grab_pid");
> + session_dtrace_unlock(self);
> + return ret;
> + }
> + ret = PyDTraceProcType.tp_alloc(&PyDTraceProcType, 0);
> + if (ret == NULL) {
> + dtrace_proc_release(self->dtp, proc);
> + session_dtrace_unlock(self);
> + return ret;
> + }
> + self->pids++;
> + session_dtrace_unlock(self);
> + 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;
> + }
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + if (proc->proc == NULL) {
> + session_dtrace_unlock(self);
> + PyErr_SetString(PyExc_DTraceError,
> + "DTrace process handle is released");
> + return NULL;
> + }
> + dtrace_proc_continue(self->dtp, proc->proc);
> + self->pids_live++;
> + session_dtrace_unlock(self);
> +
> + 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;
> + }
> +
> + if (session_dtrace_lock_open(self) < 0)
> + return NULL;
> + if (proc->proc == NULL) {
> + session_dtrace_unlock(self);
> + PyErr_SetString(PyExc_DTraceError,
> + "DTrace process handle is already released");
> + return NULL;
> + }
> + dtrace_proc_release(self->dtp, proc->proc);
> + session_dtrace_unlock(self);
> + proc->proc = NULL;
> +
> + Py_RETURN_NONE;
> +}
> +
> +static PyMethodDef PyDTraceSession_methods[] = {
> + {"close", (PyCFunction)PyDTraceSession_close, METH_NOARGS,
> + "Close the session."},
> + {"__enter__", (PyCFunction)PyDTraceSession_enter, METH_NOARGS,
> + "Context manager entry."},
> + {"__exit__", (PyCFunction)PyDTraceSession_exit, METH_VARARGS,
> + "Context manager exit."},
> + {"setopt", (PyCFunction)PyDTraceSession_setopt,
> + METH_VARARGS | METH_KEYWORDS, "Set a DTrace option."},
> + {"compile", (PyCFunction)PyDTraceSession_compile,
> + METH_VARARGS | METH_KEYWORDS, "Compile a program from a string."},
> + {"enable", (PyCFunction)PyDTraceSession_enable,
> + METH_VARARGS | METH_KEYWORDS, "Enable a compiled program."},
> + {"go", (PyCFunction)PyDTraceSession_go, METH_VARARGS | METH_KEYWORDS,
> + "Start tracing."},
> + {"stop", (PyCFunction)PyDTraceSession_stop, METH_NOARGS,
> + "Stop tracing."},
> + {"update", (PyCFunction)PyDTraceSession_update, METH_NOARGS,
> + "Update module cache."},
> + {"status", (PyCFunction)PyDTraceSession_status, METH_NOARGS,
> + "Retrieve tracing status."},
> + {"work", (PyCFunction)PyDTraceSession_work,
> + METH_VARARGS | METH_KEYWORDS, "Do consumer loop work."},
> + {"agg_snap", (PyCFunction)PyDTraceSession_agg_snap, METH_NOARGS,
> + "Snapshot aggregation buffers."},
> + {"agg_clear", (PyCFunction)PyDTraceSession_agg_clear, METH_NOARGS,
> + "Clear aggregation buffers."},
> + {"agg_walk", (PyCFunction)PyDTraceSession_agg_walk,
> + METH_VARARGS | METH_KEYWORDS, "Return aggregation results."},
> + {"proc_create", (PyCFunction)PyDTraceSession_proc_create,
> + METH_VARARGS | METH_KEYWORDS, "Create process with args for tracing."},
> + {"proc_grab_pid", (PyCFunction)PyDTraceSession_proc_grab_pid,
> + METH_VARARGS | METH_KEYWORDS, "Grab pid."},
> + {"proc_continue", (PyCFunction)PyDTraceSession_proc_continue,
> + METH_VARARGS | METH_KEYWORDS, "Continue proc execution."},
> + {"proc_release", (PyCFunction)PyDTraceSession_proc_release,
> + METH_VARARGS | METH_KEYWORDS, "Relase proc."},
> + {NULL, NULL, 0, NULL}};
> +
> +static PyTypeObject PyDTraceSessionType = {
> + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "dtrace.DTraceSession",
> + .tp_basicsize = sizeof(PyDTraceSession),
> + .tp_flags = Py_TPFLAGS_DEFAULT,
> + .tp_new = PyDTraceSession_new,
> + .tp_init = (initproc)PyDTraceSession_init,
> + .tp_dealloc = (destructor)PyDTraceSession_dealloc,
> + .tp_methods = PyDTraceSession_methods,
> +};
> +
> +/* ------------------------------------------------------------------------- */
> +/* Module definition */
> +/* ------------------------------------------------------------------------- */
> +
> +static struct PyModuleDef pydtrace_module = {
> + PyModuleDef_HEAD_INIT,
> + .m_name = "dtrace",
> + .m_doc = "Python bindings for libdtrace.",
> + .m_size = -1,
> +};
> +
> +PyMODINIT_FUNC
> +PyInit_dtrace(void)
> +{
> + PyObject *m;
> +
> + if (PyType_Ready(&PyDTraceSessionType) < 0)
> + return NULL;
> + if (PyType_Ready(&PyDTraceProgramType) < 0)
> + return NULL;
> + if (PyType_Ready(&PyDTraceProcType) < 0)
> + return NULL;
> +
> + m = PyModule_Create(&pydtrace_module);
> + if (m == NULL)
> + return NULL;
> +
> + PyExc_DTraceError = PyErr_NewException("dtrace.DTraceError",
> + PyExc_RuntimeError, NULL);
> + if (PyExc_DTraceError == NULL) {
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + Py_INCREF(&PyDTraceSessionType);
> + if (PyModule_AddObject(m, "DTraceSession",
> + (PyObject *)&PyDTraceSessionType) < 0) {
> + Py_DECREF(&PyDTraceSessionType);
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + Py_INCREF(&PyDTraceProgramType);
> + if (PyModule_AddObject(m, "DTraceProgram",
> + (PyObject *)&PyDTraceProgramType) < 0) {
> + Py_DECREF(&PyDTraceProgramType);
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + Py_INCREF(&PyDTraceProcType);
> + if (PyModule_AddObject(m, "DTraceProc", (PyObject *)&PyDTraceProcType) <
> + 0) {
> + Py_DECREF(&PyDTraceProcType);
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + Py_INCREF(PyExc_DTraceError);
> + if (PyModule_AddObject(m, "DTraceError", PyExc_DTraceError) < 0) {
> + Py_DECREF(PyExc_DTraceError);
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + if (PyModule_AddIntConstant(m, "DTRACE_VERSION", DTRACE_VERSION) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + if (PyModule_AddIntConstant(m, "DTRACE_C_CPP", DTRACE_C_CPP) < 0 ||
> + PyModule_AddIntConstant(m, "DTRACE_C_ZDEFS", DTRACE_C_ZDEFS) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + if (PyModule_AddIntConstant(m, "DTRACE_STATUS_NONE",
> + DTRACE_STATUS_NONE) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_STATUS_OKAY",
> + DTRACE_STATUS_OKAY) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_STATUS_EXITED",
> + DTRACE_STATUS_EXITED) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_STATUS_FILLED",
> + DTRACE_STATUS_FILLED) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_STATUS_STOPPED",
> + DTRACE_STATUS_STOPPED) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_DONE",
> + DTRACE_WORKSTATUS_DONE) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> + if (PyModule_AddIntConstant(m, "DTRACE_WORKSTATUS_OKAY",
> + DTRACE_WORKSTATUS_OKAY) < 0) {
> + Py_DECREF(m);
> + return NULL;
> + }
> +
> + return m;
> +}
> diff --git a/configure b/configure
> index 403b1a09..fce22e9f 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: enabled when prerequisites are present)
> 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 [flat|nested] 18+ messages in thread
* [PATCH v5 06/11] dtrace.spec: add python bindings packaging
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (4 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 05/11] python: Add cpython bindings for libdtrace Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-08-21 16:10 ` [PATCH v5 07/11] runtest.sh: Prepare Python bindings for in-tree testing Alan Maguire
` (4 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 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 | 39 +++++++++++++++++++++++++++++++++++++--
1 file changed, 37 insertions(+), 2 deletions(-)
diff --git a/dtrace.spec b/dtrace.spec
index d72baf9d..bc738c4f 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,12 @@ it always tests the installed DTrace.
%setup -q
%build
-make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2}
+%if %{with python}
+%global python_build_args WITH_PYTHON=y PYTHON=%{__python3}
+%else
+%global python_build_args %{nil}
+%endif
+make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2} %{python_build_args}
# Force off debuginfo splitting. We have no debuginfo in dtrace proper,
# and the testsuite requires debuginfo for proper operation.
@@ -150,9 +173,14 @@ make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2}
%install
mkdir -p $RPM_BUILD_ROOT/usr/sbin
+%if %{with python}
+%global python_install_args install-python PYTHON=%{__python3}
+%else
+%global python_install_args %{nil}
+%endif
make DESTDIR=$RPM_BUILD_ROOT VERSION=%{version} \
HDRPREFIX="$RPM_BUILD_ROOT/usr/include" \
- install install-test
+ install install-test %{python_install_args}
%if "%{?dist}" == ".el7"
sed -i '/^ProtectSystem=/d; /^ProtectControlGroups=/d; /^RuntimeDirectory/d;' $RPM_BUILD_ROOT/usr/lib/systemd/system/dtprobed.service
@@ -230,6 +258,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] 18+ messages in thread* [PATCH v5 07/11] runtest.sh: Prepare Python bindings for in-tree testing
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (5 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 06/11] dtrace.spec: add python bindings packaging Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-08-21 16:10 ` [PATCH v5 08/11] test: add tests for python bindings Alan Maguire
` (3 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Export the in-tree Python binding directory so tests can import the build
output without installing it.
Also export the Python interpreter selected by the build as
DTRACE_PYTHON. runtest.sh preserves that setting when invoked by make,
accepts it from a caller that sources the runner, and otherwise falls
back to PYTHON or python3.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
Makecheck | 3 +++
runtest.sh | 4 ++++
2 files changed, 7 insertions(+)
diff --git a/Makecheck b/Makecheck
index 928fadb5..87db1065 100644
--- a/Makecheck
+++ b/Makecheck
@@ -19,6 +19,9 @@
# DTrace:
# check-qa-smoke: Runs the smoke testsuite.
+DTRACE_PYTHON := $(PYTHON)
+export DTRACE_PYTHON
+
ifndef always-installed
git_version=.git-version
RUNTEST=./runtest.sh
diff --git a/runtest.sh b/runtest.sh
index 9f06a499..ca230ca7 100755
--- a/runtest.sh
+++ b/runtest.sh
@@ -19,6 +19,8 @@ unset CDPATH
unset POSIXLY_CORRECT # Interferes with 'wait'
export LC_COLLATE="C"
export dtrace="/usr/sbin/dtrace"
+DTRACE_PYTHON="${DTRACE_PYTHON:-${PYTHON:-python3}}"
+export DTRACE_PYTHON
arch="$(uname -m)"
@@ -552,6 +554,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 +565,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] 18+ messages in thread* [PATCH v5 08/11] test: add tests for python bindings
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (6 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 07/11] runtest.sh: Prepare Python bindings for in-tree testing Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-08-21 16:10 ` [PATCH v5 09/11] dtrace: Add a PCP PMDA to expose DTrace data as metrics Alan Maguire
` (2 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Add tests covering aggregation snapshot, handling of keys,
values. Also test process creation/grabbing, and ensure
we detect processes going away or exit() calls in dt.work().
Also ensure that multiple sessions can exist in the process
image, and that closing a handle does not effect operation of
another handle in the same process. Test compile-time and
general setopt() options.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
test/unittest/python/test.x | 17 ++
test/unittest/python/tst.aggr-actions.sh | 152 ++++++++++++++++++
test/unittest/python/tst.aggr-change.sh | 74 +++++++++
test/unittest/python/tst.aggr-clear.sh | 44 +++++
test/unittest/python/tst.aggr-keys.sh | 89 ++++++++++
test/unittest/python/tst.aggr-stack.sh | 62 +++++++
test/unittest/python/tst.aggr.sh | 58 +++++++
test/unittest/python/tst.compile-opts.sh | 50 ++++++
test/unittest/python/tst.exit.sh | 67 ++++++++
test/unittest/python/tst.multi-session-end.sh | 89 ++++++++++
test/unittest/python/tst.multi-session.sh | 62 +++++++
test/unittest/python/tst.proc-create.sh | 71 ++++++++
test/unittest/python/tst.proc-exit.sh | 57 +++++++
test/unittest/python/tst.proc-grab.sh | 84 ++++++++++
test/unittest/python/tst.setopt.sh | 30 ++++
test/unittest/python/tst.work-error.sh | 36 +++++
16 files changed, 1042 insertions(+)
create mode 100755 test/unittest/python/test.x
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-clear.sh
create mode 100755 test/unittest/python/tst.aggr-keys.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.compile-opts.sh
create mode 100755 test/unittest/python/tst.exit.sh
create mode 100755 test/unittest/python/tst.multi-session-end.sh
create mode 100755 test/unittest/python/tst.multi-session.sh
create mode 100755 test/unittest/python/tst.proc-create.sh
create mode 100755 test/unittest/python/tst.proc-exit.sh
create mode 100755 test/unittest/python/tst.proc-grab.sh
create mode 100755 test/unittest/python/tst.setopt.sh
create mode 100755 test/unittest/python/tst.work-error.sh
diff --git a/test/unittest/python/test.x b/test/unittest/python/test.x
new file mode 100755
index 00000000..36ea27d6
--- /dev/null
+++ b/test/unittest/python/test.x
@@ -0,0 +1,17 @@
+#!/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.
+#
+# Skip the Python binding tests when the bindings were not built or installed.
+#
+
+if ! "$DTRACE_PYTHON" -c 'import dtrace' >/dev/null 2>&1; then
+ echo "Python bindings are unavailable"
+ exit 2
+fi
+
+exit 0
diff --git a/test/unittest/python/tst.aggr-actions.sh b/test/unittest/python/tst.aggr-actions.sh
new file mode 100755
index 00000000..f09d3b13
--- /dev/null
+++ b/test/unittest/python/tst.aggr-actions.sh
@@ -0,0 +1,152 @@
+#!/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()
+
+expected = {
+ "counts", "sums", "avgs", "mins", "maxs", "stddevs", "quants",
+ "lquants", "llquants",
+}
+seen = set()
+
+if not rows:
+ raise SystemExit("no rows returned")
+
+for row in rows:
+ if row["name"] == "counts":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 3:
+ raise SystemExit(f"unexpected count value {v}")
+ if row["name"] == "avgs":
+ seen.add(row["name"])
+ v = float(row["value"])
+ if v != 4.5:
+ raise SystemExit(f"unexpected avg value {v}")
+ if row["name"] == "sums":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 182:
+ raise SystemExit(f"unexpected sum value {v}")
+ if row["name"] == "mins":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 99:
+ raise SystemExit(f"unexpected min value {v}")
+ if row["name"] == "maxs":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 19:
+ raise SystemExit(f"unexpected max value {v}")
+ if row["name"] == "stddevs":
+ seen.add(row["name"])
+ v = int(row["value"])
+ if v != 287:
+ raise SystemExit(f"unexecpted stddev value {v}")
+ if row["name"] == "quants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {1:2, 2:2, 128:1}:
+ raise SystemExit(f"unexecpted quantize value {v}")
+ if row["name"] == "lquants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {1:3, 3:1, 9:2}:
+ raise SystemExit(f"unexecpted lquantize value {v}")
+ if row["name"] == "llquants":
+ seen.add(row["name"])
+ v = row["value"]
+ if v != {0:2, 9:1, 18:1, 27:1, 162:1}:
+ raise SystemExit(f"unexecpted llquantize value {v}")
+
+missing = expected - seen
+if missing:
+ raise SystemExit(f"missing aggregation(s): {sorted(missing)}")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..7c3f8594
--- /dev/null
+++ b/test/unittest/python/tst.aggr-change.sh
@@ -0,0 +1,74 @@
+#!/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"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.aggr-clear.sh b/test/unittest/python/tst.aggr-clear.sh
new file mode 100755
index 00000000..912bab0c
--- /dev/null
+++ b/test/unittest/python/tst.aggr-clear.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Ensure aggregation buffers can be cleared through the Python binding.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+program = r'''
+BEGIN
+{
+ @counts = count();
+}
+'''
+
+with DTraceSession() as session:
+ compiled = session.compile(program)
+ session.enable(compiled)
+ session.go()
+ session.stop()
+ session.agg_snap()
+ if not session.agg_walk():
+ raise SystemExit("aggregation was not populated")
+ session.agg_clear()
+ session.agg_snap()
+ rows = session.agg_walk()
+ if not rows:
+ raise SystemExit("aggregation descriptor disappeared after agg_clear()")
+ for row in rows:
+ if row["name"] == "counts" and int(row["value"]) != 0:
+ raise SystemExit(f"aggregation was not cleared: {row}")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.aggr-keys.sh b/test/unittest/python/tst.aggr-keys.sh
new file mode 100755
index 00000000..e81b1c2d
--- /dev/null
+++ b/test/unittest/python/tst.aggr-keys.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# Verify Python aggregation-key decoding for zero, string, numeric, and
+# mixed key tuples.
+#
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+trap 'rm -rf "$tmpdir"' EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+program = r'''
+BEGIN
+{
+ @zero = count();
+ @string["alpha"] = count();
+ @numeric[17] = count();
+ @mixed["irq", 17] = count();
+}
+
+fbt::schedule:entry
+{
+ @symkey[sym(caller)] = count();
+ @modkey[mod(caller)] = count();
+ @stackkey[stack()] = count();
+}
+
+pid$target::getopt*:entry
+{
+ @usymkey[usym(ucaller)] = count();
+ @umodkey[umod(ucaller)] = count();
+}
+
+profile:::tick-5s
+{
+ exit(0);
+}
+'''
+
+with DTraceSession() as session:
+ from dtrace import DTRACE_C_ZDEFS
+ target = session.proc_create(["/bin/sleep", "2"])
+ program_obj = session.compile(program, cflags=DTRACE_C_ZDEFS)
+ session.enable(program_obj)
+ session.go()
+ session.proc_continue(target)
+ import time
+ time.sleep(2)
+ session.proc_release(target)
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+def hashable_key(key):
+ return tuple(hashable_key(item) for item in key) if isinstance(key, list) else key
+
+seen = {
+ (row["name"], tuple(hashable_key(key) for key in row["keys"]))
+ for row in rows
+}
+expected = {
+ ("zero", ()),
+ ("string", (("alpha",),)),
+ ("numeric", ((17,),)),
+ ("mixed", (("irq",), (17,))),
+}
+missing = expected - seen
+if missing:
+ raise SystemExit(f"missing aggregation keys: {missing}; rows={rows}")
+print(f"rows={rows}")
+
+for name in ("symkey", "modkey", "usymkey", "umodkey"):
+ matching = [row for row in rows if row["name"] == name]
+ if not matching or not isinstance(matching[0]["keys"][0], list):
+ raise SystemExit(f"symbolic key was not formatted: {name}: {matching}")
+stack_rows = [row for row in rows if row["name"] == "stackkey"]
+if not stack_rows or not isinstance(stack_rows[0]["keys"][0], list):
+ raise SystemExit(f"stack key was not formatted: {stack_rows}")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.aggr-stack.sh b/test/unittest/python/tst.aggr-stack.sh
new file mode 100755
index 00000000..1b398a04
--- /dev/null
+++ b/test/unittest/python/tst.aggr-stack.sh
@@ -0,0 +1,62 @@
+#!/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 not entry["keys"] or not isinstance(entry["keys"][0], list) or \
+ not entry["keys"][0] or len(entry["keys"][0][0]) < 2:
+ raise SystemExit("missing stack list")
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..235de73d
--- /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"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.compile-opts.sh b/test/unittest/python/tst.compile-opts.sh
new file mode 100755
index 00000000..7e0c3e61
--- /dev/null
+++ b/test/unittest/python/tst.compile-opts.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# Ensure compile flags and preprocessor macro arguments are accepted.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTRACE_C_ZDEFS, DTraceSession
+
+program = r'''
+missing-provider:::missing-probe
+{
+ @missing = count();
+}
+
+BEGIN
+{
+ @value = sum(TEST_VALUE);
+}
+'''
+
+with DTraceSession() as session:
+ compiled = session.compile(
+ program,
+ cflags=DTRACE_C_ZDEFS,
+ defines=["TEST_VALUE=42"],
+ )
+ session.enable(compiled)
+ session.go()
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+values = {row["name"]: row["value"] for row in rows}
+if int(values.get("value", -1)) != 42:
+ raise SystemExit(f"macro definition was not applied: {values}")
+if "missing" in values:
+ raise SystemExit("missing probe unexpectedly generated an aggregation")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.exit.sh b/test/unittest/python/tst.exit.sh
new file mode 100755
index 00000000..0d5dfb42
--- /dev/null
+++ b/test/unittest/python/tst.exit.sh
@@ -0,0 +1,67 @@
+#!/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, DTRACE_STATUS_STOPPED
+
+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
+ break
+ iters += 1
+ if not exited:
+ raise SystemExit("missing DTRACE_STATUS_EXIT")
+ work_status, _ = dt.work()
+ if work_status != DTRACE_STATUS_STOPPED:
+ raise SystemExit(f"exit() was not drained by work(): got {work_status}")
+ dt.stop()
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$tmpdir/script.d"
diff --git a/test/unittest/python/tst.multi-session-end.sh b/test/unittest/python/tst.multi-session-end.sh
new file mode 100755
index 00000000..cf3430c2
--- /dev/null
+++ b/test/unittest/python/tst.multi-session-end.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# Ensure END probes remain attached when another DTrace session in the same
+# process closes.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir/script.d" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceSession
+
+
+program = r'''
+END
+{
+ @ended = count();
+}
+'''
+
+
+def start_session():
+ session = DTraceSession()
+ program_handle = session.compile(program)
+ session.enable(program_handle)
+ session.go()
+ return session
+
+
+def assert_end_fired(session, survivor):
+ session.stop()
+ session.agg_snap()
+ rows = session.agg_walk()
+
+ for row in rows:
+ if row["name"] == "ended" and int(row["value"]) == 1:
+ return
+
+ raise SystemExit(f"END probe did not fire for surviving handle {survivor}")
+
+
+def close_session(session):
+ if session is not None:
+ try:
+ session.close()
+ except Exception:
+ pass
+
+
+# Closing A must not remove B's shared END uprobe.
+handle_a = handle_b = None
+try:
+ handle_a = start_session()
+ handle_b = start_session()
+ handle_a.close()
+ handle_a = None
+ assert_end_fired(handle_b, "B")
+finally:
+ close_session(handle_a)
+ close_session(handle_b)
+
+# Closing B must not remove A's shared END uprobe.
+handle_a = handle_b = None
+try:
+ handle_a = start_session()
+ handle_b = start_session()
+ handle_b.close()
+ handle_b = None
+ assert_end_fired(handle_a, "A")
+finally:
+ close_session(handle_a)
+ close_session(handle_b)
+EOF
+
+chmod +x "$tmpdir/script.d"
+"$DTRACE_PYTHON" "$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..3709ca1a
--- /dev/null
+++ b/test/unittest/python/tst.multi-session.sh
@@ -0,0 +1,62 @@
+#!/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();
+}
+"""
+
+sessions = []
+max_sessions = 10
+
+try:
+ for i in range(max_sessions):
+ session = DTraceSession()
+ sessions.append(session)
+ p = session.compile(prog)
+ session.enable(p)
+ session.go()
+ session.work()
+except Exception as exc:
+ raise SystemExit(f"got err with {i} multiple sessions: {exc}")
+finally:
+ for session in sessions:
+ try:
+ session.close()
+ except Exception:
+ pass
+
+EOF
+
+chmod +x "$tmpdir/script.d"
+PATH="$tmpdir:$PATH"
+
+"$DTRACE_PYTHON" "$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..95e9db85
--- /dev/null
+++ b/test/unittest/python/tst.proc-create.sh
@@ -0,0 +1,71 @@
+#!/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
+import time
+
+from dtrace import DTRACE_STATUS_STOPPED, 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)
+ deadline = time.monotonic() + 5
+ try:
+ while time.monotonic() < deadline:
+ status, _ = dt.work()
+ if status == DTRACE_STATUS_STOPPED:
+ break
+ time.sleep(0.05)
+ else:
+ raise SystemExit("created process did not complete")
+ finally:
+ dt.proc_release(proc)
+ dt.agg_snap()
+ rows = dt.agg_walk()
+
+if not rows:
+ raise SystemExit("no aggregation rows")
+
+def flatten(key):
+ if isinstance(key, list):
+ return [item for child in key for item in flatten(child)]
+ return [key]
+
+names = [flatten(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"
+
+"$DTRACE_PYTHON" "$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..2c7d02da
--- /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"
+
+"$DTRACE_PYTHON" "$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..5655303b
--- /dev/null
+++ b/test/unittest/python/tst.proc-grab.sh
@@ -0,0 +1,84 @@
+#!/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
+helper=
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ if [ -n "$helper" ]; then
+ kill "$helper" 2>/dev/null || true
+ wait "$helper" 2>/dev/null || true
+ fi
+ 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
+"$DTRACE_PYTHON" "$tmpdir/script.py"
+ret=$?
+
+exit $ret
diff --git a/test/unittest/python/tst.setopt.sh b/test/unittest/python/tst.setopt.sh
new file mode 100755
index 00000000..1efc8c06
--- /dev/null
+++ b/test/unittest/python/tst.setopt.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# Ensure valid and invalid options are handled through the Python binding.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceError, DTraceSession
+
+with DTraceSession() as session:
+ session.setopt("bufsize", "4m")
+ session.setopt("quiet", None)
+ try:
+ session.setopt("not-a-dtrace-option", "1")
+ except DTraceError:
+ pass
+ else:
+ raise SystemExit("invalid option was accepted")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
diff --git a/test/unittest/python/tst.work-error.sh b/test/unittest/python/tst.work-error.sh
new file mode 100755
index 00000000..abf6e589
--- /dev/null
+++ b/test/unittest/python/tst.work-error.sh
@@ -0,0 +1,36 @@
+#!/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 libdtrace errors from work() are reported as Python exceptions.
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+ rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+cat >"$tmpdir/script.py" <<'EOF'
+#!/usr/bin/env python3
+from dtrace import DTraceError, DTraceSession
+
+
+with DTraceSession() as session:
+ try:
+ session.work()
+ except DTraceError:
+ pass
+ else:
+ raise SystemExit("work() did not report its libdtrace error")
+EOF
+
+chmod +x "$tmpdir/script.py"
+"$DTRACE_PYTHON" "$tmpdir/script.py"
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH v5 09/11] dtrace: Add a PCP PMDA to expose DTrace data as metrics
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (7 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 08/11] test: add tests for python bindings Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-08-21 16:10 ` [PATCH v5 10/11] dtrace.spec: Add PCP PMDA packaging pcp-pmda-dtrace Alan Maguire
2026-08-21 16:10 ` [PATCH v5 11/11] test: Add a test for the PCP ManagedDTraceScript interface Alan Maguire
10 siblings, 0 replies; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
This PMDA exposes a way of running DTrace scripts through PCP via the
libdtrace Python bindings. Scripts can be registered dynamically via
`pmstore(1)` calls and optionally autostarted from an on-disk directory when
the PMDA is launched using the autostart.d/ directory.
To register and run a DTrace script to count read system calls:
$ pmstore dtrace.control.register '{"name":"read_syscalls","program":"syscall::read:entry { @c[probefunc] = count(); }", "autostart":true}'
Now we see the aggregation data exposed as the metric
`dtrace.scripts.data.read_syscalls.c`, with one instance per
aggregation key:
$ pminfo -f dtrace
dtrace.scripts.data.read_syscalls.c
inst [0 or "read"] value 9536
dtrace.scripts.runtime_seconds
inst [0 or "read_syscalls"] value 2
...
To stop, unregister
$ pmstore dtrace.control.stop read_syscalls
$ pmstore dtrace.control.unregister read_syscalls
By default, dynamic registration is restricted to root only;
other users can be added in dtrace.conf.
See pcp/README.md for details.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
GNUmakefile | 1 +
configure | 3 +
pcp/Build | 66 +
pcp/Install | 37 +
pcp/README.md | 343 +++++
pcp/Remove | 26 +
pcp/autostart/.gitkeep | 0
pcp/dtrace.conf | 12 +
pcp/examples/io_times.d | 28 +
pcp/examples/io_times.json | 8 +
pcp/examples/irq_times.d | 51 +
pcp/examples/irq_times.json | 8 +
pcp/examples/packet_drop_reasons.d | 16 +
pcp/examples/packet_drop_reasons.json | 8 +
pcp/examples/profile_kernel.d | 33 +
pcp/examples/profile_kernel.json | 11 +
pcp/examples/syscall_counts.d | 8 +
pcp/examples/syscall_counts.json | 8 +
pcp/pmdadtrace.1 | 135 ++
pcp/pmdadtrace.python | 1664 +++++++++++++++++++++++++
20 files changed, 2466 insertions(+)
create mode 100644 pcp/Build
create mode 100755 pcp/Install
create mode 100644 pcp/README.md
create mode 100755 pcp/Remove
create mode 100644 pcp/autostart/.gitkeep
create mode 100644 pcp/dtrace.conf
create mode 100644 pcp/examples/io_times.d
create mode 100644 pcp/examples/io_times.json
create mode 100644 pcp/examples/irq_times.d
create mode 100644 pcp/examples/irq_times.json
create mode 100644 pcp/examples/packet_drop_reasons.d
create mode 100644 pcp/examples/packet_drop_reasons.json
create mode 100644 pcp/examples/profile_kernel.d
create mode 100644 pcp/examples/profile_kernel.json
create mode 100644 pcp/examples/syscall_counts.d
create mode 100644 pcp/examples/syscall_counts.json
create mode 100644 pcp/pmdadtrace.1
create mode 100755 pcp/pmdadtrace.python
diff --git a/GNUmakefile b/GNUmakefile
index e26a1d84..eb30f2e9 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -109,6 +109,7 @@ WITH_SYSTEMD = y
PYTHON ?= python3
PYTHON_BINDINGS_AVAILABLE := $(shell $(PYTHON) -c 'import os, sysconfig, setuptools; assert os.path.isfile(os.path.join(sysconfig.get_path("include"), "Python.h"))' >/dev/null 2>&1 && echo y)
WITH_PYTHON ?= $(PYTHON_BINDINGS_AVAILABLE)
+WITH_PCP ?= y
TARGETS =
DTRACE ?= $(objdir)/dtrace
diff --git a/configure b/configure
index fce22e9f..30681f59 100755
--- a/configure
+++ b/configure
@@ -113,6 +113,7 @@ EOF
cat >&2 <<'EOF'
--with-systemd=[yes/no] Install the systemd unit files (default: yes)
--with-python=[yes/no] Build the Python bindings (default: enabled when prerequisites are present)
+--with-pcp=[yes/no] Install the PCP PMDA (default: yes)
EOF
echo >&2
make help-overrides
@@ -202,6 +203,8 @@ for option in "$@"; do
--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 "";;
+ --with-pcp|--with-pcp=y*) write_make_var WITH_PCP "y";;
+ --with-pcp=n*|--without-pcp) write_make_var WITH_PCP "";;
HAVE_ELF_GETSHDRSTRNDX=*) write_config_var ELF_GETSHDRSTRNDX "$option";;
--with-libctf=*) write_config_var LIBCTF "$option";;
HAVE_LIBCTF=*) write_config_var LIBCTF "$option";;
diff --git a/pcp/Build b/pcp/Build
new file mode 100644
index 00000000..8ca52542
--- /dev/null
+++ b/pcp/Build
@@ -0,0 +1,66 @@
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+
+PMDA_NAME := dtrace
+PMDA_OBJDIR := $(DESTDIR)/var/lib/pcp/pmdas/$(PMDA_NAME)
+PMDA_AUTOSTART_OBJDIR := $(PMDA_OBJDIR)/autostart
+PMDA_EXAMPLE_OBJDIR := $(PMDA_OBJDIR)/examples
+
+PMDA_EXEC_SCRIPTS := Install Remove pmdadtrace.python
+PMDA_DATA_FILES := README.md dtrace.conf
+PMDA_MANPAGE := pmdadtrace.1
+PMDA_AUTOSTART_SRC := $(wildcard pcp/autostart/*)
+PMDA_AUTOSTART_FILES := $(notdir $(PMDA_AUTOSTART_SRC))
+PMDA_EXAMPLE_SRC := $(wildcard pcp/examples/*)
+PMDA_EXAMPLE_FILES := $(notdir $(PMDA_EXAMPLE_SRC))
+
+# PCP support is enabled by default for source-tree builds.
+ifeq ($(WITH_PYTHON),y)
+ifeq ($(WITH_PCP),y)
+PHONIES += install-pmda-$(PMDA_NAME) install-pmda
+
+install:: install-pmda
+
+install-pmda:: install-pmda-$(PMDA_NAME)
+
+install-pmda-$(PMDA_NAME)::
+ $(call describe-install-target,$(PMDA_OBJDIR),$(PMDA_EXEC_SCRIPTS) $(PMDA_DATA_FILES))
+ $(call describe-install-target,$(INSTMANDIR),$(PMDA_MANPAGE))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR) $(INSTMANDIR)/man1
+ for f in $(PMDA_EXEC_SCRIPTS); do \
+ install -m 755 pcp/$$f $(PMDA_OBJDIR); \
+ done
+ for f in $(PMDA_DATA_FILES); do \
+ install -m 644 pcp/$$f $(PMDA_OBJDIR); \
+ done
+ install -m 644 pcp/$(PMDA_MANPAGE) $(INSTMANDIR)/man1
+ifneq ($(PMDA_AUTOSTART_FILES),)
+ $(call describe-install-target,$(PMDA_AUTOSTART_OBJDIR),$(PMDA_AUTOSTART_FILES))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_AUTOSTART_OBJDIR)
+ for f in $(PMDA_AUTOSTART_FILES); do \
+ mode=644; \
+ case $$f in \
+ *.d) mode=755 ;; \
+ *) ;; \
+ esac; \
+ install -m $$mode pcp/autostart/$$f $(PMDA_AUTOSTART_OBJDIR); \
+ done
+endif
+
+ifneq ($(PMDA_EXAMPLE_FILES),)
+ $(call describe-install-target,$(PMDA_EXAMPLE_OBJDIR),$(PMDA_EXAMPLE_FILES))
+ mkdir -p $(PMDA_OBJDIR) $(PMDA_EXAMPLE_OBJDIR)
+ for f in $(PMDA_EXAMPLE_FILES); do \
+ mode=644; \
+ case $$f in \
+ *.d) mode=755 ;; \
+ *) ;; \
+ esac; \
+ install -m $$mode pcp/examples/$$f $(PMDA_EXAMPLE_OBJDIR); \
+ done
+endif
+
+endif
+endif
diff --git a/pcp/Install b/pcp/Install
new file mode 100755
index 00000000..1103bbd2
--- /dev/null
+++ b/pcp/Install
@@ -0,0 +1,37 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+domain=487
+pmda_interface=2
+# Extend PMNS with the shipped namespace fragment
+pmns_update=true
+pmnsfile=$iam
+python_user=root
+python_group=root
+python_opt=true
+daemon_opt=false
+
+ipc_prot="binary notready"
+
+pmdaSetup
+pmdaInstall -U root
+exit
diff --git a/pcp/README.md b/pcp/README.md
new file mode 100644
index 00000000..a0753144
--- /dev/null
+++ b/pcp/README.md
@@ -0,0 +1,343 @@
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+# dtrace PMDA
+
+This PMDA provides support to collect PCP metrics from DTrace scripts via the
+libdtrace Python bindings. Scripts can be registered dynamically via
+`pmstore(1)` calls and optionally autostarted from an on-disk directory when
+the PMDA is launched.
+
+# Dynamic-script authorization
+
+Dynamic script registration is enabled only for `root` by default. To change
+the policy, edit the root-owned `$PCP_PMDAS_DIR/dtrace/dtrace.conf` file:
+
+```
+[dynamic_scripts]
+enabled = true
+auth_enabled = true
+allowed_users = root,mydtraceuser
+```
+
+When authentication is enabled, every write to `dtrace.control.*` must come
+from an authenticated PCP user named in `allowed_users`. Local `pmstore`
+clients are identified using their Unix UID; remote clients require PCP
+authentication. Leave `auth_enabled` set to `true`: disabling it allows every
+client with `pmcd` store permission to run DTrace programs with the PMDA's
+privileges.
+
+# Metrics
+
+- `dtrace.control.*` metrics are writable strings consumed by the PMDA to
+ register, unregister, start, stop, or reload scripts. Payloads for
+ `dtrace.control.register` must be JSON objects containing at least the
+ `name` and `program` fields and optional `options` (libdtrace `setopt`
+ key/value pairs), `autostart`, `exitonly`, `top`, `bottom`, `pid`, `command`,
+ `args`, and `defines` fields. `pid` attaches
+ to an existing process, while `command` is the JSON equivalent of
+ `dtrace -c`: it starts a whitespace- and quote-split command under DTrace
+ control. The two target fields are mutually exclusive and initialize
+ `$target` before the program is compiled. `args` is an array of string operands
+ supplied to the D program as positional macro arguments `$1`, `$2`, and so on.
+ `defines` may be an object mapping
+ macro names to values (use `null` for an unvalued macro), or an array of
+ `NAME`/`NAME=VALUE` strings; it enables preprocessing and passes definitions
+ to cpp.
+- `dtrace.scripts.*` metrics form an instance domain over the registered
+ scripts and report each script's current state, autostart flag, last error
+ message, and runtime in seconds.
+
+For example, to create a script which counts system calls that automatically
+starts and collects metrics:
+
+```
+$ pmstore dtrace.control.register '{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":true}'
+dtrace.control.register old value="" new value="{"name":"syscalls","program":"syscall:::entry { @c[probefunc] = count(); }", "autostart":true}"
+$ pminfo -f dtrace
+
+dtrace.scripts.data.syscalls.c
+ inst [0 or "bpf"] value 5360
+ inst [1 or "access"] value 278
+ inst [2 or "poll"] value 7949
+ inst [3 or "close"] value 7693
+ inst [4 or "mmap"] value 525
+ inst [5 or "rename"] value 26
+...
+```
+
+Each instance of the metric 'dtrace.scripts.data.syscalls.c' represents
+the aggregation keys/values associated with aggregation '@c'.
+
+## Quantized aggregations
+
+`quantize()`, `lquantize()`, and `llquantize()` aggregations are exported as
+histograms. Each PCP instance is one inclusive bucket range and its value is
+the DTrace count for that range. This layout is suitable for Grafana's
+pre-bucketed heatmap input.
+
+For a quantized aggregation with keys, the key is included in the metric name,
+so buckets for separate keys remain distinct. For example:
+
+```d
+@iowait["device_mapper"] = quantize(args[0]);
+```
+
+produces `dtrace.scripts.data.<script>.iowait.device_mapper`, with instances
+such as `512-1023` and `8388608-16777215`. A quantized aggregation with no
+keys instead uses `dtrace.scripts.data.<script>.<aggregation>` and the same
+range instances.
+
+The range scheme follows the DTrace aggregation action:
+
+- `quantize()` uses power-of-two ranges: `0-0`, `1-1`, `2-3`, `4-7`, and so
+ on. Negative buckets are represented by their inclusive negative ranges.
+- `lquantize()` uses the program's declared base and step; for example a
+ bucket beginning at `20` with a step of `10` is named `20-29`. Its underflow
+ and overflow buckets are named with `-inf` and `inf` endpoints.
+- `llquantize()` uses the factor, magnitude range, and steps declared by the
+ program to generate its log-linear ranges, such as `1000-1999` and
+ `2000-3999`.
+
+Only buckets with non-zero DTrace counts are exported. A bucket can therefore
+disappear from the PCP instance domain when its count is zero on a later
+snapshot.
+
+Script names must map uniquely to PCP metric prefixes. The PMDA rejects a
+registration, or skips an autostart script, when its sanitized name collides
+with an existing script name. Sanitization retains ASCII letters, digits,
+`_`, `+`, `.`, `;`, `:`, and backticks; each run of other characters is replaced
+with `_`, leading and trailing underscores are removed, and an empty result is
+named `value`.
+
+Integer scalar aggregations use 64-bit PCP metric types, preserving exact
+values. `count` and `stddev` use unsigned `PM_TYPE_U64`; `sum`, `min`, and
+`max` use signed `PM_TYPE_64`. Floating aggregations such as `avg` use
+`PM_TYPE_DOUBLE`.
+
+Aggregation names beginning with `__` are reserved for internal DTrace use.
+They are still available to the script, but the PMDA does not publish them as
+PCP metrics. For example, `@__scratch = count();` is intentionally hidden,
+while `@scratch = count();` is exported.
+
+To target a running process, supply its PID and use `$target` in the program:
+
+```
+$ pmstore dtrace.control.register '{"name":"target_reads","pid":1234,"program":"pid$target::read:entry { @reads = count(); }","autostart":true}'
+```
+
+To create a target process, use `command` (the JSON equivalent of `dtrace -c`):
+
+```
+$ pmstore dtrace.control.register '{"name":"sleep","command":"/bin/sleep 30","program":"pid$target::sleep:entry { @calls = count(); }","autostart":true}'
+```
+
+The register payload supports the following keys. `name` and `program` are
+required; the other keys are optional:
+
+```json
+{
+ "name": "example",
+ "program": "syscall:::entry { @calls[PROBEFUNC] = count(); }",
+ "autostart": true,
+ "exitonly": true,
+ "top": 10,
+ "separator": ";",
+ "options": {
+ "bufsize": "4m",
+ "aggrate": "1s",
+ "quiet": true
+ },
+ "compile": {
+ "zdefs": true
+ },
+ "args": ["1234", "read"],
+ "defines": {
+ "SAMPLE_RATE": 97,
+ "BUILD_LABEL": "production",
+ "FEATURE_ENABLED": null
+ }
+}
+```
+
+For example, a program containing `/pid == $1/` can be registered with
+`"args": ["1234"]`. For a string value, use `$$1` in the D program to force
+string-token interpretation. `args` is unrelated to `command`, whose operands
+are passed to the target process created by DTrace.
+
+`exitonly` defaults to `false`. When enabled, the PMDA does not
+snapshot or walk aggregations while the script is running; it publishes the
+final aggregation view only after the DTrace session terminates or is stopped.
+This is useful with, for example, `END { trunc(@stacks, 10); }`, so that only
+the final top ten stacks are exported. It does not limit DTrace aggregation
+memory while the script runs; use the DTrace `aggsize` option for that.
+
+`top` and `bottom` are mutually exclusive optional non-negative limits applied
+independently to each aggregation on every PMDA update. They publish the
+highest- or lowest-valued aggregation entries respectively. Entries outside the
+selected limit are removed from PCP. The limits apply to aggregation entries,
+not individual quantization buckets, and do not limit DTrace aggregation
+memory.
+
+The `compile` object controls compile-time flags. `zdefs` is equivalent to
+the `dtrace -Z` option and allows probe descriptions that match no probes.
+
+`defines` enables the C preprocessor and accepts either an object, as above,
+or an array of `NAME`/`NAME=VALUE` strings:
+
+```json
+{
+ "name": "conditional",
+ "program": "BEGIN { trace(SAMPLE_RATE); }",
+ "defines": ["SAMPLE_RATE=10", "FEATURE_ENABLED"]
+}
+```
+
+For target selection, specify exactly one of `pid` or `command`:
+
+```json
+{
+ "name": "existing-process",
+ "pid": 1234,
+ "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+```json
+{
+ "name": "new-process",
+ "command": "/usr/bin/sleep 30",
+ "program": "pid$target:::entry { @calls = count(); }"
+}
+```
+
+To stop and unregister the script
+
+```
+$ pmstore dtrace.control.unregister syscalls
+```
+
+or to simply stop (while retaining metrics):
+
+```
+$ pmstore dtrace.control.stop syscalls
+```
+
+Stopping is asynchronous: the store request acknowledges once shutdown has
+been requested. Check `dtrace.scripts.state` until it reports `stopped` if
+you need to wait for DTrace cleanup to finish.
+
+# Profiling
+
+It is possible to profile using stack keys, and the instance names
+that represent the call stacks can be made to be compatible with
+the PCP Flame Graph panel's expected comma-delimited stack format of
+`function1,function2`. To do this,
+the default "." key separator that is used to concatenate key values
+must be overridden with a `,`, i.e.
+
+```
+"separator":";"
+```
+
+Stack frames are normalized to `module:function` form and their DTrace
+`+0x...` offsets are removed before the instance name is generated.
+
+For example:
+
+```
+$ pmstore dtrace.control.register '{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":true, "separator":","}'
+dtrace.control.register old value="" new value="{"name":"profile","program":"profile:::profile-97 { @profile[stack()] = count(); }", "autostart":true}"
+
+$ pminfo -f dtrace
+
+dtrace.scripts.data.profile.profile
+ inst [0 or "vmlinux:entry_SYSCALL_64_after_hwframe,vmlinux:do_syscall_64,vmlinux:x64_sys_call,vmlinux:__x64_sys_read,vmlinux:ksys_read,vmlinux:vfs_read,vmlinux:seq_read,vmlinux:seq_read_iter,vmlinux:show_smap,vmlinux:__show_smap,vmlinux:seq_put_decimal_ull_width,vmlinux:strlen"] value 1
+ inst [1 or "vmlinux:entry_SYSCALL_64_after_hwframe,vmlinux:__audit_syscall_exit"] value 1
+...
+```
+
+# Autostart
+
+Scripts placed under `$PCP_PMDAS_DIR/dtrace/autostart/` with a `.d`
+extension are started automatically when the PMDA becomes ready. Optional
+metadata can be provided by placing a matching `.json` file alongside the `.d`
+file; its contents should mirror the register payload structure (for example,
+to define libdtrace options).
+
+Examples are provided:
+
+- `examples/irq_times.*` - measure min, max, and average IRQ/softirq
+ handler execution time
+- `examples/syscall_counts.*` - count syscalls by name
+- `examples/packet_drop_reasons.*` - count packets dropped by drop reason
+ string
+
+To run, copy .d and .json files under autostart prior to install.
+Metrics then appear under dtrace.scripts.data.syscall_counts.counts`,
+ with one instance per aggregation key (in this case syscall name):
+
+```
+# pminfo -f dtrace.scripts.data.syscall_counts.counts
+
+dtrace.scripts.data.syscall_counts.counts
+ inst [0 or "mmap"] value 7959
+ inst [1 or "futex"] value 574830
+ inst [2 or "exit"] value 202
+ inst [3 or "dup2"] value 168
+ inst [4 or "times"] value 575
+...
+```
+
+# Installation
+
+First ensure that dtrace and its associated python bindings are installed
+and running.
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+```
+
+Check there is no clash in the Performance Metrics domain defined in
+as `domain=` in `Install`. If there is a clash, edit the file.
+
+Then run
+
+```
+ # sudo ./Install
+```
+
+Verify PMDA Is running
+
+```
+ # pminfo -f dtrace
+```
+
+# De-installation
+
+```
+# cd $PCP_PMDAS_DIR/dtrace
+# sudo ./Remove
+```
+
+# Troubleshooting
+
+ + Ensure the DTrace Python bindings are installed (`python3 -c 'import dtrace'`).
+ + Confirm the PMDA log (`$PCP_LOG_DIR/pmcd/dtrace.log`) for script errors.
+ + When debugging autostart scripts, temporarily move files out of
+ `autostart/` to disable them.
diff --git a/pcp/Remove b/pcp/Remove
new file mode 100755
index 00000000..ecbf902a
--- /dev/null
+++ b/pcp/Remove
@@ -0,0 +1,26 @@
+#! /bin/sh
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#
+# Copyright (c) 2026, Oracle and/or its affiliates.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public
+# License v2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+
+. $PCP_DIR/etc/pcp.env
+. $PCP_SHARE_DIR/lib/pmdaproc.sh
+
+iam=dtrace
+
+pmdaSetup
+pmdaRemove
+exit
diff --git a/pcp/autostart/.gitkeep b/pcp/autostart/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/pcp/dtrace.conf b/pcp/dtrace.conf
new file mode 100644
index 00000000..6863cc51
--- /dev/null
+++ b/pcp/dtrace.conf
@@ -0,0 +1,12 @@
+# DTrace PMDA configuration
+#
+# Dynamic scripts execute with the PMDA's privileges. The default policy
+# permits only the authenticated root user to control them.
+[dynamic_scripts]
+enabled = true
+
+# When enabled, only authenticated users named in allowed_users may write to
+# dtrace.control.*. Disabling this is unsafe: any client permitted by pmcd to
+# store a metric can execute a DTrace program as the PMDA user.
+auth_enabled = true
+allowed_users = root
diff --git a/pcp/examples/io_times.d b/pcp/examples/io_times.d
new file mode 100644
index 00000000..9a6e7dd7
--- /dev/null
+++ b/pcp/examples/io_times.d
@@ -0,0 +1,28 @@
+#!/usr/sbin/dtrace -Cqs
+
+/* Provide histogram of io times, wait completion times by device. */
+
+io:::start
+{
+ iostart[curthread] = timestamp;
+}
+
+io:::wait-start
+{
+ iowstart[curthread] = timestamp;
+}
+
+io:::done
+/ iostart[curthread] /
+{
+ @iotime[args[1]->dev_name] = quantize((timestamp - iostart[curthread])/1000);
+ iostart[curthread] = 0;
+}
+
+
+io:::wait-done
+/ iowstart[curthread] /
+{
+ @iowait[args[1]->dev_name] = quantize((timestamp - iowstart[curthread])/1000);
+ iowstart[curthread] = 0;
+}
diff --git a/pcp/examples/io_times.json b/pcp/examples/io_times.json
new file mode 100644
index 00000000..da65b5cc
--- /dev/null
+++ b/pcp/examples/io_times.json
@@ -0,0 +1,8 @@
+{
+ "name": "io_times",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/examples/irq_times.d b/pcp/examples/irq_times.d
new file mode 100644
index 00000000..63d74a56
--- /dev/null
+++ b/pcp/examples/irq_times.d
@@ -0,0 +1,51 @@
+#!/usr/sbin/dtrace -s
+
+/* Count avg/max/min irq/softirq times */
+
+BEGIN
+{
+ softirq_names[0] = "hi";
+ softirq_names[1] = "timer";
+ softirq_names[2] = "net_tx";
+ softirq_names[3] = "net_rx";
+ softirq_names[4] = "block";
+ softirq_names[5] = "irq_poll";
+ softirq_names[6] = "tasklet";
+ softirq_names[7] = "sched";
+ softirq_names[8] = "hrtimer";
+ softirq_names[9] = "rcu";
+ softirq_max = 9;
+}
+
+sdt:::irq_handler_entry,
+sdt:::softirq_entry
+{
+ self->start = timestamp;
+}
+
+rawtp:irq::irq_handler_entry
+{
+ self->start = timestamp;
+ self->name = ((struct irqaction *)arg1)->name;
+}
+
+sdt:::irq_handler_exit
+/self->start/
+{
+ this->t = timestamp;
+ @irq_avg_time_ns[stringof(self->name)] = avg(this->t - self->start);
+ @irq_max_time_ns[stringof(self->name)] = max(this->t - self->start);
+ @irq_min_time_ns[stringof(self->name)] = min(this->t - self->start);
+ self->start = 0;
+ self->name = 0;
+}
+
+sdt:::softirq_exit
+/self->start && arg0 <= softirq_max /
+{
+ this->t = timestamp;
+ @softirq_avg_time_ns[softirq_names[arg0]] = avg(this->t - self->start);
+ @softirq_max_time_ns[softirq_names[arg0]] = max(this->t - self->start);
+ @softirq_min_time_ns[softirq_names[arg0]] = min(this->t - self->start);
+ self->start = 0;
+}
diff --git a/pcp/examples/irq_times.json b/pcp/examples/irq_times.json
new file mode 100644
index 00000000..3f8d4de8
--- /dev/null
+++ b/pcp/examples/irq_times.json
@@ -0,0 +1,8 @@
+{
+ "name": "irq_times",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/examples/packet_drop_reasons.d b/pcp/examples/packet_drop_reasons.d
new file mode 100644
index 00000000..8ae0a3b4
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.d
@@ -0,0 +1,16 @@
+#!/usr/sbin/dtrace -s
+
+/* Count packet drops by reason (SOCKET_CLOSE) etc */
+
+BEGIN
+{
+ drop_reasons = (char **)`drop_reasons_core;
+ num_core_reasons = *(size_t *)(`drop_reasons_core+sizeof(char *));
+}
+
+sdt:::kfree_skb
+/arg4 < num_core_reasons /
+{
+ reason = stringof(drop_reasons[arg4]);
+ @drops[reason] = count();
+}
diff --git a/pcp/examples/packet_drop_reasons.json b/pcp/examples/packet_drop_reasons.json
new file mode 100644
index 00000000..3b26f0f4
--- /dev/null
+++ b/pcp/examples/packet_drop_reasons.json
@@ -0,0 +1,8 @@
+{
+ "name": "packet_drop_reasons",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/examples/profile_kernel.d b/pcp/examples/profile_kernel.d
new file mode 100644
index 00000000..b106a14d
--- /dev/null
+++ b/pcp/examples/profile_kernel.d
@@ -0,0 +1,33 @@
+#!/usr/sbin/dtrace -Cqs
+
+/* Profile kernel stacks for RUNTIME_MAX sec (default 1 min) */
+
+#ifndef RUNTIME_MAX
+#define RUNTIME_MAX 60
+#endif
+
+#ifndef STACKS_MAX
+#define STACKS_MAX 512
+#endif
+
+BEGIN
+{
+ runtime = 0;
+}
+
+profile:::profile-97
+{
+ @kstacks[stack()] = count();
+}
+
+profile:::tick-10s
+{
+ runtime += 10;
+}
+
+profile:::tick-10s
+/ runtime > RUNTIME_MAX /
+{
+ trunc(@kstacks, STACKS_MAX);
+ exit(0);
+}
diff --git a/pcp/examples/profile_kernel.json b/pcp/examples/profile_kernel.json
new file mode 100644
index 00000000..c82d90fc
--- /dev/null
+++ b/pcp/examples/profile_kernel.json
@@ -0,0 +1,11 @@
+{
+ "name": "profile_kernel",
+ "defines": { "RUNTIME_MAX": 60, "STACKS_MAX": 512 },
+ "autostart": true,
+ "separator": ",",
+ "exitonly": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/examples/syscall_counts.d b/pcp/examples/syscall_counts.d
new file mode 100644
index 00000000..fd288917
--- /dev/null
+++ b/pcp/examples/syscall_counts.d
@@ -0,0 +1,8 @@
+#!/usr/sbin/dtrace -s
+
+/* Count system call invocations per syscall name */
+
+syscall:::entry
+{
+ @counts[probefunc] = count();
+}
diff --git a/pcp/examples/syscall_counts.json b/pcp/examples/syscall_counts.json
new file mode 100644
index 00000000..d4248fbc
--- /dev/null
+++ b/pcp/examples/syscall_counts.json
@@ -0,0 +1,8 @@
+{
+ "name": "syscall_counts",
+ "autostart": true,
+ "options": {
+ "bufsize": "16m",
+ "aggsize": "8m"
+ }
+}
diff --git a/pcp/pmdadtrace.1 b/pcp/pmdadtrace.1
new file mode 100644
index 00000000..be6f6246
--- /dev/null
+++ b/pcp/pmdadtrace.1
@@ -0,0 +1,135 @@
+.TH PMDADTRACE 1 "August 2026" "Performance Co-Pilot" "PMDA"
+.SH NAME
+\f3pmdadtrace\f1 \- DTrace performance metrics domain agent
+.SH SYNOPSIS
+\f3$PCP_PMDAS_DIR/dtrace/Install\f1
+.br
+\f3pmstore dtrace.control.register\f1 \f2json\f1
+.SH DESCRIPTION
+\f3pmdadtrace\f1 is a Performance Co-Pilot (PCP) PMDA that runs DTrace
+programs and exports their aggregations as PCP metrics. It is started by
+\f3pmcd\f1(1) after installation; it is not normally invoked directly.
+.PP
+Scripts may be loaded from the autostart directory when the PMDA starts, or
+registered dynamically by writing a JSON object to
+\f3dtrace.control.register\f1 with \f3pmstore\f1(1). The PMDA uses the
+libdtrace Python bindings and therefore requires DTrace and those bindings to
+be installed.
+.SH INSTALLATION
+Install the PMDA as root:
+.PP
+.EX
+# cd $PCP_PMDAS_DIR/dtrace
+# ./Install
+.EE
+.PP
+The installation registers the PMDA with \f3pmcd\f1. Check that it is
+available with:
+.PP
+.EX
+$ pminfo dtrace
+.EE
+.SH DYNAMIC SCRIPTS
+The value written to \f3dtrace.control.register\f1 must be a JSON object with
+the required string members \f3name\f1 and \f3program\f1. For example:
+.PP
+.EX
+$ pmstore dtrace.control.register '{
+ "name":"by_pid",
+ "program":"syscall:::entry /pid == $1/ { @calls = count(); }",
+ "args":["1234"], "autostart":true}'
+.EE
+.PP
+The optional members are:
+.TP
+\f3args\f1
+An array of strings supplied as positional D-script macro arguments. The
+first element is \f3$1\f1, the second is \f3$2\f1, and so on. The PMDA
+supplies the script name as \f3$0\f1. Use \f3$$1\f1 when the value must be
+interpreted as a D string token.
+.TP
+\f3autostart\f1
+A boolean. If true, start the script immediately.
+.TP
+\f3options\f1
+An object of libdtrace \f3setopt\f1 options and values, for example
+\f3{"aggsize":"4m", "quiet":true}\f1.
+.TP
+\f3compile\f1
+An object of compile-time options. \f3zdefs:true\f1 is equivalent to the
+DTrace \f3-Z\f1 option.
+.TP
+\f3defines\f1
+Either an object mapping C preprocessor macro names to values, or an array of
+\f3NAME\f1 and \f3NAME=VALUE\f1 strings. This enables C preprocessing.
+.TP
+\f3pid\f1
+A positive process ID. The PMDA attaches to that process before compiling the
+program, so the program can use \f3$target\f1.
+.TP
+\f3command\f1
+A shell-quoted command string. The PMDA creates this command under DTrace
+control, analogous to \f3dtrace -c\f1, and makes its process ID available as
+\f3$target\f1. \f3command\f1 and \f3pid\f1 are mutually exclusive.
+.TP
+\f3separator\f1
+The string used to join aggregation keys in PCP instance names. The default
+is \f3.\f1; \f3,\f1 is required for stack data consumed by the PCP Flame
+Graph panel.
+.TP
+\f3exitonly\f1
+A boolean. If true, aggregation data is published only after the DTrace
+session stops.
+.TP
+\f3top\f1, \f3bottom\f1
+Mutually exclusive non-negative limits selecting the largest or smallest
+aggregation entries to export.
+.PP
+\f3args\f1 applies to the D program at compile time. It is unrelated to
+\f3command\f1, whose arguments are for the target process.
+.PP
+Registered scripts can be stopped, restarted, or removed with:
+.PP
+.EX
+$ pmstore dtrace.control.stop by_pid
+$ pmstore dtrace.control.start by_pid
+$ pmstore dtrace.control.unregister by_pid
+.EE
+.SH AUTOSTART SCRIPTS
+Place a D program ending in \f3.d\f1 in
+\f3$PCP_PMDAS_DIR/dtrace/autostart/\f1. A matching \f3.json\f1 file can
+contain the optional members described above; \f3name\f1 defaults to the
+basename of the D program. Reload the directory with:
+.PP
+.EX
+$ pmstore dtrace.control.reload 1
+.EE
+.SH METRICS
+\f3dtrace.scripts\f1 contains an instance domain for registered scripts,
+including their state, autostart flag, last error, and running time.
+\f3dtrace.scripts.data.\f1\f2script\f1\f3.\f1\f2aggregation\f1 metrics
+contain exported DTrace aggregation values. Each aggregation key becomes a
+PCP instance. Script names are sanitized for use in PCP metric names, and
+names that sanitize to the same value cannot both be registered.
+.SH SECURITY
+Dynamic registration runs DTrace programs with the PMDA's privileges. By
+default it is permitted only for root. Configure the root-owned
+\f3$PCP_PMDAS_DIR/dtrace/dtrace.conf\f1 file to enable authenticated access
+for specific PCP users. Do not disable authentication unless every client
+with permission to store PMDA metrics is trusted.
+.SH FILES
+.TP
+\f3$PCP_PMDAS_DIR/dtrace/pmdadtrace.python\f1
+The PMDA implementation.
+.TP
+\f3$PCP_PMDAS_DIR/dtrace/dtrace.conf\f1
+Dynamic-script authorization policy.
+.TP
+\f3$PCP_PMDAS_DIR/dtrace/autostart/\f1
+Autostart D programs and their optional JSON metadata.
+.TP
+\f3$PCP_LOG_DIR/pmcd/dtrace.log\f1
+PMDA diagnostic log.
+.SH SEE ALSO
+\f3dtrace\f1(8), \f3pmcd\f1(1), \f3pminfo\f1(1), \f3pmstore\f1(1), and
+\f3PCPIntro\f1(1).
diff --git a/pcp/pmdadtrace.python b/pcp/pmdadtrace.python
new file mode 100755
index 00000000..7f33d118
--- /dev/null
+++ b/pcp/pmdadtrace.python
@@ -0,0 +1,1664 @@
+#!/usr/bin/env pmpython
+# SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+
+# Copyright (c) 2026, Oracle and/or its affiliates.
+
+"""DTrace Performance Metrics Domain Agent.
+
+This PMDA allows DTrace scripts to be registered dynamically through pmstore
+and supports autostarting scripts from a configuration directory. Scripts are
+executed using the libdtrace Python bindings.
+"""
+
+import atexit
+import configparser
+import json
+import os
+import pwd
+import re
+import shlex
+import sys
+import threading
+import time
+from collections import defaultdict
+from numbers import Number
+from pathlib import Path
+from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple, Union
+
+try:
+ from dtrace import ( # type: ignore
+ DTRACE_C_ZDEFS,
+ DTraceError,
+ DTraceSession,
+ DTraceProgram,
+ DTRACE_STATUS_EXITED,
+ DTRACE_STATUS_FILLED,
+ DTRACE_STATUS_STOPPED,
+ )
+except ImportError: # pragma: no cover - fallback for in-tree testing
+ _HERE = Path(__file__).resolve()
+ for candidate in (_HERE.parents[2] / "bindings" / "python" / "src",):
+ if candidate.is_dir() and str(candidate) not in os.sys.path:
+ os.sys.path.insert(0, str(candidate))
+ from dtrace import ( # type: ignore # noqa: E402
+ DTRACE_C_ZDEFS,
+ DTraceError,
+ DTraceSession,
+ DTraceProgram,
+ DTRACE_STATUS_EXITED,
+ DTRACE_STATUS_FILLED,
+ DTRACE_STATUS_STOPPED,
+ )
+
+import cpmapi as c_api
+import cpmda
+
+_DTRACE_GLOBAL_LOCK = threading.RLock()
+from pcp.pmapi import pmContext as PCP
+from pcp.pmapi import pmUnits
+from pcp.pmda import PMDA, pmdaGetContext, pmdaIndom, pmdaInstid, pmdaMetric
+
+_SANITIZE_COMPONENT = re.compile(r"[^A-Za-z0-9_\+\.,;:`]+")
+_STACK_FRAME_OFFSET = re.compile(r"\+0x[0-9A-Fa-f]+$")
+_IGNORED_AGGREGATION_PREFIX = "__"
+_QUANTIZED_ACTIONS = {"quantize", "lquantize", "llquantize"}
+_UNSIGNED_INTEGER_ACTIONS = {"count", "stddev"}
+_SIGNED_INTEGER_ACTIONS = {"sum", "min", "max"}
+_INTEGER_ACTIONS = _UNSIGNED_INTEGER_ACTIONS | _SIGNED_INTEGER_ACTIONS
+# PCP pmIDs reserve 12 bits for their cluster component.
+_PCP_PMID_CLUSTER_BITS = 12
+_PCP_PMID_MAX_CLUSTER = (1 << _PCP_PMID_CLUSTER_BITS) - 1
+
+def _sanitize_component(value: Any) -> str:
+ if isinstance(value, bytes):
+ value = value.decode("utf-8", errors="ignore")
+ text = str(value) if value is not None else ""
+ safe = _SANITIZE_COMPONENT.sub("_", text).strip("_")
+ return safe or "value"
+
+
+def _flamegraph_frame(value: Any) -> str:
+ """Format a DTrace stack frame for folded-stack consumers."""
+ frame = _STACK_FRAME_OFFSET.sub("", str(value))
+ return frame.replace("`", ":")
+
+class AggregationEntry(NamedTuple):
+ metric: str
+ instance: str
+ script: str
+ source: str
+ value: Optional[Union[int, float]]
+ samples: Optional[int]
+ normal: Optional[int]
+ action: str
+ keys: Tuple[str, ...]
+
+
+class DataInstance:
+
+ def __init__(
+ self,
+ metric: 'DataMetric',
+ instance: str,
+ id:int,
+ value: Any
+ ) -> None:
+ self._metric = metric
+ self._instance = instance
+ self._instance_id = id
+ self._value = value
+ self._instid = pmdaInstid(self._instance_id, instance)
+ metric._data_indom_insts.append(self._instid)
+
+class DataMetric:
+
+ def __init__(
+ self,
+ metric: pmdaMetric,
+ pmid: str,
+ name: str,
+ cluster: int,
+ script: str,
+ variant: str,
+ indom: int,
+ indom_id: int,
+ metric_id: int,
+ instance_next: int,
+ ) -> None:
+ self.metric = metric
+ self.pmid = pmid
+ self.name = name
+ self.cluster = cluster
+ self.script = script
+ self.variant = variant
+ self.indom = indom
+ self.indom_id = indom_id
+ self.metric_id = metric_id
+ self._instance_next = instance_next
+ self._data_instances = {}
+ self._data_instance_ids = {}
+ self._data_indom_insts = []
+
+class ManagedDTraceScript:
+ """Wraps a DTrace script lifecycle using libdtrace."""
+
+ _START_TIMEOUT = 10.0
+ _POLL_INTERVAL = 1.0
+
+ def __init__(
+ self,
+ name: str,
+ program: str,
+ autostart: bool,
+ options: Optional[Dict[str, Any]],
+ separator: Optional[str],
+ pid: Optional[int],
+ command: Optional[List[str]],
+ defines: Optional[List[str]],
+ args: Optional[List[str]],
+ compile_flags: int,
+ logger,
+ exitonly: bool = False,
+ top: Optional[int] = None,
+ bottom: Optional[int] = None,
+ ) -> None:
+ if top is not None and bottom is not None:
+ raise ValueError("top and bottom are mutually exclusive")
+ self.name = name
+ self.program = program
+ self.autostart = autostart
+ self.autostart_source: Optional[str] = None
+ self.options = dict(options or {})
+ self.options.setdefault("quiet", True)
+ self.separator = separator or "."
+ self.pid = pid
+ self.command = list(command) if command else None
+ self.defines = list(defines or [])
+ self.args = list(args or [])
+ self.compile_flags = compile_flags
+ self._log = logger
+ self.exitonly = exitonly
+ self.top = top
+ self.bottom = bottom
+ self._lock = threading.RLock()
+ self._thread: Optional[threading.Thread] = None
+ self._stop = threading.Event()
+ self._session: Optional[DTraceSession] = None
+ self._state = "stopped"
+ self._last_error = ""
+ self._started_at: Optional[float] = None
+ self._data_callbacks = {
+ }
+ self._aggregation_cache: Dict[str, AggregationEntry] = {}
+
+ # ------------------------------------------------------------------
+ # Public accessors
+ # ------------------------------------------------------------------
+ def metric_prefix(self) -> str:
+ return "dtrace.scripts.data." + _sanitize_component(self.name) + "."
+
+ def state(self) -> str:
+ with self._lock:
+ return self._state
+
+ def last_error(self) -> str:
+ with self._lock:
+ return self._last_error
+
+ def is_running(self) -> bool:
+ return self.state() == "running"
+
+ def runtime_seconds(self) -> int:
+ with self._lock:
+ if self._state != "running" or self._started_at is None:
+ return 0
+ return int(time.time() - self._started_at)
+
+ # ------------------------------------------------------------------
+ # Lifecycle helpers
+ # ------------------------------------------------------------------
+ def start(self) -> Tuple[bool, str]:
+ self._log(f"script {self.name} starting")
+ with self._lock:
+ if self._thread and self._thread.is_alive():
+ return True, ""
+ self._stop.clear()
+ self._state = "starting"
+ self._thread = threading.Thread(
+ target=self._run, name=f"dtrace:{self.name}", daemon=True
+ )
+ self._thread.start()
+
+ deadline = time.time() + self._START_TIMEOUT
+ while time.time() < deadline:
+ state = self.state()
+ if state == "running":
+ return True, ""
+ if state == "error":
+ return False, self.last_error() or "failed to start"
+ time.sleep(0.1)
+
+ if self.state() == "running":
+ return True, ""
+ return False, self.last_error() or "timeout waiting for DTrace startup"
+
+ def stop(self, wait: bool = False) -> None:
+ """Request that the script stop, optionally waiting for its worker.
+
+ Control stores run on the PMDA IPC thread. They must not wait for
+ DTrace teardown, which can take longer than pmcd's pipe timeout.
+ """
+ self._log(f"script {self.name} stopping")
+ with self._lock:
+ thread = self._thread
+ if not thread:
+ self._state = "stopped"
+ return
+ if self._state != "error":
+ self._state = "stopping"
+ self._stop.set()
+ if wait and thread is not threading.current_thread():
+ thread.join(timeout=self._START_TIMEOUT)
+ with self._lock:
+ if not thread.is_alive() and self._thread is thread:
+ self._thread = None
+
+ def update_definition(
+ self,
+ program: str,
+ options: Optional[Dict[str, Any]],
+ pid: Optional[int],
+ command: Optional[List[str]],
+ defines: Optional[List[str]],
+ args: Optional[List[str]],
+ compile_flags: int,
+ exitonly: bool,
+ top: Optional[int],
+ bottom: Optional[int],
+ ) -> None:
+ if top is not None and bottom is not None:
+ raise ValueError("top and bottom are mutually exclusive")
+ with self._lock:
+ restart = self._thread is not None and self._thread.is_alive()
+ if restart:
+ self.stop(wait=True)
+ with self._lock:
+ self.program = program
+ if options is not None:
+ self.options = options
+ self.pid = pid
+ self.command = list(command) if command else None
+ self.defines = list(defines or [])
+ self.args = list(args or [])
+ self.compile_flags = compile_flags
+ self.exitonly = exitonly
+ self.top = top
+ self.bottom = bottom
+ if restart:
+ with self._lock:
+ if self._thread is not None and self._thread.is_alive():
+ self._log(f"unable to restart script {self.name}: "
+ "previous worker did not stop")
+ return
+ ok, error = self.start()
+ if not ok:
+ self._log(f"failed to restart script {self.name}: {error}")
+
+ # ------------------------------------------------------------------
+ # Internal execution loop
+ # ------------------------------------------------------------------
+ def _run(self) -> None:
+ self._log(f"script {self.name} running")
+ target = None
+ try:
+ session = DTraceSession()
+ except Exception as exc: # pylint: disable=broad-except
+ self._fail("session creation", str(exc))
+ return
+
+ with self._lock:
+ self._session = session
+ self._last_error = ""
+
+ try:
+ # Compilation uses global variables; lock to prevent interference
+ with _DTRACE_GLOBAL_LOCK:
+ self._apply_options(session)
+ if self.pid is not None:
+ target = session.proc_grab_pid(self.pid)
+ elif self.command is not None:
+ target = session.proc_create(self.command)
+ compiled = session.compile(
+ self.program,
+ cflags=self.compile_flags,
+ argv=[self.name, *self.args],
+ defines=self.defines,
+ )
+ session.enable(compiled)
+ session.go()
+ if target is not None:
+ session.proc_continue(target)
+ with self._lock:
+ self._state = "running"
+ self._started_at = time.time()
+
+ while not self._stop.is_set():
+ set = self._stop.wait(timeout=self._POLL_INTERVAL)
+ if set:
+ break
+ try:
+ # The bindings release the GIL for this non-capturing
+ # work call and, when enabled, the aggregation snapshot.
+ status, _ = session.work()
+ if not self.exitonly:
+ session.agg_snap()
+ self._walk_aggregations(session)
+ if status in (
+ DTRACE_STATUS_EXITED,
+ DTRACE_STATUS_FILLED,
+ DTRACE_STATUS_STOPPED,
+ ):
+ break
+ except DTraceError as exc: # pragma: no cover - runtime error path
+ self._fail("session execution", str(exc))
+ break
+
+ except Exception as exc: # pylint: disable=broad-except
+ self._fail("session initiation", str(exc))
+ finally:
+ try:
+ session.stop()
+ # Ensure we get final view of data (truncated aggregations etc)
+ session.agg_snap()
+ self._walk_aggregations(session)
+ self._log(f"session stopped for {self.name}")
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ if target is not None:
+ try:
+ session.proc_release(target)
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ try:
+ session.close()
+ self._log(f"session closed for {self.name}")
+ except Exception: # pragma: no cover - best effort cleanup
+ pass
+ with self._lock:
+ if self._state in ("starting", "running", "stopping"):
+ self._state = "stopped"
+ self._session = None
+
+ def _apply_options(self, session: DTraceSession) -> None:
+ self._log(f"applying options to {self.name}")
+ if not self.options:
+ return
+ for key, value in self._normalize_options(self.options):
+ try:
+ session_setopt = session.setopt
+ except AttributeError: # pragma: no cover - safety net
+ continue
+ try:
+ session_setopt(key, value)
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"unable to set option '{key}' for script {self.name}: {exc}"
+ )
+
+ @staticmethod
+ def _normalize_options(options: Dict[str, Any]) -> Iterable[Tuple[str, Optional[str]]]:
+ for key, value in options.items():
+ if isinstance(value, bool):
+ yield str(key), None if value else "0"
+ elif value is None:
+ yield str(key), None
+ else:
+ yield str(key), str(value)
+
+ def _fail(self, ctx: str, message: str) -> None:
+ with self._lock:
+ self._state = "error"
+ self._last_error = message
+ self._log(f"script {self.name} encountered an error during {ctx}: {message}")
+ with self._lock:
+ self._aggregation_cache.clear()
+
+
+ def register_data_callbacks(
+ self,
+ pmda
+ ) -> None:
+ with self._lock:
+ for key in pmda._data_callbacks.keys():
+ self._data_callbacks[key] = pmda._data_callbacks[key]
+
+ def unregister_data_callbacks(
+ self,
+ pmda
+ ):
+ remove_metrics = None
+ with self._lock:
+ if "remove_metrics" in self._data_callbacks:
+ remove_metrics = self._data_callbacks["remove_metrics"]
+ self._data_callbacks = {}
+ if remove_metrics is not None:
+ remove_metrics(self)
+
+ def latest_aggregations(self) -> Dict[str, AggregationEntry]:
+ with self._lock:
+ return dict(self._aggregation_cache)
+
+ def _walk_aggregations(self, session: DTraceSession) -> Dict[str, AggregationEntry]:
+ try:
+ snapshot = self._aggregation_snapshot(session)
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(f"aggregation walk failed for {self.name}: {exc}")
+ return {}
+
+ updates: Dict[Tuple, AggregationEntry] = {}
+ for record in snapshot:
+ action = str(record.get("action", ""))
+ metric_name = self._metric_name_from_record(
+ record, include_keys=(action in _QUANTIZED_ACTIONS)
+ )
+ if not metric_name:
+ continue
+ instance_name = self._data_instance_from_record(record)
+ if not instance_name:
+ continue
+ value = record.get("value")
+
+ if action in _QUANTIZED_ACTIONS and isinstance(value, dict):
+ for bucket, count in value.items():
+ quantized_instance = self._quantized_bucket_range(
+ action, bucket, record.get("quantization")
+ )
+ entry = AggregationEntry(
+ metric=metric_name,
+ instance=quantized_instance,
+ script=self.name,
+ source=record.get("name", ""),
+ value=self._coerce_int(count),
+ samples=self._coerce_int(record.get("samples")),
+ normal=self._coerce_int(record.get("normal")),
+ action=action,
+ keys=self._key_names_from_record(record),
+ )
+ updates[(metric_name, entry.instance)] = entry
+ continue
+
+ entry = AggregationEntry(
+ metric=metric_name,
+ instance=instance_name,
+ script=self.name,
+ source=record.get("name", ""),
+ value=(
+ self._coerce_int(value)
+ if action in _INTEGER_ACTIONS
+ else self._coerce_float(value)
+ ),
+ samples=self._coerce_int(record.get("samples")),
+ normal=self._coerce_int(record.get("normal")),
+ action=action,
+ keys=self._key_names_from_record(record),
+ )
+ updates[(metric_name, instance_name)] = entry
+ with self._lock:
+ previous = self._aggregation_cache
+
+ current_keys = set(updates)
+ previous_keys = set(previous)
+ created = current_keys - previous_keys
+ removed = previous_keys - current_keys
+
+ refresh = False
+ for key in created:
+ entry = updates[key]
+ try:
+ with self._lock:
+ if "create_metric" in self._data_callbacks:
+ created_metric = self._data_callbacks["create_metric"](
+ entry
+ )
+ if created_metric is False:
+ updates.pop(key)
+ continue
+ if "create_instance" in self._data_callbacks:
+ self._data_callbacks["create_instance"](entry)
+ refresh = True
+ except Exception as exc: # pylint: disable=broad-except
+ updates.pop(key)
+ self._log(
+ f"create callback error for {self.name}:{entry.metric}: {exc}"
+ )
+
+ for key in current_keys & previous_keys:
+ entry = updates[key]
+ try:
+ with self._lock:
+ if "update_instance" in self._data_callbacks:
+ self._data_callbacks["update_instance"](entry)
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"update callback error for {self.name}:{entry.metric}: {exc}"
+ )
+
+ for key in removed:
+ entry = previous[key]
+ try:
+ with self._lock:
+ if "remove_instance" in self._data_callbacks:
+ self._data_callbacks["remove_instance"](entry)
+ refresh = True
+ except Exception as exc: # pylint: disable=broad-except
+ self._log(
+ f"remove callback error for {self.name}:{entry.metric}: {exc}"
+ )
+ if refresh:
+ with self._lock:
+ if "refresh_metrics" in self._data_callbacks:
+ self._data_callbacks["refresh_metrics"]()
+ with self._lock:
+ self._aggregation_cache = updates
+ return updates
+
+ def _aggregation_snapshot(self, session: DTraceSession) -> List[Dict[str, Any]]:
+ """Return the aggregation records selected for one PMDA update."""
+ if self.top is None and self.bottom is None:
+ return session.agg_walk()
+
+ records = []
+ for limit, mode in ((self.top, "valvarrev"),
+ (self.bottom, "valvar")):
+ if limit is None or limit == 0:
+ continue
+ selected = defaultdict(int)
+ for record in session.agg_walk(mode=mode):
+ aggid = record.get("id")
+ if selected[aggid] >= limit:
+ continue
+ selected[aggid] += 1
+ records.append(record)
+ return records
+
+ def _metric_name_from_record(
+ self, record: Dict[str, Any], include_keys: bool = False
+ ) -> Optional[str]:
+ source = record.get("name")
+ if not source or str(source).startswith(_IGNORED_AGGREGATION_PREFIX):
+ return None
+ metric = self.metric_prefix() + _sanitize_component(source)
+ if include_keys and record.get("keys"):
+ metric += "." + self._data_instance_from_record(record)
+ return metric
+
+ def _data_instance_from_record(self, record: Dict[str, Any]) -> Optional[str]:
+ parts = []
+ for key in record.get("keys", []):
+ # Each binding key is represented as a list. A scalar key is a
+ # one-element list; a stack key is a nested list of frames.
+ if isinstance(key, list):
+ if key and isinstance(key[0], list):
+ k = self.separator.join(
+ _flamegraph_frame(frame) for frame in reversed(key[0])
+ )
+ elif key:
+ k = key[0]
+ else:
+ k = "value"
+ else:
+ k = key
+ parts.append(_sanitize_component(k))
+ return self.separator.join(parts) if parts else "value"
+
+ def _quantized_instance_name(self, instance: str, bucket: Any) -> str:
+ """Append a quantization bucket using the script's key separator."""
+ return self.separator.join((
+ instance,
+ "bucket",
+ _sanitize_component(bucket),
+ ))
+
+ @staticmethod
+ def _quantize_bucket_range(bucket: Any) -> str:
+ """Return the inclusive value range for a quantize() bucket."""
+ try:
+ value = int(bucket)
+ except (TypeError, ValueError):
+ return _sanitize_component(bucket)
+ if value > 0:
+ return f"{value}-{2 * value - 1}"
+ if value < 0:
+ return f"{2 * value + 1}-{value}"
+ return "0-0"
+
+ @classmethod
+ def _quantized_bucket_range(
+ cls, action: str, bucket: Any, metadata: Any
+ ) -> str:
+ if action == "quantize":
+ return cls._quantize_bucket_range(bucket)
+ if not isinstance(metadata, dict):
+ return _sanitize_component(bucket)
+ try:
+ value = int(bucket)
+ if action == "lquantize":
+ base, step, levels = (int(metadata[key]) for key in
+ ("base", "step", "levels"))
+ if value == base - 1:
+ return f"-inf-{value}"
+ if value == base + levels * step:
+ return f"{value}-inf"
+ return f"{value}-{value + step - 1}"
+ if action == "llquantize":
+ factor = int(metadata["factor"])
+ low = factor ** int(metadata["lmag"])
+ high = factor ** (int(metadata["hmag"]) + 1)
+ if value == 0:
+ return f"{-low + 1}-{low - 1}"
+ if value > 0:
+ if value >= high:
+ return f"{value}-inf"
+ return f"{value}-{cls._llquantize_next(value, metadata, high) - 1}"
+ upper = -value
+ lower = cls._llquantize_next(upper, metadata, high)
+ return f"{-lower + 1}-{value}"
+ except (KeyError, TypeError, ValueError):
+ pass
+ return _sanitize_component(bucket)
+
+ @staticmethod
+ def _llquantize_next(value: int, metadata: Dict[str, Any], high: int) -> int:
+ """Return the next positive llquantize boundary, or its overflow."""
+ factor = int(metadata["factor"])
+ steps = int(metadata["steps"])
+ for magnitude in range(int(metadata["lmag"]), int(metadata["hmag"]) + 1):
+ scale = factor ** (magnitude + 1) // steps
+ for step in range(steps // factor + 1, steps + 1):
+ boundary = step * scale
+ if boundary > value:
+ return boundary
+ return high
+
+ def _key_names_from_record(self, record: Dict[str, Any]) -> Tuple[str, ...]:
+ """Return stable printable components for the binding key list."""
+ names = []
+ for key in record.get("keys", []):
+ if isinstance(key, list) and key and isinstance(key[0], list):
+ names.append(self.separator.join(
+ _flamegraph_frame(frame) for frame in reversed(key[0])
+ ))
+ elif isinstance(key, list):
+ names.append(str(key[0]) if key else "value")
+ else:
+ names.append(str(key))
+ return tuple(names)
+
+ @staticmethod
+ def _coerce_float(value: Any) -> Optional[float]:
+ if value is None:
+ return None
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def _coerce_int(value: Any) -> Optional[int]:
+ if value is None:
+ return None
+ if isinstance(value, int):
+ return value
+ if isinstance(value, Number):
+ return int(value)
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ return None
+
+class DTracePMDA(PMDA):
+ """PCP PMDA providing DTrace control and state metrics."""
+
+ class Control:
+ CLUSTER = 0
+ REGISTER = 0
+ UNREGISTER = 1
+ START = 2
+ STOP = 3
+ RELOAD = 4
+
+ class Scripts:
+ CLUSTER = 1
+ STATE = 0
+ AUTOSTART = 1
+ LAST_ERROR = 2
+ RUNTIME = 3
+
+ class Data:
+ CLUSTER = 2
+
+ def _script_name_collision_locked(self, name: str) -> Optional[str]:
+ """Return a distinct script name with the same PCP metric prefix."""
+ sanitized = _sanitize_component(name)
+ for existing_name in self._scripts:
+ if (
+ existing_name != name
+ and _sanitize_component(existing_name) == sanitized
+ ):
+ return existing_name
+ return None
+
+ def _create_metric(self, entry: AggregationEntry) -> bool:
+ if entry.metric in self._data_metrics:
+ return True
+ metric_id = self._metric_next
+ with self._lock:
+ if self._free_data_slots:
+ indom, indom_id = self._free_data_slots.pop()
+ self._exhausted_metrics.discard(entry.metric)
+ elif entry.metric in self._exhausted_metrics:
+ return False
+ else:
+ if self._indom_next > _PCP_PMID_MAX_CLUSTER:
+ if entry.metric not in self._exhausted_metrics:
+ self.err(
+ "dynamic metric limit reached; cannot add "
+ f"{entry.metric}"
+ )
+ self._exhausted_metrics.add(entry.metric)
+ return False
+ indom = self._indom_next
+ indom_id = self.indom(indom)
+ self._indom_next += 1
+ self.add_indom(
+ pmdaIndom(indom_id, []),
+ "DTrace metric data",
+ "DTrace data for metric",
+ )
+
+ metric_type = self._metric_type_for_action(entry.action)
+ metric = pmdaMetric(
+ self.pmid(indom, metric_id),
+ metric_type,
+ indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ )
+ try:
+ self.add_metric(entry.metric, metric)
+ except Exception as exc:
+ self.log(f"failed to add metric {entry.metric}: {exc}")
+ with self._lock:
+ self._free_data_slots.append((indom, indom_id))
+ return False
+ self._data_metrics[entry.metric] = DataMetric(
+ metric=metric,
+ pmid=metric.m_desc.pmid,
+ name=entry.metric,
+ script=entry.script,
+ cluster=indom,
+ variant=entry.source,
+ indom=indom,
+ indom_id=indom_id,
+ metric_id=metric_id,
+ instance_next=0
+ )
+ self._data_metric_ids[(indom, metric_id)] = self._data_metrics[entry.metric]
+ return True
+
+ @staticmethod
+ def _metric_type_for_action(action: str) -> int:
+ if action in _QUANTIZED_ACTIONS or action in _UNSIGNED_INTEGER_ACTIONS:
+ return c_api.PM_TYPE_U64
+ if action in _SIGNED_INTEGER_ACTIONS:
+ return c_api.PM_TYPE_64
+ return c_api.PM_TYPE_DOUBLE
+
+ def _create_instance(self, entry: AggregationEntry):
+
+ names = (entry.metric, entry.instance)
+ if entry.metric not in self._data_metrics:
+ return
+ metric = self._data_metrics[entry.metric]
+ if entry.instance in metric._data_instances:
+ return
+ instance_id = metric._instance_next
+ metric._instance_next += 1
+ metric._data_instances[entry.instance] = DataInstance(metric,
+ entry.instance,
+ instance_id,
+ entry.value)
+ metric._data_instance_ids[instance_id] = metric._data_instances[entry.instance]
+
+ def _update_instance(self, entry: AggregationEntry):
+ if entry.metric not in self._data_metrics:
+ return
+ metric = self._data_metrics[entry.metric]
+ if entry.instance not in metric._data_instances:
+ return
+ instance = metric._data_instances[entry.instance]
+ instance._value = entry.value
+
+ def _remove_instance_by_name(self, m:str, i:str):
+ if m not in self._data_metrics:
+ return
+ metric = self._data_metrics[m]
+ instance = metric._data_instances.pop(i, None)
+ if instance is None:
+ return
+ if instance._instid in metric._data_indom_insts:
+ metric._data_indom_insts.remove(instance._instid)
+ metric._data_instance_ids.pop(instance._instance_id, None)
+
+ def _remove_instance(self, entry: AggregationEntry):
+ self._remove_instance_by_name(entry.metric, entry.instance)
+
+ def _remove_metrics(self, script:ManagedDTraceScript):
+ prefix = script.metric_prefix()
+ free_slots = []
+ for m in list(self._data_metrics.keys()):
+ if not m.startswith(prefix):
+ continue
+ metric = self._data_metrics[m]
+ indom = metric.indom
+ metric_id = metric.metric_id
+ self.replace_indom(metric.indom_id, [])
+ self._data_metrics.pop(m, None)
+ self._data_metric_ids.pop((indom, metric_id), None)
+ self.remove_metric(m, metric.metric)
+ free_slots.append((indom, metric.indom_id))
+ if free_slots:
+ self.set_notify_change()
+ with self._lock:
+ self._free_data_slots.extend(free_slots)
+ self._exhausted_metrics.clear()
+
+ def _refresh_metrics(self):
+ self._rebuild_data_indom()
+ self.set_notify_change()
+ cpmda.set_need_refresh()
+
+ def __init__(self, name: str, domain: int) -> None:
+ super().__init__(name, domain)
+
+ self.set_user("root")
+
+ self._lock = threading.RLock()
+ (
+ self._dynamic_scripts_enabled,
+ self._auth_enabled,
+ self._allowed_users,
+ ) = self._load_control_config()
+ self._ctx_usernames: Dict[int, str] = {}
+ self._scripts: Dict[str, ManagedDTraceScript] = {}
+ self._inst_map: Dict[Tuple, str] = {}
+ self._next_inst = 0
+ self._indom_next = max(self.Control.CLUSTER, self.Scripts.CLUSTER) + 1
+ self._free_data_slots: List[Tuple[int, int]] = []
+ self._exhausted_metrics: Set[str] = set()
+ self._data_metrics: Dict[str, DataMetric] = {}
+ self._data_metric_ids: Dict[Tuple, DataMetric] = {}
+ self._metric_next = 0
+
+ self.script_indom_id = self.indom(0)
+
+ self._data_callbacks = {
+ "create_metric" : self._create_metric,
+ "remove_metrics" : self._remove_metrics,
+ "refresh_metrics" : self._refresh_metrics,
+ "create_instance" : self._create_instance,
+ "update_instance" : self._update_instance,
+ "remove_instance" : self._remove_instance
+ }
+ self.script_indom = pmdaIndom(self.script_indom_id, [])
+ self.add_indom(self.script_indom, "DTrace scripts", "Registered scripts")
+
+ self._units_none = pmUnits()
+ self._units_seconds = pmUnits(0, 1, 0, 0, c_api.PM_TIME_SEC, 0)
+
+ # Require credential PDUs to avoid TYPE-0 handshake downgrade.
+ self.set_comm_flags(cpmda.PMDA_FLAG_AUTHORIZE)
+
+ self.log(f"Adding metrics for PMDA..")
+ self.add_metric(
+ "dtrace.control.register",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.REGISTER),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Register a DTrace script via JSON payload",
+ )
+ self.add_metric(
+ "dtrace.control.unregister",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.UNREGISTER),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Unregister an existing DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.start",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.START),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Start a registered DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.stop",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.STOP),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Stop a running DTrace script",
+ )
+ self.add_metric(
+ "dtrace.control.reload",
+ pmdaMetric(
+ self.pmid(self.Control.CLUSTER, self.Control.RELOAD),
+ c_api.PM_TYPE_STRING,
+ c_api.PM_INDOM_NULL,
+ c_api.PM_SEM_INSTANT,
+ self._units_none,
+ ),
+ "Reload autostart scripts from disk",
+ )
+
+ self.add_metric(
+ "dtrace.scripts.state",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.STATE),
+ c_api.PM_TYPE_STRING,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "State of each registered DTrace script",
+ )
+ self.add_metric(
+ "dtrace.scripts.autostart",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.AUTOSTART),
+ c_api.PM_TYPE_U32,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "Whether the script autostarts on PMDA load",
+ )
+ self.add_metric(
+ "dtrace.scripts.last_error",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.LAST_ERROR),
+ c_api.PM_TYPE_STRING,
+ self.script_indom_id,
+ c_api.PM_SEM_DISCRETE,
+ self._units_none,
+ ),
+ "Most recent error message for the script",
+ )
+ self.add_metric(
+ "dtrace.scripts.runtime_seconds",
+ pmdaMetric(
+ self.pmid(self.Scripts.CLUSTER, self.Scripts.RUNTIME),
+ c_api.PM_TYPE_U64,
+ self.script_indom_id,
+ c_api.PM_SEM_INSTANT,
+ self._units_seconds,
+ ),
+ "Seconds the script has been running",
+ )
+
+ self.set_fetch_callback(self._fetch_callback)
+ self.set_store_callback(self._store_callback)
+ self.set_attribute_callback(self._attribute_callback)
+ self.set_endcontext_callback(self._endcontext_callback)
+ self.set_user("root")
+
+ self._autostart_dir = (
+ Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / self.read_name() / "autostart"
+ )
+
+ atexit.register(self._shutdown_scripts)
+
+ if not self._in_pmda_setup():
+ # If running a newer pcp python binding that supports credentials,
+ # explicitly tell the C layer how to fall back if the server is old.
+ if cpmda and hasattr(cpmda, 'pmdaSetFlags'):
+ try:
+ # PMDA_FLAG_CREDS_OPTIONAL allows the agent to handle
+ # credentials if sent, but fall back seamlessly if an
+ # older pmcd sends nothing.
+ # PMDA_FLAG_CREDS_OPTIONAL constant value is typically 0x4
+ cpmda.pmdaSetFlags(0x4)
+ except Exception:
+ pass
+ try:
+ self.connect_pmcd()
+ except Exception as e:
+ sys.stderr.write(f"Connection failed: {str(e)}\n")
+ sys.exit(1)
+ self.pmda_ready()
+ self.log("Ready to process DTrace control requests.")
+ threading.Thread(
+ target=self.reload_autostart,
+ name="dtrace:autostart",
+ daemon=True,
+ ).start()
+
+ # ------------------------------------------------------------------
+ # Store callback handling control metrics
+ # ------------------------------------------------------------------
+ @staticmethod
+ def _load_control_config() -> Tuple[bool, bool, Set[str]]:
+ """Load the root-owned dynamic-script authorization policy."""
+ config = configparser.ConfigParser()
+ config_path = (
+ Path(PCP.pmGetConfig("PCP_PMDAS_DIR")) / "dtrace" / "dtrace.conf"
+ )
+
+ # Secure defaults: do not permit dynamically supplied programs.
+ enabled = False
+ auth_enabled = True
+ allowed_users: Set[str] = set()
+
+ try:
+ config.read(config_path)
+ if not config.has_section("dynamic_scripts"):
+ return enabled, auth_enabled, allowed_users
+
+ enabled = config.getboolean(
+ "dynamic_scripts", "enabled", fallback=enabled
+ )
+ auth_enabled = config.getboolean(
+ "dynamic_scripts", "auth_enabled", fallback=auth_enabled
+ )
+ configured_users = config.get(
+ "dynamic_scripts", "allowed_users", fallback=""
+ )
+ except (OSError, ValueError, configparser.Error):
+ return False, True, set()
+
+ allowed_users = {
+ user.strip() for user in configured_users.split(",") if user.strip()
+ }
+ return enabled, auth_enabled, allowed_users
+
+ def _attribute_callback(self, ctx: int, attr: int, value: str) -> None:
+ """Associate an authenticated client identity with its PCP context."""
+ if attr == cpmda.PMDA_ATTR_USERNAME:
+ with self._lock:
+ self._ctx_usernames[ctx] = value
+ return
+
+ if attr != cpmda.PMDA_ATTR_USERID:
+ return
+
+ try:
+ username = pwd.getpwuid(int(value)).pw_name
+ except (KeyError, OverflowError, ValueError):
+ # Do not retain an earlier, less trustworthy identity.
+ with self._lock:
+ self._ctx_usernames.pop(ctx, None)
+ return
+
+ with self._lock:
+ self._ctx_usernames[ctx] = username
+
+ def _endcontext_callback(self, ctx: int) -> None:
+ with self._lock:
+ self._ctx_usernames.pop(ctx, None)
+
+ def _control_store_permitted(self) -> bool:
+ if not self._dynamic_scripts_enabled:
+ return False
+ if not self._auth_enabled:
+ return True
+
+ try:
+ ctx = pmdaGetContext()
+ except Exception: # pragma: no cover - PMDA library failure
+ return False
+
+ with self._lock:
+ username = self._ctx_usernames.get(ctx)
+ return username is not None and username in self._allowed_users
+
+ def _store_callback(self, cluster: int, item: int, inst: int, value: Any) -> int:
+ if cluster != self.Control.CLUSTER:
+ return c_api.PM_ERR_PMID
+
+ if not self._control_store_permitted():
+ self.err("DTrace control store denied by dynamic-script policy")
+ return c_api.PM_ERR_PERMISSION
+
+ if isinstance(value, bytes):
+ text = value.decode("utf-8", errors="ignore")
+ else:
+ text = str(value)
+
+ if item == self.Control.REGISTER:
+ return self._handle_register(text)
+ if item == self.Control.UNREGISTER:
+ return self._handle_unregister(text)
+ if item == self.Control.START:
+ return self._handle_start(text)
+ if item == self.Control.STOP:
+ return self._handle_stop(text)
+ if item == self.Control.RELOAD:
+ self.reload_autostart()
+ return 0
+ return c_api.PM_ERR_PMID
+
+ def _handle_register(self, payload: str) -> int:
+ self.log(f"handle register {payload}")
+ try:
+ data = json.loads(payload)
+ except json.JSONDecodeError:
+ self.err("register payload must be valid JSON")
+ return c_api.PM_ERR_VALUE
+ if not isinstance(data, dict):
+ self.err("register payload must be a JSON object")
+ return c_api.PM_ERR_VALUE
+
+ name = data.get("name")
+ program = data.get("program")
+ if (
+ not isinstance(name, str)
+ or not name
+ or not isinstance(program, str)
+ or not program
+ ):
+ self.err("register payload requires string 'name' and 'program'")
+ return c_api.PM_ERR_VALUE
+
+ self.log(f"program is {program}")
+ options = data.get("options") if isinstance(data.get("options"), dict) else {}
+ separator = data.get("separator") if isinstance(data.get("separator"), str) else "."
+ autostart = data.get("autostart", False)
+ if not isinstance(autostart, bool):
+ self.err("register payload 'autostart' must be a boolean")
+ return c_api.PM_ERR_VALUE
+ exitonly = data.get("exitonly", False)
+ if not isinstance(exitonly, bool):
+ self.err("register payload 'exitonly' must be a boolean")
+ return c_api.PM_ERR_VALUE
+ valid, top = self._parse_aggregation_limit(data, "top", "register payload")
+ if not valid:
+ return c_api.PM_ERR_VALUE
+ valid, bottom = self._parse_aggregation_limit(
+ data, "bottom", "register payload"
+ )
+ if not valid:
+ return c_api.PM_ERR_VALUE
+ if top is not None and bottom is not None:
+ self.err("register payload 'top' and 'bottom' are mutually exclusive")
+ return c_api.PM_ERR_VALUE
+ pid, command = self._parse_target(data)
+ if pid is None and command is None and (
+ "pid" in data or "command" in data
+ ):
+ return c_api.PM_ERR_VALUE
+ defines = self._parse_defines(data.get("defines"))
+ if defines is None:
+ return c_api.PM_ERR_VALUE
+ args = self._parse_args(data.get("args"))
+ if args is None:
+ return c_api.PM_ERR_VALUE
+ compile_flags = self._parse_compile_flags(data.get("compile"))
+ if compile_flags is None:
+ return c_api.PM_ERR_VALUE
+ self.log(f"autostart is {autostart}")
+
+ with self._lock:
+ conflict = self._script_name_collision_locked(name)
+ if conflict is not None:
+ self.err(
+ f"script name {name!r} conflicts with {conflict!r} "
+ "after PCP name sanitization"
+ )
+ return c_api.PM_ERR_VALUE
+ script = self._scripts.get(name)
+ if script:
+ existing = True
+ script.autostart = autostart
+ script.separator = separator
+ else:
+ existing = False
+ script = ManagedDTraceScript(
+ name, program, autostart, options, separator, pid, command,
+ defines, args,
+ compile_flags,
+ self.log,
+ exitonly,
+ top,
+ bottom,
+ )
+ self._scripts[name] = script
+ self._assign_instance(name)
+ if existing:
+ script.update_definition(
+ program, options, pid, command, defines, args, compile_flags,
+ exitonly,
+ top,
+ bottom,
+ )
+ script.register_data_callbacks(self)
+
+ if autostart:
+ self.log(f"starting script {name}...")
+ ok, error = script.start()
+ if not ok:
+ self.err(f"failed to start script {name}: {error}")
+ self.log(f"script {name} started ok")
+ return 0
+
+ def _parse_target(
+ self, data: Dict[str, Any]
+ ) -> Tuple[Optional[int], Optional[List[str]]]:
+ """Validate JSON equivalents of dtrace(1)'s -p and -c options."""
+ pid = data.get("pid")
+ command = data.get("command")
+
+ if pid is not None and command is not None:
+ self.err("register payload must specify only one of 'pid' or 'command'")
+ return None, None
+
+ if pid is not None:
+ if isinstance(pid, bool) or not isinstance(pid, int) or pid <= 0:
+ self.err("register payload 'pid' must be a positive integer")
+ return None, None
+ if pid > 0x7fffffff:
+ self.err("register payload 'pid' is out of range")
+ return None, None
+ return pid, None
+
+ if command is not None:
+ if not isinstance(command, str):
+ self.err("register payload 'command' must be a non-empty string")
+ return None, None
+ try:
+ argv = shlex.split(command)
+ except ValueError:
+ self.err("register payload 'command' has invalid shell quoting")
+ return None, None
+ if not argv:
+ self.err("register payload 'command' must be a non-empty string")
+ return None, None
+ return None, argv
+
+ return None, None
+
+ def _parse_aggregation_limit(
+ self, data: Dict[str, Any], field: str, context: str
+ ) -> Tuple[bool, Optional[int]]:
+ if field not in data:
+ return True, None
+ value = data[field]
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
+ self.err(f"{context} '{field}' must be a non-negative integer")
+ return False, None
+ return True, value
+
+ def _parse_defines(self, value: Any) -> Optional[List[str]]:
+ """Validate macro definitions and return NAME[=VALUE] strings."""
+ if value is None:
+ return []
+ items = value.items() if isinstance(value, dict) else value
+ if not isinstance(value, (dict, list)):
+ self.err("register payload 'defines' must be an object or array")
+ return None
+ result = []
+ for item in items:
+ if isinstance(value, dict):
+ name, macro_value = item
+ if not isinstance(name, str) or not re.fullmatch(
+ r"[A-Za-z_][A-Za-z0-9_]*", name
+ ):
+ self.err("register payload has invalid macro name")
+ return None
+ result.append(name if macro_value is None else f"{name}={macro_value}")
+ elif not isinstance(item, str) or not re.fullmatch(
+ r"[A-Za-z_][A-Za-z0-9_]*(=.*)?", item
+ ):
+ self.err(
+ "register payload 'defines' entries must be NAME or NAME=VALUE"
+ )
+ return None
+ else:
+ result.append(item)
+ return result
+
+ def _parse_args(self, value: Any) -> Optional[List[str]]:
+ """Validate positional D-script arguments supplied as JSON strings."""
+ if value is None:
+ return []
+ if not isinstance(value, list) or not all(
+ isinstance(item, str) for item in value
+ ):
+ self.err("register payload 'args' must be an array of strings")
+ return None
+ return list(value)
+
+ def _parse_compile_flags(self, value: Any) -> Optional[int]:
+ """Translate supported compile-time flags to libdtrace cflags."""
+ if value is None:
+ return 0
+ if not isinstance(value, dict):
+ self.err("register payload 'compile' must be an object")
+ return None
+ flags = 0
+ if value.get("zdefs", False):
+ flags |= DTRACE_C_ZDEFS
+ unknown = set(value) - {"zdefs"}
+ if unknown:
+ self.err("unsupported compile option(s): " + ", ".join(sorted(unknown)))
+ return None
+ return flags
+
+ def _handle_unregister(self, name: str) -> int:
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = None
+ with self._lock:
+ script = self._scripts.pop(name, None)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ self._rebuild_instances_locked()
+ if script is not None:
+ if script._state != "stopped":
+ script.stop()
+ script.unregister_data_callbacks(self)
+ return 0
+
+ def _handle_start(self, name: str) -> int:
+ self.log(f"handle start {name}")
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = self._scripts.get(name)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ ok, error = script.start()
+ if not ok:
+ self.err(f"start failed for {name}: {error}")
+ return c_api.PM_ERR_GENERIC
+ return 0
+
+ def _handle_stop(self, name: str) -> int:
+ self.log(f"handle stop {name}")
+ name = name.strip()
+ if not name:
+ return c_api.PM_ERR_VALUE
+ script = self._scripts.get(name)
+ if script is None:
+ return c_api.PM_ERR_NAME
+ script.stop()
+ return 0
+
+ # ------------------------------------------------------------------
+ # Fetch callback serving metric values
+ # ------------------------------------------------------------------
+ def _fetch_callback(self, cluster: int, item: int, inst: int):
+ if cluster > self.Scripts.CLUSTER:
+ ids = (cluster, item)
+ if ids not in self._data_metric_ids:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ metric = self._data_metric_ids[(cluster, item)]
+ if inst not in metric._data_instance_ids:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ instance = metric._data_instance_ids[inst]
+ value = instance._value
+ if value is None:
+ return [cpmda.PMDA_FETCH_NOVALUES, 0]
+ return [value, 1]
+ if cluster == self.Control.CLUSTER:
+ # Control metrics are write-only, but returning NOVALUES causes
+ # clients like pmstore to abort before issuing the store PDU.
+ # Hand back an empty string so the store callback still fires.
+ return ["", 1]
+
+ if cluster != self.Scripts.CLUSTER:
+ return [c_api.PM_ERR_PMID, 0]
+
+ name = self._inst_map.get(inst)
+ if not name:
+ return [c_api.PM_ERR_INST, 0]
+ script = self._scripts.get(name)
+ if not script:
+ return [c_api.PM_ERR_INST, 0]
+
+ if item == self.Scripts.STATE:
+ return [script.state(), 1]
+ if item == self.Scripts.AUTOSTART:
+ return [1 if script.autostart else 0, 1]
+ if item == self.Scripts.LAST_ERROR:
+ return [script.last_error(), 1]
+ if item == self.Scripts.RUNTIME:
+ return [script.runtime_seconds(), 1]
+
+ return [c_api.PM_ERR_PMID, 0]
+
+ # ------------------------------------------------------------------
+ # Autostart support
+ # ------------------------------------------------------------------
+ def reload_autostart(self) -> None:
+ if not self._autostart_dir.is_dir():
+ return
+
+ autostart_paths = set()
+ loaded_autostart_paths = {}
+ for script_path in sorted(self._autostart_dir.glob("*.d")):
+ source = str(script_path)
+ autostart_paths.add(source)
+ try:
+ program = script_path.read_text(encoding="utf-8")
+ except OSError as exc: # pragma: no cover - filesystem error
+ self.err(f"unable to read {script_path}: {exc}")
+ continue
+
+ metadata = self._load_metadata(script_path)
+ if metadata is None:
+ continue
+ name = metadata.get("name", script_path.stem)
+ if not isinstance(name, str) or not name:
+ self.err(f"invalid name in autostart metadata {script_path}")
+ continue
+ options = metadata.get("options") if isinstance(metadata.get("options"), dict) else {}
+ separator = metadata.get("separator") if isinstance(metadata.get("separator"), str) else "."
+ exitonly = metadata.get("exitonly", False)
+ if not isinstance(exitonly, bool):
+ self.err(f"invalid exitonly in autostart metadata {script_path}")
+ continue
+ valid, top = self._parse_aggregation_limit(
+ metadata, "top", f"autostart metadata {script_path}"
+ )
+ if not valid:
+ continue
+ valid, bottom = self._parse_aggregation_limit(
+ metadata, "bottom", f"autostart metadata {script_path}"
+ )
+ if not valid:
+ continue
+ if top is not None and bottom is not None:
+ self.err(
+ f"autostart metadata {script_path} specifies both "
+ "'top' and 'bottom'"
+ )
+ continue
+ pid, command = self._parse_target(metadata)
+ if pid is None and command is None and (
+ "pid" in metadata or "command" in metadata
+ ):
+ self.err(f"invalid target in autostart metadata {script_path}")
+ continue
+ defines = self._parse_defines(metadata.get("defines"))
+ if defines is None:
+ self.err(f"invalid macro definitions in {script_path}")
+ continue
+ args = self._parse_args(metadata.get("args"))
+ if args is None:
+ self.err(f"invalid script arguments in {script_path}")
+ continue
+ compile_flags = self._parse_compile_flags(metadata.get("compile"))
+ if compile_flags is None:
+ self.err(f"invalid compile options in {script_path}")
+ continue
+
+ loaded_autostart_paths[source] = name
+
+ with self._lock:
+ conflict = self._script_name_collision_locked(name)
+ if conflict is not None:
+ self.err(
+ f"autostart name {name!r} conflicts with {conflict!r} "
+ "after PCP name sanitization"
+ )
+ continue
+ script = self._scripts.get(name)
+ if script:
+ existing = True
+ script.autostart = True
+ script.autostart_source = source
+ script.separator = separator
+ else:
+ existing = False
+ script = ManagedDTraceScript(
+ name, program, True, options, separator, pid, command,
+ defines, args,
+ compile_flags,
+ self.log,
+ exitonly,
+ top,
+ bottom,
+ )
+ script.autostart_source = source
+ self._scripts[name] = script
+ self._assign_instance(name)
+ if existing:
+ script.update_definition(
+ program, options, pid, command, defines, args, compile_flags,
+ exitonly,
+ top,
+ bottom,
+ )
+ script.register_data_callbacks(self)
+
+ ok, error = script.start()
+ if not ok:
+ self.err(f"autostart of {name} failed: {error}")
+
+ stale_scripts = []
+ with self._lock:
+ for name, script in list(self._scripts.items()):
+ source = script.autostart_source
+ replacement = loaded_autostart_paths.get(source)
+ if source is None or (
+ source in autostart_paths and replacement in (None, name)
+ ):
+ continue
+ self._scripts.pop(name)
+ stale_scripts.append(script)
+ if stale_scripts:
+ self._rebuild_instances_locked()
+
+ for script in stale_scripts:
+ self.log(f"removing deleted autostart script {script.name}")
+ script.stop()
+ script.unregister_data_callbacks(self)
+
+ def _load_metadata(self, script_path: Path) -> Optional[Dict[str, Any]]:
+ meta_path = script_path.with_suffix(".json")
+ if not meta_path.is_file():
+ return {}
+ try:
+ metadata = json.loads(meta_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as exc: # pragma: no cover
+ self.err(f"failed to parse metadata {meta_path}: {exc}")
+ return None
+ if not isinstance(metadata, dict):
+ self.err(f"metadata {meta_path} must be a JSON object")
+ return None
+ return metadata
+
+ # ------------------------------------------------------------------
+ # Instance domain management
+ # ------------------------------------------------------------------
+ def _assign_instance(self, name: str) -> None:
+ with self._lock:
+ inst_id = self._next_inst
+ self._next_inst += 1
+ self._inst_map[inst_id] = name
+ self._rebuild_indom()
+
+ def _rebuild_instances_locked(self) -> None:
+ # assumes caller holds self._lock
+ self._inst_map = {
+ idx: name for idx, name in enumerate(sorted(self._scripts.keys()))
+ }
+ self._next_inst = len(self._inst_map)
+ self._rebuild_indom()
+
+ def _rebuild_data_indom(self) -> None:
+ with self._lock:
+ for m in self._data_metrics.values():
+ self.replace_indom(m.indom_id, m._data_indom_insts)
+
+
+ def _rebuild_indom(self) -> None:
+ insts = [pmdaInstid(inst, name) for inst, name in sorted(self._inst_map.items())]
+ self.replace_indom(self.script_indom_id, insts)
+
+ # ------------------------------------------------------------------
+ # Teardown helpers
+ # ------------------------------------------------------------------
+ def _shutdown_scripts(self) -> None:
+ with self._lock:
+ scripts = list(self._scripts.values())
+ for script in scripts:
+ script.unregister_data_callbacks(self)
+ # Process teardown has no PMDA IPC deadline, so wait for DTrace
+ # to release its resources before the interpreter exits.
+ script.stop(wait=True)
+
+ # ------------------------------------------------------------------
+ @staticmethod
+ def _in_pmda_setup() -> bool:
+ return bool(os.environ.get("PCP_PYTHON_DOMAIN") or os.environ.get("PCP_PYTHON_PMNS"))
+
+
+def main() -> None:
+ domain = -1
+ install_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Install")
+ with open(install_path, encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith("#") or not line.startswith("domain="):
+ continue
+ try:
+ domain = int(line.split("=", 1)[1].strip())
+ except ValueError:
+ print(f"Invalid domain value in {install_path}")
+ return
+ break
+ if domain == -1:
+ print("No domain value in Install, exiting.")
+ else:
+ DTracePMDA("dtrace", domain).run()
+
+
+if __name__ == "__main__":
+ main()
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH v5 10/11] dtrace.spec: Add PCP PMDA packaging pcp-pmda-dtrace
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (8 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 09/11] dtrace: Add a PCP PMDA to expose DTrace data as metrics Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-08-21 16:10 ` [PATCH v5 11/11] test: Add a test for the PCP ManagedDTraceScript interface Alan Maguire
10 siblings, 0 replies; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Add a PCP build conditional and package the DTrace PMDA when PCP and
Python support are enabled. Install it under an overridable PCP target
directory, defaulting to /var/lib/pcp/pmdas/dtrace and require
python3-pcp at runtime.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
dtrace.spec | 57 +++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 53 insertions(+), 4 deletions(-)
diff --git a/dtrace.spec b/dtrace.spec
index bc738c4f..0a17b00c 100644
--- a/dtrace.spec
+++ b/dtrace.spec
@@ -29,6 +29,8 @@
# By default, do not build with 32-on-64 support.
%define glibc32 %{nil}
+%global pcp_targetdir %{?pcp_targetdir}%{!?pcp_targetdir:/var/lib/pcp/pmdas/dtrace}
+
# Enable it for non-ARM64 builds excpet for OL10.
# ARM64 does not support 32-on-64 either.
%ifnarch aarch64
@@ -41,6 +43,7 @@
%global _lto_cflags %{nil}
%bcond_without python
+%bcond_without pcp
BuildRequires: rpm
Name: dtrace
@@ -146,6 +149,17 @@ Python extension module providing access to libdtrace.
%endif
+%if %{with pcp} && %{with python}
+%package -n pcp-pmda-dtrace
+Summary: PCP PMDA for DTrace
+Requires: python3-dtrace = %{version}-%{release}
+Requires: python3-pcp
+Group: Applications/System
+
+%description -n pcp-pmda-dtrace
+Performance Co-Pilot PMDA for exposing DTrace data as metrics.
+%endif
+
Installed in /usr/lib64/dtrace/testsuite.
'make check' here is just like 'make check' in the source tree, except that
@@ -158,9 +172,14 @@ it always tests the installed DTrace.
%if %{with python}
%global python_build_args WITH_PYTHON=y PYTHON=%{__python3}
%else
-%global python_build_args %{nil}
+%global python_build_args WITH_PYTHON=n
%endif
-make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2} %{python_build_args}
+%if %{with pcp} && %{with python}
+%global pcp_build_args WITH_PCP=y
+%else
+%global pcp_build_args WITH_PCP=n
+%endif
+make -j $(getconf _NPROCESSORS_ONLN) %{bpfc} %{maybe_use_fuse2} %{python_build_args} %{pcp_build_args}
# Force off debuginfo splitting. We have no debuginfo in dtrace proper,
# and the testsuite requires debuginfo for proper operation.
@@ -176,11 +195,20 @@ mkdir -p $RPM_BUILD_ROOT/usr/sbin
%if %{with python}
%global python_install_args install-python PYTHON=%{__python3}
%else
-%global python_install_args %{nil}
+%global python_install_args WITH_PYTHON=n
+%endif
+%if %{with pcp} && %{with python}
+%global pcp_install_args WITH_PCP=y PMDA_OBJDIR="$RPM_BUILD_ROOT%{pcp_targetdir}"
+%else
+%global pcp_install_args WITH_PCP=n
%endif
make DESTDIR=$RPM_BUILD_ROOT VERSION=%{version} \
HDRPREFIX="$RPM_BUILD_ROOT/usr/include" \
- install install-test %{python_install_args}
+ %{pcp_install_args} install install-test %{python_install_args}
+
+%if %{without pcp} || %{without python}
+rm -rf "$RPM_BUILD_ROOT%{pcp_targetdir}"
+%endif
%if "%{?dist}" == ".el7"
sed -i '/^ProtectSystem=/d; /^ProtectControlGroups=/d; /^RuntimeDirectory/d;' $RPM_BUILD_ROOT/usr/lib/systemd/system/dtprobed.service
@@ -222,6 +250,13 @@ fi
%preun
%systemd_preun dtprobed.service
+%if %{with pcp} && %{with python}
+%preun -n pcp-pmda-dtrace
+if [ "$1" -eq 0 ] && [ -f "%{pcp_targetdir}/Remove" ]; then
+ (cd "%{pcp_targetdir}" && ./Remove >/dev/null 2>&1) || :
+fi
+%endif
+
%postun
/sbin/ldconfig
%udev_rules_update
@@ -265,6 +300,20 @@ systemctl start dtprobed || :
%{python3_sitearch}/dtrace*.so
%doc bindings/python/README.md
%endif
+%if %{with pcp} && %{with python}
+%files -n pcp-pmda-dtrace
+%defattr(-,root,root,-)
+%dir %{pcp_targetdir}
+%{pcp_targetdir}/Install
+%{pcp_targetdir}/Remove
+%{pcp_targetdir}/pmdadtrace.python
+%{pcp_targetdir}/README.md
+%config(noreplace) %{pcp_targetdir}/dtrace.conf
+%{_mandir}/man1/pmdadtrace.1.gz
+%dir %{pcp_targetdir}/autostart
+%dir %{pcp_targetdir}/examples
+%{pcp_targetdir}/examples/*
+%endif
%files testsuite
%defattr(-,root,root,-)
%{_libdir}/dtrace/testsuite
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH v5 11/11] test: Add a test for the PCP ManagedDTraceScript interface
2026-08-21 16:10 [PATCH v5 00/11] Add python, PCP support via libdtrace interfaces Alan Maguire
` (9 preceding siblings ...)
2026-08-21 16:10 ` [PATCH v5 10/11] dtrace.spec: Add PCP PMDA packaging pcp-pmda-dtrace Alan Maguire
@ 2026-08-21 16:10 ` Alan Maguire
2026-10-05 16:28 ` Kris Van Hees
10 siblings, 1 reply; 18+ messages in thread
From: Alan Maguire @ 2026-08-21 16:10 UTC (permalink / raw)
To: dtrace; +Cc: dtrace-devel, Alan Maguire
Since tests cannot rely on a full PCP configuration, do
limited testing of a ManagedDTraceScript, ensuring that
we can create it, start it and that it collects data.
Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
---
test/unittest/python/tst.pcp.sh | 69 +++++++++++++++++++++++++++++++++
1 file changed, 69 insertions(+)
create mode 100755 test/unittest/python/tst.pcp.sh
diff --git a/test/unittest/python/tst.pcp.sh b/test/unittest/python/tst.pcp.sh
new file mode 100755
index 00000000..acc68854
--- /dev/null
+++ b/test/unittest/python/tst.pcp.sh
@@ -0,0 +1,69 @@
+#!/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.
+#
+# Test the PCP ManagedDTraceScript with a real DTrace session.
+# @@timeout: 60
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <dtrace>" >&2
+ exit 2
+fi
+
+if ! "$DTRACE_PYTHON" -c 'import dtrace, cpmapi, cpmda, pcp.pmapi, pcp.pmda' \
+ >/dev/null 2>&1; then
+ echo "DTrace or PCP Python bindings are unavailable"
+ exit 2
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() { rm -rf "$tmpdir"; }
+trap cleanup EXIT
+
+cat >"$tmpdir/test.py" <<'EOF'
+#!/usr/bin/env python3
+import importlib.machinery
+import importlib.util
+import time
+
+loader = importlib.machinery.SourceFileLoader("pmdadtrace", "pcp/pmdadtrace.python")
+spec = importlib.util.spec_from_loader("pmdadtrace", loader)
+pmdadtrace = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(pmdadtrace)
+
+created = []
+script = pmdadtrace.ManagedDTraceScript(
+ "tick_test", "profile:::tick-1s /$1 == 1/ { @ticks = count(); }", False,
+ {"quiet": True}, ".", None, None, [], ["1"], 0,
+ print,
+)
+script._data_callbacks = {
+ "create_metric": lambda entry: created.append(("metric", entry.metric)),
+ "create_instance": lambda entry: created.append(
+ ("instance", entry.metric, entry.instance)
+ ),
+ "update_instance": lambda entry: None,
+ "remove_instance": lambda entry: None,
+ "refresh_metrics": lambda: None,
+}
+
+ok, error = script.start()
+if not ok:
+ raise SystemExit(f"unable to start ManagedDTraceScript: {error}")
+time.sleep(15)
+script.stop()
+
+entries = script.latest_aggregations()
+if script.state() == "error":
+ raise SystemExit(f"ManagedDTraceScript failed: {script.last_error()}")
+if not any(entry.metric.endswith(".ticks") for entry in entries.values()):
+ raise SystemExit(f"tick aggregation was not published: {entries}")
+if not any(item[0] == "instance" for item in created):
+ raise SystemExit(f"tick metric instance was not created: {created}")
+EOF
+
+chmod +x "$tmpdir/test.py"
+timeout 60 "$DTRACE_PYTHON" "$tmpdir/test.py"
--
2.43.5
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH v5 11/11] test: Add a test for the PCP ManagedDTraceScript interface
2026-08-21 16:10 ` [PATCH v5 11/11] test: Add a test for the PCP ManagedDTraceScript interface Alan Maguire
@ 2026-10-05 16:28 ` Kris Van Hees
0 siblings, 0 replies; 18+ messages in thread
From: Kris Van Hees @ 2026-10-05 16:28 UTC (permalink / raw)
To: Alan Maguire; +Cc: dtrace, dtrace-devel
This should either add a dependency on python and pcp for the testsuite,
or there should be a .x file to exclude this test when python and pcp
are not available. As it is now, not having PCP on the system will
cause the test to fail.
On Fri, Aug 21, 2026 at 05:10:59PM +0100, Alan Maguire wrote:
> Since tests cannot rely on a full PCP configuration, do
> limited testing of a ManagedDTraceScript, ensuring that
> we can create it, start it and that it collects data.
>
> Signed-off-by: Alan Maguire <alan.maguire@oracle.com>
> ---
> test/unittest/python/tst.pcp.sh | 69 +++++++++++++++++++++++++++++++++
> 1 file changed, 69 insertions(+)
> create mode 100755 test/unittest/python/tst.pcp.sh
>
> diff --git a/test/unittest/python/tst.pcp.sh b/test/unittest/python/tst.pcp.sh
> new file mode 100755
> index 00000000..acc68854
> --- /dev/null
> +++ b/test/unittest/python/tst.pcp.sh
> @@ -0,0 +1,69 @@
> +#!/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.
> +#
> +# Test the PCP ManagedDTraceScript with a real DTrace session.
> +# @@timeout: 60
> +
> +if [ $# -ne 1 ]; then
> + echo "usage: $0 <dtrace>" >&2
> + exit 2
> +fi
> +
> +if ! "$DTRACE_PYTHON" -c 'import dtrace, cpmapi, cpmda, pcp.pmapi, pcp.pmda' \
> + >/dev/null 2>&1; then
> + echo "DTrace or PCP Python bindings are unavailable"
> + exit 2
> +fi
> +
> +tmpdir=$(mktemp -d)
> +cleanup() { rm -rf "$tmpdir"; }
> +trap cleanup EXIT
> +
> +cat >"$tmpdir/test.py" <<'EOF'
> +#!/usr/bin/env python3
> +import importlib.machinery
> +import importlib.util
> +import time
> +
> +loader = importlib.machinery.SourceFileLoader("pmdadtrace", "pcp/pmdadtrace.python")
> +spec = importlib.util.spec_from_loader("pmdadtrace", loader)
> +pmdadtrace = importlib.util.module_from_spec(spec)
> +spec.loader.exec_module(pmdadtrace)
> +
> +created = []
> +script = pmdadtrace.ManagedDTraceScript(
> + "tick_test", "profile:::tick-1s /$1 == 1/ { @ticks = count(); }", False,
> + {"quiet": True}, ".", None, None, [], ["1"], 0,
> + print,
> +)
> +script._data_callbacks = {
> + "create_metric": lambda entry: created.append(("metric", entry.metric)),
> + "create_instance": lambda entry: created.append(
> + ("instance", entry.metric, entry.instance)
> + ),
> + "update_instance": lambda entry: None,
> + "remove_instance": lambda entry: None,
> + "refresh_metrics": lambda: None,
> +}
> +
> +ok, error = script.start()
> +if not ok:
> + raise SystemExit(f"unable to start ManagedDTraceScript: {error}")
> +time.sleep(15)
> +script.stop()
> +
> +entries = script.latest_aggregations()
> +if script.state() == "error":
> + raise SystemExit(f"ManagedDTraceScript failed: {script.last_error()}")
> +if not any(entry.metric.endswith(".ticks") for entry in entries.values()):
> + raise SystemExit(f"tick aggregation was not published: {entries}")
> +if not any(item[0] == "instance" for item in created):
> + raise SystemExit(f"tick metric instance was not created: {created}")
> +EOF
> +
> +chmod +x "$tmpdir/test.py"
> +timeout 60 "$DTRACE_PYTHON" "$tmpdir/test.py"
> --
> 2.43.5
>
^ permalink raw reply [flat|nested] 18+ messages in thread