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
16 changes: 8 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,13 @@ jobs:
# unsupported as it most resembles other 1.1.1-work-a-like ssl APIs
# supported by important vendors such as AWS-LC.
- { name: openssl, version: 1.1.1w }
- { name: openssl, version: 3.0.21 }
- { name: openssl, version: 3.4.6 }
- { name: openssl, version: 3.5.7 }
- { name: openssl, version: 3.6.3 }
- { name: openssl, version: 4.0.1 }
- { name: openssl, version: 3.0.22 }
- { name: openssl, version: 3.4.7 }
- { name: openssl, version: 3.5.8 }
- { name: openssl, version: 3.6.4 }
- { name: openssl, version: 4.0.2 }
## AWS-LC
- { name: aws-lc, version: 5.0.0 }
- { name: aws-lc, version: 5.5.0 }
env:
SSLLIB_VER: ${{ matrix.ssllib.version }}
MULTISSL_DIR: ${{ github.workspace }}/multissl
Expand Down Expand Up @@ -379,7 +379,7 @@ jobs:
needs: build-context
if: needs.build-context.outputs.run-ubuntu == 'true'
env:
OPENSSL_VER: 3.5.7
OPENSSL_VER: 3.5.8
PYTHONSTRICTEXTENSIONBUILD: 1
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down Expand Up @@ -487,7 +487,7 @@ jobs:
matrix:
os: [ubuntu-26.04]
env:
OPENSSL_VER: 3.5.7
OPENSSL_VER: 3.5.8
PYTHONSTRICTEXTENSIONBUILD: 1
ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable-san.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ permissions:

env:
FORCE_COLOR: 1
OPENSSL_VER: 3.5.7
OPENSSL_VER: 3.5.8

jobs:
build-san-reusable:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
runs-on: ${{ inputs.os }}
timeout-minutes: 60
env:
OPENSSL_VER: 3.5.7
OPENSSL_VER: 3.5.8
PYTHONSTRICTEXTENSIONBUILD: 1
TERM: linux
steps:
Expand Down
14 changes: 14 additions & 0 deletions Doc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " clean to remove build files"
@echo " venv to create a venv with necessary tools"
@echo " lock to regenerate the pinned dependencies in $(REQUIREMENTS)"
@echo " html to make standalone HTML files"
@echo " gettext to generate POT files"
@echo " htmlview to open the index page built by the html target in your browser"
Expand Down Expand Up @@ -185,6 +186,19 @@ venv:
echo "The venv has been created in the $(VENVDIR) directory"; \
fi

.PHONY: lock
lock:
# Dependencies have a 14 day cooldown period to mitigate supply chain attacks,
# except for sphinx_linklint and python-docs-theme, which are maintained by
# core team members.
uv pip compile requirements.txt \
--exclude-newer P14D \
--exclude-newer-package sphinx_linklint=PT0S \
--exclude-newer-package python-docs-theme=PT0S \
--no-cache --output-file $(REQUIREMENTS) \
--python-version 3.12 --universal \
--custom-compile-command="make lock"

.PHONY: dist-no-html
dist-no-html: dist-text dist-epub dist-texinfo

Expand Down
2 changes: 1 addition & 1 deletion Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

# Skip if downstream redistributors haven't installed them
_OPTIONAL_EXTENSIONS = (
'linklint.ext',
'sphinx_linklint.ext',
'notfound.extension',
'sphinxext.opengraph',
'sphinxcontrib.rsvgconverter',
Expand Down
24 changes: 12 additions & 12 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4577,16 +4577,17 @@ Naturally, they are all only available on Linux.
- :const:`time.CLOCK_BOOTTIME` (Since Linux 3.15 for timerfd_create)

If *clockid* is :const:`time.CLOCK_REALTIME`, a settable system-wide
real-time clock is used. If system clock is changed, timer setting need
to be updated. To cancel timer when system clock is changed, see
real-time clock is used. If the system clock is changed, the timer setting
needs to be updated. To cancel the timer when the system clock is changed, see
:const:`TFD_TIMER_CANCEL_ON_SET`.

If *clockid* is :const:`time.CLOCK_MONOTONIC`, a non-settable monotonically
increasing clock is used. Even if the system clock is changed, the timer
setting will not be affected.

If *clockid* is :const:`time.CLOCK_BOOTTIME`, same as :const:`time.CLOCK_MONOTONIC`
except it includes any time that the system is suspended.
If *clockid* is :const:`time.CLOCK_BOOTTIME`, it is the same as
:const:`time.CLOCK_MONOTONIC` except it includes any time that the system
is suspended.

The file descriptor's behaviour can be modified by specifying a *flags* value.
Any of the following variables may be used, combined using bitwise OR
Expand All @@ -4597,8 +4598,8 @@ Naturally, they are all only available on Linux.

If :const:`TFD_NONBLOCK` is not set as a flag, :func:`read` blocks until
the timer expires. If it is set as a flag, :func:`read` doesn't block, but
If there hasn't been an expiration since the last call to read,
:func:`read` raises :class:`OSError` with ``errno`` is set to
if there hasn't been an expiration since the last call to read,
:func:`read` raises :class:`OSError` with ``errno`` set to
:const:`errno.EAGAIN`.

:const:`TFD_CLOEXEC` is always set by Python automatically.
Expand All @@ -4613,7 +4614,7 @@ Naturally, they are all only available on Linux.
.. versionadded:: 3.13


.. function:: timerfd_settime(fd, /, *, flags=flags, initial=0.0, interval=0.0)
.. function:: timerfd_settime(fd, /, *, flags=0, initial=0.0, interval=0.0)

Alter a timer file descriptor's internal timer.
This function operates the same interval timer as :func:`timerfd_settime_ns`.
Expand All @@ -4628,12 +4629,11 @@ Naturally, they are all only available on Linux.
- :const:`TFD_TIMER_CANCEL_ON_SET`

The timer is disabled by setting *initial* to zero (``0``).
If *initial* is equal to or greater than zero, the timer is enabled.
If *initial* is greater than zero, the timer is enabled.
If *initial* is less than zero, it raises an :class:`OSError` exception
with ``errno`` set to :const:`errno.EINVAL`
with ``errno`` set to :const:`errno.EINVAL`.

By default the timer will fire when *initial* seconds have elapsed.
(If *initial* is zero, timer will fire immediately.)

However, if the :const:`TFD_TIMER_ABSTIME` flag is set,
the timer will fire when the timer's clock
Expand All @@ -4644,13 +4644,13 @@ Naturally, they are all only available on Linux.
If *interval* is greater than zero, the timer fires every time *interval*
seconds have elapsed since the previous expiration.
If *interval* is less than zero, it raises :class:`OSError` with ``errno``
set to :const:`errno.EINVAL`
set to :const:`errno.EINVAL`.

If the :const:`TFD_TIMER_CANCEL_ON_SET` flag is set along with
:const:`TFD_TIMER_ABSTIME` and the clock for this timer is
:const:`time.CLOCK_REALTIME`, the timer is marked as cancelable if the
real-time clock is changed discontinuously. Reading the descriptor is
aborted with the error ECANCELED.
aborted with the error :const:`errno.ECANCELED`.

Linux manages system clock as UTC. A daylight-savings time transition is
done by changing time offset only and doesn't cause discontinuous system
Expand Down
14 changes: 7 additions & 7 deletions Doc/pylock.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile Doc/requirements.txt --exclude-newer P14D --exclude-newer-package linklint=PT0S --exclude-newer-package python-docs-theme=PT0S --exclude-newer-package pygments=PT0S --no-cache --output-file Doc/pylock.toml --python-version 3.12 --universal
# make lock
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.12"
Expand Down Expand Up @@ -119,12 +119,6 @@ version = "3.1.6"
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", upload-time = 2025-03-05T20:05:02Z, size = 245115, hashes = { sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", upload-time = 2025-03-05T20:05:00Z, size = 134899, hashes = { sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" } }]

[[packages]]
name = "linklint"
version = "1.0.1"
sdist = { url = "https://files.pythonhosted.org/packages/62/22/c65a97b2192c3c317c472058bf52d7ca88e421998fa8d5c183b2280ab5d4/linklint-1.0.1.tar.gz", upload-time = 2026-06-09T12:30:47Z, size = 22412, hashes = { sha256 = "10201b4366edecfbeb3281883aff857dbe77d40bb56f16f212d372f597342d5f" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/7b/d8/b2f1708f55f4dd5358b48d28be07ff8e0dadd6d23e41ead2ed1424e90256/linklint-1.0.1-py3-none-any.whl", upload-time = 2026-06-09T12:30:46Z, size = 12828, hashes = { sha256 = "c931f110482f1d17808aa2df78b116adc44d7a14e9e79922c57bf9a7cba40a2d" } }]

[[packages]]
name = "markupsafe"
version = "2.1.5"
Expand Down Expand Up @@ -190,6 +184,12 @@ version = "8.2.3"
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", upload-time = 2025-03-02T22:31:59Z, size = 8321876, hashes = { sha256 = "398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", upload-time = 2025-03-02T22:31:56Z, size = 3589741, hashes = { sha256 = "4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3" } }]

[[packages]]
name = "sphinx-linklint"
version = "2.0.0"
sdist = { url = "https://files.pythonhosted.org/packages/f6/99/f9947b30fd11e782855a13de1aac7d08634edba1c193a0ee01bf5338531c/sphinx_linklint-2.0.0.tar.gz", upload-time = 2026-08-25T12:47:33Z, size = 23297, hashes = { sha256 = "e7ea4d3b1bd83665e9c2ab9fd92c6c645c805956d2e8ca6b0f528f3a9638910d" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/d7/3d/6b96af48e139357e1e041a710d943c4bb79ad149c7aab074df784b4395ec/sphinx_linklint-2.0.0-py3-none-any.whl", upload-time = 2026-08-25T12:47:32Z, size = 13390, hashes = { sha256 = "12aa64f3b3faaa6e8a979a7d1f2c38617866652b670e3d19fa0f60a6ce3657ef" } }]

[[packages]]
name = "sphinx-notfound-page"
version = "1.0.4"
Expand Down
5 changes: 2 additions & 3 deletions Doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ pygments>=2.21

blurb

sphinxext-opengraph~=0.13.0
sphinx-linklint
sphinx-notfound-page~=1.0.0
sphinxext-opengraph~=0.13.0

# The theme used by the documentation is stored separately, so we need
# to install that as well.
python-docs-theme>=2023.3.1,!=2023.7

linklint

-c constraints.txt
4 changes: 2 additions & 2 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -955,8 +955,8 @@ function populateProfileSummary(data) {
: (stats.total_samples ?? data.value ?? 0);
const duration = stats.duration_sec || 0;
const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0);
const errorRate = stats.error_rate || 0;
const missedSamples= stats.missed_samples || 0;
const errorRate = stats.error_rate;
const missedSamples = stats.missed_samples;

const samplesEl = document.getElementById('stat-total-samples');
if (samplesEl) samplesEl.textContent = formatNumber(totalSamples);
Expand Down
70 changes: 60 additions & 10 deletions Lib/profiling/sampling/stack_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def _get_module_name(self, filename, path_info):
self._module_cache[filename] = module_name
return module_name

def _convert_to_flamegraph_format(self):
def _convert_to_flamegraph_format(self, *, min_samples=None):
if self._total_samples == 0:
return {
"name": self._string_table.intern("No Data"),
Expand Down Expand Up @@ -302,7 +302,8 @@ def convert_children(children, min_samples, path_info):

# Filter out very small functions (less than 0.1% of total samples)
total_samples = self._total_samples
min_samples = max(1, int(total_samples * 0.001))
if min_samples is None:
min_samples = max(1, int(total_samples * 0.001))
path_info = get_python_path_info()

root_children = convert_children(self._root["children"], min_samples, path_info)
Expand Down Expand Up @@ -660,9 +661,33 @@ def _add_diff_data_to_node(self, node, path, current_stats, baseline_stats, scal
current_data = current_stats.get(path_key, {"total": 0, "self": 0})
baseline_data = baseline_stats.get(path_key, {"total": 0, "self": 0})

current_self = current_data["self"]
baseline_self = baseline_data["self"] * scale
baseline_total = baseline_data["total"] * scale
current_self = node.get("self", 0)
current_total = node.get("value", 0)

current_nonself = current_total - current_self
aggregate_nonself = current_data["total"] - current_data["self"]

# Allocate self and descendant samples separately. Line-number
# changes can split one function path into several rendered nodes,
# and using independent weights for self and inclusive totals could
# otherwise assign a node more self samples than total samples.
self_weight = self._sample_weight(
current_self,
current_data["self"],
current_total,
current_data["total"],
)
nonself_weight = self._sample_weight(
current_nonself,
aggregate_nonself,
current_total,
current_data["total"],
)
baseline_self = baseline_data["self"] * scale * self_weight
baseline_nonself = (
baseline_data["total"] - baseline_data["self"]
) * scale * nonself_weight
baseline_total = baseline_self + baseline_nonself

diff = current_self - baseline_self
if baseline_self > 0:
Expand All @@ -682,6 +707,14 @@ def _add_diff_data_to_node(self, node, path, current_stats, baseline_stats, scal
for child in node["children"]:
self._add_diff_data_to_node(child, path_key, current_stats, baseline_stats, scale)

@staticmethod
def _sample_weight(value, aggregate, fallback_value, fallback_aggregate):
if aggregate > 0:
return value / aggregate
if fallback_aggregate > 0:
return fallback_value / fallback_aggregate
return 0

def _is_promoted_root(self, data):
"""Check if the data represents a promoted root node."""
return "filename" in data and "funcname" in data
Expand All @@ -690,7 +723,15 @@ def _add_elided_flamegraph(self, current_flamegraph, current_stats, baseline_sta
"""Calculate elided paths and add elided flamegraph to stats."""
self._elided_paths = baseline_stats.keys() - current_stats.keys()

current_flamegraph["stats"]["elided_count"] = len(self._elided_paths)
# A sampled stack can end at an internal path that also has elided
# descendants. Count every disappeared path with self samples, not
# just the leaves of the elided path tree.
elided_stacks = {
path
for path in self._elided_paths
if baseline_stats[path]["self"] > 0
}
current_flamegraph["stats"]["elided_count"] = len(elided_stacks)

if self._elided_paths:
elided_flamegraph = self._build_elided_flamegraph(baseline_stats, scale)
Expand All @@ -713,7 +754,9 @@ def _build_elided_flamegraph(self, baseline_stats, scale):
orig_get_source = self._baseline_collector._get_source_lines
self._baseline_collector._get_source_lines = lambda func: None
try:
baseline_data = self._baseline_collector._convert_to_flamegraph_format()
baseline_data = self._baseline_collector._convert_to_flamegraph_format(
min_samples=1
)
finally:
self._baseline_collector._get_source_lines = orig_get_source

Expand Down Expand Up @@ -758,6 +801,9 @@ def _extract_elided_nodes(self, node, path):
# elided nodes keep their original value to preserve self-samples
if elided_children and not is_elided:
node["value"] = total_value
node["self"] = 0
node.pop("opcodes", None)
node.pop("thread_opcodes", None)

# Keep this node if it's elided or has elided descendants
return is_elided or bool(node.get("children"))
Expand All @@ -773,9 +819,13 @@ def _add_elided_metadata(self, node, baseline_stats, scale, path):
baseline_self = 0
baseline_total = 0
if func_key and current_path in baseline_stats:
baseline_data = baseline_stats[current_path]
baseline_self = baseline_data["self"] * scale
baseline_total = baseline_data["total"] * scale
baseline_total = node.get("value", 0) * scale

# Matched nodes are retained only as structural ancestors. Their
# own samples are still present in the current profile and must
# not be reported as disappeared.
if current_path in self._elided_paths:
baseline_self = node.get("self", 0) * scale

node["baseline"] = baseline_self
node["baseline_total"] = baseline_total
Expand Down
Loading
Loading