///|
/// IO and exit status are confined to this executable package.
fn main {
let config = @cli.parse_config(app_args()) catch {
error => {
eprintln("moonflame: " + error_message(error))
@sys.exit(2)
return
}
}
let result = output(config) catch {
error => {
eprintln("moonflame: " + error_message(error))
@sys.exit(1)
return
}
}
// Only open the destination after every input and computation has succeeded.
match config.out {
Some(path) => {
@fs.write_string_to_file(path, result) catch {
@fs.IOError(message) => {
eprintln("moonflame: " + message)
@sys.exit(1)
return
}
}
eprintln("moonflame: wrote " + path)
}
None => if result != "" { println(result) }
}
}
///|
fn parsed_file(path : String) -> (String, @moonflame.ParseResult) raise {
let text = @fs.read_file_to_string(path)
let parsed = @moonflame.parse_folded(text)
if parsed.skipped_lines > 0 {
for issue in parsed.diagnostics {
let reason = match issue.kind {
@moonflame.MissingWeight => "missing weight"
@moonflame.InvalidNumber => "invalid number"
@moonflame.NonFiniteWeight => "non-finite weight"
@moonflame.NegativeWeight => "negative weight"
}
eprintln("moonflame: \{path}:\{issue.line}: \{reason}")
}
fail("invalid profile; no output written")
}
(text, parsed)
}
///|
fn tree_of_file(path : String, max_depth : Int) -> @moonflame.CallTree raise {
let (_, parsed) = parsed_file(path)
let tree = @moonflame.aggregate(parsed.samples, max_depth~)
if tree.total <= 0.0 {
fail("no positive-weight samples in " + path)
}
let pending = tree.roots.copy()
while pending.length() > 0 {
let node = pending[pending.length() - 1]
ignore(pending.pop())
if node.name == @moonflame.deeper_placeholder() {
eprintln(
"moonflame: depth-limited profile; (deeper) contains unexpanded frames",
)
break
}
for child in node.children {
pending.push(child)
}
}
tree
}
///|
fn picture(config : @cli.Config, tree : @moonflame.CallTree) -> String raise {
let layout = @moonflame.layout(
tree,
config.width - 2.0 * @moonflame.CANVAS_XPAD,
@moonflame.FRAME_HEIGHT,
inverted=config.inverted,
)
@moonflame.render_svg(layout, unit=config.unit)
}
///|
fn output(config : @cli.Config) -> String raise {
if config.action is @cli.Filter(pattern) {
let (text, parsed) = parsed_file(config.input)
// Validate accumulation too, even though filtering preserves original lines.
ignore(@moonflame.aggregate(parsed.samples, max_depth=32))
return @moonflame.filter_folded(text, pattern)
}
let tree = tree_of_file(config.input, config.max_depth)
match config.action {
@cli.Render => picture(config, tree)
@cli.Diff | @cli.Delta(_) => {
guard config.second is Some(path) else { fail("missing baseline") }
let before = tree_of_file(path, config.max_depth)
match config.action {
@cli.Diff => picture(config, @moonflame.diff_against(tree, before))
@cli.Delta(count) =>
@moonflame.render_delta(
@moonflame.compute_delta(before, tree, count),
before.total,
tree.total,
config.unit,
)
_ => abort("unreachable")
}
}
@cli.Hotspots(count) =>
@moonflame.render_hotspots(
@moonflame.top_hotspots(tree, count),
tree.total,
config.unit,
)
@cli.Callers(wanted) =>
@moonflame.render_relations(
@moonflame.find_callers(tree, wanted),
wanted,
"调用者",
config.unit,
matched=@moonflame.has_matching_frame(tree, wanted),
)
@cli.Callees(wanted) =>
@moonflame.render_relations(
@moonflame.find_callees(tree, wanted),
wanted,
"被调用",
config.unit,
matched=@moonflame.has_matching_frame(tree, wanted),
)
@cli.Filter(_) => abort("unreachable")
}
}
///|
#cfg(target="js")
fn app_args() -> Array[String] {
@env.args()[2:].to_owned()
}
///|
#cfg(not(target="js"))
fn app_args() -> Array[String] {
@env.args()[1:].to_owned()
}
///|
fn error_message(error : Error) -> String {
match error {
@fs.IOError(message) => message
@moonflame.InvalidWeight(message) => message
@moonflame.InvalidTree(message) => message
@moonflame.InvalidGeometry(message) => message
_ => error.to_string()
}
}