Skip to main content

turbopack_analyze/
split_chunk.rs

1use std::mem::replace;
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use turbo_rcstr::RcStr;
6use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, ValueToString, ValueToStringRef, Vc};
7use turbo_tasks_fs::{FileContent, FileLine, FileLinesContent, FileSystemPath, rope::Rope};
8use turbopack_core::{
9    asset::{Asset, AssetContent},
10    file_source::FileSource,
11    output::OutputAsset,
12    source_map::{GenerateSourceMap, OriginalToken, SourceMap, Token},
13};
14
15use crate::compressed_size::compressed_size_bytes;
16
17#[derive(Clone, Debug, Eq, NonLocalValue, PartialEq, Encode, Decode)]
18pub struct ChunkPartRange {
19    pub line: u32,
20    pub start_column: u32,
21    pub end_column: u32,
22}
23
24#[derive(Clone, Debug, Eq, NonLocalValue, PartialEq, Encode, Decode)]
25pub struct ChunkPart {
26    pub source: RcStr,
27    pub real_size: u32,
28    pub unaccounted_size: u32,
29    pub lines: ResolvedVc<FileLinesContent>,
30    pub ranges: Vec<ChunkPartRange>,
31}
32
33impl ChunkPart {
34    pub async fn get_compressed_size(&self) -> Result<Option<u32>> {
35        let lines = &*self.lines.await?;
36        let FileLinesContent::Lines(lines) = lines else {
37            return Ok(None);
38        };
39
40        if self.ranges.is_empty() {
41            let mut all_content = String::new();
42            for line in lines {
43                all_content.push_str(&line.content);
44            }
45            Ok(Some(compressed_size_bytes(all_content)?))
46        } else {
47            let mut all_range_content = String::new();
48            for range in &self.ranges {
49                append_content_between(
50                    range.line,
51                    range.start_column,
52                    range.line,
53                    range.end_column,
54                    lines,
55                    &mut all_range_content,
56                );
57            }
58            Ok(Some(compressed_size_bytes(all_range_content)?))
59        }
60    }
61}
62
63#[turbo_tasks::value(transparent)]
64#[derive(Debug)]
65pub struct ChunkParts(Vec<ChunkPart>);
66
67#[turbo_tasks::function]
68pub async fn split_traced_file_into_parts(path: FileSystemPath) -> Result<Vc<ChunkParts>> {
69    let source = FileSource::new(path.clone());
70    let content = source.content().await?;
71    let AssetContent::File(file_content) = &*content else {
72        return Ok(Vc::cell(vec![]));
73    };
74    let FileContent::Content(content) = &*file_content.await? else {
75        return Ok(Vc::cell(vec![]));
76    };
77    let content = content.content();
78    let lines_vc = file_content.lines().to_resolved().await?;
79
80    self_mapped(path.to_string_ref().await?, content, lines_vc).await
81}
82
83#[turbo_tasks::function]
84pub async fn split_output_asset_into_parts(
85    asset: Vc<Box<dyn OutputAsset>>,
86) -> Result<Vc<ChunkParts>> {
87    let content = asset.content().await?;
88    let AssetContent::File(file_content) = &*content else {
89        return Ok(Vc::cell(vec![]));
90    };
91    let FileContent::Content(content) = &*file_content.await? else {
92        return Ok(Vc::cell(vec![]));
93    };
94    let content = content.content();
95    let lines_vc = file_content.lines().to_resolved().await?;
96
97    let Some(generate_source_map) =
98        ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(asset.to_resolved().await?)
99    else {
100        return self_mapped(asset.path().to_string().owned().await?, content, lines_vc).await;
101    };
102    let source_map = generate_source_map.generate_source_map().await?;
103    let Some(source_map) = source_map.as_content() else {
104        return self_mapped(asset.path().to_string().owned().await?, content, lines_vc).await;
105    };
106    let Some(source_map) = SourceMap::new_from_rope(source_map.content())? else {
107        return unaccounted(asset, content, lines_vc).await;
108    };
109
110    let lines = lines_vc.await?;
111    let FileLinesContent::Lines(lines) = &*lines else {
112        return unaccounted(asset, content, lines_vc).await;
113    };
114
115    fn end_of_mapping_column(
116        start_line: u32,
117        end_line: u32,
118        end_column: u32,
119        lines: &[FileLine],
120    ) -> u32 {
121        let start_line = start_line.min(lines.len() as u32 - 1);
122        let line_end = lines[start_line as usize].len() as u32;
123        if start_line == end_line {
124            end_column.min(line_end)
125        } else {
126            line_end
127        }
128    }
129
130    fn len_between(
131        start_line: u32,
132        start_column: u32,
133        end_line: u32,
134        end_column: u32,
135        lines: &[FileLine],
136    ) -> u32 {
137        let start_line = start_line.min(lines.len() as u32 - 1);
138        let end_line = end_line.min(lines.len() as u32 - 1);
139        if start_line == end_line {
140            // TODO: Figure out why start is larger than end sometimes
141            return end_column.saturating_sub(start_column);
142        }
143        let mut len = lines[start_line as usize].len() as u32 - start_column + 1;
144        for line in &lines[start_line as usize + 1..end_line as usize] {
145            len += line.len() as u32 + 1;
146        }
147        len += end_column;
148        len
149    }
150
151    let mut chunk_parts = FxIndexMap::default();
152    fn add_chunk_part_range(
153        source: RcStr,
154        chunk_part_range: ChunkPartRange,
155        size: u32,
156        chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
157        lines: ResolvedVc<FileLinesContent>,
158    ) {
159        let entry = chunk_parts
160            .entry(source)
161            .or_insert_with_key(|source| ChunkPart {
162                source: source.clone(),
163                real_size: 0,
164                unaccounted_size: 0,
165                ranges: vec![],
166                lines,
167            });
168        entry.real_size += size;
169        entry.ranges.push(chunk_part_range);
170    }
171
172    fn add_unaccounted_chunk_part(
173        source: RcStr,
174        unaccounted: u32,
175        chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
176        lines: ResolvedVc<FileLinesContent>,
177    ) {
178        let entry = chunk_parts
179            .entry(source)
180            .or_insert_with_key(|source| ChunkPart {
181                source: source.clone(),
182                real_size: 0,
183                unaccounted_size: 0,
184                ranges: vec![],
185                lines,
186            });
187        entry.unaccounted_size += unaccounted;
188    }
189
190    fn end_current_mapping(
191        source: RcStr,
192        current_line: u32,
193        start_column: u32,
194        next_line: u32,
195        next_column: u32,
196        lines: &[FileLine],
197        chunk_parts: &mut FxIndexMap<RcStr, ChunkPart>,
198        lines_vc: ResolvedVc<FileLinesContent>,
199    ) -> State {
200        let mapping_end_column = end_of_mapping_column(current_line, next_line, next_column, lines);
201        let len = mapping_end_column.saturating_sub(start_column);
202        add_chunk_part_range(
203            source.clone(),
204            ChunkPartRange {
205                line: current_line,
206                start_column,
207                end_column: mapping_end_column,
208            },
209            len,
210            chunk_parts,
211            lines_vc,
212        );
213        State::AfterMapping {
214            source,
215            generated_line: current_line,
216            current_generated_column: mapping_end_column,
217        }
218    }
219
220    fn should_extend_mapping(
221        state: &State,
222        new_source: &RcStr,
223        new_line: u32,
224        new_column: u32,
225    ) -> bool {
226        if let State::InMapping {
227            source,
228            generated_line,
229            end_column,
230            ..
231        } = state
232        {
233            // Extend if same source and line, and columns are adjacent or overlapping
234            // end_column <= new_column handles both adjacent (equal) and overlapping cases
235            source == new_source && *generated_line == new_line && *end_column <= new_column
236        } else {
237            false
238        }
239    }
240
241    enum State {
242        StartOfFile,
243        InMapping {
244            source: RcStr,
245            generated_line: u32,
246            start_column: u32,
247            end_column: u32,
248        },
249        AfterMapping {
250            source: RcStr,
251            generated_line: u32,
252            current_generated_column: u32,
253        },
254    }
255
256    let mut state: State = State::StartOfFile;
257
258    for token in source_map.tokens() {
259        if let Token::Original(OriginalToken {
260            original_file,
261            generated_line,
262            generated_column,
263            ..
264        }) = token
265        {
266            // Check if we can extend the current mapping
267            if should_extend_mapping(&state, &original_file, generated_line, generated_column) {
268                // Same source and line with adjacent columns - update end to next token position
269                if let State::InMapping {
270                    source,
271                    generated_line: current_line,
272                    start_column,
273                    ..
274                } = state
275                {
276                    state = State::InMapping {
277                        source,
278                        generated_line: current_line,
279                        start_column,
280                        end_column: generated_column,
281                    };
282                    continue;
283                }
284            }
285
286            // End the current mapping if we're in one
287            if let State::InMapping {
288                source,
289                generated_line: current_line,
290                start_column,
291                ..
292            } = state
293            {
294                state = end_current_mapping(
295                    source,
296                    current_line,
297                    start_column,
298                    generated_line,
299                    generated_column,
300                    lines,
301                    &mut chunk_parts,
302                    lines_vc,
303                );
304            }
305
306            // Start a new mapping and put the unaccounted part in between somewhere
307            // Set end_column to start_column initially; it will be updated when we see the next
308            // token
309            match replace(
310                &mut state,
311                State::InMapping {
312                    source: original_file.clone(),
313                    generated_line,
314                    start_column: generated_column,
315                    end_column: generated_column,
316                },
317            ) {
318                State::InMapping { .. } => {
319                    unreachable!();
320                }
321                State::AfterMapping {
322                    source,
323                    generated_line,
324                    current_generated_column,
325                } => {
326                    let len = len_between(
327                        generated_line,
328                        current_generated_column,
329                        generated_line,
330                        generated_column,
331                        lines,
332                    );
333                    let half = len / 2;
334                    add_unaccounted_chunk_part(source, half, &mut chunk_parts, lines_vc);
335                    add_unaccounted_chunk_part(
336                        original_file.clone(),
337                        len - half,
338                        &mut chunk_parts,
339                        lines_vc,
340                    );
341                }
342                State::StartOfFile => {
343                    let len = len_between(0, 0, generated_line, generated_column, lines);
344                    add_unaccounted_chunk_part(
345                        original_file.clone(),
346                        len,
347                        &mut chunk_parts,
348                        lines_vc,
349                    );
350                }
351            }
352        }
353    }
354    let last_line = lines.len() as u32 - 1;
355    let last_column = lines[last_line as usize].len() as u32;
356
357    // End the current token at end of file
358    if let State::InMapping {
359        ref source,
360        generated_line,
361        start_column,
362        ..
363    } = state
364    {
365        state = end_current_mapping(
366            source.clone(),
367            generated_line,
368            start_column,
369            last_line,
370            last_column,
371            lines,
372            &mut chunk_parts,
373            lines_vc,
374        );
375    }
376
377    match state {
378        State::InMapping { .. } => {
379            unreachable!();
380        }
381        State::AfterMapping {
382            source,
383            generated_line,
384            current_generated_column,
385        } => {
386            let len = len_between(
387                generated_line,
388                current_generated_column,
389                last_line,
390                last_column,
391                lines,
392            );
393            add_unaccounted_chunk_part(source, len, &mut chunk_parts, lines_vc);
394        }
395        State::StartOfFile => {
396            return unaccounted(asset, content, lines_vc).await;
397        }
398    }
399
400    Ok(Vc::cell(chunk_parts.into_values().collect()))
401}
402
403pub async fn self_mapped(
404    path: RcStr,
405    content: &Rope,
406    lines: ResolvedVc<FileLinesContent>,
407) -> Result<Vc<ChunkParts>> {
408    let len = content.len().try_into().unwrap_or(u32::MAX);
409    Ok(Vc::cell(vec![ChunkPart {
410        source: path,
411        real_size: len,
412        unaccounted_size: 0,
413        ranges: vec![],
414        lines,
415    }]))
416}
417
418async fn unaccounted(
419    asset: Vc<Box<dyn OutputAsset>>,
420    content: &Rope,
421    lines: ResolvedVc<FileLinesContent>,
422) -> Result<Vc<ChunkParts>> {
423    let len = content.len().try_into().unwrap_or(u32::MAX);
424    Ok(Vc::cell(vec![ChunkPart {
425        source: asset.path().to_string().owned().await?,
426        real_size: 0,
427        unaccounted_size: len,
428        ranges: vec![],
429        lines,
430    }]))
431}
432
433fn append_content_between(
434    start_line: u32,
435    start_column: u32,
436    end_line: u32,
437    end_column: u32,
438    lines: &[FileLine],
439    out: &mut String,
440) {
441    let start_line = start_line.min(lines.len() as u32 - 1);
442    let end_line = end_line.min(lines.len() as u32 - 1);
443
444    let start_column = start_column.min(lines[start_line as usize].len() as u32);
445    let end_column = if start_line == end_line {
446        end_column.min(lines[start_line as usize].len() as u32)
447    } else {
448        lines[start_line as usize].len() as u32
449    };
450
451    if end_column <= start_column {
452        return;
453    }
454
455    out.extend(
456        lines[start_line as usize]
457            .content
458            .chars()
459            .skip(start_column as usize)
460            .take((end_column - start_column) as usize),
461    );
462
463    if start_line == end_line {
464        return;
465    }
466
467    for line in &lines[start_line as usize + 1..end_line as usize] {
468        out.push_str(&line.content);
469    }
470
471    out.extend(
472        lines[end_line as usize]
473            .content
474            .chars()
475            .take(end_column as usize),
476    );
477}