///|
/// The process contract: what the exit code means, and the size gate that
/// decides it.
///
/// The library owns this so the decisions can be tested without a process
/// around them, and so a CI job reading an exit code is reading something that
/// was specified rather than inferred.
///|
/// The analysis ran and nothing was over budget.
pub const EXIT_OK : Int = 0
///|
/// The input could not be read, or is not a WebAssembly module.
pub const EXIT_INPUT : Int = 1
///|
/// The command line did not make sense.
pub const EXIT_USAGE : Int = 2
///|
/// The module is over the size budget the invocation set.
pub const EXIT_OVER_SIZE : Int = 3
///|
/// The exit code a module of `current_size` bytes earns, measured against
/// `baseline_size` when the invocation named a baseline.
///
/// Without one, `--max-size` is a ceiling on the file. With one it is the growth
/// the file may show over the baseline, because that is the question a CI job is
/// asking when it compares a branch against the branch it merges into — an
/// absolute ceiling there would be answering something nobody asked.
pub fn size_exit_code(
options : Options,
current_size : Int,
baseline_size : Int?,
) -> Int {
match options.max_size {
None => EXIT_OK
Some(budget) =>
match baseline_size {
Some(baseline) =>
if current_size - baseline > budget {
EXIT_OVER_SIZE
} else {
EXIT_OK
}
None => if current_size > budget { EXIT_OVER_SIZE } else { EXIT_OK }
}
}
}
///|
/// Why the size gate failed, in one line, and an empty string when it passed.
///
/// It asks the gate rather than re-deriving the answer, so the message and the
/// exit code can never disagree about whether the run was over budget.
pub fn describe_size_failure(
options : Options,
current_size : Int,
baseline_size : Int?,
) -> String {
if size_exit_code(options, current_size, baseline_size) == EXIT_OK {
return ""
}
match options.max_size {
None => ""
Some(budget) =>
match baseline_size {
Some(baseline) =>
"moonsize: grew by " +
plural(current_size - baseline, "byte") +
" over the baseline, budget " +
plural(budget, "byte")
None =>
"moonsize: " +
plural(current_size, "byte") +
" is over the budget of " +
plural(budget, "byte")
}
}
}
///|
/// One row of the size comparison: a section as each module has it.
priv struct SizeRow {
label : String
baseline : Int
current : Int
}
///|
/// The size of the section carrying this label, or `None` when the module has no
/// such section.
fn labelled_section_size(parsed : WasmModule, label : String) -> Int? {
for section in parsed.sections {
if section_label(section) == label {
return Some(section.total_size)
}
}
None
}
///|
/// Both modules' sections, matched by label and ordered by their current size.
///
/// A section only one of the two has is still listed, against a zero: arriving
/// or disappearing is exactly the kind of change a comparison is for.
fn size_rows(baseline : WasmModule, current : WasmModule) -> Array[SizeRow] {
let rows : Array[SizeRow] = []
for section in ordered_sections(current) {
let label = section_label(section)
rows.push({
label,
baseline: match labelled_section_size(baseline, label) {
Some(size) => size
None => 0
},
current: section.total_size,
})
}
for section in ordered_sections(baseline) {
let label = section_label(section)
if labelled_section_size(current, label) is None {
rows.push({ label, baseline: section.total_size, current: 0, })
}
}
rows
}
///|
/// A signed byte delta and the same change as a share of the baseline.
///
/// A section the baseline never had has no share to give — the change is
/// everything it is — so it says `new` rather than dividing by zero.
fn delta_text(delta : Int, baseline : Int) -> String {
let magnitude = if delta < 0 { -delta } else { delta }
let sign = if delta > 0 { "+" } else if delta < 0 { "-" } else { "" }
if baseline <= 0 {
if delta == 0 {
return "0"
}
return sign + magnitude.to_string() + " (new)"
}
sign +
magnitude.to_string() +
" (" +
sign +
share_text(magnitude, baseline) +
")"
}
///|
/// One comparison row, with the numbers right-aligned under their headings.
fn size_row_text(
label : String,
baseline : Int,
current : Int,
label_width : Int,
baseline_width : Int,
current_width : Int,
) -> String {
" " +
pad_right(label, label_width) +
" " +
pad_left(baseline.to_string(), baseline_width) +
" " +
pad_left(current.to_string(), current_width) +
" " +
delta_text(current - baseline, baseline) +
"\n"
}
///|
/// The size comparison table: the whole file and every section, against the
/// module it is being compared with.
///
/// Sections are ordered by their current size, so the table reads like the
/// section report above it, and the totals come first because that is the number
/// a budget is spent against.
pub fn render_size_comparison(
baseline : WasmModule,
current : WasmModule,
) -> String {
let rows = size_rows(baseline, current)
let mut label_width = "total".length()
let mut baseline_width = number_width(baseline.file_size, 8)
let mut current_width = number_width(current.file_size, 7)
for row in rows {
if row.label.length() > label_width {
label_width = row.label.length()
}
baseline_width = number_width(row.baseline, baseline_width)
current_width = number_width(row.current, current_width)
}
let mut out = "SIZE COMPARISON\n\n " +
pad_right("", label_width) +
" " +
pad_left("baseline", baseline_width) +
" " +
pad_left("current", current_width) +
" delta\n" +
size_row_text(
"total",
baseline.file_size,
current.file_size,
label_width,
baseline_width,
current_width,
)
for row in rows {
out = out +
size_row_text(
row.label,
row.baseline,
row.current,
label_width,
baseline_width,
current_width,
)
}
out
}