"""One-shot board measurement for the supported-boards table.

Run this on any board, with the SDK already installed, to produce a single
comparable row. Everything in the docs table should come from this script and
nothing else — the earlier figures were gathered ad hoc at different times, so
they were never strictly comparable across boards.

    mpremote connect <port> soft-reset run tests/measure_board.py

**Hard-reset the board first if anything has touched the network.** A soft reset
does not tear down the ESP32 WiFi driver: after merely calling
`network.WLAN(STA_IF).active(True)` once, this script read 145.8 KB where a
freshly powered board reads 157.8 KB — a 12 KB phantom loss that looks exactly
like a real regression. `mpremote connect <port> reset`, wait a few seconds,
then measure.

The soft-reset matters: `free at boot` is only meaningful in a fresh
interpreter, and an already-imported tendrl sits in sys.modules where a second
import costs nothing and reports a fake 0 KB.

Method notes, so the numbers can be reproduced and defended:

* **free at boot** — gc.collect() then gc.mem_free(), before importing anything
  of ours. This is what the application would actually start with.
* **peak** — GC is disabled across the import so nothing is reclaimed mid-flight,
  and the drop in free heap is therefore the *total* allocated: transient
  compile/loader garbage plus what stays. Peak is the number that decides
  whether a board can install at all; resident is what it costs afterwards.
  Leaving GC enabled would understate it by however much happened to be
  collected mid-import, which varies with heap size and makes roomy boards look
  artificially cheap.
* **resident** — after re-enabling GC and collecting. What the SDK actually holds.
* **free after** — what's left for your code. The headline number.

Import time includes only the import itself, not the measurement scaffolding.
"""

import gc
import sys
import time


def _sizeof_fmt(n):
    """KB below a megabyte, MB above it. Printing 25004.6 KB for the N6 is
    technically correct and useless to read — 24.42 MB is the same number in a
    unit a person can hold in their head."""
    if n is None:
        return "n/a"
    if n >= 1024 * 1024:
        return "%.2f MB" % (n / 1024 / 1024)
    return "%.1f KB" % (n / 1024)


def _has(mod):
    try:
        __import__(mod)
        return True
    except Exception:
        return False


def main():
    # --- identity -----------------------------------------------------------
    try:
        uname = __import__("os").uname()
        machine = getattr(uname, "machine", "?")
        release = getattr(uname, "release", "?")
    except Exception:
        machine = release = "?"
    impl_machine = ""
    try:
        impl_machine = sys.implementation._machine
    except Exception:
        pass
    try:
        mpy = sys.implementation._mpy & 0xFF
    except Exception:
        mpy = 0

    # --- the measurement ----------------------------------------------------
    gc.collect()
    free_at_boot = gc.mem_free()

    gc.disable()
    t0 = time.ticks_us()
    import tendrl.client  # noqa: F401
    t1 = time.ticks_us()
    free_raw = gc.mem_free()
    gc.enable()
    gc.collect()
    free_after = gc.mem_free()

    import_ms = time.ticks_diff(t1, t0) / 1000
    peak = free_at_boot - free_raw
    resident = free_at_boot - free_after

    # Which install path this row actually measured. A ROMFS install executes in
    # place from a flash partition and never copies bytecode into the heap, so a
    # ROMFS row and a filesystem row are not comparable — the docs table has to
    # say which one each figure came from.
    try:
        _f = getattr(tendrl.client, "__file__", "") or ""
    except Exception:
        _f = ""
    if _f.startswith("/rom"):
        sdk_src = "romfs (" + _f + ")"
    elif _f:
        sdk_src = ("bytecode " if _f.endswith(".mpy") else "source ") + _f
    else:
        sdk_src = "frozen/unknown"

    # --- firmware capabilities -------------------------------------------
    # Deliberately AFTER the measurement. Probing allocates (importing umqtt to
    # read its __file__ costs several KB, and interned strings survive a
    # collect), which shifted free-at-boot by ~3 KB between runs when this ran
    # first. Nothing here can affect numbers already recorded.
    caps = {
        "btree": _has("btree"),
        "umqtt": _has("umqtt.simple") and _has("umqtt.robust"),
        "mqtt_openmv": _has("mqtt"),
    }
    # Frozen (in firmware) vs installed to /lib matters: a board that needs the
    # library pushed pays for it in heap, and its "free after" is only honest if
    # the library was actually present when the SDK was imported.
    umqtt_src = "absent"
    if caps["umqtt"]:
        try:
            import umqtt.simple as _us
            f = getattr(_us, "__file__", None)
            # A frozen module reports a RELATIVE __file__ ("umqtt/simple.py") —
            # its path inside the firmware manifest. An installed one reports an
            # absolute path ("/lib/umqtt/simple.mpy"). Testing for the presence
            # of __file__ alone misreads every frozen module as installed.
            if not f:
                umqtt_src = "firmware (no __file__)"
            elif f.startswith("/"):
                umqtt_src = "installed " + f.rsplit("/", 1)[0]
            else:
                umqtt_src = "firmware (frozen)"
        except Exception:
            umqtt_src = "unknown"
    elif caps["mqtt_openmv"]:
        umqtt_src = "openmv builtin"
    romfs = False
    try:
        import vfs
        romfs = bool(vfs.rom_ioctl(1)) if hasattr(vfs, "rom_ioctl") else False
    except Exception:
        pass

    # PSRAM. esp32.HEAP_SPIRAM does not exist on every build (absent on 1.28
    # generic-SPIRAM, where the probe raised AttributeError and silently reported
    # "n/a" on a board with 2 MB of it). Fall back to the board string, which
    # spells out SPIRAM when the firmware was built for it.
    psram = None
    try:
        import esp32
        if hasattr(esp32, "HEAP_SPIRAM"):
            regions = esp32.idf_heap_info(esp32.HEAP_SPIRAM)
            psram = sum(r[1] for r in regions) if regions else 0
    except Exception:
        psram = None
    psram_named = "SPIRAM" in (impl_machine or "").upper() or "PSRAM" in (impl_machine or "").upper()

    # OpenMV mounts its writable filesystem at /flash, not /. Probe both rather
    # than string-matching listdir("/"), which never matched: listdir returns
    # bare names ("flash"), so the "/flash" test was always false and OpenMV
    # boards silently reported the root mount instead.
    fs_total = fs_free = None
    fs_root = "/"
    try:
        import os as _os
        for cand in ("/flash", "/"):
            try:
                st = _os.statvfs(cand)
                fs_total = st[0] * st[2]
                fs_free = st[0] * st[3]
                fs_root = cand
                break
            except Exception:
                continue
    except Exception:
        pass


    # --- report -------------------------------------------------------------
    print("TENDRL_BOARD_MEASUREMENT")
    print("  board          :", impl_machine or machine)
    print("  platform       :", sys.platform)
    print("  micropython    :", release, "| .mpy fmt", mpy)
    print("  psram          :", _sizeof_fmt(psram) if psram
          else ("yes (per board string; size not reported)" if psram_named
                else ("none" if psram == 0 else "not detected")))
    print("  filesystem     :", _sizeof_fmt(fs_total), "total,", _sizeof_fmt(fs_free), "free", "(" + fs_root + ")")
    print("  btree          :", "yes" if caps["btree"] else "NO")
    print("  mqtt client    :", umqtt_src)
    print("  romfs partition:", "yes" if romfs else "no")
    print("  sdk loaded from:", sdk_src)
    print("  ---")
    print("  free at boot   :", _sizeof_fmt(free_at_boot), "(%d B)" % free_at_boot)
    print("  import time    : %.0f ms" % import_ms)
    print("  peak during    :", _sizeof_fmt(peak), "(%d B)" % peak)
    print("  resident       :", _sizeof_fmt(resident), "(%d B)" % resident)
    print("  free after     :", _sizeof_fmt(free_after), "(%d B)" % free_after)
    print("  ---")
    print("MEASUREMENT_JSON:{"
          '"board":"%s","platform":"%s","release":"%s","mpy":%d,'
          '"psram":%s,"fs_total":%s,"fs_free":%s,'
          '"btree":%s,"umqtt":%s,"umqtt_src":"%s","romfs":%s,"sdk_src":"%s",'
          '"free_at_boot":%d,"import_ms":%.0f,"peak":%d,"resident":%d,"free_after":%d}'
          % (impl_machine or machine, sys.platform, release, mpy,
             psram if psram is not None else "null",
             fs_total if fs_total is not None else "null",
             fs_free if fs_free is not None else "null",
             "true" if caps["btree"] else "false",
             "true" if (caps["umqtt"] or caps["mqtt_openmv"]) else "false", umqtt_src,
             "true" if romfs else "false", sdk_src,
             free_at_boot, import_ms, peak, resident, free_after))


main()
