diff --git a/README.md b/README.md index 394a6fe..414365e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Developer utilities, visual tools, and host applications for the [Sound Open Fir | Tool | Directory | Description | | :--- | :--- | :--- | +| **[`sof-top`](sof-top/sof-top)** | [`sof-top/`](sof-top/) | GTK4 Live DSP Resource Monitor — Moving Core Utilization Graph, Moving Memory Utilization Graph (HPSRAM, LPSRAM, DRAM L3/IMR, Heaps), and Unified DSP Task List | +| **[`sof-remote-server`](sof-remote-server/)** | [`sof-remote-server/`](sof-remote-server/) | Lightweight C TCP Telemetry & Control Daemon (Port `9998`) for DUTs via `/dev/sof-dsp0` or direct PCIe BAR4 MMIO + DebugFS/SysFS fallback | | **[`sof-builder`](sof-builder/)** | [`sof-builder/`](sof-builder/) | GTK4 Topology2 Visual Graph Editor, IPC4 `.tplg` Compiler & Real-Time `sof-testbench4` Simulation / Telemetry Studio | | **[`sof-testbench`](sof-testbench/)** | [`sof-testbench/`](sof-testbench/) | Multi-Core LL/DP `sof-testbench4` Simulator, `sof-tb-ctl` TCP Control CLI, Host Platform Runtime (`platform/library`), and 70-Test Suite | | **[`sof-tplg-parser`](sof-tplg-parser/)** | [`sof-tplg-parser/`](sof-tplg-parser/) | ALSA Topology (`.tplg`) IPC3/IPC4 Parser Library (`libsof_tplg_parser.a`) | @@ -69,6 +71,18 @@ See [`sof-builder/README.md`](sof-builder/README.md) for full documentation, env --- +## Quick Start: `sof-top` + +```bash +# Build sof_remote_server for DUTs: +make -C sof-remote-server + +# Launch sof-top (Moving Core Graph + Moving SRAM/DRAM Memory Graph + DSP Task List): +./sof-top/sof-top --host aphid --wake +``` + +--- + ## License Licensed under the **BSD 3-Clause License**. See [`LICENSE`](LICENSE) for details. diff --git a/sof-remote-server/sof_remote_server.c b/sof-remote-server/sof_remote_server.c index ba73b0b..3d1e5a3 100644 --- a/sof-remote-server/sof_remote_server.c +++ b/sof-remote-server/sof_remote_server.c @@ -501,6 +501,141 @@ static void discover_sof_pci_device(void) pthread_mutex_unlock(&g_hw_lock); } +static int read_debugfs_fw_state(void) +{ + int fd = open("/sys/kernel/debug/sof/fw_state", O_RDONLY); + if (fd < 0) + return -1; + uint32_t state = 0; + ssize_t n = read(fd, &state, sizeof(state)); + close(fd); + if (n == (ssize_t)sizeof(state) && state <= 16U) + return (int)state; + return -1; +} + +static int get_sof_card_index(void) +{ + discover_sof_pci_device(); + + /* 1. Check /sys/bus/pci/devices//sound/card* */ + if (g_hw_ctx.found && g_hw_ctx.bdf[0] != '\0') { + char snd_path[256]; + snprintf(snd_path, sizeof(snd_path), + "/sys/bus/pci/devices/%s/sound", g_hw_ctx.bdf); + DIR *sd = opendir(snd_path); + if (sd) { + struct dirent *de; + unsigned int idx = 0; + while ((de = readdir(sd)) != NULL) { + if (sscanf(de->d_name, "card%u", &idx) == 1) { + closedir(sd); + return (int)idx; + } + } + closedir(sd); + } + } + + /* 2. Check /sys/class/sound/card* device symlink for BDF or "sof" */ + DIR *cd = opendir("/sys/class/sound"); + if (cd) { + struct dirent *de; + unsigned int idx = 0; + while ((de = readdir(cd)) != NULL) { + if (sscanf(de->d_name, "card%u", &idx) != 1) + continue; + char dev_link[256], resolved[512]; + snprintf(dev_link, sizeof(dev_link), + "/sys/class/sound/%s/device", de->d_name); + if (realpath(dev_link, resolved)) { + if ((g_hw_ctx.found && g_hw_ctx.bdf[0] != '\0' && + strstr(resolved, g_hw_ctx.bdf) != NULL) || + strstr(resolved, "sof") != NULL) { + closedir(cd); + return (int)idx; + } + } + } + closedir(cd); + } + + /* 3. Fallback: scan /proc/asound/cards for an SOF card */ + FILE *fp = fopen("/proc/asound/cards", "r"); + if (fp) { + char line[256]; + while (fgets(line, sizeof(line), fp)) { + unsigned int idx = 0; + if (sscanf(line, " %u [", &idx) == 1 && + (strstr(line, "sof-") != NULL || strstr(line, "SOF") != NULL)) { + fclose(fp); + return (int)idx; + } + } + fclose(fp); + } + return -1; +} + +static void trigger_on_demand_dsp_boot(void) +{ + int sof_card = get_sof_card_index(); + if (sof_card < 0) + return; + + FILE *fp = fopen("/proc/asound/pcm", "r"); + if (!fp) + return; + + char line[256]; + uint32_t card = 0, dev = 0; + bool found_cap = false, found_play = false; + + while (fgets(line, sizeof(line), fp)) { + unsigned int c = 0, d = 0; + if (sscanf(line, "%u-%u:", &c, &d) == 2 && + c == (unsigned int)sof_card) { + if (strstr(line, "capture")) { + card = c; + dev = d; + found_cap = true; + break; + } + if (strstr(line, "playback") && !found_play) { + card = c; + dev = d; + found_play = true; + } + } + } + fclose(fp); + + if (!found_cap && !found_play) + return; + + char pcm_dev[32]; + snprintf(pcm_dev, sizeof(pcm_dev), "hw:%u,%u", card, dev); + uint8_t dummy[4096]; + memset(dummy, 0, sizeof(dummy)); + uint32_t frames_io = 0, rms = 0, peak = 0, xruns = 0; + + if (found_cap) { + if (alsa_pcm_capture_frames(pcm_dev, SOF_PCM_FMT_S16_LE, 16000, 2, + 160, 2, dummy, 160, &frames_io, + &rms, &peak, &xruns) < 0) { + alsa_pcm_capture_frames(pcm_dev, SOF_PCM_FMT_S16_LE, 48000, 2, + 240, 2, dummy, 240, &frames_io, + &rms, &peak, &xruns); + } + } else { + _Atomic uint64_t f_played = 0; + _Atomic uint32_t vrms = 0, vpeak = 0; + alsa_pcm_play_frames(pcm_dev, SOF_PCM_FMT_S16_LE, 48000, 2, + 240, 2, dummy, 240, NULL, 0, + &f_played, &vrms, &vpeak, &xruns); + } +} + static bool sysfs_pm_is_active(void) { discover_sof_pci_device(); @@ -518,6 +653,28 @@ static bool sysfs_pm_is_active(void) return false; } +static int sysfs_pm_get_control(char *out, size_t cap) +{ + if (!out || cap == 0) + return -EINVAL; + out[0] = '\0'; + discover_sof_pci_device(); + if (!g_hw_ctx.found) + return -ENODEV; + FILE *fp = fopen(g_hw_ctx.pm_control_path, "r"); + if (!fp) + return -errno; + if (!fgets(out, (int)cap, fp)) { + fclose(fp); + return -EIO; + } + fclose(fp); + size_t len = strlen(out); + while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r')) + out[--len] = '\0'; + return 0; +} + static int sysfs_pm_set_control(const char *mode, bool hold_flag) { discover_sof_pci_device(); @@ -530,11 +687,22 @@ static int sysfs_pm_set_control(const char *mode, bool hold_flag) int err = (n < 0) ? -errno : 0; close(fd); if (err == 0) { - g_hw_ctx.holding_d0 = hold_flag; - if (hold_flag) { + if (hold_flag) + g_hw_ctx.holding_d0 = true; + else if (strcmp(mode, "auto") == 0) + g_hw_ctx.holding_d0 = false; + if (strcmp(mode, "on") == 0) { /* Wait up to 100ms for runtime_status == active */ for (int i = 0; i < 20 && !sysfs_pm_is_active(); i++) usleep(5000); + /* + * On PTL (on_demand_dsp_boot), runtime_status transitions to + * active with fw_state == 2 (SOF_FW_BOOT_PREPARE) without booting + * the DSP until a PCM stream is opened. Trigger a short PCM open + * to complete DSP boot when waking. + */ + if (read_debugfs_fw_state() == 2) + trigger_on_demand_dsp_boot(); } } return err; @@ -716,12 +884,16 @@ static int populate_direct_cdev_info(struct sof_cdev_info *info) info->max_payload_size = 4096; info->hw_ip_version = g_hw_ctx.hw_ip_version; const char *short_drv = g_hw_ctx.driver; - if (strncmp(short_drv, "sof-audio-pci-intel-", 20) == 0) + if (strncmp(short_drv, "sof-audio-pci-intel-", 20) == 0) { snprintf(info->platform, sizeof(info->platform), "sof-%s (%s)", short_drv + 20, g_hw_ctx.bdf); - else + snprintf(info->fw_filename, sizeof(info->fw_filename), + "sof-%s.ri", short_drv + 20); + } else { snprintf(info->platform, sizeof(info->platform), "%s (%s)", short_drv, g_hw_ctx.bdf); + snprintf(info->fw_filename, sizeof(info->fw_filename), "sof.ri"); + } for (int w = 0; w < SOF_CDEV_MAX_WINDOWS; w++) { info->windows[w].win_id = (uint32_t)w; @@ -730,18 +902,44 @@ static int populate_direct_cdev_info(struct sof_cdev_info *info) info->windows[w].flags = 1; } - if (info->pm_runtime_active) { + int dfs_fw_state = read_debugfs_fw_state(); + if (dfs_fw_state >= 0) { + info->fw_state = (uint32_t)dfs_fw_state; + } else if (info->pm_runtime_active) { uint32_t fw_regs[8] = {0}; if (bar4_mmio_read(g_hw_ctx.win_bar_off[0], fw_regs, sizeof(fw_regs)) == 0 && fw_regs[0] != 0xFFFFFFFFU) { - if ((fw_regs[0] & 0x0FFFFFFFU) >= 5) + if (fw_regs[0] == 0U || (fw_regs[0] & 0x0FFFFFFFU) >= 5U) info->fw_state = 7; /* SOF_FW_BOOT_COMPLETE */ else info->fw_state = fw_regs[0] & 0xFFU; } else { info->fw_state = 7; } + } else { + info->fw_state = 7; + } + if (info->fw_state == 7) { + FILE *vfp = fopen("/sys/kernel/debug/sof/fw_version", "r"); + if (vfp) { + unsigned int maj = 0, min = 0, mic = 0, bld = 0; + char vline[128]; + while (fgets(vline, sizeof(vline), vfp)) { + if (sscanf(vline, "%u.%u.%u.%u", &maj, &min, &mic, &bld) >= 3 || + sscanf(vline, "version: %u.%u.%u.%u", &maj, &min, &mic, &bld) >= 3) { + info->fw_ver_major = (uint16_t)maj; + info->fw_ver_minor = (uint16_t)min; + info->fw_ver_micro = (uint16_t)mic; + info->fw_ver_build = (uint16_t)bld; + break; + } + } + fclose(vfp); + } + } + + if (info->pm_runtime_active && info->fw_state == 7) { /* Read MW2 Page 0 debug slot descriptors (15 x 12-byte descriptors) */ uint32_t desc_raw[15 * 3]; if (bar4_mmio_read(g_hw_ctx.win_bar_off[2], desc_raw, sizeof(desc_raw)) == 0) { @@ -761,8 +959,6 @@ static int populate_direct_cdev_info(struct sof_cdev_info *info) info->debug_slots[s].size = 4096U; } } - } else { - info->fw_state = 7; } return 0; } @@ -868,16 +1064,219 @@ static int read_large_cfg_helper(int cdev_fd, uint32_t param_id, void *buf, return 0; } +static int read_file_first_line(const char *path, char *out, size_t cap) +{ + FILE *fp = fopen(path, "r"); + if (!fp) + return -1; + if (!fgets(out, (int)cap, fp)) { + fclose(fp); + return -1; + } + fclose(fp); + size_t len = strlen(out); + while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r')) + out[--len] = '\0'; + return 0; +} + +static uint32_t count_running_alsa_substreams(void) +{ + uint32_t running = atomic_load_explicit(&g_tone_running, memory_order_acquire) ? 1U : 0U; + int sof_card = get_sof_card_index(); + if (sof_card < 0) + return running; + + char card_path[128]; + snprintf(card_path, sizeof(card_path), "/proc/asound/card%d", sof_card); + DIR *card_dir = opendir(card_path); + if (!card_dir) + return running; + + struct dirent *de; + while ((de = readdir(card_dir)) != NULL) { + if (strncmp(de->d_name, "pcm", 3) != 0) + continue; + char pcm_path[256]; + snprintf(pcm_path, sizeof(pcm_path), "%s/%s", card_path, de->d_name); + DIR *pcm_dir = opendir(pcm_path); + if (!pcm_dir) + continue; + struct dirent *sde; + while ((sde = readdir(pcm_dir)) != NULL) { + if (strncmp(sde->d_name, "sub", 3) != 0) + continue; + char st_path[384]; + snprintf(st_path, sizeof(st_path), + "%s/%s/status", pcm_path, sde->d_name); + char buf[128]; + if (read_file_first_line(st_path, buf, sizeof(buf)) == 0 && + strstr(buf, "RUNNING") != NULL) + running++; + } + closedir(pcm_dir); + } + closedir(card_dir); + return running; +} + +static pthread_mutex_t g_fw_mods_lock = PTHREAD_MUTEX_INITIALIZER; +static uint8_t g_cached_fw_mods_tlv[4096]; +static uint32_t g_cached_fw_mods_len; +static bool g_cached_fw_mods_loaded; + +static void synthesize_modules_info_from_fw(struct sof_net_telem_snapshot *snap) +{ + if (snap->flags_present & SOF_TELEM_FL_MODULES_INFO_TLV) + return; + + pthread_mutex_lock(&g_fw_mods_lock); + if (!g_cached_fw_mods_loaded) { + char fw_dir[128] = "intel/sof-ipc4/ptl/community"; + char fw_name[64] = "sof-ptl.ri"; + read_file_first_line("/sys/kernel/debug/sof/fw_profile/fw_path", + fw_dir, sizeof(fw_dir)); + read_file_first_line("/sys/kernel/debug/sof/fw_profile/fw_name", + fw_name, sizeof(fw_name)); + + char candidates[4][256]; + snprintf(candidates[0], sizeof(candidates[0]), + "/lib/firmware/%s/%s", fw_dir, fw_name); + snprintf(candidates[1], sizeof(candidates[1]), + "/lib/firmware/intel/sof-ipc4/ptl/%s", fw_name); + snprintf(candidates[2], sizeof(candidates[2]), + "/lib/firmware/intel/sof-ipc4/arl/%s", fw_name); + snprintf(candidates[3], sizeof(candidates[3]), + "/lib/firmware/intel/sof-ipc4/tgl/%s", fw_name); + + FILE *fp = NULL; + for (int i = 0; i < 4 && !fp; i++) + fp = fopen(candidates[i], "rb"); + + if (fp) { + fseek(fp, 0, SEEK_END); + long fsz = ftell(fp); + fseek(fp, 0, SEEK_SET); + if (fsz > 0 && fsz <= 16 * 1024 * 1024) { + uint8_t *fw_buf = malloc((size_t)fsz); + if (fw_buf && fread(fw_buf, 1, (size_t)fsz, fp) == (size_t)fsz) { + uint32_t mod_cnt = 0; + uint32_t out_off = 4; + for (long pos = 0; pos + 116 <= fsz && mod_cnt < 34; pos += 4) { + if (memcmp(fw_buf + pos, "$AME", 4) == 0 && + fw_buf[pos + 4] >= 'A' && fw_buf[pos + 4] <= 'Z') { + if (out_off + 116 <= sizeof(g_cached_fw_mods_tlv)) { + uint16_t rt_mod_id = (uint16_t)mod_cnt; + uint16_t rt_state = 2U; /* MODULE_INITIALIZED */ + memcpy(g_cached_fw_mods_tlv + out_off, + fw_buf + pos, 116); + memcpy(g_cached_fw_mods_tlv + out_off, + &rt_mod_id, 2); + memcpy(g_cached_fw_mods_tlv + out_off + 2, + &rt_state, 2); + out_off += 116; + mod_cnt++; + } + pos += 112; + } + } + if (mod_cnt > 0) { + memcpy(g_cached_fw_mods_tlv, &mod_cnt, 4); + g_cached_fw_mods_len = out_off; + } + } + free(fw_buf); + } + fclose(fp); + } + g_cached_fw_mods_loaded = true; + } + + if (g_cached_fw_mods_len > 0 && + g_cached_fw_mods_len <= sizeof(snap->modules_info_tlv)) { + memcpy(snap->modules_info_tlv, g_cached_fw_mods_tlv, + g_cached_fw_mods_len); + snap->modules_info_len = g_cached_fw_mods_len; + snap->flags_present |= SOF_TELEM_FL_MODULES_INFO_TLV; + } + pthread_mutex_unlock(&g_fw_mods_lock); +} + +struct client_telem_state { + uint32_t prev_core_ticks[SOF_TEL_MAX_CORES]; + bool has_prev_core_ticks; +}; + /* * Synthesize IPC4 TLVs (ext_perf_tlv, pwr_state_tlv, mem_state_tlv) directly from * TEL2 (MW2 Slot 1) and PMW2 (MW3) when /dev/sof-dsp0 IPC ioctls are unavailable. */ -static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) +static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap, + struct client_telem_state *tstate) { const uint8_t *t2 = snap->tel2_slot; - const uint8_t *w3 = snap->pmw2_win3; + uint8_t *w3 = snap->pmw2_win3; int bias = sof_tel2_slot_bias(t2, snap->tel2_slot_len); + synthesize_modules_info_from_fw(snap); + + /* + * Sanitize per-core system_tick_info in snap->telem_slot: + * 1) In Zephyr SOF, telemetry_update() is only called inside schedule_ll_callback(). + * When no LL stream/task is running, the LL timer is stopped and systick_info[c] + * remains frozen in SRAM with stale values from the end of the previous stream. + * 2) When a stream stops/restarts, begin_stamp - telemetry_prev_ccount[c] spans the + * idle gap and inflates avg_utilization / peak_utilization beyond max_time_elapsed. + */ + if (snap->flags_present & SOF_TELEM_FL_TELEM_SLOT) { + for (uint32_t c = 0; c < SOF_TEL_MAX_CORES; c++) { + uint32_t *st32 = (uint32_t *)(snap->telem_slot + 4 + c * 32); + uint32_t t_cnt = st32[0]; + uint32_t last_el = st32[1]; + uint32_t max_el = st32[2]; + uint32_t avg_u = st32[4]; + uint32_t peak_u = st32[5]; + uint32_t p4k_u = st32[6]; + uint32_t p8k_u = st32[7]; + + if (t_cnt == 0xFFFFFFFFU) + continue; + + bool ticking; + if (tstate->has_prev_core_ticks) + ticking = (t_cnt != tstate->prev_core_ticks[c]); + else + ticking = (snap->active_pcm_streams > 0 && t_cnt > 0); + tstate->prev_core_ticks[c] = t_cnt; + + if (max_el > 400000U) + max_el = (last_el <= 400000U) ? last_el : 0U; + if (peak_u > 800000U || (max_el > 0 && peak_u > max_el * 2U)) + peak_u = max_el; + if (p4k_u > 800000U || (max_el > 0 && p4k_u > max_el * 2U)) + p4k_u = peak_u; + if (p8k_u > 800000U || (max_el > 0 && p8k_u > max_el * 2U)) + p8k_u = peak_u; + + if (!ticking) { + /* LL scheduler is stopped (0 ticks/s) -> live load is 0 KCPS */ + last_el = 0U; + avg_u = 0U; + } else if (avg_u > 800000U || (max_el > 0 && avg_u > max_el) || + (last_el > 0 && avg_u > last_el * 4U)) { + avg_u = last_el; + } + + st32[1] = last_el; + st32[2] = max_el; + st32[4] = avg_u; + st32[5] = peak_u; + st32[6] = p4k_u; + st32[7] = p8k_u; + } + tstate->has_prev_core_ticks = true; + } + /* 1. Synthesize pwr_state_tlv & enabled_cores_mask from TEL2 */ if ( !(snap->flags_present & SOF_TELEM_FL_PWR_STATE_TLV) && (snap->flags_present & SOF_TELEM_FL_TELEM_SLOT)) { @@ -891,7 +1290,7 @@ static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) for (uint32_t c = 0; c < ncores; c++) { const uint32_t *st32 = (const uint32_t *)(snap->telem_slot + 4 + c * 32); - core_kcps[c] = st32[4]; /* avg_utilization */ + core_kcps[c] = st32[4]; /* sanitized avg_utilization */ if (st32[0] > 0 || st32[4] > 0) core_mask |= (1U << c); } @@ -910,26 +1309,35 @@ static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) snap->flags_present |= SOF_TELEM_FL_PWR_STATE_TLV; } - /* 2. Synthesize mem_state_tlv from TEL2 mem_sec.sram */ - if (!(snap->flags_present & SOF_TELEM_FL_MEM_STATE_TLV) && bias >= 0 && - SOF_TEL_OFF_MEM_SEC - (uint32_t)bias + sizeof(struct sof_tel_mem_section) <= - snap->tel2_slot_len) { - const struct sof_tel_mem_section *ms = - (const struct sof_tel_mem_section *) - (t2 + SOF_TEL_OFF_MEM_SEC - bias); - uint32_t hp_tot_pages = ms->sram.hp_sram_total_pages; - uint32_t hp_free_pages = ms->sram.hp_sram_free_pages; - uint32_t hp_banks = (hp_tot_pages + 15U) / 16U; + /* 2. Synthesize mem_state_tlv only when TEL2 mem_sec.sram is present */ + if (!(snap->flags_present & SOF_TELEM_FL_MEM_STATE_TLV)) { + uint32_t hp_tot_pages = 0, hp_free_pages = 0, hp_banks = 0; + uint32_t pgctl[3] = { 0xFFFFFFFFU, 0xFFFFFFFFU, 0xFFFFFFFFU }; + uint32_t lp_tot_pages = 0, lp_free_pages = 0, lp_banks = 0; + uint32_t lp_pgctl = 0xFFFFFFFFU; + + if (bias >= 0 && + SOF_TEL_OFF_MEM_SEC - (uint32_t)bias + sizeof(struct sof_tel_mem_section) <= + snap->tel2_slot_len) { + const struct sof_tel_mem_section *ms = + (const struct sof_tel_mem_section *) + (t2 + SOF_TEL_OFF_MEM_SEC - bias); + hp_tot_pages = ms->sram.hp_sram_total_pages; + hp_free_pages = ms->sram.hp_sram_free_pages; + hp_banks = (hp_tot_pages + 15U) / 16U; + pgctl[0] = ~ms->sram.ebb_power_mask_lo; + pgctl[1] = ~ms->sram.ebb_power_mask_hi; + lp_tot_pages = ms->sram.lp_sram_total_pages; + lp_free_pages = ms->sram.lp_sram_free_pages; + lp_banks = (lp_tot_pages + 15U) / 16U; + lp_pgctl = ~ms->sram.lpsram_ebb_mask; + } + if (hp_banks > 0 && hp_banks <= 96) { uint32_t off = 0; - uint32_t ttype = 1; + uint32_t ttype = 1; /* HPSRAM */ uint32_t ebb_dwords = (hp_banks + 31U) / 32U; uint32_t vlen = 12U + ebb_dwords * 4U + hp_banks * 2U; - uint32_t pgctl[3] = { - ~ms->sram.ebb_power_mask_lo, - ~ms->sram.ebb_power_mask_hi, - 0xFFFFFFFFU - }; memcpy(snap->mem_state_tlv + off, &ttype, 4); off += 4; memcpy(snap->mem_state_tlv + off, &vlen, 4); off += 4; memcpy(snap->mem_state_tlv + off, &hp_free_pages, 4); off += 4; @@ -946,6 +1354,27 @@ static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) memcpy(snap->mem_state_tlv + off, &amask, 2); off += 2; } + + if (lp_banks > 0 && lp_banks <= 32 && + off + 20U + lp_banks * 2U <= sizeof(snap->mem_state_tlv)) { + uint32_t lp_type = 0; /* LPSRAM */ + uint32_t lp_dwords = 1U; + uint32_t lp_vlen = 12U + lp_dwords * 4U + lp_banks * 2U; + memcpy(snap->mem_state_tlv + off, &lp_type, 4); off += 4; + memcpy(snap->mem_state_tlv + off, &lp_vlen, 4); off += 4; + memcpy(snap->mem_state_tlv + off, &lp_free_pages, 4); off += 4; + memcpy(snap->mem_state_tlv + off, &lp_dwords, 4); off += 4; + memcpy(snap->mem_state_tlv + off, &lp_pgctl, 4); off += 4; + memcpy(snap->mem_state_tlv + off, &lp_banks, 4); off += 4; + uint32_t lp_used = (lp_tot_pages >= lp_free_pages) ? + ((lp_tot_pages - lp_free_pages + 15U) / 16U) : 0U; + for (uint32_t b = 0; b < lp_banks; b++) { + uint16_t amask = (b < lp_used) ? 0xFFFFU : 0x0000U; + memcpy(snap->mem_state_tlv + off, &amask, 2); + off += 2; + } + } + snap->mem_state_len = off; snap->flags_present |= SOF_TELEM_FL_MEM_STATE_TLV; } @@ -985,7 +1414,7 @@ static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) count++; } - /* Add active component items from MW3 perf_data (48-byte DSP stride) */ + /* Add component items from MW3 perf_data (48-byte DSP stride) preserving FW state */ for (uint32_t i = 0; i < SOF_TEL_MW3_MAX_COMP_ITEMS && count < max_out; i++) { const uint8_t *p = w3 + i * 48U; uint32_t res_id = *(const uint32_t *)(p + 0); @@ -1019,7 +1448,8 @@ static void synthesize_tlvs_from_windows(struct sof_net_telem_snapshot *snap) } static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, - const uint8_t *req_buf, uint32_t req_len) + const uint8_t *req_buf, uint32_t req_len, + struct client_telem_state *tstate) { struct sof_net_telem_req req = { .query_mask = SOF_TELEM_FL_FW_REGS | @@ -1049,6 +1479,7 @@ static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, bool temp_hold = false; bool temp_sysfs_hold = false; + char prior_sysfs_ctl[32] = "auto"; if (req.flags & SOF_TELEM_FL_WAKE_DSP) { if (cdev_fd >= 0) { struct sof_cdev_pm_ctl pm_q = { .op = SOF_PM_OP_QUERY }; @@ -1057,9 +1488,13 @@ static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, if (ioctl(cdev_fd, SOF_IOC_PM_CTL, &pm_w) == 0) temp_hold = true; } - } else if (!sysfs_pm_is_active()) { - if (sysfs_pm_set_control("on", false) == 0) - temp_sysfs_hold = true; + } else if (!g_hw_ctx.holding_d0) { + sysfs_pm_get_control(prior_sysfs_ctl, sizeof(prior_sysfs_ctl)); + if (!sysfs_pm_is_active() || read_debugfs_fw_state() == 2) { + if (sysfs_pm_set_control("on", false) == 0 && + strcmp(prior_sysfs_ctl, "on") != 0) + temp_sysfs_hold = true; + } } } @@ -1074,14 +1509,14 @@ static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, struct sof_cdev_pm_ctl pm_r = { .op = SOF_PM_OP_RELEASE_HOLD }; ioctl(cdev_fd, SOF_IOC_PM_CTL, &pm_r); } - if (temp_sysfs_hold) - sysfs_pm_set_control("auto", false); + if (temp_sysfs_hold && !g_hw_ctx.holding_d0) + sysfs_pm_set_control(prior_sysfs_ctl[0] ? prior_sysfs_ctl : "auto", false); free(snap); return send_reply(sock, SOF_NET_OP_TELEM_SNAPSHOT, req_id, info_rc, NULL, 0); } - snap->active_pcm_streams = atomic_load_explicit(&g_tone_running, memory_order_acquire) ? 1 : 0; + snap->active_pcm_streams = count_running_alsa_substreams(); bool can_read_wins = (snap->dev_info.pm_runtime_active && snap->dev_info.fw_state == 7 /* SOF_FW_BOOT_COMPLETE */) || @@ -1234,7 +1669,7 @@ static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, } /* Synthesize any missing TLVs from TEL2 and PMW2 windows */ - synthesize_tlvs_from_windows(snap); + synthesize_tlvs_from_windows(snap, tstate); if (hot_only) { snap->mem_state_len = 0; memset(snap->mem_state_tlv, 0, sizeof(snap->mem_state_tlv)); @@ -1254,8 +1689,8 @@ static int handle_telem_snapshot(int sock, int cdev_fd, uint32_t req_id, struct sof_cdev_pm_ctl pm_r = { .op = SOF_PM_OP_RELEASE_HOLD }; ioctl(cdev_fd, SOF_IOC_PM_CTL, &pm_r); } - if (temp_sysfs_hold) - sysfs_pm_set_control("auto", false); + if (temp_sysfs_hold && !g_hw_ctx.holding_d0) + sysfs_pm_set_control(prior_sysfs_ctl[0] ? prior_sysfs_ctl : "auto", false); uint32_t send_sz = (req.query_mask & (SOF_TELEM_FL_TEL2_SLOT | SOF_TELEM_FL_PMW2_WIN3)) ? (uint32_t)sizeof(*snap) : SOF_NET_TELEM_SNAPSHOT_V1_SIZE; @@ -1394,6 +1829,7 @@ static void *client_worker_fn(void *arg) int cdev_fd = open(g_cdev_path, O_RDWR); if (cdev_fd < 0) discover_sof_pci_device(); + struct client_telem_state tstate = {0}; while (g_running) { struct sof_net_hdr hdr; @@ -1478,7 +1914,8 @@ static void *client_worker_fn(void *arg) st = sysfs_pm_set_control("auto", false); ctl.pm_runtime_active = sysfs_pm_is_active() ? 1 : 0; ctl.dsp_power_state = ctl.pm_runtime_active ? 1 : 4; - ctl.fw_state = 7; + int dfs_st = read_debugfs_fw_state(); + ctl.fw_state = (dfs_st >= 0) ? (uint32_t)dfs_st : 7; ctl.enabled_cores_mask = 1; ctl.fd_holding_d0 = g_hw_ctx.holding_d0 ? 1 : 0; } @@ -1691,7 +2128,8 @@ static void *client_worker_fn(void *arg) case SOF_NET_OP_TELEM_SNAPSHOT: rc = handle_telem_snapshot(sock, cdev_fd, hdr.req_id, - req_buf, hdr.payload_len); + req_buf, hdr.payload_len, + &tstate); break; case SOF_NET_OP_LLEXT_LOAD: { diff --git a/sof-top/README.md b/sof-top/README.md new file mode 100644 index 0000000..cd78f7f --- /dev/null +++ b/sof-top/README.md @@ -0,0 +1,81 @@ +# SOF Top (`sof-top`) + +Hardware-accelerated **GTK4 + Cairo** real-time Sound Open Firmware (SOF) DSP resource, memory, and task monitor for Linux host systems and remote DUTs (`sof_remote_server`). + +`sof-top` connects over `SOFZ`-compressed TCP (`sof_remote_server`) or a local character device (`/dev/sof-dsp0` + DebugFS/SysFS) to provide an immediate, `htop`/`btop`-style view of DSP load, memory footprint, and running tasks. + +--- + +## Features + +- **Moving DSP Core Utilization Graph**: + - Rolling 60-second time-series graph of per-core CPU load (`Core 0..N` in `%` of DSP clock and `KCPS`) plus aggregate peak/average load. + - Per-core summary cards showing clock frequency (`MHz`), active state, average/peak `KCPS`, and utilization bars. +- **Moving DSP Memory Utilization Graph (SRAM + DRAM)**: + - Rolling 60-second multi-trace time-series graph tracking: + - **HPSRAM** (`L2 High-Performance SRAM` powered banks, e.g., `24 x 128 KB = 3072 KB`) + - **LPSRAM** (`L2 Low-Power SRAM` powered banks, e.g., `1 x 64 KB = 64 KB`) + - **DRAM (`L3 / IMR`)** (`IMR / L3` backing storage, e.g., `16,384 KB` budget + `.l3_rodata` footprint + LLEXT DRAM heaps) + - **DSP Heaps** (Combined active allocations across System, Runtime, Module, Shared, Buffer, and LLEXT heaps) + - Side-by-side capacity gauges for `HPSRAM`, `LPSRAM`, `DRAM (L3/IMR)`, and `DSP Heaps`. +- **Unified DSP Task List with Resources & Status**: + - Consolidates **Scheduler Domains (`LL` / `DP`)**, **Zephyr RTOS Threads (`TEL2`)**, **Audio Processing Modules (`IPC4` / `PMW2`)**, and **DAI Gateways** into a single filterable, sortable table (`Sort: CPU %`, `Memory KB`, `Type`, `Name`) showing Task ID, Name/UUID, Type, Core, Status (`ACTIVE`, `READY`, `RESIDENT`, `IDLE`, `GATED`), `CPU %`, `KCPS` (avg/max), `SRAM (KB)`, `DRAM (KB)`, and Stack/Cycles/XRUN details. + +--- + +## Python & System Dependencies + +### 1. Python Dependencies + +`sof-top` requires **Python 3.10+** and the following Python packages (listed in [`requirements.txt`](requirements.txt) and [`pyproject.toml`](pyproject.toml)): + +| Python Package | Version | Purpose | +| :--- | :--- | :--- | +| **`PyGObject`** | `>= 3.42.0` | Python GObject Introspection bindings (`Gtk 4.0`, `Gdk 4.0`, `GLib 2.0`, `Pango 1.0`, `PangoCairo 1.0`) | +| **`pycairo`** | `>= 1.20.0` | Cairo 2D vector graphics bindings for hardware-accelerated custom GTK4 widgets and headless PNG rendering | + +### 2. System Packages (Ubuntu / Debian) +```bash +sudo apt update +sudo apt install \ + python3 \ + python3-pip \ + python3-gi \ + python3-gi-cairo \ + gir1.2-gtk-4.0 \ + gir1.2-pango-1.0 +``` + +--- + +## Usage + +`sof-top` is directly executable (`#!/usr/bin/env python3`, `chmod +x`) — no need to prefix `python3`: + +```bash +# Launch connected to remote DUT (e.g. aphid): +./sof-top/sof-top --host aphid --wake + +# Connect to a local character device (/dev/sof-dsp0): +./sof-top/sof-top --local /dev/sof-dsp0 + +# Headless high-DPI PNG snapshot export (for CI or reports): +./sof-top/sof-top --host aphid --wake --render-png /tmp/softop_user +``` + +### Command-Line Options + +| Option | Default | Description | +| :--- | :--- | :--- | +| `-H`, `--host HOST` | `aphid` | Remote DUT hostname or IP running `sof_remote_server` | +| `-p`, `--port PORT` | `9998` | Remote TCP port for `sof_remote_server` | +| `-l`, `--local DEV` | *(empty)* | Local character device path (e.g. `/dev/sof-dsp0`) instead of TCP | +| `-i`, `--interval MS` | `250` | Telemetry poll interval in milliseconds | +| `-w`, `--wake` | `false` | Hold/wake DSP via PM runtime when polling telemetry | +| `--render-png PATH` | *(empty)* | Render dashboard headlessly to PNG file and exit | + +--- + +## License + +SPDX-License-Identifier: `BSD-3-Clause` — see [`../LICENSE`](../LICENSE). diff --git a/sof-top/backend.py b/sof-top/backend.py new file mode 100644 index 0000000..8f7b73d --- /dev/null +++ b/sof-top/backend.py @@ -0,0 +1,2326 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation. All rights reserved. +""" +backend.py — Local (/dev/sof-dsp0 / debugfs) and Remote TCP Network +(sof_remote_server) backends, SOFZ wire decompressor, and TEL2/PMW2 binary +telemetry parsers for sof-top. +""" + +import collections +import ctypes +import fcntl +import math +import mmap +import os +import socket +import struct +import time +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple + +# ----------------------------------------------------------------------------- +# Protocol & IOCTL Constants matching sof_remote_proto.h +# ----------------------------------------------------------------------------- +SOF_NET_MAGIC = 0x534F4652 +SOF_NET_VERSION = 1 +SOF_NET_DEFAULT_PORT = 9998 + +SOF_NET_OP_FLAG_COMPRESSED = 0x8000 +SOF_NET_OP_MASK = 0x7FFF +SOF_NET_COMP_MAGIC = 0x5A464F53 # 'SOFZ' + +SOF_NET_OP_PING = 0x0000 +SOF_NET_OP_GET_INFO = 0x0001 +SOF_NET_OP_PM_CTL = 0x0002 +SOF_NET_OP_IPC_TX = 0x0010 +SOF_NET_OP_IPC_LARGE_CFG = 0x0011 +SOF_NET_OP_WIN_INFO = 0x0020 +SOF_NET_OP_WIN_READ = 0x0021 +SOF_NET_OP_WIN_WRITE = 0x0022 +SOF_NET_OP_WINSTREAM_IO = 0x0023 +SOF_NET_OP_TELEM_SNAPSHOT = 0x0028 +SOF_NET_OP_LLEXT_LOAD = 0x0030 +SOF_NET_OP_SHELL_EXEC = 0x0040 +SOF_NET_OP_PROBE_CTL = 0x0050 +SOF_NET_OP_PCM_CTL = 0x0060 +SOF_NET_OP_CARD_LIST = 0x0070 +SOF_NET_OP_PCM_CAPS = 0x0071 +SOF_NET_OP_DAPM_GRAPH = 0x0072 +SOF_NET_OP_COMPR_CTL = 0x0073 +SOF_NET_OP_KCTL_LIST = 0x0074 +SOF_NET_OP_KCTL_GET = 0x0075 +SOF_NET_OP_KCTL_SET = 0x0076 + +SOF_AUDIO_API_ALSA = 1 +SOF_AUDIO_API_TINYALSA = 2 +SOF_AUDIO_API_TINYCOMPRESS = 3 + +SOF_TELEM_FL_FW_REGS = 1 << 0 +SOF_TELEM_FL_TELEM_SLOT = 1 << 1 +SOF_TELEM_FL_PERF_WIN3 = 1 << 2 +SOF_TELEM_FL_MEM_STATE_TLV = 1 << 3 +SOF_TELEM_FL_PWR_STATE_TLV = 1 << 4 +SOF_TELEM_FL_EXT_PERF_TLV = 1 << 5 +SOF_TELEM_FL_MODULES_INFO_TLV = 1 << 6 +SOF_TELEM_FL_SYS_HOST_STATS = 1 << 7 +SOF_TELEM_FL_TEL2_SLOT = 1 << 8 +SOF_TELEM_FL_PMW2_WIN3 = 1 << 9 +SOF_TELEM_FL_HOT_ONLY = 1 << 10 +SOF_TELEM_FL_COMPRESS = 1 << 14 +SOF_TELEM_FL_WAKE_DSP = 1 << 15 + +SOF_PM_OP_QUERY = 0 +SOF_PM_OP_WAKE_HOLD = 1 +SOF_PM_OP_RELEASE_HOLD = 2 +SOF_PM_OP_BOOT_DSP = 3 +SOF_PM_OP_CORE_GET = 4 +SOF_PM_OP_CORE_PUT = 5 + +SOF_PCM_OP_STATUS = 0 +SOF_PCM_OP_START_TONE = 1 +SOF_PCM_OP_STOP_TONE = 2 +SOF_PCM_OP_PLAY_BUFFER = 3 +SOF_PCM_OP_CAPTURE_BUFFER = 4 + +SOF_PROBE_OP_INIT = 1 +SOF_PROBE_OP_DEINIT = 2 +SOF_PROBE_OP_INFO_ACTIVE = 3 +SOF_PROBE_OP_INFO_AVAILABLE = 4 +SOF_PROBE_OP_POINTS_ADD = 5 +SOF_PROBE_OP_POINTS_REMOVE = 6 + +# IPC4 BaseFW Param IDs +IPC4_MODULES_INFO_GET = 9 +IPC4_MEMORY_STATE_INFO_GET = 14 +IPC4_POWER_STATE_INFO_GET = 15 +IPC4_PERF_MEASUREMENTS_STATE = 17 +IPC4_GLOBAL_PERF_DATA = 18 +IPC4_EXTENDED_GLOBAL_PERF_DATA = 30 + +# Debug Slot Types +SOF_CDEV_SLOT_UNUSED = 0x00000000 +SOF_CDEV_SLOT_CRITICAL_LOG = 0x54524300 +SOF_CDEV_SLOT_DEBUG_LOG = 0x474F4C00 +SOF_CDEV_SLOT_GDB_STUB = 0x42444700 +SOF_CDEV_SLOT_TELEMETRY = 0x4C455400 +SOF_CDEV_SLOT_BROKEN = 0x44414544 +SOF_CDEV_SLOT_SHELL = 0x73686C6C +SOF_CDEV_SLOT_LLEXT_LOAD = 0x4C454C44 + +# Extended Telemetry v2 ("TEL2" & "PMW2") Constants +SOF_TEL_EXT_MAGIC = 0x324C4554 # "TEL2" +SOF_TEL_EXT_HDR_OFFSET = 0x0100 +SOF_TEL_EXT_DEBUGFS_HDR_OFFSET = 0x00FC +SOF_TEL_OFF_HOT_LINE0 = 0x0140 +SOF_TEL_OFF_GW_HOT = 0x0180 +SOF_TEL_OFF_MEM_SEC = 0x0240 +SOF_TEL_OFF_VREG_SEC = 0x0340 +SOF_TEL_OFF_PPL_SEC = 0x0540 +SOF_TEL_OFF_DAI_SEC = 0x06C0 +SOF_TEL_OFF_THD_SEC = 0x07C0 + +SOF_TEL_MW3_MAGIC = 0x32574D50 # "PMW2" +SOF_TEL_MW3_MAX_COMP_ITEMS = 88 +SOF_TEL_MW3_MAX_IO_ITEMS = 128 +SOF_TEL_MW3_COMP_PERF_STRIDE = 48 +SOF_TEL_MW3_MOD_HOT_OFFSET = 0x1080 +SOF_TEL_MW3_MOD_COLD_OFFSET = 0x1200 +SOF_TEL_MW3_IO_PERF_OFFSET = 0x1800 +SOF_TEL_MW3_HDR_OFFSET = 0x1FC0 + +SOF_NET_TELEM_SNAPSHOT_V1_SIZE = 16356 + +HEAP_TYPE_NAMES = { + 0: "NONE", + 1: "SYS_HEAP (SRAM)", + 2: "SHARED_HEAP (SRAM)", + 3: "LL_MOD_HEAP (SRAM)", + 4: "L3_IMR (DRAM)", + 5: "DRV_BUF (SRAM)", +} + +GW_TYPE_NAMES = { + 0: "NONE", + 1: "SSP/I2S", + 2: "DMIC", + 3: "ALH/SDW", + 4: "HDA", + 5: "HOST_DMA", + 6: "OTHER", +} + +PPL_STATUS_NAMES = { + 0: "INIT", + 1: "INITIALIZED", + 2: "RESET", + 3: "PAUSED", + 4: "RUNNING", + 5: "EOS", + 6: "ERROR", +} + +FW_STATE_NAMES = { + 0: "NOT_STARTED", + 1: "BOOT_NEEDED", + 2: "BOOT_PREPARE", + 3: "BOOT_IN_PROGRESS", + 4: "BOOT_FAILED", + 5: "BOOT_READY", + 6: "BOOT_READY_FAILED", + 7: "BOOT_COMPLETE", + 8: "CRASHED", +} + + +def slot_type_name(stype: int) -> str: + if stype == 0: + return "UNUSED" + if (stype & 0xFFFFFF00) == SOF_CDEV_SLOT_CRITICAL_LOG: + return f"CRIT_LOG(C{stype & 0xFF})" + if (stype & 0xFFFFFF00) == SOF_CDEV_SLOT_DEBUG_LOG: + return f"DBG_LOG(C{stype & 0xFF})" + if stype == SOF_CDEV_SLOT_TELEMETRY: + return "TELEMETRY" + if stype == SOF_CDEV_SLOT_GDB_STUB: + return "GDB_STUB" + if stype == SOF_CDEV_SLOT_SHELL: + return "SHELL('shll')" + if stype == SOF_CDEV_SLOT_LLEXT_LOAD: + return "LLEXT('LELD')" + if stype == SOF_CDEV_SLOT_BROKEN: + return "DEAD/BROKEN" + return f"0x{stype:08x}" + + +def sof_net_decompress(src: bytes) -> bytes: + """Decompress a SOFZ ('SOFZ' magic) compressed network payload.""" + if len(src) < 8: + return src + magic, raw_size = struct.unpack_from(" 16 * 1024 * 1024: + return src + + dst = bytearray(raw_size) + in_pos = 8 + out_pos = 0 + src_len = len(src) + + while in_pos < src_len and out_pos < raw_size: + tok = src[in_pos] + in_pos += 1 + tag = tok >> 6 + + if tag == 0: + zlen = tok + if zlen == 0: + if in_pos + 2 > src_len: + return b"" + zlen = src[in_pos] | (src[in_pos + 1] << 8) + in_pos += 2 + if zlen == 0 or out_pos + zlen > raw_size: + return b"" + out_pos += zlen + elif tag == 1: + llen = (tok & 0x3F) + 1 + if in_pos + llen > src_len or out_pos + llen > raw_size: + return b"" + dst[out_pos : out_pos + llen] = src[in_pos : in_pos + llen] + in_pos += llen + out_pos += llen + elif tag == 2: + rlen = (tok & 0x3F) + 3 + if in_pos + 1 > src_len or out_pos + rlen > raw_size: + return b"" + bval = src[in_pos] + in_pos += 1 + dst[out_pos : out_pos + rlen] = bytes([bval]) * rlen + out_pos += rlen + else: + mlen = (tok & 0x3F) + 4 + if in_pos + 2 > src_len: + return b"" + dist = src[in_pos] | (src[in_pos + 1] << 8) + in_pos += 2 + if dist == 0 or dist > out_pos or out_pos + mlen > raw_size: + return b"" + mpos = out_pos - dist + for k in range(mlen): + dst[out_pos + k] = dst[mpos + k] + out_pos += mlen + + return bytes(dst) if out_pos == raw_size else b"" + + +# ----------------------------------------------------------------------------- +# Structured Telemetry Models +# ----------------------------------------------------------------------------- +@dataclass +class CoreTelemetry: + core_id: int + enabled: bool = False + tick_count: int = 0 + ticks_per_sec: float = 0.0 + last_time_elapsed_cyc: int = 0 + max_time_elapsed_cyc: int = 0 + last_time_elapsed_us: float = 0.0 + max_time_elapsed_us: float = 0.0 + last_ccount: int = 0 + avg_kcps: int = 0 + peak_kcps: int = 0 + peak_4k_kcps: int = 0 + peak_8k_kcps: int = 0 + budget_kcps: int = 0 + cpu_load_pct: float = 0.0 + peak_load_pct: float = 0.0 + ll_overruns: int = 0 + dp_deadline_misses: int = 0 + active_ll_tasks: int = 0 + active_dp_tasks: int = 0 + active_ppl_count: int = 0 + + +@dataclass +class ComponentPerfItem: + resource_id: int + module_id: int + instance_id: int + module_name: str + core_id: int = 0 + power_mode: str = "LL" + is_removed: bool = False + avg_kcps: int = 0 + peak_kcps: int = 0 + iterations: int = 0 + total_cycles: int = 0 + restricted_peak_cycles: int = 0 + restricted_iterations: int = 0 + restricted_cycles: int = 0 + cpc_violations: int = 0 + xrun_count: int = 0 + heap_usage_bytes: int = 0 + heap_hwm_bytes: int = 0 + configured_cpc: int = 0 + + +@dataclass +class IoPerfItem: + resource_id: int + power_mode: int + perf_usage: int + + +@dataclass +class SramSegmentState: + name: str + total_banks: int = 0 + powered_banks: int = 0 + gated_banks: int = 0 + free_phys_pages: int = 0 + total_phys_pages: int = 0 + bank_powered: List[bool] = field(default_factory=list) + bank_alloc_mask: List[int] = field(default_factory=list) + + +@dataclass +class HeapTelemetry: + heap_type: int + name: str + is_dram: bool = False + total_bytes: int = 0 + allocated_bytes: int = 0 + free_bytes: int = 0 + max_allocated_bytes: int = 0 + alloc_fail_count: int = 0 + + +@dataclass +class VmhBundleInfo: + blk_sz_shift: int + block_size_bytes: int + total_blocks: int + used_blocks: int + max_blocks_or_oom: int + + +@dataclass +class FastGetInfo: + active_entries: int = 0 + fail_count: int = 0 + total_sram_bytes: int = 0 + peak_sram_bytes: int = 0 + total_get_calls: int = 0 + + +@dataclass +class VregionInfo: + base_va: int + total_pages: int + use_count: int + type_and_flags: int + lifetime_size_bytes: int + lifetime_used_bytes: int + interim_alloc_bytes: int + interim_free_kb: int + lifetime_free_count: int + + +@dataclass +class VregionSectionInfo: + total_pages: int = 0 + free_pages: int = 0 + min_free_pages: int = 0 + num_elems_in_use: int = 0 + alloc_fail_count: int = 0 + region_base_va: int = 0 + active_vregion_count: int = 0 + total_vregions_created: int = 0 + velems: List[Tuple[int, int]] = field(default_factory=list) + vregions: List[VregionInfo] = field(default_factory=list) + + +@dataclass +class PipelineInfo: + pipeline_id: int + core: int + priority: int + status: int + status_name: str + comp_count: int + attributes: int + period_us: int + sched_comp_id: int + last_xrun_bytes: int + xrun_count: int = 0 + + +@dataclass +class GatewayInfo: + slot_idx: int + gw_type: int + type_name: str + index: int + direction: int # 0=playback/tx, 1=capture/rx + dma_chan: int + comp_id: int + state: int + group_id: int + period_bytes: int + dma_buf_size: int + total_kb_or_frames: int = 0 + dma_buf_avail_bytes: int = 0 + xrun_or_skip_count: int = 0 + + +@dataclass +class ThreadInfo: + thread_id: int + name: str + core: int + prio: int + state: int + cpu_load_pct: float + stack_usage_pct: float + flags: int + stack_size_bytes: int + total_exec_kcycles: int + + +@dataclass +class DspTaskItem: + """Unified DSP Task entry for the User Edition Task List and Developer views.""" + task_id: str + name: str + task_type: str # "LL Module", "DP Module", "RTOS Thread", "Pipeline", "Core Sched", "DAI Gateway" + core_id: int + status: str # "RUNNING", "ACTIVE", "READY", "IDLE", "PAUSED", "XRUN!", "CPC_VIOL", "GATED" + cpu_pct: float + avg_kcps: int + peak_kcps: int + mem_used_kb: float + mem_limit_kb: float + mem_desc: str + iterations: int + xruns: int + + +@dataclass +class DebugSlotInfo: + slot_index: int + res_id: int + slot_type: int + type_name: str + vma: int + win_offset: int + bar_offset: int + size: int + + +@dataclass +class WindowInfo: + win_id: int + bar_offset: int + size: int + mapped: bool + + +@dataclass +class TelemetrySnapshot: + timestamp: float = 0.0 + connected: bool = False + platform: str = "unknown" + fw_filename: str = "" + fw_version: str = "0.0.0.0" + ipc_type: int = 1 + fw_state: int = 0 + fw_state_name: str = "NOT_STARTED" + dsp_power_state: int = 0 + pm_runtime_active: bool = False + num_cores: int = 2 + enabled_cores_mask: int = 1 + dsp_clock_hz: int = 400_000_000 + active_pcm_streams: int = 0 + wire_bytes: int = 0 + raw_bytes: int = 0 + # Window 0 FW Regs + fw_status: int = 0 + lec: int = 0 + fps: int = 0 + lnec: int = 0 + ltr: int = 0 + # Windows & Debug Slots + windows: List[WindowInfo] = field(default_factory=list) + debug_slots: List[DebugSlotInfo] = field(default_factory=list) + # Cores, Components, Pipelines, Gateways, Threads, Unified Tasks + cores: List[CoreTelemetry] = field(default_factory=list) + components: List[ComponentPerfItem] = field(default_factory=list) + io_perf_items: List[IoPerfItem] = field(default_factory=list) + pipelines: List[PipelineInfo] = field(default_factory=list) + gateways: List[GatewayInfo] = field(default_factory=list) + threads: List[ThreadInfo] = field(default_factory=list) + tasks: List[DspTaskItem] = field(default_factory=list) + total_xrun_events: int = 0 + tel2_present: bool = False + pmw2_present: bool = False + tel2_seq: int = 0 + tel2_timestamp_us: int = 0 + # Memory State (SRAM + DRAM + Heaps + VMH + VRegions) + hpsram: SramSegmentState = field(default_factory=lambda: SramSegmentState("HPSRAM")) + lpsram: SramSegmentState = field(default_factory=lambda: SramSegmentState("LPSRAM")) + heaps: List[HeapTelemetry] = field(default_factory=list) + sram_used_kb: float = 0.0 + sram_powered_kb: float = 0.0 + sram_total_kb: float = 0.0 + dram_used_kb: float = 0.0 + dram_hwm_kb: float = 0.0 + dram_total_kb: float = 0.0 + heap_allocated_bytes: int = 0 + heap_free_bytes: int = 0 + heap_max_bytes: int = 0 + heap_total_bytes: int = 0 + vmh_region_size: int = 0 + vmh_bundle_count: int = 0 + vmh_oom_total: int = 0 + vmh_bundles: List[VmhBundleInfo] = field(default_factory=list) + fast_get: FastGetInfo = field(default_factory=FastGetInfo) + vregion_sec: VregionSectionInfo = field(default_factory=VregionSectionInfo) + loaded_modules: Dict[int, str] = field(default_factory=dict) + + +# ----------------------------------------------------------------------------- +# Binary Parsers for Firmware Windows & IPC4 BaseFW TLVs +# ----------------------------------------------------------------------------- +WELL_KNOWN_IPC4_MODULES = { + 0x0000: "basefw", + 0x0001: "mixin", + 0x0002: "mixout", + 0x0003: "copier", + 0x0004: "peakvol", + 0x0005: "src", + 0x0006: "asrc", + 0x0007: "probe", + 0x0008: "updown_mix", + 0x0009: "smart_amp", + 0x000A: "dts", + 0x000B: "dolby", + 0x000C: "eq_iir", + 0x000D: "eq_fir", + 0x000E: "drc", + 0x000F: "tdfb", + 0x0010: "crossover", + 0x0011: "multiband_drc", + 0x0012: "aria", + 0x0013: "igo_nr", + 0x0094: "copier", + 0x0095: "mixin", + 0x0096: "mixout", + 0x0097: "gain", + 0x0098: "src", + 0x0099: "wov_detect", +} + + +def sof_tel2_slot_bias(buf: bytes) -> int: + if len(buf) >= SOF_TEL_EXT_HDR_OFFSET + 64: + (magic,) = struct.unpack_from("= SOF_TEL_EXT_DEBUGFS_HDR_OFFSET + 64: + (magic,) = struct.unpack_from(" str: + return ( + self.cached_modules.get(mod_id) + or self.module_name_lookup.get(mod_id, f"mod_0x{mod_id:02x}") + ) + + def parse_cdev_info_into(self, raw_info: bytes, snap: TelemetrySnapshot): + if len(raw_info) < 140: + return + ( + abi_ver, + ipc_type, + fw_state, + dsp_power_state, + pm_active, + num_cores, + enabled_cores_mask, + max_payload, + ver_maj, + ver_min, + ver_mic, + ver_bld, + hw_ip, + plat_bytes, + fw_bytes, + ) = struct.unpack_from(" len(data): + break + mod_id_rt, _ = struct.unpack_from(" len(data): + break + val = data[off : off + tlen] + off += tlen + + if len(val) < 12: + continue + free_pages, ebb_dwords = struct.unpack_from(" 8 or 8 + ebb_dwords * 4 + 4 > len(val): + continue + pgctl_masks = struct.unpack_from(f"<{ebb_dwords}I", val, 8) if ebb_dwords > 0 else () + (bank_count,) = struct.unpack_from(" 0: + bank_count = min(bank_count, ebb_dwords * 32, 64) + else: + bank_count = 0 + + seg = SramSegmentState("HPSRAM" if ttype == 1 else "LPSRAM") + seg.total_banks = bank_count + seg.total_phys_pages = bank_count * 16 + seg.free_phys_pages = free_pages + for b in range(bank_count): + dword_idx = b // 32 + bit_idx = b % 32 + is_gated = bool((pgctl_masks[dword_idx] >> bit_idx) & 1) + is_powered = not is_gated + seg.bank_powered.append(is_powered) + if is_powered: + seg.powered_banks += 1 + else: + seg.gated_banks += 1 + + alloc_off = 12 + ebb_dwords * 4 + for _ in range(bank_count): + if alloc_off + 2 <= len(val): + (amask,) = struct.unpack_from(" len(data): + break + val = data[off : off + tlen] + off += tlen + if ttype == 0 and tlen >= 4: + (mask,) = struct.unpack_from("= 4: + n = tlen // 4 + kcps_arr = struct.unpack_from(f"<{n}I", val, 0) + for cid, kv in enumerate(kcps_arr): + self.cached_core_kcps[cid] = kv + + def parse_ext_perf_tlv(self, data: bytes): + if len(data) < 4: + return + (count,) = struct.unpack_from(" 256: + return + items: List[ComponentPerfItem] = [] + off = 4 + rem = len(data) - 4 + if rem >= count * 48: + stride = 48 + elif rem >= count * 32: + stride = 32 + else: + stride = 16 + + for _ in range(count): + if off + stride > len(data): + break + if stride == 48: + ( + res_id, + flags, + peak_cyc, + total_cyc, + iters, + _r_peak, + _r_tot, + _r_it, + ) = struct.unpack_from(" 0 else peak_cyc + peak_kcps = peak_cyc // 1000 if peak_cyc >= 1000 else peak_cyc + avg_kcps = int(avg_cyc // 1000) if avg_cyc >= 1000 else int(avg_cyc) + cycles = total_cyc + elif stride == 32: + res_id, flags, peak_kcps, avg_kcps, iters, cycles = struct.unpack_from( + "> 16) & 0xFFFF + mname = ( + f"Core {inst_id} Summary" + if mod_id == 0 + else self.lookup_module_name(mod_id) + ) + pmode = "DP" if (flags & 1) else "LL" + removed = bool((flags >> 31) & 1) + items.append( + ComponentPerfItem( + resource_id=res_id, + module_id=mod_id, + instance_id=inst_id, + module_name=mname, + core_id=inst_id if mod_id == 0 else 0, + power_mode=pmode, + is_removed=removed, + avg_kcps=avg_kcps, + peak_kcps=peak_kcps, + iterations=iters, + total_cycles=cycles, + ) + ) + self.cached_ext_perf = items + + def parse_tel2_and_pmw2(self, tel2: bytes, pmw2: bytes, snap: TelemetrySnapshot): + """Parse Extended Telemetry v2 (TEL2 4KB slot & PMW2 8KB window).""" + bias = sof_tel2_slot_bias(tel2) + if bias >= 0: + snap.tel2_present = True + hdr_off = SOF_TEL_EXT_HDR_OFFSET - bias + if hdr_off + 16 <= len(tel2): + _, ver, max_cores, num_secs, seq, ts_us = struct.unpack_from( + " 0: + snap.hpsram.total_phys_pages = hp_tot + snap.hpsram.free_phys_pages = hp_free + if lp_tot > 0: + snap.lpsram.total_phys_pages = lp_tot + snap.lpsram.free_phys_pages = lp_free + + # Heaps[6] (each 20B at mem_off + 24) + snap.heaps = [] + tot_alloc = 0 + tot_free = 0 + tot_max = 0 + tot_cap = 0 + for h_idx in range(6): + hoff = mem_off + 24 + h_idx * 20 + htype, hfail, htot, halloc, hfree, hmax = struct.unpack_from( + " 0 or bused > 0: + snap.vmh_bundles.append( + VmhBundleInfo( + blk_sz_shift=bshift, + block_size_bytes=(1 << bshift) if bshift < 31 else 0, + total_blocks=btot, + used_blocks=bused, + max_blocks_or_oom=bmax, + ) + ) + fg_off = vmh_off + 88 + f_act, f_fail, f_tot, f_peak, f_calls = struct.unpack_from( + " 0: + vsec.velems.append((epages, evpage)) + act_vr, tot_vr = struct.unpack_from(" 0: + vsec.vregions.append( + VregionInfo( + base_va=r_va, + total_pages=r_pg, + use_count=r_uc, + type_and_flags=r_tf, + lifetime_size_bytes=r_lsz, + lifetime_used_bytes=r_lused, + interim_alloc_bytes=r_ialloc, + interim_free_kb=r_ifree, + lifetime_free_count=r_lfc, + ) + ) + snap.vregion_sec = vsec + + # 5. Pipelines Section (0x0540, 384B) + ppl_off = SOF_TEL_OFF_PPL_SEC - bias + if ppl_off + 384 <= len(tel2): + for c_i in range(5): + coff = ppl_off + c_i * 12 + cid, ll_c, dp_c, ap_c, kcps_b, _ = struct.unpack_from(" 0: + snap.cores[c_i].budget_kcps = kcps_b + act_ppls, tot_comps = struct.unpack_from(" 0: + snap.pipelines.append( + PipelineInfo( + pipeline_id=pid, + core=pcore, + priority=pprio, + status=pst, + status_name=PPL_STATUS_NAMES.get(pst, f"ST_{pst}"), + comp_count=pcomp, + attributes=pattr, + period_us=pper, + sched_comp_id=psched, + last_xrun_bytes=pxrun_b, + xrun_count=ppl_xruns[p_i] if p_i < len(ppl_xruns) else 0, + ) + ) + + # 6. DAI Gateways Section (0x06C0, 256B) + dai_off = SOF_TEL_OFF_DAI_SEC - bias + if dai_off + 256 <= len(tel2): + snap.gateways = [] + for g_i in range(16): + goff = dai_off + g_i * 16 + gtype, gidx, gdir, gdma, gcomp, gst, ggrp, gper, gbuf = ( + struct.unpack_from(" 0: + snap.gateways.append( + GatewayInfo( + slot_idx=g_i, + gw_type=gtype, + type_name=GW_TYPE_NAMES.get(gtype, f"GW_{gtype}"), + index=gidx, + direction=gdir, + dma_chan=gdma, + comp_id=gcomp, + state=gst, + group_id=ggrp, + period_bytes=gper, + dma_buf_size=gbuf, + total_kb_or_frames=tot_kbf, + dma_buf_avail_bytes=avail_b, + xrun_or_skip_count=xrun_c, + ) + ) + + # 7. Zephyr RTOS Threads Section (0x07C0) + thd_off = SOF_TEL_OFF_THD_SEC - bias + if thd_off + 32 + 32 * 24 <= len(tel2): + t_cnt, t_drop, t_seq = struct.unpack_from(" 0 else 32)): + toff = thd_off + 32 + t_i * 24 + tid, tcore, tprio, tst, tload, tstk, tflg, tchunks, tkcyc, tname_b = ( + struct.unpack_from("= 8192: + (mw3_magic,) = struct.unpack_from("> 16) & 0xFFFF + mname = self.lookup_module_name(mod_id) + pmw_comps.append( + ComponentPerfItem( + resource_id=res_id, + module_id=mod_id, + instance_id=inst_id, + module_name=mname, + core_id=0, + power_mode="DP" if (flags & 1) else "LL", + is_removed=bool((flags >> 31) & 1), + avg_kcps=avg_k, + peak_kcps=peak_k, + iterations=iters, + total_cycles=tot_cyc, + restricted_peak_cycles=r_peak, + restricted_iterations=r_iters, + restricted_cycles=r_cyc, + cpc_violations=cpc_v, + xrun_count=xrun_v, + heap_usage_bytes=c_heap, + heap_hwm_bytes=c_hwm, + configured_cpc=c_cpc, + ) + ) + if pmw_comps: + snap.components = pmw_comps + + # Parse io_perf[128] at 0x1800 (12B each) + snap.io_perf_items = [] + for i in range(SOF_TEL_MW3_MAX_IO_ITEMS): + ioff = SOF_TEL_MW3_IO_PERF_OFFSET + i * 12 + ires, ipm, iuse = struct.unpack_from(" 0: + snap.io_perf_items.append(IoPerfItem(ires, ipm, iuse)) + + def synthesize_fallback_memory_and_tasks(self, snap: TelemetrySnapshot): + """Populate SRAM totals from measured telemetry and build Unified DSP Task List.""" + hp = snap.hpsram + lp = snap.lpsram + hp_tot_kb = hp.total_banks * 128.0 + hp_pwr_kb = hp.powered_banks * 128.0 + hp_used_kb = ( + max(0.0, (hp.total_phys_pages - hp.free_phys_pages) * 4.0) + if hp.total_phys_pages >= hp.free_phys_pages and hp.total_phys_pages > 0 + else hp_pwr_kb + ) + lp_tot_kb = lp.total_banks * 64.0 + lp_pwr_kb = lp.powered_banks * 64.0 + lp_used_kb = ( + max(0.0, (lp.total_phys_pages - lp.free_phys_pages) * 4.0) + if lp.total_phys_pages >= hp.free_phys_pages and lp.total_phys_pages > 0 + else lp_pwr_kb + ) + + snap.sram_used_kb = hp_used_kb + lp_used_kb + snap.sram_powered_kb = hp_pwr_kb + lp_pwr_kb + snap.sram_total_kb = hp_tot_kb + lp_tot_kb + + # Build Unified DSP Task List (snap.tasks) + clock_khz = max(1.0, snap.dsp_clock_hz / 1000.0) + tasks: List[DspTaskItem] = [] + + # 1) Add Per-Core Scheduler / BaseFW tasks + for ct in snap.cores: + if not ct.enabled and ct.tick_count == 0 and ct.avg_kcps == 0: + continue + eff_kcps = ct.avg_kcps if ct.avg_kcps > 0 else ct.budget_kcps + st_str = ( + "RUNNING" + if (ct.ticks_per_sec > 0 or eff_kcps > 0) + else ("IDLE" if ct.enabled else "GATED") + ) + if ct.ll_overruns > 0 or ct.dp_deadline_misses > 0: + st_str = "XRUN!" + tasks.append( + DspTaskItem( + task_id=f"core:{ct.core_id}", + name=f"core{ct.core_id}_ll_sched", + task_type="Core Sched", + core_id=ct.core_id, + status=st_str, + cpu_pct=ct.cpu_load_pct, + avg_kcps=eff_kcps, + peak_kcps=max(ct.peak_kcps, eff_kcps), + mem_used_kb=64.0 if ct.core_id == 0 else 16.0, + mem_limit_kb=128.0, + mem_desc=f"Systick {ct.last_time_elapsed_us:.1f}us (max {ct.max_time_elapsed_us:.1f}us)", + iterations=ct.tick_count, + xruns=ct.ll_overruns + ct.dp_deadline_misses, + ) + ) + + # 2) Add Active Audio Pipeline Component Tasks + comp_by_res_id: Dict[int, ComponentPerfItem] = {} + for comp in snap.components: + comp_by_res_id[comp.resource_id] = comp + if comp.module_id == 0: + continue # Already covered by core scheduler summary + st_str = "REMOVED" if comp.is_removed else ("RUNNING" if comp.avg_kcps > 0 or comp.iterations > 0 else "READY") + if comp.xrun_count > 0: + st_str = "XRUN!" + elif comp.cpc_violations > 0: + st_str = "CPC_VIOL" + mem_kb = comp.heap_usage_bytes / 1024.0 if comp.heap_usage_bytes > 0 else 0.0 + hwm_kb = comp.heap_hwm_bytes / 1024.0 if comp.heap_hwm_bytes > 0 else mem_kb + mem_desc = f"Heap {mem_kb:.1f}/{hwm_kb:.1f}KB" if hwm_kb > 0 else "Heap N/A" + tasks.append( + DspTaskItem( + task_id=f"0x{comp.module_id:02x}:{comp.instance_id}", + name=f"{comp.module_name}:{comp.instance_id}", + task_type=f"{comp.power_mode} Module", + core_id=comp.core_id, + status=st_str, + cpu_pct=min(100.0, (comp.avg_kcps / clock_khz) * 100.0), + avg_kcps=comp.avg_kcps, + peak_kcps=comp.peak_kcps, + mem_used_kb=mem_kb, + mem_limit_kb=hwm_kb, + mem_desc=mem_desc, + iterations=comp.iterations, + xruns=comp.xrun_count + comp.cpc_violations, + ) + ) + + # 3) Add Zephyr RTOS Threads (if reported via TEL2) + for thd in snap.threads: + st_map = {0: "READY", 1: "RUNNING", 2: "WAITING", 3: "SUSPENDED", 4: "IDLE"} + st_str = "IDLE" if (thd.flags & 1) else st_map.get(thd.state, "ACTIVE") + stk_total_kb = max(1.0, thd.stack_size_bytes / 1024.0) + stk_used_kb = stk_total_kb * (thd.stack_usage_pct / 100.0) + kcps_est = int((thd.cpu_load_pct / 100.0) * clock_khz) + tasks.append( + DspTaskItem( + task_id=f"thd:{thd.thread_id}", + name=thd.name, + task_type="RTOS Thread", + core_id=thd.core, + status=st_str, + cpu_pct=thd.cpu_load_pct, + avg_kcps=kcps_est, + peak_kcps=kcps_est, + mem_used_kb=stk_used_kb, + mem_limit_kb=stk_total_kb, + mem_desc=f"Stack {thd.stack_usage_pct:.0f}% ({stk_total_kb:.1f}KB)", + iterations=thd.total_exec_kcycles, + xruns=0, + ) + ) + + # 4) Add Active DAI Gateways (using measured performance counters when available) + io_kcps_by_id: Dict[int, int] = { + io.resource_id: io.utilization_kcps for io in snap.io_perf_items + } + for gw in snap.gateways: + dir_s = "RX" if gw.direction else "TX" + st_str = "XRUN!" if gw.xrun_or_skip_count > 0 else ("RUNNING" if gw.state > 0 else "IDLE") + buf_kb = gw.dma_buf_size / 1024.0 if gw.dma_buf_size > 0 else 0.0 + avail_kb = gw.dma_buf_avail_bytes / 1024.0 + gw_comp = comp_by_res_id.get(gw.comp_id) + gw_avg_kcps = gw_comp.avg_kcps if gw_comp else io_kcps_by_id.get(gw.comp_id, 0) + gw_peak_kcps = gw_comp.peak_kcps if gw_comp else gw_avg_kcps + gw_cpu_pct = min(100.0, (gw_avg_kcps / clock_khz) * 100.0) + tasks.append( + DspTaskItem( + task_id=f"dai:{gw.slot_idx}", + name=f"{gw.type_name}{gw.index}_{dir_s}", + task_type="DAI Gateway", + core_id=0, + status=st_str, + cpu_pct=gw_cpu_pct, + avg_kcps=gw_avg_kcps, + peak_kcps=gw_peak_kcps, + mem_used_kb=avail_kb, + mem_limit_kb=buf_kb, + mem_desc=f"DMA ch{gw.dma_chan} ({avail_kb:.1f}/{buf_kb:.1f}KB)", + iterations=gw.total_kb_or_frames, + xruns=gw.xrun_or_skip_count, + ) + ) + + # 5) If no pipeline components or threads are active yet, show target-enumerated firmware modules + if len(tasks) <= len(snap.cores) and snap.loaded_modules: + for mid, mname in sorted(snap.loaded_modules.items())[:14]: + if mid == 0: + continue + tasks.append( + DspTaskItem( + task_id=f"mod:0x{mid:02x}", + name=mname, + task_type="FW Module", + core_id=0, + status="READY", + cpu_pct=0.0, + avg_kcps=0, + peak_kcps=0, + mem_used_kb=0.0, + mem_limit_kb=0.0, + mem_desc="Loaded", + iterations=0, + xruns=0, + ) + ) + + snap.tasks = tasks + + def parse_composite_snapshot(self, raw_snap: bytes, wire_len: int = 0) -> TelemetrySnapshot: + now = time.time() + dt = max(0.001, now - self.prev_ts) if self.prev_ts > 0 else 0.1 + snap = TelemetrySnapshot( + timestamp=now, + connected=True, + wire_bytes=wire_len if wire_len > 0 else len(raw_snap), + raw_bytes=len(raw_snap), + ) + + cdev_info_sz = 716 + if len(raw_snap) < cdev_info_sz + 8: + return snap + + self.parse_cdev_info_into(raw_snap[:cdev_info_sz], snap) + flags_present, active_pcm = struct.unpack_from("= off + 4 + 4096 + 4 + 8192: + (tel2_len,) = struct.unpack_from(" 0: + self.parse_modules_info_tlv(mod_info_tlv) + snap.loaded_modules = dict(self.cached_modules) + + if (flags_present & SOF_TELEM_FL_MEM_STATE_TLV) and mem_len > 0: + self.parse_memory_state_tlv(mem_tlv) + + if (flags_present & SOF_TELEM_FL_PWR_STATE_TLV) and pwr_len > 0: + self.parse_power_state_tlv(pwr_tlv, snap) + + if (flags_present & SOF_TELEM_FL_EXT_PERF_TLV) and ext_perf_len > 0: + self.parse_ext_perf_tlv(ext_perf_tlv) + + if self.cached_hpsram: + snap.hpsram = self.cached_hpsram + if self.cached_lpsram: + snap.lpsram = self.cached_lpsram + + # Parse Window 0 FW Regs + if flags_present & SOF_TELEM_FL_FW_REGS: + snap.fw_status, snap.lec, snap.fps, snap.lnec, snap.ltr = struct.unpack_from( + "> cid) & 1) + ct = CoreTelemetry( + core_id=cid, + enabled=en, + budget_kcps=self.cached_core_kcps.get(cid, 0), + ) + if (flags_present & SOF_TELEM_FL_TELEM_SLOT) and len(telem_slot) >= 4 + (cid + 1) * 32: + (sep,) = struct.unpack_from("= prev_t and self.prev_ts > 0: + ct.ticks_per_sec = (t_cnt - prev_t) / dt + self.prev_ticks[cid] = t_cnt + + # Guard against idle-gap ccount wrap and stopped LL timer in SOF telemetry.c + if max_el > clock_khz: + max_el = last_el if last_el <= clock_khz else 0 + if peak_u > clock_khz * 2 or (max_el > 0 and peak_u > max_el * 2): + peak_u = max_el + if peak_4k > clock_khz * 2 or (max_el > 0 and peak_4k > max_el * 2): + peak_4k = peak_u + if peak_8k > clock_khz * 2 or (max_el > 0 and peak_8k > max_el * 2): + peak_8k = peak_u + + if has_prev: + is_idle = ct.ticks_per_sec == 0.0 + else: + is_idle = (snap.active_pcm_streams == 0) and (ct.ticks_per_sec == 0.0) + if is_idle: + avg_u = 0 + last_el = 0 + ct.budget_kcps = 0 + elif ( + avg_u > clock_khz * 2 + or (max_el > 0 and avg_u > max_el) + or (last_el > 0 and avg_u > last_el * 4) + ): + avg_u = last_el if last_el <= clock_khz else 0 + + if ct.budget_kcps > clock_khz * 2 or (max_el > 0 and ct.budget_kcps > max_el): + ct.budget_kcps = avg_u + + ct.last_time_elapsed_cyc = last_el + ct.max_time_elapsed_cyc = max_el + ct.last_time_elapsed_us = last_el / (snap.dsp_clock_hz / 1e6) + ct.max_time_elapsed_us = max_el / (snap.dsp_clock_hz / 1e6) + ct.last_ccount = last_cc + + ct.avg_kcps = avg_u + ct.peak_kcps = peak_u + ct.peak_4k_kcps = peak_4k + ct.peak_8k_kcps = peak_8k + + eff_kcps = avg_u if avg_u > 0 else ct.budget_kcps + ct.cpu_load_pct = min(100.0, (eff_kcps / clock_khz) * 100.0) + ct.peak_load_pct = min( + 100.0, (max(peak_u, eff_kcps) / clock_khz) * 100.0 + ) + elif en and ct.budget_kcps > 0: + if ct.budget_kcps > clock_khz * 2: + ct.budget_kcps = 0 + ct.cpu_load_pct = min(100.0, (ct.budget_kcps / clock_khz) * 100.0) + ct.peak_load_pct = ct.cpu_load_pct + + snap.cores.append(ct) + + # Parse legacy Window 3 Performance Measurements Window (ADSP_PMW) + win3_items: List[ComponentPerfItem] = [] + if flags_present & SOF_TELEM_FL_PERF_WIN3: + off3 = 0 + while off3 + 32 <= len(perf_win3): + res_id, flags, peak_kcps, avg_kcps, iters, cycles = struct.unpack_from( + "> 16) & 0xFFFF + mname = self.lookup_module_name(mod_id) + win3_items.append( + ComponentPerfItem( + resource_id=res_id, + module_id=mod_id, + instance_id=inst_id, + module_name=mname, + core_id=0, + power_mode="DP" if (flags & 1) else "LL", + is_removed=bool((flags >> 1) & 1), + avg_kcps=avg_kcps if avg_kcps <= clock_khz * 2 else 0, + peak_kcps=peak_kcps if peak_kcps <= clock_khz * 2 else 0, + iterations=iters, + total_cycles=cycles, + ) + ) + + snap.components = win3_items if win3_items else list(self.cached_ext_perf) + + # Parse Extended TEL2 (MW2 Slot 1) and PMW2 (MW3) if present + if tel2_slot or pmw2_win3: + self.parse_tel2_and_pmw2(tel2_slot, pmw2_win3, snap) + + for it in snap.components: + if it.avg_kcps > clock_khz * 2: + it.avg_kcps = snap.cores[0].avg_kcps if snap.cores else 0 + if it.peak_kcps > clock_khz * 2: + it.peak_kcps = snap.cores[0].peak_kcps if snap.cores else 0 + if it.module_id == 0 and it.instance_id < len(snap.cores): + ct = snap.cores[it.instance_id] + if it.avg_kcps == 0: + it.avg_kcps = ct.avg_kcps if ct.avg_kcps > 0 else ct.budget_kcps + if it.peak_kcps == 0: + it.peak_kcps = ct.peak_kcps if ct.peak_kcps > 0 else it.avg_kcps + if it.iterations == 0: + it.iterations = ct.tick_count + if it.total_cycles <= 1: + it.total_cycles = ct.last_ccount + + self.synthesize_fallback_memory_and_tasks(snap) + self.prev_ts = now + return snap + + +# ----------------------------------------------------------------------------- +# Remote TCP Network Backend (connects to sof_remote_server) +# ----------------------------------------------------------------------------- +class RemoteTcpBackend: + def __init__(self, host: str, port: int = SOF_NET_DEFAULT_PORT): + self.host = host + self.port = port + self.sock: Optional[socket.socket] = None + self.req_id = 1 + self.parser = TelemetryParser() + self.poll_counter = 0 + self.last_wire_len = 0 + + def connect(self) -> bool: + self.close() + try: + s = socket.create_connection((self.host, self.port), timeout=2.5) + s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.sock = s + return True + except Exception: + self.sock = None + return False + + def close(self): + if self.sock: + try: + self.sock.close() + except Exception: + pass + self.sock = None + + def _recv_exact(self, n: int) -> bytes: + buf = bytearray() + while len(buf) < n: + chunk = self.sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError("Remote socket closed") + buf.extend(chunk) + return bytes(buf) + + def rpc(self, opcode: int, payload: bytes = b"") -> Tuple[int, bytes]: + if not self.sock and not self.connect(): + return -107, b"" + rid = self.req_id + self.req_id += 1 + hdr = struct.pack( + " 0 else b"" + self.last_wire_len = plen + if (rop & SOF_NET_OP_FLAG_COMPRESSED) and len(rdata) >= 8: + decomp = sof_net_decompress(rdata) + if decomp: + rdata = decomp + return status, rdata + except Exception: + self.close() + return -104, b"" + + def poll_telemetry(self, force_full: bool = False, wake_dsp: bool = False) -> TelemetrySnapshot: + qmask = ( + SOF_TELEM_FL_FW_REGS + | SOF_TELEM_FL_TELEM_SLOT + | SOF_TELEM_FL_EXT_PERF_TLV + | SOF_TELEM_FL_TEL2_SLOT + | SOF_TELEM_FL_PMW2_WIN3 + ) + if force_full or (self.poll_counter % 10 == 0): + qmask |= ( + SOF_TELEM_FL_MEM_STATE_TLV + | SOF_TELEM_FL_PWR_STATE_TLV + | SOF_TELEM_FL_EXT_PERF_TLV + | SOF_TELEM_FL_MODULES_INFO_TLV + ) + self.poll_counter += 1 + flags = SOF_TELEM_FL_COMPRESS | (SOF_TELEM_FL_WAKE_DSP if wake_dsp else 0) + req = struct.pack(" Tuple[int, bytes]: + flags = 1 if wake_dsp else 0 + req = struct.pack(" Tuple[int, dict]: + req = struct.pack("= 32: + _, _, fw_st, pwr_st, pm_act, cmask, hold, _ = struct.unpack_from( + " int: + if state_val == 2: + p0 = struct.pack(" Tuple[int, int, int, str]: + mname_b = mod_name.encode("ascii", errors="ignore")[:63].ljust(64, b"\x00") + issue_shell = 1 if mode == 0 else 0 + req_hdr = struct.pack("= 520: + out_lib_id, fw_st, sh_out = struct.unpack_from(" Tuple[int, str]: + cmd_b = cmd.encode("utf-8") + b"\x00" + req = struct.pack(" Tuple[int, List[dict]]: + op = SOF_PROBE_OP_INFO_ACTIVE if active_only else SOF_PROBE_OP_INFO_AVAILABLE + req = struct.pack("= 16: + _, _, _, count = struct.unpack_from(" len(rdata): + break + bid, purp, stag, _, wname = struct.unpack_from(" Tuple[int, dict, bytes]: + dev_b = device.encode("ascii", errors="ignore")[:31].ljust(32, b"\x00") + req = struct.pack( + "= 80: + tone_run, adev, rate, ch, freq, frames, rms, peak, dsz, api_u, fmt_u, xruns = ( + struct.unpack_from(" 0 else b"" + return st, { + "tone_running": bool(tone_run), + "device": adev.split(b"\x00", 1)[0].decode("ascii", errors="replace"), + "rate": rate, + "channels": ch, + "freq_hz": freq, + "frames_played": frames, + "rms_level": rms, + "peak_level": peak, + "api_used": api_u, + "format": fmt_u, + "xruns": xruns, + }, pcm_bytes + return st, {}, b"" + + def query_audio_graph(self, card_id: int = 0, api_backend: int = SOF_AUDIO_API_ALSA) -> dict: + result = { + "cards": [], + "pcm_nodes": [], + "pcm_caps": {}, + "dapm_hdr": {}, + "components": [], + "dapm_nodes": [], + "dapm_edges": [], + "dpcm_links": [], + "kcontrols": [], + } + + # 1. Card & PCM stream list + st, cdata = self.rpc(SOF_NET_OP_CARD_LIST) + if st == 0 and len(cdata) >= 8: + num_cards, num_nodes = struct.unpack_from(" len(cdata): + break + cid, id_b, drv_b, nm_b, lnm_b, mix_b, tplg_b, npcm, ncomp, nkctl, nact = ( + struct.unpack_from(" len(cdata): + break + ( + cid, + did, + sdir, + is_comp, + id_b, + nm_b, + scnt, + savail, + st_b, + is_run, + opid, + afmt_b, + arate, + ach, + psize, + bsize, + hw_ptr, + appl_ptr, + dly, + avail, + ) = struct.unpack_from("= 8: + num_entries, api_u = struct.unpack_from(" len(cap_data): + break + ( + cid, + did, + sdir, + is_comp, + api_used, + cst, + id_b, + nm_b, + fmask, + amask, + rmask, + rmin, + rmax, + chmin, + chmax, + sbmin, + sbmax, + psmin, + psmax, + pbmin, + pbmax, + pmin, + pmax, + bsmin, + bsmax, + bbmin, + bbmax, + ) = struct.unpack_from("= 188: + cid, cname_b, tplg_b, ncomp, nnodes, npow, nedges, nact_e, ndpcm = ( + struct.unpack_from(" len(gdata): + break + nm_b, bias_b, b_lvl, wcnt, pcnt = struct.unpack_from("<48s16sIII", gdata, off) + off += comp_sz + result["components"].append({ + "name": nm_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "bias_str": bias_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "bias_level": b_lvl, + "widget_count": wcnt, + "powered_count": pcnt, + }) + + dnode_fmt = " len(gdata): + break + ( + nid, + nm_b, + cmp_b, + wt_b, + pwr, + in_ep, + out_ep, + sact, + snm_b, + reg, + mask, + pipe_id, + lcol, + lrow, + nkctl, + _rsvd, + kn0, + kn1, + kn2, + kn3, + kname0_b, + kname1_b, + ) = struct.unpack_from(dnode_fmt, gdata, off) + off += dnode_sz + result["dapm_nodes"].append({ + "node_id": nid, + "name": nm_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "component": cmp_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "widget_type": wt_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "power": pwr, + "in_ep": in_ep, + "out_ep": out_ep, + "stream_active": bool(sact), + "stream_name": snm_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "pipeline_id": pipe_id, + "layout_col": lcol, + "layout_row": lrow, + "num_kcontrols": nkctl, + "kcontrol_numids": [x for x in (kn0, kn1, kn2, kn3) if x > 0], + "kcontrol_names": [ + x.split(b"\x00", 1)[0].decode("utf-8", errors="replace") + for x in (kname0_b, kname1_b) + if x.split(b"\x00", 1)[0] + ], + }) + + dedge_fmt = " len(gdata): + break + eid, sid, did, snm_b, dnm_b, sc_b, dc_b, ctl_b, knid, act = ( + struct.unpack_from(dedge_fmt, gdata, off) + ) + off += dedge_sz + result["dapm_edges"].append({ + "edge_id": eid, + "src_node_id": sid, + "dst_node_id": did, + "src_name": snm_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "dst_name": dnm_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "control_name": ctl_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "kcontrol_numid": knid, + "active": bool(act), + }) + + dpcm_sz = 384 + for _ in range(ndpcm): + if off + dpcm_sz > len(gdata): + break + fe_b, fdir, st_b, fmt_b, fch, frt, nbe = struct.unpack_from( + "<48sI16s16sIII", gdata, off + ) + off += dpcm_sz + result["dpcm_links"].append({ + "fe_name": fe_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "direction": fdir, + "state": st_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "format": fmt_b.split(b"\x00", 1)[0].decode("utf-8", errors="replace"), + "channels": fch, + "rate": frt, + "num_backends": nbe, + }) + + # 4. Kcontrol List with Graph Anchors + kreq = struct.pack("= 12: + _, api_u, nk = struct.unpack_from(" len(kdata): + break + ( + cid, + numid, + iface, + if_b, + dev, + subdev, + idx, + nm_b, + ktype, + tstr_b, + amask, + astr_b, + cnt, + min_v, + max_v, + step_v, + has_db, + db_min, + db_step, + db_mute, + nenums, + ) = struct.unpack_from(khead_fmt, kdata, off) + v_off = off + khead_sz + 192 + v0, v1, v2, v3, v4, v5, v6, v7 = struct.unpack_from(" int: + return (dir_bits << 30) | (size << 16) | (magic << 8) | nr + + +SOF_CDEV_IOC_MAGIC = ord("S") +SOF_IOC_GET_INFO = _ioc(2, SOF_CDEV_IOC_MAGIC, 0x01, 716) +SOF_IOC_PM_CTL = _ioc(3, SOF_CDEV_IOC_MAGIC, 0x02, 32) +SOF_IOC_IPC_LARGE_CFG = _ioc(3, SOF_CDEV_IOC_MAGIC, 0x11, 32) +SOF_IOC_WIN_READ = _ioc(3, SOF_CDEV_IOC_MAGIC, 0x21, 32) + + +class LocalCdevBackend(RemoteTcpBackend): + """Direct local character-device (/dev/sof-dsp0) ioctl and debugfs/sysfs backend.""" + + def __init__(self, cdev_path: str = "/dev/sof-dsp0", port: int = SOF_NET_DEFAULT_PORT): + super().__init__("127.0.0.1", port) + self.cdev_path = cdev_path + self.cdev_fd: int = -1 + + def connect(self) -> bool: + self.close() + try: + self.cdev_fd = os.open(self.cdev_path, os.O_RDWR) + return True + except OSError: + self.cdev_fd = -1 + return False + + def close(self): + if self.cdev_fd >= 0: + try: + os.close(self.cdev_fd) + except OSError: + pass + self.cdev_fd = -1 + super().close() + + def _ioctl_win_read( + self, + win_id: int, + offset: int, + size: int, + flags: int = 0, + slot_type: int = 0, + slot_index: int = 0, + ) -> Optional[bytes]: + if self.cdev_fd < 0: + return None + buf = (ctypes.c_uint8 * size)() + wio = bytearray( + struct.pack( + " Optional[bytes]: + if self.cdev_fd < 0: + return None + buf = (ctypes.c_uint8 * cap)() + cfg = bytearray( + struct.pack(" Tuple[int, dict]: + if self.cdev_fd < 0 and not self.connect(): + return -19, {} + ctl = bytearray(struct.pack(" int: + if self.cdev_fd < 0 and not self.connect(): + return -19 + try: + for val in ((0, state_val) if state_val == 2 else (state_val,)): + val_buf = (ctypes.c_uint32 * 1)(val) + cfg = bytearray( + struct.pack( + " TelemetrySnapshot: + if self.cdev_fd < 0 and not self.connect(): + return TelemetrySnapshot(timestamp=time.time(), connected=False) + + temp_hold = False + if wake_dsp: + st_q, q_res = self.pm_control(SOF_PM_OP_QUERY) + if st_q == 0 and not q_res.get("fd_holding_d0", False): + st_w, _ = self.pm_control(SOF_PM_OP_WAKE_HOLD) + if st_w == 0: + temp_hold = True + + try: + info_buf = bytearray(716) + try: + fcntl.ioctl(self.cdev_fd, SOF_IOC_GET_INFO, info_buf, True) + except OSError: + return TelemetrySnapshot(timestamp=time.time(), connected=False) + + full_poll = force_full or (self.poll_counter % 10 == 0) + self.poll_counter += 1 + wflags = 1 if wake_dsp else 0 + flags_present = 0 + + fw_regs = self._ioctl_win_read(SOF_CDEV_WIN_FW_REGS, 0, 512, wflags) + if fw_regs: + flags_present |= SOF_TELEM_FL_FW_REGS + else: + fw_regs = b"\x00" * 512 + + tel2_slot = self._ioctl_win_read( + SOF_CDEV_WIN_BY_SLOT_TYPE, + 0, + 4096, + wflags, + SOF_CDEV_SLOT_TELEMETRY, + 0, + ) + if not tel2_slot: + tel2_slot = self._ioctl_win_read(SOF_CDEV_WIN_DEBUG, 4096, 4096, wflags) + if tel2_slot: + flags_present |= SOF_TELEM_FL_TELEM_SLOT | SOF_TELEM_FL_TEL2_SLOT + telem_slot = bytearray(512) + if sof_tel2_slot_bias(tel2_slot) == 0: + telem_slot[:4] = tel2_slot[:4] + for c in range(5): + telem_slot[4 + c * 32 : 4 + (c + 1) * 32] = tel2_slot[ + 4 + c * 40 : 4 + c * 40 + 32 + ] + else: + telem_slot[:] = tel2_slot[:512] + telem_slot_b = bytes(telem_slot) + else: + tel2_slot = b"\x00" * 4096 + telem_slot_b = b"\x00" * 512 + + pmw2_win3 = self._ioctl_win_read(SOF_CDEV_WIN_PERF, 0, 8192, wflags) + if not pmw2_win3: + w3_4k = self._ioctl_win_read(SOF_CDEV_WIN_PERF, 0, 4096, wflags) + pmw2_win3 = (w3_4k + b"\x00" * 4096) if w3_4k else b"" + if pmw2_win3: + flags_present |= SOF_TELEM_FL_PERF_WIN3 | SOF_TELEM_FL_PMW2_WIN3 + perf_win3 = pmw2_win3[:4096] + else: + pmw2_win3 = b"\x00" * 8192 + perf_win3 = b"\x00" * 4096 + + mem_tlv = b"" + pwr_tlv = b"" + ext_perf_tlv = b"" + mod_info_tlv = b"" + if full_poll: + m_b = self._ioctl_large_cfg_get(IPC4_MEMORY_STATE_INFO_GET, 2048) + if m_b: + mem_tlv = m_b + flags_present |= SOF_TELEM_FL_MEM_STATE_TLV + p_b = self._ioctl_large_cfg_get(IPC4_POWER_STATE_INFO_GET, 256) + if p_b: + pwr_tlv = p_b + flags_present |= SOF_TELEM_FL_PWR_STATE_TLV + e_b = self._ioctl_large_cfg_get( + IPC4_EXTENDED_GLOBAL_PERF_DATA, 4096 + ) or self._ioctl_large_cfg_get(IPC4_GLOBAL_PERF_DATA, 4096) + if e_b: + ext_perf_tlv = e_b + flags_present |= SOF_TELEM_FL_EXT_PERF_TLV + mi_b = self._ioctl_large_cfg_get(IPC4_MODULES_INFO_GET, 4096) + if mi_b: + mod_info_tlv = mi_b + flags_present |= SOF_TELEM_FL_MODULES_INFO_TLV + + raw_snap = ( + bytes(info_buf) + + struct.pack("=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "sof-top" +version = "0.1.0" +description = "SOF Top — GTK4 Live DSP Resource, Memory (SRAM + DRAM) & Task Monitor" +readme = "README.md" +license = { text = "BSD-3-Clause" } +requires-python = ">=3.10" +authors = [ + { name = "Liam Girdwood", email = "liam.r.girdwood@intel.com" }, +] +keywords = ["sof", "sound-open-firmware", "alsa", "dsp", "telemetry", "ipc4", "gtk4"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: X11 Applications :: GTK", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: System :: Monitoring", +] +dependencies = [ + "PyGObject>=3.42.0", + "pycairo>=1.20.0", +] + +[project.urls] +Homepage = "https://github.com/thesofproject/sof-utils" +Repository = "https://github.com/thesofproject/sof-utils" +Issues = "https://github.com/thesofproject/sof-utils/issues" + +[tool.setuptools] +py-modules = ["backend", "widgets_cairo"] +script-files = ["sof-top"] diff --git a/sof-top/requirements.txt b/sof-top/requirements.txt new file mode 100644 index 0000000..c575a13 --- /dev/null +++ b/sof-top/requirements.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Python runtime dependencies for SOF Top (sof-top). +# +# Note: PyGObject and pycairo require the system GObject Introspection and +# GTK 4 development headers / typelibs to be installed on your distribution +# (e.g. `gir1.2-gtk-4.0`, `libgirepository1.0-dev` or `libgirepository-2.0-dev`, +# and `libcairo2-dev` on Debian/Ubuntu) when building inside a virtual environment. +PyGObject>=3.42.0 +pycairo>=1.20.0 diff --git a/sof-top/sof-top b/sof-top/sof-top new file mode 100755 index 0000000..080367d --- /dev/null +++ b/sof-top/sof-top @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation. All rights reserved. +""" +sof-top — SOF DSP User Resource, Memory (SRAM + DRAM) & Task Monitor (GTK4 + Snapshot + Cairo) + +User Edition features: + 1. Moving Graph for DSP Core Utilization (% of DSP clock & KCPS per core) + 2. Moving Graph for DSP Memory Utilization (HPSRAM, LPSRAM, DRAM L3/IMR, and Heaps) + 3. Unified DSP Task List with Resources & Status (Modules, RTOS Threads, Schedulers, DAIs) + +Usage: + sof-top --host aphid --wake + sof-top --host dragon-fly --port 9998 + sof-top --local /dev/sof-dsp0 + sof-top --host aphid --wake --render-png /tmp/softop_user +""" + +import argparse +import os +import sys +import threading +import time + +# Ensure imports resolve from the directory containing sof-top even when invoked via symlink +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +if SCRIPT_DIR not in sys.path: + sys.path.insert(0, SCRIPT_DIR) + +import gi +gi.require_version("Gtk", "4.0") +gi.require_version("Gdk", "4.0") +gi.require_version("GLib", "2.0") +gi.require_version("Pango", "1.0") +gi.require_version("PangoCairo", "1.0") +from gi.repository import Gtk, Gdk, GLib +import cairo + +from backend import ( + RemoteTcpBackend, + LocalCdevBackend, + TelemetrySnapshot, + SOF_PM_OP_WAKE_HOLD, + SOF_PM_OP_RELEASE_HOLD, + SOF_PM_OP_BOOT_DSP, +) +from widgets_cairo import UserDashboardCairoWidget, UserDashboardRenderer + + +DARK_CSS = b""" +window, .main-bg { + background-color: #0d1117; + color: #e6edf3; +} +headerbar { + background-color: #161b22; + color: #e6edf3; + border-bottom: 1px solid #30363d; +} +.toolbar-box { + background-color: #161b22; + border-bottom: 1px solid #30363d; + padding: 6px 10px; +} +.status-bar { + background-color: #161b22; + border-top: 1px solid #30363d; + padding: 4px 10px; + font-family: monospace; + font-size: 12px; +} +button { + background-image: none; + background-color: #21262d; + color: #e6edf3; + border: 1px solid #30363d; + border-radius: 5px; + padding: 4px 10px; +} +button:hover { + background-color: #30363d; +} +button:checked { + background-color: #1f6feb; + color: #ffffff; + border-color: #58a6ff; +} +entry { + background-color: #0d1117; + color: #e6edf3; + border: 1px solid #30363d; + border-radius: 4px; + font-family: monospace; +} +""" + + +class SofTopUserWindow(Gtk.ApplicationWindow): + """Main GTK4 Window for sof-top (User Edition).""" + + def __init__(self, app: Gtk.Application, args: argparse.Namespace): + super().__init__( + application=app, + title="sof-top — SOF DSP Core, Memory (SRAM/DRAM) & Task Monitor", + ) + self.set_default_size(1440, 880) + self.args = args + self.backend = None + self.io_lock = threading.Lock() + self.poll_in_flight = False + self.perf_meas_initialized = False + self.poll_timer_id = None + self.poll_interval_ms = args.interval + self.wake_on_poll = args.wake + self.holding_d0 = False + + self._apply_css() + self._build_ui() + self._connect_backend(args.target) + + if self.poll_interval_ms > 0: + self.poll_timer_id = GLib.timeout_add(self.poll_interval_ms, self._on_poll_tick) + GLib.idle_add(self._poll_once, True) + + def _apply_css(self): + provider = Gtk.CssProvider() + provider.load_from_data(DARK_CSS) + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) + + def _build_ui(self): + header = Gtk.HeaderBar() + self.set_titlebar(header) + + title_lbl = Gtk.Label( + label="sof-top — DSP Core Utilization, Memory (SRAM + DRAM) & Task Monitor" + ) + header.set_title_widget(title_lbl) + + target_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + target_box.append(Gtk.Label(label="Target:")) + + self.target_entry = Gtk.Entry() + self.target_entry.set_text(self.args.target) + self.target_entry.set_width_chars(20) + self.target_entry.connect( + "activate", lambda e: self._connect_backend(e.get_text().strip()) + ) + target_box.append(self.target_entry) + + btn_conn = Gtk.Button(label="Connect") + btn_conn.connect( + "clicked", lambda _: self._connect_backend(self.target_entry.get_text().strip()) + ) + target_box.append(btn_conn) + header.pack_start(target_box) + + btn_snap = Gtk.Button(label="Export PNG") + btn_snap.connect("clicked", self._on_export_png) + header.pack_end(btn_snap) + + root_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + root_box.add_css_class("main-bg") + self.set_child(root_box) + + # Streamlined User Toolbar + toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + toolbar.add_css_class("toolbar-box") + root_box.append(toolbar) + + btn_wake = Gtk.Button(label="Wake / Boot DSP") + btn_wake.connect("clicked", self._on_wake_dsp) + toolbar.append(btn_wake) + + self.btn_hold_d0 = Gtk.ToggleButton(label="Hold D0i0") + self.btn_hold_d0.connect("toggled", self._on_toggle_hold_d0) + toolbar.append(self.btn_hold_d0) + + self.btn_wake_poll = Gtk.ToggleButton(label="Wake on Poll") + self.btn_wake_poll.set_active(self.wake_on_poll) + self.btn_wake_poll.connect("toggled", self._on_toggle_wake_poll) + toolbar.append(self.btn_wake_poll) + + toolbar.append(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)) + + lbl_rate = Gtk.Label(label="Refresh Rate:") + toolbar.append(lbl_rate) + for ms, txt in [(100, "10Hz"), (250, "4Hz"), (500, "2Hz"), (1000, "1Hz"), (0, "Pause")]: + b = Gtk.Button(label=txt) + b.connect("clicked", self._on_set_poll_rate, ms) + toolbar.append(b) + + toolbar.append(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)) + btn_pgup = Gtk.Button(label="Tasks ▲") + btn_pgup.connect("clicked", lambda _: self.dashboard.scroll_tasks(-6)) + toolbar.append(btn_pgup) + btn_pgdn = Gtk.Button(label="Tasks ▼") + btn_pgdn.connect("clicked", lambda _: self.dashboard.scroll_tasks(6)) + toolbar.append(btn_pgdn) + + self.dashboard = UserDashboardCairoWidget() + root_box.append(self.dashboard) + + self.status_label = Gtk.Label(label="Initializing...") + self.status_label.set_xalign(0.0) + self.status_label.add_css_class("status-bar") + root_box.append(self.status_label) + + def _connect_backend(self, target: str): + self._set_status(f"Connecting to {target}...") + + def _worker(): + with self.io_lock: + if self.backend: + try: + self.backend.close() + except Exception: + pass + self.backend = None + self.perf_meas_initialized = False + + try: + if target.startswith("/") or target.startswith("local:"): + dev_path = ( + target.split("local:", 1)[-1] + if target.startswith("local:") + else target + ) + new_be = LocalCdevBackend(dev_path, self.args.port) + else: + if ":" in target: + host, port_s = target.rsplit(":", 1) + port = int(port_s) + else: + host, port = target, self.args.port + new_be = RemoteTcpBackend(host, port) + ok = new_be.connect() + self.backend = new_be + if ok: + GLib.idle_add(self._set_status, f"Connected to {target}") + self._poll_once_Async(True) + else: + GLib.idle_add(self._set_status, f"Failed to connect to {target}") + except Exception as e: + GLib.idle_add( + self._set_status, f"Connection to '{target}' failed: {e}" + ) + + threading.Thread(target=_worker, daemon=True).start() + + def _set_status(self, msg: str): + self.status_label.set_text(msg) + return False + + def _on_poll_tick(self) -> bool: + if self.poll_interval_ms <= 0: + return False + self._poll_once(False) + return True + + def _poll_once(self, force_full: bool = False) -> bool: + self._poll_once_Async(force_full) + return False + + def _poll_once_Async(self, force_full: bool = False): + if self.poll_in_flight: + return + self.poll_in_flight = True + full = force_full or self.holding_d0 + wake = self.wake_on_poll + + def _worker(): + try: + with self.io_lock: + be = self.backend + if not be: + return + snap = be.poll_telemetry(force_full=full, wake_dsp=wake) + if snap.connected and snap.pm_runtime_active and snap.fw_state == 7: + if not self.perf_meas_initialized: + st_perf = be.set_perf_measurement_state(2) + self.perf_meas_initialized = True + if st_perf == 0: + snap = be.poll_telemetry(force_full=True, wake_dsp=wake) + elif not snap.connected or snap.fw_state != 7: + self.perf_meas_initialized = False + GLib.idle_add(self._apply_snapshot, snap) + except Exception as e: + GLib.idle_add(self._set_status, f"Telemetry poll error: {e}") + finally: + self.poll_in_flight = False + + threading.Thread(target=_worker, daemon=True).start() + + def _apply_snapshot(self, snap: TelemetrySnapshot) -> bool: + if not snap.connected: + self._set_status("Target disconnected — retaining last valid telemetry snapshot") + return False + + self.dashboard.update_snapshot(snap) + pstate = "D0i0 (Active)" if snap.pm_runtime_active else "D3 (Sleep)" + hp_kb = snap.hpsram.powered_banks * 128 + hp_tot_kb = snap.hpsram.total_banks * 128 + lp_kb = snap.lpsram.powered_banks * 64 + lp_tot_kb = snap.lpsram.total_banks * 64 + self._set_status( + f"Platform: {snap.platform} | FW: v{snap.fw_version} ({snap.fw_state_name}) | " + f"Power: {pstate} | Cores: 0x{snap.enabled_cores_mask:x}/{snap.num_cores} | " + f"HPSRAM: {hp_kb}/{hp_tot_kb} KB | LPSRAM: {lp_kb}/{lp_tot_kb} KB | " + f"DRAM (L3/IMR): {snap.dram_used_kb:,.0f}/{snap.dram_total_kb:,.0f} KB | " + f"Tasks: {len(snap.tasks)}" + ) + return False + + def _on_wake_dsp(self, _btn): + def _worker(): + with self.io_lock: + if not self.backend: + return + st, res = self.backend.pm_control(SOF_PM_OP_BOOT_DSP) + GLib.idle_add(self._set_status, f"Wake DSP (st={st}): {res}") + self._poll_once_Async(True) + + threading.Thread(target=_worker, daemon=True).start() + + def _on_toggle_hold_d0(self, btn: Gtk.ToggleButton): + active = btn.get_active() + + def _worker(): + with self.io_lock: + if not self.backend: + return + op = SOF_PM_OP_WAKE_HOLD if active else SOF_PM_OP_RELEASE_HOLD + st, res = self.backend.pm_control(op) + self.holding_d0 = bool(res.get("fd_holding_d0", False)) + GLib.idle_add( + self._set_status, f"PM Hold D0 = {self.holding_d0} (st={st}): {res}" + ) + self._poll_once_Async(True) + + threading.Thread(target=_worker, daemon=True).start() + + def _on_toggle_wake_poll(self, btn: Gtk.ToggleButton): + self.wake_on_poll = btn.get_active() + self._set_status(f"Wake on Poll = {self.wake_on_poll}") + + def _on_set_poll_rate(self, _btn, ms: int): + if self.poll_timer_id: + GLib.source_remove(self.poll_timer_id) + self.poll_timer_id = None + self.poll_interval_ms = ms + if ms > 0: + self.poll_timer_id = GLib.timeout_add(ms, self._on_poll_tick) + self._set_status(f"Poll interval set to {ms} ms") + else: + self._set_status("Polling paused") + + def _on_export_png(self, _btn): + out_path = "/tmp/softop_user_dashboard.png" + render_user_dashboard_to_png(self.dashboard, out_path) + self._set_status(f"Exported {out_path}") + + +def render_user_dashboard_to_png( + dash_w, + out_path: str, + width: int = 1360, + height: int = 840, +): + if not out_path.endswith(".png"): + out_path = f"{out_path}_dashboard.png" + surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + cr = cairo.Context(surf) + dash_w.render_to_cairo(cr, float(width), float(height), None) + surf.write_to_png(out_path) + return out_path + + +def run_headless_render(args: argparse.Namespace) -> int: + target = args.target + if target.startswith("/") or target.startswith("local:"): + dev_path = target.split("local:", 1)[-1] if target.startswith("local:") else target + backend = LocalCdevBackend(dev_path, args.port) + else: + if ":" in target: + host, port_s = target.rsplit(":", 1) + port = int(port_s) + else: + host, port = target, args.port + backend = RemoteTcpBackend(host, port) + + if not backend.connect(): + print(f"ERROR: Could not connect to {target}", file=sys.stderr) + return 1 + + try: + dash_r = UserDashboardRenderer() + if args.wake: + backend.pm_control(SOF_PM_OP_WAKE_HOLD) + backend.set_perf_measurement_state(2) + + snap = None + valid_polls = 0 + for _ in range(6): + s = backend.poll_telemetry(force_full=True, wake_dsp=args.wake) + if s.connected: + if valid_polls == 0 and s.pm_runtime_active and s.fw_state == 7: + backend.set_perf_measurement_state(2) + snap = s + dash_r.update_snapshot(snap) + valid_polls += 1 + time.sleep(0.12) + + if not snap or not snap.connected: + print( + f"ERROR: Failed to retrieve telemetry snapshot from {target}", + file=sys.stderr, + ) + return 1 + + out_file = render_user_dashboard_to_png(dash_r, args.render_png) + hp_kb = snap.hpsram.powered_banks * 128 + hp_tot_kb = snap.hpsram.total_banks * 128 + lp_kb = snap.lpsram.powered_banks * 64 + lp_tot_kb = snap.lpsram.total_banks * 64 + print( + f"Rendered {out_file} " + f"(platform={snap.platform}, fw=v{snap.fw_version}, state={snap.fw_state} [{snap.fw_state_name}], " + f"hpsram={hp_kb}/{hp_tot_kb} KB, lpsram={lp_kb}/{lp_tot_kb} KB, " + f"dram={snap.dram_used_kb:.0f}/{snap.dram_total_kb:.0f} KB, " + f"tasks={len(snap.tasks)})" + ) + finally: + if args.wake: + try: + backend.pm_control(SOF_PM_OP_RELEASE_HOLD) + except Exception: + pass + backend.close() + return 0 + + +class SofTopUserApp(Gtk.Application): + def __init__(self, args: argparse.Namespace): + super().__init__(application_id="org.thesofproject.SofTop") + self.args = args + self.win = None + + def do_activate(self): + if not self.win: + self.win = SofTopUserWindow(self, self.args) + self.win.present() + + +def main(): + ap = argparse.ArgumentParser( + prog="sof-top", + description="sof-top — SOF DSP User Monitor (Moving Core & SRAM/DRAM Memory Graphs + DSP Task List)", + ) + ap.add_argument("-H", "--host", default="aphid", help="Remote host running sof_remote_server (default: aphid)") + ap.add_argument("-p", "--port", type=int, default=9998, help="Remote TCP port (default: 9998)") + ap.add_argument("-l", "--local", default="", help="Local char device path (e.g. /dev/sof-dsp0)") + ap.add_argument("-i", "--interval", type=int, default=250, help="Poll interval in ms (default: 250)") + ap.add_argument("-w", "--wake", action="store_true", help="Wake DSP on telemetry poll") + ap.add_argument("--render-png", default="", help="Render User Dashboard to PNG and exit") + args = ap.parse_args() + + if args.local: + args.target = f"local:{args.local}" + else: + args.target = f"{args.host}:{args.port}" + + if args.render_png: + sys.exit(run_headless_render(args)) + + app = SofTopUserApp(args) + sys.exit(app.run(None)) + + +if __name__ == "__main__": + main() diff --git a/sof-top/widgets_cairo.py b/sof-top/widgets_cairo.py new file mode 100644 index 0000000..0d8d5b0 --- /dev/null +++ b/sof-top/widgets_cairo.py @@ -0,0 +1,651 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2026 Intel Corporation. All rights reserved. +""" +widgets_cairo.py — Hardware-accelerated GTK4 + Gtk.Snapshot + Cairo custom widget +for sof-top: UserDashboardCairoWidget + - Moving Graph for DSP Core Utilization + - Moving Graph for DSP Memory Utilization (SRAM + DRAM) + - Unified DSP Task List with Resources & Status +""" + +import collections +import math +import time +from typing import Deque, Dict, List, Optional, Tuple + +import cairo +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Gdk", "4.0") +from gi.repository import Gdk, GLib, Graphene, Gsk, Gtk, Pango, PangoCairo + +from backend import ( + ComponentPerfItem, + CoreTelemetry, + DspTaskItem, + SramSegmentState, + TelemetrySnapshot, + SOF_CDEV_SLOT_TELEMETRY, + SOF_CDEV_SLOT_SHELL, + SOF_CDEV_SLOT_LLEXT_LOAD, +) + + +# Color Palette (Dark High-Contrast Engineering Theme) +BG_DARK = (0.07, 0.09, 0.12) +PANEL_BG = (0.10, 0.12, 0.16) +PANEL_BORDER = (0.20, 0.25, 0.32) +GRID_LINE = (1.0, 1.0, 1.0, 0.08) +TEXT_PRIMARY = (0.92, 0.94, 0.97) +TEXT_MUTED = (0.58, 0.65, 0.74) +ACCENT_GREEN = (0.18, 0.86, 0.48) +ACCENT_CYAN = (0.20, 0.76, 0.95) +ACCENT_AMBER = (0.96, 0.75, 0.20) +ACCENT_RED = (0.95, 0.30, 0.30) +ACCENT_VIOLET = (0.68, 0.46, 0.96) +ACCENT_BLUE = ACCENT_CYAN +ACCENT_PURPLE = ACCENT_VIOLET + +CORE_COLORS: List[Tuple[float, float, float]] = [ + (0.18, 0.86, 0.48), # Core 0: Emerald Green + (0.20, 0.76, 0.95), # Core 1: Electric Cyan + (0.96, 0.75, 0.20), # Core 2: Amber Gold + (0.68, 0.46, 0.96), # Core 3: Violet + (0.95, 0.42, 0.62), # Core 4: Pink + (0.45, 0.90, 0.80), # Core 5: Teal +] + + +def draw_pango_text( + widget: Optional[Gtk.Widget], + cr: cairo.Context, + text: str, + x: float, + y: float, + font_str: str = "Monospace 8.5", + color: Tuple[float, ...] = TEXT_PRIMARY, +): + if len(color) == 4: + cr.set_source_rgba(*color) + else: + cr.set_source_rgb(*color) + + if widget is not None: + layout = widget.create_pango_layout(text) + else: + layout = PangoCairo.create_layout(cr) + layout.set_text(text, -1) + layout.set_font_description(Pango.FontDescription(font_str)) + cr.move_to(x, y) + PangoCairo.show_layout(cr, layout) + + +def draw_panel_box( + cr: cairo.Context, + x: float, + y: float, + w: float, + h: float, + bg: Tuple[float, float, float] = PANEL_BG, + border: Tuple[float, float, float] = PANEL_BORDER, +): + cr.set_source_rgb(*bg) + cr.rectangle(x, y, w, h) + cr.fill() + cr.set_source_rgb(*border) + cr.set_line_width(1.0) + cr.rectangle(x + 0.5, y + 0.5, max(1.0, w - 1.0), max(1.0, h - 1.0)) + cr.stroke() + + +# ============================================================================= +# 0. USER EDITION RENDERER & WIDGET: UserDashboardRenderer / UserDashboardCairoWidget +# ============================================================================= +class UserDashboardRenderer: + """ + Display-independent state, 60-second timestamped history, scrollable task + state, and Cairo renderer for `sof-top`. Can be used directly in headless + CI environments (`--render-png`) without constructing a `Gtk.Widget`. + """ + + def __init__(self, window_seconds: float = 60.0, max_samples: int = 1200): + self.window_seconds = float(window_seconds) + self.max_samples = max_samples + self.snap: TelemetrySnapshot = TelemetrySnapshot() + self.core_pct_hist: Dict[int, Deque[Tuple[float, float]]] = collections.defaultdict( + lambda: collections.deque(maxlen=max_samples) + ) + self.sram_kb_hist: Deque[Tuple[float, float]] = collections.deque(maxlen=max_samples) + self.hpsram_kb_hist: Deque[Tuple[float, float]] = collections.deque(maxlen=max_samples) + self.lpsram_kb_hist: Deque[Tuple[float, float]] = collections.deque(maxlen=max_samples) + self.dram_kb_hist: Deque[Tuple[float, float]] = collections.deque(maxlen=max_samples) + self.heap_kb_hist: Deque[Tuple[float, float]] = collections.deque(maxlen=max_samples) + self.task_scroll_offset: int = 0 + self.last_max_rows: int = 10 + + def _prune_old(self, deq: Deque[Tuple[float, float]], cutoff: float): + while len(deq) > 1 and deq[0][0] < cutoff: + deq.popleft() + + def scroll_tasks(self, delta_rows: int) -> bool: + total = len(self.snap.tasks) + max_off = max(0, total - max(1, self.last_max_rows)) + new_off = max(0, min(max_off, self.task_scroll_offset + delta_rows)) + if new_off != self.task_scroll_offset: + self.task_scroll_offset = new_off + return True + return False + + def update_snapshot(self, snap: TelemetrySnapshot): + self.snap = snap + ts = snap.timestamp if snap.timestamp > 0 else time.time() + cutoff = ts - self.window_seconds + + for ct in snap.cores: + deq = self.core_pct_hist[ct.core_id] + deq.append((ts, float(ct.cpu_load_pct))) + self._prune_old(deq, cutoff) + + hp_kb = float(snap.hpsram.powered_banks * 128) + lp_kb = float(snap.lpsram.powered_banks * 64) + dram_kb = float(snap.dram_used_kb) + heap_kb = float(snap.heap_allocated_bytes) / 1024.0 + + for deq, val in ( + (self.sram_kb_hist, hp_kb + lp_kb), + (self.hpsram_kb_hist, hp_kb), + (self.lpsram_kb_hist, lp_kb), + (self.dram_kb_hist, dram_kb), + (self.heap_kb_hist, heap_kb), + ): + deq.append((ts, val)) + self._prune_old(deq, cutoff) + + max_off = max(0, len(snap.tasks) - max(1, self.last_max_rows)) + if self.task_scroll_offset > max_off: + self.task_scroll_offset = max_off + + def render_to_cairo( + self, + cr: cairo.Context, + w: float, + h: float, + widget: Optional[Gtk.Widget] = None, + ): + cr.set_source_rgb(*BG_DARK) + cr.rectangle(0, 0, w, h) + cr.fill() + + pad = 8.0 + top_h = max(270.0, (h - pad * 3) * 0.52) + bot_h = max(180.0, h - top_h - pad * 3) + half_w = (w - pad * 3) * 0.5 + + num_cores = max(1, len(self.snap.cores)) if self.snap.cores else 2 + clock_mhz = self.snap.dsp_clock_hz / 1e6 + clock_khz = max(1.0, self.snap.dsp_clock_hz / 1e3) + + # ===================================================================== + # 1. TOP-LEFT: Moving Graph — DSP Core Utilization + # ===================================================================== + lx = pad + ly = pad + draw_panel_box(cr, lx, ly, half_w, top_h) + + pm_txt = "D0i0 ACTIVE" if self.snap.pm_runtime_active else "D3 SLEEP" + pm_col = ACCENT_GREEN if self.snap.pm_runtime_active else ACCENT_AMBER + draw_pango_text( + widget, + cr, + f"DSP CORE UTILIZATION — {self.snap.platform.upper()} ({clock_mhz:.0f} MHz | {pm_txt})", + lx + 10, + ly + 6, + "Monospace Bold 9", + pm_col, + ) + + # Compact Per-Core Live Bars at top of left panel + bar_area_y = ly + 26.0 + row_h = min(22.0, max(16.0, (top_h * 0.36) / max(1, num_cores))) + for idx in range(num_cores): + ct = ( + self.snap.cores[idx] + if idx < len(self.snap.cores) + else CoreTelemetry(core_id=idx, enabled=(idx == 0)) + ) + ry = bar_area_y + idx * row_h + ccol = CORE_COLORS[idx % len(CORE_COLORS)] + st_lbl = "ON " if ct.enabled else "OFF" + draw_pango_text( + widget, + cr, + f"C{ct.core_id}[{st_lbl}]", + lx + 10, + ry + 1, + "Monospace Bold 8", + ccol if ct.enabled else TEXT_MUTED, + ) + + bx = lx + 72.0 + bw = max(60.0, half_w - 72.0 - 215.0) + bh = max(11.0, row_h - 5.0) + by = ry + 2.0 + + cr.set_source_rgb(0.05, 0.06, 0.08) + cr.rectangle(bx, by, bw, bh) + cr.fill() + + eff_kcps = ct.avg_kcps if ct.avg_kcps > 0 else ct.budget_kcps + pct = min(1.0, max(0.0, ct.cpu_load_pct / 100.0)) + if pct > 0: + cr.set_source_rgb(*ccol) + cr.rectangle(bx, by, bw * pct, bh) + cr.fill() + + cr.set_source_rgba(1.0, 1.0, 1.0, 0.18) + cr.set_line_width(1.0) + cr.rectangle(bx + 0.5, by + 0.5, bw - 1.0, bh - 1.0) + cr.stroke() + + info_s = f"{ct.cpu_load_pct:5.1f}% ({eff_kcps:6,d} KCPS | {ct.ticks_per_sec:4.0f} t/s)" + draw_pango_text( + widget, + cr, + info_s, + bx + bw + 8.0, + ry + 1, + "Monospace 7.8", + TEXT_PRIMARY if ct.enabled else TEXT_MUTED, + ) + + # Moving Graph for DSP Core Utilization + cplot_x = lx + 48.0 + cplot_y = bar_area_y + num_cores * row_h + 18.0 + cplot_w = max(40.0, half_w - 60.0) + cplot_h = max(50.0, (ly + top_h) - cplot_y - 12.0) + + draw_pango_text( + widget, + cr, + "MOVING GRAPH: PER-CORE CPU UTILIZATION (60s ROLLING %)", + lx + 10, + cplot_y - 15.0, + "Monospace Bold 7.8", + ACCENT_CYAN, + ) + + cr.set_source_rgba(*GRID_LINE) + cr.set_line_width(1.0) + for i in range(5): + gy_line = cplot_y + i * (cplot_h / 4.0) + cr.move_to(cplot_x, gy_line) + cr.line_to(cplot_x + cplot_w, gy_line) + cr.stroke() + + max_pct = 20.0 + for cid in range(num_cores): + deq = self.core_pct_hist.get(cid) + if deq: + max_pct = max(max_pct, max(v for _, v in deq) * 1.25) + max_pct = min(100.0, max(10.0, max_pct)) + + draw_pango_text( + widget, cr, f"{max_pct:4.0f}%", lx + 6, cplot_y - 2, "Monospace 7", TEXT_MUTED + ) + draw_pango_text( + widget, + cr, + f"{max_pct*0.5:4.0f}%", + lx + 6, + cplot_y + cplot_h * 0.5 - 6, + "Monospace 7", + TEXT_MUTED, + ) + draw_pango_text( + widget, cr, " 0%", lx + 6, cplot_y + cplot_h - 10, "Monospace 7", TEXT_MUTED + ) + + for cid in range(num_cores): + samples = list(self.core_pct_hist.get(cid, [])) + if len(samples) < 2: + continue + ccol = CORE_COLORS[cid % len(CORE_COLORS)] + t_end = samples[-1][0] + t_span = max(0.001, min(self.window_seconds, max(1.0, t_end - samples[0][0]))) + t_start = t_end - t_span + pts = [] + for ts_s, val in samples: + x_frac = min(1.0, max(0.0, (ts_s - t_start) / t_span)) + px = cplot_x + x_frac * cplot_w + norm = min(1.0, max(0.0, val / max_pct)) + py = cplot_y + cplot_h - norm * cplot_h + pts.append((px, py)) + + cr.move_to(pts[0][0], cplot_y + cplot_h) + for px, py in pts: + cr.line_to(px, py) + cr.line_to(pts[-1][0], cplot_y + cplot_h) + cr.close_path() + gfill = cairo.LinearGradient(0, cplot_y, 0, cplot_y + cplot_h) + gfill.add_color_stop_rgba(0.0, ccol[0], ccol[1], ccol[2], 0.30) + gfill.add_color_stop_rgba(1.0, ccol[0], ccol[1], ccol[2], 0.02) + cr.set_source(gfill) + cr.fill() + + cr.set_source_rgb(*ccol) + cr.set_line_width(2.0) + cr.move_to(pts[0][0], pts[0][1]) + for px, py in pts[1:]: + cr.line_to(px, py) + cr.stroke() + + # ===================================================================== + # 2. TOP-RIGHT: Moving Graph — DSP Memory Utilization (SRAM + DRAM) + # ===================================================================== + rx = pad * 2 + half_w + ry_panel = pad + draw_panel_box(cr, rx, ry_panel, half_w, top_h) + + hp = self.snap.hpsram + lp = self.snap.lpsram + hp_pwr_kb = hp.powered_banks * 128.0 + hp_tot_kb = hp.total_banks * 128.0 + lp_pwr_kb = lp.powered_banks * 64.0 + lp_tot_kb = lp.total_banks * 64.0 + dram_used_kb = self.snap.dram_used_kb + dram_tot_kb = self.snap.dram_total_kb + heap_used_kb = self.snap.heap_allocated_bytes / 1024.0 + heap_tot_kb = self.snap.heap_total_bytes / 1024.0 + + draw_pango_text( + widget, + cr, + f"DSP MEMORY UTILIZATION (SRAM + DRAM) — Total SRAM: {hp_pwr_kb + lp_pwr_kb:,.0f}/{hp_tot_kb + lp_tot_kb:,.0f} KB | DRAM: {dram_used_kb:,.0f}/{dram_tot_kb:,.0f} KB", + rx + 10, + ry_panel + 6, + "Monospace Bold 9", + ACCENT_GREEN, + ) + + # 4 Memory Segment Bars: HPSRAM, LPSRAM, DRAM (L3/IMR), DSP Heaps + mem_rows = [ + ("HPSRAM (SRAM)", hp_pwr_kb, hp_tot_kb, ACCENT_GREEN, f"{hp.powered_banks}/{hp.total_banks} banks ON"), + ("LPSRAM (SRAM)", lp_pwr_kb, lp_tot_kb, ACCENT_CYAN, f"{lp.powered_banks}/{lp.total_banks} banks ON"), + ("DRAM (L3/IMR)", dram_used_kb, dram_tot_kb, ACCENT_VIOLET, f"HWM {self.snap.dram_hwm_kb:,.0f} KB"), + ("DSP Heaps", heap_used_kb, heap_tot_kb, ACCENT_AMBER, f"Free {self.snap.heap_free_bytes/1024.0:,.0f} KB"), + ] + m_row_h = min(21.0, max(16.0, (top_h * 0.36) / len(mem_rows))) + for m_i, (mlbl, m_use, m_tot, mcol, mextra) in enumerate(mem_rows): + my = bar_area_y + m_i * m_row_h + draw_pango_text(widget, cr, f"{mlbl:<13}", rx + 10, my + 1, "Monospace Bold 7.8", mcol) + + mbx = rx + 112.0 + mbw = max(60.0, half_w - 112.0 - 215.0) + mbh = max(11.0, m_row_h - 5.0) + mby = my + 2.0 + + cr.set_source_rgb(0.05, 0.06, 0.08) + cr.rectangle(mbx, mby, mbw, mbh) + cr.fill() + + mfrac = min(1.0, max(0.0, m_use / m_tot)) if m_tot > 0 else 0.0 + if mfrac > 0: + cr.set_source_rgb(*mcol) + cr.rectangle(mbx, mby, mbw * mfrac, mbh) + cr.fill() + + cr.set_source_rgba(1.0, 1.0, 1.0, 0.18) + cr.set_line_width(1.0) + cr.rectangle(mbx + 0.5, mby + 0.5, mbw - 1.0, mbh - 1.0) + cr.stroke() + + minfo = f"{m_use:5.0f}/{m_tot:5.0f} KB ({mfrac*100:4.1f}%) | {mextra}" + draw_pango_text(widget, cr, minfo, mbx + mbw + 8.0, my + 1, "Monospace 7.5", TEXT_PRIMARY) + + # Moving Graph for DSP Memory Utilization (SRAM + DRAM) + mplot_x = rx + 52.0 + mplot_y = bar_area_y + len(mem_rows) * m_row_h + 18.0 + mplot_w = max(40.0, half_w - 64.0) + mplot_h = max(50.0, (ry_panel + top_h) - mplot_y - 12.0) + + draw_pango_text( + widget, + cr, + "MOVING GRAPH (60s): HPSRAM (Green) | LPSRAM (Cyan) | DRAM/IMR (Violet) | Heaps (Amber)", + rx + 10, + mplot_y - 15.0, + "Monospace Bold 7.6", + ACCENT_CYAN, + ) + + cr.set_source_rgba(*GRID_LINE) + cr.set_line_width(1.0) + for i in range(5): + gy_line = mplot_y + i * (mplot_h / 4.0) + cr.move_to(mplot_x, gy_line) + cr.line_to(mplot_x + mplot_w, gy_line) + cr.stroke() + + y_max_kb = float(max(1024.0, hp_tot_kb, dram_used_kb * 1.3, heap_used_kb * 1.3)) + draw_pango_text( + widget, cr, f"{y_max_kb:4.0f}K", rx + 6, mplot_y - 2, "Monospace 7", TEXT_MUTED + ) + draw_pango_text( + widget, + cr, + f"{y_max_kb*0.5:4.0f}K", + rx + 6, + mplot_y + mplot_h * 0.5 - 6, + "Monospace 7", + TEXT_MUTED, + ) + draw_pango_text( + widget, cr, " 0K", rx + 6, mplot_y + mplot_h - 10, "Monospace 7", TEXT_MUTED + ) + + for series, col in ( + (self.hpsram_kb_hist, ACCENT_GREEN), + (self.dram_kb_hist, ACCENT_VIOLET), + (self.heap_kb_hist, ACCENT_AMBER), + (self.lpsram_kb_hist, ACCENT_CYAN), + ): + s_list = list(series) + if len(s_list) < 2: + continue + t_end = s_list[-1][0] + t_span = max(0.001, min(self.window_seconds, max(1.0, t_end - s_list[0][0]))) + t_start = t_end - t_span + pts = [] + for ts_s, val in s_list: + x_frac = min(1.0, max(0.0, (ts_s - t_start) / t_span)) + px = mplot_x + x_frac * mplot_w + py = mplot_y + mplot_h - min(1.0, max(0.0, val / y_max_kb)) * mplot_h + pts.append((px, py)) + + cr.move_to(pts[0][0], mplot_y + mplot_h) + for px, py in pts: + cr.line_to(px, py) + cr.line_to(pts[-1][0], mplot_y + mplot_h) + cr.close_path() + cr.set_source_rgba(col[0], col[1], col[2], 0.16) + cr.fill() + + cr.set_source_rgb(*col) + cr.set_line_width(2.0) + cr.move_to(pts[0][0], pts[0][1]) + for px, py in pts[1:]: + cr.line_to(px, py) + cr.stroke() + + # ===================================================================== + # 3. BOTTOM: Unified Scrollable DSP Task List (Resources & Status) + # ===================================================================== + by_box = pad * 2 + top_h + bw_box = w - pad * 2 + draw_panel_box(cr, pad, by_box, bw_box, bot_h) + + tasks = self.snap.tasks + running_cnt = sum(1 for t in tasks if t.status == "RUNNING") + + th_y = by_box + 26.0 + row_y = th_y + 19.0 + row_step = 18.0 + max_rows = max(1, int((bot_h - (row_y - by_box) - 6.0) // row_step)) + self.last_max_rows = max_rows + + max_off = max(0, len(tasks) - max_rows) + start_idx = max(0, min(self.task_scroll_offset, max_off)) + end_idx = min(len(tasks), start_idx + max_rows) + range_str = ( + f"Showing {start_idx + 1}..{end_idx} of {len(tasks)} [Mouse Wheel / PgUp / PgDn]" + if len(tasks) > max_rows + else f"{len(tasks)} Tasks" + ) + + draw_pango_text( + widget, + cr, + f"DSP TASK LIST (RESOURCES & STATUS) — {range_str} ({running_cnt} Running | FW: {self.snap.fw_filename} v{self.snap.fw_version} [{self.snap.fw_state_name}])", + pad + 10, + by_box + 6, + "Monospace Bold 9", + ACCENT_GREEN, + ) + + col_hdr = ( + f"{'TASK ID':<10} {'TASK / MODULE NAME':<20} {'TYPE':<12} {'CORE':<5} " + f"{'STATUS':<10} {'CPU %':>7} {'CPU BAR':<12} {'AVG KCPS':>9} {'PEAK KCPS':>9} " + f"{'MEMORY / STACK RESOURCE':<24} {'ITERS / CYCLES':>14} {'XRUNS':>6}" + ) + draw_pango_text(widget, cr, col_hdr, pad + 12, th_y, "Monospace Bold 8", ACCENT_CYAN) + + cr.set_source_rgba(*GRID_LINE) + cr.move_to(pad + 8, th_y + 15) + cr.line_to(w - pad - 8, th_y + 15) + cr.stroke() + + for idx_t, t in enumerate(tasks[start_idx:end_idx]): + if (start_idx + idx_t) % 2 == 0: + cr.set_source_rgba(1.0, 1.0, 1.0, 0.025) + cr.rectangle(pad + 6, row_y - 1, bw_box - 18, row_step - 1) + cr.fill() + + if t.status in ("XRUN!", "CPC_VIOL"): + st_col = ACCENT_RED + elif t.status == "RUNNING": + st_col = ACCENT_GREEN + elif t.status in ("READY", "ACTIVE"): + st_col = ACCENT_CYAN + else: + st_col = TEXT_MUTED + + # Build inline ASCII bar for CPU load + filled = int(round(min(1.0, max(0.0, t.cpu_pct / 25.0)) * 10)) + bar_str = "[" + ("#" * filled).ljust(10, ".") + "]" + + row_s = ( + f"{t.task_id:<10.10} {t.name:<20.20} {t.task_type:<12.12} C{t.core_id:<4d} " + f"{t.status:<10.10} {t.cpu_pct:6.1f}% {bar_str} {t.avg_kcps:9,d} {t.peak_kcps:9,d} " + f"{t.mem_desc:<24.24} {t.iterations:14,d} {t.xruns:6d}" + ) + draw_pango_text(widget, cr, row_s, pad + 12, row_y, "Monospace 8", st_col) + row_y += row_step + + # Draw vertical scrollbar when tasks exceed visible rows + if len(tasks) > max_rows: + sb_x = pad + bw_box - 10.0 + sb_y = th_y + 18.0 + sb_h = max(24.0, bot_h - (sb_y - by_box) - 8.0) + cr.set_source_rgba(1.0, 1.0, 1.0, 0.08) + cr.rectangle(sb_x, sb_y, 5.0, sb_h) + cr.fill() + thumb_h = max(16.0, sb_h * (max_rows / float(len(tasks)))) + thumb_y = sb_y + (sb_h - thumb_h) * (start_idx / float(max(1, max_off))) + cr.set_source_rgb(*ACCENT_CYAN) + cr.rectangle(sb_x, thumb_y, 5.0, thumb_h) + cr.fill() + + +class UserDashboardCairoWidget(Gtk.Widget): + """ + Single-screen User Edition GTK4 Widget for `sof-top` wrapping `UserDashboardRenderer`. + Supports mouse-wheel and keyboard scrolling through the DSP Task List. + """ + + def __init__(self, window_seconds: float = 60.0): + super().__init__() + self.renderer = UserDashboardRenderer(window_seconds=window_seconds) + self.set_size_request(960, 620) + self.set_hexpand(True) + self.set_vexpand(True) + self.set_focusable(True) + + scroll_ctrl = Gtk.EventControllerScroll.new(Gtk.EventControllerScrollFlags.VERTICAL) + scroll_ctrl.connect("scroll", self._on_scroll) + self.add_controller(scroll_ctrl) + + key_ctrl = Gtk.EventControllerKey.new() + key_ctrl.connect("key-pressed", self._on_key_pressed) + self.add_controller(key_ctrl) + + @property + def snap(self) -> TelemetrySnapshot: + return self.renderer.snap + + def scroll_tasks(self, delta_rows: int): + if self.renderer.scroll_tasks(delta_rows): + self.queue_draw() + + def _on_scroll(self, _ctrl, _dx: float, dy: float) -> bool: + step = 3 if dy > 0 else (-3 if dy < 0 else 0) + if step != 0: + self.scroll_tasks(step) + return True + return False + + def _on_key_pressed(self, _ctrl, keyval: int, _keycode: int, _state) -> bool: + page = max(1, self.renderer.last_max_rows - 1) + if keyval == Gdk.KEY_Down: + self.scroll_tasks(1) + return True + if keyval == Gdk.KEY_Up: + self.scroll_tasks(-1) + return True + if keyval == Gdk.KEY_Page_Down: + self.scroll_tasks(page) + return True + if keyval == Gdk.KEY_Page_Up: + self.scroll_tasks(-page) + return True + if keyval == Gdk.KEY_Home: + self.scroll_tasks(-10000) + return True + if keyval == Gdk.KEY_End: + self.scroll_tasks(10000) + return True + return False + + def update_snapshot(self, snap: TelemetrySnapshot): + self.renderer.update_snapshot(snap) + self.queue_draw() + + def do_snapshot(self, snapshot: Gtk.Snapshot): + w = float(self.get_width()) + h = float(self.get_height()) + if w <= 10 or h <= 10: + return + rect = Graphene.Rect().init(0, 0, w, h) + cr = snapshot.append_cairo(rect) + self.renderer.render_to_cairo(cr, w, h, self) + + def render_to_cairo( + self, + cr: cairo.Context, + w: float, + h: float, + widget: Optional[Gtk.Widget] = None, + ): + self.renderer.render_to_cairo(cr, w, h, widget) +