Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ wasmparser = { workspace = true }
tracing = { workspace = true }
log = { workspace = true }
humantime = { workspace = true }
tempfile = { workspace = true, optional = true }

async-trait = { workspace = true }
bytes = { workspace = true }
Expand Down Expand Up @@ -417,7 +418,7 @@ gc = ["wasmtime-cli-flags/gc"]
# CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
# for more information on each subcommand.
serve = ["wasi-http", "component-model", "dep:http-body-util", "dep:http"]
explore = ["dep:wasmtime-explorer"]
explore = ["dep:wasmtime-explorer", "dep:tempfile"]
wast = ["dep:wasmtime-wast"]
config = ["cache"]
compile = ["cranelift"]
Expand Down
10 changes: 8 additions & 2 deletions crates/explorer/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ body {
}

#wat {
width: 50%;
flex: 1;
height: 100%;
overflow: scroll;
}

#clif {
flex: 1;
height: 100%;
overflow: scroll;
}

#asm {
width: 50%;
flex: 1;
height: 100%;
overflow: scroll;
}
97 changes: 95 additions & 2 deletions crates/explorer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
/*** State *********************************************************************/

class State {
constructor(wat, asm) {
constructor(wat, clif, asm) {
this.wat = wat;
this.clif = clif;
this.asm = asm;
}
}

const state = (window.STATE = new State(window.WAT, window.ASM));
const state = (window.STATE = new State(window.WAT, window.CLIF, window.ASM));

/*** Hues for Offsets **********************************************************/

Expand Down Expand Up @@ -54,6 +55,9 @@ const watByOffset = new Map();
// Get asm instruction elements by Wasm offset.
const asmByOffset = new Map();

// Get clif instruction elements by Wasm offset.
const clifByOffset = new Map();

// Get all (WAT chunk or asm instruction) elements by offset.
const anyByOffset = new Map();

Expand Down Expand Up @@ -81,6 +85,18 @@ const addAsmElem = (offset, elem) => {
anyByOffset.get(offset).push(elem);
};

const addClifElem = (offset, elem) => {
if (!clifByOffset.has(offset)) {
clifByOffset.set(offset, []);
}
clifByOffset.get(offset).push(elem);

if (!anyByOffset.has(offset)) {
anyByOffset.set(offset, []);
}
anyByOffset.get(offset).push(elem);
};

/*** Event Handlers ************************************************************/

const watElem = document.getElementById("wat");
Expand All @@ -96,6 +112,12 @@ watElem.addEventListener(
return;
}

const firstClifElem = clifByOffset.get(offset)[0];
firstClifElem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
const firstAsmElem = asmByOffset.get(offset)[0];
firstAsmElem.scrollIntoView({
behavior: "smooth",
Expand Down Expand Up @@ -125,9 +147,45 @@ asmElem.addEventListener(
block: "center",
inline: "nearest",
});
const firstClifElem = clifByOffset.get(offset)[0];
firstClifElem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
},
{ passive: true },
);
const clifElem = document.getElementById("clif");
if (clifElem) {
clifElem.addEventListener(
"click",
event => {
if (event.target.dataset.wasmOffset == null) {
return;
}

const offset = parseInt(event.target.dataset.wasmOffset);
if (!watByOffset.get(offset)) {
return;
}

const firstWatElem = watByOffset.get(offset)[0];
firstWatElem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
const firstAsmElem = asmByOffset.get(offset)[0];
firstAsmElem.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
},
{ passive: true },
);
}

const onMouseEnter = event => {
if (event.target.dataset.wasmOffset == null) {
Expand Down Expand Up @@ -184,6 +242,41 @@ const renderInst = (mnemonic, operands) => {
}
};

// Render the CLIF.

if (clifElem) {
for (const func of state.clif.functions) {
const funcElem = document.createElement("div");

const funcHeader = document.createElement("h3");
let func_name =
func.name === null ? `function[${func.func_index}]` : func.name;
let demangled_name =
func.demangled_name !== null ? func.demangled_name : func_name;
funcHeader.textContent = `Intermediate Representation of function <${demangled_name}>:`;
funcHeader.title = `Function ${func.func_index}: ${func_name}`;
funcElem.appendChild(funcHeader);

const bodyElem = document.createElement("pre");
for (const inst of func.instructions) {
const instElem = document.createElement("span");
instElem.textContent = `${inst.clif}\n`;
if (inst.wasm_offset != null) {
instElem.setAttribute("data-wasm-offset", inst.wasm_offset);
const hue = hueForOffset(inst.wasm_offset);
instElem.style.backgroundColor = `hsl(${hue} 50% 90%)`;
instElem.addEventListener("mouseenter", onMouseEnter);
instElem.addEventListener("mouseleave", onMouseLeave);
addClifElem(inst.wasm_offset, instElem);
}
bodyElem.appendChild(instElem);
}
funcElem.appendChild(bodyElem);

clifElem.appendChild(funcElem);
}
}

// Render the ASM.

for (const func of state.asm.functions) {
Expand Down
91 changes: 90 additions & 1 deletion crates/explorer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use anyhow::Result;
use capstone::arch::BuildsCapstone;
use serde_derive::Serialize;
use std::{io::Write, str::FromStr};
use std::{
fs::File,
io::{read_to_string, Write},
path::Path,
str::FromStr,
};
use wasmtime_environ::demangle_function_name;

pub fn generate(
config: &wasmtime::Config,
target: Option<&str>,
clif_dir: Option<&Path>,
wasm: &[u8],
dest: &mut dyn Write,
) -> Result<()> {
Expand All @@ -19,6 +25,12 @@ pub fn generate(
let wat_json = serde_json::to_string(&wat)?;
let asm = annotate_asm(config, &target, wasm)?;
let asm_json = serde_json::to_string(&asm)?;
let clif_json = clif_dir
.map::<anyhow::Result<String>, _>(|clif_dir| {
let clif = annotate_clif(clif_dir, &asm)?;
Ok(serde_json::to_string(&clif)?)
})
.transpose()?;

let index_css = include_str!("./index.css");
let index_js = include_str!("./index.js");
Expand All @@ -36,9 +48,30 @@ pub fn generate(
</head>
<body class="hbox">
<pre id="wat"></pre>
"#
)?;
if clif_json.is_some() {
write!(dest, r#"<div id="clif"></div>"#)?;
}
write!(
dest,
r#"
<div id="asm"></div>
<script>
window.WAT = {wat_json};
"#
)?;
if let Some(clif_json) = clif_json {
write!(
dest,
r#"
window.CLIF = {clif_json};
"#
)?;
}
write!(
dest,
r#"
window.ASM = {asm_json};
</script>
<script>
Expand Down Expand Up @@ -208,3 +241,59 @@ fn annotate_asm(

Ok(AnnotatedAsm { functions })
}

#[derive(Serialize, Debug)]
struct AnnotatedClif {
functions: Vec<AnnotatedClifFunction>,
}

#[derive(Serialize, Debug)]
struct AnnotatedClifFunction {
func_index: u32,
name: Option<String>,
demangled_name: Option<String>,
instructions: Vec<AnnotatedClifInstruction>,
}

#[derive(Serialize, Debug)]
struct AnnotatedClifInstruction {
wasm_offset: Option<WasmOffset>,
clif: String,
}

fn annotate_clif(clif_dir: &Path, asm: &AnnotatedAsm) -> Result<AnnotatedClif> {
let mut clif = AnnotatedClif {
functions: Vec::new(),
};
for function in &asm.functions {
let function_path = clif_dir.join(format!("wasm_func_{}.clif", function.func_index));
if !function_path.exists() {
continue;
}
let mut clif_function = AnnotatedClifFunction {
func_index: function.func_index,
name: function.name.clone(),
demangled_name: function.demangled_name.clone(),
instructions: Vec::new(),
};
let file = File::open(&function_path)?;
for mut line in read_to_string(file)?.lines() {
if line.is_empty() {
continue;
}
let mut wasm_offset = None;
if line.starts_with('@') {
wasm_offset = Some(WasmOffset(u32::from_str_radix(&line[1..5], 16)?));
line = &line[28..];
} else if line.starts_with(" ") {
line = &line[28..];
}
clif_function.instructions.push(AnnotatedClifInstruction {
wasm_offset,
clif: line.to_string(),
});
}
clif.functions.push(clif_function);
}
Ok(clif)
}
21 changes: 19 additions & 2 deletions src/commands/explore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use anyhow::{Context, Result};
use clap::Parser;
use std::{borrow::Cow, path::PathBuf};
use tempfile::tempdir;
use wasmtime::Strategy;
use wasmtime_cli_flags::CommonOptions;

/// Explore the compilation of a WebAssembly module to native code.
Expand Down Expand Up @@ -30,7 +32,7 @@ impl ExploreCommand {
pub fn execute(mut self) -> Result<()> {
self.common.init_logging()?;

let config = self.common.config(self.target.as_deref(), None)?;
let mut config = self.common.config(self.target.as_deref(), None)?;

let bytes =
Cow::Owned(std::fs::read(&self.module).with_context(|| {
Expand All @@ -50,7 +52,22 @@ impl ExploreCommand {
.with_context(|| format!("failed to create file: {}", output.display()))?;
let mut output_file = std::io::BufWriter::new(output_file);

wasmtime_explorer::generate(&config, self.target.as_deref(), &bytes, &mut output_file)?;
let clif_dir = if let Some(Strategy::Cranelift) | None = self.common.codegen.compiler {
let clif_dir = tempdir()?;
config.emit_clif(clif_dir.path());
Some(clif_dir)
} else {
None
};

wasmtime_explorer::generate(
&config,
self.target.as_deref(),
clif_dir.as_ref().map(|tmp_dir| tmp_dir.path()),
&bytes,
&mut output_file,
)?;

println!("Exploration written to {}", output.display());
Ok(())
}
Expand Down