Skip to content

Commit e0a0c19

Browse files
generatedunixname1734921407115435meta-codesync[bot]
authored andcommitted
Sync pre-release CPython 3.15 branch from GitHub (2026-05-23)
Summary: Imported python/cpython `3.15.0b1+` from upstream rev [`6b17d1a`](https://www.github.com/python/cpython/commit/6b17d1a783d3c0a9c8a35a94e24a2987807728ef) (committed 2026-05-23 19:27:27+00:00). # Commit Info Base: (`3.15.0b1+`) - [`081187f`](https://www.github.com/python/cpython/commit/081187f169556fb1b2d6a9b96f7b7e509f6ad985) (commit date: 2026-05-22 21:17:51+00:00) Imported: (`3.15.0b1+`) - [`6b17d1a`](https://www.github.com/python/cpython/commit/6b17d1a783d3c0a9c8a35a94e24a2987807728ef) (commit date: 2026-05-23 19:27:27+00:00) # Noteworthy file changes - Test files (1 added) - Low-signal files (3 added, 1 removed) (NEWS.d, docs, .github) Complete list of added/removed files: https://www.internalfb.com/intern/everpaste/?color=0&handle=GCXAlScs9nb4-loLAKsnFTfay4lGbr0LAAAz Differential Revision: D106216755 fbshipit-source-id: 67a13bc4c6172bfa45517af71bc30af0e62f89ea
1 parent a26dfa5 commit e0a0c19

12 files changed

Lines changed: 121 additions & 32 deletions

File tree

Doc/library/threading.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ since it is impossible to detect the termination of alien threads.
515515
This constructor should always be called with keyword arguments. Arguments
516516
are:
517517

518-
*group* should be ``None``; reserved for future extension when a
518+
*group* must be ``None`` as it is reserved for future extension when a
519519
:class:`!ThreadGroup` class is implemented.
520520

521521
*target* is the callable object to be invoked by the :meth:`run` method.

Lib/profiling/sampling/_flamegraph_assets/flamegraph.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ let invertedData = null;
77
let currentThreadFilter = 'all';
88
let isInverted = false;
99
let useModuleNames = true;
10+
let zoomedNodeValue = null;
1011

1112
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
1213
// and automatically switch with theme changes - no JS color arrays needed!
@@ -316,6 +317,7 @@ function createPythonTooltip(data) {
316317
const selfSamples = d.data.self || 0;
317318
const selfMs = (selfSamples / 1000).toFixed(2);
318319
const percentage = ((d.data.value / data.value) * 100).toFixed(2);
320+
const relativePercentage = Math.min(100, ((d.data.value / (zoomedNodeValue ?? data.value)) * 100)).toFixed(2);
319321
const calls = d.data.calls || 0;
320322
const childCount = d.children ? d.children.length : 0;
321323
const source = d.data.source;
@@ -439,6 +441,11 @@ function createPythonTooltip(data) {
439441
<span class="tooltip-stat-label">Percentage:</span>
440442
<span class="tooltip-stat-value accent">${percentage}%</span>
441443
444+
${relativePercentage != percentage && relativePercentage != "100.00" ? `
445+
<span class="tooltip-stat-label">Relative Percentage:</span>
446+
<span class="tooltip-stat-value accent">${relativePercentage}%</span>
447+
` : ''}
448+
442449
${calls > 0 ? `
443450
<span class="tooltip-stat-label">Function Calls:</span>
444451
<span class="tooltip-stat-value">${calls.toLocaleString()}</span>
@@ -620,6 +627,9 @@ function createFlamegraph(tooltip, rootValue, data) {
620627
const percentage = d.data.value / rootValue;
621628
const level = getHeatLevel(percentage);
622629
return heatColors[level];
630+
})
631+
.onClick(function (d) {
632+
zoomedNodeValue = d.data.value;
623633
});
624634

625635
return chart;
@@ -629,6 +639,7 @@ function renderFlamegraph(chart, data) {
629639
d3.select("#chart").datum(data).call(chart);
630640
window.flamegraphChart = chart;
631641
window.flamegraphData = data;
642+
zoomedNodeValue = null;
632643
populateStats(data);
633644
}
634645

@@ -1269,6 +1280,7 @@ function filterDataByThread(data, threadId) {
12691280

12701281
function resetZoom() {
12711282
if (window.flamegraphChart) {
1283+
zoomedNodeValue = null;
12721284
window.flamegraphChart.resetZoom();
12731285
}
12741286
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pickle
2+
import threading
3+
import unittest
4+
5+
from test.support import threading_helper
6+
7+
8+
@threading_helper.requires_working_threading()
9+
class TestPickleFreeThreading(unittest.TestCase):
10+
11+
def test_pickle_dumps_with_concurrent_dict_mutation(self):
12+
# gh-146452: Pickling a dict while another thread mutates it
13+
# used to segfault. batch_dict_exact() iterated dict items via
14+
# PyDict_Next() which returns borrowed references, and a
15+
# concurrent pop/replace could free the value before Py_INCREF
16+
# got to it.
17+
shared = {str(i): list(range(20)) for i in range(50)}
18+
19+
def dumper():
20+
for _ in range(1000):
21+
try:
22+
pickle.dumps(shared)
23+
except RuntimeError:
24+
# "dictionary changed size during iteration" is expected
25+
pass
26+
27+
def mutator():
28+
for j in range(1000):
29+
key = str(j % 50)
30+
shared[key] = list(range(j % 20))
31+
if j % 10 == 0:
32+
shared.pop(key, None)
33+
shared[key] = [j]
34+
35+
threads = []
36+
for _ in range(10):
37+
threads.append(threading.Thread(target=dumper))
38+
threads.append(threading.Thread(target=mutator))
39+
40+
with threading_helper.start_threads(threads):
41+
pass
42+
43+
if __name__ == "__main__":
44+
unittest.main()

Lib/test/test_type_cache.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
""" Tests for the internal type cache in CPython. """
2+
import collections.abc
23
import dis
34
import unittest
45
import warnings
@@ -114,6 +115,25 @@ class HolderSub(Holder):
114115
Holder.set_value()
115116
HolderSub.value
116117

118+
def test_abc_register_invalidates_subclass_versions(self):
119+
class Parent:
120+
pass
121+
122+
class Child(Parent):
123+
pass
124+
125+
type_assign_version(Parent)
126+
type_assign_version(Child)
127+
parent_version = type_get_version(Parent)
128+
child_version = type_get_version(Child)
129+
if parent_version == 0 or child_version == 0:
130+
self.skipTest("Could not assign valid type versions")
131+
132+
collections.abc.Mapping.register(Parent)
133+
134+
self.assertEqual(type_get_version(Parent), 0)
135+
self.assertEqual(type_get_version(Child), 0)
136+
117137
@support.cpython_only
118138
class TypeCacheWithSpecializationTests(unittest.TestCase):
119139
def tearDown(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ``abc.register()`` so it invalidates type version tags for registered classes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix segfault in :mod:`pickle` when pickling a dictionary concurrently
2+
mutated by another thread in the free-threaded build.

Misc/NEWS.d/next/Library/2026-05-18-15-30-34.gh-issue-146452.RM0EVJ.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update the tooltip on the Tachyon flame graph to show both absolute and relative percentages.

Modules/_io/bufferedio.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,9 @@ buffered_iternext(PyObject *op)
15091509
tp == state->PyBufferedRandom_Type)
15101510
{
15111511
/* Skip method call overhead for speed */
1512+
Py_BEGIN_CRITICAL_SECTION(self);
15121513
line = _buffered_readline(self, -1);
1514+
Py_END_CRITICAL_SECTION();
15131515
}
15141516
else {
15151517
line = PyObject_CallMethodNoArgs((PyObject *)self,

Modules/_pickle.c

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3450,12 +3450,9 @@ batch_dict(PickleState *state, PicklerObject *self, PyObject *iter, PyObject *or
34503450
* Returns 0 on success, -1 on error.
34513451
*
34523452
* Note that this currently doesn't work for protocol 0.
3453-
3454-
* gh-146452: Wrap the dict iteration in a critical sections to prevent
3455-
* concurrent mutation from invalidating PyDict_Next() iteration state.
34563453
*/
34573454
static int
3458-
batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
3455+
batch_dict_exact_impl(PickleState *state, PicklerObject *self, PyObject *obj)
34593456
{
34603457
PyObject *key = NULL, *value = NULL;
34613458
int i;
@@ -3469,24 +3466,15 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
34693466
assert(self->proto > 0);
34703467

34713468
dict_size = PyDict_GET_SIZE(obj);
3469+
assert(dict_size);
34723470

34733471
/* Write in batches of BATCHSIZE. */
34743472
Py_ssize_t total = 0;
34753473
do {
34763474
if (dict_size - total == 1) {
3477-
int next;
3478-
Py_BEGIN_CRITICAL_SECTION(obj);
3479-
next = PyDict_Next(obj, &ppos, &key, &value);
3480-
if (next) {
3481-
Py_INCREF(key);
3482-
Py_INCREF(value);
3483-
}
3484-
Py_END_CRITICAL_SECTION();
3485-
if (!next) {
3486-
PyErr_SetString(PyExc_RuntimeError,
3487-
"dictionary changed size during iteration");
3488-
goto error;
3489-
}
3475+
PyDict_Next(obj, &ppos, &key, &value);
3476+
Py_INCREF(key);
3477+
Py_INCREF(value);
34903478
if (save(state, self, key, 0) < 0) {
34913479
goto error;
34923480
}
@@ -3504,18 +3492,9 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
35043492
i = 0;
35053493
if (_Pickler_Write(self, &mark_op, 1) < 0)
35063494
return -1;
3507-
int next;
3508-
while (1) {
3509-
Py_BEGIN_CRITICAL_SECTION(obj);
3510-
next = PyDict_Next(obj, &ppos, &key, &value);
3511-
if (next) {
3512-
Py_INCREF(key);
3513-
Py_INCREF(value);
3514-
}
3515-
Py_END_CRITICAL_SECTION();
3516-
if (!next) {
3517-
break;
3518-
}
3495+
while (PyDict_Next(obj, &ppos, &key, &value)) {
3496+
Py_INCREF(key);
3497+
Py_INCREF(value);
35193498
if (save(state, self, key, 0) < 0) {
35203499
goto error;
35213500
}
@@ -3546,6 +3525,18 @@ batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
35463525
return -1;
35473526
}
35483527

3528+
/* gh-146452: Wrap the dict iteration in a critical section to prevent
3529+
concurrent mutation from invalidating PyDict_Next() iteration state. */
3530+
static int
3531+
batch_dict_exact(PickleState *state, PicklerObject *self, PyObject *obj)
3532+
{
3533+
int ret;
3534+
Py_BEGIN_CRITICAL_SECTION(obj);
3535+
ret = batch_dict_exact_impl(state, self, obj);
3536+
Py_END_CRITICAL_SECTION();
3537+
return ret;
3538+
}
3539+
35493540
static int
35503541
save_dict(PickleState *state, PicklerObject *self, PyObject *obj)
35513542
{

0 commit comments

Comments
 (0)