淺析 Parcel 的 Rust 打包算法 Demo
Parcel 是一個(gè)類似于 Webpack 、Rollup 的構(gòu)建工具,相較于這一類構(gòu)建工具,Parcel 主打的賣點(diǎn)是零配置并開箱即用,雖然某種程度上這種零配置的方式會(huì)使得項(xiàng)目定制化變得很困難,但 Parcel 盡量提供了一套自身的構(gòu)建最佳實(shí)踐,以后有機(jī)會(huì)去單獨(dú)介紹一下 Parcel 的整體構(gòu)造,這里不展開講解了。
Parcel 在 2.8.0 的更新中提到使用了一個(gè)新的打包算法,相比較于之前速度提升了 2.7 倍,并且體積還減小了 2.5 倍。同時(shí)還有其他的比較夸張的性能提升,例如 6 倍的熱更新速度,增量構(gòu)建的再次構(gòu)建性能提升了10倍。
同時(shí)作者強(qiáng)調(diào)該算法是由來自 Atlassian 的團(tuán)隊(duì)貢獻(xiàn)的,他們?yōu)榇嘶舜蠹s一年的時(shí)間使得其在 parcel v2.8.0 中成為默認(rèn)的打包算法,該算法帶來了巨大的性能提升,并且通過更少的重復(fù)包以及更好的瀏覽器緩存來有效減少了包產(chǎn)物的體積:
This results in both smaller bundles and much faster builds. *For a very large real-world project with over 60,000 assets, overall build time was reduced from over 25 minutes to 9 minutes (2.7x faster). The total bundle size for the whole project went from 952 MB *to 370 MB (2.5x smaller). For comparison, building the same app with webpack takes over 45 minutes.
此處測(cè)試項(xiàng)目根據(jù) twitter 信息為 jira。
實(shí)際上這個(gè)算法在 landing 過程中,它是基于 Parcel 作者 Devon Govett 本身寫的一個(gè)打包算法原型,并對(duì)這個(gè)算法本身在 Parcel 的實(shí)際場(chǎng)景中做了一些優(yōu)化。具體可以參考這個(gè) PR: https://github.com/parcel-bundler/parcel/pull/6975 的一些內(nèi)容:
在這篇文章中,我們先暫時(shí)不分析 Parcel 目前的具體的打包策略以及代碼邏輯,而是結(jié)合這個(gè)原型倉(cāng)庫(kù)來了解一下 Parcel 最原始的打包算法的運(yùn)行思路:
圖片
相比較于 Parcel 本身的打包算法,這個(gè)原型 Demo 可以要更簡(jiǎn)單一點(diǎn)(不過由于代碼是 Rust,理解難度對(duì)筆者來說也沒有很簡(jiǎn)單),在之后的文章中,我會(huì)單獨(dú)結(jié)合 Parcel 目前本身的打包算法(Default Bundlers)來做一下講解。
Rust 算法 demo
我們可以根據(jù)上面 PR 中找到對(duì)應(yīng)的算法原型倉(cāng)庫(kù),這個(gè)倉(cāng)庫(kù)的地址是: https://github.com/devongovett/bundler-algorithm, 不過由于倉(cāng)庫(kù)的一些內(nèi)容已經(jīng)實(shí)際落地了,因此作者后面將這個(gè)倉(cāng)庫(kù)給歸檔了。
這個(gè)倉(cāng)庫(kù)是基于 rust 寫的,不過整體流程上而言并不是特別復(fù)雜,我們可以簡(jiǎn)單調(diào)試一下(本地需要有 rust 環(huán)境):
git clone https://github.com/devongovett/bundler-algorithm
cd bundler-algorithm
# 使用 nightly 版本的 cargo
rustup install nightly && rustup default nightly
# 運(yùn)行 demo
cargo run
根據(jù)該倉(cāng)庫(kù)源碼,我們可以將一次打包算法執(zhí)行流程分為下面幾個(gè)步驟,注意以下步驟中的具體代碼存在一些細(xì)節(jié)省略的情況,如果你想了解更多的算法細(xì)節(jié),可以參考倉(cāng)庫(kù)源碼來進(jìn)行閱讀。
構(gòu)建依賴圖
首先在 main.rs 文件中的第一步,會(huì)先根據(jù)項(xiàng)目各文件之間的引用關(guān)系構(gòu)建一個(gè)依賴圖出來。
這里直接參考 build_graph 這個(gè)方法,例如這里項(xiàng)目中存在 html 、js 等不同類型的文件,首先會(huì)將這些資源文件作為節(jié)點(diǎn)添加到一個(gè)圖中,然后根據(jù)這些文件之間的引用關(guān)系去給對(duì)應(yīng)的節(jié)點(diǎn)添加對(duì)應(yīng)的邊,這樣就會(huì)形成一個(gè)比較初步完善的依賴圖。
// 創(chuàng)建 graph 并獲取對(duì)應(yīng)的 entries 對(duì)象
let (g, entries) = build_graph();
fn build_graph<'a>() -> (Graph<Asset<'a>, Dependency>, Vec<NodeIndex>) {
let mut g = Graph::new();
let mut entries = Vec::new();
let html = g.add_node(Asset {
name: "a.html",
asset_type: AssetType::HTML,
size: 10
});
let js = g.add_node(Asset {
name: "a.js",
asset_type: AssetType::JavaScript,
size: 10
});
// ...一些資源的初始化過程,這里節(jié)省篇幅跳過...
g.add_edge(html, js, Dependency {
is_async: false
});
entries.push(html);
entries.push(html2);
return (g, entries);
}
由于 build_graph 方法中代碼邏輯都比較單一,因此上面貼的源碼中有一些重復(fù)邏輯例如資源的初始化等的省略。
最后這個(gè)方法會(huì)構(gòu)建出一個(gè)如下圖所示的依賴圖出來,注意下面還有一些依賴之間的引用關(guān)系是異步的(這里可以理解為動(dòng)態(tài)導(dǎo)入),同時(shí)這里我們給每個(gè)靜態(tài)資源都標(biāo)注一個(gè)唯一的 asset_id,下文圖中標(biāo)注的序號(hào)均與此處對(duì)應(yīng):
圖片
其中 Parcel 會(huì)默認(rèn)將 Html 資源作為依賴圖的入口文件,因此我們可以看到這張圖是存在兩個(gè)入口的,分別為 a.html 和 b.html 。
遍歷圖創(chuàng)建獨(dú)立的 bundle
這里的 bundle 可以簡(jiǎn)單理解為最終 打包 輸出的文件,這個(gè)可以結(jié)合 Webpack 里面的一些概念來理解。
在圖創(chuàng)建完成之后,這一步主要目的則是根據(jù) graph 提供的一些信息(例如 entry、資源之間的依賴關(guān)系等)去創(chuàng)建出最后打包出來的 bundle 類型,這里首先會(huì)對(duì)下面三種情況創(chuàng)建一個(gè)單獨(dú)的 bundle:
- 入口文件,例如這里的 html 入口文件
- 不同類型資源之間的引用,例如當(dāng) js 文件中引用到圖片資源的時(shí)候
- 資源之間存在 異步 引用時(shí),例如某個(gè) js 動(dòng)態(tài)導(dǎo)入另外的 js 文件
下面我將根據(jù)這幾種情況來展開講講:
首先是根據(jù)入口文件去創(chuàng)建 bundle,這里邏輯很簡(jiǎn)單,遍歷一下前面 build_graph 方法生成的 entries 數(shù)組(實(shí)際上這里是個(gè) Rust 的 Vec ),然后往 bundle_roots 中插入對(duì)應(yīng)的 entry 以及 bundle_id 。
這里的 bundle_id 對(duì)應(yīng)前面 graph 圖中標(biāo)記的序列號(hào),例如 a.html 是 0,b.html 是 1。具體的實(shí)現(xiàn)參考以下邏輯:
let mut bundle_roots = HashMap:new();
let mut reacheable_bundles = HashSet::new();
let mut bundle_graph = Graph::new();
// 遍歷 entries,往 bundle_roots 中插入對(duì)應(yīng)的 entry 信息以及對(duì)應(yīng)的 bundle_id
for entry in &entries {
let bundle_id = bundle_graph.add_node(Bundle::from_asset(*entry, &g[*entry]));
bundle_roots.insert(*entry, (bundle_id, bundle_id));
}
這里應(yīng)用到實(shí)際開發(fā)中的場(chǎng)景可以聯(lián)想到我們開發(fā)一個(gè)單頁(yè)(SPA) 或者多頁(yè)應(yīng)用(MPA)時(shí),在打包產(chǎn)物中通常會(huì)出現(xiàn)一個(gè)或者多個(gè) html 入口文件,這里的情況也是類似的。
添加完 html 入口文件之后,接下來就會(huì)用深度優(yōu)先搜索算法(DFS)去遍歷整個(gè)圖,對(duì)以下兩種情況再生成單獨(dú)的 bundle:
- 不同類型資源之間的引用,對(duì)被引用的資源創(chuàng)建一個(gè) bundle,例如 a.js 中引用到了 style.css 那么 style.css 會(huì)被處理成一個(gè)單獨(dú)的 bundle
- 資源之間存在 異步 引用時(shí),對(duì)被引用的資源創(chuàng)建一個(gè) bundle,例如 a.js 是異步引用的 async.js ,那么 async.js 會(huì)被處理成一個(gè)單獨(dú)的 bundle
以下為遍歷圖的主要代碼邏輯,具體可以參考 DfsEvent::TreeEdge(u, v) ,這里是遍歷圖中的各個(gè)相連子節(jié)點(diǎn):
let mut stack = LinkedList::new();
depth_first_search(&g, entries, |event| {
match event {
// ...
DfsEvent::TreeEdge(u, v) => {
let asset_a = &g[u];
let asset_b = &g[v];
// 當(dāng)資源類型發(fā)生變化的時(shí)候,創(chuàng)建一個(gè)新的 bundle
if asset_a.asset_type != asset_b.asset_type {
// 舉個(gè)例子,這里 a.js -> style.css
// 這里 bundle_group_id 是 a.html,asset_b 是 style.css
// style.css 是不同類型的資源被引用,會(huì)被拆單獨(dú)的 bundle 出來
let (_, bundle_group_id) = stack.front().unwrap();
let bundle_id = bundle_graph.add_node(Bundle::from_asset(v, asset_b));
bundle_roots.insert(v, (bundle_id, *bundle_group_id));
bundle_graph.add_edge(*bundle_group_id, bundle_id, 0);
return
}
// 當(dāng)存在異步依賴的時(shí)候,創(chuàng)建一個(gè)新的 bundle
let dependency = &g[g.find_edge(u, v).unwrap()];
// 舉個(gè)例子,這里 a.js -> async.js 是異步依賴導(dǎo)入
if dependency.is_async {
// 因此這里 async.js(這里的 asset_b) 會(huì)被處理成一個(gè)單獨(dú)的 bundle
let bundle_id = bundle_graph.add_node(Bundle::from_asset(v, asset_b));
bundle_roots.insert(v, (bundle_id, bundle_id));
for (b, _) in &stack {
let a = &g[*b];
if a.asset_type != asset_b.asset_type {
break
}
reachable_bundles.insert((*b, v));
}
}
}
// ...
}
});
在經(jīng)歷過上面兩次操作之后,我們最開始的 Graph 就會(huì)創(chuàng)建以下幾個(gè)單獨(dú)的 bundle 出來,對(duì)應(yīng)的文件分別為:
- 作為入口文件的 a.html 和 b.html 各自形成一個(gè) bundle
- 被 html 文件引用的 a.js 和 b.js 以及被 js 文件引用的 style.css 各自形成一個(gè) bundle
- 被異步引用的 async.js 文件形成一個(gè) bundle
并且這些 bundle 之間還存在一定的依賴關(guān)系(這里的序號(hào)為最初始依賴圖中的序號(hào)):
圖片
上面 async2.js 這個(gè)異步的 js 資源沒有形成單獨(dú)的 bundle 原因在于其是被其它 js 資源同步引入的。
處理所有資源到對(duì)應(yīng) bundle
在上一接中利用初始化的依賴圖分析出了最初的幾個(gè)獨(dú)立的 bundle,當(dāng)然還剩下一些其它的資源還沒處理,例如 async2.js 和 shared.js 這兩個(gè) asset。
因此這一步的操作就是將這些還沒有處理成 bundle 的 assets 全部都處理到 bundles 中去,同時(shí)整合一下上一步遍歷出來的獨(dú)立 bundle。
首先這一步會(huì)根據(jù)前面的到的獨(dú)立 bundle 去建立一個(gè)可訪問的節(jié)點(diǎn),這里會(huì)用輪詢上一節(jié)生成的獨(dú)立 bundle,也就是這里的 bundle_roots ,同時(shí)根據(jù)每個(gè) bundle_root 在最原始的依賴圖中進(jìn)行一次 DFS 查找,然后剪枝掉一些同樣是 bundle_root 的 bundle,把剩余不是 asset 的添加到可訪問節(jié)點(diǎn)中來,實(shí)現(xiàn)參考如下:
let mut reachable_nodes = HashSet::new();
for (root, _) in &bundle_roots {
depth_first_search(&g, Some(*root), |event| {
if let DfsEvent::Discover(n, _) = &event {
if n == root {
return Control::Continue
}
// 如果命中了 bundle_root 就跳過
if bundle_roots.contains_key(&n) {
return Control::<()>::Prune;
}
// 否則就添加到可訪問節(jié)點(diǎn)中來
reachable_nodes.insert((*root, *n));
}
Control::Continue
});
}
舉個(gè)例子來說,例如 a.js 是個(gè)單獨(dú)的 bundle,但他同步引用了一個(gè)同類型的 async2.js ,同時(shí) async2.js 又引用了一個(gè) shared.js 。
圖片
那么這里就會(huì)形成一個(gè)以a.js為根節(jié)點(diǎn),分別到 async2.js 和 shared.js 的兩個(gè)可訪問節(jié)點(diǎn)。
同理,在上圖中我們還能找到這樣的一些訪問節(jié)點(diǎn):
圖片
拿到這些可訪問節(jié)點(diǎn)以后,這里會(huì)像最開始的的時(shí)候,根據(jù)這些節(jié)點(diǎn)再去形成一個(gè)可訪問節(jié)點(diǎn)的依賴圖,這個(gè)圖對(duì)比最初的依賴圖會(huì)小一點(diǎn),這個(gè)依賴圖的主要作用是為了幫助后續(xù)決定具體哪些 asset 被放到哪個(gè) bundle 中去,這個(gè)過程可以和 Webpack 的拆包策略一起理解。
// 根據(jù)拿到的可訪問的節(jié)點(diǎn),構(gòu)造一個(gè) graph 出來
let reachable_graph = Graph::<(), ()>::from_edges(&reachable_nodes);
這里的依賴圖實(shí)際上就是個(gè)去除一些 root_bundle 后的縮減版的依賴圖:
圖片
在完成可訪問節(jié)點(diǎn)依賴圖的構(gòu)建之后,下一步開始利用這個(gè)依賴圖將所有的 asset 都處理進(jìn) bundle 中去。其中這里每個(gè) asset 都會(huì)基于其和可訪問的 bundle_roots 之間的關(guān)系被放到單獨(dú) bundle 中去,這么做的目的是為先創(chuàng)建盡可能沒有重復(fù)代碼的 bundle graph。
// 用于存儲(chǔ) entry asset id 到 bundle id 的映射
let mut bundles: HashMap<Vec<NodeIndex>, NodeIndex> = HashMap::new();
for asset_id in g.node_indices() {
let reachable: Vec<NodeIndex> = reachable_graph.neighbors_directed(asset_id, Incoming).collect();
let reachable: Vec<NodeIndex> = reachable.iter().cloned().filter(|b| {
(&reachable).into_iter().all(|a| !reachable_bundles.contains(&(*a, *b)))
}).collect();
// 根據(jù)上面的每個(gè) asset 對(duì)應(yīng)的可訪問的 bundle 去生成一個(gè) bundle_graph
if let Some((bundle_id, _)) = bundle_roots.get(&asset_id) {
// 如果 asset 是個(gè) bundle_root,那么會(huì)給這個(gè) bundle_root 在 bundle_graph 添加上對(duì)應(yīng)的節(jié)點(diǎn)
bundles.entry(vec![asset_id]).or_insert(*bundle_id);
for a in &reachable {
if *a != asset_id {
bundle_graph.add_edge(bundle_roots[a].1, *bundle_id, 0);
}
}
} else if reachable.len() > 0 {
// 如果 asset 對(duì)于多個(gè) bundle 都是可訪問的
// 例如 shared.js(6) 就同時(shí)被 a.js(2) 和 b.js(5) 可訪問
// 這里可以根據(jù)這些 asset 組合為該 asset 創(chuàng)建一個(gè)新的 bundle
}
}
這一步的代碼邏輯會(huì)很復(fù)雜(從 rust 代碼的角度來看,畢竟筆者不是很懂 rust QAQ),因此這里筆者并沒有貼完所有的代碼。但我這里會(huì)結(jié)合具體的代碼來舉例講解一下這一步的具體操作:
首先這一步會(huì)遍歷所有的 asset ,找到那些目前還沒有形成 bundle 的 asset,如下圖所示,經(jīng)歷過前一個(gè)步驟之后,沒有形成 root bundle 的 asset 的只有 async2.js 以及 shared.js 。
圖片
這里針對(duì) async2.js 這個(gè) asset 來講解一下,對(duì)于 async2.js 來說,對(duì)它可訪問的 bundle 有 a.js (2)、async.js (3)。這里由于 async2.js 對(duì)于 async.js 的父 bundle a.js 同樣是可訪問的,這里在處理可訪問節(jié)點(diǎn)的時(shí)候會(huì)把 async.js 這個(gè) asset 給移除掉,相當(dāng)于 async2.js 實(shí)際可訪問的 bundle 只有 a.js ,這里這樣處理的作用是為了后續(xù) asset 的 bundle 合并操作,可以參考如下代碼:
for asset_id in g.node_indices() {
let reachable: Vec<NodeIndex> = reachable_graph.neighbors_directed(asset_id, Incoming).collect();
// 這一步篩掉 async2.js
let reachable: Vec<NodeIndex> = reachable.iter().cloned().filter(|b| {
(&reachable).into_iter().all(|a| !reachable_bundles.contains(&(*a, *b)))
}).collect();
}
在這一步篩出對(duì)應(yīng)可訪問的 bundle 之后(reachable),接下來就開始去繼續(xù)構(gòu)造 bundle_graph ,這上一步中 bundle_graph 中先暫時(shí)放置了一些 root_bundle 以及他們之間的引用關(guān)系。在這一步,對(duì)于沒有形成 bundle 的 asset 進(jìn)行繼續(xù)的完善。
這一步代碼比較復(fù)雜,筆者并沒有完全貼出來,大概處理過程分成了兩個(gè)分支:
- 處理成 root_bundle 的 bundle,需要將他們有依賴關(guān)系的 bundle_graph 添加上對(duì)應(yīng)的邊
- 處理 root_bundle 之外的 asset 為 bundle(async2.js 和 shared.js)
- 如果這個(gè) asset 被多個(gè) root_bundle 依賴(可訪問),那么會(huì)給這個(gè) asset 創(chuàng)建一個(gè)單獨(dú)的 bundle 并給它在 bundle_graph 加上對(duì)應(yīng)的邊,例如這里的 shared.js
- 如果這個(gè) asset 只被一個(gè) root_bundle 依賴(可訪問),那么會(huì)直接把這個(gè) asset 添加到 root_bundle 的 bundle 中組成一個(gè)新的 bundle,例如這里的 async2.js
for asset_id in g.node_indices() {
// ... 省略掉獲取 reacheable 這個(gè)變量過程
if let Some((bundle_id, _)) = bundle_roots.get(&asset_id) {
// 1. 處理 root_bundle,這一步可以理解為給 bundle_graph 添加邊
bundles.entry(vec![asset_id]).or_insert(*bundle_id);
for a in &reachable {
if *a != asset_id {
bundle_graph.add_edge(bundle_roots[a].1, *bundle_id, 0);
}
}
} else if reachable.len() > 0 {
// 2. 處理 root_bundle 之外的 asset 為 bundle
let source_bundles = reachable.iter().map(|a| bundles[&vec![*a]]).collect();
let bundle_id = bundles.entry(reachable.clone()).or_insert_with(|| {
let mut bundle = Bundle::default();
bundle.source_bundles = source_bundles;
bundle_graph.add_node(bundle)
});
let bundle = &mut bundle_graph[*bundle_id];
bundle.asset_ids.push(asset_id);
bundle.size += g[asset_id].size;
// 處理完 asset 為 bundle 之后,同樣要添加邊
for a in reachable {
if a != *bundle_id {
bundle_graph.add_edge(bundle_roots[&a].1, *bundle_id, 0);
}
}
}
}
}
這一步最后會(huì)形成一個(gè)這樣的 bundle_graph ,這里對(duì)每個(gè) bundle 進(jìn)行了重新標(biāo)號(hào),不同于之前的 asset_id
圖片
這里我們可以看到, 其中 async2.js 和 root_bundle a.js 形成了一個(gè)新的 bundle,而另外一個(gè) asset shared.js 也形成了一個(gè)單獨(dú)的 bundle,它們之間的依賴關(guān)系(graph 的邊)也能比較清晰的看到。
合并小的公共 bundle
上一步我們基本上將所有的 asset 都全處理成了 bundle 并成功構(gòu)建出了一個(gè) bundle_graph ,實(shí)際上現(xiàn)在構(gòu)建大頭基本上已經(jīng)完成了。
下面的步驟基本就是對(duì) bundle_graph 做一個(gè)優(yōu)化,這一步的處理對(duì)比前面的步驟就很簡(jiǎn)單了。
在開始介紹代碼之前,這一步我們可以理解為 webpack 的 chunkSplit 配置中的 splitChunks.minSize 。在 Parcel 也有對(duì)應(yīng)的配置可以參考,這里的 minBundleSize:
{
"@parcel/bundler-default": {
"minBundles": 1,
"minBundleSize": 3000,
"maxParallelRequests": 20
}
}
大致就是對(duì)小于 minBundleSize 的 shared_bundle 合并到對(duì)應(yīng)的可訪問 bundle 中去,這里的副作用是可能會(huì)導(dǎo)致在多個(gè) bundle 中存在一定體積的重復(fù) asset 體積(重復(fù)代碼),但帶來的好處則是可以減少請(qǐng)求數(shù)量,這里具體的取舍開始看用戶自身的配置。
這一步的代碼處理如下:
for bundle_id in bundle_graph.node_indices() {
let bundle = &bundle_graph[bundle_id];
// 當(dāng)這個(gè) bundle 本身為 commen bundle 且他本身的體積小于某個(gè)值,這里的 demo 默認(rèn)寫為了 10
// 那么則可以合并掉這個(gè) bundle
if bundle.source_bundles.len() > 0 && bundle.size < 10 {
remove_bundle(&g, &mut bundle_graph, bundle_id);
}
}
fn remove_bundle(
asset_graph: &Graph<Asset, Dependency>,
bundle_graph: &mut Graph<Bundle, i32>,
bundle_id: NodeIndex
) {
// 在 bundle_graph 中刪除掉對(duì)應(yīng)的 bundle
let bundle = bundle_graph.remove_node(bundle_id).unwrap();
// 并將該 bundle 合并對(duì)其有引用關(guān)系的 bundle 中去
for asset_id in &bundle.asset_ids {
for source_bundle_id in &bundle.source_bundles {
let bundle = &mut bundle_graph[*source_bundle_id];
bundle.asset_ids.push(*asset_id);
bundle.size += asset_graph[*asset_id].size;
}
}
合并超出并行請(qǐng)求限制的公共 bundle
在上一步處理完了最小體積的公共 bundle,這一步要處理的則是最大并行請(qǐng)求的 chunk,舉個(gè)例子,例如這里拆出來了 a.js 、common-a.js 、common-b.js 這三個(gè) bundle,并且 a.js 同時(shí)依賴 common-a.js 和 common-b.js,但現(xiàn)在用戶配置允許的最大 bundle 并行請(qǐng)求數(shù)目是 2,那么這里就會(huì)合并掉這兩個(gè) common bundle 中的其中一個(gè)到 a.js 中去。
這里在 Parcel 中也有對(duì)應(yīng)的配置項(xiàng),參考下面的 maxParallelRequests 這個(gè)參數(shù):
{
"@parcel/bundler-default": {
"minBundles": 1,
"minBundleSize": 3000,
"maxParallelRequests": 20
}
}
這一步的代碼處理可以參考如下:
// demo 這里默認(rèn)的限制請(qǐng)求數(shù)量為 3
let limit = 3;
for (_, (bundle_id, bundle_group_id)) in bundle_roots {
if bundle_id != bundle_group_id {
continue;
}
let mut neighbors: Vec<NodeIndex> = bundle_graph.neighbors(bundle_group_id).collect();
if neighbors.len() > limit {
neighbors.sort_by(|a, b| bundle_graph[*a].size.cmp(&bundle_graph[*b].size));
// Remove bundles until the bundle group is within the parallel request limit.
for bundle_id in &neighbors[0..neighbors.len() - limit] {
// Add all assets in the shared bundle into the source bundles that are within this bundle group.
let source_bundles: Vec<NodeIndex> = bundle_graph[*bundle_id].source_bundles.drain_filter(|s| neighbors.contains(s)).collect();
for source in source_bundles {
for asset_id in bundle_graph[*bundle_id].asset_ids.clone() {
let bundle_id = bundles[&vec![source]];
let bundle = &mut bundle_graph[bundle_id];
bundle.asset_ids.push(asset_id);
bundle.size += g[asset_id].size;
}
}
// Remove the edge from this bundle group to the shared bundle.
bundle_graph.remove_edge(bundle_graph.find_edge(bundle_group_id, *bundle_id).unwrap());
// If there is now only a single bundle group that contains this bundle,
// merge it into the remaining source bundles. If it is orphaned entirely, remove it.
let count = bundle_graph.neighbors_directed(*bundle_id, Incoming).count();
if count == 1 {
remove_bundle(&g, &mut bundle_graph, *bundle_id);
} else if count == 0 {
bundle_graph.remove_node(*bundle_id);
}
}
}
}
這里簡(jiǎn)單對(duì)代碼邏輯做個(gè)講解,其實(shí)這里的邏輯也比較好理解:
- 先找到對(duì)應(yīng)的 root_bundle(即這里的 bundle_group_id ),因?yàn)橐话阒挥?nbsp;root_bundle 會(huì)有多個(gè) bundle 并行請(qǐng)求。
- 找出包括 root_bundle (不包括 root_bundle)依賴的所有 bundle(即 neighbors 變量),如果這個(gè)數(shù)目大于這里設(shè)置的限制(即 limit),按照neighbors 中的所有 bundle 體積大小,把體積小的 bundle 都合并到其對(duì)應(yīng)的 source_bundles 中去(這里不一定只有 root_bundle,還可能會(huì)有其他的 bundle)。一直合并到這些 bundle 的數(shù)目小于請(qǐng)求限制即可。
- 合并到最后,如果只剩下唯一一個(gè) bundle 了,那么直接把這個(gè) bundle 也給合并進(jìn)剩下的 source_bundle 中去。
總結(jié)
在走完最后兩步的 bundle 合并之后,那么整個(gè) Rust Demo 的打包算法流程就結(jié)束了,實(shí)際上這個(gè) Demo 給的資源樣例并沒有走最后的兩步:
圖片
按照最后的 bundle_graph 生成的圖我們可以看到:
- 最小的 common_bundle 體積也要大于 10
- 最大的并行請(qǐng)求數(shù)目也并沒有超過 3 個(gè)(圖中的幾個(gè) bundle 之間還有依賴關(guān)系,這里并沒有標(biāo)注出來)
實(shí)際上最后生成的整體 bundle 也大概如圖所示。
不過這整個(gè)流程由于筆者對(duì)于 Rust 代碼并不是很熟悉,因此中間可能會(huì)有些疏漏,不過整體流程來看應(yīng)該并沒有特別大的出入。