Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
ignore:
# Optional: Official actions have moving tags like v1;
# if you use those, you don't need updates.
- dependency-name: "actions/*"
interval: "monthly"
groups:
github-actions:
patterns:
- "*"
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
# also add an aarch64 test, just one python version
include:
- os: ubuntu-24.04-arm
python-version: "3.13"
python-version: "3.14"
fail-fast: false

steps:
Expand Down
10 changes: 1 addition & 9 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ on:

jobs:
build_wheels:
name: Build wheels on ${{ matrix.archs }} for ${{ matrix.os }}
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand All @@ -27,10 +27,6 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up QEMU
if: ${{ matrix.archs == 'aarch64' }}
uses: docker/setup-qemu-action@v4

- uses: actions/setup-python@v5
name: Install Python
with:
Expand All @@ -57,10 +53,6 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up QEMU
if: ${{ matrix.archs == 'aarch64' }}
uses: docker/setup-qemu-action@v4

- uses: actions/setup-python@v5
name: Install Python
with:
Expand Down
21 changes: 8 additions & 13 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/pycqa/isort
rev: 5.13.2
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.0
hooks:
- id: isort
name: isort (python)

- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
language_version: python3
- id: ruff-check
args: [--fix]
- id: ruff-format

- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.3
hooks:
- id: codespell
exclude: "\\.(json)$|docs/_static/preview"
args:
- --ignore-words-list=vas

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.403
rev: v1.1.411
hooks:
- id: pyright
additional_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion metrics/overhead.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@ def test_func_template():
for name, time in graph_data:
chars = int((time / max_time) * GRAPH_WIDTH)
spaces = GRAPH_WIDTH - chars
print(f'{name:15} {"█" * chars}{" " * spaces} {time:.3f}s')
print(f"{name:15} {'█' * chars}{' ' * spaces} {time:.3f}s")
print()
8 changes: 7 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
nox.options.default_venv_backend = "uv|virtualenv"


@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"])
@nox.session()
def lint(session):
session.install("prek")
session.run("prek", "run", "--all-files")


@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"])
def test(session):
session.env["UV_PRERELEASE"] = "allow"
session.install("-e", ".[test]", "setuptools")
Expand Down
6 changes: 3 additions & 3 deletions pyinstrument/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ def self_check(self, recursive: bool = True) -> None:
return

calculated_time = sum(child.time for child in self.children) + self.absorbed_time
assert math.isclose(
calculated_time, self.time
), f"Frame time mismatch, should be {calculated_time}, was {self.time}, {self.children}"
assert math.isclose(calculated_time, self.time), (
f"Frame time mismatch, should be {calculated_time}, was {self.time}, {self.children}"
)

if recursive:
for child in self.children:
Expand Down
94 changes: 72 additions & 22 deletions pyinstrument/low_level/stat_profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ static void ProfilerState_SetTarget(ProfilerState *self, PyObject *target) {
static int ProfilerState_UpdateContextVar(ProfilerState *self) {
PyObject *old = self->last_context_var_value;
PyObject *new = NULL;
int status = PyContextVar_Get(self->context_var, NULL, &new);
int status = PyContextVar_Get(self->context_var, Py_None, &new);
if (status == -1) {
PyErr_SetString(PyExc_Exception, "failed to get value of the context var");
return 0;
Expand Down Expand Up @@ -96,6 +96,7 @@ static double ProfilerState_GetTime(ProfilerState *self) {

if (!PyFloat_Check(result)) {
PyErr_SetString(PyExc_RuntimeError, "custom time function must return a float");
Py_DECREF(result);
return -1.0;
}

Expand Down Expand Up @@ -286,19 +287,45 @@ local_names_from_code(PyCodeObject *code)
#endif
}

/**
* Returns a new reference to the qualified name of a type.
*/
static PyObject *
_get_type_qualname(PyTypeObject *type) {
#if PY_VERSION_HEX >= 0x030b0000 // Python 3.11.0
PyObject *qualname = PyType_GetQualName(type);
#else
PyObject *qualname = PyObject_GetAttrString((PyObject *)type, "__qualname__");
#endif
if (qualname == NULL) {
return NULL;
}
if (!PyUnicode_Check(qualname)) {
Py_DECREF(qualname);
PyErr_SetString(PyExc_TypeError, "type __qualname__ must be a string");
return NULL;
}
return qualname;
}

#if PY_VERSION_HEX >= 0x030b0000 // Python 3.11.0
/**
* Returns a C-string containing the name of the class in the frame. The
* memory belongs to the type object, so it should not be freed.
* Returns a new reference to the qualified name of the class in the frame,
* or NULL when the frame does not represent a method.
*/
static const char *
static PyObject *
_get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
PyObject *localsNames = PyCode_GetVarnames(code);

if (localsNames == NULL) {
return NULL;
}

if (PyTuple_GET_SIZE(localsNames) == 0) {
Py_DECREF(localsNames);
return NULL;
}

PyObject *firstArgName = PyTuple_GET_ITEM(localsNames, 0);

if (firstArgName == NULL) {
Expand All @@ -317,12 +344,12 @@ _get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
return NULL;
}

const char *result = NULL;
PyObject *result = NULL;

PyObject *locals = PyFrame_GetLocals(frame);

if (!PyMapping_Check(locals)) {
Py_DECREF(locals);
if (locals == NULL || !PyMapping_Check(locals)) {
Py_XDECREF(locals);
return NULL;
}

Expand All @@ -337,7 +364,7 @@ _get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
return NULL;
}

result = _PyType_Name(self->ob_type);
result = _get_type_qualname(Py_TYPE(self));
Py_DECREF(self);
}
else if (has_cls && PyMapping_HasKey(locals, CLS_STRING)) {
Expand All @@ -350,8 +377,7 @@ _get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
}

if (PyType_Check(cls)) {
PyTypeObject *type = (PyTypeObject *)cls;
result = _PyType_Name(type);
result = _get_type_qualname((PyTypeObject *)cls);
}
Py_DECREF(cls);
}
Expand Down Expand Up @@ -393,7 +419,7 @@ _get_first_arg_from_cell_variables(PyFrameObject *frame, PyCodeObject *code) {
return NULL;
}

static const char *
static PyObject *
_get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
// This code looks only at the first 'fast' frame local.
//
Expand Down Expand Up @@ -441,14 +467,12 @@ _get_class_name_of_frame(PyFrameObject *frame, PyCodeObject *code) {
}

if (first_var_is_self) {
PyTypeObject *type = first_var->ob_type;
return _PyType_Name(type);
return _get_type_qualname(Py_TYPE(first_var));
} else if (first_var_is_cls) {
if (!PyType_Check(first_var)) {
return NULL;
}
PyTypeObject *type = (PyTypeObject *)first_var;
return _PyType_Name(type);
return _get_type_qualname((PyTypeObject *)first_var);
} else {
Py_FatalError("unreachable code");
}
Expand Down Expand Up @@ -496,21 +520,28 @@ static PyObject *
_get_frame_info(PyFrameObject *frame) {
PyCodeObject *code = code_from_frame(frame);

Comment thread
joerick marked this conversation as resolved.
PyObject *class_name_attribute;
PyObject *class_name_attribute = NULL;
PyObject *line_number_attribute = NULL;
PyObject *frame_hidden_attribute = NULL;

const char *class_name = _get_class_name_of_frame(frame, code);
PyObject *class_name = _get_class_name_of_frame(frame, code);
if (class_name == NULL) {
if (PyErr_Occurred()) {
goto error;
}
class_name_attribute = PyUnicode_New(0, 127); // empty string
} else {
class_name_attribute = PyUnicode_FromFormat(
"%c%c%s",
"%c%c%U",
1, // 0x01 char denotes 'attribute'
'c', // 'c' char denotes 'class name'
class_name
);
Py_DECREF(class_name);
}
if (class_name_attribute == NULL) {
goto error;
}

PyObject *line_number_attribute;

int line_number = PyFrame_GetLineNumber(frame);
if (line_number < 1) {
Expand All @@ -523,8 +554,9 @@ _get_frame_info(PyFrameObject *frame) {
line_number
);
}

PyObject *frame_hidden_attribute;
if (line_number_attribute == NULL) {
goto error;
}

int tracebackhide = _get_tracebackhide(frame, code);
if (tracebackhide <= 0) {
Expand All @@ -537,6 +569,9 @@ _get_frame_info(PyFrameObject *frame) {
'1' // '1' char denotes 'true'
);
}
if (frame_hidden_attribute == NULL) {
goto error;
}

PyObject *result = PyUnicode_FromFormat(
"%U%c%U%c%i%U%U%U",
Expand All @@ -556,6 +591,13 @@ _get_frame_info(PyFrameObject *frame) {
Py_DECREF(frame_hidden_attribute);

return result;

error:
Py_DECREF(code);
Py_XDECREF(class_name_attribute);
Py_XDECREF(line_number_attribute);
Py_XDECREF(frame_hidden_attribute);
return NULL;
}

static int
Expand Down Expand Up @@ -651,6 +693,11 @@ profile(PyObject *op, PyFrameObject *frame, int what, PyObject *arg)

if ((what == WHAT_RETURN) && (code->co_flags & 0x80)) {
PyObject *frame_identifier = _get_frame_info(frame);
if (frame_identifier == NULL) {
Py_DECREF(code);
PyEval_SetProfile(NULL, NULL);
return -1;
}

int status = PyList_Append(pState->await_stack_list, frame_identifier);
Py_DECREF(frame_identifier);
Expand Down Expand Up @@ -769,6 +816,9 @@ setstatprofile(PyObject *m, PyObject *args, PyObject *kwds)

// initialise the last invocation to avoid immediate callback
pState->last_invocation = ProfilerState_GetTime(pState);
if (pState->last_invocation == -1.0 && PyErr_Occurred()) {
goto error;
}

if (context_var) {
Py_INCREF(context_var);
Expand Down
4 changes: 2 additions & 2 deletions pyinstrument/low_level/stat_profile_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(

self.last_invocation = self.get_time()

self.last_context_var_value = context_var.get() if context_var else None
self.last_context_var_value = context_var.get(None) if context_var else None
self.await_stack = []

def __del__(self):
Expand All @@ -61,7 +61,7 @@ def profile(self, frame: types.FrameType, event: str, arg: Any):
now = self.get_time()

if self.context_var:
context_var_value = self.context_var.get()
context_var_value = self.context_var.get(None)
last_context_var_value = self.last_context_var_value

if context_var_value is not last_context_var_value:
Expand Down
Loading
Loading