Skip to content

Commit 82fd0c0

Browse files
authored
Merge branch 'main' into pyrepl-str
2 parents 392d59b + 114de19 commit 82fd0c0

115 files changed

Lines changed: 2253 additions & 2694 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/reusable-san.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
- name: MSan option setup
7373
if: inputs.sanitizer == 'MSan'
7474
run: |
75+
sudo sysctl -w vm.mmap_rnd_bits=28 # Reduce ASLR to avoid MSan re-executing
76+
7577
echo "MSAN_OPTIONS=${SAN_LOG_OPTION} allocator_may_return_null=1 handle_segv=0" >> "$GITHUB_ENV"
7678
# MSan reports false positives for memory initialized by libraries
7779
# that are not built with MSan, so disable modules that use them.

‎Include/internal/pycore_call.h‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@ extern PyObject* _PyObject_Call(
5757
PyObject *args,
5858
PyObject *kwargs);
5959

60-
extern PyObject * _PyObject_CallMethodFormat(
61-
PyThreadState *tstate,
62-
PyObject *callable,
63-
const char *format,
64-
...);
65-
6660
// Export for 'array' shared extension
6761
PyAPI_FUNC(PyObject*) _PyObject_CallMethod(
6862
PyObject *obj,

‎Include/internal/pycore_unicodeobject.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ extern void _PyUnicode_InternStatic(PyInterpreterState *interp, PyObject **);
468468
extern void _PyUnicode_ClearInterned(PyInterpreterState *interp);
469469

470470
// Like PyUnicode_AsUTF8(), but check for embedded null characters.
471-
// Export for '_sqlite3' shared extension.
471+
// Export for '_sqlite3' shared extension, and for Argument Clinic code.
472472
PyAPI_FUNC(const char *) _PyUnicode_AsUTF8NoNUL(PyObject *);
473473

474474

‎Include/pymacro.h‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,23 @@
199199
} while(0)
200200
#endif
201201

202-
/* Get the number of elements in a visible array
203-
204-
This does not work on pointers, or arrays declared as [], or function
205-
parameters. With correct compiler support, such usage will cause a build
206-
error (see Py_BUILD_ASSERT_EXPR).
207-
208-
Written by Rusty Russell, public domain, http://ccodearchive.net/
209-
210-
Requires at GCC 3.1+ */
211-
#if (defined(__GNUC__) && !defined(__STRICT_ANSI__) && \
212-
(((__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)) || (__GNUC__ >= 4)))
213-
/* Two gcc extensions.
214-
&a[0] degrades to a pointer: a different type from an array */
202+
// Get the number of elements in a visible array.
203+
//
204+
// This does not work on pointers, or arrays declared as [], or function
205+
// parameters. With correct compiler support, such usage will cause a build
206+
// error (see Py_BUILD_ASSERT_EXPR).
207+
//
208+
// Written by Rusty Russell, public domain, http://ccodearchive.net/
209+
//
210+
// Require GCC 4 (it works on GCC 3.1).
211+
//
212+
// Two GCC extensions: &a[0] degrades to a pointer, a different type from an
213+
// array.
214+
//
215+
// gh-158810: Do not use __builtin_types_compatible_p() in strict C ANSI mode
216+
// and on C++.
217+
#if (defined(__GNUC__) && __GNUC__ >= 4 \
218+
&& !defined(__STRICT_ANSI__) && !defined(__cplusplus))
215219
#define Py_ARRAY_LENGTH(array) \
216220
(sizeof(array) / sizeof((array)[0]) \
217221
+ Py_BUILD_ASSERT_EXPR(!__builtin_types_compatible_p(typeof(array), \

‎InternalDocs/profiling_binary_format.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ The file consists of five required sections and one optional extension:
4747
| String Table | Variable size
4848
+------------------+ frame_table_offset
4949
| Frame Table | Variable size
50-
+------------------+ file_size - 64 (when stats are present)
51-
| Profile Stats | 32 bytes (optional)
50+
+------------------+ file_size - 88 (when stats are present)
51+
| Profile Stats | 56 bytes (optional)
5252
+------------------+ file_size - 32
5353
| Footer | 32 bytes (fixed)
5454
+------------------+ file_size
@@ -220,6 +220,7 @@ The status byte is a bitfield encoding thread state at sample time:
220220
| 2 | THREAD_STATUS_UNKNOWN | Thread state could not be determined |
221221
| 3 | THREAD_STATUS_GIL_REQUESTED | Thread is waiting to acquire the GIL |
222222
| 4 | THREAD_STATUS_HAS_EXCEPTION | Thread has a pending exception |
223+
| 5 | THREAD_STATUS_MAIN_THREAD | Thread is the interpreter's main thread |
223224

224225
Multiple flags can be set simultaneously (e.g., a thread can hold the GIL
225226
while also running on CPU). Analysis tools use these to filter samples or

‎Lib/profiling/sampling/_sync_coordinator.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ def _execute_script(script_path: str, script_args: List[str], cwd: str) -> None:
168168
if not os.path.isfile(script_path):
169169
raise TargetError(f"Script not found: {script_path}")
170170

171+
script_dir = os.path.dirname(os.path.realpath(script_path))
172+
if script_dir in sys.path:
173+
sys.path.remove(script_dir)
174+
sys.path.insert(0, script_dir)
175+
171176
# Replace sys.argv to match original script call
172177
sys.argv = [script_path] + script_args
173178

‎Lib/profiling/sampling/heatmap_collector.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -783,14 +783,14 @@ def _generate_file_html(self, output_path: Path, filename: str,
783783
line_counts: Dict[int, int], self_counts: Dict[int, int],
784784
file_stat: FileStats):
785785
"""Generate HTML for a single source file with heatmap coloring."""
786-
# Read source file
786+
source_lines = [f"# Source file not available: {filename}"]
787787
try:
788-
source_lines = Path(filename).read_text(encoding='utf-8', errors='replace').splitlines()
789-
except (IOError, OSError) as e:
790-
if not (filename.startswith('<') or filename.startswith('[') or
791-
filename in ('~', '...', '.') or len(filename) < 2):
792-
print(f"Warning: Could not read source file {filename}: {e}")
793-
source_lines = [f"# Source file not available: {filename}"]
788+
path = Path(filename)
789+
if path.is_file():
790+
source_lines = path.read_text(
791+
encoding='utf-8', errors='replace').splitlines()
792+
except (IOError, OSError):
793+
pass
794794

795795
# Generate HTML for each line
796796
max_samples = max(line_counts.values()) if line_counts else 1

‎Lib/profiling/sampling/stack_collector.py‎

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def export(self, filename):
6868
return True
6969

7070

71+
# Allow for tree conversion and the dict/list frames in the Python JSON encoder.
72+
_FLAMEGRAPH_RECURSION_MARGIN = 6000
73+
74+
7175
class FlamegraphCollector(StackTraceCollector):
7276
def __init__(self, *args, **kwargs):
7377
super().__init__(*args, **kwargs)
@@ -166,34 +170,41 @@ def set_mode(self, mode):
166170
self.stats["mode"] = mode
167171

168172
def export(self, filename):
169-
flamegraph_data = self._convert_to_flamegraph_format()
170-
171-
# Debug output with string table statistics
172-
num_functions = len(flamegraph_data.get("children", []))
173-
total_time = flamegraph_data.get("value", 0)
174-
string_count = len(self._string_table)
175-
s1 = "" if num_functions == 1 else "s"
176-
s2 = "" if total_time == 1 else "s"
177-
s3 = "" if string_count == 1 else "s"
178-
print(
179-
f"Flamegraph data: {num_functions} root function{s1}, "
180-
f"{total_time} total sample{s2}, "
181-
f"{string_count} unique string{s3}"
182-
)
183-
184-
if num_functions == 0:
173+
# Converting the call tree recurses to the sampled stack depth.
174+
old_limit = sys.getrecursionlimit()
175+
sys.setrecursionlimit(old_limit + _FLAMEGRAPH_RECURSION_MARGIN)
176+
try:
177+
flamegraph_data = self._convert_to_flamegraph_format()
178+
179+
# Debug output with string table statistics
180+
num_functions = len(flamegraph_data.get("children", []))
181+
total_time = flamegraph_data.get("value", 0)
182+
string_count = len(self._string_table)
183+
s1 = "" if num_functions == 1 else "s"
184+
s2 = "" if total_time == 1 else "s"
185+
s3 = "" if string_count == 1 else "s"
185186
print(
186-
"Warning: No functions found in profiling data. Check if sampling captured any data."
187+
f"Flamegraph data: {num_functions} root function{s1}, "
188+
f"{total_time} total sample{s2}, "
189+
f"{string_count} unique string{s3}"
187190
)
188-
return False
189191

190-
html_content = self._create_flamegraph_html(flamegraph_data)
192+
if num_functions == 0:
193+
print(
194+
"Warning: No functions found in profiling data. "
195+
"Check if sampling captured any data."
196+
)
197+
return False
191198

192-
with open(filename, "w", encoding="utf-8") as f:
193-
f.write(html_content)
199+
html_content = self._create_flamegraph_html(flamegraph_data)
194200

195-
print(f"Flamegraph saved to: {filename}")
196-
return True
201+
with open(filename, "w", encoding="utf-8") as f:
202+
f.write(html_content)
203+
204+
print(f"Flamegraph saved to: {filename}")
205+
return True
206+
finally:
207+
sys.setrecursionlimit(old_limit)
197208

198209
@staticmethod
199210
@functools.lru_cache(maxsize=None)
@@ -487,7 +498,12 @@ def _get_source_lines(self, func):
487498
return None
488499

489500
def _create_flamegraph_html(self, data):
490-
data_json = json.dumps(data)
501+
try:
502+
data_json = json.dumps(data)
503+
except RecursionError:
504+
# The C encoder can exhaust the C stack independently of the
505+
# Python recursion limit. iterencode() uses the Python encoder.
506+
data_json = "".join(json.JSONEncoder().iterencode(data))
491507

492508
template_dir = importlib.resources.files(__package__)
493509
vendor_dir = template_dir / "_vendor"
@@ -665,16 +681,16 @@ def _convert_to_flamegraph_format(self):
665681
current_stats = self._aggregate_path_samples(self._root)
666682
baseline_stats = self._aggregate_path_samples(self._baseline_collector._root)
667683

668-
# Scale baseline values to make them comparable, accounting for both
669-
# sample count differences and sample interval differences.
684+
# Express baseline samples in units of the current sample interval.
685+
# Do not normalize by total profile duration: doing so makes unchanged
686+
# functions appear different when another function becomes faster or
687+
# slower.
670688
baseline_total = self._baseline_collector._total_samples
671-
if baseline_total > 0 and self._total_samples > 0:
672-
current_time = self._total_samples * self.sample_interval_usec
673-
baseline_time = baseline_total * self._baseline_collector.sample_interval_usec
674-
scale = current_time / baseline_time
675-
elif baseline_total > 0:
676-
# Current profile is empty - use interval-based scale for elided display
677-
scale = self.sample_interval_usec / self._baseline_collector.sample_interval_usec
689+
if baseline_total > 0:
690+
scale = (
691+
self._baseline_collector.sample_interval_usec
692+
/ self.sample_interval_usec
693+
)
678694
else:
679695
scale = 1.0
680696

@@ -891,6 +907,10 @@ def _add_elided_metadata(self, node, baseline_stats, scale, path):
891907
else:
892908
node["diff_pct"] = 0.0
893909

910+
# Scale geometry after computing metadata from raw baseline counts.
911+
node["value"] = node.get("value", 0) * scale
912+
node["self"] = node.get("self", 0) * scale
913+
894914
if "children" in node and node["children"]:
895915
for child in node["children"]:
896916
self._add_elided_metadata(child, baseline_stats, scale, current_path)

‎Lib/tempfile.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,15 @@ def _sanitize_params(prefix, suffix, dir):
119119
output_type = _infer_return_type(prefix, suffix, dir)
120120
if suffix is None:
121121
suffix = output_type()
122+
if _os.path.dirname(suffix):
123+
raise ValueError("suffix can't contain a directory component")
122124
if prefix is None:
123125
if output_type is str:
124126
prefix = template
125127
else:
126128
prefix = _os.fsencode(template)
129+
if _os.path.dirname(prefix):
130+
raise ValueError("prefix can't contain a directory component")
127131
if dir is None:
128132
if output_type is str:
129133
dir = gettempdir()

‎Lib/test/test_cext/extension.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ test_macros(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
8383
{
8484
PyObject *obj, *dict;
8585
PyObject *slots[1];
86+
int small_array[] = {2, 5, 7};
8687

8788
// test Py_BUILD_ASSERT() and Py_BUILD_ASSERT_EXPR()
8889
Py_BUILD_ASSERT(sizeof(int) == sizeof(unsigned int));
@@ -134,6 +135,9 @@ test_macros(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
134135
Py_END_CRITICAL_SECTION();
135136
Py_DECREF(dict);
136137

138+
// Test Py_ARRAY_LENGTH()
139+
assert(Py_ARRAY_LENGTH(small_array) == 3);
140+
137141
Py_RETURN_NONE;
138142
}
139143

0 commit comments

Comments
 (0)