Skip to content

Commit 99f4d28

Browse files
authored
Merge branch 'main' into arraylen_typeof
2 parents c1adc06 + dc0add3 commit 99f4d28

103 files changed

Lines changed: 2145 additions & 2609 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

‎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: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def export(self, filename):
6868
return True
6969

7070

71+
# Bounded by the unwinder's maximum captured stack depth (MAX_FRAMES).
72+
_FLAMEGRAPH_RECURSION_MARGIN = 2000
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)
@@ -665,16 +676,16 @@ def _convert_to_flamegraph_format(self):
665676
current_stats = self._aggregate_path_samples(self._root)
666677
baseline_stats = self._aggregate_path_samples(self._baseline_collector._root)
667678

668-
# Scale baseline values to make them comparable, accounting for both
669-
# sample count differences and sample interval differences.
679+
# Express baseline samples in units of the current sample interval.
680+
# Do not normalize by total profile duration: doing so makes unchanged
681+
# functions appear different when another function becomes faster or
682+
# slower.
670683
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
684+
if baseline_total > 0:
685+
scale = (
686+
self._baseline_collector.sample_interval_usec
687+
/ self.sample_interval_usec
688+
)
678689
else:
679690
scale = 1.0
680691

@@ -891,6 +902,10 @@ def _add_elided_metadata(self, node, baseline_stats, scale, path):
891902
else:
892903
node["diff_pct"] = 0.0
893904

905+
# Scale geometry after computing metadata from raw baseline counts.
906+
node["value"] = node.get("value", 0) * scale
907+
node["self"] = node.get("self", 0) * scale
908+
894909
if "children" in node and node["children"]:
895910
for child in node["children"]:
896911
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()

0 commit comments

Comments
 (0)