A Wasm size profiler for MoonBit — section breakdown, function ranking, call graph and retained-size analysis
moon add BigSaltyMan/moonsize@0.5.1moon build --target native --release
./_build/native/release/build/cmd/main/main.exe app.wasmmoonsize app.wasm --data-segments --top 20
moonsize app.wasm --data-json new-data.json
moonsize app.wasm --html new-report.html --data-json new-data.jsonnpm run devusage: moonsize <file.wasm> [--top <n>] [--retained] [--dead-code]
[--compress] [--strict] [--call-graph <path>] [--html <path>]
[--max-size <size>] [--baseline <path.wasm>] [--diff-functions]
[--data-segments] [--diff-data] [--data-json <new-path.json>]
[--budget <config.json>] [--budget-json <path.json>] [--budget-md <path.md>]
[--experiment <report.html|report.json>] [--experiment-context <context.json>]
moonsize <file.wasm> --why-index <n> [--why-root <n>] [--why-json <path.json>] [--strict]
Report the section table of a WebAssembly binary, rank its heaviest
functions, roll the bytes up by module, and follow the call graph to
identify candidates for investigation.
--top <n> how many functions to rank (default 10)
--retained dominated function bytes in the current graph model
--dead-code functions no root can reach
--compress whole-file gzip and independent section gzip sizes
--data-segments append data-entry file bytes (payload-ranked by --top)
--data-json <path> enable details and write all entries to a new .json file
offset expressions are recorded, not evaluated
--diff-data compare exact payload content groups; requires --baseline
--data-json then writes the data comparison format
--strict refuse to report when the call graph is incomplete,
instead of reporting it with a warning
--call-graph <path> write a new .dot or .json call graph; refuses existing paths
--html <path> write a new self-contained HTML report; refuses existing paths
--data-segments or --data-json adds current-input data details
--max-size <size> fail with exit code 3 above this size, where size is
bytes or a KB/MB/GB count such as 10KB or 1.5MB
--budget <path> validated multi-metric byte budgets; growth needs --baseline
--budget-json <path> write budget JSON; requires --budget, refuses existing paths
--budget-md <path> write CI Markdown summary; requires --budget, refuses existing paths
--baseline <path> compare against another module and print the change;
--max-size then bounds the growth, not the file
--diff-functions append function and package changes; requires --baseline
--experiment <path> write measured before/after results; requires --baseline
refuses to overwrite an existing path
--experiment-context <path> attach declared build settings and validation
--why-index <n> explain a shortest model reference path to function n
--why-root <n> search from one actual model root (requires --why-index)
--why-json <path> write path JSON; refuses existing paths
query mode allows only these flags and --strict
element functions are conservative roots; paths are
not execution traces or predictions of savings
All report outputs refuse existing files, directories and link aliases.
Targets are checked together; each file is created exclusively. Output errors use exit 1.
Multiple files are not a transaction; a later failure may leave earlier new outputs.
Exit codes: 0 completed, 1 input/query/output/unavailable measurement, 2 bad arguments/config,
3 over budget, incomplete strict graph, or path search limited.moonsize app.wasm --retained --dead-code # the full text report
moonsize app.wasm --html report.html # charts, opens in a browser
moonsize app.wasm --call-graph graph.dot # for Graphvizmoonsize after.wasm --baseline before.wasm --experiment experiment.html \
--experiment-context context.json --top 20moonsize after.wasm --baseline before.wasm --budget moonsize-budget.json \
--budget-json budget-result.json --budget-md budget-summary.mdnode scripts/demo-budget.mjs _build/native/release/build/cmd/main/main.exe
_build/native/release/build/cmd/main/main.exe _build/budget-demo/after.wasm \
--baseline _build/budget-demo/before.wasm --budget examples/budget.jsonmoonsize app.wasm --why-index 47
moonsize app.wasm --why-index 47 --why-root 0
moonsize app.wasm --why-index 47 --why-json function-path.json --strictnode scripts/test-function-path.mjs _build/native/release/build/cmd/main/main.exe
node scripts/demo-function-path.mjs _build/native/release/build/cmd/main/main.exefn main {
let bytes : Bytes = read("app.wasm")
match @moonsize.parse_module(bytes) {
Ok(parsed) =>
match @moonsize.analyze(bytes, parsed) {
Ok(analysis) => {
let options = @moonsize.Options::default("app.wasm")
// What the five heaviest functions cost, as text or as numbers:
// `analysis.stats` and `analysis.retained` are there to be read.
println(@moonsize.render_top_functions(analysis.stats, analysis.data.length(), 5))
println(@moonsize.render_graph_warning(analysis.graph))
println(@moonsize.render_dead_code(analysis, 5))
println(@moonsize.render_retained(analysis, 5))
println(@moonsize.render_html(analysis, options.path))
}
Err(error) => println(@moonsize.describe_error(error))
}
Err(error) => println(@moonsize.describe_error(error))
}
}WARNING: the call graph has 1 analysis gap.
function 0, offset 42: call_ref: reference-call target analysis is not implemented.
Dead-code and retained-size results may be inaccurate; do not use them as proof that a function can be removed.| Variant | Fields |
|---|---|
| UnsupportedVersion | version-field offset, raw unsigned UInt value |
| DuplicateSection | second section's id-byte offset, kind |
| InvalidSectionOrder | current id-byte offset, kind, preceding standard kind |
| FunctionCodeCountMismatch | related section's id-byte offset, kind, local declaration count, body count |
| TrailingSectionBytes | first unconsumed payload byte offset, kind |
| Variant | Stable JSON code |
|---|---|
| NoStaticTableTargets | no_static_table_targets |
| UnmodeledReferenceCall | unmodeled_reference_call |
| ImportedTableContents | imported_table_contents |
| ExportedTableContents | exported_table_contents |
| RuntimeTableMutation | runtime_table_mutation |
| UnmodeledTableInitializer | unmodeled_table_initializer |
| UnknownTableContents | unknown_table_contents |
COMPRESSED SIZE
raw 10675 B
gzip 5207 B (48.7% of raw, ratio 2.05x)
BY SECTION — INDEPENDENT GZIP STREAMS
SECTION RAW GZIP RATIO
code 5166 2712 1.90x
custom (name) 4975 1983 2.50x
data 252 174 1.44x
custom (producers) 71 95 0.74x
type 59 66 0.89x
function 50 63 0.79x
import 37 61 0.60x
export 21 45 0.46x
global 13 34 0.38x
element 8 32 0.25x
table 7 31 0.22x
memory 5 29 0.17x
datacount 3 27 0.11x
Section gzip sizes are measured independently and are not additive.
They do not represent shares of the whole-file gzip size.Retained size
# INDEX SIZE RETAINED DIES SHARE IND FUNCTION
1 47 158 5162 46 48.3% ____moonbit__main
2 39 301 1773 10 16.6% int::Int::to__string_2einner
3 37 24 887 8 8.3% println
4 34 9 819 5 7.6% moonbit.println
5 33 206 810 4 7.5% moonbit.fprintln
6 28 49 717 7 6.7% moonbit.decref
7 29 409 668 6 6.2% moonbit.gc.free
8 43 633 633 0 5.9% int__to__string__dec
DIES counts the other functions that become unreachable with this one.
IND marks a function an indirect call could reach.
Dead code
0 of 47 functions are unreachable from the roots
0 bytes (0.0% of file)
every function is reachablemoonsize examples/fib.wasm --html new-report.html

moonsize app.wasm --max-size 100KB # a ceiling on the file
moonsize app.wasm --baseline main.wasm --max-size 5KB # at most 5 KB of growthSIZE COMPARISON
baseline current delta
total 10675 11039 +364 (+3.4%)
code 5166 5299 +133 (+2.5%)
custom (name) 4975 5205 +230 (+4.6%)
data 252 252 0 (0.0%)
custom (producers) 71 71 0 (0.0%)
... baseline current delta
custom (x) 10 13 +3 (+30.0%)moonsize current.wasm --baseline previous.wasm --diff-functions --top 20package encoded-byte sums = local function total_size sums
code section = function encoding + code overhead (header/count encoding)
function delta = matched delta + added bytes - removed bytes
+ unmatched current bytes - unmatched baseline bytesmoon build --target native --release
bash scripts/demo-function-diff.sh _build/native/release/build/cmd/main/main.exe- uses: ./.github/actions/setup-moonbit
- name: Build the example and CLI
run: |
moon build --target wasm
moon build --target native --release
- name: The example stays inside its budget
env:
EXAMPLE: _build/wasm/debug/build/examples/examples.wasm
CLI: _build/native/release/build/cmd/main/main.exe
run: |
test -s "$EXAMPLE"
printf 'budget input: %s\nbytes: %s\n' "$EXAMPLE" "$(wc -c < "$EXAMPLE")"
sha256sum "$EXAMPLE"
"$CLI" "$EXAMPLE" --max-size 100KB- name: Setup MoonBit
run: |
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s '0.10.13+cbb11c36f'
echo "$HOME/.moon/bin" >> "$GITHUB_PATH"
- run: moon update
- run: moon build --target wasm --release
- name: Size budget
run: moonsize _build/wasm/release/build/app/app.wasm --max-size 250KB- name: Build the base branch
run: |
git worktree add "$RUNNER_TEMP/base" "origin/${{ github.base_ref }}"
(cd "$RUNNER_TEMP/base" && moon build --target wasm --release)
- name: Size budget
run: |
moonsize _build/wasm/release/build/app/app.wasm \
--baseline "$RUNNER_TEMP/base/_build/wasm/release/build/app/app.wasm" \
--max-size 5KBmoon check --target native --deny-warn
moon test
moon build --target native --release
bash scripts/test-tail-calls.sh _build/native/release/build/cmd/main/main.exe
bash scripts/test-graph-diagnostics.sh _build/native/release/build/cmd/main/main.exe
bash scripts/test-parser-boundaries.sh _build/native/release/build/cmd/main/main.exemoon build --target wasm
stage=$(mktemp -d examples/report-update.XXXXXX)
cp _build/wasm/debug/build/examples/examples.wasm "$stage/fib.wasm"
moon run cmd/main -- \
--html "$stage/report.html" \
"$stage/fib.wasm"
sha256sum _build/wasm/debug/build/examples/examples.wasm "$stage/fib.wasm"cp "$stage/fib.wasm" examples/fib.wasm
cp "$stage/report.html" examples/report.htmlpub struct Analysis {
data : Bytes
parsed : WasmModule
stats : Array[FunctionStat]
graph : CallGraph
roots : Set[Int]
reachable : Set[Int]
retained : Array[RetainedSize]
indirect : Set[Int]
graph_complete : Bool
unresolved_count : Int
}pub struct BudgetArtifact {
sha256 : String
raw_bytes : Int
code_bytes : Int
gzip : Result[Int, String]?
coverage : BudgetCoverage?
}pub struct BudgetCoverage {
functions : Int
function_bytes : Int
unattributed_functions : Int
unattributed_bytes : Int
}pub struct BudgetReport {
config_version : Int
current : BudgetArtifact
baseline : BudgetArtifact?
rules : Array[BudgetRuleResult]
status : BudgetStatus
exit_code : Int
}pub struct BudgetRuleResult {
id : String
metric : String
package_name : String?
mode : String
current : Int?
baseline : Int?
delta : Int?
max_bytes : Int
status : BudgetStatus
reason : String
current_package_present : Bool?
baseline_package_present : Bool?
}pub struct CallGraph {
calls : Map[Int, Set[Int]]
called_by : Map[Int, Set[Int]]
indirect_sites : Array[IndirectSite]
unresolved_indirect : Array[IndirectSite]
diagnostics : Array[GraphDiagnostic]
edges : Array[CallEdge]
}pub enum CallKind {
Direct(Int)
Indirect(Int)
Reference
}pub struct CodeSizeSummary {
code_size : Int
function_count : Int
function_bytes : Int
overhead : Int
}type CompiledBudgetpub struct DataPayloadGroup {
id : Int
sha256 : String
payload_length : Int
kind : DataPayloadGroupKind
baseline_indices : Array[Int]
current_indices : Array[Int]
baseline : DataGroupTotals
current : DataGroupTotals
delta : DataGroupTotals
}pub struct DataSegment {
index : Int
mode : DataSegmentMode
memory_index : Int?
encoding : DataByteRange
payload : DataByteRange
offset_expression : DataByteRange?
mode_size : Int
memory_index_size : Int
payload_length_size : Int
} derive(Eq, Debug)pub struct DataSegmentsComparison {
baseline_sha256 : String
current_sha256 : String
baseline : DataSegmentsReport
current : DataSegmentsReport
groups : Array[DataPayloadGroup]
delta : DataSegmentsDelta
}pub struct DataSegmentsReport {
file_size : Int
section : Section?
datacount : Int?
section_size : Int
header_size : Int
count_size : Int
payload_size : Int
entry_metadata_size : Int
entries : Array[DataSegment]
}pub struct Export {
name : String
kind : Int
index : Int
}pub struct FunctionBody {
index : Int
body_size : Int
total_size : Int
offset : Int
}pub enum FunctionMatchStatus {
Matched
AddedSymbol
RemovedSymbol
AmbiguousSymbol
MissingSymbol
} derive(Eq)pub enum FunctionPathError {
PathInputError(WasmError)
InvalidPathTarget(Int, Int)
InvalidPathRoot(Int)
InvalidPathLimits(String)
InvalidPathRootReference(Int, Int)
InvalidPathEdgeIndex(Int, Int)
InvalidPathEdgeEvidence(Int)
}pub struct FunctionPathNode {
index : Int
name : String
raw_name : String?
imported : Bool
encoded_size : Int?
import_module : String?
import_name : String?
}pub struct FunctionPathReport {
target : FunctionPathNode
requested_root : Int?
roots : Array[FunctionRoot]
selected_roots : Array[Int]
status : FunctionPathStatus
path_root : Int?
nodes : Array[FunctionPathNode]
steps : Array[FunctionPathStep]
graph_complete : Bool
diagnostics : Array[GraphDiagnostic]
graph_warning : String
limits : PathLimits
search_stats : PathSearchStats
}pub struct FunctionRootReason {
function_index : Int
kind : FunctionRootKind
name : String?
section_offset : Int
ordinal : Int?
table_index : Int?
element_mode : String?
}pub struct FunctionSizeChange {
status : FunctionMatchStatus
raw_name : String?
baseline : FunctionStat?
current : FunctionStat?
delta : Int
}pub struct FunctionStat {
index : Int
name : String
raw_name : String?
module_name : String
body_size : Int
total_size : Int
offset : Int
}pub struct GraphDiagnostic {
kind : GraphDiagnosticKind
caller : Int
offset : Int
opcode : Opcode
type_index : Int?
table_index : Int?
source : GraphDiagnosticSource?
}pub enum GraphDiagnosticKind {
NoStaticTableTargets
UnmodeledReferenceCall
ImportedTableContents
ExportedTableContents
RuntimeTableMutation
UnmodeledTableInitializer
UnknownTableContents
}pub struct IndirectSite {
caller : Int
table_index : Int
type_index : Int
offset : Int
candidates : Array[Int]
}pub struct MatchingSummary {
matched_baseline : FunctionCoverage
matched_current : FunctionCoverage
added_symbols : FunctionCoverage
removed_symbols : FunctionCoverage
ambiguous_baseline : FunctionCoverage
ambiguous_current : FunctionCoverage
missing_baseline : FunctionCoverage
missing_current : FunctionCoverage
matched_delta : Int
}pub struct ModuleSizeDiff {
baseline_size : Int
current_size : Int
delta : Int
baseline_code : CodeSizeSummary
current_code : CodeSizeSummary
functions : Array[FunctionSizeChange]
packages : Array[PackageSizeChange]
matching : MatchingSummary
}pub enum Opcode {
Unreachable
Nop
Block
Loop
If
Else
Try
Catch
Throw
Rethrow
ThrowRef
End
Br
BrIf
BrTable
Return
Call
CallIndirect
ReturnCall
ReturnCallIndirect
CallRef
ReturnCallRef
Drop
Select
Delegate
CatchAll
SelectT
LocalGet
LocalSet
LocalTee
GlobalGet
GlobalSet
TableGet
TableSet
I32Load
I64Load
F32Load
F64Load
I32Load8S
I32Load8U
I32Load16S
I32Load16U
I64Load8S
I64Load8U
I64Load16S
I64Load16U
I64Load32S
I64Load32U
I32Store
I64Store
F32Store
F64Store
I32Store8
I32Store16
I64Store8
I64Store16
I64Store32
MemorySize
MemoryGrow
I32Const
I64Const
F32Const
F64Const
I32Eqz
I32Eq
I32Ne
I32LtS
I32LtU
I32GtS
I32GtU
I32LeS
I32LeU
I32GeS
I32GeU
I64Eqz
I64Eq
I64Ne
I64LtS
I64LtU
I64GtS
I64GtU
I64LeS
I64LeU
I64GeS
I64GeU
F32Eq
F32Ne
F32Lt
F32Gt
F32Le
F32Ge
F64Eq
F64Ne
F64Lt
F64Gt
F64Le
F64Ge
I32Clz
I32Ctz
I32Popcnt
I32Add
I32Sub
I32Mul
I32DivS
I32DivU
I32RemS
I32RemU
I32And
I32Or
I32Xor
I32Shl
I32ShrS
I32ShrU
I32Rotl
I32Rotr
I64Clz
I64Ctz
I64Popcnt
I64Add
I64Sub
I64Mul
I64DivS
I64DivU
I64RemS
I64RemU
I64And
I64Or
I64Xor
I64Shl
I64ShrS
I64ShrU
I64Rotl
I64Rotr
F32Abs
F32Neg
F32Ceil
F32Floor
F32Trunc
F32Nearest
F32Sqrt
F32Add
F32Sub
F32Mul
F32Div
F32Min
F32Max
F32Copysign
F64Abs
F64Neg
F64Ceil
F64Floor
F64Trunc
F64Nearest
F64Sqrt
F64Add
F64Sub
F64Mul
F64Div
F64Min
F64Max
F64Copysign
I32WrapI64
I32TruncF32S
I32TruncF32U
I32TruncF64S
I32TruncF64U
I64ExtendI32S
I64ExtendI32U
I64TruncF32S
I64TruncF32U
I64TruncF64S
I64TruncF64U
F32ConvertI32S
F32ConvertI32U
F32ConvertI64S
F32ConvertI64U
F32DemoteF64
F64ConvertI32S
F64ConvertI32U
F64ConvertI64S
F64ConvertI64U
F64PromoteF32
I32ReinterpretF32
I64ReinterpretF64
F32ReinterpretI32
F64ReinterpretI64
I32Extend8S
I32Extend16S
I64Extend8S
I64Extend16S
I64Extend32S
RefNull
RefIsNull
RefFunc
RefEq
RefAsNonNull
BrOnNull
BrOnNonNull
I32TruncSatF32S
I32TruncSatF32U
I32TruncSatF64S
I32TruncSatF64U
I64TruncSatF32S
I64TruncSatF32U
I64TruncSatF64S
I64TruncSatF64U
MemoryInit
DataDrop
MemoryCopy
MemoryFill
TableInit
ElemDrop
TableCopy
TableGrow
TableSize
TableFill
StructNew
StructNewDefault
StructGet
StructGetS
StructGetU
StructSet
ArrayNew
ArrayNewDefault
ArrayNewFixed
ArrayNewData
ArrayNewElem
ArrayGet
ArrayGetS
ArrayGetU
ArraySet
ArrayLen
ArrayFill
ArrayCopy
ArrayInitData
ArrayInitElem
RefTest
RefTestNull
RefCast
RefCastNull
BrOnCast
BrOnCastFail
AnyConvertExtern
ExternConvertAny
RefI31
I31GetS
I31GetU
}pub enum Operands {
None_
BlockType
Label
LabelTable
FunctionIndex
CallIndirect
LocalIndex
GlobalIndex
TableIndex
TagIndex
MemoryIndex
MemoryArg
I32Const
I64Const
F32Const
F64Const
HeapType
RefType
TypeIndex
TypeFieldPair
TypePair
TypeCountPair
TypeDataPair
TypeElemPair
DataIndex
ElemIndex
DataMemoryPair
MemoryPair
ElemTablePair
TablePair
CastPair
TypeVector
}pub struct Options {
path : String
top : Int
retained : Bool
dead_code : Bool
call_graph : String?
html : String?
max_size : Int?
baseline : String?
diff_functions : Bool
experiment : String?
experiment_context : String?
why_index : Int?
why_root : Int?
why_json : String?
budget : String?
budget_json : String?
budget_md : String?
compress : Bool
data_segments : Bool
data_json : String?
diff_data : Bool
strict : Bool
}pub struct PackageSizeChange {
name : String
baseline_functions : Int
current_functions : Int
baseline_size : Int
current_size : Int
delta : Int
}pub struct PathLimits {
max_nodes : Int
max_edges : Int
max_depth : Int
}pub struct PathSearchStats {
root_sources : Int
prepared_roots : Int
indexed_edges : Int
visited_nodes : Int
checked_edges : Int
max_depth : Int
phase : String
limit_reason : String?
}pub struct Reader {
data : Bytes
start : Int
end : Int
pos : Int
}pub struct RetainedSize {
index : Int
own_size : Int
retained : Int
retained_functions : Int
}pub struct Section {
id : Int
kind : SectionId
offset : Int
payload_size : Int
total_size : Int
custom_name : String?
}pub struct SectionCompression {
label : String
raw : Int
gzip : Result[Int, String]
}pub enum SectionId {
Custom
Type
Import
Function
Table
Memory
Global
Export
Start
Element
Code
Data
DataCount
Tag
}pub struct TableType {
element_type : Int
minimum : Int
maximum : Int?
}pub enum WasmError {
UnexpectedEnd(Int, Int)
BadMagic
InvalidSectionId(Int)
LebOverflow(Int)
InvalidReadRange(Int, Int, Int, Int)
ValueOutOfRange(Int, UInt)
UnsupportedVersion(Int, UInt)
DuplicateSection(Int, SectionId)
InvalidSectionOrder(Int, SectionId, SectionId)
FunctionCodeCountMismatch(Int, SectionId, Int, Int)
TrailingSectionBytes(Int, SectionId)
InvalidTableEncoding(Int, String)
InvalidDataEncoding(Int, String)
InvalidImportKind(Int)
UnknownOpcode(Int, Int)
UnknownPrefixedOpcode(Int, Int, Int)
}let EXIT_OVER_SIZE : Intfn collect_function_stats(parsed : WasmModule, data : Bytes) -> Result[Array[FunctionStat], WasmError]fn compare_data_segments(baseline : Bytes, current : Bytes) -> Result[DataSegmentsComparison, ComparisonError]fn compare_modules(baseline_data : Bytes, current_data : Bytes) -> Result[ModuleSizeDiff, ComparisonError]fn compress_gzip(data : Bytes) -> Result[Bytes, String]fn compressed_size(data : Bytes) -> Result[Int, String]fn decode_instructions(bytes : Bytes, body_offset : Int, body_size : Int) -> Result[Array[Instruction], WasmError]fn demangle_full(symbol : String) -> Stringfn display_name(name : String) -> Stringfn evaluate_budget(budget : CompiledBudget, current_data : Bytes, baseline_data? : Bytes) -> Result[BudgetReport, BudgetError]fn explain_function(data : Bytes, target_index : Int, root? : Int, limits? : PathLimits) -> Result[FunctionPathReport, FunctionPathError]fn module_of(name : String) -> Stringfn parse_code_section(data : Bytes, section : Section, base_index? : Int) -> Result[Array[FunctionBody], WasmError]fn parse_count(text : String) -> Int?fn parse_element_section(data : Bytes, section : Section) -> Result[Array[ElementSegment], WasmError]fn parse_size(text : String) -> Int?fn render_budget_json(report : BudgetReport, current_path : String, baseline_path? : String) -> Stringfn render_budget_markdown(report : BudgetReport, current_path : String, baseline_path? : String) -> Stringfn render_compression(raw_size : Int, gzip : Result[Int, String], sections : Array[SectionCompression]) -> Stringfn render_data_segments_diff(report : DataSegmentsComparison, baseline_path : String, current_path : String, top : Int) -> Stringfn render_data_segments_diff_json(report : DataSegmentsComparison, baseline_path : String, current_path : String) -> Stringfn render_html_with_data_segments(analysis : Analysis, file_path : String, report : DataSegmentsReport, echarts? : String) -> Stringfn render_json(path : String, stats : Array[FunctionStat], graph : CallGraph, roots : Set[Int], reachable : Set[Int], retained : Array[RetainedSize]) -> Stringfn retained_sizes(graph : CallGraph, sizes : Map[Int, Int], roots : Set[Int]) -> Array[RetainedSize]fn usage() -> StringInstall
Download zipA Wasm size profiler for MoonBit — section breakdown, function ranking, call graph and retained-size analysis