tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/core/size.py
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure footprint model for `tan size` -- port of `crates/tan-core/src/size.rs`.
|
|
3
|
+
|
|
4
|
+
Measurement-source selection, budget resolution, classification and rendering
|
|
5
|
+
are all IO-free here; the subprocess + filesystem wiring lives in
|
|
6
|
+
`tan.commands.size_cmd`.
|
|
7
|
+
|
|
8
|
+
FLASH = text+data (everything in the flashed image); RAM = data+bss (everything
|
|
9
|
+
live at runtime) -- the Berkeley `size` model, where binutils folds `.rodata`
|
|
10
|
+
into `text` and `.noinit` (NOBITS) into `bss`.
|
|
11
|
+
|
|
12
|
+
Nothing here knows a hardware fact. `resolve_budget` is handed numbers a caller
|
|
13
|
+
read out of alp-sdk `metadata/`; no SKU, address or bank name is spelled in this
|
|
14
|
+
file (I-26).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import re
|
|
22
|
+
import struct
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from tan.core.pending import is_pending_placeholder
|
|
28
|
+
from tan.core.ram_run import SRAM0_BASE
|
|
29
|
+
|
|
30
|
+
#: ELF `sh_flags` bits used to classify a section.
|
|
31
|
+
SHF_WRITE = 0x1
|
|
32
|
+
SHF_ALLOC = 0x2
|
|
33
|
+
|
|
34
|
+
#: ELF `sh_type` for a section that occupies memory but not the image.
|
|
35
|
+
SHT_NOBITS = 8
|
|
36
|
+
|
|
37
|
+
#: A slice at/above this fraction of its budget is `warn` even though it still
|
|
38
|
+
#: fits -- a pre-flight "you're close" nudge.
|
|
39
|
+
WARN_FRACTION = 0.90
|
|
40
|
+
|
|
41
|
+
#: `u64::MAX`. A corrupt/adversarial `sh_size` saturates here rather than
|
|
42
|
+
#: reporting an arbitrarily large Python int the oracle could never emit.
|
|
43
|
+
_U64_MAX = 2**64 - 1
|
|
44
|
+
|
|
45
|
+
# colorama Fore/Style literals, so the color path matches the retired command's
|
|
46
|
+
# bytes.
|
|
47
|
+
_GREEN = "\x1b[32m"
|
|
48
|
+
_YELLOW = "\x1b[33m"
|
|
49
|
+
_RED = "\x1b[31m"
|
|
50
|
+
_CYAN = "\x1b[36m"
|
|
51
|
+
_RESET = "\x1b[0m"
|
|
52
|
+
|
|
53
|
+
#: Rust's `str::parse::<u64>()`: optional `+`, digits only. Python's `int()` is
|
|
54
|
+
#: laxer (it takes `-5`, `1_000`, surrounding whitespace, unicode digits), so a
|
|
55
|
+
#: `size` output row the oracle skips as noise must be skipped here too.
|
|
56
|
+
_U64_RE = re.compile(r"\+?[0-9]+\Z")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _u64(token: str) -> int | None:
|
|
60
|
+
if not _U64_RE.match(token):
|
|
61
|
+
return None
|
|
62
|
+
value = int(token)
|
|
63
|
+
return value if value <= _U64_MAX else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_berkeley_size(text: str) -> tuple[int, int] | None:
|
|
67
|
+
"""Parse Berkeley-format `size` output into `(flash, ram)` bytes. Columns are
|
|
68
|
+
`text data bss dec hex filename`; the first row whose first three
|
|
69
|
+
whitespace-split fields all parse as u64 wins (header/noise rows skipped).
|
|
70
|
+
`None` when no data row is found."""
|
|
71
|
+
for line in text.splitlines():
|
|
72
|
+
parts = line.split()
|
|
73
|
+
if len(parts) < 3:
|
|
74
|
+
continue
|
|
75
|
+
text_b, data_b, bss_b = (_u64(p) for p in parts[:3])
|
|
76
|
+
if text_b is None or data_b is None or bss_b is None:
|
|
77
|
+
continue # header row ("text data bss ...") or noise
|
|
78
|
+
return text_b + data_b, data_b + bss_b
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sizes_from_elf_sections(
|
|
83
|
+
elf_bytes: bytes, off_budget_spans: Sequence[tuple[int, int]] = ()
|
|
84
|
+
) -> tuple[int, int] | None:
|
|
85
|
+
"""Sum ELF section sizes into `(flash, ram)` bytes with Berkeley-`size`
|
|
86
|
+
semantics, straight from the section headers -- the middle rung between the
|
|
87
|
+
external size tool and the `rom/ram.json` fallback, so a present elf is
|
|
88
|
+
measured even with no `arm-zephyr-eabi-size` on PATH.
|
|
89
|
+
|
|
90
|
+
FLASH = every `SHF_ALLOC` section that occupies the image (not NOBITS):
|
|
91
|
+
`.text` + `.rodata` + `.data`, i.e. binutils' `text`+`data` columns.
|
|
92
|
+
RAM = every `SHF_ALLOC | SHF_WRITE` section, NOBITS included: `.data` +
|
|
93
|
+
`.bss` + `.noinit`, i.e. binutils' `data`+`bss` columns.
|
|
94
|
+
|
|
95
|
+
*off_budget_spans* (`(base, size)`, tan-cli#1402): a writable section whose
|
|
96
|
+
`sh_addr` falls inside one is placed in a different region than the RAM
|
|
97
|
+
budget (e.g. SRAM0 vs the core's DTCM), so it is left out of RAM. FLASH is
|
|
98
|
+
unchanged -- an initialised `.data` there is still loaded from the image.
|
|
99
|
+
|
|
100
|
+
`None` -- so the caller falls through to the next rung instead of reporting
|
|
101
|
+
a fake 0-byte size -- when the bytes are not ELF (a PE/Mach-O/wasm container
|
|
102
|
+
parses in other readers and used to yield `(0, 0)` in the oracle), when the
|
|
103
|
+
header table does not fit in the file, or when NO allocated section exists
|
|
104
|
+
(a relocatable `.o`, a stripped partial link). Handles ELF32/ELF64 and
|
|
105
|
+
either endianness, whatever the SoM core emits. Pure.
|
|
106
|
+
"""
|
|
107
|
+
if len(elf_bytes) < 64 or elf_bytes[:4] != b"\x7fELF":
|
|
108
|
+
return None
|
|
109
|
+
ei_class, ei_data = elf_bytes[4], elf_bytes[5]
|
|
110
|
+
if ei_class not in (1, 2) or ei_data not in (1, 2):
|
|
111
|
+
return None
|
|
112
|
+
endian = "<" if ei_data == 1 else ">"
|
|
113
|
+
if ei_class == 2: # ELF64
|
|
114
|
+
e_shoff = struct.unpack_from(endian + "Q", elf_bytes, 0x28)[0]
|
|
115
|
+
e_shentsize, e_shnum = struct.unpack_from(endian + "HH", elf_bytes, 0x3A)
|
|
116
|
+
shdr = endian + "IIQQQQIIQQ"
|
|
117
|
+
type_at, flags_at, addr_at, size_at = 1, 2, 3, 5
|
|
118
|
+
else: # ELF32
|
|
119
|
+
e_shoff = struct.unpack_from(endian + "I", elf_bytes, 0x20)[0]
|
|
120
|
+
e_shentsize, e_shnum = struct.unpack_from(endian + "HH", elf_bytes, 0x2E)
|
|
121
|
+
shdr = endian + "IIIIIIIIII"
|
|
122
|
+
type_at, flags_at, addr_at, size_at = 1, 2, 3, 5
|
|
123
|
+
entry_size = struct.calcsize(shdr)
|
|
124
|
+
if e_shentsize < entry_size or e_shnum == 0:
|
|
125
|
+
return None
|
|
126
|
+
# The header TABLE is bounds-checked (a truncated/adversarial e_shoff/e_shnum
|
|
127
|
+
# must not read past the buffer); an individual `sh_size` is NOT -- it is an
|
|
128
|
+
# unvalidated field, and its bytes are never read here, only its declared
|
|
129
|
+
# length, so it is saturated instead.
|
|
130
|
+
if e_shoff + e_shnum * e_shentsize > len(elf_bytes):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
flash = 0
|
|
134
|
+
ram = 0
|
|
135
|
+
saw_alloc = False
|
|
136
|
+
for index in range(e_shnum):
|
|
137
|
+
fields = struct.unpack_from(shdr, elf_bytes, e_shoff + index * e_shentsize)
|
|
138
|
+
sh_type, sh_flags, sh_addr, sh_size = (
|
|
139
|
+
fields[type_at],
|
|
140
|
+
fields[flags_at],
|
|
141
|
+
fields[addr_at],
|
|
142
|
+
fields[size_at],
|
|
143
|
+
)
|
|
144
|
+
if not sh_flags & SHF_ALLOC:
|
|
145
|
+
continue # .symtab, .debug_*, .comment -- neither region
|
|
146
|
+
saw_alloc = True
|
|
147
|
+
if sh_type != SHT_NOBITS:
|
|
148
|
+
flash = min(flash + sh_size, _U64_MAX)
|
|
149
|
+
if sh_flags & SHF_WRITE and not any(
|
|
150
|
+
base <= sh_addr < base + span for base, span in off_budget_spans
|
|
151
|
+
):
|
|
152
|
+
ram = min(ram + sh_size, _U64_MAX)
|
|
153
|
+
return (flash, ram) if saw_alloc else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def footprint_total(json_text: str) -> int | None:
|
|
157
|
+
"""Total bytes from a Zephyr `rom.json` / `ram.json` footprint document. The
|
|
158
|
+
footprint root is `{"symbols": {"size": <total>}}`; older layouts put the
|
|
159
|
+
total at the top level. `None` when the JSON is malformed or neither shape
|
|
160
|
+
yields a non-negative integer.
|
|
161
|
+
|
|
162
|
+
`bool` is excluded explicitly: it is an `int` subclass in Python, and
|
|
163
|
+
serde_json's `as_u64()` never accepts a JSON boolean.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
data = json.loads(json_text)
|
|
167
|
+
except (ValueError, RecursionError):
|
|
168
|
+
return None
|
|
169
|
+
if not isinstance(data, dict):
|
|
170
|
+
return None
|
|
171
|
+
symbols = data.get("symbols")
|
|
172
|
+
if isinstance(symbols, dict):
|
|
173
|
+
size = _as_u64(symbols.get("size"))
|
|
174
|
+
if size is not None:
|
|
175
|
+
return size
|
|
176
|
+
return _as_u64(data.get("size"))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _as_u64(value: Any) -> int | None:
|
|
180
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
181
|
+
return None
|
|
182
|
+
return value if 0 <= value <= _U64_MAX else None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def round1(x: float) -> float:
|
|
186
|
+
"""Round to 1 decimal the way both CPython's `round` and Rust's `{:.1}` do:
|
|
187
|
+
on the TRUE binary value, so the X.X5 tie cases agree -- 0.35 is actually
|
|
188
|
+
0.34999... and gives 0.3, not the 0.4 a `(x*10).round()/10` multiply
|
|
189
|
+
yields. Formatting-then-parsing rather than `round()` keeps this identical
|
|
190
|
+
to the text-cell path, which already format-rounds."""
|
|
191
|
+
try:
|
|
192
|
+
return float(f"{x:.1f}")
|
|
193
|
+
except (OverflowError, ValueError):
|
|
194
|
+
return x
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def region_json(used: int | None, total: int | None) -> dict[str, Any]:
|
|
198
|
+
"""A `{used, total, pct}` region object. `pct` is `null` unless `used` is
|
|
199
|
+
present and `total` is present AND non-zero (no div-by-zero, no guess)."""
|
|
200
|
+
pct = None
|
|
201
|
+
if used is not None and total:
|
|
202
|
+
pct = round1(used / total * 100.0)
|
|
203
|
+
return {"used": used, "total": total, "pct": pct}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def core_token(core_id: str) -> str:
|
|
207
|
+
"""`m55_hp` -> `M55_HP` -- the token embedded in `sram_banks_kb` keys."""
|
|
208
|
+
return core_id.upper()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def classify(
|
|
212
|
+
flash_used: int | None,
|
|
213
|
+
flash_total: int | None,
|
|
214
|
+
ram_used: int | None,
|
|
215
|
+
ram_total: int | None,
|
|
216
|
+
) -> str:
|
|
217
|
+
"""`ok` / `warn` / `over` from the worst of the two regions. A region with a
|
|
218
|
+
missing `used`, or a `total` that is missing or zero, is IGNORED -- no
|
|
219
|
+
div-by-zero and no guess. That silent skip is why `budget_fully_known`
|
|
220
|
+
exists: it is the only thing that surfaces a half-resolved budget."""
|
|
221
|
+
worst = "ok"
|
|
222
|
+
for used, total in ((flash_used, flash_total), (ram_used, ram_total)):
|
|
223
|
+
if used is None or not total:
|
|
224
|
+
continue
|
|
225
|
+
frac = used / total
|
|
226
|
+
if frac > 1.0:
|
|
227
|
+
return "over"
|
|
228
|
+
if frac >= WARN_FRACTION:
|
|
229
|
+
worst = "warn"
|
|
230
|
+
return worst
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class MemoryBudget:
|
|
235
|
+
"""A resolved per-slice FLASH/RAM budget in bytes, plus a human note when a
|
|
236
|
+
coarser fallback source was used."""
|
|
237
|
+
|
|
238
|
+
flash_total: int | None = None
|
|
239
|
+
ram_total: int | None = None
|
|
240
|
+
note: str | None = None
|
|
241
|
+
#: `(base, size)` apertures of SRAM banks that are NOT the RAM budget's region
|
|
242
|
+
#: (tan-cli#1402); a section linked inside one is not counted against it.
|
|
243
|
+
ram_off_budget_spans: tuple[tuple[int, int], ...] = ()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def budget_note_only(note: str) -> MemoryBudget:
|
|
247
|
+
return MemoryBudget(None, None, note)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _saturating_u64(value: float) -> int:
|
|
251
|
+
"""Rust's `as u64` cast for a FINITE input -- truncate toward zero, 0 at or
|
|
252
|
+
below zero, `u64::MAX` above it -- and 0, never a number, for a NON-FINITE
|
|
253
|
+
one.
|
|
254
|
+
|
|
255
|
+
That cast used to sit outside its caller's `try` (tan-cli#499 defect 8), so
|
|
256
|
+
an infinite `mram_mb` / `soc_flash_mb` / `tcm_kb` / `sram_banks_kb` entry --
|
|
257
|
+
which `json.loads` produces for an `Infinity` literal or an overflowing
|
|
258
|
+
`1e400` in a SoC JSON -- collapsed the whole `tan size` run into
|
|
259
|
+
`size.internal-failure` at exit 5, with `data.slices` EMPTY (every other
|
|
260
|
+
slice's measurement discarded) and `project` null. Python's `int()` refuses
|
|
261
|
+
`inf` (`OverflowError: cannot convert float infinity to integer`) where
|
|
262
|
+
Rust's cast does not, so the two cases have to be separated by hand.
|
|
263
|
+
|
|
264
|
+
**Where the boundary sits, and why it is NOT "saturate everything"
|
|
265
|
+
(tan-cli#499 REVIEW).** The first version of this fix answered `+inf` with
|
|
266
|
+
`u64::MAX`, arguing continuity across the f64 range. Measured, that
|
|
267
|
+
manufactures a confidently WRONG budget at `ok:true`, which is the exact
|
|
268
|
+
failure class this issue exists to close:
|
|
269
|
+
|
|
270
|
+
# SoC JSON with "mram_mb": 1e400
|
|
271
|
+
this branch, before: exit 0 ok:true flash.total 18446744073709551615
|
|
272
|
+
tan 0.4.1: exit 0 ok:true flash.total null,
|
|
273
|
+
budget_note "unreadable SoM preset for E1M-AEN801"
|
|
274
|
+
|
|
275
|
+
16 EiB of flash, stated as fact, on a manifest a customer could then read as
|
|
276
|
+
"plenty of room". So the saturation is BOUNDED to the inputs where the
|
|
277
|
+
oracle demonstrably saturates -- the finite ones:
|
|
278
|
+
|
|
279
|
+
# SoC JSON with "mram_mb": 1e300 (finite; serde_json reads it)
|
|
280
|
+
this branch: exit 0 flash.total 18446744073709551615
|
|
281
|
+
tan 0.4.1: exit 0 flash.total 18446744073709551615 <- byte-identical
|
|
282
|
+
|
|
283
|
+
A NON-finite value is not a point on that curve: serde_json refuses `1e400`
|
|
284
|
+
/ `Infinity` / `NaN` outright, so there is no oracle answer to be continuous
|
|
285
|
+
WITH. Those join the negative/NaN arm at 0 -- this module's own "unresolved"
|
|
286
|
+
value (`_region_resolved` treats 0 as unresolved, `classify` skips it,
|
|
287
|
+
`budget_fully_known()` is False for it), so the envelope reports `pct: null`
|
|
288
|
+
and no budget rather than a fabricated maximum. A residual divergence from
|
|
289
|
+
the oracle stays (`total: 0` vs `total: null` plus a note); it is upstream
|
|
290
|
+
of this file and NOT fixed here -- see the PR's "Not fixed" section.
|
|
291
|
+
"""
|
|
292
|
+
if value != value or value <= 0: # NaN, -inf, and every non-positive
|
|
293
|
+
return 0
|
|
294
|
+
if value == float("inf"):
|
|
295
|
+
# NOT `_U64_MAX`: see the docstring. An input the oracle's own reader
|
|
296
|
+
# refuses is answered "no budget", not "the largest budget there is".
|
|
297
|
+
return 0
|
|
298
|
+
return min(int(value), _U64_MAX)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _mb_to_bytes(mb: float) -> int:
|
|
302
|
+
"""Megabytes -> bytes with Rust's `as u64` cast semantics: truncate toward
|
|
303
|
+
zero, and saturate a negative or NaN input to 0. A `Some(0)` total counts as
|
|
304
|
+
unresolved everywhere downstream (`region_resolved`), so a nonsense
|
|
305
|
+
`mram_mb` degrades to "no budget" rather than to "fits easily"."""
|
|
306
|
+
try:
|
|
307
|
+
value = float(mb) * 1024.0 * 1024.0
|
|
308
|
+
except (TypeError, ValueError, OverflowError):
|
|
309
|
+
return 0
|
|
310
|
+
return _saturating_u64(value)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _kib_to_bytes(kib: float) -> int:
|
|
314
|
+
try:
|
|
315
|
+
value = float(kib) * 1024.0
|
|
316
|
+
except (TypeError, ValueError, OverflowError):
|
|
317
|
+
return 0
|
|
318
|
+
return _saturating_u64(value)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def sram_banks(variant: dict) -> list[tuple[str, float]]:
|
|
322
|
+
"""A SoC-JSON variant's `sram_banks_kb` as ordered `(name, kib)` pairs,
|
|
323
|
+
dropping non-numeric and unrepresentable entries. Insertion order is the
|
|
324
|
+
wire order, so the first-matching-bank rule in [`resolve_budget`] is
|
|
325
|
+
deterministic.
|
|
326
|
+
|
|
327
|
+
A JSON integer too large for an f64 is DROPPED, not crashed on
|
|
328
|
+
(tan-cli#499 defect 8): `float(10**400)` raises `OverflowError: int too
|
|
329
|
+
large to convert to float`, which took the whole `tan size` run to
|
|
330
|
+
`size.internal-failure` at exit 5. Dropping matches what `_mb_to_bytes`
|
|
331
|
+
already does with the identical value -- its own `float()` raises inside a
|
|
332
|
+
`try` and it returns the unresolved 0 -- so both regions answer "no budget"
|
|
333
|
+
for an unrepresentable integer rather than one crashing and one not. A
|
|
334
|
+
non-finite FLOAT is passed through and reaches the same answer one level
|
|
335
|
+
down: [`_saturating_u64`] resolves it to 0, not to `u64::MAX`."""
|
|
336
|
+
raw = variant.get("sram_banks_kb")
|
|
337
|
+
if not isinstance(raw, dict):
|
|
338
|
+
return []
|
|
339
|
+
out: list[tuple[str, float]] = []
|
|
340
|
+
for name, value in raw.items():
|
|
341
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
kib = float(value)
|
|
345
|
+
except OverflowError:
|
|
346
|
+
continue
|
|
347
|
+
out.append((str(name), kib))
|
|
348
|
+
return out
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def resolve_variant(
|
|
352
|
+
silicon_variant: str | None, sku: str | None, variants: list[dict]
|
|
353
|
+
) -> dict | None:
|
|
354
|
+
"""Resolve a SoM preset to its matching SoC-JSON variant: forward via
|
|
355
|
+
`silicon_variant == order_code` (skipping empty / `TBD`, trimmed -- #276),
|
|
356
|
+
then reverse via `sku in alp_module_skus`. `None` when neither path
|
|
357
|
+
resolves. A forward value that names no variant falls THROUGH to the
|
|
358
|
+
reverse lookup."""
|
|
359
|
+
if silicon_variant and not is_pending_placeholder(silicon_variant):
|
|
360
|
+
for variant in variants:
|
|
361
|
+
if variant.get("order_code") == silicon_variant:
|
|
362
|
+
return variant
|
|
363
|
+
if sku is None:
|
|
364
|
+
return None
|
|
365
|
+
for variant in variants:
|
|
366
|
+
skus = variant.get("alp_module_skus")
|
|
367
|
+
if isinstance(skus, list) and any(s == sku for s in skus):
|
|
368
|
+
return variant
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def slot0_bytes_for_core(
|
|
373
|
+
core_id: str, memory_map: list[dict] | None
|
|
374
|
+
) -> int | None:
|
|
375
|
+
"""This core's OWN slot0 window from the SoM preset's `memory_map`, in
|
|
376
|
+
bytes -- `None` when the preset declares none for it (tan-cli#747).
|
|
377
|
+
|
|
378
|
+
Since alp-sdk#1445/#1069 a dual-M55 AEN SoM gives each core a disjoint
|
|
379
|
+
slot0 (`he_slot0` at 0x80010000, `hp_slot0` at 0x802b0000, 2688 KiB
|
|
380
|
+
each), and only that window is linkable by that core. The whole-MRAM
|
|
381
|
+
figure is the sum of every partition INCLUDING the other core's, so it
|
|
382
|
+
is nobody's budget.
|
|
383
|
+
|
|
384
|
+
Matched on the `<role>_slot0` name, role being the core id's last
|
|
385
|
+
segment -- the same `core_id.split("_")[-1]` the SDK's own
|
|
386
|
+
`gen_zephyr_board` uses to pick a role's window, not a second spelling
|
|
387
|
+
that could drift from it. `accessible_from`, when the entry declares
|
|
388
|
+
one, must name this core: a window some other core owns is not a budget
|
|
389
|
+
for this one, and silently accepting it would reintroduce exactly the
|
|
390
|
+
cross-core confusion #1069 exists to prevent.
|
|
391
|
+
"""
|
|
392
|
+
role = core_id.rsplit("_", 1)[-1]
|
|
393
|
+
if not role:
|
|
394
|
+
return None
|
|
395
|
+
want = f"{role}_slot0"
|
|
396
|
+
for entry in memory_map or []:
|
|
397
|
+
if not isinstance(entry, dict) or entry.get("name") != want:
|
|
398
|
+
continue
|
|
399
|
+
accessible = entry.get("accessible_from")
|
|
400
|
+
if isinstance(accessible, list) and accessible and core_id not in accessible:
|
|
401
|
+
continue
|
|
402
|
+
size_kib = entry.get("size_kib")
|
|
403
|
+
# bool is an int in Python; a `size_kib: true` is malformed, not 1 KiB.
|
|
404
|
+
if isinstance(size_kib, bool) or not isinstance(size_kib, (int, float)):
|
|
405
|
+
continue
|
|
406
|
+
if size_kib <= 0 or not math.isfinite(float(size_kib)):
|
|
407
|
+
continue
|
|
408
|
+
return int(size_kib) * 1024
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# Only SRAM0 (SRAM0_BASE) is excluded from the RAM budget. Limitation: SRAM1
|
|
413
|
+
# (0x08000000) placements are still charged against the DTCM budget.
|
|
414
|
+
def resolve_budget(
|
|
415
|
+
core_id: str,
|
|
416
|
+
mram_mb: float | None,
|
|
417
|
+
soc_flash_mb: float | None,
|
|
418
|
+
sram_banks_kb: list[tuple[str, float]],
|
|
419
|
+
soc_cores: list[tuple[str, float | None]],
|
|
420
|
+
memory_map: list[dict] | None = None,
|
|
421
|
+
) -> MemoryBudget:
|
|
422
|
+
"""The FLASH/RAM budget for one core, from values a caller already read out
|
|
423
|
+
of the SoM preset + SoC JSON. FLASH: this core's own `<role>_slot0` window
|
|
424
|
+
from the SoM `memory_map`, else variant `mram_mb`, else SoC
|
|
425
|
+
`soc_flash_mb` (with a note). RAM: the `*_DTCM` bank whose name contains the
|
|
426
|
+
core token, else the core's `tcm_kb` (with a note). Anything unresolvable
|
|
427
|
+
stays `None` -- the caller renders `unknown`, never a guessed number.
|
|
428
|
+
|
|
429
|
+
FLASH prefers the per-core window for the reason the RAM half below has
|
|
430
|
+
always resolved per core: a shared total is not a budget. Measured before
|
|
431
|
+
the fix, `E1M-AEN801` reported BOTH M55s against 5767168 B (the whole
|
|
432
|
+
5.5 MB MRAM) while each image links into 2752512 B, so utilisation read
|
|
433
|
+
2.0% where the linker said 4.22%, and `over_budget` could never fire --
|
|
434
|
+
an image would have to exceed the whole part, which it cannot, because
|
|
435
|
+
mcuboot + both slot0s + reserved + storage + atoc already fill it
|
|
436
|
+
(tan-cli#747).
|
|
437
|
+
|
|
438
|
+
`memory_map` defaults to `None` so every existing caller keeps today's
|
|
439
|
+
behaviour exactly; a SoM that declares no per-role window (single-M55
|
|
440
|
+
parts, non-AEN families, presets predating alp-sdk#1445) still falls
|
|
441
|
+
through to `mram_mb` with no note, byte-identical to before."""
|
|
442
|
+
notes: list[str] = []
|
|
443
|
+
|
|
444
|
+
slot0_total = slot0_bytes_for_core(core_id, memory_map)
|
|
445
|
+
if slot0_total is not None:
|
|
446
|
+
flash_total: int | None = slot0_total
|
|
447
|
+
elif mram_mb is not None:
|
|
448
|
+
flash_total = _mb_to_bytes(mram_mb)
|
|
449
|
+
elif soc_flash_mb is not None:
|
|
450
|
+
notes.append("flash=soc_flash_mb")
|
|
451
|
+
flash_total = _mb_to_bytes(soc_flash_mb)
|
|
452
|
+
else:
|
|
453
|
+
flash_total = None
|
|
454
|
+
|
|
455
|
+
token = core_token(core_id)
|
|
456
|
+
ram_total: int | None = None
|
|
457
|
+
for name, kib in sram_banks_kb:
|
|
458
|
+
if token in name and "DTCM" in name:
|
|
459
|
+
ram_total = _kib_to_bytes(kib)
|
|
460
|
+
break
|
|
461
|
+
if ram_total is None:
|
|
462
|
+
for core, tcm_kb in soc_cores:
|
|
463
|
+
if core == core_id:
|
|
464
|
+
if tcm_kb is not None:
|
|
465
|
+
ram_total = _kib_to_bytes(tcm_kb)
|
|
466
|
+
notes.append("ram=core tcm_kb (ITCM+DTCM)")
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
off_budget: tuple[tuple[int, int], ...] = ()
|
|
470
|
+
if ram_total is not None:
|
|
471
|
+
for name, kib in sram_banks_kb:
|
|
472
|
+
if name.upper() == "SRAM0":
|
|
473
|
+
off_budget = ((SRAM0_BASE, _kib_to_bytes(kib)),)
|
|
474
|
+
break
|
|
475
|
+
return MemoryBudget(flash_total, ram_total, "; ".join(notes) or None, off_budget)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@dataclass
|
|
479
|
+
class SliceSize:
|
|
480
|
+
"""One slice's measured footprint vs its resolved budget. `status` is one of
|
|
481
|
+
`ok`/`over`/`warn`/`not-built`/`n/a`/`no-budget`."""
|
|
482
|
+
|
|
483
|
+
core_id: str
|
|
484
|
+
os: str
|
|
485
|
+
status: str
|
|
486
|
+
flash_used: int | None = None
|
|
487
|
+
flash_total: int | None = None
|
|
488
|
+
ram_used: int | None = None
|
|
489
|
+
ram_total: int | None = None
|
|
490
|
+
source: str | None = None
|
|
491
|
+
note: str | None = None
|
|
492
|
+
notes: list[str] = field(default_factory=list)
|
|
493
|
+
|
|
494
|
+
def budget_fully_known(self) -> bool:
|
|
495
|
+
"""True only when BOTH regions resolved to a nonzero total.
|
|
496
|
+
|
|
497
|
+
Deliberately an AND, not an OR: `classify` silently skips whichever
|
|
498
|
+
region has no usable `total` (correct in isolation), so with an OR a
|
|
499
|
+
half-resolved budget reported as `ok` and never surfaced -- neither
|
|
500
|
+
`--fail-over-budget` nor `summary.unknown_budget` named the slice.
|
|
501
|
+
A total of exactly `0` counts as unresolved for the same reason
|
|
502
|
+
`classify` already skips it.
|
|
503
|
+
"""
|
|
504
|
+
return _region_resolved(self.flash_total) and _region_resolved(self.ram_total)
|
|
505
|
+
|
|
506
|
+
def over_budget(self) -> bool:
|
|
507
|
+
return self.status == "over"
|
|
508
|
+
|
|
509
|
+
def to_json_entry(self) -> dict[str, Any]:
|
|
510
|
+
"""This slice's `alp-size/1` JSON entry. Key order IS the wire order:
|
|
511
|
+
core_id, os, status, flash, ram, source, then optional budget_note +
|
|
512
|
+
notes."""
|
|
513
|
+
entry: dict[str, Any] = {
|
|
514
|
+
"core_id": self.core_id,
|
|
515
|
+
"os": self.os,
|
|
516
|
+
"status": self.status,
|
|
517
|
+
"flash": region_json(self.flash_used, self.flash_total),
|
|
518
|
+
"ram": region_json(self.ram_used, self.ram_total),
|
|
519
|
+
"source": self.source,
|
|
520
|
+
}
|
|
521
|
+
if self.note:
|
|
522
|
+
entry["budget_note"] = self.note
|
|
523
|
+
if self.notes:
|
|
524
|
+
entry["notes"] = list(self.notes)
|
|
525
|
+
return entry
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _region_resolved(total: int | None) -> bool:
|
|
529
|
+
return total is not None and total != 0
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def build_size_report(rows: list[SliceSize]) -> dict[str, Any]:
|
|
533
|
+
"""The full `alp-size/1` payload wrapped verbatim in the tan Envelope's
|
|
534
|
+
`data`: `{schema, slices, summary{over_budget, unknown_budget}}`."""
|
|
535
|
+
return {
|
|
536
|
+
"schema": "alp-size/1",
|
|
537
|
+
"slices": [row.to_json_entry() for row in rows],
|
|
538
|
+
"summary": {
|
|
539
|
+
"over_budget": sorted(r.core_id for r in rows if r.over_budget()),
|
|
540
|
+
"unknown_budget": sorted(r.core_id for r in unknown_budget_rows(rows)),
|
|
541
|
+
},
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def over_budget_rows(rows: list[SliceSize]) -> list[SliceSize]:
|
|
546
|
+
return [r for r in rows if r.over_budget()]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def unknown_budget_rows(rows: list[SliceSize]) -> list[SliceSize]:
|
|
550
|
+
"""Zephyr slices that were measured but whose budget could not be fully
|
|
551
|
+
resolved -- the ones `--fail-over-budget` skips. Keyed on the resolved
|
|
552
|
+
totals (`budget_fully_known`) rather than on `status == "no-budget"`, which
|
|
553
|
+
the caller only sets when BOTH regions are unresolved."""
|
|
554
|
+
return [
|
|
555
|
+
r
|
|
556
|
+
for r in rows
|
|
557
|
+
if r.os == "zephyr" and r.status != "not-built" and not r.budget_fully_known()
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def human_bytes(n: int | None) -> str:
|
|
562
|
+
"""Bytes -> a compact KiB/MiB string, or `?` for unknown."""
|
|
563
|
+
if n is None:
|
|
564
|
+
return "?"
|
|
565
|
+
if n >= 1024 * 1024:
|
|
566
|
+
return f"{n / (1024.0 * 1024.0):.2f}M"
|
|
567
|
+
if n >= 1024:
|
|
568
|
+
return f"{n / 1024.0:.1f}K"
|
|
569
|
+
return f"{n}B"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def region_cell(used: int | None, total: int | None) -> str:
|
|
573
|
+
"""A `used/total pct%` table cell, exactly 24 columns wide for the values
|
|
574
|
+
this command produces."""
|
|
575
|
+
pct = f"{used / total * 100.0:5.1f}%" if used is not None and total else " - "
|
|
576
|
+
return f"{human_bytes(used):>8}/{human_bytes(total):<8} {pct}"
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _status_hue(status: str) -> tuple[str, str]:
|
|
580
|
+
"""Status hue + label. An unknown status renders plain, with its raw text."""
|
|
581
|
+
return {
|
|
582
|
+
"ok": (_GREEN, "OK"),
|
|
583
|
+
"warn": (_YELLOW, "WARN"),
|
|
584
|
+
"over": (_RED, "OVER"),
|
|
585
|
+
"not-built": (_YELLOW, "not built"),
|
|
586
|
+
"n/a": (_CYAN, "n/a"),
|
|
587
|
+
"no-budget": (_CYAN, "no budget"),
|
|
588
|
+
}.get(status, ("", status))
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def render_table_lines(rows: list[SliceSize], use_color: bool) -> list[str]:
|
|
592
|
+
"""The per-slice table: header, separator, one row per slice, and a
|
|
593
|
+
`-> <detail>` continuation for any slice carrying a note."""
|
|
594
|
+
head = (
|
|
595
|
+
f"{'CORE':<14} {'OS':<10} {'FLASH used/total':<24} "
|
|
596
|
+
f"{'RAM used/total':<24} STATUS"
|
|
597
|
+
)
|
|
598
|
+
lines = [head, "-" * len(head)]
|
|
599
|
+
for row in rows:
|
|
600
|
+
hue, label = _status_hue(row.status)
|
|
601
|
+
status = f"{hue}{label}{_RESET}" if use_color and hue else label
|
|
602
|
+
lines.append(
|
|
603
|
+
f"{row.core_id:<14} {row.os:<10} "
|
|
604
|
+
f"{region_cell(row.flash_used, row.flash_total):<24} "
|
|
605
|
+
f"{region_cell(row.ram_used, row.ram_total):<24} {status}"
|
|
606
|
+
)
|
|
607
|
+
detail = row.note or (row.notes[0] if row.notes else None)
|
|
608
|
+
if detail is not None:
|
|
609
|
+
lines.append(f"{'':<14} {'':<10} -> {detail}")
|
|
610
|
+
return lines
|