-
-
Notifications
You must be signed in to change notification settings - Fork 639
feat(transformer): transform explicit resource management #9310
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
feat(transformer): transform explicit resource management #9310
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
CodSpeed Performance ReportMerging #9310 will degrade performances by 5.33%Comparing Summary
Benchmarks breakdown
|
c4b4c55
to
0dd6726
Compare
6b624bd
to
081e4d7
Compare
081e4d7
to
6716335
Compare
Thank you for working on this, I will review this next week, before that, can you add some documentation like other plugins do at the top of file? For example:
|
6716335
to
305c2ab
Compare
305c2ab
to
69a7a41
Compare
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.
Thanks very much for tackling this. And bravo for getting all the tests to pass, including all the semantic IDs (don't worry about that 1 weird one).
I've made comments below about some small things.
The bigger thing is the performance hit. My assumption is it's due to looping over statements. Usually we try to avoid that by setting some state in enter_statements
, then let the traversal run through the children with smaller visitors which update that state as they go, and then you check the state in exit_statements
and act on it if there's work to be done. This avoids visiting every statement twice.
However, I'm not familiar with this transform, and found it hard to follow what the code here is doing, so can't judge how amenable it'd be to that approach. So I suggest we merge this, and then work on the performance in later PRs.
What would help a lot is if you'd be able to add more comments, so the logic is easier to follow. The AstBuilder
calls are so verbose (bad API, sorry!) that it helps a lot to have a comment before a bunch of ctx.ast.blah_blah
calls saying what the code it produces. e.g. add a comment before this:
// `var var_id = <expr>;`
oxc/crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Lines 270 to 289 in 69a7a41
inner_block.push(Statement::VariableDeclaration(ctx.ast.alloc( | |
ctx.ast.variable_declaration( | |
span, | |
VariableDeclarationKind::Var, | |
ctx.ast.vec_from_array([ctx.ast.variable_declarator( | |
span, | |
VariableDeclarationKind::Var, | |
ctx.ast.binding_pattern( | |
BindingPatternKind::BindingIdentifier( | |
ctx.ast.alloc(var_id.create_binding_identifier(ctx)), | |
), | |
NONE, | |
false, | |
), | |
Some(expr), | |
false, | |
)]), | |
false, | |
), | |
))); |
Also methods should ideally have a high-level comment with a before vs after example (exactly like you did on enter_for_of_statement
).
Once the logic of the transform is clearer, it'll be easier to see what perf optimization is possible.
Sorry for the deluge of feedback. I hope it's helpful, rather than annoying!
@@ -45,6 +45,7 @@ fn main() { | |||
let ret = SemanticBuilder::new() | |||
// Estimate transformer will triple scopes, symbols, references | |||
.with_excess_capacity(2.0) | |||
.with_scope_tree_child_ids(true) |
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.
This change seems extraneous.
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 need to think more here, i'm using it
let child_ids = scopes.get_child_ids(current_scope_id); |
will look properly tommorow
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 didn't explain myself well. This file is an example. Did you mean to change the example?
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.
yeah so if you run the example and it transforms a using
declaration, then it'll panic because the child ids don't exist.
let me look and see if it's possible to do it another way
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.
what's this trying to achieve?
- we've got a vec of
Statements
- each of these statements currently has a parent scope of
x
- we are moving all of these statements into a child block
- so we need to update the scope of all of the statements
e.g.
function foo () {}
becomes
try {
function foo() {}
} ...
The current approach:
oxc/crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Lines 678 to 690 in c1b0ed3
let scope_id_children_to_move = scope_id.unwrap_or(parent_scope_id); | |
let scope_id = scope_id | |
.unwrap_or_else(|| ctx.create_child_scope(parent_scope_id, ScopeFlags::empty())); | |
let block = ctx.ast.block_statement_with_scope_id(SPAN, stmts, scope_id); | |
let scopes = ctx.scopes_mut(); | |
let child_ids = scopes.get_child_ids(scope_id_children_to_move); | |
let child_ids = child_ids.to_vec(); | |
for id in child_ids.iter().filter(|id| *id != &scope_id) { | |
scopes.change_parent_id(*id, Some(scope_id)); | |
} |
gets all child scopes, moves them to the new scope id (ignoring the scope we just created.
would it make sense to introduce a new api in oxc_traverse to achieve this?
e.g. a similar (but new) version of. something like insert_scope_above_statements
oxc/crates/oxc_traverse/src/context/scoping.rs
Lines 104 to 115 in a5cde10
/// Insert a scope into scope tree below a statement. | |
/// | |
/// Statement must be in current scope. | |
/// New scope is created as child of current scope. | |
/// All child scopes of the statement are reassigned to be children of the new scope. | |
/// | |
/// `flags` provided are amended to inherit from parent scope's flags. | |
pub fn insert_scope_below_statement(&mut self, stmt: &Statement, flags: ScopeFlags) -> ScopeId { | |
let mut collector = ChildScopeCollector::new(); | |
collector.visit_statement(stmt); | |
self.insert_scope_below(&collector.scope_ids, flags) | |
} |
^^ this is a bit of a brain dump.
Probably a better solution is to use change_parent_id
on the scope we passed in, and replace it.
needs more thought.
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.
yeah so if you run the example and it transforms a using declaration, then it'll panic because the child ids don't exist.
Ah of course. Now I understand why you changed the example. That makes sense.
But... I may be wrong, but I don't think using child scopes will work in all cases. e.g.:
function outer( x = function inParams() {} ) {
let f = function inBody() {};
using u = whatever();
}
Functions only have 1 scope, covering both the function body and its params. So in this case, inParams
's scope is a child of outer
. inBody
needs to have its parent scope changed, but inParams
should not. So, if I'm right, we'll need another mechanism to determine which scopes to re-parent.
But let's leave as is for now, and I'll try to devise a failing test that demonstrates the problem in a follow-up PR.
Also, fixing #9666 is going to throw all scopes-related stuff into disarray, so probably best we tackle this after that's done.
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
crates/oxc_transformer/src/proposals/explicit_resource_management.rs
Outdated
Show resolved
Hide resolved
fc2520a
to
6bcf4b2
Compare
Thank you for the detailed review! I appreciate it. For the perf hit, i had this thought. Would it be something like:
If that makes sense, perhapse we merge this once the other feedback has been resolved and fixed in a foillowup? I've actioned most of your comments just now. the more complex ones i need to think about more before actioning, but i'll try to do tomorrow/friday. NOTE: still need to add some more code comments about ast builder changes |
6bcf4b2
to
beed50c
Compare
This is a great help for future maintenance: https://github.com/oxc-project/oxc/blob/main/crates/oxc_transformer/README.md#style-guide-for-implementing-transforms |
beed50c
to
be9230c
Compare
Yes, pretty much exactly that. Except I'd suggest using a stack instead of a hashmap. You'd push to the stack when entering a block, and pop from it when exiting. In between, Same principle, but a stack is usually cheaper than a hash map.
|
Actually, you might be right, maybe a hash map is better, on assuption that It'd be interesting to test that and see if there's any difference. |
Follow-on after #9310. Style nit. Re-order imports with external crates before `oxc_*` crates. This is just my personal OCD preference! I find it easier to see where imports are coming from when ordered like this. Sadly there is no rustfmt rule to enforce this style.
yeah best to try it out, and see what happens |
Follow-on after #9310. Pure refactor. Add more comments, and move/amend some. Correct the link to the Babel plugin.
Follow-on after #9310. Pure refactor. Use shorter `AstBuilder` calls where possible. It's always preferable to use the `alloc_*` methods where possible, as it maximizes the chances compiler can see it can construct nodes directly in arena, rather than construct on stack and then copy to arena.
Follow-on after #9310. Pure refactor. Use shorter `AstBuilder` calls where possible. It's always preferable to use the `alloc_*` methods where possible, as it maximizes the chances compiler can see it can construct nodes directly in arena, rather than construct on stack and then copy to arena.
Follow-on after #9310. Holding large types on the stack is generally best to avoid where possible. Get the data into the arena as quickly as possible, and then only need to pass around `Box`es (which are only 8 bytes). In the case of `Class`, previously we were `unbox`-ing a `Class` (pull it out of the arena, and onto the stack) and then allocating it back into the arena again. `Class` is a large type - 160 bytes - and this extra work doesn't add any value. We can just leave the `Class` where it is in the arena, and pass around a `Box<Class>`. This is something of a micro-optimization, but they all add up...
Follow-on after #9310. Pure refactor. Just rename vars to be more descriptively named, rather than generic names like `node`.
Follow-on after #9310. Holding large types on the stack is generally best to avoid where possible. Get the data into the arena as quickly as possible, and then only need to pass around `Box`es (which are only 8 bytes). In the case of `Class`, previously we were `unbox`-ing a `Class` (pull it out of the arena, and onto the stack) and then allocating it back into the arena again. `Class` is a large type - 160 bytes - and this extra work doesn't add any value. We can just leave the `Class` where it is in the arena, and pass around a `Box<Class>`. This is something of a micro-optimization, but they all add up...
Follow-on after #9310. Pure refactor. Just rename vars to be more descriptively named, rather than generic names like `node`.
Follow-on after #9310. Holding large types on the stack is generally best to avoid where possible. Get the data into the arena as quickly as possible, and then only need to pass around `Box`es (which are only 8 bytes). In the case of `Class`, previously we were `unbox`-ing a `Class` (pull it out of the arena, and onto the stack) and then allocating it back into the arena again. `Class` is a large type - 160 bytes - and this extra work doesn't add any value. We can just leave the `Class` where it is in the arena, and pass around a `Box<Class>`. This is something of a micro-optimization, but they all add up...
Follow-on after #9310. Pure refactor. Just rename vars to be more descriptively named, rather than generic names like `node`.
Add a test case for explicit resource management (`using` declarations) which demonstrates a problem with scopes. See: #9310 (comment)
wanted to have a shot at implementing this, this is rough draft.Dunqing or overlookmotel feel free to take over or close out this PR. not sure how much time i'll have to finish it off@Dunqing @overlookmotel this should be ready when you guys have time 🙏
closes #9168