Debugging 🐞

Debugging

This page covers how to find bugs in your apps.

😎 Cheat codes #

You can add cheat codes to your apps. Cheat codes can be used to put the app into a specific state: jump to a specific level, reset NPCs’ positions, set character health, etc.

First, add [cheats] table into firefly.toml. The table maps cheat code names to numbers that get passed into the app:

[cheats]
get-health = 1
set-health = 2

Next, define the cheat callback in the app:

#[no_mangle]
extern fn cheat(cmd: i32, val: i32) -> i32 {
    match cmd {
        1 => get_health(),
        2 => {
            set_health(val);
            1
        },
        _ => 0,
    }
}
package main

import "github.com/firefly-zero/firefly-go/firefly"

func init() {
    firefly.Cheat = cheat
}

func cheat(cmd, val int) int {
    switch cmd {
    case 1:
        return getHealth()
    case 2:
        setHealth(val)
        return 1
    default:
        return 0
    }
}
pub export fn cheat(cmd: i32, val: i32) i32 {
    switch (cmd) {
        1 => {
            return GetHealth();
        },
        2 => {
            setHealth(val);
            return 1;
        },
        else => {
            return 0;
        }
    }
}
CHEAT int32_t cheat(int32_t cmd, int32_t val) {
    switch (cmd) {
    case 1:
        return get_health();
    case 2:
        set_health();
        return 1;
    default:
        return 0;
    }
}

Now, you can send cheat codes into a running app using the CLI:

firefly_cli set-health 56   # prints 1
firefly_cli get-health 0    # prints 56

πŸ—œ Decompilation #

WebAssembly specification describes 2 file formats: a binary format that Firefly Zero uses and a text format that is human-readable. You can convert the binary format into the text format using the wasm2wat tool from wabt.

Installing wabt

On Linux, wabt can be installed by running sudo apt install -y wabt. For other systems, check out Github Releases.

Having trouble installing wabt? You can use the wasm2wat online demo instead.

For example, here is how to view the text representation of the default launcher’s binary:

wasm2wat --generate-names --fold-exprs \
    $(firefly_cli vfs)/roms/sys/launcher/_bin \
    | less

When is it useful?

Looking into the wasm2wat output is useful only for compiled languages and for small binaries. For interpreted languages, like Python, you’ll see the code of the interpreter instead of the code of your app. And for big binaries, it’s just hard to find in the output what you need. It might be a good idea to create a very small example that reproduces your issue and the decompile it instead.

πŸ”¬ Inspection #

The firefly_cli inspect command shows lots of useful information about the ROM: files, their sizes, metadata, WASM binary sections, elements in them, exported callbacks, imported host functions, etc. For example, to inspect the default system launcher:

firefly_cli inspect sys.launcher

πŸ’” Finding the point of failure #

When an app explodes (panic in Rust and Go, raise in Python, etc), the runtime logs will tell you in which app callback it happened and what’s the last runtime function that was called. In many cases, this is enough to find where the code failed. If it’s not, add log_debug function calls before and after each line of code that you suspect might fail:

use firefly_rust::*;
log_debug("before foo");
foo();
log_debug("before bar");
bar();
log_debug("after bar");
firefly.LogDebug("before foo")
foo()
firefly.LogDebug("before bar")
bar()
firefly.LogDebug("after bar")
const ff = @import("firefly");
ff.logDebug("before foo");
foo();
ff.logDebug("before bar");
bar();
ff.logDebug("after bar");
log_debug("before foo");
foo();
log_debug("before bar");
bar();
log_debug("after bar");

If you see in logs the message printed before the call but not after, your suspicion is correct and you found the point of failure. You can then print values of all variables before the failure to see which input causes the issues.

Debugging big functions with “wolf fence” (aka “bisect”)

If there is a lot of code that might be failing, it might be hard to add log_debug before every line. In that case, you can do a binary search of the point of failure. First, add one log message in the middle of failing function and run the code. If you see the message, the failure happens after that point. If you don’t see, it fails before. Now, insert a message in the middle of the failing region and repeat the process until you find the exact failing line.

This debugging technique is called Wolf Fence or Bisect.

πŸ‘£ Tracebacks #

There are no tracebacks (aka “stack traces” or “back traces”) yet. See this issue: wasmi#538. To find the point of failure, use the debugging techniques covered above.

🐌 Monitoring performance #

Run firefly_cli monitor to launch a dashboard showing the runtime stats for a running app. It looks something like this:

 β”Œβ•΄cpu╢───────────────┐ β”Œβ•΄memory╢────────────┐
 β”‚ lag      0 ns   0% β”‚ β”‚ floor 1089 KB      β”‚
 β”‚ busy   313 ms  31% β”‚ β”‚ ceil  1152 KB 18p  β”‚
 β”‚ idle   693 ms  69% β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

 β”Œβ•΄fuel: update╢──────┐ β”Œβ•΄fuel: render╢──────┐
 β”‚ min    276         β”‚ β”‚ min    663         β”‚
 β”‚ max    276         β”‚ β”‚ max    663         β”‚
 β”‚ mean   276         β”‚ β”‚ mean   663         β”‚
 β”‚ stdev    0         β”‚ β”‚ stdev    0         β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • cpu shows the CPU usage.
    • lag shows the total time (per second) that the app was lagging behind. If it’s not zero, the update and render callbacks are too slow, at least sometimes. Big values may cause visual lag and annoy players. Optimize!
    • busy is how much time was spent running code, including system operations.
    • idle is how much time the runtime was sleeping and doing nothing. If it’s zero, every single update was lagging.
  • memory shows RAM usage.
    • floor shows the position of the last non-zero byte in memory. Depending on the allocator used by the app, it might either show the actual memory usage or mean absolutely nothing.
    • ceil is how many memory pages are allocated. One page is 64 KB.
  • fuel is the number of instructions executed per second.

Keep lag zero and busy, memory, and fuel low.

πŸ’’ Common errors #

Below are some of the common errors related to Firefly Zero that you may encounter.

unsupported compression method 93

You’re trying to manually extract a ROM from a ZIP archive created using firefly_cli export. ROMs use ZSTD compression that isn’t supported by your archive manager. You should use firefly_cli import for installing ROMs instead.

wasm unreachable instruction executed.

The app hit an invalid state. Usually, it would raise an “exception” (or “panic”, depending on the language) but WebAssembly used by your language doesn’t support exceptions. See Finding the point of failure and Tracebacks.

error: no global memory allocator found but one is required; link to std or add #[global_allocator] to a static item that implements the GlobalAlloc trait

You’re trying to build a Rust app that requires a global allocator but you don’t have one configured. The easiest way to fix it is to activate the talc crate feature for the SDK in Cargo.toml:

firefly-rust = { version = "*", features = ["talc"] }

⬅️ Badges and scores ➑️ Terms