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
1 change: 0 additions & 1 deletion Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,6 @@ def test_cpu_count_default(self):
res = assert_python_ok('-c', code, PYTHON_CPU_COUNT='default')
self.assertEqual(self.res2int(res), (os.cpu_count(), os.process_cpu_count()))

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_import_time(self):
# os is not imported at startup
code = 'import os; import os'
Expand Down
98 changes: 98 additions & 0 deletions crates/vm/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,108 @@ use crate::{
vm::{VirtualMachine, resolve_frozen_alias, thread},
};

use core::{cell::Cell, sync::atomic::Ordering, time::Duration};
use std::{io::Write, time::Instant};

/// Nested imports are timed per thread, so concurrent imports do not subtract
/// each other's elapsed time. The output header belongs to the interpreter.
#[derive(Clone, Copy, Default)]
pub(crate) struct ImportTimingState {
depth: usize,
children: Duration,
}

fn import_time_header(vm: &VirtualMachine) {
let mut stderr = std::io::stderr().lock();
if !vm.state.import_time_header.swap(true, Ordering::Relaxed) {
let _ = writeln!(
stderr,
"import time: self [us] | cumulative | imported package"
);
}
}

fn import_time_name(name: &Py<PyStr>) -> String {
use core::fmt::Write;
let mut output = String::with_capacity(name.as_bytes().len());
for codepoint in name.as_wtf8().code_points() {
match codepoint.to_char() {
Some(ch) => output.push(ch),
None => {
let _ = write!(output, "\\u{:04x}", codepoint.to_u32());
}
}
}
output
}

pub(crate) fn report_cached_import(name: &Py<PyStr>, vm: &VirtualMachine) {
if vm.state.config.settings.import_time == 2 {
import_time_header(vm);
let name = import_time_name(name);
// printf("%*s") counts encoded bytes, not Unicode scalar values.
let padding = (vm.import_timing.get().depth * 2).saturating_sub(name.len());
let _ = writeln!(
std::io::stderr(),
"import time: cached | cached | {:padding$}{}",
"",
name
);
}
}

struct ImportTimer<'a> {
state: &'a Cell<ImportTimingState>,
parent: ImportTimingState,
start: Instant,
name: &'a Py<PyStr>,
}

impl<'a> ImportTimer<'a> {
fn start(name: &'a Py<PyStr>, vm: &'a VirtualMachine) -> Option<Self> {
if vm.state.config.settings.import_time == 0 {
return None;
}
import_time_header(vm);
let parent = vm.import_timing.replace(ImportTimingState {
depth: vm.import_timing.get().depth + 1,
children: Duration::ZERO,
});
Some(Self {
state: &vm.import_timing,
parent,
start: Instant::now(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate import timing's clock on wasm32-unknown-unknown

When an embedder builds rustpython_vm for wasm32-unknown-unknown and sets the new public Settings::import_time field to 1 or 2, interpreter startup reaches Instant::now() on the first uncached import; Rust's standard-library implementation panics on that target because no monotonic clock is available. This makes enabling the diagnostic abort execution instead of reporting timings, so this path needs a wasm clock implementation or a target-specific graceful fallback.

AGENTS.md reference: AGENTS.md:L11-L12

Useful? React with 👍 / 👎.

name,
})
}
}

impl Drop for ImportTimer<'_> {
fn drop(&mut self) {
let cumulative = self.start.elapsed();
let own = cumulative.saturating_sub(self.state.get().children);
self.state.set(ImportTimingState {
depth: self.parent.depth,
children: self.parent.children.saturating_add(cumulative),
});
let width = self.parent.depth * 2;
let _ = writeln!(
std::io::stderr(),
"import time: {:9} | {:10} | {:width$}{}",
own.as_nanos().div_ceil(1000),
cumulative.as_nanos().div_ceil(1000),
"",
import_time_name(self.name),
);
}
}

pub(crate) fn find_and_load(
name: &Py<PyStr>,
function: &'static str,
vm: &VirtualMachine,
) -> PyResult {
let _timer = ImportTimer::start(name, vm);
vm.importlib
.get_attr(function, vm)?
.call((name.to_owned(), vm.import_func.clone()), vm)
Expand Down Expand Up @@ -194,6 +291,7 @@ fn import_ensure_initialized(
let lock_unlock = vm.importlib.get_attr("_lock_unlock_module", vm)?;
lock_unlock.call((name.to_owned(),), vm)?;
}
report_cached_import(name, vm);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions crates/vm/src/vm/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ where
atexit_funcs: PyMutex::default(),
audit_hooks: PyMutex::default(),
codec_registry,
import_time_header: AtomicBool::new(false),
lazy_imports,
struct_format_cache: crate::buffer::FormatSpecCache::default(),
finalizing: AtomicBool::new(false),
Expand Down
4 changes: 4 additions & 0 deletions crates/vm/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub struct VirtualMachine {
exceptions: RefCell<ExceptionStack>,
pub import_func: PyObjectRef,
pub(crate) importlib: PyObjectRef,
pub(crate) import_timing: Cell<import::ImportTimingState>,
pub profile_func: RefCell<PyObjectRef>,
pub trace_func: RefCell<PyObjectRef>,
pub use_tracing: Cell<bool>,
Expand Down Expand Up @@ -875,6 +876,7 @@ pub struct PyGlobalState {
/// `sys.addaudithook` hooks, shared by all threads of this interpreter.
pub(crate) audit_hooks: PyMutex<Vec<PyObjectRef>>,
pub codec_registry: CodecsRegistry,
pub(crate) import_time_header: AtomicBool,
pub(crate) lazy_imports: crate::lazy_import::LazyImportsState,
pub struct_format_cache: crate::buffer::FormatSpecCache,
pub finalizing: AtomicBool,
Expand Down Expand Up @@ -1353,6 +1355,7 @@ impl VirtualMachine {
exceptions: RefCell::default(),
import_func,
importlib,
import_timing: Cell::default(),
profile_func,
trace_func,
use_tracing: Cell::new(false),
Expand Down Expand Up @@ -3373,6 +3376,7 @@ impl VirtualMachine {
&& builtins.is(self.builtins.dict().as_object())
&& let Some(cached) = self.try_import_cached(module)?
{
import::report_cached_import(module, self);
return Ok(cached);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/vm/src/vm/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ pub struct Settings {

// int tracemalloc;
// int perf_profiling;
// int import_time;
/// -X importtime, PYTHONPROFILEIMPORTTIME: 2 also reports cached imports.
pub import_time: u8,
/// -X no_debug_ranges: disable column info in bytecode
pub code_debug_ranges: bool,
// int show_ref_count;
Expand Down Expand Up @@ -222,6 +223,7 @@ impl Default for Settings {
argv: vec![],
hash_seed: None,
faulthandler: false,
import_time: 0,
code_debug_ranges: true,
buffered_stdio: true,
check_hash_pycs_mode: CheckHashPycsMode::Default,
Expand Down
1 change: 1 addition & 0 deletions crates/vm/src/vm/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ impl VirtualMachine {
exceptions: RefCell::default(),
import_func: self.import_func.clone(),
importlib: self.importlib.clone(),
import_timing: Cell::default(),
profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())),
trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())),
use_tracing: Cell::new(use_tracing),
Expand Down
37 changes: 36 additions & 1 deletion src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,24 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> {
settings.lazy_imports = parse_lazy_imports(val.to_str(), "PYTHON_LAZY_IMPORTS");
}

// CPython uses the first occurrence of this -X option.
if let Some(value) = get_env("PYTHONPROFILEIMPORTTIME").filter(|value| !value.is_empty()) {
settings.import_time = parse_import_time(value.to_str(), "PYTHONPROFILEIMPORTTIME");
}

// CPython uses the first occurrence of these -X options.
let mut lazy_imports_option_seen = false;
let mut import_time_option_seen = false;
let xopts = args.implementation_option.into_iter().map(|s| {
let (name, value) = match s.split_once('=') {
Some((name, value)) => (name.to_owned(), Some(value)),
None => (s, None),
};
match &*name {
"dev" => settings.dev_mode = true,
"importtime" if !import_time_option_seen => {
settings.import_time = parse_import_time(value, "-X importtime");
import_time_option_seen = true;
}
"faulthandler" => settings.faulthandler = true,
"warn_default_encoding" => settings.warn_default_encoding = true,
"utf8" => {
Expand Down Expand Up @@ -504,6 +513,32 @@ fn parse_cpu_count(value: Option<&str>) -> Result<Option<NonZeroI32>, ()> {
}
}

/// = config_init_import_time
fn parse_import_time(value: Option<&str>, source: &str) -> u8 {
let value = value
.and_then(|value| {
value
.trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c'])
.parse::<i32>()
.ok()
})
.unwrap_or(1);
if !(0..=2).contains(&value) {
let numeric = if source == "PYTHONPROFILEIMPORTTIME" {
"numeric "
} else {
""
};
eprintln!(
"Fatal Python error: config_init_import_time: {source}: \
{numeric}values other than 1 and 2 are reserved for future use.\n\
Python runtime state: preinitialized\n"
);
std::process::exit(1);
}
value as u8
}

/// = config_init_lazy_imports
fn parse_lazy_imports(value: Option<&str>, source: &str) -> i8 {
match value {
Expand Down
Loading