Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: Garbage Collect in one squashed commit
feat: add double drop check in debug mode

fix: use Mutex instead of PyMutex in `ID2TYPE`

refactor: cfg cond for `Drop` trait instead

feat: add `Trace` trait

feat: trace RwLock right&trace tuple

feat: `Trace` for `PyDict`

feat: `Trace` on `PyIter`&`PyIterReturn`&`PyIterIter`

feat: `Trace` on PyEnumerate

feat: `Trace` on `ArgCallable` `ArgIterable` `ArgMapping` `ArgSequence`

feat: `Trace` on `IterStatus` `PySequenceIterator` `PyCallableIterator` `PositionIterInternal`

feat: `Trace` on `PyReverseSequenceIterator`

feat: `Trace` on `PyTuple` `PyTupleIterator` `PyTupleTyped`

feat: `Trace` on `PyFilter` `PyFunction` `PyBoundMethod`

feat: `Trace` on `PyCell`

feat: `Trace` on `PyList` `PyListIterator` `PyListReverseIterator`

feat: `Trace` on `PyMap` `PyMappingProxy` `MappingProxyInner`

feat: `Trace` on PyMemoryViewNewArgs, PyMemoryViewIterator

feat: `Trace` on PyProperty, PySet, PySetInner

feat: `Trace` on PySlice, PyStaticMethod

feat: `Trace` on FuncArgs, KwArgs, PosArgs, OptionalArg

feat: `Trace` on PySuper, PySuperNewArgs

feat: `Trace` on `PyTraceback`

feat: `Trace` for PyBaseException, PyType, PyUnion

feat: `Trace` on PyWeakProxy, PyZip, PyBuffer

feat: `Trace` on PyMapping, PyNumber, PySequence

feat: add `list_traceable` macro

fix: right lifetime for `TracerFn`

feat: `trace` PyObjectRef&PyRef

feat: garbage cycle collector

fix: put drop_only in different loop

feat: core algorithm of garbage collect

feat: add drop_only&dealloc_only to vtable

feat: modify core.rs to use gc

style: cargo fmt

feat: add `try_gc`& gc per frame

feat: add `collect` in gc module

fix: set black when safe_inc

fix: check if is gc-ing in `should_gc`

refactor: cfg(gc) for `Drop` trait

fix: not add to roots multiple times

fix: add judge for if dropped

doc: add TODO

fix: prevent dealloc cycle garbage early

fix: `partially_drop` header later

fix: add dealloc guard for deref

fix: run `__del__`&drop separately

feat: more lock to gc&drop check

feat: make gc less freq

fix: cfg compile&support attr in partially_drop

feat: `pytrace` macro

feat: use `#[pytrace]` in some types

feat: compact header

feat: change gc cond to 10007 obj cnts

fix: trace `PyRange`

fix: drop ref vtable before dealloc to prevent UB

fix: debug check&cfg cond&clippy

fix: add ref only after `__del__` is done

feat: trace(unsafely ) PyMutex

feat: prevent trace PyMutex when not gc

feat: change `PyRwlock` back to `PyMutex`

fix: testcase test_reference_loop test_unique_composite

refactor: early exit of collect_cycles

fix: cfg cond

feat: gc pause warn msg when wait too long

fix: not run __del__ in cycles

fix: expected failure for test_unique_composite

fix: allow test_ioctl_signed_unsigned_code_param

feat: split `drop` to `del`&`weakref`

fix: lock cond

fix: pause cond so high freq gc not halt all

refactor: put impl Collector together

feat: drop weak_list later

feat: unlock gc pause lock fairly

feat: print progress for two long test

feat: adjust lock order&not panic

fix: let obj handle its weakref's dealloc

fix: check stack before pop

fix: not leak weakref

log: remove some false alarm

fix: cfg flag for cond compile

fix: cfg flag for no-default-feature

fix: use non-block incref

test: change gc to 1ms&exit all if wait too long

fix: INCREF done right&not gc until last gc done

feat: add `gc` feature to root crate

doc: TODO for PEP442 del in cycle

fix: temporaily add one more `gc.collect()`

test: add gc feature in CI

refactor: make `mark/scan_roots` associated fn

refactor: `free_cycles` fn

test: flush progress prompt

docs: add TODO for modified testcases

refactor: header state's type

feat: drop_only

log: gc info

clippy: drop_only allow unused

refactor: rename `gc` feature to `gc_bacon`

refactor: remove `allow(unused)`

feat: `MaybeTrace` trait

feat: add `trace` Meta for `pyclass` proc macro

feat: cfg cond flag for `MaybeTrace`

feat: add `trace` in vtable

fix: add`#[pyclass(trace)` for manual impl trace

fix: elide null check in vtable&CR refactor

fix: change name in CI tests

feat: not use gc in wasm

refactor: accord to Code Review

doc&fix: explain gc&fix macro

fix: test_sys_setprofile
  • Loading branch information
discord9 committed Apr 17, 2023
commit 36f70ec1635323b58b169070d671e0808c9b30c0
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true

env:
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit,gc_bacon
NON_WASM_PACKAGES: >-
-p rustpython-common
-p rustpython-compiler-core
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unico
widestring = "0.5.1"

[features]
default = ["threading", "stdlib", "zlib", "importlib", "encodings", "rustpython-parser/lalrpop"]
default = ["threading", "stdlib", "zlib", "importlib", "encodings", "rustpython-parser/lalrpop", "gc_bacon"]
gc_bacon = ["rustpython-vm/gc_bacon", "rustpython-stdlib/gc"]
importlib = ["rustpython-vm/importlib"]
encodings = ["rustpython-vm/encodings"]
stdlib = ["rustpython-stdlib", "rustpython-pylib"]
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_ordered_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,6 @@ def test_dict_update(self):
dict.update(od, [('spam', 1)])
self.assertNotIn('NULL', repr(od))

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_reference_loop(self):
# Issue 25935
OrderedDict = self.OrderedDict
Expand All @@ -667,6 +665,8 @@ class A:
r = weakref.ref(A)
del A
gc.collect()
# TODO: RustPython, Need to fix this: somehow after del A, it takes two call to `gc.collect()`
# for gc to realize a loop is there and to be collected
self.assertIsNone(r())

# TODO: RUSTPYTHON
Expand Down
8 changes: 7 additions & 1 deletion Lib/test/test_sys_setprofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ def trace_call(self, frame):

def trace_return(self, frame):
self.add_event('return', frame)
self.stack.pop()
# TODO: RUSTPYTHON
# it seems pop from empty list is also related to those failed tests
# if those tests(all the tests in `ProfileHookTestCase``) can pass in RustPython,
# then we can remove this `if``
# and just use `self.stack.pop()` here
if len(self.stack)!=0:
Comment thread
discord9 marked this conversation as resolved.
self.stack.pop()

def trace_exception(self, frame):
self.testcase.fail(
Expand Down
15 changes: 13 additions & 2 deletions Lib/test/test_weakref.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ def setUp(self):
def callback(self, ref):
self.cbcalled += 1


# TODO: RUSTPYTHON, cpython's period is 0.0001, but at least now using such a small gc period is too slow
# so change to 0.001 for now
@contextlib.contextmanager
def collect_in_thread(period=0.0001):
def collect_in_thread(period=0.001):
"""
Ensure GC collections happen in a different thread, at a high frequency.
"""
Expand Down Expand Up @@ -1911,7 +1912,12 @@ def test_threaded_weak_valued_setdefault(self):
def test_threaded_weak_valued_pop(self):
d = weakref.WeakValueDictionary()
with collect_in_thread():
print("")
for i in range(100000):
if i%1000==0:
print("\rLoop:"+str(i)+"/100000 ", end="")
# TODO: RUSTPYTHON: so in log file the progress can be update in time
sys.stdout.flush()
Comment thread
discord9 marked this conversation as resolved.
Outdated
d[10] = RefCycle()
x = d.pop(10, 10)
self.assertIsNot(x, None) # we never put None in there!
Expand All @@ -1921,7 +1927,12 @@ def test_threaded_weak_valued_consistency(self):
# WeakValueDictionary when collecting from another thread.
d = weakref.WeakValueDictionary()
with collect_in_thread():
print("")
for i in range(200000):
if i%1000==0:
print("\rLoop:"+str(i)+"/200000 ", end="")
# TODO: RUSTPYTHON: so in log file the progress can be update in time
sys.stdout.flush()
o = RefCycle()
d[10] = o
# o is still alive, so the dict can't be empty
Expand Down
5 changes: 5 additions & 0 deletions derive-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod pyclass;
mod pymodule;
mod pypayload;
mod pystructseq;
mod pytrace;

use error::{extract_spans, Diagnostic};
use proc_macro2::TokenStream;
Expand Down Expand Up @@ -77,3 +78,7 @@ pub fn py_freeze(input: TokenStream, compiler: &dyn Compiler) -> TokenStream {
pub fn pypayload(input: DeriveInput) -> TokenStream {
result_to_tokens(pypayload::impl_pypayload(input))
}

pub fn pytrace(attr: AttributeArgs, item: DeriveInput) -> TokenStream {
result_to_tokens(pytrace::impl_pytrace(attr, item))
}
27 changes: 26 additions & 1 deletion derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::Diagnostic;
use crate::util::{
format_doc, pyclass_ident_and_attrs, text_signature, ClassItemMeta, ContentItem,
format_doc, path_eq, pyclass_ident_and_attrs, text_signature, ClassItemMeta, ContentItem,
ContentItemInner, ErrorVec, ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta,
ALL_ALLOWED_NAMES,
};
Expand Down Expand Up @@ -413,8 +413,33 @@ pub(crate) fn impl_pyclass(attr: AttributeArgs, item: Item) -> Result<TokenStrea
attrs,
)?;

// try to know if it have a `#[pyclass(trace)]` or a `#[pytrace] on this struct
let is_trace =
{ class_meta.is_trace()? || attrs.iter().any(|attr| path_eq(&attr.path, "pytrace")) };
let maybe_trace = {
if is_trace {
quote! {
#[cfg(feature = "gc_bacon")]
impl ::rustpython_vm::object::MaybeTrace for #ident {
const IS_TRACE: bool = true;
fn try_trace(&self, tracer_fn: &mut ::rustpython_vm::object::TracerFn) {
::rustpython_vm::object::Trace::trace(self, tracer_fn);
}
}
}
} else {
// a dummy impl, which do nothing
// #attrs
quote! {
#[cfg(feature = "gc_bacon")]
impl ::rustpython_vm::object::MaybeTrace for #ident { }
}
}
};

let ret = quote! {
#item
#maybe_trace
#class_def
};
Ok(ret)
Expand Down
70 changes: 70 additions & 0 deletions derive-impl/src/pytrace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{AttributeArgs, DeriveInput, Result};

/// also remove `#[notrace]` attr, and not trace corresponding field
fn gen_trace_code(item: &mut DeriveInput) -> Result<TokenStream> {
match &mut item.data {
syn::Data::Struct(s) => {
let fields = &mut s.fields;
if let syn::Fields::Named(ref mut fields) = fields {
let res: TokenStream = fields
.named
.iter_mut()
.map(|f| {
let name = f
.ident
.as_ref()
.expect("Field should have a name in non-tuple struct");
let mut do_trace = true;
f.attrs.retain(|attr| {
// remove #[notrace] and not trace this specifed field
if attr.path.segments.last().unwrap().ident == "notrace" {
do_trace = false;
false
} else {
true
}
});
if do_trace {
quote!(
::rustpython_vm::object::Trace::trace(&self.#name, tracer_fn);
)
} else {
quote!()
}
})
.collect();
Ok(res)
} else {
panic!("Expect only Named fields")
}
}
syn::Data::Enum(_) => todo!(),
syn::Data::Union(_) => todo!(),
}
}

pub(crate) fn impl_pytrace(attr: AttributeArgs, mut item: DeriveInput) -> Result<TokenStream> {
if !attr.is_empty() {
panic!(
"pytrace macro expect no attr(s), found {} attr(s)",
attr.len()
);
}

let trace_code = gen_trace_code(&mut item)?;

let ty = &item.ident;

let ret = quote! {
#item
#[cfg(feature = "gc_bacon")]
unsafe impl ::rustpython_vm::object::Trace for #ty {
fn trace(&self, tracer_fn: &mut ::rustpython_vm::object::TracerFn) {
#trace_code
}
}
};
Ok(ret)
}
27 changes: 25 additions & 2 deletions derive-impl/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::collections::{HashMap, HashSet};
use syn::{
spanned::Spanned, Attribute, Ident, Meta, MetaList, NestedMeta, Result, Signature, UseTree,
spanned::Spanned, Attribute, Ident, Meta, MetaList, NestedMeta, Path, Result, Signature,
UseTree,
};
use syn_ext::{
ext::{AttributeExt as SynAttributeExt, *},
Expand All @@ -25,6 +26,10 @@ pub(crate) const ALL_ALLOWED_NAMES: &[&str] = &[
"pymember",
];

pub(crate) fn path_eq(path: &Path, s: &str) -> bool {
path.get_ident().map_or(false, |id| id == s)
}

#[derive(Clone)]
struct NurseryItem {
attr_name: Ident,
Expand Down Expand Up @@ -178,6 +183,20 @@ impl ItemMetaInner {
Ok(value)
}

pub fn _exist_path(&self, key: &str) -> Result<bool> {
if let Some((_, meta)) = self.meta_map.get(key) {
match meta {
Meta::Path(p) => Ok(path_eq(p, key)),
other => Err(syn::Error::new_spanned(
other,
format!("#[{}({})] is expected", self.meta_name(), key),
)),
}
} else {
Ok(false)
}
}

pub fn _bool(&self, key: &str) -> Result<bool> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
match meta {
Expand Down Expand Up @@ -264,7 +283,7 @@ pub(crate) struct ClassItemMeta(ItemMetaInner);

impl ItemMeta for ClassItemMeta {
const ALLOWED_NAMES: &'static [&'static str] =
&["module", "name", "base", "metaclass", "unhashable"];
&["module", "name", "base", "metaclass", "unhashable", "trace"];

fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
Expand Down Expand Up @@ -308,6 +327,10 @@ impl ClassItemMeta {
self.inner()._optional_str("metaclass")
}

pub fn is_trace(&self) -> Result<bool> {
self.inner()._exist_path("trace")
}

pub fn module(&self) -> Result<Option<String>> {
const KEY: &str = "module";
let inner = self.inner();
Expand Down
17 changes: 17 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,20 @@ pub fn pypayload(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
derive_impl::pypayload(input).into()
}

/// use on struct with named fields like `struct A{x:i32, y:i32}` to impl `Trace` for datatype
///
/// use `#[notrace]` on fields you wish not to trace
///
/// add `trace` attr to `#[pyclass]` to make it
/// traceable(Even from type-erased PyObject)(i.e. write `#[pyclass(trace)]`)
/// better to place after `#[pyclass]` so pyclass know `pytrace`'s existance and impl a MaybeTrace calling Trace
#[proc_macro_attribute]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could probably be a normal derive() macro, and then you wouldn't have to deal with removing the notrace attr

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This could probably be a normal derive() macro, and then you wouldn't have to deal with removing the notrace attr

Yes, #[notrace] is only used four time in th following, still considering whether or not to remove it and hand write Trace:

#[pyclass(module = false, name = "enumerate")]
#[derive(Debug)]
#[pytrace]
pub struct PyEnumerate {
    #[notrace]
    counter: PyRwLock<BigInt>,
    iterator: PyIter,
}
#[pyclass(module = false, name = "iterator")]
#[derive(Debug)]
#[pytrace]
pub struct PySequenceIterator {
    // cached sequence methods
    #[notrace]
    seq_methods: &'static PySequenceMethods,
    internal: PyMutex<PositionIterInternal<PyObjectRef>>,
}
#[pyclass(module = false, name = "zip")]
#[derive(Debug)]
#[pytrace]
pub struct PyZip {
    iterators: Vec<PyIter>,
    #[notrace]
    strict: PyAtomic<bool>,
}
#[derive(Debug, Clone)]
#[pytrace]
pub struct ArgMapping {
    obj: PyObjectRef,
    #[notrace]
    methods: &'static PyMappingMethods,
}

pub fn pytrace(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let attr = parse_macro_input!(attr);
let item = parse_macro_input!(item);
derive_impl::pytrace(attr, item).into()
}
2 changes: 2 additions & 0 deletions stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ edition = "2021"

[features]
threading = ["rustpython-common/threading", "rustpython-vm/threading"]
gc = ["gc_bacon"]
gc_bacon = []
zlib = ["libz-sys", "flate2/zlib"]
bz2 = ["bzip2"]
ssl = ["openssl", "openssl-sys", "foreign-types-shared"]
Expand Down
38 changes: 34 additions & 4 deletions stdlib/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,52 @@ mod gc {

#[pyfunction]
fn collect(_args: FuncArgs, _vm: &VirtualMachine) -> i32 {
0
#[cfg(feature = "gc_bacon")]
{
usize::from(rustpython_vm::object::collect()) as i32
}
#[cfg(not(feature = "gc_bacon"))]
{
0
}
}

#[pyfunction]
fn isenabled(_args: FuncArgs, _vm: &VirtualMachine) -> bool {
false
#[cfg(feature = "gc_bacon")]
{
rustpython_vm::object::isenabled()
}
#[cfg(not(feature = "gc_bacon"))]
{
false
}
}

#[pyfunction]
fn enable(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
Err(vm.new_not_implemented_error("".to_owned()))
#[cfg(feature = "gc_bacon")]
{
rustpython_vm::object::enable();
Ok(vm.new_pyobj(true))
}
#[cfg(not(feature = "gc_bacon"))]
{
Err(vm.new_not_implemented_error("".to_owned()))
}
}

#[pyfunction]
fn disable(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
Err(vm.new_not_implemented_error("".to_owned()))
#[cfg(feature = "gc_bacon")]
{
rustpython_vm::object::disable();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be good for these to have more descriptive names, either a gc_ prefix or a separate module. Also the _gc module could probably get moved to rustpython-vm proper.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also the gc module could probably get moved to rustpython-vm proper.

Indeed, in CPython, the gc module is compiled into the interpreter (rather than being dynamically linked):

Python 3.11.0 (main, Oct 25 2022, 13:57:33) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import gc
>>> gc.__file__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'gc' has no attribute '__file__'. Did you mean: '__name__'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It would be good for these to have more descriptive names, either a gc_ prefix or a separate module. Also the _gc module could probably get moved to rustpython-vm proper.

how about "gc_bacon" as feature name(as bacon is the papers' author)?

Ok(vm.new_pyobj(true))
}
#[cfg(not(feature = "gc_bacon"))]
{
Err(vm.new_not_implemented_error("".to_owned()))
}
}

#[pyfunction]
Expand Down
1 change: 1 addition & 0 deletions vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include = ["src/**/*.rs", "Cargo.toml", "build.rs", "Lib/**/*.py"]

[features]
default = ["compiler"]
gc_bacon = []
importlib = []
encodings = ["importlib"]
vm-tracing-logging = []
Expand Down
1 change: 1 addition & 0 deletions vm/src/builtins/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub type DictContentType = dictdatatype::Dict;

#[pyclass(module = false, name = "dict", unhashable = true)]
#[derive(Default)]
#[pytrace]
pub struct PyDict {
entries: DictContentType,
}
Expand Down
Loading