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
2 changes: 2 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ struct _ts {
/* Pointer to currently executing frame. */
struct _PyInterpreterFrame *current_frame;

struct _PyInterpreterFrame *last_profiled_frame;

Py_tracefunc c_profilefunc;
Py_tracefunc c_tracefunc;
PyObject *c_profileobj;
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_debug_offsets.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ typedef struct _Py_DebugOffsets {
uint64_t next;
uint64_t interp;
uint64_t current_frame;
uint64_t last_profiled_frame;
uint64_t thread_id;
uint64_t native_thread_id;
uint64_t datastack_chunk;
Expand Down Expand Up @@ -272,6 +273,7 @@ typedef struct _Py_DebugOffsets {
.next = offsetof(PyThreadState, next), \
.interp = offsetof(PyThreadState, interp), \
.current_frame = offsetof(PyThreadState, current_frame), \
.last_profiled_frame = offsetof(PyThreadState, last_profiled_frame), \
.thread_id = offsetof(PyThreadState, thread_id), \
.native_thread_id = offsetof(PyThreadState, native_thread_id), \
.datastack_chunk = offsetof(PyThreadState, datastack_chunk), \
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(c_parameter_type)
STRUCT_FOR_ID(c_return)
STRUCT_FOR_ID(c_stack)
STRUCT_FOR_ID(cache_frames)
STRUCT_FOR_ID(cached_datetime_module)
STRUCT_FOR_ID(cached_statements)
STRUCT_FOR_ID(cadata)
Expand Down Expand Up @@ -776,6 +777,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(stacklevel)
STRUCT_FOR_ID(start)
STRUCT_FOR_ID(statement)
STRUCT_FOR_ID(stats)
STRUCT_FOR_ID(status)
STRUCT_FOR_ID(stderr)
STRUCT_FOR_ID(stdin)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions InternalDocs/frames.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ The shim frame points to a special code object containing the `INTERPRETER_EXIT`
instruction which cleans up the shim frame and returns.


### Remote Profiling Frame Cache

The `last_profiled_frame` field in `PyThreadState` supports an optimization for
remote profilers that sample call stacks from external processes. When a remote
profiler reads the call stack, it writes the current frame address to this field.
The eval loop then keeps this pointer valid by updating it to the parent frame
whenever a frame returns (in `_PyEval_FrameClearAndPop`).

This creates a "high-water mark" that always points to a frame still on the stack.
On subsequent samples, the profiler can walk from `current_frame` until it reaches
`last_profiled_frame`, knowing that frames from that point downward are unchanged
and can be retrieved from a cache. This significantly reduces the amount of remote
memory reads needed when call stacks are deep and stable at their base.

The update in `_PyEval_FrameClearAndPop` is guarded: it only writes when
`last_profiled_frame` is non-NULL AND matches the frame being popped. This
prevents transient frames (called and returned between profiler samples) from
corrupting the cache pointer, while avoiding any overhead when profiling is inactive.


### The Instruction Pointer

`_PyInterpreterFrame` has two fields which are used to maintain the instruction
Expand Down
14 changes: 6 additions & 8 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,16 +467,12 @@ def _get_actions_usage_parts_with_split(self, actions, groups, opt_count=None):
# produce all arg strings
elif not action.option_strings:
default = self._get_default_metavar_for_positional(action)
part = (
t.summary_action
+ self._format_args(action, default)
+ t.reset
)

part = self._format_args(action, default)
# if it's in a group, strip the outer []
if action in group_actions:
if part[0] == '[' and part[-1] == ']':
part = part[1:-1]
part = t.summary_action + part + t.reset

# produce the first way to invoke the option in brackets
else:
Expand Down Expand Up @@ -2008,14 +2004,16 @@ def add_subparsers(self, **kwargs):
self._subparsers = self._positionals

# prog defaults to the usage message of this parser, skipping
# optional arguments and with no "usage:" prefix
# non-required optional arguments and with no "usage:" prefix
if kwargs.get('prog') is None:
# Create formatter without color to avoid storing ANSI codes in prog
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(False)
positionals = self._get_positional_actions()
required_optionals = [action for action in self._get_optional_actions()
if action.required]
groups = self._mutually_exclusive_groups
formatter.add_usage(None, positionals, groups, '')
formatter.add_usage(None, required_optionals + positionals, groups, '')
kwargs['prog'] = formatter.format_help().strip()

# create the parsers action and add it to the positionals list
Expand Down
4 changes: 2 additions & 2 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ def _set_state(future, other):

def _call_check_cancel(destination):
if destination.cancelled():
if source_loop is None or source_loop is dest_loop:
if source_loop is None or source_loop is events._get_running_loop():
source.cancel()
else:
source_loop.call_soon_threadsafe(source.cancel)
Expand All @@ -398,7 +398,7 @@ def _call_set_state(source):
if (destination.cancelled() and
dest_loop is not None and dest_loop.is_closed()):
return
if dest_loop is None or dest_loop is source_loop:
if dest_loop is None or dest_loop is events._get_running_loop():
_set_state(destination, source)
else:
if dest_loop.is_closed():
Expand Down
26 changes: 26 additions & 0 deletions Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,32 @@ def _find_lineno(self, obj, source_lines):
if pat.match(source_lines[lineno]):
return lineno

# Handle __test__ string doctests formatted as triple-quoted
# strings. Find a non-blank line in the test string and match it
# in the source, verifying subsequent lines also match to handle
# duplicate lines.
if isinstance(obj, str) and source_lines is not None:
obj_lines = obj.splitlines(keepends=True)
# Skip the first line (may be on same line as opening quotes)
# and any blank lines to find a meaningful line to match.
start_index = 1
while (start_index < len(obj_lines)
and not obj_lines[start_index].strip()):
start_index += 1
if start_index < len(obj_lines):
target_line = obj_lines[start_index]
for lineno, source_line in enumerate(source_lines):
if source_line == target_line:
# Verify subsequent lines also match
for i in range(start_index + 1, len(obj_lines) - 1):
source_idx = lineno + i - start_index
if source_idx >= len(source_lines):
break
if obj_lines[i] != source_lines[source_idx]:
break
else:
return lineno - start_index

# We couldn't find the line number.
return None

Expand Down
3 changes: 3 additions & 0 deletions Lib/email/_header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2792,6 +2792,9 @@ def _steal_trailing_WSP_if_exists(lines):
if lines and lines[-1] and lines[-1][-1] in WSP:
wsp = lines[-1][-1]
lines[-1] = lines[-1][:-1]
# gh-142006: if the line is now empty, remove it entirely.
if not lines[-1]:
lines.pop()
return wsp

def _refold_parse_tree(parse_tree, *, policy):
Expand Down
7 changes: 3 additions & 4 deletions Lib/email/feedparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,10 +504,9 @@ def _parse_headers(self, lines):
self._input.unreadline(line)
return
else:
# Weirdly placed unix-from line. Note this as a defect
# and ignore it.
# Weirdly placed unix-from line.
defect = errors.MisplacedEnvelopeHeaderDefect(line)
self._cur.defects.append(defect)
self.policy.handle_defect(self._cur, defect)
continue
# Split the line on the colon separating field name from value.
# There will always be a colon, because if there wasn't the part of
Expand All @@ -519,7 +518,7 @@ def _parse_headers(self, lines):
# message. Track the error but keep going.
if i == 0:
defect = errors.InvalidHeaderDefect("Missing header name.")
self._cur.defects.append(defect)
self.policy.handle_defect(self._cur, defect)
continue

assert i>0, "_parse_headers fed line with no : and no leading WS"
Expand Down
20 changes: 18 additions & 2 deletions Lib/profiling/sampling/_heatmap_assets/heatmap.css
Original file line number Diff line number Diff line change
Expand Up @@ -1094,18 +1094,34 @@
}

#scroll_marker .marker.cold {
background: var(--heat-1);
}

#scroll_marker .marker.cool {
background: var(--heat-2);
}

#scroll_marker .marker.mild {
background: var(--heat-3);
}

#scroll_marker .marker.warm {
background: var(--heat-5);
background: var(--heat-4);
}

#scroll_marker .marker.hot {
background: var(--heat-5);
}

#scroll_marker .marker.very-hot {
background: var(--heat-6);
}

#scroll_marker .marker.intense {
background: var(--heat-7);
}

#scroll_marker .marker.vhot {
#scroll_marker .marker.extreme {
background: var(--heat-8);
}

Expand Down
49 changes: 19 additions & 30 deletions Lib/profiling/sampling/_heatmap_assets/heatmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function toggleTheme() {
if (btn) {
btn.innerHTML = next === 'dark' ? '&#9788;' : '&#9790;'; // sun or moon
}
applyLineColors();

// Rebuild scroll marker with new theme colors
buildScrollMarker();
Expand Down Expand Up @@ -160,13 +161,6 @@ function getSampleCount(line) {
return parseInt(text) || 0;
}

function getIntensityClass(ratio) {
if (ratio > 0.75) return 'vhot';
if (ratio > 0.5) return 'hot';
if (ratio > 0.25) return 'warm';
return 'cold';
}

// ============================================================================
// Scroll Minimap
// ============================================================================
Expand Down Expand Up @@ -194,7 +188,7 @@ function buildScrollMarker() {

const lineTop = Math.floor(line.offsetTop * markerScale);
const lineNumber = index + 1;
const intensityClass = maxSamples > 0 ? getIntensityClass(samples / maxSamples) : 'cold';
const intensityClass = maxSamples > 0 ? (intensityToClass(samples / maxSamples) || 'cold') : 'cold';

if (lineNumber === prevLine + 1 && lastMark?.classList.contains(intensityClass)) {
lastMark.style.height = `${lineTop + lineHeight - lastTop}px`;
Expand All @@ -212,6 +206,21 @@ function buildScrollMarker() {
document.body.appendChild(scrollMarker);
}

function applyLineColors() {
const lines = document.querySelectorAll('.code-line');
lines.forEach(line => {
let intensity;
if (colorMode === 'self') {
intensity = parseFloat(line.getAttribute('data-self-intensity')) || 0;
} else {
intensity = parseFloat(line.getAttribute('data-cumulative-intensity')) || 0;
}

const color = intensityToColor(intensity);
line.style.background = color;
});
}

// ============================================================================
// Toggle Controls
// ============================================================================
Expand Down Expand Up @@ -264,20 +273,7 @@ function applyHotFilter() {

function toggleColorMode() {
colorMode = colorMode === 'self' ? 'cumulative' : 'self';
const lines = document.querySelectorAll('.code-line');

lines.forEach(line => {
let bgColor;
if (colorMode === 'self') {
bgColor = line.getAttribute('data-self-color');
} else {
bgColor = line.getAttribute('data-cumulative-color');
}

if (bgColor) {
line.style.background = bgColor;
}
});
applyLineColors();

updateToggleUI('toggle-color-mode', colorMode === 'cumulative');

Expand All @@ -295,14 +291,7 @@ function toggleColorMode() {
document.addEventListener('DOMContentLoaded', function() {
// Restore UI state (theme, etc.)
restoreUIState();

// Apply background colors
document.querySelectorAll('.code-line[data-bg-color]').forEach(line => {
const bgColor = line.getAttribute('data-bg-color');
if (bgColor) {
line.style.background = bgColor;
}
});
applyLineColors();

// Initialize navigation buttons
document.querySelectorAll('.nav-btn').forEach(button => {
Expand Down
Loading
Loading