gear_wasm_optimizer/
optimize.rs

1// This file is part of Gear.
2
3// Copyright (C) 2022-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19#[cfg(not(feature = "wasm-opt"))]
20use colored::Colorize;
21#[cfg(not(feature = "wasm-opt"))]
22use std::process::Command;
23
24#[cfg(feature = "wasm-opt")]
25use wasm_opt::{OptimizationOptions, Pass};
26
27use crate::stack_end;
28use anyhow::{anyhow, Context, Result};
29use gear_wasm_instrument::{Module, STACK_END_EXPORT_NAME};
30use std::{
31    fs::{self, metadata},
32    path::{Path, PathBuf},
33};
34
35pub const FUNC_EXPORTS: [&str; 4] = ["init", "handle", "handle_reply", "handle_signal"];
36
37const OPTIMIZED_EXPORTS: [&str; 7] = [
38    "handle",
39    "handle_reply",
40    "handle_signal",
41    "init",
42    "state",
43    "metahash",
44    STACK_END_EXPORT_NAME,
45];
46
47pub struct Optimizer {
48    module: Module,
49}
50
51impl Optimizer {
52    pub fn new(file: &PathBuf) -> Result<Self> {
53        let contents = fs::read(file)
54            .with_context(|| format!("Failed to read file by optimizer: {file:?}"))?;
55        let module = Module::new(&contents).with_context(|| format!("File path: {file:?}"))?;
56        Ok(Self { module })
57    }
58
59    pub fn insert_start_call_in_export_funcs(&mut self) -> Result<(), &'static str> {
60        stack_end::insert_start_call_in_export_funcs(&mut self.module)
61    }
62
63    pub fn move_mut_globals_to_static(&mut self) -> Result<(), &'static str> {
64        stack_end::move_mut_globals_to_static(&mut self.module)
65    }
66
67    pub fn insert_stack_end_export(&mut self) -> Result<(), &'static str> {
68        stack_end::insert_stack_end_export(&mut self.module)
69    }
70
71    /// Strips all custom sections.
72    ///
73    /// Presently all custom sections are not required so they can be stripped
74    /// safely. The name section is already stripped by `wasm-opt`.
75    pub fn strip_custom_sections(&mut self) {
76        // we also should strip `reloc` section
77        // if it will be present in the module in the future
78        self.module.custom_section = None;
79        self.module.name_section = None;
80    }
81
82    /// Keeps only allowlisted exports.
83    pub fn strip_exports(&mut self) {
84        if let Some(export_section) = self.module.export_section.as_mut() {
85            let exports = OPTIMIZED_EXPORTS.map(str::to_string).to_vec();
86
87            export_section.retain(|export| exports.contains(&export.name.to_string()));
88        }
89    }
90
91    pub fn serialize(&self) -> Result<Vec<u8>> {
92        self.module
93            .serialize()
94            .context("Failed to serialize module")
95    }
96
97    pub fn flush_to_file(self, path: &PathBuf) {
98        fs::write(path, self.module.serialize().unwrap()).unwrap();
99    }
100}
101
102pub struct OptimizationResult {
103    pub original_size: f64,
104    pub optimized_size: f64,
105}
106
107/// Attempts to perform optional Wasm optimization using `binaryen`.
108///
109/// The intention is to reduce the size of bloated Wasm binaries as a result of
110/// missing optimizations (or bugs?) between Rust and Wasm.
111pub fn optimize_wasm<P: AsRef<Path>>(
112    source: P,
113    destination: P,
114    optimization_passes: &str,
115    keep_debug_symbols: bool,
116) -> Result<OptimizationResult> {
117    let original_size = metadata(&source)?.len() as f64 / 1000.0;
118
119    do_optimization(
120        &source,
121        &destination,
122        optimization_passes,
123        keep_debug_symbols,
124    )?;
125
126    let destination = destination.as_ref();
127    if !destination.exists() {
128        return Err(anyhow!(
129            "Optimization failed, optimized wasm output file `{}` not found.",
130            destination.display()
131        ));
132    }
133
134    let optimized_size = metadata(destination)?.len() as f64 / 1000.0;
135
136    Ok(OptimizationResult {
137        original_size,
138        optimized_size,
139    })
140}
141
142#[cfg(not(feature = "wasm-opt"))]
143/// Optimizes the Wasm supplied as `crate_metadata.dest_wasm` using
144/// the `wasm-opt` binary.
145///
146/// The supplied `optimization_level` denotes the number of optimization passes,
147/// resulting in potentially a lot of time spent optimizing.
148///
149/// If successful, the optimized Wasm is written to `dest_optimized`.
150pub fn do_optimization<P: AsRef<Path>>(
151    dest_wasm: P,
152    dest_optimized: P,
153    optimization_level: &str,
154    keep_debug_symbols: bool,
155) -> Result<()> {
156    // check `wasm-opt` is installed
157    let which = which::which("wasm-opt");
158    if which.is_err() {
159        return Err(anyhow!(
160            "wasm-opt not found! Make sure the binary is in your PATH environment.\n\n\
161            We use this tool to optimize the size of your program's Wasm binary.\n\n\
162            wasm-opt is part of the binaryen package. You can find detailed\n\
163            installation instructions on https://github.com/WebAssembly/binaryen#tools.\n\n\
164            There are ready-to-install packages for many platforms:\n\
165            * Debian/Ubuntu: apt-get install binaryen\n\
166            * Homebrew: brew install binaryen\n\
167            * Arch Linux: pacman -S binaryen\n\
168            * Windows: binary releases at https://github.com/WebAssembly/binaryen/releases"
169                .bright_yellow()
170        ));
171    }
172    let wasm_opt_path = which
173        .as_ref()
174        .expect("we just checked if `which` returned an err; qed")
175        .as_path();
176    log::info!("Path to wasm-opt executable: {}", wasm_opt_path.display());
177
178    log::info!(
179        "Optimization level passed to wasm-opt: {}",
180        optimization_level
181    );
182    let mut command = Command::new(wasm_opt_path);
183    command
184        .arg(dest_wasm.as_ref())
185        .arg(format!("-O{optimization_level}"))
186        .arg("-o")
187        .arg(dest_optimized.as_ref())
188        .arg("-mvp")
189        .arg("--enable-sign-ext")
190        .arg("--enable-mutable-globals")
191        // the memory in our module is imported, `wasm-opt` needs to be told that
192        // the memory is initialized to zeroes, otherwise it won't run the
193        // memory-packing pre-pass.
194        .arg("--zero-filled-memory")
195        .arg("--dae")
196        .arg("--vacuum");
197    if keep_debug_symbols {
198        command.arg("-g");
199    }
200    log::info!("Invoking wasm-opt with {:?}", command);
201    let output = command.output().unwrap();
202
203    if !output.status.success() {
204        let err = std::str::from_utf8(&output.stderr)
205            .expect("Cannot convert stderr output of wasm-opt to string")
206            .trim();
207        panic!(
208            "The wasm-opt optimization failed.\n\n\
209            The error which wasm-opt returned was: \n{err}"
210        );
211    }
212    Ok(())
213}
214
215#[cfg(feature = "wasm-opt")]
216/// Optimizes the Wasm supplied as `crate_metadata.dest_wasm` using
217/// `wasm-opt`.
218///
219/// The supplied `optimization_level` denotes the number of optimization passes,
220/// resulting in potentially a lot of time spent optimizing.
221///
222/// If successful, the optimized Wasm is written to `dest_optimized`.
223pub fn do_optimization<P: AsRef<Path>>(
224    dest_wasm: P,
225    dest_optimized: P,
226    optimization_level: &str,
227    keep_debug_symbols: bool,
228) -> Result<()> {
229    log::info!(
230        "Optimization level passed to wasm-opt: {}",
231        optimization_level
232    );
233    match optimization_level {
234        "0" => OptimizationOptions::new_opt_level_0(),
235        "1" => OptimizationOptions::new_opt_level_1(),
236        "2" => OptimizationOptions::new_opt_level_2(),
237        "3" => OptimizationOptions::new_opt_level_3(),
238        "4" => OptimizationOptions::new_opt_level_4(),
239        "s" => OptimizationOptions::new_optimize_for_size(),
240        "z" => OptimizationOptions::new_optimize_for_size_aggressively(),
241        _ => panic!("Invalid optimization level {}", optimization_level),
242    }
243    .mvp_features_only()
244    .enable_feature(wasm_opt::Feature::SignExt)
245    .enable_feature(wasm_opt::Feature::MutableGlobals)
246    .shrink_level(wasm_opt::ShrinkLevel::Level2)
247    .add_pass(Pass::Dae)
248    .add_pass(Pass::Vacuum)
249    // the memory in our module is imported, `wasm-opt` needs to be told that
250    // the memory is initialized to zeroes, otherwise it won't run the
251    // memory-packing pre-pass.
252    .zero_filled_memory(true)
253    .debug_info(keep_debug_symbols)
254    .run(dest_wasm, dest_optimized)?;
255
256    Ok(())
257}