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
19use colored::Colorize;
20
21use crate::stack_end;
22use anyhow::{Context, Result, anyhow};
23use gear_wasm_instrument::{Module, STACK_END_EXPORT_NAME};
24use std::{
25    fs::{self, metadata},
26    path::{Path, PathBuf},
27    process::Command,
28};
29
30pub const FUNC_EXPORTS: [&str; 4] = ["init", "handle", "handle_reply", "handle_signal"];
31
32const OPTIMIZED_EXPORTS: [&str; 7] = [
33    "handle",
34    "handle_reply",
35    "handle_signal",
36    "init",
37    "state",
38    "metahash",
39    STACK_END_EXPORT_NAME,
40];
41
42pub struct Optimizer {
43    module: Module,
44}
45
46impl Optimizer {
47    pub fn new(file: &PathBuf) -> Result<Self> {
48        let contents = fs::read(file)
49            .with_context(|| format!("Failed to read file by optimizer: {file:?}"))?;
50        let module = Module::new(&contents).with_context(|| format!("File path: {file:?}"))?;
51        Ok(Self { module })
52    }
53
54    pub fn insert_start_call_in_export_funcs(&mut self) -> Result<(), &'static str> {
55        stack_end::insert_start_call_in_export_funcs(&mut self.module)
56    }
57
58    pub fn move_mut_globals_to_static(&mut self) -> Result<(), &'static str> {
59        stack_end::move_mut_globals_to_static(&mut self.module)
60    }
61
62    pub fn insert_stack_end_export(&mut self) -> Result<(), &'static str> {
63        stack_end::insert_stack_end_export(&mut self.module)
64    }
65
66    /// Strips all custom sections.
67    ///
68    /// Presently all custom sections are not required so they can be stripped
69    /// safely. The name section is already stripped by `wasm-opt`.
70    pub fn strip_custom_sections(&mut self) {
71        // we also should strip `reloc` section
72        // if it will be present in the module in the future
73        self.module.custom_sections = None;
74        self.module.name_section = None;
75    }
76
77    /// Keeps only allowlisted exports.
78    pub fn strip_exports(&mut self) {
79        if let Some(export_section) = self.module.export_section.as_mut() {
80            let exports = OPTIMIZED_EXPORTS.map(str::to_string).to_vec();
81
82            export_section.retain(|export| exports.contains(&export.name.to_string()));
83        }
84    }
85
86    pub fn serialize(&self) -> Result<Vec<u8>> {
87        self.module
88            .serialize()
89            .context("Failed to serialize module")
90    }
91
92    pub fn flush_to_file(self, path: &PathBuf) {
93        fs::write(path, self.module.serialize().unwrap()).unwrap();
94    }
95}
96
97pub struct OptimizationResult {
98    pub original_size: f64,
99    pub optimized_size: f64,
100}
101
102/// Attempts to perform optional Wasm optimization using `binaryen`.
103///
104/// The intention is to reduce the size of bloated Wasm binaries as a result of
105/// missing optimizations (or bugs?) between Rust and Wasm.
106pub fn optimize_wasm<P: AsRef<Path>>(
107    source: P,
108    destination: P,
109    optimization_passes: &str,
110    keep_debug_symbols: bool,
111) -> Result<OptimizationResult> {
112    let original_size = metadata(&source)?.len() as f64 / 1000.0;
113
114    do_optimization(
115        &source,
116        &destination,
117        optimization_passes,
118        keep_debug_symbols,
119    )?;
120
121    let destination = destination.as_ref();
122    if !destination.exists() {
123        return Err(anyhow!(
124            "Optimization failed, optimized wasm output file `{}` not found.",
125            destination.display()
126        ));
127    }
128
129    let optimized_size = metadata(destination)?.len() as f64 / 1000.0;
130
131    Ok(OptimizationResult {
132        original_size,
133        optimized_size,
134    })
135}
136
137/// Optimizes the Wasm supplied as `crate_metadata.dest_wasm` using
138/// the `wasm-opt` binary.
139///
140/// The supplied `optimization_level` denotes the number of optimization passes,
141/// resulting in potentially a lot of time spent optimizing.
142///
143/// If successful, the optimized Wasm is written to `dest_optimized`.
144pub fn do_optimization<P: AsRef<Path>>(
145    dest_wasm: P,
146    dest_optimized: P,
147    optimization_level: &str,
148    keep_debug_symbols: bool,
149) -> Result<()> {
150    // check `wasm-opt` is installed
151    let which = which::which("wasm-opt");
152    if which.is_err() {
153        return Err(anyhow!(
154            "wasm-opt not found! Make sure the binary is in your PATH environment.\n\n\
155            We use this tool to optimize the size of your program's Wasm binary.\n\n\
156            wasm-opt is part of the binaryen package. You can find detailed\n\
157            installation instructions on https://github.com/WebAssembly/binaryen#tools.\n\n\
158            There are ready-to-install packages for many platforms:\n\
159            * Debian/Ubuntu: apt-get install binaryen\n\
160            * Homebrew: brew install binaryen\n\
161            * Arch Linux: pacman -S binaryen\n\
162            * Windows: binary releases at https://github.com/WebAssembly/binaryen/releases"
163                .bright_yellow()
164        ));
165    }
166    let wasm_opt_path = which
167        .as_ref()
168        .expect("we just checked if `which` returned an err; qed")
169        .as_path();
170    log::info!("Path to wasm-opt executable: {}", wasm_opt_path.display());
171
172    log::info!("Optimization level passed to wasm-opt: {optimization_level}");
173    let mut command = Command::new(wasm_opt_path);
174    command
175        .arg(dest_wasm.as_ref())
176        .arg(format!("-O{optimization_level}"))
177        .arg("-o")
178        .arg(dest_optimized.as_ref())
179        .arg("-mvp")
180        .arg("--enable-sign-ext")
181        .arg("--enable-mutable-globals")
182        .arg("--limit-segments")
183        .arg("--pass-arg=limit-segments@1024")
184        // the memory in our module is imported, `wasm-opt` needs to be told that
185        // the memory is initialized to zeroes, otherwise it won't run the
186        // memory-packing pre-pass.
187        .arg("--zero-filled-memory")
188        .arg("--dae")
189        .arg("--vacuum");
190    if keep_debug_symbols {
191        command.arg("-g");
192    }
193    log::info!("Invoking wasm-opt with {command:?}");
194    let output = command.output().unwrap();
195
196    if !output.status.success() {
197        let err = std::str::from_utf8(&output.stderr)
198            .expect("Cannot convert stderr output of wasm-opt to string")
199            .trim();
200        panic!(
201            "The wasm-opt optimization failed.\n\n\
202            The error which wasm-opt returned was: \n{err}"
203        );
204    }
205    Ok(())
206}