[PATCH v5 09/11] dtrace: Add a PCP PMDA to expose DTrace data as metrics

Alan Maguire <[email protected]>
Newsgroups dev.linux.lists.dtrace
Message-ID <[email protected]>
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 <[email protected]>
---
 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.