next_build_test/
lib.rs

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        // filter out the files that are not in the list
63        // we expect this to be small so linear search OK
64        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}