From 58208ef8aee994591f05d996a8b660da6e96d681 Mon Sep 17 00:00:00 2001 From: HeYunfei Date: Tue, 24 Jan 2023 14:16:23 +0800 Subject: [PATCH] perf(es/minifier): Make pure function check `O(1)` (#6840) --- crates/swc_ecma_minifier/src/metadata/mod.rs | 56 +++++++++++--- .../swc_ecma_minifier/src/metadata/tests.rs | 77 +++++++++++++++++++ 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/crates/swc_ecma_minifier/src/metadata/mod.rs b/crates/swc_ecma_minifier/src/metadata/mod.rs index 1d4b5c623a5..78006273a29 100644 --- a/crates/swc_ecma_minifier/src/metadata/mod.rs +++ b/crates/swc_ecma_minifier/src/metadata/mod.rs @@ -1,3 +1,6 @@ +use std::hash::Hash; + +use rustc_hash::FxHashSet; use swc_common::{ comments::{Comment, CommentKind, Comments}, EqIgnoreSpan, Span, SyntaxContext, @@ -11,6 +14,37 @@ use swc_ecma_visit::{ use crate::option::CompressOptions; +#[derive(Debug, Eq)] +struct HashEqIgnoreSpanExprRef<'a>(&'a Expr); + +impl<'a> PartialEq for HashEqIgnoreSpanExprRef<'a> { + fn eq(&self, other: &Self) -> bool { + Ident::within_ignored_ctxt(|| self.0.eq_ignore_span(other.0)) + } +} + +impl<'a> Hash for HashEqIgnoreSpanExprRef<'a> { + fn hash(&self, state: &mut H) { + // In pratice, most of cases/input we are dealing with are Expr::Member or + // Expr::Ident. + match self.0 { + Expr::Ident(i) => { + i.sym.hash(state); + } + Expr::Member(i) => { + Self(&i.obj).hash(state); + if let MemberProp::Ident(prop) = &i.prop { + prop.sym.hash(state); + } + } + _ => { + // Other expression kind would fallback to the same empty hash. + // So, their will spend linear time to do comparisons. + } + } + } +} + #[cfg(test)] mod tests; @@ -21,10 +55,18 @@ pub(crate) fn info_marker<'a>( marks: Marks, // unresolved_mark: Mark, ) -> impl 'a + VisitMut { + let pure_funcs = options.map(|options| { + options + .pure_funcs + .iter() + .map(|f| HashEqIgnoreSpanExprRef(f)) + .collect() + }); InfoMarker { options, comments, marks, + pure_funcs, // unresolved_mark, state: Default::default(), } @@ -37,8 +79,9 @@ struct State { } struct InfoMarker<'a> { + #[allow(dead_code)] options: Option<&'a CompressOptions>, - + pure_funcs: Option>>, comments: Option<&'a dyn Comments>, marks: Marks, // unresolved_mark: Mark, @@ -124,17 +167,12 @@ impl VisitMut for InfoMarker<'_> { if self.has_pure(n.span) { n.span = n.span.apply_mark(self.marks.pure); - } else if let Some(options) = self.options { + } else if let Some(pure_fns) = &self.pure_funcs { if let Callee::Expr(e) = &n.callee { // Check for pure_funcs - - if options - .pure_funcs - .iter() - .any(|pure_func| Ident::within_ignored_ctxt(|| e.eq_ignore_span(pure_func))) - { + if pure_fns.contains(&HashEqIgnoreSpanExprRef(e)) { n.span = n.span.apply_mark(self.marks.pure); - } + }; } } } diff --git a/crates/swc_ecma_minifier/src/metadata/tests.rs b/crates/swc_ecma_minifier/src/metadata/tests.rs index 18bf8bc1c64..3b69fdedefe 100644 --- a/crates/swc_ecma_minifier/src/metadata/tests.rs +++ b/crates/swc_ecma_minifier/src/metadata/tests.rs @@ -158,3 +158,80 @@ // fn export_default_fn_1() { // assert_standalone("export default function f(module, exports) {}", 0); // } + +use rustc_hash::FxHashSet; +use swc_common::{util::take::Take, Mark, DUMMY_SP}; +use swc_ecma_utils::{member_expr, quote_expr}; + +use super::HashEqIgnoreSpanExprRef; + +#[test] +fn test_hash_eq_ignore_span_expr_ref() { + use swc_ecma_ast::*; + + fn expr_ref(expr_ref: &Expr) -> HashEqIgnoreSpanExprRef { + HashEqIgnoreSpanExprRef(expr_ref) + } + + testing::run_test(false, |_cm, _handler| { + let dummy_sp = DUMMY_SP; + let meaningful_sp = dummy_sp.apply_mark(Mark::new()); + + let meaningful_ident_expr = Expr::Ident(Ident::new("foo".into(), meaningful_sp)); + let dummy_ident_expr = Expr::Ident(Ident::new("foo".into(), dummy_sp)); + + let meaningful_member_expr = member_expr!(meaningful_sp, foo.bar); + let dummy_member_expr = member_expr!(dummy_sp, foo.bar); + + let meaningful_null_expr = quote_expr!(meaningful_sp, null); + let dummy_null_expr = quote_expr!(dummy_sp, null); + + let meaningful_array_expr = Box::new(Expr::Array(ArrayLit { + span: meaningful_sp, + elems: Default::default(), + })); + + let dummy_array_expr = Box::new(Expr::Array(ArrayLit::dummy())); + + // Should equal ignoring span and syntax context + assert_eq!( + expr_ref(&meaningful_ident_expr), + expr_ref(&dummy_ident_expr) + ); + + assert_eq!( + expr_ref(&meaningful_array_expr), + expr_ref(&dummy_array_expr) + ); + + let mut set = FxHashSet::from_iter([ + expr_ref(&meaningful_ident_expr), + expr_ref(&meaningful_member_expr), + expr_ref(&meaningful_null_expr), + expr_ref(&meaningful_array_expr), + ]); + + // Should produce the same hash value ignoring span and syntax context + assert!(set.contains(&expr_ref(&dummy_ident_expr))); + assert!(set.contains(&expr_ref(&dummy_member_expr))); + assert!(set.contains(&expr_ref(&dummy_null_expr))); + assert!(set.contains(&expr_ref(&dummy_array_expr))); + + set.insert(expr_ref(&dummy_ident_expr)); + set.insert(expr_ref(&dummy_member_expr)); + set.insert(expr_ref(&dummy_null_expr)); + set.insert(expr_ref(&dummy_array_expr)); + assert_eq!(set.len(), 4); + + // Should not equal ignoring span and syntax context + let dummy_ident_expr = Expr::Ident(Ident::new("baz".into(), dummy_sp)); + let dummy_member_expr = member_expr!(dummy_sp, baz.bar); + let dummy_arrow_expr = Box::new(Expr::Arrow(ArrowExpr::dummy())); + assert!(!set.contains(&expr_ref(&dummy_ident_expr))); + assert!(!set.contains(&expr_ref(&dummy_member_expr))); + assert!(!set.contains(&expr_ref(&dummy_arrow_expr))); + + Ok(()) + }) + .unwrap(); +}