Skip to main content

turbopack_core/chunk/
mod.rs

1pub mod availability_info;
2pub mod available_modules;
3pub mod chunk_group;
4pub mod chunk_id_strategy;
5pub(crate) mod chunk_item_batch;
6pub mod chunking;
7pub(crate) mod chunking_context;
8pub(crate) mod data;
9pub(crate) mod evaluate;
10
11use std::{fmt::Display, hash::Hash};
12
13use anyhow::{Result, bail};
14use auto_hash_map::AutoSet;
15use bincode::{Decode, Encode};
16use serde::{Deserialize, Serialize};
17use turbo_rcstr::RcStr;
18use turbo_tasks::{
19    FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, Upcast, ValueToString, Vc,
20    debug::ValueDebugFormat,
21};
22use turbo_tasks_hash::DeterministicHash;
23
24pub use crate::chunk::{
25    chunk_item_batch::{
26        ChunkItemBatchGroup, ChunkItemBatchWithAsyncModuleInfo,
27        ChunkItemOrBatchWithAsyncModuleInfo, batch_info,
28    },
29    chunking_context::{
30        AssetSuffix, ChunkGroupResult, ChunkGroupType, ChunkingConfig, ChunkingConfigs,
31        ChunkingContext, ChunkingContextExt, EntryChunkGroupResult, HmrChunkListSource, MangleType,
32        MinifyType, SourceMapSourceType, SourceMapsType, UnusedReferences, UrlBehavior,
33        WorkerConfigurationOptions,
34    },
35    data::{ChunkData, ChunkDataOption, ChunksData},
36    evaluate::{EvaluatableAsset, EvaluatableAssetExt, EvaluatableAssets},
37};
38use crate::{
39    asset::Asset,
40    chunk::{availability_info::AvailabilityInfo, available_modules::AvailableModulesSet},
41    emit_collect::CollectingModule,
42    ident::AssetIdent,
43    module::Module,
44    module_graph::{
45        ModuleGraph,
46        module_batch::{ChunkableModuleOrBatch, ModuleBatchGroup},
47    },
48    output::{OutputAssets, OutputAssetsReference},
49};
50
51#[turbo_tasks::task_input]
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DeterministicHash, Encode, Decode)]
53pub enum ContentHashing {
54    /// Direct content hashing: Embeds the chunk content hash directly into the referencing chunk.
55    /// Benefit: No hash manifest needed.
56    /// Downside: Causes cascading hash invalidation.
57    Direct {
58        /// The length of the content hash in base38 chars. Anything lower than 7 is not
59        /// recommended due to the high risk of collisions.
60        length: u8,
61    },
62}
63
64#[turbo_tasks::value(shared)]
65#[derive(Debug, Default, Clone, Copy, Hash, Serialize, Deserialize)]
66#[serde(rename_all = "kebab-case")]
67pub enum CrossOrigin {
68    #[default]
69    None,
70    Anonymous,
71    UseCredentials,
72}
73
74impl CrossOrigin {
75    pub fn as_str(self) -> Option<&'static str> {
76        match self {
77            Self::None => None,
78            Self::Anonymous => Some("anonymous"),
79            Self::UseCredentials => Some("use-credentials"),
80        }
81    }
82}
83
84impl TryFrom<Option<&str>> for CrossOrigin {
85    type Error = anyhow::Error;
86
87    fn try_from(value: Option<&str>) -> Result<Self> {
88        match value {
89            None => Ok(Self::None),
90            Some("anonymous") => Ok(Self::Anonymous),
91            Some("use-credentials") => Ok(Self::UseCredentials),
92            Some(value) => bail!(
93                "invalid crossOrigin value `{value}`; supported values are `anonymous` and \
94                 `use-credentials`"
95            ),
96        }
97    }
98}
99
100#[turbo_tasks::value(shared)]
101#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)]
102pub struct ChunkLoadRetry {
103    /// Number of retry attempts after the initial load fails. `0` disables retries.
104    pub max_retry_attempts: u32,
105    /// Base delay before a retry, in milliseconds.
106    pub base_delay_ms: u32,
107    /// Maximum random jitter added to the base delay, in milliseconds.
108    pub max_jitter_ms: u32,
109}
110
111impl Default for ChunkLoadRetry {
112    fn default() -> Self {
113        // Retry a transient failure once after a short jittered delay. Network
114        // blips (a brief connection reset, a short CDN hiccup) often succeed on
115        // a second try.
116        Self {
117            max_retry_attempts: 1,
118            base_delay_ms: 200,
119            max_jitter_ms: 400,
120        }
121    }
122}
123
124/// A module id, which can be a number or string
125#[turbo_tasks::value(shared, operation)]
126#[derive(Debug, Clone, Hash, Ord, PartialOrd, DeterministicHash, Serialize, ValueToString)]
127#[serde(untagged)]
128pub enum ModuleId {
129    Number(u64),
130    String(RcStr),
131}
132
133impl Display for ModuleId {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            ModuleId::Number(i) => write!(f, "{i}"),
137            ModuleId::String(s) => write!(f, "{s}"),
138        }
139    }
140}
141
142impl ModuleId {
143    pub fn parse(id: &str) -> Result<ModuleId> {
144        Ok(match id.parse::<u64>() {
145            Ok(i) => ModuleId::Number(i),
146            Err(_) => ModuleId::String(id.into()),
147        })
148    }
149}
150
151/// A list of module ids.
152#[turbo_tasks::value(transparent, shared)]
153pub struct ModuleIds(Vec<ModuleId>);
154
155/// A [Module] that can be converted into a [ChunkItem].
156#[turbo_tasks::value_trait]
157pub trait ChunkableModule: Module {
158    #[turbo_tasks::function]
159    fn as_chunk_item(
160        self: Vc<Self>,
161        module_graph: Vc<ModuleGraph>,
162        chunking_context: Vc<Box<dyn ChunkingContext>>,
163    ) -> Vc<Box<dyn ChunkItem>>;
164}
165
166/// A [Module] that can be merged with other [Module]s (to perform scope hoisting)
167// TODO currently this is only used for ecmascript modules, and with the current API cannot be used
168// with other module types (as a MergeableModule cannot prevent itself from being merged with other
169// module types)
170#[turbo_tasks::value_trait]
171pub trait MergeableModule: Module {
172    /// Even though MergeableModule is implemented, this allows a dynamic condition to determine
173    /// mergeability
174    #[turbo_tasks::function]
175    fn is_mergeable(self: Vc<Self>) -> Vc<bool> {
176        Vc::cell(true)
177    }
178
179    /// Create a new module representing the merged content of the given `modules`.
180    ///
181    /// Group entry points are not referenced by any other module in the group. This list is needed
182    /// because the merged module is created by recursively inlining modules when they are imported,
183    /// but this process has to start somewhere (= with these entry points).
184    #[turbo_tasks::function]
185    fn merge(
186        self: Vc<Self>,
187        modules: Vc<MergeableModulesExposed>,
188        entry_points: Vc<MergeableModules>,
189    ) -> Vc<Box<dyn ChunkableModule>>;
190}
191#[turbo_tasks::value(transparent)]
192pub struct MergeableModules(Vec<ResolvedVc<Box<dyn MergeableModule>>>);
193
194#[turbo_tasks::value_impl]
195impl MergeableModules {
196    #[turbo_tasks::function]
197    pub fn interned(modules: Vec<ResolvedVc<Box<dyn MergeableModule>>>) -> Vc<Self> {
198        Vc::cell(modules)
199    }
200}
201
202/// Whether a given module needs to be exposed (depending on how it is imported by other modules)
203#[turbo_tasks::task_input]
204#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Encode, Decode)]
205pub enum MergeableModuleExposure {
206    // This module is only used from within the current group, and only individual exports are
207    // used (and no namespace object is required).
208    None,
209    // This module is only used from within the current group, and but the namespace object is
210    // needed.
211    Internal,
212    // The exports of this module are read from outside this group (necessitating a namespace
213    // object anyway).
214    External,
215}
216
217#[turbo_tasks::value(transparent)]
218pub struct MergeableModulesExposed(
219    Vec<(
220        ResolvedVc<Box<dyn MergeableModule>>,
221        MergeableModuleExposure,
222    )>,
223);
224
225#[turbo_tasks::value_impl]
226impl MergeableModulesExposed {
227    #[turbo_tasks::function]
228    pub fn interned(
229        modules: Vec<(
230            ResolvedVc<Box<dyn MergeableModule>>,
231            MergeableModuleExposure,
232        )>,
233    ) -> Vc<Self> {
234        Vc::cell(modules)
235    }
236}
237
238#[turbo_tasks::value(transparent)]
239pub struct Chunks(Vec<ResolvedVc<Box<dyn Chunk>>>);
240
241#[turbo_tasks::value_impl]
242impl Chunks {
243    #[turbo_tasks::function]
244    pub fn empty() -> Vc<Self> {
245        Vc::cell(vec![])
246    }
247}
248
249/// Groups chunk items together into something that will become an [`OutputAsset`]. It usually
250/// contains multiple chunk items.
251///
252/// [`OutputAsset`]: crate::output::OutputAsset
253//
254// TODO: This could be simplified to and merged with OutputChunk
255#[turbo_tasks::value_trait]
256pub trait Chunk: OutputAssetsReference {
257    #[turbo_tasks::function]
258    fn ident(self: Vc<Self>) -> Vc<AssetIdent>;
259
260    #[turbo_tasks::function]
261    fn chunking_context(self: Vc<Self>) -> Vc<Box<dyn ChunkingContext>>;
262
263    #[turbo_tasks::function]
264    fn chunk_items(self: Vc<Self>) -> Vc<ChunkItems> {
265        ChunkItems(vec![]).cell()
266    }
267}
268
269/// Aggregated information about a chunk content that can be used by the runtime
270/// code to optimize chunk loading.
271#[turbo_tasks::value(shared)]
272#[derive(Default)]
273pub struct OutputChunkRuntimeInfo {
274    pub included_ids: Option<ResolvedVc<ModuleIds>>,
275    pub excluded_ids: Option<ResolvedVc<ModuleIds>>,
276    /// List of paths of chunks containing individual modules that are part of
277    /// this chunk. This is useful for selectively loading modules from a chunk
278    /// without loading the whole chunk.
279    pub module_chunks: Option<ResolvedVc<OutputAssets>>,
280    pub placeholder_for_future_extensions: (),
281}
282
283#[turbo_tasks::value_impl]
284impl OutputChunkRuntimeInfo {
285    #[turbo_tasks::function]
286    pub fn empty() -> Vc<Self> {
287        Self::default().cell()
288    }
289}
290
291#[turbo_tasks::value_trait]
292pub trait OutputChunk: Asset {
293    #[turbo_tasks::function]
294    fn runtime_info(self: Vc<Self>) -> Vc<OutputChunkRuntimeInfo>;
295}
296
297/// Whether this reference is an entry point for a traced subgraph.
298#[derive(
299    Debug,
300    Clone,
301    Copy,
302    Hash,
303    Serialize,
304    Deserialize,
305    Eq,
306    PartialEq,
307    ValueDebugFormat,
308    Encode,
309    Decode,
310)]
311#[turbo_tasks::task_input]
312pub enum TracedMode {
313    /// Going from bundled to unbundled code, i.e. an external dependency or readFile static assets.
314    Entry,
315    /// This reference should only be respected from unbundled code (e.g. for package.json needed by
316    /// externals (sort of affecting_sources)
317    Transitive,
318}
319
320impl Display for TracedMode {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        match self {
323            TracedMode::Entry => write!(f, "Entry"),
324            TracedMode::Transitive => write!(f, "Transitive"),
325        }
326    }
327}
328
329/// Specifies how a chunk interacts with other chunks when building a chunk
330/// group
331#[derive(
332    Debug,
333    Clone,
334    Hash,
335    Serialize,
336    Deserialize,
337    Eq,
338    PartialEq,
339    ValueDebugFormat,
340    NonLocalValue,
341    Encode,
342    Decode,
343)]
344pub enum ChunkingType {
345    /// The referenced module is placed in the same chunk group and is loaded in parallel.
346    Parallel {
347        /// Whether the parent module becomes an async module when the referenced module is async.
348        /// This should happen for e.g. ESM imports, but not for CommonJS requires.
349        inherit_async: bool,
350        /// Whether the referenced module is executed always immediately before the parent module
351        /// (corresponding to ESM import semantics).
352        hoisted: bool,
353    },
354    /// An async loader is placed into the referencing chunk and loads the
355    /// separate chunk group in which the module is placed.
356    Async,
357    /// Create a new chunk group in a separate context, merging references with the same tag into a
358    /// single chunk group. It does not inherit the available modules from the parent.
359    // TODO this is currently skipped in chunking
360    Isolated {
361        _ty: ChunkGroupType,
362        merge_tag: Option<RcStr>,
363    },
364    /// Declare an emitted module (corresponds to __turboack_emit__).
365    Emitted {
366        namespace: RcStr,
367        /// false = emit to current entry, true = emit to all entries
368        emit_to_all_entries: bool,
369    },
370    /// During the build process, edges with ChunkingType::Emitted are collected and reattached to
371    /// the collecting module. These should not be used manually in a reference.
372    Collected { namespace: RcStr },
373    /// Chunk this reference once per entry, like async loaders.
374    PerEntry,
375    /// Create a new chunk group in a separate context, merging references with the same tag into a
376    /// single chunk group. It provides available modules to the current chunk group. It's assumed
377    /// to be loaded before the current chunk group.
378    Shared {
379        inherit_async: bool,
380        merge_tag: Option<RcStr>,
381    },
382    /// The module not placed in chunk group, but its references are still followed. This is used
383    /// for NFT, to list all unbundled files that are still needed at runtime (some static assets,
384    /// or externals and their transitive dependencies).
385    Traced {
386        /// Whether this reference is an entry point for a traced subgraph.
387        mode: TracedMode,
388    },
389}
390
391impl Display for ChunkingType {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        match self {
394            ChunkingType::Parallel {
395                inherit_async,
396                hoisted,
397            } => {
398                write!(
399                    f,
400                    "Parallel(inherit_async: {inherit_async}, hoisted: {hoisted})",
401                )
402            }
403            ChunkingType::Async => write!(f, "Async"),
404            ChunkingType::PerEntry => write!(f, "PerEntry"),
405            ChunkingType::Isolated {
406                _ty,
407                merge_tag: Some(merge_tag),
408            } => {
409                write!(f, "Isolated(merge_tag: {merge_tag})")
410            }
411            ChunkingType::Isolated {
412                _ty,
413                merge_tag: None,
414            } => {
415                write!(f, "Isolated")
416            }
417            ChunkingType::Emitted {
418                namespace,
419                emit_to_all_entries,
420            } => {
421                write!(
422                    f,
423                    "Emitted(namespace: {namespace}, emit_to_all_entries: {emit_to_all_entries})"
424                )
425            }
426            ChunkingType::Collected { namespace } => {
427                write!(f, "Collected(namespace: {namespace})")
428            }
429            ChunkingType::Shared {
430                inherit_async,
431                merge_tag: Some(merge_tag),
432            } => {
433                write!(
434                    f,
435                    "Shared(inherit_async: {inherit_async}, merge_tag: {merge_tag})"
436                )
437            }
438            ChunkingType::Shared {
439                inherit_async,
440                merge_tag: None,
441            } => {
442                write!(f, "Shared(inherit_async: {inherit_async})")
443            }
444            ChunkingType::Traced { mode } => write!(f, "Traced(mode: {mode})"),
445        }
446    }
447}
448
449impl ChunkingType {
450    pub fn is_inherit_async(&self) -> bool {
451        matches!(
452            self,
453            ChunkingType::Parallel {
454                inherit_async: true,
455                ..
456            } | ChunkingType::Shared {
457                inherit_async: true,
458                ..
459            }
460        )
461    }
462
463    pub fn is_parallel(&self) -> bool {
464        matches!(self, ChunkingType::Parallel { .. })
465    }
466
467    pub fn is_traced(&self) -> bool {
468        matches!(self, ChunkingType::Traced { .. })
469    }
470
471    pub fn is_merged(&self) -> bool {
472        matches!(
473            self,
474            ChunkingType::Isolated {
475                merge_tag: Some(_),
476                ..
477            } | ChunkingType::Shared {
478                merge_tag: Some(_),
479                ..
480            }
481        )
482    }
483
484    pub fn without_inherit_async(&self) -> Self {
485        match self {
486            ChunkingType::Parallel { hoisted, .. } => ChunkingType::Parallel {
487                hoisted: *hoisted,
488                inherit_async: false,
489            },
490            ChunkingType::Async => ChunkingType::Async,
491            ChunkingType::PerEntry => ChunkingType::PerEntry,
492            ChunkingType::Isolated { _ty, merge_tag } => ChunkingType::Isolated {
493                _ty: *_ty,
494                merge_tag: merge_tag.clone(),
495            },
496            ChunkingType::Emitted {
497                namespace,
498                emit_to_all_entries,
499            } => ChunkingType::Emitted {
500                namespace: namespace.clone(),
501                emit_to_all_entries: *emit_to_all_entries,
502            },
503            ChunkingType::Collected { namespace } => ChunkingType::Collected {
504                namespace: namespace.clone(),
505            },
506            ChunkingType::Shared {
507                inherit_async: _,
508                merge_tag,
509            } => ChunkingType::Shared {
510                inherit_async: false,
511                merge_tag: merge_tag.clone(),
512            },
513            ChunkingType::Traced { mode } => ChunkingType::Traced { mode: *mode },
514        }
515    }
516}
517
518/// The modules (soon to be chunk items) that were discovered after traversing a given chunk group.
519#[turbo_tasks::value(cell = "new")]
520pub struct ChunkGroupContentInner {
521    /// Regular chunkable modules/module batches
522    pub chunkable_items: Vec<ChunkableModuleOrBatch>,
523    /// As an optimization, we also keep track of the batch groups that were discovered.
524    pub batch_groups: Vec<ResolvedVc<ModuleBatchGroup>>,
525    /// The modules that were imported with ChunkingType::Async
526    #[bincode(with = "turbo_bincode::indexset")]
527    pub async_modules: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
528    /// All modules that implement CollectingModule
529    #[bincode(with = "turbo_bincode::indexset")]
530    pub collecting_modules: FxIndexSet<ResolvedVc<Box<dyn CollectingModule>>>,
531    pub available_modules: ResolvedVc<AvailableModulesSet>,
532}
533
534pub struct ChunkGroupContent {
535    pub inner: ReadRef<ChunkGroupContentInner>,
536    pub availability_info: AvailabilityInfo,
537}
538
539#[turbo_tasks::value_trait]
540pub trait ChunkItem: OutputAssetsReference {
541    /// The [AssetIdent] of the [Module] that this [ChunkItem] was created from.
542    /// For most chunk types this must uniquely identify the chunk item at
543    /// runtime as it's the source of the module id used at runtime.
544    #[turbo_tasks::function]
545    fn asset_ident(self: Vc<Self>) -> Vc<AssetIdent>;
546
547    /// A [AssetIdent] that uniquely identifies the content of this [ChunkItem].
548    /// It is usually identical to [ChunkItem::asset_ident] but can be
549    /// different when the chunk item content depends on available modules e. g.
550    /// for chunk loaders.
551    #[turbo_tasks::function]
552    fn content_ident(self: Vc<Self>) -> Vc<AssetIdent> {
553        self.asset_ident()
554    }
555
556    /// The type of chunk this item should be assembled into.
557    fn ty(&self) -> Vc<Box<dyn ChunkType>>;
558
559    /// A temporary method to retrieve the module associated with this
560    /// ChunkItem. TODO: Remove this as part of the chunk refactoring.
561    #[turbo_tasks::function]
562    fn module(self: Vc<Self>) -> Vc<Box<dyn Module>>;
563
564    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>>;
565}
566
567#[turbo_tasks::value_trait]
568pub trait ChunkType: ValueToString {
569    /// Whether the source (reference) order of items needs to be retained during chunking.
570    #[turbo_tasks::function]
571    fn is_style(self: Vc<Self>) -> Vc<bool>;
572
573    /// Create a new chunk for the given chunk items
574    #[turbo_tasks::function]
575    fn chunk(
576        &self,
577        chunking_context: Vc<Box<dyn ChunkingContext>>,
578        chunk_items: Vec<ChunkItemOrBatchWithAsyncModuleInfo>,
579        batch_groups: Vec<ResolvedVc<ChunkItemBatchGroup>>,
580        component_chunks: Vec<ResolvedVc<Box<dyn Chunk>>>,
581    ) -> Vc<Box<dyn Chunk>>;
582
583    #[turbo_tasks::function]
584    fn chunk_item_size(
585        &self,
586        chunking_context: Vc<Box<dyn ChunkingContext>>,
587        chunk_item: Vc<Box<dyn ChunkItem>>,
588        async_module_info: Option<Vc<AsyncModuleInfo>>,
589    ) -> Vc<usize>;
590}
591
592pub fn round_chunk_item_size(size: usize) -> usize {
593    let a = size.next_power_of_two();
594    size & (a | (a >> 1) | (a >> 2))
595}
596
597#[turbo_tasks::value(transparent)]
598pub struct ChunkItems(pub Vec<ResolvedVc<Box<dyn ChunkItem>>>);
599
600#[turbo_tasks::value]
601pub struct AsyncModuleInfo {
602    pub referenced_async_modules: AutoSet<ResolvedVc<Box<dyn Module>>>,
603}
604
605#[turbo_tasks::value_impl]
606impl AsyncModuleInfo {
607    #[turbo_tasks::function]
608    pub fn new(referenced_async_modules: Vec<ResolvedVc<Box<dyn Module>>>) -> Result<Vc<Self>> {
609        Ok(Self {
610            referenced_async_modules: referenced_async_modules.into_iter().collect(),
611        }
612        .cell())
613    }
614}
615
616#[turbo_tasks::task_input]
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
618pub struct ChunkItemWithAsyncModuleInfo {
619    pub chunk_item: ResolvedVc<Box<dyn ChunkItem>>,
620    pub chunk_type: ResolvedVc<Box<dyn ChunkType>>,
621    pub module: Option<ResolvedVc<Box<dyn Module>>>,
622    pub async_info: Option<ResolvedVc<AsyncModuleInfo>>,
623}
624
625pub trait ChunkItemExt {
626    /// Returns the module id of this chunk item.
627    fn id(self: Vc<Self>) -> impl Future<Output = Result<ModuleId>> + Send;
628}
629
630impl<T> ChunkItemExt for T
631where
632    T: Upcast<Box<dyn ChunkItem>> + Send,
633{
634    /// Returns the module id of this chunk item.
635    async fn id(self: Vc<Self>) -> Result<ModuleId> {
636        let chunk_item = Vc::upcast_non_strict(self);
637        chunk_item
638            .into_trait_ref()
639            .await?
640            .chunking_context()
641            .chunk_item_id_strategy()
642            .await?
643            .get_id(chunk_item)
644            .await
645    }
646}
647
648pub trait ModuleChunkItemIdExt {
649    /// Returns the chunk item id of this module.
650    fn chunk_item_id(
651        self: Vc<Self>,
652        chunking_context: Vc<Box<dyn ChunkingContext>>,
653    ) -> impl Future<Output = Result<ModuleId>> + Send;
654}
655impl<T> ModuleChunkItemIdExt for T
656where
657    T: Upcast<Box<dyn Module>> + Send,
658{
659    async fn chunk_item_id(
660        self: Vc<Self>,
661        chunking_context: Vc<Box<dyn ChunkingContext>>,
662    ) -> Result<ModuleId> {
663        chunking_context
664            .chunk_item_id_strategy()
665            .await?
666            .get_id_from_module(Vc::upcast_non_strict(self))
667            .await
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    #[test]
676    fn test_round_chunk_item_size() {
677        assert_eq!(round_chunk_item_size(0), 0);
678        assert_eq!(round_chunk_item_size(1), 1);
679        assert_eq!(round_chunk_item_size(2), 2);
680        assert_eq!(round_chunk_item_size(3), 3);
681        assert_eq!(round_chunk_item_size(4), 4);
682        assert_eq!(round_chunk_item_size(5), 4);
683        assert_eq!(round_chunk_item_size(6), 6);
684        assert_eq!(round_chunk_item_size(7), 6);
685        assert_eq!(round_chunk_item_size(8), 8);
686        assert_eq!(round_chunk_item_size(49000), 32_768);
687        assert_eq!(round_chunk_item_size(50000), 49_152);
688
689        assert_eq!(changes_in_range(0..1000), 19);
690        assert_eq!(changes_in_range(1000..2000), 2);
691        assert_eq!(changes_in_range(2000..3000), 1);
692
693        assert_eq!(changes_in_range(3000..10000), 4);
694
695        fn changes_in_range(range: std::ops::Range<usize>) -> usize {
696            let len = range.len();
697            let mut count = 0;
698            for i in range {
699                let a = round_chunk_item_size(i);
700                assert!(a >= i * 2 / 3);
701                assert!(a <= i);
702                let b = round_chunk_item_size(i + 1);
703
704                if a == b {
705                    count += 1;
706                }
707            }
708            len - count
709        }
710    }
711}