1#![feature(future_join)]
2#![feature(min_specialization)]
3#![feature(arbitrary_self_types)]
4#![feature(arbitrary_self_types_pointers)]
5
6use std::{str::FromStr, time::Instant};
7
8use anyhow::{Context, Result};
9use futures_util::{StreamExt, TryStreamExt};
10use next_api::{
11 project::{ProjectContainer, ProjectOptions},
12 route::{Endpoint, EndpointOutputPaths, Route, endpoint_write_to_disk},
13};
14use turbo_rcstr::RcStr;
15use turbo_tasks::{ReadConsistency, ResolvedVc, TransientInstance, TurboTasks, Vc, get_effects};
16use turbo_tasks_backend::{NoopBackingStorage, TurboTasksBackend};
17use turbo_tasks_malloc::TurboMalloc;
18
19pub async fn main_inner(
20 tt: &TurboTasks<TurboTasksBackend<NoopBackingStorage>>,
21 strat: Strategy,
22 factor: usize,
23 limit: usize,
24 files: Option<Vec<String>>,
25) -> Result<()> {
26 register();
27
28 let path = std::env::current_dir()?.join("project_options.json");
29 let mut file = std::fs::File::open(&path)
30 .with_context(|| format!("loading file at {}", path.display()))?;
31
32 let mut options: ProjectOptions = serde_json::from_reader(&mut file)?;
33
34 if matches!(strat, Strategy::Development { .. }) {
35 options.dev = true;
36 options.watch.enable = true;
37 } else {
38 options.dev = false;
39 options.watch.enable = false;
40 }
41
42 let project = tt
43 .run_once(async {
44 let project = ProjectContainer::new("next-build-test".into(), options.dev);
45 let project = project.to_resolved().await?;
46 project.initialize(options).await?;
47 Ok(project)
48 })
49 .await?;
50
51 tracing::info!("collecting endpoints");
52 let entrypoints = tt
53 .run_once(async move { project.entrypoints().await })
54 .await?;
55
56 let mut routes = if let Some(files) = files {
57 tracing::info!("builing only the files:");
58 for file in &files {
59 tracing::info!(" {}", file);
60 }
61
62 Box::new(files.into_iter().filter_map(|f| {
65 entrypoints
66 .routes
67 .iter()
68 .find(|(name, _)| f.as_str() == name.as_str())
69 .map(|(name, route)| (name.clone(), route.clone()))
70 })) as Box<dyn Iterator<Item = _> + Send + Sync>
71 } else {
72 Box::new(entrypoints.routes.clone().into_iter())
73 };
74
75 if strat.randomized() {
76 routes = Box::new(shuffle(routes))
77 }
78
79 let start = Instant::now();
80 let count = render_routes(tt, routes, strat, factor, limit).await?;
81 tracing::info!("rendered {} pages in {:?}", count, start.elapsed());
82
83 if count == 0 {
84 tracing::info!("No pages found, these pages exist:");
85 for (route, _) in entrypoints.routes.iter() {
86 tracing::info!(" {}", route);
87 }
88 }
89
90 if matches!(strat, Strategy::Development { .. }) {
91 hmr(tt, *project).await?;
92 }
93
94 Ok(())
95}
96
97pub fn register() {
98 next_api::register();
99 include!(concat!(env!("OUT_DIR"), "/register.rs"));
100}
101
102#[derive(PartialEq, Copy, Clone)]
103pub enum Strategy {
104 Sequential { randomized: bool },
105 Concurrent,
106 Parallel { randomized: bool },
107 Development { randomized: bool },
108}
109
110impl std::fmt::Display for Strategy {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match self {
113 Strategy::Sequential { randomized: false } => write!(f, "sequential"),
114 Strategy::Sequential { randomized: true } => write!(f, "sequential-randomized"),
115 Strategy::Concurrent => write!(f, "concurrent"),
116 Strategy::Parallel { randomized: false } => write!(f, "parallel"),
117 Strategy::Parallel { randomized: true } => write!(f, "parallel-randomized"),
118 Strategy::Development { randomized: false } => write!(f, "development"),
119 Strategy::Development { randomized: true } => write!(f, "development-randomized"),
120 }
121 }
122}
123
124impl FromStr for Strategy {
125 type Err = anyhow::Error;
126
127 fn from_str(s: &str) -> Result<Self> {
128 match s {
129 "sequential" => Ok(Strategy::Sequential { randomized: false }),
130 "sequential-randomized" => Ok(Strategy::Sequential { randomized: true }),
131 "concurrent" => Ok(Strategy::Concurrent),
132 "parallel" => Ok(Strategy::Parallel { randomized: false }),
133 "parallel-randomized" => Ok(Strategy::Parallel { randomized: true }),
134 "development" => Ok(Strategy::Development { randomized: false }),
135 "development-randomized" => Ok(Strategy::Development { randomized: true }),
136 _ => Err(anyhow::anyhow!("invalid strategy")),
137 }
138 }
139}
140
141impl Strategy {
142 pub fn randomized(&self) -> bool {
143 match self {
144 Strategy::Sequential { randomized } => *randomized,
145 Strategy::Concurrent => false,
146 Strategy::Parallel { randomized } => *randomized,
147 Strategy::Development { randomized } => *randomized,
148 }
149 }
150}
151
152pub fn shuffle<'a, T: 'a>(items: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
153 use rand::{SeedableRng, seq::SliceRandom};
154 let mut rng = rand::rngs::SmallRng::from_seed([0; 32]);
155 let mut input = items.collect::<Vec<_>>();
156 input.shuffle(&mut rng);
157 input.into_iter()
158}
159
160pub async fn render_routes(
161 tt: &TurboTasks<TurboTasksBackend<NoopBackingStorage>>,
162 routes: impl Iterator<Item = (RcStr, Route)>,
163 strategy: Strategy,
164 factor: usize,
165 limit: usize,
166) -> Result<usize> {
167 tracing::info!(
168 "rendering routes with {} parallel and strat {}",
169 factor,
170 strategy
171 );
172
173 let stream = tokio_stream::iter(routes)
174 .map(move |(name, route)| async move {
175 tracing::info!("{name}...");
176 let start = Instant::now();
177
178 let memory = TurboMalloc::memory_usage();
179
180 tt.run_once({
181 let name = name.clone();
182 async move {
183 match route {
184 Route::Page {
185 html_endpoint,
186 data_endpoint: _,
187 } => {
188 endpoint_write_to_disk_with_effects(*html_endpoint).await?;
189 }
190 Route::PageApi { endpoint } => {
191 endpoint_write_to_disk_with_effects(*endpoint).await?;
192 }
193 Route::AppPage(routes) => {
194 for route in routes {
195 endpoint_write_to_disk_with_effects(*route.html_endpoint).await?;
196 }
197 }
198 Route::AppRoute {
199 original_name: _,
200 endpoint,
201 } => {
202 endpoint_write_to_disk_with_effects(*endpoint).await?;
203 }
204 Route::Conflict => {
205 tracing::info!("WARN: conflict {}", name);
206 }
207 }
208 Ok(())
209 }
210 })
211 .await?;
212
213 let duration = start.elapsed();
214 let memory_after = TurboMalloc::memory_usage();
215 if matches!(strategy, Strategy::Sequential { .. }) {
216 if memory_after > memory {
217 tracing::info!(
218 "{name} {:?} {} MiB (memory usage increased by {} MiB)",
219 duration,
220 memory_after / 1024 / 1024,
221 (memory_after - memory) / 1024 / 1024
222 );
223 } else {
224 tracing::info!(
225 "{name} {:?} {} MiB (memory usage decreased by {} MiB)",
226 duration,
227 memory_after / 1024 / 1024,
228 (memory - memory_after) / 1024 / 1024
229 );
230 }
231 } else {
232 tracing::info!("{name} {:?} {} MiB", duration, memory_after / 1024 / 1024);
233 }
234
235 Ok::<_, anyhow::Error>(())
236 })
237 .take(limit)
238 .buffer_unordered(factor)
239 .try_collect::<Vec<_>>()
240 .await?;
241
242 Ok(stream.len())
243}
244
245#[turbo_tasks::function]
246async fn endpoint_write_to_disk_with_effects(
247 endpoint: ResolvedVc<Box<dyn Endpoint>>,
248) -> Result<Vc<EndpointOutputPaths>> {
249 let op = endpoint_write_to_disk_operation(endpoint);
250 let result = op.resolve_strongly_consistent().await?;
251 get_effects(op).await?.apply().await?;
252 Ok(*result)
253}
254
255#[turbo_tasks::function(operation)]
256pub fn endpoint_write_to_disk_operation(
257 endpoint: ResolvedVc<Box<dyn Endpoint>>,
258) -> Vc<EndpointOutputPaths> {
259 endpoint_write_to_disk(*endpoint)
260}
261
262async fn hmr(
263 tt: &TurboTasks<TurboTasksBackend<NoopBackingStorage>>,
264 project: Vc<ProjectContainer>,
265) -> Result<()> {
266 tracing::info!("HMR...");
267 let session = TransientInstance::new(());
268 let idents = tt
269 .run_once(async move { project.hmr_identifiers().await })
270 .await?;
271 let start = Instant::now();
272 for ident in idents {
273 if !ident.ends_with(".js") {
274 continue;
275 }
276 let session = session.clone();
277 let start = Instant::now();
278 let task = tt.spawn_root_task(move || {
279 let session = session.clone();
280 async move {
281 let project = project.project();
282 let state = project.hmr_version_state(ident.clone(), session);
283 project.hmr_update(ident.clone(), state).await?;
284 Ok(Vc::<()>::cell(()))
285 }
286 });
287 tt.wait_task_completion(task, ReadConsistency::Strong)
288 .await?;
289 let e = start.elapsed();
290 if e.as_millis() > 10 {
291 tracing::info!("HMR: {:?} {:?}", ident, e);
292 }
293 }
294 tracing::info!("HMR {:?}", start.elapsed());
295
296 Ok(())
297}