-
-
Notifications
You must be signed in to change notification settings - Fork 644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[internal] Port match_path_globs
to PyO3
#12225
Conversation
# Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
|
||
impl PyPathGlobs { | ||
fn parse(self) -> PyResult<PreparedPathGlobs> { | ||
self.0.clone().parse().map_err(|e| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! This clone()
might explain the slowdown in the contrived benchmark. We clone an Arc
instead with Rust-CPython.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, that's not it. And the PathGlobs
is cheap in the example. It's the paths: Vec<String>
that's so huge and would be painful to clone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this fn
takes ownership of self
, it shouldn't need to clone: it can consume self
(including self.0
).
EDIT: Oh. But we clone to render the error message. Hm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the error type is String
- presumably the glob which failed to parse? You might not need to clone
the rest of the globs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presumably the glob which failed to parse? You might not need to clone the rest of the globs.
Yeah, although we want to show what self
was in the error message so it's more useful. Not technically necessary, but I think it's fine because the PathGlobs
object is usually pretty small (a vec of a few strings).
@stuhood thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI @davidhewitt on the benchmark. Lmk if it'd help to do a profile.
|
||
impl PyPathGlobs { | ||
fn parse(self) -> PyResult<PreparedPathGlobs> { | ||
self.0.clone().parse().map_err(|e| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, that's not it. And the PathGlobs
is cheap in the example. It's the paths: Vec<String>
that's so huge and would be painful to clone.
.into_iter() | ||
.filter(|p| path_globs.matches(&PathBuf::from(p))) | ||
.collect(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is different than Rust-CPython, which first maps to PathBuf
, then filters, then converts back to strings. I keep the original strings for less work. Lmk if we need the maps.
I'll take a more detailed look at this on the weekend (probably Sunday). We should be able to avoid a slowdown, so I'll investigate and propose solutions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least for now, we don't use
Value
, which is anArc
around a Rust-CPythonPyObject
. We were using that because cloning with Rust-CPython requires the GIL.
Yea, I would be surprised if cloning a Python object with Py03 does not also require acquiring the GIL. AFAIK, adjusting the reference counts of Python objects always does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks really nice! :)
.getattr("glob_match_error_behavior")? | ||
.getattr("value")? | ||
.extract()?; | ||
let match_behavior = StrictGlobMatching::create(match_behavior_str, description_of_origin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was kind of hoping for an API closer to serde::Deserialize
here, where a derive macro would to most of the field-mapping work for you, and you'd be able to write something like:
let path_globs: PathGlobs = pyo3_serde::from_object(obj)?;
rather than having to get each field by field-name and put them together yourself... That would probably necessitatedescription_of_origin
being a field on glob_match_error_behavior
rather than a sibling field in obj
.
I guess this is PyO3/pyo3#566 and I definitely wouldn't suggest implementing it on the critical path of this conversion, but it would be nice to avoid that boilerplate... :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also worth calling out that this wrapping of a Python object is an alternative to making the object native as we do for a few types in
pants/src/rust/engine/src/externs/fs.rs
Line 67 in 8824cc3
py_class!(pub class PyDigest |py| { |
I'm kindof curious what making the object native would look like with Py03
's API... it might be less awkward than with rust-cpython
's, and I expect that it would be more performant than double-constructing objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I didn't go with a native class because we already have examples of that and to land the port successfully I want to avoid rewriting everything to do that. But after the port is finished, it makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be able to do something like this to avoid a lot of the getattr
boilerplate:
use pyo3::FromPyObject;
#[derive(FromPyObject)]
struct GlobMatchErrorBehavior<'a> {
value: &'a str,
}
#[derive(FromPyObject)]
struct Conjunction<'a> {
value: &'a str,
}
#[derive(FromPyObject)]
struct PyPathGlobs<'a> {
globs: Vec<String>,
description_of_origin: String,
glob_match_error_behaviour: GlobMatchErrorBehavior<'a>,
conjunction: Conjunction<'a>,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't had a chance to assess performance yet, however some general comments...
|
||
impl PyPathGlobs { | ||
fn parse(self) -> PyResult<PreparedPathGlobs> { | ||
self.0.clone().parse().map_err(|e| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the error type is String
- presumably the glob which failed to parse? You might not need to clone
the rest of the globs.
.getattr("glob_match_error_behavior")? | ||
.getattr("value")? | ||
.extract()?; | ||
let match_behavior = StrictGlobMatching::create(match_behavior_str, description_of_origin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be able to do something like this to avoid a lot of the getattr
boilerplate:
use pyo3::FromPyObject;
#[derive(FromPyObject)]
struct GlobMatchErrorBehavior<'a> {
value: &'a str,
}
#[derive(FromPyObject)]
struct Conjunction<'a> {
value: &'a str,
}
#[derive(FromPyObject)]
struct PyPathGlobs<'a> {
globs: Vec<String>,
description_of_origin: String,
glob_match_error_behaviour: GlobMatchErrorBehavior<'a>,
conjunction: Conjunction<'a>,
}
Great! Using PyO3 0.14.1 and Daniel's suggestion of Before
After:
|
# Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
This shows how we can convert types defined in Python into Rust, e.g.
PathGlobs
.Whereas we used to use a free function to do the conversion and asked for
PyObject
in the FFI function, now we implement the traitFromPyObject
on a wrapper typePyPathGlobs
so that we can request it directly in the function signature. This is more idiomatic and makes the#[pyfunction]
more clear what it does.We also now directly use PyO3's
.getattr()
and.extract()
at callsites, rather than using our customgetattr()
functions that wrapped those with Rust-Cpython. This removes a layer of indirection and doesn't noticeably increase boilerplate at callsites - it in fact reduces some because it's easier to chain.getattr()
multiple times.At least for now, we don't use
Value
, which is anArc
around a Rust-CPythonPyObject
. We were using that because cloning with Rust-CPython requires the GIL. Here, we're able to clone w/o requiring the GIL (inPyPathGlobs.parse()
), so I went with a simpler implementation. Some benefits of avoidingValue
are a) better type safety and b) avoiding a lock. We may end up needingValue
at the end of the day, but we can add this back.Benchmark
PyO3 does have slightly worse performance with this diff:
Before:
After:
Another
after
benchmark run with fewer apps on my machine shows726.1 ms ± 4.9 ms [User: 610.0 ms, System: 94.8 ms]
. Note that User is still the same, and only System changes. So this seems to be an actual slowdown.I am not yet sure why, but can profile if this is concerning.
[ci skip-build-wheels]