Skip to content

Commit a2f6d42

Browse files
generatedunixname1734921407115435meta-codesync[bot]
authored andcommitted
Sync pre-release CPython 3.15 branch from GitHub (2026-08-10)
Summary: Imported python/cpython `3.15.0rc1+dev` from upstream rev [`2396815`](https://www.github.com/python/cpython/commit/2396815da3ed2993cac5f13c59f749b3057ae83f) (committed 2026-08-10 17:04:58+00:00). # Commit Info - Base: (`3.15.0rc1+dev`) - [`a1ab3b0`](https://www.github.com/python/cpython/commit/a1ab3b0024d1d08eca35d63454a678c9a11b29a0) (commit date: 2026-08-08 04:34:08+00:00) - Imported: (`3.15.0rc1+dev`) - [`2396815`](https://www.github.com/python/cpython/commit/2396815da3ed2993cac5f13c59f749b3057ae83f) (commit date: 2026-08-10 17:04:58+00:00) # Noteworthy file changes - Low-signal files (2 added) (NEWS.d, docs, .github) Complete list of added/removed files: https://www.internalfb.com/intern/everpaste/?color=0&handle=GMUkRBnpG1-ZPjYGACEhiWH6DjgHbr0LAAAz Differential Revision: D115531006 fbshipit-source-id: 779bb9a26d0a5ee9b05c0c73b1ac3aa323f2d034
1 parent 0446b64 commit a2f6d42

6 files changed

Lines changed: 242 additions & 34 deletions

File tree

.github/workflows/build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ jobs:
308308
os: ${{ matrix.os }}
309309
test-opts: ${{ matrix.test-opts || '' }}
310310

311+
build-ubuntu-installed:
312+
name: 'Ubuntu (installed)'
313+
needs: build-context
314+
if: needs.build-context.outputs.run-ubuntu == 'true'
315+
uses: ./.github/workflows/reusable-install.yml
316+
311317
build-ubuntu-ssltests:
312318
name: 'Ubuntu SSL tests'
313319
runs-on: ${{ matrix.os }}
@@ -700,6 +706,7 @@ jobs:
700706
- build-windows-msi
701707
- build-macos
702708
- build-ubuntu
709+
- build-ubuntu-installed
703710
- build-ubuntu-ssltests
704711
- build-ios
705712
- build-emscripten
@@ -750,6 +757,7 @@ jobs:
750757
!fromJSON(needs.build-context.outputs.run-ubuntu)
751758
&& '
752759
build-ubuntu,
760+
build-ubuntu-installed,
753761
build-ubuntu-ssltests,
754762
test-hypothesis,
755763
build-asan,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Reusable Ubuntu (installed)
2+
3+
on:
4+
workflow_call:
5+
6+
permissions:
7+
contents: read
8+
9+
env:
10+
FORCE_COLOR: 1
11+
12+
jobs:
13+
build-install-test:
14+
name: build, install and test
15+
runs-on: ubuntu-26.04-arm
16+
timeout-minutes: 60
17+
env:
18+
PYTHONSTRICTEXTENSIONBUILD: 1
19+
steps:
20+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21+
with:
22+
persist-credentials: false
23+
- name: Register gcc problem matcher
24+
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
25+
- name: Set install dir
26+
run:
27+
echo "INSTALL_DIR=$(realpath "${GITHUB_WORKSPACE}/../installed-python")" >> "$GITHUB_ENV"
28+
- name: Install dependencies
29+
run: sudo ./.github/workflows/posix-deps-apt.sh
30+
- name: Configure CPython
31+
run: ./configure --config-cache --prefix="$INSTALL_DIR"
32+
- name: Build CPython
33+
run: make -j
34+
- name: Install CPython
35+
run: make install
36+
- name: Set installed interpreter
37+
run: |
38+
ldversion=$(./python -c 'import sysconfig; print(sysconfig.get_config_var("LDVERSION"))')
39+
echo "INSTALLED_PYTHON=${INSTALL_DIR}/bin/python${ldversion}" >> "$GITHUB_ENV"
40+
- name: Display build info
41+
run: |
42+
"$INSTALLED_PYTHON" -m test.pythoninfo
43+
- name: Test the installed Python
44+
run: xvfb-run "$INSTALLED_PYTHON" -m test --fast-ci --timeout=900

Lib/profiling/sampling/_flamegraph_assets/flamegraph.js

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,23 @@ function getDisplayName(moduleName, filename) {
9999
return filename;
100100
}
101101

102-
function selectFlamegraphData() {
103-
const baseData = isShowingElided ? elidedFlamegraphData : normalData;
102+
function selectFlamegraphData(selectedThreadId = null) {
103+
let baseData = isShowingElided ? elidedFlamegraphData : normalData;
104+
105+
if (selectedThreadId !== null) {
106+
baseData = filterDataByThread(baseData, selectedThreadId);
107+
}
104108

105109
if (!isInverted) {
106110
return baseData;
107111
}
108112

113+
// Thread-filtered trees have different values, so invert them after filtering
114+
// instead of using the cached all-thread tree.
115+
if (selectedThreadId !== null) {
116+
return generateInvertedFlamegraph(baseData);
117+
}
118+
109119
if (isShowingElided) {
110120
if (!invertedElidedData) {
111121
invertedElidedData = generateInvertedFlamegraph(baseData);
@@ -120,12 +130,11 @@ function selectFlamegraphData() {
120130
}
121131

122132
function updateFlamegraphView() {
123-
const selectedData = selectFlamegraphData();
124133
const selectedThreadId = currentThreadFilter !== 'all' ? parseInt(currentThreadFilter, 10) : null;
125-
const filteredData = selectedThreadId !== null ? filterDataByThread(selectedData, selectedThreadId) : selectedData;
126-
const tooltip = createPythonTooltip(filteredData);
127-
const chart = createFlamegraph(tooltip, filteredData.value, filteredData);
128-
renderFlamegraph(chart, filteredData);
134+
const selectedData = selectFlamegraphData(selectedThreadId);
135+
const tooltip = createPythonTooltip(selectedData);
136+
const chart = createFlamegraph(tooltip, selectedData.value, selectedData);
137+
renderFlamegraph(chart, selectedData);
129138
populateThreadStats(selectedData, selectedThreadId);
130139
}
131140

@@ -937,7 +946,9 @@ function formatDuration(seconds) {
937946

938947
function populateProfileSummary(data) {
939948
const stats = data.stats || {};
940-
const totalSamples = stats.total_samples || data.value || 0;
949+
const totalSamples = currentThreadFilter !== 'all'
950+
? (data.value ?? 0)
951+
: (stats.total_samples ?? data.value ?? 0);
941952
const duration = stats.duration_sec || 0;
942953
const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0);
943954
const errorRate = stats.error_rate || 0;
@@ -1209,7 +1220,7 @@ function initThreadFilter(data) {
12091220
const threadFilter = document.getElementById('thread-filter');
12101221
const threadSection = document.getElementById('thread-section');
12111222

1212-
if (!threadFilter || !data.threads) return;
1223+
if (!threadFilter || !data.threads || data.stats?.is_differential) return;
12131224

12141225
threadFilter.innerHTML = '<option value="all">All Threads</option>';
12151226

@@ -1238,11 +1249,23 @@ function filterByThread() {
12381249

12391250
function filterDataByThread(data, threadId) {
12401251
function filterNode(node) {
1241-
if (!node.threads || !node.threads.includes(threadId)) {
1252+
const threadValues = node.thread_values?.[threadId];
1253+
if (!threadValues) {
12421254
return null;
12431255
}
12441256

1245-
const filteredNode = { ...node, children: [] };
1257+
const {
1258+
thread_values: _threadValues,
1259+
thread_opcodes: threadOpcodes,
1260+
...sharedNode
1261+
} = node;
1262+
const filteredNode = {
1263+
...sharedNode,
1264+
value: threadValues[0],
1265+
self: threadValues[1],
1266+
opcodes: threadOpcodes?.[threadId] ?? {},
1267+
children: []
1268+
};
12461269

12471270
if (node.children && Array.isArray(node.children)) {
12481271
filteredNode.children = node.children
@@ -1253,25 +1276,7 @@ function filterDataByThread(data, threadId) {
12531276
return filteredNode;
12541277
}
12551278

1256-
function recalculateValue(node) {
1257-
if (!node.children || node.children.length === 0) {
1258-
return node.value || 0;
1259-
}
1260-
const childrenValue = node.children.reduce((sum, child) => sum + recalculateValue(child), 0);
1261-
node.value = Math.max(node.value || 0, childrenValue);
1262-
return node.value;
1263-
}
1264-
1265-
const filteredRoot = { ...data, children: [] };
1266-
1267-
if (data.children && Array.isArray(data.children)) {
1268-
filteredRoot.children = data.children
1269-
.map(child => filterNode(child))
1270-
.filter(child => child !== null);
1271-
}
1272-
1273-
recalculateValue(filteredRoot);
1274-
return filteredRoot;
1279+
return filterNode(data);
12751280
}
12761281

12771282
// ============================================================================

Lib/profiling/sampling/stack_collector.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ class FlamegraphCollector(StackTraceCollector):
7171
def __init__(self, *args, **kwargs):
7272
super().__init__(*args, **kwargs)
7373
self.stats = {}
74-
self._root = {"samples": 0, "children": {}, "threads": set()}
74+
self._root = {
75+
"samples": 0,
76+
"children": {},
77+
"threads": set(),
78+
"thread_samples": collections.Counter(),
79+
"thread_self": collections.Counter(),
80+
}
7581
self._total_samples = 0
7682
self._sample_count = 0 # Track actual number of samples (not thread traces)
7783
self._func_intern = {}
@@ -220,7 +226,18 @@ def convert_children(children, min_samples, path_info):
220226
out = []
221227
for func, node in children.items():
222228
samples = node["samples"]
223-
if samples < min_samples:
229+
significant_for_thread = any(
230+
thread_samples >= max(
231+
1,
232+
int(
233+
self._root["thread_samples"][thread_id]
234+
* 0.001
235+
),
236+
)
237+
for thread_id, thread_samples
238+
in node["thread_samples"].items()
239+
)
240+
if samples < min_samples and not significant_for_thread:
224241
continue
225242

226243
# Intern all string components for maximum efficiency
@@ -243,6 +260,15 @@ def convert_children(children, min_samples, path_info):
243260
"lineno": func[1],
244261
"funcname": funcname_idx,
245262
"threads": sorted(list(node.get("threads", set()))),
263+
"thread_values": {
264+
thread_id: [
265+
samples,
266+
node["thread_self"].get(thread_id, 0),
267+
]
268+
for thread_id, samples in sorted(
269+
node["thread_samples"].items()
270+
)
271+
},
246272
}
247273

248274
source = self._get_source_lines(func)
@@ -255,6 +281,14 @@ def convert_children(children, min_samples, path_info):
255281
opcodes = node.get("opcodes", {})
256282
if opcodes:
257283
child_entry["opcodes"] = dict(opcodes)
284+
thread_opcodes = node.get("thread_opcodes")
285+
if thread_opcodes:
286+
child_entry["thread_opcodes"] = {
287+
thread_id: dict(counts)
288+
for thread_id, counts in sorted(
289+
thread_opcodes.items()
290+
)
291+
}
258292

259293
# Recurse
260294
child_entry["children"] = convert_children(
@@ -311,7 +345,25 @@ def convert_children(children, min_samples, path_info):
311345
opcode_mapping = get_opcode_mapping()
312346

313347
# If we only have one root child, make it the root to avoid redundant level
314-
if len(root_children) == 1:
348+
root_thread_values = {
349+
thread_id: [samples, 0]
350+
for thread_id, samples in sorted(
351+
self._root["thread_samples"].items()
352+
)
353+
}
354+
sole_root_covers_profile = (
355+
len(root_children) == 1
356+
and root_children[0]["value"] == total_samples
357+
and {
358+
thread_id: values[0]
359+
for thread_id, values
360+
in root_children[0]["thread_values"].items()
361+
} == {
362+
thread_id: values[0]
363+
for thread_id, values in root_thread_values.items()
364+
}
365+
)
366+
if sole_root_covers_profile:
315367
main_child = root_children[0]
316368
# Update name and label to indicate it's the program root
317369
old_name = self._string_table.get_string(main_child["name"])
@@ -340,6 +392,7 @@ def convert_children(children, min_samples, path_info):
340392
"per_thread_stats": per_thread_stats_with_pct
341393
},
342394
"threads": sorted(list(self._all_threads)),
395+
"thread_values": root_thread_values,
343396
"strings": self._string_table.get_strings(),
344397
"opcode_mapping": opcode_mapping
345398
}
@@ -356,6 +409,7 @@ def process_frames(self, frames, thread_id, weight=1):
356409
"""
357410
# Reverse to root->leaf order for tree building
358411
self._root["samples"] += weight
412+
self._root["thread_samples"][thread_id] += weight
359413
self._total_samples += weight
360414
self._root["threads"].add(thread_id)
361415
self._all_threads.add(thread_id)
@@ -368,18 +422,32 @@ def process_frames(self, frames, thread_id, weight=1):
368422

369423
node = current["children"].get(func)
370424
if node is None:
371-
node = {"samples": 0, "children": {}, "threads": set(), "opcodes": collections.Counter(), "self": 0}
425+
node = {
426+
"samples": 0,
427+
"children": {},
428+
"threads": set(),
429+
"thread_samples": collections.Counter(),
430+
"thread_self": collections.Counter(),
431+
"opcodes": collections.Counter(),
432+
"self": 0,
433+
}
372434
current["children"][func] = node
373435
node["samples"] += weight
436+
node["thread_samples"][thread_id] += weight
374437
node["threads"].add(thread_id)
375438

376439
if opcode is not None:
377440
node["opcodes"][opcode] += weight
441+
thread_opcodes = node.setdefault("thread_opcodes", {})
442+
thread_opcodes.setdefault(
443+
thread_id, collections.Counter()
444+
)[opcode] += weight
378445

379446
current = node
380447

381448
if current is not self._root:
382449
current["self"] += weight
450+
current["thread_self"][thread_id] += weight
383451

384452
def _get_source_lines(self, func):
385453
filename, lineno, _ = func

0 commit comments

Comments
 (0)