Skip to content
Open
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
4 changes: 4 additions & 0 deletions crates/jsshaker/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod post;
mod pre;
pub mod rw_tracking;

use std::cell::RefCell;
use std::collections::BTreeSet;

use conditional::ConditionalDataMap;
Expand All @@ -28,6 +29,7 @@ use crate::{
module::{ModuleId, Modules},
scope::Scoping,
utils::ExtraData,
value::FnCacheStats,
vfs::Vfs,
};

Expand Down Expand Up @@ -55,6 +57,7 @@ pub struct Analyzer<'a> {
pub mangler: Mangler<'a>,
pub pending_deps: FxHashSet<ExhaustiveCallback<'a>>,
pub diagnostics: BTreeSet<String>,
pub fn_cache_stats: Option<RefCell<FnCacheStats>>,
}

impl<'a> Analyzer<'a> {
Expand Down Expand Up @@ -86,6 +89,7 @@ impl<'a> Analyzer<'a> {
mangler,
pending_deps: Default::default(),
diagnostics: Default::default(),
fn_cache_stats: config.enable_fn_cache_stats.then(|| RefCell::new(FnCacheStats::new())),
}
}

Expand Down
7 changes: 7 additions & 0 deletions crates/jsshaker/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct TreeShakeConfig {
pub max_recursion_depth: usize,
pub remember_exhausted_variables: bool,
pub enable_fn_cache: bool,
pub enable_fn_cache_stats: bool,

pub mangling: Option<bool>,
pub unknown_global_side_effects: bool,
Expand Down Expand Up @@ -51,6 +52,7 @@ impl TreeShakeConfig {
max_recursion_depth: 2,
remember_exhausted_variables: true,
enable_fn_cache: true,
enable_fn_cache_stats: false,

mangling: Some(false),
unknown_global_side_effects: true,
Expand Down Expand Up @@ -131,4 +133,9 @@ impl TreeShakeConfig {
self.enable_fn_cache = yes;
self
}

pub fn with_fn_cache_stats(mut self, yes: bool) -> Self {
self.enable_fn_cache_stats = yes;
self
}
}
6 changes: 6 additions & 0 deletions crates/jsshaker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ pub fn tree_shake<F: Vfs + 'static>(options: JsShakerOptions<F>, entry: String)
let module_id = analyzer.parse_module(normalize_path::normalize_str(&entry));
analyzer.exec_module(module_id);
analyzer.post_analysis();

// Print cache stats if enabled
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow().print_summary();
}

let Analyzer {
modules,
diagnostics,
Expand Down
6 changes: 5 additions & 1 deletion crates/jsshaker/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ struct Args {

#[arg(long, default_value_t = false)]
no_fn_cache: bool,

#[arg(long, default_value_t = false)]
fn_cache_stats: bool,
}

fn main() {
Expand Down Expand Up @@ -77,7 +80,8 @@ fn main() {
})
.with_max_recursion_depth(args.recursion_depth)
.with_remember_exhausted(!args.no_remember_exhausted)
.with_fn_cache(!args.no_fn_cache);
.with_fn_cache(!args.no_fn_cache)
.with_fn_cache_stats(args.fn_cache_stats);

let minify_options = MinifierOptions {
mangle: Some(MangleOptions { top_level: true, ..Default::default() }),
Expand Down
4 changes: 3 additions & 1 deletion crates/jsshaker/src/value/cacheable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ impl<'a> Cacheable<'a> {

pub fn is_copyable(&self) -> bool {
match self {
Self::Array(_) | Self::Object(_) => false,
// Enable identity-based caching for objects/arrays
// Object and Array IDs are copyable and provide identity-based equality
Self::Array(_) | Self::Object(_) => true,
Self::Union(u) => u.iter().all(|c| c.is_copyable()),
_ => true,
}
Expand Down
83 changes: 73 additions & 10 deletions crates/jsshaker/src/value/function/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct FnCachedInput<'a> {
pub is_ctor: bool,
pub this: &'a Cacheable<'a>,
pub args: &'a [Cacheable<'a>],
pub rest: Option<&'a Cacheable<'a>>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -166,18 +167,53 @@ impl<'a> FnCache<'a> {
args: ArgumentsValue<'a>,
) -> Option<FnCachedInput<'a>> {
if !analyzer.config.enable_fn_cache {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_config_disabled += 1;
}
return None;
}

let this = analyzer.factory.alloc(this.as_cacheable(analyzer)?);
if args.rest.is_some() {
return None; // TODO: Support this case
}
let Some(this_cacheable) = this.as_cacheable(analyzer) else {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_non_copyable_this += 1;
}
return None;
};
let this = analyzer.factory.alloc(this_cacheable);

let rest = match args.rest {
Some(rest_arg) => match rest_arg.as_cacheable(analyzer) {
Some(cacheable) => {
let rest_ref = &*analyzer.factory.alloc(cacheable);
Some(rest_ref)
}
None => {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_rest_params += 1;
}
return None;
}
},
None => None,
};

let mut cargs = analyzer.factory.vec();
for arg in args.elements {
cargs.push(arg.as_cacheable(analyzer)?);
if let Some(cacheable) = arg.as_cacheable(analyzer) {
cargs.push(cacheable);
} else {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_non_copyable_args += 1;
}
return None;
}
}
Some(FnCachedInput { is_ctor: IS_CTOR, this, args: cargs.into_bump_slice() })

if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().cache_attempts += 1;
}

Some(FnCachedInput { is_ctor: IS_CTOR, this, args: cargs.into_bump_slice(), rest })
}

pub fn retrieve(
Expand All @@ -200,15 +236,28 @@ impl<'a> FnCache<'a> {
if c1.is_compatible(&c2) {
analyzer.add_assoc_entity_dep(tracking_dep, e2);
} else {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_read_dep_incompatible += 1;
}
return None;
}
}
}
(None, None) => {}
_ => return None,
_ => {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_read_dep_incompatible += 1;
}
return None;
}
}
}

// Cache hit successful!
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().cache_hits += 1;
}

for (&target, &(non_det, cacheable)) in &cached.effects.writes {
analyzer.set_rw_target_current_value(
target,
Expand Down Expand Up @@ -237,6 +286,11 @@ impl<'a> FnCache<'a> {

Some(ret)
} else {
if let Some(stats) = &analyzer.fn_cache_stats {
let mut stats = stats.borrow_mut();
stats.cache_misses += 1;
stats.miss_cache_empty += 1;
}
None
}
}
Expand All @@ -251,15 +305,24 @@ impl<'a> FnCache<'a> {
has_global_effects: bool,
) {
let FnCacheTrackingData::Tracked { effects } = tracking_data else {
return;
};
if !ret.as_cacheable(analyzer).is_some_and(|c| c.is_copyable()) {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().miss_state_untrackable += 1;
}
return;
};

// Removed copyability check - allow caching functions that return objects/arrays
// The return entity is properly wrapped with dependencies

self
.table
.borrow_mut()
.insert(key, FnCachedInfo { track_deps, effects, has_global_effects, ret });

if let Some(stats) = &analyzer.fn_cache_stats {
let mut stats = stats.borrow_mut();
stats.cache_updates += 1;
stats.cache_table_size = self.table.borrow().len();
}
}
}
75 changes: 75 additions & 0 deletions crates/jsshaker/src/value/function/cache_stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#[derive(Debug, Default)]
pub struct FnCacheStats {
// Overall metrics
pub total_calls: usize,
pub cache_attempts: usize,
pub cache_hits: usize,
pub cache_misses: usize,
pub cache_updates: usize,

// Miss reason breakdown
pub miss_config_disabled: usize,
pub miss_non_copyable_this: usize,
pub miss_non_copyable_args: usize,
pub miss_rest_params: usize,
pub miss_non_copyable_return: usize,
pub miss_state_untrackable: usize,
pub miss_read_dep_incompatible: usize,
pub miss_cache_empty: usize,

// Per-function statistics (optional: implement later if needed)
pub cache_table_size: usize,
}

impl FnCacheStats {
pub fn new() -> Self {
Self::default()
}

pub fn print_summary(&self) {
println!("\n=== Function Cache Statistics ===");
println!("Total Function Calls: {}", self.total_calls);
println!("Cache Key Generated: {}", self.cache_attempts);
println!("Cache Hits: {} ({:.1}%)", self.cache_hits, self.hit_rate_percent());
println!(
"Cache Misses: {} ({:.1}%)",
self.cache_misses,
100.0 - self.hit_rate_percent()
);
println!("Successful Updates: {}", self.cache_updates);
println!("Cache Table Size: {} entries", self.cache_table_size);

if self.cache_misses > 0 {
println!("\n--- Miss Reason Breakdown ---");
self.print_miss_reason("Config Disabled", self.miss_config_disabled);
self.print_miss_reason("Non-copyable This", self.miss_non_copyable_this);
self.print_miss_reason("Non-copyable Args", self.miss_non_copyable_args);
self.print_miss_reason("Rest Parameters", self.miss_rest_params);
self.print_miss_reason("Non-copyable Return", self.miss_non_copyable_return);
self.print_miss_reason("State Untrackable", self.miss_state_untrackable);
self.print_miss_reason("Read Dep Incompatible", self.miss_read_dep_incompatible);
self.print_miss_reason("Cache Empty (First Call)", self.miss_cache_empty);
}
println!("=================================\n");
}

fn hit_rate_percent(&self) -> f64 {
if self.cache_attempts == 0 {
0.0
} else {
(self.cache_hits as f64 / self.cache_attempts as f64) * 100.0
}
}

fn print_miss_reason(&self, reason: &str, count: usize) {
if count > 0 {
let total_calls = self.total_calls.max(1);
println!(
" {:30} {:8} ({:5.1}% of total calls)",
format!("{}:", reason),
count,
(count as f64 / total_calls as f64) * 100.0
);
}
}
}
4 changes: 4 additions & 0 deletions crates/jsshaker/src/value/function/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ impl<'a> FunctionValue<'a> {
mut args: ArgumentsValue<'a>,
consume: bool,
) -> Entity<'a> {
if let Some(stats) = &analyzer.fn_cache_stats {
stats.borrow_mut().total_calls += 1;
}

let call_id = DepAtom::from_counter();
let call_dep = analyzer.dep((self.callee.into_node(), dep, call_id));

Expand Down
2 changes: 2 additions & 0 deletions crates/jsshaker/src/value/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod arguments;
pub mod bound;
mod builtin;
pub mod cache;
pub mod cache_stats;
pub mod call;

use std::cell::Cell;
Expand All @@ -23,6 +24,7 @@ use crate::{
};
pub use arguments::*;
pub use builtin::*;
pub use cache_stats::FnCacheStats;

#[derive(Debug)]
pub struct FunctionValue<'a> {
Expand Down
4 changes: 3 additions & 1 deletion crates/jsshaker/tests/snapshots/test@recursion.js.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading