Files
next.js/turbopack/scripts/analyze_cache_effectiveness.py
Luke SandbergandGitHub 236a76dd0f [turbopack] Remove turbo_tasks::function from ModuleReference getters (#91229)
### What?

Refactors the `ModuleReference` trait to make `chunking_type()` and `binding_usage()` methods return direct values instead of `Vc<T>` wrapped values, removing the need for async task functions.

Also removes the `get_referenced_asset` task from `EsmAssetReference`, inlining its logic into the callers.

### Why?

This change simplifies the API by eliminating unnecessary async overhead for methods that typically return simple, computed values. The previous implementation required `#[turbo_tasks::function]` annotations and `Vc<T>` wrappers even when the methods didn't need to perform async operations or benefit from caching.

### Impact

| Metric | Base | Change | Delta |
|--------|------|--------|-------|
| Hits | 35,678,143 | 35,845,124 | **+166,981** |
| Misses | 9,418,378 | 7,910,986 | **-1,507,392** |
| Total | 45,096,521 | 43,756,110 | **-1,340,411** |
| Task types | 1,306 | 1,277 | **-29** |

29 task types were removed, eliminating **2.6M total task invocations** (1.1M hits + 1.5M misses):

- **`chunking_type`** — 21 task types removed across all `ModuleReference` implementors (~952k invocations)
- **`binding_usage`** — 6 task types removed (~527k invocations)
- **`BindingUsage::all`** — helper task removed (~36k invocations)
- **`EsmAssetReference::get_referenced_asset`** — removed and inlined (~1.08M invocations: 628k hits + 451k misses)

The removed `get_referenced_asset` hits reappear as +628k hits on `EsmAssetReference::resolve_reference` and `ReferencedAsset::from_resolve_result` (with zero increase in misses), confirming the work is now served from cache through the existing callers.

No tasks had increased misses — the removal is clean with no cache invalidation spillover.

I also ran some builds to measure latency

```
# This branch
$ hyperfine -p 'rm -rf .next' -w 2 -r 10  'pnpm next build --turbopack --experimental-build-mode=compile'
Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile
  Time (mean ± σ):     52.752 s ±  0.658 s    [User: 376.575 s, System: 106.375 s]
  Range (min … max):   51.913 s … 54.161 s    10 runs

# on canary
$ hyperfine -p 'rm -rf .next' -w 2 -r 10  'pnpm next build --turbopack --experimental-build-mode=compile'
Benchmark 1: pnpm next build --turbopack --experimental-build-mode=compile
  Time (mean ± σ):     54.675 s ±  1.394 s    [User: 389.273 s, System: 114.642 s]
  Range (min … max):   53.434 s … 58.189 s    10 runs
```

so a solid win of almost 2 seconds

MaxRSS also went from 16,474,324,992 bytes to 16,359,309,312 bytes (from one measurement) so a savings of ~100M of max heap size.


### How?

- Changed `chunking_type()` method signature from `Vc<ChunkingTypeOption>` to `Option<ChunkingType>`
- Changed `binding_usage()` method signature from `Vc<BindingUsage>` to `BindingUsage`
- Removed `ChunkingTypeOption` type alias as it's no longer needed
- Updated all implementations across the codebase to return direct values instead of wrapped ones
- Removed `#[turbo_tasks::function]` annotations from these methods
- Updated call sites to use `into_trait_ref().await?` pattern when accessing these methods from `Vc<dyn ModuleReference>`
- Removed `EsmAssetReference::get_referenced_asset`, inlining its logic into callers
- Added validation for `turbopack-chunking-type` annotation values in import analysis
- Fixed cache effectiveness analysis script
2026-03-13 11:54:56 -07:00

142 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Cache Effectiveness Analysis Script
This script analyzes task statistics to identify which tasks are not getting
significant benefit from caching and would be candidates for removing the
caching layer.
To use this script, run a build with `NEXT_TURBOPACK_TASK_STATISTICS=path/to/stats.json` set
Then run this script with the path to the stats.json file to get a report on cache effectiveness.
The JSON format contains entries like:
{ "task_name": { "cache_hit": N, "cache_miss": N } }
"""
import json
import sys
from typing import List, Tuple
from dataclasses import dataclass
@dataclass
class TaskStats:
name: str
cache_hit: int
cache_miss: int
@property
def total_operations(self) -> int:
return self.cache_hit + self.cache_miss
@property
def cache_hit_rate(self) -> float:
if self.total_operations == 0:
return 0.0
return self.cache_hit / self.total_operations
def load_task_stats(file_path: str) -> List[TaskStats]:
"""Load and parse task statistics from JSON file."""
with open(file_path, 'r') as f:
data = json.load(f)
tasks = []
for task_name, stats in data.items():
task = TaskStats(
name=task_name,
cache_hit=stats["cache_hit"],
cache_miss=stats["cache_miss"],
)
tasks.append(task)
return tasks
def analyze_tasks(tasks: List[TaskStats]) -> List[TaskStats]:
"""Analyze all tasks and return sorted by wasted cache overhead.
Tasks with the most wasted overhead are ranked first. Wasted overhead is
estimated as cache misses (each miss pays lookup cost but gets no benefit)
plus cache hits weighted by their relative cheapness compared to a miss.
In practice this sorts by: most cache misses first, breaking ties by lower
hit rate.
"""
# Sort by cache_miss descending, then by hit rate ascending
tasks.sort(key=lambda t: (-t.cache_miss, t.cache_hit_rate))
return tasks
def print_analysis(tasks: List[TaskStats]):
"""Print the analysis results."""
print("Tasks ranked by cache effectiveness (worst first)")
print()
if not tasks:
print("No tasks found.")
return
# Print header
header = (f"{'Hit Rate':<10} {'Hits':<10} {'Misses':<10} "
f"{'Total':<10} {'Task Name'}")
print(header)
print("-" * len(header))
total_hits = 0
total_misses = 0
low_hit_rate_count = 0
# Print results
for task in tasks:
hit_rate_str = f"{task.cache_hit_rate:.1%}"
hits_str = f"{task.cache_hit:,}"
misses_str = f"{task.cache_miss:,}"
total_str = f"{task.total_operations:,}"
print(f"{hit_rate_str:<10} {hits_str:<10} {misses_str:<10} "
f"{total_str:<10} {task.name}")
total_hits += task.cache_hit
total_misses += task.cache_miss
if task.cache_hit_rate < 0.5:
low_hit_rate_count += 1
total_ops = total_hits + total_misses
overall_hit_rate = total_hits / total_ops if total_ops > 0 else 0.0
# Print summary
print()
print(f"Total tasks: {len(tasks)}")
print(f"Total cache misses: {total_misses:,}")
print(f"Overall cache hit rate: {overall_hit_rate:.1%} ({total_hits:,} hits / {total_ops:,} total)")
print(f"Tasks with <50% hit rate: {low_hit_rate_count}")
def main():
if len(sys.argv) != 2:
print("Usage: python analyze_cache_effectiveness.py <stats.json>")
sys.exit(1)
file_path = sys.argv[1]
try:
tasks = load_task_stats(file_path)
tasks = analyze_tasks(tasks)
print_analysis(tasks)
except FileNotFoundError:
print(f"Error: File '{file_path}' not found")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()